mirror of
https://github.com/gryf/ebook-converter.git
synced 2026-03-05 00:15:54 +01:00
Initial import
This commit is contained in:
759
ebook_converter/customize/__init__.py
Normal file
759
ebook_converter/customize/__init__.py
Normal file
@@ -0,0 +1,759 @@
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
import os, sys, zipfile, importlib
|
||||
|
||||
from calibre.constants import numeric_version, iswindows, isosx
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
from polyglot.builtins import unicode_type
|
||||
|
||||
platform = 'linux'
|
||||
if iswindows:
|
||||
platform = 'windows'
|
||||
elif isosx:
|
||||
platform = 'osx'
|
||||
|
||||
|
||||
class PluginNotFound(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidPlugin(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class Plugin(object): # {{{
|
||||
'''
|
||||
A calibre plugin. Useful members include:
|
||||
|
||||
* ``self.plugin_path``: Stores path to the ZIP file that contains
|
||||
this plugin or None if it is a builtin
|
||||
plugin
|
||||
* ``self.site_customization``: Stores a customization string entered
|
||||
by the user.
|
||||
|
||||
Methods that should be overridden in sub classes:
|
||||
|
||||
* :meth:`initialize`
|
||||
* :meth:`customization_help`
|
||||
|
||||
Useful methods:
|
||||
|
||||
* :meth:`temporary_file`
|
||||
* :meth:`__enter__`
|
||||
* :meth:`load_resources`
|
||||
|
||||
'''
|
||||
#: List of platforms this plugin works on.
|
||||
#: For example: ``['windows', 'osx', 'linux']``
|
||||
supported_platforms = []
|
||||
|
||||
#: The name of this plugin. You must set it something other
|
||||
#: than Trivial Plugin for it to work.
|
||||
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.
|
||||
#: Plugins with higher priority will be run first.
|
||||
#: The highest possible priority is ``sys.maxsize``.
|
||||
#: Default priority is 1.
|
||||
priority = 1
|
||||
|
||||
#: The earliest version of calibre this plugin requires
|
||||
minimum_calibre_version = (0, 4, 118)
|
||||
|
||||
#: If False, the user will not be able to disable this plugin. Use with
|
||||
#: care.
|
||||
can_be_disabled = True
|
||||
|
||||
#: The type of this plugin. Used for categorizing plugins in the
|
||||
#: GUI
|
||||
type = _('Base')
|
||||
|
||||
def __init__(self, plugin_path):
|
||||
self.plugin_path = plugin_path
|
||||
self.site_customization = None
|
||||
|
||||
def initialize(self):
|
||||
'''
|
||||
Called once when calibre plugins are initialized. Plugins are
|
||||
re-initialized every time a new plugin is added. Also note that if the
|
||||
plugin is run in a worker process, such as for adding books, then the
|
||||
plugin will be initialized for every new worker process.
|
||||
|
||||
Perform any plugin specific initialization here, such as extracting
|
||||
resources from the plugin ZIP file. The path to the ZIP file is
|
||||
available as ``self.plugin_path``.
|
||||
|
||||
Note that ``self.site_customization`` is **not** available at this point.
|
||||
'''
|
||||
pass
|
||||
|
||||
def config_widget(self):
|
||||
'''
|
||||
Implement this method and :meth:`save_settings` in your plugin to
|
||||
use a custom configuration dialog, rather then relying on the simple
|
||||
string based default customization.
|
||||
|
||||
This method, if implemented, must return a QWidget. The widget can have
|
||||
an optional method validate() that takes no arguments and is called
|
||||
immediately after the user clicks OK. Changes are applied if and only
|
||||
if the method returns True.
|
||||
|
||||
If for some reason you cannot perform the configuration at this time,
|
||||
return a tuple of two strings (message, details), these will be
|
||||
displayed as a warning dialog to the user and the process will be
|
||||
aborted.
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
def save_settings(self, config_widget):
|
||||
'''
|
||||
Save the settings specified by the user with config_widget.
|
||||
|
||||
:param config_widget: The widget returned by :meth:`config_widget`.
|
||||
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
def do_user_config(self, parent=None):
|
||||
'''
|
||||
This method shows a configuration dialog for this plugin. It returns
|
||||
True if the user clicks OK, False otherwise. The changes are
|
||||
automatically applied.
|
||||
'''
|
||||
from PyQt5.Qt import QDialog, QDialogButtonBox, QVBoxLayout, \
|
||||
QLabel, Qt, QLineEdit
|
||||
from calibre.gui2 import gprefs
|
||||
|
||||
prefname = 'plugin config dialog:'+self.type + ':' + self.name
|
||||
geom = gprefs.get(prefname, None)
|
||||
|
||||
config_dialog = QDialog(parent)
|
||||
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
v = QVBoxLayout(config_dialog)
|
||||
|
||||
def size_dialog():
|
||||
if geom is None:
|
||||
config_dialog.resize(config_dialog.sizeHint())
|
||||
else:
|
||||
from PyQt5.Qt import QApplication
|
||||
QApplication.instance().safe_restore_geometry(config_dialog, geom)
|
||||
|
||||
button_box.accepted.connect(config_dialog.accept)
|
||||
button_box.rejected.connect(config_dialog.reject)
|
||||
config_dialog.setWindowTitle(_('Customize') + ' ' + self.name)
|
||||
try:
|
||||
config_widget = self.config_widget()
|
||||
except NotImplementedError:
|
||||
config_widget = None
|
||||
|
||||
if isinstance(config_widget, tuple):
|
||||
from calibre.gui2 import warning_dialog
|
||||
warning_dialog(parent, _('Cannot configure'), config_widget[0],
|
||||
det_msg=config_widget[1], show=True)
|
||||
return False
|
||||
|
||||
if config_widget is not None:
|
||||
v.addWidget(config_widget)
|
||||
v.addWidget(button_box)
|
||||
size_dialog()
|
||||
config_dialog.exec_()
|
||||
|
||||
if config_dialog.result() == QDialog.Accepted:
|
||||
if hasattr(config_widget, 'validate'):
|
||||
if config_widget.validate():
|
||||
self.save_settings(config_widget)
|
||||
else:
|
||||
self.save_settings(config_widget)
|
||||
else:
|
||||
from calibre.customize.ui import plugin_customization, \
|
||||
customize_plugin
|
||||
help_text = self.customization_help(gui=True)
|
||||
help_text = QLabel(help_text, config_dialog)
|
||||
help_text.setWordWrap(True)
|
||||
help_text.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard)
|
||||
help_text.setOpenExternalLinks(True)
|
||||
v.addWidget(help_text)
|
||||
sc = plugin_customization(self)
|
||||
if not sc:
|
||||
sc = ''
|
||||
sc = sc.strip()
|
||||
sc = QLineEdit(sc, config_dialog)
|
||||
v.addWidget(sc)
|
||||
v.addWidget(button_box)
|
||||
size_dialog()
|
||||
config_dialog.exec_()
|
||||
|
||||
if config_dialog.result() == QDialog.Accepted:
|
||||
sc = unicode_type(sc.text()).strip()
|
||||
customize_plugin(self, sc)
|
||||
|
||||
geom = bytearray(config_dialog.saveGeometry())
|
||||
gprefs[prefname] = geom
|
||||
|
||||
return config_dialog.result()
|
||||
|
||||
def load_resources(self, names):
|
||||
'''
|
||||
If this plugin comes in a ZIP file (user added plugin), this method
|
||||
will allow you to load resources from the ZIP file.
|
||||
|
||||
For example to load an image::
|
||||
|
||||
pixmap = QPixmap()
|
||||
pixmap.loadFromData(self.load_resources(['images/icon.png'])['images/icon.png'])
|
||||
icon = QIcon(pixmap)
|
||||
|
||||
:param names: List of paths to resources in the ZIP file using / as separator
|
||||
|
||||
:return: A dictionary of the form ``{name: file_contents}``. Any names
|
||||
that were not found in the ZIP file will not be present in the
|
||||
dictionary.
|
||||
|
||||
'''
|
||||
if self.plugin_path is None:
|
||||
raise ValueError('This plugin was not loaded from a ZIP file')
|
||||
ans = {}
|
||||
with zipfile.ZipFile(self.plugin_path, 'r') as zf:
|
||||
for candidate in zf.namelist():
|
||||
if candidate in names:
|
||||
ans[candidate] = zf.read(candidate)
|
||||
return ans
|
||||
|
||||
def customization_help(self, gui=False):
|
||||
'''
|
||||
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.
|
||||
|
||||
If you re-implement this method in your subclass, the user will
|
||||
be asked to enter a string as customization for this plugin.
|
||||
The customization string will be available as
|
||||
``self.site_customization``.
|
||||
|
||||
Site customization could be anything, for example, the path to
|
||||
a needed binary on the user's computer.
|
||||
|
||||
:param gui: If True return HTML help, otherwise return plain text help.
|
||||
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
def temporary_file(self, suffix):
|
||||
'''
|
||||
Return a file-like object that is a temporary file on the file system.
|
||||
This file will remain available even after being closed and will only
|
||||
be removed on interpreter shutdown. Use the ``name`` member of the
|
||||
returned object to access the full path to the created temporary file.
|
||||
|
||||
:param suffix: The suffix that the temporary file will have.
|
||||
'''
|
||||
return PersistentTemporaryFile(suffix)
|
||||
|
||||
def is_customizable(self):
|
||||
try:
|
||||
self.customization_help()
|
||||
return True
|
||||
except NotImplementedError:
|
||||
return False
|
||||
|
||||
def __enter__(self, *args):
|
||||
'''
|
||||
Add this plugin to the python path so that it's contents become directly importable.
|
||||
Useful when bundling large python libraries into the plugin. Use it like this::
|
||||
with plugin:
|
||||
import something
|
||||
'''
|
||||
if self.plugin_path is not None:
|
||||
from calibre.utils.zipfile import ZipFile
|
||||
zf = ZipFile(self.plugin_path)
|
||||
extensions = {x.rpartition('.')[-1].lower() for x in
|
||||
zf.namelist()}
|
||||
zip_safe = True
|
||||
for ext in ('pyd', 'so', 'dll', 'dylib'):
|
||||
if ext in extensions:
|
||||
zip_safe = False
|
||||
break
|
||||
if zip_safe:
|
||||
sys.path.insert(0, self.plugin_path)
|
||||
self.sys_insertion_path = self.plugin_path
|
||||
else:
|
||||
from calibre.ptempfile import TemporaryDirectory
|
||||
self._sys_insertion_tdir = TemporaryDirectory('plugin_unzip')
|
||||
self.sys_insertion_path = self._sys_insertion_tdir.__enter__(*args)
|
||||
zf.extractall(self.sys_insertion_path)
|
||||
sys.path.insert(0, self.sys_insertion_path)
|
||||
zf.close()
|
||||
|
||||
def __exit__(self, *args):
|
||||
ip, it = getattr(self, 'sys_insertion_path', None), getattr(self,
|
||||
'_sys_insertion_tdir', None)
|
||||
if ip in sys.path:
|
||||
sys.path.remove(ip)
|
||||
if hasattr(it, '__exit__'):
|
||||
it.__exit__(*args)
|
||||
|
||||
def cli_main(self, args):
|
||||
'''
|
||||
This method is the main entry point for your plugins command line
|
||||
interface. It is called when the user does: calibre-debug -r "Plugin
|
||||
Name". Any arguments passed are present in the args variable.
|
||||
'''
|
||||
raise NotImplementedError('The %s plugin has no command line interface'
|
||||
%self.name)
|
||||
|
||||
# }}}
|
||||
|
||||
|
||||
class FileTypePlugin(Plugin): # {{{
|
||||
'''
|
||||
A plugin that is associated with a particular set of file types.
|
||||
'''
|
||||
|
||||
#: Set of file types for which this plugin should be run.
|
||||
#: Use '*' for all file types.
|
||||
#: For example: ``{'lit', 'mobi', 'prc'}``
|
||||
file_types = set()
|
||||
|
||||
#: If True, this plugin is run when books are added
|
||||
#: to the database
|
||||
on_import = False
|
||||
|
||||
#: If True, this plugin is run after books are added
|
||||
#: to the database. In this case the postimport and postadd
|
||||
#: methods of the plugin are called.
|
||||
on_postimport = False
|
||||
|
||||
#: If True, this plugin is run just before a conversion
|
||||
on_preprocess = False
|
||||
|
||||
#: If True, this plugin is run after conversion
|
||||
#: on the final file produced by the conversion output plugin.
|
||||
on_postprocess = False
|
||||
|
||||
type = _('File type')
|
||||
|
||||
def run(self, path_to_ebook):
|
||||
'''
|
||||
Run the plugin. Must be implemented in subclasses.
|
||||
It should perform whatever modifications are required
|
||||
on the e-book and return the absolute path to the
|
||||
modified e-book. If no modifications are needed, it should
|
||||
return the path to the original e-book. If an error is encountered
|
||||
it should raise an Exception. The default implementation
|
||||
simply return the path to the original e-book. Note that the path to
|
||||
the original file (before any file type plugins are run, is available as
|
||||
self.original_path_to_file).
|
||||
|
||||
The modified e-book file should be created with the
|
||||
:meth:`temporary_file` method.
|
||||
|
||||
:param path_to_ebook: Absolute path to the e-book.
|
||||
|
||||
:return: Absolute path to the modified e-book.
|
||||
'''
|
||||
# Default implementation does nothing
|
||||
return path_to_ebook
|
||||
|
||||
def postimport(self, book_id, book_format, db):
|
||||
'''
|
||||
Called post import, i.e., after the book file has been added to the database. Note that
|
||||
this is different from :meth:`postadd` which is called when the book record is created for
|
||||
the first time. This method is called whenever a new file is added to a book record. It is
|
||||
useful for modifying the book record based on the contents of the newly added file.
|
||||
|
||||
:param book_id: Database id of the added book.
|
||||
:param book_format: The file type of the book that was added.
|
||||
:param db: Library database.
|
||||
'''
|
||||
pass # Default implementation does nothing
|
||||
|
||||
def postadd(self, book_id, fmt_map, db):
|
||||
'''
|
||||
Called post add, i.e. after a book has been added to the db. Note that
|
||||
this is different from :meth:`postimport`, which is called after a single book file
|
||||
has been added to a book. postadd() is called only when an entire book record
|
||||
with possibly more than one book file has been created for the first time.
|
||||
This is useful if you wish to modify the book record in the database when the
|
||||
book is first added to calibre.
|
||||
|
||||
:param book_id: Database id of the added book.
|
||||
:param fmt_map: Map of file format to path from which the file format
|
||||
was added. Note that this might or might not point to an actual
|
||||
existing file, as sometimes files are added as streams. In which case
|
||||
it might be a dummy value or a non-existent path.
|
||||
:param db: Library database
|
||||
'''
|
||||
pass # Default implementation does nothing
|
||||
|
||||
# }}}
|
||||
|
||||
|
||||
class MetadataReaderPlugin(Plugin): # {{{
|
||||
'''
|
||||
A plugin that implements reading metadata from a set of file types.
|
||||
'''
|
||||
#: Set of file types for which this plugin should be run.
|
||||
#: For example: ``set(['lit', 'mobi', 'prc'])``
|
||||
file_types = set()
|
||||
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
version = numeric_version
|
||||
author = 'Kovid Goyal'
|
||||
|
||||
type = _('Metadata reader')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
Plugin.__init__(self, *args, **kwargs)
|
||||
self.quick = False
|
||||
|
||||
def get_metadata(self, stream, type):
|
||||
'''
|
||||
Return metadata for the file represented by stream (a file like object
|
||||
that supports reading). Raise an exception when there is an error
|
||||
with the input data.
|
||||
|
||||
:param type: The type of file. Guaranteed to be one of the entries
|
||||
in :attr:`file_types`.
|
||||
:return: A :class:`calibre.ebooks.metadata.book.Metadata` object
|
||||
'''
|
||||
return None
|
||||
# }}}
|
||||
|
||||
|
||||
class MetadataWriterPlugin(Plugin): # {{{
|
||||
'''
|
||||
A plugin that implements reading metadata from a set of file types.
|
||||
'''
|
||||
#: Set of file types for which this plugin should be run.
|
||||
#: For example: ``set(['lit', 'mobi', 'prc'])``
|
||||
file_types = set()
|
||||
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
version = numeric_version
|
||||
author = 'Kovid Goyal'
|
||||
|
||||
type = _('Metadata writer')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
Plugin.__init__(self, *args, **kwargs)
|
||||
self.apply_null = False
|
||||
|
||||
def set_metadata(self, stream, mi, type):
|
||||
'''
|
||||
Set metadata for the file represented by stream (a file like object
|
||||
that supports reading). Raise an exception when there is an error
|
||||
with the input data.
|
||||
|
||||
:param type: The type of file. Guaranteed to be one of the entries
|
||||
in :attr:`file_types`.
|
||||
:param mi: A :class:`calibre.ebooks.metadata.book.Metadata` object
|
||||
'''
|
||||
pass
|
||||
|
||||
# }}}
|
||||
|
||||
|
||||
class CatalogPlugin(Plugin): # {{{
|
||||
'''
|
||||
A plugin that implements a catalog generator.
|
||||
'''
|
||||
|
||||
resources_path = None
|
||||
|
||||
#: Output file type for which this plugin should be run.
|
||||
#: For example: 'epub' or 'xml'
|
||||
file_types = set()
|
||||
|
||||
type = _('Catalog generator')
|
||||
|
||||
#: CLI parser options specific to this plugin, declared as namedtuple Option:
|
||||
#:
|
||||
#: from collections import namedtuple
|
||||
#: Option = namedtuple('Option', 'option, default, dest, help')
|
||||
#: cli_options = [Option('--catalog-title', default = 'My Catalog',
|
||||
#: dest = 'catalog_title', help = (_('Title of generated catalog. \nDefault:') + " '" + '%default' + "'"))]
|
||||
#: cli_options parsed in calibre.db.cli.cmd_catalog:option_parser()
|
||||
#:
|
||||
cli_options = []
|
||||
|
||||
def _field_sorter(self, key):
|
||||
'''
|
||||
Custom fields sort after standard fields
|
||||
'''
|
||||
if key.startswith('#'):
|
||||
return '~%s' % key[1:]
|
||||
else:
|
||||
return key
|
||||
|
||||
def search_sort_db(self, db, opts):
|
||||
|
||||
db.search(opts.search_text)
|
||||
|
||||
if opts.sort_by:
|
||||
# 2nd arg = ascending
|
||||
db.sort(opts.sort_by, True)
|
||||
return db.get_data_as_dict(ids=opts.ids)
|
||||
|
||||
def get_output_fields(self, db, opts):
|
||||
# Return a list of requested fields
|
||||
all_std_fields = {'author_sort','authors','comments','cover','formats',
|
||||
'id','isbn','library_name','ondevice','pubdate','publisher',
|
||||
'rating','series_index','series','size','tags','timestamp',
|
||||
'title_sort','title','uuid','languages','identifiers'}
|
||||
all_custom_fields = set(db.custom_field_keys())
|
||||
for field in list(all_custom_fields):
|
||||
fm = db.field_metadata[field]
|
||||
if fm['datatype'] == 'series':
|
||||
all_custom_fields.add(field+'_index')
|
||||
all_fields = all_std_fields.union(all_custom_fields)
|
||||
|
||||
if opts.fields != 'all':
|
||||
# Make a list from opts.fields
|
||||
of = [x.strip() for x in opts.fields.split(',')]
|
||||
requested_fields = set(of)
|
||||
|
||||
# Validate requested_fields
|
||||
if requested_fields - all_fields:
|
||||
from calibre.library import current_library_name
|
||||
invalid_fields = sorted(list(requested_fields - all_fields))
|
||||
print("invalid --fields specified: %s" % ', '.join(invalid_fields))
|
||||
print("available fields in '%s': %s" %
|
||||
(current_library_name(), ', '.join(sorted(list(all_fields)))))
|
||||
raise ValueError("unable to generate catalog with specified fields")
|
||||
|
||||
fields = [x for x in of if x in all_fields]
|
||||
else:
|
||||
fields = sorted(all_fields, key=self._field_sorter)
|
||||
|
||||
if not opts.connected_device['is_device_connected'] and 'ondevice' in fields:
|
||||
fields.pop(int(fields.index('ondevice')))
|
||||
|
||||
return fields
|
||||
|
||||
def initialize(self):
|
||||
'''
|
||||
If plugin is not a built-in, copy the plugin's .ui and .py files from
|
||||
the ZIP file to $TMPDIR.
|
||||
Tab will be dynamically generated and added to the Catalog Options dialog in
|
||||
calibre.gui2.dialogs.catalog.py:Catalog
|
||||
'''
|
||||
from calibre.customize.builtins import plugins as builtin_plugins
|
||||
from calibre.customize.ui import config
|
||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||
|
||||
if not type(self) in builtin_plugins and self.name not in config['disabled_plugins']:
|
||||
files_to_copy = ["%s.%s" % (self.name.lower(),ext) for ext in ["ui","py"]]
|
||||
resources = zipfile.ZipFile(self.plugin_path,'r')
|
||||
|
||||
if self.resources_path is None:
|
||||
self.resources_path = PersistentTemporaryDirectory('_plugin_resources', prefix='')
|
||||
|
||||
for file in files_to_copy:
|
||||
try:
|
||||
resources.extract(file, self.resources_path)
|
||||
except:
|
||||
print(" customize:__init__.initialize(): %s not found in %s" % (file, os.path.basename(self.plugin_path)))
|
||||
continue
|
||||
resources.close()
|
||||
|
||||
def run(self, path_to_output, opts, db, ids, notification=None):
|
||||
'''
|
||||
Run the plugin. Must be implemented in subclasses.
|
||||
It should generate the catalog in the format specified
|
||||
in file_types, returning the absolute path to the
|
||||
generated catalog file. If an error is encountered
|
||||
it should raise an Exception.
|
||||
|
||||
The generated catalog file should be created with the
|
||||
:meth:`temporary_file` method.
|
||||
|
||||
:param path_to_output: Absolute path to the generated catalog file.
|
||||
:param opts: A dictionary of keyword arguments
|
||||
:param db: A LibraryDatabase2 object
|
||||
'''
|
||||
# Default implementation does nothing
|
||||
raise NotImplementedError('CatalogPlugin.generate_catalog() default '
|
||||
'method, should be overridden in subclass')
|
||||
|
||||
# }}}
|
||||
|
||||
|
||||
class InterfaceActionBase(Plugin): # {{{
|
||||
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
author = 'Kovid Goyal'
|
||||
type = _('User interface action')
|
||||
can_be_disabled = False
|
||||
|
||||
actual_plugin = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
Plugin.__init__(self, *args, **kwargs)
|
||||
self.actual_plugin_ = None
|
||||
|
||||
def load_actual_plugin(self, gui):
|
||||
'''
|
||||
This method must return the actual interface action plugin object.
|
||||
'''
|
||||
ac = self.actual_plugin_
|
||||
if ac is None:
|
||||
mod, cls = self.actual_plugin.split(':')
|
||||
ac = getattr(importlib.import_module(mod), cls)(gui,
|
||||
self.site_customization)
|
||||
self.actual_plugin_ = ac
|
||||
return ac
|
||||
|
||||
# }}}
|
||||
|
||||
|
||||
class PreferencesPlugin(Plugin): # {{{
|
||||
|
||||
'''
|
||||
A plugin representing a widget displayed in the Preferences dialog.
|
||||
|
||||
This plugin has only one important method :meth:`create_widget`. The
|
||||
various fields of the plugin control how it is categorized in the UI.
|
||||
'''
|
||||
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
author = 'Kovid Goyal'
|
||||
type = _('Preferences')
|
||||
can_be_disabled = False
|
||||
|
||||
#: Import path to module that contains a class named ConfigWidget
|
||||
#: which implements the ConfigWidgetInterface. Used by
|
||||
#: :meth:`create_widget`.
|
||||
config_widget = None
|
||||
|
||||
#: Where in the list of categories the :attr:`category` of this plugin should be.
|
||||
category_order = 100
|
||||
|
||||
#: Where in the list of names in a category, the :attr:`gui_name` of this
|
||||
#: plugin should be
|
||||
name_order = 100
|
||||
|
||||
#: The category this plugin should be in
|
||||
category = None
|
||||
|
||||
#: The category name displayed to the user for this plugin
|
||||
gui_category = None
|
||||
|
||||
#: The name displayed to the user for this plugin
|
||||
gui_name = None
|
||||
|
||||
#: The icon for this plugin, should be an absolute path
|
||||
icon = None
|
||||
|
||||
#: The description used for tooltips and the like
|
||||
description = None
|
||||
|
||||
def create_widget(self, parent=None):
|
||||
'''
|
||||
Create and return the actual Qt widget used for setting this group of
|
||||
preferences. The widget must implement the
|
||||
:class:`calibre.gui2.preferences.ConfigWidgetInterface`.
|
||||
|
||||
The default implementation uses :attr:`config_widget` to instantiate
|
||||
the widget.
|
||||
'''
|
||||
base, _, wc = self.config_widget.partition(':')
|
||||
if not wc:
|
||||
wc = 'ConfigWidget'
|
||||
base = importlib.import_module(base)
|
||||
widget = getattr(base, wc)
|
||||
return widget(parent)
|
||||
|
||||
# }}}
|
||||
|
||||
|
||||
class StoreBase(Plugin): # {{{
|
||||
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
author = 'John Schember'
|
||||
type = _('Store')
|
||||
# Information about the store. Should be in the primary language
|
||||
# of the store. This should not be translatable when set by
|
||||
# a subclass.
|
||||
description = _('An e-book store.')
|
||||
minimum_calibre_version = (0, 8, 0)
|
||||
version = (1, 0, 1)
|
||||
|
||||
actual_plugin = None
|
||||
|
||||
# Does the store only distribute e-books without DRM.
|
||||
drm_free_only = False
|
||||
# This is the 2 letter country code for the corporate
|
||||
# headquarters of the store.
|
||||
headquarters = ''
|
||||
# All formats the store distributes e-books in.
|
||||
formats = []
|
||||
# Is this store on an affiliate program?
|
||||
affiliate = False
|
||||
|
||||
def load_actual_plugin(self, gui):
|
||||
'''
|
||||
This method must return the actual interface action plugin object.
|
||||
'''
|
||||
mod, cls = self.actual_plugin.split(':')
|
||||
self.actual_plugin_object = getattr(importlib.import_module(mod), cls)(gui, self.name)
|
||||
return self.actual_plugin_object
|
||||
|
||||
def customization_help(self, gui=False):
|
||||
if getattr(self, 'actual_plugin_object', None) is not None:
|
||||
return self.actual_plugin_object.customization_help(gui)
|
||||
raise NotImplementedError()
|
||||
|
||||
def config_widget(self):
|
||||
if getattr(self, 'actual_plugin_object', None) is not None:
|
||||
return self.actual_plugin_object.config_widget()
|
||||
raise NotImplementedError()
|
||||
|
||||
def save_settings(self, config_widget):
|
||||
if getattr(self, 'actual_plugin_object', None) is not None:
|
||||
return self.actual_plugin_object.save_settings(config_widget)
|
||||
raise NotImplementedError()
|
||||
|
||||
# }}}
|
||||
|
||||
|
||||
class EditBookToolPlugin(Plugin): # {{{
|
||||
|
||||
type = _('Edit book tool')
|
||||
minimum_calibre_version = (1, 46, 0)
|
||||
|
||||
# }}}
|
||||
|
||||
|
||||
class LibraryClosedPlugin(Plugin): # {{{
|
||||
'''
|
||||
LibraryClosedPlugins are run when a library is closed, either at shutdown,
|
||||
when the library is changed, or when a library is used in some other way.
|
||||
At the moment these plugins won't be called by the CLI functions.
|
||||
'''
|
||||
type = _('Library closed')
|
||||
|
||||
# minimum version 2.54 because that is when support was added
|
||||
minimum_calibre_version = (2, 54, 0)
|
||||
|
||||
def run(self, db):
|
||||
'''
|
||||
The db will be a reference to the new_api (db.cache.py).
|
||||
|
||||
The plugin must run to completion. It must not use the GUI, threads, or
|
||||
any signals.
|
||||
'''
|
||||
raise NotImplementedError('LibraryClosedPlugin '
|
||||
'run method must be overridden in subclass')
|
||||
# }}}
|
||||
1973
ebook_converter/customize/builtins.py
Normal file
1973
ebook_converter/customize/builtins.py
Normal file
File diff suppressed because it is too large
Load Diff
376
ebook_converter/customize/conversion.py
Normal file
376
ebook_converter/customize/conversion.py
Normal file
@@ -0,0 +1,376 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
'''
|
||||
Defines the plugin system for conversions.
|
||||
'''
|
||||
import re, os, shutil, numbers
|
||||
|
||||
from calibre import CurrentDir
|
||||
from calibre.customize import Plugin
|
||||
from polyglot.builtins import unicode_type
|
||||
|
||||
|
||||
class ConversionOption(object):
|
||||
|
||||
'''
|
||||
Class representing conversion options
|
||||
'''
|
||||
|
||||
def __init__(self, name=None, help=None, long_switch=None,
|
||||
short_switch=None, choices=None):
|
||||
self.name = name
|
||||
self.help = help
|
||||
self.long_switch = long_switch
|
||||
self.short_switch = short_switch
|
||||
self.choices = choices
|
||||
|
||||
if self.long_switch is None:
|
||||
self.long_switch = self.name.replace('_', '-')
|
||||
|
||||
self.validate_parameters()
|
||||
|
||||
def validate_parameters(self):
|
||||
'''
|
||||
Validate the parameters passed to :meth:`__init__`.
|
||||
'''
|
||||
if re.match(r'[a-zA-Z_]([a-zA-Z0-9_])*', self.name) is None:
|
||||
raise ValueError(self.name + ' is not a valid Python identifier')
|
||||
if not self.help:
|
||||
raise ValueError('You must set the help text')
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.name)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.name == getattr(other, 'name', other)
|
||||
|
||||
def clone(self):
|
||||
return ConversionOption(name=self.name, help=self.help,
|
||||
long_switch=self.long_switch, short_switch=self.short_switch,
|
||||
choices=self.choices)
|
||||
|
||||
|
||||
class OptionRecommendation(object):
|
||||
LOW = 1
|
||||
MED = 2
|
||||
HIGH = 3
|
||||
|
||||
def __init__(self, recommended_value=None, level=LOW, **kwargs):
|
||||
'''
|
||||
An option recommendation. That is, an option as well as its recommended
|
||||
value and the level of the recommendation.
|
||||
'''
|
||||
self.level = level
|
||||
self.recommended_value = recommended_value
|
||||
self.option = kwargs.pop('option', None)
|
||||
if self.option is None:
|
||||
self.option = ConversionOption(**kwargs)
|
||||
|
||||
self.validate_parameters()
|
||||
|
||||
@property
|
||||
def help(self):
|
||||
return self.option.help
|
||||
|
||||
def clone(self):
|
||||
return OptionRecommendation(recommended_value=self.recommended_value,
|
||||
level=self.level, option=self.option.clone())
|
||||
|
||||
def validate_parameters(self):
|
||||
if self.option.choices and self.recommended_value not in \
|
||||
self.option.choices:
|
||||
raise ValueError('OpRec: %s: Recommended value not in choices'%
|
||||
self.option.name)
|
||||
if not (isinstance(self.recommended_value, (numbers.Number, bytes, unicode_type)) or self.recommended_value is None):
|
||||
raise ValueError('OpRec: %s:'%self.option.name + repr(
|
||||
self.recommended_value) + ' is not a string or a number')
|
||||
|
||||
|
||||
class DummyReporter(object):
|
||||
|
||||
def __init__(self):
|
||||
self.cancel_requested = False
|
||||
|
||||
def __call__(self, percent, msg=''):
|
||||
pass
|
||||
|
||||
|
||||
def gui_configuration_widget(name, parent, get_option_by_name,
|
||||
get_option_help, db, book_id, for_output=True):
|
||||
import importlib
|
||||
|
||||
def widget_factory(cls):
|
||||
return cls(parent, get_option_by_name,
|
||||
get_option_help, db, book_id)
|
||||
|
||||
if for_output:
|
||||
try:
|
||||
output_widget = importlib.import_module(
|
||||
'calibre.gui2.convert.'+name)
|
||||
pw = output_widget.PluginWidget
|
||||
pw.ICON = I('back.png')
|
||||
pw.HELP = _('Options specific to the output format.')
|
||||
return widget_factory(pw)
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
input_widget = importlib.import_module(
|
||||
'calibre.gui2.convert.'+name)
|
||||
pw = input_widget.PluginWidget
|
||||
pw.ICON = I('forward.png')
|
||||
pw.HELP = _('Options specific to the input format.')
|
||||
return widget_factory(pw)
|
||||
except ImportError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
class InputFormatPlugin(Plugin):
|
||||
|
||||
'''
|
||||
InputFormatPlugins are responsible for converting a document into
|
||||
HTML+OPF+CSS+etc.
|
||||
The results of the conversion *must* be encoded in UTF-8.
|
||||
The main action happens in :meth:`convert`.
|
||||
'''
|
||||
|
||||
type = _('Conversion input')
|
||||
can_be_disabled = False
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
commit_name = None # unique name under which options for this plugin are saved
|
||||
ui_data = None
|
||||
|
||||
#: Set of file types for which this plugin should be run
|
||||
#: For example: ``set(['azw', 'mobi', 'prc'])``
|
||||
file_types = set()
|
||||
|
||||
#: If True, this input plugin generates a collection of images,
|
||||
#: one per HTML file. This can be set dynamically, in the convert method
|
||||
#: if the input files can be both image collections and non-image collections.
|
||||
#: If you set this to True, you must implement the get_images() method that returns
|
||||
#: a list of images.
|
||||
is_image_collection = False
|
||||
|
||||
#: Number of CPU cores used by this plugin.
|
||||
#: A value of -1 means that it uses all available cores
|
||||
core_usage = 1
|
||||
|
||||
#: If set to True, the input plugin will perform special processing
|
||||
#: to make its output suitable for viewing
|
||||
for_viewer = False
|
||||
|
||||
#: The encoding that this input plugin creates files in. A value of
|
||||
#: None means that the encoding is undefined and must be
|
||||
#: detected individually
|
||||
output_encoding = 'utf-8'
|
||||
|
||||
#: Options shared by all Input format plugins. Do not override
|
||||
#: in sub-classes. Use :attr:`options` instead. Every option must be an
|
||||
#: instance of :class:`OptionRecommendation`.
|
||||
common_options = {
|
||||
OptionRecommendation(name='input_encoding',
|
||||
recommended_value=None, level=OptionRecommendation.LOW,
|
||||
help=_('Specify the character encoding of the input document. If '
|
||||
'set this option will override any encoding declared by the '
|
||||
'document itself. Particularly useful for documents that '
|
||||
'do not declare an encoding or that have erroneous '
|
||||
'encoding declarations.')
|
||||
)}
|
||||
|
||||
#: Options to customize the behavior of this plugin. Every option must be an
|
||||
#: instance of :class:`OptionRecommendation`.
|
||||
options = set()
|
||||
|
||||
#: A set of 3-tuples of the form
|
||||
#: (option_name, recommended_value, recommendation_level)
|
||||
recommendations = set()
|
||||
|
||||
def __init__(self, *args):
|
||||
Plugin.__init__(self, *args)
|
||||
self.report_progress = DummyReporter()
|
||||
|
||||
def get_images(self):
|
||||
'''
|
||||
Return a list of absolute paths to the images, if this input plugin
|
||||
represents an image collection. The list of images is in the same order
|
||||
as the spine and the TOC.
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
def convert(self, stream, options, file_ext, log, accelerators):
|
||||
'''
|
||||
This method must be implemented in sub-classes. It must return
|
||||
the path to the created OPF file or an :class:`OEBBook` instance.
|
||||
All output should be contained in the current directory.
|
||||
If this plugin creates files outside the current
|
||||
directory they must be deleted/marked for deletion before this method
|
||||
returns.
|
||||
|
||||
:param stream: A file like object that contains the input file.
|
||||
:param options: Options to customize the conversion process.
|
||||
Guaranteed to have attributes corresponding
|
||||
to all the options declared by this plugin. In
|
||||
addition, it will have a verbose attribute that
|
||||
takes integral values from zero upwards. Higher numbers
|
||||
mean be more verbose. Another useful attribute is
|
||||
``input_profile`` that is an instance of
|
||||
:class:`calibre.customize.profiles.InputProfile`.
|
||||
:param file_ext: The extension (without the .) of the input file. It
|
||||
is guaranteed to be one of the `file_types` supported
|
||||
by this plugin.
|
||||
:param log: A :class:`calibre.utils.logging.Log` object. All output
|
||||
should use this object.
|
||||
:param accelarators: A dictionary of various information that the input
|
||||
plugin can get easily that would speed up the
|
||||
subsequent stages of the conversion.
|
||||
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
def __call__(self, stream, options, file_ext, log,
|
||||
accelerators, output_dir):
|
||||
try:
|
||||
log('InputFormatPlugin: %s running'%self.name)
|
||||
if hasattr(stream, 'name'):
|
||||
log('on', stream.name)
|
||||
except:
|
||||
# In case stdout is broken
|
||||
pass
|
||||
|
||||
with CurrentDir(output_dir):
|
||||
for x in os.listdir('.'):
|
||||
shutil.rmtree(x) if os.path.isdir(x) else os.remove(x)
|
||||
|
||||
ret = self.convert(stream, options, file_ext,
|
||||
log, accelerators)
|
||||
|
||||
return ret
|
||||
|
||||
def postprocess_book(self, oeb, opts, log):
|
||||
'''
|
||||
Called to allow the input plugin to perform postprocessing after
|
||||
the book has been parsed.
|
||||
'''
|
||||
pass
|
||||
|
||||
def specialize(self, oeb, opts, log, output_fmt):
|
||||
'''
|
||||
Called to allow the input plugin to specialize the parsed book
|
||||
for a particular output format. Called after postprocess_book
|
||||
and before any transforms are performed on the parsed book.
|
||||
'''
|
||||
pass
|
||||
|
||||
def gui_configuration_widget(self, parent, get_option_by_name,
|
||||
get_option_help, db, book_id=None):
|
||||
'''
|
||||
Called to create the widget used for configuring this plugin in the
|
||||
calibre GUI. The widget must be an instance of the PluginWidget class.
|
||||
See the builtin input plugins for examples.
|
||||
'''
|
||||
name = self.name.lower().replace(' ', '_')
|
||||
return gui_configuration_widget(name, parent, get_option_by_name,
|
||||
get_option_help, db, book_id, for_output=False)
|
||||
|
||||
|
||||
class OutputFormatPlugin(Plugin):
|
||||
|
||||
'''
|
||||
OutputFormatPlugins are responsible for converting an OEB document
|
||||
(OPF+HTML) into an output e-book.
|
||||
|
||||
The OEB document can be assumed to be encoded in UTF-8.
|
||||
The main action happens in :meth:`convert`.
|
||||
'''
|
||||
|
||||
type = _('Conversion output')
|
||||
can_be_disabled = False
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
commit_name = None # unique name under which options for this plugin are saved
|
||||
ui_data = None
|
||||
|
||||
#: The file type (extension without leading period) that this
|
||||
#: plugin outputs
|
||||
file_type = None
|
||||
|
||||
#: Options shared by all Input format plugins. Do not override
|
||||
#: in sub-classes. Use :attr:`options` instead. Every option must be an
|
||||
#: instance of :class:`OptionRecommendation`.
|
||||
common_options = {
|
||||
OptionRecommendation(name='pretty_print',
|
||||
recommended_value=False, level=OptionRecommendation.LOW,
|
||||
help=_('If specified, the output plugin will try to create output '
|
||||
'that is as human readable as possible. May not have any effect '
|
||||
'for some output plugins.')
|
||||
)}
|
||||
|
||||
#: Options to customize the behavior of this plugin. Every option must be an
|
||||
#: instance of :class:`OptionRecommendation`.
|
||||
options = set()
|
||||
|
||||
#: A set of 3-tuples of the form
|
||||
#: (option_name, recommended_value, recommendation_level)
|
||||
recommendations = set()
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return _('Convert e-books to the %s format')%self.file_type
|
||||
|
||||
def __init__(self, *args):
|
||||
Plugin.__init__(self, *args)
|
||||
self.report_progress = DummyReporter()
|
||||
|
||||
def convert(self, oeb_book, output, input_plugin, opts, log):
|
||||
'''
|
||||
Render the contents of `oeb_book` (which is an instance of
|
||||
:class:`calibre.ebooks.oeb.OEBBook`) to the file specified by output.
|
||||
|
||||
:param output: Either a file like object or a string. If it is a string
|
||||
it is the path to a directory that may or may not exist. The output
|
||||
plugin should write its output into that directory. If it is a file like
|
||||
object, the output plugin should write its output into the file.
|
||||
:param input_plugin: The input plugin that was used at the beginning of
|
||||
the conversion pipeline.
|
||||
:param opts: Conversion options. Guaranteed to have attributes
|
||||
corresponding to the OptionRecommendations of this plugin.
|
||||
:param log: The logger. Print debug/info messages etc. using this.
|
||||
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def is_periodical(self):
|
||||
return self.oeb.metadata.publication_type and \
|
||||
unicode_type(self.oeb.metadata.publication_type[0]).startswith('periodical:')
|
||||
|
||||
def specialize_options(self, log, opts, input_fmt):
|
||||
'''
|
||||
Can be used to change the values of conversion options, as used by the
|
||||
conversion pipeline.
|
||||
'''
|
||||
pass
|
||||
|
||||
def specialize_css_for_output(self, log, opts, item, stylizer):
|
||||
'''
|
||||
Can be used to make changes to the css during the CSS flattening
|
||||
process.
|
||||
|
||||
:param item: The item (HTML file) being processed
|
||||
:param stylizer: A Stylizer object containing the flattened styles for
|
||||
item. You can get the style for any element by
|
||||
stylizer.style(element).
|
||||
|
||||
'''
|
||||
pass
|
||||
|
||||
def gui_configuration_widget(self, parent, get_option_by_name,
|
||||
get_option_help, db, book_id=None):
|
||||
'''
|
||||
Called to create the widget used for configuring this plugin in the
|
||||
calibre GUI. The widget must be an instance of the PluginWidget class.
|
||||
See the builtin output plugins for examples.
|
||||
'''
|
||||
name = self.name.lower().replace(' ', '_')
|
||||
return gui_configuration_widget(name, parent, get_option_by_name,
|
||||
get_option_help, db, book_id, for_output=True)
|
||||
873
ebook_converter/customize/profiles.py
Normal file
873
ebook_converter/customize/profiles.py
Normal file
@@ -0,0 +1,873 @@
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
__license__ = 'GPL 3'
|
||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from calibre.customize import Plugin as _Plugin
|
||||
from polyglot.builtins import zip
|
||||
|
||||
FONT_SIZES = [('xx-small', 1),
|
||||
('x-small', None),
|
||||
('small', 2),
|
||||
('medium', 3),
|
||||
('large', 4),
|
||||
('x-large', 5),
|
||||
('xx-large', 6),
|
||||
(None, 7)]
|
||||
|
||||
|
||||
class Plugin(_Plugin):
|
||||
|
||||
fbase = 12
|
||||
fsizes = [5, 7, 9, 12, 13.5, 17, 20, 22, 24]
|
||||
screen_size = (1600, 1200)
|
||||
dpi = 100
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
_Plugin.__init__(self, *args, **kwargs)
|
||||
self.width, self.height = self.screen_size
|
||||
fsizes = list(self.fsizes)
|
||||
self.fkey = list(self.fsizes)
|
||||
self.fsizes = []
|
||||
for (name, num), size in zip(FONT_SIZES, fsizes):
|
||||
self.fsizes.append((name, num, float(size)))
|
||||
self.fnames = dict((name, sz) for name, _, sz in self.fsizes if name)
|
||||
self.fnums = dict((num, sz) for _, num, sz in self.fsizes if num)
|
||||
self.width_pts = self.width * 72./self.dpi
|
||||
self.height_pts = self.height * 72./self.dpi
|
||||
|
||||
# Input profiles {{{
|
||||
|
||||
|
||||
class InputProfile(Plugin):
|
||||
|
||||
author = 'Kovid Goyal'
|
||||
supported_platforms = {'windows', 'osx', 'linux'}
|
||||
can_be_disabled = False
|
||||
type = _('Input profile')
|
||||
|
||||
name = 'Default Input Profile'
|
||||
short_name = 'default' # Used in the CLI so dont use spaces etc. in it
|
||||
description = _('This profile tries to provide sane defaults and is useful '
|
||||
'if you know nothing about the input document.')
|
||||
|
||||
|
||||
class SonyReaderInput(InputProfile):
|
||||
|
||||
name = 'Sony Reader'
|
||||
short_name = 'sony'
|
||||
description = _('This profile is intended for the SONY PRS line. '
|
||||
'The 500/505/600/700 etc.')
|
||||
|
||||
screen_size = (584, 754)
|
||||
dpi = 168.451
|
||||
fbase = 12
|
||||
fsizes = [7.5, 9, 10, 12, 15.5, 20, 22, 24]
|
||||
|
||||
|
||||
class SonyReader300Input(SonyReaderInput):
|
||||
|
||||
name = 'Sony Reader 300'
|
||||
short_name = 'sony300'
|
||||
description = _('This profile is intended for the SONY PRS 300.')
|
||||
|
||||
dpi = 200
|
||||
|
||||
|
||||
class SonyReader900Input(SonyReaderInput):
|
||||
|
||||
author = 'John Schember'
|
||||
name = 'Sony Reader 900'
|
||||
short_name = 'sony900'
|
||||
description = _('This profile is intended for the SONY PRS-900.')
|
||||
|
||||
screen_size = (584, 978)
|
||||
|
||||
|
||||
class MSReaderInput(InputProfile):
|
||||
|
||||
name = 'Microsoft Reader'
|
||||
short_name = 'msreader'
|
||||
description = _('This profile is intended for the Microsoft Reader.')
|
||||
|
||||
screen_size = (480, 652)
|
||||
dpi = 96
|
||||
fbase = 13
|
||||
fsizes = [10, 11, 13, 16, 18, 20, 22, 26]
|
||||
|
||||
|
||||
class MobipocketInput(InputProfile):
|
||||
|
||||
name = 'Mobipocket Books'
|
||||
short_name = 'mobipocket'
|
||||
description = _('This profile is intended for the Mobipocket books.')
|
||||
|
||||
# Unfortunately MOBI books are not narrowly targeted, so this information is
|
||||
# quite likely to be spurious
|
||||
screen_size = (600, 800)
|
||||
dpi = 96
|
||||
fbase = 18
|
||||
fsizes = [14, 14, 16, 18, 20, 22, 24, 26]
|
||||
|
||||
|
||||
class HanlinV3Input(InputProfile):
|
||||
|
||||
name = 'Hanlin V3'
|
||||
short_name = 'hanlinv3'
|
||||
description = _('This profile is intended for the Hanlin V3 and its clones.')
|
||||
|
||||
# Screen size is a best guess
|
||||
screen_size = (584, 754)
|
||||
dpi = 168.451
|
||||
fbase = 16
|
||||
fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
|
||||
|
||||
|
||||
class HanlinV5Input(HanlinV3Input):
|
||||
|
||||
name = 'Hanlin V5'
|
||||
short_name = 'hanlinv5'
|
||||
description = _('This profile is intended for the Hanlin V5 and its clones.')
|
||||
|
||||
# Screen size is a best guess
|
||||
screen_size = (584, 754)
|
||||
dpi = 200
|
||||
|
||||
|
||||
class CybookG3Input(InputProfile):
|
||||
|
||||
name = 'Cybook G3'
|
||||
short_name = 'cybookg3'
|
||||
description = _('This profile is intended for the Cybook G3.')
|
||||
|
||||
# Screen size is a best guess
|
||||
screen_size = (600, 800)
|
||||
dpi = 168.451
|
||||
fbase = 16
|
||||
fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
|
||||
|
||||
|
||||
class CybookOpusInput(InputProfile):
|
||||
|
||||
author = 'John Schember'
|
||||
name = 'Cybook Opus'
|
||||
short_name = 'cybook_opus'
|
||||
description = _('This profile is intended for the Cybook Opus.')
|
||||
|
||||
# Screen size is a best guess
|
||||
screen_size = (600, 800)
|
||||
dpi = 200
|
||||
fbase = 16
|
||||
fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
|
||||
|
||||
|
||||
class KindleInput(InputProfile):
|
||||
|
||||
name = 'Kindle'
|
||||
short_name = 'kindle'
|
||||
description = _('This profile is intended for the Amazon Kindle.')
|
||||
|
||||
# Screen size is a best guess
|
||||
screen_size = (525, 640)
|
||||
dpi = 168.451
|
||||
fbase = 16
|
||||
fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
|
||||
|
||||
|
||||
class IlliadInput(InputProfile):
|
||||
|
||||
name = 'Illiad'
|
||||
short_name = 'illiad'
|
||||
description = _('This profile is intended for the Irex Illiad.')
|
||||
|
||||
screen_size = (760, 925)
|
||||
dpi = 160.0
|
||||
fbase = 12
|
||||
fsizes = [7.5, 9, 10, 12, 15.5, 20, 22, 24]
|
||||
|
||||
|
||||
class IRexDR1000Input(InputProfile):
|
||||
|
||||
author = 'John Schember'
|
||||
name = 'IRex Digital Reader 1000'
|
||||
short_name = 'irexdr1000'
|
||||
description = _('This profile is intended for the IRex Digital Reader 1000.')
|
||||
|
||||
# Screen size is a best guess
|
||||
screen_size = (1024, 1280)
|
||||
dpi = 160
|
||||
fbase = 16
|
||||
fsizes = [12, 14, 16, 18, 20, 22, 24]
|
||||
|
||||
|
||||
class IRexDR800Input(InputProfile):
|
||||
|
||||
author = 'Eric Cronin'
|
||||
name = 'IRex Digital Reader 800'
|
||||
short_name = 'irexdr800'
|
||||
description = _('This profile is intended for the IRex Digital Reader 800.')
|
||||
|
||||
screen_size = (768, 1024)
|
||||
dpi = 160
|
||||
fbase = 16
|
||||
fsizes = [12, 14, 16, 18, 20, 22, 24]
|
||||
|
||||
|
||||
class NookInput(InputProfile):
|
||||
|
||||
author = 'John Schember'
|
||||
name = 'Nook'
|
||||
short_name = 'nook'
|
||||
description = _('This profile is intended for the B&N Nook.')
|
||||
|
||||
# Screen size is a best guess
|
||||
screen_size = (600, 800)
|
||||
dpi = 167
|
||||
fbase = 16
|
||||
fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
|
||||
|
||||
|
||||
input_profiles = [InputProfile, SonyReaderInput, SonyReader300Input,
|
||||
SonyReader900Input, MSReaderInput, MobipocketInput, HanlinV3Input,
|
||||
HanlinV5Input, CybookG3Input, CybookOpusInput, KindleInput, IlliadInput,
|
||||
IRexDR1000Input, IRexDR800Input, NookInput]
|
||||
|
||||
input_profiles.sort(key=lambda x: x.name.lower())
|
||||
|
||||
# }}}
|
||||
|
||||
|
||||
class OutputProfile(Plugin):
|
||||
|
||||
author = 'Kovid Goyal'
|
||||
supported_platforms = {'windows', 'osx', 'linux'}
|
||||
can_be_disabled = False
|
||||
type = _('Output profile')
|
||||
|
||||
name = 'Default Output Profile'
|
||||
short_name = 'default' # Used in the CLI so dont use spaces etc. in it
|
||||
description = _('This profile tries to provide sane defaults and is useful '
|
||||
'if you want to produce a document intended to be read at a '
|
||||
'computer or on a range of devices.')
|
||||
|
||||
#: The image size for comics
|
||||
comic_screen_size = (584, 754)
|
||||
|
||||
#: If True the MOBI renderer on the device supports MOBI indexing
|
||||
supports_mobi_indexing = False
|
||||
|
||||
#: If True output should be optimized for a touchscreen interface
|
||||
touchscreen = False
|
||||
touchscreen_news_css = ''
|
||||
#: A list of extra (beyond CSS 2.1) modules supported by the device
|
||||
#: Format is a css_parser profile dictionary (see iPad for example)
|
||||
extra_css_modules = []
|
||||
#: If True, the date is appended to the title of downloaded news
|
||||
periodical_date_in_title = True
|
||||
|
||||
#: Characters used in jackets and catalogs
|
||||
ratings_char = '*'
|
||||
empty_ratings_char = ' '
|
||||
|
||||
#: Unsupported unicode characters to be replaced during preprocessing
|
||||
unsupported_unicode_chars = []
|
||||
|
||||
#: Number of ems that the left margin of a blockquote is rendered as
|
||||
mobi_ems_per_blockquote = 1.0
|
||||
|
||||
#: Special periodical formatting needed in EPUB
|
||||
epub_periodical_format = None
|
||||
|
||||
|
||||
class iPadOutput(OutputProfile):
|
||||
|
||||
name = 'iPad'
|
||||
short_name = 'ipad'
|
||||
description = _('Intended for the iPad and similar devices with a '
|
||||
'resolution of 768x1024')
|
||||
screen_size = (768, 1024)
|
||||
comic_screen_size = (768, 1024)
|
||||
dpi = 132.0
|
||||
extra_css_modules = [
|
||||
{
|
||||
'name':'webkit',
|
||||
'props': {'-webkit-border-bottom-left-radius':'{length}',
|
||||
'-webkit-border-bottom-right-radius':'{length}',
|
||||
'-webkit-border-top-left-radius':'{length}',
|
||||
'-webkit-border-top-right-radius':'{length}',
|
||||
'-webkit-border-radius': r'{border-width}(\s+{border-width}){0,3}|inherit',
|
||||
},
|
||||
'macros': {'border-width': '{length}|medium|thick|thin'}
|
||||
}
|
||||
]
|
||||
|
||||
ratings_char = '\u2605' # filled star
|
||||
empty_ratings_char = '\u2606' # hollow star
|
||||
|
||||
touchscreen = True
|
||||
# touchscreen_news_css {{{
|
||||
touchscreen_news_css = '''
|
||||
/* hr used in articles */
|
||||
.article_articles_list {
|
||||
width:18%;
|
||||
}
|
||||
.article_link {
|
||||
color: #593f29;
|
||||
font-style: italic;
|
||||
}
|
||||
.article_next {
|
||||
-webkit-border-top-right-radius:4px;
|
||||
-webkit-border-bottom-right-radius:4px;
|
||||
font-style: italic;
|
||||
width:32%;
|
||||
}
|
||||
|
||||
.article_prev {
|
||||
-webkit-border-top-left-radius:4px;
|
||||
-webkit-border-bottom-left-radius:4px;
|
||||
font-style: italic;
|
||||
width:32%;
|
||||
}
|
||||
.article_sections_list {
|
||||
width:18%;
|
||||
}
|
||||
.articles_link {
|
||||
font-weight: bold;
|
||||
}
|
||||
.sections_link {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
.caption_divider {
|
||||
border:#ccc 1px solid;
|
||||
}
|
||||
|
||||
.touchscreen_navbar {
|
||||
background:#c3bab2;
|
||||
border:#ccc 0px solid;
|
||||
border-collapse:separate;
|
||||
border-spacing:1px;
|
||||
margin-left: 5%;
|
||||
margin-right: 5%;
|
||||
page-break-inside:avoid;
|
||||
width: 90%;
|
||||
-webkit-border-radius:4px;
|
||||
}
|
||||
.touchscreen_navbar td {
|
||||
background:#fff;
|
||||
font-family:Helvetica;
|
||||
font-size:80%;
|
||||
/* UI touchboxes use 8px padding */
|
||||
padding: 6px;
|
||||
text-align:center;
|
||||
}
|
||||
|
||||
.touchscreen_navbar td a:link {
|
||||
color: #593f29;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Index formatting */
|
||||
.publish_date {
|
||||
text-align:center;
|
||||
}
|
||||
.divider {
|
||||
border-bottom:1em solid white;
|
||||
border-top:1px solid gray;
|
||||
}
|
||||
|
||||
hr.caption_divider {
|
||||
border-color:black;
|
||||
border-style:solid;
|
||||
border-width:1px;
|
||||
}
|
||||
|
||||
/* Feed summary formatting */
|
||||
.article_summary {
|
||||
display:inline-block;
|
||||
padding-bottom:0.5em;
|
||||
}
|
||||
.feed {
|
||||
font-family:sans-serif;
|
||||
font-weight:bold;
|
||||
font-size:larger;
|
||||
}
|
||||
|
||||
.feed_link {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.feed_next {
|
||||
-webkit-border-top-right-radius:4px;
|
||||
-webkit-border-bottom-right-radius:4px;
|
||||
font-style: italic;
|
||||
width:40%;
|
||||
}
|
||||
|
||||
.feed_prev {
|
||||
-webkit-border-top-left-radius:4px;
|
||||
-webkit-border-bottom-left-radius:4px;
|
||||
font-style: italic;
|
||||
width:40%;
|
||||
}
|
||||
|
||||
.feed_title {
|
||||
text-align: center;
|
||||
font-size: 160%;
|
||||
}
|
||||
|
||||
.feed_up {
|
||||
font-weight: bold;
|
||||
width:20%;
|
||||
}
|
||||
|
||||
.summary_headline {
|
||||
font-weight:bold;
|
||||
text-align:left;
|
||||
}
|
||||
|
||||
.summary_byline {
|
||||
text-align:left;
|
||||
font-family:monospace;
|
||||
}
|
||||
|
||||
.summary_text {
|
||||
text-align:left;
|
||||
}
|
||||
|
||||
'''
|
||||
# }}}
|
||||
|
||||
|
||||
class iPad3Output(iPadOutput):
|
||||
|
||||
screen_size = comic_screen_size = (2048, 1536)
|
||||
dpi = 264.0
|
||||
name = 'iPad 3'
|
||||
short_name = 'ipad3'
|
||||
description = _('Intended for the iPad 3 and similar devices with a '
|
||||
'resolution of 1536x2048')
|
||||
|
||||
|
||||
class TabletOutput(iPadOutput):
|
||||
name = 'Tablet'
|
||||
short_name = 'tablet'
|
||||
description = _('Intended for generic tablet devices, does no resizing of images')
|
||||
|
||||
screen_size = (10000, 10000)
|
||||
comic_screen_size = (10000, 10000)
|
||||
|
||||
|
||||
class SamsungGalaxy(TabletOutput):
|
||||
name = 'Samsung Galaxy'
|
||||
short_name = 'galaxy'
|
||||
description = _('Intended for the Samsung Galaxy and similar tablet devices with '
|
||||
'a resolution of 600x1280')
|
||||
screen_size = comic_screen_size = (600, 1280)
|
||||
|
||||
|
||||
class NookHD(TabletOutput):
|
||||
name = 'Nook HD+'
|
||||
short_name = 'nook_hd_plus'
|
||||
description = _('Intended for the Nook HD+ and similar tablet devices with '
|
||||
'a resolution of 1280x1920')
|
||||
screen_size = comic_screen_size = (1280, 1920)
|
||||
|
||||
|
||||
class SonyReaderOutput(OutputProfile):
|
||||
|
||||
name = 'Sony Reader'
|
||||
short_name = 'sony'
|
||||
description = _('This profile is intended for the SONY PRS line. '
|
||||
'The 500/505/600/700 etc.')
|
||||
|
||||
screen_size = (590, 775)
|
||||
dpi = 168.451
|
||||
fbase = 12
|
||||
fsizes = [7.5, 9, 10, 12, 15.5, 20, 22, 24]
|
||||
unsupported_unicode_chars = [u'\u201f', u'\u201b']
|
||||
|
||||
epub_periodical_format = 'sony'
|
||||
# periodical_date_in_title = False
|
||||
|
||||
|
||||
class KoboReaderOutput(OutputProfile):
|
||||
|
||||
name = 'Kobo Reader'
|
||||
short_name = 'kobo'
|
||||
|
||||
description = _('This profile is intended for the Kobo Reader.')
|
||||
|
||||
screen_size = (536, 710)
|
||||
comic_screen_size = (536, 710)
|
||||
dpi = 168.451
|
||||
fbase = 12
|
||||
fsizes = [7.5, 9, 10, 12, 15.5, 20, 22, 24]
|
||||
|
||||
|
||||
class SonyReader300Output(SonyReaderOutput):
|
||||
|
||||
author = 'John Schember'
|
||||
name = 'Sony Reader 300'
|
||||
short_name = 'sony300'
|
||||
description = _('This profile is intended for the SONY PRS-300.')
|
||||
|
||||
dpi = 200
|
||||
|
||||
|
||||
class SonyReader900Output(SonyReaderOutput):
|
||||
|
||||
author = 'John Schember'
|
||||
name = 'Sony Reader 900'
|
||||
short_name = 'sony900'
|
||||
description = _('This profile is intended for the SONY PRS-900.')
|
||||
|
||||
screen_size = (600, 999)
|
||||
comic_screen_size = screen_size
|
||||
|
||||
|
||||
class SonyReaderT3Output(SonyReaderOutput):
|
||||
|
||||
author = 'Kovid Goyal'
|
||||
name = 'Sony Reader T3'
|
||||
short_name = 'sonyt3'
|
||||
description = _('This profile is intended for the SONY PRS-T3.')
|
||||
|
||||
screen_size = (758, 934)
|
||||
comic_screen_size = screen_size
|
||||
|
||||
|
||||
class GenericEink(SonyReaderOutput):
|
||||
|
||||
name = 'Generic e-ink'
|
||||
short_name = 'generic_eink'
|
||||
description = _('Suitable for use with any e-ink device')
|
||||
epub_periodical_format = None
|
||||
|
||||
|
||||
class GenericEinkLarge(GenericEink):
|
||||
|
||||
name = 'Generic e-ink large'
|
||||
short_name = 'generic_eink_large'
|
||||
description = _('Suitable for use with any large screen e-ink device')
|
||||
|
||||
screen_size = (600, 999)
|
||||
comic_screen_size = screen_size
|
||||
|
||||
|
||||
class GenericEinkHD(GenericEink):
|
||||
|
||||
name = 'Generic e-ink HD'
|
||||
short_name = 'generic_eink_hd'
|
||||
description = _('Suitable for use with any modern high resolution e-ink device')
|
||||
|
||||
screen_size = (10000, 10000)
|
||||
comic_screen_size = (10000, 10000)
|
||||
|
||||
|
||||
class JetBook5Output(OutputProfile):
|
||||
|
||||
name = 'JetBook 5-inch'
|
||||
short_name = 'jetbook5'
|
||||
description = _('This profile is intended for the 5-inch JetBook.')
|
||||
|
||||
screen_size = (480, 640)
|
||||
dpi = 168.451
|
||||
|
||||
|
||||
class SonyReaderLandscapeOutput(SonyReaderOutput):
|
||||
|
||||
name = 'Sony Reader Landscape'
|
||||
short_name = 'sony-landscape'
|
||||
description = _('This profile is intended for the SONY PRS line. '
|
||||
'The 500/505/700 etc, in landscape mode. Mainly useful '
|
||||
'for comics.')
|
||||
|
||||
screen_size = (784, 1012)
|
||||
comic_screen_size = (784, 1012)
|
||||
|
||||
|
||||
class MSReaderOutput(OutputProfile):
|
||||
|
||||
name = 'Microsoft Reader'
|
||||
short_name = 'msreader'
|
||||
description = _('This profile is intended for the Microsoft Reader.')
|
||||
|
||||
screen_size = (480, 652)
|
||||
dpi = 96
|
||||
fbase = 13
|
||||
fsizes = [10, 11, 13, 16, 18, 20, 22, 26]
|
||||
|
||||
|
||||
class MobipocketOutput(OutputProfile):
|
||||
|
||||
name = 'Mobipocket Books'
|
||||
short_name = 'mobipocket'
|
||||
description = _('This profile is intended for the Mobipocket books.')
|
||||
|
||||
# Unfortunately MOBI books are not narrowly targeted, so this information is
|
||||
# quite likely to be spurious
|
||||
screen_size = (600, 800)
|
||||
dpi = 96
|
||||
fbase = 18
|
||||
fsizes = [14, 14, 16, 18, 20, 22, 24, 26]
|
||||
|
||||
|
||||
class HanlinV3Output(OutputProfile):
|
||||
|
||||
name = 'Hanlin V3'
|
||||
short_name = 'hanlinv3'
|
||||
description = _('This profile is intended for the Hanlin V3 and its clones.')
|
||||
|
||||
# Screen size is a best guess
|
||||
screen_size = (584, 754)
|
||||
dpi = 168.451
|
||||
fbase = 16
|
||||
fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
|
||||
|
||||
|
||||
class HanlinV5Output(HanlinV3Output):
|
||||
|
||||
name = 'Hanlin V5'
|
||||
short_name = 'hanlinv5'
|
||||
description = _('This profile is intended for the Hanlin V5 and its clones.')
|
||||
|
||||
dpi = 200
|
||||
|
||||
|
||||
class CybookG3Output(OutputProfile):
|
||||
|
||||
name = 'Cybook G3'
|
||||
short_name = 'cybookg3'
|
||||
description = _('This profile is intended for the Cybook G3.')
|
||||
|
||||
# Screen size is a best guess
|
||||
screen_size = (600, 800)
|
||||
comic_screen_size = (600, 757)
|
||||
dpi = 168.451
|
||||
fbase = 16
|
||||
fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
|
||||
|
||||
|
||||
class CybookOpusOutput(SonyReaderOutput):
|
||||
|
||||
author = 'John Schember'
|
||||
name = 'Cybook Opus'
|
||||
short_name = 'cybook_opus'
|
||||
description = _('This profile is intended for the Cybook Opus.')
|
||||
|
||||
# Screen size is a best guess
|
||||
dpi = 200
|
||||
fbase = 16
|
||||
fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
|
||||
|
||||
epub_periodical_format = None
|
||||
|
||||
|
||||
class KindleOutput(OutputProfile):
|
||||
|
||||
name = 'Kindle'
|
||||
short_name = 'kindle'
|
||||
description = _('This profile is intended for the Amazon Kindle.')
|
||||
|
||||
# Screen size is a best guess
|
||||
screen_size = (525, 640)
|
||||
dpi = 168.451
|
||||
fbase = 16
|
||||
fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
|
||||
supports_mobi_indexing = True
|
||||
periodical_date_in_title = False
|
||||
|
||||
empty_ratings_char = '\u2606'
|
||||
ratings_char = '\u2605'
|
||||
|
||||
mobi_ems_per_blockquote = 2.0
|
||||
|
||||
|
||||
class KindleDXOutput(OutputProfile):
|
||||
|
||||
name = 'Kindle DX'
|
||||
short_name = 'kindle_dx'
|
||||
description = _('This profile is intended for the Amazon Kindle DX.')
|
||||
|
||||
# Screen size is a best guess
|
||||
screen_size = (744, 1022)
|
||||
dpi = 150.0
|
||||
comic_screen_size = (771, 1116)
|
||||
# comic_screen_size = (741, 1022)
|
||||
supports_mobi_indexing = True
|
||||
periodical_date_in_title = False
|
||||
empty_ratings_char = '\u2606'
|
||||
ratings_char = '\u2605'
|
||||
mobi_ems_per_blockquote = 2.0
|
||||
|
||||
|
||||
class KindlePaperWhiteOutput(KindleOutput):
|
||||
|
||||
name = 'Kindle PaperWhite'
|
||||
short_name = 'kindle_pw'
|
||||
description = _('This profile is intended for the Amazon Kindle PaperWhite 1 and 2')
|
||||
|
||||
# Screen size is a best guess
|
||||
screen_size = (658, 940)
|
||||
dpi = 212.0
|
||||
comic_screen_size = screen_size
|
||||
|
||||
|
||||
class KindleVoyageOutput(KindleOutput):
|
||||
|
||||
name = 'Kindle Voyage'
|
||||
short_name = 'kindle_voyage'
|
||||
description = _('This profile is intended for the Amazon Kindle Voyage')
|
||||
|
||||
# Screen size is currently just the spec size, actual renderable area will
|
||||
# depend on someone with the device doing tests.
|
||||
screen_size = (1080, 1430)
|
||||
dpi = 300.0
|
||||
comic_screen_size = screen_size
|
||||
|
||||
|
||||
class KindlePaperWhite3Output(KindleVoyageOutput):
|
||||
|
||||
name = 'Kindle PaperWhite 3'
|
||||
short_name = 'kindle_pw3'
|
||||
description = _('This profile is intended for the Amazon Kindle PaperWhite 3 and above')
|
||||
# Screen size is currently just the spec size, actual renderable area will
|
||||
# depend on someone with the device doing tests.
|
||||
screen_size = (1072, 1430)
|
||||
dpi = 300.0
|
||||
comic_screen_size = screen_size
|
||||
|
||||
|
||||
class KindleOasisOutput(KindlePaperWhite3Output):
|
||||
|
||||
name = 'Kindle Oasis'
|
||||
short_name = 'kindle_oasis'
|
||||
description = _('This profile is intended for the Amazon Kindle Oasis 2017 and above')
|
||||
# Screen size is currently just the spec size, actual renderable area will
|
||||
# depend on someone with the device doing tests.
|
||||
screen_size = (1264, 1680)
|
||||
dpi = 300.0
|
||||
comic_screen_size = screen_size
|
||||
|
||||
|
||||
class KindleFireOutput(KindleDXOutput):
|
||||
|
||||
name = 'Kindle Fire'
|
||||
short_name = 'kindle_fire'
|
||||
description = _('This profile is intended for the Amazon Kindle Fire.')
|
||||
|
||||
screen_size = (570, 1016)
|
||||
dpi = 169.0
|
||||
comic_screen_size = (570, 1016)
|
||||
|
||||
|
||||
class IlliadOutput(OutputProfile):
|
||||
|
||||
name = 'Illiad'
|
||||
short_name = 'illiad'
|
||||
description = _('This profile is intended for the Irex Illiad.')
|
||||
|
||||
screen_size = (760, 925)
|
||||
comic_screen_size = (760, 925)
|
||||
dpi = 160.0
|
||||
fbase = 12
|
||||
fsizes = [7.5, 9, 10, 12, 15.5, 20, 22, 24]
|
||||
|
||||
|
||||
class IRexDR1000Output(OutputProfile):
|
||||
|
||||
author = 'John Schember'
|
||||
name = 'IRex Digital Reader 1000'
|
||||
short_name = 'irexdr1000'
|
||||
description = _('This profile is intended for the IRex Digital Reader 1000.')
|
||||
|
||||
# Screen size is a best guess
|
||||
screen_size = (1024, 1280)
|
||||
comic_screen_size = (996, 1241)
|
||||
dpi = 160
|
||||
fbase = 16
|
||||
fsizes = [12, 14, 16, 18, 20, 22, 24]
|
||||
|
||||
|
||||
class IRexDR800Output(OutputProfile):
|
||||
|
||||
author = 'Eric Cronin'
|
||||
name = 'IRex Digital Reader 800'
|
||||
short_name = 'irexdr800'
|
||||
description = _('This profile is intended for the IRex Digital Reader 800.')
|
||||
|
||||
# Screen size is a best guess
|
||||
screen_size = (768, 1024)
|
||||
comic_screen_size = (768, 1024)
|
||||
dpi = 160
|
||||
fbase = 16
|
||||
fsizes = [12, 14, 16, 18, 20, 22, 24]
|
||||
|
||||
|
||||
class NookOutput(OutputProfile):
|
||||
|
||||
author = 'John Schember'
|
||||
name = 'Nook'
|
||||
short_name = 'nook'
|
||||
description = _('This profile is intended for the B&N Nook.')
|
||||
|
||||
# Screen size is a best guess
|
||||
screen_size = (600, 730)
|
||||
comic_screen_size = (584, 730)
|
||||
dpi = 167
|
||||
fbase = 16
|
||||
fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
|
||||
|
||||
|
||||
class NookColorOutput(NookOutput):
|
||||
name = 'Nook Color'
|
||||
short_name = 'nook_color'
|
||||
description = _('This profile is intended for the B&N Nook Color.')
|
||||
|
||||
screen_size = (600, 900)
|
||||
comic_screen_size = (594, 900)
|
||||
dpi = 169
|
||||
|
||||
|
||||
class PocketBook900Output(OutputProfile):
|
||||
|
||||
author = 'Chris Lockfort'
|
||||
name = 'PocketBook Pro 900'
|
||||
short_name = 'pocketbook_900'
|
||||
description = _('This profile is intended for the PocketBook Pro 900 series of devices.')
|
||||
|
||||
screen_size = (810, 1180)
|
||||
dpi = 150.0
|
||||
comic_screen_size = screen_size
|
||||
|
||||
|
||||
class PocketBookPro912Output(OutputProfile):
|
||||
|
||||
author = 'Daniele Pizzolli'
|
||||
name = 'PocketBook Pro 912'
|
||||
short_name = 'pocketbook_pro_912'
|
||||
description = _('This profile is intended for the PocketBook Pro 912 series of devices.')
|
||||
|
||||
# According to http://download.pocketbook-int.com/user-guides/E_Ink/912/User_Guide_PocketBook_912(EN).pdf
|
||||
screen_size = (825, 1200)
|
||||
dpi = 155.0
|
||||
comic_screen_size = screen_size
|
||||
|
||||
|
||||
output_profiles = [
|
||||
OutputProfile, SonyReaderOutput, SonyReader300Output, SonyReader900Output,
|
||||
SonyReaderT3Output, MSReaderOutput, MobipocketOutput, HanlinV3Output,
|
||||
HanlinV5Output, CybookG3Output, CybookOpusOutput, KindleOutput, iPadOutput,
|
||||
iPad3Output, KoboReaderOutput, TabletOutput, SamsungGalaxy,
|
||||
SonyReaderLandscapeOutput, KindleDXOutput, IlliadOutput, NookHD,
|
||||
IRexDR1000Output, IRexDR800Output, JetBook5Output, NookOutput,
|
||||
NookColorOutput, PocketBook900Output,
|
||||
PocketBookPro912Output, GenericEink, GenericEinkLarge, GenericEinkHD,
|
||||
KindleFireOutput, KindlePaperWhiteOutput, KindleVoyageOutput,
|
||||
KindlePaperWhite3Output, KindleOasisOutput
|
||||
]
|
||||
|
||||
output_profiles.sort(key=lambda x: x.name.lower())
|
||||
835
ebook_converter/customize/ui.py
Normal file
835
ebook_converter/customize/ui.py
Normal file
@@ -0,0 +1,835 @@
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
import os, shutil, traceback, functools, sys
|
||||
from collections import defaultdict
|
||||
from itertools import chain
|
||||
|
||||
from calibre.customize import (CatalogPlugin, FileTypePlugin, PluginNotFound,
|
||||
MetadataReaderPlugin, MetadataWriterPlugin,
|
||||
InterfaceActionBase as InterfaceAction,
|
||||
PreferencesPlugin, platform, InvalidPlugin,
|
||||
StoreBase as Store, EditBookToolPlugin,
|
||||
LibraryClosedPlugin)
|
||||
from calibre.customize.conversion import InputFormatPlugin, OutputFormatPlugin
|
||||
from calibre.customize.zipplugin import loader
|
||||
from calibre.customize.profiles import InputProfile, OutputProfile
|
||||
from calibre.customize.builtins import plugins as builtin_plugins
|
||||
from calibre.devices.interface import DevicePlugin
|
||||
from calibre.ebooks.metadata import MetaInformation
|
||||
from calibre.utils.config import (make_config_dir, Config, ConfigProxy,
|
||||
plugin_dir, OptionParser)
|
||||
from calibre.ebooks.metadata.sources.base import Source
|
||||
from calibre.constants import DEBUG, numeric_version
|
||||
from polyglot.builtins import iteritems, itervalues, unicode_type
|
||||
|
||||
builtin_names = frozenset(p.name for p in builtin_plugins)
|
||||
BLACKLISTED_PLUGINS = frozenset({'Marvin XD', 'iOS reader applications'})
|
||||
|
||||
|
||||
class NameConflict(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def _config():
|
||||
c = 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 ConfigProxy(c)
|
||||
|
||||
|
||||
config = _config()
|
||||
|
||||
|
||||
def find_plugin(name):
|
||||
for plugin in _initialized_plugins:
|
||||
if plugin.name == name:
|
||||
return plugin
|
||||
|
||||
|
||||
def load_plugin(path_to_zip_file): # {{{
|
||||
'''
|
||||
Load plugin from ZIP file or raise InvalidPlugin error
|
||||
|
||||
:return: A :class:`Plugin` instance.
|
||||
'''
|
||||
return loader.load(path_to_zip_file)
|
||||
|
||||
# }}}
|
||||
|
||||
# Enable/disable plugins {{{
|
||||
|
||||
|
||||
def disable_plugin(plugin_or_name):
|
||||
x = getattr(plugin_or_name, 'name', plugin_or_name)
|
||||
plugin = find_plugin(x)
|
||||
if not plugin.can_be_disabled:
|
||||
raise ValueError('Plugin %s cannot be disabled'%x)
|
||||
dp = config['disabled_plugins']
|
||||
dp.add(x)
|
||||
config['disabled_plugins'] = dp
|
||||
ep = config['enabled_plugins']
|
||||
if x in ep:
|
||||
ep.remove(x)
|
||||
config['enabled_plugins'] = ep
|
||||
|
||||
|
||||
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
|
||||
ep = config['enabled_plugins']
|
||||
ep.add(x)
|
||||
config['enabled_plugins'] = ep
|
||||
|
||||
|
||||
def restore_plugin_state_to_default(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
|
||||
ep = config['enabled_plugins']
|
||||
if x in ep:
|
||||
ep.remove(x)
|
||||
config['enabled_plugins'] = ep
|
||||
|
||||
|
||||
default_disabled_plugins = {
|
||||
'Overdrive', 'Douban Books', 'OZON.ru', 'Edelweiss', 'Google Images', 'Big Book Search',
|
||||
}
|
||||
|
||||
|
||||
def is_disabled(plugin):
|
||||
if plugin.name in config['enabled_plugins']:
|
||||
return False
|
||||
return plugin.name in config['disabled_plugins'] or \
|
||||
plugin.name in default_disabled_plugins
|
||||
# }}}
|
||||
|
||||
# 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, _on_postadd
|
||||
_on_import = defaultdict(list)
|
||||
_on_postimport = defaultdict(list)
|
||||
_on_preprocess = defaultdict(list)
|
||||
_on_postprocess = defaultdict(list)
|
||||
_on_postadd = []
|
||||
|
||||
for plugin in _initialized_plugins:
|
||||
if isinstance(plugin, 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 chain(op.get(ft, ()), op.get('*', ())):
|
||||
if not is_disabled(p):
|
||||
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, '')
|
||||
oo, oe = sys.stdout, sys.stderr # Some file type plugins out there override the output streams with buggy implementations
|
||||
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:
|
||||
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:
|
||||
if is_disabled(plugin):
|
||||
continue
|
||||
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, InputProfile):
|
||||
yield plugin
|
||||
|
||||
|
||||
def output_profiles():
|
||||
for plugin in _initialized_plugins:
|
||||
if isinstance(plugin, OutputProfile):
|
||||
yield plugin
|
||||
# }}}
|
||||
|
||||
# Interface Actions # {{{
|
||||
|
||||
|
||||
def interface_actions():
|
||||
customization = config['plugin_customization']
|
||||
for plugin in _initialized_plugins:
|
||||
if isinstance(plugin, InterfaceAction):
|
||||
if not is_disabled(plugin):
|
||||
plugin.site_customization = customization.get(plugin.name, '')
|
||||
yield plugin
|
||||
# }}}
|
||||
|
||||
# Preferences Plugins # {{{
|
||||
|
||||
|
||||
def preferences_plugins():
|
||||
customization = config['plugin_customization']
|
||||
for plugin in _initialized_plugins:
|
||||
if isinstance(plugin, PreferencesPlugin):
|
||||
if not is_disabled(plugin):
|
||||
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, LibraryClosedPlugin):
|
||||
if not is_disabled(plugin):
|
||||
plugin.site_customization = customization.get(plugin.name, '')
|
||||
yield plugin
|
||||
|
||||
|
||||
def has_library_closed_plugins():
|
||||
for plugin in _initialized_plugins:
|
||||
if isinstance(plugin, LibraryClosedPlugin):
|
||||
if not is_disabled(plugin):
|
||||
return True
|
||||
return False
|
||||
# }}}
|
||||
|
||||
# Store Plugins # {{{
|
||||
|
||||
|
||||
def store_plugins():
|
||||
customization = config['plugin_customization']
|
||||
for plugin in _initialized_plugins:
|
||||
if isinstance(plugin, Store):
|
||||
plugin.site_customization = customization.get(plugin.name, '')
|
||||
yield plugin
|
||||
|
||||
|
||||
def available_store_plugins():
|
||||
for plugin in store_plugins():
|
||||
if not is_disabled(plugin):
|
||||
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 = defaultdict(list)
|
||||
_metadata_writers = defaultdict(list)
|
||||
for plugin in _initialized_plugins:
|
||||
if isinstance(plugin, MetadataReaderPlugin):
|
||||
for ft in plugin.file_types:
|
||||
_metadata_readers[ft].append(plugin)
|
||||
elif isinstance(plugin, 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 itervalues(group):
|
||||
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
|
||||
|
||||
|
||||
force_identifiers = ForceIdentifiers()
|
||||
|
||||
|
||||
def get_file_type_metadata(stream, ftype):
|
||||
mi = MetaInformation(None, None)
|
||||
|
||||
ftype = ftype.lower().strip()
|
||||
if ftype in _metadata_readers:
|
||||
for plugin in _metadata_readers[ftype]:
|
||||
if not is_disabled(plugin):
|
||||
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:
|
||||
traceback.print_exc()
|
||||
continue
|
||||
return mi
|
||||
|
||||
|
||||
def set_file_type_metadata(stream, mi, ftype, report_error=None):
|
||||
ftype = ftype.lower().strip()
|
||||
if ftype in _metadata_writers:
|
||||
customization = config['plugin_customization']
|
||||
for plugin in _metadata_writers[ftype]:
|
||||
if not is_disabled(plugin):
|
||||
with plugin:
|
||||
try:
|
||||
plugin.apply_null = apply_null_metadata.apply_null
|
||||
plugin.force_identifiers = force_identifiers.force_identifiers
|
||||
plugin.site_customization = customization.get(plugin.name, '')
|
||||
plugin.set_metadata(stream, mi, ftype.lower().strip())
|
||||
break
|
||||
except:
|
||||
if report_error is None:
|
||||
from calibre 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, ()):
|
||||
if not is_disabled(plugin):
|
||||
return True
|
||||
return False
|
||||
|
||||
# }}}
|
||||
|
||||
# Add/remove plugins {{{
|
||||
|
||||
|
||||
def add_plugin(path_to_zip_file):
|
||||
make_config_dir()
|
||||
plugin = load_plugin(path_to_zip_file)
|
||||
if plugin.name in builtin_names:
|
||||
raise NameConflict(
|
||||
'A builtin plugin with the name %r already exists' % plugin.name)
|
||||
plugin = initialize_plugin(plugin, path_to_zip_file)
|
||||
plugins = config['plugins']
|
||||
zfp = os.path.join(plugin_dir, plugin.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 remove_plugin(plugin_or_name):
|
||||
name = getattr(plugin_or_name, 'name', plugin_or_name)
|
||||
plugins = config['plugins']
|
||||
removed = False
|
||||
if name in plugins:
|
||||
removed = True
|
||||
try:
|
||||
zfp = os.path.join(plugin_dir, name+'.zip')
|
||||
if os.path.exists(zfp):
|
||||
os.remove(zfp)
|
||||
zfp = plugins[name]
|
||||
if os.path.exists(zfp):
|
||||
os.remove(zfp)
|
||||
except:
|
||||
pass
|
||||
plugins.pop(name)
|
||||
config['plugins'] = plugins
|
||||
initialize_plugins()
|
||||
return removed
|
||||
|
||||
# }}}
|
||||
|
||||
# Input/Output format plugins {{{
|
||||
|
||||
|
||||
def input_format_plugins():
|
||||
for plugin in _initialized_plugins:
|
||||
if isinstance(plugin, 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():
|
||||
if not is_disabled(plugin):
|
||||
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, 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():
|
||||
if not is_disabled(plugin):
|
||||
formats.add(plugin.file_type)
|
||||
return formats
|
||||
|
||||
# }}}
|
||||
|
||||
# Catalog plugins {{{
|
||||
|
||||
|
||||
def catalog_plugins():
|
||||
for plugin in _initialized_plugins:
|
||||
if isinstance(plugin, CatalogPlugin):
|
||||
yield plugin
|
||||
|
||||
|
||||
def available_catalog_formats():
|
||||
formats = set()
|
||||
for plugin in catalog_plugins():
|
||||
if not is_disabled(plugin):
|
||||
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
|
||||
|
||||
# }}}
|
||||
|
||||
# Device plugins {{{
|
||||
|
||||
|
||||
def device_plugins(include_disabled=False):
|
||||
for plugin in _initialized_plugins:
|
||||
if isinstance(plugin, DevicePlugin):
|
||||
if include_disabled or not is_disabled(plugin):
|
||||
if platform in plugin.supported_platforms:
|
||||
if getattr(plugin, 'plugin_needs_delayed_initialization',
|
||||
False):
|
||||
plugin.do_delayed_plugin_initialization()
|
||||
yield plugin
|
||||
|
||||
|
||||
def disabled_device_plugins():
|
||||
for plugin in _initialized_plugins:
|
||||
if isinstance(plugin, DevicePlugin):
|
||||
if is_disabled(plugin):
|
||||
if platform in plugin.supported_platforms:
|
||||
yield plugin
|
||||
# }}}
|
||||
|
||||
# Metadata sources2 {{{
|
||||
|
||||
|
||||
def metadata_plugins(capabilities):
|
||||
capabilities = frozenset(capabilities)
|
||||
for plugin in all_metadata_plugins():
|
||||
if plugin.capabilities.intersection(capabilities) and \
|
||||
not is_disabled(plugin):
|
||||
yield plugin
|
||||
|
||||
|
||||
def all_metadata_plugins():
|
||||
for plugin in _initialized_plugins:
|
||||
if isinstance(plugin, Source):
|
||||
yield plugin
|
||||
|
||||
|
||||
def patch_metadata_plugins(possibly_updated_plugins):
|
||||
patches = {}
|
||||
for i, plugin in enumerate(_initialized_plugins):
|
||||
if isinstance(plugin, Source) and plugin.name in builtin_names:
|
||||
pup = possibly_updated_plugins.get(plugin.name)
|
||||
if pup is not None:
|
||||
if pup.version > plugin.version and pup.minimum_calibre_version <= numeric_version:
|
||||
patches[i] = pup(None)
|
||||
# Metadata source plugins dont use initialize() but that
|
||||
# might change in the future, so be safe.
|
||||
patches[i].initialize()
|
||||
for i, pup in iteritems(patches):
|
||||
_initialized_plugins[i] = pup
|
||||
# }}}
|
||||
|
||||
# Editor plugins {{{
|
||||
|
||||
|
||||
def all_edit_book_tool_plugins():
|
||||
for plugin in _initialized_plugins:
|
||||
if isinstance(plugin, 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 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(perf=False):
|
||||
global _initialized_plugins
|
||||
_initialized_plugins = []
|
||||
conflicts = [name for name in config['plugins'] if name in
|
||||
builtin_names]
|
||||
for p in conflicts:
|
||||
remove_plugin(p)
|
||||
external_plugins = config['plugins'].copy()
|
||||
for name in BLACKLISTED_PLUGINS:
|
||||
external_plugins.pop(name, None)
|
||||
ostdout, ostderr = sys.stdout, sys.stderr
|
||||
if perf:
|
||||
from collections import defaultdict
|
||||
import time
|
||||
times = defaultdict(lambda:0)
|
||||
for zfp in list(external_plugins) + builtin_plugins:
|
||||
try:
|
||||
if not isinstance(zfp, type):
|
||||
# We have a plugin name
|
||||
pname = zfp
|
||||
zfp = os.path.join(plugin_dir, zfp+'.zip')
|
||||
if not os.path.exists(zfp):
|
||||
zfp = external_plugins[pname]
|
||||
try:
|
||||
plugin = load_plugin(zfp) if not isinstance(zfp, type) else zfp
|
||||
except PluginNotFound:
|
||||
continue
|
||||
if perf:
|
||||
st = time.time()
|
||||
plugin = initialize_plugin(plugin, None if isinstance(zfp, type) else zfp)
|
||||
if perf:
|
||||
times[plugin.name] = time.time() - st
|
||||
_initialized_plugins.append(plugin)
|
||||
except:
|
||||
print('Failed to initialize plugin:', repr(zfp))
|
||||
if DEBUG:
|
||||
traceback.print_exc()
|
||||
# Prevent a custom plugin from overriding stdout/stderr as this breaks
|
||||
# ipython
|
||||
sys.stdout, sys.stderr = ostdout, ostderr
|
||||
if perf:
|
||||
for x in sorted(times, key=lambda x: times[x]):
|
||||
print('%50s: %.3f'%(x, times[x]))
|
||||
_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 build_plugin(path):
|
||||
from calibre import prints
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
from calibre.utils.zipfile import ZipFile, ZIP_STORED
|
||||
path = unicode_type(path)
|
||||
names = frozenset(os.listdir(path))
|
||||
if '__init__.py' not in names:
|
||||
prints(path, ' is not a valid plugin')
|
||||
raise SystemExit(1)
|
||||
t = PersistentTemporaryFile(u'.zip')
|
||||
with ZipFile(t, 'w', ZIP_STORED) as zf:
|
||||
zf.add_dir(path, simple_filter=lambda x:x in {'.git', '.bzr', '.svn', '.hg'})
|
||||
t.close()
|
||||
plugin = add_plugin(t.name)
|
||||
os.remove(t.name)
|
||||
prints('Plugin updated:', plugin.name, plugin.version)
|
||||
|
||||
|
||||
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('-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
|
||||
|
||||
|
||||
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.build_plugin is not None:
|
||||
build_plugin(opts.build_plugin)
|
||||
if opts.remove_plugin is not None:
|
||||
if remove_plugin(opts.remove_plugin):
|
||||
print('Plugin removed')
|
||||
else:
|
||||
print('No custom plugin named', opts.remove_plugin)
|
||||
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
|
||||
customize_plugin(plugin, custom)
|
||||
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:
|
||||
type_len = name_len = 0
|
||||
for plugin in initialized_plugins():
|
||||
type_len, name_len = max(type_len, len(plugin.type)), max(name_len, len(plugin.name))
|
||||
fmt = '%-{}s%-{}s%-15s%-15s%s'.format(type_len+1, name_len+1)
|
||||
print(fmt%tuple(('Type|Name|Version|Disabled|Site Customization'.split('|'))))
|
||||
print()
|
||||
for plugin in initialized_plugins():
|
||||
print(fmt%(
|
||||
plugin.type, plugin.name,
|
||||
plugin.version, is_disabled(plugin),
|
||||
plugin_customization(plugin)
|
||||
))
|
||||
print('\t', plugin.description)
|
||||
if plugin.is_customizable():
|
||||
try:
|
||||
print('\t', plugin.customization_help())
|
||||
except NotImplementedError:
|
||||
pass
|
||||
print()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
# }}}
|
||||
320
ebook_converter/customize/zipplugin.py
Normal file
320
ebook_converter/customize/zipplugin.py
Normal file
@@ -0,0 +1,320 @@
|
||||
#!/usr/bin/env python2
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os, zipfile, posixpath, importlib, threading, re, imp, sys
|
||||
from collections import OrderedDict
|
||||
from functools import partial
|
||||
|
||||
from calibre import as_unicode
|
||||
from calibre.constants import ispy3
|
||||
from calibre.customize import (Plugin, numeric_version, platform,
|
||||
InvalidPlugin, PluginNotFound)
|
||||
from polyglot.builtins import (itervalues, map, string_or_bytes,
|
||||
unicode_type, reload)
|
||||
|
||||
# PEP 302 based plugin loading mechanism, works around the bug in zipimport in
|
||||
# python 2.x that prevents importing from zip files in locations whose paths
|
||||
# have non ASCII characters
|
||||
|
||||
|
||||
def get_resources(zfp, name_or_list_of_names):
|
||||
'''
|
||||
Load resources from the plugin zip file
|
||||
|
||||
:param name_or_list_of_names: List of paths to resources in the zip file using / as
|
||||
separator, or a single path
|
||||
|
||||
:return: A dictionary of the form ``{name : file_contents}``. Any names
|
||||
that were not found in the zip file will not be present in the
|
||||
dictionary. If a single path is passed in the return value will
|
||||
be just the bytes of the resource or None if it wasn't found.
|
||||
'''
|
||||
names = name_or_list_of_names
|
||||
if isinstance(names, string_or_bytes):
|
||||
names = [names]
|
||||
ans = {}
|
||||
with zipfile.ZipFile(zfp) as zf:
|
||||
for name in names:
|
||||
try:
|
||||
ans[name] = zf.read(name)
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
if len(names) == 1:
|
||||
ans = ans.pop(names[0], None)
|
||||
|
||||
return ans
|
||||
|
||||
|
||||
def get_icons(zfp, name_or_list_of_names):
|
||||
'''
|
||||
Load icons from the plugin zip file
|
||||
|
||||
:param name_or_list_of_names: List of paths to resources in the zip file using / as
|
||||
separator, or a single path
|
||||
|
||||
:return: A dictionary of the form ``{name : QIcon}``. Any names
|
||||
that were not found in the zip file will be null QIcons.
|
||||
If a single path is passed in the return value will
|
||||
be A QIcon.
|
||||
'''
|
||||
from PyQt5.Qt import QIcon, QPixmap
|
||||
names = name_or_list_of_names
|
||||
ans = get_resources(zfp, names)
|
||||
if isinstance(names, string_or_bytes):
|
||||
names = [names]
|
||||
if ans is None:
|
||||
ans = {}
|
||||
if isinstance(ans, string_or_bytes):
|
||||
ans = dict([(names[0], ans)])
|
||||
|
||||
ians = {}
|
||||
for name in names:
|
||||
p = QPixmap()
|
||||
raw = ans.get(name, None)
|
||||
if raw:
|
||||
p.loadFromData(raw)
|
||||
ians[name] = QIcon(p)
|
||||
if len(names) == 1:
|
||||
ians = ians.pop(names[0])
|
||||
return ians
|
||||
|
||||
|
||||
_translations_cache = {}
|
||||
|
||||
|
||||
def load_translations(namespace, zfp):
|
||||
null = object()
|
||||
trans = _translations_cache.get(zfp, null)
|
||||
if trans is None:
|
||||
return
|
||||
if trans is null:
|
||||
from calibre.utils.localization import get_lang
|
||||
lang = get_lang()
|
||||
if not lang or lang == 'en': # performance optimization
|
||||
_translations_cache[zfp] = None
|
||||
return
|
||||
with zipfile.ZipFile(zfp) as zf:
|
||||
try:
|
||||
mo = zf.read('translations/%s.mo' % lang)
|
||||
except KeyError:
|
||||
mo = None # No translations for this language present
|
||||
if mo is None:
|
||||
_translations_cache[zfp] = None
|
||||
return
|
||||
from gettext import GNUTranslations
|
||||
from io import BytesIO
|
||||
trans = _translations_cache[zfp] = GNUTranslations(BytesIO(mo))
|
||||
|
||||
namespace['_'] = getattr(trans, 'gettext' if ispy3 else 'ugettext')
|
||||
namespace['ngettext'] = getattr(trans, 'ngettext' if ispy3 else 'ungettext')
|
||||
|
||||
|
||||
class PluginLoader(object):
|
||||
|
||||
def __init__(self):
|
||||
self.loaded_plugins = {}
|
||||
self._lock = threading.RLock()
|
||||
self._identifier_pat = re.compile(r'[a-zA-Z][_0-9a-zA-Z]*')
|
||||
|
||||
def _get_actual_fullname(self, fullname):
|
||||
parts = fullname.split('.')
|
||||
if parts[0] == 'calibre_plugins':
|
||||
if len(parts) == 1:
|
||||
return parts[0], None
|
||||
plugin_name = parts[1]
|
||||
with self._lock:
|
||||
names = self.loaded_plugins.get(plugin_name, None)
|
||||
if names is None:
|
||||
raise ImportError('No plugin named %r loaded'%plugin_name)
|
||||
names = names[1]
|
||||
fullname = '.'.join(parts[2:])
|
||||
if not fullname:
|
||||
fullname = '__init__'
|
||||
if fullname in names:
|
||||
return fullname, plugin_name
|
||||
if fullname+'.__init__' in names:
|
||||
return fullname+'.__init__', plugin_name
|
||||
return None, None
|
||||
|
||||
def find_module(self, fullname, path=None):
|
||||
fullname, plugin_name = self._get_actual_fullname(fullname)
|
||||
if fullname is None and plugin_name is None:
|
||||
return None
|
||||
return self
|
||||
|
||||
def load_module(self, fullname):
|
||||
import_name, plugin_name = self._get_actual_fullname(fullname)
|
||||
if import_name is None and plugin_name is None:
|
||||
raise ImportError('No plugin named %r is loaded'%fullname)
|
||||
mod = sys.modules.setdefault(fullname, imp.new_module(fullname))
|
||||
mod.__file__ = "<calibre Plugin Loader>"
|
||||
mod.__loader__ = self
|
||||
|
||||
if import_name.endswith('.__init__') or import_name in ('__init__',
|
||||
'calibre_plugins'):
|
||||
# We have a package
|
||||
mod.__path__ = []
|
||||
|
||||
if plugin_name is not None:
|
||||
# We have some actual code to load
|
||||
with self._lock:
|
||||
zfp, names = self.loaded_plugins.get(plugin_name, (None, None))
|
||||
if names is None:
|
||||
raise ImportError('No plugin named %r loaded'%plugin_name)
|
||||
zinfo = names.get(import_name, None)
|
||||
if zinfo is None:
|
||||
raise ImportError('Plugin %r has no module named %r' %
|
||||
(plugin_name, import_name))
|
||||
with zipfile.ZipFile(zfp) as zf:
|
||||
try:
|
||||
code = zf.read(zinfo)
|
||||
except:
|
||||
# Maybe the zip file changed from under us
|
||||
code = zf.read(zinfo.filename)
|
||||
compiled = compile(code, 'calibre_plugins.%s.%s'%(plugin_name,
|
||||
import_name), 'exec', dont_inherit=True)
|
||||
mod.__dict__['get_resources'] = partial(get_resources, zfp)
|
||||
mod.__dict__['get_icons'] = partial(get_icons, zfp)
|
||||
mod.__dict__['load_translations'] = partial(load_translations, mod.__dict__, zfp)
|
||||
exec(compiled, mod.__dict__)
|
||||
|
||||
return mod
|
||||
|
||||
def load(self, path_to_zip_file):
|
||||
if not os.access(path_to_zip_file, os.R_OK):
|
||||
raise PluginNotFound('Cannot access %r'%path_to_zip_file)
|
||||
|
||||
with zipfile.ZipFile(path_to_zip_file) as zf:
|
||||
plugin_name = self._locate_code(zf, path_to_zip_file)
|
||||
|
||||
try:
|
||||
ans = None
|
||||
plugin_module = 'calibre_plugins.%s'%plugin_name
|
||||
m = sys.modules.get(plugin_module, None)
|
||||
if m is not None:
|
||||
reload(m)
|
||||
else:
|
||||
m = importlib.import_module(plugin_module)
|
||||
plugin_classes = []
|
||||
for obj in itervalues(m.__dict__):
|
||||
if isinstance(obj, type) and issubclass(obj, Plugin) and \
|
||||
obj.name != 'Trivial Plugin':
|
||||
plugin_classes.append(obj)
|
||||
if not plugin_classes:
|
||||
raise InvalidPlugin('No plugin class found in %s:%s'%(
|
||||
as_unicode(path_to_zip_file), plugin_name))
|
||||
if len(plugin_classes) > 1:
|
||||
plugin_classes.sort(key=lambda c:(getattr(c, '__module__', None) or '').count('.'))
|
||||
|
||||
ans = plugin_classes[0]
|
||||
|
||||
if ans.minimum_calibre_version > numeric_version:
|
||||
raise InvalidPlugin(
|
||||
'The plugin at %s needs a version of calibre >= %s' %
|
||||
(as_unicode(path_to_zip_file), '.'.join(map(unicode_type,
|
||||
ans.minimum_calibre_version))))
|
||||
|
||||
if platform not in ans.supported_platforms:
|
||||
raise InvalidPlugin(
|
||||
'The plugin at %s cannot be used on %s' %
|
||||
(as_unicode(path_to_zip_file), platform))
|
||||
|
||||
return ans
|
||||
except:
|
||||
with self._lock:
|
||||
del self.loaded_plugins[plugin_name]
|
||||
raise
|
||||
|
||||
def _locate_code(self, zf, path_to_zip_file):
|
||||
names = [x if isinstance(x, unicode_type) else x.decode('utf-8') for x in
|
||||
zf.namelist()]
|
||||
names = [x[1:] if x[0] == '/' else x for x in names]
|
||||
|
||||
plugin_name = None
|
||||
for name in names:
|
||||
name, ext = posixpath.splitext(name)
|
||||
if name.startswith('plugin-import-name-') and ext == '.txt':
|
||||
plugin_name = name.rpartition('-')[-1]
|
||||
|
||||
if plugin_name is None:
|
||||
c = 0
|
||||
while True:
|
||||
c += 1
|
||||
plugin_name = 'dummy%d'%c
|
||||
if plugin_name not in self.loaded_plugins:
|
||||
break
|
||||
else:
|
||||
if self._identifier_pat.match(plugin_name) is None:
|
||||
raise InvalidPlugin((
|
||||
'The plugin at %r uses an invalid import name: %r' %
|
||||
(path_to_zip_file, plugin_name)))
|
||||
|
||||
pynames = [x for x in names if x.endswith('.py')]
|
||||
|
||||
candidates = [posixpath.dirname(x) for x in pynames if
|
||||
x.endswith('/__init__.py')]
|
||||
candidates.sort(key=lambda x: x.count('/'))
|
||||
valid_packages = set()
|
||||
|
||||
for candidate in candidates:
|
||||
parts = candidate.split('/')
|
||||
parent = '.'.join(parts[:-1])
|
||||
if parent and parent not in valid_packages:
|
||||
continue
|
||||
valid_packages.add('.'.join(parts))
|
||||
|
||||
names = OrderedDict()
|
||||
|
||||
for candidate in pynames:
|
||||
parts = posixpath.splitext(candidate)[0].split('/')
|
||||
package = '.'.join(parts[:-1])
|
||||
if package and package not in valid_packages:
|
||||
continue
|
||||
name = '.'.join(parts)
|
||||
names[name] = zf.getinfo(candidate)
|
||||
|
||||
# Legacy plugins
|
||||
if '__init__' not in names:
|
||||
for name in tuple(names):
|
||||
if '.' not in name and name.endswith('plugin'):
|
||||
names['__init__'] = names[name]
|
||||
break
|
||||
|
||||
if '__init__' not in names:
|
||||
raise InvalidPlugin(('The plugin in %r is invalid. It does not '
|
||||
'contain a top-level __init__.py file')
|
||||
% path_to_zip_file)
|
||||
|
||||
with self._lock:
|
||||
self.loaded_plugins[plugin_name] = (path_to_zip_file, names)
|
||||
|
||||
return plugin_name
|
||||
|
||||
|
||||
loader = PluginLoader()
|
||||
sys.meta_path.insert(0, loader)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from tempfile import NamedTemporaryFile
|
||||
from calibre.customize.ui import add_plugin
|
||||
from calibre import CurrentDir
|
||||
path = sys.argv[-1]
|
||||
with NamedTemporaryFile(suffix='.zip') as f:
|
||||
with zipfile.ZipFile(f, 'w') as zf:
|
||||
with CurrentDir(path):
|
||||
for x in os.listdir('.'):
|
||||
if x[0] != '.':
|
||||
print('Adding', x)
|
||||
zf.write(x)
|
||||
if os.path.isdir(x):
|
||||
for y in os.listdir(x):
|
||||
zf.write(os.path.join(x, y))
|
||||
add_plugin(f.name)
|
||||
print('Added plugin from', sys.argv[-1])
|
||||
Reference in New Issue
Block a user