Make the metadata download plugins customizable

This commit is contained in:
Kovid Goyal 2009-11-21 09:07:12 -07:00
parent 87c7c91ab2
commit 69fc173ff5
5 changed files with 159 additions and 23 deletions

View File

@ -101,8 +101,6 @@ def metadata_sources(metadata_type='basic', customize=True, isbndb_key=None):
plugin.site_customization = customization.get(plugin.name, None) plugin.site_customization = customization.get(plugin.name, None)
if plugin.name == 'IsbnDB' and isbndb_key is not None: if plugin.name == 'IsbnDB' and isbndb_key is not None:
plugin.site_customization = isbndb_key plugin.site_customization = isbndb_key
if not plugin.is_ok():
continue
yield plugin yield plugin
def get_isbndb_key(): def get_isbndb_key():

View File

@ -9,9 +9,11 @@ from threading import Thread
from calibre import prints from calibre import prints
from calibre.utils.config import OptionParser from calibre.utils.config import OptionParser
from calibre.utils.logging import default_log from calibre.utils.logging import default_log
from calibre.ebooks.metadata import MetaInformation
from calibre.customize import Plugin from calibre.customize import Plugin
metadata_config = None
class MetadataSource(Plugin): class MetadataSource(Plugin):
author = 'Kovid Goyal' author = 'Kovid Goyal'
@ -23,11 +25,17 @@ class MetadataSource(Plugin):
#: tags/rating/reviews/etc. #: tags/rating/reviews/etc.
metadata_type = 'basic' 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') type = _('Metadata download')
def __call__(self, title, author, publisher, isbn, verbose, log=None, def __call__(self, title, author, publisher, isbn, verbose, log=None,
extra=None): extra=None):
self.worker = Thread(target=self.fetch) self.worker = Thread(target=self._fetch)
self.worker.daemon = True self.worker.daemon = True
self.title = title self.title = title
self.verbose = verbose self.verbose = verbose
@ -39,23 +47,87 @@ class MetadataSource(Plugin):
self.exception, self.tb, self.results = None, None, [] self.exception, self.tb, self.results = None, None, []
self.worker.start() 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): def fetch(self):
''' '''
All the actual work is done here. All the actual work is done here.
''' '''
raise NotImplementedError 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): def join(self):
return self.worker.join() 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): class GoogleBooks(MetadataSource):
@ -102,14 +174,11 @@ class ISBNDB(MetadataSource):
self.exception = e self.exception = e
self.tb = traceback.format_exc() 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 ' ans = _('To use isbndb.com you must sign up for a %sfree account%s '
'and enter your access key below.') 'and enter your access key below.')
if gui: return '<p>'+ans%('<a href="http://www.isbndb.com">', '</a>')
ans = '<p>'+ans%('<a href="http://www.isbndb.com">', '</a>')
else:
ans = ans.replace('%s', '')
return ans
class Amazon(MetadataSource): class Amazon(MetadataSource):

View File

@ -81,7 +81,7 @@ Device Integration
What devices does |app| support? 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|? How can I help get my device supported in |app|?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -108,7 +108,7 @@ Metadata download plugins
.. class:: calibre.ebooks.metadata.fetch.MetadataSource .. class:: calibre.ebooks.metadata.fetch.MetadataSource
Represents a source to query for metadata. Subclasses must implement 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 When :meth:`fetch` is called, the `self` object will have the following
useful attributes (each of which may be None):: 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.metadata_type
.. automember:: calibre.ebooks.metadata.fetch.MetadataSource.string_customization_help
.. automethod:: calibre.ebooks.metadata.fetch.MetadataSource.fetch .. automethod:: calibre.ebooks.metadata.fetch.MetadataSource.fetch
.. automethod:: calibre.ebooks.metadata.fetch.MetadataSource.is_ok

View File

@ -6,7 +6,7 @@ __docformat__ = 'restructuredtext en'
''' '''
Manage application-wide preferences. Manage application-wide preferences.
''' '''
import os, re, cPickle, textwrap, traceback import os, re, cPickle, textwrap, traceback, plistlib
from copy import deepcopy from copy import deepcopy
from functools import partial from functools import partial
from optparse import OptionParser as _OptionParser from optparse import OptionParser as _OptionParser
@ -34,9 +34,11 @@ else:
plugin_dir = os.path.join(config_dir, 'plugins') plugin_dir = os.path.join(config_dir, 'plugins')
CONFIG_DIR_MODE = 0700
def make_config_dir(): def make_config_dir():
if not os.path.exists(plugin_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(): def check_config_write_access():
return os.access(config_dir, os.W_OK) and os.access(config_dir, os.X_OK) 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() 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(): def _prefs():
c = Config('global', 'calibre wide preferences') c = Config('global', 'calibre wide preferences')
c.add_opt('database_path', c.add_opt('database_path',