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
from functools import partial
from urllib import quote_plus
from binascii import hexlify
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')
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):
try:
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)
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):
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)
if links:
ans.append((field, row % (_('Ids')+':', links)))
elif field == 'authors' and not isdevice:
elif field == 'authors':
authors = []
formatter = EvalFormatter()
for aut in mi.authors:
link = ''
if mi.author_link_map[aut]:
if mi.author_link_map.get(aut):
link = lt = mi.author_link_map[aut]
elif default_author_link:
if default_author_link == 'search-calibre':
link = search_href('authors', aut)
lt = a(_('Search the calibre library for books by %s') % aut)
if isdevice and default_author_link == 'search-calibre':
default_author_link = DEFAULT_AUTHOR_LINK
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:
vals = {'author': aut.replace(' ', '+')}
vals = {'author': qquote(aut), 'title': qquote(mi.title)}
try:
vals['author_sort'] = mi.author_sort_map[aut].replace(' ', '+')
except:
vals['author_sort'] = aut.replace(' ', '+')
link = lt = a(formatter.safe_format(default_author_link, vals, '', vals))
vals['author_sort'] = qquote(mi.author_sort_map[aut])
except KeyError:
vals['author_sort'] = qquote(aut)
link = lt = formatter.safe_format(default_author_link, vals, '', vals)
aut = p(aut)
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:
authors.append(aut)
ans.append((field, row % (name, u' & '.join(authors))))

View File

@ -108,7 +108,6 @@ def create_defs():
defs['tags_browser_collapse_at'] = 100
defs['tag_browser_dont_collapse'] = []
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['manual_add_auto_convert'] = False
defs['auto_convert_same_fmt'] = False
@ -273,6 +272,15 @@ QSettings.setPath(QSettings.IniFormat, QSettings.SystemScope, config_dir)
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():
desktop = QCoreApplication.instance().desktop()
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.metadata.book.base import (field_metadata, Metadata)
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.img import image_from_x, blend_image
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 = [(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(),
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,
QAbstractListModel, Qt, QIcon, QKeySequence, QColor, pyqtSignal, QCursor,
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.ebooks.metadata.book.render import DEFAULT_AUTHOR_LINK
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.preferences import ConfigWidgetBase, test_widget, CommaSeparatedList
from calibre.gui2.preferences.look_feel_ui import Ui_Form
@ -42,6 +44,56 @@ class BusyCursor(object):
def __exit__(self, *args):
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 {{{
@ -280,6 +332,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.icon_theme.setText(_('Icon theme: <b>%s</b>') % self.icon_theme_title)
self.commit_icon_theme = None
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('ui_style', gprefs, restart_required=True, choices=[(_('System default'), 'system'), (_('calibre style'),
'calibre')])
@ -351,12 +405,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
(_('Partitioned'), 'partition')]
r('tags_browser_partition_method', gprefs, choices=choices)
r('tags_browser_collapse_at', gprefs)
r('default_author_link', gprefs)
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()
if (db.field_metadata[k]['is_category'] and
(db.field_metadata[k]['datatype'] in ['text', 'series', 'enumeration']) and
@ -483,6 +533,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
def initialize(self):
ConfigWidgetBase.initialize(self)
self.default_author_link.value = default_author_link()
font = gprefs['font']
if font is not None:
font = list(font)
@ -536,6 +587,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
def restore_defaults(self):
ConfigWidgetBase.restore_defaults(self)
self.default_author_link.value = DEFAULT_AUTHOR_LINK
ofont = self.current_font
self.current_font = None
if ofont is not None:
@ -637,6 +689,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
if self.commit_icon_theme is not None:
self.commit_icon_theme()
rr = True
gprefs['default_author_link'] = self.default_author_link.value
return rr
def refresh_gui(self, gui):

View File

@ -269,7 +269,7 @@
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="label_19">
<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 name="wordWrap">
<bool>true</bool>
@ -773,41 +773,6 @@ A value of zero means calculate automatically.</string>
</item>
</layout>
</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">
<widget class="QPushButton" name="id_links_button">
<property name="text">
@ -815,6 +780,16 @@ Manage Authors. You can use the values {author} and
</property>
</widget>
</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>
</widget>
<widget class="QWidget" name="tab_2">

View File

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