mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Sync to trunk
This commit is contained in:
commit
d08264e147
@ -3,7 +3,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
''' Create an OSX installer '''
|
''' Create an OSX installer '''
|
||||||
|
|
||||||
import sys, re, os, shutil, subprocess, stat, glob, zipfile
|
import sys, re, os, shutil, subprocess, stat, glob, zipfile, plistlib
|
||||||
l = {}
|
l = {}
|
||||||
exec open('setup.py').read() in l
|
exec open('setup.py').read() in l
|
||||||
VERSION = l['VERSION']
|
VERSION = l['VERSION']
|
||||||
@ -36,7 +36,7 @@ loader = open(loader_path, 'w')
|
|||||||
site_packages = glob.glob(resources_dir+'/lib/python*/site-packages.zip')[0]
|
site_packages = glob.glob(resources_dir+'/lib/python*/site-packages.zip')[0]
|
||||||
print >>loader, '#!'+python
|
print >>loader, '#!'+python
|
||||||
print >>loader, 'import sys'
|
print >>loader, 'import sys'
|
||||||
print >>loader, 'sys.path.remove('+repr(dirpath)+')'
|
print >>loader, 'if', repr(dirpath), 'in sys.path: sys.path.remove(', repr(dirpath), ')'
|
||||||
print >>loader, 'sys.path.append(', repr(site_packages), ')'
|
print >>loader, 'sys.path.append(', repr(site_packages), ')'
|
||||||
print >>loader, 'sys.frozen = "macosx_app"'
|
print >>loader, 'sys.frozen = "macosx_app"'
|
||||||
print >>loader, 'sys.frameworks_dir =', repr(frameworks_dir)
|
print >>loader, 'sys.frameworks_dir =', repr(frameworks_dir)
|
||||||
@ -294,11 +294,26 @@ sys.frameworks_dir = os.path.join(os.path.dirname(os.environ['RESOURCEPATH']), '
|
|||||||
f.close()
|
f.close()
|
||||||
print
|
print
|
||||||
print 'Adding main scripts to site-packages'
|
print 'Adding main scripts to site-packages'
|
||||||
f = zipfile.ZipFile(os.path.join(self.dist_dir, APPNAME+'.app', 'Contents', 'Resources', 'lib', 'python2.6', 'site-packages.zip'), 'a', zipfile.ZIP_DEFLATED)
|
f = zipfile.ZipFile(os.path.join(self.dist_dir, APPNAME+'.app', 'Contents', 'Resources', 'lib', 'python'+sys.version[:3], 'site-packages.zip'), 'a', zipfile.ZIP_DEFLATED)
|
||||||
for script in scripts['gui']+scripts['console']:
|
for script in scripts['gui']+scripts['console']:
|
||||||
f.write(script, script.partition('/')[-1])
|
f.write(script, script.partition('/')[-1])
|
||||||
f.close()
|
f.close()
|
||||||
print
|
print
|
||||||
|
print 'Creating console.app'
|
||||||
|
contents_dir = os.path.dirname(resource_dir)
|
||||||
|
cc_dir = os.path.join(contents_dir, 'console.app', 'Contents')
|
||||||
|
os.makedirs(cc_dir)
|
||||||
|
for x in os.listdir(contents_dir):
|
||||||
|
if x == 'console.app':
|
||||||
|
continue
|
||||||
|
if x == 'Info.plist':
|
||||||
|
plist = plistlib.readPlist(os.path.join(contents_dir, x))
|
||||||
|
plist['LSUIElement'] = '1'
|
||||||
|
plistlib.writePlist(plist, os.path.join(cc_dir, x))
|
||||||
|
else:
|
||||||
|
os.symlink(os.path.join('../..', x),
|
||||||
|
os.path.join(cc_dir, x))
|
||||||
|
print
|
||||||
print 'Building disk image'
|
print 'Building disk image'
|
||||||
BuildAPP.makedmg(os.path.join(self.dist_dir, APPNAME+'.app'), APPNAME+'-'+VERSION)
|
BuildAPP.makedmg(os.path.join(self.dist_dir, APPNAME+'.app'), APPNAME+'-'+VERSION)
|
||||||
|
|
||||||
|
@ -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.130'
|
__version__ = '0.4.131'
|
||||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||||
'''
|
'''
|
||||||
Various run time constants.
|
Various run time constants.
|
||||||
|
@ -132,7 +132,7 @@ class HTMLMetadataReader(MetadataReaderPlugin):
|
|||||||
class MOBIMetadataReader(MetadataReaderPlugin):
|
class MOBIMetadataReader(MetadataReaderPlugin):
|
||||||
|
|
||||||
name = 'Read MOBI metadata'
|
name = 'Read MOBI metadata'
|
||||||
file_types = set(['mobi'])
|
file_types = set(['mobi', 'prc'])
|
||||||
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):
|
||||||
@ -204,7 +204,7 @@ class EPUBMetadataWriter(MetadataWriterPlugin):
|
|||||||
|
|
||||||
name = 'Set EPUB metadata'
|
name = 'Set EPUB metadata'
|
||||||
file_types = set(['epub'])
|
file_types = set(['epub'])
|
||||||
description = _('Set metadata in EPUB files')
|
description = _('Set metadata in %s files')%'EPUB'
|
||||||
|
|
||||||
def set_metadata(self, stream, mi, type):
|
def set_metadata(self, stream, mi, type):
|
||||||
from calibre.ebooks.metadata.epub import set_metadata
|
from calibre.ebooks.metadata.epub import set_metadata
|
||||||
@ -214,7 +214,7 @@ class LRFMetadataWriter(MetadataWriterPlugin):
|
|||||||
|
|
||||||
name = 'Set LRF metadata'
|
name = 'Set LRF metadata'
|
||||||
file_types = set(['lrf'])
|
file_types = set(['lrf'])
|
||||||
description = _('Set metadata in LRF files')
|
description = _('Set metadata in %s files')%'LRF'
|
||||||
|
|
||||||
def set_metadata(self, stream, mi, type):
|
def set_metadata(self, stream, mi, type):
|
||||||
from calibre.ebooks.lrf.meta import set_metadata
|
from calibre.ebooks.lrf.meta import set_metadata
|
||||||
@ -224,12 +224,24 @@ class RTFMetadataWriter(MetadataWriterPlugin):
|
|||||||
|
|
||||||
name = 'Set RTF metadata'
|
name = 'Set RTF metadata'
|
||||||
file_types = set(['rtf'])
|
file_types = set(['rtf'])
|
||||||
description = _('Set metadata in RTF files')
|
description = _('Set metadata in %s files')%'RTF'
|
||||||
|
|
||||||
def set_metadata(self, stream, mi, type):
|
def set_metadata(self, stream, mi, type):
|
||||||
from calibre.ebooks.metadata.rtf import set_metadata
|
from calibre.ebooks.metadata.rtf import set_metadata
|
||||||
set_metadata(stream, mi)
|
set_metadata(stream, mi)
|
||||||
|
|
||||||
|
class MOBIMetadataWriter(MetadataWriterPlugin):
|
||||||
|
|
||||||
|
name = 'Set MOBI metadata'
|
||||||
|
file_types = set(['mobi', 'prc'])
|
||||||
|
description = _('Set metadata in %s files')%'MOBI'
|
||||||
|
author = 'Marshall T. Vandegrift'
|
||||||
|
|
||||||
|
def set_metadata(self, stream, mi, type):
|
||||||
|
from calibre.ebooks.metadata.mobi import set_metadata
|
||||||
|
set_metadata(stream, mi)
|
||||||
|
|
||||||
|
|
||||||
plugins = [HTML2ZIP]
|
plugins = [HTML2ZIP]
|
||||||
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')]
|
||||||
|
@ -21,6 +21,9 @@ Run an embedded python interpreter.
|
|||||||
'Module specifications are of the form full.name.of.module,path_to_module.py', default=None
|
'Module specifications are of the form full.name.of.module,path_to_module.py', default=None
|
||||||
)
|
)
|
||||||
parser.add_option('-c', '--command', help='Run python code.', default=None)
|
parser.add_option('-c', '--command', help='Run python code.', default=None)
|
||||||
|
parser.add_option('-e', '--exec-file', default=None, help='Run the python code in file.')
|
||||||
|
parser.add_option('-d', '--debug-device-driver', default=False, action='store_true',
|
||||||
|
help='Debug the specified device driver.')
|
||||||
parser.add_option('-g', '--gui', default=False, action='store_true',
|
parser.add_option('-g', '--gui', default=False, action='store_true',
|
||||||
help='Run the GUI',)
|
help='Run the GUI',)
|
||||||
parser.add_option('--migrate', action='store_true', default=False,
|
parser.add_option('--migrate', action='store_true', default=False,
|
||||||
@ -75,6 +78,23 @@ def migrate(old, new):
|
|||||||
prefs['library_path'] = os.path.abspath(new)
|
prefs['library_path'] = os.path.abspath(new)
|
||||||
print 'Database migrated to', os.path.abspath(new)
|
print 'Database migrated to', os.path.abspath(new)
|
||||||
|
|
||||||
|
def debug_device_driver():
|
||||||
|
from calibre.devices.scanner import DeviceScanner
|
||||||
|
s = DeviceScanner()
|
||||||
|
s.scan()
|
||||||
|
print 'USB devices on system:', repr(s.devices)
|
||||||
|
from calibre.devices import devices
|
||||||
|
for dev in devices():
|
||||||
|
print 'Looking for', dev.__name__
|
||||||
|
connected = s.is_device_connected(dev)
|
||||||
|
if connected:
|
||||||
|
print 'Device Connected:', dev
|
||||||
|
print 'Trying to open device...'
|
||||||
|
d = dev()
|
||||||
|
d.open()
|
||||||
|
print 'Total space:', d.total_space()
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
def main(args=sys.argv):
|
def main(args=sys.argv):
|
||||||
opts, args = option_parser().parse_args(args)
|
opts, args = option_parser().parse_args(args)
|
||||||
@ -87,6 +107,11 @@ def main(args=sys.argv):
|
|||||||
elif opts.command:
|
elif opts.command:
|
||||||
sys.argv = args[:1]
|
sys.argv = args[:1]
|
||||||
exec opts.command
|
exec opts.command
|
||||||
|
elif opts.exec_file:
|
||||||
|
sys.argv = args[:1]
|
||||||
|
execfile(opts.exec_file)
|
||||||
|
elif opts.debug_device_driver:
|
||||||
|
debug_device_driver()
|
||||||
elif opts.migrate:
|
elif opts.migrate:
|
||||||
if len(args) < 3:
|
if len(args) < 3:
|
||||||
print 'You must specify the path to library1.db and the path to the new library folder'
|
print 'You must specify the path to library1.db and the path to the new library folder'
|
||||||
|
@ -165,8 +165,11 @@ class HTMLProcessor(Processor, Rationalizer):
|
|||||||
br.tag = 'p'
|
br.tag = 'p'
|
||||||
br.text = u'\u00a0'
|
br.text = u'\u00a0'
|
||||||
if (br.tail and br.tail.strip()) or sibling is None or \
|
if (br.tail and br.tail.strip()) or sibling is None or \
|
||||||
getattr(sibling, 'tag', '') != 'br':
|
getattr(sibling, 'tag', '') != 'br':
|
||||||
br.set('style', br.get('style', '')+'; margin: 0pt; border:0pt; height:0pt')
|
style = br.get('style', '').split(';')
|
||||||
|
style = filter(None, map(lambda x: x.strip(), style))
|
||||||
|
style.append('margin: 0pt; border:0pt; height:0pt')
|
||||||
|
br.set('style', '; '.join(style))
|
||||||
else:
|
else:
|
||||||
sibling.getparent().remove(sibling)
|
sibling.getparent().remove(sibling)
|
||||||
if sibling.tail:
|
if sibling.tail:
|
||||||
@ -191,6 +194,8 @@ 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
for meta in list(self.root.xpath('//meta')):
|
for meta in list(self.root.xpath('//meta')):
|
||||||
meta.getparent().remove(meta)
|
meta.getparent().remove(meta)
|
||||||
|
@ -50,6 +50,7 @@ class Splitter(LoggingInterface):
|
|||||||
self.split_size = 0
|
self.split_size = 0
|
||||||
|
|
||||||
# Split on page breaks
|
# Split on page breaks
|
||||||
|
self.splitting_on_page_breaks = True
|
||||||
if not opts.dont_split_on_page_breaks:
|
if not opts.dont_split_on_page_breaks:
|
||||||
self.log_info('\tSplitting on page breaks...')
|
self.log_info('\tSplitting on page breaks...')
|
||||||
if self.path in stylesheet_map:
|
if self.path in stylesheet_map:
|
||||||
@ -61,6 +62,7 @@ class Splitter(LoggingInterface):
|
|||||||
trees = list(self.trees)
|
trees = list(self.trees)
|
||||||
|
|
||||||
# Split any remaining over-sized trees
|
# Split any remaining over-sized trees
|
||||||
|
self.splitting_on_page_breaks = False
|
||||||
if self.opts.profile.flow_size < sys.maxint:
|
if self.opts.profile.flow_size < sys.maxint:
|
||||||
lt_found = False
|
lt_found = False
|
||||||
self.log_info('\tLooking for large trees...')
|
self.log_info('\tLooking for large trees...')
|
||||||
@ -203,7 +205,8 @@ class Splitter(LoggingInterface):
|
|||||||
elem.set('style', 'display:none')
|
elem.set('style', 'display:none')
|
||||||
|
|
||||||
def fix_split_point(sp):
|
def fix_split_point(sp):
|
||||||
sp.set('style', sp.get('style', '')+'page-break-before:avoid;page-break-after:avoid')
|
if not self.splitting_on_page_breaks:
|
||||||
|
sp.set('style', sp.get('style', '')+'page-break-before:avoid;page-break-after:avoid')
|
||||||
|
|
||||||
# Tree 1
|
# Tree 1
|
||||||
hit_split_point = False
|
hit_split_point = False
|
||||||
|
@ -417,39 +417,44 @@ class Parser(PreProcessor, LoggingInterface):
|
|||||||
self.level = self.htmlfile.level
|
self.level = self.htmlfile.level
|
||||||
for f in self.htmlfiles:
|
for f in self.htmlfiles:
|
||||||
name = os.path.basename(f.path)
|
name = os.path.basename(f.path)
|
||||||
|
name = os.path.splitext(name)[0] + '.xhtml'
|
||||||
if name in self.htmlfile_map.values():
|
if name in self.htmlfile_map.values():
|
||||||
name = os.path.splitext(name)[0] + '_cr_%d'%save_counter + os.path.splitext(name)[1]
|
name = os.path.splitext(name)[0] + '_cr_%d'%save_counter + os.path.splitext(name)[1]
|
||||||
save_counter += 1
|
save_counter += 1
|
||||||
self.htmlfile_map[f.path] = name
|
self.htmlfile_map[f.path] = name
|
||||||
|
|
||||||
self.parse_html()
|
self.parse_html()
|
||||||
|
# Handle <image> tags inside embedded <svg>
|
||||||
|
# At least one source of EPUB files (Penguin) uses xlink:href
|
||||||
|
# without declaring the xlink namespace
|
||||||
|
for image in self.root.xpath('//image'):
|
||||||
|
for attr in image.attrib.keys():
|
||||||
|
if attr.endswith(':href'):
|
||||||
|
nhref = self.rewrite_links(image.get(attr))
|
||||||
|
image.set(attr, nhref)
|
||||||
|
|
||||||
self.root.rewrite_links(self.rewrite_links, resolve_base_href=False)
|
self.root.rewrite_links(self.rewrite_links, resolve_base_href=False)
|
||||||
for bad in ('xmlns', 'lang', 'xml:lang'): # lxml also adds these attributes for XHTML documents, leading to duplicates
|
for bad in ('xmlns', 'lang', 'xml:lang'): # lxml also adds these attributes for XHTML documents, leading to duplicates
|
||||||
if self.root.get(bad, None) is not None:
|
if self.root.get(bad, None) is not None:
|
||||||
self.root.attrib.pop(bad)
|
self.root.attrib.pop(bad)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def save_path(self):
|
def save_path(self):
|
||||||
return os.path.join(self.tdir, self.htmlfile_map[self.htmlfile.path])
|
return os.path.join(self.tdir, self.htmlfile_map[self.htmlfile.path])
|
||||||
|
|
||||||
def declare_xhtml_namespace(self, match):
|
|
||||||
if not match.group('raw'):
|
|
||||||
return '<html xmlns="http://www.w3.org/1999/xhtml">'
|
|
||||||
raw = match.group('raw')
|
|
||||||
m = re.search(r'(?i)xmlns\s*=\s*[\'"](?P<uri>[^"\']*)[\'"]', raw)
|
|
||||||
if not m:
|
|
||||||
return '<html xmlns="http://www.w3.org/1999/xhtml" %s>'%raw
|
|
||||||
else:
|
|
||||||
return match.group().replace(m.group('uri'), "http://www.w3.org/1999/xhtml")
|
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
'''
|
'''
|
||||||
Save processed HTML into the content directory.
|
Save processed HTML into the content directory.
|
||||||
Should be called after all HTML processing is finished.
|
Should be called after all HTML processing is finished.
|
||||||
'''
|
'''
|
||||||
ans = tostring(self.root, pretty_print=self.opts.pretty_print)
|
self.root.set('xmlns', 'http://www.w3.org/1999/xhtml')
|
||||||
ans = re.sub(r'(?i)<\s*html(?P<raw>\s+[^>]*){0,1}>', self.declare_xhtml_namespace, ans[:1000]) + ans[1000:]
|
self.root.set('xmlns:xlink', 'http://www.w3.org/1999/xlink')
|
||||||
ans = re.compile(r'<head>', re.IGNORECASE).sub('<head>\n\t<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />\n', ans[:1000])+ans[1000:]
|
for svg in self.root.xpath('//svg'):
|
||||||
|
svg.set('xmlns', 'http://www.w3.org/2000/svg')
|
||||||
|
|
||||||
|
ans = tostring(self.root, pretty_print=self.opts.pretty_print)
|
||||||
|
ans = re.compile(r'<head>', re.IGNORECASE).sub('<head>\n\t<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />\n', ans[:1000])+ans[1000:]
|
||||||
with open(self.save_path(), 'wb') as f:
|
with open(self.save_path(), 'wb') as f:
|
||||||
f.write(ans)
|
f.write(ans)
|
||||||
return f.name
|
return f.name
|
||||||
@ -823,21 +828,28 @@ class Processor(Parser):
|
|||||||
font.set('class', cn)
|
font.set('class', cn)
|
||||||
font.tag = 'span'
|
font.tag = 'span'
|
||||||
|
|
||||||
|
id_css, id_css_counter = {}, 0
|
||||||
for elem in self.root.xpath('//*[@style]'):
|
for elem in self.root.xpath('//*[@style]'):
|
||||||
setting = elem.get('style')
|
setting = elem.get('style')
|
||||||
classname = cache.get(setting, None)
|
if elem.get('id', False) or elem.get('class', False):
|
||||||
if classname is None:
|
elem.set('id', elem.get('id', 'calibre_css_id_%d'%id_css_counter))
|
||||||
classname = 'calibre_class_%d'%class_counter
|
id_css_counter += 1
|
||||||
class_counter += 1
|
id_css[elem.tag+'#'+elem.get('id')] = setting
|
||||||
cache[setting] = classname
|
else:
|
||||||
cn = elem.get('class', '')
|
classname = cache.get(setting, None)
|
||||||
if cn: cn += ' '
|
if classname is None:
|
||||||
cn += classname
|
classname = 'calibre_class_%d'%class_counter
|
||||||
elem.set('class', cn)
|
class_counter += 1
|
||||||
|
cache[setting] = classname
|
||||||
|
cn = elem.get('class', classname)
|
||||||
|
elem.set('class', cn)
|
||||||
elem.attrib.pop('style')
|
elem.attrib.pop('style')
|
||||||
|
|
||||||
css = '\n'.join(['.%s {%s;}'%(cn, setting) for \
|
css = '\n'.join(['.%s {%s;}'%(cn, setting) for \
|
||||||
setting, cn in cache.items()])
|
setting, cn in cache.items()])
|
||||||
|
css += '\n\n'
|
||||||
|
css += '\n'.join(['%s {%s;}'%(selector, setting) for \
|
||||||
|
selector, setting in id_css.items()])
|
||||||
sheet = self.css_parser.parseString(self.preprocess_css(css.replace(';;}', ';}')))
|
sheet = self.css_parser.parseString(self.preprocess_css(css.replace(';;}', ';}')))
|
||||||
for rule in sheet:
|
for rule in sheet:
|
||||||
self.stylesheet.add(rule)
|
self.stylesheet.add(rule)
|
||||||
|
@ -11,6 +11,7 @@ import sys, struct, cStringIO, os
|
|||||||
import functools
|
import functools
|
||||||
import re
|
import re
|
||||||
from urlparse import urldefrag
|
from urlparse import urldefrag
|
||||||
|
from urllib import unquote as urlunquote
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
from calibre.ebooks.lit import LitError
|
from calibre.ebooks.lit import LitError
|
||||||
from calibre.ebooks.lit.maps import OPF_MAP, HTML_MAP
|
from calibre.ebooks.lit.maps import OPF_MAP, HTML_MAP
|
||||||
@ -611,6 +612,8 @@ class LitReader(object):
|
|||||||
offset, raw = u32(raw), raw[4:]
|
offset, raw = u32(raw), raw[4:]
|
||||||
internal, raw = consume_sized_utf8_string(raw)
|
internal, raw = consume_sized_utf8_string(raw)
|
||||||
original, raw = consume_sized_utf8_string(raw)
|
original, raw = consume_sized_utf8_string(raw)
|
||||||
|
# The path should be stored unquoted, but not always
|
||||||
|
original = urlunquote(original)
|
||||||
# Is this last one UTF-8 or ASCIIZ?
|
# Is this last one UTF-8 or ASCIIZ?
|
||||||
mime_type, raw = consume_sized_utf8_string(raw, zpad=True)
|
mime_type, raw = consume_sized_utf8_string(raw, zpad=True)
|
||||||
self.manifest[internal] = ManifestItem(
|
self.manifest[internal] = ManifestItem(
|
||||||
|
@ -122,6 +122,8 @@ LZXC_CONTROL = \
|
|||||||
|
|
||||||
COLLAPSE = re.compile(r'[ \t\r\n\v]+')
|
COLLAPSE = re.compile(r'[ \t\r\n\v]+')
|
||||||
|
|
||||||
|
PAGE_BREAKS = set(['always', 'left', 'right'])
|
||||||
|
|
||||||
def decint(value):
|
def decint(value):
|
||||||
bytes = []
|
bytes = []
|
||||||
while True:
|
while True:
|
||||||
@ -202,7 +204,7 @@ class ReBinary(object):
|
|||||||
self.write(FLAG_CUSTOM, len(tag)+1, tag)
|
self.write(FLAG_CUSTOM, len(tag)+1, tag)
|
||||||
last_break = self.page_breaks[-1][0] if self.page_breaks else None
|
last_break = self.page_breaks[-1][0] if self.page_breaks else None
|
||||||
if style and last_break != tag_offset \
|
if style and last_break != tag_offset \
|
||||||
and style['page-break-before'] not in ('avoid', 'auto'):
|
and style['page-break-before'] in PAGE_BREAKS:
|
||||||
self.page_breaks.append((tag_offset, list(parents)))
|
self.page_breaks.append((tag_offset, list(parents)))
|
||||||
for attr, value in attrib.items():
|
for attr, value in attrib.items():
|
||||||
attr = prefixname(attr, nsrmap)
|
attr = prefixname(attr, nsrmap)
|
||||||
|
@ -1,23 +1,225 @@
|
|||||||
#!/usr/bin/env python
|
'''
|
||||||
|
Retrieve and modify in-place Mobipocket book metadata.
|
||||||
|
'''
|
||||||
|
|
||||||
|
from __future__ import with_statement
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2009, Kovid Goyal kovid@kovidgoyal.net'
|
__copyright__ = '2009, Kovid Goyal kovid@kovidgoyal.net and ' \
|
||||||
|
'Marshall T. Vandegrift <llasram@gmail.com>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
'''
|
import sys
|
||||||
'''
|
import os
|
||||||
|
from struct import pack, unpack
|
||||||
import sys, os
|
from cStringIO import StringIO
|
||||||
|
from calibre.ebooks.metadata import get_parser
|
||||||
|
from calibre.ebooks.mobi import MobiError
|
||||||
from calibre.ebooks.mobi.reader import get_metadata
|
from calibre.ebooks.mobi.reader import get_metadata
|
||||||
|
from calibre.ebooks.mobi.writer import rescale_image, MAX_THUMB_DIMEN
|
||||||
|
from calibre.ebooks.mobi.langcodes import iana2mobi
|
||||||
|
|
||||||
|
class StreamSlicer(object):
|
||||||
|
def __init__(self, stream, start=0, stop=None):
|
||||||
|
self._stream = stream
|
||||||
|
self.start = start
|
||||||
|
if stop is None:
|
||||||
|
stream.seek(0, 2)
|
||||||
|
stop = stream.tell()
|
||||||
|
self.stop = stop
|
||||||
|
self._len = stop - start
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return self._len
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
stream = self._stream
|
||||||
|
base = self.start
|
||||||
|
if isinstance(key, (int, long)):
|
||||||
|
stream.seek(base + key)
|
||||||
|
return stream.read(1)
|
||||||
|
if isinstance(key, slice):
|
||||||
|
start, stop, stride = key.indices(self._len)
|
||||||
|
if stride < 0:
|
||||||
|
start, stop = stop, start
|
||||||
|
size = stop - start
|
||||||
|
if size <= 0:
|
||||||
|
return ""
|
||||||
|
stream.seek(base + start)
|
||||||
|
data = stream.read(size)
|
||||||
|
if stride != 1:
|
||||||
|
data = data[::stride]
|
||||||
|
return data
|
||||||
|
raise TypeError("stream indices must be integers")
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
stream = self._stream
|
||||||
|
base = self.start
|
||||||
|
if isinstance(key, (int, long)):
|
||||||
|
if len(value) != 1:
|
||||||
|
raise ValueError("key and value lengths must match")
|
||||||
|
stream.seek(base + key)
|
||||||
|
return stream.write(value)
|
||||||
|
if isinstance(key, slice):
|
||||||
|
start, stop, stride = key.indices(self._len)
|
||||||
|
if stride < 0:
|
||||||
|
start, stop = stop, start
|
||||||
|
size = stop - start
|
||||||
|
if stride != 1:
|
||||||
|
value = value[::stride]
|
||||||
|
if len(value) != size:
|
||||||
|
raise ValueError("key and value lengths must match")
|
||||||
|
stream.seek(base + start)
|
||||||
|
return stream.write(value)
|
||||||
|
raise TypeError("stream indices must be integers")
|
||||||
|
|
||||||
|
|
||||||
|
class MetadataUpdater(object):
|
||||||
|
def __init__(self, stream):
|
||||||
|
self.stream = stream
|
||||||
|
data = self.data = StreamSlicer(stream)
|
||||||
|
type = self.type = data[60:68]
|
||||||
|
self.nrecs, = unpack('>H', data[76:78])
|
||||||
|
record0 = self.record0 = self.record(0)
|
||||||
|
codepage, = unpack('>I', record0[28:32])
|
||||||
|
self.codec = 'utf-8' if codepage == 65001 else 'cp1252'
|
||||||
|
image_base, = unpack('>I', record0[108:112])
|
||||||
|
flags, = unpack('>I', record0[128:132])
|
||||||
|
have_exth = self.have_exth = (flags & 0x40) != 0
|
||||||
|
if not have_exth:
|
||||||
|
return
|
||||||
|
self.cover_record = self.thumbnail_record = None
|
||||||
|
exth_off = unpack('>I', record0[20:24])[0] + 16 + record0.start
|
||||||
|
exth = self.exth = StreamSlicer(stream, exth_off, record0.stop)
|
||||||
|
nitems, = unpack('>I', exth[8:12])
|
||||||
|
pos = 12
|
||||||
|
for i in xrange(nitems):
|
||||||
|
id, size = unpack('>II', exth[pos:pos + 8])
|
||||||
|
content = exth[pos + 8: pos + size]
|
||||||
|
pos += size
|
||||||
|
if id == 201:
|
||||||
|
rindex, = self.cover_rindex, = unpack('>I', content)
|
||||||
|
self.cover_record = self.record(rindex + image_base)
|
||||||
|
elif id == 202:
|
||||||
|
rindex, = self.thumbnail_rindex, = unpack('>I', content)
|
||||||
|
self.thumbnail_record = self.record(rindex + image_base)
|
||||||
|
|
||||||
|
def record(self, n):
|
||||||
|
if n >= self.nrecs:
|
||||||
|
raise ValueError('non-existent record %r' % n)
|
||||||
|
offoff = 78 + (8 * n)
|
||||||
|
start, = unpack('>I', self.data[offoff + 0:offoff + 4])
|
||||||
|
stop = None
|
||||||
|
if n < (self.nrecs - 1):
|
||||||
|
stop, = unpack('>I', self.data[offoff + 8:offoff + 12])
|
||||||
|
return StreamSlicer(self.stream, start, stop)
|
||||||
|
|
||||||
|
def update(self, mi):
|
||||||
|
recs = []
|
||||||
|
if mi.authors:
|
||||||
|
authors = '; '.join(mi.authors)
|
||||||
|
recs.append((100, authors.encode(self.codec, 'replace')))
|
||||||
|
if mi.publisher:
|
||||||
|
recs.append((101, mi.publisher.encode(self.codec, 'replace')))
|
||||||
|
if mi.comments:
|
||||||
|
recs.append((103, mi.comments.encode(self.codec, 'replace')))
|
||||||
|
if mi.isbn:
|
||||||
|
recs.append((104, mi.isbn.encode(self.codec, 'replace')))
|
||||||
|
if mi.tags:
|
||||||
|
subjects = '; '.join(mi.tags)
|
||||||
|
recs.append((105, subjects.encode(self.codec, 'replace')))
|
||||||
|
if self.cover_record is not None:
|
||||||
|
recs.append((201, pack('>I', self.cover_rindex)))
|
||||||
|
recs.append((203, pack('>I', 0)))
|
||||||
|
if self.thumbnail_record is not None:
|
||||||
|
recs.append((202, pack('>I', self.thumbnail_rindex)))
|
||||||
|
exth = StringIO()
|
||||||
|
for code, data in recs:
|
||||||
|
exth.write(pack('>II', code, len(data) + 8))
|
||||||
|
exth.write(data)
|
||||||
|
exth = exth.getvalue()
|
||||||
|
trail = len(exth) % 4
|
||||||
|
pad = '\0' * (4 - trail) # Always pad w/ at least 1 byte
|
||||||
|
exth = ['EXTH', pack('>II', len(exth) + 12, len(recs)), exth, pad]
|
||||||
|
exth = ''.join(exth)
|
||||||
|
title = (mi.title or _('Unknown')).encode(self.codec, 'replace')
|
||||||
|
title_off = (self.exth.start - self.record0.start) + len(exth)
|
||||||
|
title_len = len(title)
|
||||||
|
trail = len(self.exth) - len(exth) - len(title)
|
||||||
|
if trail < 0:
|
||||||
|
raise MobiError("Insufficient space to update metadata")
|
||||||
|
self.exth[:] = ''.join([exth, title, '\0' * trail])
|
||||||
|
self.record0[84:92] = pack('>II', title_off, title_len)
|
||||||
|
self.record0[92:96] = iana2mobi(mi.language)
|
||||||
|
if mi.cover_data[1]:
|
||||||
|
data = mi.cover_data[1]
|
||||||
|
if self.cover_record is not None:
|
||||||
|
size = len(self.cover_record)
|
||||||
|
cover = rescale_image(data, size)
|
||||||
|
cover += '\0' * (size - len(cover))
|
||||||
|
self.cover_record[:] = cover
|
||||||
|
if self.thumbnail_record is not None:
|
||||||
|
size = len(self.thumbnail_record)
|
||||||
|
thumbnail = rescale_image(data, size, dimen=MAX_THUMB_DIMEN)
|
||||||
|
thumbnail += '\0' * (size - len(thumbnail))
|
||||||
|
self.thumbnail_record[:] = thumbnail
|
||||||
|
return
|
||||||
|
|
||||||
|
def set_metadata(stream, mi):
|
||||||
|
mu = MetadataUpdater(stream)
|
||||||
|
mu.update(mi)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def option_parser():
|
||||||
|
parser = get_parser('mobi')
|
||||||
|
parser.remove_option('--category')
|
||||||
|
parser.add_option('--tags', default=None,
|
||||||
|
help=_('Set the subject tags'))
|
||||||
|
parser.add_option('--language', default=None,
|
||||||
|
help=_('Set the language'))
|
||||||
|
parser.add_option('--publisher', default=None,
|
||||||
|
help=_('Set the publisher'))
|
||||||
|
parser.add_option('--isbn', default=None,
|
||||||
|
help=_('Set the ISBN'))
|
||||||
|
return parser
|
||||||
|
|
||||||
def main(args=sys.argv):
|
def main(args=sys.argv):
|
||||||
|
parser = option_parser()
|
||||||
|
opts, args = parser.parse_args(args)
|
||||||
if len(args) != 2:
|
if len(args) != 2:
|
||||||
|
parser.print_help()
|
||||||
print >>sys.stderr, 'Usage: %s file.mobi' % args[0]
|
print >>sys.stderr, 'Usage: %s file.mobi' % args[0]
|
||||||
return 1
|
return 1
|
||||||
fname = args[1]
|
fname = args[1]
|
||||||
mi = get_metadata(open(fname, 'rb'))
|
changed = False
|
||||||
print unicode(mi)
|
with open(fname, 'r+b') as stream:
|
||||||
if mi.cover_data[1]:
|
mi = get_metadata(stream)
|
||||||
|
if opts.title:
|
||||||
|
mi.title = opts.title
|
||||||
|
changed = True
|
||||||
|
if opts.authors:
|
||||||
|
mi.authors = opts.authors.split(',')
|
||||||
|
changed = True
|
||||||
|
if opts.comment:
|
||||||
|
mi.comments = opts.comment
|
||||||
|
changed = True
|
||||||
|
if opts.tags is not None:
|
||||||
|
mi.tags = opts.tags.split(',')
|
||||||
|
changed = True
|
||||||
|
if opts.language is not None:
|
||||||
|
mi.language = opts.language
|
||||||
|
changed = True
|
||||||
|
if opts.publisher is not None:
|
||||||
|
mi.publisher = opts.publisher
|
||||||
|
changed = True
|
||||||
|
if opts.isbn is not None:
|
||||||
|
mi.isbn = opts.isbn
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
|
set_metadata(stream, mi)
|
||||||
|
print unicode(get_metadata(stream))
|
||||||
|
if not changed and mi.cover_data[1]:
|
||||||
cover = os.path.abspath(
|
cover = os.path.abspath(
|
||||||
'.'.join((os.path.splitext(os.path.basename(fname))[0],
|
'.'.join((os.path.splitext(os.path.basename(fname))[0],
|
||||||
mi.cover_data[0].lower())))
|
mi.cover_data[0].lower())))
|
||||||
|
@ -27,7 +27,7 @@ TABLE_TAGS = set(['table', 'tr', 'td', 'th'])
|
|||||||
SPECIAL_TAGS = set(['hr', 'br'])
|
SPECIAL_TAGS = set(['hr', 'br'])
|
||||||
CONTENT_TAGS = set(['img', 'hr', 'br'])
|
CONTENT_TAGS = set(['img', 'hr', 'br'])
|
||||||
|
|
||||||
PAGE_BREAKS = set(['always', 'odd', 'even'])
|
PAGE_BREAKS = set(['always', 'left', 'right'])
|
||||||
|
|
||||||
COLLAPSE = re.compile(r'[ \t\r\n\v]+')
|
COLLAPSE = re.compile(r'[ \t\r\n\v]+')
|
||||||
|
|
||||||
@ -79,6 +79,9 @@ class FormatState(object):
|
|||||||
|
|
||||||
|
|
||||||
class MobiMLizer(object):
|
class MobiMLizer(object):
|
||||||
|
def __init__(self, ignore_tables=False):
|
||||||
|
self.ignore_tables = ignore_tables
|
||||||
|
|
||||||
def transform(self, oeb, context):
|
def transform(self, oeb, context):
|
||||||
oeb.logger.info('Converting XHTML to Mobipocket markup...')
|
oeb.logger.info('Converting XHTML to Mobipocket markup...')
|
||||||
self.oeb = oeb
|
self.oeb = oeb
|
||||||
@ -341,6 +344,8 @@ class MobiMLizer(object):
|
|||||||
tag = 'tr'
|
tag = 'tr'
|
||||||
elif display == 'table-cell':
|
elif display == 'table-cell':
|
||||||
tag = 'td'
|
tag = 'td'
|
||||||
|
if tag in TABLE_TAGS and self.ignore_tables:
|
||||||
|
tag = 'span' if tag == 'td' else 'div'
|
||||||
if tag in TABLE_TAGS:
|
if tag in TABLE_TAGS:
|
||||||
for attr in ('rowspan', 'colspan'):
|
for attr in ('rowspan', 'colspan'):
|
||||||
if attr in elem.attrib:
|
if attr in elem.attrib:
|
||||||
|
@ -87,6 +87,49 @@ def decint(value, direction):
|
|||||||
bytes[-1] |= 0x80
|
bytes[-1] |= 0x80
|
||||||
return ''.join(chr(b) for b in reversed(bytes))
|
return ''.join(chr(b) for b in reversed(bytes))
|
||||||
|
|
||||||
|
def rescale_image(data, maxsizeb, dimen=None):
|
||||||
|
image = Image.open(StringIO(data))
|
||||||
|
format = image.format
|
||||||
|
changed = False
|
||||||
|
if image.format not in ('JPEG', 'GIF'):
|
||||||
|
width, height = image.size
|
||||||
|
area = width * height
|
||||||
|
if area <= 40000:
|
||||||
|
format = 'GIF'
|
||||||
|
else:
|
||||||
|
image = image.convert('RGBA')
|
||||||
|
format = 'JPEG'
|
||||||
|
changed = True
|
||||||
|
if dimen is not None:
|
||||||
|
image.thumbnail(dimen, Image.ANTIALIAS)
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
|
data = StringIO()
|
||||||
|
image.save(data, format)
|
||||||
|
data = data.getvalue()
|
||||||
|
if len(data) <= maxsizeb:
|
||||||
|
return data
|
||||||
|
image = image.convert('RGBA')
|
||||||
|
for quality in xrange(95, -1, -1):
|
||||||
|
data = StringIO()
|
||||||
|
image.save(data, 'JPEG', quality=quality)
|
||||||
|
data = data.getvalue()
|
||||||
|
if len(data) <= maxsizeb:
|
||||||
|
return data
|
||||||
|
width, height = image.size
|
||||||
|
for scale in xrange(99, 0, -1):
|
||||||
|
scale = scale / 100.
|
||||||
|
data = StringIO()
|
||||||
|
scaled = image.copy()
|
||||||
|
size = (int(width * scale), (height * scale))
|
||||||
|
scaled.thumbnail(size, Image.ANTIALIAS)
|
||||||
|
scaled.save(data, 'JPEG', quality=0)
|
||||||
|
data = data.getvalue()
|
||||||
|
if len(data) <= maxsizeb:
|
||||||
|
return data
|
||||||
|
# Well, we tried?
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
class Serializer(object):
|
class Serializer(object):
|
||||||
NSRMAP = {'': None, XML_NS: 'xml', XHTML_NS: '', MBP_NS: 'mbp'}
|
NSRMAP = {'': None, XML_NS: 'xml', XHTML_NS: '', MBP_NS: 'mbp'}
|
||||||
@ -356,49 +399,6 @@ class MobiWriter(object):
|
|||||||
data, overlap = self._read_text_record(text)
|
data, overlap = self._read_text_record(text)
|
||||||
self._text_nrecords = nrecords
|
self._text_nrecords = nrecords
|
||||||
|
|
||||||
def _rescale_image(self, data, maxsizeb, dimen=None):
|
|
||||||
image = Image.open(StringIO(data))
|
|
||||||
format = image.format
|
|
||||||
changed = False
|
|
||||||
if image.format not in ('JPEG', 'GIF'):
|
|
||||||
width, height = image.size
|
|
||||||
area = width * height
|
|
||||||
if area <= 40000:
|
|
||||||
format = 'GIF'
|
|
||||||
else:
|
|
||||||
image = image.convert('RGBA')
|
|
||||||
format = 'JPEG'
|
|
||||||
changed = True
|
|
||||||
if dimen is not None:
|
|
||||||
image.thumbnail(dimen, Image.ANTIALIAS)
|
|
||||||
changed = True
|
|
||||||
if changed:
|
|
||||||
data = StringIO()
|
|
||||||
image.save(data, format)
|
|
||||||
data = data.getvalue()
|
|
||||||
if len(data) <= maxsizeb:
|
|
||||||
return data
|
|
||||||
image = image.convert('RGBA')
|
|
||||||
for quality in xrange(95, -1, -1):
|
|
||||||
data = StringIO()
|
|
||||||
image.save(data, 'JPEG', quality=quality)
|
|
||||||
data = data.getvalue()
|
|
||||||
if len(data) <= maxsizeb:
|
|
||||||
return data
|
|
||||||
width, height = image.size
|
|
||||||
for scale in xrange(99, 0, -1):
|
|
||||||
scale = scale / 100.
|
|
||||||
data = StringIO()
|
|
||||||
scaled = image.copy()
|
|
||||||
size = (int(width * scale), (height * scale))
|
|
||||||
scaled.thumbnail(size, Image.ANTIALIAS)
|
|
||||||
scaled.save(data, 'JPEG', quality=0)
|
|
||||||
data = data.getvalue()
|
|
||||||
if len(data) <= maxsizeb:
|
|
||||||
return data
|
|
||||||
# Well, we tried?
|
|
||||||
return data
|
|
||||||
|
|
||||||
def _generate_images(self):
|
def _generate_images(self):
|
||||||
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()]
|
||||||
@ -407,7 +407,7 @@ class MobiWriter(object):
|
|||||||
coverid = metadata.cover[0] if metadata.cover else None
|
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]
|
||||||
data = self._rescale_image(item.data, self._imagemax)
|
data = rescale_image(item.data, self._imagemax)
|
||||||
self._records.append(data)
|
self._records.append(data)
|
||||||
|
|
||||||
def _generate_record0(self):
|
def _generate_record0(self):
|
||||||
@ -480,7 +480,7 @@ class MobiWriter(object):
|
|||||||
return ''.join(exth)
|
return ''.join(exth)
|
||||||
|
|
||||||
def _add_thumbnail(self, item):
|
def _add_thumbnail(self, item):
|
||||||
data = self._rescale_image(item.data, MAX_THUMB_SIZE, MAX_THUMB_DIMEN)
|
data = rescale_image(item.data, MAX_THUMB_SIZE, MAX_THUMB_DIMEN)
|
||||||
manifest = self._oeb.manifest
|
manifest = self._oeb.manifest
|
||||||
id, href = manifest.generate('thumbnail', 'thumbnail.jpeg')
|
id, href = manifest.generate('thumbnail', 'thumbnail.jpeg')
|
||||||
manifest.add(id, href, 'image/jpeg', data=data)
|
manifest.add(id, href, 'image/jpeg', data=data)
|
||||||
@ -524,6 +524,10 @@ def config(defaults=None):
|
|||||||
help=_('Modify images to meet Palm device size limitations.'))
|
help=_('Modify images to meet Palm device size limitations.'))
|
||||||
mobi('toc_title', ['--toc-title'], default=None,
|
mobi('toc_title', ['--toc-title'], default=None,
|
||||||
help=_('Title for any generated in-line table of contents.'))
|
help=_('Title for any generated in-line table of contents.'))
|
||||||
|
mobi('ignore_tables', ['--ignore-tables'], default=False,
|
||||||
|
help=_('Render HTML tables as blocks of text instead of actual '
|
||||||
|
'tables. This is neccessary if the HTML contains very large '
|
||||||
|
'or complex tables.'))
|
||||||
profiles = c.add_group('profiles', _('Device renderer profiles. '
|
profiles = c.add_group('profiles', _('Device renderer profiles. '
|
||||||
'Affects conversion of font sizes, image rescaling and rasterization '
|
'Affects conversion of font sizes, image rescaling and rasterization '
|
||||||
'of tables. Valid profiles are: %s.') % ', '.join(_profiles))
|
'of tables. Valid profiles are: %s.') % ', '.join(_profiles))
|
||||||
@ -581,7 +585,7 @@ def oeb2mobi(opts, inpath):
|
|||||||
rasterizer.transform(oeb, context)
|
rasterizer.transform(oeb, context)
|
||||||
trimmer = ManifestTrimmer()
|
trimmer = ManifestTrimmer()
|
||||||
trimmer.transform(oeb, context)
|
trimmer.transform(oeb, context)
|
||||||
mobimlizer = MobiMLizer()
|
mobimlizer = MobiMLizer(ignore_tables=opts.ignore_tables)
|
||||||
mobimlizer.transform(oeb, context)
|
mobimlizer.transform(oeb, context)
|
||||||
writer = MobiWriter(compression=compression, imagemax=imagemax)
|
writer = MobiWriter(compression=compression, imagemax=imagemax)
|
||||||
writer.dump(oeb, outpath)
|
writer.dump(oeb, outpath)
|
||||||
|
@ -10,7 +10,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from types import StringTypes
|
from types import StringTypes
|
||||||
from itertools import izip, count
|
from itertools import izip, count, chain
|
||||||
from urlparse import urldefrag, urlparse, urlunparse
|
from urlparse import urldefrag, urlparse, urlunparse
|
||||||
from urllib import unquote as urlunquote
|
from urllib import unquote as urlunquote
|
||||||
import logging
|
import logging
|
||||||
@ -22,9 +22,11 @@ from lxml import html
|
|||||||
from calibre import LoggingInterface
|
from calibre import LoggingInterface
|
||||||
from calibre.translations.dynamic import translate
|
from calibre.translations.dynamic import translate
|
||||||
from calibre.startup import get_lang
|
from calibre.startup import get_lang
|
||||||
|
from calibre.ebooks.oeb.entitydefs import ENTITYDEFS
|
||||||
|
|
||||||
XML_NS = 'http://www.w3.org/XML/1998/namespace'
|
XML_NS = 'http://www.w3.org/XML/1998/namespace'
|
||||||
XHTML_NS = 'http://www.w3.org/1999/xhtml'
|
XHTML_NS = 'http://www.w3.org/1999/xhtml'
|
||||||
|
OEB_DOC_NS = 'http://openebook.org/namespaces/oeb-document/1.0/'
|
||||||
OPF1_NS = 'http://openebook.org/namespaces/oeb-package/1.0/'
|
OPF1_NS = 'http://openebook.org/namespaces/oeb-package/1.0/'
|
||||||
OPF2_NS = 'http://www.idpf.org/2007/opf'
|
OPF2_NS = 'http://www.idpf.org/2007/opf'
|
||||||
DC09_NS = 'http://purl.org/metadata/dublin_core'
|
DC09_NS = 'http://purl.org/metadata/dublin_core'
|
||||||
@ -40,6 +42,7 @@ XPNSMAP = {'h': XHTML_NS, 'o1': OPF1_NS, 'o2': OPF2_NS,
|
|||||||
'd09': DC09_NS, 'd10': DC10_NS, 'd11': DC11_NS,
|
'd09': DC09_NS, 'd10': DC10_NS, 'd11': DC11_NS,
|
||||||
'xsi': XSI_NS, 'dt': DCTERMS_NS, 'ncx': NCX_NS,
|
'xsi': XSI_NS, 'dt': DCTERMS_NS, 'ncx': NCX_NS,
|
||||||
'svg': SVG_NS, 'xl': XLINK_NS}
|
'svg': SVG_NS, 'xl': XLINK_NS}
|
||||||
|
DC_PREFIXES = ('d11', 'd10', 'd09')
|
||||||
|
|
||||||
def XML(name): return '{%s}%s' % (XML_NS, name)
|
def XML(name): return '{%s}%s' % (XML_NS, name)
|
||||||
def XHTML(name): return '{%s}%s' % (XHTML_NS, name)
|
def XHTML(name): return '{%s}%s' % (XHTML_NS, name)
|
||||||
@ -61,6 +64,7 @@ GIF_MIME = 'image/gif'
|
|||||||
JPEG_MIME = 'image/jpeg'
|
JPEG_MIME = 'image/jpeg'
|
||||||
PNG_MIME = 'image/png'
|
PNG_MIME = 'image/png'
|
||||||
SVG_MIME = 'image/svg+xml'
|
SVG_MIME = 'image/svg+xml'
|
||||||
|
BINARY_MIME = 'application/octet-stream'
|
||||||
|
|
||||||
OEB_STYLES = set([CSS_MIME, OEB_CSS_MIME, 'text/x-oeb-css'])
|
OEB_STYLES = set([CSS_MIME, OEB_CSS_MIME, 'text/x-oeb-css'])
|
||||||
OEB_DOCS = set([XHTML_MIME, 'text/html', OEB_DOC_MIME, 'text/x-oeb-document'])
|
OEB_DOCS = set([XHTML_MIME, 'text/html', OEB_DOC_MIME, 'text/x-oeb-document'])
|
||||||
@ -69,6 +73,8 @@ OEB_IMAGES = set([GIF_MIME, JPEG_MIME, PNG_MIME, SVG_MIME])
|
|||||||
|
|
||||||
MS_COVER_TYPE = 'other.ms-coverimage-standard'
|
MS_COVER_TYPE = 'other.ms-coverimage-standard'
|
||||||
|
|
||||||
|
ENTITY_RE = re.compile(r'&([a-zA-Z_:][a-zA-Z0-9.-_:]+);')
|
||||||
|
COLLAPSE_RE = re.compile(r'[ \t\r\n\v]+')
|
||||||
|
|
||||||
def element(parent, *args, **kwargs):
|
def element(parent, *args, **kwargs):
|
||||||
if parent is not None:
|
if parent is not None:
|
||||||
@ -191,11 +197,8 @@ class Metadata(object):
|
|||||||
def __init__(self, term, value, fq_attrib={}, **kwargs):
|
def __init__(self, term, value, fq_attrib={}, **kwargs):
|
||||||
self.fq_attrib = fq_attrib = dict(fq_attrib)
|
self.fq_attrib = fq_attrib = dict(fq_attrib)
|
||||||
fq_attrib.update(kwargs)
|
fq_attrib.update(kwargs)
|
||||||
if term == OPF('meta') and not value:
|
if barename(term).lower() in Metadata.TERMS and \
|
||||||
term = self.fq_attrib.pop('name')
|
(not namespace(term) or namespace(term) in DC_NSES):
|
||||||
value = self.fq_attrib.pop('content')
|
|
||||||
elif barename(term).lower() in Metadata.TERMS and \
|
|
||||||
(not namespace(term) or namespace(term) in DC_NSES):
|
|
||||||
# Anything looking like Dublin Core is coerced
|
# Anything looking like Dublin Core is coerced
|
||||||
term = DC(barename(term).lower())
|
term = DC(barename(term).lower())
|
||||||
elif namespace(term) == OPF2_NS:
|
elif namespace(term) == OPF2_NS:
|
||||||
@ -329,20 +332,74 @@ class Manifest(object):
|
|||||||
% (self.id, self.href, self.media_type)
|
% (self.id, self.href, self.media_type)
|
||||||
|
|
||||||
def _force_xhtml(self, data):
|
def _force_xhtml(self, data):
|
||||||
|
# Possibly decode in user-specified encoding
|
||||||
if self.oeb.encoding is not None:
|
if self.oeb.encoding is not None:
|
||||||
data = data.decode(self.oeb.encoding, 'replace')
|
data = data.decode(self.oeb.encoding, 'replace')
|
||||||
|
# Handle broken XHTML w/ SVG (ugh)
|
||||||
|
if 'svg:' in data and SVG_NS not in data:
|
||||||
|
data = data.replace(
|
||||||
|
'<html', '<html xmlns:svg="%s"' % SVG_NS, 1)
|
||||||
|
if 'xlink:' in data and XLINK_NS not in data:
|
||||||
|
data = data.replace(
|
||||||
|
'<html', '<html xmlns:xlink="%s"' % XLINK_NS, 1)
|
||||||
|
# Try with more & more drastic measures to parse
|
||||||
try:
|
try:
|
||||||
data = etree.fromstring(data)
|
data = etree.fromstring(data)
|
||||||
except etree.XMLSyntaxError:
|
except etree.XMLSyntaxError:
|
||||||
data = html.fromstring(data)
|
repl = lambda m: ENTITYDEFS.get(m.group(1), m.group(0))
|
||||||
data = etree.tostring(data, encoding=unicode)
|
data = ENTITY_RE.sub(repl, data)
|
||||||
data = etree.fromstring(data)
|
try:
|
||||||
if namespace(data.tag) != XHTML_NS:
|
data = etree.fromstring(data)
|
||||||
|
except etree.XMLSyntaxError:
|
||||||
|
self.oeb.logger.warn('Parsing file %r as HTML' % self.href)
|
||||||
|
data = html.fromstring(data)
|
||||||
|
data.attrib.pop('xmlns', None)
|
||||||
|
data = etree.tostring(data, encoding=unicode)
|
||||||
|
data = etree.fromstring(data)
|
||||||
|
# Force into the XHTML namespace
|
||||||
|
if barename(data.tag) != 'html':
|
||||||
|
raise OEBError(
|
||||||
|
'File %r does not appear to be (X)HTML' % self.href)
|
||||||
|
elif not namespace(data.tag):
|
||||||
data.attrib['xmlns'] = XHTML_NS
|
data.attrib['xmlns'] = XHTML_NS
|
||||||
data = etree.tostring(data, encoding=unicode)
|
data = etree.tostring(data, encoding=unicode)
|
||||||
data = etree.fromstring(data)
|
data = etree.fromstring(data)
|
||||||
|
elif namespace(data.tag) != XHTML_NS:
|
||||||
|
# OEB_DOC_NS, but possibly others
|
||||||
|
ns = namespace(data.tag)
|
||||||
|
attrib = dict(data.attrib)
|
||||||
|
nroot = etree.Element(XHTML('html'),
|
||||||
|
nsmap={None: XHTML_NS}, attrib=attrib)
|
||||||
|
for elem in data.iterdescendants():
|
||||||
|
if isinstance(elem.tag, basestring) and \
|
||||||
|
namespace(elem.tag) == ns:
|
||||||
|
elem.tag = XHTML(barename(elem.tag))
|
||||||
|
for elem in data:
|
||||||
|
nroot.append(elem)
|
||||||
|
data = nroot
|
||||||
|
# Remove any encoding-specifying <meta/> elements
|
||||||
for meta in self.META_XP(data):
|
for meta in self.META_XP(data):
|
||||||
meta.getparent().remove(meta)
|
meta.getparent().remove(meta)
|
||||||
|
# Ensure has a <head/>
|
||||||
|
head = xpath(data, '/h:html/h:head')
|
||||||
|
head = head[0] if head else None
|
||||||
|
if head is None:
|
||||||
|
self.oeb.logger.warn(
|
||||||
|
'File %r missing <head/> element' % self.href)
|
||||||
|
head = etree.Element(XHTML('head'))
|
||||||
|
data.insert(0, head)
|
||||||
|
title = etree.SubElement(head, XHTML('title'))
|
||||||
|
title.text = self.oeb.translate(__('Unknown'))
|
||||||
|
elif not xpath(data, '/h:html/h:head/h:title'):
|
||||||
|
self.oeb.logger.warn(
|
||||||
|
'File %r missing <title/> element' % self.href)
|
||||||
|
title = etree.SubElement(head, XHTML('title'))
|
||||||
|
title.text = self.oeb.translate(__('Unknown'))
|
||||||
|
# Ensure has a <body/>
|
||||||
|
if not xpath(data, '/h:html/h:body'):
|
||||||
|
self.oeb.logger.warn(
|
||||||
|
'File %r missing <body/> element' % self.href)
|
||||||
|
etree.SubElement(data, XHTML('body'))
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def data():
|
def data():
|
||||||
@ -469,9 +526,9 @@ class Manifest(object):
|
|||||||
elem = element(parent, 'manifest')
|
elem = element(parent, 'manifest')
|
||||||
for item in self.ids.values():
|
for item in self.ids.values():
|
||||||
media_type = item.media_type
|
media_type = item.media_type
|
||||||
if media_type == XHTML_MIME:
|
if media_type in OEB_DOCS:
|
||||||
media_type = OEB_DOC_MIME
|
media_type = OEB_DOC_MIME
|
||||||
elif media_type == CSS_MIME:
|
elif media_type in OEB_STYLES:
|
||||||
media_type = OEB_CSS_MIME
|
media_type = OEB_CSS_MIME
|
||||||
attrib = {'id': item.id, 'href': item.href,
|
attrib = {'id': item.id, 'href': item.href,
|
||||||
'media-type': media_type}
|
'media-type': media_type}
|
||||||
@ -483,6 +540,11 @@ class Manifest(object):
|
|||||||
def to_opf2(self, parent=None):
|
def to_opf2(self, parent=None):
|
||||||
elem = element(parent, OPF('manifest'))
|
elem = element(parent, OPF('manifest'))
|
||||||
for item in self.ids.values():
|
for item in self.ids.values():
|
||||||
|
media_type = item.media_type
|
||||||
|
if media_type in OEB_DOCS:
|
||||||
|
media_type = XHTML_MIME
|
||||||
|
elif media_type in OEB_STYLES:
|
||||||
|
media_type = CSS_MIME
|
||||||
attrib = {'id': item.id, 'href': item.href,
|
attrib = {'id': item.id, 'href': item.href,
|
||||||
'media-type': item.media_type}
|
'media-type': item.media_type}
|
||||||
if item.fallback:
|
if item.fallback:
|
||||||
@ -746,25 +808,19 @@ class OEBBook(object):
|
|||||||
opf = self._read_opf(opfpath)
|
opf = self._read_opf(opfpath)
|
||||||
self._all_from_opf(opf)
|
self._all_from_opf(opf)
|
||||||
|
|
||||||
def _convert_opf1(self, opf):
|
def _clean_opf(self, opf):
|
||||||
# Seriously, seriously wrong
|
for elem in opf.iter():
|
||||||
if namespace(opf.tag) == OPF1_NS:
|
if isinstance(elem.tag, basestring) \
|
||||||
opf.tag = barename(opf.tag)
|
and namespace(elem.tag) in ('', OPF1_NS):
|
||||||
for elem in opf.iterdescendants():
|
elem.tag = OPF(barename(elem.tag))
|
||||||
if isinstance(elem.tag, basestring) \
|
|
||||||
and namespace(elem.tag) == OPF1_NS:
|
|
||||||
elem.tag = barename(elem.tag)
|
|
||||||
attrib = dict(opf.attrib)
|
attrib = dict(opf.attrib)
|
||||||
attrib['version'] = '2.0'
|
|
||||||
nroot = etree.Element(OPF('package'),
|
nroot = etree.Element(OPF('package'),
|
||||||
nsmap={None: OPF2_NS}, attrib=attrib)
|
nsmap={None: OPF2_NS}, attrib=attrib)
|
||||||
metadata = etree.SubElement(nroot, OPF('metadata'),
|
metadata = etree.SubElement(nroot, OPF('metadata'),
|
||||||
nsmap={'opf': OPF2_NS, 'dc': DC11_NS,
|
nsmap={'opf': OPF2_NS, 'dc': DC11_NS,
|
||||||
'xsi': XSI_NS, 'dcterms': DCTERMS_NS})
|
'xsi': XSI_NS, 'dcterms': DCTERMS_NS})
|
||||||
for prefix in ('d11', 'd10', 'd09'):
|
dc = lambda prefix: xpath(opf, 'o2:metadata//%s:*' % prefix)
|
||||||
elements = xpath(opf, 'metadata//%s:*' % prefix)
|
for element in chain(*(dc(prefix) for prefix in DC_PREFIXES)):
|
||||||
if elements: break
|
|
||||||
for element in elements:
|
|
||||||
if not element.text: continue
|
if not element.text: continue
|
||||||
tag = barename(element.tag).lower()
|
tag = barename(element.tag).lower()
|
||||||
element.tag = '{%s}%s' % (DC11_NS, tag)
|
element.tag = '{%s}%s' % (DC11_NS, tag)
|
||||||
@ -774,28 +830,26 @@ class OEBBook(object):
|
|||||||
element.attrib[nsname] = element.attrib[name]
|
element.attrib[nsname] = element.attrib[name]
|
||||||
del element.attrib[name]
|
del element.attrib[name]
|
||||||
metadata.append(element)
|
metadata.append(element)
|
||||||
for element in opf.xpath('metadata//meta'):
|
for element in xpath(opf, 'o2:metadata//o2:meta'):
|
||||||
metadata.append(element)
|
metadata.append(element)
|
||||||
for item in opf.xpath('manifest/item'):
|
for tag in ('o2:manifest', 'o2:spine', 'o2:tours', 'o2:guide'):
|
||||||
media_type = item.attrib['media-type'].lower()
|
for element in xpath(opf, tag):
|
||||||
if media_type in OEB_DOCS:
|
|
||||||
media_type = XHTML_MIME
|
|
||||||
elif media_type in OEB_STYLES:
|
|
||||||
media_type = CSS_MIME
|
|
||||||
item.attrib['media-type'] = media_type
|
|
||||||
for tag in ('manifest', 'spine', 'tours', 'guide'):
|
|
||||||
for element in opf.xpath(tag):
|
|
||||||
nroot.append(element)
|
nroot.append(element)
|
||||||
return etree.fromstring(etree.tostring(nroot))
|
return nroot
|
||||||
|
|
||||||
def _read_opf(self, opfpath):
|
def _read_opf(self, opfpath):
|
||||||
opf = self.container.read_xml(opfpath)
|
opf = self.container.read(opfpath)
|
||||||
version = float(opf.get('version', 1.0))
|
try:
|
||||||
|
opf = etree.fromstring(opf)
|
||||||
|
except etree.XMLSyntaxError:
|
||||||
|
repl = lambda m: ENTITYDEFS.get(m.group(1), m.group(0))
|
||||||
|
opf = ENTITY_RE.sub(repl, opf)
|
||||||
|
opf = etree.fromstring(opf)
|
||||||
|
self.logger.warn('OPF contains invalid HTML named entities')
|
||||||
ns = namespace(opf.tag)
|
ns = namespace(opf.tag)
|
||||||
if ns not in ('', OPF1_NS, OPF2_NS):
|
if ns not in ('', OPF1_NS, OPF2_NS):
|
||||||
raise OEBError('Invalid namespace %r for OPF document' % ns)
|
raise OEBError('Invalid namespace %r for OPF document' % ns)
|
||||||
if ns != OPF2_NS or version < 2.0:
|
opf = self._clean_opf(opf)
|
||||||
opf = self._convert_opf1(opf)
|
|
||||||
return opf
|
return opf
|
||||||
|
|
||||||
def _metadata_from_opf(self, opf):
|
def _metadata_from_opf(self, opf):
|
||||||
@ -804,8 +858,16 @@ class OEBBook(object):
|
|||||||
self.metadata = metadata = Metadata(self)
|
self.metadata = metadata = Metadata(self)
|
||||||
ignored = (OPF('dc-metadata'), OPF('x-metadata'))
|
ignored = (OPF('dc-metadata'), OPF('x-metadata'))
|
||||||
for elem in xpath(opf, '/o2:package/o2:metadata//*'):
|
for elem in xpath(opf, '/o2:package/o2:metadata//*'):
|
||||||
if elem.tag not in ignored and (elem.text or elem.attrib):
|
if elem.tag in ignored: continue
|
||||||
metadata.add(elem.tag, elem.text, elem.attrib)
|
term = elem.tag
|
||||||
|
value = elem.text
|
||||||
|
if term == OPF('meta'):
|
||||||
|
term = elem.attrib.pop('name', None)
|
||||||
|
value = elem.attrib.pop('content', None)
|
||||||
|
if value:
|
||||||
|
value = COLLAPSE_RE.sub(' ', value.strip())
|
||||||
|
if term and (value or elem.attrib):
|
||||||
|
metadata.add(term, value, elem.attrib)
|
||||||
haveuuid = haveid = False
|
haveuuid = haveid = False
|
||||||
for ident in metadata.identifier:
|
for ident in metadata.identifier:
|
||||||
if unicode(ident).startswith('urn:uuid:'):
|
if unicode(ident).startswith('urn:uuid:'):
|
||||||
@ -820,36 +882,38 @@ class OEBBook(object):
|
|||||||
self.uid = item
|
self.uid = item
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
self.logger.warn(u'Unique-identifier %r not found.' % uid)
|
self.logger.warn(u'Unique-identifier %r not found' % uid)
|
||||||
for ident in metadata.identifier:
|
for ident in metadata.identifier:
|
||||||
if 'id' in ident.attrib:
|
if 'id' in ident.attrib:
|
||||||
self.uid = metadata.identifier[0]
|
self.uid = metadata.identifier[0]
|
||||||
break
|
break
|
||||||
if not metadata.language:
|
if not metadata.language:
|
||||||
self.logger.warn(u'Language not specified.')
|
self.logger.warn(u'Language not specified')
|
||||||
metadata.add('language', get_lang())
|
metadata.add('language', get_lang())
|
||||||
if not metadata.creator:
|
if not metadata.creator:
|
||||||
self.logger.warn(u'Creator not specified.')
|
self.logger.warn('Creator not specified')
|
||||||
metadata.add('creator', _('Unknown'))
|
metadata.add('creator', self.translate(__('Unknown')))
|
||||||
if not metadata.title:
|
if not metadata.title:
|
||||||
self.logger.warn(u'Title not specified.')
|
self.logger.warn('Title not specified')
|
||||||
metadata.add('title', _('Unknown'))
|
metadata.add('title', self.translate(__('Unknown')))
|
||||||
|
|
||||||
def _manifest_from_opf(self, opf):
|
def _manifest_from_opf(self, opf):
|
||||||
self.manifest = manifest = Manifest(self)
|
self.manifest = manifest = Manifest(self)
|
||||||
for elem in xpath(opf, '/o2:package/o2:manifest/o2:item'):
|
for elem in xpath(opf, '/o2:package/o2:manifest/o2:item'):
|
||||||
id = elem.get('id')
|
id = elem.get('id')
|
||||||
href = elem.get('href')
|
href = elem.get('href')
|
||||||
media_type = elem.get('media-type')
|
media_type = elem.get('media-type', None)
|
||||||
|
if media_type is None:
|
||||||
|
media_type = elem.get('mediatype', BINARY_MIME)
|
||||||
fallback = elem.get('fallback')
|
fallback = elem.get('fallback')
|
||||||
if href in manifest.hrefs:
|
if href in manifest.hrefs:
|
||||||
self.logger.warn(u'Duplicate manifest entry for %r.' % href)
|
self.logger.warn(u'Duplicate manifest entry for %r' % href)
|
||||||
continue
|
continue
|
||||||
if not self.container.exists(href):
|
if not self.container.exists(href):
|
||||||
self.logger.warn(u'Manifest item %r not found.' % href)
|
self.logger.warn(u'Manifest item %r not found' % href)
|
||||||
continue
|
continue
|
||||||
if id in manifest.ids:
|
if id in manifest.ids:
|
||||||
self.logger.warn(u'Duplicate manifest id %r.' % id)
|
self.logger.warn(u'Duplicate manifest id %r' % id)
|
||||||
id, href = manifest.generate(id, href)
|
id, href = manifest.generate(id, href)
|
||||||
manifest.add(id, href, media_type, fallback)
|
manifest.add(id, href, media_type, fallback)
|
||||||
|
|
||||||
@ -858,7 +922,7 @@ class OEBBook(object):
|
|||||||
for elem in xpath(opf, '/o2:package/o2:spine/o2:itemref'):
|
for elem in xpath(opf, '/o2:package/o2:spine/o2:itemref'):
|
||||||
idref = elem.get('idref')
|
idref = elem.get('idref')
|
||||||
if idref not in self.manifest:
|
if idref not in self.manifest:
|
||||||
self.logger.warn(u'Spine item %r not found.' % idref)
|
self.logger.warn(u'Spine item %r not found' % idref)
|
||||||
continue
|
continue
|
||||||
item = self.manifest[idref]
|
item = self.manifest[idref]
|
||||||
spine.add(item, elem.get('linear'))
|
spine.add(item, elem.get('linear'))
|
||||||
@ -906,7 +970,8 @@ class OEBBook(object):
|
|||||||
item = self.manifest.ids[id]
|
item = self.manifest.ids[id]
|
||||||
ncx = item.data
|
ncx = item.data
|
||||||
self.manifest.remove(item)
|
self.manifest.remove(item)
|
||||||
title = xpath(ncx, 'ncx:docTitle/ncx:text/text()')[0]
|
title = xpath(ncx, 'ncx:docTitle/ncx:text/text()')
|
||||||
|
title = title[0].strip() if title else unicode(self.metadata.title)
|
||||||
self.toc = toc = TOC(title)
|
self.toc = toc = TOC(title)
|
||||||
navmaps = xpath(ncx, 'ncx:navMap')
|
navmaps = xpath(ncx, 'ncx:navMap')
|
||||||
for navmap in navmaps:
|
for navmap in navmaps:
|
||||||
@ -963,7 +1028,8 @@ class OEBBook(object):
|
|||||||
if not item.linear: continue
|
if not item.linear: continue
|
||||||
html = item.data
|
html = item.data
|
||||||
title = xpath(html, '/h:html/h:head/h:title/text()')
|
title = xpath(html, '/h:html/h:head/h:title/text()')
|
||||||
if title: titles.append(title[0])
|
title = title[0].strip() if title else None
|
||||||
|
if title: titles.append(title)
|
||||||
headers.append('(unlabled)')
|
headers.append('(unlabled)')
|
||||||
for tag in ('h1', 'h2', 'h3', 'h4', 'h5', 'strong'):
|
for tag in ('h1', 'h2', 'h3', 'h4', 'h5', 'strong'):
|
||||||
expr = '/h:html/h:body//h:%s[position()=1]/text()' % (tag,)
|
expr = '/h:html/h:body//h:%s[position()=1]/text()' % (tag,)
|
||||||
@ -987,9 +1053,19 @@ class OEBBook(object):
|
|||||||
|
|
||||||
def _ensure_cover_image(self):
|
def _ensure_cover_image(self):
|
||||||
cover = None
|
cover = None
|
||||||
spine0 = self.spine[0]
|
hcover = self.spine[0]
|
||||||
html = spine0.data
|
if 'cover' in self.guide:
|
||||||
if self.metadata.cover:
|
href = self.guide['cover'].href
|
||||||
|
item = self.manifest.hrefs[href]
|
||||||
|
media_type = item.media_type
|
||||||
|
if media_type in OEB_RASTER_IMAGES:
|
||||||
|
cover = item
|
||||||
|
elif media_type in OEB_DOCS:
|
||||||
|
hcover = item
|
||||||
|
html = hcover.data
|
||||||
|
if cover is not None:
|
||||||
|
pass
|
||||||
|
elif self.metadata.cover:
|
||||||
id = str(self.metadata.cover[0])
|
id = str(self.metadata.cover[0])
|
||||||
cover = self.manifest.ids[id]
|
cover = self.manifest.ids[id]
|
||||||
elif MS_COVER_TYPE in self.guide:
|
elif MS_COVER_TYPE in self.guide:
|
||||||
@ -997,16 +1073,16 @@ class OEBBook(object):
|
|||||||
cover = self.manifest.hrefs[href]
|
cover = self.manifest.hrefs[href]
|
||||||
elif xpath(html, '//h:img[position()=1]'):
|
elif xpath(html, '//h:img[position()=1]'):
|
||||||
img = xpath(html, '//h:img[position()=1]')[0]
|
img = xpath(html, '//h:img[position()=1]')[0]
|
||||||
href = spine0.abshref(img.get('src'))
|
href = hcover.abshref(img.get('src'))
|
||||||
cover = self.manifest.hrefs[href]
|
cover = self.manifest.hrefs[href]
|
||||||
elif xpath(html, '//h:object[position()=1]'):
|
elif xpath(html, '//h:object[position()=1]'):
|
||||||
object = xpath(html, '//h:object[position()=1]')[0]
|
object = xpath(html, '//h:object[position()=1]')[0]
|
||||||
href = spine0.abshref(object.get('data'))
|
href = hcover.abshref(object.get('data'))
|
||||||
cover = self.manifest.hrefs[href]
|
cover = self.manifest.hrefs[href]
|
||||||
elif xpath(html, '//svg:svg[position()=1]'):
|
elif xpath(html, '//svg:svg[position()=1]'):
|
||||||
svg = copy.deepcopy(xpath(html, '//svg:svg[position()=1]')[0])
|
svg = copy.deepcopy(xpath(html, '//svg:svg[position()=1]')[0])
|
||||||
href = os.path.splitext(spine0.href)[0] + '.svg'
|
href = os.path.splitext(hcover.href)[0] + '.svg'
|
||||||
id, href = self.manifest.generate(spine0.id, href)
|
id, href = self.manifest.generate(hcover.id, href)
|
||||||
cover = self.manifest.add(id, href, SVG_MIME, data=svg)
|
cover = self.manifest.add(id, href, SVG_MIME, data=svg)
|
||||||
if cover and not self.metadata.cover:
|
if cover and not self.metadata.cover:
|
||||||
self.metadata.add('cover', cover.id)
|
self.metadata.add('cover', cover.id)
|
||||||
|
256
src/calibre/ebooks/oeb/entitydefs.py
Normal file
256
src/calibre/ebooks/oeb/entitydefs.py
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
"""
|
||||||
|
Replacement for htmlentitydefs which uses purely numeric entities.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>'
|
||||||
|
|
||||||
|
ENTITYDEFS = \
|
||||||
|
{'AElig': 'Æ',
|
||||||
|
'Aacute': 'Á',
|
||||||
|
'Acirc': 'Â',
|
||||||
|
'Agrave': 'À',
|
||||||
|
'Alpha': 'Α',
|
||||||
|
'Aring': 'Å',
|
||||||
|
'Atilde': 'Ã',
|
||||||
|
'Auml': 'Ä',
|
||||||
|
'Beta': 'Β',
|
||||||
|
'Ccedil': 'Ç',
|
||||||
|
'Chi': 'Χ',
|
||||||
|
'Dagger': '‡',
|
||||||
|
'Delta': 'Δ',
|
||||||
|
'ETH': 'Ð',
|
||||||
|
'Eacute': 'É',
|
||||||
|
'Ecirc': 'Ê',
|
||||||
|
'Egrave': 'È',
|
||||||
|
'Epsilon': 'Ε',
|
||||||
|
'Eta': 'Η',
|
||||||
|
'Euml': 'Ë',
|
||||||
|
'Gamma': 'Γ',
|
||||||
|
'Iacute': 'Í',
|
||||||
|
'Icirc': 'Î',
|
||||||
|
'Igrave': 'Ì',
|
||||||
|
'Iota': 'Ι',
|
||||||
|
'Iuml': 'Ï',
|
||||||
|
'Kappa': 'Κ',
|
||||||
|
'Lambda': 'Λ',
|
||||||
|
'Mu': 'Μ',
|
||||||
|
'Ntilde': 'Ñ',
|
||||||
|
'Nu': 'Ν',
|
||||||
|
'OElig': 'Œ',
|
||||||
|
'Oacute': 'Ó',
|
||||||
|
'Ocirc': 'Ô',
|
||||||
|
'Ograve': 'Ò',
|
||||||
|
'Omega': 'Ω',
|
||||||
|
'Omicron': 'Ο',
|
||||||
|
'Oslash': 'Ø',
|
||||||
|
'Otilde': 'Õ',
|
||||||
|
'Ouml': 'Ö',
|
||||||
|
'Phi': 'Φ',
|
||||||
|
'Pi': 'Π',
|
||||||
|
'Prime': '″',
|
||||||
|
'Psi': 'Ψ',
|
||||||
|
'Rho': 'Ρ',
|
||||||
|
'Scaron': 'Š',
|
||||||
|
'Sigma': 'Σ',
|
||||||
|
'THORN': 'Þ',
|
||||||
|
'Tau': 'Τ',
|
||||||
|
'Theta': 'Θ',
|
||||||
|
'Uacute': 'Ú',
|
||||||
|
'Ucirc': 'Û',
|
||||||
|
'Ugrave': 'Ù',
|
||||||
|
'Upsilon': 'Υ',
|
||||||
|
'Uuml': 'Ü',
|
||||||
|
'Xi': 'Ξ',
|
||||||
|
'Yacute': 'Ý',
|
||||||
|
'Yuml': 'Ÿ',
|
||||||
|
'Zeta': 'Ζ',
|
||||||
|
'aacute': 'á',
|
||||||
|
'acirc': 'â',
|
||||||
|
'acute': '´',
|
||||||
|
'aelig': 'æ',
|
||||||
|
'agrave': 'à',
|
||||||
|
'alefsym': 'ℵ',
|
||||||
|
'alpha': 'α',
|
||||||
|
'and': '∧',
|
||||||
|
'ang': '∠',
|
||||||
|
'aring': 'å',
|
||||||
|
'asymp': '≈',
|
||||||
|
'atilde': 'ã',
|
||||||
|
'auml': 'ä',
|
||||||
|
'bdquo': '„',
|
||||||
|
'beta': 'β',
|
||||||
|
'brvbar': '¦',
|
||||||
|
'bull': '•',
|
||||||
|
'cap': '∩',
|
||||||
|
'ccedil': 'ç',
|
||||||
|
'cedil': '¸',
|
||||||
|
'cent': '¢',
|
||||||
|
'chi': 'χ',
|
||||||
|
'circ': 'ˆ',
|
||||||
|
'clubs': '♣',
|
||||||
|
'cong': '≅',
|
||||||
|
'copy': '©',
|
||||||
|
'crarr': '↵',
|
||||||
|
'cup': '∪',
|
||||||
|
'curren': '¤',
|
||||||
|
'dArr': '⇓',
|
||||||
|
'dagger': '†',
|
||||||
|
'darr': '↓',
|
||||||
|
'deg': '°',
|
||||||
|
'delta': 'δ',
|
||||||
|
'diams': '♦',
|
||||||
|
'divide': '÷',
|
||||||
|
'eacute': 'é',
|
||||||
|
'ecirc': 'ê',
|
||||||
|
'egrave': 'è',
|
||||||
|
'empty': '∅',
|
||||||
|
'emsp': ' ',
|
||||||
|
'ensp': ' ',
|
||||||
|
'epsilon': 'ε',
|
||||||
|
'equiv': '≡',
|
||||||
|
'eta': 'η',
|
||||||
|
'eth': 'ð',
|
||||||
|
'euml': 'ë',
|
||||||
|
'euro': '€',
|
||||||
|
'exist': '∃',
|
||||||
|
'fnof': 'ƒ',
|
||||||
|
'forall': '∀',
|
||||||
|
'frac12': '½',
|
||||||
|
'frac14': '¼',
|
||||||
|
'frac34': '¾',
|
||||||
|
'frasl': '⁄',
|
||||||
|
'gamma': 'γ',
|
||||||
|
'ge': '≥',
|
||||||
|
'hArr': '⇔',
|
||||||
|
'harr': '↔',
|
||||||
|
'hearts': '♥',
|
||||||
|
'hellip': '…',
|
||||||
|
'iacute': 'í',
|
||||||
|
'icirc': 'î',
|
||||||
|
'iexcl': '¡',
|
||||||
|
'igrave': 'ì',
|
||||||
|
'image': 'ℑ',
|
||||||
|
'infin': '∞',
|
||||||
|
'int': '∫',
|
||||||
|
'iota': 'ι',
|
||||||
|
'iquest': '¿',
|
||||||
|
'isin': '∈',
|
||||||
|
'iuml': 'ï',
|
||||||
|
'kappa': 'κ',
|
||||||
|
'lArr': '⇐',
|
||||||
|
'lambda': 'λ',
|
||||||
|
'lang': '〈',
|
||||||
|
'laquo': '«',
|
||||||
|
'larr': '←',
|
||||||
|
'lceil': '⌈',
|
||||||
|
'ldquo': '“',
|
||||||
|
'le': '≤',
|
||||||
|
'lfloor': '⌊',
|
||||||
|
'lowast': '∗',
|
||||||
|
'loz': '◊',
|
||||||
|
'lrm': '‎',
|
||||||
|
'lsaquo': '‹',
|
||||||
|
'lsquo': '‘',
|
||||||
|
'macr': '¯',
|
||||||
|
'mdash': '—',
|
||||||
|
'micro': 'µ',
|
||||||
|
'middot': '·',
|
||||||
|
'minus': '−',
|
||||||
|
'mu': 'μ',
|
||||||
|
'nabla': '∇',
|
||||||
|
'nbsp': ' ',
|
||||||
|
'ndash': '–',
|
||||||
|
'ne': '≠',
|
||||||
|
'ni': '∋',
|
||||||
|
'not': '¬',
|
||||||
|
'notin': '∉',
|
||||||
|
'nsub': '⊄',
|
||||||
|
'ntilde': 'ñ',
|
||||||
|
'nu': 'ν',
|
||||||
|
'oacute': 'ó',
|
||||||
|
'ocirc': 'ô',
|
||||||
|
'oelig': 'œ',
|
||||||
|
'ograve': 'ò',
|
||||||
|
'oline': '‾',
|
||||||
|
'omega': 'ω',
|
||||||
|
'omicron': 'ο',
|
||||||
|
'oplus': '⊕',
|
||||||
|
'or': '∨',
|
||||||
|
'ordf': 'ª',
|
||||||
|
'ordm': 'º',
|
||||||
|
'oslash': 'ø',
|
||||||
|
'otilde': 'õ',
|
||||||
|
'otimes': '⊗',
|
||||||
|
'ouml': 'ö',
|
||||||
|
'para': '¶',
|
||||||
|
'part': '∂',
|
||||||
|
'permil': '‰',
|
||||||
|
'perp': '⊥',
|
||||||
|
'phi': 'φ',
|
||||||
|
'pi': 'π',
|
||||||
|
'piv': 'ϖ',
|
||||||
|
'plusmn': '±',
|
||||||
|
'pound': '£',
|
||||||
|
'prime': '′',
|
||||||
|
'prod': '∏',
|
||||||
|
'prop': '∝',
|
||||||
|
'psi': 'ψ',
|
||||||
|
'rArr': '⇒',
|
||||||
|
'radic': '√',
|
||||||
|
'rang': '〉',
|
||||||
|
'raquo': '»',
|
||||||
|
'rarr': '→',
|
||||||
|
'rceil': '⌉',
|
||||||
|
'rdquo': '”',
|
||||||
|
'real': 'ℜ',
|
||||||
|
'reg': '®',
|
||||||
|
'rfloor': '⌋',
|
||||||
|
'rho': 'ρ',
|
||||||
|
'rlm': '‏',
|
||||||
|
'rsaquo': '›',
|
||||||
|
'rsquo': '’',
|
||||||
|
'sbquo': '‚',
|
||||||
|
'scaron': 'š',
|
||||||
|
'sdot': '⋅',
|
||||||
|
'sect': '§',
|
||||||
|
'shy': '­',
|
||||||
|
'sigma': 'σ',
|
||||||
|
'sigmaf': 'ς',
|
||||||
|
'sim': '∼',
|
||||||
|
'spades': '♠',
|
||||||
|
'sub': '⊂',
|
||||||
|
'sube': '⊆',
|
||||||
|
'sum': '∑',
|
||||||
|
'sup': '⊃',
|
||||||
|
'sup1': '¹',
|
||||||
|
'sup2': '²',
|
||||||
|
'sup3': '³',
|
||||||
|
'supe': '⊇',
|
||||||
|
'szlig': 'ß',
|
||||||
|
'tau': 'τ',
|
||||||
|
'there4': '∴',
|
||||||
|
'theta': 'θ',
|
||||||
|
'thetasym': 'ϑ',
|
||||||
|
'thinsp': ' ',
|
||||||
|
'thorn': 'þ',
|
||||||
|
'tilde': '˜',
|
||||||
|
'times': '×',
|
||||||
|
'trade': '™',
|
||||||
|
'uArr': '⇑',
|
||||||
|
'uacute': 'ú',
|
||||||
|
'uarr': '↑',
|
||||||
|
'ucirc': 'û',
|
||||||
|
'ugrave': 'ù',
|
||||||
|
'uml': '¨',
|
||||||
|
'upsih': 'ϒ',
|
||||||
|
'upsilon': 'υ',
|
||||||
|
'uuml': 'ü',
|
||||||
|
'weierp': '℘',
|
||||||
|
'xi': 'ξ',
|
||||||
|
'yacute': 'ý',
|
||||||
|
'yen': '¥',
|
||||||
|
'yuml': 'ÿ',
|
||||||
|
'zeta': 'ζ',
|
||||||
|
'zwj': '‍',
|
||||||
|
'zwnj': '‌'}
|
@ -110,7 +110,8 @@ class Stylizer(object):
|
|||||||
|
|
||||||
def __init__(self, tree, path, oeb, profile=PROFILES['PRS505']):
|
def __init__(self, tree, path, oeb, profile=PROFILES['PRS505']):
|
||||||
self.profile = profile
|
self.profile = profile
|
||||||
base = os.path.dirname(path)
|
self.logger = oeb.logger
|
||||||
|
item = oeb.manifest.hrefs[path]
|
||||||
basename = os.path.basename(path)
|
basename = os.path.basename(path)
|
||||||
cssname = os.path.splitext(basename)[0] + '.css'
|
cssname = os.path.splitext(basename)[0] + '.css'
|
||||||
stylesheets = [HTML_CSS_STYLESHEET]
|
stylesheets = [HTML_CSS_STYLESHEET]
|
||||||
@ -128,8 +129,12 @@ class Stylizer(object):
|
|||||||
and elem.get('rel', 'stylesheet') == 'stylesheet' \
|
and elem.get('rel', 'stylesheet') == 'stylesheet' \
|
||||||
and elem.get('type', CSS_MIME) in OEB_STYLES:
|
and elem.get('type', CSS_MIME) in OEB_STYLES:
|
||||||
href = urlnormalize(elem.attrib['href'])
|
href = urlnormalize(elem.attrib['href'])
|
||||||
path = os.path.join(base, href)
|
path = item.abshref(href)
|
||||||
path = os.path.normpath(path).replace('\\', '/')
|
if path not in oeb.manifest.hrefs:
|
||||||
|
self.logger.warn(
|
||||||
|
'Stylesheet %r referenced by file %r not in manifest' %
|
||||||
|
(path, item.href))
|
||||||
|
continue
|
||||||
if path in self.STYLESHEETS:
|
if path in self.STYLESHEETS:
|
||||||
stylesheet = self.STYLESHEETS[path]
|
stylesheet = self.STYLESHEETS[path]
|
||||||
else:
|
else:
|
||||||
@ -277,10 +282,9 @@ class Style(object):
|
|||||||
def _apply_style_attr(self):
|
def _apply_style_attr(self):
|
||||||
attrib = self._element.attrib
|
attrib = self._element.attrib
|
||||||
if 'style' in attrib:
|
if 'style' in attrib:
|
||||||
css = attrib['style'].strip()
|
css = attrib['style'].split(';')
|
||||||
if css.startswith(';'):
|
css = filter(None, map(lambda x: x.strip(), css))
|
||||||
css = css[1:]
|
style = CSSStyleDeclaration('; '.join(css))
|
||||||
style = CSSStyleDeclaration(css)
|
|
||||||
self._style.update(self._stylizer.flatten_style(style))
|
self._style.update(self._stylizer.flatten_style(style))
|
||||||
|
|
||||||
def _has_parent(self):
|
def _has_parent(self):
|
||||||
|
@ -207,7 +207,8 @@ class CSSFlattener(object):
|
|||||||
items = cssdict.items()
|
items = cssdict.items()
|
||||||
items.sort()
|
items.sort()
|
||||||
css = u';\n'.join(u'%s: %s' % (key, val) for key, val in items)
|
css = u';\n'.join(u'%s: %s' % (key, val) for key, val in items)
|
||||||
klass = STRIPNUM.sub('', node.get('class', 'calibre').split()[0])
|
classes = node.get('class', None) or 'calibre'
|
||||||
|
klass = STRIPNUM.sub('', classes.split()[0])
|
||||||
if css in styles:
|
if css in styles:
|
||||||
match = styles[css]
|
match = styles[css]
|
||||||
else:
|
else:
|
||||||
|
@ -46,9 +46,10 @@ class SVGRasterizer(object):
|
|||||||
data = QByteArray(xml2str(elem))
|
data = QByteArray(xml2str(elem))
|
||||||
svg = QSvgRenderer(data)
|
svg = QSvgRenderer(data)
|
||||||
size = svg.defaultSize()
|
size = svg.defaultSize()
|
||||||
|
view_box = elem.get('viewBox', elem.get('viewbox', None))
|
||||||
if size.width() == 100 and size.height() == 100 \
|
if size.width() == 100 and size.height() == 100 \
|
||||||
and 'viewBox' in elem.attrib:
|
and view_box is not None:
|
||||||
box = [float(x) for x in elem.attrib['viewBox'].split()]
|
box = [float(x) for x in view_box.split()]
|
||||||
size.setWidth(box[2] - box[0])
|
size.setWidth(box[2] - box[0])
|
||||||
size.setHeight(box[3] - box[1])
|
size.setHeight(box[3] - box[1])
|
||||||
if width or height:
|
if width or height:
|
||||||
|
@ -13,6 +13,7 @@ from urlparse import urldefrag
|
|||||||
from lxml import etree
|
from lxml import etree
|
||||||
import cssutils
|
import cssutils
|
||||||
from calibre.ebooks.oeb.base import XPNSMAP, CSS_MIME, OEB_DOCS
|
from calibre.ebooks.oeb.base import XPNSMAP, CSS_MIME, OEB_DOCS
|
||||||
|
from calibre.ebooks.oeb.base import urlnormalize
|
||||||
|
|
||||||
LINK_SELECTORS = []
|
LINK_SELECTORS = []
|
||||||
for expr in ('//h:link/@href', '//h:img/@src', '//h:object/@data',
|
for expr in ('//h:link/@href', '//h:img/@src', '//h:object/@data',
|
||||||
@ -46,7 +47,7 @@ class ManifestTrimmer(object):
|
|||||||
item.data is not None:
|
item.data is not None:
|
||||||
hrefs = [sel(item.data) for sel in LINK_SELECTORS]
|
hrefs = [sel(item.data) for sel in LINK_SELECTORS]
|
||||||
for href in chain(*hrefs):
|
for href in chain(*hrefs):
|
||||||
href = item.abshref(href)
|
href = item.abshref(urlnormalize(href))
|
||||||
if href in oeb.manifest.hrefs:
|
if href in oeb.manifest.hrefs:
|
||||||
found = oeb.manifest.hrefs[href]
|
found = oeb.manifest.hrefs[href]
|
||||||
if found not in used:
|
if found not in used:
|
||||||
|
@ -61,6 +61,7 @@ class Config(ResizableDialog, Ui_Dialog):
|
|||||||
self.opt_toc_title.setVisible(False)
|
self.opt_toc_title.setVisible(False)
|
||||||
self.toc_title_label.setVisible(False)
|
self.toc_title_label.setVisible(False)
|
||||||
self.opt_rescale_images.setVisible(False)
|
self.opt_rescale_images.setVisible(False)
|
||||||
|
self.opt_ignore_tables.setVisible(False)
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
self.__w = []
|
self.__w = []
|
||||||
|
@ -93,7 +93,7 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="QStackedWidget" name="stack" >
|
<widget class="QStackedWidget" name="stack" >
|
||||||
<property name="currentIndex" >
|
<property name="currentIndex" >
|
||||||
<number>0</number>
|
<number>1</number>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QWidget" name="metadata_page" >
|
<widget class="QWidget" name="metadata_page" >
|
||||||
<layout class="QGridLayout" name="gridLayout_4" >
|
<layout class="QGridLayout" name="gridLayout_4" >
|
||||||
@ -105,6 +105,36 @@
|
|||||||
<string>Book Cover</string>
|
<string>Book Cover</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="_2" >
|
<layout class="QGridLayout" name="_2" >
|
||||||
|
<item row="0" column="0" >
|
||||||
|
<layout class="QHBoxLayout" name="_3" >
|
||||||
|
<item>
|
||||||
|
<widget class="ImageView" name="cover" >
|
||||||
|
<property name="text" >
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
<property name="pixmap" >
|
||||||
|
<pixmap resource="../images.qrc" >:/images/book.svg</pixmap>
|
||||||
|
</property>
|
||||||
|
<property name="scaledContents" >
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="alignment" >
|
||||||
|
<set>Qt::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0" >
|
||||||
|
<widget class="QCheckBox" name="opt_prefer_metadata_cover" >
|
||||||
|
<property name="text" >
|
||||||
|
<string>Use cover from &source file</string>
|
||||||
|
</property>
|
||||||
|
<property name="checked" >
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item row="1" column="0" >
|
<item row="1" column="0" >
|
||||||
<layout class="QVBoxLayout" name="_4" >
|
<layout class="QVBoxLayout" name="_4" >
|
||||||
<property name="spacing" >
|
<property name="spacing" >
|
||||||
@ -156,36 +186,6 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="0" >
|
|
||||||
<widget class="QCheckBox" name="opt_prefer_metadata_cover" >
|
|
||||||
<property name="text" >
|
|
||||||
<string>Use cover from &source file</string>
|
|
||||||
</property>
|
|
||||||
<property name="checked" >
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="0" >
|
|
||||||
<layout class="QHBoxLayout" name="_3" >
|
|
||||||
<item>
|
|
||||||
<widget class="ImageView" name="cover" >
|
|
||||||
<property name="text" >
|
|
||||||
<string/>
|
|
||||||
</property>
|
|
||||||
<property name="pixmap" >
|
|
||||||
<pixmap resource="../images.qrc" >:/images/book.svg</pixmap>
|
|
||||||
</property>
|
|
||||||
<property name="scaledContents" >
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
<property name="alignment" >
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
</layout>
|
</layout>
|
||||||
<zorder>opt_prefer_metadata_cover</zorder>
|
<zorder>opt_prefer_metadata_cover</zorder>
|
||||||
<zorder></zorder>
|
<zorder></zorder>
|
||||||
@ -479,6 +479,13 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="5" column="0" >
|
||||||
|
<widget class="QCheckBox" name="opt_ignore_tables" >
|
||||||
|
<property name="text" >
|
||||||
|
<string>&Ignore tables</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>887</width>
|
<width>887</width>
|
||||||
<height>740</height>
|
<height>750</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="sizePolicy" >
|
<property name="sizePolicy" >
|
||||||
@ -43,7 +43,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>879</width>
|
<width>879</width>
|
||||||
<height>700</height>
|
<height>710</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_5" >
|
<layout class="QVBoxLayout" name="verticalLayout_5" >
|
||||||
@ -55,7 +55,7 @@
|
|||||||
<property name="minimumSize" >
|
<property name="minimumSize" >
|
||||||
<size>
|
<size>
|
||||||
<width>800</width>
|
<width>800</width>
|
||||||
<height>685</height>
|
<height>665</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_3" >
|
<layout class="QVBoxLayout" name="verticalLayout_3" >
|
||||||
|
@ -7,11 +7,11 @@ __docformat__ = 'restructuredtext en'
|
|||||||
Scheduler for automated recipe downloads
|
Scheduler for automated recipe downloads
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import sys, copy
|
import sys, copy, time
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from PyQt4.Qt import QDialog, QApplication, QLineEdit, QPalette, SIGNAL, QBrush, \
|
from PyQt4.Qt import QDialog, QApplication, QLineEdit, QPalette, SIGNAL, QBrush, \
|
||||||
QColor, QAbstractListModel, Qt, QVariant, QFont, QIcon, \
|
QColor, QAbstractListModel, Qt, QVariant, QFont, QIcon, \
|
||||||
QFile, QObject, QTimer, QMutex, QMenu, QAction
|
QFile, QObject, QTimer, QMutex, QMenu, QAction, QTime
|
||||||
|
|
||||||
from calibre import english_sort
|
from calibre import english_sort
|
||||||
from calibre.gui2.dialogs.scheduler_ui import Ui_Dialog
|
from calibre.gui2.dialogs.scheduler_ui import Ui_Dialog
|
||||||
@ -66,7 +66,10 @@ class Recipe(object):
|
|||||||
return self.id == getattr(other, 'id', None)
|
return self.id == getattr(other, 'id', None)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return u'%s|%s|%s|%s'%(self.id, self.title, self.last_downloaded.ctime(), self.schedule)
|
schedule = self.schedule
|
||||||
|
if schedule and schedule > 1e5:
|
||||||
|
schedule = decode_schedule(schedule)
|
||||||
|
return u'%s|%s|%s|%s'%(self.id, self.title, self.last_downloaded.ctime(), schedule)
|
||||||
|
|
||||||
builtin_recipes = [Recipe(m, r, True) for r, m in zip(recipes, recipe_modules)]
|
builtin_recipes = [Recipe(m, r, True) for r, m in zip(recipes, recipe_modules)]
|
||||||
|
|
||||||
@ -170,6 +173,11 @@ class RecipeModel(QAbstractListModel, SearchQueryParser):
|
|||||||
|
|
||||||
return NONE
|
return NONE
|
||||||
|
|
||||||
|
def update_recipe_schedule(self, recipe):
|
||||||
|
for srecipe in self.recipes:
|
||||||
|
if srecipe == recipe:
|
||||||
|
srecipe.schedule = recipe.schedule
|
||||||
|
|
||||||
|
|
||||||
class Search(QLineEdit):
|
class Search(QLineEdit):
|
||||||
|
|
||||||
@ -210,7 +218,17 @@ class Search(QLineEdit):
|
|||||||
text = unicode(self.text())
|
text = unicode(self.text())
|
||||||
self.emit(SIGNAL('search(PyQt_PyObject)'), text)
|
self.emit(SIGNAL('search(PyQt_PyObject)'), text)
|
||||||
|
|
||||||
|
def encode_schedule(day, hour, minute):
|
||||||
|
day = 1e7 * (day+1)
|
||||||
|
hour = 1e4 * (hour+1)
|
||||||
|
return day + hour + minute + 1
|
||||||
|
|
||||||
|
def decode_schedule(num):
|
||||||
|
raw = '%d'%int(num)
|
||||||
|
day = int(raw[0])
|
||||||
|
hour = int(raw[2:4])
|
||||||
|
minute = int(raw[-2:])
|
||||||
|
return day-1, hour-1, minute-1
|
||||||
|
|
||||||
class SchedulerDialog(QDialog, Ui_Dialog):
|
class SchedulerDialog(QDialog, Ui_Dialog):
|
||||||
|
|
||||||
@ -228,17 +246,22 @@ class SchedulerDialog(QDialog, Ui_Dialog):
|
|||||||
self.connect(self.username, SIGNAL('textEdited(QString)'), self.set_account_info)
|
self.connect(self.username, SIGNAL('textEdited(QString)'), self.set_account_info)
|
||||||
self.connect(self.password, SIGNAL('textEdited(QString)'), self.set_account_info)
|
self.connect(self.password, SIGNAL('textEdited(QString)'), self.set_account_info)
|
||||||
self.connect(self.schedule, SIGNAL('stateChanged(int)'), self.do_schedule)
|
self.connect(self.schedule, SIGNAL('stateChanged(int)'), self.do_schedule)
|
||||||
self.connect(self.schedule, SIGNAL('stateChanged(int)'),
|
|
||||||
lambda state: self.interval.setEnabled(state == Qt.Checked))
|
|
||||||
self.connect(self.show_password, SIGNAL('stateChanged(int)'),
|
self.connect(self.show_password, SIGNAL('stateChanged(int)'),
|
||||||
lambda state: self.password.setEchoMode(self.password.Normal if state == Qt.Checked else self.password.Password))
|
lambda state: self.password.setEchoMode(self.password.Normal if state == Qt.Checked else self.password.Password))
|
||||||
self.connect(self.interval, SIGNAL('valueChanged(double)'), self.do_schedule)
|
self.connect(self.interval, SIGNAL('valueChanged(double)'), self.do_schedule)
|
||||||
|
self.connect(self.day, SIGNAL('currentIndexChanged(int)'), self.do_schedule)
|
||||||
|
self.connect(self.time, SIGNAL('timeChanged(QTime)'), self.do_schedule)
|
||||||
|
for button in (self.daily_button, self.interval_button):
|
||||||
|
self.connect(button, SIGNAL('toggled(bool)'), self.do_schedule)
|
||||||
self.connect(self.search, SIGNAL('search(PyQt_PyObject)'), self._model.search)
|
self.connect(self.search, SIGNAL('search(PyQt_PyObject)'), self._model.search)
|
||||||
self.connect(self._model, SIGNAL('modelReset()'), lambda : self.detail_box.setVisible(False))
|
self.connect(self._model, SIGNAL('modelReset()'), lambda : self.detail_box.setVisible(False))
|
||||||
self.connect(self.download, SIGNAL('clicked()'), self.download_now)
|
self.connect(self.download, SIGNAL('clicked()'), self.download_now)
|
||||||
self.search.setFocus(Qt.OtherFocusReason)
|
self.search.setFocus(Qt.OtherFocusReason)
|
||||||
self.old_news.setValue(gconf['oldest_news'])
|
self.old_news.setValue(gconf['oldest_news'])
|
||||||
self.rnumber.setText(_('%d recipes')%self._model.rowCount(None))
|
self.rnumber.setText(_('%d recipes')%self._model.rowCount(None))
|
||||||
|
for day in (_('day'), _('Monday'), _('Tuesday'), _('Wednesday'),
|
||||||
|
_('Thursday'), _('Friday'), _('Saturday'), _('Sunday')):
|
||||||
|
self.day.addItem(day)
|
||||||
|
|
||||||
def download_now(self):
|
def download_now(self):
|
||||||
recipe = self._model.data(self.recipes.currentIndex(), Qt.UserRole)
|
recipe = self._model.data(self.recipes.currentIndex(), Qt.UserRole)
|
||||||
@ -252,6 +275,8 @@ class SchedulerDialog(QDialog, Ui_Dialog):
|
|||||||
config[key] = (username, password) if username and password else None
|
config[key] = (username, password) if username and password else None
|
||||||
|
|
||||||
def do_schedule(self, *args):
|
def do_schedule(self, *args):
|
||||||
|
if not getattr(self, 'allow_scheduling', False):
|
||||||
|
return
|
||||||
recipe = self.recipes.currentIndex()
|
recipe = self.recipes.currentIndex()
|
||||||
if not recipe.isValid():
|
if not recipe.isValid():
|
||||||
return
|
return
|
||||||
@ -263,17 +288,26 @@ class SchedulerDialog(QDialog, Ui_Dialog):
|
|||||||
else:
|
else:
|
||||||
recipe.last_downloaded = datetime.fromordinal(1)
|
recipe.last_downloaded = datetime.fromordinal(1)
|
||||||
recipes.append(recipe)
|
recipes.append(recipe)
|
||||||
recipe.schedule = self.interval.value()
|
|
||||||
if recipe.schedule < 0.1:
|
|
||||||
recipe.schedule = 1/24.
|
|
||||||
if recipe.needs_subscription and not config['recipe_account_info_%s'%recipe.id]:
|
if recipe.needs_subscription and not config['recipe_account_info_%s'%recipe.id]:
|
||||||
error_dialog(self, _('Must set account information'), _('This recipe requires a username and password')).exec_()
|
error_dialog(self, _('Must set account information'), _('This recipe requires a username and password')).exec_()
|
||||||
self.schedule.setCheckState(Qt.Unchecked)
|
self.schedule.setCheckState(Qt.Unchecked)
|
||||||
return
|
return
|
||||||
|
if self.interval_button.isChecked():
|
||||||
|
recipe.schedule = self.interval.value()
|
||||||
|
if recipe.schedule < 0.1:
|
||||||
|
recipe.schedule = 1/24.
|
||||||
|
else:
|
||||||
|
day_of_week = self.day.currentIndex() - 1
|
||||||
|
if day_of_week < 0:
|
||||||
|
day_of_week = 7
|
||||||
|
t = self.time.time()
|
||||||
|
hour, minute = t.hour(), t.minute()
|
||||||
|
recipe.schedule = encode_schedule(day_of_week, hour, minute)
|
||||||
else:
|
else:
|
||||||
if recipe in recipes:
|
if recipe in recipes:
|
||||||
recipes.remove(recipe)
|
recipes.remove(recipe)
|
||||||
save_recipes(recipes)
|
save_recipes(recipes)
|
||||||
|
self._model.update_recipe_schedule(recipe)
|
||||||
self.emit(SIGNAL('new_schedule(PyQt_PyObject)'), recipes)
|
self.emit(SIGNAL('new_schedule(PyQt_PyObject)'), recipes)
|
||||||
|
|
||||||
def show_recipe(self, index):
|
def show_recipe(self, index):
|
||||||
@ -282,8 +316,26 @@ class SchedulerDialog(QDialog, Ui_Dialog):
|
|||||||
self.title.setText(recipe.title)
|
self.title.setText(recipe.title)
|
||||||
self.author.setText(_('Created by: ') + recipe.author)
|
self.author.setText(_('Created by: ') + recipe.author)
|
||||||
self.description.setText(recipe.description if recipe.description else '')
|
self.description.setText(recipe.description if recipe.description else '')
|
||||||
|
self.allow_scheduling = False
|
||||||
|
schedule = -1 if recipe.schedule is None else recipe.schedule
|
||||||
|
if schedule < 1e5 and schedule >= 0:
|
||||||
|
self.interval.setValue(schedule)
|
||||||
|
self.interval_button.setChecked(True)
|
||||||
|
self.day.setEnabled(False), self.time.setEnabled(False)
|
||||||
|
else:
|
||||||
|
if schedule > 0:
|
||||||
|
day, hour, minute = decode_schedule(schedule)
|
||||||
|
else:
|
||||||
|
day, hour, minute = 7, 12, 0
|
||||||
|
if day == 7:
|
||||||
|
day = -1
|
||||||
|
self.day.setCurrentIndex(day+1)
|
||||||
|
self.time.setTime(QTime(hour, minute))
|
||||||
|
self.daily_button.setChecked(True)
|
||||||
|
self.interval_button.setChecked(False)
|
||||||
|
self.interval.setEnabled(False)
|
||||||
self.schedule.setChecked(recipe.schedule is not None)
|
self.schedule.setChecked(recipe.schedule is not None)
|
||||||
self.interval.setValue(recipe.schedule if recipe.schedule is not None else 1)
|
self.allow_scheduling = True
|
||||||
self.detail_box.setVisible(True)
|
self.detail_box.setVisible(True)
|
||||||
self.account.setVisible(recipe.needs_subscription)
|
self.account.setVisible(recipe.needs_subscription)
|
||||||
self.interval.setEnabled(self.schedule.checkState() == Qt.Checked)
|
self.interval.setEnabled(self.schedule.checkState() == Qt.Checked)
|
||||||
@ -365,13 +417,22 @@ class Scheduler(QObject):
|
|||||||
self.dirtied = False
|
self.dirtied = False
|
||||||
needs_downloading = set([])
|
needs_downloading = set([])
|
||||||
self.debug('Checking...')
|
self.debug('Checking...')
|
||||||
now = datetime.utcnow()
|
nowt = datetime.utcnow()
|
||||||
for recipe in self.recipes:
|
for recipe in self.recipes:
|
||||||
if recipe.schedule is None:
|
if recipe.schedule is None:
|
||||||
continue
|
continue
|
||||||
delta = now - recipe.last_downloaded
|
delta = nowt - recipe.last_downloaded
|
||||||
if delta > timedelta(days=recipe.schedule):
|
if recipe.schedule < 1e5:
|
||||||
needs_downloading.add(recipe)
|
if delta > timedelta(days=recipe.schedule):
|
||||||
|
needs_downloading.add(recipe)
|
||||||
|
else:
|
||||||
|
day, hour, minute = decode_schedule(recipe.schedule)
|
||||||
|
now = time.localtime()
|
||||||
|
day_matches = day > 6 or day == now.tm_wday
|
||||||
|
tnow = now.tm_hour*60 + now.tm_min
|
||||||
|
matches = day_matches and (hour*60+minute) < tnow
|
||||||
|
if matches and delta >= timedelta(days=1):
|
||||||
|
needs_downloading.add(recipe)
|
||||||
|
|
||||||
self.debug('Needs downloading:', needs_downloading)
|
self.debug('Needs downloading:', needs_downloading)
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>726</width>
|
<width>726</width>
|
||||||
<height>551</height>
|
<height>575</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle" >
|
<property name="windowTitle" >
|
||||||
@ -42,25 +42,12 @@
|
|||||||
</item>
|
</item>
|
||||||
<item row="0" column="1" >
|
<item row="0" column="1" >
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_3" >
|
<layout class="QVBoxLayout" name="verticalLayout_3" >
|
||||||
<item>
|
|
||||||
<spacer name="horizontalSpacer_3" >
|
|
||||||
<property name="orientation" >
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0" >
|
|
||||||
<size>
|
|
||||||
<width>40</width>
|
|
||||||
<height>0</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
<item>
|
<item>
|
||||||
<widget class="QGroupBox" name="detail_box" >
|
<widget class="QGroupBox" name="detail_box" >
|
||||||
<property name="title" >
|
<property name="title" >
|
||||||
<string>Schedule for download</string>
|
<string>Schedule for download</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_2" >
|
<layout class="QVBoxLayout" name="verticalLayout_4" >
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="title" >
|
<widget class="QLabel" name="title" >
|
||||||
<property name="font" >
|
<property name="font" >
|
||||||
@ -110,70 +97,97 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="QCheckBox" name="schedule" >
|
<widget class="QCheckBox" name="schedule" >
|
||||||
<property name="text" >
|
<property name="text" >
|
||||||
<string>&Schedule for download every:</string>
|
<string>&Schedule for download:</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout" >
|
<widget class="QWidget" native="1" name="widget" >
|
||||||
<item>
|
<property name="enabled" >
|
||||||
<spacer name="horizontalSpacer" >
|
<bool>false</bool>
|
||||||
<property name="orientation" >
|
</property>
|
||||||
<enum>Qt::Horizontal</enum>
|
<layout class="QVBoxLayout" name="verticalLayout_2" >
|
||||||
</property>
|
<item>
|
||||||
<property name="sizeHint" stdset="0" >
|
<layout class="QHBoxLayout" name="horizontalLayout_2" >
|
||||||
<size>
|
<item>
|
||||||
<width>40</width>
|
<widget class="QRadioButton" name="daily_button" >
|
||||||
<height>20</height>
|
<property name="text" >
|
||||||
</size>
|
<string>Every </string>
|
||||||
</property>
|
</property>
|
||||||
</spacer>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QDoubleSpinBox" name="interval" >
|
<widget class="QComboBox" name="day" />
|
||||||
<property name="sizePolicy" >
|
</item>
|
||||||
<sizepolicy vsizetype="Fixed" hsizetype="Expanding" >
|
<item>
|
||||||
<horstretch>0</horstretch>
|
<widget class="QLabel" name="label_4" >
|
||||||
<verstretch>0</verstretch>
|
<property name="text" >
|
||||||
</sizepolicy>
|
<string>at</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="toolTip" >
|
</widget>
|
||||||
<string>Interval at which to download this recipe. A value of zero means that the recipe will be downloaded every hour.</string>
|
</item>
|
||||||
</property>
|
<item>
|
||||||
<property name="suffix" >
|
<widget class="QTimeEdit" name="time" />
|
||||||
<string> days</string>
|
</item>
|
||||||
</property>
|
<item>
|
||||||
<property name="decimals" >
|
<spacer name="horizontalSpacer" >
|
||||||
<number>1</number>
|
<property name="orientation" >
|
||||||
</property>
|
<enum>Qt::Horizontal</enum>
|
||||||
<property name="minimum" >
|
</property>
|
||||||
<double>0.000000000000000</double>
|
<property name="sizeHint" stdset="0" >
|
||||||
</property>
|
<size>
|
||||||
<property name="maximum" >
|
<width>40</width>
|
||||||
<double>365.100000000000023</double>
|
<height>20</height>
|
||||||
</property>
|
</size>
|
||||||
<property name="singleStep" >
|
</property>
|
||||||
<double>1.000000000000000</double>
|
</spacer>
|
||||||
</property>
|
</item>
|
||||||
<property name="value" >
|
</layout>
|
||||||
<double>1.000000000000000</double>
|
</item>
|
||||||
</property>
|
<item>
|
||||||
</widget>
|
<layout class="QHBoxLayout" name="horizontalLayout" >
|
||||||
</item>
|
<item>
|
||||||
<item>
|
<widget class="QRadioButton" name="interval_button" >
|
||||||
<spacer name="horizontalSpacer_2" >
|
<property name="text" >
|
||||||
<property name="orientation" >
|
<string>Every </string>
|
||||||
<enum>Qt::Horizontal</enum>
|
</property>
|
||||||
</property>
|
</widget>
|
||||||
<property name="sizeHint" stdset="0" >
|
</item>
|
||||||
<size>
|
<item>
|
||||||
<width>40</width>
|
<widget class="QDoubleSpinBox" name="interval" >
|
||||||
<height>20</height>
|
<property name="sizePolicy" >
|
||||||
</size>
|
<sizepolicy vsizetype="Fixed" hsizetype="Expanding" >
|
||||||
</property>
|
<horstretch>0</horstretch>
|
||||||
</spacer>
|
<verstretch>0</verstretch>
|
||||||
</item>
|
</sizepolicy>
|
||||||
</layout>
|
</property>
|
||||||
|
<property name="toolTip" >
|
||||||
|
<string>Interval at which to download this recipe. A value of zero means that the recipe will be downloaded every hour.</string>
|
||||||
|
</property>
|
||||||
|
<property name="suffix" >
|
||||||
|
<string> days</string>
|
||||||
|
</property>
|
||||||
|
<property name="decimals" >
|
||||||
|
<number>1</number>
|
||||||
|
</property>
|
||||||
|
<property name="minimum" >
|
||||||
|
<double>0.000000000000000</double>
|
||||||
|
</property>
|
||||||
|
<property name="maximum" >
|
||||||
|
<double>365.100000000000023</double>
|
||||||
|
</property>
|
||||||
|
<property name="singleStep" >
|
||||||
|
<double>1.000000000000000</double>
|
||||||
|
</property>
|
||||||
|
<property name="value" >
|
||||||
|
<double>1.000000000000000</double>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="last_downloaded" >
|
<widget class="QLabel" name="last_downloaded" >
|
||||||
@ -315,8 +329,8 @@
|
|||||||
<slot>accept()</slot>
|
<slot>accept()</slot>
|
||||||
<hints>
|
<hints>
|
||||||
<hint type="sourcelabel" >
|
<hint type="sourcelabel" >
|
||||||
<x>248</x>
|
<x>613</x>
|
||||||
<y>254</y>
|
<y>824</y>
|
||||||
</hint>
|
</hint>
|
||||||
<hint type="destinationlabel" >
|
<hint type="destinationlabel" >
|
||||||
<x>157</x>
|
<x>157</x>
|
||||||
@ -331,8 +345,8 @@
|
|||||||
<slot>reject()</slot>
|
<slot>reject()</slot>
|
||||||
<hints>
|
<hints>
|
||||||
<hint type="sourcelabel" >
|
<hint type="sourcelabel" >
|
||||||
<x>316</x>
|
<x>681</x>
|
||||||
<y>260</y>
|
<y>824</y>
|
||||||
</hint>
|
</hint>
|
||||||
<hint type="destinationlabel" >
|
<hint type="destinationlabel" >
|
||||||
<x>286</x>
|
<x>286</x>
|
||||||
@ -340,5 +354,85 @@
|
|||||||
</hint>
|
</hint>
|
||||||
</hints>
|
</hints>
|
||||||
</connection>
|
</connection>
|
||||||
|
<connection>
|
||||||
|
<sender>schedule</sender>
|
||||||
|
<signal>toggled(bool)</signal>
|
||||||
|
<receiver>widget</receiver>
|
||||||
|
<slot>setDisabled(bool)</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel" >
|
||||||
|
<x>454</x>
|
||||||
|
<y>147</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel" >
|
||||||
|
<x>461</x>
|
||||||
|
<y>168</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
<connection>
|
||||||
|
<sender>schedule</sender>
|
||||||
|
<signal>toggled(bool)</signal>
|
||||||
|
<receiver>widget</receiver>
|
||||||
|
<slot>setEnabled(bool)</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel" >
|
||||||
|
<x>458</x>
|
||||||
|
<y>137</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel" >
|
||||||
|
<x>461</x>
|
||||||
|
<y>169</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
<connection>
|
||||||
|
<sender>daily_button</sender>
|
||||||
|
<signal>toggled(bool)</signal>
|
||||||
|
<receiver>day</receiver>
|
||||||
|
<slot>setEnabled(bool)</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel" >
|
||||||
|
<x>421</x>
|
||||||
|
<y>186</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel" >
|
||||||
|
<x>500</x>
|
||||||
|
<y>184</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
<connection>
|
||||||
|
<sender>daily_button</sender>
|
||||||
|
<signal>toggled(bool)</signal>
|
||||||
|
<receiver>time</receiver>
|
||||||
|
<slot>setEnabled(bool)</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel" >
|
||||||
|
<x>442</x>
|
||||||
|
<y>193</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel" >
|
||||||
|
<x>603</x>
|
||||||
|
<y>183</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
<connection>
|
||||||
|
<sender>interval_button</sender>
|
||||||
|
<signal>toggled(bool)</signal>
|
||||||
|
<receiver>interval</receiver>
|
||||||
|
<slot>setEnabled(bool)</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel" >
|
||||||
|
<x>428</x>
|
||||||
|
<y>213</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel" >
|
||||||
|
<x>495</x>
|
||||||
|
<y>218</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
</connections>
|
</connections>
|
||||||
</ui>
|
</ui>
|
||||||
|
@ -5,8 +5,8 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>719</width>
|
<width>738</width>
|
||||||
<height>612</height>
|
<height>640</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle" >
|
<property name="windowTitle" >
|
||||||
@ -33,8 +33,8 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>711</width>
|
<width>730</width>
|
||||||
<height>572</height>
|
<height>600</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_3" >
|
<layout class="QVBoxLayout" name="verticalLayout_3" >
|
||||||
|
BIN
src/calibre/gui2/images/news/teleread.png
Normal file
BIN
src/calibre/gui2/images/news/teleread.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 287 B |
@ -63,7 +63,8 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
p.end()
|
p.end()
|
||||||
self.default_thumbnail = (pixmap.width(), pixmap.height(), pixmap_to_data(pixmap))
|
self.default_thumbnail = (pixmap.width(), pixmap.height(), pixmap_to_data(pixmap))
|
||||||
|
|
||||||
def __init__(self, single_instance, opts, parent=None):
|
def __init__(self, single_instance, opts, actions, parent=None):
|
||||||
|
self.preferences_action, self.quit_action = actions
|
||||||
MainWindow.__init__(self, opts, parent)
|
MainWindow.__init__(self, opts, parent)
|
||||||
# Initialize fontconfig in a separate thread as this can be a lengthy
|
# Initialize fontconfig in a separate thread as this can be a lengthy
|
||||||
# process if run for the first time on this machine
|
# process if run for the first time on this machine
|
||||||
@ -99,8 +100,8 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
self.system_tray_icon.show()
|
self.system_tray_icon.show()
|
||||||
self.system_tray_menu = QMenu()
|
self.system_tray_menu = QMenu()
|
||||||
self.restore_action = self.system_tray_menu.addAction(QIcon(':/images/page.svg'), _('&Restore'))
|
self.restore_action = self.system_tray_menu.addAction(QIcon(':/images/page.svg'), _('&Restore'))
|
||||||
self.donate_action = self.system_tray_menu.addAction(QIcon(':/images/donate.svg'), _('&Donate'))
|
self.donate_action = self.system_tray_menu.addAction(QIcon(':/images/donate.svg'), _('&Donate to support calibre'))
|
||||||
self.quit_action = QAction(QIcon(':/images/window-close.svg'), _('&Quit'), self)
|
self.donate_button.setDefaultAction(self.donate_action)
|
||||||
self.addAction(self.quit_action)
|
self.addAction(self.quit_action)
|
||||||
self.action_restart = QAction(_('&Restart'), self)
|
self.action_restart = QAction(_('&Restart'), self)
|
||||||
self.addAction(self.action_restart)
|
self.addAction(self.action_restart)
|
||||||
@ -242,6 +243,7 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
self.tool_bar.setContextMenuPolicy(Qt.PreventContextMenu)
|
self.tool_bar.setContextMenuPolicy(Qt.PreventContextMenu)
|
||||||
|
|
||||||
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)
|
||||||
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 ########################
|
||||||
@ -912,13 +914,14 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
_files = self.library_view.model().get_preferred_formats(rows,
|
_files = self.library_view.model().get_preferred_formats(rows,
|
||||||
self.device_manager.device_class.FORMATS, paths=True)
|
self.device_manager.device_class.FORMATS, paths=True)
|
||||||
files = [getattr(f, 'name', None) for f in _files]
|
files = [getattr(f, 'name', None) for f in _files]
|
||||||
bad, good, gf, names = [], [], [], []
|
bad, good, gf, names, remove_ids = [], [], [], [], []
|
||||||
for f in files:
|
for f in files:
|
||||||
mi = metadata.next()
|
mi = metadata.next()
|
||||||
id = ids.next()
|
id = ids.next()
|
||||||
if f is None:
|
if f is None:
|
||||||
bad.append(mi['title'])
|
bad.append(mi['title'])
|
||||||
else:
|
else:
|
||||||
|
remove_ids.append(id)
|
||||||
aus = mi['authors'].split(',')
|
aus = mi['authors'].split(',')
|
||||||
aus2 = []
|
aus2 = []
|
||||||
for a in aus:
|
for a in aus:
|
||||||
@ -945,7 +948,7 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
prefix = prefix.decode(preferred_encoding, 'replace')
|
prefix = prefix.decode(preferred_encoding, 'replace')
|
||||||
prefix = ascii_filename(prefix)
|
prefix = ascii_filename(prefix)
|
||||||
names.append('%s_%d%s'%(prefix, id, os.path.splitext(f)[1]))
|
names.append('%s_%d%s'%(prefix, id, os.path.splitext(f)[1]))
|
||||||
remove = [self.library_view.model().id(r) for r in rows] 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:
|
||||||
@ -1420,7 +1423,7 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
self.restart_after_quit = restart
|
self.restart_after_quit = restart
|
||||||
QApplication.instance().quit()
|
QApplication.instance().quit()
|
||||||
|
|
||||||
def donate(self):
|
def donate(self, *args):
|
||||||
BUTTON = '''
|
BUTTON = '''
|
||||||
<form action="https://www.paypal.com/cgi-bin/webscr" method="post">
|
<form action="https://www.paypal.com/cgi-bin/webscr" method="post">
|
||||||
<input type="hidden" name="cmd" value="_s-xclick">
|
<input type="hidden" name="cmd" value="_s-xclick">
|
||||||
@ -1546,6 +1549,7 @@ def main(args=sys.argv):
|
|||||||
prefs.set('library_path', opts.with_library)
|
prefs.set('library_path', opts.with_library)
|
||||||
print 'Using library at', prefs['library_path']
|
print 'Using library at', prefs['library_path']
|
||||||
app = Application(args)
|
app = Application(args)
|
||||||
|
actions = tuple(Main.create_application_menubar())
|
||||||
app.setWindowIcon(QIcon(':/library'))
|
app.setWindowIcon(QIcon(':/library'))
|
||||||
QCoreApplication.setOrganizationName(ORG_NAME)
|
QCoreApplication.setOrganizationName(ORG_NAME)
|
||||||
QCoreApplication.setApplicationName(APP_UID)
|
QCoreApplication.setApplicationName(APP_UID)
|
||||||
@ -1560,7 +1564,7 @@ def main(args=sys.argv):
|
|||||||
'<p>%s is already running. %s</p>'%(__appname__, extra))
|
'<p>%s is already running. %s</p>'%(__appname__, extra))
|
||||||
return 1
|
return 1
|
||||||
initialize_file_icon_provider()
|
initialize_file_icon_provider()
|
||||||
main = Main(single_instance, opts)
|
main = Main(single_instance, opts, actions)
|
||||||
sys.excepthook = main.unhandled_exception
|
sys.excepthook = main.unhandled_exception
|
||||||
if len(args) > 1:
|
if len(args) > 1:
|
||||||
main.add_filesystem_book(args[1])
|
main.add_filesystem_book(args[1])
|
||||||
|
@ -82,6 +82,29 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="donate_button" >
|
||||||
|
<property name="cursor" >
|
||||||
|
<cursorShape>PointingHandCursor</cursorShape>
|
||||||
|
</property>
|
||||||
|
<property name="text" >
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon" >
|
||||||
|
<iconset resource="images.qrc" >
|
||||||
|
<normaloff>:/images/donate.svg</normaloff>:/images/donate.svg</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="iconSize" >
|
||||||
|
<size>
|
||||||
|
<width>64</width>
|
||||||
|
<height>64</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="autoRaise" >
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_3" >
|
<layout class="QVBoxLayout" name="verticalLayout_3" >
|
||||||
<item>
|
<item>
|
||||||
|
@ -3,7 +3,8 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
|
|
||||||
import StringIO, traceback, sys
|
import StringIO, traceback, sys
|
||||||
|
|
||||||
from PyQt4.Qt import QMainWindow, QString, Qt, QFont, QCoreApplication, SIGNAL
|
from PyQt4.Qt import QMainWindow, QString, Qt, QFont, QCoreApplication, SIGNAL,\
|
||||||
|
QAction, QMenu, QMenuBar, QIcon
|
||||||
from calibre.gui2.dialogs.conversion_error import ConversionErrorDialog
|
from calibre.gui2.dialogs.conversion_error import ConversionErrorDialog
|
||||||
from calibre.utils.config import OptionParser
|
from calibre.utils.config import OptionParser
|
||||||
|
|
||||||
@ -34,6 +35,27 @@ class DebugWindow(ConversionErrorDialog):
|
|||||||
|
|
||||||
class MainWindow(QMainWindow):
|
class MainWindow(QMainWindow):
|
||||||
|
|
||||||
|
_menu_bar = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_application_menubar(cls):
|
||||||
|
mb = QMenuBar(None)
|
||||||
|
menu = QMenu()
|
||||||
|
for action in cls.get_menubar_actions():
|
||||||
|
menu.addAction(action)
|
||||||
|
yield action
|
||||||
|
mb.addMenu(menu)
|
||||||
|
cls._menu_bar = mb
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_menubar_actions(cls):
|
||||||
|
preferences_action = QAction(QIcon(':/images/config.svg'), _('&Preferences'), None)
|
||||||
|
quit_action = QAction(QIcon(':/images/window-close.svg'), _('&Quit'), None)
|
||||||
|
preferences_action.setMenuRole(QAction.PreferencesRole)
|
||||||
|
quit_action.setMenuRole(QAction.QuitRole)
|
||||||
|
return preferences_action, quit_action
|
||||||
|
|
||||||
def __init__(self, opts, parent=None):
|
def __init__(self, opts, parent=None):
|
||||||
QMainWindow.__init__(self, parent)
|
QMainWindow.__init__(self, parent)
|
||||||
app = QCoreApplication.instance()
|
app = QCoreApplication.instance()
|
||||||
|
@ -148,6 +148,7 @@ class CoverFlowButton(QToolButton):
|
|||||||
self.setSizePolicy(QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding))
|
self.setSizePolicy(QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding))
|
||||||
self.connect(self, SIGNAL('toggled(bool)'), self.adjust_tooltip)
|
self.connect(self, SIGNAL('toggled(bool)'), self.adjust_tooltip)
|
||||||
self.adjust_tooltip(False)
|
self.adjust_tooltip(False)
|
||||||
|
self.setCursor(Qt.PointingHandCursor)
|
||||||
|
|
||||||
def adjust_tooltip(self, on):
|
def adjust_tooltip(self, on):
|
||||||
tt = _('Click to turn off Cover Browsing') if on else _('Click to browse books by their covers')
|
tt = _('Click to turn off Cover Browsing') if on else _('Click to browse books by their covers')
|
||||||
@ -165,6 +166,7 @@ class TagViewButton(QToolButton):
|
|||||||
self.setIcon(QIcon(':/images/tags.svg'))
|
self.setIcon(QIcon(':/images/tags.svg'))
|
||||||
self.setToolTip(_('Click to browse books by tags'))
|
self.setToolTip(_('Click to browse books by tags'))
|
||||||
self.setSizePolicy(QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding))
|
self.setSizePolicy(QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding))
|
||||||
|
self.setCursor(Qt.PointingHandCursor)
|
||||||
self.setCheckable(True)
|
self.setCheckable(True)
|
||||||
self.setChecked(False)
|
self.setChecked(False)
|
||||||
self.setAutoRaise(True)
|
self.setAutoRaise(True)
|
||||||
|
@ -46,7 +46,7 @@ What formats does |app| support conversion to/from?
|
|||||||
| | | | | |
|
| | | | | |
|
||||||
| | PDF | ✔ | ✔ | ✔ |
|
| | PDF | ✔ | ✔ | ✔ |
|
||||||
| | | | | |
|
| | | | | |
|
||||||
| | LRS | | ✔ | ✔ |
|
| | LRS | | ✔ | |
|
||||||
+-------------------+--------+------------------+-----------------------+-----------------------+
|
+-------------------+--------+------------------+-----------------------+-----------------------+
|
||||||
|
|
||||||
|
|
||||||
|
@ -158,21 +158,27 @@ class WorkerMother(object):
|
|||||||
self.executable = os.path.join(os.path.dirname(sys.executable),
|
self.executable = os.path.join(os.path.dirname(sys.executable),
|
||||||
'calibre-parallel.exe' if isfrozen else 'Scripts\\calibre-parallel.exe')
|
'calibre-parallel.exe' if isfrozen else 'Scripts\\calibre-parallel.exe')
|
||||||
elif isosx:
|
elif isosx:
|
||||||
self.executable = sys.executable
|
self.executable = self.gui_executable = sys.executable
|
||||||
self.prefix = ''
|
self.prefix = ''
|
||||||
if isfrozen:
|
if isfrozen:
|
||||||
fd = getattr(sys, 'frameworks_dir')
|
fd = getattr(sys, 'frameworks_dir')
|
||||||
contents = os.path.dirname(fd)
|
contents = os.path.dirname(fd)
|
||||||
resources = os.path.join(contents, 'Resources')
|
self.gui_executable = os.path.join(contents, 'MacOS',
|
||||||
sp = os.path.join(resources, 'lib', 'python'+sys.version[:3], 'site-packages.zip')
|
os.path.basename(sys.executable))
|
||||||
|
contents = os.path.join(contents, 'console.app', 'Contents')
|
||||||
|
self.executable = os.path.join(contents, 'MacOS',
|
||||||
|
os.path.basename(sys.executable))
|
||||||
|
|
||||||
|
resources = os.path.join(contents, 'Resources')
|
||||||
|
fd = os.path.join(contents, 'Frameworks')
|
||||||
|
sp = os.path.join(resources, 'lib', 'python'+sys.version[:3], 'site-packages.zip')
|
||||||
self.prefix += 'import sys; sys.frameworks_dir = "%s"; sys.frozen = "macosx_app"; '%fd
|
self.prefix += 'import sys; sys.frameworks_dir = "%s"; sys.frozen = "macosx_app"; '%fd
|
||||||
self.prefix += 'sys.path.insert(0, %s); '%repr(sp)
|
self.prefix += 'sys.path.insert(0, %s); '%repr(sp)
|
||||||
if fd not in os.environ['PATH']:
|
if fd not in os.environ['PATH']:
|
||||||
self.env['PATH'] = os.environ['PATH']+':'+fd
|
self.env['PATH'] = os.environ['PATH']+':'+fd
|
||||||
self.env['PYTHONHOME'] = resources
|
self.env['PYTHONHOME'] = resources
|
||||||
self.env['MAGICK_HOME'] = os.path.join(getattr(sys, 'frameworks_dir'), 'ImageMagick')
|
self.env['MAGICK_HOME'] = os.path.join(fd, 'ImageMagick')
|
||||||
self.env['DYLD_LIBRARY_PATH'] = os.path.join(getattr(sys, 'frameworks_dir'), 'ImageMagick', 'lib')
|
self.env['DYLD_LIBRARY_PATH'] = os.path.join(fd, 'ImageMagick', 'lib')
|
||||||
else:
|
else:
|
||||||
self.executable = os.path.join(getattr(sys, 'frozen_path'), 'calibre-parallel') \
|
self.executable = os.path.join(getattr(sys, 'frozen_path'), 'calibre-parallel') \
|
||||||
if isfrozen else 'calibre-parallel'
|
if isfrozen else 'calibre-parallel'
|
||||||
@ -219,7 +225,8 @@ class WorkerMother(object):
|
|||||||
|
|
||||||
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)
|
||||||
cmdline = [self.executable, '-c', self.prefix+script]
|
exe = self.gui_executable if type == 'free_spirit' else self.executable
|
||||||
|
cmdline = [exe, '-c', self.prefix+script]
|
||||||
child = WorkerStatus(subprocess.Popen(cmdline, env=self.get_env()))
|
child = WorkerStatus(subprocess.Popen(cmdline, env=self.get_env()))
|
||||||
atexit.register(self.cleanup_child_linux, child)
|
atexit.register(self.cleanup_child_linux, child)
|
||||||
return child
|
return child
|
||||||
|
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
@ -97,7 +97,7 @@ class OptionParser(_OptionParser):
|
|||||||
|
|
||||||
def merge(self, parser):
|
def merge(self, parser):
|
||||||
'''
|
'''
|
||||||
Add options from parser to self. In case of conflicts, confilicting options from
|
Add options from parser to self. In case of conflicts, conflicting options from
|
||||||
parser are skipped.
|
parser are skipped.
|
||||||
'''
|
'''
|
||||||
opts = list(parser.option_list)
|
opts = list(parser.option_list)
|
||||||
@ -224,6 +224,8 @@ class OptionSet(object):
|
|||||||
def update(self, other):
|
def update(self, other):
|
||||||
for name in other.groups.keys():
|
for name in other.groups.keys():
|
||||||
self.groups[name] = other.groups[name]
|
self.groups[name] = other.groups[name]
|
||||||
|
if name not in self.group_list:
|
||||||
|
self.group_list.append(name)
|
||||||
for pref in other.preferences:
|
for pref in other.preferences:
|
||||||
if pref in self.preferences:
|
if pref in self.preferences:
|
||||||
self.preferences.remove(pref)
|
self.preferences.remove(pref)
|
||||||
|
@ -24,6 +24,7 @@ recipe_modules = ['recipe_' + r for r in (
|
|||||||
'joelonsoftware', 'telepolis', 'common_dreams', 'nin', 'tomshardware_de',
|
'joelonsoftware', 'telepolis', 'common_dreams', 'nin', 'tomshardware_de',
|
||||||
'pagina12', 'infobae', 'ambito', 'elargentino', 'sueddeutsche', 'the_age',
|
'pagina12', 'infobae', 'ambito', 'elargentino', 'sueddeutsche', 'the_age',
|
||||||
'laprensa', 'amspec', 'freakonomics', 'criticadigital', 'elcronista',
|
'laprensa', 'amspec', 'freakonomics', 'criticadigital', 'elcronista',
|
||||||
|
'shacknews', 'teleread',
|
||||||
)]
|
)]
|
||||||
|
|
||||||
import re, imp, inspect, time, os
|
import re, imp, inspect, time, os
|
||||||
|
26
src/calibre/web/feeds/recipes/recipe_shacknews.py
Normal file
26
src/calibre/web/feeds/recipes/recipe_shacknews.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class Shacknews(BasicNewsRecipe):
|
||||||
|
__author__ = 'Docbrown00'
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
title = u'Shacknews'
|
||||||
|
oldest_article = 7
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
no_stylesheets = True
|
||||||
|
remove_tags = [dict(name='div', attrs={'class': ['nuggets', 'comments']}),
|
||||||
|
dict(name='p', attrs={'class': 'videoembed'})]
|
||||||
|
keep_only_tags = [dict(name='div', attrs={'class':'story'})]
|
||||||
|
feeds = [
|
||||||
|
(u'Latest News', u'http://feed.shacknews.com/shackfeed.xml'),
|
||||||
|
(u'PC', u'http://feed.shacknews.com/extras/tag_rss.x/PC'),
|
||||||
|
(u'Wii', u'http://feed.shacknews.com/extras/tag_rss.x/Nintendo+Wii'),
|
||||||
|
(u'Xbox 360', u'http://feed.shacknews.com/extras/tag_rss.x/Xbox+360'),
|
||||||
|
(u'Playstation 3', u'http://feed.shacknews.com/extras/tag_rss.x/PlayStation+3'),
|
||||||
|
(u'PSP', u'http://feed.shacknews.com/extras/tag_rss.x/PSP'),
|
||||||
|
(u'Nintendo DS', u'http://feed.shacknews.com/extras/tag_rss.x/Nintendo+DS'),
|
||||||
|
(u'iPhone', u'http://feed.shacknews.com/extras/tag_rss.x/iPhone'),
|
||||||
|
(u'DLC', u'http://feed.shacknews.com/extras/tag_rss.x/DLC'),
|
||||||
|
(u'Valve', u'http://feed.shacknews.com/extras/tag_rss.x/Valve'),
|
||||||
|
(u'Electronic Arts', u'http://feed.shacknews.com/extras/tag_rss.x/Electronic+Arts')
|
||||||
|
]
|
24
src/calibre/web/feeds/recipes/recipe_teleread.py
Normal file
24
src/calibre/web/feeds/recipes/recipe_teleread.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2009, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
'''
|
||||||
|
teleread.org
|
||||||
|
'''
|
||||||
|
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class Teleread(BasicNewsRecipe):
|
||||||
|
|
||||||
|
title = 'Teleread Blog'
|
||||||
|
description = 'News & views on e-books, libraries, publishing and related topics'
|
||||||
|
__author__ = 'Kovid Goyal'
|
||||||
|
|
||||||
|
feeds = [('Entries', 'http://www.teleread.org/feed/')]
|
||||||
|
|
||||||
|
remove_tags = [dict(attrs={'class':['sociable', 'comments',
|
||||||
|
'wlWriterSmartContent', 'feedflare']})]
|
||||||
|
|
||||||
|
def get_article_url(self, article):
|
||||||
|
return article.get('feedburner_origlink', article.get('link', article.get('guid')))
|
Loading…
x
Reference in New Issue
Block a user