mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
pre 0.6.20 changes
This commit is contained in:
commit
4566b13691
BIN
resources/images/news/variety.png
Normal file
BIN
resources/images/news/variety.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 332 B |
@ -12,7 +12,7 @@ from calibre.web.feeds.news import BasicNewsRecipe
|
|||||||
class DailyTelegraph(BasicNewsRecipe):
|
class DailyTelegraph(BasicNewsRecipe):
|
||||||
title = u'Daily Telegraph'
|
title = u'Daily Telegraph'
|
||||||
__author__ = u'AprilHare'
|
__author__ = u'AprilHare'
|
||||||
language = 'en'
|
language = 'en_AU'
|
||||||
|
|
||||||
description = u'News from down under'
|
description = u'News from down under'
|
||||||
oldest_article = 2
|
oldest_article = 2
|
||||||
|
@ -14,7 +14,6 @@ class OutlookIndia(BasicNewsRecipe):
|
|||||||
encoding = 'utf-8'
|
encoding = 'utf-8'
|
||||||
language = 'en_IN'
|
language = 'en_IN'
|
||||||
|
|
||||||
recursions = 1
|
|
||||||
extra_css = '''
|
extra_css = '''
|
||||||
body{font-family:Arial,Helvetica,sans-serif; font-size:xx-small;}
|
body{font-family:Arial,Helvetica,sans-serif; font-size:xx-small;}
|
||||||
.fspheading{color:#AF0E25 ; font-family:"Times New Roman",Times,serif; font-weight:bold ; font-size:large; }
|
.fspheading{color:#AF0E25 ; font-family:"Times New Roman",Times,serif; font-weight:bold ; font-size:large; }
|
||||||
|
46
resources/recipes/variety.recipe
Normal file
46
resources/recipes/variety.recipe
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>'
|
||||||
|
'''
|
||||||
|
www.variety.com
|
||||||
|
'''
|
||||||
|
|
||||||
|
from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||||
|
|
||||||
|
class Variety(BasicNewsRecipe):
|
||||||
|
title = 'Variety'
|
||||||
|
__author__ = 'Darko Miletic'
|
||||||
|
description = 'Breaking entertainment movie news, movie reviews, entertainment industry events, news and reviews from Cannes, Oscars, and Hollywood awards. Featuring box office charts, archives and more.'
|
||||||
|
oldest_article = 2
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
no_stylesheets = True
|
||||||
|
use_embedded_content = False
|
||||||
|
encoding = 'cp1252'
|
||||||
|
publisher = 'Red Business Information'
|
||||||
|
category = 'Entertainment Industry News, Daily Variety, Movie Reviews, TV, Awards, Oscars, Cannes, Box Office, Hollywood'
|
||||||
|
language = 'en'
|
||||||
|
|
||||||
|
conversion_options = {
|
||||||
|
'comments' : description
|
||||||
|
,'tags' : category
|
||||||
|
,'language' : language
|
||||||
|
,'publisher' : publisher
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_tags = [dict(name=['object','link','map'])]
|
||||||
|
|
||||||
|
keep_only_tags = [dict(name='div', attrs={'id':'article'})]
|
||||||
|
|
||||||
|
feeds = [(u'News & Articles', u'http://feeds.feedburner.com/variety/headlines' )]
|
||||||
|
|
||||||
|
def print_version(self, url):
|
||||||
|
rpt = url.rpartition('?')[0]
|
||||||
|
artid = rpt.rpartition('/')[2]
|
||||||
|
catidr = url.rpartition('categoryid=')[2]
|
||||||
|
catid = catidr.partition('&')[0]
|
||||||
|
return 'http://www.variety.com/index.asp?layout=print_story&articleid=' + artid + '&categoryid=' + catid
|
||||||
|
|
||||||
|
def get_article_url(self, article):
|
||||||
|
return article.get('feedburner_origlink', None)
|
||||||
|
|
@ -6,10 +6,9 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from setup.installer import VMInstaller
|
from setup.installer import VMInstaller
|
||||||
from setup import Command, installer_name
|
from setup import Command
|
||||||
|
|
||||||
class Linux32(VMInstaller):
|
class Linux32(VMInstaller):
|
||||||
|
|
||||||
@ -21,17 +20,12 @@ class Linux32(VMInstaller):
|
|||||||
FREEZE_COMMAND = 'linux_freeze'
|
FREEZE_COMMAND = 'linux_freeze'
|
||||||
|
|
||||||
|
|
||||||
class Linux64(Command):
|
class Linux64(Linux32):
|
||||||
|
|
||||||
description = 'Build 64bit linux binary installer'
|
description = 'Build 64bit linux binary installer'
|
||||||
|
VM_NAME = 'gentoo64_build'
|
||||||
sub_commands = ['linux_freeze']
|
VM = '/vmware/bin/gentoo64_build'
|
||||||
|
IS_64_BIT = True
|
||||||
def run(self, opts):
|
|
||||||
installer = installer_name('tar.bz2', True)
|
|
||||||
if not os.path.exists(installer):
|
|
||||||
raise Exception('Failed to build installer '+installer)
|
|
||||||
return os.path.basename(installer)
|
|
||||||
|
|
||||||
class Linux(Command):
|
class Linux(Command):
|
||||||
|
|
||||||
|
@ -157,6 +157,16 @@ def add_simple_plugin(path_to_plugin):
|
|||||||
def main(args=sys.argv):
|
def main(args=sys.argv):
|
||||||
from calibre.constants import debug
|
from calibre.constants import debug
|
||||||
debug()
|
debug()
|
||||||
|
if len(args) > 2 and args[1] in ('-e', '--exec-file'):
|
||||||
|
sys.argv = [args[2]] + args[3:]
|
||||||
|
ef = os.path.abspath(args[2])
|
||||||
|
base = os.path.dirname(ef)
|
||||||
|
sys.path.insert(0, base)
|
||||||
|
g = globals()
|
||||||
|
g['__name__'] = '__main__'
|
||||||
|
execfile(ef, g)
|
||||||
|
return
|
||||||
|
|
||||||
opts, args = option_parser().parse_args(args)
|
opts, args = option_parser().parse_args(args)
|
||||||
if opts.gui:
|
if opts.gui:
|
||||||
from calibre.gui2.main import main
|
from calibre.gui2.main import main
|
||||||
|
@ -102,7 +102,6 @@ def render_html(path_to_html, width=590, height=750):
|
|||||||
page.mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff)
|
page.mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff)
|
||||||
loop = QEventLoop()
|
loop = QEventLoop()
|
||||||
renderer = HTMLRenderer(page, loop)
|
renderer = HTMLRenderer(page, loop)
|
||||||
|
|
||||||
page.connect(page, SIGNAL('loadFinished(bool)'), renderer,
|
page.connect(page, SIGNAL('loadFinished(bool)'), renderer,
|
||||||
Qt.QueuedConnection)
|
Qt.QueuedConnection)
|
||||||
page.mainFrame().load(QUrl.fromLocalFile(path_to_html))
|
page.mainFrame().load(QUrl.fromLocalFile(path_to_html))
|
||||||
|
@ -228,17 +228,16 @@ class HTMLPreProcessor(object):
|
|||||||
else:
|
else:
|
||||||
rules = []
|
rules = []
|
||||||
|
|
||||||
pre_rules = []
|
end_rules = []
|
||||||
if getattr(self.extra_opts, 'remove_header', None):
|
if getattr(self.extra_opts, 'remove_header', None):
|
||||||
pre_rules.append(
|
end_rules.append(
|
||||||
(re.compile(getattr(self.extra_opts, 'header_regex')), lambda match : '')
|
(re.compile(getattr(self.extra_opts, 'header_regex')), lambda match : '')
|
||||||
)
|
)
|
||||||
if getattr(self.extra_opts, 'remove_footer', None):
|
if getattr(self.extra_opts, 'remove_footer', None):
|
||||||
pre_rules.append(
|
end_rules.append(
|
||||||
(re.compile(getattr(self.extra_opts, 'footer_regex')), lambda match : '')
|
(re.compile(getattr(self.extra_opts, 'footer_regex')), lambda match : '')
|
||||||
)
|
)
|
||||||
|
|
||||||
end_rules = []
|
|
||||||
if getattr(self.extra_opts, 'unwrap_factor', 0.0) > 0.01:
|
if getattr(self.extra_opts, 'unwrap_factor', 0.0) > 0.01:
|
||||||
length = line_length(html, getattr(self.extra_opts, 'unwrap_factor'))
|
length = line_length(html, getattr(self.extra_opts, 'unwrap_factor'))
|
||||||
if length:
|
if length:
|
||||||
@ -247,7 +246,7 @@ class HTMLPreProcessor(object):
|
|||||||
(re.compile(r'(?<=.{%i}[a-z\.,;:)-IA])\s*(?P<ital></(i|b|u)>)?\s*(<p.*?>)\s*(?=(<(i|b|u)>)?\s*[\w\d(])' % length, re.UNICODE), wrap_lines),
|
(re.compile(r'(?<=.{%i}[a-z\.,;:)-IA])\s*(?P<ital></(i|b|u)>)?\s*(<p.*?>)\s*(?=(<(i|b|u)>)?\s*[\w\d(])' % length, re.UNICODE), wrap_lines),
|
||||||
)
|
)
|
||||||
|
|
||||||
for rule in self.PREPROCESS + pre_rules + rules + end_rules:
|
for rule in self.PREPROCESS + rules + end_rules:
|
||||||
html = rule[0].sub(rule[1], html)
|
html = rule[0].sub(rule[1], html)
|
||||||
|
|
||||||
# Handle broken XHTML w/ SVG (ugh)
|
# Handle broken XHTML w/ SVG (ugh)
|
||||||
|
@ -320,8 +320,8 @@ class HTMLInput(InputFormatPlugin):
|
|||||||
oeb.logger.warn('Title not specified')
|
oeb.logger.warn('Title not specified')
|
||||||
metadata.add('title', self.oeb.translate(__('Unknown')))
|
metadata.add('title', self.oeb.translate(__('Unknown')))
|
||||||
|
|
||||||
bookid = "urn:uuid:%s" % str(uuid.uuid4())
|
bookid = str(uuid.uuid4())
|
||||||
metadata.add('identifier', bookid, id='calibre-uuid')
|
metadata.add('identifier', bookid, id='uuid_id', scheme='uuid')
|
||||||
for ident in metadata.identifier:
|
for ident in metadata.identifier:
|
||||||
if 'id' in ident.attrib:
|
if 'id' in ident.attrib:
|
||||||
self.oeb.uid = metadata.identifier[0]
|
self.oeb.uid = metadata.identifier[0]
|
||||||
@ -409,6 +409,9 @@ class HTMLInput(InputFormatPlugin):
|
|||||||
link = os.path.abspath(link)
|
link = os.path.abspath(link)
|
||||||
if not os.access(link, os.R_OK):
|
if not os.access(link, os.R_OK):
|
||||||
return link_
|
return link_
|
||||||
|
if os.path.isdir(link):
|
||||||
|
self.log.warn(link_, 'is a link to a directory. Ignoring.')
|
||||||
|
return link_
|
||||||
if not islinux:
|
if not islinux:
|
||||||
link = link.lower()
|
link = link.lower()
|
||||||
if link not in self.added_resources:
|
if link not in self.added_resources:
|
||||||
|
@ -218,7 +218,7 @@ class MetaInformation(object):
|
|||||||
'isbn', 'tags', 'cover_data', 'application_id', 'guide',
|
'isbn', 'tags', 'cover_data', 'application_id', 'guide',
|
||||||
'manifest', 'spine', 'toc', 'cover', 'language',
|
'manifest', 'spine', 'toc', 'cover', 'language',
|
||||||
'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc',
|
'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc',
|
||||||
'pubdate', 'rights', 'publication_type'):
|
'pubdate', 'rights', 'publication_type', 'uuid'):
|
||||||
if hasattr(mi, attr):
|
if hasattr(mi, attr):
|
||||||
setattr(ans, attr, getattr(mi, attr))
|
setattr(ans, attr, getattr(mi, attr))
|
||||||
|
|
||||||
@ -244,7 +244,7 @@ class MetaInformation(object):
|
|||||||
'series', 'series_index', 'rating', 'isbn', 'language',
|
'series', 'series_index', 'rating', 'isbn', 'language',
|
||||||
'application_id', 'manifest', 'toc', 'spine', 'guide', 'cover',
|
'application_id', 'manifest', 'toc', 'spine', 'guide', 'cover',
|
||||||
'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate',
|
'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate',
|
||||||
'rights', 'publication_type',
|
'rights', 'publication_type', 'uuid',
|
||||||
):
|
):
|
||||||
setattr(self, x, getattr(mi, x, None))
|
setattr(self, x, getattr(mi, x, None))
|
||||||
|
|
||||||
@ -264,7 +264,7 @@ class MetaInformation(object):
|
|||||||
'isbn', 'application_id', 'manifest', 'spine', 'toc',
|
'isbn', 'application_id', 'manifest', 'spine', 'toc',
|
||||||
'cover', 'language', 'guide', 'book_producer',
|
'cover', 'language', 'guide', 'book_producer',
|
||||||
'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate', 'rights',
|
'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate', 'rights',
|
||||||
'publication_type'):
|
'publication_type', 'uuid',):
|
||||||
if hasattr(mi, attr):
|
if hasattr(mi, attr):
|
||||||
val = getattr(mi, attr)
|
val = getattr(mi, attr)
|
||||||
if val is not None:
|
if val is not None:
|
||||||
|
@ -432,6 +432,9 @@ class OPF(object):
|
|||||||
identifier_path = XPath('descendant::*[re:match(name(), "identifier", "i")]')
|
identifier_path = XPath('descendant::*[re:match(name(), "identifier", "i")]')
|
||||||
application_id_path = XPath('descendant::*[re:match(name(), "identifier", "i") and '+
|
application_id_path = XPath('descendant::*[re:match(name(), "identifier", "i") and '+
|
||||||
'(re:match(@opf:scheme, "calibre|libprs500", "i") or re:match(@scheme, "calibre|libprs500", "i"))]')
|
'(re:match(@opf:scheme, "calibre|libprs500", "i") or re:match(@scheme, "calibre|libprs500", "i"))]')
|
||||||
|
uuid_id_path = XPath('descendant::*[re:match(name(), "identifier", "i") and '+
|
||||||
|
'(re:match(@opf:scheme, "uuid", "i") or re:match(@scheme, "uuid", "i"))]')
|
||||||
|
|
||||||
manifest_path = XPath('descendant::*[re:match(name(), "manifest", "i")]/*[re:match(name(), "item", "i")]')
|
manifest_path = XPath('descendant::*[re:match(name(), "manifest", "i")]/*[re:match(name(), "item", "i")]')
|
||||||
manifest_ppath = XPath('descendant::*[re:match(name(), "manifest", "i")]')
|
manifest_ppath = XPath('descendant::*[re:match(name(), "manifest", "i")]')
|
||||||
spine_path = XPath('descendant::*[re:match(name(), "spine", "i")]/*[re:match(name(), "itemref", "i")]')
|
spine_path = XPath('descendant::*[re:match(name(), "spine", "i")]/*[re:match(name(), "itemref", "i")]')
|
||||||
@ -747,6 +750,25 @@ class OPF(object):
|
|||||||
|
|
||||||
return property(fget=fget, fset=fset)
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
|
@dynamic_property
|
||||||
|
def uuid(self):
|
||||||
|
|
||||||
|
def fget(self):
|
||||||
|
for match in self.uuid_id_path(self.metadata):
|
||||||
|
return self.get_text(match) or None
|
||||||
|
|
||||||
|
def fset(self, val):
|
||||||
|
matches = self.uuid_id_path(self.metadata)
|
||||||
|
if not matches:
|
||||||
|
attrib = {'{%s}scheme'%self.NAMESPACES['opf']: 'uuid'}
|
||||||
|
matches = [self.create_metadata_element('identifier',
|
||||||
|
attrib=attrib)]
|
||||||
|
self.set_text(matches[0], unicode(val))
|
||||||
|
|
||||||
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@dynamic_property
|
@dynamic_property
|
||||||
def book_producer(self):
|
def book_producer(self):
|
||||||
|
|
||||||
@ -977,6 +999,9 @@ def metadata_to_opf(mi, as_string=True):
|
|||||||
if not mi.application_id:
|
if not mi.application_id:
|
||||||
mi.application_id = str(uuid.uuid4())
|
mi.application_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
if not mi.uuid:
|
||||||
|
mi.uuid = str(uuid.uuid4())
|
||||||
|
|
||||||
if not mi.book_producer:
|
if not mi.book_producer:
|
||||||
mi.book_producer = __appname__ + ' (%s) '%__version__ + \
|
mi.book_producer = __appname__ + ' (%s) '%__version__ + \
|
||||||
'[http://calibre-ebook.com]'
|
'[http://calibre-ebook.com]'
|
||||||
@ -986,13 +1011,14 @@ def metadata_to_opf(mi, as_string=True):
|
|||||||
|
|
||||||
root = etree.fromstring(textwrap.dedent(
|
root = etree.fromstring(textwrap.dedent(
|
||||||
'''
|
'''
|
||||||
<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="%(a)s_id">
|
<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="uuid_id">
|
||||||
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">
|
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">
|
||||||
<dc:identifier opf:scheme="%(a)s" id="%(a)s_id">%(id)s</dc:identifier>
|
<dc:identifier opf:scheme="%(a)s" id="%(a)s_id">%(id)s</dc:identifier>
|
||||||
|
<dc:identifier opf:scheme="uuid" id="uuid_id">%(uuid)s</dc:identifier>
|
||||||
</metadata>
|
</metadata>
|
||||||
<guide/>
|
<guide/>
|
||||||
</package>
|
</package>
|
||||||
'''%dict(a=__appname__, id=mi.application_id)))
|
'''%dict(a=__appname__, id=mi.application_id, uuid=mi.uuid)))
|
||||||
metadata = root[0]
|
metadata = root[0]
|
||||||
guide = root[1]
|
guide = root[1]
|
||||||
metadata[0].tail = '\n'+(' '*8)
|
metadata[0].tail = '\n'+(' '*8)
|
||||||
|
@ -123,7 +123,7 @@ class EbookIterator(object):
|
|||||||
else:
|
else:
|
||||||
print 'Loaded embedded font:', repr(family)
|
print 'Loaded embedded font:', repr(family)
|
||||||
|
|
||||||
def __enter__(self, raw_only=False):
|
def __enter__(self, processed=False):
|
||||||
self.delete_on_exit = []
|
self.delete_on_exit = []
|
||||||
self._tdir = TemporaryDirectory('_ebook_iter')
|
self._tdir = TemporaryDirectory('_ebook_iter')
|
||||||
self.base = self._tdir.__enter__()
|
self.base = self._tdir.__enter__()
|
||||||
@ -140,7 +140,7 @@ class EbookIterator(object):
|
|||||||
plumber.opts, plumber.input_fmt, self.log,
|
plumber.opts, plumber.input_fmt, self.log,
|
||||||
{}, self.base)
|
{}, self.base)
|
||||||
|
|
||||||
if not raw_only and plumber.input_fmt.lower() in ('pdf', 'rb'):
|
if processed or plumber.input_fmt.lower() in ('pdf', 'rb'):
|
||||||
self.pathtoopf = create_oebbook(self.log, self.pathtoopf, plumber.opts,
|
self.pathtoopf = create_oebbook(self.log, self.pathtoopf, plumber.opts,
|
||||||
plumber.input_plugin)
|
plumber.input_plugin)
|
||||||
if hasattr(self.pathtoopf, 'manifest'):
|
if hasattr(self.pathtoopf, 'manifest'):
|
||||||
|
@ -139,10 +139,9 @@ class OEBReader(object):
|
|||||||
mi.book_producer = '%(a)s (%(v)s) [http://%(a)s.kovidgoyal.net]'%\
|
mi.book_producer = '%(a)s (%(v)s) [http://%(a)s.kovidgoyal.net]'%\
|
||||||
dict(a=__appname__, v=__version__)
|
dict(a=__appname__, v=__version__)
|
||||||
meta_info_to_oeb_metadata(mi, self.oeb.metadata, self.logger)
|
meta_info_to_oeb_metadata(mi, self.oeb.metadata, self.logger)
|
||||||
bookid = "urn:uuid:%s" % str(uuid.uuid4()) if mi.application_id is None \
|
self.oeb.metadata.add('identifier', str(uuid.uuid4()), id='uuid_id',
|
||||||
else mi.application_id
|
scheme='uuid')
|
||||||
self.oeb.metadata.add('identifier', bookid, id='calibre-uuid')
|
self.oeb.uid = self.oeb.metadata.identifier[-1]
|
||||||
self.oeb.uid = self.oeb.metadata.identifier[0]
|
|
||||||
|
|
||||||
def _manifest_prune_invalid(self):
|
def _manifest_prune_invalid(self):
|
||||||
'''
|
'''
|
||||||
|
@ -80,12 +80,19 @@ class MergeMetadata(object):
|
|||||||
def __call__(self, oeb, mi, opts):
|
def __call__(self, oeb, mi, opts):
|
||||||
self.oeb, self.log = oeb, oeb.log
|
self.oeb, self.log = oeb, oeb.log
|
||||||
m = self.oeb.metadata
|
m = self.oeb.metadata
|
||||||
meta_info_to_oeb_metadata(mi, m, oeb.log)
|
|
||||||
self.log('Merging user specified metadata...')
|
self.log('Merging user specified metadata...')
|
||||||
|
meta_info_to_oeb_metadata(mi, m, oeb.log)
|
||||||
cover_id = self.set_cover(mi, opts.prefer_metadata_cover)
|
cover_id = self.set_cover(mi, opts.prefer_metadata_cover)
|
||||||
m.clear('cover')
|
m.clear('cover')
|
||||||
if cover_id is not None:
|
if cover_id is not None:
|
||||||
m.add('cover', cover_id)
|
m.add('cover', cover_id)
|
||||||
|
if mi.uuid is not None:
|
||||||
|
m.filter('identifier', lambda x:x.id=='uuid_id')
|
||||||
|
self.oeb.metadata.add('identifier', mi.uuid, id='uuid_id',
|
||||||
|
scheme='uuid')
|
||||||
|
self.oeb.uid = self.oeb.metadata.identifier[-1]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def set_cover(self, mi, prefer_metadata_cover):
|
def set_cover(self, mi, prefer_metadata_cover):
|
||||||
|
@ -153,6 +153,10 @@ class PMLMLizer(object):
|
|||||||
for unused in anchors.difference(links):
|
for unused in anchors.difference(links):
|
||||||
text = text.replace('\\Q="%s"' % unused, '')
|
text = text.replace('\\Q="%s"' % unused, '')
|
||||||
|
|
||||||
|
# Replace bad characters.
|
||||||
|
text = text.replace(u'\xc2', '')
|
||||||
|
text = text.replace(u'\xa0', ' ')
|
||||||
|
|
||||||
# Turn all html entities into unicode. This should not be necessary as
|
# Turn all html entities into unicode. This should not be necessary as
|
||||||
# lxml should have already done this but we want to be sure it happens.
|
# lxml should have already done this but we want to be sure it happens.
|
||||||
for entity in set(re.findall('&.+?;', text)):
|
for entity in set(re.findall('&.+?;', text)):
|
||||||
|
@ -130,7 +130,7 @@ class CopyButton(QPushButton):
|
|||||||
return
|
return
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
return QPushButton.event(self, ev)
|
QPushButton.keyPressEvent(self, ev)
|
||||||
|
|
||||||
|
|
||||||
def keyReleaseEvent(self, ev):
|
def keyReleaseEvent(self, ev):
|
||||||
@ -139,7 +139,7 @@ class CopyButton(QPushButton):
|
|||||||
return
|
return
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
return QPushButton.event(self, ev)
|
QPushButton.keyReleaseEvent(self, ev)
|
||||||
|
|
||||||
def mouseReleaseEvent(self, ev):
|
def mouseReleaseEvent(self, ev):
|
||||||
ev.accept()
|
ev.accept()
|
||||||
|
@ -87,12 +87,12 @@ class RegexBuilder(QDialog, Ui_RegexBuilder):
|
|||||||
|
|
||||||
def open_book(self, pathtoebook):
|
def open_book(self, pathtoebook):
|
||||||
self.iterator = EbookIterator(pathtoebook)
|
self.iterator = EbookIterator(pathtoebook)
|
||||||
self.iterator.__enter__(raw_only=True)
|
self.iterator.__enter__(processed=True)
|
||||||
text = [u'']
|
text = [u'']
|
||||||
for path in self.iterator.spine:
|
for path in self.iterator.spine:
|
||||||
html = open(path, 'rb').read().decode(path.encoding, 'replace')
|
html = open(path, 'rb').read().decode('utf-8', 'replace')
|
||||||
text.append(html)
|
text.append(html)
|
||||||
self.preview.setPlainText('\n\n'.join(text))
|
self.preview.setPlainText('\n---\n'.join(text))
|
||||||
|
|
||||||
def button_clicked(self, button):
|
def button_clicked(self, button):
|
||||||
if button == self.button_box.button(QDialogButtonBox.Open):
|
if button == self.button_box.button(QDialogButtonBox.Open):
|
||||||
|
@ -11,7 +11,7 @@ from Queue import Empty, Queue
|
|||||||
|
|
||||||
from PyQt4.Qt import QAbstractTableModel, QVariant, QModelIndex, Qt, \
|
from PyQt4.Qt import QAbstractTableModel, QVariant, QModelIndex, Qt, \
|
||||||
QTimer, SIGNAL, QIcon, QDialog, QAbstractItemDelegate, QApplication, \
|
QTimer, SIGNAL, QIcon, QDialog, QAbstractItemDelegate, QApplication, \
|
||||||
QSize, QStyleOptionProgressBarV2, QString, QStyle
|
QSize, QStyleOptionProgressBarV2, QString, QStyle, QToolTip
|
||||||
|
|
||||||
from calibre.utils.ipc.server import Server
|
from calibre.utils.ipc.server import Server
|
||||||
from calibre.utils.ipc.job import ParallelJob
|
from calibre.utils.ipc.job import ParallelJob
|
||||||
@ -57,6 +57,28 @@ class JobManager(QAbstractTableModel):
|
|||||||
else:
|
else:
|
||||||
return QVariant(section+1)
|
return QVariant(section+1)
|
||||||
|
|
||||||
|
def show_tooltip(self, arg):
|
||||||
|
widget, pos = arg
|
||||||
|
QToolTip.showText(pos, self.get_tooltip())
|
||||||
|
|
||||||
|
def get_tooltip(self):
|
||||||
|
running_jobs = [j for j in self.jobs if j.run_state == j.RUNNING]
|
||||||
|
waiting_jobs = [j for j in self.jobs if j.run_state == j.WAITING]
|
||||||
|
lines = [_('There are %d running jobs:')%len(running_jobs)]
|
||||||
|
for job in running_jobs:
|
||||||
|
desc = job.description
|
||||||
|
if not desc:
|
||||||
|
desc = _('Unknown job')
|
||||||
|
p = 100. if job.is_finished else job.percent
|
||||||
|
lines.append('%s: %.0f%% done'%(desc, p))
|
||||||
|
lines.extend(['', _('There are %d waiting jobs:')%len(waiting_jobs)])
|
||||||
|
for job in waiting_jobs:
|
||||||
|
desc = job.description
|
||||||
|
if not desc:
|
||||||
|
desc = _('Unknown job')
|
||||||
|
lines.append(desc)
|
||||||
|
return '\n'.join(['calibre', '']+ lines)
|
||||||
|
|
||||||
def data(self, index, role):
|
def data(self, index, role):
|
||||||
try:
|
try:
|
||||||
if role not in (Qt.DisplayRole, Qt.DecorationRole):
|
if role not in (Qt.DisplayRole, Qt.DecorationRole):
|
||||||
|
@ -11,7 +11,7 @@ from PyQt4.Qt import Qt, SIGNAL, QObject, QCoreApplication, QUrl, QTimer, \
|
|||||||
QModelIndex, QPixmap, QColor, QPainter, QMenu, QIcon, \
|
QModelIndex, QPixmap, QColor, QPainter, QMenu, QIcon, \
|
||||||
QToolButton, QDialog, QDesktopServices, QFileDialog, \
|
QToolButton, QDialog, QDesktopServices, QFileDialog, \
|
||||||
QSystemTrayIcon, QApplication, QKeySequence, QAction, \
|
QSystemTrayIcon, QApplication, QKeySequence, QAction, \
|
||||||
QMessageBox, QStackedLayout
|
QMessageBox, QStackedLayout, QHelpEvent
|
||||||
from PyQt4.QtSvg import QSvgRenderer
|
from PyQt4.QtSvg import QSvgRenderer
|
||||||
|
|
||||||
from calibre import prints, patheq
|
from calibre import prints, patheq
|
||||||
@ -89,6 +89,18 @@ class Listener(Thread):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class SystemTrayIcon(QSystemTrayIcon):
|
||||||
|
|
||||||
|
def __init__(self, icon, parent):
|
||||||
|
QSystemTrayIcon.__init__(self, icon, parent)
|
||||||
|
|
||||||
|
def event(self, ev):
|
||||||
|
if ev.type() == ev.ToolTip:
|
||||||
|
evh = QHelpEvent(ev)
|
||||||
|
self.emit(SIGNAL('tooltip_requested(PyQt_PyObject)'),
|
||||||
|
(self, evh.globalPos()))
|
||||||
|
return True
|
||||||
|
return QSystemTrayIcon.event(self, ev)
|
||||||
|
|
||||||
class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
||||||
'The main GUI'
|
'The main GUI'
|
||||||
@ -144,8 +156,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.device_connected = False
|
self.device_connected = False
|
||||||
self.viewers = collections.deque()
|
self.viewers = collections.deque()
|
||||||
self.content_server = None
|
self.content_server = None
|
||||||
self.system_tray_icon = QSystemTrayIcon(QIcon(I('library.png')), self)
|
self.system_tray_icon = SystemTrayIcon(QIcon(I('library.png')), self)
|
||||||
self.system_tray_icon.setToolTip('calibre')
|
self.system_tray_icon.setToolTip('calibre')
|
||||||
|
self.connect(self.system_tray_icon,
|
||||||
|
SIGNAL('tooltip_requested(PyQt_PyObject)'),
|
||||||
|
self.job_manager.show_tooltip)
|
||||||
if not config['systray_icon']:
|
if not config['systray_icon']:
|
||||||
self.system_tray_icon.hide()
|
self.system_tray_icon.hide()
|
||||||
else:
|
else:
|
||||||
|
@ -18,7 +18,9 @@ from calibre.library.database2 import LibraryDatabase2
|
|||||||
from calibre.ebooks.metadata.opf2 import OPFCreator, OPF
|
from calibre.ebooks.metadata.opf2 import OPFCreator, OPF
|
||||||
from calibre.utils.genshi.template import MarkupTemplate
|
from calibre.utils.genshi.template import MarkupTemplate
|
||||||
|
|
||||||
FIELDS = set(['title', 'authors', 'author_sort', 'publisher', 'rating', 'timestamp', 'size', 'tags', 'comments', 'series', 'series_index', 'formats', 'isbn', 'cover'])
|
FIELDS = set(['title', 'authors', 'author_sort', 'publisher', 'rating',
|
||||||
|
'timestamp', 'size', 'tags', 'comments', 'series', 'series_index',
|
||||||
|
'formats', 'isbn', 'uuid', 'cover'])
|
||||||
|
|
||||||
XML_TEMPLATE = '''\
|
XML_TEMPLATE = '''\
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
@ -26,6 +28,7 @@ XML_TEMPLATE = '''\
|
|||||||
<py:for each="record in data">
|
<py:for each="record in data">
|
||||||
<record>
|
<record>
|
||||||
<id>${record['id']}</id>
|
<id>${record['id']}</id>
|
||||||
|
<uuid>${record['uuid']}</uuid>
|
||||||
<title>${record['title']}</title>
|
<title>${record['title']}</title>
|
||||||
<authors sort="${record['author_sort']}">
|
<authors sort="${record['author_sort']}">
|
||||||
<py:for each="author in record['authors']">
|
<py:for each="author in record['authors']">
|
||||||
@ -71,7 +74,7 @@ STANZA_TEMPLATE='''\
|
|||||||
<py:for each="record in data">
|
<py:for each="record in data">
|
||||||
<entry>
|
<entry>
|
||||||
<title>${record['title']}</title>
|
<title>${record['title']}</title>
|
||||||
<id>urn:calibre:${record['id']}</id>
|
<id>urn:calibre:${record['uuid']}</id>
|
||||||
<author><name>${record['author_sort']}</name></author>
|
<author><name>${record['author_sort']}</name></author>
|
||||||
<updated>${record['timestamp'].strftime('%Y-%m-%dT%H:%M:%SZ')}</updated>
|
<updated>${record['timestamp'].strftime('%Y-%m-%dT%H:%M:%SZ')}</updated>
|
||||||
<link type="application/epub+zip" href="${quote(record['fmt_epub'].replace(sep, '/')).replace('http%3A', 'http:')}" />
|
<link type="application/epub+zip" href="${quote(record['fmt_epub'].replace(sep, '/')).replace('http%3A', 'http:')}" />
|
||||||
@ -227,7 +230,7 @@ def command_list(args, dbpath):
|
|||||||
if not set(fields).issubset(FIELDS):
|
if not set(fields).issubset(FIELDS):
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
print
|
print
|
||||||
print >>sys.stderr, _('Invalid fields. Available fields:'), ','.join(FIELDS)
|
print >>sys.stderr, _('Invalid fields. Available fields:'), ','.join(sorted(FIELDS))
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
db = get_db(dbpath, opts)
|
db = get_db(dbpath, opts)
|
||||||
|
@ -59,7 +59,7 @@ copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
|
|||||||
FIELD_MAP = {'id':0, 'title':1, 'authors':2, 'publisher':3, 'rating':4, 'timestamp':5,
|
FIELD_MAP = {'id':0, 'title':1, 'authors':2, 'publisher':3, 'rating':4, 'timestamp':5,
|
||||||
'size':6, 'tags':7, 'comments':8, 'series':9, 'series_index':10,
|
'size':6, 'tags':7, 'comments':8, 'series':9, 'series_index':10,
|
||||||
'sort':11, 'author_sort':12, 'formats':13, 'isbn':14, 'path':15,
|
'sort':11, 'author_sort':12, 'formats':13, 'isbn':14, 'path':15,
|
||||||
'lccn':16, 'pubdate':17, 'flags':18, 'cover':19}
|
'lccn':16, 'pubdate':17, 'flags':18, 'uuid':19, 'cover':20}
|
||||||
INDEX_MAP = dict(zip(FIELD_MAP.values(), FIELD_MAP.keys()))
|
INDEX_MAP = dict(zip(FIELD_MAP.values(), FIELD_MAP.keys()))
|
||||||
|
|
||||||
|
|
||||||
@ -447,7 +447,7 @@ class LibraryDatabase2(LibraryDatabase):
|
|||||||
|
|
||||||
for prop in ('author_sort', 'authors', 'comment', 'comments', 'isbn',
|
for prop in ('author_sort', 'authors', 'comment', 'comments', 'isbn',
|
||||||
'publisher', 'rating', 'series', 'series_index', 'tags',
|
'publisher', 'rating', 'series', 'series_index', 'tags',
|
||||||
'title', 'timestamp'):
|
'title', 'timestamp', 'uuid'):
|
||||||
setattr(self, prop, functools.partial(get_property,
|
setattr(self, prop, functools.partial(get_property,
|
||||||
loc=FIELD_MAP['comments' if prop == 'comment' else prop]))
|
loc=FIELD_MAP['comments' if prop == 'comment' else prop]))
|
||||||
|
|
||||||
@ -622,6 +622,50 @@ class LibraryDatabase2(LibraryDatabase):
|
|||||||
END TRANSACTION;
|
END TRANSACTION;
|
||||||
''')
|
''')
|
||||||
|
|
||||||
|
def upgrade_version_7(self):
|
||||||
|
'Add uuid column'
|
||||||
|
self.conn.executescript('''
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
ALTER TABLE books ADD COLUMN uuid TEXT;
|
||||||
|
DROP TRIGGER IF EXISTS books_insert_trg;
|
||||||
|
DROP TRIGGER IF EXISTS books_update_trg;
|
||||||
|
UPDATE books SET uuid=uuid4();
|
||||||
|
|
||||||
|
CREATE TRIGGER books_insert_trg AFTER INSERT ON books
|
||||||
|
BEGIN
|
||||||
|
UPDATE books SET sort=title_sort(NEW.title),uuid=uuid4() WHERE id=NEW.id;
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER books_update_trg AFTER UPDATE ON books
|
||||||
|
BEGIN
|
||||||
|
UPDATE books SET sort=title_sort(NEW.title) WHERE id=NEW.id;
|
||||||
|
END;
|
||||||
|
|
||||||
|
DROP VIEW meta;
|
||||||
|
CREATE VIEW meta AS
|
||||||
|
SELECT id, title,
|
||||||
|
(SELECT sortconcat(bal.id, name) FROM books_authors_link AS bal JOIN authors ON(author = authors.id) WHERE book = books.id) authors,
|
||||||
|
(SELECT name FROM publishers WHERE publishers.id IN (SELECT publisher from books_publishers_link WHERE book=books.id)) publisher,
|
||||||
|
(SELECT rating FROM ratings WHERE ratings.id IN (SELECT rating from books_ratings_link WHERE book=books.id)) rating,
|
||||||
|
timestamp,
|
||||||
|
(SELECT MAX(uncompressed_size) FROM data WHERE book=books.id) size,
|
||||||
|
(SELECT concat(name) FROM tags WHERE tags.id IN (SELECT tag from books_tags_link WHERE book=books.id)) tags,
|
||||||
|
(SELECT text FROM comments WHERE book=books.id) comments,
|
||||||
|
(SELECT name FROM series WHERE series.id IN (SELECT series FROM books_series_link WHERE book=books.id)) series,
|
||||||
|
series_index,
|
||||||
|
sort,
|
||||||
|
author_sort,
|
||||||
|
(SELECT concat(format) FROM data WHERE data.book=books.id) formats,
|
||||||
|
isbn,
|
||||||
|
path,
|
||||||
|
lccn,
|
||||||
|
pubdate,
|
||||||
|
flags,
|
||||||
|
uuid
|
||||||
|
FROM books;
|
||||||
|
|
||||||
|
END TRANSACTION;
|
||||||
|
''')
|
||||||
|
|
||||||
|
|
||||||
def last_modified(self):
|
def last_modified(self):
|
||||||
@ -785,6 +829,7 @@ class LibraryDatabase2(LibraryDatabase):
|
|||||||
mi.publisher = self.publisher(idx, index_is_id=index_is_id)
|
mi.publisher = self.publisher(idx, index_is_id=index_is_id)
|
||||||
mi.timestamp = self.timestamp(idx, index_is_id=index_is_id)
|
mi.timestamp = self.timestamp(idx, index_is_id=index_is_id)
|
||||||
mi.pubdate = self.pubdate(idx, index_is_id=index_is_id)
|
mi.pubdate = self.pubdate(idx, index_is_id=index_is_id)
|
||||||
|
mi.uuid = self.uuid(idx, index_is_id=index_is_id)
|
||||||
tags = self.tags(idx, index_is_id=index_is_id)
|
tags = self.tags(idx, index_is_id=index_is_id)
|
||||||
if tags:
|
if tags:
|
||||||
mi.tags = [i.strip() for i in tags.split(',')]
|
mi.tags = [i.strip() for i in tags.split(',')]
|
||||||
@ -1530,7 +1575,9 @@ class LibraryDatabase2(LibraryDatabase):
|
|||||||
'''
|
'''
|
||||||
if prefix is None:
|
if prefix is None:
|
||||||
prefix = self.library_path
|
prefix = self.library_path
|
||||||
FIELDS = set(['title', 'authors', 'author_sort', 'publisher', 'rating', 'timestamp', 'size', 'tags', 'comments', 'series', 'series_index', 'isbn'])
|
FIELDS = set(['title', 'authors', 'author_sort', 'publisher', 'rating',
|
||||||
|
'timestamp', 'size', 'tags', 'comments', 'series', 'series_index',
|
||||||
|
'isbn', 'uuid'])
|
||||||
data = []
|
data = []
|
||||||
for record in self.data:
|
for record in self.data:
|
||||||
if record is None: continue
|
if record is None: continue
|
||||||
|
@ -24,7 +24,7 @@ except ImportError:
|
|||||||
from calibre.constants import __version__, __appname__
|
from calibre.constants import __version__, __appname__
|
||||||
from calibre.utils.genshi.template import MarkupTemplate
|
from calibre.utils.genshi.template import MarkupTemplate
|
||||||
from calibre import fit_image, guess_type, prepare_string_for_xml, \
|
from calibre import fit_image, guess_type, prepare_string_for_xml, \
|
||||||
strftime as _strftime
|
strftime as _strftime, prints
|
||||||
from calibre.library import server_config as config
|
from calibre.library import server_config as config
|
||||||
from calibre.library.database2 import LibraryDatabase2, FIELD_MAP
|
from calibre.library.database2 import LibraryDatabase2, FIELD_MAP
|
||||||
from calibre.utils.config import config_dir
|
from calibre.utils.config import config_dir
|
||||||
@ -77,6 +77,159 @@ class LibraryServer(object):
|
|||||||
</book>
|
</book>
|
||||||
''')
|
''')
|
||||||
|
|
||||||
|
MOBILE_UA = re.compile('(?i)(?:iPhone|Opera Mini|NetFront|webOS|Mobile|Android|imode|DoCoMo|Minimo|Blackberry|MIDP|Symbian)')
|
||||||
|
|
||||||
|
MOBILE_BOOK = textwrap.dedent('''\
|
||||||
|
<tr xmlns:py="http://genshi.edgewall.org/">
|
||||||
|
<td class="thumbnail">
|
||||||
|
<img type="image/jpeg" src="/get/thumb/${r[0]}" border="0"/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<py:for each="format in r[13].split(',')">
|
||||||
|
<span class="button"><a href="/get/${format}/${authors}-${r[1]}_${r[0]}.${format}">${format.lower()}</a></span>
|
||||||
|
</py:for>
|
||||||
|
${r[1]} by ${authors} - ${r[6]/1024}k - ${r[3] if r[3] else ''} ${pubdate} ${'['+r[7]+']' if r[7] else ''}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
''')
|
||||||
|
|
||||||
|
MOBILE = MarkupTemplate(textwrap.dedent('''\
|
||||||
|
<html xmlns:py="http://genshi.edgewall.org/">
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
.navigation table.buttons {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.navigation .button {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
.button a, .button:visited a {
|
||||||
|
padding: 0.5em;
|
||||||
|
font-size: 1.25em;
|
||||||
|
border: 1px solid black;
|
||||||
|
text-color: black;
|
||||||
|
background-color: #ddd;
|
||||||
|
border-top: 1px solid ThreeDLightShadow;
|
||||||
|
border-right: 1px solid ButtonShadow;
|
||||||
|
border-bottom: 1px solid ButtonShadow;
|
||||||
|
border-left: 1 px solid ThreeDLightShadow;
|
||||||
|
-moz-border-radius: 0.25em;
|
||||||
|
-webkit-border-radius: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover a {
|
||||||
|
border-top: 1px solid #666;
|
||||||
|
border-right: 1px solid #CCC;
|
||||||
|
border-bottom: 1 px solid #CCC;
|
||||||
|
border-left: 1 px solid #666;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
div.navigation {
|
||||||
|
padding-bottom: 1em;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search_box {
|
||||||
|
border: 1px solid #393;
|
||||||
|
-moz-border-radius: 0.5em;
|
||||||
|
-webkit-border-radius: 0.5em;
|
||||||
|
padding: 1em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#listing {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
#listing td {
|
||||||
|
padding: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#listing td.thumbnail {
|
||||||
|
height: 60px;
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#listing tr:nth-child(even) {
|
||||||
|
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
#listing .button a{
|
||||||
|
display: inline-block;
|
||||||
|
width: 2.5em;
|
||||||
|
padding-left: 0em;
|
||||||
|
padding-right: 0em;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#logo {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
#spacer {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<link rel="icon" href="http://calibre.kovidgoyal.net/chrome/site/favicon.ico" type="image/x-icon" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="logo">
|
||||||
|
<img src="/static/calibre.png" alt="Calibre" />
|
||||||
|
</div>
|
||||||
|
<div id="search_box">
|
||||||
|
<form method="get" action="/mobile">
|
||||||
|
Show <select name="num">
|
||||||
|
<py:for each="option in [5,10,25,100]">
|
||||||
|
<option py:if="option == num" value="${option}" SELECTED="SELECTED">${option}</option>
|
||||||
|
<option py:if="option != num" value="${option}">${option}</option>
|
||||||
|
</py:for>
|
||||||
|
</select>
|
||||||
|
books matching <input name="search" id="s" value="${search}" /> sorted by
|
||||||
|
|
||||||
|
<select name="sort">
|
||||||
|
<py:for each="option in ['date','author','title','rating','size','tags','series']">
|
||||||
|
<option py:if="option == sort" value="${option}" SELECTED="SELECTED">${option}</option>
|
||||||
|
<option py:if="option != sort" value="${option}">${option}</option>
|
||||||
|
</py:for>
|
||||||
|
</select>
|
||||||
|
<select name="order">
|
||||||
|
<py:for each="option in ['ascending','descending']">
|
||||||
|
<option py:if="option == order" value="${option}" SELECTED="SELECTED">${option}</option>
|
||||||
|
<option py:if="option != order" value="${option}">${option}</option>
|
||||||
|
</py:for>
|
||||||
|
</select>
|
||||||
|
<input id="go" type="submit" value="Search"/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="navigation">
|
||||||
|
<span style="display: block; text-align: center;">Books ${start} to ${ min((start+num-1) , total) } of ${total}</span>
|
||||||
|
<table class="buttons">
|
||||||
|
<tr>
|
||||||
|
<td class="button" style="text-align:left;">
|
||||||
|
<a py:if="start > 1" href="${url_base};start=1">First</a>
|
||||||
|
<a py:if="start > 1" href="${url_base};start=${max(start-(num+1),1)}">Previous</a>
|
||||||
|
</td>
|
||||||
|
<td class="button" style="text-align: right;">
|
||||||
|
<a py:if=" total > (start + num) " href="${url_base};start=${start+num}">Next</a>
|
||||||
|
<a py:if=" total > (start + num) " href="${url_base};start=${total-num+1}">Last</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<hr class="spacer" />
|
||||||
|
<table id="listing">
|
||||||
|
<py:for each="book in books">
|
||||||
|
${Markup(book)}
|
||||||
|
</py:for>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
'''))
|
||||||
|
|
||||||
LIBRARY = MarkupTemplate(textwrap.dedent('''\
|
LIBRARY = MarkupTemplate(textwrap.dedent('''\
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<library xmlns:py="http://genshi.edgewall.org/" start="$start" num="${len(books)}" total="$total" updated="${updated.strftime('%Y-%m-%dT%H:%M:%S+00:00')}">
|
<library xmlns:py="http://genshi.edgewall.org/" start="$start" num="${len(books)}" total="$total" updated="${updated.strftime('%Y-%m-%dT%H:%M:%S+00:00')}">
|
||||||
@ -89,7 +242,7 @@ class LibraryServer(object):
|
|||||||
STANZA_ENTRY=MarkupTemplate(textwrap.dedent('''\
|
STANZA_ENTRY=MarkupTemplate(textwrap.dedent('''\
|
||||||
<entry xmlns:py="http://genshi.edgewall.org/">
|
<entry xmlns:py="http://genshi.edgewall.org/">
|
||||||
<title>${record[FM['title']]}</title>
|
<title>${record[FM['title']]}</title>
|
||||||
<id>urn:calibre:${record[FM['id']]}</id>
|
<id>urn:calibre:${urn}</id>
|
||||||
<author><name>${authors}</name></author>
|
<author><name>${authors}</name></author>
|
||||||
<updated>${timestamp}</updated>
|
<updated>${timestamp}</updated>
|
||||||
<link type="${mimetype}" href="/get/${fmt}/${record[FM['id']]}" />
|
<link type="${mimetype}" href="/get/${fmt}/${record[FM['id']]}" />
|
||||||
@ -525,6 +678,7 @@ class LibraryServer(object):
|
|||||||
extra='\n'.join(extra),
|
extra='\n'.join(extra),
|
||||||
mimetype=mimetype,
|
mimetype=mimetype,
|
||||||
fmt=fmt,
|
fmt=fmt,
|
||||||
|
urn=record[FIELD_MAP['uuid']],
|
||||||
timestamp=strftime('%Y-%m-%dT%H:%M:%S+00:00', record[5])
|
timestamp=strftime('%Y-%m-%dT%H:%M:%S+00:00', record[5])
|
||||||
)
|
)
|
||||||
books.append(self.STANZA_ENTRY.generate(**data)\
|
books.append(self.STANZA_ENTRY.generate(**data)\
|
||||||
@ -534,6 +688,52 @@ class LibraryServer(object):
|
|||||||
next_link=next_link, updated=updated, id='urn:calibre:main').render('xml')
|
next_link=next_link, updated=updated, id='urn:calibre:main').render('xml')
|
||||||
|
|
||||||
|
|
||||||
|
@expose
|
||||||
|
def mobile(self, start='1', num='25', sort='date', search='',
|
||||||
|
_=None, order='descending'):
|
||||||
|
'''
|
||||||
|
Serves metadata from the calibre database as XML.
|
||||||
|
|
||||||
|
:param sort: Sort results by ``sort``. Can be one of `title,author,rating`.
|
||||||
|
:param search: Filter results by ``search`` query. See :class:`SearchQueryParser` for query syntax
|
||||||
|
:param start,num: Return the slice `[start:start+num]` of the sorted and filtered results
|
||||||
|
:param _: Firefox seems to sometimes send this when using XMLHttpRequest with no caching
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
start = int(start)
|
||||||
|
except ValueError:
|
||||||
|
raise cherrypy.HTTPError(400, 'start: %s is not an integer'%start)
|
||||||
|
try:
|
||||||
|
num = int(num)
|
||||||
|
except ValueError:
|
||||||
|
raise cherrypy.HTTPError(400, 'num: %s is not an integer'%num)
|
||||||
|
ids = self.db.data.parse(search) if search and search.strip() else self.db.data.universal_set()
|
||||||
|
ids = sorted(ids)
|
||||||
|
items = [r for r in iter(self.db) if r[0] in ids]
|
||||||
|
if sort is not None:
|
||||||
|
self.sort(items, sort, (order.lower().strip() == 'ascending'))
|
||||||
|
|
||||||
|
book, books = MarkupTemplate(self.MOBILE_BOOK), []
|
||||||
|
for record in items[(start-1):(start-1)+num]:
|
||||||
|
aus = record[2] if record[2] else __builtin__._('Unknown')
|
||||||
|
authors = '|'.join([i.replace('|', ',') for i in aus.split(',')])
|
||||||
|
record[10] = fmt_sidx(float(record[10]))
|
||||||
|
ts, pd = strftime('%Y/%m/%d %H:%M:%S', record[5]), \
|
||||||
|
strftime('%Y/%m/%d %H:%M:%S', record[FIELD_MAP['pubdate']])
|
||||||
|
books.append(book.generate(r=record, authors=authors, timestamp=ts,
|
||||||
|
pubdate=pd).render('xml').decode('utf-8'))
|
||||||
|
updated = self.db.last_modified()
|
||||||
|
|
||||||
|
cherrypy.response.headers['Content-Type'] = 'text/html; charset=utf-8'
|
||||||
|
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
|
||||||
|
|
||||||
|
|
||||||
|
url_base = "/mobile?search=" + search+";order="+order+";sort="+sort+";num="+str(num)
|
||||||
|
|
||||||
|
return self.MOBILE.generate(books=books, start=start, updated=updated, search=search, sort=sort, order=order, num=num,
|
||||||
|
total=len(ids), url_base=url_base).render('html')
|
||||||
|
|
||||||
|
|
||||||
@expose
|
@expose
|
||||||
def library(self, start='0', num='50', sort=None, search=None,
|
def library(self, start='0', num='50', sort=None, search=None,
|
||||||
_=None, order='ascending'):
|
_=None, order='ascending'):
|
||||||
@ -584,10 +784,22 @@ class LibraryServer(object):
|
|||||||
cherrypy.request.headers.get('Stanza-Device-Name', 919) != 919 or \
|
cherrypy.request.headers.get('Stanza-Device-Name', 919) != 919 or \
|
||||||
cherrypy.request.headers.get('Want-OPDS-Catalog', 919) != 919 or \
|
cherrypy.request.headers.get('Want-OPDS-Catalog', 919) != 919 or \
|
||||||
ua.startswith('Stanza')
|
ua.startswith('Stanza')
|
||||||
|
|
||||||
|
# A better search would be great
|
||||||
|
want_mobile = self.MOBILE_UA.search(ua) is not None
|
||||||
|
if self.opts.develop and not want_mobile:
|
||||||
|
prints('User agent:', ua)
|
||||||
|
|
||||||
|
if want_opds:
|
||||||
return self.stanza(search=kwargs.get('search', None), sortby=kwargs.get('sortby',None), authorid=kwargs.get('authorid',None),
|
return self.stanza(search=kwargs.get('search', None), sortby=kwargs.get('sortby',None), authorid=kwargs.get('authorid',None),
|
||||||
tagid=kwargs.get('tagid',None),
|
tagid=kwargs.get('tagid',None),
|
||||||
seriesid=kwargs.get('seriesid',None),
|
seriesid=kwargs.get('seriesid',None),
|
||||||
offset=kwargs.get('offset', 0)) if want_opds else self.static('index.html')
|
offset=kwargs.get('offset', 0))
|
||||||
|
|
||||||
|
if want_mobile:
|
||||||
|
return self.mobile()
|
||||||
|
|
||||||
|
return self.static('index.html')
|
||||||
|
|
||||||
|
|
||||||
@expose
|
@expose
|
||||||
|
@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
Wrapper for multi-threaded access to a single sqlite database connection. Serializes
|
Wrapper for multi-threaded access to a single sqlite database connection. Serializes
|
||||||
all calls.
|
all calls.
|
||||||
'''
|
'''
|
||||||
import sqlite3 as sqlite, traceback, time
|
import sqlite3 as sqlite, traceback, time, uuid
|
||||||
from sqlite3 import IntegrityError
|
from sqlite3 import IntegrityError
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from Queue import Queue
|
from Queue import Queue
|
||||||
@ -121,6 +121,7 @@ class DBThread(Thread):
|
|||||||
self.conn.create_aggregate('concat', 1, Concatenate)
|
self.conn.create_aggregate('concat', 1, Concatenate)
|
||||||
self.conn.create_aggregate('sortconcat', 2, SortedConcatenate)
|
self.conn.create_aggregate('sortconcat', 2, SortedConcatenate)
|
||||||
self.conn.create_function('title_sort', 1, title_sort)
|
self.conn.create_function('title_sort', 1, title_sort)
|
||||||
|
self.conn.create_function('uuid4', 0, lambda : str(uuid.uuid4()))
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
try:
|
try:
|
||||||
|
@ -301,11 +301,17 @@ Removing headers and footers
|
|||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
These options are useful primarily for conversion of PDF documents. Often, the conversion leaves
|
These options are useful primarily for conversion of PDF documents. Often, the conversion leaves
|
||||||
behing page headers and footers in the text. These options use regular expressions to try and detect
|
behind page headers and footers in the text. These options use regular expressions to try and detect
|
||||||
the headers and footers and remove them. Remember that they operate on the intermediate XHTML produced
|
the headers and footers and remove them. Remember that they operate on the intermediate XHTML produced
|
||||||
by the conversion pipeline. There is also a wizard to help you customize the regular expressions for
|
by the conversion pipeline. There is also a wizard to help you customize the regular expressions for
|
||||||
your document.
|
your document.
|
||||||
|
|
||||||
|
The header and footer regular expressions are used in conjunction with the remove header and footer options.
|
||||||
|
If the remove option is not enabled the regular expression will not be applied to remove the matched text.
|
||||||
|
The removal works by using a python regular expression. All matched text is simply removed from
|
||||||
|
the document. You can learn more about regular expressions and their syntax at
|
||||||
|
http://docs.python.org/library/re.html.
|
||||||
|
|
||||||
Miscellaneous
|
Miscellaneous
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
@ -403,23 +409,76 @@ This will result in an automatically generated two level Table of Contents that
|
|||||||
Format specific tips
|
Format specific tips
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
Here you will find tips specific to the conversion of particular formats.
|
Here you will find tips specific to the conversion of particular formats. Options specific to particular
|
||||||
|
format, whether input or output are available in the conversion dialog under their own section, for example
|
||||||
|
`TXT Input` or `EPUB Output`.
|
||||||
|
|
||||||
Convert Microsoft Word documents
|
Convert Microsoft Word documents
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|app| does not directly convert .doc files from Microsoft Word. However, in Word, you can save the document
|
|app| does not directly convert .doc files from Microsoft Word. However, in Word, you can save the document
|
||||||
as HTML and then convert the resulting HTML file with |app|. When saving as HTML, be sure to use the
|
as HTML and then convert the resulting HTML file with |app|. When saving as HTML, be sure to use the
|
||||||
"Save as filtered HTML" option as this will produce clean HTML that will convert well.
|
"Save as Web Page, Filtered" option as this will produce clean HTML that will convert well.
|
||||||
|
|
||||||
There is a Word macro package that can automate the conversion of Word documents using |app|. It also makes
|
There is a Word macro package that can automate the conversion of Word documents using |app|. It also makes
|
||||||
generating the Table of Contents much simpler. It is called BookCreator and is available for free
|
generating the Table of Contents much simpler. It is called BookCreator and is available for free
|
||||||
`here <http://www.mobileread.com/forums/showthread.php?t=36098>`_.
|
`here <http://www.mobileread.com/forums/showthread.php?t=28313>`_.
|
||||||
|
|
||||||
Convert TXT documents
|
Convert TXT documents
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
TXT documents have no well defined way to specify formatting like bold, italics, etc, or document structure like paragraphs, headings, sections and so on.
|
||||||
|
Since TXT documents provide no way to explicitly mark parts of
|
||||||
|
the text, by default |app| only groups lines in the input document into paragraphs. The default is to assume one or
|
||||||
|
more blank lines are a paragraph boundary::
|
||||||
|
|
||||||
|
This is the first.
|
||||||
|
|
||||||
|
This is the
|
||||||
|
second paragraph.
|
||||||
|
|
||||||
|
TXT input supports a number of options to differentiate how paragraphs are detected.
|
||||||
|
|
||||||
|
:guilabel:`Treat each line as a paragraph`
|
||||||
|
Assumes that every line is a paragraph::
|
||||||
|
|
||||||
|
This is the first.
|
||||||
|
This is the second.
|
||||||
|
This is the third.
|
||||||
|
|
||||||
|
:guilabel:`Assume print formatting`
|
||||||
|
Assumes that every paragraph starts with an indent (either a tab or 2+ spaces). Paragraphs end when
|
||||||
|
the next line that starts with an indent is reached::
|
||||||
|
|
||||||
|
This is the
|
||||||
|
first.
|
||||||
|
This is the second.
|
||||||
|
|
||||||
|
This is the
|
||||||
|
third.
|
||||||
|
|
||||||
|
:guilabel:`Process using markdown`
|
||||||
|
|app| also supports running TXT input though a transformation preprocessor known as markdown. Markdown
|
||||||
|
allows for basic formatting to be added to TXT documents, such as bold, italics, section headings, tables,
|
||||||
|
loists, a Table of Contents, etc. Marking chapter headings with a leading # and setting the chapter XPath detection
|
||||||
|
expression to "//h:h1" is the easiest way to have a proper table of contents generated from a TXT document.
|
||||||
|
You can learn more about the markdown syntax at http://daringfireball.net/projects/markdown/syntax.
|
||||||
|
|
||||||
|
|
||||||
Convert PDF documents
|
Convert PDF documents
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
PDF documents are one of the worst formats to convert from. They are a fixed page size and text placement format.
|
||||||
|
Meaning, it is very difficult to determine where one paragraph ends and another begins. |app| will try to unwrap
|
||||||
|
paragraphs using a configurable, :guilabel:`Line Un-Wrapping Factor`. This is a scale used to determine the length
|
||||||
|
at which a line should be unwrapped. Valid values are a decimal
|
||||||
|
between 0 and 1. The default is 0.5, this is the median line length. Lower this value to include more
|
||||||
|
text in the unwrapping. Increase to include less.
|
||||||
|
|
||||||
|
Also, they often have headers and footers as part of the document that will become included with the text.
|
||||||
|
Use the options to remove headers and footers to mitigate this issue. If the headers and footers are not
|
||||||
|
removed from the text it can throw off the paragraph unwrapping.
|
||||||
|
|
||||||
|
Some limitations of PDF input is complex, multi-column, and image based documents are not supported.
|
||||||
|
Extraction of vector images and tables from within the document is also not supported.
|
||||||
|
|
||||||
|
@ -65,6 +65,7 @@ def split(src):
|
|||||||
|
|
||||||
|
|
||||||
def files_and_dirs(prefix, allowed_exts=[]):
|
def files_and_dirs(prefix, allowed_exts=[]):
|
||||||
|
prefix = os.path.expanduser(prefix)
|
||||||
for i in glob.iglob(prefix+'*'):
|
for i in glob.iglob(prefix+'*'):
|
||||||
_, ext = os.path.splitext(i)
|
_, ext = os.path.splitext(i)
|
||||||
ext = ext.lower().replace('.', '')
|
ext = ext.lower().replace('.', '')
|
||||||
|
Loading…
x
Reference in New Issue
Block a user