mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Content server: Allow specifying custom URLs for the "Search the internet" feature via Preferences->Sharing over the net->Search the internet. Fixes #1810923 [[Enhancement]: Search the internet on Content Server](https://bugs.launchpad.net/calibre/+bug/1810923)
This commit is contained in:
parent
30e25ecf21
commit
24a9ebd3af
@ -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,6 +396,8 @@ class MainTab(QWidget): # {{{
|
||||
|
||||
def update_ip_info(self):
|
||||
from calibre.gui2.ui import get_gui
|
||||
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)
|
||||
@ -428,6 +432,7 @@ class MainTab(QWidget): # {{{
|
||||
def update_button_state(self):
|
||||
from calibre.gui2.ui import get_gui
|
||||
gui = get_gui()
|
||||
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()
|
||||
@ -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__':
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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 <i>{}</i> 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 <i>{}</i> 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,
|
||||
))
|
||||
|
||||
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user