mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
IGN:Framework to support 3rd party plugins
This commit is contained in:
parent
30e98dc705
commit
61e0f3db20
69
src/calibre/customize/__init__.py
Normal file
69
src/calibre/customize/__init__.py
Normal file
@ -0,0 +1,69 @@
|
||||
from __future__ import with_statement
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
class Plugin(object):
|
||||
|
||||
#: List of platforms this plugin works on
|
||||
#: For example: ``['windows', 'osx', 'linux']
|
||||
supported_platforms = []
|
||||
|
||||
#: The name of this plugin
|
||||
name = 'Trivial Plugin'
|
||||
|
||||
#: The version of this plugin as a 3-tuple (major, minor, revision)
|
||||
version = (1, 0, 0)
|
||||
|
||||
#: A short string describing what this plugin does
|
||||
description = _('Does absolutely nothing')
|
||||
|
||||
#: The author of this plugin
|
||||
author = _('Unknown')
|
||||
|
||||
#: When more than one plugin exists for a filetype,
|
||||
#: the plugins are run in order of decreasing priority
|
||||
#: i.e. plugins with higher priority will be run first.
|
||||
#: The highest possible priority is ``sys.maxint``.
|
||||
#: Default pririty is 1.
|
||||
priority = 1
|
||||
|
||||
#: The earliest version of calibre this plugin requires
|
||||
minimum_calibre_version = (0, 4, 118)
|
||||
|
||||
def __init__(self, plugin_path):
|
||||
'''
|
||||
Called once when calibre plugins are initialized. Plugins are re-initialized
|
||||
every time a new plugin is added.
|
||||
|
||||
:param plugin_path: Path to the zip file this plugin is distributed in.
|
||||
'''
|
||||
self.plugin_path = plugin_path
|
||||
|
||||
def customization_help(self):
|
||||
'''
|
||||
Return a string giving help on how to customize this plugin.
|
||||
By default raise a :class:`NotImplementedError`, which indicates that
|
||||
the plugin does not require customization.
|
||||
'''
|
||||
raise NotImplementedError
|
||||
|
||||
def run(self, path_to_ebook, site_customization=''):
|
||||
'''
|
||||
Run the plugin. Must be implemented in subclasses.
|
||||
It should perform whatever modifications are required
|
||||
on the ebook and return the absolute path to the
|
||||
modified ebook. If no modifications are needed, it should
|
||||
return the path to the original ebook. If an error is encountered
|
||||
it should raise an Exception. The default implementation
|
||||
simply return the path to the original ebook.
|
||||
|
||||
:param path_to_ebook: Absolute path to the ebook
|
||||
:param site_customization: A (possibly empty) string that the user
|
||||
has specified to customize this plugin.
|
||||
For example, it could be the path to a needed
|
||||
executable on her system.
|
||||
|
||||
:return: Absolute path to the modified ebook.
|
||||
'''
|
||||
# Default implementation does nothing
|
||||
return path_to_ebook
|
28
src/calibre/customize/filetype.py
Normal file
28
src/calibre/customize/filetype.py
Normal file
@ -0,0 +1,28 @@
|
||||
from __future__ import with_statement
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
from calibre.customize import Plugin as PluginBase
|
||||
|
||||
class Plugin(PluginBase):
|
||||
'''
|
||||
A plugin that is associated with a particular set of file types.
|
||||
'''
|
||||
|
||||
#: List of file types for which this plugin should be run
|
||||
#: For example: ``['lit', 'mobi', 'prc']``
|
||||
file_types = []
|
||||
|
||||
#: If True, this plugin is run when books are added
|
||||
#: to the database
|
||||
on_import = False
|
||||
|
||||
#: If True, this plugin is run whenever an any2* tool
|
||||
#: is used, on the file passed to the any2* tool.
|
||||
on_preprocess = False
|
||||
|
||||
#: If True, this plugin is run after an any2* tool is
|
||||
#: used, on the final file produced by the tool.
|
||||
on_postprocess = False
|
||||
|
||||
|
228
src/calibre/customize/ui.py
Normal file
228
src/calibre/customize/ui.py
Normal file
@ -0,0 +1,228 @@
|
||||
from __future__ import with_statement
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
import os, shutil, traceback, functools, sys
|
||||
|
||||
from calibre.customize import Plugin
|
||||
from calibre.customize.filetype import Plugin as FileTypePlugin
|
||||
from calibre.constants import __version__, iswindows, isosx
|
||||
from calibre.utils.config import make_config_dir, Config, ConfigProxy, \
|
||||
plugin_dir, OptionParser
|
||||
|
||||
|
||||
version = tuple([int(x) for x in __version__.split('.')])
|
||||
|
||||
platform = 'linux'
|
||||
if iswindows:
|
||||
platform = 'windows'
|
||||
if isosx:
|
||||
platform = 'osx'
|
||||
|
||||
from zipfile import ZipFile
|
||||
|
||||
def _config():
|
||||
c = Config('customize')
|
||||
c.add_opt('plugins', default={}, help=_('Installed plugins'))
|
||||
c.add_opt('filetype_mapping', default={}, help=_('Maping for filetype plugins'))
|
||||
c.add_opt('plugin_customization', default={}, help=_('Local plugin customization'))
|
||||
c.add_opt('disabled_plugins', default=set([]), help=_('Disabled plugins'))
|
||||
|
||||
return ConfigProxy(c)
|
||||
|
||||
config = _config()
|
||||
|
||||
|
||||
class InvalidPlugin(ValueError):
|
||||
pass
|
||||
|
||||
def load_plugin(path_to_zip_file):
|
||||
'''
|
||||
Load plugin from zip file or raise InvalidPlugin error
|
||||
|
||||
:return: A :class:`Plugin` instance.
|
||||
'''
|
||||
print 'Loading plugin from', path_to_zip_file
|
||||
zf = ZipFile(path_to_zip_file)
|
||||
for name in zf.namelist():
|
||||
if name.lower().endswith('plugin.py'):
|
||||
locals = {}
|
||||
exec zf.read(name) in locals
|
||||
for x in locals.values():
|
||||
if isinstance(x, type) and issubclass(x, Plugin):
|
||||
if x.minimum_calibre_version > version:
|
||||
raise InvalidPlugin(_('%s needs calibre version at least %s')%
|
||||
(x.name, x.minimum_calibre_version))
|
||||
if platform not in x.supported_platforms:
|
||||
raise InvalidPlugin(_('%s is not supported on %s')%
|
||||
(x.name, platform))
|
||||
|
||||
return x
|
||||
raise InvalidPlugin(_('No valid plugin found in ')+path_to_zip_file)
|
||||
|
||||
_initialized_plugins = []
|
||||
_on_import = {}
|
||||
_on_preprocess = {}
|
||||
_on_postprocess = {}
|
||||
|
||||
|
||||
|
||||
def reread_filetype_plugins():
|
||||
global _on_import
|
||||
global _on_preprocess
|
||||
global _on_postprocess
|
||||
_on_import = {}
|
||||
_on_preprocess = {}
|
||||
_on_postprocess = {}
|
||||
|
||||
for plugin in _initialized_plugins:
|
||||
if isinstance(plugin, FileTypePlugin):
|
||||
for ft in plugin.file_types:
|
||||
if plugin.on_import:
|
||||
if not _on_import.has_key(ft):
|
||||
_on_import[ft] = []
|
||||
_on_import[ft].append(plugin)
|
||||
if plugin.on_preprocess:
|
||||
if not _on_preprocess.has_key(ft):
|
||||
_on_preprocess[ft] = []
|
||||
_on_preprocess[ft].append(plugin)
|
||||
if plugin.on_postprocess:
|
||||
if not _on_postprocess.has_key(ft):
|
||||
_on_postprocess[ft] = []
|
||||
_on_postprocess[ft].append(plugin)
|
||||
|
||||
|
||||
def _run_filetype_plugins(path_to_file, ft, occasion='preprocess'):
|
||||
occasion = {'import':_on_import, 'preprocess':_on_preprocess,
|
||||
'postprocess':_on_postprocess}[occasion]
|
||||
customization = config['plugin_customization']
|
||||
nfp = path_to_file
|
||||
for plugin in occasion.get(ft, []):
|
||||
if is_disabled(plugin):
|
||||
continue
|
||||
sc = customization.get(plugin.name, '')
|
||||
try:
|
||||
nfp = plugin.run(path_to_file, sc)
|
||||
except:
|
||||
print 'Running file type plugin %s failed with traceback:'%plugin.name
|
||||
traceback.print_exc()
|
||||
return nfp
|
||||
|
||||
run_plugins_on_import = functools.partial(_run_filetype_plugins,
|
||||
occasion='import')
|
||||
run_plugins_on_preprocess = functools.partial(_run_filetype_plugins,
|
||||
occasion='preprocess')
|
||||
run_plugins_on_postprocess = functools.partial(_run_filetype_plugins,
|
||||
occasion='postprocess')
|
||||
|
||||
|
||||
def initialize_plugin(plugin, path_to_zip_file):
|
||||
print 'Initializing plugin', plugin.name
|
||||
try:
|
||||
plugin(path_to_zip_file)
|
||||
except Exception:
|
||||
tb = traceback.format_exc()
|
||||
raise InvalidPlugin((_('Initialization of plugin %s failed with traceback:')
|
||||
%tb) + '\n'+tb)
|
||||
|
||||
|
||||
def add_plugin(path_to_zip_file):
|
||||
make_config_dir()
|
||||
plugin = load_plugin(path_to_zip_file)
|
||||
initialize_plugin(plugin, path_to_zip_file)
|
||||
plugins = config['plugins']
|
||||
zfp = os.path.join(plugin_dir, 'name.zip')
|
||||
if os.path.exists(zfp):
|
||||
os.remove(zfp)
|
||||
shutil.copyfile(path_to_zip_file, zfp)
|
||||
plugins[plugin.name] = zfp
|
||||
config['plugins'] = plugins
|
||||
initialize_plugins()
|
||||
return plugin
|
||||
|
||||
def is_disabled(plugin):
|
||||
return plugin.name in config['disabled_plugins']
|
||||
|
||||
def find_plugin(name):
|
||||
for plugin in _initialized_plugins:
|
||||
if plugin.name == name:
|
||||
return plugin
|
||||
|
||||
def disable_plugin(plugin_or_name):
|
||||
x = getattr(plugin_or_name, 'name', plugin_or_name)
|
||||
dp = config['disabled_plugins']
|
||||
dp.add(x)
|
||||
config['disabled_plugins'] = dp
|
||||
|
||||
def enable_plugin(plugin_or_name):
|
||||
x = getattr(plugin_or_name, 'name', plugin_or_name)
|
||||
dp = config['disabled_plugins']
|
||||
if x in dp:
|
||||
dp.remove(x)
|
||||
config['disabled_plugins'] = dp
|
||||
|
||||
def initialize_plugins():
|
||||
global _initialized_plugins
|
||||
_initialized_plugins = []
|
||||
for zfp in config['plugins'].values():
|
||||
try:
|
||||
plugin = load_plugin(zfp)
|
||||
initialize_plugin(plugin, zfp)
|
||||
_initialized_plugins.append(plugin)
|
||||
except:
|
||||
print 'Failed to initialize plugin...'
|
||||
traceback.print_exc()
|
||||
_initialized_plugins.sort(cmp=lambda x,y:cmp(x.priority, y.priority), reverse=True)
|
||||
reread_filetype_plugins()
|
||||
|
||||
initialize_plugins()
|
||||
|
||||
def option_parser():
|
||||
parser = OptionParser(usage=_('''\
|
||||
%prog options
|
||||
|
||||
Customize calibre by loading external plugins.
|
||||
'''))
|
||||
parser.add_option('-a', '--add-plugin', default=None,
|
||||
help=_('Add a plugin by specifying the path to the zip file containing it.'))
|
||||
parser.add_option('--customize-plugin', default=None,
|
||||
help=_('Customize plugin. Specify name of plugin and customization string separated by a comma.'))
|
||||
parser.add_option('-l', '--list-plugins', default=False, action='store_true',
|
||||
help=_('List all installed plugins'))
|
||||
parser.add_option('--enable-plugin', default=None,
|
||||
help=_('Enable the named plugin'))
|
||||
parser.add_option('--disable-plugin', default=None,
|
||||
help=_('Disable the named plugin'))
|
||||
return parser
|
||||
|
||||
def main(args=sys.argv):
|
||||
parser = option_parser()
|
||||
if len(args) < 2:
|
||||
parser.print_help()
|
||||
return 1
|
||||
opts, args = parser.parse_args(args)
|
||||
if opts.add_plugin is not None:
|
||||
plugin = add_plugin(opts.add_plugin)
|
||||
print 'Plugin added:', plugin.name, plugin.version
|
||||
if opts.customize_plugin is not None:
|
||||
name, custom = opts.customize_plugin.split(',')
|
||||
plugin = find_plugin(name.strip())
|
||||
if plugin is None:
|
||||
print 'No plugin with the name %s exists'%name
|
||||
return 1
|
||||
config['plugin_customization'][plugin.name] = custom.strip()
|
||||
if opts.enable_plugin is not None:
|
||||
enable_plugin(opts.enable_plugin.strip())
|
||||
if opts.disable_plugin is not None:
|
||||
disable_plugin(opts.disable_plugin.strip())
|
||||
if opts.list_plugins:
|
||||
print 'Name\tVersion\tDisabled\tLocal Customization'
|
||||
for plugin in _initialized_plugins:
|
||||
print '%s\t%s\t%s\t%s'%(plugin.name, plugin.version, is_disabled(plugin),
|
||||
config['plugin_customization'].get(plugin.name))
|
||||
print '\t', plugin.customization_help()
|
||||
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
@ -21,6 +21,8 @@ Run an embedded python interpreter.
|
||||
'Module specifications are of the form full.name.of.module,path_to_module.py', default=None
|
||||
)
|
||||
parser.add_option('-c', '--command', help='Run python code.', default=None)
|
||||
parser.add_option('-g', '--run-gui', help='Run the GUI', default=False,
|
||||
action='store_true')
|
||||
parser.add_option('--migrate', action='store_true', default=False,
|
||||
help='Migrate old database. Needs two arguments. Path to library1.db and path to new library folder.')
|
||||
return parser
|
||||
@ -72,7 +74,10 @@ def migrate(old, new):
|
||||
|
||||
def main(args=sys.argv):
|
||||
opts, args = option_parser().parse_args(args)
|
||||
if opts.update_module:
|
||||
if opts.run_gui:
|
||||
from calibre.gui2.main import main
|
||||
main()
|
||||
elif opts.update_module:
|
||||
mod, path = opts.update_module.partition(',')[0], opts.update_module.partition(',')[-1]
|
||||
update_module(mod, os.path.expanduser(path))
|
||||
elif opts.command:
|
||||
|
@ -62,7 +62,8 @@ entry_points = {
|
||||
'calibre-debug = calibre.debug:main',
|
||||
'calibredb = calibre.library.cli:main',
|
||||
'calibre-fontconfig = calibre.utils.fontconfig:main',
|
||||
'calibre-parallel = calibre.parallel:main',
|
||||
'calibre-parallel = calibre.parallel:main',
|
||||
'calibre-customize = calibre.customize.ui:main',
|
||||
],
|
||||
'gui_scripts' : [
|
||||
__appname__+' = calibre.gui2.main:main',
|
||||
|
@ -28,9 +28,11 @@ else:
|
||||
bdir = os.path.abspath(os.path.expanduser(os.environ.get('XDG_CONFIG_HOME', '~/.config')))
|
||||
config_dir = os.path.join(bdir, 'calibre')
|
||||
|
||||
plugin_dir = os.path.join(config_dir, 'plugins')
|
||||
|
||||
def make_config_dir():
|
||||
if not os.path.exists(config_dir):
|
||||
os.makedirs(config_dir, mode=448) # 0700 == 448
|
||||
if not os.path.exists(plugin_dir):
|
||||
os.makedirs(plugin_dir, mode=448) # 0700 == 448
|
||||
|
||||
|
||||
class CustomHelpFormatter(IndentedHelpFormatter):
|
||||
@ -78,6 +80,7 @@ class OptionParser(_OptionParser):
|
||||
gui_mode=False,
|
||||
conflict_handler='resolve',
|
||||
**kwds):
|
||||
usage = textwrap.dedent(usage)
|
||||
usage += '''\n\nWhenever you pass arguments to %prog that have spaces in them, '''\
|
||||
'''enclose the arguments in quotation marks.'''
|
||||
_OptionParser.__init__(self, usage=usage, version=version, epilog=epilog,
|
||||
|
Loading…
x
Reference in New Issue
Block a user