mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
merge from trunk
This commit is contained in:
commit
70c8087f7e
@ -18,7 +18,8 @@ class Economist(BasicNewsRecipe):
|
||||
|
||||
__author__ = "Kovid Goyal"
|
||||
INDEX = 'http://www.economist.com/printedition'
|
||||
description = 'Global news and current affairs from a European perspective.'
|
||||
description = ('Global news and current affairs from a European'
|
||||
' perspective. Best downloaded on Friday mornings (GMT)')
|
||||
|
||||
oldest_article = 7.0
|
||||
cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg'
|
||||
|
@ -11,7 +11,8 @@ class Economist(BasicNewsRecipe):
|
||||
language = 'en'
|
||||
|
||||
__author__ = "Kovid Goyal"
|
||||
description = ('Global news and current affairs from a European perspective.'
|
||||
description = ('Global news and current affairs from a European'
|
||||
' perspective. Best downloaded on Friday mornings (GMT).'
|
||||
' Much slower than the print edition based version.')
|
||||
|
||||
oldest_article = 7.0
|
||||
|
@ -11,7 +11,8 @@ from calibre.web.feeds.news import BasicNewsRecipe
|
||||
class FinancialTimes(BasicNewsRecipe):
|
||||
title = u'Financial Times'
|
||||
__author__ = 'Darko Miletic and Sujata Raman'
|
||||
description = 'Financial world news'
|
||||
description = ('Financial world news. Available after 5AM '
|
||||
'GMT, daily.')
|
||||
oldest_article = 2
|
||||
language = 'en'
|
||||
|
||||
|
@ -217,6 +217,10 @@ class ISBNMerge(object):
|
||||
for r in results:
|
||||
ans.identifiers.update(r.identifiers)
|
||||
|
||||
# Cover URL
|
||||
ans.has_cached_cover_url = bool([r for r in results if
|
||||
getattr(r, 'has_cached_cover_url', False)])
|
||||
|
||||
# Merge any other fields with no special handling (random merge)
|
||||
touched_fields = set()
|
||||
for r in results:
|
||||
@ -253,10 +257,10 @@ def identify(log, abort, # {{{
|
||||
plugins = [p for p in metadata_plugins(['identify']) if p.is_configured()]
|
||||
|
||||
kwargs = {
|
||||
'title': title,
|
||||
'authors': authors,
|
||||
'identifiers': identifiers,
|
||||
'timeout': timeout,
|
||||
'title': title,
|
||||
'authors': authors,
|
||||
'identifiers': identifiers,
|
||||
'timeout': timeout,
|
||||
}
|
||||
|
||||
log('Running identify query with parameters:')
|
||||
|
@ -145,11 +145,10 @@ class InterfaceAction(QObject):
|
||||
ans[candidate] = zf.read(candidate)
|
||||
return ans
|
||||
|
||||
|
||||
def genesis(self):
|
||||
'''
|
||||
Setup this plugin. Only called once during initialization. self.gui is
|
||||
available. The action secified by :attr:`action_spec` is available as
|
||||
available. The action specified by :attr:`action_spec` is available as
|
||||
``self.qaction``.
|
||||
'''
|
||||
pass
|
||||
|
@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
|
||||
import os, shutil
|
||||
from functools import partial
|
||||
|
||||
from PyQt4.Qt import QMenu, Qt, QInputDialog
|
||||
from PyQt4.Qt import QMenu, Qt, QInputDialog, QToolButton
|
||||
|
||||
from calibre import isbytestring
|
||||
from calibre.constants import filesystem_encoding
|
||||
@ -88,6 +88,9 @@ class ChooseLibraryAction(InterfaceAction):
|
||||
type=Qt.QueuedConnection)
|
||||
|
||||
self.stats = LibraryUsageStats()
|
||||
self.popup_type = (QToolButton.InstantPopup if len(self.stats.stats) > 1 else
|
||||
QToolButton.MenuButtonPopup)
|
||||
|
||||
self.create_action(spec=(_('Switch/create library...'), 'lt.png', None,
|
||||
None), attr='action_choose')
|
||||
self.action_choose.triggered.connect(self.choose_library,
|
||||
@ -123,6 +126,7 @@ class ChooseLibraryAction(InterfaceAction):
|
||||
type=Qt.QueuedConnection)
|
||||
self.choose_menu.addAction(ac)
|
||||
|
||||
|
||||
self.rename_separator = self.choose_menu.addSeparator()
|
||||
|
||||
self.maintenance_menu = QMenu(_('Library Maintenance'))
|
||||
@ -172,6 +176,7 @@ class ChooseLibraryAction(InterfaceAction):
|
||||
return
|
||||
db = self.gui.library_view.model().db
|
||||
locations = list(self.stats.locations(db))
|
||||
|
||||
for ac in self.switch_actions:
|
||||
ac.setVisible(False)
|
||||
self.quick_menu.clear()
|
||||
|
@ -7,9 +7,9 @@ __docformat__ = 'restructuredtext en'
|
||||
|
||||
from functools import partial
|
||||
|
||||
from PyQt4.Qt import QIcon, Qt, QWidget, QToolBar, QSize, \
|
||||
pyqtSignal, QToolButton, QMenu, \
|
||||
QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup
|
||||
from PyQt4.Qt import (QIcon, Qt, QWidget, QToolBar, QSize,
|
||||
pyqtSignal, QToolButton, QMenu,
|
||||
QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup)
|
||||
|
||||
|
||||
from calibre.constants import __appname__
|
||||
@ -264,11 +264,11 @@ class ToolBar(QToolBar): # {{{
|
||||
|
||||
def apply_settings(self):
|
||||
sz = gprefs['toolbar_icon_size']
|
||||
sz = {'small':24, 'medium':48, 'large':64}[sz]
|
||||
sz = {'off':0, 'small':24, 'medium':48, 'large':64}[sz]
|
||||
self.setIconSize(QSize(sz, sz))
|
||||
self.child_bar.setIconSize(QSize(sz, sz))
|
||||
style = Qt.ToolButtonTextUnderIcon
|
||||
if gprefs['toolbar_text'] == 'never':
|
||||
if sz > 0 and gprefs['toolbar_text'] == 'never':
|
||||
style = Qt.ToolButtonIconOnly
|
||||
self.setToolButtonStyle(style)
|
||||
self.child_bar.setToolButtonStyle(style)
|
||||
|
@ -11,23 +11,18 @@ from threading import Thread, Event
|
||||
|
||||
from PyQt4.Qt import (QStyledItemDelegate, QTextDocument, QRectF, QIcon, Qt,
|
||||
QStyle, QApplication, QDialog, QVBoxLayout, QLabel, QDialogButtonBox,
|
||||
QStackedWidget, QWidget, QTableView, QGridLayout, QFontInfo, QPalette)
|
||||
QStackedWidget, QWidget, QTableView, QGridLayout, QFontInfo, QPalette,
|
||||
QTimer, pyqtSignal, QAbstractTableModel, QVariant, QSize)
|
||||
from PyQt4.QtWebKit import QWebView
|
||||
|
||||
from calibre.customize.ui import metadata_plugins
|
||||
from calibre.ebooks.metadata import authors_to_string
|
||||
from calibre.utils.logging import ThreadSafeLog, UnicodeHTMLStream
|
||||
from calibre.utils.logging import GUILog as Log
|
||||
from calibre.ebooks.metadata.sources.identify import identify
|
||||
from calibre.ebooks.metadata.book.base import Metadata
|
||||
from calibre.gui2 import error_dialog, NONE
|
||||
from calibre.utils.date import utcnow, fromordinal, format_date
|
||||
|
||||
class Log(ThreadSafeLog): # {{{
|
||||
|
||||
def __init__(self):
|
||||
ThreadSafeLog.__init__(self, level=self.DEBUG)
|
||||
self.outputs = [UnicodeHTMLStream()]
|
||||
|
||||
def clear(self):
|
||||
self.outputs[0].clear()
|
||||
# }}}
|
||||
|
||||
class RichTextDelegate(QStyledItemDelegate): # {{{
|
||||
|
||||
@ -56,18 +51,91 @@ class RichTextDelegate(QStyledItemDelegate): # {{{
|
||||
painter.restore()
|
||||
# }}}
|
||||
|
||||
class ResultsView(QTableView):
|
||||
class ResultsModel(QAbstractTableModel):
|
||||
|
||||
COLUMNS = (
|
||||
'#', _('Title'), _('Published'), _('Has cover'), _('Has summary')
|
||||
)
|
||||
HTML_COLS = (1, 2)
|
||||
ICON_COLS = (3, 4)
|
||||
|
||||
def __init__(self, results, parent=None):
|
||||
QAbstractTableModel.__init__(self, parent)
|
||||
self.results = results
|
||||
self.yes_icon = QVariant(QIcon(I('ok.png')))
|
||||
|
||||
def rowCount(self, parent=None):
|
||||
return len(self.results)
|
||||
|
||||
def columnCount(self, parent=None):
|
||||
return len(self.COLUMNS)
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
|
||||
try:
|
||||
return QVariant(self.COLUMNS[section])
|
||||
except:
|
||||
return NONE
|
||||
return NONE
|
||||
|
||||
def data_as_text(self, book, col):
|
||||
if col == 0:
|
||||
return unicode(book.gui_rank+1)
|
||||
if col == 1:
|
||||
t = book.title if book.title else _('Unknown')
|
||||
a = authors_to_string(book.authors) if book.authors else ''
|
||||
return '<b>%s</b><br><i>%s</i>' % (t, a)
|
||||
if col == 2:
|
||||
d = format_date(book.pubdate, 'yyyy') if book.pubdate else _('Unknown')
|
||||
p = book.publisher if book.publisher else ''
|
||||
return '<b>%s</b><br><i>%s</i>' % (d, p)
|
||||
|
||||
|
||||
def data(self, index, role):
|
||||
row, col = index.row(), index.column()
|
||||
try:
|
||||
book = self.results[row]
|
||||
except:
|
||||
return NONE
|
||||
if role == Qt.DisplayRole and col not in self.ICON_COLS:
|
||||
res = self.data_as_text(book, col)
|
||||
if res:
|
||||
return QVariant(res)
|
||||
return NONE
|
||||
elif role == Qt.DecorationRole and col in self.ICON_COLS:
|
||||
if col == 3 and getattr(book, 'has_cached_cover_url', False):
|
||||
return self.yes_icon
|
||||
if col == 4 and book.comments:
|
||||
return self.yes_icon
|
||||
return NONE
|
||||
|
||||
class ResultsView(QTableView): # {{{
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QTableView.__init__(self, parent)
|
||||
self.rt_delegate = RichTextDelegate(self)
|
||||
self.setSelectionMode(self.SingleSelection)
|
||||
self.setAlternatingRowColors(True)
|
||||
self.setSelectionBehavior(self.SelectRows)
|
||||
self.setIconSize(QSize(24, 24))
|
||||
|
||||
def show_results(self, results):
|
||||
self._model = ResultsModel(results, self)
|
||||
self.setModel(self._model)
|
||||
for i in self._model.HTML_COLS:
|
||||
self.setItemDelegateForColumn(i, self.rt_delegate)
|
||||
self.resizeRowsToContents()
|
||||
self.resizeColumnsToContents()
|
||||
|
||||
# }}}
|
||||
|
||||
class Comments(QWebView): # {{{
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QWebView.__init__(self, parent)
|
||||
self.setAcceptDrops(False)
|
||||
self.setMaximumWidth(270)
|
||||
self.setMinimumWidth(270)
|
||||
self.setMaximumWidth(300)
|
||||
self.setMinimumWidth(300)
|
||||
|
||||
palette = self.palette()
|
||||
palette.setBrush(QPalette.Base, Qt.transparent)
|
||||
@ -109,7 +177,7 @@ class Comments(QWebView): # {{{
|
||||
self.setHtml(templ%html)
|
||||
# }}}
|
||||
|
||||
class IdentifyWorker(Thread):
|
||||
class IdentifyWorker(Thread): # {{{
|
||||
|
||||
def __init__(self, log, abort, title, authors, identifiers):
|
||||
Thread.__init__(self)
|
||||
@ -122,17 +190,40 @@ class IdentifyWorker(Thread):
|
||||
self.results = []
|
||||
self.error = None
|
||||
|
||||
def sample_results(self):
|
||||
m1 = Metadata('The Great Gatsby', ['Francis Scott Fitzgerald'])
|
||||
m2 = Metadata('The Great Gatsby', ['F. Scott Fitzgerald'])
|
||||
m1.has_cached_cover_url = True
|
||||
m2.has_cached_cover_url = False
|
||||
m1.comments = 'Some comments '*10
|
||||
m1.tags = ['tag%d'%i for i in range(20)]
|
||||
m1.rating = 4.4
|
||||
m1.language = 'en'
|
||||
m2.language = 'fr'
|
||||
m1.pubdate = utcnow()
|
||||
m2.pubdate = fromordinal(1000000)
|
||||
m1.publisher = 'Publisher 1'
|
||||
m2.publisher = 'Publisher 2'
|
||||
|
||||
return [m1, m2]
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.results = identify(self.log, self.abort, title=self.title,
|
||||
authors=self.authors, identifiers=self.identifiers)
|
||||
if True:
|
||||
self.results = self.sample_results()
|
||||
else:
|
||||
self.results = identify(self.log, self.abort, title=self.title,
|
||||
authors=self.authors, identifiers=self.identifiers)
|
||||
for i, result in enumerate(self.results):
|
||||
result.gui_rank = i
|
||||
except:
|
||||
import traceback
|
||||
self.error = traceback.format_exc()
|
||||
# }}}
|
||||
|
||||
class IdentifyWidget(QWidget):
|
||||
class IdentifyWidget(QWidget): # {{{
|
||||
|
||||
rejected = pyqtSignal()
|
||||
|
||||
def __init__(self, log, parent=None):
|
||||
QWidget.__init__(self, parent)
|
||||
@ -197,7 +288,48 @@ class IdentifyWidget(QWidget):
|
||||
self.worker = IdentifyWorker(self.log, self.abort, title,
|
||||
authors, identifiers)
|
||||
|
||||
# self.worker.start()
|
||||
self.worker.start()
|
||||
|
||||
QTimer.singleShot(50, self.update)
|
||||
|
||||
def update(self):
|
||||
if self.worker.is_alive():
|
||||
QTimer.singleShot(50, self.update)
|
||||
else:
|
||||
self.process_results()
|
||||
|
||||
def process_results(self):
|
||||
if self.worker.error is not None:
|
||||
error_dialog(self, _('Download failed'),
|
||||
_('Failed to download metadata. Click '
|
||||
'Show Details to see details'),
|
||||
show=True, det_msg=self.worker.error)
|
||||
self.rejected.emit()
|
||||
return
|
||||
|
||||
if not self.worker.results:
|
||||
log = ''.join(self.log.plain_text)
|
||||
error_dialog(self, _('No matches found'), '<p>' +
|
||||
_('Failed to find any books that '
|
||||
'match your search. Try making the search <b>less '
|
||||
'specific</b>. For example, use only the author\'s '
|
||||
'last name and a single distinctive word from '
|
||||
'the title.<p>To see the full log, click Show Details.'),
|
||||
show=True, det_msg=log)
|
||||
self.rejected.emit()
|
||||
return
|
||||
|
||||
self.results_view.show_results(self.worker.results)
|
||||
|
||||
self.comments_view.show_data('''
|
||||
<div style="margin-bottom:2ex">Found <b>%d</b> results</div>
|
||||
<div>To see <b>details</b>, click on any result</div>''' %
|
||||
len(self.worker.results))
|
||||
|
||||
|
||||
def cancel(self):
|
||||
self.abort.set()
|
||||
# }}}
|
||||
|
||||
class FullFetch(QDialog): # {{{
|
||||
|
||||
@ -218,6 +350,7 @@ class FullFetch(QDialog): # {{{
|
||||
self.bb.rejected.connect(self.reject)
|
||||
|
||||
self.identify_widget = IdentifyWidget(log, self)
|
||||
self.identify_widget.rejected.connect(self.reject)
|
||||
self.stack.addWidget(self.identify_widget)
|
||||
self.resize(850, 500)
|
||||
|
||||
@ -225,6 +358,10 @@ class FullFetch(QDialog): # {{{
|
||||
# Prevent pressing Enter from closing the dialog
|
||||
pass
|
||||
|
||||
def reject(self):
|
||||
self.identify_widget.cancel()
|
||||
return QDialog.reject(self)
|
||||
|
||||
def start(self, title=None, authors=None, identifiers={}):
|
||||
self.identify_widget.start(title=title, authors=authors,
|
||||
identifiers=identifiers)
|
||||
|
@ -49,8 +49,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
r('use_roman_numerals_for_series_number', config)
|
||||
r('separate_cover_flow', config, restart_required=True)
|
||||
|
||||
choices = [(_('Small'), 'small'), (_('Medium'), 'medium'),
|
||||
(_('Large'), 'large')]
|
||||
choices = [(_('Off'), 'off'), (_('Small'), 'small'),
|
||||
(_('Medium'), 'medium'), (_('Large'), 'large')]
|
||||
r('toolbar_icon_size', gprefs, choices=choices)
|
||||
|
||||
choices = [(_('Automatic'), 'auto'), (_('Always'), 'always'),
|
||||
|
@ -18,6 +18,7 @@ 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
|
||||
|
||||
class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
|
||||
@ -42,8 +43,11 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
else self.opt_password.Password))
|
||||
self.opt_password.setEchoMode(self.opt_password.Password)
|
||||
|
||||
restrictions = sorted(saved_searches().names(),
|
||||
cmp=lambda x,y: cmp(x.lower(), y.lower()))
|
||||
restrictions = sorted(saved_searches().names(), key=sort_key)
|
||||
# verify that the current restriction still exists. If not, clear it.
|
||||
csr = db.prefs.get('cs_restriction', None)
|
||||
if csr and csr not in restrictions:
|
||||
db.prefs.set('cs_restriction', '')
|
||||
choices = [('', '')] + [(x, x) for x in restrictions]
|
||||
r('cs_restriction', db.prefs, choices=choices)
|
||||
|
||||
|
@ -108,10 +108,13 @@ class UnicodeHTMLStream(HTMLStream):
|
||||
elif not isinstance(arg, unicode):
|
||||
arg = as_unicode(arg)
|
||||
self.data.append(arg+sep)
|
||||
self.plain_text.append(arg+sep)
|
||||
self.data.append(end)
|
||||
self.plain_text.append(end)
|
||||
|
||||
def clear(self):
|
||||
self.data = []
|
||||
self.plain_text = []
|
||||
self.last_col = self.color[INFO]
|
||||
|
||||
@property
|
||||
@ -162,4 +165,25 @@ class ThreadSafeLog(Log):
|
||||
with self._lock:
|
||||
Log.prints(self, *args, **kwargs)
|
||||
|
||||
class GUILog(ThreadSafeLog):
|
||||
|
||||
'''
|
||||
Logs in HTML and plain text as unicode. Ideal for display in a GUI context.
|
||||
'''
|
||||
|
||||
def __init__(self):
|
||||
ThreadSafeLog.__init__(self, level=self.DEBUG)
|
||||
self.outputs = [UnicodeHTMLStream()]
|
||||
|
||||
def clear(self):
|
||||
self.outputs[0].clear()
|
||||
|
||||
@property
|
||||
def html(self):
|
||||
return self.outputs[0].html
|
||||
|
||||
@property
|
||||
def plain_text(self):
|
||||
return u''.join(self.outputs[0].plain_text)
|
||||
|
||||
default_log = Log()
|
||||
|
Loading…
x
Reference in New Issue
Block a user