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/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 298799daa5..8f6c597ee5 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) + #}}} 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/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 4722873f77..2ad5bfbacc 100644 --- a/src/calibre/ebooks/metadata/sources/amazon.py +++ b/src/calibre/ebooks/metadata/sources/amazon.py @@ -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 # }}} diff --git a/src/calibre/ebooks/metadata/sources/base.py b/src/calibre/ebooks/metadata/sources/base.py index d4e090084c..67a80d5785 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 '') @@ -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 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..cd658a0daf 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)) @@ -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 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/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/single.py b/src/calibre/gui2/metadata/single.py index 2ef183fca5..175af291f1 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -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) diff --git a/src/calibre/gui2/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py index 35c66340c6..8f01c6df1e 100644 --- a/src/calibre/gui2/metadata/single_download.py +++ b/src/calibre/gui2/metadata/single_download.py @@ -7,6 +7,9 @@ __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 @@ -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('
%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: @@ -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('

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

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

'+ '''