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

@ -615,7 +615,7 @@ class StoreBase(Plugin): # {{{
version = (1, 0, 1)
actual_plugin = None
# Does the store only distribute ebooks without DRM.
drm_free_only = False
# This is the 2 letter country code for the corporate
@ -623,6 +623,8 @@ class StoreBase(Plugin): # {{{
headquarters = ''
# All formats the store distributes ebooks in.
formats = []
# Is this store on an affiliate program?
affiliate = False
def load_actual_plugin(self, gui):
'''

View File

@ -1111,6 +1111,7 @@ class StoreAmazonKindleStore(StoreBase):
drm_free_only = False
headquarters = 'US'
formats = ['KINDLE']
affiliate = True
class StoreAmazonDEKindleStore(StoreBase):
name = 'Amazon DE Kindle'
@ -1121,6 +1122,7 @@ class StoreAmazonDEKindleStore(StoreBase):
drm_free_only = False
headquarters = 'DE'
formats = ['KINDLE']
affiliate = True
class StoreAmazonUKKindleStore(StoreBase):
name = 'Amazon UK Kindle'
@ -1131,6 +1133,7 @@ class StoreAmazonUKKindleStore(StoreBase):
drm_free_only = False
headquarters = 'UK'
formats = ['KINDLE']
affiliate = True
class StoreArchiveOrgStore(StoreBase):
name = 'Archive.org'
@ -1168,6 +1171,7 @@ class StoreBeamEBooksDEStore(StoreBase):
drm_free_only = True
headquarters = 'DE'
formats = ['EPUB', 'MOBI', 'PDF']
affiliate = True
class StoreBeWriteStore(StoreBase):
name = 'BeWrite Books'
@ -1196,6 +1200,17 @@ class StoreEbookscomStore(StoreBase):
headquarters = 'US'
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):
name = 'EPUBBuy DE'
author = 'Charles Haley'
@ -1233,6 +1248,7 @@ class StoreFoylesUKStore(StoreBase):
drm_free_only = False
headquarters = 'UK'
formats = ['EPUB', 'PDF']
affiliate = True
class StoreGandalfStore(StoreBase):
name = 'Gandalf'
@ -1355,6 +1371,16 @@ class StoreVirtualoStore(StoreBase):
headquarters = 'PL'
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):
name = 'Weightless Books'
description = u'An independent DRM-free ebooksite devoted to ebooks of all sorts.'
@ -1394,6 +1420,7 @@ plugins += [
StoreBeWriteStore,
StoreDieselEbooksStore,
StoreEbookscomStore,
#StoreEBookShoppeUKStore,
StoreEPubBuyDEStore,
StoreEHarlequinStore,
StoreFeedbooksStore,
@ -1411,6 +1438,7 @@ plugins += [
StorePragmaticBookshelfStore,
StoreSmashwordsStore,
StoreVirtualoStore,
StoreWaterstonesUKStore,
StoreWeightlessBooksStore,
StoreWizardsTowerBooksStore,
StoreWoblinkStore

View File

@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
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.actions import InterfaceAction
@ -32,8 +32,13 @@ class StoreAction(InterfaceAction):
self.store_menu.addAction(_('Search for this book'), self.search_author_title)
self.store_menu.addSeparator()
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()):
self.store_list_menu.addAction(n, partial(self.open_store, p))
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_menu.addSeparator()
self.store_menu.addAction(_('Choose stores'), self.choose)
self.qaction.setMenu(self.store_menu)

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):
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'
if external or self.config.get('open_external', False):
if detail_item:
url = url + url_redirect + detail_item
url = detail_url + url_redirect + detail_item
open_url(QUrl(url_slash_cleaner(url)))
else:
detail_url = None
@ -54,6 +55,10 @@ class FoylesUKStore(BasicStoreConfig, StorePlugin):
if not id:
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'))
if cover_url:
cover_url = 'http://www.foyles.co.uk' + cover_url

View File

@ -38,7 +38,7 @@ class GenericDownloadThreadPool(object):
This must be implemented in a sub class and this function
must be called at the end of the add_task function in
the sub class.
The implementation of this function (in this base class)
starts any threads necessary to fill the pool if it is
not already full.
@ -91,7 +91,7 @@ class SearchThreadPool(GenericDownloadThreadPool):
sp = SearchThreadPool(3)
sp.add_task(...)
'''
def __init__(self, thread_count):
GenericDownloadThreadPool.__init__(self, SearchThread, thread_count)
@ -120,6 +120,7 @@ class SearchThread(Thread):
if not self._run:
return
res.store_name = store_name
res.affiliate = store_plugin.base_plugin.affiliate
self.results.put((res, store_plugin))
self.tasks.task_done()
except:
@ -167,7 +168,7 @@ class CoverThread(Thread):
class DetailsThreadPool(GenericDownloadThreadPool):
def __init__(self, thread_count):
GenericDownloadThreadPool.__init__(self, DetailsThread, thread_count)

View File

@ -10,7 +10,7 @@ import re
from operator import attrgetter
from PyQt4.Qt import (Qt, QAbstractItemModel, QVariant, QPixmap, QModelIndex, QSize,
pyqtSignal)
pyqtSignal, QIcon)
from calibre.gui2 import NONE, FunctionDispatcher
from calibre.gui2.store.search_result import SearchResult
@ -33,7 +33,7 @@ class Matches(QAbstractItemModel):
total_changed = pyqtSignal(int)
HEADERS = [_('Cover'), _('Title'), _('Price'), _('DRM'), _('Store')]
HEADERS = [_('Cover'), _('Title'), _('Price'), _('DRM'), _('Store'), _('')]
HTML_COLS = (1, 4)
def __init__(self, cover_thread_count=2, detail_thread_count=4):
@ -153,6 +153,8 @@ class Matches(QAbstractItemModel):
def data(self, index, role):
row, col = index.row(), index.column()
if row >= len(self.matches):
return NONE
result = self.matches[row]
if role == Qt.DisplayRole:
if col == 1:
@ -176,6 +178,14 @@ class Matches(QAbstractItemModel):
return QVariant(self.DRM_UNLOCKED_ICON)
elif result.drm == SearchResult.DRM_UNKNOWN:
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:
if col == 1:
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>')
elif col == 4:
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:
return QSize(64, 64)
return NONE
@ -209,6 +222,11 @@ class Matches(QAbstractItemModel):
text = 'c'
elif col == 4:
text = result.store_name
elif col == 5:
if result.affiliate:
text = 'a'
else:
text = 'b'
return text
def sort(self, col, order, reset=True):
@ -237,6 +255,7 @@ class SearchFilter(SearchQueryParser):
USABLE_LOCATIONS = [
'all',
'affiliate',
'author',
'authors',
'cover',
@ -287,6 +306,7 @@ class SearchFilter(SearchQueryParser):
all_locs = set(self.USABLE_LOCATIONS) - set(['all'])
locations = all_locs if location == 'all' else [location]
q = {
'affiliate': attrgetter('affiliate'),
'author': lambda x: x.author.lower(),
'cover': attrgetter('cover_url'),
'drm': attrgetter('drm'),
@ -301,23 +321,35 @@ class SearchFilter(SearchQueryParser):
for locvalue in locations:
accessor = q[locvalue]
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:
matches.add(sr)
# Testing for something or nothing.
else:
if accessor(sr) is not None:
matches.add(sr)
continue
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:
matches.add(sr)
# Testing for something or nothing.
else:
if accessor(sr) is None:
matches.add(sr)
continue
# this is bool, so can't match below
if locvalue == 'drm':
# this is bool or treated as bool, so can't match below.
if locvalue in ('affiliate', 'drm'):
continue
try:
### Can't separate authors because comma is used for name sep and author sep

View File

@ -7,11 +7,11 @@ __copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
class SearchResult(object):
DRM_LOCKED = 1
DRM_UNLOCKED = 2
DRM_UNKNOWN = 3
def __init__(self):
self.store_name = ''
self.cover_url = ''
@ -22,6 +22,7 @@ class SearchResult(object):
self.detail_item = ''
self.drm = None
self.formats = ''
self.affiliate = False
def __eq__(self, other):
return self.title == other.title and self.author == other.author and self.store_name == other.store_name