Book details panel: Make clicking author names search Goodreads for the author, by default. Can be changed in Preferences->Look & Feel->Book details

Fixes #653 (Switch author link to Goodreads.)
This commit is contained in:
Kovid Goyal 2017-05-08 16:28:03 +05:30
parent 8d1b8ea795
commit 8bb80c764b
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
6 changed files with 139 additions and 57 deletions

View File

@ -8,6 +8,7 @@ __copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
import os, cPickle import os, cPickle
from functools import partial from functools import partial
from urllib import quote_plus
from binascii import hexlify from binascii import hexlify
from calibre import prepare_string_for_xml, force_unicode from calibre import prepare_string_for_xml, force_unicode
@ -23,6 +24,12 @@ from calibre.utils.localization import calibre_langcode_to_name
default_sort = ('title', 'title_sort', 'authors', 'author_sort', 'series', 'rating', 'pubdate', 'tags', 'publisher', 'identifiers') default_sort = ('title', 'title_sort', 'authors', 'author_sort', 'series', 'rating', 'pubdate', 'tags', 'publisher', 'identifiers')
def qquote(val):
if not isinstance(val, bytes):
val = val.encode('utf-8')
return quote_plus(val).decode('utf-8')
def field_sort(mi, name): def field_sort(mi, name):
try: try:
title = mi.metadata_for_field(name)['name'] title = mi.metadata_for_field(name)['name']
@ -55,6 +62,34 @@ def search_href(search_term, value):
return prepare_string_for_xml('search:' + hexlify(search.encode('utf-8')), True) return prepare_string_for_xml('search:' + hexlify(search.encode('utf-8')), True)
DEFAULT_AUTHOR_LINK = 'search-goodreads'
def author_search_href(which, title=None, author=None):
if which == 'calibre':
return search_href('authors', author), _('Search the calibre library for books by %s') % author
tt_map = getattr(author_search_href, 'tt_map', None)
if tt_map is None:
tt = _('Search {0} for the author: {1}')
tt_map = author_search_href.tt_map = {
'goodreads': tt.format('Goodreads', author),
'wikipedia': tt.format('Wikipedia', author),
'goodreads-book': _('Search Goodreads for the book: {0} by the author {1}').format(title, author)
}
tt = tt_map.get(which)
if tt is None:
which = DEFAULT_AUTHOR_LINK.partition('-')[2]
tt = tt_map[which]
link_map = getattr(author_search_href, 'link_map', None)
if link_map is None:
link_map = author_search_href.link_map = {
'goodreads': 'https://www.goodreads.com/search?q={author}&search%5Bfield%5D=author&search%5Bsource%5D=goodreads&search_type=people&tab=people',
'wikipedia': 'https://en.wikipedia.org/w/index.php?search={author}',
'goodreads-book': 'https://www.goodreads.com/search?q={author}+{title}&search%5Bsource%5D=goodreads&search_type=books&tab=books'
}
return link_map[which].format(title=qquote(title), author=qquote(author)), tt
def item_data(field_name, value, book_id): def item_data(field_name, value, book_id):
return hexlify(cPickle.dumps((field_name, value, book_id), -1)) return hexlify(cPickle.dumps((field_name, value, book_id), -1))
@ -175,27 +210,29 @@ def mi_to_html(mi, field_list=None, default_author_link=None, use_roman_numbers=
links = u', '.join(links) links = u', '.join(links)
if links: if links:
ans.append((field, row % (_('Ids')+':', links))) ans.append((field, row % (_('Ids')+':', links)))
elif field == 'authors' and not isdevice: elif field == 'authors':
authors = [] authors = []
formatter = EvalFormatter() formatter = EvalFormatter()
for aut in mi.authors: for aut in mi.authors:
link = '' link = ''
if mi.author_link_map[aut]: if mi.author_link_map.get(aut):
link = lt = mi.author_link_map[aut] link = lt = mi.author_link_map[aut]
elif default_author_link: elif default_author_link:
if default_author_link == 'search-calibre': if isdevice and default_author_link == 'search-calibre':
link = search_href('authors', aut) default_author_link = DEFAULT_AUTHOR_LINK
lt = a(_('Search the calibre library for books by %s') % aut) if default_author_link.startswith('search-'):
which_src = default_author_link.partition('-')[2]
link, lt = author_search_href(which_src, title=mi.title, author=aut)
else: else:
vals = {'author': aut.replace(' ', '+')} vals = {'author': qquote(aut), 'title': qquote(mi.title)}
try: try:
vals['author_sort'] = mi.author_sort_map[aut].replace(' ', '+') vals['author_sort'] = qquote(mi.author_sort_map[aut])
except: except KeyError:
vals['author_sort'] = aut.replace(' ', '+') vals['author_sort'] = qquote(aut)
link = lt = a(formatter.safe_format(default_author_link, vals, '', vals)) link = lt = formatter.safe_format(default_author_link, vals, '', vals)
aut = p(aut) aut = p(aut)
if link: if link:
authors.append(u'<a calibre-data="authors" title="%s" href="%s">%s</a>'%(lt, link, aut)) authors.append(u'<a calibre-data="authors" title="%s" href="%s">%s</a>'%(a(lt), a(link), aut))
else: else:
authors.append(aut) authors.append(aut)
ans.append((field, row % (name, u' & '.join(authors)))) ans.append((field, row % (name, u' & '.join(authors))))

View File

@ -108,7 +108,6 @@ def create_defs():
defs['tags_browser_collapse_at'] = 100 defs['tags_browser_collapse_at'] = 100
defs['tag_browser_dont_collapse'] = [] defs['tag_browser_dont_collapse'] = []
defs['edit_metadata_single_layout'] = 'default' defs['edit_metadata_single_layout'] = 'default'
defs['default_author_link'] = 'https://en.wikipedia.org/w/index.php?search={author}'
defs['preserve_date_on_ctl'] = True defs['preserve_date_on_ctl'] = True
defs['manual_add_auto_convert'] = False defs['manual_add_auto_convert'] = False
defs['auto_convert_same_fmt'] = False defs['auto_convert_same_fmt'] = False
@ -273,6 +272,15 @@ QSettings.setPath(QSettings.IniFormat, QSettings.SystemScope, config_dir)
QSettings.setDefaultFormat(QSettings.IniFormat) QSettings.setDefaultFormat(QSettings.IniFormat)
def default_author_link():
from calibre.ebooks.metadata.book.render import DEFAULT_AUTHOR_LINK
ans = gprefs.get('default_author_link')
if ans == 'https://en.wikipedia.org/w/index.php?search={author}':
# The old default value for this setting
ans = DEFAULT_AUTHOR_LINK
return ans or DEFAULT_AUTHOR_LINK
def available_heights(): def available_heights():
desktop = QCoreApplication.instance().desktop() desktop = QCoreApplication.instance().desktop()
return map(lambda x: x.height(), map(desktop.availableGeometry, range(desktop.screenCount()))) return map(lambda x: x.height(), map(desktop.availableGeometry, range(desktop.screenCount())))

View File

@ -21,7 +21,7 @@ from calibre.gui2.dnd import (dnd_has_image, dnd_get_image, dnd_get_files,
from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.metadata.book.base import (field_metadata, Metadata) from calibre.ebooks.metadata.book.base import (field_metadata, Metadata)
from calibre.ebooks.metadata.book.render import mi_to_html from calibre.ebooks.metadata.book.render import mi_to_html
from calibre.gui2 import (config, open_url, pixmap_to_data, gprefs, rating_font, NO_URL_FORMATTING) from calibre.gui2 import (config, open_url, pixmap_to_data, gprefs, rating_font, NO_URL_FORMATTING, default_author_link)
from calibre.utils.config import tweaks from calibre.utils.config import tweaks
from calibre.utils.img import image_from_x, blend_image from calibre.utils.img import image_from_x, blend_image
from calibre.utils.localization import is_rtl from calibre.utils.localization import is_rtl
@ -127,7 +127,7 @@ def render_data(mi, use_roman_numbers=True, all_fields=False):
field_list = get_field_list(getattr(mi, 'field_metadata', field_metadata)) field_list = get_field_list(getattr(mi, 'field_metadata', field_metadata))
field_list = [(x, all_fields or display) for x, display in field_list] field_list = [(x, all_fields or display) for x, display in field_list]
return mi_to_html(mi, field_list=field_list, use_roman_numbers=use_roman_numbers, rtl=is_rtl(), return mi_to_html(mi, field_list=field_list, use_roman_numbers=use_roman_numbers, rtl=is_rtl(),
rating_font=rating_font(), default_author_link=gprefs.get('default_author_link')) rating_font=rating_font(), default_author_link=default_author_link())
# }}} # }}}

View File

@ -15,11 +15,13 @@ from PyQt5.Qt import (
QApplication, QFont, QFontInfo, QFontDialog, QColorDialog, QPainter, QApplication, QFont, QFontInfo, QFontDialog, QColorDialog, QPainter,
QAbstractListModel, Qt, QIcon, QKeySequence, QColor, pyqtSignal, QCursor, QAbstractListModel, Qt, QIcon, QKeySequence, QColor, pyqtSignal, QCursor,
QWidget, QSizePolicy, QBrush, QPixmap, QSize, QPushButton, QVBoxLayout, QWidget, QSizePolicy, QBrush, QPixmap, QSize, QPushButton, QVBoxLayout,
QTableWidget, QTableWidgetItem, QLabel, QFormLayout, QLineEdit QTableWidget, QTableWidgetItem, QLabel, QFormLayout, QLineEdit, QComboBox
) )
from calibre import human_readable from calibre import human_readable
from calibre.ebooks.metadata.book.render import DEFAULT_AUTHOR_LINK
from calibre.ebooks.metadata.sources.prefs import msprefs from calibre.ebooks.metadata.sources.prefs import msprefs
from calibre.gui2 import default_author_link
from calibre.gui2.dialogs.template_dialog import TemplateDialog from calibre.gui2.dialogs.template_dialog import TemplateDialog
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, CommaSeparatedList from calibre.gui2.preferences import ConfigWidgetBase, test_widget, CommaSeparatedList
from calibre.gui2.preferences.look_feel_ui import Ui_Form from calibre.gui2.preferences.look_feel_ui import Ui_Form
@ -42,6 +44,56 @@ class BusyCursor(object):
def __exit__(self, *args): def __exit__(self, *args):
QApplication.restoreOverrideCursor() QApplication.restoreOverrideCursor()
class DefaultAuthorLink(QWidget): # {{{
changed_signal = pyqtSignal()
def __init__(self, parent):
QWidget.__init__(self, parent)
l = QVBoxLayout(parent)
l.addWidget(self)
l.setContentsMargins(0, 0, 0, 0)
l = QFormLayout(self)
l.setContentsMargins(0, 0, 0, 0)
l.setFieldGrowthPolicy(l.AllNonFixedFieldsGrow)
self.choices = c = QComboBox()
c.setMinimumContentsLength(30)
for text, data in [
(_('Search for the author on Goodreads'), 'search-goodreads'),
(_('Search for the author in your calibre library'), 'search-calibre'),
(_('Search for the author on Wikipedia'), 'search-wikipedia'),
(_('Search for the book on Goodreads'), 'search-goodreads-book'),
(_('Use a custom search URL'), 'url'),
]:
c.addItem(text, data)
l.addRow(_('Clicking on &author names should:'), c)
self.custom_url = u = QLineEdit(self)
c.currentIndexChanged.connect(self.current_changed)
l.addRow(u)
self.current_changed()
c.currentIndexChanged.connect(self.changed_signal)
@property
def value(self):
k = self.choices.currentData()
if k == 'url':
return self.custom_url.text()
return k if k != DEFAULT_AUTHOR_LINK else None
@value.setter
def value(self, val):
i = self.choices.findData(val)
if i < 0:
i = self.choices.findData('url')
self.custom_url.setText(val)
self.choices.setCurrentIndex(i)
def current_changed(self):
k = self.choices.currentData()
self.custom_url.setVisible(k == 'url')
# }}}
# IdLinksEditor {{{ # IdLinksEditor {{{
@ -280,6 +332,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.icon_theme.setText(_('Icon theme: <b>%s</b>') % self.icon_theme_title) self.icon_theme.setText(_('Icon theme: <b>%s</b>') % self.icon_theme_title)
self.commit_icon_theme = None self.commit_icon_theme = None
self.icon_theme_button.clicked.connect(self.choose_icon_theme) self.icon_theme_button.clicked.connect(self.choose_icon_theme)
self.default_author_link = DefaultAuthorLink(self.default_author_link_container)
self.default_author_link.changed_signal.connect(self.changed_signal)
r('gui_layout', config, restart_required=True, choices=[(_('Wide'), 'wide'), (_('Narrow'), 'narrow')]) r('gui_layout', config, restart_required=True, choices=[(_('Wide'), 'wide'), (_('Narrow'), 'narrow')])
r('ui_style', gprefs, restart_required=True, choices=[(_('System default'), 'system'), (_('calibre style'), r('ui_style', gprefs, restart_required=True, choices=[(_('System default'), 'system'), (_('calibre style'),
'calibre')]) 'calibre')])
@ -351,12 +405,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
(_('Partitioned'), 'partition')] (_('Partitioned'), 'partition')]
r('tags_browser_partition_method', gprefs, choices=choices) r('tags_browser_partition_method', gprefs, choices=choices)
r('tags_browser_collapse_at', gprefs) r('tags_browser_collapse_at', gprefs)
r('default_author_link', gprefs)
r('tag_browser_dont_collapse', gprefs, setting=CommaSeparatedList) r('tag_browser_dont_collapse', gprefs, setting=CommaSeparatedList)
self.search_library_for_author_button.clicked.connect(
lambda : self.opt_default_author_link.setText('search-calibre'))
choices = set([k for k in db.field_metadata.all_field_keys() choices = set([k for k in db.field_metadata.all_field_keys()
if (db.field_metadata[k]['is_category'] and if (db.field_metadata[k]['is_category'] and
(db.field_metadata[k]['datatype'] in ['text', 'series', 'enumeration']) and (db.field_metadata[k]['datatype'] in ['text', 'series', 'enumeration']) and
@ -483,6 +533,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
def initialize(self): def initialize(self):
ConfigWidgetBase.initialize(self) ConfigWidgetBase.initialize(self)
self.default_author_link.value = default_author_link()
font = gprefs['font'] font = gprefs['font']
if font is not None: if font is not None:
font = list(font) font = list(font)
@ -536,6 +587,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
def restore_defaults(self): def restore_defaults(self):
ConfigWidgetBase.restore_defaults(self) ConfigWidgetBase.restore_defaults(self)
self.default_author_link.value = DEFAULT_AUTHOR_LINK
ofont = self.current_font ofont = self.current_font
self.current_font = None self.current_font = None
if ofont is not None: if ofont is not None:
@ -637,6 +689,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
if self.commit_icon_theme is not None: if self.commit_icon_theme is not None:
self.commit_icon_theme() self.commit_icon_theme()
rr = True rr = True
gprefs['default_author_link'] = self.default_author_link.value
return rr return rr
def refresh_gui(self, gui): def refresh_gui(self, gui):

View File

@ -269,7 +269,7 @@
<item row="0" column="0" colspan="2"> <item row="0" column="0" colspan="2">
<widget class="QLabel" name="label_19"> <widget class="QLabel" name="label_19">
<property name="text"> <property name="text">
<string>Control the Cover grid view. You can enable this view by clicking the "Cover grid" button in the bottom right corner of the main calibre window.</string> <string>Control the Cover grid view. You can enable this view by clicking the &quot;Cover grid&quot; button in the bottom right corner of the main calibre window.</string>
</property> </property>
<property name="wordWrap"> <property name="wordWrap">
<bool>true</bool> <bool>true</bool>
@ -773,41 +773,6 @@ A value of zero means calculate automatically.</string>
</item> </item>
</layout> </layout>
</item> </item>
<item row="0" column="0" colspan="2">
<layout class="QHBoxLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Default author &amp;link template:</string>
</property>
<property name="buddy">
<cstring>opt_default_author_link</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="opt_default_author_link">
<property name="toolTip">
<string>&lt;p&gt;Enter a template to be used to create a link for
an author in the books information dialog. This template will
be used when no link has been provided for the author using
Manage Authors. You can use the values {author} and
{author_sort}, and any template function.</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="search_library_for_author_button">
<property name="toolTip">
<string>Search the calibre library for the author, instead of opening a link</string>
</property>
<property name="text">
<string>Search &amp;calibre library for author</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0" colspan="2"> <item row="2" column="0" colspan="2">
<widget class="QPushButton" name="id_links_button"> <widget class="QPushButton" name="id_links_button">
<property name="text"> <property name="text">
@ -815,6 +780,16 @@ Manage Authors. You can use the values {author} and
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="0" colspan="2">
<widget class="QWidget" name="default_author_link_container" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="tab_2"> <widget class="QWidget" name="tab_2">

View File

@ -14,7 +14,7 @@ from PyQt5.Qt import (
QRegExpValidator, QRegExp, QPalette, QColor, QBrush, QPainter, QRegExpValidator, QRegExp, QPalette, QColor, QBrush, QPainter,
QDockWidget, QSize, QWebView, QLabel, QVBoxLayout) QDockWidget, QSize, QWebView, QLabel, QVBoxLayout)
from calibre.gui2 import rating_font, error_dialog from calibre.gui2 import rating_font, error_dialog, open_url
from calibre.gui2.main_window import MainWindow from calibre.gui2.main_window import MainWindow
from calibre.gui2.search_box import SearchBox2 from calibre.gui2.search_box import SearchBox2
from calibre.gui2.viewer.documentview import DocumentView from calibre.gui2.viewer.documentview import DocumentView
@ -74,21 +74,30 @@ class Metadata(QWebView): # {{{
s = self.settings() s = self.settings()
s.setAttribute(s.JavascriptEnabled, False) s.setAttribute(s.JavascriptEnabled, False)
self.page().setLinkDelegationPolicy(self.page().DelegateAllLinks) self.page().setLinkDelegationPolicy(self.page().DelegateAllLinks)
self.page().linkClicked.connect(self.link_clicked)
self.setAttribute(Qt.WA_OpaquePaintEvent, False) self.setAttribute(Qt.WA_OpaquePaintEvent, False)
palette = self.palette() palette = self.palette()
palette.setBrush(QPalette.Base, Qt.transparent) palette.setBrush(QPalette.Base, Qt.transparent)
self.page().setPalette(palette) self.page().setPalette(palette)
self.setVisible(False) self.setVisible(False)
def link_clicked(self, qurl):
if qurl.scheme() in ('http', 'https'):
return open_url(qurl)
def update_layout(self): def update_layout(self):
self.setGeometry(0, 0, self.parent().width(), self.parent().height()) self.setGeometry(0, 0, self.parent().width(), self.parent().height())
def show_metadata(self, mi, ext=''): def show_metadata(self, mi, ext=''):
from calibre.gui2 import default_author_link
from calibre.gui2.book_details import render_html, css from calibre.gui2.book_details import render_html, css
from calibre.ebooks.metadata.book.render import mi_to_html from calibre.ebooks.metadata.book.render import mi_to_html
def render_data(mi, use_roman_numbers=True, all_fields=False): def render_data(mi, use_roman_numbers=True, all_fields=False):
return mi_to_html(mi, use_roman_numbers=use_roman_numbers, rating_font=rating_font(), rtl=is_rtl()) return mi_to_html(
mi, use_roman_numbers=use_roman_numbers, rating_font=rating_font(), rtl=is_rtl(),
default_author_link=default_author_link()
)
html = render_html(mi, css(), True, self, render_data_func=render_data) html = render_html(mi, css(), True, self, render_data_func=render_data)
self.setHtml(html) self.setHtml(html)