Merge upstream changes.
@ -36,7 +36,9 @@ def freeze():
|
|||||||
'/usr/lib/libpoppler.so.4',
|
'/usr/lib/libpoppler.so.4',
|
||||||
'/usr/lib/libxml2.so.2',
|
'/usr/lib/libxml2.so.2',
|
||||||
'/usr/lib/libxslt.so.1',
|
'/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]
|
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))
|
dest = os.path.join(dest_dir, os.path.basename(src))
|
||||||
if not os.path.exists(dest_dir):
|
if not os.path.exists(dest_dir):
|
||||||
os.makedirs(dest_dir)
|
os.makedirs(dest_dir)
|
||||||
shutil.copyfile(src, dest)
|
shutil.copyfile(os.path.realpath(src), dest)
|
||||||
shutil.copymode(src, dest)
|
shutil.copymode(os.path.realpath(src), dest)
|
||||||
|
|
||||||
for f in binary_includes:
|
for f in binary_includes:
|
||||||
copy_binary(f, FREEZE_DIR)
|
copy_binary(f, FREEZE_DIR)
|
||||||
|
51
setup.py
@ -47,6 +47,7 @@ main_functions = {
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
from setuptools import setup, find_packages
|
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.command.build import build as _build
|
||||||
from distutils.core import Command as _Command
|
from distutils.core import Command as _Command
|
||||||
from pyqtdistutils import PyQtExtension, build_ext, Extension
|
from pyqtdistutils import PyQtExtension, build_ext, Extension
|
||||||
@ -65,6 +66,25 @@ if __name__ == '__main__':
|
|||||||
newest_source, oldest_target = max(stimes), min(ttimes)
|
newest_source, oldest_target = max(stimes), min(ttimes)
|
||||||
return newest_source > oldest_target
|
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):
|
class Command(_Command):
|
||||||
user_options = []
|
user_options = []
|
||||||
def initialize_options(self): pass
|
def initialize_options(self): pass
|
||||||
@ -252,6 +272,7 @@ if __name__ == '__main__':
|
|||||||
description='''Compile all GUI forms and images'''
|
description='''Compile all GUI forms and images'''
|
||||||
PATH = os.path.join('src', APPNAME, 'gui2')
|
PATH = os.path.join('src', APPNAME, 'gui2')
|
||||||
IMAGES_DEST = os.path.join(PATH, 'images_rc.py')
|
IMAGES_DEST = os.path.join(PATH, 'images_rc.py')
|
||||||
|
QRC = os.path.join(PATH, 'images.qrc')
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_forms(cls):
|
def find_forms(cls):
|
||||||
@ -331,9 +352,9 @@ if __name__ == '__main__':
|
|||||||
c = cls.form_to_compiled_form(form)
|
c = cls.form_to_compiled_form(form)
|
||||||
if os.path.exists(c):
|
if os.path.exists(c):
|
||||||
os.remove(c)
|
os.remove(c)
|
||||||
images = cls.IMAGES_DEST
|
for x in (cls.IMAGES_DEST, cls.QRC):
|
||||||
if os.path.exists(images):
|
if os.path.exists(x):
|
||||||
os.remove(images)
|
os.remove(x)
|
||||||
|
|
||||||
class clean(Command):
|
class clean(Command):
|
||||||
description='''Delete all computer generated files in the source tree'''
|
description='''Delete all computer generated files in the source tree'''
|
||||||
@ -349,17 +370,13 @@ if __name__ == '__main__':
|
|||||||
os.remove(f)
|
os.remove(f)
|
||||||
for root, dirs, files in os.walk('.'):
|
for root, dirs, files in os.walk('.'):
|
||||||
for name in files:
|
for name in files:
|
||||||
if name.endswith('~') or \
|
for t in ('.pyc', '.pyo', '~'):
|
||||||
name.endswith('.pyc') or \
|
if name.endswith(t):
|
||||||
name.endswith('.pyo'):
|
os.remove(os.path.join(root, name))
|
||||||
os.remove(os.path.join(root, name))
|
break
|
||||||
|
|
||||||
for dir in 'build', 'dist':
|
for dir in ('build', 'dist', os.path.join('src', 'calibre.egg-info')):
|
||||||
for f in os.listdir(dir):
|
shutil.rmtree(dir, ignore_errors=True)
|
||||||
if os.path.isdir(dir + os.sep + f):
|
|
||||||
shutil.rmtree(dir + os.sep + f)
|
|
||||||
else:
|
|
||||||
os.remove(dir + os.sep + f)
|
|
||||||
|
|
||||||
class build(_build):
|
class build(_build):
|
||||||
|
|
||||||
@ -405,6 +422,8 @@ if __name__ == '__main__':
|
|||||||
extra_link_args=['-framework', 'IOKit'])
|
extra_link_args=['-framework', 'IOKit'])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
plugins = ['plugins/%s.so'%(x.name.rpartition('.')[-1]) for x in ext_modules]
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name = APPNAME,
|
name = APPNAME,
|
||||||
packages = find_packages('src'),
|
packages = find_packages('src'),
|
||||||
@ -413,8 +432,7 @@ if __name__ == '__main__':
|
|||||||
author = 'Kovid Goyal',
|
author = 'Kovid Goyal',
|
||||||
author_email = 'kovid@kovidgoyal.net',
|
author_email = 'kovid@kovidgoyal.net',
|
||||||
url = 'http://%s.kovidgoyal.net'%APPNAME,
|
url = 'http://%s.kovidgoyal.net'%APPNAME,
|
||||||
package_data = {'calibre':['plugins/*']},
|
package_data = {'calibre':plugins},
|
||||||
include_package_data = True,
|
|
||||||
entry_points = entry_points,
|
entry_points = entry_points,
|
||||||
zip_safe = False,
|
zip_safe = False,
|
||||||
options = { 'bdist_egg' : {'exclude_source_files': True,}, },
|
options = { 'bdist_egg' : {'exclude_source_files': True,}, },
|
||||||
@ -455,7 +473,8 @@ if __name__ == '__main__':
|
|||||||
],
|
],
|
||||||
cmdclass = {
|
cmdclass = {
|
||||||
'build_ext' : build_ext,
|
'build_ext' : build_ext,
|
||||||
'build' : build,
|
'build' : build,
|
||||||
|
'build_py' : build_py,
|
||||||
'pot' : pot,
|
'pot' : pot,
|
||||||
'manual' : manual,
|
'manual' : manual,
|
||||||
'resources' : resources,
|
'resources' : resources,
|
||||||
|
@ -2,7 +2,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
__appname__ = 'calibre'
|
__appname__ = 'calibre'
|
||||||
__version__ = '0.4.117'
|
__version__ = '0.4.121'
|
||||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||||
'''
|
'''
|
||||||
Various run time constants.
|
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
|
'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('-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,
|
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.')
|
help='Migrate old database. Needs two arguments. Path to library1.db and path to new library folder.')
|
||||||
return parser
|
return parser
|
||||||
@ -72,7 +74,10 @@ def migrate(old, new):
|
|||||||
|
|
||||||
def main(args=sys.argv):
|
def main(args=sys.argv):
|
||||||
opts, args = option_parser().parse_args(args)
|
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]
|
mod, path = opts.update_module.partition(',')[0], opts.update_module.partition(',')[-1]
|
||||||
update_module(mod, os.path.expanduser(path))
|
update_module(mod, os.path.expanduser(path))
|
||||||
elif opts.command:
|
elif opts.command:
|
||||||
|
@ -18,6 +18,7 @@ from calibre.ptempfile import TemporaryDirectory
|
|||||||
from calibre.ebooks.metadata import MetaInformation
|
from calibre.ebooks.metadata import MetaInformation
|
||||||
from calibre.ebooks.metadata.opf2 import OPFCreator
|
from calibre.ebooks.metadata.opf2 import OPFCreator
|
||||||
from calibre.utils.zipfile import ZipFile
|
from calibre.utils.zipfile import ZipFile
|
||||||
|
from calibre.customize.ui import run_plugins_on_preprocess
|
||||||
|
|
||||||
def lit2opf(path, tdir, opts):
|
def lit2opf(path, tdir, opts):
|
||||||
from calibre.ebooks.lit.reader import LitReader
|
from calibre.ebooks.lit.reader import LitReader
|
||||||
@ -30,7 +31,7 @@ def lit2opf(path, tdir, opts):
|
|||||||
|
|
||||||
def mobi2opf(path, tdir, opts):
|
def mobi2opf(path, tdir, opts):
|
||||||
from calibre.ebooks.mobi.reader import MobiReader
|
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 = MobiReader(path)
|
||||||
reader.extract_content(tdir)
|
reader.extract_content(tdir)
|
||||||
files = list(walk(tdir))
|
files = list(walk(tdir))
|
||||||
@ -118,6 +119,7 @@ def unarchive(path, tdir):
|
|||||||
|
|
||||||
def any2epub(opts, path, notification=None, create_epub=True,
|
def any2epub(opts, path, notification=None, create_epub=True,
|
||||||
oeb_cover=False, extract_to=None):
|
oeb_cover=False, extract_to=None):
|
||||||
|
path = run_plugins_on_preprocess(path)
|
||||||
ext = os.path.splitext(path)[1]
|
ext = os.path.splitext(path)[1]
|
||||||
if not ext:
|
if not ext:
|
||||||
raise ValueError('Unknown file type: '+path)
|
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.split import split
|
||||||
from calibre.ebooks.epub.fonts import Rationalizer
|
from calibre.ebooks.epub.fonts import Rationalizer
|
||||||
from calibre.constants import preferred_encoding
|
from calibre.constants import preferred_encoding
|
||||||
|
from calibre.customize.ui import run_plugins_on_postprocess
|
||||||
from calibre import walk, CurrentDir, to_unicode
|
from calibre import walk, CurrentDir, to_unicode
|
||||||
|
|
||||||
content = functools.partial(os.path.join, u'content')
|
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 = initialize_container(opts.output)
|
||||||
epub.add_dir(tdir)
|
epub.add_dir(tdir)
|
||||||
epub.close()
|
epub.close()
|
||||||
|
run_plugins_on_postprocess(opts.output, 'epub')
|
||||||
logger.info(_('Output written to ')+opts.output)
|
logger.info(_('Output written to ')+opts.output)
|
||||||
|
|
||||||
if opts.show_opf:
|
if opts.show_opf:
|
||||||
|
@ -447,6 +447,7 @@ class Parser(PreProcessor, LoggingInterface):
|
|||||||
''' Create lxml ElementTree from HTML '''
|
''' Create lxml ElementTree from HTML '''
|
||||||
self.log_info('\tParsing '+os.sep.join(self.htmlfile.path.split(os.sep)[-3:]))
|
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 = open(self.htmlfile.path, 'rb').read().decode(self.htmlfile.encoding, 'replace').strip()
|
||||||
|
src = src.replace('\x00', '')
|
||||||
src = self.preprocess(src)
|
src = self.preprocess(src)
|
||||||
# lxml chokes on unicode input when it contains encoding declarations
|
# lxml chokes on unicode input when it contains encoding declarations
|
||||||
for pat in ENCODING_PATS:
|
for pat in ENCODING_PATS:
|
||||||
@ -527,6 +528,7 @@ class Processor(Parser):
|
|||||||
|
|
||||||
LINKS_PATH = XPath('//a[@href]')
|
LINKS_PATH = XPath('//a[@href]')
|
||||||
PIXEL_PAT = re.compile(r'([-]?\d+|[-]?\d*\.\d+)px')
|
PIXEL_PAT = re.compile(r'([-]?\d+|[-]?\d*\.\d+)px')
|
||||||
|
PAGE_PAT = re.compile(r'@page[^{]*?{[^}]*?}')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
Parser.__init__(self, *args, **kwargs)
|
Parser.__init__(self, *args, **kwargs)
|
||||||
@ -696,7 +698,9 @@ class Processor(Parser):
|
|||||||
return ''
|
return ''
|
||||||
return '%fpt'%(72 * val/dpi)
|
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):
|
def extract_css(self, parsed_sheets):
|
||||||
'''
|
'''
|
||||||
@ -732,7 +736,12 @@ class Processor(Parser):
|
|||||||
self.log_error('Failed to open stylesheet: %s'%file)
|
self.log_error('Failed to open stylesheet: %s'%file)
|
||||||
else:
|
else:
|
||||||
try:
|
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:
|
except:
|
||||||
parsed_sheets[file] = css.decode('utf8', 'replace')
|
parsed_sheets[file] = css.decode('utf8', 'replace')
|
||||||
self.log_warning('Failed to parse stylesheet: %s'%file)
|
self.log_warning('Failed to parse stylesheet: %s'%file)
|
||||||
@ -762,9 +771,11 @@ class Processor(Parser):
|
|||||||
class_counter = 0
|
class_counter = 0
|
||||||
for font in self.root.xpath('//font'):
|
for font in self.root.xpath('//font'):
|
||||||
try:
|
try:
|
||||||
size = int(font.attrib.pop('size', '3'))
|
size = font.attrib.pop('size', '3')
|
||||||
except:
|
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)
|
setting = 'font-size: %d%%;'%int((float(size)/3) * 100)
|
||||||
face = font.attrib.pop('face', None)
|
face = font.attrib.pop('face', None)
|
||||||
if face is not None:
|
if face is not None:
|
||||||
@ -1043,11 +1054,12 @@ def main(args=sys.argv):
|
|||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def gui_main(htmlfile):
|
def gui_main(htmlfile, pt=None):
|
||||||
'''
|
'''
|
||||||
Convenience wrapper for use in recursively importing HTML files.
|
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()
|
pt.close()
|
||||||
opts = '''
|
opts = '''
|
||||||
pretty_print = True
|
pretty_print = True
|
||||||
|
@ -34,6 +34,7 @@ from calibre import LoggingInterface
|
|||||||
from calibre import plugins
|
from calibre import plugins
|
||||||
msdes, msdeserror = plugins['msdes']
|
msdes, msdeserror = plugins['msdes']
|
||||||
import calibre.ebooks.lit.mssha1 as mssha1
|
import calibre.ebooks.lit.mssha1 as mssha1
|
||||||
|
from calibre.customize.ui import run_plugins_on_postprocess
|
||||||
|
|
||||||
__all__ = ['LitWriter']
|
__all__ = ['LitWriter']
|
||||||
|
|
||||||
@ -734,6 +735,7 @@ def oeb2lit(opts, opfpath):
|
|||||||
lit = LitWriter(OEBBook(opfpath, logger=logger), logger=logger)
|
lit = LitWriter(OEBBook(opfpath, logger=logger), logger=logger)
|
||||||
with open(litpath, 'wb') as f:
|
with open(litpath, 'wb') as f:
|
||||||
lit.dump(f)
|
lit.dump(f)
|
||||||
|
run_plugins_on_postprocess(litpath, 'lit')
|
||||||
logger.log_info(_('Output written to ')+litpath)
|
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.mobi.convert_from import process_file as mobi2lrf
|
||||||
from calibre.ebooks.lrf.fb2.convert_from import process_file as fb22lrf
|
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):
|
def largest_file(files):
|
||||||
maxsize, file = 0, None
|
maxsize, file = 0, None
|
||||||
for f in files:
|
for f in files:
|
||||||
@ -108,6 +110,7 @@ def odt2lrf(path, options, logger):
|
|||||||
|
|
||||||
def process_file(path, options, logger=None):
|
def process_file(path, options, logger=None):
|
||||||
path = os.path.abspath(os.path.expanduser(path))
|
path = os.path.abspath(os.path.expanduser(path))
|
||||||
|
path = run_plugins_on_preprocess(path)
|
||||||
tdir = None
|
tdir = None
|
||||||
if logger is None:
|
if logger is None:
|
||||||
level = logging.DEBUG if options.verbose else logging.INFO
|
level = logging.DEBUG if options.verbose else logging.INFO
|
||||||
@ -160,6 +163,7 @@ def process_file(path, options, logger=None):
|
|||||||
if not convertor:
|
if not convertor:
|
||||||
raise UnknownFormatError(_('Converting from %s to LRF is not supported.')%ext)
|
raise UnknownFormatError(_('Converting from %s to LRF is not supported.')%ext)
|
||||||
convertor(path, options, logger)
|
convertor(path, options, logger)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
os.chdir(cwd)
|
os.chdir(cwd)
|
||||||
if tdir and os.path.exists(tdir):
|
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 import MetaInformation
|
||||||
from calibre.ebooks.metadata.opf import OPFCreator
|
from calibre.ebooks.metadata.opf import OPFCreator
|
||||||
from calibre.ebooks.epub.from_html import config as html2epub_config, convert as html2epub
|
from calibre.ebooks.epub.from_html import config as html2epub_config, convert as html2epub
|
||||||
|
from calibre.customize.ui import run_plugins_on_preprocess
|
||||||
try:
|
try:
|
||||||
from calibre.utils.PythonMagickWand import \
|
from calibre.utils.PythonMagickWand import \
|
||||||
NewMagickWand, NewPixelWand, \
|
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'):
|
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
|
source = path_to_file
|
||||||
|
|
||||||
if not opts.title:
|
if not opts.title:
|
||||||
opts.title = os.path.splitext(os.path.basename(source))[0]
|
opts.title = os.path.splitext(os.path.basename(source))[0]
|
||||||
if not opts.output:
|
if not opts.output:
|
||||||
|
@ -12,6 +12,7 @@ from urllib import unquote
|
|||||||
from urlparse import urlparse
|
from urlparse import urlparse
|
||||||
from math import ceil, floor
|
from math import ceil, floor
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
from calibre.customize.ui import run_plugins_on_postprocess
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from PIL import Image as PILImage
|
from PIL import Image as PILImage
|
||||||
@ -1852,7 +1853,7 @@ def process_file(path, options, logger=None):
|
|||||||
scaled else im
|
scaled else im
|
||||||
cf = PersistentTemporaryFile(prefix=__appname__+"_", suffix=".jpg")
|
cf = PersistentTemporaryFile(prefix=__appname__+"_", suffix=".jpg")
|
||||||
cf.close()
|
cf.close()
|
||||||
cim.save(cf.name)
|
cim.convert('RGB').save(cf.name)
|
||||||
options.cover = cf.name
|
options.cover = cf.name
|
||||||
|
|
||||||
tim = im.resize((int(0.75*th), th), PILImage.ANTIALIAS).convert('RGB')
|
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.join(os.getcwd(), name)
|
||||||
oname = os.path.abspath(os.path.expanduser(oname))
|
oname = os.path.abspath(os.path.expanduser(oname))
|
||||||
conv.writeto(oname, lrs=options.lrs)
|
conv.writeto(oname, lrs=options.lrs)
|
||||||
|
run_plugins_on_postprocess(oname, 'lrf')
|
||||||
logger.info('Output written to %s', oname)
|
logger.info('Output written to %s', oname)
|
||||||
conv.cleanup()
|
conv.cleanup()
|
||||||
return oname
|
return oname
|
||||||
|
@ -16,7 +16,7 @@ def get_metadata(stream):
|
|||||||
|
|
||||||
# Title
|
# Title
|
||||||
title = None
|
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)
|
match = pat.search(src)
|
||||||
if match:
|
if match:
|
||||||
title = match.group(2)
|
title = match.group(2)
|
||||||
@ -28,7 +28,7 @@ def get_metadata(stream):
|
|||||||
|
|
||||||
# Author
|
# Author
|
||||||
author = None
|
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)
|
match = pat.search(src)
|
||||||
if match:
|
if match:
|
||||||
author = match.group(2).replace(',', ';')
|
author = match.group(2).replace(',', ';')
|
||||||
@ -36,7 +36,7 @@ def get_metadata(stream):
|
|||||||
mi = MetaInformation(title, [author] if author else None)
|
mi = MetaInformation(title, [author] if author else None)
|
||||||
|
|
||||||
# Publisher
|
# 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)
|
match = pat.search(src)
|
||||||
if match:
|
if match:
|
||||||
mi.publisher = match.group(2)
|
mi.publisher = match.group(2)
|
||||||
|
@ -5,36 +5,16 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
import os, re, collections
|
import os, re, collections
|
||||||
|
|
||||||
from calibre.utils.config import prefs
|
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.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
|
from calibre.ebooks.metadata import MetaInformation
|
||||||
|
|
||||||
_METADATA_PRIORITIES = [
|
_METADATA_PRIORITIES = [
|
||||||
'html', 'htm', 'xhtml', 'xhtm',
|
'html', 'htm', 'xhtml', 'xhtm',
|
||||||
'rtf', 'fb2', 'pdf', 'prc', 'odt',
|
'rtf', 'fb2', 'pdf', 'prc', 'odt',
|
||||||
'epub', 'lit', 'lrx', 'lrf', 'mobi',
|
'epub', 'lit', 'lrx', 'lrf', 'mobi',
|
||||||
'rb', 'imp'
|
'rb', 'imp'
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -88,11 +68,7 @@ def get_metadata(stream, stream_type='lrf', use_libprs_metadata=False):
|
|||||||
|
|
||||||
mi = MetaInformation(None, None)
|
mi = MetaInformation(None, None)
|
||||||
if prefs['read_file_metadata']:
|
if prefs['read_file_metadata']:
|
||||||
try:
|
mi = get_file_type_metadata(stream, stream_type)
|
||||||
func = eval(stream_type + '_metadata')
|
|
||||||
mi = func(stream)
|
|
||||||
except NameError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
name = os.path.basename(getattr(stream, 'name', ''))
|
name = os.path.basename(getattr(stream, 'name', ''))
|
||||||
base = metadata_from_filename(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:
|
if opf is not None:
|
||||||
base.smart_update(opf)
|
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
|
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'):
|
def set_metadata(stream, mi, stream_type='lrf'):
|
||||||
if stream_type: stream_type = stream_type.lower()
|
if stream_type:
|
||||||
if stream_type == 'lrf':
|
stream_type = stream_type.lower()
|
||||||
set_lrf_metadata(stream, mi)
|
set_file_type_metadata(stream, mi, stream_type)
|
||||||
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)
|
|
||||||
|
|
||||||
def metadata_from_filename(name, pat=None):
|
def metadata_from_filename(name, pat=None):
|
||||||
name = os.path.splitext(name)[0]
|
name = os.path.splitext(name)[0]
|
||||||
mi = MetaInformation(None, None)
|
mi = MetaInformation(None, None)
|
||||||
|
@ -233,7 +233,7 @@ class ParseRtf:
|
|||||||
bug_handler = RtfInvalidCodeException,
|
bug_handler = RtfInvalidCodeException,
|
||||||
)
|
)
|
||||||
check_encoding_obj.check_encoding(self.__file)
|
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
|
raise InvalidRtfException, msg
|
||||||
delete_info_obj = delete_info.DeleteInfo(
|
delete_info_obj = delete_info.DeleteInfo(
|
||||||
in_file = self.__temp_file,
|
in_file = self.__temp_file,
|
||||||
|
@ -23,7 +23,10 @@ class CheckEncoding:
|
|||||||
try:
|
try:
|
||||||
line.decode(encoding)
|
line.decode(encoding)
|
||||||
except UnicodeError:
|
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__':
|
if __name__ == '__main__':
|
||||||
check_encoding_obj = CheckEncoding()
|
check_encoding_obj = CheckEncoding()
|
||||||
check_encoding_obj.check_encoding(sys.argv[1])
|
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.startup import get_lang
|
||||||
from calibre.utils.config import Config, ConfigProxy, dynamic
|
from calibre.utils.config import Config, ConfigProxy, dynamic
|
||||||
import calibre.resources as resources
|
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
|
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)
|
pixmap.save(buf, format)
|
||||||
return str(ba.data())
|
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:
|
try:
|
||||||
from calibre.utils.single_qt_application import SingleApplication
|
from calibre.utils.single_qt_application import SingleApplication
|
||||||
|
@ -1,32 +1,131 @@
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
__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, \
|
QDesktopServices, QVBoxLayout, QLabel, QPlainTextEdit, \
|
||||||
QStringListModel
|
QStringListModel, QAbstractItemModel, \
|
||||||
from PyQt4.QtCore import SIGNAL, QTimer, Qt, QSize, QVariant, QUrl
|
SIGNAL, QTimer, Qt, QSize, QVariant, QUrl, \
|
||||||
|
QModelIndex, QInputDialog
|
||||||
|
|
||||||
from calibre.constants import islinux, iswindows
|
from calibre.constants import islinux, iswindows
|
||||||
from calibre.gui2.dialogs.config_ui import Ui_Dialog
|
from calibre.gui2.dialogs.config_ui import Ui_Dialog
|
||||||
from calibre.gui2 import qstring_to_unicode, choose_dir, error_dialog, config, \
|
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.utils.config import prefs
|
||||||
from calibre.gui2.widgets import FilenamePattern
|
from calibre.gui2.widgets import FilenamePattern
|
||||||
from calibre.gui2.library import BooksModel
|
from calibre.gui2.library import BooksModel
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS
|
from calibre.ebooks import BOOK_EXTENSIONS
|
||||||
from calibre.ebooks.epub.iterator import is_supported
|
from calibre.ebooks.epub.iterator import is_supported
|
||||||
from calibre.library import server_config
|
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):
|
class CategoryModel(QStringListModel):
|
||||||
|
|
||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
QStringListModel.__init__(self, *args)
|
QStringListModel.__init__(self, *args)
|
||||||
self.setStringList([_('General'), _('Interface'), _('Advanced'),
|
self.setStringList([_('General'), _('Interface'), _('Advanced'),
|
||||||
_('Content\nServer')])
|
_('Content\nServer'), _('Plugins')])
|
||||||
self.icons = list(map(QVariant, map(QIcon,
|
self.icons = list(map(QVariant, map(QIcon,
|
||||||
[':/images/dialog_information.svg', ':/images/lookfeel.svg',
|
[':/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):
|
def data(self, index, role):
|
||||||
if role == Qt.DecorationRole:
|
if role == Qt.DecorationRole:
|
||||||
@ -139,6 +238,56 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
|||||||
self.priority.setVisible(iswindows)
|
self.priority.setVisible(iswindows)
|
||||||
self.priority_label.setVisible(iswindows)
|
self.priority_label.setVisible(iswindows)
|
||||||
self.category_view.setCurrentIndex(self._category_model.index(0))
|
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):
|
def up_column(self):
|
||||||
idx = self.columns.currentRow()
|
idx = self.columns.currentRow()
|
||||||
|
@ -767,6 +767,115 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</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>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
|
@ -49,11 +49,8 @@ class JobsDialog(QDialog, Ui_JobsDialog):
|
|||||||
self.connect(self.running_time_timer, SIGNAL('timeout()'), self.update_running_time)
|
self.connect(self.running_time_timer, SIGNAL('timeout()'), self.update_running_time)
|
||||||
self.running_time_timer.start(1000)
|
self.running_time_timer.start(1000)
|
||||||
|
|
||||||
def update_running_time(self):
|
def update_running_time(self, *args):
|
||||||
model = self.model
|
self.model.running_time_updated()
|
||||||
for row, job in enumerate(model.jobs):
|
|
||||||
if job.is_running:
|
|
||||||
self.jobs_view.dataChanged(model.index(row, 3), model.index(row, 3))
|
|
||||||
|
|
||||||
def kill_job(self):
|
def kill_job(self):
|
||||||
for index in self.jobs_view.selectedIndexes():
|
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, \
|
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.metadata_single_ui import Ui_MetadataSingleDialog
|
||||||
from calibre.gui2.dialogs.fetch_metadata import FetchMetadata
|
from calibre.gui2.dialogs.fetch_metadata import FetchMetadata
|
||||||
from calibre.gui2.dialogs.tag_editor import TagEditor
|
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.ebooks.metadata.library_thing import login, cover_from_isbn, LibraryThingError
|
||||||
from calibre import islinux
|
from calibre import islinux
|
||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs
|
||||||
|
from calibre.customize.ui import run_plugins_on_import
|
||||||
|
|
||||||
class Format(QListWidgetItem):
|
class Format(QListWidgetItem):
|
||||||
def __init__(self, parent, ext, size, path=None):
|
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 "+\
|
QErrorMessage(self.window).showMessage("You do not have "+\
|
||||||
"permission to read the file: " + _file)
|
"permission to read the file: " + _file)
|
||||||
continue
|
continue
|
||||||
nf = import_format(_file)[0]
|
_file = run_plugins_on_import(_file)
|
||||||
if nf is not None:
|
|
||||||
_file = nf
|
|
||||||
size = os.stat(_file).st_size
|
size = os.stat(_file).st_size
|
||||||
ext = os.path.splitext(_file)[1].lower()
|
ext = os.path.splitext(_file)[1].lower().replace('.', '')
|
||||||
if '.' in ext:
|
|
||||||
ext = ext.replace('.', '')
|
|
||||||
for row in range(self.formats.count()):
|
for row in range(self.formats.count()):
|
||||||
fmt = self.formats.item(row)
|
fmt = self.formats.item(row)
|
||||||
if fmt.ext == ext:
|
if fmt.ext == ext:
|
||||||
|
@ -2254,7 +2254,7 @@
|
|||||||
id="mask7160"
|
id="mask7160"
|
||||||
maskUnits="userSpaceOnUse">
|
maskUnits="userSpaceOnUse">
|
||||||
<path
|
<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 "
|
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" />
|
id="path7162" />
|
||||||
</mask>
|
</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.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'),
|
||||||
self.index(row, 0), self.index(row, 3))
|
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):
|
def has_device_jobs(self):
|
||||||
for job in self.jobs:
|
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.QtCore import Qt, SIGNAL, QObject, QCoreApplication, QUrl, QTimer
|
||||||
from PyQt4.QtGui import QPixmap, QColor, QPainter, QMenu, QIcon, QMessageBox, \
|
from PyQt4.QtGui import QPixmap, QColor, QPainter, QMenu, QIcon, QMessageBox, \
|
||||||
QToolButton, QDialog, QDesktopServices, QFileDialog, \
|
QToolButton, QDialog, QDesktopServices, QFileDialog, \
|
||||||
QSystemTrayIcon, QApplication, QKeySequence, QAction
|
QSystemTrayIcon, QApplication, QKeySequence, QAction, \
|
||||||
|
QProgressDialog
|
||||||
from PyQt4.QtSvg import QSvgRenderer
|
from PyQt4.QtSvg import QSvgRenderer
|
||||||
|
|
||||||
from calibre import __version__, __appname__, islinux, sanitize_file_name, \
|
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, \
|
pixmap_to_data, choose_dir, ORG_NAME, \
|
||||||
set_sidebar_directories, Dispatcher, \
|
set_sidebar_directories, Dispatcher, \
|
||||||
SingleApplication, Application, available_height, \
|
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.gui2.cover_flow import CoverFlow, DatabaseImages, pictureflowerror
|
||||||
from calibre.library.database import LibraryDatabase
|
from calibre.library.database import LibraryDatabase
|
||||||
from calibre.gui2.dialogs.scheduler import Scheduler
|
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.hide() if self.isVisible() else self.show()
|
||||||
self.connect(self.system_tray_icon, SIGNAL('activated(QSystemTrayIcon::ActivationReason)'), sta)
|
self.connect(self.system_tray_icon, SIGNAL('activated(QSystemTrayIcon::ActivationReason)'), sta)
|
||||||
def tcme(self, *args):
|
def tcme(self, *args):
|
||||||
print args
|
pass
|
||||||
self.tool_bar.contextMenuEvent = tcme
|
self.tool_bar.contextMenuEvent = tcme
|
||||||
####################### Location View ########################
|
####################### Location View ########################
|
||||||
QObject.connect(self.location_view, SIGNAL('location_selected(PyQt_PyObject)'),
|
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')
|
root = choose_dir(self, 'recursive book import root dir dialog', 'Select root folder')
|
||||||
if not root:
|
if not root:
|
||||||
return
|
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:
|
if duplicates:
|
||||||
files = _('<p>Books with the same title as the following already exist in the database. Add them anyway?<ul>')
|
files = _('<p>Books with the same title as the following already exist in the database. Add them anyway?<ul>')
|
||||||
for mi, formats in duplicates:
|
for mi, formats in duplicates:
|
||||||
@ -634,49 +650,59 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
def _add_books(self, paths, to_device, on_card=None):
|
def _add_books(self, paths, to_device, on_card=None):
|
||||||
if on_card is None:
|
if on_card is None:
|
||||||
on_card = self.stack.currentIndex() == 2
|
on_card = self.stack.currentIndex() == 2
|
||||||
|
if not paths:
|
||||||
|
return
|
||||||
# Get format and metadata information
|
# Get format and metadata information
|
||||||
formats, metadata, names, infos = [], [], [], []
|
formats, metadata, names, infos = [], [], [], []
|
||||||
for book in paths:
|
progress = QProgressDialog(_('Reading metadata...'), _('Stop'), 0, len(paths), self)
|
||||||
format = os.path.splitext(book)[1]
|
progress.setWindowTitle(_('Adding books...'))
|
||||||
format = format[1:] if format else None
|
progress.setWindowModality(Qt.ApplicationModal)
|
||||||
stream = open(book, 'rb')
|
progress.setLabelText(_('Reading metadata...'))
|
||||||
try:
|
progress.show()
|
||||||
mi = get_metadata(stream, stream_type=format, use_libprs_metadata=True)
|
try:
|
||||||
except:
|
for c, book in enumerate(paths):
|
||||||
mi = MetaInformation(None, None)
|
progress.setValue(c)
|
||||||
if not mi.title:
|
if progress.wasCanceled():
|
||||||
mi.title = os.path.splitext(os.path.basename(book))[0]
|
return
|
||||||
if not mi.authors:
|
format = os.path.splitext(book)[1]
|
||||||
mi.authors = [_('Unknown')]
|
format = format[1:] if format else None
|
||||||
formats.append(format)
|
stream = open(book, 'rb')
|
||||||
metadata.append(mi)
|
try:
|
||||||
names.append(os.path.basename(book))
|
mi = get_metadata(stream, stream_type=format, use_libprs_metadata=True)
|
||||||
infos.append({'title':mi.title, 'authors':', '.join(mi.authors),
|
except:
|
||||||
'cover':self.default_thumbnail, 'tags':[]})
|
mi = MetaInformation(None, None)
|
||||||
|
if not mi.title:
|
||||||
if not to_device:
|
mi.title = os.path.splitext(os.path.basename(book))[0]
|
||||||
model = self.library_view.model()
|
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)
|
if not to_device:
|
||||||
for i, path in enumerate(paths):
|
progress.setLabelText(_('Adding books to database...'))
|
||||||
npath, fmt = import_format(path)
|
model = self.library_view.model()
|
||||||
if npath is not None:
|
|
||||||
paths[i] = npath
|
paths = list(paths)
|
||||||
formats[i] = fmt
|
duplicates, number_added = model.add_books(paths, formats, metadata)
|
||||||
duplicates, number_added = model.add_books(paths, formats, metadata)
|
progress.cancel()
|
||||||
if duplicates:
|
if duplicates:
|
||||||
files = _('<p>Books with the same title as the following already exist in the database. Add them anyway?<ul>')
|
files = _('<p>Books with the same title as the following already exist in the database. Add them anyway?<ul>')
|
||||||
for mi in duplicates[2]:
|
for mi in duplicates[2]:
|
||||||
files += '<li>'+mi.title+'</li>\n'
|
files += '<li>'+mi.title+'</li>\n'
|
||||||
d = WarningDialog(_('Duplicates found!'), _('Duplicates found!'), files+'</ul></p>', parent=self)
|
d = WarningDialog(_('Duplicates found!'), _('Duplicates found!'), files+'</ul></p>', parent=self)
|
||||||
if d.exec_() == QDialog.Accepted:
|
if d.exec_() == QDialog.Accepted:
|
||||||
num = model.add_books(*duplicates, **dict(add_duplicates=True))[1]
|
num = model.add_books(*duplicates, **dict(add_duplicates=True))[1]
|
||||||
number_added += num
|
number_added += num
|
||||||
#self.library_view.sortByColumn(3, Qt.DescendingOrder)
|
model.books_added(number_added)
|
||||||
#model.research()
|
else:
|
||||||
model.books_added(number_added)
|
self.upload_books(paths, list(map(sanitize_file_name, names)), infos, on_card=on_card)
|
||||||
else:
|
finally:
|
||||||
self.upload_books(paths, list(map(sanitize_file_name, names)), infos, on_card=on_card)
|
progress.setValue(len(paths))
|
||||||
|
|
||||||
def upload_books(self, files, names, metadata, on_card=False, memory=None):
|
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, \
|
from PyQt4.QtGui import QStatusBar, QMovie, QLabel, QWidget, QHBoxLayout, QPixmap, \
|
||||||
QVBoxLayout, QSizePolicy, QToolButton, QIcon, QScrollArea, QFrame
|
QVBoxLayout, QSizePolicy, QToolButton, QIcon, QScrollArea, QFrame
|
||||||
from PyQt4.QtCore import Qt, QSize, SIGNAL, QCoreApplication
|
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
|
from calibre.gui2 import qstring_to_unicode
|
||||||
|
|
||||||
class BookInfoDisplay(QWidget):
|
class BookInfoDisplay(QWidget):
|
||||||
@ -196,9 +196,12 @@ class StatusBar(QStatusBar):
|
|||||||
self.book_info.show_data({})
|
self.book_info.show_data({})
|
||||||
|
|
||||||
def showMessage(self, msg, timeout=0):
|
def showMessage(self, msg, timeout=0):
|
||||||
|
ret = QStatusBar.showMessage(self, msg, timeout)
|
||||||
if self.systray is not None:
|
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)
|
self.systray.showMessage('calibre', msg, self.systray.Information, 10000)
|
||||||
return QStatusBar.showMessage(self, msg, timeout)
|
return ret
|
||||||
|
|
||||||
def jobs(self):
|
def jobs(self):
|
||||||
src = qstring_to_unicode(self.movie_button.jobs.text())
|
src = qstring_to_unicode(self.movie_button.jobs.text())
|
||||||
|
@ -242,6 +242,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
|||||||
lambda x:self.view.previous_page())
|
lambda x:self.view.previous_page())
|
||||||
self.connect(self.action_find_next, SIGNAL('triggered(bool)'),
|
self.connect(self.action_find_next, SIGNAL('triggered(bool)'),
|
||||||
lambda x:self.find(unicode(self.search.text()), True, repeat=True))
|
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_back, SIGNAL('triggered(bool)'), self.back)
|
||||||
self.connect(self.action_bookmark, SIGNAL('triggered(bool)'), self.bookmark)
|
self.connect(self.action_bookmark, SIGNAL('triggered(bool)'), self.bookmark)
|
||||||
self.connect(self.action_forward, SIGNAL('triggered(bool)'), self.forward)
|
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_bar.setContextMenuPolicy(Qt.PreventContextMenu)
|
||||||
self.tool_bar2.setContextMenuPolicy(Qt.PreventContextMenu)
|
self.tool_bar2.setContextMenuPolicy(Qt.PreventContextMenu)
|
||||||
self.tool_bar.widgetForAction(self.action_bookmark).setPopupMode(QToolButton.MenuButtonPopup)
|
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):
|
def goto(self, ref):
|
||||||
if ref:
|
if ref:
|
||||||
@ -574,7 +583,7 @@ View an ebook.
|
|||||||
|
|
||||||
def main(args=sys.argv):
|
def main(args=sys.argv):
|
||||||
parser = option_parser()
|
parser = option_parser()
|
||||||
opts, args = parser.parse_args(args)
|
args = parser.parse_args(args)[-1]
|
||||||
pid = os.fork() if islinux else -1
|
pid = os.fork() if islinux else -1
|
||||||
if pid <= 0:
|
if pid <= 0:
|
||||||
app = Application(args)
|
app = Application(args)
|
||||||
|
@ -27,8 +27,8 @@
|
|||||||
<widget class="QWidget" name="layoutWidget" >
|
<widget class="QWidget" name="layoutWidget" >
|
||||||
<layout class="QGridLayout" name="gridLayout" >
|
<layout class="QGridLayout" name="gridLayout" >
|
||||||
<item row="0" column="0" >
|
<item row="0" column="0" >
|
||||||
<widget class="QWebView" name="view" >
|
<widget class="QWebView" native="1" name="view" >
|
||||||
<property name="url" >
|
<property name="url" stdset="0" >
|
||||||
<url>
|
<url>
|
||||||
<string>about:blank</string>
|
<string>about:blank</string>
|
||||||
</url>
|
</url>
|
||||||
@ -88,6 +88,7 @@
|
|||||||
<addaction name="action_reference_mode" />
|
<addaction name="action_reference_mode" />
|
||||||
<addaction name="separator" />
|
<addaction name="separator" />
|
||||||
<addaction name="action_preferences" />
|
<addaction name="action_preferences" />
|
||||||
|
<addaction name="action_full_screen" />
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QToolBar" name="tool_bar2" >
|
<widget class="QToolBar" name="tool_bar2" >
|
||||||
<attribute name="toolBarArea" >
|
<attribute name="toolBarArea" >
|
||||||
@ -224,6 +225,15 @@
|
|||||||
<string>Bookmark</string>
|
<string>Bookmark</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</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>
|
</widget>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
|
@ -358,8 +358,8 @@ list of id numbers (you can get id numbers by using the list command). For examp
|
|||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def do_add_format(db, id, fmt, buffer):
|
def do_add_format(db, id, fmt, path):
|
||||||
db.add_format(id, fmt.upper(), buffer, index_is_id=True)
|
db.add_format_with_hooks(id, fmt.upper(), path, index_is_id=True)
|
||||||
|
|
||||||
|
|
||||||
def command_add_format(args, dbpath):
|
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')
|
print >>sys.stderr, _('You must specify an id and an ebook file')
|
||||||
return 1
|
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:
|
if not fmt:
|
||||||
print _('ebook file must have an extension')
|
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
|
return 0
|
||||||
|
|
||||||
def do_remove_format(db, id, fmt):
|
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))
|
(usize, data, id, ext))
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
|
||||||
def import_book_directory_multiple(self, dirpath):
|
def import_book_directory_multiple(self, dirpath, callback=None):
|
||||||
dirpath = os.path.abspath(dirpath)
|
dirpath = os.path.abspath(dirpath)
|
||||||
duplicates = []
|
duplicates = []
|
||||||
books = {}
|
books = {}
|
||||||
for path in os.listdir(dirpath):
|
for path in os.listdir(dirpath):
|
||||||
|
if callable(callback):
|
||||||
|
callback('.')
|
||||||
path = os.path.abspath(os.path.join(dirpath, path))
|
path = os.path.abspath(os.path.join(dirpath, path))
|
||||||
if os.path.isdir(path) or not os.access(path, os.R_OK):
|
if os.path.isdir(path) or not os.access(path, os.R_OK):
|
||||||
continue
|
continue
|
||||||
@ -1500,13 +1502,18 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
|
|||||||
duplicates.append((mi, formats))
|
duplicates.append((mi, formats))
|
||||||
continue
|
continue
|
||||||
self.import_book(mi, formats)
|
self.import_book(mi, formats)
|
||||||
|
if callable(callback):
|
||||||
|
if callback(mi.title):
|
||||||
|
break
|
||||||
return duplicates
|
return duplicates
|
||||||
|
|
||||||
|
|
||||||
def import_book_directory(self, dirpath):
|
def import_book_directory(self, dirpath, callback=None):
|
||||||
dirpath = os.path.abspath(dirpath)
|
dirpath = os.path.abspath(dirpath)
|
||||||
formats = []
|
formats = []
|
||||||
for path in os.listdir(dirpath):
|
for path in os.listdir(dirpath):
|
||||||
|
if callable(callback):
|
||||||
|
callback('.')
|
||||||
path = os.path.abspath(os.path.join(dirpath, path))
|
path = os.path.abspath(os.path.join(dirpath, path))
|
||||||
if os.path.isdir(path) or not os.access(path, os.R_OK):
|
if os.path.isdir(path) or not os.access(path, os.R_OK):
|
||||||
continue
|
continue
|
||||||
@ -1527,6 +1534,9 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
|
|||||||
if self.has_book(mi):
|
if self.has_book(mi):
|
||||||
return [(mi, formats)]
|
return [(mi, formats)]
|
||||||
self.import_book(mi, formats)
|
self.import_book(mi, formats)
|
||||||
|
if callable(callback):
|
||||||
|
callback(mi.title)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def has_book(self, mi):
|
def has_book(self, mi):
|
||||||
@ -1535,13 +1545,19 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
|
|||||||
def has_id(self, id):
|
def has_id(self, id):
|
||||||
return self.conn.get('SELECT id FROM books where id=?', (id,), all=False) is not None
|
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)
|
root = os.path.abspath(root)
|
||||||
duplicates = []
|
duplicates = []
|
||||||
for dirpath in os.walk(root):
|
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:
|
if res is not None:
|
||||||
duplicates.extend(res)
|
duplicates.extend(res)
|
||||||
|
if callable(callback):
|
||||||
|
if callback(''):
|
||||||
|
break
|
||||||
|
|
||||||
return duplicates
|
return duplicates
|
||||||
|
|
||||||
def export_single_format_to_dir(self, dir, indices, format, index_is_id=False):
|
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 import string_to_authors, authors_to_string
|
||||||
from calibre.ebooks.metadata.meta import get_metadata
|
from calibre.ebooks.metadata.meta import get_metadata
|
||||||
from calibre.constants import preferred_encoding, iswindows, isosx
|
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
|
copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
|
||||||
filesystem_encoding = sys.getfilesystemencoding()
|
filesystem_encoding = sys.getfilesystemencoding()
|
||||||
@ -37,6 +38,7 @@ def normpath(x):
|
|||||||
return x
|
return x
|
||||||
|
|
||||||
_filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+\[\]/]')
|
_filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+\[\]/]')
|
||||||
|
|
||||||
def sanitize_file_name(name, substitute='_'):
|
def sanitize_file_name(name, substitute='_'):
|
||||||
'''
|
'''
|
||||||
Sanitize the filename `name`. All invalid characters are replaced by `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):
|
if self.has_format(index, format, index_is_id):
|
||||||
self.remove_format(id, format, index_is_id=True)
|
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):
|
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)
|
id = index if index_is_id else self.id(index)
|
||||||
if path is None:
|
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
|
self.data.refresh_ids(self.conn, [id]) # Needed to update format list and size
|
||||||
return id
|
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):
|
def add_books(self, paths, formats, metadata, uris=[], add_duplicates=True):
|
||||||
'''
|
'''
|
||||||
Add a book to the database. The result cache is not updated.
|
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.set_path(id, True)
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
self.set_metadata(id, mi)
|
self.set_metadata(id, mi)
|
||||||
stream = path if hasattr(path, 'read') else open(path, 'rb')
|
npath = self.run_import_plugins(path, format)
|
||||||
stream.seek(0)
|
format = os.path.splitext(npath)[-1].lower().replace('.', '').upper()
|
||||||
|
stream = open(npath, 'rb')
|
||||||
self.add_format(id, format, stream, index_is_id=True)
|
self.add_format(id, format, stream, index_is_id=True)
|
||||||
if not hasattr(path, 'read'):
|
stream.close()
|
||||||
stream.close()
|
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
self.data.refresh_ids(self.conn, ids) # Needed to update format list and size
|
self.data.refresh_ids(self.conn, ids) # Needed to update format list and size
|
||||||
if duplicates:
|
if duplicates:
|
||||||
|
@ -62,7 +62,8 @@ entry_points = {
|
|||||||
'calibre-debug = calibre.debug:main',
|
'calibre-debug = calibre.debug:main',
|
||||||
'calibredb = calibre.library.cli:main',
|
'calibredb = calibre.library.cli:main',
|
||||||
'calibre-fontconfig = calibre.utils.fontconfig: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' : [
|
'gui_scripts' : [
|
||||||
__appname__+' = calibre.gui2.main:main',
|
__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::
|
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>`_.
|
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
|
metadata
|
||||||
faq
|
faq
|
||||||
xpath
|
xpath
|
||||||
|
customize
|
||||||
glossary
|
glossary
|
||||||
|
|
||||||
Convenience
|
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):
|
class Server(object):
|
||||||
|
|
||||||
TRENDS = '/tmp/donations_trend.png'
|
TRENDS = '/tmp/donations_trend.png'
|
||||||
|
MONTH_TRENDS = '/tmp/donations_month_trend.png'
|
||||||
|
|
||||||
def __init__(self, apache=False, root='/', data_file='/tmp/donations.xml'):
|
def __init__(self, apache=False, root='/', data_file='/tmp/donations.xml'):
|
||||||
self.apache = apache
|
self.apache = apache
|
||||||
self.document_root = root
|
self.document_root = root
|
||||||
self.data_file = data_file
|
self.data_file = data_file
|
||||||
self.read_records()
|
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 calculate_trend(self):
|
||||||
def months(start, end):
|
def months(start, end):
|
||||||
pos = range_for_month(start.year, start.month)[0]
|
pos = range_for_month(start.year, start.month)[0]
|
||||||
@ -208,7 +222,7 @@ class Server(object):
|
|||||||
x = [m.min for m in _months]
|
x = [m.min for m in _months]
|
||||||
y = [m.total for m in _months]
|
y = [m.total for m in _months]
|
||||||
ml = mdates.MonthLocator() # every month
|
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 = fig.add_subplot(111)
|
||||||
ax.bar(x, y, align='center', width=20, color='g')
|
ax.bar(x, y, align='center', width=20, color='g')
|
||||||
ax.xaxis.set_major_locator(ml)
|
ax.xaxis.set_major_locator(ml)
|
||||||
@ -235,6 +249,7 @@ class Server(object):
|
|||||||
max_date = max(max_date, d)
|
max_date = max(max_date, d)
|
||||||
self.earliest, self.latest = min_date, max_date
|
self.earliest, self.latest = min_date, max_date
|
||||||
self.calculate_trend()
|
self.calculate_trend()
|
||||||
|
self.calculate_month_trend()
|
||||||
|
|
||||||
def get_slice(self, start_date, end_date):
|
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],
|
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:
|
else:
|
||||||
range_stats = self.get_slice(*range_stats).to_html(num_of_countries=10)
|
range_stats = self.get_slice(*range_stats).to_html(num_of_countries=10)
|
||||||
|
|
||||||
|
today = self.get_slice(date.today(), date.today())
|
||||||
|
|
||||||
return textwrap.dedent('''\
|
return textwrap.dedent('''\
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?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">
|
<!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" />
|
<input type="submit" value="Update" />
|
||||||
</form>
|
</form>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
<b>Donations today: $%(today).2f</b><br />
|
||||||
%(range_stats)s
|
%(range_stats)s
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -428,6 +446,8 @@ class Server(object):
|
|||||||
<div style="text-align:center">
|
<div style="text-align:center">
|
||||||
<h3>Income trends for the last year</h3>
|
<h3>Income trends for the last year</h3>
|
||||||
<img src="%(root)strend.png" alt="Income trends" />
|
<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>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -438,6 +458,7 @@ class Server(object):
|
|||||||
rc = 'checked="checked"' if period_type=="range" else '',
|
rc = 'checked="checked"' if period_type=="range" else '',
|
||||||
month_month=mmlist, month_year=mylist, year_year=yylist,
|
month_month=mmlist, month_year=mylist, year_year=yylist,
|
||||||
rl=rl, rr=rr, range_stats=range_stats, root=self.document_root,
|
rl=rl, rr=rr, range_stats=range_stats, root=self.document_root,
|
||||||
|
today=today.total
|
||||||
)
|
)
|
||||||
|
|
||||||
@expose
|
@expose
|
||||||
@ -452,6 +473,11 @@ class Server(object):
|
|||||||
cherrypy.response.headers['Content-Type'] = 'image/png'
|
cherrypy.response.headers['Content-Type'] = 'image/png'
|
||||||
return open(self.TRENDS, 'rb').read()
|
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
|
@expose
|
||||||
def show(self, period_type='month', month_month='', month_year='',
|
def show(self, period_type='month', month_month='', month_year='',
|
||||||
year_year='', range_left='', range_right=''):
|
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')))
|
bdir = os.path.abspath(os.path.expanduser(os.environ.get('XDG_CONFIG_HOME', '~/.config')))
|
||||||
config_dir = os.path.join(bdir, 'calibre')
|
config_dir = os.path.join(bdir, 'calibre')
|
||||||
|
|
||||||
|
plugin_dir = os.path.join(config_dir, 'plugins')
|
||||||
|
|
||||||
def make_config_dir():
|
def make_config_dir():
|
||||||
if not os.path.exists(config_dir):
|
if not os.path.exists(plugin_dir):
|
||||||
os.makedirs(config_dir, mode=448) # 0700 == 448
|
os.makedirs(plugin_dir, mode=448) # 0700 == 448
|
||||||
|
|
||||||
|
|
||||||
class CustomHelpFormatter(IndentedHelpFormatter):
|
class CustomHelpFormatter(IndentedHelpFormatter):
|
||||||
@ -78,6 +80,7 @@ class OptionParser(_OptionParser):
|
|||||||
gui_mode=False,
|
gui_mode=False,
|
||||||
conflict_handler='resolve',
|
conflict_handler='resolve',
|
||||||
**kwds):
|
**kwds):
|
||||||
|
usage = textwrap.dedent(usage)
|
||||||
usage += '''\n\nWhenever you pass arguments to %prog that have spaces in them, '''\
|
usage += '''\n\nWhenever you pass arguments to %prog that have spaces in them, '''\
|
||||||
'''enclose the arguments in quotation marks.'''
|
'''enclose the arguments in quotation marks.'''
|
||||||
_OptionParser.__init__(self, usage=usage, version=version, epilog=epilog,
|
_OptionParser.__init__(self, usage=usage, version=version, epilog=epilog,
|
||||||
|
@ -19,7 +19,7 @@ recipe_modules = [
|
|||||||
'clarin', 'financial_times', 'heise', 'le_monde', 'harpers', 'science_aas',
|
'clarin', 'financial_times', 'heise', 'le_monde', 'harpers', 'science_aas',
|
||||||
'science_news', 'the_nation', 'lrb', 'harpers_full', 'liberation',
|
'science_news', 'the_nation', 'lrb', 'harpers_full', 'liberation',
|
||||||
'linux_magazine', 'telegraph_uk', 'utne', 'sciencedaily', 'forbes',
|
'linux_magazine', 'telegraph_uk', 'utne', 'sciencedaily', 'forbes',
|
||||||
'time',
|
'time_magazine', 'endgadget', 'fudzilla',
|
||||||
]
|
]
|
||||||
|
|
||||||
import re, imp, inspect, time, os
|
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
|
no_stylesheets = False
|
||||||
use_embedded_content = 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'})]
|
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'Politics', u'http://feedproxy.google.com/time/politics')
|
||||||
,(u'Travel', u'http://feedproxy.google.com/time/travel')
|
,(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):
|
def print_version(self, url):
|
||||||
raw = self.browser.open(url).read()
|
raw = self.browser.open(url).read()
|
||||||
soup = BeautifulSoup(raw.decode('utf8', 'replace'))
|
soup = BeautifulSoup(raw.decode('utf8', 'replace'))
|
10
upload.py
@ -217,7 +217,7 @@ def stage_one():
|
|||||||
os.mkdir('build')
|
os.mkdir('build')
|
||||||
shutil.rmtree('docs')
|
shutil.rmtree('docs')
|
||||||
os.mkdir('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)
|
check_call('sudo python setup.py develop', shell=True)
|
||||||
tag_release()
|
tag_release()
|
||||||
upload_demo()
|
upload_demo()
|
||||||
@ -235,12 +235,12 @@ def stage_three():
|
|||||||
print 'Uploading to PyPI...'
|
print 'Uploading to PyPI...'
|
||||||
check_call('rm -f dist/*')
|
check_call('rm -f dist/*')
|
||||||
check_call('python setup.py register')
|
check_call('python setup.py register')
|
||||||
check_call('sudo rm -rf build')
|
check_call('sudo rm -rf build src/calibre/plugins/*')
|
||||||
os.mkdir('build')
|
os.mkdir('build')
|
||||||
check_call('python2.5 setup.py bdist_egg --exclude-source-files upload')
|
check_call('python2.5 setup.py build_ext bdist_egg --exclude-source-files upload')
|
||||||
shutil.rmtree('build')
|
check_call('sudo rm -rf build src/calibre/plugins/*')
|
||||||
os.mkdir('build')
|
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')
|
check_call('python setup.py sdist upload')
|
||||||
upload_src_tarball()
|
upload_src_tarball()
|
||||||
check_call('''rm -rf dist/* build/*''')
|
check_call('''rm -rf dist/* build/*''')
|
||||||
|