From 535622519776599f3ded7b62dfb9c16a6b4acf8d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 6 Apr 2011 21:51:03 -0600 Subject: [PATCH 1/8] Start work on new metadata download GUIs --- src/calibre/ebooks/metadata/sources/amazon.py | 2 +- src/calibre/ebooks/metadata/sources/base.py | 7 + src/calibre/ebooks/metadata/sources/covers.py | 2 +- .../ebooks/metadata/sources/identify.py | 2 +- src/calibre/ebooks/metadata/sources/isbndb.py | 3 + src/calibre/gui2/metadata/single_download.py | 154 +++++++++++++++++- src/calibre/manual/server.rst | 4 +- 7 files changed, 167 insertions(+), 7 deletions(-) diff --git a/src/calibre/ebooks/metadata/sources/amazon.py b/src/calibre/ebooks/metadata/sources/amazon.py index d48f502c29..b070132de9 100644 --- a/src/calibre/ebooks/metadata/sources/amazon.py +++ b/src/calibre/ebooks/metadata/sources/amazon.py @@ -279,7 +279,7 @@ class Worker(Thread): # Get details {{{ class Amazon(Source): - name = 'Amazon Metadata' + name = 'Amazon Store' description = _('Downloads metadata from Amazon') capabilities = frozenset(['identify', 'cover']) diff --git a/src/calibre/ebooks/metadata/sources/base.py b/src/calibre/ebooks/metadata/sources/base.py index faa7420081..d4e090084c 100644 --- a/src/calibre/ebooks/metadata/sources/base.py +++ b/src/calibre/ebooks/metadata/sources/base.py @@ -167,6 +167,13 @@ class Source(Plugin): # Configuration {{{ + def is_configured(self): + ''' + Return False if your plugin needs to be configured before it can be + used. For example, it might need a username/password/API key. + ''' + return True + @property def prefs(self): if self._config_obj is None: diff --git a/src/calibre/ebooks/metadata/sources/covers.py b/src/calibre/ebooks/metadata/sources/covers.py index 46b278397c..cf6ec90c54 100644 --- a/src/calibre/ebooks/metadata/sources/covers.py +++ b/src/calibre/ebooks/metadata/sources/covers.py @@ -76,7 +76,7 @@ def run_download(log, results, abort, (plugin, width, height, fmt, bytes) ''' - plugins = list(metadata_plugins(['cover'])) + plugins = [p for p in metadata_plugins(['cover']) if p.is_configured()] rq = Queue() workers = [Worker(p, abort, title, authors, identifiers, timeout, rq) for p diff --git a/src/calibre/ebooks/metadata/sources/identify.py b/src/calibre/ebooks/metadata/sources/identify.py index cbc12b6167..8c6172f0e2 100644 --- a/src/calibre/ebooks/metadata/sources/identify.py +++ b/src/calibre/ebooks/metadata/sources/identify.py @@ -250,7 +250,7 @@ def merge_identify_results(result_map, log): def identify(log, abort, # {{{ title=None, authors=None, identifiers={}, timeout=30): start_time = time.time() - plugins = list(metadata_plugins(['identify'])) + plugins = [p for p in metadata_plugins(['identify']) if p.is_configured()] kwargs = { 'title': title, diff --git a/src/calibre/ebooks/metadata/sources/isbndb.py b/src/calibre/ebooks/metadata/sources/isbndb.py index 3cd9d96c81..ab9342c6cb 100644 --- a/src/calibre/ebooks/metadata/sources/isbndb.py +++ b/src/calibre/ebooks/metadata/sources/isbndb.py @@ -37,4 +37,7 @@ class ISBNDB(Source): self.isbndb_key = prefs['isbndb_key'] + def is_configured(self): + return self.isbndb_key is not None + diff --git a/src/calibre/gui2/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py index ace4133d7a..be521b6000 100644 --- a/src/calibre/gui2/metadata/single_download.py +++ b/src/calibre/gui2/metadata/single_download.py @@ -7,8 +7,13 @@ __license__ = 'GPL v3' __copyright__ = '2011, Kovid Goyal ' __docformat__ = 'restructuredtext en' -from PyQt4.Qt import (QStyledItemDelegate, QTextDocument, QRectF, - QStyle, QApplication) +from PyQt4.Qt import (QStyledItemDelegate, QTextDocument, QRectF, QIcon, Qt, + QStyle, QApplication, QDialog, QVBoxLayout, QLabel, QDialogButtonBox, + QStackedWidget, QWidget, QTableView, QGridLayout, QFontInfo, QPalette) +from PyQt4.QtWebKit import QWebView + +from calibre.customize.ui import metadata_plugins +from calibre.ebooks.metadata import authors_to_string class RichTextDelegate(QStyledItemDelegate): # {{{ @@ -37,3 +42,148 @@ class RichTextDelegate(QStyledItemDelegate): # {{{ painter.restore() # }}} +class ResultsView(QTableView): + + def __init__(self, parent=None): + QTableView.__init__(self, parent) + +class Comments(QWebView): # {{{ + + def __init__(self, parent=None): + QWebView.__init__(self, parent) + self.setAcceptDrops(False) + self.setMaximumWidth(270) + self.setMinimumWidth(270) + + palette = self.palette() + palette.setBrush(QPalette.Base, Qt.transparent) + self.page().setPalette(palette) + self.setAttribute(Qt.WA_OpaquePaintEvent, False) + + def turnoff_scrollbar(self, *args): + self.page().mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff) + + def show_data(self, html): + def color_to_string(col): + ans = '#000000' + if col.isValid(): + col = col.toRgb() + if col.isValid(): + ans = unicode(col.name()) + return ans + + f = QFontInfo(QApplication.font(self.parent())).pixelSize() + c = color_to_string(QApplication.palette().color(QPalette.Normal, + QPalette.WindowText)) + templ = '''\ + + + + + +
+ %%s +
+ + + '''%(f, c) + self.setHtml(templ%html) +# }}} + +class IdentifyWidget(QWidget): + + def __init__(self, parent=None): + QWidget.__init__(self, parent) + + self.l = l = QGridLayout() + self.setLayout(l) + + names = [''+p.name+'' for p in metadata_plugins(['identify']) if + p.is_configured()] + self.top = QLabel('

'+_('calibre is downloading metadata from: ') + + ', '.join(names)) + self.top.setWordWrap(True) + l.addWidget(self.top, 0, 0) + + self.results_view = ResultsView(self) + l.addWidget(self.results_view, 1, 0) + + self.comments_view = Comments(self) + l.addWidget(self.comments_view, 1, 1) + + self.query = QLabel('download starting...') + f = self.query.font() + f.setPointSize(f.pointSize()-2) + self.query.setFont(f) + self.query.setWordWrap(True) + l.addWidget(self.query, 2, 0, 1, 2) + + def start(self, title=None, authors=None, identifiers={}): + parts = [] + if title: + parts.append('title:'+title) + if authors: + parts.append('authors:'+authors_to_string(authors)) + if identifiers: + x = ', '.join('%s:%s'%(k, v) for k, v in identifiers) + parts.append(x) + self.query.setText(_('Query: ')+'; '.join(parts)) + self.comments_view.show_data('

'+_('Downloading, please wait')+ + '.

'+ + ''' + + ''') + +class FullFetch(QDialog): # {{{ + + def __init__(self, parent=None): + QDialog.__init__(self, parent) + + self.setWindowTitle(_('Downloading metadata...')) + self.setWindowIcon(QIcon(I('metadata.png'))) + + self.stack = QStackedWidget() + self.l = l = QVBoxLayout() + self.setLayout(l) + l.addWidget(self.stack) + + self.bb = QDialogButtonBox(QDialogButtonBox.Cancel) + l.addWidget(self.bb) + self.bb.rejected.connect(self.reject) + + self.identify_widget = IdentifyWidget(self) + self.stack.addWidget(self.identify_widget) + self.resize(850, 500) + + def accept(self): + # Prevent pressing Enter from closing the dialog + pass + + def start(self, title=None, authors=None, identifiers={}): + self.identify_widget.start(title=title, authors=authors, + identifiers=identifiers) + self.exec_() +# }}} + +if __name__ == '__main__': + app = QApplication([]) + d = FullFetch() + d.start(title='great gatsby', authors=['Fitzgerald']) + diff --git a/src/calibre/manual/server.rst b/src/calibre/manual/server.rst index 6d1adc88cd..82ec5c2927 100644 --- a/src/calibre/manual/server.rst +++ b/src/calibre/manual/server.rst @@ -16,7 +16,7 @@ Here, we will show you how to integrate the |app| content server into another se Using a reverse proxy ----------------------- -This is the simplest approach as it allows you to use the binary calibre install with no external dependencies/system integration requirements. +A reverse proxy is when your normal server accepts incoming requests and passes them onto the calibre server. It then reads the response from the calibre server and forwards it to the client. This means that you can simply run the calibre server as normal without trying to integrate it closely with your main server, and you can take advantage of whatever authentication systems you main server has in place. This is the simplest approach as it allows you to use the binary calibre install with no external dependencies/system integration requirements. Below, is an example of how to achieve this with Apache as your main server, but it will work with any server that supports Reverse Proxies. First start the |app| content server as shown below:: @@ -33,7 +33,7 @@ The exact technique for enabling the proxy modules will vary depending on your A RewriteRule ^/calibre/(.*) http://localhost:8080/calibre/$1 [proxy] RewriteRule ^/calibre http://localhost:8080 [proxy] -That's all, you will now be able to access the |app| Content Server under the /calibre URL in your apache server. +That's all, you will now be able to access the |app| Content Server under the /calibre URL in your apache server. The above rules pass all requests under /calibre to the calibre server running on port 8080 and thanks to the --url-prefix option above, the calibre server handles them transparently. .. note:: If you are willing to devote an entire VirtualHost to the content server, then there is no need to use --url-prefix and RewriteRule, instead just use the ProxyPass directive. From fc1e9175fcb40d95c701f4b2d8a3c1025c4c2aad Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 6 Apr 2011 22:00:53 -0600 Subject: [PATCH 2/8] Fix some server settings not being applied when clicking start server in Preferences->Sharing over the net --- src/calibre/gui2/preferences/server.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/calibre/gui2/preferences/server.py b/src/calibre/gui2/preferences/server.py index 82519f17cd..421dbe737f 100644 --- a/src/calibre/gui2/preferences/server.py +++ b/src/calibre/gui2/preferences/server.py @@ -57,17 +57,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): r('autolaunch_server', config) - def set_server_options(self): - c = self.proxy - c.set('port', self.opt_port.value()) - c.set('username', unicode(self.opt_username.text()).strip()) - p = unicode(self.opt_password.text()).strip() - if not p: - p = None - c.set('password', p) - def start_server(self): - self.set_server_options() + ConfigWidgetBase.commit(self) self.gui.start_content_server(check_started=False) while not self.gui.content_server.is_running and self.gui.content_server.exception is None: time.sleep(1) From 2befe1eb584186f7cff24088e7a1a0edd2ee3b74 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 6 Apr 2011 22:53:08 -0600 Subject: [PATCH 3/8] ... --- src/calibre/gui2/metadata/single_download.py | 91 +++++++++++++++----- src/calibre/utils/logging.py | 54 ++++++++++-- 2 files changed, 116 insertions(+), 29 deletions(-) diff --git a/src/calibre/gui2/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py index be521b6000..426d0b9e78 100644 --- a/src/calibre/gui2/metadata/single_download.py +++ b/src/calibre/gui2/metadata/single_download.py @@ -7,6 +7,8 @@ __license__ = 'GPL v3' __copyright__ = '2011, Kovid Goyal ' __docformat__ = 'restructuredtext en' +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) @@ -14,6 +16,18 @@ 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.ebooks.metadata.sources.identify import identify + +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): # {{{ @@ -95,10 +109,35 @@ class Comments(QWebView): # {{{ self.setHtml(templ%html) # }}} +class IdentifyWorker(Thread): + + def __init__(self, log, abort, title, authors, identifiers): + Thread.__init__(self) + self.daemon = True + + self.log, self.abort = log, abort + self.title, self.authors, self.identifiers = (title, authors. + identifiers) + + self.results = [] + self.error = None + + def run(self): + try: + 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): - def __init__(self, parent=None): + def __init__(self, log, parent=None): QWidget.__init__(self, parent) + self.log = log + self.abort = Event() self.l = l = QGridLayout() self.setLayout(l) @@ -123,7 +162,27 @@ class IdentifyWidget(QWidget): self.query.setWordWrap(True) l.addWidget(self.query, 2, 0, 1, 2) + self.comments_view.show_data('

'+_('Downloading')+ + '
.

'+ + ''' + + ''') + def start(self, title=None, authors=None, identifiers={}): + self.log.clear() + self.log('Starting download') parts = [] if title: parts.append('title:'+title) @@ -133,28 +192,18 @@ class IdentifyWidget(QWidget): x = ', '.join('%s:%s'%(k, v) for k, v in identifiers) parts.append(x) self.query.setText(_('Query: ')+'; '.join(parts)) - self.comments_view.show_data('

'+_('Downloading, please wait')+ - '.

'+ - ''' - - ''') + self.log(unicode(self.query.text())) + + self.worker = IdentifyWorker(self.log, self.abort, self.title, + self.authors, self.identifiers) + + # self.worker.start() class FullFetch(QDialog): # {{{ - def __init__(self, parent=None): + def __init__(self, log, parent=None): QDialog.__init__(self, parent) + self.log = log self.setWindowTitle(_('Downloading metadata...')) self.setWindowIcon(QIcon(I('metadata.png'))) @@ -168,7 +217,7 @@ class FullFetch(QDialog): # {{{ l.addWidget(self.bb) self.bb.rejected.connect(self.reject) - self.identify_widget = IdentifyWidget(self) + self.identify_widget = IdentifyWidget(log, self) self.stack.addWidget(self.identify_widget) self.resize(850, 500) @@ -184,6 +233,6 @@ class FullFetch(QDialog): # {{{ if __name__ == '__main__': app = QApplication([]) - d = FullFetch() + d = FullFetch(Log()) d.start(title='great gatsby', authors=['Fitzgerald']) diff --git a/src/calibre/utils/logging.py b/src/calibre/utils/logging.py index f4b2e6f0b6..45e21ded39 100644 --- a/src/calibre/utils/logging.py +++ b/src/calibre/utils/logging.py @@ -14,7 +14,7 @@ import sys, traceback, cStringIO from functools import partial from threading import RLock - +from calibre import isbytestring, force_unicode, as_unicode class Stream(object): @@ -63,15 +63,16 @@ class FileStream(Stream): class HTMLStream(Stream): + color = { + DEBUG: '', + INFO:'', + WARN: '', + ERROR: '' + } + normal = '' + def __init__(self, stream=sys.stdout): Stream.__init__(self, stream) - self.color = { - DEBUG: '', - INFO:'', - WARN: '', - ERROR: '' - } - self.normal = '' def prints(self, level, *args, **kwargs): self.stream.write(self.color[level]) @@ -82,6 +83,43 @@ class HTMLStream(Stream): def flush(self): self.stream.flush() +class UnicodeHTMLStream(HTMLStream): + + def __init__(self): + self.clear() + + def flush(self): + pass + + def prints(self, level, *args, **kwargs): + col = self.color[level] + if col != self.last_col: + if self.data: + self.data.append(self.normal) + self.data.append(col) + self.last_col = col + + sep = kwargs.get(u'sep', u' ') + end = kwargs.get(u'end', u'\n') + + for arg in args: + if isbytestring(arg): + arg = force_unicode(arg) + elif not isinstance(arg, unicode): + arg = as_unicode(arg) + self.data.append(arg+sep) + self.data.append(end) + + def clear(self): + self.data = [] + self.last_col = self.color[INFO] + + @property + def html(self): + end = self.normal if self.data else u'' + return u''.join(self.data) + end + + class Log(object): DEBUG = DEBUG From 011403978718034d2817e19ce0b91a20fc766f76 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 6 Apr 2011 22:54:15 -0600 Subject: [PATCH 4/8] ... --- src/calibre/gui2/metadata/single_download.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py index 426d0b9e78..049ac611c5 100644 --- a/src/calibre/gui2/metadata/single_download.py +++ b/src/calibre/gui2/metadata/single_download.py @@ -116,7 +116,7 @@ class IdentifyWorker(Thread): self.daemon = True self.log, self.abort = log, abort - self.title, self.authors, self.identifiers = (title, authors. + self.title, self.authors, self.identifiers = (title, authors, identifiers) self.results = [] @@ -194,8 +194,8 @@ class IdentifyWidget(QWidget): self.query.setText(_('Query: ')+'; '.join(parts)) self.log(unicode(self.query.text())) - self.worker = IdentifyWorker(self.log, self.abort, self.title, - self.authors, self.identifiers) + self.worker = IdentifyWorker(self.log, self.abort, title, + authors, identifiers) # self.worker.start() From 4d27e7fa9d32f4e05973395c32fb0f1eab5f6a47 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 7 Apr 2011 13:12:23 +0100 Subject: [PATCH 5/8] Fix 753122: in preferences, clear a content server restriction if the saved search no longer exists. --- src/calibre/gui2/preferences/server.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/preferences/server.py b/src/calibre/gui2/preferences/server.py index 421dbe737f..f4a00c0932 100644 --- a/src/calibre/gui2/preferences/server.py +++ b/src/calibre/gui2/preferences/server.py @@ -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) From 0ba4f9a8adc6f9b2e4b77aa701a214c890f4fb99 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 7 Apr 2011 09:33:47 -0600 Subject: [PATCH 6/8] ... --- recipes/economist.recipe | 3 ++- recipes/economist_free.recipe | 3 ++- recipes/financial_times.recipe | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/recipes/economist.recipe b/recipes/economist.recipe index 9447fe2193..894f5880b3 100644 --- a/recipes/economist.recipe +++ b/recipes/economist.recipe @@ -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' diff --git a/recipes/economist_free.recipe b/recipes/economist_free.recipe index d1766211d7..4f060dc487 100644 --- a/recipes/economist_free.recipe +++ b/recipes/economist_free.recipe @@ -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 diff --git a/recipes/financial_times.recipe b/recipes/financial_times.recipe index 25efc56e45..0e3c91d3e3 100644 --- a/recipes/financial_times.recipe +++ b/recipes/financial_times.recipe @@ -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' From ebe5b28567e467df846ca15bb8c31123b5106026 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 7 Apr 2011 11:01:08 -0600 Subject: [PATCH 7/8] ... --- .../ebooks/metadata/sources/identify.py | 8 +-- src/calibre/gui2/metadata/single_download.py | 64 +++++++++++++++---- src/calibre/utils/logging.py | 24 +++++++ 3 files changed, 78 insertions(+), 18 deletions(-) diff --git a/src/calibre/ebooks/metadata/sources/identify.py b/src/calibre/ebooks/metadata/sources/identify.py index 8c6172f0e2..075c780596 100644 --- a/src/calibre/ebooks/metadata/sources/identify.py +++ b/src/calibre/ebooks/metadata/sources/identify.py @@ -253,10 +253,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:') diff --git a/src/calibre/gui2/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py index 049ac611c5..19e92adf4f 100644 --- a/src/calibre/gui2/metadata/single_download.py +++ b/src/calibre/gui2/metadata/single_download.py @@ -11,23 +11,16 @@ 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) 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.gui2 import error_dialog -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,10 +49,11 @@ class RichTextDelegate(QStyledItemDelegate): # {{{ painter.restore() # }}} -class ResultsView(QTableView): +class ResultsView(QTableView): # {{{ def __init__(self, parent=None): QTableView.__init__(self, parent) +# }}} class Comments(QWebView): # {{{ @@ -109,7 +103,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) @@ -131,8 +125,11 @@ class IdentifyWorker(Thread): 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) @@ -199,6 +196,40 @@ class IdentifyWidget(QWidget): # self.worker.start() + QTimer.singleShot(50, self.update) + + def update(self): + if self.worker.is_alive(): + QTimer.singleShot(50, self.update) + return + 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.wroker.error) + self.rejected.emit() + return + + if not self.worker.results: + log = ''.join(self.log.plain_text) + error_dialog(self, _('No matches found'), '

' + + _('Failed to find any books that ' + 'match your search. Try making the search less ' + 'specific. For example, use only the author\'s ' + 'last name and a single distinctive word from ' + 'the title.

To see the full log, click Show Details.'), + show=True, det_msg=log) + self.rejected.emit() + return + + + def cancel(self): + self.abort.set() +# }}} + class FullFetch(QDialog): # {{{ def __init__(self, log, parent=None): @@ -218,6 +249,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 +257,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) diff --git a/src/calibre/utils/logging.py b/src/calibre/utils/logging.py index 45e21ded39..dbbca6806b 100644 --- a/src/calibre/utils/logging.py +++ b/src/calibre/utils/logging.py @@ -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() From 903f628f70045d5bdb41b6ab19a0e7bdee29d8da Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 7 Apr 2011 11:59:47 -0600 Subject: [PATCH 8/8] ... --- .../ebooks/metadata/sources/identify.py | 4 + src/calibre/gui2/metadata/single_download.py | 119 ++++++++++++++++-- 2 files changed, 113 insertions(+), 10 deletions(-) diff --git a/src/calibre/ebooks/metadata/sources/identify.py b/src/calibre/ebooks/metadata/sources/identify.py index 075c780596..85549904e7 100644 --- a/src/calibre/ebooks/metadata/sources/identify.py +++ b/src/calibre/ebooks/metadata/sources/identify.py @@ -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: diff --git a/src/calibre/gui2/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py index 19e92adf4f..94ee8f8a13 100644 --- a/src/calibre/gui2/metadata/single_download.py +++ b/src/calibre/gui2/metadata/single_download.py @@ -12,14 +12,16 @@ 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, - QTimer, pyqtSignal) + 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 GUILog as Log from calibre.ebooks.metadata.sources.identify import identify -from calibre.gui2 import error_dialog +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 RichTextDelegate(QStyledItemDelegate): # {{{ @@ -49,10 +51,85 @@ class RichTextDelegate(QStyledItemDelegate): # {{{ painter.restore() # }}} +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 role != Qt.DisplayRole: + return NONE + if orientation == Qt.Horizontal: + try: + return QVariant(self.COLUMNS[section]) + except: + return NONE + else: + return QVariant(unicode(section+1)) + + 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 '%s
%s' % (t, a) + if col == 2: + d = format_date(book.pubdate, 'yyyy') if book.pubdate else _('Unknown') + p = book.publisher if book.publisher else '' + return '%s
%s' % (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): # {{{ @@ -60,8 +137,8 @@ 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) @@ -116,10 +193,30 @@ 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: @@ -194,22 +291,22 @@ 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) - return - self.process_results() + 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.wroker.error) + show=True, det_msg=self.worker.error) self.rejected.emit() return @@ -225,6 +322,8 @@ class IdentifyWidget(QWidget): # {{{ self.rejected.emit() return + self.results_view.show_results(self.worker.results) + def cancel(self): self.abort.set()