IGN:Initial implementation of the calibre content server

This commit is contained in:
Kovid Goyal 2008-10-31 20:33:56 -07:00
parent 3af8332413
commit 7bc0b414cb
3 changed files with 206 additions and 106 deletions

View File

@ -2,7 +2,7 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import sys, os, re, logging, time, subprocess, atexit
import sys, os, re, logging, time, subprocess, atexit, mimetypes
from htmlentitydefs import name2codepoint
from math import floor
from logging import Formatter
@ -16,6 +16,10 @@ from calibre.constants import iswindows, isosx, islinux, isfrozen, \
win32event, win32api, winerror, fcntl
import mechanize
mimetypes.add_type('application/epub+zip', '.epub')
mimetypes.add_type('text/x-sony-bbeb+xml', '.lrs')
mimetypes.add_type('application/x-sony-bbeb', '.lrf')
def unicode_path(path, abs=False):
if not isinstance(path, unicode):
path = path.decode(sys.getfilesystemencoding())

View File

@ -589,13 +589,22 @@ class LibraryDatabase2(LibraryDatabase):
ans.append(format)
return ','.join(ans)
def has_format(self, index, format, index_is_id=False):
id = index if index_is_id else self.id(index)
name = self.conn.get('SELECT name FROM data WHERE book=? AND format=?', (id, format), all=False)
if name:
path = os.path.join(self.library_path, self.path(id, index_is_id=True))
format = ('.' + format.lower()) if format else ''
path = os.path.join(path, name+format)
return os.access(path, os.R_OK|os.W_OK)
return False
def format_abspath(self, index, format, index_is_id=False):
'Return absolute path to the ebook file of format `format`'
id = index if index_is_id else self.id(index)
path = os.path.join(self.library_path, self.path(id, index_is_id=True))
name = self.conn.get('SELECT name FROM data WHERE book=? AND format=?', (id, format), all=False)
if name:
path = os.path.join(self.library_path, self.path(id, index_is_id=True))
format = ('.' + format.lower()) if format else ''
path = os.path.join(path, name+format)
if os.access(path, os.R_OK|os.W_OK):
@ -612,6 +621,7 @@ class LibraryDatabase2(LibraryDatabase):
if path is not None:
f = open(path, mode)
return f if as_file else f.read()
if self.has_format(index, format, index_is_id):
self.remove_format(id, format, index_is_id=True)
def add_format(self, index, format, stream, index_is_id=False, path=None):
@ -982,6 +992,14 @@ class LibraryDatabase2(LibraryDatabase):
progress.hide()
def __iter__(self):
if len(self.data) == 0:
self.refresh('timestamp', True)
for record in self.data:
if record is not None:
yield record
def get_data_as_dict(self, prefix=None, authors_as_string=False):
'''
Return all metadata stored in the database as a dict. Includes paths to
@ -994,11 +1012,7 @@ class LibraryDatabase2(LibraryDatabase):
prefix = self.library_path
FIELDS = set(['title', 'authors', 'publisher', 'rating', 'timestamp', 'size', 'tags', 'comments', 'series', 'series_index', 'isbn'])
data = []
if len(self.data) == 0:
self.refresh('timestamp', True)
for record in self.data:
if record is None:
continue
for record in iter(self):
x = {}
for field in FIELDS:
x[field] = record[field]

View File

@ -7,96 +7,182 @@ __docformat__ = 'restructuredtext en'
HTTP server for remote access to the calibre database.
'''
import sys, logging, re, SocketServer, gzip, cStringIO
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
import sys, textwrap, cStringIO, mimetypes
import cherrypy
from PIL import Image
from calibre.constants import __version__
from calibre.constants import __version__, __appname__
from calibre.utils.config import StringConfig, Config
from calibre import ColoredFormatter
from calibre.utils.genshi.template import MarkupTemplate
from calibre import fit_image
class Server(SocketServer.ThreadingMixIn, HTTPServer):
def __init__(self, opts, db):
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)
class LibraryServer(object):
server_name = __appname__ + '/' + __version__
BOOK = textwrap.dedent('''\
<book xmlns:py="http://genshi.edgewall.org/"
id="${r[0]}"
title="${r[1]}"
sort="${r[11]}"
author_sort="${r[12]}"
authors="${authors}"
rating="${r[4]}"
timestamp="${timestamp.ctime()}"
size="${r[6]}"
isbn="${r[14] if r[14] else ''}"
formats="${r[13] if r[13] else ''}"
series = "${r[9] if r[9] else ''}"
series_index="${r[10]}"
tags="${r[7] if r[7] else ''}"
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/" size="${len(books)}">
<py:for each="book in books">
${Markup(book)}
</py:for>
</library>
'''))
STANZA_ENTRY=MarkupTemplate(textwrap.dedent('''\
<entry xmlns:py="http://genshi.edgewall.org/">
<title>${record['title']}</title>
<id>urn:calibre:${record['id']}</id>
<author><name>${authors}</name></author>
<updated>${record['timestamp'].strftime('%Y-%m-%dT%H:%M:%S+0000')}</updated>
<link type="application/epub+zip" href="http://${server}:${port}/get/epub/${record['id']}" />
<link rel="x-stanza-cover-image" type="image/jpeg" href="http://${server}:${port}/get/cover/${record['id']}" />
<link rel="x-stanza-cover-image-thumbnail" type="image/jpeg" href="http://${server}:${port}/get/thumb/${record['id']}" />
<content py:if="record['comments']" type="xhtml">
<pre>${record['comments']}</pre>
</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/">
<title>calibre Library</title>
<author>
<name>calibre</name>
<uri>http://calibre.kovidgoyal.net</uri>
</author>
<subtitle>
${subtitle}
</subtitle>
<py:for each="entry in data">
${Markup(entry)}
</py:for>
</feed>
'''))
def __init__(self, db, opts):
self.db = db
HTTPServer.__init__(self, ('', opts.port), DBHandler)
for item in self.db:
item
break
self.opts = opts
cherrypy.config.update({
'server.socket_port': opts.port,
'server.socket_timeout': opts.timeout, #seconds
'server.thread_pool': opts.thread_pool, # number of threads
})
self.config = textwrap.dedent('''\
[global]
engine.autoreload_on = %(autoreload)s
tools.gzip.on = True
tools.gzip.mime_types = ['text/html', 'text/plain', 'text/xml']
''')%dict(autoreload=opts.develop)
def serve_forever(self):
logging.getLogger('calibre.server').info('calibre-server starting...')
HTTPServer.serve_forever(self)
def to_xml(self):
books = []
book = MarkupTemplate(self.BOOK)
for record in iter(self.db):
authors = ' & '.join([i.replace('|', ',') for i in record[2].split(',')])
books.append(book.generate(r=record, authors=authors).render('xml').decode('utf-8'))
return self.LIBRARY.generate(books=books).render('xml')
def start(self):
cherrypy.quickstart(self, config=cStringIO.StringIO(self.config))
class DBHandler(BaseHTTPRequestHandler):
def get_cover(self, id, thumbnail=False):
cover = self.db.cover(id, index_is_id=True, as_file=True)
if cover is None:
raise cherrypy.HTTPError(404, 'no cover available for id: %d'%id)
cherrypy.response.headers['Content-Type'] = 'image/jpeg'
if not thumbnail:
return cover.read()
try:
im = Image.open(cover)
width, height = im.size
scaled, width, height = fit_image(width, height, 80, 60)
if not scaled:
return cover.read()
im.thumbnail((width, height))
o = cStringIO.StringIO()
im.save(o, 'JPEG')
return o.getvalue()
except Exception, err:
raise cherrypy.HTTPError(404, 'failed to generate thumbnail: %s'%err)
server_version = 'calibre/'+__version__
protocol_version = 'HTTP/1.0'
cover_request = re.compile(r'/(\d+)/cover', re.IGNORECASE)
thumbnail_request = re.compile(r'/(\d+)/thumb', re.IGNORECASE)
fmt_request = re.compile(r'/(\d+)/([a-z0-9]+)', re.IGNORECASE)
def get_format(self, id, format):
format = format.upper()
fmt = self.db.format(id, format, index_is_id=True)
if fmt is None:
raise cherrypy.HTTPError(404, 'book: %d does not have format: %s'%(id, format))
mt = mimetypes.guess_type('dummy.'+format.lower())[0]
if mt is None:
mt = 'application/octet-stream'
cherrypy.response.headers['Content-Type'] = mt
return fmt
@expose
def stanza(self):
cherrypy.response.headers['Content-Type'] = 'text/xml'
books = []
for record in iter(self.db):
authors = ' & '.join([i.replace('|', ',') for i in record[2].split(',')])
books.append(self.STANZA_ENTRY.generate(authors=authors,
record=record,
port=self.opts.port,
server=self.opts.hostname,
).render('xml').decode('utf8'))
return self.STANZA.generate(subtitle='', data=books).render('xml')
def __init__(self, request, client_address, server, *args, **kwargs):
self.l = logging.getLogger('calibre.server')
self.db = server.db
BaseHTTPRequestHandler.__init__(self, request, client_address, server, *args, **kwargs)
@expose
def library(self):
cherrypy.response.headers['Content-Type'] = 'text/xml'
return self.to_xml()
@expose
def index(self):
return 'Hello, World!'
def log_message(self, fmt, *args):
self.l.info("%s - - [%s] %s\n" %
(self.address_string(),
self.log_date_time_string(),
fmt%args))
def log_error(self, fmt, *args):
self.l.error("%s - - [%s] %s\n" %
(self.address_string(),
self.log_date_time_string(),
fmt%args))
def do_GET(self):
cover = self.cover_request.match(self.path)
thumb = self.thumbnail_request.match(self.path)
fmt = self.fmt_request.match(self.path)
if self.path == '/':
self.send_index()
elif self.path == '/stanza.atom':
self.send_stanza_index()
elif self.path == '/library':
self.send_library()
elif thumb:
self.send_cover(int(thumb.group(1)), thumbnail=True)
elif cover:
self.send_cover(int(cover.group(1)))
elif fmt:
self.send_format(int(fmt.group(1)), fmt.group(2).upper())
elif self.path == '/help':
self.send_help()
self.send_error(400, 'Bad request. Try /help for usage.')
def compress(buf):
zbuf = cStringIO.StringIO()
zfile = gzip.GzipFile(mode = 'wb', fileobj = zbuf, compresslevel = 9)
zfile.write(buf)
zfile.close()
return zbuf.getvalue()
def send_help(self):
self.send_error(501, 'Not Implemented')
def send_index(self):
self.send_error(501, 'Not Implemented')
def send_stanza_index(self):
self.send_error(501, 'Not Implemented')
def send_library(self):
self.send_error(501, 'Not Implemented')
def send_cover(self, id, thumbnail=False):
self.send_error(501, 'Not Implemented')
def send_format(self, id, fmt):
self.send_error(501, 'Not Implemented')
@expose
def get(self, what, id):
try:
id = int(id)
except ValueError:
raise cherrypy.HTTPError(400, 'id:%s not an integer'%id)
if not self.db.has_id(id):
raise cherrypy.HTTPError(400, 'id:%d does not exist in database'%id)
if what == 'thumb':
return self.get_cover(id, thumbnail=True)
if what == 'cover':
return self.get_cover(id)
return self.get_format(id, what)
def config(defaults=None):
desc=_('Settings to control the calibre content server')
@ -104,8 +190,15 @@ def config(defaults=None):
c.add_opt('port', ['-p', '--port'], default=8080,
help=_('The port on which to listen. Default is %default'))
c.add_opt('debug', ['--debug'], default=False,
help=_('Detailed logging'))
c.add_opt('timeout', ['-t', '--timeout'], default=120,
help=_('The server timeout in seconds. Default is %default'))
c.add_opt('thread_pool', ['--thread-pool'], default=30,
help=_('The max number of worker threads to use. Default is %default'))
c.add_opt('hostname', ['--hostname'], default='localhost',
help=_('The hostname of the machine the server is running on. Used when generating the stanza feeds. Default is %default'))
c.add_opt('develop', ['--develop'], default=False,
help='Development mode. Server automatically restarts on file changes.')
return c
def option_parser():
@ -114,23 +207,12 @@ def option_parser():
def main(args=sys.argv):
parser = option_parser()
opts, args = parser.parse_args(args)
l = logging.getLogger('calibre.server')
l.setLevel(logging.DEBUG if opts.debug else logging.INFO)
l.addHandler(logging.StreamHandler(sys.stdout))
l.handlers[-1].setLevel(logging.DEBUG if opts.debug else logging.INFO)
formatter = ColoredFormatter('%(levelname)s: %(message)s')
l.handlers[-1].setFormatter(formatter)
cherrypy.log.screen = True
from calibre.utils.config import prefs
from calibre.library.database2 import LibraryDatabase2
db = LibraryDatabase2(prefs['library_path'])
try:
print 'Starting server...'
s = Server(opts, db)
s.serve_forever()
except KeyboardInterrupt:
print 'Server interrupted'
s.socket.close()
db = LibraryDatabase2(prefs['library_path'], row_factory=True)
server = LibraryServer(db, opts)
server.start()
return 0
if __name__ == '__main__':