mirror of
https://github.com/gryf/ebook-converter.git
synced 2026-01-01 15:32:26 +01:00
Removed some more code
This commit is contained in:
@@ -1,12 +1,11 @@
|
||||
import os, sys, zipfile, importlib
|
||||
import importlib
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
from ebook_converter.constants import numeric_version, iswindows, isosx
|
||||
from ebook_converter.ptempfile import PersistentTemporaryFile
|
||||
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
platform = 'linux'
|
||||
if iswindows:
|
||||
platform = 'windows'
|
||||
@@ -22,8 +21,8 @@ class InvalidPlugin(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class Plugin(object): # {{{
|
||||
'''
|
||||
class Plugin(object):
|
||||
"""
|
||||
A calibre plugin. Useful members include:
|
||||
|
||||
* ``self.plugin_path``: Stores path to the ZIP file that contains
|
||||
@@ -43,23 +42,23 @@ class Plugin(object): # {{{
|
||||
* :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'
|
||||
name = 'Trivial Plugin'
|
||||
|
||||
#: The version of this plugin as a 3-tuple (major, minor, revision)
|
||||
version = (1, 0, 0)
|
||||
version = (1, 0, 0)
|
||||
|
||||
#: A short string describing what this plugin does
|
||||
description = _('Does absolutely nothing')
|
||||
description = _('Does absolutely nothing')
|
||||
|
||||
#: The author of this plugin
|
||||
author = _('Unknown')
|
||||
author = _('Unknown')
|
||||
|
||||
#: When more than one plugin exists for a filetype,
|
||||
#: the plugins are run in order of decreasing priority.
|
||||
@@ -80,11 +79,11 @@ class Plugin(object): # {{{
|
||||
type = _('Base')
|
||||
|
||||
def __init__(self, plugin_path):
|
||||
self.plugin_path = 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
|
||||
@@ -94,12 +93,13 @@ class Plugin(object): # {{{
|
||||
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.
|
||||
'''
|
||||
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.
|
||||
@@ -113,98 +113,20 @@ class Plugin(object): # {{{
|
||||
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 ebook_converter.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 ebook_converter.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 ebook_converter.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 = str(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.
|
||||
|
||||
@@ -214,13 +136,14 @@ class Plugin(object): # {{{
|
||||
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
|
||||
: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 = {}
|
||||
@@ -231,7 +154,7 @@ class Plugin(object): # {{{
|
||||
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.
|
||||
@@ -246,18 +169,18 @@ class Plugin(object): # {{{
|
||||
|
||||
: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):
|
||||
@@ -268,17 +191,18 @@ class Plugin(object): # {{{
|
||||
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::
|
||||
"""
|
||||
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 ebook_converter.utils.zipfile import ZipFile
|
||||
zf = ZipFile(self.plugin_path)
|
||||
extensions = {x.rpartition('.')[-1].lower() for x in
|
||||
zf.namelist()}
|
||||
zf.namelist()}
|
||||
zip_safe = True
|
||||
for ext in ('pyd', 'so', 'dll', 'dylib'):
|
||||
if ext in extensions:
|
||||
@@ -290,52 +214,51 @@ class Plugin(object): # {{{
|
||||
else:
|
||||
from ebook_converter.ptempfile import TemporaryDirectory
|
||||
self._sys_insertion_tdir = TemporaryDirectory('plugin_unzip')
|
||||
self.sys_insertion_path = self._sys_insertion_tdir.__enter__(*args)
|
||||
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)
|
||||
ip = getattr(self, 'sys_insertion_path', None),
|
||||
it = 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)
|
||||
|
||||
# }}}
|
||||
% self.name)
|
||||
|
||||
|
||||
class FileTypePlugin(Plugin): # {{{
|
||||
'''
|
||||
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()
|
||||
file_types = set()
|
||||
|
||||
#: If True, this plugin is run when books are added
|
||||
#: to the database
|
||||
on_import = False
|
||||
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
|
||||
on_postimport = False
|
||||
|
||||
#: If True, this plugin is run just before a conversion
|
||||
on_preprocess = False
|
||||
on_preprocess = False
|
||||
|
||||
#: If True, this plugin is run after conversion
|
||||
#: on the final file produced by the conversion output plugin.
|
||||
@@ -344,7 +267,7 @@ class FileTypePlugin(Plugin): # {{{
|
||||
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
|
||||
@@ -352,8 +275,8 @@ class FileTypePlugin(Plugin): # {{{
|
||||
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 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.
|
||||
@@ -361,55 +284,56 @@ class FileTypePlugin(Plugin): # {{{
|
||||
: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.
|
||||
"""
|
||||
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.
|
||||
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.
|
||||
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): # {{{
|
||||
'''
|
||||
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()
|
||||
file_types = set()
|
||||
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
version = numeric_version
|
||||
author = 'Kovid Goyal'
|
||||
author = 'Kovid Goyal'
|
||||
|
||||
type = _('Metadata reader')
|
||||
|
||||
@@ -418,30 +342,30 @@ class MetadataReaderPlugin(Plugin): # {{{
|
||||
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:`ebook_converter.ebooks.metadata.book.Metadata` object
|
||||
'''
|
||||
in :attr:`file_types`.
|
||||
:return: A :class:`ebook_converter.ebooks.metadata.book.Metadata`
|
||||
object
|
||||
"""
|
||||
return None
|
||||
# }}}
|
||||
|
||||
|
||||
class MetadataWriterPlugin(Plugin): # {{{
|
||||
'''
|
||||
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()
|
||||
file_types = set()
|
||||
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
version = numeric_version
|
||||
author = 'Kovid Goyal'
|
||||
author = 'Kovid Goyal'
|
||||
|
||||
type = _('Metadata writer')
|
||||
|
||||
@@ -450,24 +374,23 @@ class MetadataWriterPlugin(Plugin): # {{{
|
||||
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:`ebook_converter.ebooks.metadata.book.Metadata` object
|
||||
'''
|
||||
in :attr:`file_types`.
|
||||
:param mi: A :class:`ebook_converter.ebooks.metadata.book.Metadata`
|
||||
object
|
||||
"""
|
||||
pass
|
||||
|
||||
# }}}
|
||||
|
||||
|
||||
class CatalogPlugin(Plugin): # {{{
|
||||
'''
|
||||
class CatalogPlugin(Plugin):
|
||||
"""
|
||||
A plugin that implements a catalog generator.
|
||||
'''
|
||||
"""
|
||||
|
||||
resources_path = None
|
||||
|
||||
@@ -477,20 +400,24 @@ class CatalogPlugin(Plugin): # {{{
|
||||
|
||||
type = _('Catalog generator')
|
||||
|
||||
#: CLI parser options specific to this plugin, declared as namedtuple Option:
|
||||
#: 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 ebook_converter.db.cli.cmd_catalog:option_parser()
|
||||
#: dest = 'catalog_title', help = (_('Title of generated catalog. '
|
||||
#: '\nDefault:') +
|
||||
#: " '" + '%default' + "'"))]
|
||||
#: cli_options parsed in
|
||||
#: ebook_converter.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:
|
||||
@@ -507,10 +434,11 @@ class CatalogPlugin(Plugin): # {{{
|
||||
|
||||
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_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]
|
||||
@@ -527,48 +455,40 @@ class CatalogPlugin(Plugin): # {{{
|
||||
if requested_fields - all_fields:
|
||||
from ebook_converter.library import current_library_name
|
||||
invalid_fields = sorted(list(requested_fields - all_fields))
|
||||
print("invalid --fields specified: %s" % ', '.join(invalid_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")
|
||||
(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:
|
||||
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
|
||||
ebook_converter.gui2.dialogs.catalog.py:Catalog
|
||||
'''
|
||||
Tab will be dynamically generated and added to the Catalog Options
|
||||
dialog in ebook_converter.gui2.dialogs.catalog.py:Catalog
|
||||
"""
|
||||
|
||||
# TODO(gryf): remove this entire abomination in favor of map and lazy
|
||||
# importing if needed.
|
||||
from ebook_converter.customize.builtins import plugins as builtin_plugins
|
||||
from ebook_converter.customize.ui import config
|
||||
from ebook_converter.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()
|
||||
if not type(self) in builtin_plugins:
|
||||
raise ValueError(f'Plugin type "{self.__str__}" not found')
|
||||
|
||||
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
|
||||
@@ -581,18 +501,16 @@ class CatalogPlugin(Plugin): # {{{
|
||||
: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')
|
||||
|
||||
# }}}
|
||||
'method, should be overridden in subclass')
|
||||
|
||||
|
||||
class InterfaceActionBase(Plugin): # {{{
|
||||
class InterfaceActionBase(Plugin):
|
||||
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
author = 'Kovid Goyal'
|
||||
author = 'Kovid Goyal'
|
||||
type = _('User interface action')
|
||||
can_be_disabled = False
|
||||
|
||||
@@ -603,9 +521,9 @@ class InterfaceActionBase(Plugin): # {{{
|
||||
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(':')
|
||||
@@ -614,20 +532,18 @@ class InterfaceActionBase(Plugin): # {{{
|
||||
self.actual_plugin_ = ac
|
||||
return ac
|
||||
|
||||
# }}}
|
||||
|
||||
class PreferencesPlugin(Plugin):
|
||||
|
||||
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'
|
||||
author = 'Kovid Goyal'
|
||||
type = _('Preferences')
|
||||
can_be_disabled = False
|
||||
|
||||
@@ -636,7 +552,8 @@ class PreferencesPlugin(Plugin): # {{{
|
||||
#: :meth:`create_widget`.
|
||||
config_widget = None
|
||||
|
||||
#: Where in the list of categories the :attr:`category` of this plugin should be.
|
||||
#: 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
|
||||
@@ -659,14 +576,14 @@ class PreferencesPlugin(Plugin): # {{{
|
||||
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:`ebook_converter.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'
|
||||
@@ -674,20 +591,18 @@ class PreferencesPlugin(Plugin): # {{{
|
||||
widget = getattr(base, wc)
|
||||
return widget(parent)
|
||||
|
||||
# }}}
|
||||
|
||||
|
||||
class StoreBase(Plugin): # {{{
|
||||
class StoreBase(Plugin):
|
||||
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
author = 'John Schember'
|
||||
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)
|
||||
version = (1, 0, 1)
|
||||
|
||||
actual_plugin = None
|
||||
|
||||
@@ -702,11 +617,12 @@ class StoreBase(Plugin): # {{{
|
||||
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)
|
||||
self.actual_plugin_object = getattr(importlib.import_module(mod),
|
||||
cls)(gui, self.name)
|
||||
return self.actual_plugin_object
|
||||
|
||||
def customization_help(self, gui=False):
|
||||
@@ -724,35 +640,30 @@ class StoreBase(Plugin): # {{{
|
||||
return self.actual_plugin_object.save_settings(config_widget)
|
||||
raise NotImplementedError()
|
||||
|
||||
# }}}
|
||||
|
||||
|
||||
class EditBookToolPlugin(Plugin): # {{{
|
||||
class EditBookToolPlugin(Plugin):
|
||||
|
||||
type = _('Edit book tool')
|
||||
minimum_calibre_version = (1, 46, 0)
|
||||
|
||||
# }}}
|
||||
|
||||
|
||||
class LibraryClosedPlugin(Plugin): # {{{
|
||||
'''
|
||||
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')
|
||||
# }}}
|
||||
'run method must be overridden in subclass')
|
||||
|
||||
@@ -109,208 +109,3 @@ def load_translations(namespace, zfp):
|
||||
|
||||
namespace['_'] = getattr(trans, 'gettext')
|
||||
namespace['ngettext'] = getattr(trans, 'ngettext')
|
||||
|
||||
|
||||
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 m.__dict__.values():
|
||||
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(str,
|
||||
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, str) 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 ebook_converter.customize.ui import add_plugin
|
||||
from ebook_converter 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