diff --git a/src/calibre/customize/ui.py b/src/calibre/customize/ui.py index 11810b1644..967c5ab47a 100644 --- a/src/calibre/customize/ui.py +++ b/src/calibre/customize/ui.py @@ -101,8 +101,6 @@ def metadata_sources(metadata_type='basic', customize=True, isbndb_key=None): plugin.site_customization = customization.get(plugin.name, None) if plugin.name == 'IsbnDB' and isbndb_key is not None: plugin.site_customization = isbndb_key - if not plugin.is_ok(): - continue yield plugin def get_isbndb_key(): diff --git a/src/calibre/ebooks/metadata/fetch.py b/src/calibre/ebooks/metadata/fetch.py index c56926b78c..f308ddb043 100644 --- a/src/calibre/ebooks/metadata/fetch.py +++ b/src/calibre/ebooks/metadata/fetch.py @@ -9,9 +9,11 @@ from threading import Thread from calibre import prints from calibre.utils.config import OptionParser from calibre.utils.logging import default_log - +from calibre.ebooks.metadata import MetaInformation from calibre.customize import Plugin +metadata_config = None + class MetadataSource(Plugin): author = 'Kovid Goyal' @@ -23,11 +25,17 @@ class MetadataSource(Plugin): #: tags/rating/reviews/etc. metadata_type = 'basic' + #: If not None, the customization dialog will allow for string + #: based customization as well the default customization. The + #: string customization will be saved in the site_customization + #: member. + string_customization_help = None + type = _('Metadata download') def __call__(self, title, author, publisher, isbn, verbose, log=None, extra=None): - self.worker = Thread(target=self.fetch) + self.worker = Thread(target=self._fetch) self.worker.daemon = True self.title = title self.verbose = verbose @@ -39,23 +47,87 @@ class MetadataSource(Plugin): self.exception, self.tb, self.results = None, None, [] self.worker.start() + def _fetch(self): + try: + self.fetch() + if self.results: + c = self.config_store().get(self.name, {}) + res = self.results + if isinstance(res, MetaInformation): + res = [res] + for mi in res: + if not c.get('rating', True): + mi.rating = None + if not c.get('comments', True): + mi.comments = None + if not c.get('tags', True): + mi.tags = [] + + except Exception, e: + self.exception = e + self.tb = traceback.format_exc() + def fetch(self): ''' All the actual work is done here. ''' raise NotImplementedError - def is_ok(self): - ''' - Used to check if the plugin has been correctly customized. - For example: The isbndb plugin checks to see if the site_customization - has been set with an isbndb.com access key. - ''' - return True - def join(self): return self.worker.join() + def is_customizable(self): + return True + + def config_store(self): + global metadata_config + if metadata_config is None: + from calibre.utils.config import XMLConfig + metadata_config = XMLConfig('plugins/metadata_download') + return metadata_config + + def config_widget(self): + from PyQt4.Qt import QWidget, QVBoxLayout, QLabel, Qt, QLineEdit, \ + QCheckBox + from calibre.customize.ui import config + w = QWidget() + w._layout = QVBoxLayout(w) + w.setLayout(w._layout) + if self.string_customization_help is not None: + w._sc_label = QLabel(self.string_customization_help, w) + w._layout.addWidget(w._sc_label) + customization = config['plugin_customization'] + def_sc = customization.get(self.name, '') + if not def_sc: + def_sc = '' + w._sc = QLineEdit(def_sc, w) + w._layout.addWidget(w._sc) + w._sc_label.setWordWrap(True) + w._sc_label.setTextInteractionFlags(Qt.LinksAccessibleByMouse + | Qt.LinksAccessibleByKeyboard) + w._sc_label.setOpenExternalLinks(True) + c = self.config_store() + c = c.get(self.name, {}) + for x, l in {'rating':_('ratings'), 'tags':_('tags'), + 'comments':_('description/reviews')}.items(): + cb = QCheckBox(_('Download %s from %s')%(l, + self.name)) + setattr(w, '_'+x, cb) + cb.setChecked(c.get(x, True)) + w._layout.addWidget(cb) + return w + + def save_settings(self, w): + dl_settings = {} + for x in ('rating', 'tags', 'comments'): + dl_settings[x] = getattr(w, '_'+x).isChecked() + c = self.config_store() + c.set(self.name, dl_settings) + if hasattr(w, '_sc'): + sc = unicode(w._sc.text()).strip() + from calibre.customize.ui import customize_plugin + customize_plugin(self, sc) + class GoogleBooks(MetadataSource): @@ -102,14 +174,11 @@ class ISBNDB(MetadataSource): self.exception = e self.tb = traceback.format_exc() - def customization_help(self, gui=False): + @property + def string_customization_help(self): ans = _('To use isbndb.com you must sign up for a %sfree account%s ' 'and enter your access key below.') - if gui: - ans = '

'+ans%('', '') - else: - ans = ans.replace('%s', '') - return ans + return '

'+ans%('', '') class Amazon(MetadataSource): diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst index 13b139bd05..ae32d9dbd6 100644 --- a/src/calibre/manual/faq.rst +++ b/src/calibre/manual/faq.rst @@ -81,7 +81,7 @@ Device Integration What devices does |app| support? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -At the moment |app| has full support for the SONY PRS 300/500/505/600/700, Cybook Gen 3/Opus, Amazon Kindle 1/2/DX, Netronix EB600, Ectaco Jetbook, BeBook/BeBook Mini, Irex Illiad/DR1000, Foxit eSlick, Android phones and the iPhone. In addition, using the :guilabel:`Save to disk` function you can use it with any ebook reader that exports itself as a USB disk. +At the moment |app| has full support for the SONY PRS 300/500/505/600/700, Cybook Gen 3/Opus, Amazon Kindle 1/2/DX, Netronix EB600, Ectaco Jetbook, BeBook/BeBook Mini, Irex Illiad/DR1000, Foxit eSlick, PocketBook 360, Android phones and the iPhone. In addition, using the :guilabel:`Save to disk` function you can use it with any ebook reader that exports itself as a USB disk. How can I help get my device supported in |app|? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/calibre/manual/plugins.rst b/src/calibre/manual/plugins.rst index 1969a70a4b..ccf85c40ca 100644 --- a/src/calibre/manual/plugins.rst +++ b/src/calibre/manual/plugins.rst @@ -108,7 +108,7 @@ Metadata download plugins .. class:: calibre.ebooks.metadata.fetch.MetadataSource Represents a source to query for metadata. Subclasses must implement - at least the fetch method and optionally the is_ok method. + at least the fetch method. When :meth:`fetch` is called, the `self` object will have the following useful attributes (each of which may be None):: @@ -124,8 +124,9 @@ Metadata download plugins .. automember:: calibre.ebooks.metadata.fetch.MetadataSource.metadata_type +.. automember:: calibre.ebooks.metadata.fetch.MetadataSource.string_customization_help + .. automethod:: calibre.ebooks.metadata.fetch.MetadataSource.fetch -.. automethod:: calibre.ebooks.metadata.fetch.MetadataSource.is_ok diff --git a/src/calibre/utils/config.py b/src/calibre/utils/config.py index c9424717f0..cf829621e3 100644 --- a/src/calibre/utils/config.py +++ b/src/calibre/utils/config.py @@ -6,7 +6,7 @@ __docformat__ = 'restructuredtext en' ''' Manage application-wide preferences. ''' -import os, re, cPickle, textwrap, traceback +import os, re, cPickle, textwrap, traceback, plistlib from copy import deepcopy from functools import partial from optparse import OptionParser as _OptionParser @@ -34,9 +34,11 @@ else: plugin_dir = os.path.join(config_dir, 'plugins') +CONFIG_DIR_MODE = 0700 + def make_config_dir(): if not os.path.exists(plugin_dir): - os.makedirs(plugin_dir, mode=448) # 0700 == 448 + os.makedirs(plugin_dir, mode=CONFIG_DIR_MODE) def check_config_write_access(): return os.access(config_dir, os.W_OK) and os.access(config_dir, os.X_OK) @@ -552,6 +554,72 @@ class DynamicConfig(dict): dynamic = DynamicConfig() +class XMLConfig(dict): + + ''' + Similar to :class:`DynamicConfig`, except that it uses an XML storage + backend instead of a pickle file. + + See `http://docs.python.org/dev/library/plistlib.html`_ for the supported + data types. + ''' + + def __init__(self, rel_path_to_cf_file): + dict.__init__(self) + self.file_path = os.path.join(config_dir, + *(rel_path_to_cf_file.split('/'))) + self.file_path = os.path.abspath(self.file_path) + if not self.file_path.endswith('.plist'): + self.file_path += '.plist' + + self.refresh() + + def refresh(self): + d = {} + if os.path.exists(self.file_path): + with ExclusiveFile(self.file_path) as f: + raw = f.read() + try: + d = plistlib.readPlistFromString(raw) if raw.strip() else {} + except SystemError: + pass + except: + import traceback + traceback.print_exc() + d = {} + self.clear() + self.update(d) + + def __getitem__(self, key): + try: + ans = dict.__getitem__(self, key) + if isinstance(ans, plistlib.Data): + ans = ans.data + return ans + except KeyError: + return None + + def __setitem__(self, key, val): + if isinstance(val, (bytes, str)): + val = plistlib.Data(val) + dict.__setitem__(self, key, val) + self.commit() + + def set(self, key, val): + self.__setitem__(key, val) + + def commit(self): + if hasattr(self, 'file_path') and self.file_path: + dpath = os.path.dirname(self.file_path) + if not os.path.exists(dpath): + os.makedirs(dpath, mode=CONFIG_DIR_MODE) + with ExclusiveFile(self.file_path) as f: + raw = plistlib.writePlistToString(self) + f.seek(0) + f.truncate() + f.write(raw) + + def _prefs(): c = Config('global', 'calibre wide preferences') c.add_opt('database_path',