mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-23 15:30:45 -04:00
Fix #2273 (Little bug in Calibra Server)
This commit is contained in:
parent
d8ed8c0c07
commit
ead9de4002
@ -157,35 +157,35 @@ class BookHeader(object):
|
||||
class MetadataHeader(BookHeader):
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
|
||||
|
||||
self.ident = self.identity()
|
||||
self.num_sections = self.section_count()
|
||||
|
||||
|
||||
if self.num_sections >= 2:
|
||||
header = self.header()
|
||||
BookHeader.__init__(self, header, self.ident, None)
|
||||
BookHeader.__init__(self, header, self.ident)
|
||||
else:
|
||||
self.exth = None
|
||||
|
||||
|
||||
def identity(self):
|
||||
self.stream.seek(60)
|
||||
ident = self.stream.read(8).upper()
|
||||
|
||||
|
||||
if ident not in ['BOOKMOBI', 'TEXTREAD']:
|
||||
raise MobiError('Unknown book type: %s' % ident)
|
||||
return ident
|
||||
|
||||
|
||||
def section_count(self):
|
||||
self.stream.seek(76)
|
||||
return struct.unpack('>H', self.stream.read(2))[0]
|
||||
|
||||
|
||||
def section_offset(self, number):
|
||||
self.stream.seek(78+number*8)
|
||||
return struct.unpack('>LBBBB', self.stream.read(8))[0]
|
||||
|
||||
|
||||
def header(self):
|
||||
section_headers = []
|
||||
|
||||
|
||||
# First section with the metadata
|
||||
section_headers.append(self.section_offset(0))
|
||||
# Second section used to get the lengh of the first
|
||||
@ -193,20 +193,20 @@ class MetadataHeader(BookHeader):
|
||||
|
||||
end_off = section_headers[1]
|
||||
off = section_headers[0]
|
||||
|
||||
|
||||
self.stream.seek(off)
|
||||
return self.stream.read(end_off - off)
|
||||
|
||||
def section_data(self, number):
|
||||
start = self.section_offset(number)
|
||||
|
||||
|
||||
if number == self.num_sections -1:
|
||||
end = os.stat(self.stream.name).st_size
|
||||
else:
|
||||
end = self.section_offset(number + 1)
|
||||
|
||||
|
||||
self.stream.seek(start)
|
||||
|
||||
|
||||
return self.stream.read(end - start)
|
||||
|
||||
|
||||
@ -618,7 +618,7 @@ class MobiReader(object):
|
||||
self.image_names.append(os.path.basename(path))
|
||||
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')])
|
||||
try:
|
||||
mh = MetadataHeader(stream)
|
||||
@ -632,7 +632,7 @@ def get_metadata(stream):
|
||||
mr.extract_content(tdir)
|
||||
if mr.embedded_mi is not None:
|
||||
mi = mr.embedded_mi
|
||||
|
||||
|
||||
if hasattr(mh.exth, 'cover_offset'):
|
||||
cover_index = mh.first_image_index + mh.exth.cover_offset
|
||||
data = mh.section_data(int(cover_index))
|
||||
@ -646,7 +646,7 @@ def get_metadata(stream):
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
return mi
|
||||
|
||||
|
||||
|
@ -30,31 +30,31 @@ build_time = datetime.strptime(build_time, '%d %m %Y %H%M%S')
|
||||
server_resources['jquery.js'] = jquery
|
||||
|
||||
def expose(func):
|
||||
|
||||
|
||||
def do(self, *args, **kwargs):
|
||||
dict.update(cherrypy.response.headers, {'Server':self.server_name})
|
||||
return func(self, *args, **kwargs)
|
||||
|
||||
|
||||
return cherrypy.expose(do)
|
||||
|
||||
log_access_file = os.path.join(config_dir, 'server_access_log.txt')
|
||||
log_error_file = os.path.join(config_dir, 'server_error_log.txt')
|
||||
|
||||
|
||||
|
||||
class LibraryServer(object):
|
||||
|
||||
|
||||
server_name = __appname__ + '/' + __version__
|
||||
|
||||
BOOK = textwrap.dedent('''\
|
||||
<book xmlns:py="http://genshi.edgewall.org/"
|
||||
id="${r[0]}"
|
||||
<book xmlns:py="http://genshi.edgewall.org/"
|
||||
id="${r[0]}"
|
||||
title="${r[1]}"
|
||||
sort="${r[11]}"
|
||||
author_sort="${r[12]}"
|
||||
authors="${authors}"
|
||||
authors="${authors}"
|
||||
rating="${r[4]}"
|
||||
timestamp="${r[5].strftime('%Y/%m/%d %H:%M:%S')}"
|
||||
size="${r[6]}"
|
||||
timestamp="${r[5].strftime('%Y/%m/%d %H:%M:%S')}"
|
||||
size="${r[6]}"
|
||||
isbn="${r[14] if r[14] else ''}"
|
||||
formats="${r[13] if r[13] 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 ''}
|
||||
</book>
|
||||
''')
|
||||
|
||||
|
||||
LIBRARY = MarkupTemplate(textwrap.dedent('''\
|
||||
<?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')}">
|
||||
@ -72,7 +72,7 @@ class LibraryServer(object):
|
||||
</py:for>
|
||||
</library>
|
||||
'''))
|
||||
|
||||
|
||||
STANZA_ENTRY=MarkupTemplate(textwrap.dedent('''\
|
||||
<entry xmlns:py="http://genshi.edgewall.org/">
|
||||
<title>${record[FM['title']]}</title>
|
||||
@ -87,7 +87,7 @@ class LibraryServer(object):
|
||||
</content>
|
||||
</entry>
|
||||
'''))
|
||||
|
||||
|
||||
STANZA = MarkupTemplate(textwrap.dedent('''\
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:py="http://genshi.edgewall.org/">
|
||||
@ -107,7 +107,7 @@ class LibraryServer(object):
|
||||
</feed>
|
||||
'''))
|
||||
|
||||
|
||||
|
||||
def __init__(self, db, opts, embedded=False, show_tracebacks=True):
|
||||
self.db = db
|
||||
for item in self.db:
|
||||
@ -116,7 +116,7 @@ class LibraryServer(object):
|
||||
self.opts = opts
|
||||
self.max_cover_width, self.max_cover_height = \
|
||||
map(int, self.opts.max_cover.split('x'))
|
||||
|
||||
|
||||
cherrypy.config.update({
|
||||
'log.screen' : 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.users' : {opts.username.strip():opts.password.strip()},
|
||||
}
|
||||
|
||||
|
||||
self.is_running = False
|
||||
self.exception = None
|
||||
|
||||
|
||||
def setup_loggers(self):
|
||||
access_file = log_access_file
|
||||
error_file = log_error_file
|
||||
@ -152,20 +152,20 @@ class LibraryServer(object):
|
||||
|
||||
maxBytes = getattr(log, "rot_maxBytes", 10000000)
|
||||
backupCount = getattr(log, "rot_backupCount", 1000)
|
||||
|
||||
|
||||
# Make a new RotatingFileHandler for the error log.
|
||||
h = RotatingFileHandler(error_file, 'a', maxBytes, backupCount)
|
||||
h.setLevel(logging.DEBUG)
|
||||
h.setFormatter(cherrypy._cplogging.logfmt)
|
||||
log.error_log.addHandler(h)
|
||||
|
||||
|
||||
# Make a new RotatingFileHandler for the access log.
|
||||
h = RotatingFileHandler(access_file, 'a', maxBytes, backupCount)
|
||||
h.setLevel(logging.DEBUG)
|
||||
h.setFormatter(cherrypy._cplogging.logfmt)
|
||||
log.access_log.addHandler(h)
|
||||
|
||||
|
||||
|
||||
def start(self):
|
||||
self.is_running = False
|
||||
self.setup_loggers()
|
||||
@ -173,7 +173,7 @@ class LibraryServer(object):
|
||||
try:
|
||||
cherrypy.engine.start()
|
||||
self.is_running = True
|
||||
publish_zeroconf('Books in calibre', '_stanza._tcp',
|
||||
publish_zeroconf('Books in calibre', '_stanza._tcp',
|
||||
self.opts.port, {'path':'/stanza'})
|
||||
cherrypy.engine.block()
|
||||
except Exception, e:
|
||||
@ -181,10 +181,10 @@ class LibraryServer(object):
|
||||
finally:
|
||||
self.is_running = False
|
||||
stop_zeroconf()
|
||||
|
||||
|
||||
def exit(self):
|
||||
cherrypy.engine.exit()
|
||||
|
||||
|
||||
def get_cover(self, id, thumbnail=False):
|
||||
cover = self.db.cover(id, index_is_id=True, as_file=False)
|
||||
if cover is None:
|
||||
@ -196,14 +196,14 @@ class LibraryServer(object):
|
||||
try:
|
||||
if QApplication.instance() is None:
|
||||
QApplication([])
|
||||
|
||||
|
||||
im = QImage()
|
||||
im.loadFromData(cover)
|
||||
if im.isNull():
|
||||
raise cherrypy.HTTPError(404, 'No valid cover found')
|
||||
width, height = im.width(), im.height()
|
||||
scaled, width, height = fit_image(width, height,
|
||||
60 if thumbnail else self.max_cover_width,
|
||||
scaled, width, height = fit_image(width, height,
|
||||
60 if thumbnail else self.max_cover_width,
|
||||
80 if thumbnail else self.max_cover_height)
|
||||
if not scaled:
|
||||
return cover
|
||||
@ -217,7 +217,7 @@ class LibraryServer(object):
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise cherrypy.HTTPError(404, 'Failed to generate cover: %s'%err)
|
||||
|
||||
|
||||
def get_format(self, id, format):
|
||||
format = format.upper()
|
||||
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)
|
||||
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
|
||||
return fmt.read()
|
||||
|
||||
|
||||
def sort(self, items, field, order):
|
||||
field = field.lower().strip()
|
||||
if field == 'author':
|
||||
@ -243,10 +243,23 @@ class LibraryServer(object):
|
||||
raise cherrypy.HTTPError(400, '%s is not a valid sort field'%field)
|
||||
cmpf = cmp if field in ('rating', 'size', 'timestamp') else \
|
||||
lambda x, y: cmp(x.lower() if x else '', y.lower() if y else '')
|
||||
field = FIELD_MAP[field]
|
||||
getter = operator.itemgetter(field)
|
||||
items.sort(cmp=lambda x, y: cmpf(getter(x), getter(y)), reverse=not order)
|
||||
|
||||
if field == 'series':
|
||||
items.sort(cmp=self.seriescmp, 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):
|
||||
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'}
|
||||
@ -254,8 +267,8 @@ class LibraryServer(object):
|
||||
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'}
|
||||
return lm.replace('month', month[updated.month])
|
||||
|
||||
|
||||
|
||||
|
||||
@expose
|
||||
def stanza(self):
|
||||
' Feeds to read calibre books on a ipod with stanza.'
|
||||
@ -264,7 +277,7 @@ class LibraryServer(object):
|
||||
r = record[FIELD_MAP['formats']]
|
||||
r = r.upper() if r else ''
|
||||
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(',')])
|
||||
extra = []
|
||||
rating = record[FIELD_MAP['rating']]
|
||||
@ -276,7 +289,7 @@ class LibraryServer(object):
|
||||
extra.append('TAGS: %s<br />'%', '.join(tags.split(',')))
|
||||
series = record[FIELD_MAP['series']]
|
||||
if series:
|
||||
extra.append('SERIES: %s [%d]<br />'%(series,
|
||||
extra.append('SERIES: %s [%d]<br />'%(series,
|
||||
record[FIELD_MAP['series_index']]))
|
||||
fmt = 'epub' if 'EPUB' in r else 'pdb'
|
||||
mimetype = guess_type('dummy.'+fmt)[0]
|
||||
@ -288,24 +301,24 @@ class LibraryServer(object):
|
||||
mimetype=mimetype,
|
||||
fmt=fmt,
|
||||
).render('xml').decode('utf8'))
|
||||
|
||||
|
||||
updated = self.db.last_modified()
|
||||
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
|
||||
cherrypy.response.headers['Content-Type'] = 'text/xml'
|
||||
|
||||
|
||||
return self.STANZA.generate(subtitle='', data=books, FM=FIELD_MAP,
|
||||
updated=updated, id='urn:calibre:main').render('xml')
|
||||
|
||||
|
||||
@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'):
|
||||
'''
|
||||
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
|
||||
:param _: Firefox seems to sometimes send this when using XMLHttpRequest with no caching
|
||||
'''
|
||||
try:
|
||||
start = int(start)
|
||||
@ -321,19 +334,19 @@ class LibraryServer(object):
|
||||
items = [r for r in iter(self.db) if r[0] in ids]
|
||||
if sort is not None:
|
||||
self.sort(items, sort, order)
|
||||
|
||||
|
||||
book, books = MarkupTemplate(self.BOOK), []
|
||||
for record in items[start:start+num]:
|
||||
aus = record[2] if record[2] else _('Unknown')
|
||||
authors = '|'.join([i.replace('|', ',') for i in aus.split(',')])
|
||||
books.append(book.generate(r=record, authors=authors).render('xml').decode('utf-8'))
|
||||
updated = self.db.last_modified()
|
||||
|
||||
|
||||
cherrypy.response.headers['Content-Type'] = 'text/xml'
|
||||
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')
|
||||
|
||||
|
||||
@expose
|
||||
def index(self, **kwargs):
|
||||
'The / URL'
|
||||
@ -341,8 +354,8 @@ class LibraryServer(object):
|
||||
if stanza == 919:
|
||||
return self.static('index.html')
|
||||
return self.stanza()
|
||||
|
||||
|
||||
|
||||
|
||||
@expose
|
||||
def get(self, what, id):
|
||||
'Serves files, covers, thumbnails from the calibre database'
|
||||
@ -361,7 +374,7 @@ class LibraryServer(object):
|
||||
if what == 'cover':
|
||||
return self.get_cover(id)
|
||||
return self.get_format(id, what)
|
||||
|
||||
|
||||
@expose
|
||||
def static(self, name):
|
||||
'Serves static content'
|
||||
@ -392,11 +405,11 @@ def start_threaded_server(db, opts):
|
||||
server.thread.setDaemon(True)
|
||||
server.thread.start()
|
||||
return server
|
||||
|
||||
|
||||
def stop_threaded_server(server):
|
||||
server.exit()
|
||||
server.thread = None
|
||||
|
||||
|
||||
def option_parser():
|
||||
return config().option_parser('%prog '+ _('[options]\n\nStart the calibre content server.'))
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user