diff --git a/src/calibre/gui2/preferences/server.py b/src/calibre/gui2/preferences/server.py
index 7dcfa07960..338a2b0d4d 100644
--- a/src/calibre/gui2/preferences/server.py
+++ b/src/calibre/gui2/preferences/server.py
@@ -24,7 +24,7 @@ from calibre.gui2 import (
)
from calibre.gui2.preferences import AbortCommit, ConfigWidgetBase, test_widget
from calibre.srv.code import custom_list_template as default_custom_list_template
-from calibre.srv.embedded import custom_list_template
+from calibre.srv.embedded import custom_list_template, search_the_net_urls
from calibre.srv.library_broker import load_gui_libraries
from calibre.srv.opts import change_settings, options, server_config
from calibre.srv.users import (
@@ -364,7 +364,9 @@ class MainTab(QWidget): # {{{
self.ip_info = QLabel(self)
self.update_ip_info()
from calibre.gui2.ui import get_gui
- get_gui().iactions['Connect Share'].share_conn_menu.server_state_changed_signal.connect(self.update_ip_info)
+ gui = get_gui()
+ if gui is not None:
+ gui.iactions['Connect Share'].share_conn_menu.server_state_changed_signal.connect(self.update_ip_info)
l.addSpacing(10)
l.addWidget(self.ip_info)
if set_run_at_startup is not None:
@@ -394,9 +396,11 @@ class MainTab(QWidget): # {{{
def update_ip_info(self):
from calibre.gui2.ui import get_gui
- t = get_gui().iactions['Connect Share'].share_conn_menu.ip_text
- t = t.strip().strip('[]')
- self.ip_info.setText(_('Content server listening at: %s') % t)
+ gui = get_gui()
+ if gui is not None:
+ t = get_gui().iactions['Connect Share'].share_conn_menu.ip_text
+ t = t.strip().strip('[]')
+ self.ip_info.setText(_('Content server listening at: %s') % t)
def genesis(self):
opts = server_config()
@@ -428,12 +432,13 @@ class MainTab(QWidget): # {{{
def update_button_state(self):
from calibre.gui2.ui import get_gui
gui = get_gui()
- is_running = gui.content_server is not None and gui.content_server.is_running
- self.ip_info.setVisible(is_running)
- self.update_ip_info()
- self.start_server_button.setEnabled(not is_running)
- self.stop_server_button.setEnabled(is_running)
- self.test_server_button.setEnabled(is_running)
+ if gui is not None:
+ is_running = gui.content_server is not None and gui.content_server.is_running
+ self.ip_info.setVisible(is_running)
+ self.update_ip_info()
+ self.start_server_button.setEnabled(not is_running)
+ self.stop_server_button.setEnabled(is_running)
+ self.test_server_button.setEnabled(is_running)
@property
def settings(self):
@@ -1019,6 +1024,191 @@ class CustomList(QWidget): # {{{
# }}}
+# Search the internet {{{
+
+class URLItem(QWidget):
+
+ changed_signal = pyqtSignal()
+
+ def __init__(self, as_dict, parent=None):
+ QWidget.__init__(self, parent)
+ self.changed_signal.connect(parent.changed_signal)
+ self.l = l = QFormLayout(self)
+ self.type_widget = t = QComboBox(self)
+ l.setFieldGrowthPolicy(l.ExpandingFieldsGrow)
+ t.addItems([_('Book'), _('Author')])
+ l.addRow(_('URL type:'), t)
+ self.name_widget = n = QLineEdit(self)
+ n.setClearButtonEnabled(True)
+ l.addRow(_('Name:'), n)
+ self.url_widget = w = QLineEdit(self)
+ w.setClearButtonEnabled(True)
+ l.addRow(_('URL:'), w)
+ if as_dict:
+ self.name = as_dict['name']
+ self.url = as_dict['url']
+ self.url_type = as_dict['type']
+ self.type_widget.currentIndexChanged.connect(self.changed_signal)
+ self.name_widget.textChanged.connect(self.changed_signal)
+ self.url_widget.textChanged.connect(self.changed_signal)
+
+ @property
+ def is_empty(self):
+ return not self.name or not self.url
+
+ @property
+ def url_type(self):
+ return 'book' if self.type_widget.currentIndex() == 0 else 'author'
+
+ @url_type.setter
+ def url_type(self, val):
+ self.type_widget.setCurrentIndex(1 if val == 'author' else 0)
+
+ @property
+ def name(self):
+ return self.name_widget.text().strip()
+
+ @name.setter
+ def name(self, val):
+ self.name_widget.setText((val or '').strip())
+
+ @property
+ def url(self):
+ return self.url_widget.text().strip()
+
+ @url.setter
+ def url(self, val):
+ self.url_widget.setText((val or '').strip())
+
+ @property
+ def as_dict(self):
+ return {'name': self.name, 'url': self.url, 'type': self.url_type}
+
+ def validate(self):
+ if self.is_empty:
+ return True
+ if '{author}' not in self.url:
+ error_dialog(self.parent(), _('Missing author placeholder'), _(
+ 'The URL {0} does not contain the {1} placeholder').format(self.url, '{author}'), show=True)
+ return False
+ if self.url_type == 'book' and '{title}' not in self.url:
+ error_dialog(self.parent(), _('Missing title placeholder'), _(
+ 'The URL {0} does not contain the {1} placeholder').format(self.url, '{title}'), show=True)
+ return False
+ return True
+
+
+class SearchTheInternet(QWidget):
+
+ changed_signal = pyqtSignal()
+
+ def __init__(self, parent):
+ QWidget.__init__(self, parent)
+ self.sa = QScrollArea(self)
+ self.lw = QWidget(self)
+ self.l = QVBoxLayout(self.lw)
+ self.sa.setWidget(self.lw), self.sa.setWidgetResizable(True)
+ self.gl = gl = QVBoxLayout(self)
+ self.la = QLabel(_(
+ 'Add new locations to search for books or authors using the "Search the internet" feature'
+ ' of the Content server. The URLs should contain {author} which will be'
+ ' replaced by the author name and, for book URLs, {title} which will'
+ ' be replaced by the book title.'))
+ self.la.setWordWrap(True)
+ gl.addWidget(self.la)
+
+ self.h = QHBoxLayout()
+ gl.addLayout(self.h)
+ self.add_url_button = b = QPushButton(QIcon(I('plus.png')), _('&Add URL'))
+ b.clicked.connect(self.add_url)
+ self.h.addWidget(b)
+ self.export_button = b = QPushButton(_('Export URLs'))
+ b.clicked.connect(self.export_urls)
+ self.h.addWidget(b)
+ self.import_button = b = QPushButton(_('Import URLs'))
+ b.clicked.connect(self.import_urls)
+ self.h.addWidget(b)
+ self.clear_button = b = QPushButton(_('Clear'))
+ b.clicked.connect(self.clear)
+ self.h.addWidget(b)
+
+ self.h.addStretch(10)
+ gl.addWidget(self.sa, stretch=10)
+ self.items = []
+
+ def genesis(self):
+ self.current_urls = search_the_net_urls() or []
+
+ @property
+ def current_urls(self):
+ return [item.as_dict for item in self.items if not item.is_empty]
+
+ def append_item(self, item_as_dict):
+ self.items.append(URLItem(item_as_dict, self))
+ self.l.addWidget(self.items[-1])
+
+ def clear(self):
+ [(self.l.removeWidget(w), w.setParent(None), w.deleteLater()) for w in self.items]
+ self.items = []
+ self.changed_signal.emit()
+
+ @current_urls.setter
+ def current_urls(self, val):
+ self.clear()
+ for entry in val:
+ self.append_item(entry)
+
+ def add_url(self):
+ self.items.append(URLItem(None, self))
+ self.l.addWidget(self.items[-1])
+ QTimer.singleShot(100, self.scroll_to_bottom)
+
+ def scroll_to_bottom(self):
+ sb = self.sa.verticalScrollBar()
+ if sb:
+ sb.setValue(sb.maximum())
+ self.items[-1].name_widget.setFocus(Qt.OtherFocusReason)
+
+ @property
+ def serialized_urls(self):
+ return json.dumps(self.current_urls, indent=2)
+
+ def commit(self):
+ for item in self.items:
+ if not item.validate():
+ return False
+ cu = self.current_urls
+ if cu:
+ with lopen(search_the_net_urls.path, 'wb') as f:
+ f.write(self.serialized_urls)
+ else:
+ try:
+ os.remove(search_the_net_urls.path)
+ except EnvironmentError as err:
+ if err.errno != errno.ENOENT:
+ raise
+ return True
+
+ def export_urls(self):
+ path = choose_save_file(
+ self, 'search-net-urls', _('Choose URLs file'),
+ filters=[(_('URL files'), ['json'])], initial_filename='search-urls.json')
+ if path:
+ with lopen(path, 'wb') as f:
+ f.write(self.serialized_urls)
+
+ def import_urls(self):
+ paths = choose_files(self, 'search-net-urls', _('Choose URLs file'),
+ filters=[(_('URL files'), ['json'])], all_files=False, select_only_single_file=True)
+ if paths:
+ with lopen(paths[0], 'rb') as f:
+ items = json.loads(f.read())
+ [self.append_item(x) for x in items]
+ self.changed_signal.emit()
+
+# }}}
+
+
class ConfigWidget(ConfigWidgetBase):
def __init__(self, *args, **kw):
@@ -1044,6 +1234,9 @@ class ConfigWidget(ConfigWidgetBase):
sa = QScrollArea(self)
sa.setWidget(clt), sa.setWidgetResizable(True)
t.addTab(sa, _('Book &list template'))
+ self.search_net_tab = SearchTheInternet(self)
+ t.addTab(self.search_net_tab, _('&Search the internet'))
+
for tab in self.tabs:
if hasattr(tab, 'changed_signal'):
tab.changed_signal.connect(self.changed_signal.emit)
@@ -1201,6 +1394,8 @@ class ConfigWidget(ConfigWidgetBase):
return False
if not self.custom_list_tab.commit():
return False
+ if not self.search_net_tab.commit():
+ return False
ConfigWidgetBase.commit(self)
change_settings(**settings)
UserManager().user_data = users
@@ -1222,6 +1417,7 @@ class ConfigWidget(ConfigWidgetBase):
if self.server:
self.server.user_manager.refresh()
self.server.ctx.custom_list_template = custom_list_template()
+ self.server.ctx.search_the_net_urls = search_the_net_urls()
if __name__ == '__main__':
diff --git a/src/calibre/srv/code.py b/src/calibre/srv/code.py
index 7c10db898a..d7cfe47219 100644
--- a/src/calibre/srv/code.py
+++ b/src/calibre/srv/code.py
@@ -148,6 +148,7 @@ def basic_interface_data(ctx, rd):
'icon_map': icon_map(),
'icon_path': ctx.url_for('/icon', which=''),
'custom_list_template': getattr(ctx, 'custom_list_template', None) or custom_list_template(),
+ 'search_the_net_urls': getattr(ctx, 'search_the_net_urls', None) or [],
'num_per_page': rd.opts.num_per_page,
}
ans['library_map'], ans['default_library_id'] = ctx.library_info(rd)
diff --git a/src/calibre/srv/embedded.py b/src/calibre/srv/embedded.py
index 2c75b9008c..b76e07ce5e 100644
--- a/src/calibre/srv/embedded.py
+++ b/src/calibre/srv/embedded.py
@@ -24,9 +24,9 @@ def log_paths():
)
-def custom_list_template():
+def read_json(path):
try:
- with lopen(custom_list_template.path, 'rb') as f:
+ with lopen(path, 'rb') as f:
raw = f.read()
except EnvironmentError as err:
if err.errno != errno.ENOENT:
@@ -35,7 +35,16 @@ def custom_list_template():
return json.loads(raw)
+def custom_list_template():
+ return read_json(custom_list_template.path)
+
+
+def search_the_net_urls():
+ return read_json(search_the_net_urls.path)
+
+
custom_list_template.path = os.path.join(config_dir, 'server-custom-list-template.json')
+search_the_net_urls.path = os.path.join(config_dir, 'server-search-the-net.json')
class Server(object):
@@ -62,6 +71,7 @@ class Server(object):
self.log, self.access_log = log, access_log
self.handler.set_log(self.log)
self.handler.router.ctx.custom_list_template = custom_list_template()
+ self.handler.router.ctx.search_the_net_urls = search_the_net_urls()
@property
def ctx(self):
diff --git a/src/calibre/srv/standalone.py b/src/calibre/srv/standalone.py
index 87b67fca5f..9f585510cc 100644
--- a/src/calibre/srv/standalone.py
+++ b/src/calibre/srv/standalone.py
@@ -67,8 +67,11 @@ class Server(object):
access_log = RotatingLog(opts.access_log, max_size=log_size)
self.handler = Handler(libraries, opts)
if opts.custom_list_template:
- with lopen(opts.custom_list_template, 'rb') as f:
+ with lopen(os.path.expanduser(opts.custom_list_template), 'rb') as f:
self.handler.router.ctx.custom_list_template = json.load(f)
+ if opts.search_the_net_urls:
+ with lopen(os.path.expanduser(opts.search_the_net_urls), 'rb') as f:
+ self.handler.router.ctx.search_the_net_urls = json.load(f)
plugins = []
if opts.use_bonjour:
plugins.append(BonJour())
@@ -117,6 +120,14 @@ libraries that the main calibre program knows about will be used.
' Sharing over the net-> Book list template in calibre, create the'
' template and export it.'
))
+ parser.add_option(
+ '--search-the-net-urls', help=_(
+ 'Path to a JSON file containing URLs for the "Search the internet" feature.'
+ ' The easiest way to create such a file is to go to Preferences->'
+ ' Sharing over the net->Search the internet in calibre, create the'
+ ' URLs and export them.'
+ ))
+
if not iswindows and not isosx:
# Does not work on macOS because if we fork() we cannot connect to Core
# Serives which is needed by the QApplication() constructor, which in
diff --git a/src/pyj/book_list/book_details.pyj b/src/pyj/book_list/book_details.pyj
index 72348a8c9f..ec63f7e455 100644
--- a/src/pyj/book_list/book_details.pyj
+++ b/src/pyj/book_list/book_details.pyj
@@ -665,25 +665,38 @@ def search_internet(container_id):
create_top_bar(container, title=_('Search the internet'), action=back, icon='close')
mi = book_metadata(render_book.book_id)
data = {'title':mi.title, 'author':mi.authors[0] if mi.authors else _('Unknown')}
+ interface_data = get_interface_data()
def link_for(name, template):
return E.a(name, class_='simple-link', href=url_for(template, data), target="_blank")
+ author_links = E.ul()
+ book_links = E.ul()
+
+ if interface_data.search_the_net_urls:
+ for entry in interface_data.search_the_net_urls:
+ links = book_links if entry.type is 'book' else author_links
+ links.appendChild(E.li(link_for(entry.name, entry.url)))
+ for name, url in [
+ (_('Goodreads'), 'https://www.goodreads.com/book/author/{author}'),
+ (_('Wikipedia'), 'https://en.wikipedia.org/w/index.php?search={author}'),
+ (_('Google books'), 'https://www.google.com/search?tbm=bks&q=inauthor:%22{author}%22'),
+ ]:
+ author_links.appendChild(E.li(link_for(name, url)))
+
+ for name, url in [
+ (_('Goodreads'), 'https://www.goodreads.com/search?q={author}+{title}&search%5Bsource%5D=goodreads&search_type=books&tab=books'),
+ (_('Google books'), 'https://www.google.com/search?tbm=bks&q=inauthor:%22{author}%22+intitle:%22{title}%22'),
+ (_('Amazon'), 'https://www.amazon.com/s/ref=nb_sb_noss?url=search-alias%3Dstripbooks&field-keywords={author}+{title}'),
+ ]:
+ book_links.appendChild(E.li(link_for(name, url)))
+
container.appendChild(E.div(class_=SEARCH_INTERNET_CLASS,
safe_set_inner_html(E.h2(), _('Search for the author {} at:').format(data.author)),
- E.ul(
- E.li(link_for(_('Goodreads'), 'https://www.goodreads.com/book/author/{author}')),
- E.li(link_for(_('Wikipedia'), 'https://en.wikipedia.org/w/index.php?search={author}')),
- E.li(link_for(_('Google books'), 'https://www.google.com/search?tbm=bks&q=inauthor:%22{author}%22')),
- ),
+ author_links,
E.hr(),
safe_set_inner_html(E.h2(), _('Search for the book {} at:').format(data.title)),
- E.ul(
- E.li(link_for(_('Goodreads'), 'https://www.goodreads.com/search?q={author}+{title}&search%5Bsource%5D=goodreads&search_type=books&tab=books')),
- E.li(link_for(_('Google books'), 'https://www.google.com/search?tbm=bks&q=inauthor:%22{author}%22+intitle:%22{title}%22')),
- E.li(link_for(_('Amazon'), 'https://www.amazon.com/s/ref=nb_sb_noss?url=search-alias%3Dstripbooks&field-keywords={author}+{title}')),
- ),
-
+ book_links,
))
diff --git a/src/pyj/session.pyj b/src/pyj/session.pyj
index 5870d87cae..cc8c8cbd68 100644
--- a/src/pyj/session.pyj
+++ b/src/pyj/session.pyj
@@ -169,6 +169,7 @@ default_interface_data = {
'use_roman_numerals_for_series_number': True,
'default_library_id': None,
'library_map': None,
+ 'search_the_net_urls': [],
'icon_map': {},
'icon_path': '',
'custom_list_template': None,