diff --git a/Changelog.yaml b/Changelog.yaml index c750c4a5f3..24ec402a49 100644 --- a/Changelog.yaml +++ b/Changelog.yaml @@ -19,6 +19,45 @@ # new recipes: # - title: +- version: 0.7.56 + date: 2011-04-17 + + new features: + - title: "This is primarily a bug fix release that fixes a bug in 0.7.55 that caused calibre to rescan the files on the device every time the device is connected. If you updated to 0.7.55 it is highly recommended you update to 0.7.56" + + - title: "Device driver for Coby Kyros" + + - title: "Remove the quick access to search options from next to the search bar, as we now have a separate search highlights toggle button" + + - title: "MOBI Output: Ensure that MOBI files always have 8KB worth of null bytes at the end of record 0. This appears to be necessary for Amazon to be able to add DRM to calibre generated MOBI files sent to their publishing service." + + - title: "Add a tool to inspect MOBI files. To use: calibre-debug -m file.mobi" + + bug fixes: + - title: "Fixed regression taht caused calibre to rescan files on the device on every reconnect" + + - title: "Fix donate button causing the toolbar to be too large on OS X" + + - title: "MOBI Input: Fix detection of Table of Contents for MOBI files that have a page break between the location designated as the Table of Contents and the actual table of contents." + tickets: [763504] + + - title: "Comic Input: Fix handling of some CBZ files that have wrongly encoded non ASCII filenames on windows." + tickets: [763280] + + - title: "PML Input: Fix multi-line chapter title causing a spurious page break" + tickets: [763238] + + - title: "EPUB Input: Speed up processing of files with very large manifest/spines" + + - title: "Fix regression that broke cover:False searches in 0.7.55" + + improved recipes: + - Suedduetsche Zeitung + - Irish Times + - Big Oven + - NSPM + + - version: 0.7.55 date: 2011-04-15 diff --git a/resources/images/store.png b/resources/images/store.png new file mode 100644 index 0000000000..947fb794b8 Binary files /dev/null and b/resources/images/store.png differ diff --git a/src/calibre/__init__.py b/src/calibre/__init__.py index 2f457bf2bc..0fddb9de9d 100644 --- a/src/calibre/__init__.py +++ b/src/calibre/__init__.py @@ -5,7 +5,9 @@ __docformat__ = 'restructuredtext en' import uuid, sys, os, re, logging, time, random, \ __builtin__, warnings, multiprocessing +from contextlib import closing from urllib import getproxies +from urllib2 import unquote as urllib2_unquote __builtin__.__dict__['dynamic_property'] = lambda(func): func(None) from htmlentitydefs import name2codepoint from math import floor @@ -290,6 +292,9 @@ def get_parsed_proxy(typ='http', debug=True): prints('Using http proxy', str(ans)) return ans +USER_AGENT = 'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.13) Gecko/20101210 Gentoo Firefox/3.6.13' +USER_AGENT_MOBILE = 'Mozilla/5.0 (Windows; U; Windows CE 5.1; rv:1.8.1a3) Gecko/20060610 Minimo/0.016' + def random_user_agent(): choices = [ 'Mozilla/5.0 (Windows NT 5.2; rv:2.0.1) Gecko/20100101 Firefox/4.0.1', @@ -305,7 +310,6 @@ def random_user_agent(): #return choices[-1] return choices[random.randint(0, len(choices)-1)] - def browser(honor_time=True, max_time=2, mobile_browser=False, user_agent=None): ''' Create a mechanize browser for web scraping. The browser handles cookies, @@ -319,8 +323,7 @@ def browser(honor_time=True, max_time=2, mobile_browser=False, user_agent=None): opener.set_handle_refresh(True, max_time=max_time, honor_time=honor_time) opener.set_handle_robots(False) if user_agent is None: - user_agent = ' Mozilla/5.0 (Windows; U; Windows CE 5.1; rv:1.8.1a3) Gecko/20060610 Minimo/0.016' if mobile_browser else \ - 'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.13) Gecko/20101210 Gentoo Firefox/3.6.13' + user_agent = USER_AGENT_MOBILE if mobile_browser else USER_AGENT opener.addheaders = [('User-agent', user_agent)] http_proxy = get_proxies().get('http', None) if http_proxy: @@ -537,7 +540,49 @@ def as_unicode(obj, enc=preferred_encoding): obj = repr(obj) return force_unicode(obj, enc=enc) +def url_slash_cleaner(url): + ''' + Removes redundant /'s from url's. + ''' + return re.sub(r'(?" import re, importlib diff --git a/src/calibre/customize/__init__.py b/src/calibre/customize/__init__.py index 816c275f72..c0996442ba 100644 --- a/src/calibre/customize/__init__.py +++ b/src/calibre/customize/__init__.py @@ -602,3 +602,35 @@ class PreferencesPlugin(Plugin): # {{{ # }}} +class StoreBase(Plugin): # {{{ + + supported_platforms = ['windows', 'osx', 'linux'] + author = 'John Schember' + type = _('Store') + + actual_plugin = None + + def load_actual_plugin(self, gui): + ''' + This method must return the actual interface action plugin object. + ''' + mod, cls = self.actual_plugin.split(':') + self.actual_plugin_object = getattr(importlib.import_module(mod), cls)(gui, self.name) + return self.actual_plugin_object + + def customization_help(self, gui=False): + if getattr(self, 'actual_plugin_object', None) is not None: + return self.actual_plugin_object.customization_help(gui) + raise NotImplementedError() + + def config_widget(self): + if getattr(self, 'actual_plugin_object', None) is not None: + return self.actual_plugin_object.config_widget() + raise NotImplementedError() + + def save_settings(self, config_widget): + if getattr(self, 'actual_plugin_object', None) is not None: + return self.actual_plugin_object.save_settings(config_widget) + raise NotImplementedError() + +# }}} diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index d5957eb70a..d3b0b8409d 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -5,7 +5,7 @@ __copyright__ = '2008, Kovid Goyal ' import textwrap, os, glob, functools, re from calibre import guess_type from calibre.customize import FileTypePlugin, MetadataReaderPlugin, \ - MetadataWriterPlugin, PreferencesPlugin, InterfaceActionBase + MetadataWriterPlugin, PreferencesPlugin, InterfaceActionBase, StoreBase from calibre.constants import numeric_version from calibre.ebooks.metadata.archive import ArchiveExtract, get_cbz_metadata from calibre.ebooks.metadata.opf2 import metadata_to_opf @@ -854,6 +854,11 @@ class ActionNextMatch(InterfaceActionBase): name = 'Next Match' actual_plugin = 'calibre.gui2.actions.next_match:NextMatchAction' +class ActionStore(InterfaceActionBase): + name = 'Store' + author = 'John Schember' + actual_plugin = 'calibre.gui2.actions.store:StoreAction' + plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog, ActionConvert, ActionDelete, ActionEditMetadata, ActionView, ActionFetchNews, ActionSaveToDisk, ActionShowBookDetails, @@ -862,6 +867,9 @@ plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog, ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary, ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch] +if test_eight_code: + plugins += [ActionStore] + # }}} # Preferences Plugins {{{ @@ -1093,4 +1101,81 @@ if test_eight_code: #}}} +# Store plugins {{{ +class StoreAmazonKindleStore(StoreBase): + name = 'Amazon Kindle' + description = _('Kindle books from Amazon') + actual_plugin = 'calibre.gui2.store.amazon_plugin:AmazonKindleStore' +class StoreBaenWebScriptionStore(StoreBase): + name = 'Baen WebScription' + description = _('Ebooks for readers.') + actual_plugin = 'calibre.gui2.store.baen_webscription_plugin:BaenWebScriptionStore' + +class StoreBNStore(StoreBase): + name = 'Barnes and Noble' + description = _('Books, Textbooks, eBooks, Toys, Games and More.') + actual_plugin = 'calibre.gui2.store.bn_plugin:BNStore' + +class StoreBeWriteStore(StoreBase): + name = 'BeWrite Books' + description = _('Publishers of fine books.') + actual_plugin = 'calibre.gui2.store.bewrite_plugin:BeWriteStore' + +class StoreDieselEbooksStore(StoreBase): + name = 'Diesel eBooks' + description = _('World Famous eBook Store.') + actual_plugin = 'calibre.gui2.store.diesel_ebooks_plugin:DieselEbooksStore' + +class StoreEbookscomStore(StoreBase): + name = 'eBooks.com' + description = _('The digital bookstore.') + actual_plugin = 'calibre.gui2.store.ebooks_com_plugin:EbookscomStore' + +class StoreEHarlequinStoretore(StoreBase): + name = 'eHarlequin' + description = _('entertain, enrich, inspire.') + actual_plugin = 'calibre.gui2.store.eharlequin_plugin:EHarlequinStore' + +class StoreFeedbooksStore(StoreBase): + name = 'Feedbooks' + description = _('Read anywhere.') + actual_plugin = 'calibre.gui2.store.feedbooks_plugin:FeedbooksStore' + +class StoreGutenbergStore(StoreBase): + name = 'Project Gutenberg' + description = _('The first producer of free ebooks.') + actual_plugin = 'calibre.gui2.store.gutenberg_plugin:GutenbergStore' + +class StoreKoboStore(StoreBase): + name = 'Kobo' + description = _('eReading: anytime. anyplace.') + actual_plugin = 'calibre.gui2.store.kobo_plugin:KoboStore' + +class StoreManyBooksStore(StoreBase): + name = 'ManyBooks' + description = _('The best ebooks at the best price: free!') + actual_plugin = 'calibre.gui2.store.manybooks_plugin:ManyBooksStore' + +class StoreMobileReadStore(StoreBase): + name = 'MobileRead' + description = _('Ebooks handcrafted with the utmost care') + actual_plugin = 'calibre.gui2.store.mobileread_plugin:MobileReadStore' + +class StoreOpenLibraryStore(StoreBase): + name = 'Open Library' + description = _('One web page for every book.') + actual_plugin = 'calibre.gui2.store.open_library_plugin:OpenLibraryStore' + +class StoreSmashwordsStore(StoreBase): + name = 'Smashwords' + description = _('Your ebook. Your way.') + actual_plugin = 'calibre.gui2.store.smashwords_plugin:SmashwordsStore' + +plugins += [StoreAmazonKindleStore, StoreBaenWebScriptionStore, StoreBNStore, + StoreBeWriteStore, StoreDieselEbooksStore, StoreEbookscomStore, + StoreEHarlequinStoretore, + StoreFeedbooksStore, StoreGutenbergStore, StoreKoboStore, StoreManyBooksStore, + StoreMobileReadStore, StoreOpenLibraryStore, StoreSmashwordsStore] + +# }}} diff --git a/src/calibre/customize/ui.py b/src/calibre/customize/ui.py index e8011e9ad8..c58f36524e 100644 --- a/src/calibre/customize/ui.py +++ b/src/calibre/customize/ui.py @@ -7,7 +7,8 @@ import os, shutil, traceback, functools, sys from calibre.customize import (CatalogPlugin, FileTypePlugin, PluginNotFound, MetadataReaderPlugin, MetadataWriterPlugin, InterfaceActionBase as InterfaceAction, - PreferencesPlugin, platform, InvalidPlugin) + PreferencesPlugin, platform, InvalidPlugin, + StoreBase as Store) from calibre.customize.conversion import InputFormatPlugin, OutputFormatPlugin from calibre.customize.zipplugin import loader from calibre.customize.profiles import InputProfile, OutputProfile @@ -244,6 +245,17 @@ def preferences_plugins(): yield plugin # }}} +# Store Plugins # {{{ + +def store_plugins(): + customization = config['plugin_customization'] + for plugin in _initialized_plugins: + if isinstance(plugin, Store): + if not is_disabled(plugin): + plugin.site_customization = customization.get(plugin.name, '') + yield plugin +# }}} + # Metadata read/write {{{ _metadata_readers = {} _metadata_writers = {} diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 73de6e1200..44d9bc1e49 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -55,7 +55,7 @@ class ANDROID(USBMS): }, # Viewsonic - 0x0489 : { 0xc001 : [0x0226] }, + 0x0489 : { 0xc001 : [0x0226], 0xc004 : [0x0226], }, # Acer 0x502 : { 0x3203 : [0x0100]}, diff --git a/src/calibre/ebooks/chm/input.py b/src/calibre/ebooks/chm/input.py index 61160e8dac..fce07c2359 100644 --- a/src/calibre/ebooks/chm/input.py +++ b/src/calibre/ebooks/chm/input.py @@ -52,6 +52,9 @@ class CHMInput(InputFormatPlugin): metadata = get_metadata_from_reader(self._chm_reader) self._chm_reader.CloseCHM() + #print tdir + #from calibre import ipython + #ipython() odi = options.debug_pipeline options.debug_pipeline = None diff --git a/src/calibre/ebooks/chm/reader.py b/src/calibre/ebooks/chm/reader.py index 34d228ef3b..7c9a6bf48a 100644 --- a/src/calibre/ebooks/chm/reader.py +++ b/src/calibre/ebooks/chm/reader.py @@ -147,7 +147,8 @@ class CHMReader(CHMFile): if self.hhc_path == '.hhc' and self.hhc_path not in files: from calibre import walk for x in walk(output_dir): - if os.path.basename(x).lower() in ('index.htm', 'index.html'): + if os.path.basename(x).lower() in ('index.htm', 'index.html', + 'contents.htm', 'contents.html'): self.hhc_path = os.path.relpath(x, output_dir) break diff --git a/src/calibre/ebooks/comic/input.py b/src/calibre/ebooks/comic/input.py index 56fa123249..56f7683c57 100755 --- a/src/calibre/ebooks/comic/input.py +++ b/src/calibre/ebooks/comic/input.py @@ -12,6 +12,7 @@ from Queue import Empty from calibre.customize.conversion import InputFormatPlugin, OptionRecommendation from calibre import extract, CurrentDir, prints +from calibre.constants import filesystem_encoding from calibre.ptempfile import PersistentTemporaryDirectory from calibre.utils.ipc.server import Server from calibre.utils.ipc.job import ParallelJob @@ -21,6 +22,10 @@ def extract_comic(path_to_comic_file): Un-archive the comic file. ''' tdir = PersistentTemporaryDirectory(suffix='_comic_extract') + if not isinstance(tdir, unicode): + # Needed in case the zip file has wrongly encoded unicode file/dir + # names + tdir = tdir.decode(filesystem_encoding) extract(path_to_comic_file, tdir) return tdir diff --git a/src/calibre/ebooks/metadata/sources/base.py b/src/calibre/ebooks/metadata/sources/base.py index f322fcdb56..c9639bf531 100644 --- a/src/calibre/ebooks/metadata/sources/base.py +++ b/src/calibre/ebooks/metadata/sources/base.py @@ -294,8 +294,24 @@ class Source(Plugin): Excludes connectives and punctuation. ''' if title: - pat = re.compile(r'''[-,:;+!@#$%^&*(){}.`~"'\s\[\]/]''') - title = pat.sub(' ', title) + title_patterns = [(re.compile(pat, re.IGNORECASE), repl) for pat, repl in + [ + # Remove things like: (2010) (Omnibus) etc. + (r'(?i)[({\[](\d{4}|omnibus|anthology|hardcover|paperback|mass\s*market|edition|ed\.)[\])}]', ''), + # Remove any strings that contain the substring edition inside + # parentheses + (r'(?i)[({\[].*?(edition|ed.).*?[\]})]', ''), + # Remove commas used a separators in numbers + (r'(\d+),(\d+)', r'\1\2'), + # Remove hyphens only if they have whitespace before them + (r'(\s-)', ' '), + # Remove single quotes + (r"'", ''), + # Replace other special chars with a space + (r'''[:,;+!@#$%^&*(){}.`~"\s\[\]/]''', ' ') + ]] + for pat, repl in title_patterns: + title = pat.sub(repl, title) tokens = title.split() for token in tokens: token = token.strip() diff --git a/src/calibre/ebooks/metadata/sources/identify.py b/src/calibre/ebooks/metadata/sources/identify.py index 87c1e9a644..1fb1a74679 100644 --- a/src/calibre/ebooks/metadata/sources/identify.py +++ b/src/calibre/ebooks/metadata/sources/identify.py @@ -114,8 +114,12 @@ class ISBNMerge(object): return self.results - def merge_metadata_results(self): - ' Merge results with identical title and authors ' + def merge_metadata_results(self, merge_on_identifiers=False): + ''' + Merge results with identical title and authors or an identical + identifier + ''' + # First title/author groups = {} for result in self.results: title = lower(result.title if result.title else '') @@ -135,6 +139,44 @@ class ISBNMerge(object): result = rgroup[0] self.results.append(result) + if merge_on_identifiers: + # Now identifiers + groups, empty = {}, [] + for result in self.results: + key = set() + for typ, val in result.identifiers.iteritems(): + if typ and val: + key.add((typ, val)) + if key: + key = frozenset(key) + match = None + for candidate in list(groups): + if candidate.intersection(key): + # We have at least one identifier in common + match = candidate.union(key) + results = groups.pop(candidate) + results.append(result) + groups[match] = results + break + if match is None: + groups[key] = [result] + else: + empty.append(result) + + if len(groups) != len(self.results): + self.results = [] + for rgroup in groups.itervalues(): + rel = [r.average_source_relevance for r in rgroup] + if len(rgroup) > 1: + result = self.merge(rgroup, None, do_asr=False) + result.average_source_relevance = sum(rel)/len(rel) + elif rgroup: + result = rgroup[0] + self.results.append(result) + + if empty: + self.results.extend(empty) + self.results.sort(key=attrgetter('average_source_relevance')) def merge_isbn_results(self): @@ -408,7 +450,7 @@ if __name__ == '__main__': # tests {{{ {'identifiers':{'isbn': '9780307459671'}, 'title':'Invisible Gorilla', 'authors':['Christopher Chabris']}, [title_test('The Invisible Gorilla', - exact=True), authors_test(['Christopher F. Chabris', 'Daniel Simons'])] + exact=True), authors_test(['Christopher Chabris', 'Daniel Simons'])] ), diff --git a/src/calibre/ebooks/mobi/reader.py b/src/calibre/ebooks/mobi/reader.py index 8877ecdd0b..a65649dfd2 100644 --- a/src/calibre/ebooks/mobi/reader.py +++ b/src/calibre/ebooks/mobi/reader.py @@ -716,6 +716,7 @@ class MobiReader(object): ent_pat = re.compile(r'&(\S+?);') if elems: tocobj = TOC() + found = False reached = False for x in root.iter(): if x == elems[-1]: @@ -732,7 +733,8 @@ class MobiReader(object): text = ent_pat.sub(entity_to_unicode, text) tocobj.add_item(toc.partition('#')[0], href[1:], text) - if reached and x.get('class', None) == 'mbp_pagebreak': + found = True + if reached and found and x.get('class', None) == 'mbp_pagebreak': break if tocobj is not None: opf.set_toc(tocobj) diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py index e5f2cace7f..58083f807f 100644 --- a/src/calibre/ebooks/oeb/base.py +++ b/src/calibre/ebooks/oeb/base.py @@ -24,7 +24,7 @@ from calibre.translations.dynamic import translate from calibre.ebooks.chardet import xml_to_unicode from calibre.ebooks.oeb.entitydefs import ENTITYDEFS from calibre.ebooks.conversion.preprocess import CSSPreProcessor -from calibre import isbytestring +from calibre import isbytestring, as_unicode RECOVER_PARSER = etree.XMLParser(recover=True, no_network=True) @@ -643,7 +643,7 @@ class Metadata(object): return unicode(self.value).encode('ascii', 'xmlcharrefreplace') def __unicode__(self): - return unicode(self.value) + return as_unicode(self.value) def to_opf1(self, dcmeta=None, xmeta=None, nsrmap={}): attrib = {} diff --git a/src/calibre/ebooks/pml/pmlconverter.py b/src/calibre/ebooks/pml/pmlconverter.py index 04813888ce..89a495cfc6 100644 --- a/src/calibre/ebooks/pml/pmlconverter.py +++ b/src/calibre/ebooks/pml/pmlconverter.py @@ -11,6 +11,7 @@ __docformat__ = 'restructuredtext en' import os import re import StringIO +from copy import deepcopy from calibre import my_unichr, prepare_string_for_xml from calibre.ebooks.metadata.toc import TOC @@ -25,6 +26,7 @@ class PML_HTMLizer(object): 'sp', 'sb', 'h1', + 'h1c', 'h2', 'h3', 'h4', @@ -58,6 +60,7 @@ class PML_HTMLizer(object): STATES_TAGS = { 'h1': ('

', '

'), + 'h1c': ('

', '

'), 'h2': ('

', '

'), 'h3': ('

', '

'), 'h4': ('

', '

'), @@ -140,6 +143,10 @@ class PML_HTMLizer(object): 'd', 'b', ] + + NEW_LINE_EXCHANGE_STATES = { + 'h1': 'h1c', + } def __init__(self): self.state = {} @@ -219,11 +226,17 @@ class PML_HTMLizer(object): def start_line(self): start = u'' + state = deepcopy(self.state) div = [] span = [] other = [] + + for key, val in state.items(): + if key in self.NEW_LINE_EXCHANGE_STATES and val[0]: + state[self.NEW_LINE_EXCHANGE_STATES[key]] = val + state[key] = [False, ''] - for key, val in self.state.items(): + for key, val in state.items(): if val[0]: if key in self.DIV_STATES: div.append((key, val[1])) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index e39427021e..773aea3002 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -648,6 +648,18 @@ def open_url(qurl): if isfrozen and islinux and paths: os.environ['LD_LIBRARY_PATH'] = os.pathsep.join(paths) +def get_current_db(): + ''' + This method will try to return the current database in use by the user as + efficiently as possible, i.e. without constructing duplicate + LibraryDatabase objects. + ''' + from calibre.gui2.ui import get_gui + gui = get_gui() + if gui is not None and gui.current_db is not None: + return gui.current_db + from calibre.library import db + return db() def open_local_file(path): if iswindows: diff --git a/src/calibre/gui2/actions/store.py b/src/calibre/gui2/actions/store.py new file mode 100644 index 0000000000..4e96960243 --- /dev/null +++ b/src/calibre/gui2/actions/store.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +from functools import partial + +from PyQt4.Qt import QMenu + +from calibre.gui2.actions import InterfaceAction + +class StoreAction(InterfaceAction): + + name = 'Store' + action_spec = (_('Get books'), 'store.png', None, None) + + def genesis(self): + self.qaction.triggered.connect(self.search) + self.store_menu = QMenu() + self.load_menu() + + def load_menu(self): + self.store_menu.clear() + self.store_menu.addAction(_('Search'), self.search) + self.store_menu.addSeparator() + for n, p in self.gui.istores.items(): + self.store_menu.addAction(n, partial(self.open_store, p)) + self.qaction.setMenu(self.store_menu) + + def search(self): + from calibre.gui2.store.search import SearchDialog + sd = SearchDialog(self.gui.istores, self.gui) + sd.exec_() + + def open_store(self, store_plugin): + store_plugin.open(self.gui) diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index 609c2b30f3..6c3dae3c94 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -483,8 +483,15 @@ class BookDetails(QWidget): # {{{ self.book_info.show_data(data) self.cover_view.show_data(data) self._layout.do_layout(self.rect()) - self.setToolTip('

'+_('Double-click to open Book Details window') + - '

' + _('Path') + ': ' + data.get(_('Path'), '')) + try: + sz = self.cover_view.pixmap.size() + except: + sz = QSize(0, 0) + self.setToolTip( + '

'+_('Double-click to open Book Details window') + + '

' + _('Path') + ': ' + data.get(_('Path'), '') + + '

' + _('Cover size: %dx%d')%(sz.width(), sz.height()) + ) def reset_info(self): self.show_data({}) diff --git a/src/calibre/gui2/dialogs/book_info.py b/src/calibre/gui2/dialogs/book_info.py index e860579fdf..46d26c2f4a 100644 --- a/src/calibre/gui2/dialogs/book_info.py +++ b/src/calibre/gui2/dialogs/book_info.py @@ -109,6 +109,8 @@ class BookInfo(QDialog, Ui_BookInfo): pixmap = pixmap.scaled(new_width, new_height, Qt.KeepAspectRatio, Qt.SmoothTransformation) self.cover.set_pixmap(pixmap) + sz = pixmap.size() + self.cover.setToolTip(_('Cover size: %dx%d')%(sz.width(), sz.height())) def refresh(self, row): if isinstance(row, QModelIndex): diff --git a/src/calibre/gui2/dialogs/saved_search_editor.ui b/src/calibre/gui2/dialogs/saved_search_editor.ui index 99672b5b8e..af6d6f4d55 100644 --- a/src/calibre/gui2/dialogs/saved_search_editor.ui +++ b/src/calibre/gui2/dialogs/saved_search_editor.ui @@ -90,7 +90,7 @@ - :/images/minus.png:/images/minus.png + :/images/trash.png:/images/trash.png diff --git a/src/calibre/gui2/dialogs/scheduler.py b/src/calibre/gui2/dialogs/scheduler.py index 9b1fe67f29..b25d66979d 100644 --- a/src/calibre/gui2/dialogs/scheduler.py +++ b/src/calibre/gui2/dialogs/scheduler.py @@ -68,7 +68,7 @@ class DaysOfWeek(Base): def initialize(self, typ=None, val=None): if typ is None: typ = 'day/time' - val = (-1, 9, 0) + val = (-1, 6, 0) if typ == 'day/time': val = convert_day_time_schedule(val) @@ -118,7 +118,7 @@ class DaysOfMonth(Base): def initialize(self, typ=None, val=None): if val is None: - val = ((1,), 9, 0) + val = ((1,), 6, 0) days_of_month, hour, minute = val self.days.setText(', '.join(map(str, map(int, days_of_month)))) self.time.setTime(QTime(hour, minute)) @@ -380,7 +380,7 @@ class SchedulerDialog(QDialog, Ui_Dialog): if d < timedelta(days=366): ld_text = tm else: - typ, sch = 'day/time', (-1, 9, 0) + typ, sch = 'day/time', (-1, 6, 0) sch_widget = {'day/time': 0, 'days_of_week': 0, 'days_of_month':1, 'interval':2}[typ] rb = getattr(self, list(self.SCHEDULE_TYPES)[sch_widget]) diff --git a/src/calibre/gui2/dialogs/tag_categories.ui b/src/calibre/gui2/dialogs/tag_categories.ui index 0b17ccac05..e6fedf9bde 100644 --- a/src/calibre/gui2/dialogs/tag_categories.ui +++ b/src/calibre/gui2/dialogs/tag_categories.ui @@ -79,7 +79,7 @@ - :/images/minus.png:/images/minus.png + :/images/trash.png:/images/trash.png diff --git a/src/calibre/gui2/ebook_download.py b/src/calibre/gui2/ebook_download.py new file mode 100644 index 0000000000..2fea67b9f0 --- /dev/null +++ b/src/calibre/gui2/ebook_download.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +import os +import shutil +from contextlib import closing +from mechanize import MozillaCookieJar + +from calibre import browser, get_download_filename +from calibre.ebooks import BOOK_EXTENSIONS +from calibre.gui2 import Dispatcher +from calibre.gui2.threaded_jobs import ThreadedJob +from calibre.ptempfile import PersistentTemporaryFile + +class EbookDownload(object): + + def __call__(self, gui, cookie_file=None, url='', filename='', save_loc='', add_to_lib=True, tags=[], log=None, abort=None, notifications=None): + dfilename = '' + try: + dfilename = self._download(cookie_file, url, filename, save_loc, add_to_lib) + self._add(dfilename, gui, add_to_lib, tags) + self._save_as(dfilename, save_loc) + except Exception as e: + raise e + finally: + try: + if dfilename: + os.remove(dfilename) + except: + pass + + def _download(self, cookie_file, url, filename, save_loc, add_to_lib): + dfilename = '' + + if not url: + raise Exception(_('No file specified to download.')) + if not save_loc and not add_to_lib: + # Nothing to do. + return dfilename + + if not filename: + filename = get_download_filename(url, cookie_file) + + br = browser() + if cookie_file: + cj = MozillaCookieJar() + cj.load(cookie_file) + br.set_cookiejar(cj) + with closing(br.open(url)) as r: + tf = PersistentTemporaryFile(suffix=filename) + tf.write(r.read()) + dfilename = tf.name + + return dfilename + + def _add(self, filename, gui, add_to_lib, tags): + if not add_to_lib or not filename: + return + ext = os.path.splitext(filename)[1][1:].lower() + if ext not in BOOK_EXTENSIONS: + raise Exception(_('Not a support ebook format.')) + + from calibre.ebooks.metadata.meta import get_metadata + with open(filename) as f: + mi = get_metadata(f, ext) + mi.tags.extend(tags) + + id = gui.library_view.model().db.create_book_entry(mi) + gui.library_view.model().db.add_format_with_hooks(id, ext.upper(), filename, index_is_id=True) + gui.library_view.model().books_added(1) + gui.library_view.model().count_changed() + + def _save_as(self, dfilename, save_loc): + if not save_loc or not dfilename: + return + shutil.copy(dfilename, save_loc) + + +gui_ebook_download = EbookDownload() + +def start_ebook_download(callback, job_manager, gui, cookie_file=None, url='', filename='', save_loc='', add_to_lib=True, tags=[]): + description = _('Downloading %s') % filename if filename else url + job = ThreadedJob('ebook_download', description, gui_ebook_download, (gui, cookie_file, url, filename, save_loc, add_to_lib, tags), {}, callback, max_concurrent_count=2, killable=False) + job_manager.run_threaded_job(job) + + +class EbookDownloadMixin(object): + + def download_ebook(self, url='', cookie_file=None, filename='', save_loc='', add_to_lib=True, tags=[]): + if tags: + if isinstance(tags, basestring): + tags = tags.split(',') + start_ebook_download(Dispatcher(self.downloaded_ebook), self.job_manager, self, cookie_file, url, filename, save_loc, add_to_lib, tags) + self.status_bar.show_message(_('Downloading') + ' ' + filename if filename else url, 3000) + + def downloaded_ebook(self, job): + if job.failed: + self.job_exception(job, dialog_title=_('Failed to download ebook')) + return + + self.status_bar.show_message(job.description + ' ' + _('finished'), 5000) diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index 390c266019..c72b074463 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -200,13 +200,6 @@ class SearchBar(QWidget): # {{{ x.setIcon(QIcon(I('arrow-down.png'))) l.addWidget(x) - x = parent.search_options_button = QToolButton(self) - x.setIcon(QIcon(I('config.png'))) - x.setObjectName("search_option_button") - l.addWidget(x) - x.setToolTip(_("Change the way searching for books works")) - x.setVisible(False) - x = parent.saved_search = SavedSearchBox(self) x.setMaximumSize(QSize(150, 16777215)) x.setMinimumContentsLength(15) @@ -225,13 +218,6 @@ class SearchBar(QWidget): # {{{ l.addWidget(x) x.setToolTip(_("Save current search under the name shown in the box")) - x = parent.delete_search_button = QToolButton(self) - x.setIcon(QIcon(I("search_delete_saved.png"))) - x.setObjectName("delete_search_button") - l.addWidget(x) - x.setToolTip(_("Delete current saved search")) - - # }}} class Spacer(QWidget): # {{{ @@ -331,6 +317,8 @@ class BaseToolBar(QToolBar): # {{{ QToolBar.resizeEvent(self, ev) style = self.get_text_style() self.setToolButtonStyle(style) + if hasattr(self, 'd_widget') and hasattr(self.d_widget, 'filler'): + self.d_widget.filler.setVisible(style != Qt.ToolButtonIconOnly) def get_text_style(self): style = Qt.ToolButtonTextUnderIcon @@ -413,7 +401,10 @@ class ToolBar(BaseToolBar): # {{{ self.d_widget.layout().addWidget(self.donate_button) if isosx: self.d_widget.setStyleSheet('QWidget, QToolButton {background-color: none; border: none; }') - self.d_widget.layout().addWidget(QLabel(u'\u00a0')) + self.d_widget.layout().setContentsMargins(0,0,0,0) + self.d_widget.setContentsMargins(0,0,0,0) + self.d_widget.filler = QLabel(u'\u00a0') + self.d_widget.layout().addWidget(self.d_widget.filler) bar.addWidget(self.d_widget) self.showing_donate = True elif what in self.gui.iactions: diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index 48fbfb7291..ce5e0d9877 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -743,6 +743,8 @@ class BooksView(QTableView): # {{{ id_to_select = self._model.get_current_highlighted_id() if id_to_select is not None: self.select_rows([id_to_select], using_ids=True) + elif self._model.highlight_only: + self.clearSelection() self.setFocus(Qt.OtherFocusReason) def connect_to_search_box(self, sb, search_done): diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py index 73913ba58f..9502fcb205 100644 --- a/src/calibre/gui2/metadata/basic_widgets.py +++ b/src/calibre/gui2/metadata/basic_widgets.py @@ -222,7 +222,8 @@ class AuthorSortEdit(EnLineEdit): 'red, then the authors and this text do not match.') LABEL = _('Author s&ort:') - def __init__(self, parent, authors_edit, autogen_button, db): + def __init__(self, parent, authors_edit, autogen_button, db, + copy_a_to_as_action, copy_as_to_a_action): EnLineEdit.__init__(self, parent) self.authors_edit = authors_edit self.db = db @@ -241,6 +242,8 @@ class AuthorSortEdit(EnLineEdit): self.textChanged.connect(self.update_state) autogen_button.clicked.connect(self.auto_generate) + copy_a_to_as_action.triggered.connect(self.auto_generate) + copy_as_to_a_action.triggered.connect(self.copy_to_authors) self.update_state() @dynamic_property @@ -273,6 +276,14 @@ class AuthorSortEdit(EnLineEdit): self.setToolTip(tt) self.setWhatsThis(tt) + def copy_to_authors(self): + aus = self.current_val + if aus: + ln, _, rest = aus.partition(',') + if rest: + au = rest.strip() + ' ' + ln.strip() + self.authors_edit.current_val = [au] + def auto_generate(self, *args): au = unicode(self.authors_edit.text()) au = re.sub(r'\s+et al\.$', '', au) diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index 2e5b43ceba..52b9e99872 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -13,7 +13,7 @@ from functools import partial from PyQt4.Qt import (Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont, QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox, QSpacerItem, - QSizePolicy, QPalette, QFrame, QSize, QKeySequence) + QSizePolicy, QPalette, QFrame, QSize, QKeySequence, QMenu) from calibre.ebooks.metadata import authors_to_string, string_to_authors from calibre.gui2 import ResizableDialog, error_dialog, gprefs, pixmap_to_data @@ -102,15 +102,19 @@ class MetadataSingleDialogBase(ResizableDialog): self.deduce_title_sort_button) self.basic_metadata_widgets.extend([self.title, self.title_sort]) - self.authors = AuthorsEdit(self) - self.deduce_author_sort_button = QToolButton(self) - self.deduce_author_sort_button.setToolTip(_( + self.deduce_author_sort_button = b = QToolButton(self) + b.setToolTip(_( 'Automatically create the author sort entry based on the current' ' author entry.\n' 'Using this button to create author sort will change author sort from' ' red to green.')) - self.author_sort = AuthorSortEdit(self, self.authors, - self.deduce_author_sort_button, self.db) + b.m = m = QMenu() + ac = m.addAction(QIcon(I('forward.png')), _('Set author sort from author')) + ac2 = m.addAction(QIcon(I('back.png')), _('Set author from author sort')) + b.setMenu(m) + self.authors = AuthorsEdit(self) + self.author_sort = AuthorSortEdit(self, self.authors, b, self.db, ac, + ac2) self.basic_metadata_widgets.extend([self.authors, self.author_sort]) self.swap_title_author_button = QToolButton(self) diff --git a/src/calibre/gui2/preferences/__init__.py b/src/calibre/gui2/preferences/__init__.py index 1669e24059..649a58448d 100644 --- a/src/calibre/gui2/preferences/__init__.py +++ b/src/calibre/gui2/preferences/__init__.py @@ -319,9 +319,12 @@ def show_config_widget(category, name, gui=None, show_restart_msg=False, :return: True iff a restart is required for the changes made by the user to take effect ''' + from calibre.gui2 import gprefs pl = get_plugin(category, name) d = ConfigDialog(parent) d.resize(750, 550) + conf_name = 'config_widget_dialog_geometry_%s_%s'%(category, name) + geom = gprefs.get(conf_name, None) d.setWindowTitle(_('Configure ') + name) d.setWindowIcon(QIcon(I('config.png'))) bb = QDialogButtonBox(d) @@ -345,7 +348,11 @@ def show_config_widget(category, name, gui=None, show_restart_msg=False, mygui = True w.genesis(gui) w.initialize() + if geom is not None: + d.restoreGeometry(geom) d.exec_() + geom = bytearray(d.saveGeometry()) + gprefs[conf_name] = geom rr = getattr(d, 'restart_required', False) if show_restart_msg and rr: from calibre.gui2 import warning_dialog diff --git a/src/calibre/gui2/preferences/columns.ui b/src/calibre/gui2/preferences/columns.ui index a9d82530ec..423d5dd106 100644 --- a/src/calibre/gui2/preferences/columns.ui +++ b/src/calibre/gui2/preferences/columns.ui @@ -79,7 +79,7 @@ - :/images/minus.png:/images/minus.png + :/images/trash.png:/images/trash.png diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index 71b9e38667..9f06d9a6ab 100644 --- a/src/calibre/gui2/preferences/look_feel.py +++ b/src/calibre/gui2/preferences/look_feel.py @@ -73,13 +73,13 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): choices=sorted(list(choices), key=sort_key)) - self.current_font = None + self.current_font = self.initial_font = None self.change_font_button.clicked.connect(self.change_font) def initialize(self): ConfigWidgetBase.initialize(self) - self.current_font = gprefs['font'] + self.current_font = self.initial_font = gprefs['font'] self.update_font_display() def restore_defaults(self): @@ -119,7 +119,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): def commit(self, *args): rr = ConfigWidgetBase.commit(self, *args) - if self.current_font != gprefs['font']: + if self.current_font != self.initial_font: gprefs['font'] = self.current_font QApplication.setFont(self.font_display.font()) rr = True diff --git a/src/calibre/gui2/preferences/plugins.py b/src/calibre/gui2/preferences/plugins.py index 8151fe6021..79cd2b1ce4 100644 --- a/src/calibre/gui2/preferences/plugins.py +++ b/src/calibre/gui2/preferences/plugins.py @@ -218,6 +218,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.search.search.connect(self.find) self.next_button.clicked.connect(self.find_next) self.previous_button.clicked.connect(self.find_previous) + self.changed_signal.connect(self.reload_store_plugins) def find(self, query): idx = self._plugin_model.find(query) @@ -344,6 +345,11 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): plugin.name + _(' cannot be removed. It is a ' 'builtin plugin. Try disabling it instead.')).exec_() + def reload_store_plugins(self): + self.gui.load_store_plugins() + if self.gui.iactions.has_key('Store'): + self.gui.iactions['Store'].load_menu() + def check_for_add_to_toolbars(self, plugin): from calibre.gui2.preferences.toolbar import ConfigWidget from calibre.customize import InterfaceActionBase @@ -376,6 +382,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): installed_actions.append(plugin_action.name) gprefs['action-layout-'+key] = tuple(installed_actions) + if __name__ == '__main__': from PyQt4.Qt import QApplication app = QApplication([]) diff --git a/src/calibre/gui2/search_box.py b/src/calibre/gui2/search_box.py index 359cb0b2f6..c349d84a68 100644 --- a/src/calibre/gui2/search_box.py +++ b/src/calibre/gui2/search_box.py @@ -13,7 +13,6 @@ from PyQt4.Qt import QComboBox, Qt, QLineEdit, QStringList, pyqtSlot, QDialog, \ QString, QIcon from calibre.gui2 import config -from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor from calibre.gui2.dialogs.search import SearchDialog from calibre.utils.search_query_parser import saved_searches @@ -316,23 +315,6 @@ class SavedSearchBox(QComboBox): # {{{ self.addItems(qnames) self.setCurrentIndex(-1) - # SIGNALed from the main UI - def delete_search_button_clicked(self): - if not confirm('

'+_('The selected search will be ' - 'permanently deleted. Are you sure?') - +'

', 'saved_search_delete', self): - return - idx = self.currentIndex - if idx < 0: - return - ss = saved_searches().lookup(unicode(self.currentText())) - if ss is None: - return - saved_searches().delete(unicode(self.currentText())) - self.clear() - self.search_box.clear() - self.changed.emit() - # SIGNALed from the main UI def save_search_button_clicked(self): name = unicode(self.currentText()) @@ -382,7 +364,6 @@ class SearchBoxMixin(object): # {{{ unicode(self.search.toolTip()))) self.advanced_search_button.setStatusTip(self.advanced_search_button.toolTip()) self.clear_button.setStatusTip(self.clear_button.toolTip()) - self.search_options_button.clicked.connect(self.search_options_button_clicked) self.set_highlight_only_button_icon() self.highlight_only_button.clicked.connect(self.highlight_only_clicked) tt = _('Enable or disable search highlighting.') + '

' @@ -392,6 +373,8 @@ class SearchBoxMixin(object): # {{{ def highlight_only_clicked(self, state): config['highlight_search_matches'] = not config['highlight_search_matches'] self.set_highlight_only_button_icon() + self.search.do_search() + self.focus_to_library() def set_highlight_only_button_icon(self): if config['highlight_search_matches']: @@ -422,10 +405,6 @@ class SearchBoxMixin(object): # {{{ self.search.do_search() self.focus_to_library() - def search_options_button_clicked(self): - self.iactions['Preferences'].do_config(initial_plugin=('Interface', - 'Search'), close_after_initial=True) - def focus_to_library(self): self.current_view().setFocus(Qt.OtherFocusReason) @@ -438,8 +417,6 @@ class SavedSearchBoxMixin(object): # {{{ self.clear_button.clicked.connect(self.saved_search.clear) self.save_search_button.clicked.connect( self.saved_search.save_search_button_clicked) - self.delete_search_button.clicked.connect( - self.saved_search.delete_search_button_clicked) self.copy_search_button.clicked.connect( self.saved_search.copy_search_button_clicked) self.saved_searches_changed() @@ -448,7 +425,7 @@ class SavedSearchBoxMixin(object): # {{{ self.saved_search.setToolTip( _('Choose saved search or enter name for new saved search')) self.saved_search.setStatusTip(self.saved_search.toolTip()) - for x in ('copy', 'save', 'delete'): + for x in ('copy', 'save'): b = getattr(self, x+'_search_button') b.setStatusTip(b.toolTip()) diff --git a/src/calibre/gui2/store/__init__.py b/src/calibre/gui2/store/__init__.py new file mode 100644 index 0000000000..26bafd2c95 --- /dev/null +++ b/src/calibre/gui2/store/__init__.py @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +class StorePlugin(object): # {{{ + ''' + A plugin representing an online ebook repository (store). The store can + be a comercial store that sells ebooks or a source of free downloadable + ebooks. + + Note that this class is the base class for these plugins, however, to + integrate the plugin with calibre's plugin system, you have to make a + wrapper class that references the actual plugin. See the + :mod:`calibre.customize.builtins` module for examples. + + If two :class:`StorePlugin` objects have the same name, the one with higher + priority takes precedence. + + Sub-classes must implement :meth:`open`, and :meth:`search`. + + Regarding :meth:`open`. Most stores only make themselves available + though a web site thus most store plugins will open using + :class:`calibre.gui2.store.web_store_dialog.WebStoreDialog`. This will + open a modal window and display the store website in a QWebView. + + Sub-classes should implement and use the :meth:`genesis` if they require + plugin specific initialization. They should not override or otherwise + reimplement :meth:`__init__`. + + Once initialized, this plugin has access to the main calibre GUI via the + :attr:`gui` member. You can access other plugins by name, for example:: + + self.gui.istores['Amazon Kindle'] + + Plugin authors can use affiliate programs within their plugin. The + distribution of money earned from a store plugin is 70/30. 70% going + to the pluin author / maintainer and 30% going to the calibre project. + + The easiest way to handle affiliate money payouts is to randomly select + between the author's affiliate id and calibre's affiliate id so that + 70% of the time the author's id is used. + ''' + + def __init__(self, gui, name): + self.gui = gui + self.name = name + self.base_plugin = None + + def open(self, gui, parent=None, detail_item=None, external=False): + ''' + Open the store. + + :param gui: The main GUI. This will be used to have the job + system start downloading an item from the store. + + :param parent: The parent of the store dialog. This is used + to create modal dialogs. + + :param detail_item: A plugin specific reference to an item + in the store that the user should be shown. + + :param external: When False open an internal dialog with the + store. When True open the users default browser to the store's + web site. :param:`detail_item` should still be respected when external + is True. + ''' + raise NotImplementedError() + + def search(self, query, max_results=10, timeout=60): + ''' + Searches the store for items matching query. This should + return items as a generator. + + Don't be lazy with the search! Load as much data as possible in the + :class:`calibre.gui2.store.search_result.SearchResult` object. If you have to parse + multiple pages to get all of the data then do so. However, if data (such as cover_url) + isn't available because the store does not display cover images then it's okay to + ignore it. + + Also, by default search results can only include ebooks. A plugin can offer users + an option to include physical books in the search results but this must be + disabled by default. + + If a store doesn't provide search on it's own use something like a site specific + google search to get search results for this funtion. + + :param query: The string query search with. + :param max_results: The maximum number of results to return. + :param timeout: The maximum amount of time in seconds to spend download the search results. + + :return: :class:`calibre.gui2.store.search_result.SearchResult` objects + item_data is plugin specific and is used in :meth:`open` to open to a specifc place in the store. + ''' + raise NotImplementedError() + + def get_settings(self): + ''' + This is only useful for plugins that implement + :attr:`config_widget` that is the only way to save + settings. This is used by plugins to get the saved + settings and apply when necessary. + + :return: A dictionary filled with the settings used + by this plugin. + ''' + raise NotImplementedError() + + def do_genesis(self): + self.genesis() + + def genesis(self): + ''' + Plugin specific initialization. + ''' + pass + + def config_widget(self): + ''' + See :class:`calibre.customize.Plugin` for details. + ''' + raise NotImplementedError() + + def save_settings(self, config_widget): + ''' + See :class:`calibre.customize.Plugin` for details. + ''' + raise NotImplementedError() + + def customization_help(self, gui=False): + ''' + See :class:`calibre.customize.Plugin` for details. + ''' + raise NotImplementedError() + +# }}} diff --git a/src/calibre/gui2/store/amazon_plugin.py b/src/calibre/gui2/store/amazon_plugin.py new file mode 100644 index 0000000000..51986ee4df --- /dev/null +++ b/src/calibre/gui2/store/amazon_plugin.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +import random +import re +import urllib2 +from contextlib import closing + +from lxml import html + +from PyQt4.Qt import QUrl + +from calibre import browser +from calibre.gui2 import open_url +from calibre.gui2.store import StorePlugin +from calibre.gui2.store.search_result import SearchResult + +class AmazonKindleStore(StorePlugin): + + def open(self, parent=None, detail_item=None, external=False): + ''' + Amazon comes with a number of difficulties. + + QWebView has major issues with Amazon.com. The largest of + issues is it simply doesn't work on a number of pages. + + When connecting to a number parts of Amazon.com (Kindle library + for instance) QNetworkAccessManager fails to connect with a + NetworkError of 399 - ProtocolFailure. The strange thing is, + when I check QNetworkRequest.HttpStatusCodeAttribute when the + 399 error is returned the status code is 200 (Ok). However, once + the QNetworkAccessManager decides there was a NetworkError it + does not download the page from Amazon. So I can't even set the + HTML in the QWebView myself. + + There is http://bugreports.qt.nokia.com/browse/QTWEBKIT-259 an + open bug about the issue but it is not correct. We can set the + useragent (Arora does) to something else and the above issue + will persist. This http://developer.qt.nokia.com/forums/viewthread/793 + gives a bit more information about the issue but as of now (27/Feb/2011) + there is no solution or work around. + + We cannot change the The linkDelegationPolicy to allow us to avoid + QNetworkAccessManager because it only works links. Forms aren't + included so the same issue persists on any part of the site (login) + that use a form to load a new page. + + Using an aStore was evaluated but I've decided against using it. + There are three major issues with an aStore. Because checkout is + handled by sending the user to Amazon we can't put it in a QWebView. + If we're sending the user to Amazon sending them there directly is + nicer. Also, we cannot put the aStore in a QWebView and let it open the + redirection the users default browser because the cookies with the + shopping cart won't transfer. + + Another issue with the aStore is how it handles the referral. It only + counts the referral for the items in the shopping card / the item + that directed the user to Amazon. Kindle books do not use the shopping + cart and send the user directly to Amazon for the purchase. In this + instance we would only get referral credit for the one book that the + aStore directs to Amazon that the user buys. Any other purchases we + won't get credit for. + + The last issue with the aStore is performance. Even though it's an + Amazon site it's alow. So much slower than Amazon.com that it makes + me not want to browse books using it. The look and feel are lesser + issues. So is the fact that it almost seems like the purchase is + with calibre. This can cause some support issues because we can't + do much for issues with Amazon.com purchase hiccups. + + Another option that was evaluated was the Product Advertising API. + The reasons against this are complexity. It would take a lot of work + to basically re-create Amazon.com within calibre. The Product + Advertising API is also designed with being run on a server not + in an app. The signing keys would have to be made avaliable to ever + calibre user which means bad things could be done with our account. + + The Product Advertising API also assumes the same browser for easy + shopping cart transfer to Amazon. With QWebView not working and there + not being an easy way to transfer cookies between a QWebView and the + users default browser this won't work well. + + We could create our own website on the calibre server and create an + Amazon Product Advertising API store. However, this goes back to the + complexity argument. Why spend the time recreating Amazon.com + + The final and largest issue against using the Product Advertising API + is the Efficiency Guidelines: + + "Each account used to access the Product Advertising API will be allowed + an initial usage limit of 2,000 requests per hour. Each account will + receive an additional 500 requests per hour (up to a maximum of 25,000 + requests per hour) for every $1 of shipped item revenue driven per hour + in a trailing 30-day period. Usage thresholds are recalculated daily based + on revenue performance." + + With over two million users a limit of 2,000 request per hour could + render our store unusable for no other reason than Amazon rate + limiting our traffic. + + The best (I use the term lightly here) solution is to open Amazon.com + in the users default browser and set the affiliate id as part of the url. + ''' + aff_id = {'tag': 'josbl0e-cpb-20'} + # Use Kovid's affiliate id 30% of the time. + if random.randint(1, 10) in (1, 2, 3): + aff_id['tag'] = 'calibrebs-20' + store_link = 'http://www.amazon.com/Kindle-eBooks/b/?ie=UTF&node=1286228011&ref_=%(tag)s&ref=%(tag)s&tag=%(tag)s&linkCode=ur2&camp=1789&creative=390957' % aff_id + if detail_item: + aff_id['asin'] = detail_item + store_link = 'http://www.amazon.com/dp/%(asin)s/?tag=%(tag)s' % aff_id + open_url(QUrl(store_link)) + + def search(self, query, max_results=10, timeout=60): + url = 'http://www.amazon.com/s/url=search-alias%3Ddigital-text&field-keywords=' + urllib2.quote(query) + br = browser() + + counter = max_results + with closing(br.open(url, timeout=timeout)) as f: + doc = html.fromstring(f.read()) + for data in doc.xpath('//div[@class="productData"]'): + if counter <= 0: + break + + # Even though we are searching digital-text only Amazon will still + # put in results for non Kindle books (author pages). Se we need + # to explicitly check if the item is a Kindle book and ignore it + # if it isn't. + type = ''.join(data.xpath('//span[@class="format"]/text()')) + if 'kindle' not in type.lower(): + continue + + # We must have an asin otherwise we can't easily reference the + # book later. + asin_href = None + asin_a = data.xpath('div[@class="productTitle"]/a[1]') + if asin_a: + asin_href = asin_a[0].get('href', '') + m = re.search(r'/dp/(?P.+?)(/|$)', asin_href) + if m: + asin = m.group('asin') + else: + continue + else: + continue + + cover_url = '' + if asin_href: + cover_img = data.xpath('//div[@class="productImage"]/a[@href="%s"]/img/@src' % asin_href) + if cover_img: + cover_url = cover_img[0] + + title = ''.join(data.xpath('div[@class="productTitle"]/a/text()')) + author = ''.join(data.xpath('div[@class="productTitle"]/span[@class="ptBrand"]/text()')) + author = author.split('by')[-1] + price = ''.join(data.xpath('div[@class="newPrice"]/span/text()')) + + counter -= 1 + + s = SearchResult() + s.cover_url = cover_url + s.title = title.strip() + s.author = author.strip() + s.price = price.strip() + s.detail_item = asin.strip() + + yield s diff --git a/src/calibre/gui2/store/baen_webscription_plugin.py b/src/calibre/gui2/store/baen_webscription_plugin.py new file mode 100644 index 0000000000..46a7c1ec7c --- /dev/null +++ b/src/calibre/gui2/store/baen_webscription_plugin.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +import re +import urllib2 +from contextlib import closing + +from lxml import html + +from PyQt4.Qt import QUrl + +from calibre import browser, url_slash_cleaner +from calibre.gui2 import open_url +from calibre.gui2.store import StorePlugin +from calibre.gui2.store.basic_config import BasicStoreConfig +from calibre.gui2.store.search_result import SearchResult +from calibre.gui2.store.web_store_dialog import WebStoreDialog + +class BaenWebScriptionStore(BasicStoreConfig, StorePlugin): + + def open(self, parent=None, detail_item=None, external=False): + settings = self.get_settings() + url = 'http://www.webscription.net/' + + if external or settings.get(self.name + '_open_external', False): + if detail_item: + url = url + detail_item + open_url(QUrl(url_slash_cleaner(url))) + else: + detail_url = None + if detail_item: + detail_url = url + detail_item + d = WebStoreDialog(self.gui, url, parent, detail_url) + d.setWindowTitle(self.name) + d.set_tags(settings.get(self.name + '_tags', '')) + d.exec_() + + def search(self, query, max_results=10, timeout=60): + url = 'http://www.webscription.net/searchadv.aspx?IsSubmit=true&SearchTerm=' + urllib2.quote(query) + + br = browser() + + counter = max_results + with closing(br.open(url, timeout=timeout)) as f: + doc = html.fromstring(f.read()) + for data in doc.xpath('//table/tr/td/img[@src="skins/Skin_1/images/matchingproducts.gif"]/..//tr'): + if counter <= 0: + break + + id = ''.join(data.xpath('./td[1]/a/@href')) + if not id: + continue + + title = ''.join(data.xpath('./td[1]/a/text()')) + + author = '' + cover_url = '' + price = '' + + with closing(br.open('http://www.webscription.net/' + id.strip(), timeout=timeout/4)) as nf: + idata = html.fromstring(nf.read()) + author = ''.join(idata.xpath('//span[@class="ProductNameText"]/../b/text()')) + author = author.split('by ')[-1] + price = ''.join(idata.xpath('//span[@class="variantprice"]/text()')) + a, b, price = price.partition('$') + price = b + price + + pnum = '' + mo = re.search(r'p-(?P\d+)-', id.strip()) + if mo: + pnum = mo.group('num') + if pnum: + cover_url = 'http://www.webscription.net/' + ''.join(idata.xpath('//img[@id="ProductPic%s"]/@src' % pnum)) + + counter -= 1 + + s = SearchResult() + s.cover_url = cover_url + s.title = title.strip() + s.author = author.strip() + s.price = price + s.detail_item = id.strip() + + yield s diff --git a/src/calibre/gui2/store/basic_config.py b/src/calibre/gui2/store/basic_config.py new file mode 100644 index 0000000000..88ee197146 --- /dev/null +++ b/src/calibre/gui2/store/basic_config.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +from PyQt4.Qt import QWidget + +from calibre.gui2 import gprefs +from calibre.gui2.store.basic_config_widget_ui import Ui_Form + +def save_settings(config_widget): + gprefs[config_widget.store.name + '_open_external'] = config_widget.open_external.isChecked() + tags = unicode(config_widget.tags.text()) + gprefs[config_widget.store.name + '_tags'] = tags + +class BasicStoreConfigWidget(QWidget, Ui_Form): + + def __init__(self, store): + QWidget.__init__(self) + self.setupUi(self) + + self.store = store + + self.load_setings() + + def load_setings(self): + settings = self.store.get_settings() + + self.open_external.setChecked(settings.get(self.store.name + '_open_external')) + self.tags.setText(settings.get(self.store.name + '_tags', '')) + +class BasicStoreConfig(object): + + def customization_help(self, gui=False): + return 'Customize the behavior of this store.' + + def config_widget(self): + return BasicStoreConfigWidget(self) + + def save_settings(self, config_widget): + save_settings(config_widget) + + def get_settings(self): + settings = {} + + settings[self.name + '_open_external'] = gprefs.get(self.name + '_open_external', False) + settings[self.name + '_tags'] = gprefs.get(self.name + '_tags', self.name + ', store, download') + + return settings diff --git a/src/calibre/gui2/store/basic_config_widget.ui b/src/calibre/gui2/store/basic_config_widget.ui new file mode 100644 index 0000000000..cadf7813d6 --- /dev/null +++ b/src/calibre/gui2/store/basic_config_widget.ui @@ -0,0 +1,38 @@ + + + Form + + + + 0 + 0 + 460 + 69 + + + + Form + + + + + + Added Tags: + + + + + + + + + + Open store in external web browswer + + + + + + + + diff --git a/src/calibre/gui2/store/bewrite_plugin.py b/src/calibre/gui2/store/bewrite_plugin.py new file mode 100644 index 0000000000..37bd9cf9a5 --- /dev/null +++ b/src/calibre/gui2/store/bewrite_plugin.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +import urllib2 +from contextlib import closing + +from lxml import html + +from PyQt4.Qt import QUrl + +from calibre import browser, url_slash_cleaner +from calibre.gui2 import open_url +from calibre.gui2.store import StorePlugin +from calibre.gui2.store.basic_config import BasicStoreConfig +from calibre.gui2.store.search_result import SearchResult +from calibre.gui2.store.web_store_dialog import WebStoreDialog + +class BeWriteStore(BasicStoreConfig, StorePlugin): + + def open(self, parent=None, detail_item=None, external=False): + settings = self.get_settings() + url = 'http://www.bewrite.net/mm5/merchant.mvc?Screen=SFNT' + + if external or settings.get(self.name + '_open_external', False): + if detail_item: + url = url + detail_item + open_url(QUrl(url_slash_cleaner(url))) + else: + detail_url = None + if detail_item: + detail_url = url + detail_item + d = WebStoreDialog(self.gui, url, parent, detail_url) + d.setWindowTitle(self.name) + d.set_tags(settings.get(self.name + '_tags', '')) + d.exec_() + + def search(self, query, max_results=10, timeout=60): + url = 'http://www.bewrite.net/mm5/merchant.mvc?Search_Code=B&Screen=SRCH&Search=' + urllib2.quote(query) + + br = browser() + + counter = max_results + with closing(br.open(url, timeout=timeout)) as f: + doc = html.fromstring(f.read()) + for data in doc.xpath('//div[@id="content"]//table/tr[position() > 1]'): + if counter <= 0: + break + + id = ''.join(data.xpath('.//a/@href')) + if not id: + continue + + heading = ''.join(data.xpath('./td[2]//text()')) + title, q, author = heading.partition('by ') + cover_url = '' + price = '' + + with closing(br.open(id.strip(), timeout=timeout/4)) as nf: + idata = html.fromstring(nf.read()) + price = ''.join(idata.xpath('//div[@id="content"]//td[contains(text(), "ePub")]/text()')) + price = '$' + price.split('$')[-1] + cover_img = idata.xpath('//div[@id="content"]//img[1]/@src') + if cover_img: + cover_url = 'http://www.bewrite.net/mm5/' + cover_img[0] + + counter -= 1 + + s = SearchResult() + s.cover_url = cover_url.strip() + s.title = title.strip() + s.author = author.strip() + s.price = price.strip() + s.detail_item = id.strip() + + yield s diff --git a/src/calibre/gui2/store/bn_plugin.py b/src/calibre/gui2/store/bn_plugin.py new file mode 100644 index 0000000000..4da551fd92 --- /dev/null +++ b/src/calibre/gui2/store/bn_plugin.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +import random +import re +import urllib2 +from contextlib import closing + +from lxml import html + +from PyQt4.Qt import QUrl + +from calibre import browser, url_slash_cleaner +from calibre.gui2 import open_url +from calibre.gui2.store import StorePlugin +from calibre.gui2.store.basic_config import BasicStoreConfig +from calibre.gui2.store.search_result import SearchResult +from calibre.gui2.store.web_store_dialog import WebStoreDialog + +class BNStore(BasicStoreConfig, StorePlugin): + + def open(self, parent=None, detail_item=None, external=False): + settings = self.get_settings() + + pub_id = '21000000000352219' + # Use Kovid's affiliate id 30% of the time. + if random.randint(1, 10) in (1, 2, 3): + pub_id = '21000000000352583' + + url = 'http://gan.doubleclick.net/gan_click?lid=41000000028437369&pubid=' + pub_id + + if detail_item: + mo = re.search(r'(?<=/)(?P\d+)(?=/|$)', detail_item) + if mo: + isbn = mo.group('isbn') + detail_item = 'http://gan.doubleclick.net/gan_click?lid=41000000012871747&pid=' + isbn + '&adurl=' + detail_item + '&pubid=' + pub_id + + if external or settings.get(self.name + '_open_external', False): + open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url))) + else: + d = WebStoreDialog(self.gui, url, parent, detail_item) + d.setWindowTitle(self.name) + d.set_tags(settings.get(self.name + '_tags', '')) + d.exec_() + + def search(self, query, max_results=10, timeout=60): + url = 'http://productsearch.barnesandnoble.com/search/results.aspx?STORE=EBOOK&SZE=%s&WRD=' % max_results + url += urllib2.quote(query) + + br = browser() + + counter = max_results + with closing(br.open(url, timeout=timeout)) as f: + doc = html.fromstring(f.read()) + for data in doc.xpath('//ul[contains(@class, "wgt-search-results-display")]/li[contains(@class, "search-result-item") and contains(@class, "nook-result-item")]'): + if counter <= 0: + break + + id = ''.join(data.xpath('.//div[contains(@class, "wgt-product-image-module")]/a/@href')) + if not id: + continue + cover_url = ''.join(data.xpath('.//div[contains(@class, "wgt-product-image-module")]/a/img/@src')) + + title = ''.join(data.xpath('.//span[@class="product-title"]/a/text()')) + author = ', '.join(data.xpath('.//span[@class="contributers-line"]/a/text()')) + price = ''.join(data.xpath('.//span[contains(@class, "onlinePriceValue2")]/text()')) + + counter -= 1 + + s = SearchResult() + s.cover_url = cover_url + s.title = title.strip() + s.author = author.strip() + s.price = price + s.detail_item = id.strip() + + yield s diff --git a/src/calibre/gui2/store/diesel_ebooks_plugin.py b/src/calibre/gui2/store/diesel_ebooks_plugin.py new file mode 100644 index 0000000000..66c22f847f --- /dev/null +++ b/src/calibre/gui2/store/diesel_ebooks_plugin.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +import random +import urllib2 +from contextlib import closing + +from lxml import html + +from PyQt4.Qt import QUrl + +from calibre import browser, url_slash_cleaner +from calibre.gui2 import open_url +from calibre.gui2.store import StorePlugin +from calibre.gui2.store.basic_config import BasicStoreConfig +from calibre.gui2.store.search_result import SearchResult +from calibre.gui2.store.web_store_dialog import WebStoreDialog + +class DieselEbooksStore(BasicStoreConfig, StorePlugin): + + def open(self, parent=None, detail_item=None, external=False): + settings = self.get_settings() + url = 'http://www.diesel-ebooks.com/' + + aff_id = '?aid=2049' + # Use Kovid's affiliate id 30% of the time. + if random.randint(1, 10) in (1, 2, 3): + aff_id = '?aid=2053' + + detail_url = None + if detail_item: + detail_url = url + detail_item + aff_id + url = url + aff_id + + if external or settings.get(self.name + '_open_external', False): + open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url))) + else: + d = WebStoreDialog(self.gui, url, parent, detail_url) + d.setWindowTitle(self.name) + d.set_tags(settings.get(self.name + '_tags', '')) + d.exec_() + + def search(self, query, max_results=10, timeout=60): + url = 'http://www.diesel-ebooks.com/index.php?page=seek&id[m]=&id[c]=scope%253Dinventory&id[q]=' + urllib2.quote(query) + + br = browser() + + counter = max_results + with closing(br.open(url, timeout=timeout)) as f: + doc = html.fromstring(f.read()) + for data in doc.xpath('//div[@class="item clearfix"]'): + data = html.fromstring(html.tostring(data)) + if counter <= 0: + break + + id = ''.join(data.xpath('div[@class="cover"]/a/@href')) + if not id or '/item/' not in id: + continue + a, b, id = id.partition('/item/') + + cover_url = ''.join(data.xpath('div[@class="cover"]//img/@src')) + if cover_url.startswith('/'): + cover_url = cover_url[1:] + cover_url = 'http://www.diesel-ebooks.com/' + cover_url + + title = ''.join(data.xpath('.//div[@class="content"]//h2/text()')) + author = ''.join(data.xpath('//div[@class="content"]//div[@class="author"]/a/text()')) + price = '' + price_elem = data.xpath('//td[@class="price"]/text()') + if price_elem: + price = price_elem[0] + + counter -= 1 + + s = SearchResult() + s.cover_url = cover_url + s.title = title.strip() + s.author = author.strip() + s.price = price.strip() + s.detail_item = '/item/' + id.strip() + + yield s diff --git a/src/calibre/gui2/store/ebooks_com_plugin.py b/src/calibre/gui2/store/ebooks_com_plugin.py new file mode 100644 index 0000000000..259e996ebe --- /dev/null +++ b/src/calibre/gui2/store/ebooks_com_plugin.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +import random +import urllib2 +from contextlib import closing + +from lxml import html + +from PyQt4.Qt import QUrl + +from calibre import browser, url_slash_cleaner +from calibre.gui2 import open_url +from calibre.gui2.store import StorePlugin +from calibre.gui2.store.basic_config import BasicStoreConfig +from calibre.gui2.store.search_result import SearchResult +from calibre.gui2.store.web_store_dialog import WebStoreDialog + +class EbookscomStore(BasicStoreConfig, StorePlugin): + + def open(self, parent=None, detail_item=None, external=False): + settings = self.get_settings() + + m_url = 'http://www.dpbolvw.net/' + h_click = 'click-4879827-10364500' + d_click = 'click-4879827-10281551' + # Use Kovid's affiliate id 30% of the time. + if random.randint(1, 10) in (1, 2, 3): + h_click = 'click-4913808-10364500' + d_click = 'click-4913808-10281551' + + url = m_url + h_click + detail_url = None + if detail_item: + detail_url = m_url + d_click + detail_item + + if external or settings.get(self.name + '_open_external', False): + open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url))) + else: + d = WebStoreDialog(self.gui, url, parent, detail_url) + d.setWindowTitle(self.name) + d.set_tags(settings.get(self.name + '_tags', '')) + d.exec_() + + def search(self, query, max_results=10, timeout=60): + url = 'http://www.ebooks.com/SearchApp/SearchResults.net?term=' + urllib2.quote(query) + + br = browser() + + counter = max_results + with closing(br.open(url, timeout=timeout)) as f: + doc = html.fromstring(f.read()) + for data in doc.xpath('//div[@class="book_a" or @class="book_b"]'): + if counter <= 0: + break + + id = ''.join(data.xpath('.//a[1]/@href')) + id = id.split('=')[-1] + if not id: + continue + + price = '' + with closing(br.open('http://www.ebooks.com/ebooks/book_display.asp?IID=' + id.strip(), timeout=timeout)) as fp: + pdoc = html.fromstring(fp.read()) + pdata = pdoc.xpath('//table[@class="price"]/tr/td/text()') + if len(pdata) >= 2: + price = pdata[1] + if not price: + continue + + cover_url = ''.join(data.xpath('.//img[1]/@src')) + + title = '' + author = '' + heading_a = data.xpath('.//a[1]/text()') + if heading_a: + title = heading_a[0] + if len(heading_a) >= 2: + author = heading_a[1] + + counter -= 1 + + s = SearchResult() + s.cover_url = cover_url + s.title = title.strip() + s.author = author.strip() + s.price = price.strip() + s.detail_item = '?url=http://www.ebooks.com/cj.asp?IID=' + id.strip() + '&cjsku=' + id.strip() + + yield s diff --git a/src/calibre/gui2/store/eharlequin_plugin.py b/src/calibre/gui2/store/eharlequin_plugin.py new file mode 100644 index 0000000000..1886671b0a --- /dev/null +++ b/src/calibre/gui2/store/eharlequin_plugin.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +import random +import urllib2 +from contextlib import closing + +from lxml import html + +from PyQt4.Qt import QUrl + +from calibre import browser, url_slash_cleaner +from calibre.gui2 import open_url +from calibre.gui2.store import StorePlugin +from calibre.gui2.store.basic_config import BasicStoreConfig +from calibre.gui2.store.search_result import SearchResult +from calibre.gui2.store.web_store_dialog import WebStoreDialog + +class EHarlequinStore(BasicStoreConfig, StorePlugin): + + def open(self, parent=None, detail_item=None, external=False): + settings = self.get_settings() + + m_url = 'http://www.dpbolvw.net/' + h_click = 'click-4879827-534091' + d_click = 'click-4879827-10375439' + # Use Kovid's affiliate id 30% of the time. + if random.randint(1, 10) in (1, 2, 3): + h_click = 'click-4913808-534091' + d_click = 'click-4913808-10375439' + + url = m_url + h_click + detail_url = None + if detail_item: + detail_url = m_url + d_click + detail_item + + if external or settings.get(self.name + '_open_external', False): + open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url))) + else: + d = WebStoreDialog(self.gui, url, parent, detail_url) + d.setWindowTitle(self.name) + d.set_tags(settings.get(self.name + '_tags', '')) + d.exec_() + + def search(self, query, max_results=10, timeout=60): + url = 'http://ebooks.eharlequin.com/BANGSearch.dll?Type=FullText&FullTextField=All&FullTextCriteria=' + urllib2.quote(query) + + br = browser() + + counter = max_results + with closing(br.open(url, timeout=timeout)) as f: + doc = html.fromstring(f.read()) + for data in doc.xpath('//table[not(.//@class="sidelink")]/tr[.//ul[@id="details"]]'): + if counter <= 0: + break + + id = ''.join(data.xpath('.//ul[@id="details"]/li[@id="title-results"]/a/@href')) + if not id: + continue + + title = ''.join(data.xpath('.//ul[@id="details"]/li[@id="title-results"]/a/text()')) + author = ''.join(data.xpath('.//ul[@id="details"]/li[@id="author"][1]//a/text()')) + price = ''.join(data.xpath('.//div[@class="ourprice"]/font/text()')) + cover_url = ''.join(data.xpath('.//a[@href="%s"]/img/@src' % id)) + + counter -= 1 + + s = SearchResult() + s.cover_url = cover_url + s.title = title.strip() + s.author = author.strip() + s.price = price.strip() + s.detail_item = '?url=http://ebooks.eharlequin.com/' + id.strip() + + yield s diff --git a/src/calibre/gui2/store/feedbooks_plugin.py b/src/calibre/gui2/store/feedbooks_plugin.py new file mode 100644 index 0000000000..12873f8bc9 --- /dev/null +++ b/src/calibre/gui2/store/feedbooks_plugin.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +import urllib2 +from contextlib import closing + +from lxml import html + +from PyQt4.Qt import QUrl + +from calibre import browser, url_slash_cleaner +from calibre.gui2 import open_url +from calibre.gui2.store import StorePlugin +from calibre.gui2.store.basic_config import BasicStoreConfig +from calibre.gui2.store.search_result import SearchResult +from calibre.gui2.store.web_store_dialog import WebStoreDialog + +class FeedbooksStore(BasicStoreConfig, StorePlugin): + + def open(self, parent=None, detail_item=None, external=False): + settings = self.get_settings() + url = 'http://m.feedbooks.com/' + ext_url = 'http://feedbooks.com/' + + if external or settings.get(self.name + '_open_external', False): + if detail_item: + ext_url = ext_url + detail_item + open_url(QUrl(url_slash_cleaner(ext_url))) + else: + detail_url = None + if detail_item: + detail_url = url + detail_item + d = WebStoreDialog(self.gui, url, parent, detail_url) + d.setWindowTitle(self.name) + d.set_tags(settings.get(self.name + '_tags', '')) + d.exec_() + + def search(self, query, max_results=10, timeout=60): + url = 'http://m.feedbooks.com/search?query=' + urllib2.quote(query) + + br = browser() + + counter = max_results + with closing(br.open(url, timeout=timeout)) as f: + doc = html.fromstring(f.read()) + for data in doc.xpath('//ul[@class="m-list"]//li'): + if counter <= 0: + break + data = html.fromstring(html.tostring(data)) + + id = '' + id_a = data.xpath('//a[@class="buy"]') + if id_a: + id = id_a[0].get('href', None) + id = id.split('/')[-2] + id = '/item/' + id + else: + id_a = data.xpath('//a[@class="download"]') + if id_a: + id = id_a[0].get('href', None) + id = id.split('/')[-1] + id = id.split('.')[0] + id = '/book/' + id + if not id: + continue + + title = ''.join(data.xpath('//h5//a/text()')) + author = ''.join(data.xpath('//h6//a/text()')) + price = ''.join(data.xpath('//a[@class="buy"]/text()')) + if not price: + price = '$0.00' + cover_url = '' + cover_url_img = data.xpath('//img') + if cover_url_img: + cover_url = cover_url_img[0].get('src') + cover_url.split('?')[0] + + counter -= 1 + + s = SearchResult() + s.cover_url = cover_url + s.title = title.strip() + s.author = author.strip() + s.price = price.replace(' ', '').strip() + s.detail_item = id.strip() + + yield s diff --git a/src/calibre/gui2/store/gutenberg_plugin.py b/src/calibre/gui2/store/gutenberg_plugin.py new file mode 100644 index 0000000000..8d04b6236d --- /dev/null +++ b/src/calibre/gui2/store/gutenberg_plugin.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +import urllib2 +from contextlib import closing + +from lxml import html + +from PyQt4.Qt import QUrl + +from calibre import browser, url_slash_cleaner +from calibre.gui2 import open_url +from calibre.gui2.store import StorePlugin +from calibre.gui2.store.basic_config import BasicStoreConfig +from calibre.gui2.store.search_result import SearchResult +from calibre.gui2.store.web_store_dialog import WebStoreDialog + +class GutenbergStore(BasicStoreConfig, StorePlugin): + + def open(self, parent=None, detail_item=None, external=False): + settings = self.get_settings() + url = 'http://m.gutenberg.org/' + ext_url = 'http://gutenberg.org/' + + if external or settings.get(self.name + '_open_external', False): + if detail_item: + ext_url = ext_url + detail_item + open_url(QUrl(url_slash_cleaner(ext_url))) + else: + detail_url = None + if detail_item: + detail_url = url + detail_item + d = WebStoreDialog(self.gui, url, parent, detail_url) + d.setWindowTitle(self.name) + d.set_tags(settings.get(self.name + '_tags', '')) + d.exec_() + + def search(self, query, max_results=10, timeout=60): + # Gutenberg's website does not allow searching both author and title. + # Using a google search so we can search on both fields at once. + url = 'http://www.google.com/xhtml?q=site:gutenberg.org+' + urllib2.quote(query) + + br = browser() + + counter = max_results + with closing(br.open(url, timeout=timeout)) as f: + doc = html.fromstring(f.read()) + for data in doc.xpath('//div[@class="edewpi"]//div[@class="r ld"]'): + if counter <= 0: + break + + url = '' + url_a = data.xpath('div[@class="jd"]/a') + if url_a: + url_a = url_a[0] + url = url_a.get('href', None) + if url: + url = url.split('u=')[-1].split('&')[0] + if '/ebooks/' not in url: + continue + id = url.split('/')[-1] + + url_a = html.fromstring(html.tostring(url_a)) + heading = ''.join(url_a.xpath('//text()')) + title, _, author = heading.rpartition('by ') + author = author.split('-')[0] + price = '$0.00' + + counter -= 1 + + s = SearchResult() + s.cover_url = '' + s.title = title.strip() + s.author = author.strip() + s.price = price.strip() + s.detail_item = '/ebooks/' + id.strip() + + yield s diff --git a/src/calibre/gui2/store/kobo_plugin.py b/src/calibre/gui2/store/kobo_plugin.py new file mode 100644 index 0000000000..d37e806c3f --- /dev/null +++ b/src/calibre/gui2/store/kobo_plugin.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +import random +import urllib2 +from contextlib import closing + +from lxml import html + +from PyQt4.Qt import QUrl + +from calibre import browser, url_slash_cleaner +from calibre.gui2 import open_url +from calibre.gui2.store import StorePlugin +from calibre.gui2.store.basic_config import BasicStoreConfig +from calibre.gui2.store.search_result import SearchResult +from calibre.gui2.store.web_store_dialog import WebStoreDialog + +class KoboStore(BasicStoreConfig, StorePlugin): + + def open(self, parent=None, detail_item=None, external=False): + settings = self.get_settings() + + m_url = 'http://www.dpbolvw.net/' + h_click = 'click-4879827-10762497' + d_click = 'click-4879827-10772898' + # Use Kovid's affiliate id 30% of the time. + if random.randint(1, 10) in (1, 2, 3): + h_click = 'click-4913808-10762497' + d_click = 'click-4913808-10772898' + + url = m_url + h_click + detail_url = None + if detail_item: + detail_url = m_url + d_click + detail_item + + if external or settings.get(self.name + '_open_external', False): + open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url))) + else: + d = WebStoreDialog(self.gui, url, parent, detail_url) + d.setWindowTitle(self.name) + d.set_tags(settings.get(self.name + '_tags', '')) + d.exec_() + + def search(self, query, max_results=10, timeout=60): + url = 'http://www.kobobooks.com/search/search.html?q=' + urllib2.quote(query) + + br = browser() + + counter = max_results + with closing(br.open(url, timeout=timeout)) as f: + doc = html.fromstring(f.read()) + for data in doc.xpath('//ul[@class="SCShortCoverList"]/li'): + if counter <= 0: + break + + id = ''.join(data.xpath('.//div[@class="SearchImageContainer"]/a[1]/@href')) + if not id: + continue + + price = ''.join(data.xpath('.//span[@class="SCOurPrice"]/strong/text()')) + if not price: + price = '$0.00' + + cover_url = ''.join(data.xpath('.//div[@class="SearchImageContainer"]//img[1]/@src')) + + title = ''.join(data.xpath('.//div[@class="SCItemHeader"]/h1/a[1]/text()')) + author = ''.join(data.xpath('.//div[@class="SCItemSummary"]/span/a[1]/text()')) + + counter -= 1 + + s = SearchResult() + s.cover_url = cover_url + s.title = title.strip() + s.author = author.strip() + s.price = price.strip() + s.detail_item = '?url=http://www.kobobooks.com/' + id.strip() + + yield s diff --git a/src/calibre/gui2/store/manybooks_plugin.py b/src/calibre/gui2/store/manybooks_plugin.py new file mode 100644 index 0000000000..72fe54c427 --- /dev/null +++ b/src/calibre/gui2/store/manybooks_plugin.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +import re +import urllib2 +from contextlib import closing + +from lxml import html + +from PyQt4.Qt import QUrl + +from calibre import browser, url_slash_cleaner +from calibre.gui2 import open_url +from calibre.gui2.store import StorePlugin +from calibre.gui2.store.basic_config import BasicStoreConfig +from calibre.gui2.store.search_result import SearchResult +from calibre.gui2.store.web_store_dialog import WebStoreDialog + +class ManyBooksStore(BasicStoreConfig, StorePlugin): + + def open(self, parent=None, detail_item=None, external=False): + settings = self.get_settings() + url = 'http://manybooks.net/' + + detail_url = None + if detail_item: + detail_url = url + detail_item + + if external or settings.get(self.name + '_open_external', False): + open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url))) + else: + d = WebStoreDialog(self.gui, url, parent, detail_url) + d.setWindowTitle(self.name) + d.set_tags(settings.get(self.name + '_tags', '')) + d.exec_() + + def search(self, query, max_results=10, timeout=60): + # ManyBooks website separates results for title and author. + # It also doesn't do a clear job of references authors and + # secondary titles. Google is also faster. + # Using a google search so we can search on both fields at once. + url = 'http://www.google.com/xhtml?q=site:manybooks.net+' + urllib2.quote(query) + + br = browser() + + counter = max_results + with closing(br.open(url, timeout=timeout)) as f: + doc = html.fromstring(f.read()) + for data in doc.xpath('//div[@class="edewpi"]//div[@class="r ld"]'): + if counter <= 0: + break + + url = '' + url_a = data.xpath('div[@class="jd"]/a') + if url_a: + url_a = url_a[0] + url = url_a.get('href', None) + if url: + url = url.split('u=')[-1][:-2] + if '/titles/' not in url: + continue + id = url.split('/')[-1] + id = id.strip() + + url_a = html.fromstring(html.tostring(url_a)) + heading = ''.join(url_a.xpath('//text()')) + title, _, author = heading.rpartition('by ') + author = author.split('-')[0] + price = '$0.00' + + cover_url = '' + mo = re.match('^\D+', id) + if mo: + cover_name = mo.group() + cover_name = cover_name.replace('etext', '') + cover_id = id.split('.')[0] + cover_url = 'http://manybooks_images.s3.amazonaws.com/original_covers/' + id[0] + '/' + cover_name + '/' + cover_id + '-thumb.jpg' + + counter -= 1 + + s = SearchResult() + s.cover_url = cover_url + s.title = title.strip() + s.author = author.strip() + s.price = price.strip() + s.detail_item = '/titles/' + id + + yield s diff --git a/src/calibre/gui2/store/mobileread_plugin.py b/src/calibre/gui2/store/mobileread_plugin.py new file mode 100644 index 0000000000..49c265d7fe --- /dev/null +++ b/src/calibre/gui2/store/mobileread_plugin.py @@ -0,0 +1,304 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +import difflib +import heapq +import time +from contextlib import closing +from threading import RLock + +from lxml import html + +from PyQt4.Qt import Qt, QUrl, QDialog, QAbstractItemModel, QModelIndex, QVariant, \ + pyqtSignal + +from calibre import browser +from calibre.gui2 import open_url, NONE +from calibre.gui2.store import StorePlugin +from calibre.gui2.store.basic_config import BasicStoreConfig +from calibre.gui2.store.mobileread_store_dialog_ui import Ui_Dialog +from calibre.gui2.store.search_result import SearchResult +from calibre.gui2.store.web_store_dialog import WebStoreDialog +from calibre.utils.config import DynamicConfig +from calibre.utils.icu import sort_key + +class MobileReadStore(BasicStoreConfig, StorePlugin): + + def genesis(self): + self.config = DynamicConfig('store_' + self.name) + self.rlock = RLock() + + def open(self, parent=None, detail_item=None, external=False): + settings = self.get_settings() + url = 'http://www.mobileread.com/' + + if external or settings.get(self.name + '_open_external', False): + open_url(QUrl(detail_item if detail_item else url)) + else: + if detail_item: + d = WebStoreDialog(self.gui, url, parent, detail_item) + d.setWindowTitle(self.name) + d.set_tags(settings.get(self.name + '_tags', '')) + d.exec_() + else: + d = MobeReadStoreDialog(self, parent) + d.setWindowTitle(self.name) + d.exec_() + + def search(self, query, max_results=10, timeout=60): + books = self.get_book_list(timeout=timeout) + + query = query.lower() + query_parts = query.split(' ') + matches = [] + s = difflib.SequenceMatcher() + for x in books: + ratio = 0 + t_string = '%s %s' % (x.author.lower(), x.title.lower()) + query_matches = [] + for q in query_parts: + if q in t_string: + query_matches.append(q) + for q in query_matches: + s.set_seq2(q) + for p in t_string.split(' '): + s.set_seq1(p) + ratio += s.ratio() + if ratio > 0: + matches.append((ratio, x)) + + # Move the best scorers to head of list. + matches = heapq.nlargest(max_results, matches) + for score, book in matches: + book.price = '$0.00' + yield book + + def update_book_list(self, timeout=10): + with self.rlock: + url = 'http://www.mobileread.com/forums/ebooks.php?do=getlist&type=html' + + last_download = self.config.get(self.name + '_last_download', None) + # Don't update the book list if our cache is less than one week old. + if last_download and (time.time() - last_download) < 604800: + return + + # Download the book list HTML file from MobileRead. + br = browser() + raw_data = None + with closing(br.open(url, timeout=timeout)) as f: + raw_data = f.read() + + if not raw_data: + return + + # Turn books listed in the HTML file into BookRef's. + books = [] + try: + data = html.fromstring(raw_data) + for book_data in data.xpath('//ul/li'): + book = BookRef() + book.detail_item = ''.join(book_data.xpath('.//a/@href')) + book.format = ''.join(book_data.xpath('.//i/text()')) + book.format = book.format.strip() + + text = ''.join(book_data.xpath('.//a/text()')) + if ':' in text: + book.author, q, text = text.partition(':') + book.author = book.author.strip() + book.title = text.strip() + books.append(book) + except: + pass + + # Save the book list and it's create time. + if books: + self.config[self.name + '_last_download'] = time.time() + self.config[self.name + '_book_list'] = books + + def get_book_list(self, timeout=10): + self.update_book_list(timeout=timeout) + return self.config.get(self.name + '_book_list', []) + + +class BookRef(SearchResult): + + def __init__(self): + SearchResult.__init__(self) + + self.format = '' + + +class MobeReadStoreDialog(QDialog, Ui_Dialog): + + def __init__(self, plugin, *args): + QDialog.__init__(self, *args) + self.setupUi(self) + + self.plugin = plugin + + self.model = BooksModel() + self.results_view.setModel(self.model) + self.results_view.model().set_books(self.plugin.get_book_list()) + self.total.setText('%s' % self.model.rowCount()) + + self.results_view.activated.connect(self.open_store) + self.search_query.textChanged.connect(self.model.set_filter) + self.results_view.model().total_changed.connect(self.total.setText) + self.finished.connect(self.dialog_closed) + + self.restore_state() + + def open_store(self, index): + result = self.results_view.model().get_book(index) + if result: + self.plugin.open(self, result.detail_item) + + def restore_state(self): + geometry = self.plugin.config['store_mobileread_dialog_geometry'] + if geometry: + self.restoreGeometry(geometry) + + results_cwidth = self.plugin.config['store_mobileread_dialog_results_view_column_width'] + if results_cwidth: + for i, x in enumerate(results_cwidth): + if i >= self.results_view.model().columnCount(): + break + self.results_view.setColumnWidth(i, x) + else: + for i in xrange(self.results_view.model().columnCount()): + self.results_view.resizeColumnToContents(i) + + self.results_view.model().sort_col = self.plugin.config.get('store_mobileread_dialog_sort_col', 0) + self.results_view.model().sort_order = self.plugin.config.get('store_mobileread_dialog_sort_order', Qt.AscendingOrder) + self.results_view.model().sort(self.results_view.model().sort_col, self.results_view.model().sort_order) + self.results_view.header().setSortIndicator(self.results_view.model().sort_col, self.results_view.model().sort_order) + + def save_state(self): + self.plugin.config['store_mobileread_dialog_geometry'] = self.saveGeometry() + self.plugin.config['store_mobileread_dialog_results_view_column_width'] = [self.results_view.columnWidth(i) for i in range(self.model.columnCount())] + self.plugin.config['store_mobileread_dialog_sort_col'] = self.results_view.model().sort_col + self.plugin.config['store_mobileread_dialog_sort_order'] = self.results_view.model().sort_order + + def dialog_closed(self, result): + self.save_state() + + +class BooksModel(QAbstractItemModel): + + total_changed = pyqtSignal(unicode) + + HEADERS = [_('Title'), _('Author(s)'), _('Format')] + + def __init__(self): + QAbstractItemModel.__init__(self) + self.books = [] + self.all_books = [] + self.filter = '' + self.sort_col = 0 + self.sort_order = Qt.AscendingOrder + + def set_books(self, books): + self.books = books + self.all_books = books + + self.sort(self.sort_col, self.sort_order) + + def get_book(self, index): + row = index.row() + if row < len(self.books): + return self.books[row] + else: + return None + + def set_filter(self, filter): + #self.layoutAboutToBeChanged.emit() + self.beginResetModel() + + self.filter = unicode(filter) + self.books = [] + if self.filter: + for b in self.all_books: + test = '%s %s %s' % (b.title, b.author, b.format) + test = test.lower() + include = True + for item in self.filter.split(' '): + item = item.lower() + if item not in test: + include = False + break + if include: + self.books.append(b) + else: + self.books = self.all_books + + self.sort(self.sort_col, self.sort_order, reset=False) + self.total_changed.emit('%s' % self.rowCount()) + + self.endResetModel() + #self.layoutChanged.emit() + + def index(self, row, column, parent=QModelIndex()): + return self.createIndex(row, column) + + def parent(self, index): + if not index.isValid() or index.internalId() == 0: + return QModelIndex() + return self.createIndex(0, 0) + + def rowCount(self, *args): + return len(self.books) + + def columnCount(self, *args): + return len(self.HEADERS) + + def headerData(self, section, orientation, role): + if role != Qt.DisplayRole: + return NONE + text = '' + if orientation == Qt.Horizontal: + if section < len(self.HEADERS): + text = self.HEADERS[section] + return QVariant(text) + else: + return QVariant(section+1) + + def data(self, index, role): + row, col = index.row(), index.column() + result = self.books[row] + if role == Qt.DisplayRole: + if col == 0: + return QVariant(result.title) + elif col == 1: + return QVariant(result.author) + elif col == 2: + return QVariant(result.format) + return NONE + + def data_as_text(self, result, col): + text = '' + if col == 0: + text = result.title + elif col == 1: + text = result.author + elif col == 2: + text = result.format + return text + + def sort(self, col, order, reset=True): + self.sort_col = col + self.sort_order = order + + if not self.books: + return + descending = order == Qt.DescendingOrder + self.books.sort(None, + lambda x: sort_key(unicode(self.data_as_text(x, col))), + descending) + if reset: + self.reset() + diff --git a/src/calibre/gui2/store/mobileread_store_dialog.ui b/src/calibre/gui2/store/mobileread_store_dialog.ui new file mode 100644 index 0000000000..027d5994f0 --- /dev/null +++ b/src/calibre/gui2/store/mobileread_store_dialog.ui @@ -0,0 +1,112 @@ + + + Dialog + + + + 0 + 0 + 691 + 614 + + + + Dialog + + + + + + + + Search: + + + + + + + + + + + + true + + + false + + + false + + + true + + + false + + + false + + + + + + + + + Books: + + + + + + + 0 + + + + + + + Qt::Horizontal + + + + 308 + 20 + + + + + + + + Close + + + + + + + + + + + close_button + clicked() + Dialog + accept() + + + 440 + 432 + + + 245 + 230 + + + + + diff --git a/src/calibre/gui2/store/open_library_plugin.py b/src/calibre/gui2/store/open_library_plugin.py new file mode 100644 index 0000000000..15b674f262 --- /dev/null +++ b/src/calibre/gui2/store/open_library_plugin.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +import urllib2 +from contextlib import closing + +from lxml import html + +from PyQt4.Qt import QUrl + +from calibre import browser, url_slash_cleaner +from calibre.gui2 import open_url +from calibre.gui2.store import StorePlugin +from calibre.gui2.store.basic_config import BasicStoreConfig +from calibre.gui2.store.search_result import SearchResult +from calibre.gui2.store.web_store_dialog import WebStoreDialog + +class OpenLibraryStore(BasicStoreConfig, StorePlugin): + + def open(self, parent=None, detail_item=None, external=False): + settings = self.get_settings() + url = 'http://openlibrary.org/' + + if external or settings.get(self.name + '_open_external', False): + if detail_item: + url = url + detail_item + open_url(QUrl(url_slash_cleaner(url))) + else: + detail_url = None + if detail_item: + detail_url = url + detail_item + d = WebStoreDialog(self.gui, url, parent, detail_url) + d.setWindowTitle(self.name) + d.set_tags(settings.get(self.name + '_tags', '')) + d.exec_() + + def search(self, query, max_results=10, timeout=60): + url = 'http://openlibrary.org/search?q=' + urllib2.quote(query) + '&has_fulltext=true' + + br = browser() + + counter = max_results + with closing(br.open(url, timeout=timeout)) as f: + doc = html.fromstring(f.read()) + for data in doc.xpath('//div[@id="searchResults"]/ul[@id="siteSearch"]/li'): + if counter <= 0: + break + + id = ''.join(data.xpath('./span[@class="bookcover"]/a/@href')) + if not id: + continue + cover_url = ''.join(data.xpath('./span[@class="bookcover"]/a/img/@src')) + + title = ''.join(data.xpath('.//h3[@class="booktitle"]/a[@class="results"]/text()')) + author = ''.join(data.xpath('.//span[@class="bookauthor"]/a/text()')) + price = '$0.00' + + counter -= 1 + + s = SearchResult() + s.cover_url = cover_url + s.title = title.strip() + s.author = author.strip() + s.price = price + s.detail_item = id.strip() + + yield s diff --git a/src/calibre/gui2/store/search.py b/src/calibre/gui2/store/search.py new file mode 100644 index 0000000000..1d263959ef --- /dev/null +++ b/src/calibre/gui2/store/search.py @@ -0,0 +1,452 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +import re +import time +from contextlib import closing +from random import shuffle +from threading import Thread +from Queue import Queue + +from PyQt4.Qt import (Qt, QAbstractItemModel, QDialog, QTimer, QVariant, + QModelIndex, QPixmap, QSize, QCheckBox, QVBoxLayout) + +from calibre import browser +from calibre.gui2 import NONE +from calibre.gui2.progress_indicator import ProgressIndicator +from calibre.gui2.store.search_ui import Ui_Dialog +from calibre.utils.config import DynamicConfig +from calibre.utils.icu import sort_key +from calibre.utils.magick.draw import thumbnail + +HANG_TIME = 75000 # milliseconds seconds +TIMEOUT = 75 # seconds +SEARCH_THREAD_TOTAL = 4 +COVER_DOWNLOAD_THREAD_TOTAL = 2 + +class SearchDialog(QDialog, Ui_Dialog): + + def __init__(self, istores, *args): + QDialog.__init__(self, *args) + self.setupUi(self) + + self.config = DynamicConfig('store_search') + + # We keep a cache of store plugins and reference them by name. + self.store_plugins = istores + self.search_pool = SearchThreadPool(SearchThread, SEARCH_THREAD_TOTAL) + # Check for results and hung threads. + self.checker = QTimer() + self.hang_check = 0 + + self.model = Matches() + self.results_view.setModel(self.model) + + # Add check boxes for each store so the user + # can disable searching specific stores on a + # per search basis. + stores_group_layout = QVBoxLayout() + self.stores_group.setLayout(stores_group_layout) + for x in self.store_plugins: + cbox = QCheckBox(x) + cbox.setChecked(True) + stores_group_layout.addWidget(cbox) + setattr(self, 'store_check_' + x, cbox) + stores_group_layout.addStretch() + + # Create and add the progress indicator + self.pi = ProgressIndicator(self, 24) + self.bottom_layout.insertWidget(0, self.pi) + + self.search.clicked.connect(self.do_search) + self.checker.timeout.connect(self.get_results) + self.results_view.activated.connect(self.open_store) + self.select_all_stores.clicked.connect(self.stores_select_all) + self.select_invert_stores.clicked.connect(self.stores_select_invert) + self.select_none_stores.clicked.connect(self.stores_select_none) + self.finished.connect(self.dialog_closed) + + self.restore_state() + + def resize_columns(self): + total = 600 + # Cover + self.results_view.setColumnWidth(0, 85) + total = total - 85 + # Title + self.results_view.setColumnWidth(1,int(total*.35)) + # Author + self.results_view.setColumnWidth(2,int(total*.35)) + # Price + self.results_view.setColumnWidth(3, int(total*.10)) + # Store + self.results_view.setColumnWidth(4, int(total*.20)) + + def do_search(self, checked=False): + # Stop all running threads. + self.checker.stop() + self.search_pool.abort() + # Clear the visible results. + self.results_view.model().clear_results() + + # Don't start a search if there is nothing to search for. + query = unicode(self.search_edit.text()) + if not query.strip(): + return + + # Plugins are in alphebetic order. Randomize the + # order of plugin names. This way plugins closer + # to a don't have an unfair advantage over + # plugins further from a. + store_names = self.store_plugins.keys() + if not store_names: + return + shuffle(store_names) + # Add plugins that the user has checked to the search pool's work queue. + for n in store_names: + if getattr(self, 'store_check_' + n).isChecked(): + self.search_pool.add_task(query, n, self.store_plugins[n], TIMEOUT) + if self.search_pool.has_tasks(): + self.hang_check = 0 + self.checker.start(100) + self.search_pool.start_threads() + self.pi.startAnimation() + + def save_state(self): + self.config['store_search_geometry'] = self.saveGeometry() + self.config['store_search_store_splitter_state'] = self.store_splitter.saveState() + self.config['store_search_results_view_column_width'] = [self.results_view.columnWidth(i) for i in range(self.model.columnCount())] + + store_check = {} + for n in self.store_plugins: + store_check[n] = getattr(self, 'store_check_' + n).isChecked() + self.config['store_search_store_checked'] = store_check + + def restore_state(self): + geometry = self.config['store_search_geometry'] + if geometry: + self.restoreGeometry(geometry) + + splitter_state = self.config['store_search_store_splitter_state'] + if splitter_state: + self.store_splitter.restoreState(splitter_state) + + results_cwidth = self.config['store_search_results_view_column_width'] + if results_cwidth: + for i, x in enumerate(results_cwidth): + if i >= self.model.columnCount(): + break + self.results_view.setColumnWidth(i, x) + else: + self.resize_columns() + + store_check = self.config['store_search_store_checked'] + if store_check: + for n in store_check: + if hasattr(self, 'store_check_' + n): + getattr(self, 'store_check_' + n).setChecked(store_check[n]) + + def get_results(self): + # We only want the search plugins to run + # a maximum set amount of time before giving up. + self.hang_check += 1 + if self.hang_check >= HANG_TIME: + self.search_pool.abort() + self.checker.stop() + self.pi.stopAnimation() + else: + # Stop the checker if not threads are running. + if not self.search_pool.threads_running() and not self.search_pool.has_tasks(): + self.checker.stop() + self.pi.stopAnimation() + + while self.search_pool.has_results(): + res = self.search_pool.get_result() + if res: + self.results_view.model().add_result(res) + + def open_store(self, index): + result = self.results_view.model().get_result(index) + self.store_plugins[result.store_name].open(self, result.detail_item) + + def get_store_checks(self): + ''' + Returns a list of QCheckBox's for each store. + ''' + checks = [] + for x in self.store_plugins: + check = getattr(self, 'store_check_' + x, None) + if check: + checks.append(check) + return checks + + def stores_select_all(self): + for check in self.get_store_checks(): + check.setChecked(True) + + def stores_select_invert(self): + for check in self.get_store_checks(): + check.setChecked(not check.isChecked()) + + def stores_select_none(self): + for check in self.get_store_checks(): + check.setChecked(False) + + def dialog_closed(self, result): + self.model.closing() + self.search_pool.abort() + self.save_state() + + +class GenericDownloadThreadPool(object): + ''' + add_task must be implemented in a subclass. + ''' + + def __init__(self, thread_type, thread_count): + self.thread_type = thread_type + self.thread_count = thread_count + + self.tasks = Queue() + self.results = Queue() + self.threads = [] + + def add_task(self): + raise NotImplementedError() + + def start_threads(self): + for i in range(self.thread_count): + t = self.thread_type(self.tasks, self.results) + self.threads.append(t) + t.start() + + def abort(self): + self.tasks = Queue() + self.results = Queue() + for t in self.threads: + t.abort() + self.threads = [] + + def has_tasks(self): + return not self.tasks.empty() + + def get_result(self): + return self.results.get() + + def get_result_no_wait(self): + return self.results.get_nowait() + + def result_count(self): + return len(self.results) + + def has_results(self): + return not self.results.empty() + + def threads_running(self): + for t in self.threads: + if t.is_alive(): + return True + return False + + +class SearchThreadPool(GenericDownloadThreadPool): + ''' + Threads will run until there is no work or + abort is called. Create and start new threads + using start_threads(). Reset by calling abort(). + + Example: + sp = SearchThreadPool(SearchThread, 3) + add tasks using add_task(...) + sp.start_threads() + all threads have finished. + sp.abort() + add tasks using add_task(...) + sp.start_threads() + ''' + + def add_task(self, query, store_name, store_plugin, timeout): + self.tasks.put((query, store_name, store_plugin, timeout)) + + +class SearchThread(Thread): + + def __init__(self, tasks, results): + Thread.__init__(self) + self.daemon = True + self.tasks = tasks + self.results = results + self._run = True + + def abort(self): + self._run = False + + def run(self): + while self._run and not self.tasks.empty(): + try: + query, store_name, store_plugin, timeout = self.tasks.get() + for res in store_plugin.search(query, timeout=timeout): + if not self._run: + return + res.store_name = store_name + self.results.put(res) + self.tasks.task_done() + except: + pass + + +class CoverThreadPool(GenericDownloadThreadPool): + ''' + Once started all threads run until abort is called. + ''' + + def add_task(self, search_result, update_callback, timeout=5): + self.tasks.put((search_result, update_callback, timeout)) + + +class CoverThread(Thread): + + def __init__(self, tasks, results): + Thread.__init__(self) + self.daemon = True + self.tasks = tasks + self.results = results + self._run = True + + self.br = browser() + + def abort(self): + self._run = False + + def run(self): + while self._run: + try: + time.sleep(.1) + while not self.tasks.empty(): + if not self._run: + break + result, callback, timeout = self.tasks.get() + if result and result.cover_url: + with closing(self.br.open(result.cover_url, timeout=timeout)) as f: + result.cover_data = f.read() + result.cover_data = thumbnail(result.cover_data, 64, 64)[2] + callback() + self.tasks.task_done() + except: + continue + + +class Matches(QAbstractItemModel): + + HEADERS = [_('Cover'), _('Title'), _('Author(s)'), _('Price'), _('Store')] + + def __init__(self): + QAbstractItemModel.__init__(self) + self.matches = [] + self.cover_pool = CoverThreadPool(CoverThread, 2) + self.cover_pool.start_threads() + + def closing(self): + self.cover_pool.abort() + + def clear_results(self): + self.matches = [] + self.cover_pool.abort() + self.cover_pool.start_threads() + self.reset() + + def add_result(self, result): + self.layoutAboutToBeChanged.emit() + self.matches.append(result) + self.cover_pool.add_task(result, self.update_result) + self.layoutChanged.emit() + + def get_result(self, index): + row = index.row() + if row < len(self.matches): + return self.matches[row] + else: + return None + + def update_result(self): + self.layoutAboutToBeChanged.emit() + self.layoutChanged.emit() + + def index(self, row, column, parent=QModelIndex()): + return self.createIndex(row, column) + + def parent(self, index): + if not index.isValid() or index.internalId() == 0: + return QModelIndex() + return self.createIndex(0, 0) + + def rowCount(self, *args): + return len(self.matches) + + def columnCount(self, *args): + return len(self.HEADERS) + + def headerData(self, section, orientation, role): + if role != Qt.DisplayRole: + return NONE + text = '' + if orientation == Qt.Horizontal: + if section < len(self.HEADERS): + text = self.HEADERS[section] + return QVariant(text) + else: + return QVariant(section+1) + + def data(self, index, role): + row, col = index.row(), index.column() + result = self.matches[row] + if role == Qt.DisplayRole: + if col == 1: + return QVariant(result.title) + elif col == 2: + return QVariant(result.author) + elif col == 3: + return QVariant(result.price) + elif col == 4: + return QVariant(result.store_name) + return NONE + elif role == Qt.DecorationRole: + if col == 0 and result.cover_data: + p = QPixmap() + p.loadFromData(result.cover_data) + return QVariant(p) + elif role == Qt.SizeHintRole: + return QSize(64, 64) + return NONE + + def data_as_text(self, result, col): + text = '' + if col == 1: + text = result.title + elif col == 2: + text = result.author + elif col == 3: + text = result.price + if len(text) < 3 or text[-3] not in ('.', ','): + text += '00' + text = re.sub(r'\D', '', text) + text = text.rjust(6, '0') + elif col == 4: + text = result.store_name + return text + + def sort(self, col, order, reset=True): + if not self.matches: + return + descending = order == Qt.DescendingOrder + self.matches.sort(None, + lambda x: sort_key(unicode(self.data_as_text(x, col))), + descending) + if reset: + self.reset() + diff --git a/src/calibre/gui2/store/search.ui b/src/calibre/gui2/store/search.ui new file mode 100644 index 0000000000..16fc0c4deb --- /dev/null +++ b/src/calibre/gui2/store/search.ui @@ -0,0 +1,196 @@ + + + Dialog + + + + 0 + 0 + 937 + 669 + + + + calibre Store Search + + + true + + + + + + + + Query: + + + + + + + + + + Search + + + + + + + + + Qt::Horizontal + + + + Stores + + + + + + true + + + + + 0 + 0 + 215 + 116 + + + + + + + + + + + All + + + + + + + Invert + + + + + + + None + + + + + + + + + + + 2 + 0 + + + + Qt::Horizontal + + + + Qt::Horizontal + + + + + 1 + 0 + + + + + 0 + 0 + + + + true + + + + 32 + 32 + + + + false + + + false + + + false + + + true + + + false + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Close + + + + + + + + + + + close + clicked() + Dialog + accept() + + + 526 + 525 + + + 307 + 272 + + + + + diff --git a/src/calibre/gui2/store/search_result.py b/src/calibre/gui2/store/search_result.py new file mode 100644 index 0000000000..6e0ed0b572 --- /dev/null +++ b/src/calibre/gui2/store/search_result.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +class SearchResult(object): + + def __init__(self): + self.store_name = '' + self.cover_url = '' + self.cover_data = None + self.title = '' + self.author = '' + self.price = '' + self.detail_item = '' diff --git a/src/calibre/gui2/store/smashwords_plugin.py b/src/calibre/gui2/store/smashwords_plugin.py new file mode 100644 index 0000000000..1806e9f4e1 --- /dev/null +++ b/src/calibre/gui2/store/smashwords_plugin.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +import random +import re +import urllib2 +from contextlib import closing + +from lxml import html + +from PyQt4.Qt import QUrl + +from calibre import browser, url_slash_cleaner +from calibre.gui2 import open_url +from calibre.gui2.store import StorePlugin +from calibre.gui2.store.basic_config import BasicStoreConfig +from calibre.gui2.store.search_result import SearchResult +from calibre.gui2.store.web_store_dialog import WebStoreDialog + +class SmashwordsStore(BasicStoreConfig, StorePlugin): + + def open(self, parent=None, detail_item=None, external=False): + settings = self.get_settings() + url = 'http://www.smashwords.com/' + + aff_id = '?ref=usernone' + # Use Kovid's affiliate id 30% of the time. + if random.randint(1, 10) in (1, 2, 3): + aff_id = '?ref=kovidgoyal' + + detail_url = None + if detail_item: + detail_url = url + detail_item + aff_id + url = url + aff_id + + if external or settings.get(self.name + '_open_external', False): + open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url))) + else: + d = WebStoreDialog(self.gui, url, parent, detail_url) + d.setWindowTitle(self.name) + d.set_tags(settings.get(self.name + '_tags', '')) + d.exec_() + + def search(self, query, max_results=10, timeout=60): + url = 'http://www.smashwords.com/books/search?query=' + urllib2.quote(query) + + br = browser() + + counter = max_results + with closing(br.open(url, timeout=timeout)) as f: + doc = html.fromstring(f.read()) + for data in doc.xpath('//div[@id="pageCenterContent2"]//div[@class="bookCoverImg"]'): + if counter <= 0: + break + data = html.fromstring(html.tostring(data)) + + id = None + id_a = data.xpath('//a[@class="bookTitle"]') + if id_a: + id = id_a[0].get('href', None) + if id: + id = id.split('/')[-1] + if not id: + continue + + cover_url = '' + c_url = data.get('style', None) + if c_url: + mo = re.search(r'http://[^\'"]+', c_url) + if mo: + cover_url = mo.group() + + title = ''.join(data.xpath('//a[@class="bookTitle"]/text()')) + subnote = ''.join(data.xpath('//span[@class="subnote"]/text()')) + author = ''.join(data.xpath('//span[@class="subnote"]/a/text()')) + price = subnote.partition('$')[2] + price = price.split(u'\xa0')[0] + price = '$' + price + + counter -= 1 + + s = SearchResult() + s.cover_url = cover_url + s.title = title.strip() + s.author = author.strip() + s.price = price.strip() + s.detail_item = '/books/view/' + id.strip() + + yield s diff --git a/src/calibre/gui2/store/web_control.py b/src/calibre/gui2/store/web_control.py new file mode 100644 index 0000000000..874328f872 --- /dev/null +++ b/src/calibre/gui2/store/web_control.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +import os +from urlparse import urlparse + +from PyQt4.Qt import QNetworkCookieJar, QFileDialog, QNetworkProxy +from PyQt4.QtWebKit import QWebView, QWebPage + +from calibre import USER_AGENT, get_proxies, get_download_filename +from calibre.ebooks import BOOK_EXTENSIONS +from calibre.ptempfile import PersistentTemporaryFile + +class NPWebView(QWebView): + + def __init__(self, *args): + QWebView.__init__(self, *args) + self.gui = None + self.tags = '' + + self.setPage(NPWebPage()) + self.page().networkAccessManager().setCookieJar(QNetworkCookieJar()) + + http_proxy = get_proxies().get('http', None) + if http_proxy: + proxy_parts = urlparse(http_proxy) + proxy = QNetworkProxy() + proxy.setType(QNetworkProxy.HttpProxy) + proxy.setUser(proxy_parts.username) + proxy.setPassword(proxy_parts.password) + proxy.setHostName(proxy_parts.hostname) + proxy.setPort(proxy_parts.port) + self.page().networkAccessManager().setProxy(proxy) + + self.page().setForwardUnsupportedContent(True) + self.page().unsupportedContent.connect(self.start_download) + self.page().downloadRequested.connect(self.start_download) + self.page().networkAccessManager().sslErrors.connect(self.ignore_ssl_errors) + + def createWindow(self, type): + if type == QWebPage.WebBrowserWindow: + return self + else: + return None + + def set_gui(self, gui): + self.gui = gui + + def set_tags(self, tags): + self.tags = tags + + def start_download(self, request): + if not self.gui: + return + + url = unicode(request.url().toString()) + cf = self.get_cookies() + + filename = get_download_filename(url, cf) + ext = os.path.splitext(filename)[1][1:].lower() + if ext not in BOOK_EXTENSIONS: + home = os.path.expanduser('~') + name = QFileDialog.getSaveFileName(self, + _('File is not a supported ebook type. Save to disk?'), + os.path.join(home, filename), + '*.*') + if name: + self.gui.download_ebook(url, cf, name, name, False) + else: + self.gui.download_ebook(url, cf, filename, tags=self.tags) + + def ignore_ssl_errors(self, reply, errors): + reply.ignoreSslErrors(errors) + + def get_cookies(self): + ''' + Writes QNetworkCookies to Mozilla cookie .txt file. + + :return: The file path to the cookie file. + ''' + cf = PersistentTemporaryFile(suffix='.txt') + + cf.write('# Netscape HTTP Cookie File\n\n') + + for c in self.page().networkAccessManager().cookieJar().allCookies(): + cookie = [] + domain = unicode(c.domain()) + + cookie.append(domain) + cookie.append('TRUE' if domain.startswith('.') else 'FALSE') + cookie.append(unicode(c.path())) + cookie.append('TRUE' if c.isSecure() else 'FALSE') + cookie.append(unicode(c.expirationDate().toTime_t())) + cookie.append(unicode(c.name())) + cookie.append(unicode(c.value())) + + cf.write('\t'.join(cookie)) + cf.write('\n') + + cf.close() + return cf.name + + +class NPWebPage(QWebPage): + + def userAgentForUrl(self, url): + return USER_AGENT diff --git a/src/calibre/gui2/store/web_store_dialog.py b/src/calibre/gui2/store/web_store_dialog.py new file mode 100644 index 0000000000..20fb016b6b --- /dev/null +++ b/src/calibre/gui2/store/web_store_dialog.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +from PyQt4.Qt import QDialog, QUrl + +from calibre import url_slash_cleaner +from calibre.gui2.store.web_store_dialog_ui import Ui_Dialog + +class WebStoreDialog(QDialog, Ui_Dialog): + + def __init__(self, gui, base_url, parent=None, detail_url=None): + QDialog.__init__(self, parent=parent) + self.setupUi(self) + + self.gui = gui + self.base_url = base_url + + self.view.set_gui(self.gui) + self.view.loadStarted.connect(self.load_started) + self.view.loadProgress.connect(self.load_progress) + self.view.loadFinished.connect(self.load_finished) + self.home.clicked.connect(self.go_home) + self.reload.clicked.connect(self.view.reload) + self.back.clicked.connect(self.view.back) + + self.go_home(detail_url=detail_url) + + def set_tags(self, tags): + self.view.set_tags(tags) + + def load_started(self): + self.progress.setValue(0) + + def load_progress(self, val): + self.progress.setValue(val) + + def load_finished(self, ok=True): + self.progress.setValue(100) + + def go_home(self, checked=False, detail_url=None): + if detail_url: + url = detail_url + else: + url = self.base_url + + # Reduce redundant /'s because some stores + # (Feedbooks) and server frameworks (cherrypy) + # choke on them. + url = url_slash_cleaner(url) + self.view.load(QUrl(url)) diff --git a/src/calibre/gui2/store/web_store_dialog.ui b/src/calibre/gui2/store/web_store_dialog.ui new file mode 100644 index 0000000000..b89b9305be --- /dev/null +++ b/src/calibre/gui2/store/web_store_dialog.ui @@ -0,0 +1,115 @@ + + + Dialog + + + + 0 + 0 + 962 + 656 + + + + + + + true + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + + + + about:blank + + + + + + + + + + + Home + + + + + + + Reload + + + + + + + 0 + + + %p% + + + + + + + Back + + + + + + + Close + + + + + + + + QWebView + QWidget +
QtWebKit/QWebView
+
+ + NPWebView + QWebView +
web_control.h
+
+
+ + + + close + clicked() + Dialog + accept() + + + 917 + 635 + + + 480 + 327 + + + + +
diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 6ad6f053cb..7b68229da0 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -12,11 +12,11 @@ import traceback, copy, cPickle from itertools import izip, repeat from functools import partial -from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QFont, QSize, \ - QIcon, QPoint, QVBoxLayout, QHBoxLayout, QComboBox, QTimer,\ - QAbstractItemModel, QVariant, QModelIndex, QMenu, QFrame,\ - QPushButton, QWidget, QItemDelegate, QString, QLabel, \ - QShortcut, QKeySequence, SIGNAL, QMimeData, QToolButton +from PyQt4.Qt import (Qt, QTreeView, QApplication, pyqtSignal, QFont, QSize, + QIcon, QPoint, QVBoxLayout, QHBoxLayout, QComboBox, QTimer, + QAbstractItemModel, QVariant, QModelIndex, QMenu, QFrame, + QWidget, QItemDelegate, QString, QLabel, QPushButton, + QShortcut, QKeySequence, SIGNAL, QMimeData, QToolButton) from calibre.ebooks.metadata import title_sort from calibre.gui2 import config, NONE, gprefs @@ -1829,8 +1829,24 @@ class TagBrowserMixin(object): # {{{ self.tags_view.drag_drop_finished.connect(self.drag_drop_finished) self.tags_view.restriction_error.connect(self.do_restriction_error, type=Qt.QueuedConnection) - self.edit_categories.clicked.connect(lambda x: - self.do_edit_user_categories()) + + for text, func, args, cat_name in ( + (_('Manage Authors'), + self.do_author_sort_edit, (self, None), 'authors'), + (_('Manage Series'), + self.do_tags_list_edit, (None, 'series'), 'series'), + (_('Manage Publishers'), + self.do_tags_list_edit, (None, 'publisher'), 'publisher'), + (_('Manage Tags'), + self.do_tags_list_edit, (None, 'tags'), 'tags'), + (_('Manage User Categories'), + self.do_edit_user_categories, (None,), 'user:'), + (_('Manage Saved Searches'), + self.do_saved_search_edit, (None,), 'search') + ): + self.manage_items_button.menu().addAction( + QIcon(I(category_icon_map[cat_name])), + text, partial(func, *args)) def do_restriction_error(self): error_dialog(self.tags_view, _('Invalid search restriction'), @@ -2149,11 +2165,15 @@ class TagBrowserWidget(QWidget): # {{{ 'match any or all of them')) parent.tag_match.setStatusTip(parent.tag_match.toolTip()) - parent.edit_categories = QPushButton(_('Manage &user categories'), parent) - self._layout.addWidget(parent.edit_categories) - parent.edit_categories.setToolTip( - _('Add your own categories to the Tag Browser')) - parent.edit_categories.setStatusTip(parent.edit_categories.toolTip()) + + l = parent.manage_items_button = QPushButton(self) + l.setStyleSheet('QPushButton {text-align: left; }') + l.setText(_('Manage authors, tags, etc')) + l.setToolTip(_('All of these category_managers are available by right-clicking ' + 'on items in the tag browser above')) + l.m = QMenu() + l.setMenu(l.m) + self._layout.addWidget(l) # self.leak_test_timer = QTimer(self) # self.leak_test_timer.timeout.connect(self.test_for_leak) diff --git a/src/calibre/gui2/threaded_jobs.py b/src/calibre/gui2/threaded_jobs.py index 9c791c5b0d..ad295503a0 100644 --- a/src/calibre/gui2/threaded_jobs.py +++ b/src/calibre/gui2/threaded_jobs.py @@ -38,7 +38,7 @@ class ThreadedJob(BaseJob): :func: The function that actually does the work. This function *must* accept at least three keyword arguments: abort, log and notifications. abort is - An Event object. func should periodically check abort.is_set(0 and if + An Event object. func should periodically check abort.is_set() and if it is True, it should stop processing as soon as possible. notifications is a Queue. func should put progress notifications into it in the form of a tuple (frac, msg). frac is a number between 0 and 1 indicating diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index e7853b9491..9a4e0ca70a 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -23,7 +23,7 @@ from calibre.constants import __appname__, isosx from calibre.utils.config import prefs, dynamic from calibre.utils.ipc.server import Server from calibre.library.database2 import LibraryDatabase2 -from calibre.customize.ui import interface_actions +from calibre.customize.ui import interface_actions, store_plugins from calibre.gui2 import error_dialog, GetMetadata, open_url, \ gprefs, max_available_height, config, info_dialog, Dispatcher, \ question_dialog @@ -34,6 +34,7 @@ from calibre.gui2.main_window import MainWindow from calibre.gui2.layout import MainWindowMixin from calibre.gui2.device import DeviceMixin from calibre.gui2.email import EmailMixin +from calibre.gui2.ebook_download import EbookDownloadMixin from calibre.gui2.jobs import JobManager, JobsDialog, JobsButton from calibre.gui2.init import LibraryViewMixin, LayoutMixin from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin @@ -87,19 +88,28 @@ class SystemTrayIcon(QSystemTrayIcon): # {{{ # }}} +_gui = None + +def get_gui(): + return _gui + class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin, - SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin + SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin, + EbookDownloadMixin ): 'The main GUI' def __init__(self, opts, parent=None, gui_debug=None): + global _gui MainWindow.__init__(self, opts, parent=parent, disable_automatic_gc=True) + _gui = self self.opts = opts self.device_connected = None self.gui_debug = gui_debug self.iactions = OrderedDict() + # Actions for action in interface_actions(): if opts.ignore_plugins and action.plugin_path is not None: continue @@ -112,11 +122,10 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ if action.plugin_path is None: raise continue - ac.plugin_path = action.plugin_path ac.interface_action_base_plugin = action - self.add_iaction(ac) + self.load_store_plugins() def init_iaction(self, action): ac = action.load_actual_plugin(self) @@ -133,6 +142,37 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ else: acmap[ac.name] = ac + def load_store_plugins(self): + self.istores = OrderedDict() + for store in store_plugins(): + if self.opts.ignore_plugins and store.plugin_path is not None: + continue + try: + st = self.init_istore(store) + self.add_istore(st) + except: + # Ignore errors in loading user supplied plugins + import traceback + traceback.print_exc() + if store.plugin_path is None: + raise + continue + + def init_istore(self, store): + st = store.load_actual_plugin(self) + st.plugin_path = store.plugin_path + st.base_plugin = store + store.actual_istore_plugin_loaded = True + return st + + def add_istore(self, st): + stmap = self.istores + if st.name in stmap: + if st.priority >= stmap[st.name].priority: + stmap[st.name] = st + else: + stmap[st.name] = st + def initialize(self, library_path, db, listener, actions, show_gui=True): opts = self.opts @@ -154,6 +194,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ for ac in self.iactions.values(): ac.do_genesis() self.donate_action = QAction(QIcon(I('donate.png')), _('&Donate to support calibre'), self) + for st in self.istores.values(): + st.do_genesis() MainWindowMixin.__init__(self, db) # Jobs Button {{{ @@ -165,6 +207,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ LayoutMixin.__init__(self) EmailMixin.__init__(self) + EbookDownloadMixin.__init__(self) DeviceMixin.__init__(self) self.progress_indicator = ProgressIndicator(self) @@ -493,10 +536,10 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ action.location_selected(location) if location == 'library': self.search_restriction.setEnabled(True) - self.search_options_button.setEnabled(True) + self.highlight_only_button.setEnabled(True) else: self.search_restriction.setEnabled(False) - self.search_options_button.setEnabled(False) + self.highlight_only_button.setEnabled(False) # Reset the view in case something changed while it was invisible self.current_view().reset() self.set_number_of_books_shown() diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py index f062aecc26..b1a8236151 100644 --- a/src/calibre/library/cli.py +++ b/src/calibre/library/cli.py @@ -426,7 +426,7 @@ def do_show_metadata(db, id, as_opf): mi = OPFCreator(os.getcwd(), mi) mi.render(sys.stdout) else: - print unicode(mi).encode(preferred_encoding) + prints(unicode(mi)) def show_metadata_option_parser(): parser = get_parser(_( diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index b5155368c7..bdcefd13a2 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -854,7 +854,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): mi.uuid = row[fm['uuid']] mi.title_sort = row[fm['sort']] mi.last_modified = row[fm['last_modified']] - mi.size = row[fm['size']] formats = row[fm['formats']] if not formats: formats = None diff --git a/src/calibre/translations/calibre.pot b/src/calibre/translations/calibre.pot index 9f38c7fb8b..8895eb64b3 100644 --- a/src/calibre/translations/calibre.pot +++ b/src/calibre/translations/calibre.pot @@ -4,9 +4,9 @@ # msgid "" msgstr "" -"Project-Id-Version: calibre 0.7.55\n" -"POT-Creation-Date: 2011-04-15 09:45+MDT\n" -"PO-Revision-Date: 2011-04-15 09:45+MDT\n" +"Project-Id-Version: calibre 0.7.56\n" +"POT-Creation-Date: 2011-04-17 09:36+MDT\n" +"PO-Revision-Date: 2011-04-17 09:36+MDT\n" "Last-Translator: Automatically generated\n" "Language-Team: LANGUAGE\n" "MIME-Version: 1.0\n" @@ -34,7 +34,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/ebooks/chm/input.py:99 #: /home/kovid/work/calibre/src/calibre/ebooks/chm/input.py:102 #: /home/kovid/work/calibre/src/calibre/ebooks/chm/metadata.py:56 -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:430 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:435 #: /home/kovid/work/calibre/src/calibre/ebooks/epub/periodical.py:127 #: /home/kovid/work/calibre/src/calibre/ebooks/fb2/input.py:100 #: /home/kovid/work/calibre/src/calibre/ebooks/fb2/input.py:102 @@ -84,8 +84,8 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:125 #: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:159 #: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:667 -#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:883 #: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:885 +#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:887 #: /home/kovid/work/calibre/src/calibre/ebooks/odt/input.py:49 #: /home/kovid/work/calibre/src/calibre/ebooks/odt/input.py:51 #: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:1001 @@ -151,7 +151,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:188 #: /home/kovid/work/calibre/src/calibre/gui2/metadata/bulk_download.py:112 #: /home/kovid/work/calibre/src/calibre/gui2/metadata/bulk_download2.py:252 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:320 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:324 #: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:156 #: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:160 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:199 @@ -160,11 +160,11 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/library/database2.py:500 #: /home/kovid/work/calibre/src/calibre/library/database2.py:508 #: /home/kovid/work/calibre/src/calibre/library/database2.py:519 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:1801 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:1925 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:2914 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:2916 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:3049 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:1800 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:1924 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:2913 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:2915 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:3048 #: /home/kovid/work/calibre/src/calibre/library/server/mobile.py:233 #: /home/kovid/work/calibre/src/calibre/library/server/opds.py:156 #: /home/kovid/work/calibre/src/calibre/library/server/opds.py:159 @@ -222,6 +222,11 @@ msgstr "" msgid "Preferences" msgstr "" +#: /home/kovid/work/calibre/src/calibre/customize/__init__.py:609 +#: /home/kovid/work/calibre/src/calibre/gui2/store/search.py:346 +msgid "Store" +msgstr "" + #: /home/kovid/work/calibre/src/calibre/customize/builtins.py:19 msgid "Follow all local links in an HTML file and create a ZIP file containing all linked files. This plugin is run every time you add an HTML file to the library." msgstr "" @@ -289,195 +294,251 @@ msgstr "" msgid "Set metadata from %s files" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:872 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:880 msgid "Look and Feel" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:874 -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:886 -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:897 -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:908 -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:920 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:882 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:894 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:905 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:916 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:928 msgid "Interface" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:878 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:886 msgid "Adjust the look and feel of the calibre interface to suit your tastes" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:884 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:892 msgid "Behavior" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:890 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:898 msgid "Change the way calibre behaves" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:895 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:903 #: /home/kovid/work/calibre/src/calibre/gui2/library/views.py:218 msgid "Add your own columns" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:901 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:909 msgid "Add/remove your own columns to the calibre book list" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:906 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:914 msgid "Toolbar" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:912 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:920 msgid "Customize the toolbars and context menus, changing which actions are available in each" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:918 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:926 msgid "Searching" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:924 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:932 msgid "Customize the way searching for books works in calibre" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:929 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:937 msgid "Input Options" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:931 -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:942 -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:953 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:939 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:950 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:961 msgid "Conversion" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:935 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:943 msgid "Set conversion options specific to each input format" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:940 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:948 msgid "Common Options" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:946 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:954 msgid "Set conversion options common to all formats" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:951 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:959 msgid "Output Options" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:957 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:965 msgid "Set conversion options specific to each output format" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:962 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:970 msgid "Adding books" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:964 -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:976 -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:988 -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1000 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:972 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:984 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:996 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1008 msgid "Import/Export" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:968 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:976 msgid "Control how calibre reads metadata from files when adding books" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:974 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:982 msgid "Saving books to disk" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:980 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:988 msgid "Control how calibre exports files from its database to disk when using Save to disk" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:986 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:994 msgid "Sending books to devices" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:992 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1000 msgid "Control how calibre transfers files to your ebook reader" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:998 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1006 msgid "Metadata plugboards" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1004 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1012 msgid "Change metadata fields before saving/sending" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1009 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1017 msgid "Template Functions" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1011 -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1058 -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1070 -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1081 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1019 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1066 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1078 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1089 msgid "Advanced" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1015 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1023 msgid "Create your own template functions" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1020 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1028 msgid "Sharing books by email" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1022 -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1034 -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1047 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1030 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1042 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1055 msgid "Sharing" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1026 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1034 msgid "Setup sharing of books via email. Can be used for automatic sending of downloaded news to your devices" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1032 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1040 msgid "Sharing over the net" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1038 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1046 msgid "Setup the calibre Content Server which will give you access to your calibre library from anywhere, on any device, over the internet" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1045 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1053 #: /home/kovid/work/calibre/src/calibre/ebooks/metadata/fetch.py:57 msgid "Metadata download" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1051 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1059 msgid "Control how calibre downloads ebook metadata from the net" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1056 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:268 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1064 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:269 msgid "Plugins" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1062 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1070 msgid "Add/remove/customize various bits of calibre functionality" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1068 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1076 msgid "Tweaks" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1074 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1082 msgid "Fine tune how calibre behaves in various contexts" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1079 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1087 msgid "Miscellaneous" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1085 +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1093 msgid "Miscellaneous advanced configuration" msgstr "" +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1107 +msgid "Kindle books from Amazon" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1112 +msgid "Ebooks for readers." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1117 +msgid "Books, Textbooks, eBooks, Toys, Games and More." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1122 +msgid "Publishers of fine books." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1127 +msgid "World Famous eBook Store." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1132 +msgid "The digital bookstore." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1137 +msgid "entertain, enrich, inspire." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1142 +msgid "Read anywhere." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1147 +msgid "The first producer of free ebooks." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1152 +msgid "eReading: anytime. anyplace." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1157 +msgid "The best ebooks at the best price: free!" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1162 +msgid "Ebooks handcrafted with the utmost care" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1167 +msgid "One web page for every book." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1172 +msgid "Your ebook. Your way." +msgstr "" + #: /home/kovid/work/calibre/src/calibre/customize/conversion.py:102 msgid "Conversion Input" msgstr "" @@ -627,31 +688,31 @@ msgstr "" msgid "This profile is intended for the Sanda Bambook." msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/ui.py:26 +#: /home/kovid/work/calibre/src/calibre/customize/ui.py:27 msgid "Installed plugins" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/ui.py:27 +#: /home/kovid/work/calibre/src/calibre/customize/ui.py:28 msgid "Mapping for filetype plugins" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/ui.py:28 +#: /home/kovid/work/calibre/src/calibre/customize/ui.py:29 msgid "Local plugin customization" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/ui.py:29 +#: /home/kovid/work/calibre/src/calibre/customize/ui.py:30 msgid "Disabled plugins" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/ui.py:30 +#: /home/kovid/work/calibre/src/calibre/customize/ui.py:31 msgid "Enabled plugins" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/ui.py:490 +#: /home/kovid/work/calibre/src/calibre/customize/ui.py:502 msgid "Initialization of plugin %s failed with traceback:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/ui.py:523 +#: /home/kovid/work/calibre/src/calibre/customize/ui.py:535 msgid "" " %prog options\n" "\n" @@ -659,31 +720,31 @@ msgid "" " " msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/ui.py:529 +#: /home/kovid/work/calibre/src/calibre/customize/ui.py:541 msgid "Add a plugin by specifying the path to the zip file containing it." msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/ui.py:531 +#: /home/kovid/work/calibre/src/calibre/customize/ui.py:543 msgid "Remove a custom plugin by name. Has no effect on builtin plugins" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/ui.py:533 +#: /home/kovid/work/calibre/src/calibre/customize/ui.py:545 msgid "Customize plugin. Specify name of plugin and customization string separated by a comma." msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/ui.py:535 +#: /home/kovid/work/calibre/src/calibre/customize/ui.py:547 msgid "List all installed plugins" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/ui.py:537 +#: /home/kovid/work/calibre/src/calibre/customize/ui.py:549 msgid "Enable the named plugin" msgstr "" -#: /home/kovid/work/calibre/src/calibre/customize/ui.py:539 +#: /home/kovid/work/calibre/src/calibre/customize/ui.py:551 msgid "Disable the named plugin" msgstr "" -#: /home/kovid/work/calibre/src/calibre/debug.py:150 +#: /home/kovid/work/calibre/src/calibre/debug.py:152 msgid "Debug log" msgstr "" @@ -760,6 +821,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:477 #: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:1094 #: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:3119 +#: /home/kovid/work/calibre/src/calibre/gui2/ebook_download.py:106 msgid "finished" msgstr "" @@ -787,7 +849,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:445 #: /home/kovid/work/calibre/src/calibre/library/database2.py:299 #: /home/kovid/work/calibre/src/calibre/library/database2.py:312 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:2778 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:2777 #: /home/kovid/work/calibre/src/calibre/library/field_metadata.py:159 msgid "News" msgstr "" @@ -795,8 +857,8 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:2656 #: /home/kovid/work/calibre/src/calibre/gui2/catalog/catalog_epub_mobi.py:65 #: /home/kovid/work/calibre/src/calibre/library/catalog.py:634 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:2740 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:2758 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:2739 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:2757 msgid "Catalog" msgstr "" @@ -1356,83 +1418,83 @@ msgstr "" msgid "Get device information..." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:197 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:202 msgid "Rendered %s" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:200 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:205 msgid "Failed %s" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:254 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:259 msgid "" "Failed to process comic: \n" "\n" "%s" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:273 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:278 msgid "Number of colors for grayscale image conversion. Default: %default. Values of less than 256 may result in blurred text on your device if you are creating your comics in EPUB format." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:277 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:282 msgid "Disable normalize (improve contrast) color range for pictures. Default: False" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:280 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:285 msgid "Maintain picture aspect ratio. Default is to fill the screen." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:282 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:287 msgid "Disable sharpening." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:284 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:289 msgid "Disable trimming of comic pages. For some comics, trimming might remove content as well as borders." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:287 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:292 msgid "Don't split landscape images into two portrait images" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:289 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:294 msgid "Keep aspect ratio and scale image using screen height as image width for viewing in landscape mode." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:292 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:297 msgid "Used for right-to-left publications like manga. Causes landscape pages to be split into portrait pages from right to left." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:296 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:301 msgid "Enable Despeckle. Reduces speckle noise. May greatly increase processing time." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:299 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:304 msgid "Don't sort the files found in the comic alphabetically by name. Instead use the order they were added to the comic." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:303 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:308 msgid "The format that images in the created ebook are converted to. You can experiment to see which format gives you optimal size and look on your device." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:307 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:312 msgid "Apply no processing to the image" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:309 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:314 msgid "Do not convert the image to grayscale (black and white)" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:311 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:316 msgid "Specify the image size as widthxheight pixels. Normally, an image size is automatically calculated from the output profile, this option overrides it." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:315 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:320 msgid "When converting a CBC do not add links to each page to the TOC. Note this only applies if the TOC has more than one section" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:454 -#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:466 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:459 +#: /home/kovid/work/calibre/src/calibre/ebooks/comic/input.py:471 msgid "Page" msgstr "" @@ -2273,6 +2335,8 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1028 #: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:128 #: /home/kovid/work/calibre/src/calibre/gui2/preferences/metadata_sources.py:151 +#: /home/kovid/work/calibre/src/calibre/gui2/store/mobileread_plugin.py:195 +#: /home/kovid/work/calibre/src/calibre/gui2/store/search.py:346 #: /home/kovid/work/calibre/src/calibre/library/field_metadata.py:331 #: /home/kovid/work/calibre/src/calibre/library/server/opds.py:574 msgid "Title" @@ -2283,6 +2347,8 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:66 #: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:438 #: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1029 +#: /home/kovid/work/calibre/src/calibre/gui2/store/mobileread_plugin.py:195 +#: /home/kovid/work/calibre/src/calibre/gui2/store/search.py:346 msgid "Author(s)" msgstr "" @@ -2689,6 +2755,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/ebooks/metadata/opf2.py:1356 #: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:1491 +#: /home/kovid/work/calibre/src/calibre/gui2/store/search.py:346 msgid "Cover" msgstr "" @@ -2848,7 +2915,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/ebooks/oeb/transforms/cover.py:98 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:176 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:220 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:723 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:734 msgid "Book %s of %s" msgstr "" @@ -3364,7 +3431,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:30 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:308 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:534 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:545 msgid "Books" msgstr "" @@ -3706,7 +3773,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:457 #: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:463 #: /home/kovid/work/calibre/src/calibre/gui2/preferences/columns.py:102 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:274 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:275 #: /home/kovid/work/calibre/src/calibre/gui2/wizard/send_email.py:223 msgid "Are you sure?" msgstr "" @@ -3738,8 +3805,8 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:310 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/restore_library.py:106 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/restore_library.py:111 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:286 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:340 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:287 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:341 msgid "Success" msgstr "" @@ -3782,7 +3849,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:404 #: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:167 #: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:101 -#: /home/kovid/work/calibre/src/calibre/gui2/library/views.py:829 +#: /home/kovid/work/calibre/src/calibre/gui2/library/views.py:831 msgid "Not allowed" msgstr "" @@ -3811,7 +3878,7 @@ msgid "Bulk convert" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/actions/convert.py:86 -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:518 +#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:554 msgid "Cannot convert" msgstr "" @@ -4357,6 +4424,19 @@ msgstr "" msgid "Books with the same tags" msgstr "" +#: /home/kovid/work/calibre/src/calibre/gui2/actions/store.py:18 +msgid "Get books" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/store.py:27 +#: /home/kovid/work/calibre/src/calibre/gui2/search_box.py:92 +#: /home/kovid/work/calibre/src/calibre/gui2/search_box.py:276 +#: /home/kovid/work/calibre/src/calibre/gui2/store/search_ui.py:106 +#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:653 +#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:280 +msgid "Search" +msgstr "" + #: /home/kovid/work/calibre/src/calibre/gui2/actions/tweak_epub.py:15 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tweak_epub_ui.py:54 msgid "Tweak ePub" @@ -4691,6 +4771,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/shortcuts.py:48 #: /home/kovid/work/calibre/src/calibre/gui2/shortcuts_ui.py:78 #: /home/kovid/work/calibre/src/calibre/gui2/shortcuts_ui.py:83 +#: /home/kovid/work/calibre/src/calibre/gui2/store/search_ui.py:110 #: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:351 msgid "None" msgstr "" @@ -4805,6 +4886,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/preferences/template_functions_ui.py:95 #: /home/kovid/work/calibre/src/calibre/gui2/preferences/toolbar_ui.py:98 #: /home/kovid/work/calibre/src/calibre/gui2/preferences/tweaks_ui.py:87 +#: /home/kovid/work/calibre/src/calibre/gui2/store/basic_config_widget_ui.py:37 #: /home/kovid/work/calibre/src/calibre/gui2/wizard/send_email_ui.py:123 msgid "Form" msgstr "" @@ -5683,38 +5765,38 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:180 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:171 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:666 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:677 msgid "Choose cover for " msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:187 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:178 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:674 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:685 msgid "Cannot read" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:188 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:179 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:675 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:686 msgid "You do not have permission to read the file: " msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:196 #: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:203 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:187 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:683 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:694 msgid "Error reading file" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:197 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:188 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:684 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:695 msgid "

There was an error reading from file:
" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:204 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:196 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:694 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:705 msgid " is not a valid picture" msgstr "" @@ -5775,7 +5857,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:171 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:537 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:430 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:848 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:859 msgid "Tags categorize the book. This is particularly useful while searching.

They can be any words or phrases, separated by commas." msgstr "" @@ -5783,7 +5865,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:544 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:433 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_ui.py:214 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:296 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:307 msgid "&Series:" msgstr "" @@ -5793,7 +5875,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:546 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:434 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:435 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:295 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:306 msgid "List of known series. You can add new series." msgstr "" @@ -5981,6 +6063,7 @@ msgid "Occurrences:" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/convert/regex_builder_ui.py:94 +#: /home/kovid/work/calibre/src/calibre/gui2/store/mobileread_store_dialog_ui.py:64 msgid "0" msgstr "" @@ -6067,6 +6150,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:96 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/message_box_ui.py:52 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/progress_ui.py:53 +#: /home/kovid/work/calibre/src/calibre/gui2/store/mobileread_store_dialog_ui.py:61 msgid "Dialog" msgstr "" @@ -6391,7 +6475,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:215 #: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:248 #: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:252 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:1031 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:1042 msgid "Undefined" msgstr "" @@ -6535,7 +6619,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/device.py:611 #: /home/kovid/work/calibre/src/calibre/gui2/preferences/misc.py:41 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:304 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:305 #: /home/kovid/work/calibre/src/calibre/utils/ipc/job.py:54 msgid "Error" msgstr "" @@ -6895,6 +6979,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_format_device_ui.py:49 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/delete_matching_from_device.py:76 #: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1201 +#: /home/kovid/work/calibre/src/calibre/gui2/store/mobileread_plugin.py:195 msgid "Format" msgstr "" @@ -7454,7 +7539,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:530 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:424 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:806 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:817 msgid "&Rating:" msgstr "" @@ -7462,7 +7547,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:532 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:425 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:426 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:807 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:818 msgid "Rating of this book. 0-5 stars" msgstr "" @@ -7483,7 +7568,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:539 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:431 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:432 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:148 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:152 msgid "Open Tag Editor" msgstr "" @@ -7536,7 +7621,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:558 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:440 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:1015 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:1026 msgid "&Date:" msgstr "" @@ -7609,14 +7694,14 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:581 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:465 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:456 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:609 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:460 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:613 msgid "&Basic metadata" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:582 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:466 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:463 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:467 msgid "&Custom metadata" msgstr "" @@ -7776,38 +7861,38 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:122 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:128 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:274 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:281 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:278 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:285 msgid "Could not read cover" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:123 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:275 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:279 msgid "Could not read cover from %s format" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:129 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:282 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:286 msgid "The cover in the %s format is invalid" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:158 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:766 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:777 msgid "Cover size: %dx%d pixels" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:195 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:693 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:704 msgid "Not a valid picture" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:214 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:717 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:728 msgid "Specify title and author" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:215 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:718 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:729 msgid "You must specify a title and author before generating a cover" msgstr "" @@ -7850,44 +7935,44 @@ msgid "The cover is not a valid picture" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:307 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:532 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:543 msgid "Choose formats for " msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:338 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:564 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:575 msgid "No permission" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:339 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:565 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:576 msgid "You do not have permission to read the following files:" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:366 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:367 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:595 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:596 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:606 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:607 msgid "No format selected" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:378 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:607 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:618 msgid "Could not read metadata" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:379 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:608 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:619 msgid "Could not read metadata from %s format" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:453 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:232 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:233 msgid " The green color indicates that the current author sort matches the current author" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:456 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:235 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:236 msgid " The red color indicates that the current author sort does not match the current author. No action is required if this is what you want." msgstr "" @@ -7911,8 +7996,8 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:475 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:484 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:411 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:416 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:415 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:420 msgid "Save changes and edit the metadata of %s" msgstr "" @@ -7927,22 +8012,22 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:690 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:695 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:954 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:965 msgid "This ISBN number is valid" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:698 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:957 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:968 msgid "This ISBN number is invalid" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:783 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:883 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:894 msgid "Tags changed" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:784 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:884 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:895 msgid "You have changed the tags. In order to use the tags editor, you must either discard or apply these changes. Apply changes?" msgstr "" @@ -7971,12 +8056,12 @@ msgid "You must specify at least one of ISBN, Title, Authors or Publisher" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:961 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:358 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:362 msgid "Permission denied" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:962 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:359 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:363 msgid "Could not open %s. Is it being used by another program?" msgstr "" @@ -7996,12 +8081,12 @@ msgid "" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:413 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:118 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:122 msgid "Swap the author and title" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:415 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:107 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:106 msgid "" "Automatically create the author sort entry based on the current author entry.\n" "Using this button to create author sort will change author sort from red to green." @@ -8028,7 +8113,7 @@ msgid "" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:436 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:125 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:129 msgid "Remove unused series (Series that have no books)" msgstr "" @@ -8041,7 +8126,7 @@ msgid "dd MMM yyyy" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:442 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:1066 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:1077 msgid "Publishe&d:" msgstr "" @@ -8050,7 +8135,7 @@ msgid "&Fetch metadata from server" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:448 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:627 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:638 msgid "&Browse" msgstr "" @@ -8059,7 +8144,7 @@ msgid "Remove border (if any) from cover" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:450 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:629 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:640 msgid "T&rim" msgstr "" @@ -8068,12 +8153,12 @@ msgid "Reset cover to default" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:452 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:631 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:642 msgid "&Remove" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:453 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:637 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:648 msgid "Download co&ver" msgstr "" @@ -8082,7 +8167,7 @@ msgid "Generate a default cover based on the title and author" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:455 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:638 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:649 msgid "&Generate cover" msgstr "" @@ -8099,7 +8184,7 @@ msgid "Remove the selected formats for this book from the database." msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:461 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:450 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:461 msgid "Set the cover for the book from the selected format" msgstr "" @@ -8108,7 +8193,7 @@ msgid "Update metadata from the metadata in the selected format" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:464 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:674 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:678 msgid "&Comments" msgstr "" @@ -8524,7 +8609,7 @@ msgid "&Author:" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_ui.py:215 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:847 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:858 msgid "Ta&gs:" msgstr "" @@ -8997,6 +9082,26 @@ msgstr "" msgid "Failed to download from %r with error: %s" msgstr "" +#: /home/kovid/work/calibre/src/calibre/gui2/ebook_download.py:41 +msgid "No file specified to download." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/ebook_download.py:66 +msgid "Not a support ebook format." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/ebook_download.py:87 +msgid "Downloading %s" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/ebook_download.py:99 +msgid "Downloading" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/ebook_download.py:103 +msgid "Failed to download ebook" +msgstr "" + #: /home/kovid/work/calibre/src/calibre/gui2/email.py:91 msgid "Email %s to %s" msgstr "" @@ -9273,7 +9378,7 @@ msgid "Show books in the main memory of the device" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/layout.py:67 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:1017 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:1016 msgid "Card A" msgstr "" @@ -9282,7 +9387,7 @@ msgid "Show books in storage card A" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/layout.py:69 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:1019 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:1018 msgid "Card B" msgstr "" @@ -9322,23 +9427,15 @@ msgstr "" msgid "Reset Quick Search" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:207 -msgid "Change the way searching for books works" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:219 +#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:213 msgid "Copy current search text (instead of search name)" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:225 +#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:219 msgid "Save current search under the name shown in the box" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:231 -msgid "Delete current saved search" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:267 +#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:254 msgid "Donate" msgstr "" @@ -9435,7 +9532,7 @@ msgstr "" msgid "Restore default layout" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/library/views.py:830 +#: /home/kovid/work/calibre/src/calibre/gui2/library/views.py:832 msgid "Dropping onto a device is not supported. First add the book to the calibre library." msgstr "" @@ -9492,6 +9589,7 @@ msgid "Previous Page" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/lrf_renderer/main_ui.py:133 +#: /home/kovid/work/calibre/src/calibre/gui2/store/web_store_dialog_ui.py:62 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main_ui.py:193 msgid "Back" msgstr "" @@ -9654,49 +9752,49 @@ msgstr "" msgid "Author s&ort:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:352 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:363 msgid "&Number:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:433 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:444 msgid "" "Last modified: %s\n" "\n" "Double click to view" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:736 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:747 msgid "Invalid cover" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:737 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:748 msgid "Could not change cover as the image is invalid." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:764 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:775 msgid "This book has no cover" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:814 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:825 msgid "stars" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:907 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:918 msgid "I&ds:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:908 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:919 msgid "" "Edit the identifiers for this book. For example: \n" "\n" "%s" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:964 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:975 msgid "&Publisher:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:1034 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:1045 msgid "Clear date" msgstr "" @@ -9837,40 +9935,48 @@ msgid "Downloaded metadata fields" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:75 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:233 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:237 msgid "Edit Metadata" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:164 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:112 +msgid "Set author sort from author" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:113 +msgid "Set author from author sort" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:168 msgid "&Download metadata" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:174 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:178 msgid "Change how calibre downloads metadata" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:504 -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:693 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:508 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:697 msgid "Change cover" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:553 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:557 msgid "Co&mments" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:592 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:596 msgid "&Metadata" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:597 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:601 msgid "&Cover and formats" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:616 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:620 msgid "Configure metadata downloading" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:662 +#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:666 msgid "C&ustom metadata" msgstr "" @@ -9946,7 +10052,7 @@ msgstr "" msgid "Restore settings to default values. You have to click Apply to actually save the default settings." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/__init__.py:325 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/__init__.py:328 msgid "Configure " msgstr "" @@ -10948,71 +11054,71 @@ msgstr "" msgid "Search for plugin" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:225 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:226 msgid "No matches" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:226 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:227 msgid "Could not find any matching plugins" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:267 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:268 msgid "Add plugin" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:275 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:276 msgid "Installing plugins is a security risk. Plugins can contain a virus/malware. Only install it if you got it from a trusted source. Are you sure you want to proceed?" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:287 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:288 msgid "Plugin {0} successfully installed under {1} plugins. You may have to restart calibre for the plugin to take effect." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:295 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:296 msgid "No valid plugin path" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:296 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:297 msgid "%s is not a valid plugin path" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:305 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:306 msgid "Select an actual plugin under %s to customize" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:311 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:312 msgid "Plugin cannot be disabled" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:312 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:313 msgid "The plugin: %s cannot be disabled" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:322 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:323 msgid "Plugin not customizable" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:323 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:324 msgid "Plugin: %s does not need customization" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:329 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:330 msgid "Must restart" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:330 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:331 msgid "You must restart calibre before you can configure the %s plugin" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:335 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:336 msgid "Plugin {0} successfully removed" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:343 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:344 msgid "Cannot remove builtin plugin" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:344 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:345 msgid " cannot be removed. It is a builtin plugin. Try disabling it instead." msgstr "" @@ -11242,7 +11348,7 @@ msgid "Here you can control how calibre will save your books when you click the msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/preferences/server.py:70 -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:382 +#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:418 msgid "Failed to start content server" msgstr "" @@ -11591,34 +11697,23 @@ msgstr "" msgid "Apply any changes you made to this tweak" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/search_box.py:93 -#: /home/kovid/work/calibre/src/calibre/gui2/search_box.py:277 -#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:653 -#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:280 -msgid "Search" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/search_box.py:321 -msgid "The selected search will be permanently deleted. Are you sure?" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/search_box.py:364 +#: /home/kovid/work/calibre/src/calibre/gui2/search_box.py:346 msgid "Search (For Advanced Search click the button to the left)" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/search_box.py:388 +#: /home/kovid/work/calibre/src/calibre/gui2/search_box.py:369 msgid "Enable or disable search highlighting." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/search_box.py:447 +#: /home/kovid/work/calibre/src/calibre/gui2/search_box.py:424 msgid "Saved Searches" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/search_box.py:449 +#: /home/kovid/work/calibre/src/calibre/gui2/search_box.py:426 msgid "Choose saved search or enter name for new saved search" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/search_box.py:462 +#: /home/kovid/work/calibre/src/calibre/gui2/search_box.py:439 #: /home/kovid/work/calibre/src/calibre/gui2/search_restriction_mixin.py:34 #: /home/kovid/work/calibre/src/calibre/gui2/search_restriction_mixin.py:42 msgid "*Current search" @@ -11696,6 +11791,73 @@ msgstr "" msgid "&Alternate shortcut:" msgstr "" +#: +#: /home/kovid/work/calibre/src/calibre/gui2/store/basic_config_widget_ui.py:38 +msgid "Added Tags:" +msgstr "" + +#: +#: /home/kovid/work/calibre/src/calibre/gui2/store/basic_config_widget_ui.py:39 +msgid "Open store in external web browswer" +msgstr "" + +#: +#: /home/kovid/work/calibre/src/calibre/gui2/store/mobileread_store_dialog_ui.py:62 +msgid "Search:" +msgstr "" + +#: +#: /home/kovid/work/calibre/src/calibre/gui2/store/mobileread_store_dialog_ui.py:63 +msgid "Books:" +msgstr "" + +#: +#: /home/kovid/work/calibre/src/calibre/gui2/store/mobileread_store_dialog_ui.py:65 +#: /home/kovid/work/calibre/src/calibre/gui2/store/search_ui.py:111 +#: /home/kovid/work/calibre/src/calibre/gui2/store/web_store_dialog_ui.py:63 +msgid "Close" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/store/search.py:346 +msgid "Price" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/store/search_ui.py:104 +msgid "calibre Store Search" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/store/search_ui.py:105 +msgid "Query:" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/store/search_ui.py:107 +msgid "Stores" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/store/search_ui.py:108 +msgid "All" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/store/search_ui.py:109 +msgid "Invert" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/store/web_control.py:69 +msgid "File is not a supported ebook type. Save to disk?" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/store/web_store_dialog_ui.py:59 +msgid "Home" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/store/web_store_dialog_ui.py:60 +msgid "Reload" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/store/web_store_dialog_ui.py:61 +msgid "%p%" +msgstr "" + #: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:345 #: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:375 #: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:404 @@ -11760,11 +11922,13 @@ msgid "Manage %s" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:454 +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1844 msgid "Manage Saved Searches" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:462 #: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:466 +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1842 msgid "Manage User Categories" msgstr "" @@ -11816,52 +11980,68 @@ msgstr "" msgid "The saved search name %s is already used." msgstr "" +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1834 +msgid "Manage Authors" +msgstr "" + #: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1836 +msgid "Manage Series" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1838 +msgid "Manage Publishers" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1840 +msgid "Manage Tags" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1852 msgid "Invalid search restriction" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1837 +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1853 msgid "The current search restriction is invalid" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1853 +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1869 msgid "New Category" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1904 -#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1907 +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1920 +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1923 msgid "Delete user category" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1905 +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1921 msgid "%s is not a user category" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1908 +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1924 msgid "%s contains items. Do you really want to delete it?" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1929 +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1945 msgid "Remove category" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1930 +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1946 msgid "User category %s does not exist" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1949 +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1965 msgid "Add to user category" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1950 +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1966 msgid "A user category %s does not exist" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2073 +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2089 msgid "Find item in tag browser" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2076 +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2092 msgid "" "Search for items. This is a \"contains\" search; items containing the\n" "text anywhere in the name will be found. You can limit the search\n" @@ -11871,60 +12051,60 @@ msgid "" "containing the text \"foo\"" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2085 +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2101 msgid "ALT+f" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2089 +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2105 msgid "F&ind" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2090 +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2106 msgid "Find the first/next matching item" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2095 +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2111 msgid "Collapse all categories" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2119 +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2135 msgid "No More Matches.

Click Find again to go to first match" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2132 +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2148 msgid "Sort by name" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2132 +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2148 msgid "Sort by popularity" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2133 +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2149 msgid "Sort by average rating" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2136 +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2152 msgid "Set the sort order for entries in the Tag Browser" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2143 +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2159 msgid "Match all" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2143 +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2159 msgid "Match any" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2148 +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2164 msgid "When selecting multiple entries in the Tag Browser match any or all of them" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2152 -msgid "Manage &user categories" +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2171 +msgid "Manage authors, tags, etc" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2155 -msgid "Add your own categories to the Tag Browser" +#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2172 +msgid "All of these category_managers are available by right-clicking on items in the tag browser above" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/tools.py:65 @@ -11965,58 +12145,58 @@ msgstr "" msgid "The following books have already been converted to %s format. Do you wish to reconvert them?" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:156 +#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:189 msgid "&Donate to support calibre" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:189 +#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:225 msgid "&Restore" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:194 +#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:230 msgid "&Eject connected device" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:239 +#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:275 msgid "Calibre Quick Start Guide" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:305 +#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:341 msgid "Debug mode" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:306 +#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:342 msgid "You have started calibre in debug mode. After you quit calibre, the debug log will be available in the file: %s

The log will be displayed automatically." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:506 +#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:542 msgid "Conversion Error" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:529 +#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:565 msgid "Recipe Disabled" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:545 +#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:581 msgid "Failed" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:578 +#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:614 msgid "There are active jobs. Are you sure you want to quit?" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:581 +#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:617 msgid "" " is communicating with the device!
\n" " Quitting may cause corruption on the device.
\n" " Are you sure you want to quit?" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:585 +#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:621 msgid "Active jobs" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:653 +#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:689 msgid "will keep running in the system tray. To close it, choose Quit in the context menu of the system tray." msgstr "" @@ -13613,19 +13793,19 @@ msgstr "" msgid "%sAverage rating is %3.1f" msgstr "" -#: /home/kovid/work/calibre/src/calibre/library/database2.py:1015 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:1014 msgid "Main" msgstr "" -#: /home/kovid/work/calibre/src/calibre/library/database2.py:3075 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:3074 msgid "

Migrating old database to ebook library in %s

" msgstr "" -#: /home/kovid/work/calibre/src/calibre/library/database2.py:3104 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:3103 msgid "Copying %s" msgstr "" -#: /home/kovid/work/calibre/src/calibre/library/database2.py:3121 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:3120 msgid "Compacting database" msgstr ""