Merge from main branch

This commit is contained in:
Tom Scholl 2011-04-10 23:00:19 +00:00
commit 6ffb782ee9
25 changed files with 1382 additions and 411 deletions

View File

@ -3,8 +3,7 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Constantin Hofstetter <consti at consti.de>, Steffen Siebert <calibre at steffensiebert.de>'
__version__ = '0.97'
__version__ = '0.98' # 2011-04-10
''' http://brandeins.de - Wirtschaftsmagazin '''
import re
import string
@ -14,8 +13,8 @@ from calibre.web.feeds.recipes import BasicNewsRecipe
class BrandEins(BasicNewsRecipe):
title = u'brand eins'
__author__ = 'Constantin Hofstetter'
description = u'Wirtschaftsmagazin'
__author__ = 'Constantin Hofstetter; Steffen Siebert'
description = u'Wirtschaftsmagazin: Gets the last full issue on default. Set a integer value for the username-field to get older issues: 1 -> the newest (but not complete) issue, 2 -> the last complete issue (default), 3 -> the issue before 2 etc.'
publisher ='brandeins.de'
category = 'politics, business, wirtschaft, Germany'
use_embedded_content = False

View File

@ -170,8 +170,8 @@ from setup import __appname__, __version__ as version
# there.
pot_header = '''\
# Translation template file..
# Copyright (C) 2007 Kovid Goyal
# Kovid Goyal <kovid@kovidgoyal.net>, 2007.
# Copyright (C) %(year)s Kovid Goyal
# Kovid Goyal <kovid@kovidgoyal.net>, %(year)s.
#
msgid ""
msgstr ""
@ -185,7 +185,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\\n"
"Generated-By: pygettext.py %%(version)s\\n"
'''%dict(appname=__appname__, version=version)
'''%dict(appname=__appname__, version=version, year=time.strftime('%Y'))
def usage(code, msg=''):

View File

@ -26,6 +26,38 @@ class POT(Command):
ans.append(os.path.abspath(os.path.join(root, name)))
return ans
def get_tweaks_docs(self):
path = self.a(self.j(self.SRC, '..', 'resources', 'default_tweaks.py'))
with open(path, 'rb') as f:
raw = f.read().decode('utf-8')
msgs = []
lines = list(raw.splitlines())
for i, line in enumerate(lines):
if line.startswith('#:'):
msgs.append((i, line[2:].strip()))
j = i
block = []
while True:
j += 1
line = lines[j]
if not line.startswith('#'):
break
block.append(line[1:].strip())
if block:
msgs.append((i+1, '\n'.join(block)))
ans = []
for lineno, msg in msgs:
ans.append('#: %s:%d'%(path, lineno))
slash = unichr(92)
msg = msg.replace(slash, slash*2).replace('"', r'\"').replace('\n',
r'\n').replace('\r', r'\r').replace('\t', r'\t')
ans.append('msgid "%s"'%msg)
ans.append('msgstr ""')
ans.append('')
return '\n'.join(ans)
def run(self, opts):
files = self.source_files()
@ -35,10 +67,10 @@ class POT(Command):
atexit.register(shutil.rmtree, tempdir)
pygettext(buf, ['-k', '__', '-p', tempdir]+files)
src = buf.getvalue()
src += '\n\n' + self.get_tweaks_docs()
pot = os.path.join(self.PATH, __appname__+'.pot')
f = open(pot, 'wb')
f.write(src)
f.close()
with open(pot, 'wb') as f:
f.write(src)
self.info('Translations template:', os.path.abspath(pot))
return pot

View File

@ -173,7 +173,7 @@ class ComicMetadataReader(MetadataReaderPlugin):
stream.seek(pos)
if id_ == b'Rar':
ftype = 'cbr'
elif id.startswith(b'PK'):
elif id_.startswith(b'PK'):
ftype = 'cbz'
if ftype == 'cbr':
from calibre.libunrar import extract_first_alphabetically as extract_first
@ -1038,6 +1038,17 @@ class Server(PreferencesPlugin):
'give you access to your calibre library from anywhere, '
'on any device, over the internet')
class MetadataSources(PreferencesPlugin):
name = 'Metadata download'
icon = I('metadata.png')
gui_name = _('Metadata download')
category = 'Sharing'
gui_category = _('Sharing')
category_order = 4
name_order = 3
config_widget = 'calibre.gui2.preferences.metadata_sources'
description = _('Control how calibre downloads ebook metadata from the net')
class Plugins(PreferencesPlugin):
name = 'Plugins'
icon = I('plugins.png')
@ -1076,6 +1087,9 @@ plugins += [LookAndFeel, Behavior, Columns, Toolbar, Search, InputOptions,
CommonOptions, OutputOptions, Adding, Saving, Sending, Plugboard,
Email, Server, Plugins, Tweaks, Misc, TemplateFunctions]
if test_eight_code:
plugins.append(MetadataSources)
#}}}

View File

@ -75,6 +75,17 @@ def enable_plugin(plugin_or_name):
ep.add(x)
config['enabled_plugins'] = ep
def restore_plugin_state_to_default(plugin_or_name):
x = getattr(plugin_or_name, 'name', plugin_or_name)
dp = config['disabled_plugins']
if x in dp:
dp.remove(x)
config['disabled_plugins'] = dp
ep = config['enabled_plugins']
if x in ep:
ep.remove(x)
config['enabled_plugins'] = ep
default_disabled_plugins = set([
'Douban Books', 'Douban.com covers', 'Nicebooks', 'Nicebooks covers',
'Kent District Library'
@ -453,12 +464,15 @@ def epub_fixers():
# Metadata sources2 {{{
def metadata_plugins(capabilities):
capabilities = frozenset(capabilities)
for plugin in _initialized_plugins:
if isinstance(plugin, Source) and \
plugin.capabilities.intersection(capabilities) and \
for plugin in all_metadata_plugins():
if plugin.capabilities.intersection(capabilities) and \
not is_disabled(plugin):
yield plugin
def all_metadata_plugins():
for plugin in _initialized_plugins:
if isinstance(plugin, Source):
yield plugin
# }}}
# Initialize plugins {{{

View File

@ -37,7 +37,7 @@ class ANDROID(USBMS):
0x22b8 : { 0x41d9 : [0x216], 0x2d61 : [0x100], 0x2d67 : [0x100],
0x41db : [0x216], 0x4285 : [0x216], 0x42a3 : [0x216],
0x4286 : [0x216], 0x42b3 : [0x216], 0x42b4 : [0x216],
0x7086 : [0x0226],
0x7086 : [0x0226], 0x70a8: [0x9999],
},
# Sony Ericsson
@ -96,7 +96,8 @@ class ANDROID(USBMS):
VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER',
'GT-I5700', 'SAMSUNG', 'DELL', 'LINUX', 'GOOGLE', 'ARCHOS',
'TELECHIP', 'HUAWEI', 'T-MOBILE', 'SEMC', 'LGE', 'NVIDIA']
'TELECHIP', 'HUAWEI', 'T-MOBILE', 'SEMC', 'LGE', 'NVIDIA',
'GENERIC-']
WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE',
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897',
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID',
@ -104,7 +105,7 @@ class ANDROID(USBMS):
'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT', 'A70H',
'IDEOS_TABLET', 'MYTOUCH_4G', 'UMS_COMPOSITE', 'SCH-I800_CARD',
'7', 'A956', 'A955', 'A43', 'ANDROID_PLATFORM', 'TEGRA_2',
'MB860']
'MB860', 'MULTI-CARD']
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
'A70S', 'A101IT', '7']

View File

@ -155,7 +155,7 @@ class FB2Output(OutputFormatPlugin):
OptionRecommendation(name='fb2_genre',
recommended_value='antique', level=OptionRecommendation.LOW,
choices=FB2_GENRES,
help=_('Genre for the book. Choices: %s\n\n See: ' % FB2_GENRES) + 'http://www.fictionbook.org/index.php/Eng:FictionBook_2.1_genres ' \
help=(_('Genre for the book. Choices: %s\n\n See: ') % FB2_GENRES) + 'http://www.fictionbook.org/index.php/Eng:FictionBook_2.1_genres ' \
+ _('for a complete list with descriptions.')),
])

View File

@ -279,7 +279,7 @@ class Worker(Thread): # Get details {{{
class Amazon(Source):
name = 'Amazon Web'
name = 'Amazon.com'
description = _('Downloads metadata from Amazon')
capabilities = frozenset(['identify', 'cover'])
@ -295,6 +295,14 @@ class Amazon(Source):
'uk' : _('UK'),
}
def get_book_url(self, identifiers): # {{{
asin = identifiers.get('amazon', None)
if asin is None:
asin = identifiers.get('asin', None)
if asin:
return 'http://amzn.com/%s'%asin
# }}}
def create_query(self, log, title=None, authors=None, identifiers={}): # {{{
domain = self.prefs.get('domain', 'com')
@ -333,9 +341,10 @@ class Amazon(Source):
# Insufficient metadata to make an identify query
return None
utf8q = dict([(x.encode('utf-8'), y.encode('utf-8')) for x, y in
latin1q = dict([(x.encode('latin1', 'ignore'), y.encode('latin1',
'ignore')) for x, y in
q.iteritems()])
url = 'http://www.amazon.%s/s/?'%domain + urlencode(utf8q)
url = 'http://www.amazon.%s/s/?'%domain + urlencode(latin1q)
return url
# }}}

View File

@ -78,8 +78,8 @@ class InternalMetadataCompareKeyGen(object):
exact_title = 1 if title and \
cleanup_title(title) == cleanup_title(mi.title) else 2
has_cover = 2 if source_plugin.get_cached_cover_url(mi.identifiers)\
is None else 1
has_cover = 2 if (not source_plugin.cached_cover_url_is_reliable or
source_plugin.get_cached_cover_url(mi.identifiers) is None) else 1
self.base = (isbn, has_cover, all_fields, exact_title)
self.comments_len = len(mi.comments.strip() if mi.comments else '')
@ -157,6 +157,12 @@ class Source(Plugin):
#: correctly first
supports_gzip_transfer_encoding = False
#: Cached cover URLs can sometimes be unreliable (i.e. the download could
#: fail or the returned image could be bogus. If that is the case set this to
#: False
cached_cover_url_is_reliable = True
def __init__(self, *args, **kwargs):
Plugin.__init__(self, *args, **kwargs)
self._isbn_to_identifier_cache = {}
@ -301,6 +307,13 @@ class Source(Plugin):
# Metadata API {{{
def get_book_url(self, identifiers):
'''
Return the URL for the book identified by identifiers at this source.
If no URL is found, return None.
'''
return None
def get_cached_cover_url(self, identifiers):
'''
Return cached cover URL for the book identified by

View File

@ -7,7 +7,7 @@ __license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import time
import time, hashlib
from urllib import urlencode
from functools import partial
from Queue import Queue, Empty
@ -133,7 +133,7 @@ def to_metadata(browser, log, entry_, timeout): # {{{
default = utcnow().replace(day=15)
mi.pubdate = parse_date(pubdate, assume_utc=True, default=default)
except:
log.exception('Failed to parse pubdate')
log.error('Failed to parse pubdate %r'%pubdate)
# Ratings
for x in rating(extra):
@ -164,9 +164,18 @@ class GoogleBooks(Source):
'comments', 'publisher', 'identifier:isbn', 'rating',
'identifier:google']) # language currently disabled
supports_gzip_transfer_encoding = True
cached_cover_url_is_reliable = False
GOOGLE_COVER = 'http://books.google.com/books?id=%s&printsec=frontcover&img=1'
DUMMY_IMAGE_MD5 = frozenset(['0de4383ebad0adad5eeb8975cd796657'])
def get_book_url(self, identifiers): # {{{
goog = identifiers.get('google', None)
if goog is not None:
return 'http://books.google.com/books?id=%s'%goog
# }}}
def create_query(self, log, title=None, authors=None, identifiers={}): # {{{
BASE_URL = 'http://books.google.com/books/feeds/volumes?'
isbn = check_isbn(identifiers.get('isbn', None))
@ -229,7 +238,11 @@ class GoogleBooks(Source):
log('Downloading cover from:', cached_url)
try:
cdata = br.open_novisit(cached_url, timeout=timeout).read()
result_queue.put((self, cdata))
if cdata:
if hashlib.md5(cdata).hexdigest() in self.DUMMY_IMAGE_MD5:
log.warning('Google returned a dummy image, ignoring')
else:
result_queue.put((self, cdata))
except:
log.exception('Failed to download cover from:', cached_url)

View File

@ -14,7 +14,7 @@ from threading import Thread
from io import BytesIO
from operator import attrgetter
from calibre.customize.ui import metadata_plugins
from calibre.customize.ui import metadata_plugins, all_metadata_plugins
from calibre.ebooks.metadata.sources.base import create_log, msprefs
from calibre.ebooks.metadata.xisbn import xisbn
from calibre.ebooks.metadata.book.base import Metadata
@ -338,8 +338,9 @@ def identify(log, abort, # {{{
for i, result in enumerate(presults):
result.relevance_in_source = i
result.has_cached_cover_url = \
plugin.get_cached_cover_url(result.identifiers) is not None
result.has_cached_cover_url = (plugin.cached_cover_url_is_reliable
and plugin.get_cached_cover_url(result.identifiers) is not
None)
result.identify_plugin = plugin
log('The identify phase took %.2f seconds'%(time.time() - start_time))
@ -366,6 +367,22 @@ def identify(log, abort, # {{{
return results
# }}}
def urls_from_identifiers(identifiers): # {{{
ans = []
for plugin in all_metadata_plugins():
try:
url = plugin.get_book_url(identifiers)
if url is not None:
ans.append((plugin.name, url))
except:
pass
isbn = identifiers.get('isbn', None)
if isbn:
ans.append(('ISBN',
'http://www.worldcat.org/search?q=bn%%3A%s&qt=advanced'%isbn))
return ans
# }}}
if __name__ == '__main__': # tests {{{
# To run these test use: calibre-debug -e
# src/calibre/ebooks/metadata/sources/identify.py

View File

@ -193,7 +193,10 @@ class PluginWidget(QWidget,Ui_Form):
opts_dict['header_note_source_field'] = self.header_note_source_field_name
# Append the output profile
opts_dict['output_profile'] = [load_defaults('page_setup')['output_profile']]
try:
opts_dict['output_profile'] = [load_defaults('page_setup')['output_profile']]
except:
opts_dict['output_profile'] = ['default']
if False:
print "opts_dict"
for opt in sorted(opts_dict.keys()):

View File

@ -604,7 +604,10 @@ class BooksModel(QAbstractTableModel): # {{{
def size(r, idx=-1):
size = self.db.data[r][idx]
if size:
return QVariant('%.1f'%(float(size)/(1024*1024)))
ans = '%.1f'%(float(size)/(1024*1024))
if size > 0 and ans == '0.0':
ans = '<0.1'
return QVariant(ans)
return None
def rating_type(r, idx=-1):

View File

@ -9,7 +9,7 @@ __docformat__ = 'restructuredtext en'
import textwrap, re, os
from PyQt4.Qt import (Qt, QDateEdit, QDate,
from PyQt4.Qt import (Qt, QDateEdit, QDate, pyqtSignal,
QIcon, QToolButton, QWidget, QLabel, QGridLayout,
QDoubleSpinBox, QListWidgetItem, QSize, QPixmap,
QPushButton, QSpinBox, QLineEdit, QSizePolicy)
@ -172,6 +172,7 @@ class AuthorsEdit(MultiCompleteComboBox):
self.books_to_refresh = set([])
all_authors = db.all_authors()
all_authors.sort(key=lambda x : sort_key(x[1]))
self.clear()
for i in all_authors:
id, name = i
name = [name.strip().replace('|', ',') for n in name.split(',')]
@ -315,7 +316,7 @@ class SeriesEdit(MultiCompleteComboBox):
if not val:
val = ''
self.setEditText(val.strip())
self.setCursorPosition(0)
self.lineEdit().setCursorPosition(0)
return property(fget=fget, fset=fset)
@ -326,6 +327,7 @@ class SeriesEdit(MultiCompleteComboBox):
self.update_items_cache([x[1] for x in all_series])
series_id = db.series_id(id_, index_is_id=True)
idx, c = None, 0
self.clear()
for i in all_series:
id, name = i
if id == series_id:
@ -613,6 +615,8 @@ class FormatsManager(QWidget): # {{{
class Cover(ImageView): # {{{
download_cover = pyqtSignal()
def __init__(self, parent):
ImageView.__init__(self, parent)
self.dialog = parent
@ -703,9 +707,6 @@ class Cover(ImageView): # {{{
cdata = im.export('png')
self.current_val = cdata
def download_cover(self, *args):
pass # TODO: Implement this
def generate_cover(self, *args):
from calibre.ebooks import calibre_cover
from calibre.ebooks.metadata import fmt_sidx
@ -862,6 +863,7 @@ class TagsEdit(MultiCompleteLineEdit): # {{{
if not val:
val = []
self.setText(', '.join([x.strip() for x in val]))
self.setCursorPosition(0)
return property(fget=fget, fset=fset)
def initialize(self, db, id_):
@ -928,6 +930,7 @@ class IdentifiersEdit(QLineEdit): # {{{
val = {}
txt = ', '.join(['%s:%s'%(k, v) for k, v in val.iteritems()])
self.setText(txt.strip())
self.setCursorPosition(0)
return property(fget=fget, fset=fset)
def initialize(self, db, id_):
@ -977,7 +980,7 @@ class PublisherEdit(MultiCompleteComboBox): # {{{
if not val:
val = ''
self.setEditText(val.strip())
self.setCursorPosition(0)
self.lineEdit().setCursorPosition(0)
return property(fget=fget, fset=fset)
@ -987,13 +990,13 @@ class PublisherEdit(MultiCompleteComboBox): # {{{
all_publishers.sort(key=lambda x : sort_key(x[1]))
self.update_items_cache([x[1] for x in all_publishers])
publisher_id = db.publisher_id(id_, index_is_id=True)
idx, c = None, 0
for i in all_publishers:
id, name = i
if id == publisher_id:
idx = c
idx = None
self.clear()
for i, x in enumerate(all_publishers):
id_, name = x
if id_ == publisher_id:
idx = i
self.addItem(name)
c += 1
self.setEditText('')
if idx is not None:

View File

@ -16,11 +16,12 @@ from PyQt4.Qt import (Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton,
QSizePolicy, QPalette, QFrame, QSize, QKeySequence)
from calibre.ebooks.metadata import authors_to_string, string_to_authors
from calibre.gui2 import ResizableDialog, error_dialog, gprefs
from calibre.gui2 import ResizableDialog, error_dialog, gprefs, pixmap_to_data
from calibre.gui2.metadata.basic_widgets import (TitleEdit, AuthorsEdit,
AuthorSortEdit, TitleSortEdit, SeriesEdit, SeriesIndexEdit, IdentifiersEdit,
RatingEdit, PublisherEdit, TagsEdit, FormatsManager, Cover, CommentsEdit,
BuddyLabel, DateEdit, PubdateEdit)
from calibre.gui2.metadata.single_download import FullFetch
from calibre.gui2.custom_column_widgets import populate_metadata_page
from calibre.utils.config import tweaks
@ -132,6 +133,7 @@ class MetadataSingleDialogBase(ResizableDialog):
self.formats_manager.cover_from_format_button.clicked.connect(
self.cover_from_format)
self.cover = Cover(self)
self.cover.download_cover.connect(self.download_cover)
self.basic_metadata_widgets.append(self.cover)
self.comments = CommentsEdit(self, self.one_line_comments_toolbar)
@ -158,7 +160,7 @@ class MetadataSingleDialogBase(ResizableDialog):
self.basic_metadata_widgets.extend([self.timestamp, self.pubdate])
self.fetch_metadata_button = QPushButton(
_('&Fetch metadata from server'), self)
_('&Download metadata'), self)
self.fetch_metadata_button.clicked.connect(self.fetch_metadata)
font = self.fmb_font = QFont()
font.setBold(True)
@ -303,7 +305,26 @@ class MetadataSingleDialogBase(ResizableDialog):
self.comments.current_val = mi.comments
def fetch_metadata(self, *args):
pass # TODO: fetch metadata
d = FullFetch(self.cover.pixmap(), self)
ret = d.start(title=self.title.current_val, authors=self.authors.current_val,
identifiers=self.identifiers.current_val)
if ret == d.Accepted:
mi = d.book
if mi is not None:
self.update_from_mi(mi)
if d.cover_pixmap is not None:
self.cover.current_val = pixmap_to_data(d.cover_pixmap)
def download_cover(self, *args):
from calibre.gui2.metadata.single_download import CoverFetch
d = CoverFetch(self.cover.pixmap(), self)
ret = d.start(self.title.current_val, self.authors.current_val,
self.identifiers.current_val)
if ret == d.Accepted:
if d.cover_pixmap is not None:
self.cover.current_val = pixmap_to_data(d.cover_pixmap)
# }}}
def apply_changes(self):
@ -521,18 +542,35 @@ class MetadataSingleDialog(MetadataSingleDialogBase): # {{{
# }}}
class DragTrackingWidget(QWidget): # {{{
def __init__(self, parent, on_drag_enter):
QWidget.__init__(self, parent)
self.on_drag_enter = on_drag_enter
def dragEnterEvent(self, ev):
self.on_drag_enter.emit()
# }}}
class MetadataSingleDialogAlt1(MetadataSingleDialogBase): # {{{
cc_two_column = False
one_line_comments_toolbar = True
on_drag_enter = pyqtSignal()
def handle_drag_enter(self):
self.central_widget.setCurrentIndex(1)
def do_layout(self):
self.central_widget.clear()
self.tabs = []
self.labels = []
sto = QWidget.setTabOrder
self.tabs.append(QWidget(self))
self.on_drag_enter.connect(self.handle_drag_enter)
self.tabs.append(DragTrackingWidget(self, self.on_drag_enter))
self.central_widget.addTab(self.tabs[0], _("&Metadata"))
self.tabs[0].l = QGridLayout()
self.tabs[0].setLayout(self.tabs[0].l)
@ -542,6 +580,10 @@ class MetadataSingleDialogAlt1(MetadataSingleDialogBase): # {{{
self.tabs[1].l = QGridLayout()
self.tabs[1].setLayout(self.tabs[1].l)
# accept drop events so we can automatically switch to the second tab to
# drop covers and formats
self.tabs[0].setAcceptDrops(True)
# Tab 0
tab0 = self.tabs[0]
@ -550,6 +592,8 @@ class MetadataSingleDialogAlt1(MetadataSingleDialogBase): # {{{
self.tabs[0].l.addWidget(gb, 0, 0, 1, 1)
gb.setLayout(tl)
self.button_box.addButton(self.fetch_metadata_button,
QDialogButtonBox.ActionRole)
sto(self.button_box, self.title)
def create_row(row, widget, tab_to, button=None, icon=None, span=1):
@ -639,7 +683,6 @@ class MetadataSingleDialogAlt1(MetadataSingleDialogBase): # {{{
wgl.addWidget(gb)
wgl.addItem(QSpacerItem(10, 10, QSizePolicy.Expanding,
QSizePolicy.Expanding))
wgl.addWidget(self.fetch_metadata_button)
wgl.addItem(QSpacerItem(10, 10, QSizePolicy.Expanding,
QSizePolicy.Expanding))
wgl.addWidget(self.formats_manager)

View File

@ -7,6 +7,9 @@ __license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
DEBUG_DIALOG = False
# Imports {{{
from threading import Thread, Event
from operator import attrgetter
from Queue import Queue, Empty
@ -21,14 +24,14 @@ from PyQt4.QtWebKit import QWebView
from calibre.customize.ui import metadata_plugins
from calibre.ebooks.metadata import authors_to_string
from calibre.utils.logging import GUILog as Log
from calibre.ebooks.metadata.sources.identify import identify
from calibre.ebooks.metadata.sources.identify import (identify,
urls_from_identifiers)
from calibre.ebooks.metadata.book.base import Metadata
from calibre.gui2 import error_dialog, NONE
from calibre.utils.date import utcnow, fromordinal, format_date
from calibre.library.comments import comments_to_html
from calibre import force_unicode
DEBUG_DIALOG = False
# }}}
class RichTextDelegate(QStyledItemDelegate): # {{{
@ -41,7 +44,10 @@ class RichTextDelegate(QStyledItemDelegate): # {{{
return doc
def sizeHint(self, option, index):
ans = self.to_doc(index).size().toSize()
doc = self.to_doc(index)
ans = doc.size().toSize()
if ans.width() > 150:
ans.setWidth(160)
ans.setHeight(ans.height()+10)
return ans
@ -174,6 +180,13 @@ class ResultsModel(QAbstractTableModel): # {{{
return self.yes_icon
elif role == Qt.UserRole:
return book
elif role == Qt.ToolTipRole and col == 3:
return QVariant(
_('The has cover indication is not fully\n'
'reliable. Sometimes results marked as not\n'
'having a cover will find a cover in the download\n'
'cover stage, and vice versa.'))
return NONE
def sort(self, col, order=Qt.AscendingOrder):
@ -183,7 +196,7 @@ class ResultsModel(QAbstractTableModel): # {{{
elif col == 1:
key = attrgetter('title')
elif col == 2:
key = attrgetter('authors')
key = attrgetter('pubdate')
elif col == 3:
key = attrgetter('has_cached_cover_url')
elif key == 4:
@ -234,6 +247,11 @@ class ResultsView(QTableView): # {{{
if not book.is_null('rating'):
parts.append('<div>%s</div>'%('\u2605'*int(book.rating)))
parts.append('</center>')
if book.identifiers:
urls = urls_from_identifiers(book.identifiers)
ids = ['<a href="%s">%s</a>'%(url, name) for name, url in urls]
if ids:
parts.append('<div><b>%s:</b> %s</div><br>'%(_('See at'), ', '.join(ids)))
if book.tags:
parts.append('<div>%s</div><div>\u00a0</div>'%', '.join(book.tags))
if book.comments:
@ -265,6 +283,14 @@ class Comments(QWebView): # {{{
self.page().setPalette(palette)
self.setAttribute(Qt.WA_OpaquePaintEvent, False)
self.page().setLinkDelegationPolicy(self.page().DelegateAllLinks)
self.linkClicked.connect(self.link_clicked)
def link_clicked(self, url):
from calibre.gui2 import open_url
if unicode(url.toString()).startswith('http://'):
open_url(url)
def turnoff_scrollbar(self, *args):
self.page().mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff)
@ -382,7 +408,7 @@ class IdentifyWidget(QWidget): # {{{
self.query.setWordWrap(True)
l.addWidget(self.query, 2, 0, 1, 2)
self.comments_view.show_data('<h2>'+_('Downloading')+
self.comments_view.show_data('<h2>'+_('Please wait')+
'<br><span id="dots">.</span></h2>'+
'''
<script type="text/javascript">
@ -409,7 +435,7 @@ class IdentifyWidget(QWidget): # {{{
if authors:
parts.append('authors:'+authors_to_string(authors))
if identifiers:
x = ', '.join('%s:%s'%(k, v) for k, v in identifiers)
x = ', '.join('%s:%s'%(k, v) for k, v in identifiers.iteritems())
parts.append(x)
self.query.setText(_('Query: ')+'; '.join(parts))
self.log(unicode(self.query.text()))
@ -541,16 +567,23 @@ class CoversModel(QAbstractListModel): # {{{
if v == row:
return k
def cover_keygen(self, x):
pmap = x[2]
if pmap is None:
return 1
return pmap.width()*pmap.height()
def clear_failed(self):
good = []
pmap = {}
for i, x in enumerate(self.covers):
dcovers = sorted(self.covers[1:], key=self.cover_keygen, reverse=True)
for i, x in enumerate(self.covers[0:1] + dcovers):
if not x[-1]:
good.append(x)
if i > 0:
plugin = self.plugin_for_index(i)
pmap[plugin] = len(good) - 1
good = [x for x in self.covers if not x[-1]]
self.covers = good
self.plugin_map = pmap
self.reset()
@ -645,7 +678,8 @@ class CoversWidget(QWidget): # {{{
def start(self, book, current_cover, title, authors):
self.book, self.current_cover = book, current_cover
self.title, self.authors = title, authors
self.log('\n\nStarting cover download for:', book.title)
self.log('Starting cover download for:', book.title)
self.log('Query:', title, authors, self.book.identifiers)
self.msg.setText('<p>'+_('Downloading covers for <b>%s</b>, please wait...')%book.title)
self.covers_view.start()
@ -729,6 +763,10 @@ class LogViewer(QDialog): # {{{
self.bb = QDialogButtonBox(QDialogButtonBox.Close)
l.addWidget(self.bb)
self.copy_button = self.bb.addButton(_('Copy to clipboard'),
self.bb.ActionRole)
self.copy_button.clicked.connect(self.copy_to_clipboard)
self.copy_button.setIcon(QIcon(I('edit-copy.png')))
self.bb.rejected.connect(self.reject)
self.bb.accepted.connect(self.accept)
@ -739,10 +777,13 @@ class LogViewer(QDialog): # {{{
self.keep_updating = True
self.last_html = None
self.finished.connect(self.stop)
QTimer.singleShot(1000, self.update_log)
QTimer.singleShot(100, self.update_log)
self.show()
def copy_to_clipboard(self):
QApplication.clipboard().setText(''.join(self.log.plain_text))
def stop(self, *args):
self.keep_updating = False
@ -752,16 +793,17 @@ class LogViewer(QDialog): # {{{
html = self.log.html
if html != self.last_html:
self.last_html = html
self.tb.setHtml('<pre>%s</pre>'%html)
self.tb.setHtml('<pre style="font-family:monospace">%s</pre>'%html)
QTimer.singleShot(1000, self.update_log)
# }}}
class FullFetch(QDialog): # {{{
def __init__(self, log, current_cover=None, parent=None):
def __init__(self, current_cover=None, parent=None):
QDialog.__init__(self, parent)
self.log, self.current_cover = log, current_cover
self.current_cover = current_cover
self.log = Log()
self.book = self.cover_pixmap = None
self.setWindowTitle(_('Downloading metadata...'))
@ -787,7 +829,7 @@ class FullFetch(QDialog): # {{{
self.log_button.setIcon(QIcon(I('debug.png')))
self.ok_button.setVisible(False)
self.identify_widget = IdentifyWidget(log, self)
self.identify_widget = IdentifyWidget(self.log, self)
self.identify_widget.rejected.connect(self.reject)
self.identify_widget.results_found.connect(self.identify_results_found)
self.identify_widget.book_selected.connect(self.book_selected)
@ -809,6 +851,7 @@ class FullFetch(QDialog): # {{{
self.ok_button.setVisible(True)
self.book = book
self.stack.setCurrentIndex(1)
self.log('\n\n')
self.covers_widget.start(book, self.current_cover,
self.title, self.authors)
@ -818,6 +861,7 @@ class FullFetch(QDialog): # {{{
def reject(self):
self.identify_widget.cancel()
self.covers_widget.cancel()
return QDialog.reject(self)
def cleanup(self):
@ -844,12 +888,65 @@ class FullFetch(QDialog): # {{{
self.title, self.authors = title, authors
self.identify_widget.start(title=title, authors=authors,
identifiers=identifiers)
self.exec_()
return self.exec_()
# }}}
class CoverFetch(QDialog): # {{{
def __init__(self, current_cover=None, parent=None):
QDialog.__init__(self, parent)
self.current_cover = current_cover
self.log = Log()
self.cover_pixmap = None
self.setWindowTitle(_('Downloading cover...'))
self.setWindowIcon(QIcon(I('book.png')))
self.l = l = QVBoxLayout()
self.setLayout(l)
self.covers_widget = CoversWidget(self.log, self.current_cover, parent=self)
self.covers_widget.chosen.connect(self.accept)
l.addWidget(self.covers_widget)
self.resize(850, 550)
self.finished.connect(self.cleanup)
self.bb = QDialogButtonBox(QDialogButtonBox.Cancel|QDialogButtonBox.Ok)
l.addWidget(self.bb)
self.log_button = self.bb.addButton(_('View log'), self.bb.ActionRole)
self.log_button.clicked.connect(self.view_log)
self.log_button.setIcon(QIcon(I('debug.png')))
self.bb.rejected.connect(self.reject)
self.bb.accepted.connect(self.accept)
def cleanup(self):
self.covers_widget.cleanup()
def reject(self):
self.covers_widget.cancel()
return QDialog.reject(self)
def accept(self, *args):
self.cover_pixmap = self.covers_widget.cover_pixmap()
QDialog.accept(self)
def start(self, title, authors, identifiers):
book = Metadata(title, authors)
book.identifiers = identifiers
self.covers_widget.start(book, self.current_cover,
title, authors)
return self.exec_()
def view_log(self):
self._lv = LogViewer(self.log, self)
# }}}
if __name__ == '__main__':
DEBUG_DIALOG = True
#DEBUG_DIALOG = True
app = QApplication([])
d = FullFetch(Log())
d.start(title='great gatsby', authors=['Fitzgerald'])
d = FullFetch()
d.start(title='great gatsby', authors=['fitzgerald'])

View File

@ -21,7 +21,7 @@ class ConfigWidgetInterface(object):
'''
This class defines the interface that all widgets displayed in the
Preferences dialog must implement. See :class:`ConfigWidgetBase` for
a base class that implements this interface and defines various conveninece
a base class that implements this interface and defines various convenience
methods as well.
'''

View File

@ -43,6 +43,9 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
r('overwrite_author_title_metadata', config)
r('get_social_metadata', config)
if test_eight_code:
self.opt_overwrite_author_title_metadata.setVisible(False)
self.opt_get_social_metadata.setVisible(False)
r('new_version_notification', config)
r('upload_news_to_device', config)
r('delete_news_from_library_on_upload', config)

View File

@ -0,0 +1,190 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from operator import attrgetter
from PyQt4.Qt import (QAbstractTableModel, Qt, QAbstractListModel)
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
from calibre.gui2.preferences.metadata_sources_ui import Ui_Form
from calibre.ebooks.metadata.sources.base import msprefs
from calibre.customize.ui import (all_metadata_plugins, is_disabled,
enable_plugin, disable_plugin, restore_plugin_state_to_default)
from calibre.gui2 import NONE
class SourcesModel(QAbstractTableModel): # {{{
def __init__(self, parent=None):
QAbstractTableModel.__init__(self, parent)
self.plugins = []
self.enabled_overrides = {}
self.cover_overrides = {}
def initialize(self):
self.plugins = list(all_metadata_plugins())
self.plugins.sort(key=attrgetter('name'))
self.enabled_overrides = {}
self.cover_overrides = {}
self.reset()
def rowCount(self, parent=None):
return len(self.plugins)
def columnCount(self, parent=None):
return 2
def headerData(self, section, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
if section == 0:
return _('Source')
if section == 1:
return _('Cover priority')
return NONE
def data(self, index, role):
try:
plugin = self.plugins[index.row()]
except:
return NONE
col = index.column()
if role == Qt.DisplayRole:
if col == 0:
return plugin.name
elif col == 1:
orig = msprefs['cover_priorities'].get(plugin.name, 1)
return self.cover_overrides.get(plugin, orig)
elif role == Qt.CheckStateRole and col == 0:
orig = Qt.Unchecked if is_disabled(plugin) else Qt.Checked
return self.enabled_overrides.get(plugin, orig)
return NONE
def setData(self, index, val, role):
try:
plugin = self.plugins[index.row()]
except:
return False
col = index.column()
ret = False
if col == 0 and role == Qt.CheckStateRole:
val, ok = val.toInt()
if ok:
self.enabled_overrides[plugin] = val
ret = True
if col == 1 and role == Qt.EditRole:
val, ok = val.toInt()
if ok:
self.cover_overrides[plugin] = val
ret = True
if ret:
self.dataChanged.emit(index, index)
return ret
def flags(self, index):
col = index.column()
ans = QAbstractTableModel.flags(self, index)
if col == 0:
return ans | Qt.ItemIsUserCheckable
return Qt.ItemIsEditable | ans
def commit(self):
for plugin, val in self.enabled_overrides.iteritems():
if val == Qt.Checked:
enable_plugin(plugin)
elif val == Qt.Unchecked:
disable_plugin(plugin)
if self.cover_overrides:
cp = msprefs['cover_priorities']
for plugin, val in self.cover_overrides.iteritems():
if val == 1:
cp.pop(plugin.name, None)
else:
cp[plugin.name] = val
msprefs['cover_priorities'] = cp
self.enabled_overrides = {}
self.cover_overrides = {}
def restore_defaults(self):
del msprefs['cover_priorities']
self.enabled_overrides = {}
self.cover_overrides = {}
for plugin in self.plugins:
restore_plugin_state_to_default(plugin)
self.reset()
# }}}
class FieldsModel(QAbstractListModel): # {{{
def __init__(self, parent=None):
QAbstractTableModel.__init__(self, parent)
self.fields = []
def rowCount(self, parent=None):
return len(self.fields)
def initialize(self):
fields = set()
for p in all_metadata_plugins():
fields |= p.touched_fields
self.fields = []
for x in fields:
if not x.startswith('identifiers:'):
self.fields.append(x)
self.reset()
# }}}
class ConfigWidget(ConfigWidgetBase, Ui_Form):
def genesis(self, gui):
r = self.register
r('txt_comments', msprefs)
r('max_tags', msprefs)
r('wait_after_first_identify_result', msprefs)
r('wait_after_first_cover_result', msprefs)
self.configure_plugin_button.clicked.connect(self.configure_plugin)
self.sources_model = SourcesModel(self)
self.sources_view.setModel(self.sources_model)
self.sources_model.dataChanged.connect(self.changed_signal)
self.fields_model = FieldsModel(self)
self.fields_view.setModel(self.fields_model)
self.fields_model.dataChanged.connect(self.changed_signal)
def configure_plugin(self):
pass
def initialize(self):
ConfigWidgetBase.initialize(self)
self.sources_model.initialize()
self.sources_view.resizeColumnsToContents()
def restore_defaults(self):
ConfigWidgetBase.restore_defaults(self)
self.sources_model.restore_defaults()
self.changed_signal.emit()
def commit(self):
self.sources_model.commit()
return ConfigWidgetBase.commit(self)
if __name__ == '__main__':
from PyQt4.Qt import QApplication
app = QApplication([])
test_widget('Sharing', 'Metadata download')

View File

@ -0,0 +1,146 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>781</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QStackedWidget" name="stack">
<widget class="QWidget" name="page">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" rowspan="5">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Metadata sources</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Disable any metadata sources you do not want by unchecking them. You can also set the cover priority. Covers from sources that have a higher (smaller) priority will be preferred when bulk downloading metadata.
</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QTableView" name="sources_view">
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="configure_plugin_button">
<property name="text">
<string>Configure selected source</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/plugins.png</normaloff>:/images/plugins.png</iconset>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="1" colspan="2">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Downloaded metadata fields</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QListView" name="fields_view">
<property name="toolTip">
<string>If you uncheck any fields, metadata for those fields will not be downloaded</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="1" column="1" colspan="2">
<widget class="QCheckBox" name="opt_txt_comments">
<property name="text">
<string>Convert all downloaded comments to plain &amp;text</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Max. number of &amp;tags to download:</string>
</property>
<property name="buddy">
<cstring>opt_max_tags</cstring>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QSpinBox" name="opt_max_tags"/>
</item>
<item row="3" column="1">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Max. &amp;time to wait after first match is found:</string>
</property>
<property name="buddy">
<cstring>opt_wait_after_first_identify_result</cstring>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QSpinBox" name="opt_wait_after_first_identify_result">
<property name="suffix">
<string> secs</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Max. time to wait after first &amp;cover is found:</string>
</property>
<property name="buddy">
<cstring>opt_wait_after_first_cover_result</cstring>
</property>
</widget>
</item>
<item row="4" column="2">
<widget class="QSpinBox" name="opt_wait_after_first_cover_result">
<property name="suffix">
<string> secs</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_2"/>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../../../../resources/images.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -14,9 +14,9 @@ from calibre.utils.config import read_raw_tweaks, write_tweaks
from calibre.gui2.widgets import PythonHighlighter
from calibre import isbytestring
from PyQt4.Qt import QAbstractListModel, Qt, QStyledItemDelegate, QStyle, \
QStyleOptionViewItem, QFont, QDialogButtonBox, QDialog, \
QVBoxLayout, QPlainTextEdit, QLabel
from PyQt4.Qt import (QAbstractListModel, Qt, QStyledItemDelegate, QStyle,
QStyleOptionViewItem, QFont, QDialogButtonBox, QDialog,
QVBoxLayout, QPlainTextEdit, QLabel)
class Delegate(QStyledItemDelegate): # {{{
def __init__(self, view):
@ -35,8 +35,9 @@ class Delegate(QStyledItemDelegate): # {{{
class Tweak(object): # {{{
def __init__(self, name, doc, var_names, defaults, custom):
self.name = name
self.doc = doc.strip()
translate = __builtins__['_']
self.name = translate(name)
self.doc = translate(doc.strip())
self.var_names = var_names
self.default_values = {}
for x in var_names:

View File

@ -1518,7 +1518,7 @@ class TagsModel(QAbstractItemModel): # {{{
if node.tag.category in \
('tags', 'series', 'authors', 'rating', 'publisher') or \
(fm['is_custom'] and \
fm['datatype'] in ['text', 'rating', 'series']):
fm['datatype'] in ['text', 'rating', 'series', 'enumeration']):
ans |= Qt.ItemIsDropEnabled
else:
ans |= Qt.ItemIsDropEnabled

File diff suppressed because it is too large Load Diff

View File

@ -785,8 +785,6 @@ def write_tweaks(raw):
tweaks = read_tweaks()
test_eight_code = tweaks.get('test_eight_code', False)
# test_eight_code notes
# Change Amazon plugin name to just Amazon
def migrate():
if hasattr(os, 'geteuid') and os.geteuid() == 0:

View File

@ -66,7 +66,7 @@ class HTMLStream(Stream):
color = {
DEBUG: '<span style="color:green">',
INFO:'<span>',
WARN: '<span style="color:yellow">',
WARN: '<span style="color:blue">',
ERROR: '<span style="color:red">'
}
normal = '</span>'