Merge upstream changes.

This commit is contained in:
Marshall T. Vandegrift 2008-12-28 15:49:23 -05:00
commit d148d60640
67 changed files with 8783 additions and 5790 deletions

View File

@ -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)

View File

@ -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,

View File

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

View File

@ -0,0 +1,223 @@
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
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

View File

@ -0,0 +1,205 @@
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
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')]

299
src/calibre/customize/ui.py Normal file
View File

@ -0,0 +1,299 @@
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
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())

View File

@ -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:

View File

@ -18,6 +18,7 @@ from calibre.ptempfile import TemporaryDirectory
from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.metadata.opf2 import OPFCreator
from calibre.utils.zipfile import ZipFile
from calibre.customize.ui import run_plugins_on_preprocess
def lit2opf(path, tdir, opts):
from calibre.ebooks.lit.reader import LitReader
@ -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)

View File

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

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -12,6 +12,7 @@ from urllib import unquote
from urlparse import urlparse
from math import ceil, floor
from functools import partial
from calibre.customize.ui import run_plugins_on_postprocess
try:
from PIL import Image as PILImage
@ -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

View File

@ -16,7 +16,7 @@ def get_metadata(stream):
# Title
title = None
pat = re.compile(r'<!--.*?TITLE=(?P<q>[\'"])(.+)(?P=q).*?-->', re.DOTALL)
pat = re.compile(r'<!--.*?TITLE=(?P<q>[\'"])(.+?)(?P=q).*?-->', 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'<!--.*?AUTHOR=(?P<q>[\'"])(.+)(?P=q).*?-->', re.DOTALL)
pat = re.compile(r'<!--.*?AUTHOR=(?P<q>[\'"])(.+?)(?P=q).*?-->', 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'<!--.*?PUBLISHER=(?P<q>[\'"])(.+)(?P=q).*?-->', re.DOTALL)
pat = re.compile(r'<!--.*?PUBLISHER=(?P<q>[\'"])(.+?)(?P=q).*?-->', re.DOTALL)
match = pat.search(src)
if match:
mi.publisher = match.group(2)

View File

@ -5,36 +5,16 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
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)

View File

@ -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,

View File

@ -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])

View File

@ -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

View File

@ -1,32 +1,131 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
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()

View File

@ -767,6 +767,115 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="page_5" >
<layout class="QVBoxLayout" name="verticalLayout_6" >
<item>
<widget class="QLabel" name="label_8" >
<property name="text" >
<string>Here you can customize the behavior of Calibre by controlling what plugins it uses.</string>
</property>
<property name="wordWrap" >
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QTreeView" name="plugin_view" >
<property name="iconSize" >
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="animated" >
<bool>true</bool>
</property>
<property name="headerHidden" >
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6" >
<item>
<widget class="QPushButton" name="toggle_plugin" >
<property name="text" >
<string>Enable/&amp;Disable plugin</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="customize_plugin" >
<property name="text" >
<string>&amp;Customize plugin</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox_4" >
<property name="title" >
<string>Add new plugin</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5" >
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5" >
<item>
<widget class="QLabel" name="label_14" >
<property name="text" >
<string>Plugin &amp;file:</string>
</property>
<property name="buddy" >
<cstring>plugin_path</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="plugin_path" />
</item>
<item>
<widget class="QToolButton" name="button_plugin_browse" >
<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>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4" >
<item>
<spacer name="horizontalSpacer_2" >
<property name="orientation" >
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0" >
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="button_plugin_add" >
<property name="text" >
<string>&amp;Add</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>

View File

@ -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():

View File

@ -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:

View File

@ -2254,7 +2254,7 @@
id="mask7160"
maskUnits="userSpaceOnUse">
<path
style="opacity:1;fill:url(#radialGradient7164);fill-opacity:1;stroke:none;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.08779998;stroke-opacity:1"
style="opacity:1;fill-opacity:1;stroke:none;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.08779998;stroke-opacity:1"
d="M 137.37423,-51.650649 C 136.90963,-51.650649 136.53048,-51.183286 136.53048,-50.619399 L 136.53048,-0.18189907 C 141.47984,0.40821793 146.5415,0.72435094 151.68673,0.72435094 C 164.60291,0.72435094 176.96094,-1.191205 188.40548,-4.713149 L 188.40548,-50.619399 C 188.40548,-51.183286 188.02634,-51.650649 187.56173,-51.650649 L 137.37423,-51.650649 z "
id="path7162" />
</mask>

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 B

View File

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 282 B

View File

@ -0,0 +1,694 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://web.resource.org/cc/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.0"
id="Layer_1"
x="0px"
y="0px"
width="128"
height="128"
viewBox="0 0 112 112"
enable-background="new 0 0 112 112"
xml:space="preserve"
sodipodi:version="0.32"
inkscape:version="0.45.1"
sodipodi:docname="preferences-plugin.svg.svgz"
sodipodi:docbase="/home/david/Desktop"
inkscape:output_extension="org.inkscape.output.svgz.inkscape"><metadata
id="metadata95"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs93"><linearGradient
inkscape:collect="always"
id="linearGradient13107"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop13109" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop13111" /></linearGradient><linearGradient
id="linearGradient7263"><stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="0"
id="stop7265" /><stop
style="stop-color:#ffffff;stop-opacity:0.48453608;"
offset="1"
id="stop7267" /></linearGradient><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_9_"
id="linearGradient3216"
x1="1.999"
y1="55.862"
x2="109.998"
y2="55.862"
gradientUnits="userSpaceOnUse" /><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_9_"
id="linearGradient3218"
gradientUnits="userSpaceOnUse"
x1="1.999"
y1="55.862"
x2="109.998"
y2="55.862" /><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_9_"
id="linearGradient3222"
gradientUnits="userSpaceOnUse"
x1="1.999"
y1="55.862"
x2="109.998"
y2="55.862" /><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_9_"
id="linearGradient3226"
gradientUnits="userSpaceOnUse"
x1="1.999"
y1="55.862"
x2="109.998"
y2="55.862" /><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_9_"
id="linearGradient3230"
gradientUnits="userSpaceOnUse"
x1="1.999"
y1="55.862"
x2="109.998"
y2="55.862" /><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_9_"
id="linearGradient3234"
gradientUnits="userSpaceOnUse"
x1="1.999"
y1="55.862"
x2="109.998"
y2="55.862" /><filter
inkscape:collect="always"
id="filter3248"><feGaussianBlur
inkscape:collect="always"
stdDeviation="0.5392541"
id="feGaussianBlur3250" /></filter><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_9_"
id="linearGradient3304"
gradientUnits="userSpaceOnUse"
x1="1.999"
y1="55.862"
x2="109.998"
y2="55.862" /><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_9_"
id="linearGradient3306"
gradientUnits="userSpaceOnUse"
x1="1.999"
y1="55.862"
x2="109.998"
y2="55.862" /><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_9_"
id="linearGradient3310"
gradientUnits="userSpaceOnUse"
x1="1.999"
y1="55.862"
x2="109.998"
y2="55.862" /><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_9_"
id="linearGradient3314"
gradientUnits="userSpaceOnUse"
x1="1.999"
y1="55.862"
x2="109.998"
y2="55.862" /><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_9_"
id="linearGradient3318"
gradientUnits="userSpaceOnUse"
x1="1.999"
y1="55.862"
x2="109.998"
y2="55.862" /><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_9_"
id="linearGradient3322"
gradientUnits="userSpaceOnUse"
x1="1.999"
y1="55.862"
x2="109.998"
y2="55.862" /><filter
inkscape:collect="always"
id="filter4299"><feGaussianBlur
inkscape:collect="always"
stdDeviation="0.26962705"
id="feGaussianBlur4301" /></filter><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_9_"
id="linearGradient4328"
gradientUnits="userSpaceOnUse"
x1="88.745003"
y1="55.154892"
x2="94.44165"
y2="95.81353" /><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_9_"
id="linearGradient4334"
gradientUnits="userSpaceOnUse"
x1="91.39418"
y1="55.862"
x2="89.845459"
y2="26.322001" /><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_9_"
id="linearGradient4338"
gradientUnits="userSpaceOnUse"
x1="56.799774"
y1="11.643"
x2="80.800003"
y2="26.163515" /><filter
inkscape:collect="always"
id="filter6290"><feGaussianBlur
inkscape:collect="always"
stdDeviation="2.24"
id="feGaussianBlur6292" /></filter><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4344"
id="linearGradient9215"
x1="75.195961"
y1="-4"
x2="94.433243"
y2="113.6292"
gradientUnits="userSpaceOnUse" /><filter
inkscape:collect="always"
id="filter12134"><feGaussianBlur
inkscape:collect="always"
stdDeviation="2.4"
id="feGaussianBlur12136" /></filter><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient13107"
id="linearGradient13113"
x1="46.91169"
y1="116"
x2="48.325901"
y2="-2.5101759"
gradientUnits="userSpaceOnUse" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient13107"
id="linearGradient13161"
gradientUnits="userSpaceOnUse"
x1="46.91169"
y1="116"
x2="48.325901"
y2="-2.5101759" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient13107"
id="linearGradient13163"
gradientUnits="userSpaceOnUse"
x1="46.91169"
y1="116"
x2="48.325901"
y2="-2.5101759" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4344"
id="linearGradient13165"
gradientUnits="userSpaceOnUse"
x1="75.195961"
y1="-4"
x2="94.433243"
y2="113.6292" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4344"
id="linearGradient13167"
gradientUnits="userSpaceOnUse"
x1="75.195961"
y1="-4"
x2="94.433243"
y2="113.6292" /><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_9_"
id="linearGradient13175"
gradientUnits="userSpaceOnUse"
x1="91.39418"
y1="55.862"
x2="89.845459"
y2="26.322001" /><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_9_"
id="linearGradient13179"
gradientUnits="userSpaceOnUse"
x1="88.745003"
y1="55.154892"
x2="94.44165"
y2="95.81353" /><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_9_"
id="linearGradient13183"
gradientUnits="userSpaceOnUse"
x1="1.999"
y1="55.862"
x2="109.998"
y2="55.862" /><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_9_"
id="linearGradient13187"
gradientUnits="userSpaceOnUse"
x1="1.999"
y1="55.862"
x2="109.998"
y2="55.862" /><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_9_"
id="linearGradient13191"
gradientUnits="userSpaceOnUse"
x1="56.799774"
y1="11.643"
x2="80.800003"
y2="26.163515" /><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_9_"
id="linearGradient13221"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.009217,0,0,0.9999274,-0.5151412,-5.9972184)"
x1="35.38496"
y1="13.143368"
x2="70.239304"
y2="69.509804" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient7263"
id="linearGradient13223"
gradientUnits="userSpaceOnUse"
x1="1.999"
y1="35.897853"
x2="109.99844"
y2="35.897853"
gradientTransform="matrix(1.0092222,0,0,0.9999326,-0.5151516,-5.9975816)" /><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_2_"
id="linearGradient13243"
x1="62.578949"
y1="127.77134"
x2="58.966991"
y2="-31.68124"
gradientUnits="userSpaceOnUse" /><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_9_"
id="linearGradient13245"
gradientUnits="userSpaceOnUse"
x1="1.999"
y1="55.862"
x2="109.998"
y2="55.862" /><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_9_"
id="linearGradient13247"
gradientUnits="userSpaceOnUse"
x1="23.533312"
y1="107.83435"
x2="28.680723"
y2="78.514572" /><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_9_"
id="linearGradient13249"
gradientUnits="userSpaceOnUse"
x1="1.999"
y1="55.862"
x2="109.998"
y2="55.862" /><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_9_"
id="linearGradient13251"
gradientUnits="userSpaceOnUse"
x1="1.999"
y1="55.862"
x2="109.998"
y2="55.862" /><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_9_"
id="linearGradient13257"
gradientUnits="userSpaceOnUse"
x1="88.745003"
y1="55.154892"
x2="94.44165"
y2="95.81353" /><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_9_"
id="linearGradient13259"
gradientUnits="userSpaceOnUse"
x1="63.870842"
y1="86.705002"
x2="109.998"
y2="55.862" /><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_9_"
id="linearGradient13265"
gradientUnits="userSpaceOnUse"
x1="56.799774"
y1="11.643"
x2="80.800003"
y2="26.163515" /><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_9_"
id="linearGradient13267"
gradientUnits="userSpaceOnUse"
x1="45.839619"
y1="18.38534"
x2="109.998"
y2="55.862" /><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_9_"
id="linearGradient13269"
gradientUnits="userSpaceOnUse"
x1="91.39418"
y1="55.862"
x2="89.845459"
y2="26.322001" /><linearGradient
inkscape:collect="always"
xlink:href="#SVGID_9_"
id="linearGradient13271"
gradientUnits="userSpaceOnUse"
x1="63.870842"
y1="21.023001"
x2="109.998"
y2="55.862" /><filter
inkscape:collect="always"
id="filter18426"><feGaussianBlur
inkscape:collect="always"
stdDeviation="1.12"
id="feGaussianBlur18428" /></filter></defs><sodipodi:namedview
inkscape:window-height="713"
inkscape:window-width="1024"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
guidetolerance="10.0"
gridtolerance="10.0"
objecttolerance="10.0"
borderopacity="1.0"
bordercolor="#666666"
pagecolor="#ffffff"
id="base"
width="128px"
height="128px"
inkscape:zoom="2.8284271"
inkscape:cx="56"
inkscape:cy="62.542861"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:current-layer="g3266" />
<linearGradient
id="SVGID_2_"
gradientUnits="userSpaceOnUse"
x1="55.998501"
y1="122.1953"
x2="55.998501"
y2="-27.688801">
<stop
offset="0"
style="stop-color:#00892C"
id="stop13" />
<stop
offset="0.0791"
style="stop-color:#259E34"
id="stop15" />
<stop
offset="0.1681"
style="stop-color:#48B23B"
id="stop17" />
<stop
offset="0.2522"
style="stop-color:#61C041"
id="stop19" />
<stop
offset="0.3286"
style="stop-color:#71C944"
id="stop21" />
<stop
offset="0.3901"
style="stop-color:#71d73a;stop-opacity:1;"
id="stop23" />
<stop
offset="1"
style="stop-color:#00892C"
id="stop25" />
</linearGradient>
<linearGradient
id="SVGID_8_"
gradientUnits="userSpaceOnUse"
x1="65.537102"
y1="106.5479"
x2="65.537102"
y2="2.0360999">
<stop
offset="0"
style="stop-color:#F8FFBF"
id="stop77" />
<stop
offset="0.4"
style="stop-color:#FFFFFF"
id="stop79" />
<stop
offset="1"
style="stop-color:#F8FFBF"
id="stop81" />
</linearGradient>
<linearGradient
id="SVGID_9_"
gradientUnits="userSpaceOnUse"
x1="29.021"
y1="6.0723"
x2="70.239304"
y2="69.509804">
<stop
offset="0"
style="stop-color:#FFFFFF"
id="stop86" />
<stop
offset="1"
style="stop-color:#ffffff;stop-opacity:0;"
id="stop88" />
</linearGradient>
<g
id="g3266"
transform="matrix(0.875,0,0,0.875,6.9999998,7)"><g
style="opacity:0.5;fill:#000000;fill-opacity:1;stroke:url(#linearGradient13161);stroke-width:8;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter12134)"
id="g11155"
transform="translate(0,-2)">
<linearGradient
style="stroke:url(#linearGradient13113)"
y2="144.4818"
x2="126.7421"
y1="5.7275"
x1="19.7563"
gradientUnits="userSpaceOnUse"
id="linearGradient11157">
<stop
id="stop11159"
style="stop-color:#64ba32;stop-opacity:1;stroke:url(#linearGradient13113)"
offset="0" />
<stop
id="stop11161"
style="stop-color:#00892C;stroke:url(#linearGradient13113)"
offset="1" />
</linearGradient>
<path
id="path11163"
d="M 63.718,0 C 71.232,0 78.445,0.652 82,6 C 85.555,11.348 75.978,14.839 78.645,19.459 C 85.19,22.815 101.277,22.06 112,22 C 112,22 112,53.03 112,53.376 C 106.208,56.712 100.089,51.931 95,51 C 85.395,55.274 85.348,78.515 95,82.68 C 100.722,83.13 100.419,77.649 107,78.96 C 112.013,85.694 112,112 112,112 L 22,112 C 22,112 22.422,83.565 19.538,80.682 C 16.654,77.799 14.483,85.954 7.26,84.41 C 2.943,82.345 0,78.605 0,68 C 0,57.395 2.812,52.71 7.068,50.316 C 13.635,48.93 14.699,57.379 18.178,53.9 C 21.657,50.421 22.07,34.826 22,22 C 32.448,22 45.674,24 52,19 C 52.428,13.665 48.199,13.695 48,9 C 49.149,2.904 56.22,0.222 63.718,0 z "
clip-rule="evenodd"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient13163);stroke-width:8;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g><g
id="g4342"
style="fill:none;stroke:url(#linearGradient13165);stroke-width:8;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
transform="translate(0,-4)">
<linearGradient
id="linearGradient4344"
gradientUnits="userSpaceOnUse"
x1="19.7563"
y1="5.7275"
x2="126.7421"
y2="144.4818"
style="stroke:url(#linearGradient9215)">
<stop
offset="0"
style="stop-color:#64ba32;stop-opacity:1;"
id="stop4346" />
<stop
offset="1"
style="stop-color:#00892C;stroke:url(#linearGradient9215)"
id="stop4348" />
</linearGradient>
<path
style="fill:none;fill-rule:evenodd;stroke:url(#linearGradient13167);stroke-width:8;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
clip-rule="evenodd"
d="M 63.718,0 C 71.232,0 78.445,0.652 82,6 C 85.555,11.348 75.978,14.839 78.645,19.459 C 85.19,22.815 101.277,22.06 112,22 C 112,22 112,53.03 112,53.376 C 106.208,56.712 100.089,51.931 95,51 C 85.395,55.274 85.348,78.515 95,82.68 C 100.722,83.13 100.419,77.649 107,78.96 C 112.013,85.694 112,112 112,112 L 22,112 C 22,112 22.422,83.565 19.538,80.682 C 16.654,77.799 14.483,85.954 7.26,84.41 C 2.943,82.345 0,78.605 0,68 C 0,57.395 2.812,52.71 7.068,50.316 C 13.635,48.93 14.699,57.379 18.178,53.9 C 21.657,50.421 22.07,34.826 22,22 C 32.448,22 45.674,24 52,19 C 52.428,13.665 48.199,13.695 48,9 C 49.149,2.904 56.22,0.222 63.718,0 z "
id="path4350" />
</g><g
id="g3"
style="filter:url(#filter18426);fill:url(#linearGradient13243)"
transform="translate(0,-4)">
<linearGradient
y2="144.4818"
x2="126.7421"
y1="5.7275"
x1="19.7563"
gradientUnits="userSpaceOnUse"
id="SVGID_1_"
style="fill:url(#linearGradient13243)">
<stop
id="stop6"
style="stop-color:#76CC45;fill:url(#linearGradient13243)"
offset="0" />
<stop
id="stop8"
style="stop-color:#00892C;fill:url(#linearGradient13243)"
offset="1" />
</linearGradient>
<path
id="path10"
d="M 63.718,0 C 71.232,0 78.445,0.652 82,6 C 85.555,11.348 75.978,14.839 78.645,19.459 C 85.19,22.815 101.277,22.06 112,22 C 112,22 112,53.03 112,53.376 C 106.208,56.712 100.089,51.931 95,51 C 85.395,55.274 85.348,78.515 95,82.68 C 100.722,83.13 100.419,77.649 107,78.96 C 112.013,85.694 112,112 112,112 L 22,112 C 22,112 22.422,83.565 19.538,80.682 C 16.654,77.799 14.483,85.954 7.26,84.41 C 2.943,82.345 0,78.605 0,68 C 0,57.395 2.812,52.71 7.068,50.316 C 13.635,48.93 14.699,57.379 18.178,53.9 C 21.657,50.421 22.07,34.826 22,22 C 32.448,22 45.674,24 52,19 C 52.428,13.665 48.199,13.695 48,9 C 49.149,2.904 56.22,0.222 63.718,0 z "
clip-rule="evenodd"
style="fill:url(#linearGradient13243);fill-rule:evenodd" />
</g><path
id="path90"
d="M 108.47928,18.045036 C 108.47121,18.045036 108.46414,18.045036 108.45708,18.045036 L 107.81521,18.053036 C 105.52429,18.084034 103.11328,18.115032 100.68005,18.115032 C 88.864141,18.115032 81.849073,17.229095 77.934321,15.239241 L 77.4065,14.96926 L 77.107772,14.460297 C 75.306319,11.364522 76.910974,8.8507039 79.038404,6.1039029 C 80.439197,4.2960347 80.948851,3.5030919 81.02858,2.6521537 C 81.049773,2.4281699 81.030598,2.2031862 80.9761,1.9852021 C 80.907474,1.7072222 80.767192,1.4132435 80.562321,1.1072657 C 77.722385,-3.1224273 71.824521,-3.994364 63.779044,-3.9963639 C 61.025899,-3.9093702 51.906615,-3.1664241 50.081951,2.6851512 C 49.984057,3.0041281 49.964882,3.3441034 50.03149,3.6710797 C 50.244435,4.7130036 50.809597,5.3829555 51.522103,6.2298944 C 52.749312,7.6837885 54.27222,9.4956574 53.977528,13.160391 L 53.908902,14.028328 L 53.217588,14.569289 C 48.912269,17.942044 42.37557,18.396011 36.548351,18.396011 C 34.030355,18.396011 31.426576,18.294019 28.850044,18.195025 C 27.809541,18.154029 26.791242,18.114032 25.799182,18.081034 C 25.77597,18.081034 25.753767,18.079034 25.730555,18.079034 C 25.209799,18.079034 24.708218,18.28002 24.330771,18.637994 C 23.937176,19.012966 23.716157,19.528929 23.71212,20.06889 C 23.599089,42.061294 21.398995,47.195921 19.260464,49.316767 C 18.284552,50.279697 17.279372,50.747663 16.18639,50.747663 C 14.451546,50.747663 13.214247,49.531751 12.11319,48.44883 L 11.91942,48.260844 L 11.54702,47.90087 C 10.410641,46.825948 9.4296827,46.161996 8.0591661,46.161996 C 7.658507,46.161996 7.2174791,46.281987 6.8824192,46.503971 C 3.2118971,48.951793 1.5022835,53.875437 1.5022835,61.997847 C 1.5022835,62.619802 1.5133849,63.212759 1.5345785,63.780717 C 31.217668,64.093694 70.03518,58.029135 110.49772,33.866889 L 110.49772,20.045892 C 110.49772,19.51193 110.28275,18.999967 109.89824,18.622995 C 109.5218,18.254021 109.01013,18.045036 108.47928,18.045036 z "
clip-rule="evenodd"
style="fill:url(#linearGradient13221);fill-rule:evenodd;stroke:url(#linearGradient13223);stroke-width:1.00456667" /><g
id="g31"
style="fill:url(#linearGradient13269);filter:url(#filter4299)"
transform="translate(0,-6)">
<linearGradient
id="SVGID_3_"
gradientUnits="userSpaceOnUse"
x1="73.623001"
y1="23.672899"
x2="118.9906"
y2="23.672899"
style="fill:url(#linearGradient3216)">
<stop
offset="0"
style="stop-color:#FFFFFF;fill:url(#linearGradient3216)"
id="stop34" />
<stop
offset="1"
style="stop-color:#000000;fill:url(#linearGradient3216)"
id="stop36" />
</linearGradient>
<path
clip-rule="evenodd"
d="M 77.314,21.023 C 77.438,21.49 77.628,21.966 77.913,22.46 L 78.209,22.969 L 78.732,23.239 C 82.611,25.229 89.562,26.115 101.27,26.115 C 103.681,26.115 106.071,26.084 108.34,26.053 L 108.976,26.045 C 108.984,26.045 108.991,26.045 108.998,26.045 C 109.353,26.045 109.696,26.147 109.998,26.322 L 109.998,26.045 C 109.998,25.511 109.785,24.999 109.404,24.622 C 109.031,24.253 108.524,24.044 107.998,24.044 C 107.99,24.044 107.983,24.044 107.976,24.044 L 107.34,24.052 C 105.07,24.083 102.681,24.114 100.27,24.114 C 88.562,24.114 81.611,23.228 77.732,21.238 L 77.314,21.023 z "
id="path38"
style="fill:url(#linearGradient13271);fill-rule:evenodd" />
</g><g
id="g40"
style="fill:url(#linearGradient13257);filter:url(#filter4299)"
transform="translate(2,-6)">
<linearGradient
id="SVGID_4_"
gradientUnits="userSpaceOnUse"
x1="86.693398"
y1="83.060501"
x2="111.9058"
y2="83.060501"
style="fill:url(#linearGradient3216)">
<stop
offset="0"
style="stop-color:#FFFFFF;fill:url(#linearGradient3216)"
id="stop43" />
<stop
offset="1"
style="stop-color:#000000;fill:url(#linearGradient3216)"
id="stop45" />
</linearGradient>
<path
clip-rule="evenodd"
d="M 95.208,86.515 L 95.513,86.648 L 95.842,86.672 C 96.125,86.694 96.395,86.705 96.652,86.705 C 99.594,86.705 101.274,85.371 102.622,84.301 C 103.601,83.521 104.245,83.038 105.18,82.843 C 105.317,82.815 105.451,82.802 105.585,82.802 C 106.084,82.802 106.547,83.005 106.91,83.332 C 106.755,82.863 106.595,82.417 106.426,82.02 C 106.108,81.27 105.374,80.802 104.585,80.802 C 104.451,80.802 104.316,80.815 104.18,80.843 C 103.245,81.038 102.602,81.521 101.622,82.301 C 100.274,83.371 98.595,84.705 95.652,84.705 C 95.395,84.705 95.126,84.694 94.842,84.672 L 94.513,84.648 L 94.208,84.515 C 91.836,83.493 90.052,81.66 88.745,79.415 C 90.08,82.559 92.163,85.202 95.208,86.515 z "
id="path47"
style="fill:url(#linearGradient13259);fill-rule:evenodd" />
</g><g
id="g49"
style="fill:url(#linearGradient13249);filter:url(#filter4299)"
transform="translate(0,-6)">
<linearGradient
id="SVGID_5_"
gradientUnits="userSpaceOnUse"
x1="-3.9907"
y1="47.521"
x2="69.618896"
y2="47.521"
style="fill:url(#linearGradient3216)">
<stop
offset="0"
style="stop-color:#FFFFFF;fill:url(#linearGradient3216)"
id="stop52" />
<stop
offset="1"
style="stop-color:#000000;fill:url(#linearGradient3216)"
id="stop54" />
</linearGradient>
<path
clip-rule="evenodd"
d="M 2.999,70 C 2.999,61.877 4.693,56.953 8.327,54.503 C 8.659,54.281 9.096,54.161 9.493,54.161 C 10.851,54.161 11.823,54.826 12.949,55.9 L 13.318,56.26 L 13.51,56.448 C 14.601,57.531 15.827,58.747 17.546,58.747 C 18.629,58.747 19.625,58.278 20.592,57.315 C 22.711,55.195 24.891,50.06 25.003,28.066 C 25.007,27.526 25.227,27.01 25.616,26.635 C 25.991,26.277 26.488,26.076 27.003,26.076 C 27.026,26.076 27.048,26.078 27.071,26.078 C 28.054,26.111 29.063,26.151 30.094,26.192 C 32.647,26.292 35.227,26.393 37.722,26.393 C 43.495,26.393 49.972,25.939 54.239,22.566 L 54.924,22.025 L 54.992,21.157 C 55.2,18.538 54.487,16.873 53.62,15.601 C 53.936,16.562 54.107,17.711 53.992,19.157 L 53.924,20.025 L 53.239,20.566 C 48.973,23.939 42.496,24.393 36.722,24.393 C 34.227,24.393 31.647,24.291 29.094,24.192 C 28.063,24.151 27.054,24.111 26.071,24.078 C 26.048,24.078 26.026,24.076 26.003,24.076 C 25.487,24.076 24.99,24.277 24.616,24.635 C 24.226,25.01 24.007,25.526 24.003,26.066 C 23.891,48.06 21.711,53.195 19.592,55.316 C 18.625,56.279 17.629,56.747 16.546,56.747 C 14.827,56.747 13.601,55.531 12.51,54.448 L 12.318,54.26 L 11.949,53.9 C 10.823,52.825 9.851,52.161 8.493,52.161 C 8.096,52.161 7.659,52.281 7.327,52.503 C 3.693,54.953 1.999,59.877 1.999,68 C 1.999,73.927 2.971,77.338 4.329,79.438 C 3.52,77.321 2.999,74.334 2.999,70 z "
id="path56"
style="fill:url(#linearGradient13251);fill-rule:evenodd" />
</g><g
id="g58"
style="fill:url(#linearGradient13245);filter:url(#filter4299)"
transform="translate(-2,-4)">
<linearGradient
id="SVGID_6_"
gradientUnits="userSpaceOnUse"
x1="21.6807"
y1="95.531197"
x2="25.875299"
y2="95.531197"
style="fill:url(#linearGradient3216)">
<stop
offset="0"
style="stop-color:#FFFFFF;fill:url(#linearGradient3216)"
id="stop61" />
<stop
offset="1"
style="stop-color:#000000;fill:url(#linearGradient3216)"
id="stop63" />
</linearGradient>
<path
clip-rule="evenodd"
d="M 22.022,81.343 C 23.062,84.495 24.117,91.695 24.041,107.991 C 24.039,108.52 24.248,109.032 24.623,109.409 C 24.748,109.534 24.892,109.634 25.042,109.721 C 25.125,88.172 23.22,82.671 22.022,81.343 z "
id="path65"
style="fill:url(#linearGradient13247);fill-rule:evenodd" />
</g><g
id="g67"
style="fill:url(#linearGradient13265);filter:url(#filter4299)"
transform="translate(0,-6)">
<linearGradient
id="SVGID_7_"
gradientUnits="userSpaceOnUse"
x1="46.571301"
y1="6.8223"
x2="89.260399"
y2="6.8223"
style="fill:url(#linearGradient3216)">
<stop
offset="0"
style="stop-color:#FFFFFF;fill:url(#linearGradient3216)"
id="stop70" />
<stop
offset="1"
style="stop-color:#000000;fill:url(#linearGradient3216)"
id="stop72" />
</linearGradient>
<path
clip-rule="evenodd"
d="M 51.082,11.643 C 51.022,11.324 51.039,10.995 51.134,10.684 C 52.942,4.832 61.979,4.089 64.706,4.002 C 72.203,4.004 77.816,4.776 80.8,8.387 C 80.794,8.252 80.779,8.116 80.746,7.985 C 80.678,7.707 80.539,7.413 80.336,7.107 C 77.522,2.877 71.678,2.005 63.706,2.003 C 60.978,2.09 51.942,2.833 50.134,8.685 C 50.037,9.004 50.018,9.344 50.084,9.671 C 50.245,10.461 50.607,11.04 51.082,11.643 z "
id="path74"
style="fill:url(#linearGradient13267);fill-rule:evenodd" />
</g></g>
</svg>

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -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:

View File

@ -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 = _('<p>Books with the same title as the following already exist in the database. Add them anyway?<ul>')
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 = _('<p>Books with the same title as the following already exist in the database. Add them anyway?<ul>')
for mi in duplicates[2]:
files += '<li>'+mi.title+'</li>\n'
d = WarningDialog(_('Duplicates found!'), _('Duplicates found!'), files+'</ul></p>', 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 = _('<p>Books with the same title as the following already exist in the database. Add them anyway?<ul>')
for mi in duplicates[2]:
files += '<li>'+mi.title+'</li>\n'
d = WarningDialog(_('Duplicates found!'), _('Duplicates found!'), files+'</ul></p>', 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):
'''

View File

@ -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())

View File

@ -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)

View File

@ -27,8 +27,8 @@
<widget class="QWidget" name="layoutWidget" >
<layout class="QGridLayout" name="gridLayout" >
<item row="0" column="0" >
<widget class="QWebView" name="view" >
<property name="url" >
<widget class="QWebView" native="1" name="view" >
<property name="url" stdset="0" >
<url>
<string>about:blank</string>
</url>
@ -88,6 +88,7 @@
<addaction name="action_reference_mode" />
<addaction name="separator" />
<addaction name="action_preferences" />
<addaction name="action_full_screen" />
</widget>
<widget class="QToolBar" name="tool_bar2" >
<attribute name="toolBarArea" >
@ -224,6 +225,15 @@
<string>Bookmark</string>
</property>
</action>
<action name="action_full_screen" >
<property name="icon" >
<iconset resource="../images.qrc" >
<normaloff>:/images/page.svg</normaloff>:/images/page.svg</iconset>
</property>
<property name="text" >
<string>Toggle full screen</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>

View File

@ -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):

View File

@ -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):

View File

@ -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:

View File

@ -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',

View File

@ -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 <http://calibre.kovidgoyal.net/downloads/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.

View File

@ -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 <http://www.mobileread.com/forums/forumdisplay.php?f=166>`_.

View File

@ -30,6 +30,7 @@ Sections
metadata
faq
xpath
customize
glossary
Convenience

View File

@ -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

View File

@ -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('''\
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
@ -420,6 +437,7 @@ class Server(object):
<input type="submit" value="Update" />
</form>
</fieldset>
<b>Donations today: $%(today).2f</b><br />
%(range_stats)s
</td>
</tr>
@ -428,6 +446,8 @@ class Server(object):
<div style="text-align:center">
<h3>Income trends for the last year</h3>
<img src="%(root)strend.png" alt="Income trends" />
<h3>Income trends for the last 31 days</h3>
<img src="%(root)smonth_trend.png" alt="Month income trend" />
</div>
</body>
</html>
@ -438,6 +458,7 @@ class Server(object):
rc = 'checked="checked"' if period_type=="range" else '',
month_month=mmlist, month_year=mylist, year_year=yylist,
rl=rl, rr=rr, range_stats=range_stats, root=self.document_root,
today=today.total
)
@expose
@ -452,6 +473,11 @@ class Server(object):
cherrypy.response.headers['Content-Type'] = 'image/png'
return open(self.TRENDS, 'rb').read()
@expose
def month_trend_png(self):
cherrypy.response.headers['Content-Type'] = 'image/png'
return open(self.MONTH_TRENDS, 'rb').read()
@expose
def show(self, period_type='month', month_month='', month_year='',
year_year='', range_left='', range_right=''):

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -28,9 +28,11 @@ else:
bdir = os.path.abspath(os.path.expanduser(os.environ.get('XDG_CONFIG_HOME', '~/.config')))
config_dir = os.path.join(bdir, 'calibre')
plugin_dir = os.path.join(config_dir, 'plugins')
def make_config_dir():
if not os.path.exists(config_dir):
os.makedirs(config_dir, mode=448) # 0700 == 448
if not os.path.exists(plugin_dir):
os.makedirs(plugin_dir, mode=448) # 0700 == 448
class CustomHelpFormatter(IndentedHelpFormatter):
@ -78,6 +80,7 @@ class OptionParser(_OptionParser):
gui_mode=False,
conflict_handler='resolve',
**kwds):
usage = textwrap.dedent(usage)
usage += '''\n\nWhenever you pass arguments to %prog that have spaces in them, '''\
'''enclose the arguments in quotation marks.'''
_OptionParser.__init__(self, usage=usage, version=version, epilog=epilog,

View File

@ -19,7 +19,7 @@ recipe_modules = [
'clarin', 'financial_times', 'heise', 'le_monde', 'harpers', 'science_aas',
'science_news', 'the_nation', 'lrb', 'harpers_full', 'liberation',
'linux_magazine', 'telegraph_uk', 'utne', 'sciencedaily', 'forbes',
'time',
'time_magazine', 'endgadget', 'fudzilla',
]
import re, imp, inspect, time, os

View File

@ -0,0 +1,30 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Darko Miletic <darko.miletic at gmail.com>'
'''
engadget.com
'''
import string,re
from calibre.web.feeds.news import BasicNewsRecipe
class Engadget(BasicNewsRecipe):
title = u'Engadget'
__author__ = 'Darko Miletic'
description = 'Tech news'
oldest_article = 7
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
keep_only_tags = [ dict(name='div', attrs={'class':'post'}) ]
remove_tags = [
dict(name='object')
,dict(name='div', attrs={'class':'postmeta'})
,dict(name='div', attrs={'class':'quigoads'})
]
feeds = [ (u'Posts', u'http://www.engadget.com/rss.xml')]

View File

@ -0,0 +1,26 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Darko Miletic <darko.miletic at gmail.com>'
'''
fudzilla.com
'''
import string,re
from calibre.web.feeds.news import BasicNewsRecipe
class Fudzilla(BasicNewsRecipe):
title = u'Fudzilla'
__author__ = 'Darko Miletic'
description = 'Tech news'
oldest_article = 7
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
feeds = [ (u'Posts', u'http://www.fudzilla.com/index.php?option=com_rss&feed=RSS2.0&no_html=1')]
def print_version(self, url):
nurl = url.replace('http://www.fudzilla.com/index.php','http://www.fudzilla.com/index2.php')
nmain, nsep, nrest = nurl.partition('&Itemid=')
return nmain + '&pop=1&page=0&Itemid=1'

View File

@ -18,7 +18,7 @@ class Time(BasicNewsRecipe):
no_stylesheets = False
use_embedded_content = False
cover_url = 'http://img.timeinc.net/time/rd/trunk/www/web/feds/i/logo_time_home.gif'
#cover_url = 'http://img.timeinc.net/time/rd/trunk/www/web/feds/i/logo_time_home.gif'
keep_only_tags = [dict(name='div', attrs={'class':'tout1'})]
@ -32,7 +32,14 @@ class Time(BasicNewsRecipe):
,(u'Politics', u'http://feedproxy.google.com/time/politics')
,(u'Travel', u'http://feedproxy.google.com/time/travel')
]
def get_cover_url(self):
soup = self.index_to_soup('http://www.time.com/time/')
img = soup.find('img', alt='Current Time.com Cover', width='107')
if img is not None:
return img.get('src', None)
def print_version(self, url):
raw = self.browser.open(url).read()
soup = BeautifulSoup(raw.decode('utf8', 'replace'))

View File

@ -217,7 +217,7 @@ def stage_one():
os.mkdir('build')
shutil.rmtree('docs')
os.mkdir('docs')
check_call('python setup.py build', shell=True)
check_call('python setup.py build_ext build', shell=True)
check_call('sudo python setup.py develop', shell=True)
tag_release()
upload_demo()
@ -235,12 +235,12 @@ def stage_three():
print 'Uploading to PyPI...'
check_call('rm -f dist/*')
check_call('python setup.py register')
check_call('sudo rm -rf build')
check_call('sudo rm -rf build src/calibre/plugins/*')
os.mkdir('build')
check_call('python2.5 setup.py bdist_egg --exclude-source-files upload')
shutil.rmtree('build')
check_call('python2.5 setup.py build_ext bdist_egg --exclude-source-files upload')
check_call('sudo rm -rf build src/calibre/plugins/*')
os.mkdir('build')
check_call('python setup.py bdist_egg --exclude-source-files upload')
check_call('python setup.py build_ext bdist_egg --exclude-source-files upload')
check_call('python setup.py sdist upload')
upload_src_tarball()
check_call('''rm -rf dist/* build/*''')