mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 10:44:09 -04:00
Merge upsteam changes.
This commit is contained in:
commit
57aebdff7d
4
setup.py
4
setup.py
@ -52,7 +52,8 @@ if __name__ == '__main__':
|
|||||||
resources, clean, gui, translations, update, \
|
resources, clean, gui, translations, update, \
|
||||||
tag_release, upload_demo, build_linux, build_windows, \
|
tag_release, upload_demo, build_linux, build_windows, \
|
||||||
build_osx, upload_installers, upload_user_manual, \
|
build_osx, upload_installers, upload_user_manual, \
|
||||||
upload_to_pypi, stage3, stage2, stage1, upload
|
upload_to_pypi, stage3, stage2, stage1, upload, \
|
||||||
|
upload_rss
|
||||||
|
|
||||||
entry_points['console_scripts'].append(
|
entry_points['console_scripts'].append(
|
||||||
'calibre_postinstall = calibre.linux:post_install')
|
'calibre_postinstall = calibre.linux:post_install')
|
||||||
@ -170,6 +171,7 @@ if __name__ == '__main__':
|
|||||||
'upload_installers': upload_installers,
|
'upload_installers': upload_installers,
|
||||||
'upload_user_manual': upload_user_manual,
|
'upload_user_manual': upload_user_manual,
|
||||||
'upload_to_pypi': upload_to_pypi,
|
'upload_to_pypi': upload_to_pypi,
|
||||||
|
'upload_rss' : upload_rss,
|
||||||
'stage3' : stage3,
|
'stage3' : stage3,
|
||||||
'stage2' : stage2,
|
'stage2' : stage2,
|
||||||
'stage1' : stage1,
|
'stage1' : stage1,
|
||||||
|
@ -3,11 +3,13 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2008, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
import sys, os, re, logging, time, subprocess, atexit, mimetypes, \
|
import sys, os, re, logging, time, subprocess, atexit, mimetypes, \
|
||||||
__builtin__
|
__builtin__, warnings
|
||||||
__builtin__.__dict__['dynamic_property'] = lambda(func): func(None)
|
__builtin__.__dict__['dynamic_property'] = lambda(func): func(None)
|
||||||
from htmlentitydefs import name2codepoint
|
from htmlentitydefs import name2codepoint
|
||||||
from math import floor
|
from math import floor
|
||||||
from logging import Formatter
|
|
||||||
|
warnings.simplefilter('ignore', DeprecationWarning)
|
||||||
|
|
||||||
|
|
||||||
from PyQt4.QtCore import QUrl
|
from PyQt4.QtCore import QUrl
|
||||||
from PyQt4.QtGui import QDesktopServices
|
from PyQt4.QtGui import QDesktopServices
|
||||||
@ -24,11 +26,17 @@ mimetypes.add_type('text/x-sony-bbeb+xml', '.lrs')
|
|||||||
mimetypes.add_type('application/xhtml+xml', '.xhtml')
|
mimetypes.add_type('application/xhtml+xml', '.xhtml')
|
||||||
mimetypes.add_type('image/svg+xml', '.svg')
|
mimetypes.add_type('image/svg+xml', '.svg')
|
||||||
mimetypes.add_type('application/x-sony-bbeb', '.lrf')
|
mimetypes.add_type('application/x-sony-bbeb', '.lrf')
|
||||||
|
mimetypes.add_type('application/x-sony-bbeb', '.lrx')
|
||||||
mimetypes.add_type('application/x-dtbncx+xml', '.ncx')
|
mimetypes.add_type('application/x-dtbncx+xml', '.ncx')
|
||||||
mimetypes.add_type('application/adobe-page-template+xml', '.xpgt')
|
mimetypes.add_type('application/adobe-page-template+xml', '.xpgt')
|
||||||
mimetypes.add_type('application/x-font-opentype', '.otf')
|
mimetypes.add_type('application/x-font-opentype', '.otf')
|
||||||
mimetypes.add_type('application/x-font-truetype', '.ttf')
|
mimetypes.add_type('application/x-font-truetype', '.ttf')
|
||||||
mimetypes.add_type('application/oebps-package+xml', '.opf')
|
mimetypes.add_type('application/oebps-package+xml', '.opf')
|
||||||
|
mimetypes.add_type('application/ereader', '.pdb')
|
||||||
|
mimetypes.add_type('application/mobi', '.mobi')
|
||||||
|
mimetypes.add_type('application/mobi', '.prc')
|
||||||
|
mimetypes.add_type('application/mobi', '.azw')
|
||||||
|
guess_type = mimetypes.guess_type
|
||||||
import cssutils
|
import cssutils
|
||||||
cssutils.log.setLevel(logging.WARN)
|
cssutils.log.setLevel(logging.WARN)
|
||||||
|
|
||||||
@ -86,6 +94,8 @@ def prints(*args, **kwargs):
|
|||||||
for i, arg in enumerate(args):
|
for i, arg in enumerate(args):
|
||||||
if isinstance(arg, unicode):
|
if isinstance(arg, unicode):
|
||||||
arg = arg.encode(preferred_encoding)
|
arg = arg.encode(preferred_encoding)
|
||||||
|
if not isinstance(arg, str):
|
||||||
|
arg = str(arg)
|
||||||
file.write(arg)
|
file.write(arg)
|
||||||
if i != len(args)-1:
|
if i != len(args)-1:
|
||||||
file.write(sep)
|
file.write(sep)
|
||||||
@ -318,24 +328,6 @@ def english_sort(x, y):
|
|||||||
'''
|
'''
|
||||||
return cmp(_spat.sub('', x), _spat.sub('', y))
|
return cmp(_spat.sub('', x), _spat.sub('', y))
|
||||||
|
|
||||||
class ColoredFormatter(Formatter):
|
|
||||||
|
|
||||||
def format(self, record):
|
|
||||||
ln = record.__dict__['levelname']
|
|
||||||
col = ''
|
|
||||||
if ln == 'CRITICAL':
|
|
||||||
col = terminal_controller.YELLOW
|
|
||||||
elif ln == 'ERROR':
|
|
||||||
col = terminal_controller.RED
|
|
||||||
elif ln in ['WARN', 'WARNING']:
|
|
||||||
col = terminal_controller.BLUE
|
|
||||||
elif ln == 'INFO':
|
|
||||||
col = terminal_controller.GREEN
|
|
||||||
elif ln == 'DEBUG':
|
|
||||||
col = terminal_controller.CYAN
|
|
||||||
record.__dict__['levelname'] = col + record.__dict__['levelname'] + terminal_controller.NORMAL
|
|
||||||
return Formatter.format(self, record)
|
|
||||||
|
|
||||||
def walk(dir):
|
def walk(dir):
|
||||||
''' A nice interface to os.walk '''
|
''' A nice interface to os.walk '''
|
||||||
for record in os.walk(dir):
|
for record in os.walk(dir):
|
||||||
|
@ -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.143'
|
__version__ = '0.5.1'
|
||||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||||
'''
|
'''
|
||||||
Various run time constants.
|
Various run time constants.
|
||||||
|
@ -132,13 +132,24 @@ class HTMLMetadataReader(MetadataReaderPlugin):
|
|||||||
class MOBIMetadataReader(MetadataReaderPlugin):
|
class MOBIMetadataReader(MetadataReaderPlugin):
|
||||||
|
|
||||||
name = 'Read MOBI metadata'
|
name = 'Read MOBI metadata'
|
||||||
file_types = set(['mobi', 'prc', '.azw'])
|
file_types = set(['mobi', 'prc', 'azw'])
|
||||||
description = _('Read metadata from %s files')%'MOBI'
|
description = _('Read metadata from %s files')%'MOBI'
|
||||||
|
|
||||||
def get_metadata(self, stream, ftype):
|
def get_metadata(self, stream, ftype):
|
||||||
from calibre.ebooks.mobi.reader import get_metadata
|
from calibre.ebooks.mobi.reader import get_metadata
|
||||||
return get_metadata(stream)
|
return get_metadata(stream)
|
||||||
|
|
||||||
|
|
||||||
|
class TOPAZMetadataReader(MetadataReaderPlugin):
|
||||||
|
|
||||||
|
name = 'Read Topaz metadata'
|
||||||
|
file_types = set(['tpz', 'azw1'])
|
||||||
|
description = _('Read metadata from %s files')%'MOBI'
|
||||||
|
|
||||||
|
def get_metadata(self, stream, ftype):
|
||||||
|
from calibre.ebooks.metadata.topaz import get_metadata
|
||||||
|
return get_metadata(stream)
|
||||||
|
|
||||||
class ODTMetadataReader(MetadataReaderPlugin):
|
class ODTMetadataReader(MetadataReaderPlugin):
|
||||||
|
|
||||||
name = 'Read ODT metadata'
|
name = 'Read ODT metadata'
|
||||||
@ -244,11 +255,12 @@ class MOBIMetadataWriter(MetadataWriterPlugin):
|
|||||||
|
|
||||||
from calibre.ebooks.epub.input import EPUBInput
|
from calibre.ebooks.epub.input import EPUBInput
|
||||||
from calibre.ebooks.mobi.input import MOBIInput
|
from calibre.ebooks.mobi.input import MOBIInput
|
||||||
from calibre.customize.profiles import input_profiles
|
from calibre.ebooks.oeb.output import OEBOutput
|
||||||
|
from calibre.customize.profiles import input_profiles, output_profiles
|
||||||
|
|
||||||
plugins = [HTML2ZIP, EPUBInput, MOBIInput]
|
plugins = [HTML2ZIP, EPUBInput, MOBIInput, OEBOutput]
|
||||||
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \
|
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \
|
||||||
x.__name__.endswith('MetadataReader')]
|
x.__name__.endswith('MetadataReader')]
|
||||||
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \
|
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \
|
||||||
x.__name__.endswith('MetadataWriter')]
|
x.__name__.endswith('MetadataWriter')]
|
||||||
plugins += input_profiles
|
plugins += input_profiles + output_profiles
|
@ -24,7 +24,7 @@ class ConversionOption(object):
|
|||||||
self.choices = choices
|
self.choices = choices
|
||||||
|
|
||||||
if self.long_switch is None:
|
if self.long_switch is None:
|
||||||
self.long_switch = '--'+self.name.replace('_', '-')
|
self.long_switch = self.name.replace('_', '-')
|
||||||
|
|
||||||
self.validate_parameters()
|
self.validate_parameters()
|
||||||
|
|
||||||
@ -37,19 +37,24 @@ class ConversionOption(object):
|
|||||||
if not self.help:
|
if not self.help:
|
||||||
raise ValueError('You must set the help text')
|
raise ValueError('You must set the help text')
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(self.name)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return hash(self) == hash(other)
|
||||||
|
|
||||||
class OptionRecommendation(object):
|
class OptionRecommendation(object):
|
||||||
LOW = 1
|
LOW = 1
|
||||||
MED = 2
|
MED = 2
|
||||||
HIGH = 3
|
HIGH = 3
|
||||||
|
|
||||||
def __init__(self, recommeded_value, level=LOW, **kwargs):
|
def __init__(self, recommended_value=None, level=LOW, **kwargs):
|
||||||
'''
|
'''
|
||||||
An option recommendation. That is, an option as well as its recommended
|
An option recommendation. That is, an option as well as its recommended
|
||||||
value and the level of the recommendation.
|
value and the level of the recommendation.
|
||||||
'''
|
'''
|
||||||
self.level = level
|
self.level = level
|
||||||
self.recommended_value = recommeded_value
|
self.recommended_value = recommended_value
|
||||||
self.option = kwargs.pop('option', None)
|
self.option = kwargs.pop('option', None)
|
||||||
if self.option is None:
|
if self.option is None:
|
||||||
self.option = ConversionOption(**kwargs)
|
self.option = ConversionOption(**kwargs)
|
||||||
@ -59,10 +64,12 @@ class OptionRecommendation(object):
|
|||||||
def validate_parameters(self):
|
def validate_parameters(self):
|
||||||
if self.option.choices and self.recommended_value not in \
|
if self.option.choices and self.recommended_value not in \
|
||||||
self.option.choices:
|
self.option.choices:
|
||||||
raise ValueError('Recommended value not in choices')
|
raise ValueError('OpRec: %s: Recommended value not in choices'%
|
||||||
|
self.option.name)
|
||||||
if not (isinstance(self.recommended_value, (int, float, str, unicode))\
|
if not (isinstance(self.recommended_value, (int, float, str, unicode))\
|
||||||
or self.default is None):
|
or self.recommended_value is None):
|
||||||
raise ValueError(unicode(self.default) +
|
raise ValueError('OpRec: %s:'%self.option.name +
|
||||||
|
repr(self.recommended_value) +
|
||||||
' is not a string or a number')
|
' is not a string or a number')
|
||||||
|
|
||||||
|
|
||||||
@ -110,7 +117,11 @@ class InputFormatPlugin(Plugin):
|
|||||||
#: instance of :class:`OptionRecommendation`.
|
#: instance of :class:`OptionRecommendation`.
|
||||||
options = set([])
|
options = set([])
|
||||||
|
|
||||||
def convert(self, stream, options, file_ext, parse_cache, log):
|
#: A set of 3-tuples of the form
|
||||||
|
#: (option_name, recommended_value, recommendation_level)
|
||||||
|
recommendations = set([])
|
||||||
|
|
||||||
|
def convert(self, stream, options, file_ext, parse_cache, log, accelerators):
|
||||||
'''
|
'''
|
||||||
This method must be implemented in sub-classes. It must return
|
This method must be implemented in sub-classes. It must return
|
||||||
the path to the created OPF file. All output should be contained in
|
the path to the created OPF file. All output should be contained in
|
||||||
@ -146,10 +157,16 @@ class InputFormatPlugin(Plugin):
|
|||||||
|
|
||||||
:param log: A :class:`calibre.utils.logging.Log` object. All output
|
:param log: A :class:`calibre.utils.logging.Log` object. All output
|
||||||
should use this object.
|
should use this object.
|
||||||
|
|
||||||
|
:param accelarators: A dictionary of various information that the input
|
||||||
|
plugin can get easily that would speed up the
|
||||||
|
subsequent stages of the conversion.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def __call__(self, stream, options, file_ext, parse_cache, log, output_dir):
|
def __call__(self, stream, options, file_ext, parse_cache, log,
|
||||||
|
accelerators, output_dir):
|
||||||
log('InputFormatPlugin: %s running'%self.name, end=' ')
|
log('InputFormatPlugin: %s running'%self.name, end=' ')
|
||||||
if hasattr(stream, 'name'):
|
if hasattr(stream, 'name'):
|
||||||
log('on', stream.name)
|
log('on', stream.name)
|
||||||
@ -159,7 +176,8 @@ class InputFormatPlugin(Plugin):
|
|||||||
shutil.rmtree(x) if os.path.isdir(x) else os.remove(x)
|
shutil.rmtree(x) if os.path.isdir(x) else os.remove(x)
|
||||||
|
|
||||||
|
|
||||||
ret = self.convert(stream, options, file_ext, parse_cache, log)
|
ret = self.convert(stream, options, file_ext, parse_cache,
|
||||||
|
log, accelerators)
|
||||||
for key in list(parse_cache.keys()):
|
for key in list(parse_cache.keys()):
|
||||||
if os.path.abspath(key) != key:
|
if os.path.abspath(key) != key:
|
||||||
log.warn(('InputFormatPlugin: %s returned a '
|
log.warn(('InputFormatPlugin: %s returned a '
|
||||||
@ -187,3 +205,37 @@ class InputFormatPlugin(Plugin):
|
|||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
class OutputFormatPlugin(Plugin):
|
||||||
|
'''
|
||||||
|
OutputFormatPlugins are responsible for converting an OEB document
|
||||||
|
(OPF+HTML) into an output ebook.
|
||||||
|
|
||||||
|
The OEB document can be assumed to be encoded in UTF-8.
|
||||||
|
The main action happens in :method:`convert`.
|
||||||
|
'''
|
||||||
|
|
||||||
|
type = _('Conversion Output')
|
||||||
|
can_be_disabled = False
|
||||||
|
supported_platforms = ['windows', 'osx', 'linux']
|
||||||
|
|
||||||
|
#: The file type (extension without leading period) that this
|
||||||
|
#: plugin outputs
|
||||||
|
file_type = None
|
||||||
|
|
||||||
|
#: Options shared by all Input format plugins. Do not override
|
||||||
|
#: in sub-classes. Use :member:`options` instead. Every option must be an
|
||||||
|
#: instance of :class:`OptionRecommendation`.
|
||||||
|
common_options = set([])
|
||||||
|
|
||||||
|
#: Options to customize the behavior of this plugin. Every option must be an
|
||||||
|
#: instance of :class:`OptionRecommendation`.
|
||||||
|
options = set([])
|
||||||
|
|
||||||
|
#: A set of 3-tuples of the form
|
||||||
|
#: (option_name, recommended_value, recommendation_level)
|
||||||
|
recommendations = set([])
|
||||||
|
|
||||||
|
def convert(self, oeb_book, input_plugin, options, parse_cache, log):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ __license__ = 'GPL 3'
|
|||||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import sys, re
|
||||||
from calibre.customize import Plugin
|
from calibre.customize import Plugin
|
||||||
|
|
||||||
class InputProfile(Plugin):
|
class InputProfile(Plugin):
|
||||||
@ -16,12 +17,43 @@ class InputProfile(Plugin):
|
|||||||
# inherit from this profile and override as needed
|
# inherit from this profile and override as needed
|
||||||
|
|
||||||
name = 'Default Input Profile'
|
name = 'Default Input Profile'
|
||||||
short_name = 'default' # Used in the CLI so dont spaces etc. in it
|
short_name = 'default' # Used in the CLI so dont use spaces etc. in it
|
||||||
description = _('This profile tries to provide sane defaults and is useful '
|
description = _('This profile tries to provide sane defaults and is useful '
|
||||||
'if you know nothing about the input document.')
|
'if you know nothing about the input document.')
|
||||||
|
|
||||||
input_profiles = [InputProfile]
|
input_profiles = [InputProfile]
|
||||||
|
|
||||||
|
|
||||||
|
class OutputProfile(Plugin):
|
||||||
|
|
||||||
|
author = 'Kovid Goyal'
|
||||||
|
supported_platforms = set(['windows', 'osx', 'linux'])
|
||||||
|
can_be_disabled = False
|
||||||
|
type = _('Output profile')
|
||||||
|
|
||||||
|
name = 'Default Output Profile'
|
||||||
|
short_name = 'default' # Used in the CLI so dont use spaces etc. in it
|
||||||
|
description = _('This profile tries to provide sane defaults and is useful '
|
||||||
|
'if you want to produce a document intended to be read at a '
|
||||||
|
'computer or on a range of devices.')
|
||||||
|
|
||||||
|
epub_flow_size = sys.maxint
|
||||||
|
screen_size = None
|
||||||
|
remove_special_chars = False
|
||||||
|
remove_object_tags = False
|
||||||
|
|
||||||
|
class SonyReader(OutputProfile):
|
||||||
|
|
||||||
|
name = 'Sony Reader'
|
||||||
|
short_name = 'sony'
|
||||||
|
description = _('This profile is intended for the SONY PRS line. '
|
||||||
|
'The 500/505/700 etc.')
|
||||||
|
|
||||||
|
epub_flow_size = 270000
|
||||||
|
screen_size = (590, 765)
|
||||||
|
remove_special_chars = re.compile(u'[\u200b\u00ad]')
|
||||||
|
remove_object_tags = True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
output_profiles = [OutputProfile, SonyReader]
|
@ -6,8 +6,8 @@ import os, shutil, traceback, functools, sys
|
|||||||
|
|
||||||
from calibre.customize import Plugin, FileTypePlugin, MetadataReaderPlugin, \
|
from calibre.customize import Plugin, FileTypePlugin, MetadataReaderPlugin, \
|
||||||
MetadataWriterPlugin
|
MetadataWriterPlugin
|
||||||
from calibre.customize.conversion import InputFormatPlugin
|
from calibre.customize.conversion import InputFormatPlugin, OutputFormatPlugin
|
||||||
from calibre.customize.profiles import InputProfile
|
from calibre.customize.profiles import InputProfile, OutputProfile
|
||||||
from calibre.customize.builtins import plugins as builtin_plugins
|
from calibre.customize.builtins import plugins as builtin_plugins
|
||||||
from calibre.constants import __version__, iswindows, isosx
|
from calibre.constants import __version__, iswindows, isosx
|
||||||
from calibre.ebooks.metadata import MetaInformation
|
from calibre.ebooks.metadata import MetaInformation
|
||||||
@ -76,6 +76,12 @@ def input_profiles():
|
|||||||
if isinstance(plugin, InputProfile):
|
if isinstance(plugin, InputProfile):
|
||||||
yield plugin
|
yield plugin
|
||||||
|
|
||||||
|
def output_profiles():
|
||||||
|
for plugin in _initialized_plugins:
|
||||||
|
if isinstance(plugin, OutputProfile):
|
||||||
|
yield plugin
|
||||||
|
|
||||||
|
|
||||||
def reread_filetype_plugins():
|
def reread_filetype_plugins():
|
||||||
global _on_import
|
global _on_import
|
||||||
global _on_preprocess
|
global _on_preprocess
|
||||||
@ -245,7 +251,17 @@ def input_format_plugins():
|
|||||||
|
|
||||||
def plugin_for_input_format(fmt):
|
def plugin_for_input_format(fmt):
|
||||||
for plugin in input_format_plugins():
|
for plugin in input_format_plugins():
|
||||||
if fmt in plugin.file_types:
|
if fmt.lower() in plugin.file_types:
|
||||||
|
return plugin
|
||||||
|
|
||||||
|
def output_format_plugins():
|
||||||
|
for plugin in _initialized_plugins:
|
||||||
|
if isinstance(plugin, OutputFormatPlugin):
|
||||||
|
yield plugin
|
||||||
|
|
||||||
|
def plugin_for_output_format(fmt):
|
||||||
|
for plugin in output_format_plugins():
|
||||||
|
if fmt.lower() == plugin.file_type:
|
||||||
return plugin
|
return plugin
|
||||||
|
|
||||||
|
|
||||||
|
@ -4,9 +4,9 @@ __copyright__ = '2009, John Schember <john at nachtimwald.com>'
|
|||||||
Device driver for Amazon's Kindle
|
Device driver for Amazon's Kindle
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import os
|
import os, re
|
||||||
|
|
||||||
from calibre.devices.usbms.driver import USBMS
|
from calibre.devices.usbms.driver import USBMS, metadata_from_formats
|
||||||
|
|
||||||
class KINDLE(USBMS):
|
class KINDLE(USBMS):
|
||||||
# Ordered list of supported formats
|
# Ordered list of supported formats
|
||||||
@ -30,6 +30,9 @@ class KINDLE(USBMS):
|
|||||||
EBOOK_DIR_CARD = "documents"
|
EBOOK_DIR_CARD = "documents"
|
||||||
SUPPORTS_SUB_DIRS = True
|
SUPPORTS_SUB_DIRS = True
|
||||||
|
|
||||||
|
WIRELESS_FILE_NAME_PATTERN = re.compile(
|
||||||
|
r'(?P<title>[^-]+)-asin_(?P<asin>[a-zA-Z\d]{10,})-type_(?P<type>\w{4})-v_(?P<index>\d+).*')
|
||||||
|
|
||||||
def delete_books(self, paths, end_session=True):
|
def delete_books(self, paths, end_session=True):
|
||||||
for path in paths:
|
for path in paths:
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
@ -41,6 +44,16 @@ class KINDLE(USBMS):
|
|||||||
if os.path.exists(filepath + '.mbp'):
|
if os.path.exists(filepath + '.mbp'):
|
||||||
os.unlink(filepath + '.mbp')
|
os.unlink(filepath + '.mbp')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def metadata_from_path(cls, path):
|
||||||
|
mi = metadata_from_formats([path])
|
||||||
|
if mi.title == _('Unknown') or ('-asin' in mi.title and '-type' in mi.title):
|
||||||
|
match = cls.WIRELESS_FILE_NAME_PATTERN.match(os.path.basename(path))
|
||||||
|
if match is not None:
|
||||||
|
mi.title = match.group('title')
|
||||||
|
return mi
|
||||||
|
|
||||||
|
|
||||||
class KINDLE2(KINDLE):
|
class KINDLE2(KINDLE):
|
||||||
|
|
||||||
PRODUCT_ID = [0x0002]
|
PRODUCT_ID = [0x0002]
|
||||||
|
@ -1,19 +1,20 @@
|
|||||||
__license__ = 'GPL v3'
|
from __future__ import with_statement
|
||||||
__copyright__ = '2009, John Schember <john at nachtimwald.com>'
|
__license__ = 'GPL 3'
|
||||||
'''
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
Global Mime mapping of ebook types.
|
__docformat__ = 'restructuredtext en'
|
||||||
'''
|
|
||||||
|
|
||||||
MIME_MAP = {
|
from calibre import guess_type
|
||||||
'azw' : 'application/azw',
|
|
||||||
'epub' : 'application/epub+zip',
|
|
||||||
'html' : 'text/html',
|
|
||||||
'lrf' : 'application/x-sony-bbeb',
|
|
||||||
'lrx' : 'application/x-sony-bbeb',
|
|
||||||
'mobi' : 'application/mobi',
|
|
||||||
'pdf' : 'application/pdf',
|
|
||||||
'prc' : 'application/prc',
|
|
||||||
'rtf' : 'application/rtf',
|
|
||||||
'txt' : 'text/plain',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
def _mt(path):
|
||||||
|
mt = guess_type(path)[0]
|
||||||
|
if not mt:
|
||||||
|
mt = 'application/octet-stream'
|
||||||
|
return mt
|
||||||
|
|
||||||
|
def mime_type_ext(ext):
|
||||||
|
if not ext.startswith('.'):
|
||||||
|
ext = '.'+ext
|
||||||
|
return _mt('a'+ext)
|
||||||
|
|
||||||
|
def mime_type_path(path):
|
||||||
|
return _mt(path)
|
@ -15,7 +15,7 @@ from calibre.ebooks.metadata import authors_to_string
|
|||||||
from calibre.devices.usbms.device import Device
|
from calibre.devices.usbms.device import Device
|
||||||
from calibre.devices.usbms.books import BookList, Book
|
from calibre.devices.usbms.books import BookList, Book
|
||||||
from calibre.devices.errors import FreeSpaceError, PathError
|
from calibre.devices.errors import FreeSpaceError, PathError
|
||||||
from calibre.devices.mime import MIME_MAP
|
from calibre.devices.mime import mime_type_ext
|
||||||
|
|
||||||
class File(object):
|
class File(object):
|
||||||
def __init__(self, path):
|
def __init__(self, path):
|
||||||
@ -226,14 +226,17 @@ class USBMS(Device):
|
|||||||
if not os.path.isdir(path):
|
if not os.path.isdir(path):
|
||||||
os.utime(path, None)
|
os.utime(path, None)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def metadata_from_path(cls, path):
|
||||||
|
return metadata_from_formats([path])
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def book_from_path(cls, path):
|
def book_from_path(cls, path):
|
||||||
fileext = path_to_ext(path)
|
fileext = path_to_ext(path)
|
||||||
|
mi = cls.metadata_from_path(path)
|
||||||
mi = metadata_from_formats([path])
|
mime = mime_type_ext(fileext)
|
||||||
mime = MIME_MAP[fileext] if fileext in MIME_MAP.keys() else 'Unknown'
|
|
||||||
|
|
||||||
authors = authors_to_string(mi.authors)
|
authors = authors_to_string(mi.authors)
|
||||||
|
|
||||||
return Book(path, mi.title, authors, mime)
|
book = Book(path, mi.title, authors, mime)
|
||||||
|
return book
|
||||||
|
|
||||||
|
@ -1306,7 +1306,10 @@ class BeautifulStoneSoup(Tag, SGMLParser):
|
|||||||
if self.convertEntities:
|
if self.convertEntities:
|
||||||
if ref.lower().startswith('x'): #
|
if ref.lower().startswith('x'): #
|
||||||
ref = int(ref[1:], 16) # Added by Kovid to handle hex numeric entities
|
ref = int(ref[1:], 16) # Added by Kovid to handle hex numeric entities
|
||||||
|
try:
|
||||||
data = unichr(int(ref))
|
data = unichr(int(ref))
|
||||||
|
except ValueError: # Bad numerical entity. Added by Kovid
|
||||||
|
data = u''
|
||||||
else:
|
else:
|
||||||
data = '&#%s;' % ref
|
data = '&#%s;' % ref
|
||||||
self.handle_data(data)
|
self.handle_data(data)
|
||||||
|
@ -97,9 +97,12 @@ def xml_to_unicode(raw, verbose=False, strip_encoding_pats=False,
|
|||||||
if encoding is None:
|
if encoding is None:
|
||||||
encoding = force_encoding(raw, verbose)
|
encoding = force_encoding(raw, verbose)
|
||||||
try:
|
try:
|
||||||
|
if encoding.lower().strip() == 'macintosh':
|
||||||
|
encoding = 'mac-roman'
|
||||||
raw = raw.decode(encoding, 'replace')
|
raw = raw.decode(encoding, 'replace')
|
||||||
except LookupError:
|
except LookupError:
|
||||||
raw = raw.decode('utf-8', 'replace')
|
encoding = 'utf-8'
|
||||||
|
raw = raw.decode(encoding, 'replace')
|
||||||
|
|
||||||
if strip_encoding_pats:
|
if strip_encoding_pats:
|
||||||
raw = strip_encoding_declarations(raw)
|
raw = strip_encoding_declarations(raw)
|
||||||
|
167
src/calibre/ebooks/conversion/cli.py
Normal file
167
src/calibre/ebooks/conversion/cli.py
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
from __future__ import with_statement
|
||||||
|
__license__ = 'GPL 3'
|
||||||
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
'''
|
||||||
|
Command line interface to conversion sub-system
|
||||||
|
'''
|
||||||
|
|
||||||
|
USAGE = '%prog ' + _('''\
|
||||||
|
input_file output_file [options]
|
||||||
|
|
||||||
|
Convert an ebook from one format to another.
|
||||||
|
|
||||||
|
input_file is the input and output_file is the output. Both must be \
|
||||||
|
specified as the first two arguments to the command.
|
||||||
|
|
||||||
|
The output ebook format is guessed from the file extension of \
|
||||||
|
output_file. output_file can also be of the special format .EXT where \
|
||||||
|
EXT is the output file extension. In this case, the name of the output \
|
||||||
|
file is derived the name of the input file. Note that the filenames must \
|
||||||
|
not start with a hyphen. Finally, if output_file has no extension, then \
|
||||||
|
it is treated as a directory and an "open ebook" (OEB) consisting of HTML \
|
||||||
|
files is written to that directory. These files are the files that would \
|
||||||
|
normally have been passed to the output plugin.
|
||||||
|
|
||||||
|
After specifying the input \
|
||||||
|
and output file you can customize the conversion by specifying various \
|
||||||
|
options. the available options depend on the input and output file types. \
|
||||||
|
To get help on them specify the input and output file and then use the -h \
|
||||||
|
option.
|
||||||
|
|
||||||
|
For full documentation of the conversion system see
|
||||||
|
''') + 'http://calibre.kovidgoyal.net/user_manual/conversion.html'
|
||||||
|
|
||||||
|
import sys, os
|
||||||
|
from optparse import OptionGroup, Option
|
||||||
|
|
||||||
|
from calibre.utils.config import OptionParser
|
||||||
|
from calibre.utils.logging import Log
|
||||||
|
from calibre.constants import preferred_encoding
|
||||||
|
from calibre.customize.conversion import OptionRecommendation
|
||||||
|
|
||||||
|
def print_help(parser, log):
|
||||||
|
help = parser.format_help().encode(preferred_encoding, 'replace')
|
||||||
|
log(help)
|
||||||
|
|
||||||
|
def check_command_line_options(parser, args, log):
|
||||||
|
if len(args) < 3 or args[1].startswith('-') or args[2].startswith('-'):
|
||||||
|
print_help(parser)
|
||||||
|
log.error('\n\nYou must specify the input AND output files')
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
input = os.path.abspath(args[1])
|
||||||
|
if not os.access(input, os.R_OK):
|
||||||
|
log.error('Cannot read from', input)
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
output = args[2]
|
||||||
|
if output.startswith('.'):
|
||||||
|
output = os.path.splitext(os.path.basename(input))[0]+output
|
||||||
|
output = os.path.abspath(output)
|
||||||
|
|
||||||
|
if '.' in output:
|
||||||
|
if os.path.exists(output):
|
||||||
|
log.warn('WARNING:', output, 'exists. Deleting.')
|
||||||
|
os.remove(output)
|
||||||
|
|
||||||
|
return input, output
|
||||||
|
|
||||||
|
def option_recommendation_to_cli_option(add_option, rec):
|
||||||
|
opt = rec.option
|
||||||
|
switches = ['-'+opt.short_switch] if opt.short_switch else []
|
||||||
|
switches.append('--'+opt.long_switch)
|
||||||
|
attrs = dict(dest=opt.name, help=opt.help,
|
||||||
|
choices=opt.choices, default=rec.recommended_value)
|
||||||
|
add_option(Option(*switches, **attrs))
|
||||||
|
|
||||||
|
def add_input_output_options(parser, plumber):
|
||||||
|
input_options, output_options = \
|
||||||
|
plumber.input_options, plumber.output_options
|
||||||
|
|
||||||
|
def add_options(group, options):
|
||||||
|
for opt in options:
|
||||||
|
option_recommendation_to_cli_option(group, opt)
|
||||||
|
|
||||||
|
if input_options:
|
||||||
|
title = _('INPUT OPTIONS')
|
||||||
|
io = OptionGroup(parser, title, _('Options to control the processing'
|
||||||
|
' of the input %s file')%plumber.input_fmt)
|
||||||
|
add_options(io.add_option, input_options)
|
||||||
|
parser.add_option_group(io)
|
||||||
|
|
||||||
|
if output_options:
|
||||||
|
title = plumber.output_fmt.upper() + ' ' + _('OPTIONS')
|
||||||
|
oo = OptionGroup(parser, title, _('Options to control the processing'
|
||||||
|
' of the output %s file')%plumber.input_fmt)
|
||||||
|
add_options(oo.add_option, output_options)
|
||||||
|
parser.add_option_group(oo)
|
||||||
|
|
||||||
|
def add_pipeline_options(parser, plumber):
|
||||||
|
groups = {
|
||||||
|
'' : ('',
|
||||||
|
[
|
||||||
|
'input_profile',
|
||||||
|
'output_profile',
|
||||||
|
]
|
||||||
|
),
|
||||||
|
|
||||||
|
'METADATA' : (_('Options to set metadata in the output'),
|
||||||
|
plumber.metadata_option_names,
|
||||||
|
),
|
||||||
|
'DEBUG': (_('Options to help with debugging the conversion'),
|
||||||
|
[
|
||||||
|
'verbose',
|
||||||
|
]),
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
group_order = ['', 'METADATA', 'DEBUG']
|
||||||
|
|
||||||
|
for group in group_order:
|
||||||
|
desc, options = groups[group]
|
||||||
|
if group:
|
||||||
|
group = OptionGroup(parser, group, desc)
|
||||||
|
parser.add_option_group(group)
|
||||||
|
add_option = group.add_option if group != '' else parser.add_option
|
||||||
|
|
||||||
|
for name in options:
|
||||||
|
rec = plumber.get_option_by_name(name)
|
||||||
|
if rec.level < rec.HIGH:
|
||||||
|
option_recommendation_to_cli_option(add_option, rec)
|
||||||
|
|
||||||
|
def option_parser():
|
||||||
|
return OptionParser(usage=USAGE)
|
||||||
|
|
||||||
|
def main(args=sys.argv):
|
||||||
|
log = Log()
|
||||||
|
parser = option_parser()
|
||||||
|
if len(args) < 3:
|
||||||
|
print_help(parser, log)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
input, output = check_command_line_options(parser, args, log)
|
||||||
|
|
||||||
|
from calibre.ebooks.conversion.plumber import Plumber
|
||||||
|
|
||||||
|
plumber = Plumber(input, output, log)
|
||||||
|
add_input_output_options(parser, plumber)
|
||||||
|
add_pipeline_options(parser, plumber)
|
||||||
|
|
||||||
|
opts = parser.parse_args(args)[0]
|
||||||
|
recommendations = [(n.dest, getattr(opts, n.dest),
|
||||||
|
OptionRecommendation.HIGH) \
|
||||||
|
for n in parser.options_iter()
|
||||||
|
if n.dest]
|
||||||
|
plumber.merge_ui_recommendations(recommendations)
|
||||||
|
|
||||||
|
plumber.run()
|
||||||
|
|
||||||
|
log(_('Output saved to'), ' ', plumber.output)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
@ -3,11 +3,29 @@ __license__ = 'GPL 3'
|
|||||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
from calibre.customize.conversion import OptionRecommendation
|
from calibre.customize.conversion import OptionRecommendation
|
||||||
from calibre.customize.ui import input_profiles
|
from calibre.customize.ui import input_profiles, output_profiles, \
|
||||||
|
plugin_for_input_format, plugin_for_output_format
|
||||||
|
|
||||||
pipeline_options = [
|
class OptionValues(object):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Plumber(object):
|
||||||
|
|
||||||
|
metadata_option_names = [
|
||||||
|
'title', 'authors', 'title_sort', 'author_sort', 'cover', 'comments',
|
||||||
|
'publisher', 'series', 'series_index', 'rating', 'isbn',
|
||||||
|
'tags', 'book_producer', 'language'
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, input, output, log):
|
||||||
|
self.input = input
|
||||||
|
self.output = output
|
||||||
|
self.log = log
|
||||||
|
|
||||||
|
self.pipeline_options = [
|
||||||
|
|
||||||
OptionRecommendation(name='verbose',
|
OptionRecommendation(name='verbose',
|
||||||
recommended_value=0, level=OptionRecommendation.LOW,
|
recommended_value=0, level=OptionRecommendation.LOW,
|
||||||
@ -16,7 +34,6 @@ OptionRecommendation(name='verbose',
|
|||||||
'verbosity.')
|
'verbosity.')
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
||||||
OptionRecommendation(name='input_profile',
|
OptionRecommendation(name='input_profile',
|
||||||
recommended_value='default', level=OptionRecommendation.LOW,
|
recommended_value='default', level=OptionRecommendation.LOW,
|
||||||
choices=[x.short_name for x in input_profiles()],
|
choices=[x.short_name for x in input_profiles()],
|
||||||
@ -27,4 +44,193 @@ OptionRecommendation(name='input_profile',
|
|||||||
'pixels).')
|
'pixels).')
|
||||||
),
|
),
|
||||||
|
|
||||||
|
OptionRecommendation(name='output_profile',
|
||||||
|
recommended_value='default', level=OptionRecommendation.LOW,
|
||||||
|
choices=[x.short_name for x in output_profiles()],
|
||||||
|
help=_('Specify the output profile. The output profile '
|
||||||
|
'tells the conversion system how to optimize the '
|
||||||
|
'created document for the specified device. In some cases, '
|
||||||
|
'an output profile is required to produce documents that '
|
||||||
|
'will work on a device. For example EPUB on the SONY reader.'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
OptionRecommendation(name='read_metadata_from_opf',
|
||||||
|
recommended_value=None, level=OptionRecommendation.LOW,
|
||||||
|
short_switch='m',
|
||||||
|
help=_('Read metadata from the specified OPF file. Metadata read '
|
||||||
|
'from this file will override any metadata in the source '
|
||||||
|
'file.')
|
||||||
|
),
|
||||||
|
|
||||||
|
OptionRecommendation(name='title',
|
||||||
|
recommended_value=None, level=OptionRecommendation.LOW,
|
||||||
|
help=_('Set the title.')),
|
||||||
|
|
||||||
|
OptionRecommendation(name='authors',
|
||||||
|
recommended_value=None, level=OptionRecommendation.LOW,
|
||||||
|
help=_('Set the authors. Multiple authors should be separated ')),
|
||||||
|
|
||||||
|
OptionRecommendation(name='title_sort',
|
||||||
|
recommended_value=None, level=OptionRecommendation.LOW,
|
||||||
|
help=_('The version of the title to be used for sorting. ')),
|
||||||
|
|
||||||
|
OptionRecommendation(name='author_sort',
|
||||||
|
recommended_value=None, level=OptionRecommendation.LOW,
|
||||||
|
help=_('String to be used when sorting by author. ')),
|
||||||
|
|
||||||
|
OptionRecommendation(name='cover',
|
||||||
|
recommended_value=None, level=OptionRecommendation.LOW,
|
||||||
|
help=_('Set the cover to the specified file.')),
|
||||||
|
|
||||||
|
OptionRecommendation(name='comments',
|
||||||
|
recommended_value=None, level=OptionRecommendation.LOW,
|
||||||
|
help=_('Set the ebook description.')),
|
||||||
|
|
||||||
|
OptionRecommendation(name='publisher',
|
||||||
|
recommended_value=None, level=OptionRecommendation.LOW,
|
||||||
|
help=_('Set the ebook publisher.')),
|
||||||
|
|
||||||
|
OptionRecommendation(name='series',
|
||||||
|
recommended_value=None, level=OptionRecommendation.LOW,
|
||||||
|
help=_('Set the series this ebook belongs to.')),
|
||||||
|
|
||||||
|
OptionRecommendation(name='series_index',
|
||||||
|
recommended_value=None, level=OptionRecommendation.LOW,
|
||||||
|
help=_('Set the index of the book in this series.')),
|
||||||
|
|
||||||
|
OptionRecommendation(name='rating',
|
||||||
|
recommended_value=None, level=OptionRecommendation.LOW,
|
||||||
|
help=_('Set the rating. Should be a number between 1 and 5.')),
|
||||||
|
|
||||||
|
OptionRecommendation(name='isbn',
|
||||||
|
recommended_value=None, level=OptionRecommendation.LOW,
|
||||||
|
help=_('Set the ISBN of the book.')),
|
||||||
|
|
||||||
|
OptionRecommendation(name='tags',
|
||||||
|
recommended_value=None, level=OptionRecommendation.LOW,
|
||||||
|
help=_('Set the tags for the book. Should be a comma separated list.')),
|
||||||
|
|
||||||
|
OptionRecommendation(name='book_producer',
|
||||||
|
recommended_value=None, level=OptionRecommendation.LOW,
|
||||||
|
help=_('Set the book producer.')),
|
||||||
|
|
||||||
|
OptionRecommendation(name='language',
|
||||||
|
recommended_value=None, level=OptionRecommendation.LOW,
|
||||||
|
help=_('Set the language.')),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
input_fmt = os.path.splitext(input)[1]
|
||||||
|
if not input_fmt:
|
||||||
|
raise ValueError('Input file must have an extension')
|
||||||
|
input_fmt = input_fmt[1:].lower()
|
||||||
|
|
||||||
|
output_fmt = os.path.splitext(output)[1]
|
||||||
|
if not output_fmt:
|
||||||
|
output_fmt = '.oeb'
|
||||||
|
output_fmt = output_fmt[1:].lower()
|
||||||
|
|
||||||
|
self.input_plugin = plugin_for_input_format(input_fmt)
|
||||||
|
self.output_plugin = plugin_for_output_format(output_fmt)
|
||||||
|
|
||||||
|
if self.input_plugin is None:
|
||||||
|
raise ValueError('No plugin to handle input format: '+input_fmt)
|
||||||
|
|
||||||
|
if self.output_plugin is None:
|
||||||
|
raise ValueError('No plugin to handle output format: '+output_fmt)
|
||||||
|
|
||||||
|
self.input_fmt = input_fmt
|
||||||
|
self.output_fmt = output_fmt
|
||||||
|
|
||||||
|
self.input_options = self.input_plugin.options.union(
|
||||||
|
self.input_plugin.common_options)
|
||||||
|
self.output_options = self.output_plugin.options.union(
|
||||||
|
self.output_plugin.common_options)
|
||||||
|
|
||||||
|
self.merge_plugin_recommendations()
|
||||||
|
|
||||||
|
def get_option_by_name(self, name):
|
||||||
|
for group in (self.input_options, self.pipeline_options,
|
||||||
|
self.output_options):
|
||||||
|
for rec in group:
|
||||||
|
if rec.option == name:
|
||||||
|
return rec
|
||||||
|
|
||||||
|
def merge_plugin_recommendations(self):
|
||||||
|
for source in (self.input_plugin, self.output_plugin):
|
||||||
|
for name, val, level in source.recommendations:
|
||||||
|
rec = self.get_option_by_name(name)
|
||||||
|
if rec is not None and rec.level <= level:
|
||||||
|
rec.recommended_value = val
|
||||||
|
|
||||||
|
def merge_ui_recommendations(self, recommendations):
|
||||||
|
for name, val, level in recommendations:
|
||||||
|
rec = self.get_option_by_name(name)
|
||||||
|
if rec is not None and rec.level <= level and rec.level < rec.HIGH:
|
||||||
|
rec.recommended_value = val
|
||||||
|
|
||||||
|
def read_user_metadata(self):
|
||||||
|
from calibre.ebooks.metadata import MetaInformation, string_to_authors
|
||||||
|
from calibre.ebooks.metadata.opf2 import OPF
|
||||||
|
mi = MetaInformation(None, [])
|
||||||
|
if self.opts.read_metadata_from_opf is not None:
|
||||||
|
self.opts.read_metadata_from_opf = os.path.abspath(
|
||||||
|
self.opts.read_metadata_from_opf)
|
||||||
|
opf = OPF(open(self.opts.read_metadata_from_opf, 'rb'),
|
||||||
|
os.path.dirname(self.opts.read_metadata_from_opf))
|
||||||
|
mi = MetaInformation(opf)
|
||||||
|
for x in self.metadata_option_names:
|
||||||
|
val = getattr(self.opts, x, None)
|
||||||
|
if val is not None:
|
||||||
|
if x == 'authors':
|
||||||
|
val = string_to_authors(val)
|
||||||
|
elif x == 'tags':
|
||||||
|
val = [i.strip() for i in val.split(',')]
|
||||||
|
elif x in ('rating', 'series_index'):
|
||||||
|
val = float(val)
|
||||||
|
setattr(mi, x, val)
|
||||||
|
if mi.cover:
|
||||||
|
mi.cover_data = ('', open(mi.cover, 'rb').read())
|
||||||
|
mi.cover = None
|
||||||
|
self.user_metadata = mi
|
||||||
|
|
||||||
|
|
||||||
|
def setup_options(self):
|
||||||
|
self.opts = OptionValues()
|
||||||
|
for group in (self.input_options, self.pipeline_options,
|
||||||
|
self.output_options):
|
||||||
|
for rec in group:
|
||||||
|
setattr(self.opts, rec.option.name, rec.recommended_value)
|
||||||
|
|
||||||
|
for x in input_profiles():
|
||||||
|
if x.short_name == self.opts.input_profile:
|
||||||
|
self.opts.input_profile = x
|
||||||
|
break
|
||||||
|
|
||||||
|
for x in output_profiles():
|
||||||
|
if x.short_name == self.opts.output_profile:
|
||||||
|
self.opts.output_profile = x
|
||||||
|
break
|
||||||
|
|
||||||
|
self.read_user_metadata()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.setup_options()
|
||||||
|
from calibre.customize.ui import run_plugins_on_preprocess
|
||||||
|
self.input = run_plugins_on_preprocess(self.input)
|
||||||
|
|
||||||
|
from calibre.ebooks.oeb.reader import OEBReader
|
||||||
|
from calibre.ebooks.oeb.base import OEBBook
|
||||||
|
parse_cache, accelerators = {}, {}
|
||||||
|
|
||||||
|
opfpath = self.input_plugin(open(self.input, 'rb'), self.opts,
|
||||||
|
self.input_fmt, parse_cache, self.log,
|
||||||
|
accelerators)
|
||||||
|
|
||||||
|
self.reader = OEBReader()
|
||||||
|
self.oeb = OEBBook(self.log, parse_cache=parse_cache)
|
||||||
|
self.reader(self.oeb, opfpath)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -12,7 +12,7 @@ from contextlib import nested
|
|||||||
|
|
||||||
from calibre import extract, walk
|
from calibre import extract, walk
|
||||||
from calibre.ebooks import DRMError
|
from calibre.ebooks import DRMError
|
||||||
from calibre.ebooks.epub import config as common_config, process_encryption
|
from calibre.ebooks.epub import config as common_config
|
||||||
from calibre.ebooks.epub.from_html import convert as html2epub, find_html_index
|
from calibre.ebooks.epub.from_html import convert as html2epub, find_html_index
|
||||||
from calibre.ptempfile import TemporaryDirectory
|
from calibre.ptempfile import TemporaryDirectory
|
||||||
from calibre.ebooks.metadata import MetaInformation
|
from calibre.ebooks.metadata import MetaInformation
|
||||||
|
@ -197,6 +197,9 @@ class HTMLProcessor(Processor, Rationalizer):
|
|||||||
if not tag.text and not tag.get('src', False):
|
if not tag.text and not tag.get('src', False):
|
||||||
tag.getparent().remove(tag)
|
tag.getparent().remove(tag)
|
||||||
|
|
||||||
|
for tag in self.root.xpath('//form'):
|
||||||
|
tag.getparent().remove(tag)
|
||||||
|
|
||||||
if self.opts.linearize_tables:
|
if self.opts.linearize_tables:
|
||||||
for tag in self.root.xpath('//table | //tr | //th | //td'):
|
for tag in self.root.xpath('//table | //tr | //th | //td'):
|
||||||
tag.tag = 'div'
|
tag.tag = 'div'
|
||||||
|
@ -51,7 +51,8 @@ class EPUBInput(InputFormatPlugin):
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def convert(self, stream, options, file_ext, parse_cache, log):
|
def convert(self, stream, options, file_ext, parse_cache, log,
|
||||||
|
accelerators):
|
||||||
from calibre.utils.zipfile import ZipFile
|
from calibre.utils.zipfile import ZipFile
|
||||||
from calibre import walk
|
from calibre import walk
|
||||||
from calibre.ebooks import DRMError
|
from calibre.ebooks import DRMError
|
||||||
|
@ -118,11 +118,17 @@ class EbookIterator(object):
|
|||||||
self.spine = [SpineItem(i.path) for i in self.opf.spine]
|
self.spine = [SpineItem(i.path) for i in self.opf.spine]
|
||||||
|
|
||||||
cover = self.opf.cover
|
cover = self.opf.cover
|
||||||
if os.path.splitext(self.pathtoebook)[1].lower() in ('.lit', '.mobi', '.prc') and cover:
|
if os.path.splitext(self.pathtoebook)[1].lower() in \
|
||||||
|
('.lit', '.mobi', '.prc') and cover:
|
||||||
cfile = os.path.join(os.path.dirname(self.spine[0]), 'calibre_ei_cover.html')
|
cfile = os.path.join(os.path.dirname(self.spine[0]), 'calibre_ei_cover.html')
|
||||||
open(cfile, 'wb').write(TITLEPAGE%cover)
|
open(cfile, 'wb').write(TITLEPAGE%cover)
|
||||||
self.spine[0:0] = [SpineItem(cfile)]
|
self.spine[0:0] = [SpineItem(cfile)]
|
||||||
|
|
||||||
|
if self.opf.path_to_html_toc is not None and \
|
||||||
|
self.opf.path_to_html_toc not in self.spine:
|
||||||
|
self.spine.append(SpineItem(self.opf.path_to_html_toc))
|
||||||
|
|
||||||
|
|
||||||
sizes = [i.character_count for i in self.spine]
|
sizes = [i.character_count for i in self.spine]
|
||||||
self.pages = [math.ceil(i/float(self.CHARACTERS_PER_PAGE)) for i in sizes]
|
self.pages = [math.ceil(i/float(self.CHARACTERS_PER_PAGE)) for i in sizes]
|
||||||
for p, s in zip(self.pages, self.spine):
|
for p, s in zip(self.pages, self.spine):
|
||||||
|
@ -11,7 +11,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
import os, re
|
import os, re
|
||||||
from itertools import count, chain
|
from itertools import count, chain
|
||||||
from calibre.ebooks.oeb.base import XHTML, XHTML_NS
|
from calibre.ebooks.oeb.base import XHTML, XHTML_NS
|
||||||
from calibre.ebooks.oeb.base import OEBBook, DirWriter
|
from calibre.ebooks.oeb.base import OEBBook
|
||||||
from lxml import etree, html
|
from lxml import etree, html
|
||||||
from lxml.etree import XPath
|
from lxml.etree import XPath
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ from lxml.cssselect import CSSSelector
|
|||||||
|
|
||||||
from calibre.ebooks.metadata.opf2 import OPF
|
from calibre.ebooks.metadata.opf2 import OPF
|
||||||
from calibre.ebooks.epub import tostring, rules
|
from calibre.ebooks.epub import tostring, rules
|
||||||
from calibre import CurrentDir, LoggingInterface
|
from calibre import CurrentDir
|
||||||
|
|
||||||
XPath = functools.partial(_XPath, namespaces={'re':'http://exslt.org/regular-expressions'})
|
XPath = functools.partial(_XPath, namespaces={'re':'http://exslt.org/regular-expressions'})
|
||||||
content = functools.partial(os.path.join, 'content')
|
content = functools.partial(os.path.join, 'content')
|
||||||
@ -32,10 +32,9 @@ class SplitError(ValueError):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Splitter(LoggingInterface):
|
class Splitter(object):
|
||||||
|
|
||||||
def __init__(self, path, opts, stylesheet_map, opf):
|
def __init__(self, path, opts, stylesheet_map, opf):
|
||||||
LoggingInterface.__init__(self, logging.getLogger('htmlsplit'))
|
|
||||||
self.setup_cli_handler(opts.verbose)
|
self.setup_cli_handler(opts.verbose)
|
||||||
self.path = path
|
self.path = path
|
||||||
self.always_remove = not opts.preserve_tag_structure or \
|
self.always_remove = not opts.preserve_tag_structure or \
|
||||||
|
@ -19,11 +19,10 @@ from lxml.html import HtmlElementClassLookup, HTMLParser as _HTMLParser, \
|
|||||||
from lxml.etree import XPath
|
from lxml.etree import XPath
|
||||||
get_text = XPath("//text()")
|
get_text = XPath("//text()")
|
||||||
|
|
||||||
from calibre import LoggingInterface, unicode_path, entity_to_unicode
|
from calibre import unicode_path, entity_to_unicode
|
||||||
from calibre.ebooks.chardet import xml_to_unicode, ENCODING_PATS
|
from calibre.ebooks.chardet import xml_to_unicode, ENCODING_PATS
|
||||||
from calibre.utils.config import Config, StringConfig
|
from calibre.utils.config import Config, StringConfig
|
||||||
from calibre.ebooks.metadata import MetaInformation
|
from calibre.ebooks.metadata import MetaInformation
|
||||||
from calibre.ebooks.metadata.meta import get_metadata
|
|
||||||
from calibre.ebooks.metadata.opf2 import OPF, OPFCreator
|
from calibre.ebooks.metadata.opf2 import OPF, OPFCreator
|
||||||
from calibre.ptempfile import PersistentTemporaryDirectory, PersistentTemporaryFile
|
from calibre.ptempfile import PersistentTemporaryDirectory, PersistentTemporaryFile
|
||||||
from calibre.utils.zipfile import ZipFile
|
from calibre.utils.zipfile import ZipFile
|
||||||
@ -401,7 +400,7 @@ class PreProcessor(object):
|
|||||||
html = rule[0].sub(rule[1], html)
|
html = rule[0].sub(rule[1], html)
|
||||||
return html
|
return html
|
||||||
|
|
||||||
class Parser(PreProcessor, LoggingInterface):
|
class Parser(PreProcessor):
|
||||||
# SELF_CLOSING_TAGS = 'hr|br|link|img|meta|input|area|base|basefont'
|
# SELF_CLOSING_TAGS = 'hr|br|link|img|meta|input|area|base|basefont'
|
||||||
# SELF_CLOSING_RULES = [re.compile(p[0]%SELF_CLOSING_TAGS, re.IGNORECASE) for p in
|
# SELF_CLOSING_RULES = [re.compile(p[0]%SELF_CLOSING_TAGS, re.IGNORECASE) for p in
|
||||||
# [
|
# [
|
||||||
@ -412,7 +411,6 @@ class Parser(PreProcessor, LoggingInterface):
|
|||||||
# ]
|
# ]
|
||||||
|
|
||||||
def __init__(self, htmlfile, opts, tdir, resource_map, htmlfiles, name='htmlparser'):
|
def __init__(self, htmlfile, opts, tdir, resource_map, htmlfiles, name='htmlparser'):
|
||||||
LoggingInterface.__init__(self, logging.getLogger(name))
|
|
||||||
self.setup_cli_handler(opts.verbose)
|
self.setup_cli_handler(opts.verbose)
|
||||||
self.htmlfile = htmlfile
|
self.htmlfile = htmlfile
|
||||||
self.opts = opts
|
self.opts = opts
|
||||||
@ -859,7 +857,7 @@ class Processor(Parser):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
setting = ''
|
setting = ''
|
||||||
face = font.attrib.pop('face', None)
|
face = font.attrib.pop('face', None)
|
||||||
if face is not None:
|
if face:
|
||||||
faces = []
|
faces = []
|
||||||
for face in face.split(','):
|
for face in face.split(','):
|
||||||
face = face.strip()
|
face = face.strip()
|
||||||
@ -1038,6 +1036,7 @@ def merge_metadata(htmlfile, opf, opts):
|
|||||||
if opf:
|
if opf:
|
||||||
mi = MetaInformation(opf)
|
mi = MetaInformation(opf)
|
||||||
elif htmlfile:
|
elif htmlfile:
|
||||||
|
from calibre.ebooks.metadata.meta import get_metadata
|
||||||
try:
|
try:
|
||||||
mi = get_metadata(open(htmlfile, 'rb'), 'html')
|
mi = get_metadata(open(htmlfile, 'rb'), 'html')
|
||||||
except:
|
except:
|
||||||
|
@ -143,6 +143,7 @@ class PageProcessor(list):
|
|||||||
MagickRotateImage(wand, pw, -90)
|
MagickRotateImage(wand, pw, -90)
|
||||||
|
|
||||||
# 25 percent fuzzy trim?
|
# 25 percent fuzzy trim?
|
||||||
|
if not self.opts.disable_trim:
|
||||||
MagickTrimImage(wand, 25*65535/100)
|
MagickTrimImage(wand, 25*65535/100)
|
||||||
MagickSetImagePage(wand, 0,0,0,0) #Clear page after trim, like a "+repage"
|
MagickSetImagePage(wand, 0,0,0,0) #Clear page after trim, like a "+repage"
|
||||||
# Do the Photoshop "Auto Levels" equivalent
|
# Do the Photoshop "Auto Levels" equivalent
|
||||||
@ -303,6 +304,9 @@ def config(defaults=None,output_format='lrf'):
|
|||||||
help=_('Maintain picture aspect ratio. Default is to fill the screen.'))
|
help=_('Maintain picture aspect ratio. Default is to fill the screen.'))
|
||||||
c.add_opt('dont_sharpen', ['-s', '--disable-sharpen'], default=False,
|
c.add_opt('dont_sharpen', ['-s', '--disable-sharpen'], default=False,
|
||||||
help=_('Disable sharpening.'))
|
help=_('Disable sharpening.'))
|
||||||
|
c.add_opt('disable_trim', ['--disable-trim'], default=False,
|
||||||
|
help=_('Disable trimming of comic pages. For some comics, '
|
||||||
|
'trimming might remove content as well as borders.'))
|
||||||
c.add_opt('landscape', ['-l', '--landscape'], default=False,
|
c.add_opt('landscape', ['-l', '--landscape'], default=False,
|
||||||
help=_("Don't split landscape images into two portrait images"))
|
help=_("Don't split landscape images into two portrait images"))
|
||||||
c.add_opt('wide', ['-w', '--wide-aspect'], default=False,
|
c.add_opt('wide', ['-w', '--wide-aspect'], default=False,
|
||||||
|
@ -31,7 +31,7 @@ from calibre.ebooks.lrf import option_parser as lrf_option_parser
|
|||||||
from calibre.ebooks import ConversionError
|
from calibre.ebooks import ConversionError
|
||||||
from calibre.ebooks.lrf.html.table import Table
|
from calibre.ebooks.lrf.html.table import Table
|
||||||
from calibre import filename_to_utf8, setup_cli_handlers, __appname__, \
|
from calibre import filename_to_utf8, setup_cli_handlers, __appname__, \
|
||||||
fit_image, LoggingInterface, preferred_encoding
|
fit_image, preferred_encoding
|
||||||
from calibre.ptempfile import PersistentTemporaryFile
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
from calibre.devices.interface import Device
|
from calibre.devices.interface import Device
|
||||||
from calibre.ebooks.lrf.html.color_map import lrs_color
|
from calibre.ebooks.lrf.html.color_map import lrs_color
|
||||||
@ -78,7 +78,7 @@ def tag_regex(tagname):
|
|||||||
return dict(open=r'(?:<\s*%(t)s\s+[^<>]*?>|<\s*%(t)s\s*>)'%dict(t=tagname), \
|
return dict(open=r'(?:<\s*%(t)s\s+[^<>]*?>|<\s*%(t)s\s*>)'%dict(t=tagname), \
|
||||||
close=r'</\s*%(t)s\s*>'%dict(t=tagname))
|
close=r'</\s*%(t)s\s*>'%dict(t=tagname))
|
||||||
|
|
||||||
class HTMLConverter(object, LoggingInterface):
|
class HTMLConverter(object):
|
||||||
SELECTOR_PAT = re.compile(r"([A-Za-z0-9\-\_\:\.]+[A-Za-z0-9\-\_\:\.\s\,]*)\s*\{([^\}]*)\}")
|
SELECTOR_PAT = re.compile(r"([A-Za-z0-9\-\_\:\.]+[A-Za-z0-9\-\_\:\.\s\,]*)\s*\{([^\}]*)\}")
|
||||||
PAGE_BREAK_PAT = re.compile(r'page-break-(?:after|before)\s*:\s*(\w+)', re.IGNORECASE)
|
PAGE_BREAK_PAT = re.compile(r'page-break-(?:after|before)\s*:\s*(\w+)', re.IGNORECASE)
|
||||||
IGNORED_TAGS = (Comment, Declaration, ProcessingInstruction)
|
IGNORED_TAGS = (Comment, Declaration, ProcessingInstruction)
|
||||||
@ -99,6 +99,10 @@ class HTMLConverter(object, LoggingInterface):
|
|||||||
# Replace common line break patterns with line breaks
|
# Replace common line break patterns with line breaks
|
||||||
(re.compile(r'<p>( |\s)*</p>', re.IGNORECASE), lambda m: '<br />'),
|
(re.compile(r'<p>( |\s)*</p>', re.IGNORECASE), lambda m: '<br />'),
|
||||||
|
|
||||||
|
# Replace empty headers with line breaks
|
||||||
|
(re.compile(r'<h[0-5]?>( |\s)*</h[0-5]?>',
|
||||||
|
re.IGNORECASE), lambda m: '<br />'),
|
||||||
|
|
||||||
# Replace entities
|
# Replace entities
|
||||||
(re.compile(ur'&(\S+?);'), partial(entity_to_unicode,
|
(re.compile(ur'&(\S+?);'), partial(entity_to_unicode,
|
||||||
exceptions=['lt', 'gt', 'amp'])),
|
exceptions=['lt', 'gt', 'amp'])),
|
||||||
@ -209,7 +213,6 @@ class HTMLConverter(object, LoggingInterface):
|
|||||||
'''
|
'''
|
||||||
# Defaults for various formatting tags
|
# Defaults for various formatting tags
|
||||||
object.__setattr__(self, 'options', options)
|
object.__setattr__(self, 'options', options)
|
||||||
LoggingInterface.__init__(self, logger)
|
|
||||||
self.fonts = fonts #: dict specifying font families to use
|
self.fonts = fonts #: dict specifying font families to use
|
||||||
# Memory
|
# Memory
|
||||||
self.scaled_images = {} #: Temporary files with scaled version of images
|
self.scaled_images = {} #: Temporary files with scaled version of images
|
||||||
|
@ -29,6 +29,7 @@ class LrsParser(object):
|
|||||||
self.logger = logger
|
self.logger = logger
|
||||||
src = stream.read()
|
src = stream.read()
|
||||||
self.soup = BeautifulStoneSoup(xml_to_unicode(src)[0],
|
self.soup = BeautifulStoneSoup(xml_to_unicode(src)[0],
|
||||||
|
convertEntities=BeautifulStoneSoup.XML_ENTITIES,
|
||||||
selfClosingTags=self.SELF_CLOSING_TAGS)
|
selfClosingTags=self.SELF_CLOSING_TAGS)
|
||||||
self.objects = {}
|
self.objects = {}
|
||||||
for obj in self.soup.findAll(objid=True):
|
for obj in self.soup.findAll(objid=True):
|
||||||
|
@ -530,7 +530,7 @@ class LRFMetaFile(object):
|
|||||||
""" See L{file.write} """
|
""" See L{file.write} """
|
||||||
self._file.write(val)
|
self._file.write(val)
|
||||||
|
|
||||||
def objects(self):
|
def _objects(self):
|
||||||
self._file.seek(self.object_index_offset)
|
self._file.seek(self.object_index_offset)
|
||||||
c = self.number_of_objects
|
c = self.number_of_objects
|
||||||
while c > 0:
|
while c > 0:
|
||||||
@ -543,7 +543,7 @@ class LRFMetaFile(object):
|
|||||||
def get_objects_by_type(self, type):
|
def get_objects_by_type(self, type):
|
||||||
from calibre.ebooks.lrf.tags import Tag
|
from calibre.ebooks.lrf.tags import Tag
|
||||||
objects = []
|
objects = []
|
||||||
for id, offset, size in self.objects():
|
for id, offset, size in self._objects():
|
||||||
self._file.seek(offset)
|
self._file.seek(offset)
|
||||||
tag = Tag(self._file)
|
tag = Tag(self._file)
|
||||||
if tag.id == 0xF500:
|
if tag.id == 0xF500:
|
||||||
@ -554,7 +554,7 @@ class LRFMetaFile(object):
|
|||||||
|
|
||||||
def get_object_by_id(self, tid):
|
def get_object_by_id(self, tid):
|
||||||
from calibre.ebooks.lrf.tags import Tag
|
from calibre.ebooks.lrf.tags import Tag
|
||||||
for id, offset, size in self.objects():
|
for id, offset, size in self._objects():
|
||||||
self._file.seek(offset)
|
self._file.seek(offset)
|
||||||
tag = Tag(self._file)
|
tag = Tag(self._file)
|
||||||
if tag.id == 0xF500:
|
if tag.id == 0xF500:
|
||||||
|
@ -112,7 +112,8 @@ key is the account key you generate after signing up for a free account from isb
|
|||||||
default=None, help=_('The title of the book to search for.'))
|
default=None, help=_('The title of the book to search for.'))
|
||||||
parser.add_option('-p', '--publisher', default=None, dest='publisher',
|
parser.add_option('-p', '--publisher', default=None, dest='publisher',
|
||||||
help=_('The publisher of the book to search for.'))
|
help=_('The publisher of the book to search for.'))
|
||||||
parser.add_option('--verbose', default=False, action='store_true', help=_('Verbose processing'))
|
parser.add_option('-v', '--verbose', default=False,
|
||||||
|
action='store_true', help=_('Verbose processing'))
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
@ -19,14 +19,22 @@ def get_metadata(stream):
|
|||||||
for item in opf.iterguide():
|
for item in opf.iterguide():
|
||||||
if 'cover' not in item.get('type', '').lower():
|
if 'cover' not in item.get('type', '').lower():
|
||||||
continue
|
continue
|
||||||
|
ctype = item.get('type')
|
||||||
href = item.get('href', '')
|
href = item.get('href', '')
|
||||||
candidates = [href, href.replace('&', '%26')]
|
candidates = [href, href.replace('&', '%26')]
|
||||||
for item in litfile.manifest.values():
|
for item in litfile.manifest.values():
|
||||||
if item.path in candidates:
|
if item.path in candidates:
|
||||||
covers.append(item.internal)
|
try:
|
||||||
|
covers.append((litfile.get_file('/data/'+item.internal),
|
||||||
|
ctype))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
break
|
break
|
||||||
covers = [litfile.get_file('/data/' + i) for i in covers]
|
covers.sort(cmp=lambda x, y:cmp(len(x[0]), len(y[0])), reverse=True)
|
||||||
covers.sort(cmp=lambda x, y:cmp(len(x), len(y)))
|
idx = 0
|
||||||
mi.cover_data = ('jpg', covers[-1])
|
if len(covers) > 1:
|
||||||
|
if covers[1][1] == covers[1][0]+'-standard':
|
||||||
|
idx = 1
|
||||||
|
mi.cover_data = ('jpg', covers[idx][0])
|
||||||
return mi
|
return mi
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ _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', 'azw'
|
||||||
]
|
]
|
||||||
|
|
||||||
# The priorities for loading metadata from different file types
|
# The priorities for loading metadata from different file types
|
||||||
@ -41,7 +41,9 @@ def metadata_from_formats(formats):
|
|||||||
for path, ext in zip(formats, extensions):
|
for path, ext in zip(formats, extensions):
|
||||||
with open(path, 'rb') as stream:
|
with open(path, 'rb') as stream:
|
||||||
try:
|
try:
|
||||||
mi.smart_update(get_metadata(stream, stream_type=ext, use_libprs_metadata=True))
|
newmi = get_metadata(stream, stream_type=ext,
|
||||||
|
use_libprs_metadata=True)
|
||||||
|
mi.smart_update(newmi)
|
||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
if getattr(mi, 'application_id', None) is not None:
|
if getattr(mi, 'application_id', None) is not None:
|
||||||
@ -58,7 +60,7 @@ def get_metadata(stream, stream_type='lrf', use_libprs_metadata=False):
|
|||||||
if stream_type: stream_type = stream_type.lower()
|
if stream_type: stream_type = stream_type.lower()
|
||||||
if stream_type in ('html', 'html', 'xhtml', 'xhtm', 'xml'):
|
if stream_type in ('html', 'html', 'xhtml', 'xhtm', 'xml'):
|
||||||
stream_type = 'html'
|
stream_type = 'html'
|
||||||
if stream_type in ('mobi', 'prc'):
|
if stream_type in ('mobi', 'prc', 'azw'):
|
||||||
stream_type = 'mobi'
|
stream_type = 'mobi'
|
||||||
if stream_type in ('odt', 'ods', 'odp', 'odg', 'odf'):
|
if stream_type in ('odt', 'ods', 'odp', 'odg', 'odf'):
|
||||||
stream_type = 'odt'
|
stream_type = 'odt'
|
||||||
|
@ -444,6 +444,7 @@ class OPF(object):
|
|||||||
if not hasattr(stream, 'read'):
|
if not hasattr(stream, 'read'):
|
||||||
stream = open(stream, 'rb')
|
stream = open(stream, 'rb')
|
||||||
self.basedir = self.base_dir = basedir
|
self.basedir = self.base_dir = basedir
|
||||||
|
self.path_to_html_toc = None
|
||||||
raw, self.encoding = xml_to_unicode(stream.read(), strip_encoding_pats=True, resolve_entities=True)
|
raw, self.encoding = xml_to_unicode(stream.read(), strip_encoding_pats=True, resolve_entities=True)
|
||||||
raw = raw[raw.find('<'):]
|
raw = raw[raw.find('<'):]
|
||||||
self.root = etree.fromstring(raw, self.PARSER)
|
self.root = etree.fromstring(raw, self.PARSER)
|
||||||
@ -495,6 +496,7 @@ class OPF(object):
|
|||||||
if f:
|
if f:
|
||||||
self.toc.read_ncx_toc(f[0])
|
self.toc.read_ncx_toc(f[0])
|
||||||
else:
|
else:
|
||||||
|
self.path_to_html_toc = toc
|
||||||
self.toc.read_html_toc(toc)
|
self.toc.read_html_toc(toc)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
40
src/calibre/ebooks/metadata/topaz.py
Normal file
40
src/calibre/ebooks/metadata/topaz.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
from __future__ import with_statement
|
||||||
|
__license__ = 'GPL 3'
|
||||||
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
''' Read metadata from Amazon's topaz format '''
|
||||||
|
|
||||||
|
def read_record(raw, name):
|
||||||
|
idx = raw.find(name)
|
||||||
|
if idx > -1:
|
||||||
|
length = ord(raw[idx+len(name)])
|
||||||
|
return raw[idx+len(name)+1:idx+len(name)+1+length]
|
||||||
|
|
||||||
|
def get_metadata(stream):
|
||||||
|
raw = stream.read(8*1024)
|
||||||
|
if not raw.startswith('TPZ'):
|
||||||
|
raise ValueError('Not a Topaz file')
|
||||||
|
first = raw.find('metadata')
|
||||||
|
if first < 0:
|
||||||
|
raise ValueError('Invalid Topaz file')
|
||||||
|
second = raw.find('metadata', first+10)
|
||||||
|
if second < 0:
|
||||||
|
raise ValueError('Invalid Topaz file')
|
||||||
|
raw = raw[second:second+1000]
|
||||||
|
authors = read_record(raw, 'Authors')
|
||||||
|
if authors:
|
||||||
|
authors = authors.decode('utf-8', 'replace').split(';')
|
||||||
|
else:
|
||||||
|
authors = [_('Unknown')]
|
||||||
|
title = read_record(raw, 'Title')
|
||||||
|
if title:
|
||||||
|
title = title.decode('utf-8', 'replace')
|
||||||
|
else:
|
||||||
|
raise ValueError('No metadata in file')
|
||||||
|
from calibre.ebooks.metadata import MetaInformation
|
||||||
|
return MetaInformation(title, authors)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
print get_metadata(open(sys.argv[1], 'rb'))
|
@ -3,8 +3,6 @@ __license__ = 'GPL 3'
|
|||||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from calibre.customize.conversion import InputFormatPlugin
|
from calibre.customize.conversion import InputFormatPlugin
|
||||||
|
|
||||||
class MOBIInput(InputFormatPlugin):
|
class MOBIInput(InputFormatPlugin):
|
||||||
@ -14,16 +12,19 @@ class MOBIInput(InputFormatPlugin):
|
|||||||
description = 'Convert MOBI files (.mobi, .prc, .azw) to HTML'
|
description = 'Convert MOBI files (.mobi, .prc, .azw) to HTML'
|
||||||
file_types = set(['mobi', 'prc', 'azw'])
|
file_types = set(['mobi', 'prc', 'azw'])
|
||||||
|
|
||||||
def convert(self, stream, options, file_ext, parse_cache, log):
|
def convert(self, stream, options, file_ext, parse_cache, log,
|
||||||
|
accelerators):
|
||||||
from calibre.ebooks.mobi.reader import MobiReader
|
from calibre.ebooks.mobi.reader import MobiReader
|
||||||
mr = MobiReader(stream, log, options.input_encoding,
|
mr = MobiReader(stream, log, options.input_encoding,
|
||||||
options.debug_input)
|
options.debug_input)
|
||||||
mr.extract_content(output_dir=os.getcwdu(), parse_cache)
|
mr.extract_content('.', parse_cache)
|
||||||
raw = parse_cache.get('calibre_raw_mobi_markup', False)
|
raw = parse_cache.get('calibre_raw_mobi_markup', False)
|
||||||
if raw:
|
if raw:
|
||||||
if isinstance(raw, unicode):
|
if isinstance(raw, unicode):
|
||||||
raw = raw.encode('utf-8')
|
raw = raw.encode('utf-8')
|
||||||
open('debug-raw.html', 'wb').write(raw)
|
open('debug-raw.html', 'wb').write(raw)
|
||||||
|
for f, root in parse_cache.items():
|
||||||
|
if '.' in f:
|
||||||
|
accelerators[f] = {'pagebreaks':root.xpath(
|
||||||
|
'//div[@class="mbp_pagebreak"]')}
|
||||||
return mr.created_opf_path
|
return mr.created_opf_path
|
||||||
|
|
||||||
|
@ -92,6 +92,8 @@ class BookHeader(object):
|
|||||||
self.sublanguage = 'NEUTRAL'
|
self.sublanguage = 'NEUTRAL'
|
||||||
self.exth_flag, self.exth = 0, None
|
self.exth_flag, self.exth = 0, None
|
||||||
self.ancient = True
|
self.ancient = True
|
||||||
|
self.first_image_index = -1
|
||||||
|
self.mobi_version = 1
|
||||||
else:
|
else:
|
||||||
self.ancient = False
|
self.ancient = False
|
||||||
self.doctype = raw[16:20]
|
self.doctype = raw[16:20]
|
||||||
@ -312,7 +314,7 @@ class MobiReader(object):
|
|||||||
mobi_version = self.book_header.mobi_version
|
mobi_version = self.book_header.mobi_version
|
||||||
for i, tag in enumerate(root.iter(etree.Element)):
|
for i, tag in enumerate(root.iter(etree.Element)):
|
||||||
if tag.tag in ('country-region', 'place', 'placetype', 'placename',
|
if tag.tag in ('country-region', 'place', 'placetype', 'placename',
|
||||||
'state', 'city'):
|
'state', 'city', 'street', 'address'):
|
||||||
tag.tag = 'span'
|
tag.tag = 'span'
|
||||||
for key in tag.attrib.keys():
|
for key in tag.attrib.keys():
|
||||||
tag.attrib.pop(key)
|
tag.attrib.pop(key)
|
||||||
@ -389,6 +391,12 @@ class MobiReader(object):
|
|||||||
opf.cover = 'images/%05d.jpg'%(self.book_header.exth.cover_offset+1)
|
opf.cover = 'images/%05d.jpg'%(self.book_header.exth.cover_offset+1)
|
||||||
elif mi.cover is not None:
|
elif mi.cover is not None:
|
||||||
opf.cover = mi.cover
|
opf.cover = mi.cover
|
||||||
|
else:
|
||||||
|
opf.cover = 'images/%05d.jpg'%1
|
||||||
|
if not os.path.exists(os.path.join(os.path.dirname(htmlfile),
|
||||||
|
*opf.cover.split('/'))):
|
||||||
|
opf.cover = None
|
||||||
|
|
||||||
manifest = [(htmlfile, 'text/x-oeb1-document'),
|
manifest = [(htmlfile, 'text/x-oeb1-document'),
|
||||||
(os.path.abspath('styles.css'), 'text/css')]
|
(os.path.abspath('styles.css'), 'text/css')]
|
||||||
bp = os.path.dirname(htmlfile)
|
bp = os.path.dirname(htmlfile)
|
||||||
@ -531,7 +539,7 @@ class MobiReader(object):
|
|||||||
os.makedirs(output_dir)
|
os.makedirs(output_dir)
|
||||||
image_index = 0
|
image_index = 0
|
||||||
self.image_names = []
|
self.image_names = []
|
||||||
start = self.book_header.first_image_index
|
start = getattr(self.book_header, 'first_image_index', -1)
|
||||||
if start > self.num_sections or start < 0:
|
if start > self.num_sections or start < 0:
|
||||||
# BAEN PRC files have bad headers
|
# BAEN PRC files have bad headers
|
||||||
start=0
|
start=0
|
||||||
|
@ -9,7 +9,6 @@ __copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.cam>'
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from struct import pack
|
from struct import pack
|
||||||
import functools
|
|
||||||
import time
|
import time
|
||||||
import random
|
import random
|
||||||
from cStringIO import StringIO
|
from cStringIO import StringIO
|
||||||
@ -18,13 +17,12 @@ from itertools import izip, count
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from urlparse import urldefrag
|
from urlparse import urldefrag
|
||||||
import logging
|
import logging
|
||||||
from lxml import etree
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from calibre.ebooks.oeb.base import XML_NS, XHTML, XHTML_NS, OEB_DOCS, \
|
from calibre.ebooks.oeb.base import XML_NS, XHTML, XHTML_NS, OEB_DOCS, \
|
||||||
OEB_RASTER_IMAGES
|
OEB_RASTER_IMAGES
|
||||||
from calibre.ebooks.oeb.base import xpath, barename, namespace, prefixname
|
from calibre.ebooks.oeb.base import namespace, prefixname
|
||||||
from calibre.ebooks.oeb.base import urlnormalize
|
from calibre.ebooks.oeb.base import urlnormalize
|
||||||
from calibre.ebooks.oeb.base import Logger, OEBBook
|
from calibre.ebooks.oeb.base import OEBBook
|
||||||
from calibre.ebooks.oeb.profile import Context
|
from calibre.ebooks.oeb.profile import Context
|
||||||
from calibre.ebooks.oeb.transforms.flatcss import CSSFlattener
|
from calibre.ebooks.oeb.transforms.flatcss import CSSFlattener
|
||||||
from calibre.ebooks.oeb.transforms.rasterize import SVGRasterizer
|
from calibre.ebooks.oeb.transforms.rasterize import SVGRasterizer
|
||||||
@ -33,7 +31,7 @@ from calibre.ebooks.oeb.transforms.htmltoc import HTMLTOCAdder
|
|||||||
from calibre.ebooks.oeb.transforms.manglecase import CaseMangler
|
from calibre.ebooks.oeb.transforms.manglecase import CaseMangler
|
||||||
from calibre.ebooks.mobi.palmdoc import compress_doc
|
from calibre.ebooks.mobi.palmdoc import compress_doc
|
||||||
from calibre.ebooks.mobi.langcodes import iana2mobi
|
from calibre.ebooks.mobi.langcodes import iana2mobi
|
||||||
from calibre.ebooks.mobi.mobiml import MBP_NS, MBP, MobiMLizer
|
from calibre.ebooks.mobi.mobiml import MBP_NS, MobiMLizer
|
||||||
from calibre.customize.ui import run_plugins_on_postprocess
|
from calibre.customize.ui import run_plugins_on_postprocess
|
||||||
from calibre.utils.config import Config, StringConfig
|
from calibre.utils.config import Config, StringConfig
|
||||||
|
|
||||||
@ -162,7 +160,7 @@ class Serializer(object):
|
|||||||
hrefs = self.oeb.manifest.hrefs
|
hrefs = self.oeb.manifest.hrefs
|
||||||
buffer.write('<guide>')
|
buffer.write('<guide>')
|
||||||
for ref in self.oeb.guide.values():
|
for ref in self.oeb.guide.values():
|
||||||
path, frag = urldefrag(ref.href)
|
path = urldefrag(ref.href)[0]
|
||||||
if hrefs[path].media_type not in OEB_DOCS:
|
if hrefs[path].media_type not in OEB_DOCS:
|
||||||
continue
|
continue
|
||||||
buffer.write('<reference type="')
|
buffer.write('<reference type="')
|
||||||
@ -457,8 +455,6 @@ class MobiWriter(object):
|
|||||||
self._oeb.logger.info('Serializing images...')
|
self._oeb.logger.info('Serializing images...')
|
||||||
images = [(index, href) for href, index in self._images.items()]
|
images = [(index, href) for href, index in self._images.items()]
|
||||||
images.sort()
|
images.sort()
|
||||||
metadata = self._oeb.metadata
|
|
||||||
coverid = metadata.cover[0] if metadata.cover else None
|
|
||||||
for _, href in images:
|
for _, href in images:
|
||||||
item = self._oeb.manifest.hrefs[href]
|
item = self._oeb.manifest.hrefs[href]
|
||||||
try:
|
try:
|
||||||
|
@ -7,7 +7,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>'
|
__copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import os, sys, re, uuid
|
import os, re, uuid
|
||||||
from mimetypes import types_map
|
from mimetypes import types_map
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from itertools import count
|
from itertools import count
|
||||||
@ -15,7 +15,6 @@ from urlparse import urldefrag, urlparse, urlunparse
|
|||||||
from urllib import unquote as urlunquote
|
from urllib import unquote as urlunquote
|
||||||
from lxml import etree, html
|
from lxml import etree, html
|
||||||
import calibre
|
import calibre
|
||||||
from calibre import LoggingInterface
|
|
||||||
from calibre.translations.dynamic import translate
|
from calibre.translations.dynamic import translate
|
||||||
from calibre.ebooks.chardet import xml_to_unicode
|
from calibre.ebooks.chardet import xml_to_unicode
|
||||||
from calibre.ebooks.oeb.entitydefs import ENTITYDEFS
|
from calibre.ebooks.oeb.entitydefs import ENTITYDEFS
|
||||||
@ -204,22 +203,6 @@ class OEBError(Exception):
|
|||||||
"""Generic OEB-processing error."""
|
"""Generic OEB-processing error."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class FauxLogger(object):
|
|
||||||
"""Fake logging interface."""
|
|
||||||
def __getattr__(self, name):
|
|
||||||
return self
|
|
||||||
def __call__(self, message):
|
|
||||||
print message
|
|
||||||
|
|
||||||
class Logger(LoggingInterface, object):
|
|
||||||
"""A logging object which provides both the standard `logging.Logger` and
|
|
||||||
calibre-specific interfaces.
|
|
||||||
"""
|
|
||||||
def __getattr__(self, name):
|
|
||||||
return object.__getattribute__(self, 'log_' + name)
|
|
||||||
|
|
||||||
|
|
||||||
class NullContainer(object):
|
class NullContainer(object):
|
||||||
"""An empty container.
|
"""An empty container.
|
||||||
|
|
||||||
@ -1233,16 +1216,20 @@ class PageList(object):
|
|||||||
class OEBBook(object):
|
class OEBBook(object):
|
||||||
"""Representation of a book in the IDPF OEB data model."""
|
"""Representation of a book in the IDPF OEB data model."""
|
||||||
|
|
||||||
def __init__(self, encoding=None, pretty_print=False, logger=FauxLogger()):
|
def __init__(self, logger, parse_cache={}, encoding='utf-8',
|
||||||
|
pretty_print=False):
|
||||||
"""Create empty book. Optional arguments:
|
"""Create empty book. Optional arguments:
|
||||||
|
|
||||||
|
:param parse_cache: A cache of parsed XHTML/CSS. Keys are absolute
|
||||||
|
paths to te cached files and values are lxml root objects and
|
||||||
|
cssutils stylesheets.
|
||||||
:param:`encoding`: Default encoding for textual content read
|
:param:`encoding`: Default encoding for textual content read
|
||||||
from an external container.
|
from an external container.
|
||||||
:param:`pretty_print`: Whether or not the canonical string form
|
:param:`pretty_print`: Whether or not the canonical string form
|
||||||
of XML markup is pretty-printed.
|
of XML markup is pretty-printed.
|
||||||
:prama:`logger`: A Logger object to use for logging all messages
|
:param:`logger`: A Log object to use for logging all messages
|
||||||
related to the processing of this book. It is accessible
|
related to the processing of this book. It is accessible
|
||||||
via the instance data member :attr:`logger`.
|
via the instance data members :attr:`logger,log`.
|
||||||
|
|
||||||
It provides the following public instance data members for
|
It provides the following public instance data members for
|
||||||
accessing various parts of the OEB data model:
|
accessing various parts of the OEB data model:
|
||||||
@ -1260,7 +1247,7 @@ class OEBBook(object):
|
|||||||
"""
|
"""
|
||||||
self.encoding = encoding
|
self.encoding = encoding
|
||||||
self.pretty_print = pretty_print
|
self.pretty_print = pretty_print
|
||||||
self.logger = logger
|
self.logger = self.log = logger
|
||||||
self.version = '2.0'
|
self.version = '2.0'
|
||||||
self.container = NullContainer()
|
self.container = NullContainer()
|
||||||
self.metadata = Metadata(self)
|
self.metadata = Metadata(self)
|
||||||
|
17
src/calibre/ebooks/oeb/output.py
Normal file
17
src/calibre/ebooks/oeb/output.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from __future__ import with_statement
|
||||||
|
__license__ = 'GPL 3'
|
||||||
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
from calibre.customize.conversion import OutputFormatPlugin
|
||||||
|
|
||||||
|
class OEBOutput(OutputFormatPlugin):
|
||||||
|
|
||||||
|
name = 'OEB Output'
|
||||||
|
author = 'Kovid Goyal'
|
||||||
|
file_type = 'oeb'
|
||||||
|
|
||||||
|
|
||||||
|
def convert(self, oeb_book, input_plugin, options, parse_cache, log):
|
||||||
|
pass
|
||||||
|
|
@ -19,9 +19,9 @@ from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES, OEB_IMAGES, \
|
|||||||
PAGE_MAP_MIME, JPEG_MIME, NCX_MIME, SVG_MIME
|
PAGE_MAP_MIME, JPEG_MIME, NCX_MIME, SVG_MIME
|
||||||
from calibre.ebooks.oeb.base import XMLDECL_RE, COLLAPSE_RE, CSSURL_RE, \
|
from calibre.ebooks.oeb.base import XMLDECL_RE, COLLAPSE_RE, CSSURL_RE, \
|
||||||
ENTITY_RE, LINK_SELECTORS, MS_COVER_TYPE
|
ENTITY_RE, LINK_SELECTORS, MS_COVER_TYPE
|
||||||
from calibre.ebooks.oeb.base import namespace, barename, qname, XPath, xpath
|
from calibre.ebooks.oeb.base import namespace, barename, qname, XPath, xpath, \
|
||||||
from calibre.ebooks.oeb.base import urlnormalize, xml2str
|
urlnormalize, BINARY_MIME, \
|
||||||
from calibre.ebooks.oeb.base import OEBError, OEBBook, DirContainer
|
OEBError, OEBBook, DirContainer
|
||||||
from calibre.ebooks.oeb.writer import OEBWriter
|
from calibre.ebooks.oeb.writer import OEBWriter
|
||||||
from calibre.ebooks.oeb.entitydefs import ENTITYDEFS
|
from calibre.ebooks.oeb.entitydefs import ENTITYDEFS
|
||||||
from calibre.ebooks.metadata.epub import CoverRenderer
|
from calibre.ebooks.metadata.epub import CoverRenderer
|
||||||
@ -45,9 +45,6 @@ class OEBReader(object):
|
|||||||
TRANSFORMS = []
|
TRANSFORMS = []
|
||||||
"""List of transforms to apply to content read with this Reader."""
|
"""List of transforms to apply to content read with this Reader."""
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
return
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def config(cls, cfg):
|
def config(cls, cfg):
|
||||||
"""Add any book-reading options to the :class:`Config` object
|
"""Add any book-reading options to the :class:`Config` object
|
||||||
@ -65,7 +62,7 @@ class OEBReader(object):
|
|||||||
:param:`oeb`.
|
:param:`oeb`.
|
||||||
"""
|
"""
|
||||||
self.oeb = oeb
|
self.oeb = oeb
|
||||||
self.logger = oeb.logger
|
self.logger = self.log = oeb.logger
|
||||||
oeb.container = self.Container(path)
|
oeb.container = self.Container(path)
|
||||||
opf = self._read_opf()
|
opf = self._read_opf()
|
||||||
self._all_from_opf(opf)
|
self._all_from_opf(opf)
|
||||||
|
@ -19,7 +19,7 @@ from cssutils.css import CSSStyleRule, CSSPageRule, CSSStyleDeclaration, \
|
|||||||
CSSValueList, cssproperties
|
CSSValueList, cssproperties
|
||||||
from cssutils.profiles import profiles as cssprofiles
|
from cssutils.profiles import profiles as cssprofiles
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
from lxml.cssselect import css_to_xpath, ExpressionError
|
from lxml.cssselect import css_to_xpath, ExpressionError, SelectorSyntaxError
|
||||||
from calibre.ebooks.oeb.base import XHTML, XHTML_NS, CSS_MIME, OEB_STYLES
|
from calibre.ebooks.oeb.base import XHTML, XHTML_NS, CSS_MIME, OEB_STYLES
|
||||||
from calibre.ebooks.oeb.base import XPNSMAP, xpath, urlnormalize
|
from calibre.ebooks.oeb.base import XPNSMAP, xpath, urlnormalize
|
||||||
from calibre.ebooks.oeb.profile import PROFILES
|
from calibre.ebooks.oeb.profile import PROFILES
|
||||||
@ -159,7 +159,9 @@ class Stylizer(object):
|
|||||||
for _, _, cssdict, text, _ in rules:
|
for _, _, cssdict, text, _ in rules:
|
||||||
try:
|
try:
|
||||||
selector = CSSSelector(text)
|
selector = CSSSelector(text)
|
||||||
except ExpressionError:
|
except (AssertionError, ExpressionError, etree.XPathSyntaxError,\
|
||||||
|
NameError, # gets thrown on OS X instead of SelectorSyntaxError
|
||||||
|
SelectorSyntaxError):
|
||||||
continue
|
continue
|
||||||
for elem in selector(tree):
|
for elem in selector(tree):
|
||||||
self.style(elem)._update_cssdict(cssdict)
|
self.style(elem)._update_cssdict(cssdict)
|
||||||
|
@ -6,18 +6,14 @@ from __future__ import with_statement
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>'
|
__copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>'
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
import operator
|
import operator
|
||||||
import math
|
import math
|
||||||
from itertools import chain
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
from calibre.ebooks.oeb.base import XHTML, XHTML_NS
|
from calibre.ebooks.oeb.base import XHTML, XHTML_NS
|
||||||
from calibre.ebooks.oeb.base import CSS_MIME, OEB_STYLES
|
from calibre.ebooks.oeb.base import CSS_MIME, OEB_STYLES
|
||||||
from calibre.ebooks.oeb.base import namespace, barename
|
from calibre.ebooks.oeb.base import namespace, barename
|
||||||
from calibre.ebooks.oeb.base import OEBBook
|
|
||||||
from calibre.ebooks.oeb.stylizer import Stylizer
|
from calibre.ebooks.oeb.stylizer import Stylizer
|
||||||
|
|
||||||
COLLAPSE = re.compile(r'[ \t\r\n\v]+')
|
COLLAPSE = re.compile(r'[ \t\r\n\v]+')
|
||||||
|
@ -6,9 +6,6 @@ from __future__ import with_statement
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>'
|
__copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>'
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
from lxml import etree
|
|
||||||
from calibre.ebooks.oeb.base import XML, XHTML, XHTML_NS
|
from calibre.ebooks.oeb.base import XML, XHTML, XHTML_NS
|
||||||
from calibre.ebooks.oeb.base import XHTML_MIME, CSS_MIME
|
from calibre.ebooks.oeb.base import XHTML_MIME, CSS_MIME
|
||||||
from calibre.ebooks.oeb.base import element
|
from calibre.ebooks.oeb.base import element
|
||||||
@ -66,6 +63,8 @@ class HTMLTOCAdder(object):
|
|||||||
def __call__(self, oeb, context):
|
def __call__(self, oeb, context):
|
||||||
if 'toc' in oeb.guide:
|
if 'toc' in oeb.guide:
|
||||||
return
|
return
|
||||||
|
if not getattr(getattr(oeb, 'toc', False), 'nodes', False):
|
||||||
|
return
|
||||||
oeb.logger.info('Generating in-line TOC...')
|
oeb.logger.info('Generating in-line TOC...')
|
||||||
title = self.title or oeb.translate(DEFAULT_TITLE)
|
title = self.title or oeb.translate(DEFAULT_TITLE)
|
||||||
style = self.style
|
style = self.style
|
||||||
|
@ -6,13 +6,6 @@ from __future__ import with_statement
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>'
|
__copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>'
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import operator
|
|
||||||
import math
|
|
||||||
from itertools import chain
|
|
||||||
from collections import defaultdict
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
from calibre.ebooks.oeb.base import XHTML, XHTML_NS
|
from calibre.ebooks.oeb.base import XHTML, XHTML_NS
|
||||||
from calibre.ebooks.oeb.base import CSS_MIME
|
from calibre.ebooks.oeb.base import CSS_MIME
|
||||||
|
@ -6,7 +6,6 @@ from __future__ import with_statement
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>'
|
__copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>'
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
from urlparse import urldefrag
|
from urlparse import urldefrag
|
||||||
import base64
|
import base64
|
||||||
@ -20,9 +19,9 @@ from PyQt4.QtGui import QImage
|
|||||||
from PyQt4.QtGui import QPainter
|
from PyQt4.QtGui import QPainter
|
||||||
from PyQt4.QtSvg import QSvgRenderer
|
from PyQt4.QtSvg import QSvgRenderer
|
||||||
from PyQt4.QtGui import QApplication
|
from PyQt4.QtGui import QApplication
|
||||||
from calibre.ebooks.oeb.base import XHTML_NS, XHTML, SVG_NS, SVG, XLINK
|
from calibre.ebooks.oeb.base import XHTML, XLINK
|
||||||
from calibre.ebooks.oeb.base import SVG_MIME, PNG_MIME, JPEG_MIME
|
from calibre.ebooks.oeb.base import SVG_MIME, PNG_MIME
|
||||||
from calibre.ebooks.oeb.base import xml2str, xpath, namespace, barename
|
from calibre.ebooks.oeb.base import xml2str, xpath
|
||||||
from calibre.ebooks.oeb.base import urlnormalize
|
from calibre.ebooks.oeb.base import urlnormalize
|
||||||
from calibre.ebooks.oeb.stylizer import Stylizer
|
from calibre.ebooks.oeb.stylizer import Stylizer
|
||||||
|
|
||||||
@ -88,7 +87,7 @@ class SVGRasterizer(object):
|
|||||||
hrefs = self.oeb.manifest.hrefs
|
hrefs = self.oeb.manifest.hrefs
|
||||||
for elem in xpath(svg, '//svg:*[@xl:href]'):
|
for elem in xpath(svg, '//svg:*[@xl:href]'):
|
||||||
href = urlnormalize(elem.attrib[XLINK('href')])
|
href = urlnormalize(elem.attrib[XLINK('href')])
|
||||||
path, frag = urldefrag(href)
|
path = urldefrag(href)[0]
|
||||||
if not path:
|
if not path:
|
||||||
continue
|
continue
|
||||||
abshref = item.abshref(path)
|
abshref = item.abshref(path)
|
||||||
|
@ -224,7 +224,10 @@ class AddRecursive(Add):
|
|||||||
files = _('<p>Books with the same title as the following already '
|
files = _('<p>Books with the same title as the following already '
|
||||||
'exist in the database. Add them anyway?<ul>')
|
'exist in the database. Add them anyway?<ul>')
|
||||||
for mi in self.duplicates:
|
for mi in self.duplicates:
|
||||||
files += '<li>'+mi[0].title+'</li>\n'
|
title = mi[0].title
|
||||||
|
if not isinstance(title, unicode):
|
||||||
|
title = title.decode(preferred_encoding, 'replace')
|
||||||
|
files += '<li>'+title+'</li>\n'
|
||||||
d = WarningDialog (_('Duplicates found!'),
|
d = WarningDialog (_('Duplicates found!'),
|
||||||
_('Duplicates found!'),
|
_('Duplicates found!'),
|
||||||
files+'</ul></p>', parent=self._parent)
|
files+'</ul></p>', parent=self._parent)
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<ui version="4.0">
|
<ui version="4.0">
|
||||||
<class>Dialog</class>
|
<class>Dialog</class>
|
||||||
<widget class="QDialog" name="Dialog">
|
<widget class="QDialog" name="Dialog">
|
||||||
@ -6,7 +7,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>646</width>
|
<width>646</width>
|
||||||
<height>468</height>
|
<height>503</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
@ -100,21 +101,21 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="8" column="0" >
|
<item row="9" column="0">
|
||||||
<widget class="QCheckBox" name="opt_landscape">
|
<widget class="QCheckBox" name="opt_landscape">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>&Landscape</string>
|
<string>&Landscape</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="10" column="0" >
|
<item row="11" column="0">
|
||||||
<widget class="QCheckBox" name="opt_no_sort">
|
<widget class="QCheckBox" name="opt_no_sort">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Don't so&rt</string>
|
<string>Don't so&rt</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="12" column="1" >
|
<item row="13" column="1">
|
||||||
<widget class="QDialogButtonBox" name="buttonBox">
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Horizontal</enum>
|
<enum>Qt::Horizontal</enum>
|
||||||
@ -124,27 +125,34 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="9" column="0" >
|
<item row="10" column="0">
|
||||||
<widget class="QCheckBox" name="opt_right2left">
|
<widget class="QCheckBox" name="opt_right2left">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>&Right to left</string>
|
<string>&Right to left</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="11" column="0" >
|
<item row="12" column="0">
|
||||||
<widget class="QCheckBox" name="opt_despeckle">
|
<widget class="QCheckBox" name="opt_despeckle">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>De&speckle</string>
|
<string>De&speckle</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="7" column="0" >
|
<item row="8" column="0">
|
||||||
<widget class="QCheckBox" name="opt_wide">
|
<widget class="QCheckBox" name="opt_wide">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>&Wide</string>
|
<string>&Wide</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="7" column="0">
|
||||||
|
<widget class="QCheckBox" name="opt_disable_trim">
|
||||||
|
<property name="text">
|
||||||
|
<string>Disable &Trimming</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<resources>
|
<resources>
|
||||||
|
@ -196,7 +196,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
|||||||
self.language.addItem(language_codes[lang], QVariant(lang))
|
self.language.addItem(language_codes[lang], QVariant(lang))
|
||||||
else:
|
else:
|
||||||
lang = 'en'
|
lang = 'en'
|
||||||
self.language.addItem('English', 'en')
|
self.language.addItem('English', QVariant('en'))
|
||||||
items = [(l, language_codes[l]) for l in translations.keys() \
|
items = [(l, language_codes[l]) for l in translations.keys() \
|
||||||
if l != lang]
|
if l != lang]
|
||||||
if lang != 'en':
|
if lang != 'en':
|
||||||
|
@ -346,19 +346,22 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
pix = QPixmap()
|
pix = QPixmap()
|
||||||
pix.loadFromData(cover_data)
|
pix.loadFromData(cover_data)
|
||||||
if pix.isNull():
|
if pix.isNull():
|
||||||
error_dialog(self.window, "The cover is not a valid picture").exec_()
|
error_dialog(self.window, _('Bad cover'),
|
||||||
|
_('The cover is not a valid picture')).exec_()
|
||||||
else:
|
else:
|
||||||
self.cover.setPixmap(pix)
|
self.cover.setPixmap(pix)
|
||||||
self.cover_changed = True
|
self.cover_changed = True
|
||||||
self.cpixmap = pix
|
self.cpixmap = pix
|
||||||
except LibraryThingError, err:
|
except LibraryThingError, err:
|
||||||
error_dialog(self, _('Could not fetch cover'), _('<b>Could not fetch cover.</b><br/>')+repr(err)).exec_()
|
error_dialog(self, _('Cannot fetch cover'),
|
||||||
|
_('<b>Could not fetch cover.</b><br/>')+repr(err)).exec_()
|
||||||
finally:
|
finally:
|
||||||
self.fetch_cover_button.setEnabled(True)
|
self.fetch_cover_button.setEnabled(True)
|
||||||
self.unsetCursor()
|
self.unsetCursor()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
error_dialog(self, _('Cannot fetch cover'), _('You must specify the ISBN identifier for this book.')).exec_()
|
error_dialog(self, _('Cannot fetch cover'),
|
||||||
|
_('You must specify the ISBN identifier for this book.')).exec_()
|
||||||
|
|
||||||
|
|
||||||
def fetch_metadata(self):
|
def fetch_metadata(self):
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<ui version="4.0">
|
<ui version="4.0">
|
||||||
<class>MetadataSingleDialog</class>
|
<class>MetadataSingleDialog</class>
|
||||||
<widget class="QDialog" name="MetadataSingleDialog">
|
<widget class="QDialog" name="MetadataSingleDialog">
|
||||||
@ -10,7 +11,7 @@
|
|||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy vsizetype="MinimumExpanding" hsizetype="MinimumExpanding" >
|
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
|
||||||
<horstretch>0</horstretch>
|
<horstretch>0</horstretch>
|
||||||
<verstretch>0</verstretch>
|
<verstretch>0</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
@ -51,7 +52,7 @@
|
|||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QWidget" native="1" name="central_widget" >
|
<widget class="QWidget" name="central_widget" native="true">
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
<size>
|
<size>
|
||||||
<width>800</width>
|
<width>800</width>
|
||||||
@ -92,7 +93,7 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item rowspan="2" row="0" column="2" >
|
<item row="0" column="2" rowspan="2">
|
||||||
<widget class="QToolButton" name="swap_button">
|
<widget class="QToolButton" name="swap_button">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>Swap the author and title</string>
|
<string>Swap the author and title</string>
|
||||||
@ -226,7 +227,7 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="QLineEdit" name="tags">
|
<widget class="QLineEdit" name="tags">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas.</string>
|
<string>Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas.</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@ -270,7 +271,7 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="QComboBox" name="series">
|
<widget class="QComboBox" name="series">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy vsizetype="Fixed" hsizetype="MinimumExpanding" >
|
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
|
||||||
<horstretch>0</horstretch>
|
<horstretch>0</horstretch>
|
||||||
<verstretch>0</verstretch>
|
<verstretch>0</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
@ -374,7 +375,7 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="fetch_metadata_button">
|
<widget class="QPushButton" name="fetch_metadata_button">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Fetch metadata from server</string>
|
<string>&Fetch metadata from server</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@ -385,7 +386,7 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="QGroupBox" name="af_group_box">
|
<widget class="QGroupBox" name="af_group_box">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy vsizetype="Minimum" hsizetype="Preferred" >
|
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
|
||||||
<horstretch>0</horstretch>
|
<horstretch>0</horstretch>
|
||||||
<verstretch>0</verstretch>
|
<verstretch>0</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
@ -396,10 +397,10 @@
|
|||||||
<layout class="QVBoxLayout" name="verticalLayout">
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
<item>
|
<item>
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
<item rowspan="3" row="0" column="0" >
|
<item row="0" column="0" rowspan="3">
|
||||||
<widget class="QListWidget" name="formats">
|
<widget class="QListWidget" name="formats">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy vsizetype="Minimum" hsizetype="Minimum" >
|
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
||||||
<horstretch>0</horstretch>
|
<horstretch>0</horstretch>
|
||||||
<verstretch>0</verstretch>
|
<verstretch>0</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
@ -487,7 +488,7 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="QGroupBox" name="bc_box">
|
<widget class="QGroupBox" name="bc_box">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy vsizetype="Expanding" hsizetype="Preferred" >
|
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||||
<horstretch>0</horstretch>
|
<horstretch>0</horstretch>
|
||||||
<verstretch>10</verstretch>
|
<verstretch>10</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
@ -499,7 +500,7 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="ImageView" name="cover">
|
<widget class="ImageView" name="cover">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
|
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||||
<horstretch>0</horstretch>
|
<horstretch>0</horstretch>
|
||||||
<verstretch>0</verstretch>
|
<verstretch>0</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
@ -588,7 +589,7 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="fetch_cover_button">
|
<widget class="QPushButton" name="fetch_cover_button">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Fetch cover image from server</string>
|
<string>Fetch &cover image from server</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@ -598,7 +599,7 @@
|
|||||||
<string>Change the username and/or password for your account at LibraryThing.com</string>
|
<string>Change the username and/or password for your account at LibraryThing.com</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Change password</string>
|
<string>Change &password</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
BIN
src/calibre/gui2/images/news/nytimes_sub.png
Normal file
BIN
src/calibre/gui2/images/news/nytimes_sub.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 866 B |
BIN
src/calibre/gui2/images/news/politico.png
Normal file
BIN
src/calibre/gui2/images/news/politico.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 572 B |
BIN
src/calibre/gui2/images/news/wikinews_en.png
Normal file
BIN
src/calibre/gui2/images/news/wikinews_en.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 951 B |
@ -94,6 +94,7 @@ class DateDelegate(QStyledItemDelegate):
|
|||||||
def createEditor(self, parent, option, index):
|
def createEditor(self, parent, option, index):
|
||||||
qde = QStyledItemDelegate.createEditor(self, parent, option, index)
|
qde = QStyledItemDelegate.createEditor(self, parent, option, index)
|
||||||
qde.setDisplayFormat('MM/dd/yyyy')
|
qde.setDisplayFormat('MM/dd/yyyy')
|
||||||
|
qde.setMinimumDate(QDate(100,1,1))
|
||||||
return qde
|
return qde
|
||||||
|
|
||||||
class BooksModel(QAbstractTableModel):
|
class BooksModel(QAbstractTableModel):
|
||||||
@ -420,6 +421,7 @@ class BooksModel(QAbstractTableModel):
|
|||||||
def get_preferred_formats(self, rows, formats, paths=False,
|
def get_preferred_formats(self, rows, formats, paths=False,
|
||||||
set_metadata=False, specific_format=None):
|
set_metadata=False, specific_format=None):
|
||||||
ans = []
|
ans = []
|
||||||
|
need_auto = []
|
||||||
if specific_format is not None:
|
if specific_format is not None:
|
||||||
formats = [specific_format.lower()]
|
formats = [specific_format.lower()]
|
||||||
for row in (row.row() for row in rows):
|
for row in (row.row() for row in rows):
|
||||||
@ -444,8 +446,9 @@ class BooksModel(QAbstractTableModel):
|
|||||||
pt.close() if paths else pt.seek(0)
|
pt.close() if paths else pt.seek(0)
|
||||||
ans.append(pt)
|
ans.append(pt)
|
||||||
else:
|
else:
|
||||||
|
need_auto.append(row)
|
||||||
ans.append(None)
|
ans.append(None)
|
||||||
return ans
|
return ans, need_auto
|
||||||
|
|
||||||
def id(self, row):
|
def id(self, row):
|
||||||
return self.db.id(getattr(row, 'row', lambda:row)())
|
return self.db.id(getattr(row, 'row', lambda:row)())
|
||||||
@ -1069,3 +1072,4 @@ class SearchBox(QLineEdit):
|
|||||||
self.emit(SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), txt, False)
|
self.emit(SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), txt, False)
|
||||||
self.end(False)
|
self.end(False)
|
||||||
self.initial_state = False
|
self.initial_state = False
|
||||||
|
|
||||||
|
@ -38,7 +38,8 @@ from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog
|
|||||||
from calibre.gui2.dialogs.jobs import JobsDialog
|
from calibre.gui2.dialogs.jobs import JobsDialog
|
||||||
from calibre.gui2.dialogs.conversion_error import ConversionErrorDialog
|
from calibre.gui2.dialogs.conversion_error import ConversionErrorDialog
|
||||||
from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebooks, \
|
from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebooks, \
|
||||||
set_conversion_defaults, fetch_scheduled_recipe
|
set_conversion_defaults, fetch_scheduled_recipe, \
|
||||||
|
auto_convert_ebook
|
||||||
from calibre.gui2.dialogs.config import ConfigDialog
|
from calibre.gui2.dialogs.config import ConfigDialog
|
||||||
from calibre.gui2.dialogs.search import SearchDialog
|
from calibre.gui2.dialogs.search import SearchDialog
|
||||||
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
||||||
@ -237,6 +238,7 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
|
|
||||||
QObject.connect(self.config_button, SIGNAL('clicked(bool)'), self.do_config)
|
QObject.connect(self.config_button, SIGNAL('clicked(bool)'), self.do_config)
|
||||||
self.connect(self.preferences_action, SIGNAL('triggered(bool)'), self.do_config)
|
self.connect(self.preferences_action, SIGNAL('triggered(bool)'), self.do_config)
|
||||||
|
self.connect(self.action_preferences, SIGNAL('triggered(bool)'), self.do_config)
|
||||||
QObject.connect(self.advanced_search_button, SIGNAL('clicked(bool)'), self.do_advanced_search)
|
QObject.connect(self.advanced_search_button, SIGNAL('clicked(bool)'), self.do_advanced_search)
|
||||||
|
|
||||||
####################### Library view ########################
|
####################### Library view ########################
|
||||||
@ -818,7 +820,8 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
rows = self.library_view.selectionModel().selectedRows()
|
rows = self.library_view.selectionModel().selectedRows()
|
||||||
previous = self.library_view.currentIndex()
|
previous = self.library_view.currentIndex()
|
||||||
if not rows or len(rows) == 0:
|
if not rows or len(rows) == 0:
|
||||||
d = error_dialog(self, _('Cannot edit metadata'), _('No books selected'))
|
d = error_dialog(self, _('Cannot edit metadata'),
|
||||||
|
_('No books selected'))
|
||||||
d.exec_()
|
d.exec_()
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -904,9 +907,8 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
on_card = config['send_to_storage_card_by_default']
|
on_card = config['send_to_storage_card_by_default']
|
||||||
self.sync_to_device(on_card, False, specific_format=fmt)
|
self.sync_to_device(on_card, False, specific_format=fmt)
|
||||||
|
|
||||||
|
def sync_to_device(self, on_card, delete_from_library, specific_format=None, send_rows=None, auto_convert=True):
|
||||||
def sync_to_device(self, on_card, delete_from_library, specific_format=None):
|
rows = self.library_view.selectionModel().selectedRows() if send_rows is None else send_rows
|
||||||
rows = self.library_view.selectionModel().selectedRows()
|
|
||||||
if not self.device_manager or not rows or len(rows) == 0:
|
if not self.device_manager or not rows or len(rows) == 0:
|
||||||
return
|
return
|
||||||
ids = iter(self.library_view.model().id(r) for r in rows)
|
ids = iter(self.library_view.model().id(r) for r in rows)
|
||||||
@ -917,7 +919,7 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
if cdata:
|
if cdata:
|
||||||
mi['cover'] = self.cover_to_thumbnail(cdata)
|
mi['cover'] = self.cover_to_thumbnail(cdata)
|
||||||
metadata, full_metadata = iter(metadata), iter(full_metadata)
|
metadata, full_metadata = iter(metadata), iter(full_metadata)
|
||||||
_files = self.library_view.model().get_preferred_formats(rows,
|
_files, _auto_rows = self.library_view.model().get_preferred_formats(rows,
|
||||||
self.device_manager.device_class.FORMATS,
|
self.device_manager.device_class.FORMATS,
|
||||||
paths=True, set_metadata=True,
|
paths=True, set_metadata=True,
|
||||||
specific_format=specific_format)
|
specific_format=specific_format)
|
||||||
@ -952,8 +954,27 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
remove = remove_ids if delete_from_library else []
|
remove = remove_ids if delete_from_library else []
|
||||||
self.upload_books(gf, names, good, on_card, memory=(_files, remove))
|
self.upload_books(gf, names, good, on_card, memory=(_files, remove))
|
||||||
self.status_bar.showMessage(_('Sending books to device.'), 5000)
|
self.status_bar.showMessage(_('Sending books to device.'), 5000)
|
||||||
|
|
||||||
if bad:
|
if bad:
|
||||||
|
if specific_format is None:
|
||||||
|
if 'epub' in self.device_manager.device_class.FORMATS:
|
||||||
|
format = 'epub'
|
||||||
|
elif 'mobi' in self.device_manager.device_class.FORMATS or 'prc' in self.device_manager.device_class.FORMATS:
|
||||||
|
format = 'mobi'
|
||||||
|
elif 'lrf' in self.device_manager.device_class.FORMATS:
|
||||||
|
format = 'lrf'
|
||||||
|
else:
|
||||||
|
format = specific_format
|
||||||
|
|
||||||
|
if format not in ('epub', 'mobi'):
|
||||||
|
auto_convert = False
|
||||||
|
|
||||||
bad = '\n'.join('<li>%s</li>'%(i,) for i in bad)
|
bad = '\n'.join('<li>%s</li>'%(i,) for i in bad)
|
||||||
|
if auto_convert:
|
||||||
|
d = info_dialog(self, _('No suitable formats'),
|
||||||
|
_('Auto converting the following books before uploading to the device:<br><ul>%s</ul>')%(bad,))
|
||||||
|
self.auto_convert(_auto_rows, on_card, format)
|
||||||
|
else:
|
||||||
d = warning_dialog(self, _('No suitable formats'),
|
d = warning_dialog(self, _('No suitable formats'),
|
||||||
_('Could not upload the following books to the device, as no suitable formats were found:<br><ul>%s</ul>')%(bad,))
|
_('Could not upload the following books to the device, as no suitable formats were found:<br><ul>%s</ul>')%(bad,))
|
||||||
d.exec_()
|
d.exec_()
|
||||||
@ -1048,6 +1069,32 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
|
|
||||||
############################### Convert ####################################
|
############################### Convert ####################################
|
||||||
|
|
||||||
|
def auto_convert(self, rows, on_card, format):
|
||||||
|
previous = self.library_view.currentIndex()
|
||||||
|
|
||||||
|
comics, others = [], []
|
||||||
|
db = self.library_view.model().db
|
||||||
|
for r in rows:
|
||||||
|
formats = db.formats(r)
|
||||||
|
if not formats: continue
|
||||||
|
formats = formats.lower().split(',')
|
||||||
|
if 'cbr' in formats or 'cbz' in formats:
|
||||||
|
comics.append(r)
|
||||||
|
else:
|
||||||
|
others.append(r)
|
||||||
|
|
||||||
|
jobs, changed, bad_rows = auto_convert_ebook(format, self, self.library_view.model().db, comics, others)
|
||||||
|
for func, args, desc, fmt, id, temp_files in jobs:
|
||||||
|
if id not in bad_rows:
|
||||||
|
job = self.job_manager.run_job(Dispatcher(self.book_auto_converted),
|
||||||
|
func, args=args, description=desc)
|
||||||
|
self.conversion_jobs[job] = (temp_files, fmt, id, on_card)
|
||||||
|
|
||||||
|
if changed:
|
||||||
|
self.library_view.model().refresh_rows(rows)
|
||||||
|
current = self.library_view.currentIndex()
|
||||||
|
self.library_view.model().current_changed(current, previous)
|
||||||
|
|
||||||
def get_books_for_conversion(self):
|
def get_books_for_conversion(self):
|
||||||
rows = [r.row() for r in self.library_view.selectionModel().selectedRows()]
|
rows = [r.row() for r in self.library_view.selectionModel().selectedRows()]
|
||||||
if not rows or len(rows) == 0:
|
if not rows or len(rows) == 0:
|
||||||
@ -1109,6 +1156,31 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
current = self.library_view.currentIndex()
|
current = self.library_view.currentIndex()
|
||||||
self.library_view.model().current_changed(current, previous)
|
self.library_view.model().current_changed(current, previous)
|
||||||
|
|
||||||
|
def book_auto_converted(self, job):
|
||||||
|
temp_files, fmt, book_id, on_card = self.conversion_jobs.pop(job)
|
||||||
|
try:
|
||||||
|
if job.exception is not None:
|
||||||
|
self.job_exception(job)
|
||||||
|
return
|
||||||
|
data = open(temp_files[-1].name, 'rb')
|
||||||
|
self.library_view.model().db.add_format(book_id, fmt, data, index_is_id=True)
|
||||||
|
data.close()
|
||||||
|
self.status_bar.showMessage(job.description + (' completed'), 2000)
|
||||||
|
finally:
|
||||||
|
for f in temp_files:
|
||||||
|
try:
|
||||||
|
if os.path.exists(f.name):
|
||||||
|
os.remove(f.name)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
self.tags_view.recount()
|
||||||
|
if self.current_view() is self.library_view:
|
||||||
|
current = self.library_view.currentIndex()
|
||||||
|
self.library_view.model().current_changed(current, QModelIndex())
|
||||||
|
|
||||||
|
r = self.library_view.model().index(self.library_view.model().db.row(book_id), 0)
|
||||||
|
self.sync_to_device(on_card, False, specific_format=fmt, send_rows=[r], auto_convert=False)
|
||||||
|
|
||||||
def book_converted(self, job):
|
def book_converted(self, job):
|
||||||
temp_files, fmt, book_id = self.conversion_jobs.pop(job)
|
temp_files, fmt, book_id = self.conversion_jobs.pop(job)
|
||||||
try:
|
try:
|
||||||
@ -1149,10 +1221,14 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
if ext in config['internally_viewed_formats']:
|
if ext in config['internally_viewed_formats']:
|
||||||
if ext == 'LRF':
|
if ext == 'LRF':
|
||||||
args = ['lrfviewer', name]
|
args = ['lrfviewer', name]
|
||||||
self.job_manager.server.run_free_job('lrfviewer', kwdargs=dict(args=args))
|
self.job_manager.server.run_free_job('lrfviewer',
|
||||||
|
kwdargs=dict(args=args))
|
||||||
else:
|
else:
|
||||||
args = ['ebook-viewer', name]
|
args = ['ebook-viewer', name]
|
||||||
self.job_manager.server.run_free_job('ebook-viewer', kwdargs=dict(args=args))
|
if isosx:
|
||||||
|
args.append('--raise-window')
|
||||||
|
self.job_manager.server.run_free_job('ebook-viewer',
|
||||||
|
kwdargs=dict(args=args))
|
||||||
else:
|
else:
|
||||||
QDesktopServices.openUrl(QUrl('file:'+name))#launch(name)
|
QDesktopServices.openUrl(QUrl('file:'+name))#launch(name)
|
||||||
time.sleep(5) # User feedback
|
time.sleep(5) # User feedback
|
||||||
@ -1247,7 +1323,7 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
|
|
||||||
############################### Do config ##################################
|
############################### Do config ##################################
|
||||||
|
|
||||||
def do_config(self):
|
def do_config(self, *args):
|
||||||
if self.job_manager.has_jobs():
|
if self.job_manager.has_jobs():
|
||||||
d = error_dialog(self, _('Cannot configure'), _('Cannot configure while there are running jobs.'))
|
d = error_dialog(self, _('Cannot configure'), _('Cannot configure while there are running jobs.'))
|
||||||
d.exec_()
|
d.exec_()
|
||||||
@ -1406,6 +1482,14 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
dir = os.path.expanduser('~/Library')
|
dir = os.path.expanduser('~/Library')
|
||||||
self.library_path = os.path.abspath(dir)
|
self.library_path = os.path.abspath(dir)
|
||||||
if not os.path.exists(self.library_path):
|
if not os.path.exists(self.library_path):
|
||||||
|
try:
|
||||||
|
os.makedirs(self.library_path)
|
||||||
|
except:
|
||||||
|
self.library_path = os.path.expanduser('~/Library')
|
||||||
|
error_dialog(self, _('Invalid library location'),
|
||||||
|
_('Could not access %s. Using %s as the library.')%
|
||||||
|
(repr(self.library_path), repr(self.library_path))
|
||||||
|
).exec_()
|
||||||
os.makedirs(self.library_path)
|
os.makedirs(self.library_path)
|
||||||
|
|
||||||
|
|
||||||
@ -1610,3 +1694,4 @@ if __name__ == '__main__':
|
|||||||
log = open(logfile).read().decode('utf-8', 'ignore')
|
log = open(logfile).read().decode('utf-8', 'ignore')
|
||||||
d = QErrorMessage('<b>Error:</b>%s<br><b>Traceback:</b><br>%s<b>Log:</b><br>'%(unicode(err), unicode(tb), log))
|
d = QErrorMessage('<b>Error:</b>%s<br><b>Traceback:</b><br>%s<b>Log:</b><br>'%(unicode(err), unicode(tb), log))
|
||||||
d.exec_()
|
d.exec_()
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<ui version="4.0">
|
<ui version="4.0">
|
||||||
<author>Kovid Goyal</author>
|
<author>Kovid Goyal</author>
|
||||||
<class>MainWindow</class>
|
<class>MainWindow</class>
|
||||||
@ -6,12 +7,12 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>865</width>
|
<width>1012</width>
|
||||||
<height>822</height>
|
<height>822</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy vsizetype="Preferred" hsizetype="Preferred" >
|
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||||
<horstretch>0</horstretch>
|
<horstretch>0</horstretch>
|
||||||
<verstretch>0</verstretch>
|
<verstretch>0</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
@ -33,7 +34,7 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="LocationView" name="location_view">
|
<widget class="LocationView" name="location_view">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
|
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||||
<horstretch>0</horstretch>
|
<horstretch>0</horstretch>
|
||||||
<verstretch>0</verstretch>
|
<verstretch>0</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
@ -110,7 +111,7 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="vanity">
|
<widget class="QLabel" name="vanity">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy vsizetype="Preferred" hsizetype="Preferred" >
|
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||||
<horstretch>0</horstretch>
|
<horstretch>0</horstretch>
|
||||||
<verstretch>0</verstretch>
|
<verstretch>0</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
@ -195,7 +196,7 @@
|
|||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy vsizetype="Fixed" hsizetype="Expanding" >
|
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||||
<horstretch>1</horstretch>
|
<horstretch>1</horstretch>
|
||||||
<verstretch>0</verstretch>
|
<verstretch>0</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
@ -204,10 +205,10 @@
|
|||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>Search the list of books by title or author<br><br>Words separated by spaces are ANDed</string>
|
<string>Search the list of books by title or author<br><br>Words separated by spaces are ANDed</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="whatsThis">
|
<property name="whatsThis">
|
||||||
<string>Search the list of books by title, author, publisher, tags and comments<br><br>Words separated by spaces are ANDed</string>
|
<string>Search the list of books by title, author, publisher, tags and comments<br><br>Words separated by spaces are ANDed</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="autoFillBackground">
|
<property name="autoFillBackground">
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
@ -273,7 +274,7 @@
|
|||||||
<item row="2" column="0">
|
<item row="2" column="0">
|
||||||
<widget class="QStackedWidget" name="stack">
|
<widget class="QStackedWidget" name="stack">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
|
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||||
<horstretch>100</horstretch>
|
<horstretch>100</horstretch>
|
||||||
<verstretch>100</verstretch>
|
<verstretch>100</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
@ -335,7 +336,7 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="BooksView" name="library_view">
|
<widget class="BooksView" name="library_view">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
|
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||||
<horstretch>100</horstretch>
|
<horstretch>100</horstretch>
|
||||||
<verstretch>10</verstretch>
|
<verstretch>10</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
@ -375,7 +376,7 @@
|
|||||||
<item row="0" column="0">
|
<item row="0" column="0">
|
||||||
<widget class="DeviceBooksView" name="memory_view">
|
<widget class="DeviceBooksView" name="memory_view">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
|
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||||
<horstretch>100</horstretch>
|
<horstretch>100</horstretch>
|
||||||
<verstretch>10</verstretch>
|
<verstretch>10</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
@ -413,7 +414,7 @@
|
|||||||
<item row="0" column="0">
|
<item row="0" column="0">
|
||||||
<widget class="DeviceBooksView" name="card_view">
|
<widget class="DeviceBooksView" name="card_view">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy vsizetype="Expanding" hsizetype="Preferred" >
|
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||||
<horstretch>10</horstretch>
|
<horstretch>10</horstretch>
|
||||||
<verstretch>10</verstretch>
|
<verstretch>10</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
@ -482,15 +483,16 @@
|
|||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</attribute>
|
</attribute>
|
||||||
<addaction name="action_add"/>
|
<addaction name="action_add"/>
|
||||||
<addaction name="action_del" />
|
|
||||||
<addaction name="action_edit"/>
|
<addaction name="action_edit"/>
|
||||||
|
<addaction name="action_convert"/>
|
||||||
|
<addaction name="action_view"/>
|
||||||
|
<addaction name="action_news"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="action_sync"/>
|
<addaction name="action_sync"/>
|
||||||
<addaction name="action_save"/>
|
<addaction name="action_save"/>
|
||||||
|
<addaction name="action_del"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="action_news" />
|
<addaction name="action_preferences"/>
|
||||||
<addaction name="action_convert" />
|
|
||||||
<addaction name="action_view" />
|
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QStatusBar" name="statusBar">
|
<widget class="QStatusBar" name="statusBar">
|
||||||
<property name="mouseTracking">
|
<property name="mouseTracking">
|
||||||
@ -665,6 +667,21 @@
|
|||||||
<string>Send specific format to device</string>
|
<string>Send specific format to device</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="action_preferences">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="images.qrc">
|
||||||
|
<normaloff>:/images/config.svg</normaloff>:/images/config.svg</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Preferences</string>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Configure calibre</string>
|
||||||
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string>Ctrl+P</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
|
@ -18,7 +18,9 @@ from calibre.gui2 import warning_dialog
|
|||||||
from calibre.ptempfile import PersistentTemporaryFile
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
from calibre.ebooks.lrf import preferred_source_formats as LRF_PREFERRED_SOURCE_FORMATS
|
from calibre.ebooks.lrf import preferred_source_formats as LRF_PREFERRED_SOURCE_FORMATS
|
||||||
from calibre.ebooks.metadata.opf import OPFCreator
|
from calibre.ebooks.metadata.opf import OPFCreator
|
||||||
from calibre.ebooks.epub.from_any import SOURCE_FORMATS as EPUB_PREFERRED_SOURCE_FORMATS
|
from calibre.ebooks.epub.from_any import SOURCE_FORMATS as EPUB_PREFERRED_SOURCE_FORMATS, config as epubconfig
|
||||||
|
from calibre.ebooks.mobi.from_any import config as mobiconfig
|
||||||
|
from calibre.ebooks.lrf.comic.convert_from import config as comicconfig
|
||||||
|
|
||||||
def get_dialog(fmt):
|
def get_dialog(fmt):
|
||||||
return {
|
return {
|
||||||
@ -26,6 +28,122 @@ def get_dialog(fmt):
|
|||||||
'mobi':MOBIConvert,
|
'mobi':MOBIConvert,
|
||||||
}[fmt]
|
}[fmt]
|
||||||
|
|
||||||
|
def get_config(fmt):
|
||||||
|
return {
|
||||||
|
'epub':epubconfig,
|
||||||
|
'mobi':mobiconfig,
|
||||||
|
}[fmt]
|
||||||
|
|
||||||
|
def auto_convert(fmt, parent, db, comics, others):
|
||||||
|
changed = False
|
||||||
|
jobs = []
|
||||||
|
|
||||||
|
total = sum(map(len, (others, comics)))
|
||||||
|
if total == 0:
|
||||||
|
return
|
||||||
|
parent.status_bar.showMessage(_('Starting auto conversion of %d books')%total, 2000)
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
bad_rows = []
|
||||||
|
|
||||||
|
for i, row in enumerate(others+comics):
|
||||||
|
row_id = db.id(row)
|
||||||
|
|
||||||
|
if row in others:
|
||||||
|
temp_files = []
|
||||||
|
|
||||||
|
data = None
|
||||||
|
for _fmt in EPUB_PREFERRED_SOURCE_FORMATS:
|
||||||
|
try:
|
||||||
|
data = db.format(row, _fmt.upper())
|
||||||
|
if data is not None:
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
if data is None:
|
||||||
|
bad_rows.append(row)
|
||||||
|
continue
|
||||||
|
|
||||||
|
defaults = db.conversion_options(db.id(row), fmt)
|
||||||
|
defaults = defaults if defaults else ''
|
||||||
|
options = get_config(fmt)(defaults=defaults).parse()
|
||||||
|
|
||||||
|
mi = db.get_metadata(row)
|
||||||
|
opf = OPFCreator(os.getcwdu(), mi)
|
||||||
|
opf_file = PersistentTemporaryFile('.opf')
|
||||||
|
opf.render(opf_file)
|
||||||
|
opf_file.close()
|
||||||
|
pt = PersistentTemporaryFile('.'+_fmt.lower())
|
||||||
|
pt.write(data)
|
||||||
|
pt.close()
|
||||||
|
of = PersistentTemporaryFile('.'+fmt)
|
||||||
|
of.close()
|
||||||
|
cover = db.cover(row)
|
||||||
|
cf = None
|
||||||
|
if cover:
|
||||||
|
cf = PersistentTemporaryFile('.jpeg')
|
||||||
|
cf.write(cover)
|
||||||
|
cf.close()
|
||||||
|
options.cover = cf.name
|
||||||
|
options.output = of.name
|
||||||
|
options.from_opf = opf_file.name
|
||||||
|
args = [options, pt.name]
|
||||||
|
desc = _('Auto convert book %d of %d (%s)')%(i+1, total, repr(mi.title))
|
||||||
|
temp_files = [cf] if cf is not None else []
|
||||||
|
temp_files.extend([opf_file, pt, of])
|
||||||
|
jobs.append(('any2'+fmt, args, desc, fmt.upper(), row_id, temp_files))
|
||||||
|
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
defaults = db.conversion_options(db.id(row), fmt)
|
||||||
|
defaults = defaults if defaults else ''
|
||||||
|
options = comicconfig(defaults=defaults).parse()
|
||||||
|
|
||||||
|
mi = db.get_metadata(row)
|
||||||
|
if mi.title:
|
||||||
|
options.title = mi.title
|
||||||
|
if mi.authors:
|
||||||
|
options.author = ','.join(mi.authors)
|
||||||
|
data = None
|
||||||
|
for _fmt in ['cbz', 'cbr']:
|
||||||
|
try:
|
||||||
|
data = db.format(row, _fmt.upper())
|
||||||
|
if data is not None:
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if data is None:
|
||||||
|
bad_rows.append(row)
|
||||||
|
continue
|
||||||
|
|
||||||
|
pt = PersistentTemporaryFile('.'+_fmt.lower())
|
||||||
|
pt.write(data)
|
||||||
|
pt.close()
|
||||||
|
of = PersistentTemporaryFile('.'+fmt)
|
||||||
|
of.close()
|
||||||
|
setattr(options, 'output', of.name)
|
||||||
|
options.verbose = 1
|
||||||
|
args = [pt.name, options]
|
||||||
|
desc = _('Convert book %d of %d (%s)')%(i+1, total, repr(mi.title))
|
||||||
|
jobs.append(('comic2'+fmt, args, desc, fmt.upper(), row_id, [pt, of]))
|
||||||
|
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
if bad_rows:
|
||||||
|
res = []
|
||||||
|
for row in bad_rows:
|
||||||
|
title = db.title(row)
|
||||||
|
res.append('<li>%s</li>'%title)
|
||||||
|
|
||||||
|
msg = _('<p>Could not convert %d of %d books, because no suitable source format was found.<ul>%s</ul>')%(len(res), total, '\n'.join(res))
|
||||||
|
warning_dialog(parent, _('Could not convert some books'), msg).exec_()
|
||||||
|
|
||||||
|
return jobs, changed, bad_rows
|
||||||
|
|
||||||
|
def auto_convert_lrf(fmt, parent, db, comics, others):
|
||||||
|
pass
|
||||||
|
|
||||||
def convert_single(fmt, parent, db, comics, others):
|
def convert_single(fmt, parent, db, comics, others):
|
||||||
changed = False
|
changed = False
|
||||||
jobs = []
|
jobs = []
|
||||||
@ -386,6 +504,12 @@ def fetch_scheduled_recipe(recipe, script):
|
|||||||
args.append(script)
|
args.append(script)
|
||||||
return 'feeds2'+fmt, [args], _('Fetch news from ')+recipe.title, fmt.upper(), [pt]
|
return 'feeds2'+fmt, [args], _('Fetch news from ')+recipe.title, fmt.upper(), [pt]
|
||||||
|
|
||||||
|
def auto_convert_ebook(*args):
|
||||||
|
fmt = args[0] if args[0] else 'epub'
|
||||||
|
if fmt == 'lrf':
|
||||||
|
return auto_convert_lrf()
|
||||||
|
elif fmt in ('epub', 'mobi'):
|
||||||
|
return auto_convert(*args)
|
||||||
|
|
||||||
def convert_single_ebook(*args):
|
def convert_single_ebook(*args):
|
||||||
fmt = prefs['output_format'].lower()
|
fmt = prefs['output_format'].lower()
|
||||||
@ -411,3 +535,4 @@ def set_conversion_defaults(comic, parent, db):
|
|||||||
def fetch_news(data):
|
def fetch_news(data):
|
||||||
fmt = prefs['output_format'].lower()
|
fmt = prefs['output_format'].lower()
|
||||||
return _fetch_news(data, fmt)
|
return _fetch_news(data, fmt)
|
||||||
|
|
||||||
|
@ -450,7 +450,9 @@ class DocumentView(QWebView):
|
|||||||
self.manager.scrolled(self.scroll_fraction)
|
self.manager.scrolled(self.scroll_fraction)
|
||||||
|
|
||||||
def wheel_event(self, down=True):
|
def wheel_event(self, down=True):
|
||||||
QWebView.wheelEvent(self, QWheelEvent(QPoint(100, 100), (-120 if down else 120), Qt.NoButton, Qt.NoModifier))
|
QWebView.wheelEvent(self,
|
||||||
|
QWheelEvent(QPoint(100, 100), (-120 if down else 120),
|
||||||
|
Qt.NoButton, Qt.NoModifier))
|
||||||
|
|
||||||
def next_page(self):
|
def next_page(self):
|
||||||
if self.document.at_bottom:
|
if self.document.at_bottom:
|
||||||
@ -538,6 +540,26 @@ class DocumentView(QWebView):
|
|||||||
self.next_page()
|
self.next_page()
|
||||||
elif key in [Qt.Key_PageUp, Qt.Key_Backspace, Qt.Key_Up]:
|
elif key in [Qt.Key_PageUp, Qt.Key_Backspace, Qt.Key_Up]:
|
||||||
self.previous_page()
|
self.previous_page()
|
||||||
|
elif key in [Qt.Key_Home]:
|
||||||
|
if event.modifiers() & Qt.ControlModifier:
|
||||||
|
if self.manager is not None:
|
||||||
|
self.manager.goto_start()
|
||||||
|
else:
|
||||||
|
self.scroll_to(0)
|
||||||
|
elif key in [Qt.Key_End]:
|
||||||
|
if event.modifiers() & Qt.ControlModifier:
|
||||||
|
if self.manager is not None:
|
||||||
|
self.manager.goto_end()
|
||||||
|
else:
|
||||||
|
self.scroll_to(1)
|
||||||
|
elif key in [Qt.Key_J]:
|
||||||
|
self.wheel_event()
|
||||||
|
elif key in [Qt.Key_K]:
|
||||||
|
self.wheel_event(down=False)
|
||||||
|
elif key in [Qt.Key_H]:
|
||||||
|
self.scroll_by(x=-15)
|
||||||
|
elif key in [Qt.Key_L]:
|
||||||
|
self.scroll_by(x=15)
|
||||||
else:
|
else:
|
||||||
return QWebView.keyPressEvent(self, event)
|
return QWebView.keyPressEvent(self, event)
|
||||||
|
|
||||||
|
@ -342,6 +342,12 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
|||||||
if pos is not None:
|
if pos is not None:
|
||||||
self.goto_page(pos)
|
self.goto_page(pos)
|
||||||
|
|
||||||
|
def goto_start(self):
|
||||||
|
self.goto_page(1)
|
||||||
|
|
||||||
|
def goto_end(self):
|
||||||
|
self.goto_page(self.pos.maximum())
|
||||||
|
|
||||||
def goto_page(self, new_page):
|
def goto_page(self, new_page):
|
||||||
if self.current_page is not None:
|
if self.current_page is not None:
|
||||||
for page in self.iterator.spine:
|
for page in self.iterator.spine:
|
||||||
@ -604,6 +610,10 @@ def config(defaults=None):
|
|||||||
c = Config('viewer', desc)
|
c = Config('viewer', desc)
|
||||||
else:
|
else:
|
||||||
c = StringConfig(defaults, desc)
|
c = StringConfig(defaults, desc)
|
||||||
|
|
||||||
|
c.add_opt('raise_window', ['--raise-window'], default=False,
|
||||||
|
help=_('If specified, viewer window will try to come to the '
|
||||||
|
'front when started.'))
|
||||||
return c
|
return c
|
||||||
|
|
||||||
def option_parser():
|
def option_parser():
|
||||||
@ -617,7 +627,7 @@ View an ebook.
|
|||||||
|
|
||||||
def main(args=sys.argv):
|
def main(args=sys.argv):
|
||||||
parser = option_parser()
|
parser = option_parser()
|
||||||
args = parser.parse_args(args)[-1]
|
opts, args = parser.parse_args(args)
|
||||||
pid = os.fork() if False and islinux else -1
|
pid = os.fork() if False and islinux else -1
|
||||||
if pid <= 0:
|
if pid <= 0:
|
||||||
app = Application(args)
|
app = Application(args)
|
||||||
@ -627,6 +637,8 @@ def main(args=sys.argv):
|
|||||||
main = EbookViewer(args[1] if len(args) > 1 else None)
|
main = EbookViewer(args[1] if len(args) > 1 else None)
|
||||||
sys.excepthook = main.unhandled_exception
|
sys.excepthook = main.unhandled_exception
|
||||||
main.show()
|
main.show()
|
||||||
|
if opts.raise_window:
|
||||||
|
main.raise_()
|
||||||
with main:
|
with main:
|
||||||
return app.exec_()
|
return app.exec_()
|
||||||
return 0
|
return 0
|
||||||
|
@ -244,7 +244,10 @@ def do_add(db, paths, one_book_per_directory, recurse, add_duplicates):
|
|||||||
if os.path.isdir(path):
|
if os.path.isdir(path):
|
||||||
dirs.append(path)
|
dirs.append(path)
|
||||||
else:
|
else:
|
||||||
|
if os.path.exists(path):
|
||||||
files.append(path)
|
files.append(path)
|
||||||
|
else:
|
||||||
|
print path, 'not found'
|
||||||
|
|
||||||
formats, metadata = [], []
|
formats, metadata = [], []
|
||||||
for book in files:
|
for book in files:
|
||||||
@ -262,12 +265,14 @@ def do_add(db, paths, one_book_per_directory, recurse, add_duplicates):
|
|||||||
formats.append(format)
|
formats.append(format)
|
||||||
metadata.append(mi)
|
metadata.append(mi)
|
||||||
|
|
||||||
file_duplicates = db.add_books(files, formats, metadata, add_duplicates=add_duplicates)
|
|
||||||
if not file_duplicates[0]:
|
|
||||||
file_duplicates = []
|
file_duplicates = []
|
||||||
else:
|
if files:
|
||||||
|
file_duplicates = db.add_books(files, formats, metadata,
|
||||||
|
add_duplicates=add_duplicates)
|
||||||
|
if file_duplicates:
|
||||||
file_duplicates = file_duplicates[0]
|
file_duplicates = file_duplicates[0]
|
||||||
|
|
||||||
|
|
||||||
dir_dups = []
|
dir_dups = []
|
||||||
for dir in dirs:
|
for dir in dirs:
|
||||||
if recurse:
|
if recurse:
|
||||||
@ -286,7 +291,9 @@ def do_add(db, paths, one_book_per_directory, recurse, add_duplicates):
|
|||||||
db.import_book(mi, formats)
|
db.import_book(mi, formats)
|
||||||
else:
|
else:
|
||||||
if dir_dups or file_duplicates:
|
if dir_dups or file_duplicates:
|
||||||
print >>sys.stderr, _('The following books were not added as they already exist in the database (see --duplicates option):')
|
print >>sys.stderr, _('The following books were not added as '
|
||||||
|
'they already exist in the database '
|
||||||
|
'(see --duplicates option):')
|
||||||
for mi, formats in dir_dups:
|
for mi, formats in dir_dups:
|
||||||
title = mi.title
|
title = mi.title
|
||||||
if isinstance(title, unicode):
|
if isinstance(title, unicode):
|
||||||
|
@ -15,7 +15,7 @@ from PyQt4.QtCore import QCoreApplication, QThread, QReadWriteLock
|
|||||||
from PyQt4.QtGui import QApplication, QImage
|
from PyQt4.QtGui import QApplication, QImage
|
||||||
__app = None
|
__app = None
|
||||||
|
|
||||||
from calibre.library import title_sort
|
from calibre.ebooks.metadata import title_sort
|
||||||
from calibre.library.database import LibraryDatabase
|
from calibre.library.database import LibraryDatabase
|
||||||
from calibre.library.sqlite import connect, IntegrityError
|
from calibre.library.sqlite import connect, IntegrityError
|
||||||
from calibre.utils.search_query_parser import SearchQueryParser
|
from calibre.utils.search_query_parser import SearchQueryParser
|
||||||
@ -1567,3 +1567,4 @@ books_series_link feeds
|
|||||||
break
|
break
|
||||||
|
|
||||||
return duplicates
|
return duplicates
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
HTTP server for remote access to the calibre database.
|
HTTP server for remote access to the calibre database.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import sys, textwrap, mimetypes, operator, os, re, logging
|
import sys, textwrap, operator, os, re, logging
|
||||||
from itertools import repeat
|
from itertools import repeat
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@ -18,11 +18,13 @@ from PyQt4.Qt import QImage, QApplication, QByteArray, Qt, QBuffer
|
|||||||
|
|
||||||
from calibre.constants import __version__, __appname__
|
from calibre.constants import __version__, __appname__
|
||||||
from calibre.utils.genshi.template import MarkupTemplate
|
from calibre.utils.genshi.template import MarkupTemplate
|
||||||
from calibre import fit_image
|
from calibre import fit_image, guess_type
|
||||||
from calibre.resources import jquery, server_resources, build_time
|
from calibre.resources import jquery, server_resources, build_time
|
||||||
from calibre.library import server_config as config
|
from calibre.library import server_config as config
|
||||||
from calibre.library.database2 import LibraryDatabase2, FIELD_MAP
|
from calibre.library.database2 import LibraryDatabase2, FIELD_MAP
|
||||||
from calibre.utils.config import config_dir
|
from calibre.utils.config import config_dir
|
||||||
|
from calibre.utils.mdns import publish as publish_zeroconf, \
|
||||||
|
stop_server as stop_zeroconf
|
||||||
|
|
||||||
build_time = datetime.strptime(build_time, '%d %m %Y %H%M%S')
|
build_time = datetime.strptime(build_time, '%d %m %Y %H%M%S')
|
||||||
server_resources['jquery.js'] = jquery
|
server_resources['jquery.js'] = jquery
|
||||||
@ -77,7 +79,7 @@ class LibraryServer(object):
|
|||||||
<id>urn:calibre:${record[FM['id']]}</id>
|
<id>urn:calibre:${record[FM['id']]}</id>
|
||||||
<author><name>${authors}</name></author>
|
<author><name>${authors}</name></author>
|
||||||
<updated>${record[FM['timestamp']].strftime('%Y-%m-%dT%H:%M:%S+00:00')}</updated>
|
<updated>${record[FM['timestamp']].strftime('%Y-%m-%dT%H:%M:%S+00:00')}</updated>
|
||||||
<link type="application/epub+zip" href="/get/epub/${record[FM['id']]}" />
|
<link type="${mimetype}" href="/get/${fmt}/${record[FM['id']]}" />
|
||||||
<link rel="x-stanza-cover-image" type="image/jpeg" href="/get/cover/${record[FM['id']]}" />
|
<link rel="x-stanza-cover-image" type="image/jpeg" href="/get/cover/${record[FM['id']]}" />
|
||||||
<link rel="x-stanza-cover-image-thumbnail" type="image/jpeg" href="/get/thumb/${record[FM['id']]}" />
|
<link rel="x-stanza-cover-image-thumbnail" type="image/jpeg" href="/get/thumb/${record[FM['id']]}" />
|
||||||
<content type="xhtml">
|
<content type="xhtml">
|
||||||
@ -171,11 +173,14 @@ class LibraryServer(object):
|
|||||||
try:
|
try:
|
||||||
cherrypy.engine.start()
|
cherrypy.engine.start()
|
||||||
self.is_running = True
|
self.is_running = True
|
||||||
|
publish_zeroconf('Books in calibre', '_stanza._tcp',
|
||||||
|
self.opts.port, {'path':'/stanza'})
|
||||||
cherrypy.engine.block()
|
cherrypy.engine.block()
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
self.exception = e
|
self.exception = e
|
||||||
finally:
|
finally:
|
||||||
self.is_running = False
|
self.is_running = False
|
||||||
|
stop_zeroconf()
|
||||||
|
|
||||||
def exit(self):
|
def exit(self):
|
||||||
cherrypy.engine.exit()
|
cherrypy.engine.exit()
|
||||||
@ -218,7 +223,7 @@ class LibraryServer(object):
|
|||||||
fmt = self.db.format(id, format, index_is_id=True, as_file=True, mode='rb')
|
fmt = self.db.format(id, format, index_is_id=True, as_file=True, mode='rb')
|
||||||
if fmt is None:
|
if fmt is None:
|
||||||
raise cherrypy.HTTPError(404, 'book: %d does not have format: %s'%(id, format))
|
raise cherrypy.HTTPError(404, 'book: %d does not have format: %s'%(id, format))
|
||||||
mt = mimetypes.guess_type('dummy.'+format.lower())[0]
|
mt = guess_type('dummy.'+format.lower())[0]
|
||||||
if mt is None:
|
if mt is None:
|
||||||
mt = 'application/octet-stream'
|
mt = 'application/octet-stream'
|
||||||
cherrypy.response.headers['Content-Type'] = mt
|
cherrypy.response.headers['Content-Type'] = mt
|
||||||
@ -258,8 +263,9 @@ class LibraryServer(object):
|
|||||||
for record in iter(self.db):
|
for record in iter(self.db):
|
||||||
r = record[FIELD_MAP['formats']]
|
r = record[FIELD_MAP['formats']]
|
||||||
r = r.upper() if r else ''
|
r = r.upper() if r else ''
|
||||||
if 'EPUB' in r:
|
if 'EPUB' in r or 'PDB' in r:
|
||||||
authors = ' & '.join([i.replace('|', ',') for i in record[FIELD_MAP['authors']].split(',')])
|
authors = ' & '.join([i.replace('|', ',') for i in
|
||||||
|
record[FIELD_MAP['authors']].split(',')])
|
||||||
extra = []
|
extra = []
|
||||||
rating = record[FIELD_MAP['rating']]
|
rating = record[FIELD_MAP['rating']]
|
||||||
if rating > 0:
|
if rating > 0:
|
||||||
@ -270,11 +276,17 @@ class LibraryServer(object):
|
|||||||
extra.append('TAGS: %s<br />'%', '.join(tags.split(',')))
|
extra.append('TAGS: %s<br />'%', '.join(tags.split(',')))
|
||||||
series = record[FIELD_MAP['series']]
|
series = record[FIELD_MAP['series']]
|
||||||
if series:
|
if series:
|
||||||
extra.append('SERIES: %s [%d]<br />'%(series, record[FIELD_MAP['series_index']]))
|
extra.append('SERIES: %s [%d]<br />'%(series,
|
||||||
books.append(self.STANZA_ENTRY.generate(authors=authors,
|
record[FIELD_MAP['series_index']]))
|
||||||
|
fmt = 'epub' if 'EPUB' in r else 'pdb'
|
||||||
|
mimetype = guess_type('dummy.'+fmt)[0]
|
||||||
|
books.append(self.STANZA_ENTRY.generate(
|
||||||
|
authors=authors,
|
||||||
record=record, FM=FIELD_MAP,
|
record=record, FM=FIELD_MAP,
|
||||||
port=self.opts.port,
|
port=self.opts.port,
|
||||||
extra = ''.join(extra),
|
extra = ''.join(extra),
|
||||||
|
mimetype=mimetype,
|
||||||
|
fmt=fmt,
|
||||||
).render('xml').decode('utf8'))
|
).render('xml').decode('utf8'))
|
||||||
|
|
||||||
updated = self.db.last_modified()
|
updated = self.db.last_modified()
|
||||||
@ -325,7 +337,11 @@ class LibraryServer(object):
|
|||||||
@expose
|
@expose
|
||||||
def index(self, **kwargs):
|
def index(self, **kwargs):
|
||||||
'The / URL'
|
'The / URL'
|
||||||
|
stanza = cherrypy.request.headers.get('Stanza-Device-Name', 919)
|
||||||
|
if stanza == 919:
|
||||||
return self.static('index.html')
|
return self.static('index.html')
|
||||||
|
return self.stanza()
|
||||||
|
|
||||||
|
|
||||||
@expose
|
@expose
|
||||||
def get(self, what, id):
|
def get(self, what, id):
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
''' Post installation script for linux '''
|
''' Post installation script for linux '''
|
||||||
import sys, os, re, shutil
|
import sys, os, shutil
|
||||||
from subprocess import check_call, call
|
from subprocess import check_call, call
|
||||||
from tempfile import NamedTemporaryFile
|
|
||||||
|
|
||||||
from calibre import __version__, __appname__
|
from calibre import __version__, __appname__
|
||||||
from calibre.devices import devices
|
from calibre.devices import devices
|
||||||
@ -18,15 +17,8 @@ entry_points = {
|
|||||||
'console_scripts': [ \
|
'console_scripts': [ \
|
||||||
'ebook-device = calibre.devices.prs500.cli.main:main',
|
'ebook-device = calibre.devices.prs500.cli.main:main',
|
||||||
'ebook-meta = calibre.ebooks.metadata.cli:main',
|
'ebook-meta = calibre.ebooks.metadata.cli:main',
|
||||||
'txt2lrf = calibre.ebooks.lrf.txt.convert_from:main',
|
'ebook-convert = calibre.ebooks.conversion.cli:main',
|
||||||
'html2lrf = calibre.ebooks.lrf.html.convert_from:main',
|
|
||||||
'html2oeb = calibre.ebooks.html:main',
|
|
||||||
'html2epub = calibre.ebooks.epub.from_html:main',
|
|
||||||
'odt2oeb = calibre.ebooks.odt.to_oeb:main',
|
|
||||||
'markdown-calibre = calibre.ebooks.markdown.markdown:main',
|
'markdown-calibre = calibre.ebooks.markdown.markdown:main',
|
||||||
'lit2lrf = calibre.ebooks.lrf.lit.convert_from:main',
|
|
||||||
'epub2lrf = calibre.ebooks.lrf.epub.convert_from:main',
|
|
||||||
'rtf2lrf = calibre.ebooks.lrf.rtf.convert_from:main',
|
|
||||||
'web2disk = calibre.web.fetch.simple:main',
|
'web2disk = calibre.web.fetch.simple:main',
|
||||||
'feeds2disk = calibre.web.feeds.main:main',
|
'feeds2disk = calibre.web.feeds.main:main',
|
||||||
'calibre-server = calibre.library.server:main',
|
'calibre-server = calibre.library.server:main',
|
||||||
@ -34,22 +26,10 @@ entry_points = {
|
|||||||
'feeds2epub = calibre.ebooks.epub.from_feeds:main',
|
'feeds2epub = calibre.ebooks.epub.from_feeds:main',
|
||||||
'feeds2mobi = calibre.ebooks.mobi.from_feeds:main',
|
'feeds2mobi = calibre.ebooks.mobi.from_feeds:main',
|
||||||
'web2lrf = calibre.ebooks.lrf.web.convert_from:main',
|
'web2lrf = calibre.ebooks.lrf.web.convert_from:main',
|
||||||
'pdf2lrf = calibre.ebooks.lrf.pdf.convert_from:main',
|
|
||||||
'mobi2lrf = calibre.ebooks.lrf.mobi.convert_from:main',
|
|
||||||
'fb22lrf = calibre.ebooks.lrf.fb2.convert_from:main',
|
|
||||||
'any2lrf = calibre.ebooks.lrf.any.convert_from:main',
|
|
||||||
'any2epub = calibre.ebooks.epub.from_any:main',
|
|
||||||
'any2lit = calibre.ebooks.lit.from_any:main',
|
|
||||||
'any2mobi = calibre.ebooks.mobi.from_any:main',
|
|
||||||
'lrf2lrs = calibre.ebooks.lrf.lrfparser:main',
|
'lrf2lrs = calibre.ebooks.lrf.lrfparser:main',
|
||||||
'lrs2lrf = calibre.ebooks.lrf.lrs.convert_from:main',
|
'lrs2lrf = calibre.ebooks.lrf.lrs.convert_from:main',
|
||||||
'pdfreflow = calibre.ebooks.lrf.pdf.reflow:main',
|
|
||||||
'isbndb = calibre.ebooks.metadata.isbndb:main',
|
'isbndb = calibre.ebooks.metadata.isbndb:main',
|
||||||
'librarything = calibre.ebooks.metadata.library_thing:main',
|
'librarything = calibre.ebooks.metadata.library_thing:main',
|
||||||
'mobi2oeb = calibre.ebooks.mobi.reader:main',
|
|
||||||
'oeb2mobi = calibre.ebooks.mobi.writer:main',
|
|
||||||
'lit2oeb = calibre.ebooks.lit.reader:main',
|
|
||||||
'oeb2lit = calibre.ebooks.lit.writer:main',
|
|
||||||
'comic2lrf = calibre.ebooks.lrf.comic.convert_from:main',
|
'comic2lrf = calibre.ebooks.lrf.comic.convert_from:main',
|
||||||
'comic2epub = calibre.ebooks.epub.from_comic:main',
|
'comic2epub = calibre.ebooks.epub.from_comic:main',
|
||||||
'comic2mobi = calibre.ebooks.mobi.from_comic:main',
|
'comic2mobi = calibre.ebooks.mobi.from_comic:main',
|
||||||
@ -60,7 +40,6 @@ entry_points = {
|
|||||||
'calibre-parallel = calibre.parallel:main',
|
'calibre-parallel = calibre.parallel:main',
|
||||||
'calibre-customize = calibre.customize.ui:main',
|
'calibre-customize = calibre.customize.ui:main',
|
||||||
'pdftrim = calibre.ebooks.pdf.pdftrim:main' ,
|
'pdftrim = calibre.ebooks.pdf.pdftrim:main' ,
|
||||||
'any2pdf = calibre.ebooks.pdf.from_any:main',
|
|
||||||
],
|
],
|
||||||
'gui_scripts' : [
|
'gui_scripts' : [
|
||||||
__appname__+' = calibre.gui2.main:main',
|
__appname__+' = calibre.gui2.main:main',
|
||||||
@ -171,22 +150,13 @@ def setup_completion(fatal_errors):
|
|||||||
from calibre.ebooks.lrf.lrfparser import option_parser as lrf2lrsop
|
from calibre.ebooks.lrf.lrfparser import option_parser as lrf2lrsop
|
||||||
from calibre.gui2.lrf_renderer.main import option_parser as lrfviewerop
|
from calibre.gui2.lrf_renderer.main import option_parser as lrfviewerop
|
||||||
from calibre.ebooks.lrf.pdf.reflow import option_parser as pdfhtmlop
|
from calibre.ebooks.lrf.pdf.reflow import option_parser as pdfhtmlop
|
||||||
from calibre.ebooks.mobi.reader import option_parser as mobioeb
|
|
||||||
from calibre.ebooks.lit.reader import option_parser as lit2oeb
|
|
||||||
from calibre.web.feeds.main import option_parser as feeds2disk
|
from calibre.web.feeds.main import option_parser as feeds2disk
|
||||||
from calibre.web.feeds.recipes import titles as feed_titles
|
from calibre.web.feeds.recipes import titles as feed_titles
|
||||||
from calibre.ebooks.lrf.feeds.convert_from import option_parser as feeds2lrf
|
from calibre.ebooks.lrf.feeds.convert_from import option_parser as feeds2lrf
|
||||||
from calibre.ebooks.lrf.comic.convert_from import option_parser as comicop
|
from calibre.ebooks.lrf.comic.convert_from import option_parser as comicop
|
||||||
from calibre.ebooks.epub.from_html import option_parser as html2epub
|
|
||||||
from calibre.ebooks.html import option_parser as html2oeb
|
|
||||||
from calibre.ebooks.odt.to_oeb import option_parser as odt2oeb
|
|
||||||
from calibre.ebooks.epub.from_feeds import option_parser as feeds2epub
|
from calibre.ebooks.epub.from_feeds import option_parser as feeds2epub
|
||||||
from calibre.ebooks.mobi.from_feeds import option_parser as feeds2mobi
|
from calibre.ebooks.mobi.from_feeds import option_parser as feeds2mobi
|
||||||
from calibre.ebooks.epub.from_any import option_parser as any2epub
|
|
||||||
from calibre.ebooks.lit.from_any import option_parser as any2lit
|
|
||||||
from calibre.ebooks.epub.from_comic import option_parser as comic2epub
|
from calibre.ebooks.epub.from_comic import option_parser as comic2epub
|
||||||
from calibre.ebooks.mobi.from_any import option_parser as any2mobi
|
|
||||||
from calibre.ebooks.mobi.writer import option_parser as oeb2mobi
|
|
||||||
from calibre.gui2.main import option_parser as guiop
|
from calibre.gui2.main import option_parser as guiop
|
||||||
any_formats = ['epub', 'htm', 'html', 'xhtml', 'xhtm', 'rar', 'zip',
|
any_formats = ['epub', 'htm', 'html', 'xhtml', 'xhtm', 'rar', 'zip',
|
||||||
'txt', 'lit', 'rtf', 'pdf', 'prc', 'mobi', 'fb2', 'odt']
|
'txt', 'lit', 'rtf', 'pdf', 'prc', 'mobi', 'fb2', 'odt']
|
||||||
@ -209,16 +179,10 @@ def setup_completion(fatal_errors):
|
|||||||
f.write(opts_and_exts('pdf2lrf', htmlop, ['pdf']))
|
f.write(opts_and_exts('pdf2lrf', htmlop, ['pdf']))
|
||||||
f.write(opts_and_exts('any2lrf', htmlop, any_formats))
|
f.write(opts_and_exts('any2lrf', htmlop, any_formats))
|
||||||
f.write(opts_and_exts('calibre', guiop, any_formats))
|
f.write(opts_and_exts('calibre', guiop, any_formats))
|
||||||
f.write(opts_and_exts('any2epub', any2epub, any_formats))
|
|
||||||
f.write(opts_and_exts('any2lit', any2lit, any_formats))
|
|
||||||
f.write(opts_and_exts('any2mobi', any2mobi, any_formats))
|
|
||||||
f.write(opts_and_exts('oeb2mobi', oeb2mobi, ['opf']))
|
|
||||||
f.write(opts_and_exts('lrf2lrs', lrf2lrsop, ['lrf']))
|
f.write(opts_and_exts('lrf2lrs', lrf2lrsop, ['lrf']))
|
||||||
f.write(opts_and_exts('ebook-meta', metaop, list(meta_filetypes())))
|
f.write(opts_and_exts('ebook-meta', metaop, list(meta_filetypes())))
|
||||||
f.write(opts_and_exts('lrfviewer', lrfviewerop, ['lrf']))
|
f.write(opts_and_exts('lrfviewer', lrfviewerop, ['lrf']))
|
||||||
f.write(opts_and_exts('pdfrelow', pdfhtmlop, ['pdf']))
|
f.write(opts_and_exts('pdfrelow', pdfhtmlop, ['pdf']))
|
||||||
f.write(opts_and_exts('mobi2oeb', mobioeb, ['mobi', 'prc']))
|
|
||||||
f.write(opts_and_exts('lit2oeb', lit2oeb, ['lit']))
|
|
||||||
f.write(opts_and_exts('comic2lrf', comicop, ['cbz', 'cbr']))
|
f.write(opts_and_exts('comic2lrf', comicop, ['cbz', 'cbr']))
|
||||||
f.write(opts_and_exts('comic2epub', comic2epub, ['cbz', 'cbr']))
|
f.write(opts_and_exts('comic2epub', comic2epub, ['cbz', 'cbr']))
|
||||||
f.write(opts_and_exts('comic2mobi', comic2epub, ['cbz', 'cbr']))
|
f.write(opts_and_exts('comic2mobi', comic2epub, ['cbz', 'cbr']))
|
||||||
@ -227,9 +191,6 @@ def setup_completion(fatal_errors):
|
|||||||
f.write(opts_and_words('feeds2lrf', feeds2lrf, feed_titles))
|
f.write(opts_and_words('feeds2lrf', feeds2lrf, feed_titles))
|
||||||
f.write(opts_and_words('feeds2epub', feeds2epub, feed_titles))
|
f.write(opts_and_words('feeds2epub', feeds2epub, feed_titles))
|
||||||
f.write(opts_and_words('feeds2mobi', feeds2mobi, feed_titles))
|
f.write(opts_and_words('feeds2mobi', feeds2mobi, feed_titles))
|
||||||
f.write(opts_and_exts('html2epub', html2epub, ['html', 'htm', 'xhtm', 'xhtml', 'opf']))
|
|
||||||
f.write(opts_and_exts('html2oeb', html2oeb, ['html', 'htm', 'xhtm', 'xhtml']))
|
|
||||||
f.write(opts_and_exts('odt2oeb', odt2oeb, ['odt']))
|
|
||||||
f.write('''
|
f.write('''
|
||||||
_prs500_ls()
|
_prs500_ls()
|
||||||
{
|
{
|
||||||
@ -392,43 +353,27 @@ def option_parser():
|
|||||||
help='Save a manifest of all installed files to the specified location')
|
help='Save a manifest of all installed files to the specified location')
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
def install_man_pages(fatal_errors):
|
def install_man_pages(fatal_errors, use_destdir=False):
|
||||||
from bz2 import compress
|
from calibre.utils.help2man import create_man_page
|
||||||
import subprocess
|
prefix = os.environ.get('DESTDIR', '/') if use_destdir else '/'
|
||||||
|
manpath = os.path.join(prefix, 'usr/share/man/man1')
|
||||||
|
if not os.path.exists(manpath):
|
||||||
|
os.makedirs(manpath)
|
||||||
print 'Installing MAN pages...'
|
print 'Installing MAN pages...'
|
||||||
manpath = '/usr/share/man/man1'
|
|
||||||
f = NamedTemporaryFile()
|
|
||||||
f.write('[see also]\nhttp://%s.kovidgoyal.net\n'%__appname__)
|
|
||||||
f.flush()
|
|
||||||
manifest = []
|
manifest = []
|
||||||
os.environ['PATH'] += ':'+os.path.expanduser('~/bin')
|
|
||||||
for src in entry_points['console_scripts']:
|
for src in entry_points['console_scripts']:
|
||||||
prog = src[:src.index('=')].strip()
|
prog, right = src.split('=')
|
||||||
if prog in ('ebook-device', 'markdown-calibre',
|
prog = prog.strip()
|
||||||
'calibre-fontconfig', 'calibre-parallel'):
|
module = __import__(right.split(':')[0].strip(), fromlist=['a'])
|
||||||
|
parser = getattr(module, 'option_parser', None)
|
||||||
|
if parser is None:
|
||||||
continue
|
continue
|
||||||
|
parser = parser()
|
||||||
help2man = ('help2man', prog, '--name', 'part of %s'%__appname__,
|
raw = create_man_page(prog, parser)
|
||||||
'--section', '1', '--no-info', '--include',
|
|
||||||
f.name, '--manual', __appname__)
|
|
||||||
manfile = os.path.join(manpath, prog+'.1'+__appname__+'.bz2')
|
manfile = os.path.join(manpath, prog+'.1'+__appname__+'.bz2')
|
||||||
print '\tInstalling MAN page for', prog
|
print '\tInstalling MAN page for', prog
|
||||||
try:
|
open(manfile, 'wb').write(raw)
|
||||||
p = subprocess.Popen(help2man, stdout=subprocess.PIPE)
|
manifest.append(manfile)
|
||||||
except OSError, err:
|
|
||||||
import errno
|
|
||||||
if err.errno != errno.ENOENT:
|
|
||||||
raise
|
|
||||||
print 'Failed to install MAN pages as help2man is missing from your system'
|
|
||||||
break
|
|
||||||
o = p.stdout.read()
|
|
||||||
raw = re.compile(r'^\.IP\s*^([A-Z :]+)$', re.MULTILINE).sub(r'.SS\n\1', o)
|
|
||||||
if not raw.strip():
|
|
||||||
print 'Unable to create MAN page for', prog
|
|
||||||
continue
|
|
||||||
f2 = open_file(manfile)
|
|
||||||
manifest.append(f2.name)
|
|
||||||
f2.write(compress(raw))
|
|
||||||
return manifest
|
return manifest
|
||||||
|
|
||||||
def post_install():
|
def post_install():
|
||||||
@ -440,9 +385,9 @@ def post_install():
|
|||||||
manifest = []
|
manifest = []
|
||||||
setup_desktop_integration(opts.fatal_errors)
|
setup_desktop_integration(opts.fatal_errors)
|
||||||
if opts.no_root or os.geteuid() == 0:
|
if opts.no_root or os.geteuid() == 0:
|
||||||
|
manifest += install_man_pages(opts.fatal_errors, use_destdir)
|
||||||
manifest += setup_udev_rules(opts.group_file, not opts.dont_reload, opts.fatal_errors)
|
manifest += setup_udev_rules(opts.group_file, not opts.dont_reload, opts.fatal_errors)
|
||||||
manifest += setup_completion(opts.fatal_errors)
|
manifest += setup_completion(opts.fatal_errors)
|
||||||
manifest += install_man_pages(opts.fatal_errors)
|
|
||||||
else:
|
else:
|
||||||
print "Skipping udev, completion, and man-page install for non-root user."
|
print "Skipping udev, completion, and man-page install for non-root user."
|
||||||
|
|
||||||
|
@ -34,6 +34,8 @@ What formats does |app| support conversion to/from?
|
|||||||
| | | | | |
|
| | | | | |
|
||||||
| | ODT | ✔ | ✔ | ✔ |
|
| | ODT | ✔ | ✔ | ✔ |
|
||||||
| | | | | |
|
| | | | | |
|
||||||
|
| | FB2 | ✔ | ✔ | ✔ |
|
||||||
|
| | | | | |
|
||||||
| | HTML | ✔ | ✔ | ✔ |
|
| | HTML | ✔ | ✔ | ✔ |
|
||||||
| | | | | |
|
| | | | | |
|
||||||
| **Input formats** | CBR | ✔ | ✔ | ✔ |
|
| **Input formats** | CBR | ✔ | ✔ | ✔ |
|
||||||
@ -121,13 +123,12 @@ turned into a collection on the reader. Note that the PRS-500 does not support c
|
|||||||
How do I use |app| with my iPhone?
|
How do I use |app| with my iPhone?
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
First install the Stanza reader on your iPhone from http://www.lexcycle.com . Then,
|
First install the Stanza reader on your iPhone from http://www.lexcycle.com . Then,
|
||||||
* Set the output format for calibre to EPUB (this can be done in the configuration dialog accessed by the little hammer icon next to the search bar)
|
|
||||||
|
* Set the output format for calibre to EPUB (The output format can be set next to the big red heart)
|
||||||
* Convert the books you want to read on your iPhone to EPUB format by selecting them and clicking the Convert button.
|
* Convert the books you want to read on your iPhone to EPUB format by selecting them and clicking the Convert button.
|
||||||
* Turn on the Content Server in the configurations dialog and leave |app| running.
|
* Turn on the Content Server in |app|'s preferences and leave |app| running.
|
||||||
* In the Stanza reader on your iPhone, add a new catalog. The URL of the catalog is of the form
|
|
||||||
``http://10.34.56.89:8080/stanza``, where you should replace the IP address ``10.34.56.89``
|
Now you should be able to access your books on your iPhone.
|
||||||
with the IP address of your computer. Stanza will the use the |app| content server to access all the
|
|
||||||
EPUB books in your |app| database.
|
|
||||||
|
|
||||||
Library Management
|
Library Management
|
||||||
------------------
|
------------------
|
||||||
|
@ -227,7 +227,8 @@ class WorkerMother(object):
|
|||||||
return env
|
return env
|
||||||
|
|
||||||
def spawn_free_spirit_osx(self, arg, type='free_spirit'):
|
def spawn_free_spirit_osx(self, arg, type='free_spirit'):
|
||||||
script = 'from calibre.parallel import main; main(args=["calibre-parallel", %s]);'%repr(arg)
|
script = ('from calibre.parallel import main; '
|
||||||
|
'main(args=["calibre-parallel", %s]);')%repr(arg)
|
||||||
exe = self.gui_executable if type == 'free_spirit' else self.executable
|
exe = self.gui_executable if type == 'free_spirit' else self.executable
|
||||||
cmdline = [exe, '-c', self.prefix+script]
|
cmdline = [exe, '-c', self.prefix+script]
|
||||||
child = WorkerStatus(subprocess.Popen(cmdline, env=self.get_env()))
|
child = WorkerStatus(subprocess.Popen(cmdline, env=self.get_env()))
|
||||||
|
@ -196,7 +196,7 @@ class Server(object):
|
|||||||
|
|
||||||
def calculate_month_trend(self, days=31):
|
def calculate_month_trend(self, days=31):
|
||||||
stats = self.get_slice(date.today()-timedelta(days=days-1), date.today())
|
stats = self.get_slice(date.today()-timedelta(days=days-1), date.today())
|
||||||
fig = plt.figure(2, (12, 4), 96)#, facecolor, edgecolor, frameon, FigureClass)
|
fig = plt.figure(2, (10, 4), 96)#, facecolor, edgecolor, frameon, FigureClass)
|
||||||
fig.clear()
|
fig.clear()
|
||||||
ax = fig.add_subplot(111)
|
ax = fig.add_subplot(111)
|
||||||
x = list(range(days-1, -1, -1))
|
x = list(range(days-1, -1, -1))
|
||||||
@ -216,7 +216,7 @@ Donors per day: %(dpd).2f
|
|||||||
ad=stats.average_deviation,
|
ad=stats.average_deviation,
|
||||||
dpd=len(stats.totals)/float(stats.period.days),
|
dpd=len(stats.totals)/float(stats.period.days),
|
||||||
)
|
)
|
||||||
text = ax.annotate(text, (0.6, 0.65), textcoords='axes fraction')
|
text = ax.annotate(text, (0.5, 0.65), textcoords='axes fraction')
|
||||||
fig.savefig(self.MONTH_TRENDS)
|
fig.savefig(self.MONTH_TRENDS)
|
||||||
|
|
||||||
def calculate_trend(self):
|
def calculate_trend(self):
|
||||||
|
@ -18,7 +18,6 @@ DEPENDENCIES = [
|
|||||||
('lxml', '2.1.5', 'lxml', 'python-lxml', 'python-lxml'),
|
('lxml', '2.1.5', 'lxml', 'python-lxml', 'python-lxml'),
|
||||||
('python-dateutil', '1.4.1', 'python-dateutil', 'python-dateutil', 'python-dateutil'),
|
('python-dateutil', '1.4.1', 'python-dateutil', 'python-dateutil', 'python-dateutil'),
|
||||||
('BeautifulSoup', '3.0.5', 'beautifulsoup', 'python-beautifulsoup', 'python-BeautifulSoup'),
|
('BeautifulSoup', '3.0.5', 'beautifulsoup', 'python-beautifulsoup', 'python-BeautifulSoup'),
|
||||||
('help2man', '1.36.4', 'help2man', 'help2man', 'help2man'),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -34,9 +33,10 @@ def get_linux_data(version='1.0.0'):
|
|||||||
data['title'] = 'Download calibre for linux'
|
data['title'] = 'Download calibre for linux'
|
||||||
data['supported'] = []
|
data['supported'] = []
|
||||||
for name, title in [
|
for name, title in [
|
||||||
('ubuntu', 'Ubuntu Jaunty Jackalope'),
|
|
||||||
('debian', 'Debian Sid'),
|
('debian', 'Debian Sid'),
|
||||||
('exherbo', 'Exherbo'),
|
('exherbo', 'Exherbo'),
|
||||||
|
('foresight', 'Foresight 2.1'),
|
||||||
|
('ubuntu', 'Ubuntu Jaunty Jackalope'),
|
||||||
]:
|
]:
|
||||||
data['supported'].append(CoolDistro(name, title,
|
data['supported'].append(CoolDistro(name, title,
|
||||||
prefix='http://calibre.kovidgoyal.net'))
|
prefix='http://calibre.kovidgoyal.net'))
|
||||||
@ -177,11 +177,11 @@ else:
|
|||||||
compatibility='%s works on OS X Tiger and above.'%(__appname__,),
|
compatibility='%s works on OS X Tiger and above.'%(__appname__,),
|
||||||
path=MOBILEREAD+file, app=__appname__,
|
path=MOBILEREAD+file, app=__appname__,
|
||||||
note=Markup(\
|
note=Markup(\
|
||||||
'''
|
u'''
|
||||||
<ol>
|
<ol>
|
||||||
<li>Before trying to use the command line tools, you must run the app at least once. This will ask you for you password and then setup the symbolic links for the command line tools.</li>
|
<li>Before trying to use the command line tools, you must run the app at least once. This will ask you for you password and then setup the symbolic links for the command line tools.</li>
|
||||||
<li>The app cannot be run from within the dmg. You must drag it to a folder on your filesystem (The Desktop, Applications, wherever).</li>
|
<li>The app cannot be run from within the dmg. You must drag it to a folder on your filesystem (The Desktop, Applications, wherever).</li>
|
||||||
<li>In order for localization of the user interface in your language, select your language in the configuration dialog (by clicking the hammer icon next to the search bar) and select your language.</li>
|
<li>In order for localization of the user interface in your language, select your language in the preferences (by pressing u\2318+P) and select your language.</li>
|
||||||
</ol>
|
</ol>
|
||||||
'''))
|
'''))
|
||||||
return 'binary.html', data, None
|
return 'binary.html', data, None
|
||||||
|
@ -88,7 +88,7 @@ sudo python -c "import urllib2; exec urllib2.urlopen('http://calibre.kovidgoyal.
|
|||||||
be ignored.
|
be ignored.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
You must have help2man and xdg-utils installed
|
You must have xdg-utils installed
|
||||||
on your system before running the installer.
|
on your system before running the installer.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1578
src/calibre/utils/Zeroconf.py
Executable file
1578
src/calibre/utils/Zeroconf.py
Executable file
File diff suppressed because it is too large
Load Diff
59
src/calibre/utils/help2man.py
Normal file
59
src/calibre/utils/help2man.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
from __future__ import with_statement
|
||||||
|
__license__ = 'GPL 3'
|
||||||
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import time, bz2
|
||||||
|
|
||||||
|
from calibre.constants import __version__, __appname__, __author__
|
||||||
|
|
||||||
|
|
||||||
|
def create_man_page(prog, parser):
|
||||||
|
usage = parser.usage.splitlines()
|
||||||
|
for i, line in enumerate(list(usage)):
|
||||||
|
if not line.strip():
|
||||||
|
usage[i] = '.PP'
|
||||||
|
else:
|
||||||
|
usage[i] = line.replace('%prog', prog)
|
||||||
|
lines = [
|
||||||
|
'.TH ' + prog.upper() + ' "1" ' + time.strftime('"%B %Y"') +
|
||||||
|
' "%s (%s %s)" "%s"'%(prog, __appname__, __version__, __appname__),
|
||||||
|
'.SH NAME',
|
||||||
|
prog + r' \- part of '+__appname__,
|
||||||
|
'.SH SYNOPSIS',
|
||||||
|
'.B "%s"'%prog + r'\fR '+' '.join(usage[0].split()[1:]),
|
||||||
|
'.SH DESCRIPTION',
|
||||||
|
]
|
||||||
|
lines += usage[1:]
|
||||||
|
|
||||||
|
lines += [
|
||||||
|
'.SH OPTIONS'
|
||||||
|
]
|
||||||
|
def format_option(opt):
|
||||||
|
ans = ['.TP']
|
||||||
|
opts = []
|
||||||
|
opts += opt._short_opts
|
||||||
|
opts.append(opt.get_opt_string())
|
||||||
|
opts = [r'\fB'+x.replace('-', r'\-')+r'\fR' for x in opts]
|
||||||
|
ans.append(', '.join(opts))
|
||||||
|
help = opt.help if opt.help else ''
|
||||||
|
ans.append(help.replace('%prog', prog).replace('%default', str(opt.default)))
|
||||||
|
return ans
|
||||||
|
|
||||||
|
for opt in parser.option_list:
|
||||||
|
lines.extend(format_option(opt))
|
||||||
|
for group in parser.option_groups:
|
||||||
|
lines.append('.SS '+group.title)
|
||||||
|
if group.description:
|
||||||
|
lines.extend(['.PP', group.description])
|
||||||
|
for opt in group.option_list:
|
||||||
|
lines.extend(format_option(opt))
|
||||||
|
|
||||||
|
lines += ['.SH SEE ALSO',
|
||||||
|
'The User Manual is available at '
|
||||||
|
'http://calibre.kovidgoyal.net/user_manual',
|
||||||
|
'.PP', '.B Created by '+__author__]
|
||||||
|
|
||||||
|
return bz2.compress('\n'.join(lines))
|
||||||
|
|
||||||
|
|
@ -13,13 +13,25 @@ ERROR = 3
|
|||||||
import sys, traceback
|
import sys, traceback
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from calibre import prints
|
|
||||||
from calibre.utils.terminfo import TerminalController
|
|
||||||
|
|
||||||
class ANSIStream:
|
|
||||||
|
|
||||||
|
class Stream(object):
|
||||||
|
|
||||||
|
def __init__(self, stream):
|
||||||
|
from calibre import prints
|
||||||
|
self._prints = prints
|
||||||
|
self.stream = stream
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
self.stream.flush()
|
||||||
|
|
||||||
|
|
||||||
|
class ANSIStream(Stream):
|
||||||
|
|
||||||
def __init__(self, stream=sys.stdout):
|
def __init__(self, stream=sys.stdout):
|
||||||
self.stream = stream
|
Stream.__init__(self, stream)
|
||||||
|
from calibre.utils.terminfo import TerminalController
|
||||||
tc = TerminalController(stream)
|
tc = TerminalController(stream)
|
||||||
self.color = {
|
self.color = {
|
||||||
DEBUG: tc.GREEN,
|
DEBUG: tc.GREEN,
|
||||||
@ -32,16 +44,16 @@ class ANSIStream:
|
|||||||
def prints(self, level, *args, **kwargs):
|
def prints(self, level, *args, **kwargs):
|
||||||
self.stream.write(self.color[level])
|
self.stream.write(self.color[level])
|
||||||
kwargs['file'] = self.stream
|
kwargs['file'] = self.stream
|
||||||
prints(*args, **kwargs)
|
self._prints(*args, **kwargs)
|
||||||
self.stream.write(self.normal)
|
self.stream.write(self.normal)
|
||||||
|
|
||||||
def flush(self):
|
def flush(self):
|
||||||
self.stream.flush()
|
self.stream.flush()
|
||||||
|
|
||||||
class HTMLStream:
|
class HTMLStream(Stream):
|
||||||
|
|
||||||
def __init__(self, stream=sys.stdout):
|
def __init__(self, stream=sys.stdout):
|
||||||
self.stream = stream
|
Stream.__init__(self, stream)
|
||||||
self.color = {
|
self.color = {
|
||||||
DEBUG: '<span style="color:green">',
|
DEBUG: '<span style="color:green">',
|
||||||
INFO:'<span>',
|
INFO:'<span>',
|
||||||
@ -53,7 +65,7 @@ class HTMLStream:
|
|||||||
def prints(self, level, *args, **kwargs):
|
def prints(self, level, *args, **kwargs):
|
||||||
self.stream.write(self.color[level])
|
self.stream.write(self.color[level])
|
||||||
kwargs['file'] = self.stream
|
kwargs['file'] = self.stream
|
||||||
prints(*args, **kwargs)
|
self._prints(*args, **kwargs)
|
||||||
self.stream.write(self.normal)
|
self.stream.write(self.normal)
|
||||||
|
|
||||||
def flush(self):
|
def flush(self):
|
||||||
|
60
src/calibre/utils/mdns.py
Normal file
60
src/calibre/utils/mdns.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
from __future__ import with_statement
|
||||||
|
__license__ = 'GPL 3'
|
||||||
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import socket
|
||||||
|
|
||||||
|
_server = None
|
||||||
|
|
||||||
|
def get_external_ip():
|
||||||
|
'Get IP address of interface used to connect to the outside world'
|
||||||
|
try:
|
||||||
|
ipaddr = socket.gethostbyname(socket.gethostname())
|
||||||
|
except:
|
||||||
|
ipaddr = '127.0.0.1'
|
||||||
|
if ipaddr == '127.0.0.1':
|
||||||
|
try:
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
s.connect(('google.com', 0))
|
||||||
|
ipaddr = s.getsockname()[0]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return ipaddr
|
||||||
|
|
||||||
|
def start_server():
|
||||||
|
global _server
|
||||||
|
if _server is None:
|
||||||
|
from calibre.utils.Zeroconf import Zeroconf
|
||||||
|
_server = Zeroconf()
|
||||||
|
return _server
|
||||||
|
|
||||||
|
def publish(desc, type, port, properties=None, add_hostname=True):
|
||||||
|
'''
|
||||||
|
Publish a service.
|
||||||
|
|
||||||
|
:param desc: Description of service
|
||||||
|
:param type: Name and type of service. For example _stanza._tcp
|
||||||
|
:param port: Port the service listens on
|
||||||
|
:param properties: An optional dictionary whose keys and values will be put
|
||||||
|
into the TXT record.
|
||||||
|
'''
|
||||||
|
port = int(port)
|
||||||
|
server = start_server()
|
||||||
|
hostname = socket.gethostname().partition('.')[0]
|
||||||
|
if add_hostname:
|
||||||
|
desc += ' (on %s)'%hostname
|
||||||
|
local_ip = get_external_ip()
|
||||||
|
type = type+'.local.'
|
||||||
|
from calibre.utils.Zeroconf import ServiceInfo
|
||||||
|
service = ServiceInfo(type, desc+'.'+type,
|
||||||
|
address=socket.inet_aton(local_ip),
|
||||||
|
port=port,
|
||||||
|
properties=properties,
|
||||||
|
server=hostname+'.local.')
|
||||||
|
server.registerService(service)
|
||||||
|
|
||||||
|
def stop_server():
|
||||||
|
global _server
|
||||||
|
if _server is not None:
|
||||||
|
_server.close()
|
443
src/calibre/utils/rss_gen.py
Normal file
443
src/calibre/utils/rss_gen.py
Normal file
@ -0,0 +1,443 @@
|
|||||||
|
"""PyRSS2Gen - A Python library for generating RSS 2.0 feeds."""
|
||||||
|
|
||||||
|
__name__ = "PyRSS2Gen"
|
||||||
|
__version__ = (1, 0, 0)
|
||||||
|
__author__ = "Andrew Dalke <dalke@dalkescientific.com>"
|
||||||
|
|
||||||
|
_generator_name = __name__ + "-" + ".".join(map(str, __version__))
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
# Could make this the base class; will need to add 'publish'
|
||||||
|
class WriteXmlMixin:
|
||||||
|
def write_xml(self, outfile, encoding = "iso-8859-1"):
|
||||||
|
from xml.sax import saxutils
|
||||||
|
handler = saxutils.XMLGenerator(outfile, encoding)
|
||||||
|
handler.startDocument()
|
||||||
|
self.publish(handler)
|
||||||
|
handler.endDocument()
|
||||||
|
|
||||||
|
def to_xml(self, encoding = "iso-8859-1"):
|
||||||
|
try:
|
||||||
|
import cStringIO as StringIO
|
||||||
|
except ImportError:
|
||||||
|
import StringIO
|
||||||
|
f = StringIO.StringIO()
|
||||||
|
self.write_xml(f, encoding)
|
||||||
|
return f.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def _element(handler, name, obj, d = {}):
|
||||||
|
if isinstance(obj, basestring) or obj is None:
|
||||||
|
# special-case handling to make the API easier
|
||||||
|
# to use for the common case.
|
||||||
|
handler.startElement(name, d)
|
||||||
|
if obj is not None:
|
||||||
|
handler.characters(obj)
|
||||||
|
handler.endElement(name)
|
||||||
|
else:
|
||||||
|
# It better know how to emit the correct XML.
|
||||||
|
obj.publish(handler)
|
||||||
|
|
||||||
|
def _opt_element(handler, name, obj):
|
||||||
|
if obj is None:
|
||||||
|
return
|
||||||
|
_element(handler, name, obj)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_date(dt):
|
||||||
|
"""convert a datetime into an RFC 822 formatted date
|
||||||
|
|
||||||
|
Input date must be in GMT.
|
||||||
|
"""
|
||||||
|
# Looks like:
|
||||||
|
# Sat, 07 Sep 2002 00:00:01 GMT
|
||||||
|
# Can't use strftime because that's locale dependent
|
||||||
|
#
|
||||||
|
# Isn't there a standard way to do this for Python? The
|
||||||
|
# rfc822 and email.Utils modules assume a timestamp. The
|
||||||
|
# following is based on the rfc822 module.
|
||||||
|
return "%s, %02d %s %04d %02d:%02d:%02d GMT" % (
|
||||||
|
["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][dt.weekday()],
|
||||||
|
dt.day,
|
||||||
|
["Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
||||||
|
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][dt.month-1],
|
||||||
|
dt.year, dt.hour, dt.minute, dt.second)
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# A couple simple wrapper objects for the fields which
|
||||||
|
# take a simple value other than a string.
|
||||||
|
class IntElement:
|
||||||
|
"""implements the 'publish' API for integers
|
||||||
|
|
||||||
|
Takes the tag name and the integer value to publish.
|
||||||
|
|
||||||
|
(Could be used for anything which uses str() to be published
|
||||||
|
to text for XML.)
|
||||||
|
"""
|
||||||
|
element_attrs = {}
|
||||||
|
def __init__(self, name, val):
|
||||||
|
self.name = name
|
||||||
|
self.val = val
|
||||||
|
def publish(self, handler):
|
||||||
|
handler.startElement(self.name, self.element_attrs)
|
||||||
|
handler.characters(str(self.val))
|
||||||
|
handler.endElement(self.name)
|
||||||
|
|
||||||
|
class DateElement:
|
||||||
|
"""implements the 'publish' API for a datetime.datetime
|
||||||
|
|
||||||
|
Takes the tag name and the datetime to publish.
|
||||||
|
|
||||||
|
Converts the datetime to RFC 2822 timestamp (4-digit year).
|
||||||
|
"""
|
||||||
|
def __init__(self, name, dt):
|
||||||
|
self.name = name
|
||||||
|
self.dt = dt
|
||||||
|
def publish(self, handler):
|
||||||
|
_element(handler, self.name, _format_date(self.dt))
|
||||||
|
####
|
||||||
|
|
||||||
|
class Category:
|
||||||
|
"""Publish a category element"""
|
||||||
|
def __init__(self, category, domain = None):
|
||||||
|
self.category = category
|
||||||
|
self.domain = domain
|
||||||
|
def publish(self, handler):
|
||||||
|
d = {}
|
||||||
|
if self.domain is not None:
|
||||||
|
d["domain"] = self.domain
|
||||||
|
_element(handler, "category", self.category, d)
|
||||||
|
|
||||||
|
class Cloud:
|
||||||
|
"""Publish a cloud"""
|
||||||
|
def __init__(self, domain, port, path,
|
||||||
|
registerProcedure, protocol):
|
||||||
|
self.domain = domain
|
||||||
|
self.port = port
|
||||||
|
self.path = path
|
||||||
|
self.registerProcedure = registerProcedure
|
||||||
|
self.protocol = protocol
|
||||||
|
def publish(self, handler):
|
||||||
|
_element(handler, "cloud", None, {
|
||||||
|
"domain": self.domain,
|
||||||
|
"port": str(self.port),
|
||||||
|
"path": self.path,
|
||||||
|
"registerProcedure": self.registerProcedure,
|
||||||
|
"protocol": self.protocol})
|
||||||
|
|
||||||
|
class Image:
|
||||||
|
"""Publish a channel Image"""
|
||||||
|
element_attrs = {}
|
||||||
|
def __init__(self, url, title, link,
|
||||||
|
width = None, height = None, description = None):
|
||||||
|
self.url = url
|
||||||
|
self.title = title
|
||||||
|
self.link = link
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.description = description
|
||||||
|
|
||||||
|
def publish(self, handler):
|
||||||
|
handler.startElement("image", self.element_attrs)
|
||||||
|
|
||||||
|
_element(handler, "url", self.url)
|
||||||
|
_element(handler, "title", self.title)
|
||||||
|
_element(handler, "link", self.link)
|
||||||
|
|
||||||
|
width = self.width
|
||||||
|
if isinstance(width, int):
|
||||||
|
width = IntElement("width", width)
|
||||||
|
_opt_element(handler, "width", width)
|
||||||
|
|
||||||
|
height = self.height
|
||||||
|
if isinstance(height, int):
|
||||||
|
height = IntElement("height", height)
|
||||||
|
_opt_element(handler, "height", height)
|
||||||
|
|
||||||
|
_opt_element(handler, "description", self.description)
|
||||||
|
|
||||||
|
handler.endElement("image")
|
||||||
|
|
||||||
|
class Guid:
|
||||||
|
"""Publish a guid
|
||||||
|
|
||||||
|
Defaults to being a permalink, which is the assumption if it's
|
||||||
|
omitted. Hence strings are always permalinks.
|
||||||
|
"""
|
||||||
|
def __init__(self, guid, isPermaLink = 1):
|
||||||
|
self.guid = guid
|
||||||
|
self.isPermaLink = isPermaLink
|
||||||
|
def publish(self, handler):
|
||||||
|
d = {}
|
||||||
|
if self.isPermaLink:
|
||||||
|
d["isPermaLink"] = "true"
|
||||||
|
else:
|
||||||
|
d["isPermaLink"] = "false"
|
||||||
|
_element(handler, "guid", self.guid, d)
|
||||||
|
|
||||||
|
class TextInput:
|
||||||
|
"""Publish a textInput
|
||||||
|
|
||||||
|
Apparently this is rarely used.
|
||||||
|
"""
|
||||||
|
element_attrs = {}
|
||||||
|
def __init__(self, title, description, name, link):
|
||||||
|
self.title = title
|
||||||
|
self.description = description
|
||||||
|
self.name = name
|
||||||
|
self.link = link
|
||||||
|
|
||||||
|
def publish(self, handler):
|
||||||
|
handler.startElement("textInput", self.element_attrs)
|
||||||
|
_element(handler, "title", self.title)
|
||||||
|
_element(handler, "description", self.description)
|
||||||
|
_element(handler, "name", self.name)
|
||||||
|
_element(handler, "link", self.link)
|
||||||
|
handler.endElement("textInput")
|
||||||
|
|
||||||
|
|
||||||
|
class Enclosure:
|
||||||
|
"""Publish an enclosure"""
|
||||||
|
def __init__(self, url, length, type):
|
||||||
|
self.url = url
|
||||||
|
self.length = length
|
||||||
|
self.type = type
|
||||||
|
def publish(self, handler):
|
||||||
|
_element(handler, "enclosure", None,
|
||||||
|
{"url": self.url,
|
||||||
|
"length": str(self.length),
|
||||||
|
"type": self.type,
|
||||||
|
})
|
||||||
|
|
||||||
|
class Source:
|
||||||
|
"""Publish the item's original source, used by aggregators"""
|
||||||
|
def __init__(self, name, url):
|
||||||
|
self.name = name
|
||||||
|
self.url = url
|
||||||
|
def publish(self, handler):
|
||||||
|
_element(handler, "source", self.name, {"url": self.url})
|
||||||
|
|
||||||
|
class SkipHours:
|
||||||
|
"""Publish the skipHours
|
||||||
|
|
||||||
|
This takes a list of hours, as integers.
|
||||||
|
"""
|
||||||
|
element_attrs = {}
|
||||||
|
def __init__(self, hours):
|
||||||
|
self.hours = hours
|
||||||
|
def publish(self, handler):
|
||||||
|
if self.hours:
|
||||||
|
handler.startElement("skipHours", self.element_attrs)
|
||||||
|
for hour in self.hours:
|
||||||
|
_element(handler, "hour", str(hour))
|
||||||
|
handler.endElement("skipHours")
|
||||||
|
|
||||||
|
class SkipDays:
|
||||||
|
"""Publish the skipDays
|
||||||
|
|
||||||
|
This takes a list of days as strings.
|
||||||
|
"""
|
||||||
|
element_attrs = {}
|
||||||
|
def __init__(self, days):
|
||||||
|
self.days = days
|
||||||
|
def publish(self, handler):
|
||||||
|
if self.days:
|
||||||
|
handler.startElement("skipDays", self.element_attrs)
|
||||||
|
for day in self.days:
|
||||||
|
_element(handler, "day", day)
|
||||||
|
handler.endElement("skipDays")
|
||||||
|
|
||||||
|
class RSS2(WriteXmlMixin):
|
||||||
|
"""The main RSS class.
|
||||||
|
|
||||||
|
Stores the channel attributes, with the "category" elements under
|
||||||
|
".categories" and the RSS items under ".items".
|
||||||
|
"""
|
||||||
|
|
||||||
|
rss_attrs = {"version": "2.0"}
|
||||||
|
element_attrs = {}
|
||||||
|
def __init__(self,
|
||||||
|
title,
|
||||||
|
link,
|
||||||
|
description,
|
||||||
|
|
||||||
|
language = None,
|
||||||
|
copyright = None,
|
||||||
|
managingEditor = None,
|
||||||
|
webMaster = None,
|
||||||
|
pubDate = None, # a datetime, *in* *GMT*
|
||||||
|
lastBuildDate = None, # a datetime
|
||||||
|
|
||||||
|
categories = None, # list of strings or Category
|
||||||
|
generator = _generator_name,
|
||||||
|
docs = "http://blogs.law.harvard.edu/tech/rss",
|
||||||
|
cloud = None, # a Cloud
|
||||||
|
ttl = None, # integer number of minutes
|
||||||
|
|
||||||
|
image = None, # an Image
|
||||||
|
rating = None, # a string; I don't know how it's used
|
||||||
|
textInput = None, # a TextInput
|
||||||
|
skipHours = None, # a SkipHours with a list of integers
|
||||||
|
skipDays = None, # a SkipDays with a list of strings
|
||||||
|
|
||||||
|
items = None, # list of RSSItems
|
||||||
|
):
|
||||||
|
self.title = title
|
||||||
|
self.link = link
|
||||||
|
self.description = description
|
||||||
|
self.language = language
|
||||||
|
self.copyright = copyright
|
||||||
|
self.managingEditor = managingEditor
|
||||||
|
|
||||||
|
self.webMaster = webMaster
|
||||||
|
self.pubDate = pubDate
|
||||||
|
self.lastBuildDate = lastBuildDate
|
||||||
|
|
||||||
|
if categories is None:
|
||||||
|
categories = []
|
||||||
|
self.categories = categories
|
||||||
|
self.generator = generator
|
||||||
|
self.docs = docs
|
||||||
|
self.cloud = cloud
|
||||||
|
self.ttl = ttl
|
||||||
|
self.image = image
|
||||||
|
self.rating = rating
|
||||||
|
self.textInput = textInput
|
||||||
|
self.skipHours = skipHours
|
||||||
|
self.skipDays = skipDays
|
||||||
|
|
||||||
|
if items is None:
|
||||||
|
items = []
|
||||||
|
self.items = items
|
||||||
|
|
||||||
|
def publish(self, handler):
|
||||||
|
handler.startElement("rss", self.rss_attrs)
|
||||||
|
handler.startElement("channel", self.element_attrs)
|
||||||
|
_element(handler, "title", self.title)
|
||||||
|
_element(handler, "link", self.link)
|
||||||
|
_element(handler, "description", self.description)
|
||||||
|
|
||||||
|
self.publish_extensions(handler)
|
||||||
|
|
||||||
|
_opt_element(handler, "language", self.language)
|
||||||
|
_opt_element(handler, "copyright", self.copyright)
|
||||||
|
_opt_element(handler, "managingEditor", self.managingEditor)
|
||||||
|
_opt_element(handler, "webMaster", self.webMaster)
|
||||||
|
|
||||||
|
pubDate = self.pubDate
|
||||||
|
if isinstance(pubDate, datetime.datetime):
|
||||||
|
pubDate = DateElement("pubDate", pubDate)
|
||||||
|
_opt_element(handler, "pubDate", pubDate)
|
||||||
|
|
||||||
|
lastBuildDate = self.lastBuildDate
|
||||||
|
if isinstance(lastBuildDate, datetime.datetime):
|
||||||
|
lastBuildDate = DateElement("lastBuildDate", lastBuildDate)
|
||||||
|
_opt_element(handler, "lastBuildDate", lastBuildDate)
|
||||||
|
|
||||||
|
for category in self.categories:
|
||||||
|
if isinstance(category, basestring):
|
||||||
|
category = Category(category)
|
||||||
|
category.publish(handler)
|
||||||
|
|
||||||
|
_opt_element(handler, "generator", self.generator)
|
||||||
|
_opt_element(handler, "docs", self.docs)
|
||||||
|
|
||||||
|
if self.cloud is not None:
|
||||||
|
self.cloud.publish(handler)
|
||||||
|
|
||||||
|
ttl = self.ttl
|
||||||
|
if isinstance(self.ttl, int):
|
||||||
|
ttl = IntElement("ttl", ttl)
|
||||||
|
_opt_element(handler, "tt", ttl)
|
||||||
|
|
||||||
|
if self.image is not None:
|
||||||
|
self.image.publish(handler)
|
||||||
|
|
||||||
|
_opt_element(handler, "rating", self.rating)
|
||||||
|
if self.textInput is not None:
|
||||||
|
self.textInput.publish(handler)
|
||||||
|
if self.skipHours is not None:
|
||||||
|
self.skipHours.publish(handler)
|
||||||
|
if self.skipDays is not None:
|
||||||
|
self.skipDays.publish(handler)
|
||||||
|
|
||||||
|
for item in self.items:
|
||||||
|
item.publish(handler)
|
||||||
|
|
||||||
|
handler.endElement("channel")
|
||||||
|
handler.endElement("rss")
|
||||||
|
|
||||||
|
def publish_extensions(self, handler):
|
||||||
|
# Derived classes can hook into this to insert
|
||||||
|
# output after the three required fields.
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class RSSItem(WriteXmlMixin):
|
||||||
|
"""Publish an RSS Item"""
|
||||||
|
element_attrs = {}
|
||||||
|
def __init__(self,
|
||||||
|
title = None, # string
|
||||||
|
link = None, # url as string
|
||||||
|
description = None, # string
|
||||||
|
author = None, # email address as string
|
||||||
|
categories = None, # list of string or Category
|
||||||
|
comments = None, # url as string
|
||||||
|
enclosure = None, # an Enclosure
|
||||||
|
guid = None, # a unique string
|
||||||
|
pubDate = None, # a datetime
|
||||||
|
source = None, # a Source
|
||||||
|
):
|
||||||
|
|
||||||
|
if title is None and description is None:
|
||||||
|
raise TypeError(
|
||||||
|
"must define at least one of 'title' or 'description'")
|
||||||
|
self.title = title
|
||||||
|
self.link = link
|
||||||
|
self.description = description
|
||||||
|
self.author = author
|
||||||
|
if categories is None:
|
||||||
|
categories = []
|
||||||
|
self.categories = categories
|
||||||
|
self.comments = comments
|
||||||
|
self.enclosure = enclosure
|
||||||
|
self.guid = guid
|
||||||
|
self.pubDate = pubDate
|
||||||
|
self.source = source
|
||||||
|
# It sure does get tedious typing these names three times...
|
||||||
|
|
||||||
|
def publish(self, handler):
|
||||||
|
handler.startElement("item", self.element_attrs)
|
||||||
|
_opt_element(handler, "title", self.title)
|
||||||
|
_opt_element(handler, "link", self.link)
|
||||||
|
self.publish_extensions(handler)
|
||||||
|
_opt_element(handler, "description", self.description)
|
||||||
|
_opt_element(handler, "author", self.author)
|
||||||
|
|
||||||
|
for category in self.categories:
|
||||||
|
if isinstance(category, basestring):
|
||||||
|
category = Category(category)
|
||||||
|
category.publish(handler)
|
||||||
|
|
||||||
|
_opt_element(handler, "comments", self.comments)
|
||||||
|
if self.enclosure is not None:
|
||||||
|
self.enclosure.publish(handler)
|
||||||
|
_opt_element(handler, "guid", self.guid)
|
||||||
|
|
||||||
|
pubDate = self.pubDate
|
||||||
|
if isinstance(pubDate, datetime.datetime):
|
||||||
|
pubDate = DateElement("pubDate", pubDate)
|
||||||
|
_opt_element(handler, "pubDate", pubDate)
|
||||||
|
|
||||||
|
if self.source is not None:
|
||||||
|
self.source.publish(handler)
|
||||||
|
|
||||||
|
handler.endElement("item")
|
||||||
|
|
||||||
|
def publish_extensions(self, handler):
|
||||||
|
# Derived classes can hook into this to insert
|
||||||
|
# output after the title and link elements
|
||||||
|
pass
|
@ -156,7 +156,6 @@ class Feed(object):
|
|||||||
content = None
|
content = None
|
||||||
if not link and not content:
|
if not link and not content:
|
||||||
return
|
return
|
||||||
|
|
||||||
article = Article(id, title, link, description, published, content)
|
article = Article(id, title, link, description, published, content)
|
||||||
delta = datetime.utcnow() - article.utctime
|
delta = datetime.utcnow() - article.utctime
|
||||||
if delta.days*24*3600 + delta.seconds <= 24*3600*self.oldest_article:
|
if delta.days*24*3600 + delta.seconds <= 24*3600*self.oldest_article:
|
||||||
|
@ -17,7 +17,7 @@ from PyQt4.Qt import QApplication, QFile, Qt, QPalette, QSize, QImage, QPainter,
|
|||||||
from PyQt4.QtWebKit import QWebPage
|
from PyQt4.QtWebKit import QWebPage
|
||||||
|
|
||||||
|
|
||||||
from calibre import browser, __appname__, iswindows, LoggingInterface, \
|
from calibre import browser, __appname__, iswindows, \
|
||||||
strftime, __version__, preferred_encoding
|
strftime, __version__, preferred_encoding
|
||||||
from calibre.ebooks.BeautifulSoup import BeautifulSoup, NavigableString, CData, Tag
|
from calibre.ebooks.BeautifulSoup import BeautifulSoup, NavigableString, CData, Tag
|
||||||
from calibre.ebooks.metadata.opf2 import OPFCreator
|
from calibre.ebooks.metadata.opf2 import OPFCreator
|
||||||
@ -32,7 +32,7 @@ from calibre.ptempfile import PersistentTemporaryFile
|
|||||||
from calibre.gui2 import images_rc # Needed for default cover
|
from calibre.gui2 import images_rc # Needed for default cover
|
||||||
|
|
||||||
|
|
||||||
class BasicNewsRecipe(object, LoggingInterface):
|
class BasicNewsRecipe(object):
|
||||||
'''
|
'''
|
||||||
Abstract base class that contains logic needed in all feed fetchers.
|
Abstract base class that contains logic needed in all feed fetchers.
|
||||||
'''
|
'''
|
||||||
@ -444,7 +444,6 @@ class BasicNewsRecipe(object, LoggingInterface):
|
|||||||
:param parser: Command line option parser. Used to intelligently merge options.
|
:param parser: Command line option parser. Used to intelligently merge options.
|
||||||
:param progress_reporter: A Callable that takes two arguments: progress (a number between 0 and 1) and a string message. The message should be optional.
|
:param progress_reporter: A Callable that takes two arguments: progress (a number between 0 and 1) and a string message. The message should be optional.
|
||||||
'''
|
'''
|
||||||
LoggingInterface.__init__(self, logging.getLogger('feeds2disk'))
|
|
||||||
if not isinstance(self.title, unicode):
|
if not isinstance(self.title, unicode):
|
||||||
self.title = unicode(self.title, 'utf-8', 'replace')
|
self.title = unicode(self.title, 'utf-8', 'replace')
|
||||||
|
|
||||||
@ -1013,6 +1012,7 @@ class BasicNewsRecipe(object, LoggingInterface):
|
|||||||
parsed_feeds.append(feed)
|
parsed_feeds.append(feed)
|
||||||
self.log_exception(msg)
|
self.log_exception(msg)
|
||||||
|
|
||||||
|
|
||||||
return parsed_feeds
|
return parsed_feeds
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -5,8 +5,8 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
Builtin recipes.
|
Builtin recipes.
|
||||||
'''
|
'''
|
||||||
recipe_modules = ['recipe_' + r for r in (
|
recipe_modules = ['recipe_' + r for r in (
|
||||||
'newsweek', 'atlantic', 'economist', 'portfolio',
|
'newsweek', 'atlantic', 'economist', 'portfolio', 'the_register',
|
||||||
'nytimes', 'usatoday', 'outlook_india', 'bbc', 'greader', 'wsj',
|
'usatoday', 'outlook_india', 'bbc', 'greader', 'wsj',
|
||||||
'wired', 'globe_and_mail', 'smh', 'espn', 'business_week', 'miami_herald',
|
'wired', 'globe_and_mail', 'smh', 'espn', 'business_week', 'miami_herald',
|
||||||
'ars_technica', 'upi', 'new_yorker', 'irish_times', 'iht', 'lanacion',
|
'ars_technica', 'upi', 'new_yorker', 'irish_times', 'iht', 'lanacion',
|
||||||
'discover_magazine', 'scientific_american', 'new_york_review_of_books',
|
'discover_magazine', 'scientific_american', 'new_york_review_of_books',
|
||||||
@ -33,7 +33,8 @@ recipe_modules = ['recipe_' + r for r in (
|
|||||||
'la_republica', 'physics_today', 'chicago_tribune', 'e_novine',
|
'la_republica', 'physics_today', 'chicago_tribune', 'e_novine',
|
||||||
'al_jazeera', 'winsupersite', 'borba', 'courrierinternational',
|
'al_jazeera', 'winsupersite', 'borba', 'courrierinternational',
|
||||||
'lamujerdemivida', 'soldiers', 'theonion', 'news_times',
|
'lamujerdemivida', 'soldiers', 'theonion', 'news_times',
|
||||||
'el_universal', 'mediapart',
|
'el_universal', 'mediapart', 'wikinews_en', 'ecogeek', 'daily_mail',
|
||||||
|
'new_york_review_of_books_no_sub', 'politico',
|
||||||
)]
|
)]
|
||||||
|
|
||||||
import re, imp, inspect, time, os
|
import re, imp, inspect, time, os
|
||||||
@ -86,11 +87,15 @@ def compile_recipe(src):
|
|||||||
match = re.search(r'coding[:=]\s*([-\w.]+)', src[:200])
|
match = re.search(r'coding[:=]\s*([-\w.]+)', src[:200])
|
||||||
enc = match.group(1) if match else 'utf-8'
|
enc = match.group(1) if match else 'utf-8'
|
||||||
src = src.decode(enc)
|
src = src.decode(enc)
|
||||||
|
src = re.sub(r'from __future__.*', '', src)
|
||||||
f = open(temp, 'wb')
|
f = open(temp, 'wb')
|
||||||
src = 'from %s.web.feeds.news import BasicNewsRecipe, AutomaticNewsRecipe\n'%__appname__ + src
|
src = 'from %s.web.feeds.news import BasicNewsRecipe, AutomaticNewsRecipe\n'%__appname__ + src
|
||||||
src = 'from %s.ebooks.lrf.web.profiles import DefaultProfile, FullContentProfile\n'%__appname__ + src
|
src = 'from %s.ebooks.lrf.web.profiles import DefaultProfile, FullContentProfile\n'%__appname__ + src
|
||||||
src = '# coding: utf-8\n' + src
|
src = '# coding: utf-8\n' + src
|
||||||
f.write(src.replace('from libprs500', 'from calibre').encode('utf-8'))
|
src = 'from __future__ import with_statement\n' + src
|
||||||
|
|
||||||
|
src = src.replace('from libprs500', 'from calibre').encode('utf-8')
|
||||||
|
f.write(src)
|
||||||
f.close()
|
f.close()
|
||||||
module = imp.find_module(temp.namebase, [temp.dirname()])
|
module = imp.find_module(temp.namebase, [temp.dirname()])
|
||||||
module = imp.load_module(temp.namebase, *module)
|
module = imp.load_module(temp.namebase, *module)
|
||||||
|
33
src/calibre/web/feeds/recipes/recipe_daily_mail.py
Normal file
33
src/calibre/web/feeds/recipes/recipe_daily_mail.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class TheDailyMail(BasicNewsRecipe):
|
||||||
|
title = u'The Daily Mail'
|
||||||
|
oldest_article = 2
|
||||||
|
language = _('English')
|
||||||
|
author = 'RufusA'
|
||||||
|
simultaneous_downloads= 1
|
||||||
|
max_articles_per_feed = 50
|
||||||
|
|
||||||
|
extra_css = 'h1 {text-align: left;}'
|
||||||
|
|
||||||
|
remove_tags = [ dict(name='ul', attrs={'class':'article-icons-links'}) ]
|
||||||
|
remove_tags_after = dict(name='h3', attrs={'class':'social-links-title'})
|
||||||
|
remove_tags_before = dict(name='div', attrs={'id':'content'})
|
||||||
|
no_stylesheets = True
|
||||||
|
|
||||||
|
feeds = [
|
||||||
|
(u'Home', u'http://www.dailymail.co.uk/home/index.rss'),
|
||||||
|
(u'News', u'http://www.dailymail.co.uk/news/index.rss'),
|
||||||
|
(u'Sport', u'http://www.dailymail.co.uk/sport/index.rss'),
|
||||||
|
(u'TV and Showbiz', u'http://www.dailymail.co.uk/tvshowbiz/index.rss'),
|
||||||
|
(u'Femail', u'http://www.dailymail.co.uk/femail/index.rss'),
|
||||||
|
(u'Health', u'http://www.dailymail.co.uk/health/index.rss'),
|
||||||
|
(u'Science and Technology', u'http://www.dailymail.co.uk/sciencetech/index.rss'),
|
||||||
|
(u'Money', u'http://www.dailymail.co.uk/money/index.rss'),
|
||||||
|
(u'Property', u'http://www.dailymail.co.uk/property/index.rss'),
|
||||||
|
(u'Motoring', u'http://www.dailymail.co.uk/motoring/index.rss'),
|
||||||
|
(u'Travel', u'http://www.dailymail.co.uk/travel/index.rss')]
|
||||||
|
|
||||||
|
def print_version(self, url):
|
||||||
|
main = url.partition('?')[0]
|
||||||
|
return main + '?printingPage=true'
|
31
src/calibre/web/feeds/recipes/recipe_ecogeek.py
Normal file
31
src/calibre/web/feeds/recipes/recipe_ecogeek.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>'
|
||||||
|
'''
|
||||||
|
EcoGeek.org
|
||||||
|
'''
|
||||||
|
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class EcoGeek(BasicNewsRecipe):
|
||||||
|
title = 'EcoGeek'
|
||||||
|
__author__ = 'Darko Miletic'
|
||||||
|
description = 'EcoGeek - Technology for the Environment Blog Feed'
|
||||||
|
publisher = 'EcoGeek'
|
||||||
|
language = _('English')
|
||||||
|
category = 'news, ecology, blog'
|
||||||
|
oldest_article = 7
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
no_stylesheets = True
|
||||||
|
use_embedded_content = True
|
||||||
|
|
||||||
|
html2lrf_options = [
|
||||||
|
'--comment', description
|
||||||
|
, '--category', category
|
||||||
|
, '--publisher', publisher
|
||||||
|
]
|
||||||
|
|
||||||
|
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"'
|
||||||
|
|
||||||
|
feeds = [(u'Posts', u'http://feeds2.feedburner.com/EcoGeek')]
|
@ -3,6 +3,7 @@ __copyright__ = '2008, Derry FitzGerald'
|
|||||||
'''
|
'''
|
||||||
iht.com
|
iht.com
|
||||||
'''
|
'''
|
||||||
|
import re
|
||||||
|
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
from calibre.ptempfile import PersistentTemporaryFile
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
@ -16,7 +17,12 @@ class InternationalHeraldTribune(BasicNewsRecipe):
|
|||||||
max_articles_per_feed = 10
|
max_articles_per_feed = 10
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
|
|
||||||
remove_tags = [dict(name='div', attrs={'class':'footer'})]
|
remove_tags = [dict(name='div', attrs={'class':'footer'}),
|
||||||
|
dict(name=['form'])]
|
||||||
|
preprocess_regexps = [
|
||||||
|
(re.compile(r'<!-- webtrends.*', re.DOTALL),
|
||||||
|
lambda m:'</body></html>')
|
||||||
|
]
|
||||||
extra_css = '.headline {font-size: x-large;} \n .fact { padding-top: 10pt }'
|
extra_css = '.headline {font-size: x-large;} \n .fact { padding-top: 10pt }'
|
||||||
|
|
||||||
feeds = [
|
feeds = [
|
||||||
|
@ -1,14 +1,7 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2008, Darko Miletic <darko.miletic at gmail.com>'
|
__copyright__ = '2008-2009, AprilHare, Darko Miletic <darko.miletic at gmail.com>'
|
||||||
'''
|
|
||||||
newscientist.com
|
|
||||||
'''
|
|
||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
|
||||||
__copyright__ = '2008, Darko Miletic <darko.miletic at gmail.com>, AprilHare'
|
|
||||||
'''
|
'''
|
||||||
newscientist.com
|
newscientist.com
|
||||||
'''
|
'''
|
||||||
@ -16,23 +9,34 @@ newscientist.com
|
|||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
class NewScientist(BasicNewsRecipe):
|
class NewScientist(BasicNewsRecipe):
|
||||||
title = u'New Scientist - Online News'
|
title = 'New Scientist - Online News'
|
||||||
__author__ = 'Darko Miletic'
|
__author__ = 'Darko Miletic'
|
||||||
description = 'News from Science'
|
description = 'Science news and science articles from New Scientist.'
|
||||||
language = _('English')
|
language = _('English')
|
||||||
|
publisher = 'New Scientist'
|
||||||
|
category = 'science news, science articles, science jobs, drugs, cancer, depression, computer software, sex'
|
||||||
|
delay = 3
|
||||||
oldest_article = 7
|
oldest_article = 7
|
||||||
max_articles_per_feed = 100
|
max_articles_per_feed = 100
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
use_embedded_content = False
|
use_embedded_content = False
|
||||||
|
remove_javascript = True
|
||||||
|
encoding = 'utf-8'
|
||||||
|
|
||||||
keep_only_tags = [
|
html2lrf_options = [
|
||||||
dict(name='div' , attrs={'id':'pgtop' })
|
'--comment', description
|
||||||
,dict(name='div' , attrs={'id':'maincol' })
|
, '--category', category
|
||||||
|
, '--publisher', publisher
|
||||||
]
|
]
|
||||||
|
|
||||||
|
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"'
|
||||||
|
|
||||||
|
keep_only_tags = [dict(name='div', attrs={'id':['pgtop','maincol']})]
|
||||||
|
|
||||||
remove_tags = [
|
remove_tags = [
|
||||||
dict(name='div' , attrs={'class':'hldBd' })
|
dict(name='div', attrs={'class':['hldBd','adline','pnl','infotext' ]})
|
||||||
,dict(name='div' , attrs={'id':'compnl' })
|
,dict(name='div', attrs={'id' :['compnl','artIssueInfo','artTools']})
|
||||||
,dict(name='div' , attrs={'id':'artIssueInfo' })
|
,dict(name='p' , attrs={'class':['marker','infotext' ]})
|
||||||
]
|
]
|
||||||
|
|
||||||
feeds = [
|
feeds = [
|
||||||
@ -46,3 +50,20 @@ class NewScientist(BasicNewsRecipe):
|
|||||||
,(u'Science in Society' , u'http://www.newscientist.com/feed/view?id=5&type=channel' )
|
,(u'Science in Society' , u'http://www.newscientist.com/feed/view?id=5&type=channel' )
|
||||||
,(u'Tech' , u'http://www.newscientist.com/feed/view?id=7&type=channel' )
|
,(u'Tech' , u'http://www.newscientist.com/feed/view?id=7&type=channel' )
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def get_article_url(self, article):
|
||||||
|
url = article.get('link', None)
|
||||||
|
raw = article.get('description', None)
|
||||||
|
rsoup = self.index_to_soup(raw)
|
||||||
|
atags = rsoup.findAll('a',href=True)
|
||||||
|
for atag in atags:
|
||||||
|
if atag['href'].startswith('http://res.feedsportal.com/viral/sendemail2.html?'):
|
||||||
|
st, sep, rest = atag['href'].partition('&link=')
|
||||||
|
real_url, sep2, drest = rest.partition('" target=')
|
||||||
|
return real_url
|
||||||
|
return url
|
||||||
|
|
||||||
|
def print_version(self, url):
|
||||||
|
rawurl, sep, params = url.partition('?')
|
||||||
|
return rawurl + '?full=true&print=true'
|
||||||
|
|
@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
'''
|
||||||
|
nybooks.com
|
||||||
|
'''
|
||||||
|
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
from lxml import html
|
||||||
|
from calibre.constants import preferred_encoding
|
||||||
|
|
||||||
|
class NewYorkReviewOfBooks(BasicNewsRecipe):
|
||||||
|
|
||||||
|
title = u'New York Review of Books (no subscription)'
|
||||||
|
description = u'Book reviews'
|
||||||
|
language = _('English')
|
||||||
|
__author__ = 'Kovid Goyal'
|
||||||
|
remove_tags_before = {'id':'container'}
|
||||||
|
remove_tags = [{'class':['noprint', 'ad', 'footer']}, {'id':'right-content'}]
|
||||||
|
|
||||||
|
def parse_index(self):
|
||||||
|
root = html.fromstring(self.browser.open('http://www.nybooks.com/current-issue').read())
|
||||||
|
date = root.xpath('//h4[@class = "date"]')[0]
|
||||||
|
self.timefmt = ' ['+date.text.encode(preferred_encoding)+']'
|
||||||
|
articles = []
|
||||||
|
for tag in date.itersiblings():
|
||||||
|
if tag.tag == 'h4': break
|
||||||
|
if tag.tag == 'p':
|
||||||
|
if tag.get('class') == 'indented':
|
||||||
|
articles[-1]['description'] += html.tostring(tag)
|
||||||
|
else:
|
||||||
|
href = tag.xpath('descendant::a[@href]')[0].get('href')
|
||||||
|
article = {
|
||||||
|
'title': u''.join(tag.xpath('descendant::text()')),
|
||||||
|
'date' : '',
|
||||||
|
'url' : 'http://www.nybooks.com'+href,
|
||||||
|
'description': '',
|
||||||
|
}
|
||||||
|
articles.append(article)
|
||||||
|
|
||||||
|
return [('Current Issue', articles)]
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user