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 @@
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 @@
# 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 @@
"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 @@ def source_files(self):
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 @@ def run(self, opts):
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 @@ def get_metadata(self, stream, ftype):
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 @@ class Misc(PreferencesPlugin):
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

@ -28,7 +28,7 @@ class FB2Output(OutputFormatPlugin):
'sf_horror', # Horror & mystic
'sf_humor', # Humor
'sf_fantasy', # Fantasy
'sf', # Science Fiction
'sf', # Science Fiction
# Detectives & Thrillers
'det_classic', # Classical detectives
'det_police', # Police Stories
@ -41,20 +41,20 @@ class FB2Output(OutputFormatPlugin):
'det_maniac', # Maniacs
'det_hard', # Hard#boiled
'thriller', # Thrillers
'detective', # Detectives
'detective', # Detectives
# Prose
'prose_classic', # Classics prose
'prose_history', # Historical prose
'prose_contemporary', # Contemporary prose
'prose_counter', # Counterculture
'prose_rus_classic', # Russial classics prose
'prose_su_classics', # Soviet classics prose
'prose_su_classics', # Soviet classics prose
# Romance
'love_contemporary', # Contemporary Romance
'love_history', # Historical Romance
'love_detective', # Detective Romance
'love_short', # Short Romance
'love_erotica', # Erotica
'love_erotica', # Erotica
# Adventure
'adv_western', # Western
'adv_history', # History
@ -62,7 +62,7 @@ class FB2Output(OutputFormatPlugin):
'adv_maritime', # Maritime Fiction
'adv_geo', # Travel & geography
'adv_animal', # Nature & animals
'adventure', # Other
'adventure', # Other
# Children's
'child_tale', # Fairy Tales
'child_verse', # Verses
@ -71,17 +71,17 @@ class FB2Output(OutputFormatPlugin):
'child_det', # Detectives & Thrillers
'child_adv', # Adventures
'child_education', # Educational
'children', # Other
'children', # Other
# Poetry & Dramaturgy
'poetry', # Poetry
'dramaturgy', # Dramaturgy
'dramaturgy', # Dramaturgy
# Antique literature
'antique_ant', # Antique
'antique_european', # European
'antique_russian', # Old russian
'antique_east', # Old east
'antique_myths', # Myths. Legends. Epos
'antique', # Other
'antique', # Other
# Scientific#educational
'sci_history', # History
'sci_psychology', # Psychology
@ -98,7 +98,7 @@ class FB2Output(OutputFormatPlugin):
'sci_chem', # Chemistry
'sci_biology', # Biology
'sci_tech', # Technical
'science', # Other
'science', # Other
# Computers & Internet
'comp_www', # Internet
'comp_programming', # Programming
@ -106,29 +106,29 @@ class FB2Output(OutputFormatPlugin):
'comp_soft', # Software
'comp_db', # Databases
'comp_osnet', # OS & Networking
'computers', # Other
'computers', # Other
# Reference
'ref_encyc', # Encyclopedias
'ref_dict', # Dictionaries
'ref_ref', # Reference
'ref_guide', # Guidebooks
'reference', # Other
'reference', # Other
# Nonfiction
'nonf_biography', # Biography & Memoirs
'nonf_publicism', # Publicism
'nonf_criticism', # Criticism
'design', # Art & design
'nonfiction', # Other
'nonfiction', # Other
# Religion & Inspiration
'religion_rel', # Religion
'religion_esoterics', # Esoterics
'religion_self', # Self#improvement
'religion', # Other
'religion', # Other
# Humor
'humor_anecdote', # Anecdote (funny stories)
'humor_prose', # Prose
'humor_verse', # Verses
'humor', # Other
'humor', # Other
# Home & Family
'home_cooking', # Cooking
'home_pets', # Pets
@ -155,14 +155,14 @@ 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.')),
])
def convert(self, oeb_book, output_path, input_plugin, opts, log):
from calibre.ebooks.oeb.transforms.jacket import linearize_jacket
from calibre.ebooks.oeb.transforms.rasterize import SVGRasterizer, Unavailable
try:
rasterizer = SVGRasterizer()
rasterizer(oeb_book, opts)

View file

@ -279,7 +279,7 @@ def parse_language(self, pd):
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 @@ def create_query(self, log, title=None, authors=None, identifiers={}): # {{{
# 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 @@ def __init__(self, mi, source_plugin, title, authors, identifiers):
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 @@ def clean_downloaded_metadata(self, mi):
# 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 @@
__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 get_text(extra, x):
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 @@ def download_cover(self, log, result_queue, abort, # {{{
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 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 get_results():
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 get_results():
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 @@ def options(self):
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 @@ def series_type(r, idx=-1, siix=-1):
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 @@
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 @@ def initialize(self, db, id_):
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 @@ def fset(self, val):
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 @@ def initialize(self, db, id_):
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 @@ def get_selected_format_metadata(self, db, id_):
class Cover(ImageView): # {{{
download_cover = pyqtSignal()
def __init__(self, parent):
ImageView.__init__(self, parent)
self.dialog = parent
@ -703,9 +707,6 @@ def trim_cover(self, *args):
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 @@ def fset(self, val):
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 @@ def fset(self, val):
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 @@ def fset(self, val):
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 @@ def initialize(self, db, id_):
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 @@
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 @@ def create_basic_metadata_widgets(self): # {{{
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 @@ def create_basic_metadata_widgets(self): # {{{
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 @@ def update_from_mi(self, mi):
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 @@ def create_row2(row, widget, button=None):
# }}}
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 @@ def do_layout(self):
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 @@ def do_layout(self):
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 @@ def create_row(row, widget, tab_to, button=None, icon=None, span=1):
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 @@
__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 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 @@ def to_doc(self, index):
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 @@ def data(self, index, role):
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 @@ def sort(self, col, order=Qt.AscendingOrder):
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 @@ def show_details(self, index):
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 @@ def __init__(self, parent=None):
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 @@ def __init__(self, log, parent=None):
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 @@ def start(self, title=None, authors=None, identifiers={}):
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 @@ def plugin_for_index(self, index):
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 @@ def __init__(self, log, current_cover, parent=None):
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 @@ def __init__(self, log, parent=None):
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 @@ def __init__(self, log, parent=None):
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 @@ def update_log(self):
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 @@ def __init__(self, log, current_cover=None, parent=None):
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 @@ def book_selected(self, book):
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 @@ def accept(self):
def reject(self):
self.identify_widget.cancel()
self.covers_widget.cancel()
return QDialog.reject(self)
def cleanup(self):
@ -844,12 +888,65 @@ def start(self, title=None, authors=None, identifiers={}):
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 @@ def genesis(self, gui):
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.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 @@ def paint(self, p, opt, idx):
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 @@ def flags(self, index, *args):
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>'