mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Add support for file type plugins to calibre, so that 3rd party developers can customize calibre.
This commit is contained in:
parent
4d82cb5196
commit
660fa430f8
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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():
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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>
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user