Add support for file type plugins to calibre, so that 3rd party developers can customize calibre.

This commit is contained in:
Kovid Goyal 2008-12-23 20:13:25 -08:00
parent 4d82cb5196
commit 660fa430f8
13 changed files with 134 additions and 24 deletions

View File

@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__appname__ = 'calibre'
__version__ = '0.4.117'
__version__ = '0.4.118'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
'''
Various run time constants.

View File

@ -52,6 +52,12 @@ class Plugin(object):
#: 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):
@ -71,8 +77,7 @@ class Plugin(object):
'''
pass
@classmethod
def customization_help(cls):
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
@ -84,7 +89,9 @@ class Plugin(object):
``self.site_customization``.
Site customization could be anything, for example, the path to
a needed binary on the user's computer.
a needed binary on the user's computer.
:param gui: If True return HTML help, otherwise return plain text help.
'''
raise NotImplementedError
@ -99,10 +106,9 @@ class Plugin(object):
'''
return PersistentTemporaryFile(suffix)
@classmethod
def is_customizable(cls):
def is_customizable(self):
try:
cls.customization_help()
self.customization_help()
return True
except NotImplementedError:
return False

View File

@ -92,10 +92,12 @@ def reread_filetype_plugins():
_on_postprocess[ft].append(plugin)
def _run_filetype_plugins(path_to_file, ft, occasion='preprocess'):
def _run_filetype_plugins(path_to_file, ft=None, occasion='preprocess'):
occasion = {'import':_on_import, 'preprocess':_on_preprocess,
'postprocess':_on_postprocess}[occasion]
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 occasion.get(ft, []):
if is_disabled(plugin):
@ -107,6 +109,10 @@ def _run_filetype_plugins(path_to_file, ft, occasion='preprocess'):
except:
print 'Running file type plugin %s failed with traceback:'%plugin.name
traceback.print_exc()
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,
@ -119,7 +125,7 @@ run_plugins_on_postprocess = functools.partial(_run_filetype_plugins,
def initialize_plugin(plugin, path_to_zip_file):
try:
plugin(path_to_zip_file)
return plugin(path_to_zip_file)
except Exception:
print 'Failed to initialize plugin:', plugin.name, plugin.version
tb = traceback.format_exc()
@ -130,9 +136,9 @@ def initialize_plugin(plugin, path_to_zip_file):
def add_plugin(path_to_zip_file):
make_config_dir()
plugin = load_plugin(path_to_zip_file)
initialize_plugin(plugin, path_to_zip_file)
plugin = initialize_plugin(plugin, path_to_zip_file)
plugins = config['plugins']
zfp = os.path.join(plugin_dir, 'name.zip')
zfp = os.path.join(plugin_dir, plugin.name+'.zip')
if os.path.exists(zfp):
os.remove(zfp)
shutil.copyfile(path_to_zip_file, zfp)
@ -151,6 +157,9 @@ def find_plugin(name):
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
@ -168,7 +177,7 @@ def initialize_plugins():
for zfp in list(config['plugins'].values()) + builtin_plugins:
try:
plugin = load_plugin(zfp) if not isinstance(zfp, type) else zfp
initialize_plugin(plugin, zfp if not isinstance(zfp, type) else zfp)
plugin = initialize_plugin(plugin, zfp if not isinstance(zfp, type) else zfp)
_initialized_plugins.append(plugin)
except:
print 'Failed to initialize plugin...'
@ -199,6 +208,14 @@ def option_parser():
def initialized_plugins():
return _initialized_plugins
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, '')
def main(args=sys.argv):
parser = option_parser()
if len(args) < 2:
@ -214,7 +231,7 @@ def main(args=sys.argv):
if plugin is None:
print 'No plugin with the name %s exists'%name
return 1
config['plugin_customization'][plugin.name] = custom.strip()
customize_plugin(plugin, custom)
if opts.enable_plugin is not None:
enable_plugin(opts.enable_plugin.strip())
if opts.disable_plugin is not None:
@ -227,7 +244,7 @@ def main(args=sys.argv):
print fmt%(
plugin.type, plugin.name,
plugin.version, is_disabled(plugin),
config['plugin_customization'].get(plugin.name, '')
plugin_customization(plugin)
)
print '\t', plugin.description
if plugin.is_customizable():

View File

@ -18,6 +18,7 @@ from calibre.ptempfile import TemporaryDirectory
from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.metadata.opf2 import OPFCreator
from calibre.utils.zipfile import ZipFile
from calibre.customize.ui import run_plugins_on_preprocess
def lit2opf(path, tdir, opts):
from calibre.ebooks.lit.reader import LitReader
@ -118,6 +119,7 @@ def unarchive(path, tdir):
def any2epub(opts, path, notification=None, create_epub=True,
oeb_cover=False, extract_to=None):
path = run_plugins_on_preprocess(path)
ext = os.path.splitext(path)[1]
if not ext:
raise ValueError('Unknown file type: '+path)

View File

@ -48,6 +48,7 @@ from calibre.ebooks.epub import initialize_container, PROFILES
from calibre.ebooks.epub.split import split
from calibre.ebooks.epub.fonts import Rationalizer
from calibre.constants import preferred_encoding
from calibre.customize.ui import run_plugins_on_postprocess
from calibre import walk, CurrentDir, to_unicode
content = functools.partial(os.path.join, u'content')
@ -386,6 +387,7 @@ def convert(htmlfile, opts, notification=None, create_epub=True,
epub = initialize_container(opts.output)
epub.add_dir(tdir)
epub.close()
run_plugins_on_postprocess(opts.output, 'epub')
logger.info(_('Output written to ')+opts.output)
if opts.show_opf:

View File

@ -34,6 +34,7 @@ from calibre import LoggingInterface
from calibre import plugins
msdes, msdeserror = plugins['msdes']
import calibre.ebooks.lit.mssha1 as mssha1
from calibre.customize.ui import run_plugins_on_postprocess
__all__ = ['LitWriter']
@ -734,6 +735,7 @@ def oeb2lit(opts, opfpath):
lit = LitWriter(OEBBook(opfpath, logger=logger), logger=logger)
with open(litpath, 'wb') as f:
lit.dump(f)
run_plugins_on_postprocess(litpath, 'lit')
logger.log_info(_('Output written to ')+litpath)

View File

@ -18,6 +18,8 @@ from calibre.ebooks.lrf.epub.convert_from import process_file as epub2lrf
from calibre.ebooks.lrf.mobi.convert_from import process_file as mobi2lrf
from calibre.ebooks.lrf.fb2.convert_from import process_file as fb22lrf
from calibre.customize.ui import run_plugins_on_postprocess, run_plugins_on_preprocess
def largest_file(files):
maxsize, file = 0, None
for f in files:
@ -108,6 +110,7 @@ def odt2lrf(path, options, logger):
def process_file(path, options, logger=None):
path = os.path.abspath(os.path.expanduser(path))
path = run_plugins_on_preprocess(path)
tdir = None
if logger is None:
level = logging.DEBUG if options.verbose else logging.INFO
@ -160,6 +163,7 @@ def process_file(path, options, logger=None):
if not convertor:
raise UnknownFormatError(_('Converting from %s to LRF is not supported.')%ext)
convertor(path, options, logger)
finally:
os.chdir(cwd)
if tdir and os.path.exists(tdir):

View File

@ -19,6 +19,7 @@ from calibre.ebooks.lrf.pylrs.pylrs import Book, BookSetting, ImageStream, Image
from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.metadata.opf import OPFCreator
from calibre.ebooks.epub.from_html import config as html2epub_config, convert as html2epub
from calibre.customize.ui import run_plugins_on_preprocess
try:
from calibre.utils.PythonMagickWand import \
NewMagickWand, NewPixelWand, \
@ -383,7 +384,9 @@ def create_lrf(pages, profile, opts, thumbnail=None):
def do_convert(path_to_file, opts, notification=lambda m, p: p, output_format='lrf'):
path_to_file = run_plugins_on_preprocess(path_to_file)
source = path_to_file
if not opts.title:
opts.title = os.path.splitext(os.path.basename(source))[0]
if not opts.output:

View File

@ -12,6 +12,7 @@ from urllib import unquote
from urlparse import urlparse
from math import ceil, floor
from functools import partial
from calibre.customize.ui import run_plugins_on_postprocess
try:
from PIL import Image as PILImage
@ -1931,6 +1932,7 @@ def process_file(path, options, logger=None):
oname = os.path.join(os.getcwd(), name)
oname = os.path.abspath(os.path.expanduser(oname))
conv.writeto(oname, lrs=options.lrs)
run_plugins_on_postprocess(oname, 'lrf')
logger.info('Output written to %s', oname)
conv.cleanup()
return oname

View File

@ -6,19 +6,21 @@ from PyQt4.Qt import QDialog, QMessageBox, QListWidgetItem, QIcon, \
QDesktopServices, QVBoxLayout, QLabel, QPlainTextEdit, \
QStringListModel, QAbstractItemModel, \
SIGNAL, QTimer, Qt, QSize, QVariant, QUrl, \
QModelIndex
QModelIndex, QInputDialog
from calibre.constants import islinux, iswindows
from calibre.gui2.dialogs.config_ui import Ui_Dialog
from calibre.gui2 import qstring_to_unicode, choose_dir, error_dialog, config, \
ALL_COLUMNS, NONE
ALL_COLUMNS, NONE, info_dialog, choose_files
from calibre.utils.config import prefs
from calibre.gui2.widgets import FilenamePattern
from calibre.gui2.library import BooksModel
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.epub.iterator import is_supported
from calibre.library import server_config
from calibre.customize.ui import initialized_plugins, is_disabled
from calibre.customize.ui import initialized_plugins, is_disabled, enable_plugin, \
disable_plugin, customize_plugin, \
plugin_customization, add_plugin
class PluginModel(QAbstractItemModel):
@ -64,7 +66,21 @@ class PluginModel(QAbstractItemModel):
def index_to_plugin(self, index):
category = self.categories[index.parent().row()]
return self._data[category][index.row()]
def plugin_to_index(self, plugin):
for i, category in enumerate(self.categories):
parent = self.index(i, 0, QModelIndex())
for j, p in enumerate(self._data[category]):
if plugin == p:
return self.index(j, 0, parent)
return QModelIndex()
def refresh_plugin(self, plugin, rescan=False):
if rescan:
self.populate()
idx = self.plugin_to_index(plugin)
self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'), idx, idx)
def flags(self, index):
if not index.isValid():
return 0
@ -86,8 +102,12 @@ class PluginModel(QAbstractItemModel):
plugin = self.index_to_plugin(index)
if role == Qt.DisplayRole:
ver = '.'.join(map(str, plugin.version))
desc = '\n'.join(textwrap.wrap(plugin.description, 70))
return QVariant('%s (%s) by %s\n%s'%(plugin.name, ver, plugin.author, desc))
desc = '\n'.join(textwrap.wrap(plugin.description, 50))
ans='%s (%s) by %s\n%s'%(plugin.name, ver, plugin.author, desc)
c = plugin_customization(plugin)
if c:
ans += '\nCustomization: '+c
return QVariant(ans)
if role == Qt.DecorationRole:
return self.icon
if role == Qt.UserRole:
@ -220,6 +240,54 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.category_view.setCurrentIndex(self._category_model.index(0))
self._plugin_model = PluginModel()
self.plugin_view.setModel(self._plugin_model)
self.connect(self.toggle_plugin, SIGNAL('clicked()'), lambda : self.modify_plugin(op='toggle'))
self.connect(self.customize_plugin, SIGNAL('clicked()'), lambda : self.modify_plugin(op='customize'))
self.connect(self.button_plugin_browse, SIGNAL('clicked()'), self.find_plugin)
self.connect(self.button_plugin_add, SIGNAL('clicked()'), self.add_plugin)
def add_plugin(self):
path = unicode(self.plugin_path.text())
if path and os.access(path, os.R_OK) and path.lower().endswith('.zip'):
add_plugin(path)
self._plugin_model.populate()
self._plugin_model.reset()
else:
error_dialog(self, _('No valid plugin path'),
_('%s is not a valid plugin path')%path).exec_()
def find_plugin(self):
path = choose_files(self, 'choose plugin dialog', _('Choose plugin'),
filters=[('Plugins', ['zip'])], all_files=False,
select_only_single_file=True)
if path:
self.plugin_path.setText(path[0])
def modify_plugin(self, op=''):
index = self.plugin_view.currentIndex()
if index.isValid():
plugin = self._plugin_model.index_to_plugin(index)
if not plugin.can_be_disabled:
error_dialog(self,_('Plugin cannot be disabled'),
_('The plugin %s cannot be disabled')%plugin.name).exec_()
return
if op == 'toggle':
if is_disabled(plugin):
enable_plugin(plugin)
else:
disable_plugin(plugin)
self._plugin_model.refresh_plugin(plugin)
if op == 'customize':
if not plugin.is_customizable():
info_dialog(self, _('Plugin not customizable'),
_('Plugin %s does not need customization')%plugin.name).exec_()
return
help = plugin.customization_help()
text, ok = QInputDialog.getText(self, _('Customize %s')%plugin.name,
help)
if ok:
customize_plugin(plugin, unicode(text))
self._plugin_model.refresh_plugin(plugin)
def up_column(self):
idx = self.columns.currentRow()

View File

@ -839,6 +839,10 @@
<property name="text" >
<string>...</string>
</property>
<property name="icon" >
<iconset resource="../images.qrc" >
<normaloff>:/images/document_open.svg</normaloff>:/images/document_open.svg</iconset>
</property>
</widget>
</item>
</layout>

View File

@ -85,11 +85,9 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
QErrorMessage(self.window).showMessage("You do not have "+\
"permission to read the file: " + _file)
continue
_file = run_plugins_on_import(_file, os.path.splitext(_file)[1].lower())
_file = run_plugins_on_import(_file)
size = os.stat(_file).st_size
ext = os.path.splitext(_file)[1].lower()
if '.' in ext:
ext = ext.replace('.', '')
ext = os.path.splitext(_file)[1].lower().replace('.', '')
for row in range(self.formats.count()):
fmt = self.formats.item(row)
if fmt.ext == ext:

View File

@ -661,6 +661,7 @@ class LibraryDatabase2(LibraryDatabase):
def add_format_with_hooks(self, index, format, fpath, index_is_id=False,
path=None, notify=True):
npath = self.run_import_plugins(fpath, format)
format = os.path.splitext(npath)[-1].lower().replace('.', '').upper()
return self.add_format(index, format, open(npath, 'rb'),
index_is_id=index_is_id, path=path, notify=notify)
@ -1126,6 +1127,7 @@ class LibraryDatabase2(LibraryDatabase):
self.conn.commit()
self.set_metadata(id, mi)
npath = self.run_import_plugins(path, format)
format = os.path.splitext(npath)[-1].lower().replace('.', '').upper()
stream = open(npath, 'rb')
self.add_format(id, format, stream, index_is_id=True)
stream.close()