diff --git a/installer/linux/freeze.py b/installer/linux/freeze.py
index 4fdeb69c37..32581abee2 100644
--- a/installer/linux/freeze.py
+++ b/installer/linux/freeze.py
@@ -36,7 +36,9 @@ def freeze():
'/usr/lib/libpoppler.so.4',
'/usr/lib/libxml2.so.2',
'/usr/lib/libxslt.so.1',
- '/usr/lib/libxslt.so.1'
+ '/usr/lib/libxslt.so.1',
+ '/usr/lib/libMagickWand.so',
+ '/usr/lib/libMagickCore.so',
]
binary_includes += [os.path.join(QTDIR, 'lib%s.so.4'%x) for x in QTDLLS]
@@ -158,8 +160,8 @@ def freeze():
dest = os.path.join(dest_dir, os.path.basename(src))
if not os.path.exists(dest_dir):
os.makedirs(dest_dir)
- shutil.copyfile(src, dest)
- shutil.copymode(src, dest)
+ shutil.copyfile(os.path.realpath(src), dest)
+ shutil.copymode(os.path.realpath(src), dest)
for f in binary_includes:
copy_binary(f, FREEZE_DIR)
diff --git a/setup.py b/setup.py
index 5438dffd7a..d31ea9d523 100644
--- a/setup.py
+++ b/setup.py
@@ -47,6 +47,7 @@ main_functions = {
if __name__ == '__main__':
from setuptools import setup, find_packages
+ from setuptools.command.build_py import build_py as _build_py, convert_path
from distutils.command.build import build as _build
from distutils.core import Command as _Command
from pyqtdistutils import PyQtExtension, build_ext, Extension
@@ -65,6 +66,25 @@ if __name__ == '__main__':
newest_source, oldest_target = max(stimes), min(ttimes)
return newest_source > oldest_target
+ class build_py(_build_py):
+
+ def find_data_files(self, package, src_dir):
+ """
+ Return filenames for package's data files in 'src_dir'
+ Modified to treat data file specs as paths not globs
+ """
+ globs = (self.package_data.get('', [])
+ + self.package_data.get(package, []))
+ files = self.manifest_files.get(package, [])[:]
+ for pattern in globs:
+ # Each pattern has to be converted to a platform-specific path
+ pattern = os.path.join(src_dir, convert_path(pattern))
+ next = glob.glob(pattern)
+ files.extend(next if next else [pattern])
+
+ return self.exclude_data_files(package, src_dir, files)
+
+
class Command(_Command):
user_options = []
def initialize_options(self): pass
@@ -252,6 +272,7 @@ if __name__ == '__main__':
description='''Compile all GUI forms and images'''
PATH = os.path.join('src', APPNAME, 'gui2')
IMAGES_DEST = os.path.join(PATH, 'images_rc.py')
+ QRC = os.path.join(PATH, 'images.qrc')
@classmethod
def find_forms(cls):
@@ -331,9 +352,9 @@ if __name__ == '__main__':
c = cls.form_to_compiled_form(form)
if os.path.exists(c):
os.remove(c)
- images = cls.IMAGES_DEST
- if os.path.exists(images):
- os.remove(images)
+ for x in (cls.IMAGES_DEST, cls.QRC):
+ if os.path.exists(x):
+ os.remove(x)
class clean(Command):
description='''Delete all computer generated files in the source tree'''
@@ -349,17 +370,13 @@ if __name__ == '__main__':
os.remove(f)
for root, dirs, files in os.walk('.'):
for name in files:
- if name.endswith('~') or \
- name.endswith('.pyc') or \
- name.endswith('.pyo'):
- os.remove(os.path.join(root, name))
+ for t in ('.pyc', '.pyo', '~'):
+ if name.endswith(t):
+ os.remove(os.path.join(root, name))
+ break
- for dir in 'build', 'dist':
- for f in os.listdir(dir):
- if os.path.isdir(dir + os.sep + f):
- shutil.rmtree(dir + os.sep + f)
- else:
- os.remove(dir + os.sep + f)
+ for dir in ('build', 'dist', os.path.join('src', 'calibre.egg-info')):
+ shutil.rmtree(dir, ignore_errors=True)
class build(_build):
@@ -405,6 +422,8 @@ if __name__ == '__main__':
extra_link_args=['-framework', 'IOKit'])
)
+ plugins = ['plugins/%s.so'%(x.name.rpartition('.')[-1]) for x in ext_modules]
+
setup(
name = APPNAME,
packages = find_packages('src'),
@@ -413,8 +432,7 @@ if __name__ == '__main__':
author = 'Kovid Goyal',
author_email = 'kovid@kovidgoyal.net',
url = 'http://%s.kovidgoyal.net'%APPNAME,
- package_data = {'calibre':['plugins/*']},
- include_package_data = True,
+ package_data = {'calibre':plugins},
entry_points = entry_points,
zip_safe = False,
options = { 'bdist_egg' : {'exclude_source_files': True,}, },
@@ -455,7 +473,8 @@ if __name__ == '__main__':
],
cmdclass = {
'build_ext' : build_ext,
- 'build' : build,
+ 'build' : build,
+ 'build_py' : build_py,
'pot' : pot,
'manual' : manual,
'resources' : resources,
diff --git a/src/calibre/constants.py b/src/calibre/constants.py
index 01f77f9cfb..bfbebd5273 100644
--- a/src/calibre/constants.py
+++ b/src/calibre/constants.py
@@ -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.121'
__author__ = "Kovid Goyal "
'''
Various run time constants.
diff --git a/src/calibre/customize/__init__.py b/src/calibre/customize/__init__.py
new file mode 100644
index 0000000000..3d48f42535
--- /dev/null
+++ b/src/calibre/customize/__init__.py
@@ -0,0 +1,223 @@
+from __future__ import with_statement
+__license__ = 'GPL v3'
+__copyright__ = '2008, Kovid Goyal '
+
+import sys
+
+from calibre.ptempfile import PersistentTemporaryFile
+from calibre.constants import __version__, __author__
+
+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`
+
+ '''
+ #: List of platforms this plugin works on
+ #: For example: ``['windows', 'osx', 'linux']
+ supported_platforms = []
+
+ #: The name of this plugin
+ 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
+ #: i.e. plugins with higher priority will be run first.
+ #: The highest possible priority is ``sys.maxint``.
+ #: Default pririty 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.
+
+ 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 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):
+ if self.plugin_path is not None:
+ sys.path.insert(0, self.plugin_path)
+
+ def __exit__(self, *args):
+ if self.plugin_path in sys.path:
+ sys.path.remove(self.plugin_path)
+
+
+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
+ #: For example: ``set(['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 whenever an any2* tool
+ #: is used, on the file passed to the any2* tool.
+ on_preprocess = False
+
+ #: If True, this plugin is run after an any2* tool is
+ #: used, on the final file produced by the tool.
+ 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 ebook and return the absolute path to the
+ modified ebook. If no modifications are needed, it should
+ return the path to the original ebook. If an error is encountered
+ it should raise an Exception. The default implementation
+ simply return the path to the original ebook.
+
+ The modified ebook file should be created with the
+ :meth:`temporary_file` method.
+
+ :param path_to_ebook: Absolute path to the ebook.
+
+ :return: Absolute path to the modified ebook.
+ '''
+ # Default implementation does nothing
+ return path_to_ebook
+
+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 = tuple(map(int, (__version__.split('.'))[:3]))
+ author = 'Kovid Goyal'
+
+ type = _('Metadata reader')
+
+ 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.MetaInformation` 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 = tuple(map(int, (__version__.split('.'))[:3]))
+ author = 'Kovid Goyal'
+
+ type = _('Metadata writer')
+
+ 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.MetaInformation` object
+
+ '''
+ pass
+
diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py
new file mode 100644
index 0000000000..28be488dce
--- /dev/null
+++ b/src/calibre/customize/builtins.py
@@ -0,0 +1,205 @@
+from __future__ import with_statement
+__license__ = 'GPL v3'
+__copyright__ = '2008, Kovid Goyal '
+
+import textwrap, os
+from calibre.customize import FileTypePlugin, MetadataReaderPlugin, MetadataWriterPlugin
+from calibre.constants import __version__
+
+class HTML2ZIP(FileTypePlugin):
+ name = 'HTML to ZIP'
+ author = 'Kovid Goyal'
+ description = textwrap.dedent(_('''\
+Follow all local links in an HTML file and create a ZIP \
+file containing all linked files. This plugin is run \
+every time you add an HTML file to the library.\
+'''))
+ version = tuple(map(int, (__version__.split('.'))[:3]))
+ file_types = set(['html', 'htm', 'xhtml', 'xhtm'])
+ supported_platforms = ['windows', 'osx', 'linux']
+ on_import = True
+
+ def run(self, htmlfile):
+ of = self.temporary_file('_plugin_html2zip.zip')
+ from calibre.ebooks.html import gui_main as html2oeb
+ html2oeb(htmlfile, of)
+ return of.name
+
+class RTFMetadataReader(MetadataReaderPlugin):
+
+ name = 'Read RTF metadata'
+ file_types = set(['rtf'])
+ description = _('Read metadata from %s files')%'RTF'
+
+ def get_metadata(self, stream, ftype):
+ from calibre.ebooks.metadata.rtf import get_metadata
+ return get_metadata(stream)
+
+class FB2MetadataReader(MetadataReaderPlugin):
+
+ name = 'Read FB2 metadata'
+ file_types = set(['fb2'])
+ description = _('Read metadata from %s files')%'FB2'
+
+ def get_metadata(self, stream, ftype):
+ from calibre.ebooks.metadata.fb2 import get_metadata
+ return get_metadata(stream)
+
+
+class LRFMetadataReader(MetadataReaderPlugin):
+
+ name = 'Read LRF metadata'
+ file_types = set(['lrf'])
+ description = _('Read metadata from %s files')%'LRF'
+
+ def get_metadata(self, stream, ftype):
+ from calibre.ebooks.lrf.meta import get_metadata
+ return get_metadata(stream)
+
+class PDFMetadataReader(MetadataReaderPlugin):
+
+ name = 'Read PDF metadata'
+ file_types = set(['pdf'])
+ description = _('Read metadata from %s files')%'PDF'
+
+ def get_metadata(self, stream, ftype):
+ from calibre.ebooks.metadata.pdf import get_metadata
+ return get_metadata(stream)
+
+class LITMetadataReader(MetadataReaderPlugin):
+
+ name = 'Read LIT metadata'
+ file_types = set(['lit'])
+ description = _('Read metadata from %s files')%'LIT'
+
+ def get_metadata(self, stream, ftype):
+ from calibre.ebooks.metadata.lit import get_metadata
+ return get_metadata(stream)
+
+class IMPMetadataReader(MetadataReaderPlugin):
+
+ name = 'Read IMP metadata'
+ file_types = set(['imp'])
+ description = _('Read metadata from %s files')%'IMP'
+ author = 'Ashish Kulkarni'
+
+ def get_metadata(self, stream, ftype):
+ from calibre.ebooks.metadata.imp import get_metadata
+ return get_metadata(stream)
+
+class RBMetadataReader(MetadataReaderPlugin):
+
+ name = 'Read RB metadata'
+ file_types = set(['rb'])
+ description = _('Read metadata from %s files')%'RB'
+ author = 'Ashish Kulkarni'
+
+ def get_metadata(self, stream, ftype):
+ from calibre.ebooks.metadata.rb import get_metadata
+ return get_metadata(stream)
+
+class EPUBMetadataReader(MetadataReaderPlugin):
+
+ name = 'Read EPUB metadata'
+ file_types = set(['epub'])
+ description = _('Read metadata from %s files')%'EPUB'
+
+ def get_metadata(self, stream, ftype):
+ from calibre.ebooks.metadata.epub import get_metadata
+ return get_metadata(stream)
+
+class HTMLMetadataReader(MetadataReaderPlugin):
+
+ name = 'Read HTML metadata'
+ file_types = set(['html'])
+ description = _('Read metadata from %s files')%'HTML'
+
+ def get_metadata(self, stream, ftype):
+ from calibre.ebooks.metadata.html import get_metadata
+ return get_metadata(stream)
+
+class MOBIMetadataReader(MetadataReaderPlugin):
+
+ name = 'Read MOBI metadata'
+ file_types = set(['mobi'])
+ description = _('Read metadata from %s files')%'MOBI'
+
+ def get_metadata(self, stream, ftype):
+ from calibre.ebooks.mobi.reader import get_metadata
+ return get_metadata(stream)
+
+class ODTMetadataReader(MetadataReaderPlugin):
+
+ name = 'Read ODT metadata'
+ file_types = set(['odt'])
+ description = _('Read metadata from %s files')%'ODT'
+
+ def get_metadata(self, stream, ftype):
+ from calibre.ebooks.metadata.odt import get_metadata
+ return get_metadata(stream)
+
+class LRXMetadataReader(MetadataReaderPlugin):
+
+ name = 'Read LRX metadata'
+ file_types = set(['lrx'])
+ description = _('Read metadata from %s files')%'LRX'
+
+ def get_metadata(self, stream, ftype):
+ from calibre.ebooks.metadata.lrx import get_metadata
+ return get_metadata(stream)
+
+class ComicMetadataReader(MetadataReaderPlugin):
+
+ name = 'Read comic metadata'
+ file_types = set(['cbr', 'cbz'])
+ description = _('Extract cover from comic files')
+
+ def get_metadata(self, stream, ftype):
+ if ftype == 'cbr':
+ from calibre.libunrar import extract_member as extract_first
+ else:
+ from calibre.libunzip import extract_member as extract_first
+ from calibre.ebooks.metadata import MetaInformation
+ ret = extract_first(stream)
+ mi = MetaInformation(None, None)
+ if ret is not None:
+ path, data = ret
+ ext = os.path.splitext(path)[1][1:]
+ mi.cover_data = (ext.lower(), data)
+ return mi
+
+class EPUBMetadataWriter(MetadataWriterPlugin):
+
+ name = 'Set EPUB metadata'
+ file_types = set(['epub'])
+ description = _('Set metadata in EPUB files')
+
+ def set_metadata(self, stream, mi, type):
+ from calibre.ebooks.metadata.epub import set_metadata
+ set_metadata(stream, mi)
+
+class LRFMetadataWriter(MetadataWriterPlugin):
+
+ name = 'Set LRF metadata'
+ file_types = set(['lrf'])
+ description = _('Set metadata in LRF files')
+
+ def set_metadata(self, stream, mi, type):
+ from calibre.ebooks.lrf.meta import set_metadata
+ set_metadata(stream, mi)
+
+class RTFMetadataWriter(MetadataWriterPlugin):
+
+ name = 'Set RTF metadata'
+ file_types = set(['rtf'])
+ description = _('Set metadata in RTF files')
+
+ def set_metadata(self, stream, mi, type):
+ from calibre.ebooks.metadata.rtf import set_metadata
+ set_metadata(stream, mi)
+
+plugins = [HTML2ZIP]
+plugins += [x for x in list(locals().values()) if isinstance(x, type) and \
+ x.__name__.endswith('MetadataReader')]
+plugins += [x for x in list(locals().values()) if isinstance(x, type) and \
+ x.__name__.endswith('MetadataWriter')]
\ No newline at end of file
diff --git a/src/calibre/customize/ui.py b/src/calibre/customize/ui.py
new file mode 100644
index 0000000000..68e17fd0aa
--- /dev/null
+++ b/src/calibre/customize/ui.py
@@ -0,0 +1,299 @@
+from __future__ import with_statement
+__license__ = 'GPL v3'
+__copyright__ = '2008, Kovid Goyal '
+
+import os, shutil, traceback, functools, sys
+
+from calibre.customize import Plugin, FileTypePlugin, MetadataReaderPlugin, \
+ MetadataWriterPlugin
+from calibre.customize.builtins import plugins as builtin_plugins
+from calibre.constants import __version__, iswindows, isosx
+from calibre.ebooks.metadata import MetaInformation
+from calibre.utils.config import make_config_dir, Config, ConfigProxy, \
+ plugin_dir, OptionParser
+
+
+version = tuple([int(x) for x in __version__.split('.')])
+
+platform = 'linux'
+if iswindows:
+ platform = 'windows'
+if isosx:
+ platform = 'osx'
+
+from zipfile import ZipFile
+
+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'))
+
+ return ConfigProxy(c)
+
+config = _config()
+
+
+class InvalidPlugin(ValueError):
+ pass
+
+class PluginNotFound(ValueError):
+ pass
+
+def load_plugin(path_to_zip_file):
+ '''
+ Load plugin from zip file or raise InvalidPlugin error
+
+ :return: A :class:`Plugin` instance.
+ '''
+ print 'Loading plugin from', path_to_zip_file
+ if not os.access(path_to_zip_file, os.R_OK):
+ raise PluginNotFound
+ zf = ZipFile(path_to_zip_file)
+ for name in zf.namelist():
+ if name.lower().endswith('plugin.py'):
+ locals = {}
+ exec zf.read(name) in locals
+ for x in locals.values():
+ if isinstance(x, type) and issubclass(x, Plugin):
+ if x.minimum_calibre_version > version or \
+ platform not in x.supported_platforms:
+ continue
+
+ return x
+
+ raise InvalidPlugin(_('No valid plugin found in ')+path_to_zip_file)
+
+_initialized_plugins = []
+_on_import = {}
+_on_preprocess = {}
+_on_postprocess = {}
+
+
+
+def reread_filetype_plugins():
+ global _on_import
+ global _on_preprocess
+ global _on_postprocess
+ _on_import = {}
+ _on_preprocess = {}
+ _on_postprocess = {}
+
+ for plugin in _initialized_plugins:
+ if isinstance(plugin, FileTypePlugin):
+ for ft in plugin.file_types:
+ if plugin.on_import:
+ if not _on_import.has_key(ft):
+ _on_import[ft] = []
+ _on_import[ft].append(plugin)
+ if plugin.on_preprocess:
+ if not _on_preprocess.has_key(ft):
+ _on_preprocess[ft] = []
+ _on_preprocess[ft].append(plugin)
+ if plugin.on_postprocess:
+ if not _on_postprocess.has_key(ft):
+ _on_postprocess[ft] = []
+ _on_postprocess[ft].append(plugin)
+
+_metadata_readers = {}
+_metadata_writers = {}
+def reread_metadata_plugins():
+ global _metadata_readers
+ global _metadata_writers
+ _metadata_readers = {}
+ for plugin in _initialized_plugins:
+ if isinstance(plugin, MetadataReaderPlugin):
+ for ft in plugin.file_types:
+ _metadata_readers[ft] = plugin
+ elif isinstance(plugin, MetadataWriterPlugin):
+ for ft in plugin.file_types:
+ _metadata_writers[ft] = plugin
+
+def get_file_type_metadata(stream, ftype):
+ mi = MetaInformation(None, None)
+ try:
+ plugin = _metadata_readers[ftype.lower().strip()]
+ if not is_disabled(plugin):
+ with plugin:
+ mi = plugin.get_metadata(stream, ftype.lower().strip())
+ except:
+ pass
+ return mi
+
+def set_file_type_metadata(stream, mi, ftype):
+ try:
+ plugin = _metadata_writers[ftype.lower().strip()]
+ if not is_disabled(plugin):
+ with plugin:
+ plugin.set_metadata(stream, mi, ftype.lower().strip())
+ except:
+ traceback.print_exc()
+
+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):
+ continue
+ plugin.site_customization = customization.get(plugin.name, '')
+ with plugin:
+ try:
+ nfp = plugin.run(path_to_file)
+ 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,
+ 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 initialize_plugin(plugin, path_to_zip_file):
+ try:
+ return plugin(path_to_zip_file)
+ 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 add_plugin(path_to_zip_file):
+ make_config_dir()
+ plugin = load_plugin(path_to_zip_file)
+ 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 is_disabled(plugin):
+ return plugin.name in config['disabled_plugins']
+
+def find_plugin(name):
+ for plugin in _initialized_plugins:
+ if plugin.name == name:
+ return plugin
+
+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
+
+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
+
+def initialize_plugins():
+ global _initialized_plugins
+ _initialized_plugins = []
+ for zfp in list(config['plugins'].values()) + builtin_plugins:
+ try:
+ try:
+ plugin = load_plugin(zfp) if not isinstance(zfp, type) else zfp
+ except PluginNotFound:
+ continue
+ plugin = initialize_plugin(plugin, zfp if not isinstance(zfp, type) else zfp)
+ _initialized_plugins.append(plugin)
+ except:
+ print 'Failed to initialize plugin...'
+ traceback.print_exc()
+ _initialized_plugins.sort(cmp=lambda x,y:cmp(x.priority, y.priority), reverse=True)
+ reread_filetype_plugins()
+ reread_metadata_plugins()
+
+initialize_plugins()
+
+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('--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 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:
+ 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.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:
+ fmt = '%-15s%-20s%-15s%-15s%s'
+ 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():
+ print '\t', plugin.customization_help()
+ print
+
+ return 0
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/src/calibre/debug.py b/src/calibre/debug.py
index ea21a5613d..274089baa1 100644
--- a/src/calibre/debug.py
+++ b/src/calibre/debug.py
@@ -21,6 +21,8 @@ Run an embedded python interpreter.
'Module specifications are of the form full.name.of.module,path_to_module.py', default=None
)
parser.add_option('-c', '--command', help='Run python code.', default=None)
+ parser.add_option('-g', '--gui', default=False, action='store_true',
+ help='Run the GUI',)
parser.add_option('--migrate', action='store_true', default=False,
help='Migrate old database. Needs two arguments. Path to library1.db and path to new library folder.')
return parser
@@ -72,7 +74,10 @@ def migrate(old, new):
def main(args=sys.argv):
opts, args = option_parser().parse_args(args)
- if opts.update_module:
+ if opts.gui:
+ from calibre.gui2.main import main
+ main(['calibre'])
+ elif opts.update_module:
mod, path = opts.update_module.partition(',')[0], opts.update_module.partition(',')[-1]
update_module(mod, os.path.expanduser(path))
elif opts.command:
diff --git a/src/calibre/ebooks/epub/from_any.py b/src/calibre/ebooks/epub/from_any.py
index 6cf56aa43c..e204b38b03 100644
--- a/src/calibre/ebooks/epub/from_any.py
+++ b/src/calibre/ebooks/epub/from_any.py
@@ -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
@@ -30,7 +31,7 @@ def lit2opf(path, tdir, opts):
def mobi2opf(path, tdir, opts):
from calibre.ebooks.mobi.reader import MobiReader
- print 'Exploding MOBI file:', path
+ print 'Exploding MOBI file:', path.encode('utf-8') if isinstance(path, unicode) else path
reader = MobiReader(path)
reader.extract_content(tdir)
files = list(walk(tdir))
@@ -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)
diff --git a/src/calibre/ebooks/epub/from_html.py b/src/calibre/ebooks/epub/from_html.py
index d62bb936b2..8ebd426a14 100644
--- a/src/calibre/ebooks/epub/from_html.py
+++ b/src/calibre/ebooks/epub/from_html.py
@@ -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:
diff --git a/src/calibre/ebooks/html.py b/src/calibre/ebooks/html.py
index 8c5cc6f8a4..630dbc1264 100644
--- a/src/calibre/ebooks/html.py
+++ b/src/calibre/ebooks/html.py
@@ -447,6 +447,7 @@ class Parser(PreProcessor, LoggingInterface):
''' Create lxml ElementTree from HTML '''
self.log_info('\tParsing '+os.sep.join(self.htmlfile.path.split(os.sep)[-3:]))
src = open(self.htmlfile.path, 'rb').read().decode(self.htmlfile.encoding, 'replace').strip()
+ src = src.replace('\x00', '')
src = self.preprocess(src)
# lxml chokes on unicode input when it contains encoding declarations
for pat in ENCODING_PATS:
@@ -527,6 +528,7 @@ class Processor(Parser):
LINKS_PATH = XPath('//a[@href]')
PIXEL_PAT = re.compile(r'([-]?\d+|[-]?\d*\.\d+)px')
+ PAGE_PAT = re.compile(r'@page[^{]*?{[^}]*?}')
def __init__(self, *args, **kwargs):
Parser.__init__(self, *args, **kwargs)
@@ -696,7 +698,9 @@ class Processor(Parser):
return ''
return '%fpt'%(72 * val/dpi)
- return cls.PIXEL_PAT.sub(rescale, css)
+ css = cls.PIXEL_PAT.sub(rescale, css)
+ css = cls.PAGE_PAT.sub('', css)
+ return css
def extract_css(self, parsed_sheets):
'''
@@ -732,7 +736,12 @@ class Processor(Parser):
self.log_error('Failed to open stylesheet: %s'%file)
else:
try:
- parsed_sheets[file] = self.css_parser.parseString(css)
+ try:
+ parsed_sheets[file] = self.css_parser.parseString(css)
+ except ValueError:
+ parsed_sheets[file] = \
+ self.css_parser.parseString(\
+ css.decode('utf8', 'replace'))
except:
parsed_sheets[file] = css.decode('utf8', 'replace')
self.log_warning('Failed to parse stylesheet: %s'%file)
@@ -762,9 +771,11 @@ class Processor(Parser):
class_counter = 0
for font in self.root.xpath('//font'):
try:
- size = int(font.attrib.pop('size', '3'))
+ size = font.attrib.pop('size', '3')
except:
- size = 3
+ size = '3'
+ if size and size.strip() and size.strip()[0] in ('+', '-'):
+ size = 3 + float(size) # Hack assumes basefont=3
setting = 'font-size: %d%%;'%int((float(size)/3) * 100)
face = font.attrib.pop('face', None)
if face is not None:
@@ -1043,11 +1054,12 @@ def main(args=sys.argv):
return 0
-def gui_main(htmlfile):
+def gui_main(htmlfile, pt=None):
'''
Convenience wrapper for use in recursively importing HTML files.
'''
- pt = PersistentTemporaryFile('_html2oeb_gui.oeb.zip')
+ if pt is None:
+ pt = PersistentTemporaryFile('_html2oeb_gui.oeb.zip')
pt.close()
opts = '''
pretty_print = True
diff --git a/src/calibre/ebooks/lit/writer.py b/src/calibre/ebooks/lit/writer.py
index 6e2d079e1c..b64345b75c 100644
--- a/src/calibre/ebooks/lit/writer.py
+++ b/src/calibre/ebooks/lit/writer.py
@@ -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)
diff --git a/src/calibre/ebooks/lrf/any/convert_from.py b/src/calibre/ebooks/lrf/any/convert_from.py
index 710fbe1690..002064bced 100644
--- a/src/calibre/ebooks/lrf/any/convert_from.py
+++ b/src/calibre/ebooks/lrf/any/convert_from.py
@@ -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):
diff --git a/src/calibre/ebooks/lrf/comic/convert_from.py b/src/calibre/ebooks/lrf/comic/convert_from.py
index 4cd45dd893..5bc4a9171b 100755
--- a/src/calibre/ebooks/lrf/comic/convert_from.py
+++ b/src/calibre/ebooks/lrf/comic/convert_from.py
@@ -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:
diff --git a/src/calibre/ebooks/lrf/html/convert_from.py b/src/calibre/ebooks/lrf/html/convert_from.py
index ae2ab233a5..2ed8d29468 100644
--- a/src/calibre/ebooks/lrf/html/convert_from.py
+++ b/src/calibre/ebooks/lrf/html/convert_from.py
@@ -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
@@ -1852,7 +1853,7 @@ def process_file(path, options, logger=None):
scaled else im
cf = PersistentTemporaryFile(prefix=__appname__+"_", suffix=".jpg")
cf.close()
- cim.save(cf.name)
+ cim.convert('RGB').save(cf.name)
options.cover = cf.name
tim = im.resize((int(0.75*th), th), PILImage.ANTIALIAS).convert('RGB')
@@ -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
diff --git a/src/calibre/ebooks/metadata/html.py b/src/calibre/ebooks/metadata/html.py
index b267a9e647..8d61a746fa 100644
--- a/src/calibre/ebooks/metadata/html.py
+++ b/src/calibre/ebooks/metadata/html.py
@@ -16,7 +16,7 @@ def get_metadata(stream):
# Title
title = None
- pat = re.compile(r'', re.DOTALL)
+ pat = re.compile(r'', re.DOTALL)
match = pat.search(src)
if match:
title = match.group(2)
@@ -28,7 +28,7 @@ def get_metadata(stream):
# Author
author = None
- pat = re.compile(r'', re.DOTALL)
+ pat = re.compile(r'', re.DOTALL)
match = pat.search(src)
if match:
author = match.group(2).replace(',', ';')
@@ -36,7 +36,7 @@ def get_metadata(stream):
mi = MetaInformation(title, [author] if author else None)
# Publisher
- pat = re.compile(r'', re.DOTALL)
+ pat = re.compile(r'', re.DOTALL)
match = pat.search(src)
if match:
mi.publisher = match.group(2)
diff --git a/src/calibre/ebooks/metadata/meta.py b/src/calibre/ebooks/metadata/meta.py
index 3264655f9c..c6d3cf479a 100644
--- a/src/calibre/ebooks/metadata/meta.py
+++ b/src/calibre/ebooks/metadata/meta.py
@@ -5,36 +5,16 @@ __copyright__ = '2008, Kovid Goyal '
import os, re, collections
from calibre.utils.config import prefs
-from calibre.ebooks.metadata.rtf import get_metadata as rtf_metadata
-from calibre.ebooks.metadata.fb2 import get_metadata as fb2_metadata
-from calibre.ebooks.lrf.meta import get_metadata as lrf_metadata
-from calibre.ebooks.metadata.pdf import get_metadata as pdf_metadata
-from calibre.ebooks.metadata.lit import get_metadata as lit_metadata
-from calibre.ebooks.metadata.imp import get_metadata as imp_metadata
-from calibre.ebooks.metadata.rb import get_metadata as rb_metadata
-from calibre.ebooks.metadata.epub import get_metadata as epub_metadata
-from calibre.ebooks.metadata.html import get_metadata as html_metadata
-from calibre.ebooks.mobi.reader import get_metadata as mobi_metadata
-from calibre.ebooks.metadata.odt import get_metadata as odt_metadata
-from calibre.ebooks.metadata.lrx import get_metadata as lrx_metadata
+
from calibre.ebooks.metadata.opf2 import OPF
-from calibre.ebooks.metadata.rtf import set_metadata as set_rtf_metadata
-from calibre.ebooks.lrf.meta import set_metadata as set_lrf_metadata
-from calibre.ebooks.metadata.epub import set_metadata as set_epub_metadata
-from calibre.ebooks.metadata.pdf import set_metadata as set_pdf_metadata
-try:
- from calibre.libunrar import extract_member as rar_extract_first
-except OSError:
- rar_extract_first = None
-
-from calibre.libunzip import extract_member as zip_extract_first
+from calibre.customize.ui import get_file_type_metadata, set_file_type_metadata
from calibre.ebooks.metadata import MetaInformation
_METADATA_PRIORITIES = [
'html', 'htm', 'xhtml', 'xhtm',
'rtf', 'fb2', 'pdf', 'prc', 'odt',
- 'epub', 'lit', 'lrx', 'lrf', 'mobi',
+ 'epub', 'lit', 'lrx', 'lrf', 'mobi',
'rb', 'imp'
]
@@ -88,11 +68,7 @@ def get_metadata(stream, stream_type='lrf', use_libprs_metadata=False):
mi = MetaInformation(None, None)
if prefs['read_file_metadata']:
- try:
- func = eval(stream_type + '_metadata')
- mi = func(stream)
- except NameError:
- pass
+ mi = get_file_type_metadata(stream, stream_type)
name = os.path.basename(getattr(stream, 'name', ''))
base = metadata_from_filename(name)
@@ -104,37 +80,14 @@ def get_metadata(stream, stream_type='lrf', use_libprs_metadata=False):
if opf is not None:
base.smart_update(opf)
- if stream_type in ('cbr', 'cbz'):
- try:
- cdata = get_comic_cover(stream, stream_type)
- if cdata is not None:
- base.cover_data = cdata
- except:
- import traceback
- traceback.print_exc()
- pass
-
return base
-def get_comic_cover(stream, type):
- extract_first = zip_extract_first if type.lower() == 'cbz' else rar_extract_first
- ret = extract_first(stream)
- if ret is not None:
- path, data = ret
- ext = os.path.splitext(path)[1][1:]
- return (ext.lower(), data)
-
def set_metadata(stream, mi, stream_type='lrf'):
- if stream_type: stream_type = stream_type.lower()
- if stream_type == 'lrf':
- set_lrf_metadata(stream, mi)
- elif stream_type == 'epub':
- set_epub_metadata(stream, mi)
- elif stream_type == 'rtf':
- set_rtf_metadata(stream, mi)
- #elif stream_type == 'pdf':
- # set_pdf_metadata(stream, mi)
-
+ if stream_type:
+ stream_type = stream_type.lower()
+ set_file_type_metadata(stream, mi, stream_type)
+
+
def metadata_from_filename(name, pat=None):
name = os.path.splitext(name)[0]
mi = MetaInformation(None, None)
diff --git a/src/calibre/ebooks/rtf2xml/ParseRtf.py b/src/calibre/ebooks/rtf2xml/ParseRtf.py
index 2dacc0c49d..5b008df615 100755
--- a/src/calibre/ebooks/rtf2xml/ParseRtf.py
+++ b/src/calibre/ebooks/rtf2xml/ParseRtf.py
@@ -233,7 +233,7 @@ class ParseRtf:
bug_handler = RtfInvalidCodeException,
)
check_encoding_obj.check_encoding(self.__file)
- sys.stderr.write('File "%s" does not appear to be RTF.\n' % self.__file)
+ sys.stderr.write('File "%s" does not appear to be RTF.\n' % self.__file if isinstance(self.__file, str) else self.__file.encode('utf-8'))
raise InvalidRtfException, msg
delete_info_obj = delete_info.DeleteInfo(
in_file = self.__temp_file,
diff --git a/src/calibre/ebooks/rtf2xml/check_encoding.py b/src/calibre/ebooks/rtf2xml/check_encoding.py
index d2bd4f8ce1..f6810e4909 100755
--- a/src/calibre/ebooks/rtf2xml/check_encoding.py
+++ b/src/calibre/ebooks/rtf2xml/check_encoding.py
@@ -23,7 +23,10 @@ class CheckEncoding:
try:
line.decode(encoding)
except UnicodeError:
- self.__get_position_error(line, encoding, line_num)
+ if len(line) < 1000:
+ self.__get_position_error(line, encoding, line_num)
+ else:
+ sys.stderr.write('line: %d has bad encoding\n'%line_num)
if __name__ == '__main__':
check_encoding_obj = CheckEncoding()
check_encoding_obj.check_encoding(sys.argv[1])
diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py
index 3799e1d1de..a393843156 100644
--- a/src/calibre/gui2/__init__.py
+++ b/src/calibre/gui2/__init__.py
@@ -14,7 +14,6 @@ from calibre import __author__, islinux, iswindows, isosx
from calibre.startup import get_lang
from calibre.utils.config import Config, ConfigProxy, dynamic
import calibre.resources as resources
-from calibre.ebooks.html import gui_main as html2oeb
NONE = QVariant() #: Null value to return from the data function of item models
@@ -389,14 +388,6 @@ def pixmap_to_data(pixmap, format='JPEG'):
pixmap.save(buf, format)
return str(ba.data())
-html_pat = re.compile(r'\.x{0,1}htm(l{0,1})\s*$', re.IGNORECASE)
-def import_format(path):
- if html_pat.search(path) is not None:
- try:
- return html2oeb(path), 'zip'
- except:
- traceback.print_exc()
- return None, None
try:
from calibre.utils.single_qt_application import SingleApplication
diff --git a/src/calibre/gui2/dialogs/config.py b/src/calibre/gui2/dialogs/config.py
index a4d60552c6..82b07e81e7 100644
--- a/src/calibre/gui2/dialogs/config.py
+++ b/src/calibre/gui2/dialogs/config.py
@@ -1,32 +1,131 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal '
-import os, re, time
+import os, re, time, textwrap
-from PyQt4.QtGui import QDialog, QMessageBox, QListWidgetItem, QIcon, \
+from PyQt4.Qt import QDialog, QMessageBox, QListWidgetItem, QIcon, \
QDesktopServices, QVBoxLayout, QLabel, QPlainTextEdit, \
- QStringListModel
-from PyQt4.QtCore import SIGNAL, QTimer, Qt, QSize, QVariant, QUrl
+ QStringListModel, QAbstractItemModel, \
+ SIGNAL, QTimer, Qt, QSize, QVariant, QUrl, \
+ 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, \
- warning_dialog, ALL_COLUMNS
+ 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, enable_plugin, \
+ disable_plugin, customize_plugin, \
+ plugin_customization, add_plugin
+
+class PluginModel(QAbstractItemModel):
+
+ def __init__(self, *args):
+ QAbstractItemModel.__init__(self, *args)
+ self.icon = QVariant(QIcon(':/images/plugins.svg'))
+ self.populate()
+
+ def populate(self):
+ self._data = {}
+ for plugin in initialized_plugins():
+ if plugin.type not in self._data:
+ self._data[plugin.type] = [plugin]
+ else:
+ self._data[plugin.type].append(plugin)
+ self.categories = sorted(self._data.keys())
+
+ def index(self, row, column, parent):
+ if not self.hasIndex(row, column, parent):
+ return QModelIndex()
+
+ if parent.isValid():
+ return self.createIndex(row, column, parent.row())
+ else:
+ return self.createIndex(row, column, -1)
+
+ def parent(self, index):
+ if not index.isValid() or index.internalId() == -1:
+ return QModelIndex()
+ return self.createIndex(index.internalId(), 0, -1)
+
+ def rowCount(self, parent):
+ if not parent.isValid():
+ return len(self.categories)
+ if parent.internalId() == -1:
+ category = self.categories[parent.row()]
+ return len(self._data[category])
+ return 0
+
+ def columnCount(self, parent):
+ return 1
+
+ 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
+ if index.internalId() == -1:
+ return Qt.ItemIsEnabled
+ flags = Qt.ItemIsSelectable
+ if not is_disabled(self.data(index, Qt.UserRole)):
+ flags |= Qt.ItemIsEnabled
+ return flags
+
+ def data(self, index, role):
+ if not index.isValid():
+ return NONE
+ if index.internalId() == -1:
+ if role == Qt.DisplayRole:
+ category = self.categories[index.row()]
+ return QVariant(category + _(' plugins'))
+ else:
+ plugin = self.index_to_plugin(index)
+ if role == Qt.DisplayRole:
+ ver = '.'.join(map(str, plugin.version))
+ desc = '\n'.join(textwrap.wrap(plugin.description, 50))
+ ans='%s (%s) %s %s\n%s'%(plugin.name, ver, _('by'), 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:
+ return plugin
+ return NONE
+
+
class CategoryModel(QStringListModel):
def __init__(self, *args):
QStringListModel.__init__(self, *args)
self.setStringList([_('General'), _('Interface'), _('Advanced'),
- _('Content\nServer')])
+ _('Content\nServer'), _('Plugins')])
self.icons = list(map(QVariant, map(QIcon,
[':/images/dialog_information.svg', ':/images/lookfeel.svg',
- ':/images/view.svg', ':/images/network-server.svg'])))
+ ':/images/view.svg', ':/images/network-server.svg',
+ ':/images/plugins.svg'])))
def data(self, index, role):
if role == Qt.DecorationRole:
@@ -139,6 +238,56 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.priority.setVisible(iswindows)
self.priority_label.setVisible(iswindows)
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()
diff --git a/src/calibre/gui2/dialogs/config.ui b/src/calibre/gui2/dialogs/config.ui
index 2def8741c1..05b66cac88 100644
--- a/src/calibre/gui2/dialogs/config.ui
+++ b/src/calibre/gui2/dialogs/config.ui
@@ -767,6 +767,115 @@
+
+
+
+
+
+ Here you can customize the behavior of Calibre by controlling what plugins it uses.
+
+
+ true
+
+
+
+
+
+
+
+ 32
+ 32
+
+
+
+ true
+
+
+ true
+
+
+
+
+
+
+
+
+ Enable/&Disable plugin
+
+
+
+
+
+
+ &Customize plugin
+
+
+
+
+
+
+
+
+ Add new plugin
+
+
+
+
+
+
+
+ Plugin &file:
+
+
+ plugin_path
+
+
+
+
+
+
+
+
+
+ ...
+
+
+
+ :/images/document_open.svg:/images/document_open.svg
+
+
+
+
+
+
+
+
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+ &Add
+
+
+
+
+
+
+
+
+
+
diff --git a/src/calibre/gui2/dialogs/jobs.py b/src/calibre/gui2/dialogs/jobs.py
index 8e22f4c43c..5b97934a84 100644
--- a/src/calibre/gui2/dialogs/jobs.py
+++ b/src/calibre/gui2/dialogs/jobs.py
@@ -49,11 +49,8 @@ class JobsDialog(QDialog, Ui_JobsDialog):
self.connect(self.running_time_timer, SIGNAL('timeout()'), self.update_running_time)
self.running_time_timer.start(1000)
- def update_running_time(self):
- model = self.model
- for row, job in enumerate(model.jobs):
- if job.is_running:
- self.jobs_view.dataChanged(model.index(row, 3), model.index(row, 3))
+ def update_running_time(self, *args):
+ self.model.running_time_updated()
def kill_job(self):
for index in self.jobs_view.selectedIndexes():
diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py
index c7670f989f..d12dcf5cd3 100644
--- a/src/calibre/gui2/dialogs/metadata_single.py
+++ b/src/calibre/gui2/dialogs/metadata_single.py
@@ -11,7 +11,7 @@ from PyQt4.QtGui import QPixmap, QListWidgetItem, QErrorMessage, QDialog
from calibre.gui2 import qstring_to_unicode, error_dialog, file_icon_provider, \
- choose_files, pixmap_to_data, choose_images, import_format
+ choose_files, pixmap_to_data, choose_images
from calibre.gui2.dialogs.metadata_single_ui import Ui_MetadataSingleDialog
from calibre.gui2.dialogs.fetch_metadata import FetchMetadata
from calibre.gui2.dialogs.tag_editor import TagEditor
@@ -21,6 +21,7 @@ from calibre.ebooks.metadata import authors_to_sort_string, string_to_authors, a
from calibre.ebooks.metadata.library_thing import login, cover_from_isbn, LibraryThingError
from calibre import islinux
from calibre.utils.config import prefs
+from calibre.customize.ui import run_plugins_on_import
class Format(QListWidgetItem):
def __init__(self, parent, ext, size, path=None):
@@ -84,13 +85,9 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
QErrorMessage(self.window).showMessage("You do not have "+\
"permission to read the file: " + _file)
continue
- nf = import_format(_file)[0]
- if nf is not None:
- _file = nf
+ _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:
diff --git a/src/calibre/gui2/images/network-server.svg b/src/calibre/gui2/images/network-server.svg
index 80f54e090d..4adef49a37 100644
--- a/src/calibre/gui2/images/network-server.svg
+++ b/src/calibre/gui2/images/network-server.svg
@@ -2254,7 +2254,7 @@
id="mask7160"
maskUnits="userSpaceOnUse">
diff --git a/src/calibre/gui2/images/news/endgadget.png b/src/calibre/gui2/images/news/endgadget.png
new file mode 100644
index 0000000000..94e8f1219c
Binary files /dev/null and b/src/calibre/gui2/images/news/endgadget.png differ
diff --git a/src/calibre/gui2/images/news/fudzilla.png b/src/calibre/gui2/images/news/fudzilla.png
new file mode 100644
index 0000000000..ad1e67335d
Binary files /dev/null and b/src/calibre/gui2/images/news/fudzilla.png differ
diff --git a/src/calibre/gui2/images/news/time.png b/src/calibre/gui2/images/news/time_magazine.png
similarity index 100%
rename from src/calibre/gui2/images/news/time.png
rename to src/calibre/gui2/images/news/time_magazine.png
diff --git a/src/calibre/gui2/images/plugins.svg b/src/calibre/gui2/images/plugins.svg
new file mode 100644
index 0000000000..9444112bb5
--- /dev/null
+++ b/src/calibre/gui2/images/plugins.svg
@@ -0,0 +1,694 @@
+
+
+
\ No newline at end of file
diff --git a/src/calibre/gui2/jobs2.py b/src/calibre/gui2/jobs2.py
index e5b17ef664..a485b93ab5 100644
--- a/src/calibre/gui2/jobs2.py
+++ b/src/calibre/gui2/jobs2.py
@@ -132,6 +132,13 @@ class JobManager(QAbstractTableModel):
self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'),
self.index(row, 0), self.index(row, 3))
+ def running_time_updated(self):
+ for job in self.jobs:
+ if not job.is_running:
+ continue
+ row = self.jobs.index(job)
+ self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'),
+ self.index(row, 3), self.index(row, 3))
def has_device_jobs(self):
for job in self.jobs:
diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py
index 267699a3ad..4bbde629e5 100644
--- a/src/calibre/gui2/main.py
+++ b/src/calibre/gui2/main.py
@@ -6,7 +6,8 @@ from functools import partial
from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, QUrl, QTimer
from PyQt4.QtGui import QPixmap, QColor, QPainter, QMenu, QIcon, QMessageBox, \
QToolButton, QDialog, QDesktopServices, QFileDialog, \
- QSystemTrayIcon, QApplication, QKeySequence, QAction
+ QSystemTrayIcon, QApplication, QKeySequence, QAction, \
+ QProgressDialog
from PyQt4.QtSvg import QSvgRenderer
from calibre import __version__, __appname__, islinux, sanitize_file_name, \
@@ -21,7 +22,7 @@ from calibre.gui2 import APP_UID, warning_dialog, choose_files, error_dialog, \
pixmap_to_data, choose_dir, ORG_NAME, \
set_sidebar_directories, Dispatcher, \
SingleApplication, Application, available_height, \
- max_available_height, config, info_dialog, import_format
+ max_available_height, config, info_dialog
from calibre.gui2.cover_flow import CoverFlow, DatabaseImages, pictureflowerror
from calibre.library.database import LibraryDatabase
from calibre.gui2.dialogs.scheduler import Scheduler
@@ -119,7 +120,7 @@ class Main(MainWindow, Ui_MainWindow):
self.hide() if self.isVisible() else self.show()
self.connect(self.system_tray_icon, SIGNAL('activated(QSystemTrayIcon::ActivationReason)'), sta)
def tcme(self, *args):
- print args
+ pass
self.tool_bar.contextMenuEvent = tcme
####################### Location View ########################
QObject.connect(self.location_view, SIGNAL('location_selected(PyQt_PyObject)'),
@@ -566,8 +567,23 @@ class Main(MainWindow, Ui_MainWindow):
root = choose_dir(self, 'recursive book import root dir dialog', 'Select root folder')
if not root:
return
- duplicates = self.library_view.model().db.recursive_import(root, single)
-
+ progress = QProgressDialog('', '&'+_('Stop'),
+ 0, 0, self)
+ progress.setWindowModality(Qt.ApplicationModal)
+ progress.setWindowTitle(_('Adding books recursively...'))
+ progress.show()
+ def callback(msg):
+ if msg != '.':
+ progress.setLabelText((_('Added ')+msg) if msg else _('Searching...'))
+ stop = progress.wasCanceled()
+ QApplication.processEvents()
+ QApplication.sendPostedEvents()
+ QApplication.flush()
+ return stop
+ try:
+ duplicates = self.library_view.model().db.recursive_import(root, single, callback=callback)
+ finally:
+ progress.close()
if duplicates:
files = _('
Books with the same title as the following already exist in the database. Add them anyway?
')
for mi, formats in duplicates:
@@ -634,49 +650,59 @@ class Main(MainWindow, Ui_MainWindow):
def _add_books(self, paths, to_device, on_card=None):
if on_card is None:
on_card = self.stack.currentIndex() == 2
+ if not paths:
+ return
# Get format and metadata information
formats, metadata, names, infos = [], [], [], []
- for book in paths:
- format = os.path.splitext(book)[1]
- format = format[1:] if format else None
- stream = open(book, 'rb')
- try:
- mi = get_metadata(stream, stream_type=format, use_libprs_metadata=True)
- except:
- mi = MetaInformation(None, None)
- if not mi.title:
- mi.title = os.path.splitext(os.path.basename(book))[0]
- if not mi.authors:
- mi.authors = [_('Unknown')]
- formats.append(format)
- metadata.append(mi)
- names.append(os.path.basename(book))
- infos.append({'title':mi.title, 'authors':', '.join(mi.authors),
- 'cover':self.default_thumbnail, 'tags':[]})
-
- if not to_device:
- model = self.library_view.model()
+ progress = QProgressDialog(_('Reading metadata...'), _('Stop'), 0, len(paths), self)
+ progress.setWindowTitle(_('Adding books...'))
+ progress.setWindowModality(Qt.ApplicationModal)
+ progress.setLabelText(_('Reading metadata...'))
+ progress.show()
+ try:
+ for c, book in enumerate(paths):
+ progress.setValue(c)
+ if progress.wasCanceled():
+ return
+ format = os.path.splitext(book)[1]
+ format = format[1:] if format else None
+ stream = open(book, 'rb')
+ try:
+ mi = get_metadata(stream, stream_type=format, use_libprs_metadata=True)
+ except:
+ mi = MetaInformation(None, None)
+ if not mi.title:
+ mi.title = os.path.splitext(os.path.basename(book))[0]
+ if not mi.authors:
+ mi.authors = [_('Unknown')]
+ formats.append(format)
+ metadata.append(mi)
+ names.append(os.path.basename(book))
+ infos.append({'title':mi.title, 'authors':', '.join(mi.authors),
+ 'cover':self.default_thumbnail, 'tags':[]})
+ title = mi.title if isinstance(mi.title, unicode) else mi.title.decode(preferred_encoding, 'replace')
+ progress.setLabelText(_('Read metadata from ')+title)
- paths = list(paths)
- for i, path in enumerate(paths):
- npath, fmt = import_format(path)
- if npath is not None:
- paths[i] = npath
- formats[i] = fmt
- duplicates, number_added = model.add_books(paths, formats, metadata)
- if duplicates:
- files = _('
Books with the same title as the following already exist in the database. Add them anyway?
')
- for mi in duplicates[2]:
- files += '
'+mi.title+'
\n'
- d = WarningDialog(_('Duplicates found!'), _('Duplicates found!'), files+'
', parent=self)
- if d.exec_() == QDialog.Accepted:
- num = model.add_books(*duplicates, **dict(add_duplicates=True))[1]
- number_added += num
- #self.library_view.sortByColumn(3, Qt.DescendingOrder)
- #model.research()
- model.books_added(number_added)
- else:
- self.upload_books(paths, list(map(sanitize_file_name, names)), infos, on_card=on_card)
+ if not to_device:
+ progress.setLabelText(_('Adding books to database...'))
+ model = self.library_view.model()
+
+ paths = list(paths)
+ duplicates, number_added = model.add_books(paths, formats, metadata)
+ progress.cancel()
+ if duplicates:
+ files = _('
Books with the same title as the following already exist in the database. Add them anyway?
')
+ for mi in duplicates[2]:
+ files += '
'+mi.title+'
\n'
+ d = WarningDialog(_('Duplicates found!'), _('Duplicates found!'), files+'
', parent=self)
+ if d.exec_() == QDialog.Accepted:
+ num = model.add_books(*duplicates, **dict(add_duplicates=True))[1]
+ number_added += num
+ model.books_added(number_added)
+ else:
+ self.upload_books(paths, list(map(sanitize_file_name, names)), infos, on_card=on_card)
+ finally:
+ progress.setValue(len(paths))
def upload_books(self, files, names, metadata, on_card=False, memory=None):
'''
diff --git a/src/calibre/gui2/status.py b/src/calibre/gui2/status.py
index 221a6fd12b..31fd0c9112 100644
--- a/src/calibre/gui2/status.py
+++ b/src/calibre/gui2/status.py
@@ -5,7 +5,7 @@ import re, collections
from PyQt4.QtGui import QStatusBar, QMovie, QLabel, QWidget, QHBoxLayout, QPixmap, \
QVBoxLayout, QSizePolicy, QToolButton, QIcon, QScrollArea, QFrame
from PyQt4.QtCore import Qt, QSize, SIGNAL, QCoreApplication
-from calibre import fit_image, preferred_encoding
+from calibre import fit_image, preferred_encoding, isosx
from calibre.gui2 import qstring_to_unicode
class BookInfoDisplay(QWidget):
@@ -196,9 +196,12 @@ class StatusBar(QStatusBar):
self.book_info.show_data({})
def showMessage(self, msg, timeout=0):
+ ret = QStatusBar.showMessage(self, msg, timeout)
if self.systray is not None:
+ if isosx and isinstance(msg, unicode):
+ msg = msg.encode(preferred_encoding)
self.systray.showMessage('calibre', msg, self.systray.Information, 10000)
- return QStatusBar.showMessage(self, msg, timeout)
+ return ret
def jobs(self):
src = qstring_to_unicode(self.movie_button.jobs.text())
diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py
index e2ac7d2bd6..09d8aaf81c 100644
--- a/src/calibre/gui2/viewer/main.py
+++ b/src/calibre/gui2/viewer/main.py
@@ -242,6 +242,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
lambda x:self.view.previous_page())
self.connect(self.action_find_next, SIGNAL('triggered(bool)'),
lambda x:self.find(unicode(self.search.text()), True, repeat=True))
+ self.connect(self.action_full_screen, SIGNAL('triggered(bool)'),
+ self.toggle_fullscreen)
self.connect(self.action_back, SIGNAL('triggered(bool)'), self.back)
self.connect(self.action_bookmark, SIGNAL('triggered(bool)'), self.bookmark)
self.connect(self.action_forward, SIGNAL('triggered(bool)'), self.forward)
@@ -263,6 +265,13 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.tool_bar.setContextMenuPolicy(Qt.PreventContextMenu)
self.tool_bar2.setContextMenuPolicy(Qt.PreventContextMenu)
self.tool_bar.widgetForAction(self.action_bookmark).setPopupMode(QToolButton.MenuButtonPopup)
+ self.action_full_screen.setCheckable(True)
+
+ def toggle_fullscreen(self, x):
+ if self.isFullScreen():
+ self.showNormal()
+ else:
+ self.showFullScreen()
def goto(self, ref):
if ref:
@@ -574,7 +583,7 @@ View an ebook.
def main(args=sys.argv):
parser = option_parser()
- opts, args = parser.parse_args(args)
+ args = parser.parse_args(args)[-1]
pid = os.fork() if islinux else -1
if pid <= 0:
app = Application(args)
diff --git a/src/calibre/gui2/viewer/main.ui b/src/calibre/gui2/viewer/main.ui
index c409b437a2..59f813b2bd 100644
--- a/src/calibre/gui2/viewer/main.ui
+++ b/src/calibre/gui2/viewer/main.ui
@@ -27,8 +27,8 @@
-
-
+
+ about:blank
@@ -88,6 +88,7 @@
+
@@ -224,6 +225,15 @@
Bookmark
+
+
+
+ :/images/page.svg:/images/page.svg
+
+
+ Toggle full screen
+
+
diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py
index d0158393e5..fa2785ddc1 100644
--- a/src/calibre/library/cli.py
+++ b/src/calibre/library/cli.py
@@ -358,8 +358,8 @@ list of id numbers (you can get id numbers by using the list command). For examp
return 0
-def do_add_format(db, id, fmt, buffer):
- db.add_format(id, fmt.upper(), buffer, index_is_id=True)
+def do_add_format(db, id, fmt, path):
+ db.add_format_with_hooks(id, fmt.upper(), path, index_is_id=True)
def command_add_format(args, dbpath):
@@ -377,10 +377,10 @@ by id. You can get id by using the list command. If the format already exists, i
print >>sys.stderr, _('You must specify an id and an ebook file')
return 1
- id, file, fmt = int(args[1]), open(args[2], 'rb'), os.path.splitext(args[2])[-1]
+ id, path, fmt = int(args[1]), args[2], os.path.splitext(args[2])[-1]
if not fmt:
print _('ebook file must have an extension')
- do_add_format(get_db(dbpath, opts), id, fmt[1:], file)
+ do_add_format(get_db(dbpath, opts), id, fmt[1:], path)
return 0
def do_remove_format(db, id, fmt):
diff --git a/src/calibre/library/database.py b/src/calibre/library/database.py
index 1c1129db6b..122bded333 100644
--- a/src/calibre/library/database.py
+++ b/src/calibre/library/database.py
@@ -1471,11 +1471,13 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
(usize, data, id, ext))
self.conn.commit()
- def import_book_directory_multiple(self, dirpath):
+ def import_book_directory_multiple(self, dirpath, callback=None):
dirpath = os.path.abspath(dirpath)
duplicates = []
books = {}
for path in os.listdir(dirpath):
+ if callable(callback):
+ callback('.')
path = os.path.abspath(os.path.join(dirpath, path))
if os.path.isdir(path) or not os.access(path, os.R_OK):
continue
@@ -1500,13 +1502,18 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
duplicates.append((mi, formats))
continue
self.import_book(mi, formats)
+ if callable(callback):
+ if callback(mi.title):
+ break
return duplicates
- def import_book_directory(self, dirpath):
+ def import_book_directory(self, dirpath, callback=None):
dirpath = os.path.abspath(dirpath)
formats = []
for path in os.listdir(dirpath):
+ if callable(callback):
+ callback('.')
path = os.path.abspath(os.path.join(dirpath, path))
if os.path.isdir(path) or not os.access(path, os.R_OK):
continue
@@ -1527,6 +1534,9 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
if self.has_book(mi):
return [(mi, formats)]
self.import_book(mi, formats)
+ if callable(callback):
+ callback(mi.title)
+
def has_book(self, mi):
@@ -1535,13 +1545,19 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
def has_id(self, id):
return self.conn.get('SELECT id FROM books where id=?', (id,), all=False) is not None
- def recursive_import(self, root, single_book_per_directory=True):
+ def recursive_import(self, root, single_book_per_directory=True, callback=None):
root = os.path.abspath(root)
duplicates = []
for dirpath in os.walk(root):
- res = self.import_book_directory(dirpath[0]) if single_book_per_directory else self.import_book_directory_multiple(dirpath[0])
+ res = self.import_book_directory(dirpath[0], callback=callback) if \
+ single_book_per_directory else \
+ self.import_book_directory_multiple(dirpath[0], callback=callback)
if res is not None:
duplicates.extend(res)
+ if callable(callback):
+ if callback(''):
+ break
+
return duplicates
def export_single_format_to_dir(self, dir, indices, format, index_is_id=False):
diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
index 9f5dae70dc..244ae72aeb 100644
--- a/src/calibre/library/database2.py
+++ b/src/calibre/library/database2.py
@@ -22,7 +22,8 @@ from calibre.utils.search_query_parser import SearchQueryParser
from calibre.ebooks.metadata import string_to_authors, authors_to_string
from calibre.ebooks.metadata.meta import get_metadata
from calibre.constants import preferred_encoding, iswindows, isosx
-
+from calibre.ptempfile import PersistentTemporaryFile
+from calibre.customize.ui import run_plugins_on_import
copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
filesystem_encoding = sys.getfilesystemencoding()
@@ -37,6 +38,7 @@ def normpath(x):
return x
_filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+\[\]/]')
+
def sanitize_file_name(name, substitute='_'):
'''
Sanitize the filename `name`. All invalid characters are replaced by `substitute`.
@@ -656,6 +658,13 @@ class LibraryDatabase2(LibraryDatabase):
if self.has_format(index, format, index_is_id):
self.remove_format(id, format, index_is_id=True)
+ 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)
+
def add_format(self, index, format, stream, index_is_id=False, path=None, notify=True):
id = index if index_is_id else self.id(index)
if path is None:
@@ -1077,6 +1086,18 @@ class LibraryDatabase2(LibraryDatabase):
self.data.refresh_ids(self.conn, [id]) # Needed to update format list and size
return id
+ def run_import_plugins(self, path_or_stream, format):
+ format = format.lower()
+ if hasattr(path_or_stream, 'seek'):
+ path_or_stream.seek(0)
+ pt = PersistentTemporaryFile('_import_plugin.'+format)
+ shutil.copyfileobj(path_or_stream, pt, 1024**2)
+ pt.close()
+ path = pt.name
+ else:
+ path = path_or_stream
+ return run_plugins_on_import(path, format)
+
def add_books(self, paths, formats, metadata, uris=[], add_duplicates=True):
'''
Add a book to the database. The result cache is not updated.
@@ -1105,12 +1126,11 @@ class LibraryDatabase2(LibraryDatabase):
self.set_path(id, True)
self.conn.commit()
self.set_metadata(id, mi)
- stream = path if hasattr(path, 'read') else open(path, 'rb')
- stream.seek(0)
-
+ 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)
- if not hasattr(path, 'read'):
- stream.close()
+ stream.close()
self.conn.commit()
self.data.refresh_ids(self.conn, ids) # Needed to update format list and size
if duplicates:
diff --git a/src/calibre/linux.py b/src/calibre/linux.py
index 9e8dea355c..296024c251 100644
--- a/src/calibre/linux.py
+++ b/src/calibre/linux.py
@@ -62,7 +62,8 @@ entry_points = {
'calibre-debug = calibre.debug:main',
'calibredb = calibre.library.cli:main',
'calibre-fontconfig = calibre.utils.fontconfig:main',
- 'calibre-parallel = calibre.parallel:main',
+ 'calibre-parallel = calibre.parallel:main',
+ 'calibre-customize = calibre.customize.ui:main',
],
'gui_scripts' : [
__appname__+' = calibre.gui2.main:main',
diff --git a/src/calibre/manual/customize.rst b/src/calibre/manual/customize.rst
new file mode 100644
index 0000000000..3c00787881
--- /dev/null
+++ b/src/calibre/manual/customize.rst
@@ -0,0 +1,114 @@
+.. include:: global.rst
+
+.. currentmodule:: calibre.customize.__init__
+
+.. _customize:
+
+Customizing |app|
+==================================
+
+|app| has a highly modular design. Various parts of it can be customized. You can learn how to create
+*recipes* to add new sources of online content to |app| in the Section :ref:`news`. Here, you will learn how to
+use *plugins* to customize and control various aspects of |app|'s behavior.
+
+Theer are different kinds of plugins, corresponding to different aspects of |app|. As more and more aspects of |app|
+are modularized, new plugin types will be added.
+
+.. contents::
+ :depth: 2
+ :local:
+
+A Hello World plugin
+------------------------
+
+Suppose you have an installation of |app| that you are using to self publish various e-documents in EPUB and LRF
+format. You would like all file generated by |app| to have their publisher set as "Hello world", here's how to do it.
+Create a file name :file:`my_plugin.py` (the file name must end with plugin.py) and enter the following Python code into it:
+
+.. code-block:: python
+
+ import os
+ from calibre.customize import FileTypePlugin
+
+ class HelloWorld(FileTypePlugin):
+
+ name = 'Hello World Plugin' # Name of the plugin
+ description = 'Set the publisher to Hello World for all new conversions'
+ supported_platforms = ['windows', 'osx', 'linux'] # Platforms this plugin will run on
+ author = 'Acme Inc.' # The author of this plugin
+ version = (1, 0, 0) # The version number of this plugin
+ file_types = set(['epub', 'lrf']) # The file types that this plugin will be applied to
+ on_postprocess = True # Run this plugin after conversion is complete
+
+ def run(self, path_to_ebook):
+ from calibre.ebooks.metadata.meta import get_metadata, set_metadata
+ file = open(path_to_ebook, 'r+b')
+ ext = os.path.splitext(path_to_ebook)[-1][1:].lower()
+ mi = get_metadata(file, ext)
+ mi.publisher = 'Hello World'
+ set_metadata(file, ext, mi)
+ return path_to_ebook
+
+That's all. To add this code to |app| as a plugin, simply create a zip file with::
+
+ zip plugin.zip my_plugin.py
+
+You can download the Hello World plugin from
+`helloworld_plugin.zip `_.
+Now either use the configuration dialog in |app| GUI to add this zip file as a plugin, or
+use the command::
+
+ calibre-customize -a plugin.zip
+
+Every time you use calibre to convert a book, the plugin's :meth:`run` method will be called and the
+converted book will have its publisher set to "Hello World". For more information about
+|app|'s plugin system, read on...
+
+The Plugin base class
+------------------------
+
+As you may have noticed above, all |app| plugins are classes. The Plugin classes are organized in a hierarchy at the top of which
+is :class:`calibre.customize.Plugin`. The has excellent in source documentation for its various features, here I will discuss a
+few of the important ones.
+
+First, all plugins must supply a list of platforms they have been tested on by setting the ``supported_platforms`` member as in the
+example above.
+
+If the plugin needs to do any initialization, it should implement the :meth:`initialize` method. The path to the plugin zip file
+is available as ``self.plugin_path``. The initialization method could be used to load any needed resources from the zip file.
+
+If the plugin needs to be customized (i.e. it needs some information from the user), it should implement the :meth:`customization_help`
+method, to indicate to |app| that it needs user input. This can be useful, for example, to ask the user to input the path to a needed system
+binary or the URL of a website, etc. When |app| asks the user for the customization information, the string retuned by the :meth:`customization_help`
+method is used as help text to le thte user know what information is needed.
+
+Another useful method is :meth:`temporary_file`, which returns a file handle to an opened temporary file. If your plugin needs to make use
+of temporary files, it should use this method. Temporary file cleanup is then taken care of automatically.
+
+In addition, whenever plugins are run, their zip files are automatically added to the start of ``sys.path``, so you can directly import
+any python files you bundle in the zip files. Note that this is not available when the plugin is being initialized, only when it is being run.
+
+Finally, plugins can have a priority (a positive integer). Higher priority plugins are run in preference tolower priority ones in a given context.
+By default all plugins have priority 1. You can change that by setting the member :attr:'priority` in your subclass.
+
+See :ref:`pluginsPlugin` for details.
+
+File type plugins
+-------------------
+
+File type plugins are intended to be associated with specific file types (as identified by extension). They can be run on several different occassions.
+
+ * When books/formats are added ot the |app| database (if :attr:`on_import` is set to True).
+ * Just before an any2whatever converter is run on an input file (if :attr:`on_preprocess` is set to True).
+ * After an any2whatever converter has run, on the output file (if :attr:`on_postprocess` is set to True).
+
+File type plugins specify which file types they are associated with by specifying the :attr:`file_types` member as in the above example.
+the actual work should be done in the :meth:`run` method, which must return the path to the modified ebook (it can be the same as the original
+if the modifcations are done in place).
+
+See :ref:`pluginsFTPlugin` for details.
+
+Metadata plugins
+-------------------
+
+Metadata plugins add the ability to read/write metadata from ebook files to |app|. See :ref:`pluginsMetadataPlugin` for details.
diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst
index 450f860640..7276e177c9 100644
--- a/src/calibre/manual/faq.rst
+++ b/src/calibre/manual/faq.rst
@@ -203,7 +203,7 @@ There can be several causes for this:
If it still wont launch, start a command prompt (press the windows key and R; then type :command:`cmd.exe` in the Run dialog that appears). At the command prompt type the following command and press Enter::
- calibre-debug -c "from calibre.gui2.main import main; main()"
+ calibre-debug -g
Post any output you see in a help message on the `Forum `_.
diff --git a/src/calibre/manual/index.rst b/src/calibre/manual/index.rst
index cfda23762a..376145873e 100644
--- a/src/calibre/manual/index.rst
+++ b/src/calibre/manual/index.rst
@@ -30,6 +30,7 @@ Sections
metadata
faq
xpath
+ customize
glossary
Convenience
diff --git a/src/calibre/manual/plugins.rst b/src/calibre/manual/plugins.rst
new file mode 100644
index 0000000000..706dc281d3
--- /dev/null
+++ b/src/calibre/manual/plugins.rst
@@ -0,0 +1,100 @@
+.. include:: global.rst
+
+.. _plugins:
+
+API Documentation for plugins
+===============================
+
+.. module:: calibre.customize.__init__
+ :synopsis: Defines various abstract base classes that can be subclassed to create plugins.
+
+Defines various abstract base classes that can be subclassed to create powerful plugins. The useful
+classes are:
+
+.. contents::
+ :depth: 1
+ :local:
+
+.. _pluginsPlugin:
+
+Plugin
+-----------------
+
+.. class:: Plugin
+
+ Abstract base class that contains a number of members and methods to create your plugin. All
+ plugins must inherit from this class or a subclass of it.
+
+ The members and methods are:
+
+.. automember:: Plugin.name
+
+.. automember:: Plugin.author
+
+.. automember:: Plugin.description
+
+.. automember:: Plugin.version
+
+.. automember:: Plugin.supported_platforms
+
+.. automember:: Plugin.priority
+
+.. automember:: Plugin.minimum_calibre_version
+
+.. automember:: Plugin.can_be_disabled
+
+.. automethod:: Plugin.initialize
+
+.. automethod:: Plugin.customization_help
+
+.. automethod:: Plugin.temporary_file
+
+.. _pluginsFTPlugin:
+
+FileTypePlugin
+-----------------
+
+.. class:: Plugin
+
+ Abstract base class that contains a number of members and methods to create your file type plugin. All file type
+ plugins must inherit from this class or a subclass of it.
+
+ The members and methods are:
+
+.. automember:: FileTypePlugin.file_types
+
+.. automember:: FileTypePlugin.on_import
+
+.. automember:: FileTypePlugin.on_preprocess
+
+.. automember:: FileTypePlugin.on_postprocess
+
+.. automethod:: FileTypePlugin.run
+
+.. _pluginsMetadataPlugin:
+
+Metadata plugins
+-------------------
+
+.. class:: MetadataReaderPlugin
+
+ Abstract base class that contains a number of members and methods to create your metadata reader plugin. All metadata
+ reader plugins must inherit from this class or a subclass of it.
+
+ The members and methods are:
+
+.. automember:: MetadataReaderPlugin.file_types
+
+.. automethod:: MetadataReaderPlugin.get_metadata
+
+
+.. class:: MetadataWriterPlugin
+
+ Abstract base class that contains a number of members and methods to create your metadata writer plugin. All metadata
+ writer plugins must inherit from this class or a subclass of it.
+
+ The members and methods are:
+
+.. automember:: MetadataWriterPlugin.file_types
+
+.. automethod:: MetadataWriterPlugin.set_metadata
diff --git a/src/calibre/trac/donations/server.py b/src/calibre/trac/donations/server.py
index a251206df7..2472524e95 100644
--- a/src/calibre/trac/donations/server.py
+++ b/src/calibre/trac/donations/server.py
@@ -184,13 +184,27 @@ def expose(func):
class Server(object):
TRENDS = '/tmp/donations_trend.png'
+ MONTH_TRENDS = '/tmp/donations_month_trend.png'
def __init__(self, apache=False, root='/', data_file='/tmp/donations.xml'):
self.apache = apache
self.document_root = root
self.data_file = data_file
self.read_records()
-
+
+ def calculate_month_trend(self, days=31):
+ stats = self.get_slice(date.today()-timedelta(days=days-1), date.today())
+ fig = plt.figure(2, (8, 3), 96)#, facecolor, edgecolor, frameon, FigureClass)
+ ax = fig.add_subplot(111)
+ x = list(range(days-1, -1, -1))
+ y = stats.daily_totals
+ ax.plot(x, y)#, align='center', width=20, color='g')
+ ax.set_xlabel('Day')
+ ax.set_ylabel('Income ($)')
+ ax.hlines([stats.daily_average], 0, days-1)
+ ax.set_xlim([0, days-1])
+ fig.savefig(self.MONTH_TRENDS)
+
def calculate_trend(self):
def months(start, end):
pos = range_for_month(start.year, start.month)[0]
@@ -208,7 +222,7 @@ class Server(object):
x = [m.min for m in _months]
y = [m.total for m in _months]
ml = mdates.MonthLocator() # every month
- fig = plt.figure(None, (8, 3), 96)#, facecolor, edgecolor, frameon, FigureClass)
+ fig = plt.figure(1, (8, 3), 96)#, facecolor, edgecolor, frameon, FigureClass)
ax = fig.add_subplot(111)
ax.bar(x, y, align='center', width=20, color='g')
ax.xaxis.set_major_locator(ml)
@@ -235,6 +249,7 @@ class Server(object):
max_date = max(max_date, d)
self.earliest, self.latest = min_date, max_date
self.calculate_trend()
+ self.calculate_month_trend()
def get_slice(self, start_date, end_date):
stats = Stats([r for r in self.records if r.date >= start_date and r.date <= end_date],
@@ -299,6 +314,8 @@ class Server(object):
else:
range_stats = self.get_slice(*range_stats).to_html(num_of_countries=10)
+ today = self.get_slice(date.today(), date.today())
+
return textwrap.dedent('''\
@@ -420,6 +437,7 @@ class Server(object):
+ Donations today: $%(today).2f
%(range_stats)s
@@ -428,6 +446,8 @@ class Server(object):