mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Make the metadata download plugins customizable
This commit is contained in:
parent
87c7c91ab2
commit
69fc173ff5
@ -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():
|
||||||
|
@ -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):
|
||||||
|
|
||||||
|
@ -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|?
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
@ -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
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -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',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user