Start work on embedding the new content server in the GUI

This commit is contained in:
Kovid Goyal 2017-04-10 15:46:24 +05:30
parent 420841377c
commit 3c64011526
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
10 changed files with 503 additions and 510 deletions

View File

@ -316,6 +316,11 @@ class Connection(apsw.Connection): # {{{
# }}}
def set_global_state(backend):
load_user_template_functions(backend.library_id,
backend.prefs.get('user_template_functions', []))
class DB(object):
PATH_LIMIT = 40 if iswindows else 100
@ -402,8 +407,7 @@ class DB(object):
self.initialize_custom_columns()
self.initialize_tables()
if load_user_formatter_functions:
load_user_template_functions(self.library_id,
self.prefs.get('user_template_functions', []))
set_global_state(self)
def initialize_prefs(self, default_prefs, restore_all_prefs, progress_callback): # {{{
self.prefs = DBPrefs(self)

View File

@ -15,7 +15,7 @@ from calibre.db import _get_next_series_num_for_list, _get_series_values, get_da
from calibre.db.adding import (
find_books_in_directory, import_book_directory_multiple,
import_book_directory, recursive_import, add_catalog, add_news)
from calibre.db.backend import DB
from calibre.db.backend import DB, set_global_state as backend_set_global_state
from calibre.db.cache import Cache
from calibre.db.errors import NoSuchFormat
from calibre.db.categories import CATEGORY_SORTS
@ -48,6 +48,11 @@ def create_backend(
load_user_formatter_functions=load_user_formatter_functions)
def set_global_state(db):
backend_set_global_state(db.backend)
set_saved_searches(db, 'saved_searches')
class LibraryDatabase(object):
''' Emulate the old LibraryDatabase2 interface '''
@ -92,14 +97,16 @@ class LibraryDatabase(object):
set_saved_searches(self, 'saved_searches')
def close(self):
self.new_api.close()
if hasattr(self, 'new_api'):
self.new_api.close()
def break_cycles(self):
delattr(self.backend, 'field_metadata')
self.data.cache.backend = None
self.data.cache = None
for x in ('data', 'backend', 'new_api', 'listeners',):
delattr(self, x)
if hasattr(self, 'backend'):
delattr(self.backend, 'field_metadata')
self.data.cache.backend = None
self.data.cache = None
for x in ('data', 'backend', 'new_api', 'listeners',):
delattr(self, x)
# Library wide properties {{{
@property
@ -742,6 +749,7 @@ class LibraryDatabase(object):
# }}}
MT = lambda func: types.MethodType(func, None, LibraryDatabase)
# Legacy getter API {{{
@ -936,4 +944,3 @@ LibraryDatabase.commit = MT(lambda self:None)
# }}}
del MT

View File

@ -421,6 +421,7 @@ class ChooseLibraryAction(InterfaceAction):
'Try switching to this library first, then switch back '
'and retry the renaming.')%loc, show=True)
return
self.gui.library_broker.remove_library(loc)
try:
os.rename(loc, newloc)
except:
@ -448,6 +449,7 @@ class ChooseLibraryAction(InterfaceAction):
yes_text=_('&OK'), no_text=_('&Undo'), yes_icon='ok.png', no_icon='edit-undo.png'):
return
self.stats.remove(location)
self.gui.library_broker.remove_library(location)
self.build_menus()
self.gui.iactions['Copy To Library'].build_menus()
if os.path.exists(loc):
@ -484,7 +486,7 @@ class ChooseLibraryAction(InterfaceAction):
db = m.db
db.prefs.disable_setting = True
if restore_database(db, self.gui):
self.gui.library_moved(db.library_path, call_close=False)
self.gui.library_moved(db.library_path)
def check_library(self):
from calibre.gui2.dialogs.check_library import CheckLibraryDialog, DBCheck
@ -502,7 +504,7 @@ class ChooseLibraryAction(InterfaceAction):
except:
pass
d.break_cycles()
self.gui.library_moved(library_path, call_close=False)
self.gui.library_moved(library_path)
if d.rejected:
return
if d.error is None:

View File

@ -7,59 +7,127 @@ __docformat__ = 'restructuredtext en'
import time
from PyQt5.Qt import Qt, QUrl, QDialog, QSize, QVBoxLayout, QLabel, \
QPlainTextEdit, QDialogButtonBox, QTimer
from PyQt5.Qt import (
QCheckBox, QDialog, QDialogButtonBox, QLabel, QPlainTextEdit, QSize, Qt,
QTabWidget, QTimer, QUrl, QVBoxLayout, QWidget, pyqtSignal, QHBoxLayout,
QPushButton
)
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
from calibre.gui2.preferences.server_ui import Ui_Form
from calibre.library.server import server_config
from calibre.utils.config import ConfigProxy
from calibre.gui2 import error_dialog, config, open_url, warning_dialog, \
Dispatcher, info_dialog
from calibre import as_unicode
from calibre.utils.icu import sort_key
from calibre.gui2 import (
Dispatcher, config, error_dialog, info_dialog, open_url, warning_dialog
)
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
from calibre.srv.opts import server_config, options
class ConfigWidget(ConfigWidgetBase, Ui_Form):
class MainTab(QWidget):
changed_signal = pyqtSignal()
start_server = pyqtSignal()
stop_server = pyqtSignal()
test_server = pyqtSignal()
show_logs = pyqtSignal()
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self.l = l = QVBoxLayout(self)
self.la = la = QLabel(_(
'calibre contains an internet server that allows you to'
' access your book collection using a browser from anywhere'
' in the world. Any changes to the settings will only take'
' effect after a server restart.'))
la.setWordWrap(True)
l.addWidget(la)
l.addSpacing(10)
self.opt_auth = cb = QCheckBox(_('Require username/password to access the content server'))
l.addWidget(cb)
self.auth_desc = la = QLabel(self)
la.setStyleSheet('QLabel { font-size: smaller }')
la.setWordWrap(True)
l.addSpacing(25)
self.opt_autolaunch_server = al = QCheckBox(_('Run server &automatically when calibre starts'))
l.addWidget(al)
self.h = h = QHBoxLayout()
l.addLayout(h)
for text, name in [(_('&Start server'), 'start_server'), (_('St&op server'), 'stop_server'),
(_('&Test server'), 'test_server'), (_('Show server &logs'), 'show_logs')]:
b = QPushButton(text)
b.clicked.connect(getattr(self, name).emit)
setattr(self, name + '_button', b)
if name == 'show_logs':
h.addStretch(10)
h.addWidget(b)
def genesis(self):
opts = server_config()
self.opt_auth.setChecked(opts.auth)
self.opt_auth.stateChanged.connect(self.auth_changed)
self.change_auth_desc()
self.update_button_state()
def change_auth_desc(self):
self.auth_desc.setText(
_('Remember to create some user accounts in the "Users" tab') if self.opt_auth.isChecked() else
_('Requiring a username/password prevents unauthorized people from'
' accessing your calibre library. It is also needed for some features'
' such as last read position/annotation syncing and making'
' changes to the library.')
)
def auth_changed(self):
self.changed_signal.emit()
self.change_auth_desc()
def restore_defaults(self):
self.auth_changed.setChecked(options['auth'].default)
def update_button_state(self):
gui = self.parent().gui
is_running = gui.content_server is not None and gui.content_server.is_running
self.start_server_button.setEnabled(not is_running)
self.stop_server_button.setEnabled(is_running)
self.test_server_button.setEnabled(is_running)
class ConfigWidget(ConfigWidgetBase):
def __init__(self, *args, **kw):
ConfigWidgetBase.__init__(self, *args, **kw)
self.l = l = QVBoxLayout(self)
l.setContentsMargins(0, 0, 0, 0)
self.tabs_widget = t = QTabWidget(self)
self.main_tab = m = MainTab(self)
t.addTab(m, _('Main'))
m.start_server.connect(self.start_server)
m.stop_server.connect(self.stop_server)
m.test_server.connect(self.test_server)
m.show_logs.connect(self.view_server_logs)
self.opt_autolaunch_server = m.opt_autolaunch_server
for tab in self.tabs:
if hasattr(tab, 'changed_signal'):
tab.changed_signal.connect(self.changed_signal.emit)
@property
def tabs(self):
return (self.tabs_widget.widget(i) for i in range(self.tabs_widget.count()))
@property
def server(self):
return self.gui.server
def restore_defaults(self):
ConfigWidgetBase.restore_defaults(self)
for tab in self.tabs:
if hasattr(tab, 'restore_defaults'):
tab.restore_defaults()
def genesis(self, gui):
self.gui = gui
self.proxy = ConfigProxy(server_config())
db = self.db = gui.library_view.model().db
self.server = self.gui.content_server
for tab in self.tabs:
tab.genesis()
r = self.register
r('port', self.proxy)
r('username', self.proxy)
r('password', self.proxy)
r('max_cover', self.proxy)
r('max_opds_items', self.proxy)
r('max_opds_ungrouped_items', self.proxy)
r('url_prefix', self.proxy)
self.show_server_password.stateChanged[int].connect(
lambda s: self.opt_password.setEchoMode(
self.opt_password.Normal if s == Qt.Checked
else self.opt_password.Password))
self.opt_password.setEchoMode(self.opt_password.Password)
restrictions = sorted(db.prefs['virtual_libraries'].iterkeys(), key=sort_key)
choices = [('', '')] + [(x, x) for x in restrictions]
# check that the virtual library still exists
vls = db.prefs['cs_virtual_lib_on_startup']
if vls and vls not in restrictions:
db.prefs['cs_virtual_lib_on_startup'] = ''
r('cs_virtual_lib_on_startup', db.prefs, choices=choices)
self.start_button.setEnabled(not getattr(self.server, 'is_running', False))
self.test_button.setEnabled(not self.start_button.isEnabled())
self.stop_button.setDisabled(self.start_button.isEnabled())
self.start_button.clicked.connect(self.start_server)
self.stop_button.clicked.connect(self.stop_server)
self.test_button.clicked.connect(self.test_server)
self.view_logs.clicked.connect(self.view_server_logs)
r('autolaunch_server', config)
def start_server(self):
@ -74,9 +142,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
error_dialog(self, _('Failed to start content server'),
as_unicode(self.gui.content_server.exception)).exec_()
return
self.start_button.setEnabled(False)
self.test_button.setEnabled(True)
self.stop_button.setEnabled(True)
self.main_tab.update_button_state()
finally:
self.unsetCursor()
@ -94,9 +160,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
return
self.gui.content_server = None
self.start_button.setEnabled(True)
self.test_button.setEnabled(False)
self.stop_button.setEnabled(False)
self.main_tab.update_button_state()
self.stopping_msg.accept()
def test_server(self):
@ -104,7 +168,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
open_url(QUrl('http://127.0.0.1:'+str(self.opt_port.value())+prefix))
def view_server_logs(self):
from calibre.library.server import log_access_file, log_error_file
from calibre.srv.embedded import log_paths
log_error_file, log_access_file = log_paths()
d = QDialog(self)
d.resize(QSize(800, 600))
layout = QVBoxLayout()
@ -113,15 +178,15 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
el = QPlainTextEdit(d)
layout.addWidget(el)
try:
el.setPlainText(open(log_error_file, 'rb').read().decode('utf8', 'replace'))
except IOError:
el.setPlainText(lopen(log_error_file, 'rb').read().decode('utf8', 'replace'))
except EnvironmentError:
el.setPlainText('No error log found')
layout.addWidget(QLabel(_('Access log:')))
al = QPlainTextEdit(d)
layout.addWidget(al)
try:
al.setPlainText(open(log_access_file, 'rb').read().decode('utf8', 'replace'))
except IOError:
al.setPlainText(lopen(log_access_file, 'rb').read().decode('utf8', 'replace'))
except EnvironmentError:
al.setPlainText('No access log found')
bx = QDialogButtonBox(QDialogButtonBox.Ok)
layout.addWidget(bx)
@ -147,4 +212,3 @@ if __name__ == '__main__':
from PyQt5.Qt import QApplication
app = QApplication([])
test_widget('Sharing', 'Server')

View File

@ -1,303 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>641</width>
<height>563</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QGridLayout" name="gridLayout_5">
<item row="1" column="1" colspan="2">
<widget class="QLineEdit" name="opt_username"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_12">
<property name="text">
<string>&amp;Password:</string>
</property>
<property name="buddy">
<cstring>opt_password</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="opt_password">
<property name="toolTip">
<string>&lt;p&gt;If you leave the password blank, anyone will be able to
access your book collection using the web interface.
&lt;br&gt;
&lt;p&gt;Some devices have browsers that do not support authentication. If you are having trouble downloading files from the content server, try removing the password.</string>
</property>
</widget>
</item>
<item row="4" column="1" colspan="2">
<widget class="QLineEdit" name="opt_max_cover">
<property name="toolTip">
<string>The maximum size (widthxheight) for displayed covers. Larger covers are resized. </string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Max. &amp;cover size:</string>
</property>
<property name="buddy">
<cstring>opt_max_cover</cstring>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_10">
<property name="text">
<string>Server &amp;port:</string>
</property>
<property name="buddy">
<cstring>opt_port</cstring>
</property>
</widget>
</item>
<item row="0" column="1" colspan="2">
<widget class="QSpinBox" name="opt_port">
<property name="maximum">
<number>65535</number>
</property>
<property name="value">
<number>8080</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_11">
<property name="text">
<string>&amp;Username:</string>
</property>
<property name="buddy">
<cstring>opt_username</cstring>
</property>
</widget>
</item>
<item row="3" column="1" colspan="2">
<widget class="QCheckBox" name="show_server_password">
<property name="text">
<string>&amp;Show password</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_15">
<property name="text">
<string>Max. &amp;OPDS items per query:</string>
</property>
<property name="buddy">
<cstring>opt_max_opds_items</cstring>
</property>
</widget>
</item>
<item row="5" column="1" colspan="2">
<widget class="QSpinBox" name="opt_max_opds_items">
<property name="minimum">
<number>10</number>
</property>
<property name="maximum">
<number>10000</number>
</property>
</widget>
</item>
<item row="6" column="1" colspan="2">
<widget class="QSpinBox" name="opt_max_opds_ungrouped_items">
<property name="minimum">
<number>25</number>
</property>
<property name="maximum">
<number>1000000</number>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_16">
<property name="text">
<string>Max. &amp;ungrouped items:</string>
</property>
<property name="buddy">
<cstring>opt_max_opds_ungrouped_items</cstring>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_164">
<property name="text">
<string>Virtual library to apply:</string>
</property>
</widget>
</item>
<item row="7" column="1" colspan="2">
<widget class="QComboBox" name="opt_cs_virtual_lib_on_startup">
<property name="toolTip">
<string>Setting a virtual library will restrict the books the content server makes available to those in the library. This setting is per library (i.e. you can have a different value per library).</string>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
</property>
<property name="minimumContentsLength">
<number>20</number>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QLabel" name="label">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="toolTip">
<string>&lt;p&gt;Some devices have browsers that do not support authentication. If you are having trouble downloading files from the content server, trying removing the password.</string>
</property>
<property name="styleSheet">
<string notr="true">QLabel {color:red}</string>
</property>
<property name="text">
<string>Passwords are incompatible with some devices</string>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>&amp;URL Prefix:</string>
</property>
<property name="buddy">
<cstring>opt_url_prefix</cstring>
</property>
</widget>
</item>
<item row="8" column="1" colspan="2">
<widget class="QLineEdit" name="opt_url_prefix">
<property name="toolTip">
<string>A prefix that is applied to all URLs in the content server. Useful only if you plan to put the server behind another server like Apache, with a reverse proxy.</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QPushButton" name="start_button">
<property name="text">
<string>&amp;Start Server</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="stop_button">
<property name="text">
<string>St&amp;op Server</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="test_button">
<property name="text">
<string>&amp;Test Server</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label_9">
<property name="text">
<string>calibre contains a network server that allows you to access your book collection using a browser from anywhere in the world. Any changes to the settings will only take effect after a server restart.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="opt_autolaunch_server">
<property name="text">
<string>Run server &amp;automatically when calibre starts</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="view_logs">
<property name="text">
<string>View &amp;server logs</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>36</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_13">
<property name="text">
<string>&lt;p&gt;Remember to leave calibre running as the server only runs as long as calibre is running. If you wish to run the server as a standalone entity, please refer to the &lt;a href=&quot;https://manual.calibre-ebook.com/generated/en/calibre-server.html&quot;&gt;command-line documentation&lt;/a&gt;.
&lt;p&gt;To connect to the calibre server from your device you should use a URL of the form &lt;b&gt;http://myhostname:8080&lt;/b&gt;. Here myhostname should be either the fully qualified hostname or the IP address of the computer calibre is running on. If you want to access the server from anywhere in the world, you will have to setup port forwarding for it on your router.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>37</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -51,6 +51,7 @@ from calibre.gui2.job_indicator import Pointer
from calibre.gui2.dbus_export.widgets import factory
from calibre.gui2.open_with import register_keyboard_shortcuts
from calibre.library import current_library_name
from calibre.srv.library_broker import GuiLibraryBroker
class Listener(Thread): # {{{
@ -220,6 +221,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
opts = self.opts
self.preferences_action, self.quit_action = actions
self.library_path = library_path
self.library_broker = GuiLibraryBroker(db)
self.content_server = None
self._spare_pool = None
self.must_restart_before_config = False
@ -626,81 +628,76 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
def booklists(self):
return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db
def library_moved(self, newloc, copy_structure=False, call_close=True,
allow_rebuild=False):
def library_moved(self, newloc, copy_structure=False, allow_rebuild=False):
if newloc is None:
return
default_prefs = None
try:
olddb = self.library_view.model().db
if copy_structure:
default_prefs = olddb.prefs
except:
olddb = None
if copy_structure and olddb is not None and default_prefs is not None:
default_prefs['field_metadata'] = olddb.new_api.field_metadata.all_metadata()
try:
db = LibraryDatabase(newloc, default_prefs=default_prefs)
except apsw.Error:
if not allow_rebuild:
raise
import traceback
repair = question_dialog(self, _('Corrupted database'),
_('The library database at %s appears to be corrupted. Do '
'you want calibre to try and rebuild it automatically? '
'The rebuild may not be completely successful.')
% force_unicode(newloc, filesystem_encoding),
det_msg=traceback.format_exc()
)
if repair:
from calibre.gui2.dialogs.restore_library import repair_library_at
if repair_library_at(newloc, parent=self):
db = LibraryDatabase(newloc, default_prefs=default_prefs)
else:
return
else:
return
if self.content_server is not None:
self.content_server.set_database(db)
self.library_path = newloc
prefs['library_path'] = self.library_path
self.book_on_device(None, reset=True)
db.set_book_on_device_func(self.book_on_device)
self.library_view.set_database(db)
self.tags_view.set_database(db, self.alter_tb)
self.library_view.model().set_book_on_device_func(self.book_on_device)
self.status_bar.clear_message()
self.search.clear()
self.saved_search.clear()
self.book_details.reset_info()
# self.library_view.model().count_changed()
db = self.library_view.model().db
self.iactions['Choose Library'].count_changed(db.count())
self.set_window_title()
self.apply_named_search_restriction('') # reset restriction to null
self.saved_searches_changed(recount=False) # reload the search restrictions combo box
if db.prefs['virtual_lib_on_startup']:
self.apply_virtual_library(db.prefs['virtual_lib_on_startup'])
self.rebuild_vl_tabs()
for action in self.iactions.values():
action.library_changed(db)
if olddb is not None:
with self.library_broker:
default_prefs = None
try:
if call_close:
olddb.close()
olddb = self.library_view.model().db
if copy_structure:
default_prefs = olddb.prefs
except:
import traceback
traceback.print_exc()
olddb.break_cycles()
if self.device_connected:
self.set_books_in_library(self.booklists(), reset=True)
self.refresh_ondevice()
self.memory_view.reset()
self.card_a_view.reset()
self.card_b_view.reset()
self.set_current_library_information(current_library_name(), db.library_id,
db.field_metadata)
self.library_view.set_current_row(0)
olddb = None
if copy_structure and olddb is not None and default_prefs is not None:
default_prefs['field_metadata'] = olddb.new_api.field_metadata.all_metadata()
db = self.library_broker.prepare_for_gui_library_change(newloc)
if db is None:
try:
db = LibraryDatabase(newloc, default_prefs=default_prefs)
except apsw.Error:
if not allow_rebuild:
raise
import traceback
repair = question_dialog(self, _('Corrupted database'),
_('The library database at %s appears to be corrupted. Do '
'you want calibre to try and rebuild it automatically? '
'The rebuild may not be completely successful.')
% force_unicode(newloc, filesystem_encoding),
det_msg=traceback.format_exc()
)
if repair:
from calibre.gui2.dialogs.restore_library import repair_library_at
if repair_library_at(newloc, parent=self):
db = LibraryDatabase(newloc, default_prefs=default_prefs)
else:
return
else:
return
self.library_path = newloc
prefs['library_path'] = self.library_path
self.book_on_device(None, reset=True)
db.set_book_on_device_func(self.book_on_device)
self.library_view.set_database(db)
self.tags_view.set_database(db, self.alter_tb)
self.library_view.model().set_book_on_device_func(self.book_on_device)
self.status_bar.clear_message()
self.search.clear()
self.saved_search.clear()
self.book_details.reset_info()
# self.library_view.model().count_changed()
db = self.library_view.model().db
self.iactions['Choose Library'].count_changed(db.count())
self.set_window_title()
self.apply_named_search_restriction('') # reset restriction to null
self.saved_searches_changed(recount=False) # reload the search restrictions combo box
if db.prefs['virtual_lib_on_startup']:
self.apply_virtual_library(db.prefs['virtual_lib_on_startup'])
self.rebuild_vl_tabs()
for action in self.iactions.values():
action.library_changed(db)
self.library_broker.gui_library_changed(db)
if olddb is not None:
olddb.close(), olddb.break_cycles()
if self.device_connected:
self.set_books_in_library(self.booklists(), reset=True)
self.refresh_ondevice()
self.memory_view.reset()
self.card_a_view.reset()
self.card_b_view.reset()
self.set_current_library_information(current_library_name(), db.library_id,
db.field_metadata)
self.library_view.set_current_row(0)
# Run a garbage collection now so that it does not freeze the
# interface later
gc.collect()

101
src/calibre/srv/embedded.py Normal file
View File

@ -0,0 +1,101 @@
#!/usr/bin/env python2
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
from __future__ import absolute_import, division, print_function, unicode_literals
import os
from threading import Thread
from calibre import as_unicode
from calibre.constants import cache_dir
from calibre.srv.bonjour import BonJour
from calibre.srv.handler import Handler
from calibre.srv.http_response import create_http_handler
from calibre.srv.loop import ServerLoop
from calibre.srv.utils import RotatingLog
def log_paths():
return os.path.join(cache_dir(), 'server-log.txt'), os.path.join(
cache_dir(), 'server-access-log.txt'
)
class Server(object):
loop = current_thread = exception = None
state_callback = start_failure_callback = None
def __init__(self, opts):
lp, lap = log_paths()
log_size = opts.max_log_size * 1024 * 1024
log = RotatingLog(lp, max_size=log_size)
access_log = RotatingLog(lap, max_size=log_size)
self.handler = Handler(libraries, opts)
plugins = self.plugins = []
if opts.use_bonjour:
plugins.append(BonJour())
self.opts = opts
self.log, self.access_log = log, access_log
self.handler.set_log(self.log)
_df = os.environ.get('CALIBRE_DEVELOP_FROM', None)
if _df and os.path.exists(_df):
from calibre.utils.rapydscript import compile_srv
compile_srv()
def start(self):
if self.current_thread is None:
try:
self.loop = ServerLoop(
create_http_handler(self.handler.dispatch),
opts=self.opts,
log=self.log,
access_log=self.access_log,
plugins=self.plugins
)
self.loop.initialize_socket()
except Exception as e:
self.loop = None
if self.start_failure_callback is not None:
try:
self.start_failure_callback(as_unicode(e))
except Exception:
pass
return
self.handler.set_jobs_manager(self.loop.jobs_manager)
self.current_thread = t = Thread(
name='EmbeddedServer', target=self.serve_forever
)
t.daemon = True
t.start()
def serve_forever(self):
if self.state_callback is not None:
try:
self.state_callback(True)
except Exception:
pass
try:
self.loop.serve_forever()
except BaseException as e:
self.exception = e
if self.state_callback is not None:
try:
self.state_callback(False)
except Exception:
pass
def stop(self):
if self.loop is not None:
self.loop.stop()
self.loop = None
def exit(self):
if self.current_thread is not None:
self.stop()
self.current_thread.join()
self.current_thread = None
@property
def is_running(self):
return self.current_thread is not None and self.current_thread.is_alive()

View File

@ -6,79 +6,17 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3'
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
import os, json
from collections import OrderedDict
import json
from importlib import import_module
from threading import Lock
from calibre import force_unicode, filesystem_encoding
from calibre.db.cache import Cache
from calibre.db.legacy import create_backend, LibraryDatabase
from calibre.srv.auth import AuthController
from calibre.srv.routes import Router
from calibre.srv.users import UserManager
from calibre.srv.library_broker import LibraryBroker
from calibre.utils.date import utcnow
def init_library(library_path):
db = Cache(create_backend(library_path))
db.init()
return db
class LibraryBroker(object):
def __init__(self, libraries):
self.lock = Lock()
self.lmap = {}
seen = set()
for i, path in enumerate(os.path.abspath(p) for p in libraries):
if path in seen:
continue
seen.add(path)
if not LibraryDatabase.exists_at(path):
continue
bname = library_id = force_unicode(os.path.basename(path), filesystem_encoding).replace(' ', '_')
c = 0
while library_id in self.lmap:
c += 1
library_id = bname + '%d' % c
if i == 0:
self.default_library = library_id
self.lmap[library_id] = path
self.category_caches = {lid:OrderedDict() for lid in self.lmap}
self.search_caches = {lid:OrderedDict() for lid in self.lmap}
self.tag_browser_caches = {lid:OrderedDict() for lid in self.lmap}
def get(self, library_id=None):
with self.lock:
library_id = library_id or self.default_library
ans = self.lmap.get(library_id)
if ans is None:
return
if not callable(getattr(ans, 'init', None)):
try:
self.lmap[library_id] = ans = init_library(ans)
ans.server_library_id = library_id
except Exception:
self.lmap[library_id] = ans = None
raise
return ans
def close(self):
for db in self.lmap.itervalues():
getattr(db, 'close', lambda : None)()
self.lmap = {}
@property
def library_map(self):
def lpath(x):
if hasattr(x, 'rpartition'):
return x
return x.backend.library_path
return {k:os.path.basename(lpath(v)) for k, v in self.lmap.iteritems()}
class Context(object):
log = None
@ -209,4 +147,3 @@ class Handler(object):
def close(self):
self.router.ctx.library_broker.close()

View File

@ -0,0 +1,180 @@
#!/usr/bin/env python2
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
from __future__ import absolute_import, division, print_function, unicode_literals
import os
from collections import OrderedDict, defaultdict
from threading import Lock
from calibre import filesystem_encoding
from calibre.db.cache import Cache
from calibre.db.legacy import LibraryDatabase, create_backend, set_global_state
from calibre.utils.filenames import samefile
from calibre.utils.monotonic import monotonic
def init_library(library_path, is_default_library):
db = Cache(
create_backend(
library_path, load_user_formatter_functions=is_default_library
)
)
db.init()
return db
def make_library_id_unique(library_id, existing):
bname = library_id
c = 0
while library_id in existing:
c += 1
library_id = bname + ('%d' % c)
return library_id
def canonicalize_path(p):
if isinstance(p, bytes):
p = p.decode(filesystem_encoding)
p = os.path.abspath(p).replace(os.sep, '/').rstrip('/')
return p
def library_id_from_path(path, existing):
library_id = os.path.basename(path).replace(' ', '_')
return make_library_id_unique(library_id, existing)
class LibraryBroker(object):
def __init__(self, libraries):
self.lock = Lock()
self.lmap = {}
seen = set()
for i, path in enumerate(canonicalize_path(p) for p in libraries):
if path in seen:
continue
seen.add(path)
if not LibraryDatabase.exists_at(path):
continue
library_id = library_id_from_path(path, self.lmap)
if i == 0:
self.default_library = library_id
self.lmap[library_id] = path
self.loaded_dbs = {}
self.category_caches, self.search_caches, self.tag_browser_caches = (
defaultdict(OrderedDict), defaultdict(OrderedDict), defaultdict(OrderedDict))
def get(self, library_id=None):
with self:
library_id = library_id or self.default_library
if library_id in self.loaded_dbs:
return self.loaded_dbs[library_id]
path = self.lmap.get(library_id)
if path is None:
return
try:
self.loaded_dbs[library_id] = ans = self.init_library(
path, library_id == self.default_library
)
ans.new_api.server_library_id = library_id
except Exception:
self.loaded_dbs[library_id] = None
raise
return ans
def init_library(self, library_path, is_default_library):
return init_library(library_path, is_default_library)
def close(self):
with self:
for db in self.loaded_dbs.itervalues():
getattr(db, 'close', lambda: None)()
self.lmap, self.loaded_dbs = {}, {}
@property
def library_map(self):
return {k: os.path.basename(v) for k, v in self.lmap.iteritems()}
def __enter__(self):
self.lock.acquire()
def __exit__(self, *a):
self.lock.release()
EXPIRED_AGE = 300 # seconds
class GuiLibraryBroker(LibraryBroker):
def __init__(self, db):
from calibre.gui2 import gprefs
stats = gprefs.get('library_usage_stats', {})
libraries = sorted(stats, key=stats.get, reverse=True)
self.last_used_times = defaultdict(lambda: -EXPIRED_AGE)
self.gui_library_id = None
LibraryBroker.__init__(self, libraries)
self.gui_library_changed(db)
def init_library(self, library_path, is_default_library):
return LibraryDatabase(library_path, is_second_db=True)
def get(self, library_id=None):
try:
return getattr(LibraryBroker.get(self, library_id), 'new_api', None)
finally:
self.last_used_times[library_id or self.default_library] = monotonic()
def prepare_for_gui_library_change(self, newloc):
# Must be called with lock held
for library_id, path in self.lmap.iteritems():
db = self.loaded_dbs.get(library_id)
if db is not None and samefile(newloc, path):
if library_id == self.gui_library_id:
# Have to reload db
self.loaded_dbs.pop(library_id, None)
return
set_global_state(db)
return db
def gui_library_changed(self, db, prune=True):
# Must be called with lock held
newloc = canonicalize_path(db.backend.library_path)
for library_id, path in self.lmap.iteritems():
if samefile(newloc, path):
self.loaded_dbs[library_id] = db
self.gui_library_id = library_id
break
else:
library_id = self.gui_library_id = library_id_from_path(newloc, self.lmap)
self.lmap[library_id] = newloc
self.loaded_dbs[library_id] = db
if prune:
self._prune_loaded_dbs()
def _prune_loaded_dbs(self):
now = monotonic()
for library_id in tuple(self.loaded_dbs):
if library_id != self.gui_library_id and now - self.last_used_times[library_id] > EXPIRED_AGE:
db = self.loaded_dbs.pop(library_id)
db.close()
db.break_cycles()
def prune_loaded_dbs(self):
with self:
self._prune_loaded_dbs()
def remove_library(self, path):
with self:
path = canonicalize_path(path)
for library_id, q in self.lmap.iteritems():
if samefile(path, q):
break
else:
return
self.lmap.pop(library_id, None)
db = self.loaded_dbs.pop(library_id)
if db is not None:
db.close(), db.break_cycles()

View File

@ -387,9 +387,7 @@ class ServerLoop(object):
if not self.socket:
raise socket.error(msg)
def serve_forever(self):
""" Listen for incoming connections. """
def initialize_socket(self):
if self.pre_activated_socket is None:
try:
self.do_bind()
@ -408,6 +406,7 @@ class ServerLoop(object):
self.pre_activated_socket = None
self.setup_socket()
def serve(self):
self.connection_map = {}
self.socket.listen(min(socket.SOMAXCONN, 128))
self.bound_address = ba = self.socket.getsockname()
@ -433,6 +432,11 @@ class ServerLoop(object):
self.log.exception('Error in ServerLoop.tick')
self.shutdown()
def serve_forever(self):
""" Listen for incoming connections. """
self.initialize_socket()
self.serve()
def setup_socket(self):
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)