Store: Include changes from lp:~cbhaley/calibre/charles_store

This commit is contained in:
John Schember 2011-05-28 10:16:09 -04:00
commit 7078f39e71
8 changed files with 178 additions and 16 deletions

View File

@ -623,6 +623,8 @@ class StoreBase(Plugin): # {{{
headquarters = '' headquarters = ''
# All formats the store distributes ebooks in. # All formats the store distributes ebooks in.
formats = [] formats = []
# Is this store on an affiliate program?
affiliate = False
def load_actual_plugin(self, gui): def load_actual_plugin(self, gui):
''' '''

View File

@ -1111,6 +1111,7 @@ class StoreAmazonKindleStore(StoreBase):
drm_free_only = False drm_free_only = False
headquarters = 'US' headquarters = 'US'
formats = ['KINDLE'] formats = ['KINDLE']
affiliate = True
class StoreAmazonDEKindleStore(StoreBase): class StoreAmazonDEKindleStore(StoreBase):
name = 'Amazon DE Kindle' name = 'Amazon DE Kindle'
@ -1121,6 +1122,7 @@ class StoreAmazonDEKindleStore(StoreBase):
drm_free_only = False drm_free_only = False
headquarters = 'DE' headquarters = 'DE'
formats = ['KINDLE'] formats = ['KINDLE']
affiliate = True
class StoreAmazonUKKindleStore(StoreBase): class StoreAmazonUKKindleStore(StoreBase):
name = 'Amazon UK Kindle' name = 'Amazon UK Kindle'
@ -1131,6 +1133,7 @@ class StoreAmazonUKKindleStore(StoreBase):
drm_free_only = False drm_free_only = False
headquarters = 'UK' headquarters = 'UK'
formats = ['KINDLE'] formats = ['KINDLE']
affiliate = True
class StoreArchiveOrgStore(StoreBase): class StoreArchiveOrgStore(StoreBase):
name = 'Archive.org' name = 'Archive.org'
@ -1168,6 +1171,7 @@ class StoreBeamEBooksDEStore(StoreBase):
drm_free_only = True drm_free_only = True
headquarters = 'DE' headquarters = 'DE'
formats = ['EPUB', 'MOBI', 'PDF'] formats = ['EPUB', 'MOBI', 'PDF']
affiliate = True
class StoreBeWriteStore(StoreBase): class StoreBeWriteStore(StoreBase):
name = 'BeWrite Books' name = 'BeWrite Books'
@ -1196,6 +1200,17 @@ class StoreEbookscomStore(StoreBase):
headquarters = 'US' headquarters = 'US'
formats = ['EPUB', 'LIT', 'MOBI', 'PDF'] formats = ['EPUB', 'LIT', 'MOBI', 'PDF']
class StoreEBookShoppeUKStore(StoreBase):
name = 'ebookShoppe UK'
author = u'Charles Haley'
description = u'We made this website in an attempt to offer the widest range of UK eBooks possible across and as many formats as we could manage.'
actual_plugin = 'calibre.gui2.store.ebookshoppe_uk_plugin:EBookShoppeUKStore'
drm_free_only = False
headquarters = 'UK'
formats = ['EPUB', 'PDF']
affiliate = True
class StoreEPubBuyDEStore(StoreBase): class StoreEPubBuyDEStore(StoreBase):
name = 'EPUBBuy DE' name = 'EPUBBuy DE'
author = 'Charles Haley' author = 'Charles Haley'
@ -1233,6 +1248,7 @@ class StoreFoylesUKStore(StoreBase):
drm_free_only = False drm_free_only = False
headquarters = 'UK' headquarters = 'UK'
formats = ['EPUB', 'PDF'] formats = ['EPUB', 'PDF']
affiliate = True
class StoreGandalfStore(StoreBase): class StoreGandalfStore(StoreBase):
name = 'Gandalf' name = 'Gandalf'
@ -1355,6 +1371,16 @@ class StoreVirtualoStore(StoreBase):
headquarters = 'PL' headquarters = 'PL'
formats = ['EPUB', 'PDF'] formats = ['EPUB', 'PDF']
class StoreWaterstonesUKStore(StoreBase):
name = 'Waterstones UK'
author = 'Charles Haley'
description = u'Waterstone\'s mission is to be the leading Bookseller on the High Street and online providing customers the widest choice, great value and expert advice from a team passionate about Bookselling.'
actual_plugin = 'calibre.gui2.store.waterstones_uk_plugin:WaterstonesUKStore'
drm_free_only = False
headquarters = 'UK'
formats = ['EPUB', 'PDF']
class StoreWeightlessBooksStore(StoreBase): class StoreWeightlessBooksStore(StoreBase):
name = 'Weightless Books' name = 'Weightless Books'
description = u'An independent DRM-free ebooksite devoted to ebooks of all sorts.' description = u'An independent DRM-free ebooksite devoted to ebooks of all sorts.'
@ -1394,6 +1420,7 @@ plugins += [
StoreBeWriteStore, StoreBeWriteStore,
StoreDieselEbooksStore, StoreDieselEbooksStore,
StoreEbookscomStore, StoreEbookscomStore,
#StoreEBookShoppeUKStore,
StoreEPubBuyDEStore, StoreEPubBuyDEStore,
StoreEHarlequinStore, StoreEHarlequinStore,
StoreFeedbooksStore, StoreFeedbooksStore,
@ -1411,6 +1438,7 @@ plugins += [
StorePragmaticBookshelfStore, StorePragmaticBookshelfStore,
StoreSmashwordsStore, StoreSmashwordsStore,
StoreVirtualoStore, StoreVirtualoStore,
StoreWaterstonesUKStore,
StoreWeightlessBooksStore, StoreWeightlessBooksStore,
StoreWizardsTowerBooksStore, StoreWizardsTowerBooksStore,
StoreWoblinkStore StoreWoblinkStore

View File

@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
from functools import partial from functools import partial
from PyQt4.Qt import QMenu from PyQt4.Qt import QMenu, QIcon, QSize
from calibre.gui2 import error_dialog from calibre.gui2 import error_dialog
from calibre.gui2.actions import InterfaceAction from calibre.gui2.actions import InterfaceAction
@ -32,7 +32,12 @@ class StoreAction(InterfaceAction):
self.store_menu.addAction(_('Search for this book'), self.search_author_title) self.store_menu.addAction(_('Search for this book'), self.search_author_title)
self.store_menu.addSeparator() self.store_menu.addSeparator()
self.store_list_menu = self.store_menu.addMenu(_('Stores')) self.store_list_menu = self.store_menu.addMenu(_('Stores'))
icon = QIcon()
icon.addFile(I('donate.png'), QSize(16, 16))
for n, p in sorted(self.gui.istores.items(), key=lambda x: x[0].lower()): for n, p in sorted(self.gui.istores.items(), key=lambda x: x[0].lower()):
if p.base_plugin.affiliate:
self.store_list_menu.addAction(icon, n, partial(self.open_store, p))
else:
self.store_list_menu.addAction(n, partial(self.open_store, p)) self.store_list_menu.addAction(n, partial(self.open_store, p))
self.store_menu.addSeparator() self.store_menu.addSeparator()
self.store_menu.addAction(_('Choose stores'), self.choose) self.store_menu.addAction(_('Choose stores'), self.choose)

View File

@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import, print_function)
__license__ = 'GPL 3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
import urllib2
from contextlib import closing
from lxml import html
from PyQt4.Qt import QUrl
from calibre import browser
from calibre.gui2 import open_url
from calibre.gui2.store import StorePlugin
from calibre.gui2.store.basic_config import BasicStoreConfig
from calibre.gui2.store.search_result import SearchResult
from calibre.gui2.store.web_store_dialog import WebStoreDialog
class EBookShoppeUKStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False):
url_details = 'http://www.awin1.com/cread.php?awinmid=1414&awinaffid=120917&clickref=&p={0}'
url = 'http://www.awin1.com/awclick.php?mid=2666&id=120917'
if external or self.config.get('open_external', False):
if detail_item:
url = url_details.format(detail_item)
open_url(QUrl(url))
else:
detail_url = None
if detail_item:
detail_url = url_details.format(detail_item)
d = WebStoreDialog(self.gui, url, parent, detail_url)
d.setWindowTitle(self.name)
d.set_tags(self.config.get('tags', ''))
d.exec_()
def search(self, query, max_results=10, timeout=60):
url = 'http://www.ebookshoppe.com/search.php?search_query=' + urllib2.quote(query)
br = browser()
counter = max_results
with closing(br.open(url, timeout=timeout)) as f:
doc = html.fromstring(f.read())
for data in doc.xpath('//ul[@class="ProductList"]/li'):
if counter <= 0:
break
id = ''.join(data.xpath('./div[@class="ProductDetails"]/'
'strong/a/@href')).strip()
if not id:
continue
cover_url = ''.join(data.xpath('./div[@class="ProductImage"]/a/img/@src'))
title = ''.join(data.xpath('./div[@class="ProductDetails"]/strong/a/text()'))
price = ''.join(data.xpath('./div[@class="ProductPriceRating"]/em/text()'))
counter -= 1
s = SearchResult()
s.cover_url = cover_url
s.title = title.strip()
# Set the author to the query terms to ensure that author
# queries match something when pruning searches. Of course, this
# means that all books will match. Sigh...
s.author = query
s.price = price
s.drm = SearchResult.DRM_UNLOCKED
s.detail_item = id
s.formats = ''
yield s
def my_get_details(self, search_result, timeout):
br = browser()
with closing(br.open(search_result.detail_item, timeout=timeout)) as nf:
idata = html.fromstring(nf.read())
author = ''.join(idata.xpath('//div[@id="ProductOtherDetails"]/dl/dd[1]/text()'))
if author:
search_result.author = author
formats = idata.xpath('//dl[@class="ProductAddToCart"]/dd/'
'ul[@class="ProductOptionList"]/li/label/text()')
if formats:
search_result.formats = ', '.join(formats)
search_result.drm = SearchResult.DRM_UNKNOWN
return True

View File

@ -23,12 +23,13 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog
class FoylesUKStore(BasicStoreConfig, StorePlugin): class FoylesUKStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False): def open(self, parent=None, detail_item=None, external=False):
url = 'http://www.awin1.com/cread.php?awinmid=1414&awinaffid=120917&clickref=&p=' url = 'http://www.awin1.com/awclick.php?mid=1414&id=120917'
detail_url = 'http://www.awin1.com/cread.php?awinmid=1414&awinaffid=120917&clickref=&p='
url_redirect = 'http://www.foyles.co.uk' url_redirect = 'http://www.foyles.co.uk'
if external or self.config.get('open_external', False): if external or self.config.get('open_external', False):
if detail_item: if detail_item:
url = url + url_redirect + detail_item url = detail_url + url_redirect + detail_item
open_url(QUrl(url_slash_cleaner(url))) open_url(QUrl(url_slash_cleaner(url)))
else: else:
detail_url = None detail_url = None
@ -54,6 +55,10 @@ class FoylesUKStore(BasicStoreConfig, StorePlugin):
if not id: if not id:
continue continue
# filter out the audio books
if not data.xpath('boolean(.//div[@class="Relative"]/ul/li[contains(text(), "ePub")])'):
continue
cover_url = ''.join(data.xpath('.//a[@class="Jacket"]/img/@src')) cover_url = ''.join(data.xpath('.//a[@class="Jacket"]/img/@src'))
if cover_url: if cover_url:
cover_url = 'http://www.foyles.co.uk' + cover_url cover_url = 'http://www.foyles.co.uk' + cover_url

View File

@ -120,6 +120,7 @@ class SearchThread(Thread):
if not self._run: if not self._run:
return return
res.store_name = store_name res.store_name = store_name
res.affiliate = store_plugin.base_plugin.affiliate
self.results.put((res, store_plugin)) self.results.put((res, store_plugin))
self.tasks.task_done() self.tasks.task_done()
except: except:

View File

@ -10,7 +10,7 @@ import re
from operator import attrgetter from operator import attrgetter
from PyQt4.Qt import (Qt, QAbstractItemModel, QVariant, QPixmap, QModelIndex, QSize, from PyQt4.Qt import (Qt, QAbstractItemModel, QVariant, QPixmap, QModelIndex, QSize,
pyqtSignal) pyqtSignal, QIcon)
from calibre.gui2 import NONE, FunctionDispatcher from calibre.gui2 import NONE, FunctionDispatcher
from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.search_result import SearchResult
@ -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'), _('')]
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):
@ -153,6 +153,8 @@ class Matches(QAbstractItemModel):
def data(self, index, role): def data(self, index, role):
row, col = index.row(), index.column() row, col = index.row(), index.column()
if row >= len(self.matches):
return NONE
result = self.matches[row] result = self.matches[row]
if role == Qt.DisplayRole: if role == Qt.DisplayRole:
if col == 1: if col == 1:
@ -176,6 +178,14 @@ class Matches(QAbstractItemModel):
return QVariant(self.DRM_UNLOCKED_ICON) return QVariant(self.DRM_UNLOCKED_ICON)
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 result.affiliate:
# For some reason the size(16, 16) is forgotten if the icon
# is a class attribute. Don't know why...
icon = QIcon()
icon.addFile(I('donate.png'), QSize(16, 16))
return QVariant(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)
@ -190,6 +200,9 @@ class Matches(QAbstractItemModel):
return QVariant('<p>' + _('The DRM status of this book could not be determined. There is a very high likelihood that this book is actually DRM restricted.') + '</p>') return QVariant('<p>' + _('The DRM status of this book could not be determined. There is a very high likelihood that this book is actually DRM restricted.') + '</p>')
elif col == 4: elif col == 4:
return QVariant('<p>%s</p>' % result.formats) return QVariant('<p>%s</p>' % result.formats)
elif col == 5:
if result.affiliate:
return QVariant(_('Buying from this store supports a calibre developer'))
elif role == Qt.SizeHintRole: elif role == Qt.SizeHintRole:
return QSize(64, 64) return QSize(64, 64)
return NONE return NONE
@ -209,6 +222,11 @@ class Matches(QAbstractItemModel):
text = 'c' text = 'c'
elif col == 4: elif col == 4:
text = result.store_name text = result.store_name
elif col == 5:
if result.affiliate:
text = 'a'
else:
text = 'b'
return text return text
def sort(self, col, order, reset=True): def sort(self, col, order, reset=True):
@ -237,6 +255,7 @@ class SearchFilter(SearchQueryParser):
USABLE_LOCATIONS = [ USABLE_LOCATIONS = [
'all', 'all',
'affiliate',
'author', 'author',
'authors', 'authors',
'cover', 'cover',
@ -287,6 +306,7 @@ class SearchFilter(SearchQueryParser):
all_locs = set(self.USABLE_LOCATIONS) - set(['all']) all_locs = set(self.USABLE_LOCATIONS) - set(['all'])
locations = all_locs if location == 'all' else [location] locations = all_locs if location == 'all' else [location]
q = { q = {
'affiliate': attrgetter('affiliate'),
'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'),
@ -301,23 +321,35 @@ class SearchFilter(SearchQueryParser):
for locvalue in locations: for locvalue in locations:
accessor = q[locvalue] accessor = q[locvalue]
if query == 'true': if query == 'true':
if locvalue == 'drm': # True/False.
if locvalue == 'affiliate':
if accessor(sr):
matches.add(sr)
# Special that are treated as True/False.
elif locvalue == 'drm':
if accessor(sr) == SearchResult.DRM_LOCKED: if accessor(sr) == SearchResult.DRM_LOCKED:
matches.add(sr) matches.add(sr)
# Testing for something or nothing.
else: else:
if accessor(sr) is not None: if accessor(sr) is not None:
matches.add(sr) matches.add(sr)
continue continue
if query == 'false': if query == 'false':
if locvalue == 'drm': # True/False.
if locvalue == 'affiliate':
if not accessor(sr):
matches.add(sr)
# Special that are treated as True/False.
elif locvalue == 'drm':
if accessor(sr) == SearchResult.DRM_UNLOCKED: if accessor(sr) == SearchResult.DRM_UNLOCKED:
matches.add(sr) matches.add(sr)
# Testing for something or nothing.
else: else:
if accessor(sr) is None: if accessor(sr) is None:
matches.add(sr) matches.add(sr)
continue continue
# this is bool, so can't match below # this is bool or treated as bool, so can't match below.
if locvalue == 'drm': if locvalue in ('affiliate', 'drm'):
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

@ -22,6 +22,7 @@ class SearchResult(object):
self.detail_item = '' self.detail_item = ''
self.drm = None self.drm = None
self.formats = '' self.formats = ''
self.affiliate = False
def __eq__(self, other): def __eq__(self, other):
return self.title == other.title and self.author == other.author and self.store_name == other.store_name return self.title == other.title and self.author == other.author and self.store_name == other.store_name