Integrate content server into the GUI

This commit is contained in:
Kovid Goyal 2008-11-05 11:09:00 -08:00
parent 1c4a68e1d4
commit d13c39c87e
6 changed files with 4583 additions and 45 deletions

View File

@ -1,6 +1,6 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, re import os, re, time
from PyQt4.QtGui import QDialog, QMessageBox, QListWidgetItem, QIcon, \ from PyQt4.QtGui import QDialog, QMessageBox, QListWidgetItem, QIcon, \
QDesktopServices, QVBoxLayout, QLabel, QPlainTextEdit QDesktopServices, QVBoxLayout, QLabel, QPlainTextEdit
@ -29,7 +29,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.item3 = QListWidgetItem(QIcon(':/images/view.svg'), _('Advanced'), self.category_list) self.item3 = QListWidgetItem(QIcon(':/images/view.svg'), _('Advanced'), self.category_list)
self.item4 = QListWidgetItem(QIcon(':/images/network-server.svg'), _('Content\nServer'), self.category_list) self.item4 = QListWidgetItem(QIcon(':/images/network-server.svg'), _('Content\nServer'), self.category_list)
self.db = db self.db = db
self.server = None self.server = server
path = prefs['library_path'] path = prefs['library_path']
self.location.setText(path if path else '') self.location.setText(path if path else '')
self.connect(self.browse_button, SIGNAL('clicked(bool)'), self.browse) self.connect(self.browse_button, SIGNAL('clicked(bool)'), self.browse)
@ -104,7 +104,6 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.viewer.item(self.viewer.count()-1).setCheckState(Qt.Checked if ext.upper() in config['internally_viewed_formats'] else Qt.Unchecked) self.viewer.item(self.viewer.count()-1).setCheckState(Qt.Checked if ext.upper() in config['internally_viewed_formats'] else Qt.Unchecked)
added_html = ext == 'html' added_html = ext == 'html'
self.viewer.sortItems() self.viewer.sortItems()
self.start.setEnabled(not getattr(self.server, 'is_running', False)) self.start.setEnabled(not getattr(self.server, 'is_running', False))
self.test.setEnabled(not self.start.isEnabled()) self.test.setEnabled(not self.start.isEnabled())
self.stop.setDisabled(self.start.isEnabled()) self.stop.setDisabled(self.start.isEnabled())
@ -159,6 +158,12 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.set_server_options() self.set_server_options()
from calibre.library.server import start_threaded_server from calibre.library.server import start_threaded_server
self.server = start_threaded_server(self.db, server_config().parse()) self.server = start_threaded_server(self.db, server_config().parse())
while not self.server.is_running and self.server.exception is None:
time.sleep(1)
if self.server.exception is not None:
error_dialog(self, _('Failed to start content server'),
unicode(self.server.exception)).exec_()
return
self.start.setEnabled(False) self.start.setEnabled(False)
self.test.setEnabled(True) self.test.setEnabled(True)
self.stop.setEnabled(True) self.stop.setEnabled(True)

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 168 KiB

View File

@ -3,7 +3,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, sys, textwrap, collections, traceback, time, re import os, sys, textwrap, collections, traceback, time, re
from xml.parsers.expat import ExpatError from xml.parsers.expat import ExpatError
from functools import partial from functools import partial
from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, QUrl from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, QUrl, QTimer
from PyQt4.QtGui import QPixmap, QColor, QPainter, QMenu, QIcon, QMessageBox, \ from PyQt4.QtGui import QPixmap, QColor, QPainter, QMenu, QIcon, QMessageBox, \
QToolButton, QDialog, QDesktopServices, QFileDialog QToolButton, QDialog, QDesktopServices, QFileDialog
from PyQt4.QtSvg import QSvgRenderer from PyQt4.QtSvg import QSvgRenderer
@ -287,8 +287,13 @@ class Main(MainWindow, Ui_MainWindow):
if config['autolaunch_server']: if config['autolaunch_server']:
from calibre.library.server import start_threaded_server from calibre.library.server import start_threaded_server
from calibre.library import server_config from calibre.library import server_config
self.server = start_threaded_server(db, server_config().parse()) self.content_server = start_threaded_server(db, server_config().parse())
self.test_server_timer = QTimer.singleShot(10000, self.test_server)
def test_server(self, *args):
if self.content_server.exception is not None:
error_dialog(self, _('Failed to start content server'),
unicode(self.content_server.exception)).exec_()
def toggle_cover_flow(self, show): def toggle_cover_flow(self, show):
if show: if show:
@ -1011,7 +1016,7 @@ class Main(MainWindow, Ui_MainWindow):
d = error_dialog(self, _('Cannot configure'), _('Cannot configure while there are running jobs.')) d = error_dialog(self, _('Cannot configure'), _('Cannot configure while there are running jobs.'))
d.exec_() d.exec_()
return return
d = ConfigDialog(self, self.library_view.model().db, self.content_server) d = ConfigDialog(self, self.library_view.model().db, server=self.content_server)
d.exec_() d.exec_()
self.content_server = d.server self.content_server = d.server
if d.result() == d.Accepted: if d.result() == d.Accepted:
@ -1222,11 +1227,14 @@ in which you want to store your books files. Any existing books will be automati
self.hide() self.hide()
self.cover_cache.terminate() self.cover_cache.terminate()
try: try:
if self.server is not None: try:
self.server.exit() if self.content_server is not None:
except: self.content_server.exit()
except:
pass
time.sleep(2)
except KeyboardInterrupt:
pass pass
time.sleep(2)
e.accept() e.accept()
def update_found(self, version): def update_found(self, version):

View File

@ -2,6 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
''' Code to manage ebook library''' ''' Code to manage ebook library'''
import re import re
from calibre.utils.config import Config, StringConfig
title_pat = re.compile('^(A|The|An)\s+', re.IGNORECASE) title_pat = re.compile('^(A|The|An)\s+', re.IGNORECASE)
def title_sort(title): def title_sort(title):
@ -10,3 +11,23 @@ def title_sort(title):
prep = match.group(1) prep = match.group(1)
title = title.replace(prep, '') + ', ' + prep title = title.replace(prep, '') + ', ' + prep
return title.strip() return title.strip()
def server_config(defaults=None):
desc=_('Settings to control the calibre content server')
c = Config('server', desc) if defaults is None else StringConfig(defaults, desc)
c.add_opt('port', ['-p', '--port'], default=8080,
help=_('The port on which to listen. Default is %default'))
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('password', ['--password'], default=None,
help=_('Set a password to restrict access. By default access is unrestricted.'))
c.add_opt('username', ['--username'], default='calibre',
help=_('Username for access. By default, it is: %default'))
c.add_opt('develop', ['--develop'], default=False,
help='Development mode. Server automatically restarts on file changes and serves code files (html, css, js) from the file system instead of calibre\'s resource system.')
return c

View File

@ -7,17 +7,21 @@ __docformat__ = 'restructuredtext en'
HTTP server for remote access to the calibre database. HTTP server for remote access to the calibre database.
''' '''
import sys, textwrap, cStringIO, mimetypes, operator, os, re import sys, textwrap, cStringIO, mimetypes, operator, os, re, logging
from logging.handlers import RotatingFileHandler
from datetime import datetime from datetime import datetime
from threading import Thread
import cherrypy import cherrypy
from PIL import Image from PIL import Image
from calibre.constants import __version__, __appname__ from calibre.constants import __version__, __appname__
from calibre.utils.config import StringConfig, Config
from calibre.utils.genshi.template import MarkupTemplate from calibre.utils.genshi.template import MarkupTemplate
from calibre import fit_image from calibre import fit_image
from calibre.resources import jquery, server_resources, build_time from calibre.resources import jquery, server_resources, build_time
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
build_time = datetime.strptime(build_time, '%d %m %Y %H%M%S') build_time = datetime.strptime(build_time, '%d %m %Y %H%M%S')
server_resources['jquery.js'] = jquery server_resources['jquery.js'] = jquery
@ -30,6 +34,10 @@ def expose(func):
return cherrypy.expose(do) 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): class LibraryServer(object):
server_name = __appname__ + '/' + __version__ server_name = __appname__ + '/' + __version__
@ -97,7 +105,7 @@ class LibraryServer(object):
''')) '''))
def __init__(self, db, opts): 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:
item item
@ -105,20 +113,68 @@ class LibraryServer(object):
self.opts = opts self.opts = opts
cherrypy.config.update({ cherrypy.config.update({
'server.socket_host': '0.0.0.0', 'log.screen' : opts.develop,
'server.socket_port': opts.port, 'engine.autoreload_on' : opts.develop,
'server.socket_timeout': opts.timeout, #seconds 'tools.log_headers.on' : opts.develop,
'server.thread_pool': opts.thread_pool, # number of threads 'checker.on' : opts.develop,
'request.show_tracebacks': show_tracebacks,
'server.socket_host' : '0.0.0.0',
'server.socket_port' : opts.port,
'server.socket_timeout' : opts.timeout, #seconds
'server.thread_pool' : opts.thread_pool, # number of threads
}) })
self.config = textwrap.dedent('''\ if embedded:
[global] cherrypy.config.update({'engine.SIGHUP' : None,
engine.autoreload_on = %(autoreload)s 'engine.SIGTERM' : None,})
tools.gzip.on = True self.config = {'global': {
tools.gzip.mime_types = ['text/html', 'text/plain', 'text/xml', 'text/javascript', 'text/css'] 'tools.gzip.on' : True,
''')%dict(autoreload=opts.develop) 'tools.gzip.mime_types': ['text/html', 'text/plain', 'text/xml', 'text/javascript', 'text/css'],
}}
if opts.password:
g = self.config['global']
g['tools.digest_auth.on'] = True
g['tools.digest_auth.realm'] = _('Password to access your calibre library. Username is ') + opts.username.strip()
g['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
log = cherrypy.log
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): def start(self):
cherrypy.quickstart(self, config=cStringIO.StringIO(self.config)) self.is_running = False
self.setup_loggers()
cherrypy.tree.mount(self, '', config=self.config)
try:
cherrypy.engine.start()
self.is_running = True
cherrypy.engine.block()
except Exception, e:
self.exception = e
finally:
self.is_running = False
def exit(self):
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=True) cover = self.db.cover(id, index_is_id=True, as_file=True)
@ -284,24 +340,16 @@ class LibraryServer(object):
return server_resources[name] return server_resources[name]
raise cherrypy.HTTPError(404, '%s not found'%name) raise cherrypy.HTTPError(404, '%s not found'%name)
def start_threaded_server(db, opts):
server = LibraryServer(db, opts, embedded=True)
server.thread = Thread(target=server.start)
server.thread.setDaemon(True)
server.thread.start()
return server
def stop_threaded_server(server):
def config(defaults=None): server.exit()
desc=_('Settings to control the calibre content server') server.thread = None
c = Config('server', desc) if defaults is None else StringConfig(defaults, desc)
c.add_opt('port', ['-p', '--port'], default=8080,
help=_('The port on which to listen. Default is %default'))
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 and serves code files (html, css, js) from the file system instead of calibre\'s resource system.')
return c
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.'))