From b244246fceffdc5781add6a58ee17c4e3d30ed56 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 28 May 2011 16:21:45 +0100 Subject: [PATCH 1/3] Add WH Smith. Improve ebookshoppe_uk --- src/calibre/customize/builtins.py | 60 +++++++++++--- .../gui2/store/ebookshoppe_uk_plugin.py | 8 +- src/calibre/gui2/store/whsmith_uk_plugin.py | 83 +++++++++++++++++++ 3 files changed, 134 insertions(+), 17 deletions(-) create mode 100644 src/calibre/gui2/store/whsmith_uk_plugin.py diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 0b83d0f45b..f1afb62cbc 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -1143,6 +1143,7 @@ class StoreArchiveOrgStore(StoreBase): drm_free_only = True headquarters = 'US' formats = ['DAISY', 'DJVU', 'EPUB', 'MOBI', 'PDF', 'TXT'] + affiliate = False class StoreBaenWebScriptionStore(StoreBase): name = 'Baen WebScription' @@ -1152,6 +1153,7 @@ class StoreBaenWebScriptionStore(StoreBase): drm_free_only = True headquarters = 'US' formats = ['EPUB', 'LIT', 'LRF', 'MOBI', 'RB', 'RTF', 'ZIP'] + affiliate = False class StoreBNStore(StoreBase): name = 'Barnes and Noble' @@ -1161,6 +1163,7 @@ class StoreBNStore(StoreBase): drm_free_only = False headquarters = 'US' formats = ['NOOK'] + affiliate = True class StoreBeamEBooksDEStore(StoreBase): name = 'Beam EBooks DE' @@ -1181,6 +1184,7 @@ class StoreBeWriteStore(StoreBase): drm_free_only = True headquarters = 'US' formats = ['EPUB', 'MOBI', 'PDF'] + affiliate = False class StoreDieselEbooksStore(StoreBase): name = 'Diesel eBooks' @@ -1190,6 +1194,7 @@ class StoreDieselEbooksStore(StoreBase): drm_free_only = False headquarters = 'US' formats = ['EPUB', 'PDF'] + affiliate = True class StoreEbookscomStore(StoreBase): name = 'eBooks.com' @@ -1199,6 +1204,18 @@ class StoreEbookscomStore(StoreBase): drm_free_only = False headquarters = 'US' formats = ['EPUB', 'LIT', 'MOBI', 'PDF'] + affiliate = True + +class StoreEPubBuyDEStore(StoreBase): + name = 'EPUBBuy DE' + author = 'Charles Haley' + description = u'Bei EPUBBuy.com finden Sie ausschliesslich eBooks im weitverbreiteten EPUB-Format und ohne DRM. So haben Sie die freie Wahl, wo Sie Ihr eBook lesen: Tablet, eBook-Reader, Smartphone oder einfach auf Ihrem PC. So macht eBook-Lesen Spaß!' + actual_plugin = 'calibre.gui2.store.epubbuy_de_plugin:EPubBuyDEStore' + + drm_free_only = True + headquarters = 'DE' + formats = ['EPUB'] + affiliate = True class StoreEBookShoppeUKStore(StoreBase): name = 'ebookShoppe UK' @@ -1211,16 +1228,6 @@ class StoreEBookShoppeUKStore(StoreBase): formats = ['EPUB', 'PDF'] affiliate = True -class StoreEPubBuyDEStore(StoreBase): - name = 'EPUBBuy DE' - author = 'Charles Haley' - description = u'Bei EPUBBuy.com finden Sie ausschliesslich eBooks im weitverbreiteten EPUB-Format und ohne DRM. So haben Sie die freie Wahl, wo Sie Ihr eBook lesen: Tablet, eBook-Reader, Smartphone oder einfach auf Ihrem PC. So macht eBook-Lesen Spaß!' - actual_plugin = 'calibre.gui2.store.epubbuy_de_plugin:EPubBuyDEStore' - - drm_free_only = True - headquarters = 'DE' - formats = ['EPUB'] - class StoreEHarlequinStore(StoreBase): name = 'eHarlequin' description = u'A global leader in series romance and one of the world\'s leading publishers of books for women. Offers women a broad range of reading from romance to bestseller fiction, from young adult novels to erotic literature, from nonfiction to fantasy, from African-American novels to inspirational romance, and more.' @@ -1229,6 +1236,7 @@ class StoreEHarlequinStore(StoreBase): drm_free_only = False headquarters = 'CA' formats = ['EPUB', 'PDF'] + affiliate = True class StoreFeedbooksStore(StoreBase): name = 'Feedbooks' @@ -1238,6 +1246,7 @@ class StoreFeedbooksStore(StoreBase): drm_free_only = False headquarters = 'FR' formats = ['EPUB', 'MOBI', 'PDF'] + affiliate = False class StoreFoylesUKStore(StoreBase): name = 'Foyles UK' @@ -1259,6 +1268,7 @@ class StoreGandalfStore(StoreBase): drm_free_only = False headquarters = 'PL' formats = ['EPUB', 'PDF'] + affiliate = False class StoreGoogleBooksStore(StoreBase): name = 'Google Books' @@ -1268,6 +1278,7 @@ class StoreGoogleBooksStore(StoreBase): drm_free_only = False headquarters = 'US' formats = ['EPUB', 'PDF', 'TXT'] + affiliate = False class StoreGutenbergStore(StoreBase): name = 'Project Gutenberg' @@ -1277,6 +1288,7 @@ class StoreGutenbergStore(StoreBase): drm_free_only = True headquarters = 'US' formats = ['EPUB', 'HTML', 'MOBI', 'PDB', 'TXT'] + affiliate = False class StoreKoboStore(StoreBase): name = 'Kobo' @@ -1286,6 +1298,7 @@ class StoreKoboStore(StoreBase): drm_free_only = False headquarters = 'CA' formats = ['EPUB'] + affiliate = True class StoreLegimiStore(StoreBase): name = 'Legimi' @@ -1296,6 +1309,7 @@ class StoreLegimiStore(StoreBase): drm_free_only = False headquarters = 'PL' formats = ['EPUB'] + affiliate = False class StoreManyBooksStore(StoreBase): name = 'ManyBooks' @@ -1305,6 +1319,7 @@ class StoreManyBooksStore(StoreBase): drm_free_only = True headquarters = 'US' formats = ['EPUB', 'FB2', 'JAR', 'LIT', 'LRF', 'MOBI', 'PDB', 'PDF', 'RB', 'RTF', 'TCR', 'TXT', 'ZIP'] + affiliate = False class StoreMobileReadStore(StoreBase): name = 'MobileRead' @@ -1314,6 +1329,7 @@ class StoreMobileReadStore(StoreBase): drm_free_only = True headquarters = 'CH' formats = ['EPUB', 'IMP', 'LRF', 'LIT', 'MOBI', 'PDF'] + affiliate = False class StoreNextoStore(StoreBase): name = 'Nexto' @@ -1324,6 +1340,7 @@ class StoreNextoStore(StoreBase): drm_free_only = False headquarters = 'PL' formats = ['EPUB', 'PDF'] + affiliate = True class StoreOpenLibraryStore(StoreBase): name = 'Open Library' @@ -1333,6 +1350,7 @@ class StoreOpenLibraryStore(StoreBase): drm_free_only = True headquarters = 'US' formats = ['DAISY', 'DJVU', 'EPUB', 'MOBI', 'PDF', 'TXT'] + affiliate = False class StoreOReillyStore(StoreBase): name = 'OReilly' @@ -1342,6 +1360,7 @@ class StoreOReillyStore(StoreBase): drm_free_only = True headquarters = 'US' formats = ['APK', 'DAISY', 'EPUB', 'MOBI', 'PDF'] + affiliate = False class StorePragmaticBookshelfStore(StoreBase): name = 'Pragmatic Bookshelf' @@ -1351,6 +1370,7 @@ class StorePragmaticBookshelfStore(StoreBase): drm_free_only = True headquarters = 'US' formats = ['EPUB', 'MOBI', 'PDF'] + affiliate = False class StoreSmashwordsStore(StoreBase): name = 'Smashwords' @@ -1360,6 +1380,7 @@ class StoreSmashwordsStore(StoreBase): drm_free_only = True headquarters = 'US' formats = ['EPUB', 'HTML', 'LRF', 'MOBI', 'PDB', 'RTF', 'TXT'] + affiliate = True class StoreVirtualoStore(StoreBase): name = 'Virtualo' @@ -1370,6 +1391,7 @@ class StoreVirtualoStore(StoreBase): drm_free_only = False headquarters = 'PL' formats = ['EPUB', 'PDF'] + affiliate = False class StoreWaterstonesUKStore(StoreBase): name = 'Waterstones UK' @@ -1380,6 +1402,7 @@ class StoreWaterstonesUKStore(StoreBase): drm_free_only = False headquarters = 'UK' formats = ['EPUB', 'PDF'] + affiliate = False class StoreWeightlessBooksStore(StoreBase): name = 'Weightless Books' @@ -1389,6 +1412,18 @@ class StoreWeightlessBooksStore(StoreBase): drm_free_only = True headquarters = 'US' formats = ['EPUB', 'HTML', 'LIT', 'MOBI', 'PDF'] + affiliate = False + +class StoreWHSmithUKStore(StoreBase): + name = 'WH Smith UK' + author = 'Charles Haley' + description = u"With over 550 stores on the high street and 490 stores at airports, train stations, hospitals and motorway services, WHSmith is one of the UK's leading retail groups and a household name." + actual_plugin = 'calibre.gui2.store.whsmith_uk_plugin:WHSmithUKStore' + + drm_free_only = False + headquarters = 'UK' + formats = ['EPUB', 'PDF'] + affiliate = False class StoreWizardsTowerBooksStore(StoreBase): name = 'Wizards Tower Books' @@ -1398,6 +1433,7 @@ class StoreWizardsTowerBooksStore(StoreBase): drm_free_only = True headquarters = 'UK' formats = ['EPUB', 'MOBI'] + affiliate = False class StoreWoblinkStore(StoreBase): name = 'Woblink' @@ -1408,6 +1444,7 @@ class StoreWoblinkStore(StoreBase): drm_free_only = False headquarters = 'PL' formats = ['EPUB'] + affiliate = False plugins += [ StoreArchiveOrgStore, @@ -1420,7 +1457,7 @@ plugins += [ StoreBeWriteStore, StoreDieselEbooksStore, StoreEbookscomStore, - #StoreEBookShoppeUKStore, + StoreEBookShoppeUKStore, StoreEPubBuyDEStore, StoreEHarlequinStore, StoreFeedbooksStore, @@ -1440,6 +1477,7 @@ plugins += [ StoreVirtualoStore, StoreWaterstonesUKStore, StoreWeightlessBooksStore, + StoreWHSmithUKStore, StoreWizardsTowerBooksStore, StoreWoblinkStore ] diff --git a/src/calibre/gui2/store/ebookshoppe_uk_plugin.py b/src/calibre/gui2/store/ebookshoppe_uk_plugin.py index b45b0e99d5..21bef85db9 100644 --- a/src/calibre/gui2/store/ebookshoppe_uk_plugin.py +++ b/src/calibre/gui2/store/ebookshoppe_uk_plugin.py @@ -62,18 +62,14 @@ class EBookShoppeUKStore(BasicStoreConfig, StorePlugin): 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 = '' + self.get_author_and_formats(s, timeout) yield s - def get_details(self, search_result, timeout): + def get_author_and_formats(self, search_result, timeout): br = browser() with closing(br.open(search_result.detail_item, timeout=timeout)) as nf: idata = html.fromstring(nf.read()) diff --git a/src/calibre/gui2/store/whsmith_uk_plugin.py b/src/calibre/gui2/store/whsmith_uk_plugin.py new file mode 100644 index 0000000000..66d81258f7 --- /dev/null +++ b/src/calibre/gui2/store/whsmith_uk_plugin.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__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 WHSmithUKStore(BasicStoreConfig, StorePlugin): + + def open(self, parent=None, detail_item=None, external=False): + url = 'http://www.whsmith.co.uk/' + url_details = '' + + if external or self.config.get('open_external', False): + if detail_item: + url = url_details + detail_item + open_url(QUrl(url)) + else: + detail_url = None + if detail_item: + detail_url = url_details + 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.whsmith.co.uk/CatalogAndSearch/SearchWithinCategory.aspx' + '?cat=\Books\eb_eBooks&gq=' + 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('//div[@class="product-search"]/' + 'div[contains(@id, "whsSearchResultItem")]'): + if counter <= 0: + break + + id = ''.join(data.xpath('.//a[contains(@id, "labelProductTitle")]/@href')) + if not id: + continue + cover_url = ''.join(data.xpath('.//a[contains(@id, "hlinkProductImage")]/img/@src')) + title = ''.join(data.xpath('.//a[contains(@id, "labelProductTitle")]/text()')) + author = ', '.join(data.xpath('.//div[@class="author"]/h3/span/text()')) + price = ''.join(data.xpath('.//span[contains(@id, "labelProductPrice")]/text()')) + pdf = data.xpath('boolean(.//span[contains(@id, "labelFormatText") and ' + 'contains(., "PDF")])') + epub = data.xpath('boolean(.//span[contains(@id, "labelFormatText") and ' + 'contains(., "ePub")])') + counter -= 1 + + s = SearchResult() + s.cover_url = cover_url + s.title = title.strip() + s.author = author.strip() + s.price = price + s.drm = SearchResult.DRM_LOCKED + s.detail_item = id + formats = [] + if epub: + formats.append('ePub') + if pdf: + formats.append('PDF') + s.formats = ', '.join(formats) + + yield s From 66571550e9181a39debcbdad8ded2c80d89886d7 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 28 May 2011 16:57:37 +0100 Subject: [PATCH 2/3] Add the heart to the search store chooser --- src/calibre/customize/builtins.py | 2 +- src/calibre/gui2/store/search/search.py | 28 ++++++++++++++++--------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index f1afb62cbc..cd5f81067f 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -1417,7 +1417,7 @@ class StoreWeightlessBooksStore(StoreBase): class StoreWHSmithUKStore(StoreBase): name = 'WH Smith UK' author = 'Charles Haley' - description = u"With over 550 stores on the high street and 490 stores at airports, train stations, hospitals and motorway services, WHSmith is one of the UK's leading retail groups and a household name." + description = u"Shop for savings on Books, discounted Magazine subscriptions and great prices on Stationery, Toys & Games" actual_plugin = 'calibre.gui2.store.whsmith_uk_plugin:WHSmithUKStore' drm_free_only = False diff --git a/src/calibre/gui2/store/search/search.py b/src/calibre/gui2/store/search/search.py index faeaf507c9..7ce6c93c68 100644 --- a/src/calibre/gui2/store/search/search.py +++ b/src/calibre/gui2/store/search/search.py @@ -9,8 +9,8 @@ __docformat__ = 'restructuredtext en' import re from random import shuffle -from PyQt4.Qt import (Qt, QDialog, QDialogButtonBox, QTimer, QCheckBox, - QVBoxLayout, QIcon, QWidget, QTabWidget) +from PyQt4.Qt import (Qt, QDialog, QDialogButtonBox, QTimer, QCheckBox, QLabel, + QVBoxLayout, QIcon, QWidget, QTabWidget, QGridLayout) from calibre.gui2 import JSONConfig, info_dialog from calibre.gui2.progress_indicator import ProgressIndicator @@ -80,7 +80,7 @@ class SearchDialog(QDialog, Ui_Dialog): self.progress_checker.start(100) self.restore_state() - + def setup_store_checks(self): # Add check boxes for each store so the user # can disable searching specific stores on a @@ -88,18 +88,26 @@ class SearchDialog(QDialog, Ui_Dialog): existing = {} for n in self.store_checks: existing[n] = self.store_checks[n].isChecked() - + self.store_checks = {} stores_check_widget = QWidget() - store_list_layout = QVBoxLayout() + store_list_layout = QGridLayout() stores_check_widget.setLayout(store_list_layout) - for x in sorted(self.gui.istores.keys(), key=lambda x: x.lower()): + + icon = QIcon(I('donate.png')) + i = 0 # just in case the list of stores is empty + for i, x in enumerate(sorted(self.gui.istores.keys(), key=lambda x: x.lower())): cbox = QCheckBox(x) cbox.setChecked(existing.get(x, False)) - store_list_layout.addWidget(cbox) + store_list_layout.addWidget(cbox, i, 0, 1, 1) + if self.gui.istores[x].base_plugin.affiliate: + iw = QLabel(self) + iw.setPixmap(icon.pixmap(16, 16)) + store_list_layout.addWidget(iw, i, 1, 1, 1) self.store_checks[x] = cbox - store_list_layout.addStretch() + i += 1 + store_list_layout.setRowStretch(i, 10) self.store_list.setWidget(stores_check_widget) def build_adv_search(self): @@ -250,14 +258,14 @@ class SearchDialog(QDialog, Ui_Dialog): button_box.accepted.connect(d.accept) button_box.rejected.connect(d.reject) d.setWindowTitle(_('Customize get books search')) - + tab_widget = QTabWidget(d) v.addWidget(tab_widget) v.addWidget(button_box) chooser_config_widget = StoreChooserWidget() search_config_widget = StoreConfigWidget(self.config) - + tab_widget.addTab(chooser_config_widget, _('Choose stores')) tab_widget.addTab(search_config_widget, _('Configure search')) From 3a93e4561ce20dd76bfb1f6af3593b9989818f2b Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 28 May 2011 18:59:30 +0100 Subject: [PATCH 3/3] Fix amazon UK grid. Add tooltip to heart on menu. --- src/calibre/gui2/store/amazon_uk_plugin.py | 84 ++++++++++++++++++++++ src/calibre/gui2/store/search/search.py | 1 + 2 files changed, 85 insertions(+) diff --git a/src/calibre/gui2/store/amazon_uk_plugin.py b/src/calibre/gui2/store/amazon_uk_plugin.py index 9544add17c..1448e1548a 100644 --- a/src/calibre/gui2/store/amazon_uk_plugin.py +++ b/src/calibre/gui2/store/amazon_uk_plugin.py @@ -6,11 +6,17 @@ __license__ = 'GPL 3' __copyright__ = '2011, John Schember ' __docformat__ = 'restructuredtext en' +import urllib +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.amazon_plugin import AmazonKindleStore +from calibre.gui2.store.search_result import SearchResult class AmazonUKKindleStore(AmazonKindleStore): ''' @@ -28,3 +34,81 @@ class AmazonUKKindleStore(AmazonKindleStore): aff_id['asin'] = detail_item store_link = 'http://www.amazon.co.uk/gp/redirect.html?ie=UTF8&location=http://www.amazon.co.uk/dp/%(asin)s&tag=%(tag)s&linkCode=ur2&camp=1634&creative=6738' % aff_id open_url(QUrl(store_link)) + + def search(self, query, max_results=10, timeout=60): + url = self.search_url + urllib.quote_plus(query) + br = browser() + + counter = max_results + with closing(br.open(url, timeout=timeout)) as f: + doc = html.fromstring(f.read()) + + # Amazon has two results pages. + is_shot = doc.xpath('boolean(//div[@id="shotgunMainResults"])') + # Horizontal grid of books. + if is_shot: + data_xpath = '//div[contains(@class, "result")]' + cover_xpath = './/div[@class="productTitle"]//img/@src' + # Vertical list of books. + else: + data_xpath = '//div[contains(@class, "product")]' + cover_xpath = './div[@class="productImage"]/a/img/@src' + + for data in doc.xpath(data_xpath): + if counter <= 0: + break + + # We must have an asin otherwise we can't easily reference the + # book later. + asin = ''.join(data.xpath('./@name')) + if not asin: + continue + cover_url = ''.join(data.xpath(cover_xpath)) + + title = ''.join(data.xpath('.//div[@class="productTitle"]/a/text()')) + price = ''.join(data.xpath('.//div[@class="newPrice"]/span/text()')) + + counter -= 1 + + s = SearchResult() + s.cover_url = cover_url.strip() + s.title = title.strip() + s.price = price.strip() + s.detail_item = asin.strip() + s.formats = 'Kindle' + + if is_shot: + # Amazon UK does not include the author on the grid layout + s.author = '' + self.get_details(s, timeout) + else: + author = ''.join(data.xpath('.//div[@class="productTitle"]/span[@class="ptBrand"]/text()')) + s.author = author.split(' by ')[-1].strip() + + yield s + + def get_details(self, search_result, timeout): + # We might already have been called. + if search_result.drm: + return + + url = self.details_url + + br = browser() + with closing(br.open(url + search_result.detail_item, timeout=timeout)) as nf: + idata = html.fromstring(nf.read()) + if not search_result.author: + search_result.author = ''.join(idata.xpath('//div[@class="buying" and contains(., "Author")]/a/text()')) + if idata.xpath('boolean(//div[@class="content"]//li/b[contains(text(), "' + + self.drm_search_text + '")])'): + if idata.xpath('boolean(//div[@class="content"]//li[contains(., "' + + self.drm_free_text + '") and contains(b, "' + + self.drm_search_text + '")])'): + search_result.drm = SearchResult.DRM_UNLOCKED + else: + search_result.drm = SearchResult.DRM_UNKNOWN + else: + search_result.drm = SearchResult.DRM_LOCKED + return True + + diff --git a/src/calibre/gui2/store/search/search.py b/src/calibre/gui2/store/search/search.py index 7ce6c93c68..9e60223a3d 100644 --- a/src/calibre/gui2/store/search/search.py +++ b/src/calibre/gui2/store/search/search.py @@ -103,6 +103,7 @@ class SearchDialog(QDialog, Ui_Dialog): store_list_layout.addWidget(cbox, i, 0, 1, 1) if self.gui.istores[x].base_plugin.affiliate: iw = QLabel(self) + iw.setToolTip(_('Buying from this store supports a calibre developer')) iw.setPixmap(icon.pixmap(16, 16)) store_list_layout.addWidget(iw, i, 1, 1, 1) self.store_checks[x] = cbox