From 3c83b7873a108764bf78a8d04d5d9bbe8755d2a1 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 26 Jun 2011 12:29:58 -0400 Subject: [PATCH] Store: Allow for direct downloading of ebooks in the Get Books search dialog. Allow the user to right click and choose between downloading or opening in the store. --- src/calibre/gui2/store/opensearch_store.py | 14 ++++---- .../gui2/store/search/adv_search_builder.py | 4 +++ .../gui2/store/search/adv_search_builder.ui | 34 ++++++++++++++++--- src/calibre/gui2/store/search/models.py | 25 +++++++++++--- src/calibre/gui2/store/search/results_view.py | 22 +++++++++++- src/calibre/gui2/store/search/search.py | 28 ++++++++++++--- src/calibre/gui2/store/search_result.py | 4 ++- .../gui2/store/stores/epubbud_plugin.py | 2 ++ 8 files changed, 112 insertions(+), 21 deletions(-) diff --git a/src/calibre/gui2/store/opensearch_store.py b/src/calibre/gui2/store/opensearch_store.py index a3dc0e7941..8a25a80132 100644 --- a/src/calibre/gui2/store/opensearch_store.py +++ b/src/calibre/gui2/store/opensearch_store.py @@ -59,14 +59,14 @@ class OpenSearchStore(StorePlugin): elif l['rel'] == u'http://opds-spec.org/acquisition/buy': s.detail_item = l.get('href', s.detail_item) elif l['rel'] == u'http://opds-spec.org/acquisition': - s.downloads.append((l.get('type', ''), l.get('href', ''))) + mime = l.get('type', '') + if mime: + ext = mimetypes.guess_extension(mime) + if ext: + ext = ext[1:].upper() + s.downloads[ext] = l.get('href', '') - formats = [] - for mime, url in s.downloads: - ext = mimetypes.guess_extension(mime) - if ext: - formats.append(ext[1:]) - s.formats = ', '.join(formats) + s.formats = ', '.join(s.downloads.keys()) s.title = r.get('title', '') s.author = r.get('author', '') diff --git a/src/calibre/gui2/store/search/adv_search_builder.py b/src/calibre/gui2/store/search/adv_search_builder.py index cc89ca4eb7..127ac27acb 100644 --- a/src/calibre/gui2/store/search/adv_search_builder.py +++ b/src/calibre/gui2/store/search/adv_search_builder.py @@ -45,6 +45,7 @@ class AdvSearchBuilderDialog(QDialog, Ui_Dialog): self.author_box.setText('') self.price_box.setText('') self.format_box.setText('') + self.download_combo.setCurrentIndex(0) self.affiliate_combo.setCurrentIndex(0) def tokens(self, raw): @@ -119,6 +120,9 @@ class AdvSearchBuilderDialog(QDialog, Ui_Dialog): format = unicode(self.format_box.text()).strip() if format: ans.append('format:"' + self.mc + format + '"') + download = unicode(self.download_combo.currentText()).strip() + if download: + ans.append('download:' + download) affiliate = unicode(self.affiliate_combo.currentText()).strip() if affiliate: ans.append('affiliate:' + affiliate) diff --git a/src/calibre/gui2/store/search/adv_search_builder.ui b/src/calibre/gui2/store/search/adv_search_builder.ui index ab12dbbc00..02eb8f6aa1 100644 --- a/src/calibre/gui2/store/search/adv_search_builder.ui +++ b/src/calibre/gui2/store/search/adv_search_builder.ui @@ -226,7 +226,7 @@ - + @@ -244,7 +244,7 @@ - + Qt::Vertical @@ -283,14 +283,14 @@ - + Affiliate: - + @@ -309,6 +309,32 @@ + + + + Download: + + + + + + + + + + + + + true + + + + + false + + + + diff --git a/src/calibre/gui2/store/search/models.py b/src/calibre/gui2/store/search/models.py index e2c1a03e90..1fb0e5327b 100644 --- a/src/calibre/gui2/store/search/models.py +++ b/src/calibre/gui2/store/search/models.py @@ -33,7 +33,7 @@ class Matches(QAbstractItemModel): total_changed = pyqtSignal(int) - HEADERS = [_('Cover'), _('Title'), _('Price'), _('DRM'), _('Store'), ''] + HEADERS = [_('Cover'), _('Title'), _('Price'), _('DRM'), _('Store'), _('Download'), _('Affiliate')] HTML_COLS = (1, 4) def __init__(self, cover_thread_count=2, detail_thread_count=4): @@ -47,6 +47,8 @@ class Matches(QAbstractItemModel): Qt.SmoothTransformation) self.DONATE_ICON = QPixmap(I('donate.png')).scaledToHeight(16, Qt.SmoothTransformation) + self.DOWNLOAD_ICON = QPixmap(I('arrow-down.png')).scaledToHeight(16, + Qt.SmoothTransformation) # All matches. Used to determine the order to display # self.matches because the SearchFilter returns @@ -181,9 +183,11 @@ class Matches(QAbstractItemModel): elif result.drm == SearchResult.DRM_UNKNOWN: return QVariant(self.DRM_UNKNOWN_ICON) if col == 5: + if result.downloads: + return QVariant(self.DOWNLOAD_ICON) + if col == 6: if result.affiliate: return QVariant(self.DONATE_ICON) - return NONE elif role == Qt.ToolTipRole: if col == 1: return QVariant('

%s

' % result.title) @@ -199,6 +203,9 @@ class Matches(QAbstractItemModel): elif col == 4: return QVariant('

%s

' % result.formats) elif col == 5: + if result.downloads: + return QVariant('

' + _('The following formats can be downloaded directly: %s.') % ', '.join(result.downloads.keys()) + '

') + elif col == 6: if result.affiliate: return QVariant('

' + _('Buying from this store supports the calibre developer: %s.') % result.plugin_author + '

') elif role == Qt.SizeHintRole: @@ -221,6 +228,11 @@ class Matches(QAbstractItemModel): elif col == 4: text = result.store_name elif col == 5: + if result.downloads: + text = 'a' + else: + text = 'b' + elif col == 6: if result.affiliate: text = 'a' else: @@ -257,6 +269,8 @@ class SearchFilter(SearchQueryParser): 'author', 'authors', 'cover', + 'download', + 'downloads', 'drm', 'format', 'formats', @@ -282,6 +296,8 @@ class SearchFilter(SearchQueryParser): location = location.lower().strip() if location == 'authors': location = 'author' + elif location == 'downloads': + location = 'download' elif location == 'formats': location = 'format' @@ -308,12 +324,13 @@ class SearchFilter(SearchQueryParser): 'author': lambda x: x.author.lower(), 'cover': attrgetter('cover_url'), 'drm': attrgetter('drm'), + 'download': attrgetter('downloads'), 'format': attrgetter('formats'), 'price': lambda x: comparable_price(x.price), 'store': lambda x: x.store_name.lower(), 'title': lambda x: x.title.lower(), } - for x in ('author', 'format'): + for x in ('author', 'download', 'format'): q[x+'s'] = q[x] for sr in self.srs: for locvalue in locations: @@ -347,7 +364,7 @@ class SearchFilter(SearchQueryParser): matches.add(sr) continue # this is bool or treated as bool, so can't match below. - if locvalue in ('affiliate', 'drm'): + if locvalue in ('affiliate', 'drm', 'download', 'downloads'): continue try: ### Can't separate authors because comma is used for name sep and author sep diff --git a/src/calibre/gui2/store/search/results_view.py b/src/calibre/gui2/store/search/results_view.py index 91c067006e..3957e18ef6 100644 --- a/src/calibre/gui2/store/search/results_view.py +++ b/src/calibre/gui2/store/search/results_view.py @@ -6,13 +6,18 @@ __license__ = 'GPL 3' __copyright__ = '2011, John Schember ' __docformat__ = 'restructuredtext en' -from PyQt4.Qt import (QTreeView) +from functools import partial + +from PyQt4.Qt import (pyqtSignal, QMenu, QTreeView) from calibre.gui2.metadata.single_download import RichTextDelegate from calibre.gui2.store.search.models import Matches class ResultsView(QTreeView): + download_requested = pyqtSignal(object) + open_requested = pyqtSignal(object) + def __init__(self, *args): QTreeView.__init__(self,*args) @@ -24,3 +29,18 @@ class ResultsView(QTreeView): for i in self._model.HTML_COLS: self.setItemDelegateForColumn(i, self.rt_delegate) + def contextMenuEvent(self, event): + index = self.indexAt(event.pos()) + + if not index.isValid(): + return + + result = self.model().get_result(index) + + menu = QMenu() + da = menu.addAction(_('Download...'), partial(self.download_requested.emit, result)) + if not result.downloads: + da.setEnabled(False) + menu.addSeparator() + menu.addAction(_('Goto in store...'), partial(self.open_requested.emit, result)) + menu.exec_(event.globalPos()) diff --git a/src/calibre/gui2/store/search/search.py b/src/calibre/gui2/store/search/search.py index 7db4d1bbaf..fd20669f09 100644 --- a/src/calibre/gui2/store/search/search.py +++ b/src/calibre/gui2/store/search/search.py @@ -14,6 +14,7 @@ from PyQt4.Qt import (Qt, QDialog, QDialogButtonBox, QTimer, QCheckBox, QLabel, QComboBox) from calibre.gui2 import JSONConfig, info_dialog +from calibre.gui2.dialogs.choose_format import ChooseFormatDialog from calibre.gui2.progress_indicator import ProgressIndicator from calibre.gui2.store.config.chooser.chooser_widget import StoreChooserWidget from calibre.gui2.store.config.search.search_widget import StoreConfigWidget @@ -72,7 +73,9 @@ class SearchDialog(QDialog, Ui_Dialog): self.search.clicked.connect(self.do_search) self.checker.timeout.connect(self.get_results) self.progress_checker.timeout.connect(self.check_progress) - self.results_view.activated.connect(self.open_store) + self.results_view.activated.connect(self.result_item_activated) + self.results_view.download_requested.connect(self.download_book) + self.results_view.open_requested.connect(self.open_store) self.results_view.model().total_changed.connect(self.update_book_total) self.select_all_stores.clicked.connect(self.stores_select_all) self.select_invert_stores.clicked.connect(self.stores_select_invert) @@ -129,11 +132,15 @@ class SearchDialog(QDialog, Ui_Dialog): # Title / Author self.results_view.setColumnWidth(1,int(total*.40)) # Price - self.results_view.setColumnWidth(2,int(total*.20)) + self.results_view.setColumnWidth(2,int(total*.12)) # DRM self.results_view.setColumnWidth(3, int(total*.15)) # Store / Formats self.results_view.setColumnWidth(4, int(total*.25)) + # Download + self.results_view.setColumnWidth(5, 20) + # Affiliate + self.results_view.setColumnWidth(6, 20) def do_search(self): # Stop all running threads. @@ -183,7 +190,7 @@ class SearchDialog(QDialog, Ui_Dialog): query = re.sub(r'%s:"(?P[^\s"]+)"' % loc, '\g', query) query = query.replace('%s:' % loc, '') # Remove the prefix and search text. - for loc in ('cover', 'drm', 'format', 'formats', 'price', 'store'): + for loc in ('cover', 'download', 'downloads', 'drm', 'format', 'formats', 'price', 'store'): query = re.sub(r'%s:"[^"]"' % loc, '', query) query = re.sub(r'%s:[^\s]*' % loc, '', query) # Remove logic. @@ -330,8 +337,21 @@ class SearchDialog(QDialog, Ui_Dialog): def update_book_total(self, total): self.total.setText('%s' % total) - def open_store(self, index): + def result_item_activated(self, index): result = self.results_view.model().get_result(index) + + if result.downloads: + self.download_book(result) + else: + self.open_store(result) + + def download_book(self, result): + d = ChooseFormatDialog(self, _('Choose format to download to your library.'), result.downloads.keys()) + if d.exec_() == d.Accepted: + ext = d.format() + self.gui.download_ebook(result.downloads[ext]) + + def open_store(self, result): self.gui.istores[result.store_name].open(self, result.detail_item, self.open_external.isChecked()) def check_progress(self): diff --git a/src/calibre/gui2/store/search_result.py b/src/calibre/gui2/store/search_result.py index bbe5b83c38..5cf71f011e 100644 --- a/src/calibre/gui2/store/search_result.py +++ b/src/calibre/gui2/store/search_result.py @@ -22,7 +22,9 @@ class SearchResult(object): self.detail_item = '' self.drm = None self.formats = '' - self.downloads = [] + # key = format in upper case. + # value = url to download the file. + self.downloads = {} self.affiliate = False self.plugin_author = '' diff --git a/src/calibre/gui2/store/stores/epubbud_plugin.py b/src/calibre/gui2/store/stores/epubbud_plugin.py index 7c581b9578..b4d642f62b 100644 --- a/src/calibre/gui2/store/stores/epubbud_plugin.py +++ b/src/calibre/gui2/store/stores/epubbud_plugin.py @@ -22,4 +22,6 @@ class EpubBudStore(BasicStoreConfig, OpenSearchStore): s.price = '$0.00' s.drm = SearchResult.DRM_UNLOCKED s.formats = 'EPUB' + # Download links are broken for this store. + s.downloads = {} yield s