Fix #2273 (Little bug in Calibra Server)

This commit is contained in:
Kovid Goyal 2009-04-14 23:38:22 -07:00
parent d8ed8c0c07
commit ead9de4002
2 changed files with 79 additions and 66 deletions

View File

@ -157,35 +157,35 @@ class BookHeader(object):
class MetadataHeader(BookHeader): class MetadataHeader(BookHeader):
def __init__(self, stream): def __init__(self, stream):
self.stream = stream self.stream = stream
self.ident = self.identity() self.ident = self.identity()
self.num_sections = self.section_count() self.num_sections = self.section_count()
if self.num_sections >= 2: if self.num_sections >= 2:
header = self.header() header = self.header()
BookHeader.__init__(self, header, self.ident, None) BookHeader.__init__(self, header, self.ident)
else: else:
self.exth = None self.exth = None
def identity(self): def identity(self):
self.stream.seek(60) self.stream.seek(60)
ident = self.stream.read(8).upper() ident = self.stream.read(8).upper()
if ident not in ['BOOKMOBI', 'TEXTREAD']: if ident not in ['BOOKMOBI', 'TEXTREAD']:
raise MobiError('Unknown book type: %s' % ident) raise MobiError('Unknown book type: %s' % ident)
return ident return ident
def section_count(self): def section_count(self):
self.stream.seek(76) self.stream.seek(76)
return struct.unpack('>H', self.stream.read(2))[0] return struct.unpack('>H', self.stream.read(2))[0]
def section_offset(self, number): def section_offset(self, number):
self.stream.seek(78+number*8) self.stream.seek(78+number*8)
return struct.unpack('>LBBBB', self.stream.read(8))[0] return struct.unpack('>LBBBB', self.stream.read(8))[0]
def header(self): def header(self):
section_headers = [] section_headers = []
# First section with the metadata # First section with the metadata
section_headers.append(self.section_offset(0)) section_headers.append(self.section_offset(0))
# Second section used to get the lengh of the first # Second section used to get the lengh of the first
@ -193,20 +193,20 @@ class MetadataHeader(BookHeader):
end_off = section_headers[1] end_off = section_headers[1]
off = section_headers[0] off = section_headers[0]
self.stream.seek(off) self.stream.seek(off)
return self.stream.read(end_off - off) return self.stream.read(end_off - off)
def section_data(self, number): def section_data(self, number):
start = self.section_offset(number) start = self.section_offset(number)
if number == self.num_sections -1: if number == self.num_sections -1:
end = os.stat(self.stream.name).st_size end = os.stat(self.stream.name).st_size
else: else:
end = self.section_offset(number + 1) end = self.section_offset(number + 1)
self.stream.seek(start) self.stream.seek(start)
return self.stream.read(end - start) return self.stream.read(end - start)
@ -618,7 +618,7 @@ class MobiReader(object):
self.image_names.append(os.path.basename(path)) self.image_names.append(os.path.basename(path))
im.convert('RGB').save(open(path, 'wb'), format='JPEG') im.convert('RGB').save(open(path, 'wb'), format='JPEG')
def get_metadata(stream): def get_metadata(stream):
mi = MetaInformation(os.path.basename(stream.name), [_('Unknown')]) mi = MetaInformation(os.path.basename(stream.name), [_('Unknown')])
try: try:
mh = MetadataHeader(stream) mh = MetadataHeader(stream)
@ -632,7 +632,7 @@ def get_metadata(stream):
mr.extract_content(tdir) mr.extract_content(tdir)
if mr.embedded_mi is not None: if mr.embedded_mi is not None:
mi = mr.embedded_mi mi = mr.embedded_mi
if hasattr(mh.exth, 'cover_offset'): if hasattr(mh.exth, 'cover_offset'):
cover_index = mh.first_image_index + mh.exth.cover_offset cover_index = mh.first_image_index + mh.exth.cover_offset
data = mh.section_data(int(cover_index)) data = mh.section_data(int(cover_index))
@ -646,7 +646,7 @@ def get_metadata(stream):
except: except:
import traceback import traceback
traceback.print_exc() traceback.print_exc()
return mi return mi

View File

@ -30,31 +30,31 @@ build_time = datetime.strptime(build_time, '%d %m %Y %H%M%S')
server_resources['jquery.js'] = jquery server_resources['jquery.js'] = jquery
def expose(func): def expose(func):
def do(self, *args, **kwargs): def do(self, *args, **kwargs):
dict.update(cherrypy.response.headers, {'Server':self.server_name}) dict.update(cherrypy.response.headers, {'Server':self.server_name})
return func(self, *args, **kwargs) return func(self, *args, **kwargs)
return cherrypy.expose(do) return cherrypy.expose(do)
log_access_file = os.path.join(config_dir, 'server_access_log.txt') log_access_file = os.path.join(config_dir, 'server_access_log.txt')
log_error_file = os.path.join(config_dir, 'server_error_log.txt') log_error_file = os.path.join(config_dir, 'server_error_log.txt')
class LibraryServer(object): class LibraryServer(object):
server_name = __appname__ + '/' + __version__ server_name = __appname__ + '/' + __version__
BOOK = textwrap.dedent('''\ BOOK = textwrap.dedent('''\
<book xmlns:py="http://genshi.edgewall.org/" <book xmlns:py="http://genshi.edgewall.org/"
id="${r[0]}" id="${r[0]}"
title="${r[1]}" title="${r[1]}"
sort="${r[11]}" sort="${r[11]}"
author_sort="${r[12]}" author_sort="${r[12]}"
authors="${authors}" authors="${authors}"
rating="${r[4]}" rating="${r[4]}"
timestamp="${r[5].strftime('%Y/%m/%d %H:%M:%S')}" timestamp="${r[5].strftime('%Y/%m/%d %H:%M:%S')}"
size="${r[6]}" size="${r[6]}"
isbn="${r[14] if r[14] else ''}" isbn="${r[14] if r[14] else ''}"
formats="${r[13] if r[13] else ''}" formats="${r[13] if r[13] else ''}"
series = "${r[9] if r[9] else ''}" series = "${r[9] if r[9] else ''}"
@ -63,7 +63,7 @@ class LibraryServer(object):
publisher="${r[3] if r[3] else ''}">${r[8] if r[8] else ''} publisher="${r[3] if r[3] else ''}">${r[8] if r[8] else ''}
</book> </book>
''') ''')
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')}">
@ -72,7 +72,7 @@ class LibraryServer(object):
</py:for> </py:for>
</library> </library>
''')) '''))
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>
@ -87,7 +87,7 @@ class LibraryServer(object):
</content> </content>
</entry> </entry>
''')) '''))
STANZA = MarkupTemplate(textwrap.dedent('''\ STANZA = MarkupTemplate(textwrap.dedent('''\
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:py="http://genshi.edgewall.org/"> <feed xmlns="http://www.w3.org/2005/Atom" xmlns:py="http://genshi.edgewall.org/">
@ -107,7 +107,7 @@ class LibraryServer(object):
</feed> </feed>
''')) '''))
def __init__(self, db, opts, embedded=False, show_tracebacks=True): def __init__(self, db, opts, embedded=False, show_tracebacks=True):
self.db = db self.db = db
for item in self.db: for item in self.db:
@ -116,7 +116,7 @@ class LibraryServer(object):
self.opts = opts self.opts = opts
self.max_cover_width, self.max_cover_height = \ self.max_cover_width, self.max_cover_height = \
map(int, self.opts.max_cover.split('x')) map(int, self.opts.max_cover.split('x'))
cherrypy.config.update({ cherrypy.config.update({
'log.screen' : opts.develop, 'log.screen' : opts.develop,
'engine.autoreload_on' : opts.develop, 'engine.autoreload_on' : opts.develop,
@ -141,10 +141,10 @@ class LibraryServer(object):
'tools.digest_auth.realm' : (_('Password to access your calibre library. Username is ') + opts.username.strip()).encode('ascii', 'replace'), 'tools.digest_auth.realm' : (_('Password to access your calibre library. Username is ') + opts.username.strip()).encode('ascii', 'replace'),
'tools.digest_auth.users' : {opts.username.strip():opts.password.strip()}, 'tools.digest_auth.users' : {opts.username.strip():opts.password.strip()},
} }
self.is_running = False self.is_running = False
self.exception = None self.exception = None
def setup_loggers(self): def setup_loggers(self):
access_file = log_access_file access_file = log_access_file
error_file = log_error_file error_file = log_error_file
@ -152,20 +152,20 @@ class LibraryServer(object):
maxBytes = getattr(log, "rot_maxBytes", 10000000) maxBytes = getattr(log, "rot_maxBytes", 10000000)
backupCount = getattr(log, "rot_backupCount", 1000) backupCount = getattr(log, "rot_backupCount", 1000)
# Make a new RotatingFileHandler for the error log. # Make a new RotatingFileHandler for the error log.
h = RotatingFileHandler(error_file, 'a', maxBytes, backupCount) h = RotatingFileHandler(error_file, 'a', maxBytes, backupCount)
h.setLevel(logging.DEBUG) h.setLevel(logging.DEBUG)
h.setFormatter(cherrypy._cplogging.logfmt) h.setFormatter(cherrypy._cplogging.logfmt)
log.error_log.addHandler(h) log.error_log.addHandler(h)
# Make a new RotatingFileHandler for the access log. # Make a new RotatingFileHandler for the access log.
h = RotatingFileHandler(access_file, 'a', maxBytes, backupCount) h = RotatingFileHandler(access_file, 'a', maxBytes, backupCount)
h.setLevel(logging.DEBUG) h.setLevel(logging.DEBUG)
h.setFormatter(cherrypy._cplogging.logfmt) h.setFormatter(cherrypy._cplogging.logfmt)
log.access_log.addHandler(h) log.access_log.addHandler(h)
def start(self): def start(self):
self.is_running = False self.is_running = False
self.setup_loggers() self.setup_loggers()
@ -173,7 +173,7 @@ class LibraryServer(object):
try: try:
cherrypy.engine.start() cherrypy.engine.start()
self.is_running = True self.is_running = True
publish_zeroconf('Books in calibre', '_stanza._tcp', publish_zeroconf('Books in calibre', '_stanza._tcp',
self.opts.port, {'path':'/stanza'}) self.opts.port, {'path':'/stanza'})
cherrypy.engine.block() cherrypy.engine.block()
except Exception, e: except Exception, e:
@ -181,10 +181,10 @@ class LibraryServer(object):
finally: finally:
self.is_running = False self.is_running = False
stop_zeroconf() stop_zeroconf()
def exit(self): def exit(self):
cherrypy.engine.exit() cherrypy.engine.exit()
def get_cover(self, id, thumbnail=False): def get_cover(self, id, thumbnail=False):
cover = self.db.cover(id, index_is_id=True, as_file=False) cover = self.db.cover(id, index_is_id=True, as_file=False)
if cover is None: if cover is None:
@ -196,14 +196,14 @@ class LibraryServer(object):
try: try:
if QApplication.instance() is None: if QApplication.instance() is None:
QApplication([]) QApplication([])
im = QImage() im = QImage()
im.loadFromData(cover) im.loadFromData(cover)
if im.isNull(): if im.isNull():
raise cherrypy.HTTPError(404, 'No valid cover found') raise cherrypy.HTTPError(404, 'No valid cover found')
width, height = im.width(), im.height() width, height = im.width(), im.height()
scaled, width, height = fit_image(width, height, scaled, width, height = fit_image(width, height,
60 if thumbnail else self.max_cover_width, 60 if thumbnail else self.max_cover_width,
80 if thumbnail else self.max_cover_height) 80 if thumbnail else self.max_cover_height)
if not scaled: if not scaled:
return cover return cover
@ -217,7 +217,7 @@ class LibraryServer(object):
import traceback import traceback
traceback.print_exc() traceback.print_exc()
raise cherrypy.HTTPError(404, 'Failed to generate cover: %s'%err) raise cherrypy.HTTPError(404, 'Failed to generate cover: %s'%err)
def get_format(self, id, format): def get_format(self, id, format):
format = format.upper() format = format.upper()
fmt = self.db.format(id, format, index_is_id=True, as_file=True, mode='rb') fmt = self.db.format(id, format, index_is_id=True, as_file=True, mode='rb')
@ -232,7 +232,7 @@ class LibraryServer(object):
updated = datetime.utcfromtimestamp(os.stat(path).st_mtime) updated = datetime.utcfromtimestamp(os.stat(path).st_mtime)
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated) cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
return fmt.read() return fmt.read()
def sort(self, items, field, order): def sort(self, items, field, order):
field = field.lower().strip() field = field.lower().strip()
if field == 'author': if field == 'author':
@ -243,10 +243,23 @@ class LibraryServer(object):
raise cherrypy.HTTPError(400, '%s is not a valid sort field'%field) raise cherrypy.HTTPError(400, '%s is not a valid sort field'%field)
cmpf = cmp if field in ('rating', 'size', 'timestamp') else \ cmpf = cmp if field in ('rating', 'size', 'timestamp') else \
lambda x, y: cmp(x.lower() if x else '', y.lower() if y else '') lambda x, y: cmp(x.lower() if x else '', y.lower() if y else '')
field = FIELD_MAP[field] if field == 'series':
getter = operator.itemgetter(field) items.sort(cmp=self.seriescmp, reverse=not order)
items.sort(cmp=lambda x, y: cmpf(getter(x), getter(y)), reverse=not order) else:
field = FIELD_MAP[field]
getter = operator.itemgetter(field)
items.sort(cmp=lambda x, y: cmpf(getter(x), getter(y)), reverse=not order)
def seriescmp(self, x, y):
si = FIELD_MAP['series']
try:
ans = cmp(x[si].lower(), y[si].lower())
except AttributeError: # Some entries may be None
ans = cmp(x[si], y[si])
if ans != 0: return ans
return cmp(x[FIELD_MAP['series_index']], y[FIELD_MAP['series_index']])
def last_modified(self, updated): def last_modified(self, updated):
lm = updated.strftime('day, %d month %Y %H:%M:%S GMT') lm = updated.strftime('day, %d month %Y %H:%M:%S GMT')
day ={0:'Sun', 1:'Mon', 2:'Tue', 3:'Wed', 4:'Thu', 5:'Fri', 6:'Sat'} day ={0:'Sun', 1:'Mon', 2:'Tue', 3:'Wed', 4:'Thu', 5:'Fri', 6:'Sat'}
@ -254,8 +267,8 @@ class LibraryServer(object):
month = {1:'Jan', 2:'Feb', 3:'Mar', 4:'Apr', 5:'May', 6:'Jun', 7:'Jul', month = {1:'Jan', 2:'Feb', 3:'Mar', 4:'Apr', 5:'May', 6:'Jun', 7:'Jul',
8:'Aug', 9:'Sep', 10:'Oct', 11:'Nov', 12:'Dec'} 8:'Aug', 9:'Sep', 10:'Oct', 11:'Nov', 12:'Dec'}
return lm.replace('month', month[updated.month]) return lm.replace('month', month[updated.month])
@expose @expose
def stanza(self): def stanza(self):
' Feeds to read calibre books on a ipod with stanza.' ' Feeds to read calibre books on a ipod with stanza.'
@ -264,7 +277,7 @@ class LibraryServer(object):
r = record[FIELD_MAP['formats']] r = record[FIELD_MAP['formats']]
r = r.upper() if r else '' r = r.upper() if r else ''
if 'EPUB' in r or 'PDB' in r: if 'EPUB' in r or 'PDB' in r:
authors = ' & '.join([i.replace('|', ',') for i in authors = ' & '.join([i.replace('|', ',') for i in
record[FIELD_MAP['authors']].split(',')]) record[FIELD_MAP['authors']].split(',')])
extra = [] extra = []
rating = record[FIELD_MAP['rating']] rating = record[FIELD_MAP['rating']]
@ -276,7 +289,7 @@ class LibraryServer(object):
extra.append('TAGS: %s<br />'%', '.join(tags.split(','))) extra.append('TAGS: %s<br />'%', '.join(tags.split(',')))
series = record[FIELD_MAP['series']] series = record[FIELD_MAP['series']]
if series: if series:
extra.append('SERIES: %s [%d]<br />'%(series, extra.append('SERIES: %s [%d]<br />'%(series,
record[FIELD_MAP['series_index']])) record[FIELD_MAP['series_index']]))
fmt = 'epub' if 'EPUB' in r else 'pdb' fmt = 'epub' if 'EPUB' in r else 'pdb'
mimetype = guess_type('dummy.'+fmt)[0] mimetype = guess_type('dummy.'+fmt)[0]
@ -288,24 +301,24 @@ class LibraryServer(object):
mimetype=mimetype, mimetype=mimetype,
fmt=fmt, fmt=fmt,
).render('xml').decode('utf8')) ).render('xml').decode('utf8'))
updated = self.db.last_modified() updated = self.db.last_modified()
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated) cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
cherrypy.response.headers['Content-Type'] = 'text/xml' cherrypy.response.headers['Content-Type'] = 'text/xml'
return self.STANZA.generate(subtitle='', data=books, FM=FIELD_MAP, return self.STANZA.generate(subtitle='', data=books, FM=FIELD_MAP,
updated=updated, id='urn:calibre:main').render('xml') updated=updated, id='urn:calibre:main').render('xml')
@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'):
''' '''
Serves metadata from the calibre database as XML. Serves metadata from the calibre database as XML.
:param sort: Sort results by ``sort``. Can be one of `title,author,rating`. :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 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 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 :param _: Firefox seems to sometimes send this when using XMLHttpRequest with no caching
''' '''
try: try:
start = int(start) start = int(start)
@ -321,19 +334,19 @@ class LibraryServer(object):
items = [r for r in iter(self.db) if r[0] in ids] items = [r for r in iter(self.db) if r[0] in ids]
if sort is not None: if sort is not None:
self.sort(items, sort, order) self.sort(items, sort, order)
book, books = MarkupTemplate(self.BOOK), [] book, books = MarkupTemplate(self.BOOK), []
for record in items[start:start+num]: for record in items[start:start+num]:
aus = record[2] if record[2] else _('Unknown') aus = record[2] if record[2] else _('Unknown')
authors = '|'.join([i.replace('|', ',') for i in aus.split(',')]) authors = '|'.join([i.replace('|', ',') for i in aus.split(',')])
books.append(book.generate(r=record, authors=authors).render('xml').decode('utf-8')) books.append(book.generate(r=record, authors=authors).render('xml').decode('utf-8'))
updated = self.db.last_modified() updated = self.db.last_modified()
cherrypy.response.headers['Content-Type'] = 'text/xml' cherrypy.response.headers['Content-Type'] = 'text/xml'
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated) cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
return self.LIBRARY.generate(books=books, start=start, updated=updated, return self.LIBRARY.generate(books=books, start=start, updated=updated,
total=len(ids)).render('xml') total=len(ids)).render('xml')
@expose @expose
def index(self, **kwargs): def index(self, **kwargs):
'The / URL' 'The / URL'
@ -341,8 +354,8 @@ class LibraryServer(object):
if stanza == 919: if stanza == 919:
return self.static('index.html') return self.static('index.html')
return self.stanza() return self.stanza()
@expose @expose
def get(self, what, id): def get(self, what, id):
'Serves files, covers, thumbnails from the calibre database' 'Serves files, covers, thumbnails from the calibre database'
@ -361,7 +374,7 @@ class LibraryServer(object):
if what == 'cover': if what == 'cover':
return self.get_cover(id) return self.get_cover(id)
return self.get_format(id, what) return self.get_format(id, what)
@expose @expose
def static(self, name): def static(self, name):
'Serves static content' 'Serves static content'
@ -392,11 +405,11 @@ def start_threaded_server(db, opts):
server.thread.setDaemon(True) server.thread.setDaemon(True)
server.thread.start() server.thread.start()
return server return server
def stop_threaded_server(server): def stop_threaded_server(server):
server.exit() server.exit()
server.thread = None server.thread = None
def option_parser(): def option_parser():
return config().option_parser('%prog '+ _('[options]\n\nStart the calibre content server.')) return config().option_parser('%prog '+ _('[options]\n\nStart the calibre content server.'))