Merge upstream changes.
@ -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)
|
||||
|
51
setup.py
@ -47,6 +47,7 @@ main_functions = {
|
||||
|
||||
if __name__ == '__main__':
|
||||
from setuptools import setup, find_packages
|
||||
from setuptools.command.build_py import build_py as _build_py, convert_path
|
||||
from distutils.command.build import build as _build
|
||||
from distutils.core import Command as _Command
|
||||
from pyqtdistutils import PyQtExtension, build_ext, Extension
|
||||
@ -65,6 +66,25 @@ if __name__ == '__main__':
|
||||
newest_source, oldest_target = max(stimes), min(ttimes)
|
||||
return newest_source > oldest_target
|
||||
|
||||
class build_py(_build_py):
|
||||
|
||||
def find_data_files(self, package, src_dir):
|
||||
"""
|
||||
Return filenames for package's data files in 'src_dir'
|
||||
Modified to treat data file specs as paths not globs
|
||||
"""
|
||||
globs = (self.package_data.get('', [])
|
||||
+ self.package_data.get(package, []))
|
||||
files = self.manifest_files.get(package, [])[:]
|
||||
for pattern in globs:
|
||||
# Each pattern has to be converted to a platform-specific path
|
||||
pattern = os.path.join(src_dir, convert_path(pattern))
|
||||
next = glob.glob(pattern)
|
||||
files.extend(next if next else [pattern])
|
||||
|
||||
return self.exclude_data_files(package, src_dir, files)
|
||||
|
||||
|
||||
class Command(_Command):
|
||||
user_options = []
|
||||
def initialize_options(self): pass
|
||||
@ -252,6 +272,7 @@ if __name__ == '__main__':
|
||||
description='''Compile all GUI forms and images'''
|
||||
PATH = os.path.join('src', APPNAME, 'gui2')
|
||||
IMAGES_DEST = os.path.join(PATH, 'images_rc.py')
|
||||
QRC = os.path.join(PATH, 'images.qrc')
|
||||
|
||||
@classmethod
|
||||
def find_forms(cls):
|
||||
@ -331,9 +352,9 @@ if __name__ == '__main__':
|
||||
c = cls.form_to_compiled_form(form)
|
||||
if os.path.exists(c):
|
||||
os.remove(c)
|
||||
images = cls.IMAGES_DEST
|
||||
if os.path.exists(images):
|
||||
os.remove(images)
|
||||
for x in (cls.IMAGES_DEST, cls.QRC):
|
||||
if os.path.exists(x):
|
||||
os.remove(x)
|
||||
|
||||
class clean(Command):
|
||||
description='''Delete all computer generated files in the source tree'''
|
||||
@ -349,17 +370,13 @@ if __name__ == '__main__':
|
||||
os.remove(f)
|
||||
for root, dirs, files in os.walk('.'):
|
||||
for name in files:
|
||||
if name.endswith('~') or \
|
||||
name.endswith('.pyc') or \
|
||||
name.endswith('.pyo'):
|
||||
os.remove(os.path.join(root, name))
|
||||
for t in ('.pyc', '.pyo', '~'):
|
||||
if name.endswith(t):
|
||||
os.remove(os.path.join(root, name))
|
||||
break
|
||||
|
||||
for dir in 'build', 'dist':
|
||||
for f in os.listdir(dir):
|
||||
if os.path.isdir(dir + os.sep + f):
|
||||
shutil.rmtree(dir + os.sep + f)
|
||||
else:
|
||||
os.remove(dir + os.sep + f)
|
||||
for dir in ('build', 'dist', os.path.join('src', 'calibre.egg-info')):
|
||||
shutil.rmtree(dir, ignore_errors=True)
|
||||
|
||||
class build(_build):
|
||||
|
||||
@ -405,6 +422,8 @@ if __name__ == '__main__':
|
||||
extra_link_args=['-framework', 'IOKit'])
|
||||
)
|
||||
|
||||
plugins = ['plugins/%s.so'%(x.name.rpartition('.')[-1]) for x in ext_modules]
|
||||
|
||||
setup(
|
||||
name = APPNAME,
|
||||
packages = find_packages('src'),
|
||||
@ -413,8 +432,7 @@ if __name__ == '__main__':
|
||||
author = 'Kovid Goyal',
|
||||
author_email = 'kovid@kovidgoyal.net',
|
||||
url = 'http://%s.kovidgoyal.net'%APPNAME,
|
||||
package_data = {'calibre':['plugins/*']},
|
||||
include_package_data = True,
|
||||
package_data = {'calibre':plugins},
|
||||
entry_points = entry_points,
|
||||
zip_safe = False,
|
||||
options = { 'bdist_egg' : {'exclude_source_files': True,}, },
|
||||
@ -455,7 +473,8 @@ if __name__ == '__main__':
|
||||
],
|
||||
cmdclass = {
|
||||
'build_ext' : build_ext,
|
||||
'build' : build,
|
||||
'build' : build,
|
||||
'build_py' : build_py,
|
||||
'pot' : pot,
|
||||
'manual' : manual,
|
||||
'resources' : resources,
|
||||
|
@ -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.
|
||||
|
223
src/calibre/customize/__init__.py
Normal 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
|
||||
|
205
src/calibre/customize/builtins.py
Normal 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
@ -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())
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -34,6 +34,7 @@ from calibre import LoggingInterface
|
||||
from calibre import plugins
|
||||
msdes, msdeserror = plugins['msdes']
|
||||
import calibre.ebooks.lit.mssha1 as mssha1
|
||||
from calibre.customize.ui import run_plugins_on_postprocess
|
||||
|
||||
__all__ = ['LitWriter']
|
||||
|
||||
@ -734,6 +735,7 @@ def oeb2lit(opts, opfpath):
|
||||
lit = LitWriter(OEBBook(opfpath, logger=logger), logger=logger)
|
||||
with open(litpath, 'wb') as f:
|
||||
lit.dump(f)
|
||||
run_plugins_on_postprocess(litpath, 'lit')
|
||||
logger.log_info(_('Output written to ')+litpath)
|
||||
|
||||
|
||||
|
@ -18,6 +18,8 @@ from calibre.ebooks.lrf.epub.convert_from import process_file as epub2lrf
|
||||
from calibre.ebooks.lrf.mobi.convert_from import process_file as mobi2lrf
|
||||
from calibre.ebooks.lrf.fb2.convert_from import process_file as fb22lrf
|
||||
|
||||
from calibre.customize.ui import run_plugins_on_postprocess, run_plugins_on_preprocess
|
||||
|
||||
def largest_file(files):
|
||||
maxsize, file = 0, None
|
||||
for f in files:
|
||||
@ -108,6 +110,7 @@ def odt2lrf(path, options, logger):
|
||||
|
||||
def process_file(path, options, logger=None):
|
||||
path = os.path.abspath(os.path.expanduser(path))
|
||||
path = run_plugins_on_preprocess(path)
|
||||
tdir = None
|
||||
if logger is None:
|
||||
level = logging.DEBUG if options.verbose else logging.INFO
|
||||
@ -160,6 +163,7 @@ def process_file(path, options, logger=None):
|
||||
if not convertor:
|
||||
raise UnknownFormatError(_('Converting from %s to LRF is not supported.')%ext)
|
||||
convertor(path, options, logger)
|
||||
|
||||
finally:
|
||||
os.chdir(cwd)
|
||||
if tdir and os.path.exists(tdir):
|
||||
|
@ -19,6 +19,7 @@ from calibre.ebooks.lrf.pylrs.pylrs import Book, BookSetting, ImageStream, Image
|
||||
from calibre.ebooks.metadata import MetaInformation
|
||||
from calibre.ebooks.metadata.opf import OPFCreator
|
||||
from calibre.ebooks.epub.from_html import config as html2epub_config, convert as html2epub
|
||||
from calibre.customize.ui import run_plugins_on_preprocess
|
||||
try:
|
||||
from calibre.utils.PythonMagickWand import \
|
||||
NewMagickWand, NewPixelWand, \
|
||||
@ -383,7 +384,9 @@ def create_lrf(pages, profile, opts, thumbnail=None):
|
||||
|
||||
|
||||
def do_convert(path_to_file, opts, notification=lambda m, p: p, output_format='lrf'):
|
||||
path_to_file = run_plugins_on_preprocess(path_to_file)
|
||||
source = path_to_file
|
||||
|
||||
if not opts.title:
|
||||
opts.title = os.path.splitext(os.path.basename(source))[0]
|
||||
if not opts.output:
|
||||
|
@ -12,6 +12,7 @@ from urllib import unquote
|
||||
from urlparse import urlparse
|
||||
from math import ceil, floor
|
||||
from functools import partial
|
||||
from calibre.customize.ui import run_plugins_on_postprocess
|
||||
|
||||
try:
|
||||
from PIL import Image as PILImage
|
||||
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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])
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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/&Disable plugin</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="customize_plugin" >
|
||||
<property name="text" >
|
||||
<string>&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 &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>&Add</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
|
@ -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():
|
||||
|
@ -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:
|
||||
|
@ -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 |
BIN
src/calibre/gui2/images/news/endgadget.png
Normal file
After Width: | Height: | Size: 698 B |
BIN
src/calibre/gui2/images/news/fudzilla.png
Normal file
After Width: | Height: | Size: 519 B |
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 282 B |
694
src/calibre/gui2/images/plugins.svg
Normal 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 |
@ -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:
|
||||
|
@ -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):
|
||||
'''
|
||||
|
@ -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())
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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',
|
||||
|
114
src/calibre/manual/customize.rst
Normal 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.
|
@ -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>`_.
|
||||
|
||||
|
@ -30,6 +30,7 @@ Sections
|
||||
metadata
|
||||
faq
|
||||
xpath
|
||||
customize
|
||||
glossary
|
||||
|
||||
Convenience
|
||||
|
100
src/calibre/manual/plugins.rst
Normal 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
|
@ -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=''):
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
30
src/calibre/web/feeds/recipes/endgadget.py
Normal 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')]
|
||||
|
26
src/calibre/web/feeds/recipes/fudzilla.py
Normal 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'
|
@ -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'))
|
10
upload.py
@ -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/*''')
|
||||
|