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.

This commit is contained in:
John Schember 2011-06-26 12:29:58 -04:00
parent 12e9b6194e
commit 3c83b7873a
8 changed files with 112 additions and 21 deletions

View File

@ -59,14 +59,14 @@ class OpenSearchStore(StorePlugin):
elif l['rel'] == u'http://opds-spec.org/acquisition/buy': elif l['rel'] == u'http://opds-spec.org/acquisition/buy':
s.detail_item = l.get('href', s.detail_item) s.detail_item = l.get('href', s.detail_item)
elif l['rel'] == u'http://opds-spec.org/acquisition': 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 = [] s.formats = ', '.join(s.downloads.keys())
for mime, url in s.downloads:
ext = mimetypes.guess_extension(mime)
if ext:
formats.append(ext[1:])
s.formats = ', '.join(formats)
s.title = r.get('title', '') s.title = r.get('title', '')
s.author = r.get('author', '') s.author = r.get('author', '')

View File

@ -45,6 +45,7 @@ class AdvSearchBuilderDialog(QDialog, Ui_Dialog):
self.author_box.setText('') self.author_box.setText('')
self.price_box.setText('') self.price_box.setText('')
self.format_box.setText('') self.format_box.setText('')
self.download_combo.setCurrentIndex(0)
self.affiliate_combo.setCurrentIndex(0) self.affiliate_combo.setCurrentIndex(0)
def tokens(self, raw): def tokens(self, raw):
@ -119,6 +120,9 @@ class AdvSearchBuilderDialog(QDialog, Ui_Dialog):
format = unicode(self.format_box.text()).strip() format = unicode(self.format_box.text()).strip()
if format: if format:
ans.append('format:"' + self.mc + 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() affiliate = unicode(self.affiliate_combo.currentText()).strip()
if affiliate: if affiliate:
ans.append('affiliate:' + affiliate) ans.append('affiliate:' + affiliate)

View File

@ -226,7 +226,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="7" column="0" colspan="2"> <item row="8" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_6"> <layout class="QHBoxLayout" name="horizontalLayout_6">
<item> <item>
<widget class="QPushButton" name="clear_button"> <widget class="QPushButton" name="clear_button">
@ -244,7 +244,7 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="6" column="1"> <item row="7" column="1">
<spacer name="verticalSpacer"> <spacer name="verticalSpacer">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Vertical</enum>
@ -283,14 +283,14 @@
<item row="3" column="1"> <item row="3" column="1">
<widget class="EnLineEdit" name="price_box"/> <widget class="EnLineEdit" name="price_box"/>
</item> </item>
<item row="5" column="0"> <item row="6" column="0">
<widget class="QLabel" name="label_9"> <widget class="QLabel" name="label_9">
<property name="text"> <property name="text">
<string>Affiliate:</string> <string>Affiliate:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="1"> <item row="6" column="1">
<widget class="QComboBox" name="affiliate_combo"> <widget class="QComboBox" name="affiliate_combo">
<item> <item>
<property name="text"> <property name="text">
@ -309,6 +309,32 @@
</item> </item>
</widget> </widget>
</item> </item>
<item row="5" column="0">
<widget class="QLabel" name="label_12">
<property name="text">
<string>Download:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QComboBox" name="download_combo">
<item>
<property name="text">
<string/>
</property>
</item>
<item>
<property name="text">
<string>true</string>
</property>
</item>
<item>
<property name="text">
<string>false</string>
</property>
</item>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</widget> </widget>

View File

@ -33,7 +33,7 @@ class Matches(QAbstractItemModel):
total_changed = pyqtSignal(int) total_changed = pyqtSignal(int)
HEADERS = [_('Cover'), _('Title'), _('Price'), _('DRM'), _('Store'), ''] HEADERS = [_('Cover'), _('Title'), _('Price'), _('DRM'), _('Store'), _('Download'), _('Affiliate')]
HTML_COLS = (1, 4) HTML_COLS = (1, 4)
def __init__(self, cover_thread_count=2, detail_thread_count=4): def __init__(self, cover_thread_count=2, detail_thread_count=4):
@ -47,6 +47,8 @@ class Matches(QAbstractItemModel):
Qt.SmoothTransformation) Qt.SmoothTransformation)
self.DONATE_ICON = QPixmap(I('donate.png')).scaledToHeight(16, self.DONATE_ICON = QPixmap(I('donate.png')).scaledToHeight(16,
Qt.SmoothTransformation) Qt.SmoothTransformation)
self.DOWNLOAD_ICON = QPixmap(I('arrow-down.png')).scaledToHeight(16,
Qt.SmoothTransformation)
# All matches. Used to determine the order to display # All matches. Used to determine the order to display
# self.matches because the SearchFilter returns # self.matches because the SearchFilter returns
@ -181,9 +183,11 @@ class Matches(QAbstractItemModel):
elif result.drm == SearchResult.DRM_UNKNOWN: elif result.drm == SearchResult.DRM_UNKNOWN:
return QVariant(self.DRM_UNKNOWN_ICON) return QVariant(self.DRM_UNKNOWN_ICON)
if col == 5: if col == 5:
if result.downloads:
return QVariant(self.DOWNLOAD_ICON)
if col == 6:
if result.affiliate: if result.affiliate:
return QVariant(self.DONATE_ICON) return QVariant(self.DONATE_ICON)
return NONE
elif role == Qt.ToolTipRole: elif role == Qt.ToolTipRole:
if col == 1: if col == 1:
return QVariant('<p>%s</p>' % result.title) return QVariant('<p>%s</p>' % result.title)
@ -199,6 +203,9 @@ class Matches(QAbstractItemModel):
elif col == 4: elif col == 4:
return QVariant('<p>%s</p>' % result.formats) return QVariant('<p>%s</p>' % result.formats)
elif col == 5: elif col == 5:
if result.downloads:
return QVariant('<p>' + _('The following formats can be downloaded directly: %s.') % ', '.join(result.downloads.keys()) + '</p>')
elif col == 6:
if result.affiliate: if result.affiliate:
return QVariant('<p>' + _('Buying from this store supports the calibre developer: %s.') % result.plugin_author + '</p>') return QVariant('<p>' + _('Buying from this store supports the calibre developer: %s.') % result.plugin_author + '</p>')
elif role == Qt.SizeHintRole: elif role == Qt.SizeHintRole:
@ -221,6 +228,11 @@ class Matches(QAbstractItemModel):
elif col == 4: elif col == 4:
text = result.store_name text = result.store_name
elif col == 5: elif col == 5:
if result.downloads:
text = 'a'
else:
text = 'b'
elif col == 6:
if result.affiliate: if result.affiliate:
text = 'a' text = 'a'
else: else:
@ -257,6 +269,8 @@ class SearchFilter(SearchQueryParser):
'author', 'author',
'authors', 'authors',
'cover', 'cover',
'download',
'downloads',
'drm', 'drm',
'format', 'format',
'formats', 'formats',
@ -282,6 +296,8 @@ class SearchFilter(SearchQueryParser):
location = location.lower().strip() location = location.lower().strip()
if location == 'authors': if location == 'authors':
location = 'author' location = 'author'
elif location == 'downloads':
location = 'download'
elif location == 'formats': elif location == 'formats':
location = 'format' location = 'format'
@ -308,12 +324,13 @@ class SearchFilter(SearchQueryParser):
'author': lambda x: x.author.lower(), 'author': lambda x: x.author.lower(),
'cover': attrgetter('cover_url'), 'cover': attrgetter('cover_url'),
'drm': attrgetter('drm'), 'drm': attrgetter('drm'),
'download': attrgetter('downloads'),
'format': attrgetter('formats'), 'format': attrgetter('formats'),
'price': lambda x: comparable_price(x.price), 'price': lambda x: comparable_price(x.price),
'store': lambda x: x.store_name.lower(), 'store': lambda x: x.store_name.lower(),
'title': lambda x: x.title.lower(), 'title': lambda x: x.title.lower(),
} }
for x in ('author', 'format'): for x in ('author', 'download', 'format'):
q[x+'s'] = q[x] q[x+'s'] = q[x]
for sr in self.srs: for sr in self.srs:
for locvalue in locations: for locvalue in locations:
@ -347,7 +364,7 @@ class SearchFilter(SearchQueryParser):
matches.add(sr) matches.add(sr)
continue continue
# this is bool or treated as bool, so can't match below. # 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 continue
try: try:
### Can't separate authors because comma is used for name sep and author sep ### Can't separate authors because comma is used for name sep and author sep

View File

@ -6,13 +6,18 @@ __license__ = 'GPL 3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>' __copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en' __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.metadata.single_download import RichTextDelegate
from calibre.gui2.store.search.models import Matches from calibre.gui2.store.search.models import Matches
class ResultsView(QTreeView): class ResultsView(QTreeView):
download_requested = pyqtSignal(object)
open_requested = pyqtSignal(object)
def __init__(self, *args): def __init__(self, *args):
QTreeView.__init__(self,*args) QTreeView.__init__(self,*args)
@ -24,3 +29,18 @@ class ResultsView(QTreeView):
for i in self._model.HTML_COLS: for i in self._model.HTML_COLS:
self.setItemDelegateForColumn(i, self.rt_delegate) 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())

View File

@ -14,6 +14,7 @@ from PyQt4.Qt import (Qt, QDialog, QDialogButtonBox, QTimer, QCheckBox, QLabel,
QComboBox) QComboBox)
from calibre.gui2 import JSONConfig, info_dialog 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.progress_indicator import ProgressIndicator
from calibre.gui2.store.config.chooser.chooser_widget import StoreChooserWidget from calibre.gui2.store.config.chooser.chooser_widget import StoreChooserWidget
from calibre.gui2.store.config.search.search_widget import StoreConfigWidget 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.search.clicked.connect(self.do_search)
self.checker.timeout.connect(self.get_results) self.checker.timeout.connect(self.get_results)
self.progress_checker.timeout.connect(self.check_progress) 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.results_view.model().total_changed.connect(self.update_book_total)
self.select_all_stores.clicked.connect(self.stores_select_all) self.select_all_stores.clicked.connect(self.stores_select_all)
self.select_invert_stores.clicked.connect(self.stores_select_invert) self.select_invert_stores.clicked.connect(self.stores_select_invert)
@ -129,11 +132,15 @@ class SearchDialog(QDialog, Ui_Dialog):
# Title / Author # Title / Author
self.results_view.setColumnWidth(1,int(total*.40)) self.results_view.setColumnWidth(1,int(total*.40))
# Price # Price
self.results_view.setColumnWidth(2,int(total*.20)) self.results_view.setColumnWidth(2,int(total*.12))
# DRM # DRM
self.results_view.setColumnWidth(3, int(total*.15)) self.results_view.setColumnWidth(3, int(total*.15))
# Store / Formats # Store / Formats
self.results_view.setColumnWidth(4, int(total*.25)) 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): def do_search(self):
# Stop all running threads. # Stop all running threads.
@ -183,7 +190,7 @@ class SearchDialog(QDialog, Ui_Dialog):
query = re.sub(r'%s:"(?P<a>[^\s"]+)"' % loc, '\g<a>', query) query = re.sub(r'%s:"(?P<a>[^\s"]+)"' % loc, '\g<a>', query)
query = query.replace('%s:' % loc, '') query = query.replace('%s:' % loc, '')
# Remove the prefix and search text. # 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:"[^"]"' % loc, '', query)
query = re.sub(r'%s:[^\s]*' % loc, '', query) query = re.sub(r'%s:[^\s]*' % loc, '', query)
# Remove logic. # Remove logic.
@ -330,8 +337,21 @@ class SearchDialog(QDialog, Ui_Dialog):
def update_book_total(self, total): def update_book_total(self, total):
self.total.setText('%s' % 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) 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()) self.gui.istores[result.store_name].open(self, result.detail_item, self.open_external.isChecked())
def check_progress(self): def check_progress(self):

View File

@ -22,7 +22,9 @@ class SearchResult(object):
self.detail_item = '' self.detail_item = ''
self.drm = None self.drm = None
self.formats = '' self.formats = ''
self.downloads = [] # key = format in upper case.
# value = url to download the file.
self.downloads = {}
self.affiliate = False self.affiliate = False
self.plugin_author = '' self.plugin_author = ''

View File

@ -22,4 +22,6 @@ class EpubBudStore(BasicStoreConfig, OpenSearchStore):
s.price = '$0.00' s.price = '$0.00'
s.drm = SearchResult.DRM_UNLOCKED s.drm = SearchResult.DRM_UNLOCKED
s.formats = 'EPUB' s.formats = 'EPUB'
# Download links are broken for this store.
s.downloads = {}
yield s yield s