diff --git a/recipes/brand_eins.recipe b/recipes/brand_eins.recipe index 9b77c7f279..15e1d3ccca 100644 --- a/recipes/brand_eins.recipe +++ b/recipes/brand_eins.recipe @@ -3,8 +3,7 @@ __license__ = 'GPL v3' __copyright__ = '2010, Constantin Hofstetter , Steffen Siebert ' -__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 diff --git a/recipes/dvhn.recipe b/recipes/dvhn.recipe new file mode 100644 index 0000000000..4c093aa9d2 --- /dev/null +++ b/recipes/dvhn.recipe @@ -0,0 +1,32 @@ +from calibre.web.feeds.news import BasicNewsRecipe + +class AdvancedUserRecipe1302341394(BasicNewsRecipe): + title = u'DvhN' + oldest_article = 1 + max_articles_per_feed = 200 + + __author__ = 'Reijndert' + no_stylesheets = True + cover_url = 'http://www.dvhn.nl/template/Dagblad_v2.0/gfx/logo_DvhN.gif' + language = 'nl' + country = 'NL' + version = 1 + publisher = u'Dagblad van het Noorden' + category = u'Nieuws' + description = u'Nieuws uit Noord Nederland' + + + keep_only_tags = [dict(name='div', attrs={'id':'fullPicture'}) + ,dict(name='div', attrs={'id':'articleText'}) + ] + + remove_tags = [ + dict(name=['object','link','iframe','base']) + ,dict(name='span',attrs={'class':'copyright'}) + ] + + feeds = [(u'Drenthe', u'http://www.dvhn.nl/nieuws/drenthe/index.jsp?service=rss'), (u'Groningen', u'http://www.dvhn.nl/nieuws/groningen/index.jsp?service=rss'), (u'Nederland', u'http://www.dvhn.nl/nieuws/nederland/index.jsp?service=rss'), (u'Wereld', u'http://www.dvhn.nl/nieuws/wereld/index.jsp?service=rss'), (u'Economie', u'http://www.dvhn.nl/nieuws/economie/index.jsp?service=rss'), (u'Sport', u'http://www.dvhn.nl/nieuws/sport/index.jsp?service=rss'), (u'Cultuur', u'http://www.dvhn.nl/nieuws/kunst/index.jsp?service=rss'), (u'24 Uur', u'http://www.dvhn.nl/nieuws/24uurdvhn/index.jsp?service=rss&selectiontype=last24hours')] + + extra_css = ''' + body {font-family: verdana, arial, helvetica, geneva, sans-serif;} + ''' diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index 1cf699efa3..c4c951f980 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -88,13 +88,6 @@ categories_collapsed_rating_template = r'{first.avg_rating:4.2f:ifempty(0)} - {l categories_collapsed_popularity_template = r'{first.count:d} - {last.count:d}' -#: Set boolean custom columns to be tristate -# Set whether boolean custom columns are two- or three-valued. -# Two-values for true booleans -# three-values for yes/no/unknown -# Set to 'yes' for three-values, 'no' for two-values -bool_custom_columns_are_tristate = 'yes' - #: Specify columns to sort the booklist by on startup # Provide a set of columns to be sorted on when calibre starts # The argument is None if saved sort history is to be used diff --git a/resources/images/connect_share_on.png b/resources/images/connect_share_on.png new file mode 100644 index 0000000000..3d431be1a1 Binary files /dev/null and b/resources/images/connect_share_on.png differ diff --git a/setup/pygettext.py b/setup/pygettext.py index bc171396f4..322758871d 100644 --- a/setup/pygettext.py +++ b/setup/pygettext.py @@ -170,8 +170,8 @@ from setup import __appname__, __version__ as version # there. pot_header = '''\ # Translation template file.. -# Copyright (C) 2007 Kovid Goyal -# Kovid Goyal , 2007. +# Copyright (C) %(year)s Kovid Goyal +# Kovid Goyal , %(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=''): diff --git a/setup/translations.py b/setup/translations.py index 7f81abf8f5..1f026555ec 100644 --- a/setup/translations.py +++ b/setup/translations.py @@ -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 diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 8dbc72f8ac..470b4f5a35 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -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) + #}}} # New metadata download plugins {{{ diff --git a/src/calibre/customize/ui.py b/src/calibre/customize/ui.py index 9c8f80544b..e8011e9ad8 100644 --- a/src/calibre/customize/ui.py +++ b/src/calibre/customize/ui.py @@ -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 {{{ diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 7702a7caf0..a63ce8c581 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -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'] diff --git a/src/calibre/devices/edge/driver.py b/src/calibre/devices/edge/driver.py index d14763f313..9491b9bc68 100644 --- a/src/calibre/devices/edge/driver.py +++ b/src/calibre/devices/edge/driver.py @@ -26,9 +26,9 @@ class EDGE(USBMS): PRODUCT_ID = [0x0c02] BCD = [0x0223] - VENDOR_NAME = 'ANDROID' - WINDOWS_MAIN_MEM = '__FILE-STOR_GADG' - WINDOWS_CARD_A_MEM = '__FILE-STOR_GADG' + VENDOR_NAME = ['ANDROID', 'LINUX'] + WINDOWS_MAIN_MEM = ['__FILE-STOR_GADG', 'FILE-CD_GADGET'] + WINDOWS_CARD_A_MEM = ['__FILE-STOR_GADG', 'FILE-CD_GADGET'] MAIN_MEMORY_VOLUME_LABEL = 'Edge Main Memory' STORAGE_CARD_VOLUME_LABEL = 'Edge Storage Card' diff --git a/src/calibre/ebooks/fb2/output.py b/src/calibre/ebooks/fb2/output.py index bccc665d35..54bb4550d5 100644 --- a/src/calibre/ebooks/fb2/output.py +++ b/src/calibre/ebooks/fb2/output.py @@ -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) diff --git a/src/calibre/ebooks/metadata/sources/amazon.py b/src/calibre/ebooks/metadata/sources/amazon.py index b070132de9..24df68e51d 100644 --- a/src/calibre/ebooks/metadata/sources/amazon.py +++ b/src/calibre/ebooks/metadata/sources/amazon.py @@ -279,12 +279,13 @@ class Worker(Thread): # Get details {{{ class Amazon(Source): - name = 'Amazon Store' + name = 'Amazon.com' description = _('Downloads metadata from Amazon') capabilities = frozenset(['identify', 'cover']) touched_fields = frozenset(['title', 'authors', 'identifier:amazon', - 'identifier:isbn', 'rating', 'comments', 'publisher', 'pubdate']) + 'identifier:isbn', 'rating', 'comments', 'publisher', 'pubdate', + 'language']) has_html_comments = True supports_gzip_transfer_encoding = True @@ -295,6 +296,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 +342,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 # }}} diff --git a/src/calibre/ebooks/metadata/sources/base.py b/src/calibre/ebooks/metadata/sources/base.py index d2a3c05bb9..00c6ea5e09 100644 --- a/src/calibre/ebooks/metadata/sources/base.py +++ b/src/calibre/ebooks/metadata/sources/base.py @@ -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 '') @@ -131,7 +131,22 @@ def fixcase(x): x = titlecase(x) return x +class Option(object): + __slots__ = ['type', 'default', 'label', 'desc', 'name', 'choices'] + def __init__(self, name, type_, default, label, desc, choices=None): + ''' + :param name: The name of this option. Must be a valid python identifier + :param type_: The type of this option, one of ('number', 'string', + 'bool', 'choices') + :param default: The default value for this option + :param label: A short (few words) description of this option + :param desc: A longer description of this option + :param choices: A list of possible values, used only if type='choices' + ''' + self.name, self.type, self.default, self.label, self.desc = (name, + type_, default, label, desc) + self.choices = choices class Source(Plugin): @@ -157,6 +172,16 @@ 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 often the case + #: with this source set to False + cached_cover_url_is_reliable = True + + #: A list of :class:`Option` objects. They will be used to automatically + #: construct the configuration widget for this plugin + options = () + + def __init__(self, *args, **kwargs): Plugin.__init__(self, *args, **kwargs) self._isbn_to_identifier_cache = {} @@ -164,6 +189,9 @@ class Source(Plugin): self.cache_lock = threading.RLock() self._config_obj = None self._browser = None + self.prefs.defaults['ignore_fields'] = [] + for opt in self.options: + self.prefs.defaults[opt.name] = opt.default # Configuration {{{ @@ -174,6 +202,16 @@ class Source(Plugin): ''' return True + def is_customizable(self): + return True + + def config_widget(self): + from calibre.gui2.metadata.config import ConfigWidget + return ConfigWidget(self) + + def save_settings(self, config_widget): + config_widget.commit() + @property def prefs(self): if self._config_obj is None: @@ -309,6 +347,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 diff --git a/src/calibre/ebooks/metadata/sources/google.py b/src/calibre/ebooks/metadata/sources/google.py index 47cfb823bb..4133d4d527 100644 --- a/src/calibre/ebooks/metadata/sources/google.py +++ b/src/calibre/ebooks/metadata/sources/google.py @@ -7,7 +7,7 @@ __license__ = 'GPL v3' __copyright__ = '2011, Kovid Goyal ' __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) diff --git a/src/calibre/ebooks/metadata/sources/identify.py b/src/calibre/ebooks/metadata/sources/identify.py index 85549904e7..fad810c26e 100644 --- a/src/calibre/ebooks/metadata/sources/identify.py +++ b/src/calibre/ebooks/metadata/sources/identify.py @@ -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)) @@ -356,16 +357,29 @@ def identify(log, abort, # {{{ if r.plugin.has_html_comments and r.comments: r.comments = html2text(r.comments) - dummy = Metadata(_('Unknown')) max_tags = msprefs['max_tags'] for r in results: - for f in msprefs['ignore_fields']: - setattr(r, f, getattr(dummy, f)) r.tags = r.tags[:max_tags] 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 diff --git a/src/calibre/ebooks/metadata/sources/openlibrary.py b/src/calibre/ebooks/metadata/sources/openlibrary.py index 19b8747265..4645d2a18a 100644 --- a/src/calibre/ebooks/metadata/sources/openlibrary.py +++ b/src/calibre/ebooks/metadata/sources/openlibrary.py @@ -12,7 +12,7 @@ from calibre.ebooks.metadata.sources.base import Source class OpenLibrary(Source): name = 'Open Library' - description = _('Downloads metadata from The Open Library') + description = _('Downloads covers from The Open Library') capabilities = frozenset(['cover']) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index ddef5e1265..22aaabf592 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -81,6 +81,7 @@ gprefs.defaults['toolbar_text'] = 'auto' gprefs.defaults['font'] = None gprefs.defaults['tags_browser_partition_method'] = 'first letter' gprefs.defaults['tags_browser_collapse_at'] = 100 +gprefs.defaults['edit_metadata_single_layout'] = 'default' # }}} diff --git a/src/calibre/gui2/actions/device.py b/src/calibre/gui2/actions/device.py index 64bc4e69d7..debcbb6c1a 100644 --- a/src/calibre/gui2/actions/device.py +++ b/src/calibre/gui2/actions/device.py @@ -165,6 +165,10 @@ class ConnectShareAction(InterfaceAction): def content_server_state_changed(self, running): self.share_conn_menu.server_state_changed(running) + if running: + self.qaction.setIcon(QIcon(I('connect_share_on.png'))) + else: + self.qaction.setIcon(QIcon(I('connect_share.png'))) def toggle_content_server(self): if self.gui.content_server is None: diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.py b/src/calibre/gui2/catalog/catalog_epub_mobi.py index d5149569be..8af72e51c0 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.py +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.py @@ -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()): diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index e102c35fbe..e98817a02f 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -308,22 +308,47 @@ class MenuBar(QMenuBar): # {{{ ac.setMenu(m) return ac - - # }}} -class ToolBar(QToolBar): # {{{ +class BaseToolBar(QToolBar): # {{{ - def __init__(self, donate, location_manager, child_bar, parent): + def __init__(self, parent): QToolBar.__init__(self, parent) - self.gui = parent - self.child_bar = child_bar self.setContextMenuPolicy(Qt.PreventContextMenu) self.setMovable(False) self.setFloatable(False) self.setOrientation(Qt.Horizontal) self.setAllowedAreas(Qt.TopToolBarArea|Qt.BottomToolBarArea) self.setStyleSheet('QToolButton:checked { font-weight: bold }') + self.preferred_width = self.sizeHint().width() + + def resizeEvent(self, ev): + QToolBar.resizeEvent(self, ev) + style = self.get_text_style() + self.setToolButtonStyle(style) + + def get_text_style(self): + style = Qt.ToolButtonTextUnderIcon + s = gprefs['toolbar_icon_size'] + if s != 'off': + p = gprefs['toolbar_text'] + if p == 'never': + style = Qt.ToolButtonIconOnly + elif p == 'auto' and self.preferred_width > self.width()+35: + style = Qt.ToolButtonIconOnly + return style + + def contextMenuEvent(self, *args): + pass + +# }}} + +class ToolBar(BaseToolBar): # {{{ + + def __init__(self, donate, location_manager, child_bar, parent): + BaseToolBar.__init__(self, parent) + self.gui = parent + self.child_bar = child_bar self.donate_button = donate self.apply_settings() @@ -333,7 +358,6 @@ class ToolBar(QToolBar): # {{{ donate.setCursor(Qt.PointingHandCursor) self.added_actions = [] self.build_bar() - self.preferred_width = self.sizeHint().width() self.setAcceptDrops(True) def apply_settings(self): @@ -348,9 +372,6 @@ class ToolBar(QToolBar): # {{{ self.child_bar.setToolButtonStyle(style) self.donate_button.set_normal_icon_size(sz, sz) - def contextMenuEvent(self, *args): - pass - def build_bar(self): self.showing_donate = False showing_device = self.location_manager.has_device @@ -394,6 +415,8 @@ class ToolBar(QToolBar): # {{{ bar.addAction(action.qaction) self.added_actions.append(action.qaction) self.setup_tool_button(bar, action.qaction, action.popup_type) + self.preferred_width = self.sizeHint().width() + self.child_bar.preferred_width = self.child_bar.sizeHint().width() def setup_tool_button(self, bar, ac, menu_mode=None): ch = bar.widgetForAction(ac) @@ -405,21 +428,6 @@ class ToolBar(QToolBar): # {{{ ch.setPopupMode(menu_mode) return ch - def resizeEvent(self, ev): - QToolBar.resizeEvent(self, ev) - style = Qt.ToolButtonTextUnderIcon - s = gprefs['toolbar_icon_size'] - if s != 'off': - p = gprefs['toolbar_text'] - if p == 'never': - style = Qt.ToolButtonIconOnly - - if p == 'auto' and self.preferred_width > self.width()+35 and \ - not gprefs['action-layout-toolbar-child']: - style = Qt.ToolButtonIconOnly - - self.setToolButtonStyle(style) - def database_changed(self, db): pass @@ -497,7 +505,7 @@ class MainWindowMixin(object): # {{{ self.iactions['Fetch News'].init_scheduler(db) self.search_bar = SearchBar(self) - self.child_bar = QToolBar(self) + self.child_bar = BaseToolBar(self) self.tool_bar = ToolBar(self.donate_button, self.location_manager, self.child_bar, self) self.addToolBar(Qt.TopToolBarArea, self.tool_bar) diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 653a8ea2f6..f7074a6fee 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -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): diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py index b0b7115ca1..b2ee79c9c0 100644 --- a/src/calibre/gui2/metadata/basic_widgets.py +++ b/src/calibre/gui2/metadata/basic_widgets.py @@ -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: diff --git a/src/calibre/gui2/metadata/config.py b/src/calibre/gui2/metadata/config.py new file mode 100644 index 0000000000..68c935061d --- /dev/null +++ b/src/calibre/gui2/metadata/config.py @@ -0,0 +1,122 @@ +#!/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 ' +__docformat__ = 'restructuredtext en' + +import textwrap + +from PyQt4.Qt import (QWidget, QGridLayout, QGroupBox, QListView, Qt, QSpinBox, + QDoubleSpinBox, QCheckBox, QLineEdit, QComboBox, QLabel) + +from calibre.gui2.preferences.metadata_sources import FieldsModel as FM + +class FieldsModel(FM): # {{{ + + def __init__(self, plugin): + FM.__init__(self) + self.plugin = plugin + self.exclude = frozenset(['title', 'authors']) | self.exclude + self.prefs = self.plugin.prefs + + def initialize(self): + fields = self.plugin.touched_fields + self.fields = [] + for x in fields: + if not x.startswith('identifier:') and x not in self.exclude: + self.fields.append(x) + self.fields.sort(key=lambda x:self.descs.get(x, x)) + self.reset() + + def state(self, field, defaults=False): + src = self.prefs.defaults if defaults else self.prefs + return (Qt.Unchecked if field in src['ignore_fields'] + else Qt.Checked) + + def restore_defaults(self): + self.overrides = dict([(f, self.state(f, True)) for f in self.fields]) + self.reset() + + def commit(self): + val = [k for k, v in self.overrides.iteritems() if v == Qt.Unchecked] + self.prefs['ignore_fields'] = val + +# }}} + +class ConfigWidget(QWidget): + + def __init__(self, plugin): + QWidget.__init__(self) + self.plugin = plugin + + self.l = l = QGridLayout() + self.setLayout(l) + + self.gb = QGroupBox(_('Downloaded metadata fields'), self) + l.addWidget(self.gb, 0, 0, 1, 2) + self.gb.l = QGridLayout() + self.gb.setLayout(self.gb.l) + self.fields_view = v = QListView(self) + self.gb.l.addWidget(v, 0, 0) + v.setFlow(v.LeftToRight) + v.setWrapping(True) + v.setResizeMode(v.Adjust) + self.fields_model = FieldsModel(self.plugin) + self.fields_model.initialize() + v.setModel(self.fields_model) + + self.memory = [] + self.widgets = [] + for opt in plugin.options: + self.create_widgets(opt) + + def create_widgets(self, opt): + val = self.plugin.prefs[opt.name] + if opt.type == 'number': + c = QSpinBox if isinstance(opt.default, int) else QDoubleSpinBox + widget = c(self) + widget.setValue(val) + elif opt.type == 'string': + widget = QLineEdit(self) + widget.setText(val) + elif opt.type == 'bool': + widget = QCheckBox(opt.label, self) + widget.setChecked(bool(val)) + elif opt.type == 'choices': + widget = QComboBox(self) + for x in opt.choices: + widget.addItem(x) + idx = opt.choices.index(val) + widget.setCurrentIndex(idx) + widget.opt = opt + widget.setToolTip(textwrap.fill(opt.desc)) + self.widgets.append(widget) + r = self.l.rowCount() + if opt.type == 'bool': + self.l.addWidget(widget, r, 0, 1, self.l.columnCount()) + else: + l = QLabel(opt.label) + l.setToolTip(widget.toolTip()) + self.memory.append(l) + l.setBuddy(widget) + self.l.addWidget(l, r, 0, 1, 1) + self.l.addWidget(widget, r, 1, 1, 1) + + + def commit(self): + self.fields_model.commit() + for w in self.widgets: + if isinstance(w, (QSpinBox, QDoubleSpinBox)): + val = w.value() + elif isinstance(w, QLineEdit): + val = unicode(w.text()) + elif isinstance(w, QCheckBox): + val = w.isChecked() + elif isinstance(w, QComboBox): + val = unicode(w.currentText()) + self.plugin.prefs[w.opt.name] = val + + diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index cba877b249..4ce76d8cc8 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -16,13 +16,15 @@ 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 +from calibre.ebooks.metadata.book.base import Metadata class MetadataSingleDialogBase(ResizableDialog): @@ -132,6 +134,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,12 +161,17 @@ 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) self.fetch_metadata_button.setFont(font) + self.config_metadata_button = QToolButton(self) + self.config_metadata_button.setIcon(QIcon(I('config.png'))) + self.config_metadata_button.clicked.connect(self.configure_metadata) + self.config_metadata_button.setToolTip( + _('Change how calibre downloads metadata')) # }}} @@ -303,7 +311,36 @@ 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: + from calibre.ebooks.metadata.sources.base import msprefs + mi = d.book + dummy = Metadata(_('Unknown')) + for f in msprefs['ignore_fields']: + setattr(mi, f, getattr(dummy, f)) + 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 configure_metadata(self): + from calibre.gui2.preferences import show_config_widget + gui = self.parent() + show_config_widget('Sharing', 'Metadata download', parent=self, + gui=gui, never_shutdown=True) + + 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): @@ -430,7 +467,8 @@ class MetadataSingleDialog(MetadataSingleDialogBase): # {{{ sto = QWidget.setTabOrder sto(self.button_box, self.fetch_metadata_button) - sto(self.fetch_metadata_button, self.title) + sto(self.fetch_metadata_button, self.config_metadata_button) + sto(self.config_metadata_button, self.title) def create_row(row, one, two, three, col=1, icon='forward.png'): ql = BuddyLabel(one) @@ -509,7 +547,8 @@ class MetadataSingleDialog(MetadataSingleDialogBase): # {{{ self.tabs[0].spc_two = QSpacerItem(10, 10, QSizePolicy.Expanding, QSizePolicy.Expanding) l.addItem(self.tabs[0].spc_two, 8, 0, 1, 3) - l.addWidget(self.fetch_metadata_button, 9, 0, 1, 3) + l.addWidget(self.fetch_metadata_button, 9, 0, 1, 2) + l.addWidget(self.config_metadata_button, 9, 2, 1, 1) self.tabs[0].gb2 = gb = QGroupBox(_('Co&mments'), self) gb.l = l = QVBoxLayout() @@ -521,18 +560,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 +598,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 +610,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 +701,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) @@ -658,7 +719,7 @@ editors = {'default': MetadataSingleDialog, 'alt1': MetadataSingleDialogAlt1} def edit_metadata(db, row_list, current_row, parent=None, view_slot=None, set_current_callback=None): - cls = db.prefs.get('edit_metadata_single_layout', '') + cls = gprefs.get('edit_metadata_single_layout', '') if cls not in editors: cls = 'default' d = editors[cls](db, parent) diff --git a/src/calibre/gui2/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py index 7fa052844f..8f01c6df1e 100644 --- a/src/calibre/gui2/metadata/single_download.py +++ b/src/calibre/gui2/metadata/single_download.py @@ -7,23 +7,31 @@ __license__ = 'GPL v3' __copyright__ = '2011, Kovid Goyal ' __docformat__ = 'restructuredtext en' +DEBUG_DIALOG = False + +# Imports {{{ from threading import Thread, Event from operator import attrgetter +from Queue import Queue, Empty from PyQt4.Qt import (QStyledItemDelegate, QTextDocument, QRectF, QIcon, Qt, QStyle, QApplication, QDialog, QVBoxLayout, QLabel, QDialogButtonBox, QStackedWidget, QWidget, QTableView, QGridLayout, QFontInfo, QPalette, - QTimer, pyqtSignal, QAbstractTableModel, QVariant, QSize) + QTimer, pyqtSignal, QAbstractTableModel, QVariant, QSize, QListView, + QPixmap, QAbstractListModel, QColor, QRect, QTextBrowser) 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 +# }}} class RichTextDelegate(QStyledItemDelegate): # {{{ @@ -36,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 @@ -52,6 +63,65 @@ class RichTextDelegate(QStyledItemDelegate): # {{{ painter.restore() # }}} +class CoverDelegate(QStyledItemDelegate): # {{{ + + needs_redraw = pyqtSignal() + + def __init__(self, parent): + QStyledItemDelegate.__init__(self, parent) + + self.angle = 0 + self.timer = QTimer(self) + self.timer.timeout.connect(self.frame_changed) + self.color = parent.palette().color(QPalette.WindowText) + self.spinner_width = 64 + + def frame_changed(self, *args): + self.angle = (self.angle+30)%360 + self.needs_redraw.emit() + + def start_animation(self): + self.angle = 0 + self.timer.start(200) + + def stop_animation(self): + self.timer.stop() + + def draw_spinner(self, painter, rect): + width = rect.width() + + outer_radius = (width-1)*0.5 + inner_radius = (width-1)*0.5*0.38 + + capsule_height = outer_radius - inner_radius + capsule_width = int(capsule_height * (0.23 if width > 32 else 0.35)) + capsule_radius = capsule_width//2 + + painter.save() + painter.setRenderHint(painter.Antialiasing) + + for i in xrange(12): + color = QColor(self.color) + color.setAlphaF(1.0 - (i/12.0)) + painter.setPen(Qt.NoPen) + painter.setBrush(color) + painter.save() + painter.translate(rect.center()) + painter.rotate(self.angle - i*30.0) + painter.drawRoundedRect(-capsule_width*0.5, + -(inner_radius+capsule_height), capsule_width, + capsule_height, capsule_radius, capsule_radius) + painter.restore() + painter.restore() + + def paint(self, painter, option, index): + QStyledItemDelegate.paint(self, painter, option, index) + if self.timer.isActive() and index.data(Qt.UserRole).toBool(): + rect = QRect(0, 0, self.spinner_width, self.spinner_width) + rect.moveCenter(option.rect.center()) + self.draw_spinner(painter, rect) +# }}} + class ResultsModel(QAbstractTableModel): # {{{ COLUMNS = ( @@ -110,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): @@ -119,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: @@ -170,6 +247,11 @@ class ResultsView(QTableView): # {{{ if not book.is_null('rating'): parts.append('
%s
'%('\u2605'*int(book.rating))) parts.append('') + if book.identifiers: + urls = urls_from_identifiers(book.identifiers) + ids = ['%s'%(url, name) for name, url in urls] + if ids: + parts.append('
%s: %s

'%(_('See at'), ', '.join(ids))) if book.tags: parts.append('
%s
\u00a0
'%', '.join(book.tags)) if book.comments: @@ -201,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) @@ -268,7 +358,7 @@ class IdentifyWorker(Thread): # {{{ def run(self): try: - if True: + if DEBUG_DIALOG: self.results = self.sample_results() else: self.results = identify(self.log, self.abort, title=self.title, @@ -277,7 +367,7 @@ class IdentifyWorker(Thread): # {{{ result.gui_rank = i except: import traceback - self.error = traceback.format_exc() + self.error = force_unicode(traceback.format_exc()) # }}} class IdentifyWidget(QWidget): # {{{ @@ -318,7 +408,7 @@ class IdentifyWidget(QWidget): # {{{ self.query.setWordWrap(True) l.addWidget(self.query, 2, 0, 1, 2) - self.comments_view.show_data('

'+_('Downloading')+ + self.comments_view.show_data('

'+_('Please wait')+ '
.

'+ '''