mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 02:34:06 -04:00
Integrate content server into the GUI
This commit is contained in:
parent
1c4a68e1d4
commit
d13c39c87e
@ -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)
|
||||||
|
4456
src/calibre/gui2/images/network-server.svg
Normal file
4456
src/calibre/gui2/images/network-server.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 168 KiB |
@ -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:
|
||||||
|
self.content_server.exit()
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
e.accept()
|
e.accept()
|
||||||
|
|
||||||
def update_found(self, version):
|
def update_found(self, version):
|
||||||
|
@ -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
|
||||||
|
@ -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({
|
||||||
|
'log.screen' : opts.develop,
|
||||||
|
'engine.autoreload_on' : opts.develop,
|
||||||
|
'tools.log_headers.on' : opts.develop,
|
||||||
|
'checker.on' : opts.develop,
|
||||||
|
'request.show_tracebacks': show_tracebacks,
|
||||||
'server.socket_host' : '0.0.0.0',
|
'server.socket_host' : '0.0.0.0',
|
||||||
'server.socket_port' : opts.port,
|
'server.socket_port' : opts.port,
|
||||||
'server.socket_timeout' : opts.timeout, #seconds
|
'server.socket_timeout' : opts.timeout, #seconds
|
||||||
'server.thread_pool' : opts.thread_pool, # number of threads
|
'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.'))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user