mirror of
https://github.com/gryf/ebook-converter.git
synced 2026-01-01 07:22:26 +01:00
529 lines
16 KiB
Python
529 lines
16 KiB
Python
import collections
|
|
import functools
|
|
import itertools
|
|
import os
|
|
import shutil
|
|
import sys
|
|
import traceback
|
|
|
|
from ebook_converter import customize
|
|
from ebook_converter.customize import conversion
|
|
from ebook_converter.customize import profiles
|
|
from ebook_converter.customize import builtins
|
|
from ebook_converter.ebooks import metadata
|
|
from ebook_converter.utils import config as cfg
|
|
|
|
|
|
builtin_names = frozenset(p.name for p in builtins.plugins)
|
|
BLACKLISTED_PLUGINS = frozenset({'Marvin XD', 'iOS reader applications'})
|
|
|
|
|
|
class NameConflict(ValueError):
|
|
pass
|
|
|
|
|
|
def _config():
|
|
c = cfg.Config('customize')
|
|
c.add_opt('plugins', default={}, help='Installed plugins')
|
|
c.add_opt('filetype_mapping', default={},
|
|
help='Mapping for filetype plugins')
|
|
c.add_opt('plugin_customization', default={},
|
|
help='Local plugin customization')
|
|
c.add_opt('disabled_plugins', default=set(), help='Disabled plugins')
|
|
c.add_opt('enabled_plugins', default=set(), help='Enabled plugins')
|
|
|
|
return cfg.ConfigProxy(c)
|
|
|
|
|
|
config = _config()
|
|
|
|
|
|
# File type plugins
|
|
_on_import = {}
|
|
_on_postimport = {}
|
|
_on_preprocess = {}
|
|
_on_postprocess = {}
|
|
_on_postadd = []
|
|
|
|
|
|
def reread_filetype_plugins():
|
|
global _on_import, _on_postimport, _on_preprocess, _on_postprocess
|
|
global _on_postadd
|
|
_on_import = collections.defaultdict(list)
|
|
_on_postimport = collections.defaultdict(list)
|
|
_on_preprocess = collections.defaultdict(list)
|
|
_on_postprocess = collections.defaultdict(list)
|
|
_on_postadd = []
|
|
|
|
for plugin in _initialized_plugins:
|
|
if isinstance(plugin, customize.FileTypePlugin):
|
|
for ft in plugin.file_types:
|
|
if plugin.on_import:
|
|
_on_import[ft].append(plugin)
|
|
if plugin.on_postimport:
|
|
_on_postimport[ft].append(plugin)
|
|
_on_postadd.append(plugin)
|
|
if plugin.on_preprocess:
|
|
_on_preprocess[ft].append(plugin)
|
|
if plugin.on_postprocess:
|
|
_on_postprocess[ft].append(plugin)
|
|
|
|
|
|
def plugins_for_ft(ft, occasion):
|
|
op = {'import': _on_import,
|
|
'preprocess': _on_preprocess,
|
|
'postprocess': _on_postprocess,
|
|
'postimport': _on_postimport}[occasion]
|
|
for p in itertools.chain(op.get(ft, ()), op.get('*', ())):
|
|
yield p
|
|
|
|
|
|
def _run_filetype_plugins(path_to_file, ft=None, occasion='preprocess'):
|
|
customization = config['plugin_customization']
|
|
if ft is None:
|
|
ft = os.path.splitext(path_to_file)[-1].lower().replace('.', '')
|
|
nfp = path_to_file
|
|
for plugin in plugins_for_ft(ft, occasion):
|
|
plugin.site_customization = customization.get(plugin.name, '')
|
|
# Some file type plugins out there override the output streams with
|
|
# buggy implementations
|
|
oo, oe = sys.stdout, sys.stderr
|
|
with plugin:
|
|
try:
|
|
plugin.original_path_to_file = path_to_file
|
|
except Exception:
|
|
pass
|
|
try:
|
|
nfp = plugin.run(nfp) or nfp
|
|
except:
|
|
print('Running file type plugin %s failed with traceback:' %
|
|
plugin.name, file=oe)
|
|
traceback.print_exc(file=oe)
|
|
sys.stdout, sys.stderr = oo, oe
|
|
x = lambda j: os.path.normpath(os.path.normcase(j))
|
|
if occasion == 'postprocess' and x(nfp) != x(path_to_file):
|
|
shutil.copyfile(nfp, path_to_file)
|
|
nfp = path_to_file
|
|
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 run_plugins_on_postimport(db, book_id, fmt):
|
|
customization = config['plugin_customization']
|
|
fmt = fmt.lower()
|
|
for plugin in plugins_for_ft(fmt, 'postimport'):
|
|
plugin.site_customization = customization.get(plugin.name, '')
|
|
with plugin:
|
|
try:
|
|
plugin.postimport(book_id, fmt, db)
|
|
except Exception:
|
|
print('Running file type plugin %s failed with traceback:' %
|
|
plugin.name)
|
|
traceback.print_exc()
|
|
|
|
|
|
def run_plugins_on_postadd(db, book_id, fmt_map):
|
|
customization = config['plugin_customization']
|
|
for plugin in _on_postadd:
|
|
plugin.site_customization = customization.get(plugin.name, '')
|
|
with plugin:
|
|
try:
|
|
plugin.postadd(book_id, fmt_map, db)
|
|
except Exception:
|
|
print('Running file type plugin %s failed with traceback:' %
|
|
plugin.name)
|
|
traceback.print_exc()
|
|
|
|
|
|
# Plugin customization
|
|
def customize_plugin(plugin, custom):
|
|
d = config['plugin_customization']
|
|
d[plugin.name] = custom.strip()
|
|
config['plugin_customization'] = d
|
|
|
|
|
|
def plugin_customization(plugin):
|
|
return config['plugin_customization'].get(plugin.name, '')
|
|
|
|
|
|
# Input/Output profiles
|
|
def input_profiles():
|
|
for plugin in _initialized_plugins:
|
|
if isinstance(plugin, profiles.InputProfile):
|
|
yield plugin
|
|
|
|
|
|
def output_profiles():
|
|
for plugin in _initialized_plugins:
|
|
if isinstance(plugin, profiles.OutputProfile):
|
|
yield plugin
|
|
|
|
|
|
# Interface Actions #
|
|
def interface_actions():
|
|
for plugin in _initialized_plugins:
|
|
if isinstance(plugin, customize.InterfaceActionBase):
|
|
yield plugin
|
|
|
|
|
|
# Preferences Plugins
|
|
def preferences_plugins():
|
|
customization = config['plugin_customization']
|
|
for plugin in _initialized_plugins:
|
|
if isinstance(plugin, customize.PreferencesPlugin):
|
|
plugin.site_customization = customization.get(plugin.name, '')
|
|
yield plugin
|
|
|
|
|
|
# Library Closed Plugins
|
|
def available_library_closed_plugins():
|
|
customization = config['plugin_customization']
|
|
for plugin in _initialized_plugins:
|
|
if isinstance(plugin, customize.LibraryClosedPlugin):
|
|
plugin.site_customization = customization.get(plugin.name, '')
|
|
yield plugin
|
|
|
|
|
|
def has_library_closed_plugins():
|
|
for plugin in _initialized_plugins:
|
|
if isinstance(plugin, customize.LibraryClosedPlugin):
|
|
return True
|
|
return False
|
|
|
|
|
|
# Store Plugins
|
|
def store_plugins():
|
|
customization = config['plugin_customization']
|
|
for plugin in _initialized_plugins:
|
|
if isinstance(plugin, customize.StoreBase):
|
|
plugin.site_customization = customization.get(plugin.name, '')
|
|
yield plugin
|
|
|
|
|
|
def available_store_plugins():
|
|
for plugin in store_plugins():
|
|
yield plugin
|
|
|
|
|
|
def stores():
|
|
stores = set()
|
|
for plugin in store_plugins():
|
|
stores.add(plugin.name)
|
|
return stores
|
|
|
|
|
|
def available_stores():
|
|
stores = set()
|
|
for plugin in available_store_plugins():
|
|
stores.add(plugin.name)
|
|
return stores
|
|
|
|
|
|
# Metadata read/write
|
|
_metadata_readers = {}
|
|
_metadata_writers = {}
|
|
|
|
|
|
def reread_metadata_plugins():
|
|
global _metadata_readers
|
|
global _metadata_writers
|
|
_metadata_readers = collections.defaultdict(list)
|
|
_metadata_writers = collections.defaultdict(list)
|
|
for plugin in _initialized_plugins:
|
|
if isinstance(plugin, customize.MetadataReaderPlugin):
|
|
for ft in plugin.file_types:
|
|
_metadata_readers[ft].append(plugin)
|
|
elif isinstance(plugin, customize.MetadataWriterPlugin):
|
|
for ft in plugin.file_types:
|
|
_metadata_writers[ft].append(plugin)
|
|
|
|
# Ensure custom metadata plugins are used in preference to builtin
|
|
# ones for a given filetype
|
|
def key(plugin):
|
|
return (1 if plugin.plugin_path is None else 0), plugin.name
|
|
|
|
for group in (_metadata_readers, _metadata_writers):
|
|
for plugins in group.values():
|
|
if len(plugins) > 1:
|
|
plugins.sort(key=key)
|
|
|
|
|
|
def metadata_readers():
|
|
ans = set()
|
|
for plugins in _metadata_readers.values():
|
|
for plugin in plugins:
|
|
ans.add(plugin)
|
|
return ans
|
|
|
|
|
|
def metadata_writers():
|
|
ans = set()
|
|
for plugins in _metadata_writers.values():
|
|
for plugin in plugins:
|
|
ans.add(plugin)
|
|
return ans
|
|
|
|
|
|
class QuickMetadata(object):
|
|
|
|
def __init__(self):
|
|
self.quick = False
|
|
|
|
def __enter__(self):
|
|
self.quick = True
|
|
|
|
def __exit__(self, *args):
|
|
self.quick = False
|
|
|
|
|
|
quick_metadata = QuickMetadata()
|
|
|
|
|
|
class ApplyNullMetadata(object):
|
|
|
|
def __init__(self):
|
|
self.apply_null = False
|
|
|
|
def __enter__(self):
|
|
self.apply_null = True
|
|
|
|
def __exit__(self, *args):
|
|
self.apply_null = False
|
|
|
|
|
|
apply_null_metadata = ApplyNullMetadata()
|
|
|
|
|
|
class ForceIdentifiers(object):
|
|
|
|
def __init__(self):
|
|
self.force_identifiers = False
|
|
|
|
def __enter__(self):
|
|
self.force_identifiers = True
|
|
|
|
def __exit__(self, *args):
|
|
self.force_identifiers = False
|
|
|
|
|
|
def get_file_type_metadata(stream, ftype):
|
|
mi = metadata.MetaInformation(None, None)
|
|
|
|
ftype = ftype.lower().strip()
|
|
if ftype in _metadata_readers:
|
|
for plugin in _metadata_readers[ftype]:
|
|
with plugin:
|
|
try:
|
|
plugin.quick = quick_metadata.quick
|
|
if hasattr(stream, 'seek'):
|
|
stream.seek(0)
|
|
mi = plugin.get_metadata(stream, ftype.lower().strip())
|
|
break
|
|
except Exception:
|
|
traceback.print_exc()
|
|
continue
|
|
return mi
|
|
|
|
|
|
def set_file_type_metadata(stream, mi, ftype, report_error=None):
|
|
fi = ForceIdentifiers()
|
|
ftype = ftype.lower().strip()
|
|
if ftype in _metadata_writers:
|
|
customization = config['plugin_customization']
|
|
for plugin in _metadata_writers[ftype]:
|
|
with plugin:
|
|
try:
|
|
plugin.apply_null = apply_null_metadata.apply_null
|
|
plugin.force_identifiers = fi.force_identifiers
|
|
plugin.site_customization = customization.get(
|
|
plugin.name, '')
|
|
plugin.set_metadata(stream, mi, ftype.lower().strip())
|
|
break
|
|
except Exception:
|
|
if report_error is None:
|
|
from ebook_converter import prints
|
|
prints('Failed to set metadata for the', ftype.upper(),
|
|
'format of:', getattr(mi, 'title', ''),
|
|
file=sys.stderr)
|
|
traceback.print_exc()
|
|
else:
|
|
report_error(mi, ftype, traceback.format_exc())
|
|
|
|
|
|
def can_set_metadata(ftype):
|
|
ftype = ftype.lower().strip()
|
|
for plugin in _metadata_writers.get(ftype, ()):
|
|
return True
|
|
return False
|
|
|
|
|
|
# Input/Output format plugins
|
|
def input_format_plugins():
|
|
for plugin in _initialized_plugins:
|
|
if isinstance(plugin, conversion.InputFormatPlugin):
|
|
yield plugin
|
|
|
|
|
|
def plugin_for_input_format(fmt):
|
|
customization = config['plugin_customization']
|
|
for plugin in input_format_plugins():
|
|
if fmt.lower() in plugin.file_types:
|
|
plugin.site_customization = customization.get(plugin.name, None)
|
|
return plugin
|
|
|
|
|
|
def all_input_formats():
|
|
formats = set()
|
|
for plugin in input_format_plugins():
|
|
for format in plugin.file_types:
|
|
formats.add(format)
|
|
return formats
|
|
|
|
|
|
def available_input_formats():
|
|
formats = set()
|
|
for plugin in input_format_plugins():
|
|
for format in plugin.file_types:
|
|
formats.add(format)
|
|
formats.add('zip')
|
|
formats.add('rar')
|
|
return formats
|
|
|
|
|
|
def output_format_plugins():
|
|
for plugin in _initialized_plugins:
|
|
if isinstance(plugin, conversion.OutputFormatPlugin):
|
|
yield plugin
|
|
|
|
|
|
def plugin_for_output_format(fmt):
|
|
customization = config['plugin_customization']
|
|
for plugin in output_format_plugins():
|
|
if fmt.lower() == plugin.file_type:
|
|
plugin.site_customization = customization.get(plugin.name, None)
|
|
return plugin
|
|
|
|
|
|
def available_output_formats():
|
|
formats = set()
|
|
for plugin in output_format_plugins():
|
|
formats.add(plugin.file_type)
|
|
return formats
|
|
|
|
|
|
# Catalog plugins
|
|
def catalog_plugins():
|
|
for plugin in _initialized_plugins:
|
|
if isinstance(plugin, customize.CatalogPlugin):
|
|
yield plugin
|
|
|
|
|
|
def available_catalog_formats():
|
|
formats = set()
|
|
for plugin in catalog_plugins():
|
|
for format in plugin.file_types:
|
|
formats.add(format)
|
|
return formats
|
|
|
|
|
|
def plugin_for_catalog_format(fmt):
|
|
for plugin in catalog_plugins():
|
|
if fmt.lower() in plugin.file_types:
|
|
return plugin
|
|
|
|
|
|
# Editor plugins
|
|
def all_edit_book_tool_plugins():
|
|
for plugin in _initialized_plugins:
|
|
if isinstance(plugin, customize.EditBookToolPlugin):
|
|
yield plugin
|
|
|
|
|
|
# Initialize plugins
|
|
_initialized_plugins = []
|
|
|
|
|
|
def initialize_plugin(plugin, path_to_zip_file):
|
|
try:
|
|
p = plugin(path_to_zip_file)
|
|
p.initialize()
|
|
return p
|
|
except Exception:
|
|
print('Failed to initialize plugin:', plugin.name, plugin.version)
|
|
tb = traceback.format_exc()
|
|
raise customize.InvalidPlugin(('Initialization of plugin %s failed '
|
|
'with traceback:' % tb) + '\n'+tb)
|
|
|
|
|
|
def has_external_plugins():
|
|
'True if there are updateable (ZIP file based) plugins'
|
|
return bool(config['plugins'])
|
|
|
|
|
|
def initialize_plugins():
|
|
global _initialized_plugins
|
|
_initialized_plugins = []
|
|
external_plugins = config['plugins'].copy()
|
|
for name in BLACKLISTED_PLUGINS:
|
|
external_plugins.pop(name, None)
|
|
ostdout, ostderr = sys.stdout, sys.stderr
|
|
|
|
for zfp in list(external_plugins) + builtins.plugins:
|
|
try:
|
|
plugin = initialize_plugin(zfp, None)
|
|
_initialized_plugins.append(plugin)
|
|
except Exception:
|
|
print('Failed to initialize plugin:', repr(zfp))
|
|
# Prevent a custom plugin from overriding stdout/stderr as this breaks
|
|
# ipython
|
|
sys.stdout, sys.stderr = ostdout, ostderr
|
|
_initialized_plugins.sort(key=lambda x: x.priority, reverse=True)
|
|
reread_filetype_plugins()
|
|
reread_metadata_plugins()
|
|
|
|
|
|
initialize_plugins()
|
|
|
|
|
|
def initialized_plugins():
|
|
for plugin in _initialized_plugins:
|
|
yield plugin
|
|
|
|
|
|
# CLI
|
|
|
|
def option_parser():
|
|
parser = cfg.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('-b', '--build-plugin', default=None,
|
|
help='For plugin developers: Path to the directory '
|
|
'where you are developing the plugin. This command will '
|
|
'automatically zip up the plugin and update it in '
|
|
'calibre.')
|
|
parser.add_option('-r', '--remove-plugin', default=None,
|
|
help='Remove a custom plugin by name. Has no effect on '
|
|
'builtin plugins')
|
|
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
|