From 02c48304e2b648de5bcbaf5a77b8d8f3a14aa2ce Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 26 Apr 2011 10:34:46 +0100 Subject: [PATCH 1/9] Add amazon_uk_plugin.py to repository --- src/calibre/customize/builtins.py | 8 +- src/calibre/gui2/store/amazon_uk_plugin.py | 197 +++++++++++++++++++++ 2 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 src/calibre/gui2/store/amazon_uk_plugin.py diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index c27fa2a57b..4f426d7490 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -1109,6 +1109,11 @@ class StoreAmazonKindleStore(StoreBase): description = _('Kindle books from Amazon') actual_plugin = 'calibre.gui2.store.amazon_plugin:AmazonKindleStore' +class StoreAmazonUKKindleStore(StoreBase): + name = 'Amazon UK Kindle' + description = _('Kindle books from Amazon.uk') + actual_plugin = 'calibre.gui2.store.amazon_uk_plugin:AmazonUKKindleStore' + class StoreBaenWebScriptionStore(StoreBase): name = 'Baen WebScription' description = _('Ebooks for readers.') @@ -1174,7 +1179,8 @@ class StoreSmashwordsStore(StoreBase): description = _('Your ebook. Your way.') actual_plugin = 'calibre.gui2.store.smashwords_plugin:SmashwordsStore' -plugins += [StoreAmazonKindleStore, StoreBaenWebScriptionStore, StoreBNStore, +plugins += [StoreAmazonKindleStore, StoreAmazonUKKindleStore, + StoreBaenWebScriptionStore, StoreBNStore, StoreBeWriteStore, StoreDieselEbooksStore, StoreEbookscomStore, StoreEHarlequinStoretore, StoreFeedbooksStore, StoreGutenbergStore, StoreKoboStore, StoreManyBooksStore, diff --git a/src/calibre/gui2/store/amazon_uk_plugin.py b/src/calibre/gui2/store/amazon_uk_plugin.py new file mode 100644 index 0000000000..a68e4611f0 --- /dev/null +++ b/src/calibre/gui2/store/amazon_uk_plugin.py @@ -0,0 +1,197 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +import random +import re +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.search_result import SearchResult + +class AmazonKindleStore(StorePlugin): + + def open(self, parent=None, detail_item=None, external=False): + ''' + Amazon comes with a number of difficulties. + + QWebView has major issues with Amazon.com. The largest of + issues is it simply doesn't work on a number of pages. + + When connecting to a number parts of Amazon.com (Kindle library + for instance) QNetworkAccessManager fails to connect with a + NetworkError of 399 - ProtocolFailure. The strange thing is, + when I check QNetworkRequest.HttpStatusCodeAttribute when the + 399 error is returned the status code is 200 (Ok). However, once + the QNetworkAccessManager decides there was a NetworkError it + does not download the page from Amazon. So I can't even set the + HTML in the QWebView myself. + + There is http://bugreports.qt.nokia.com/browse/QTWEBKIT-259 an + open bug about the issue but it is not correct. We can set the + useragent (Arora does) to something else and the above issue + will persist. This http://developer.qt.nokia.com/forums/viewthread/793 + gives a bit more information about the issue but as of now (27/Feb/2011) + there is no solution or work around. + + We cannot change the The linkDelegationPolicy to allow us to avoid + QNetworkAccessManager because it only works links. Forms aren't + included so the same issue persists on any part of the site (login) + that use a form to load a new page. + + Using an aStore was evaluated but I've decided against using it. + There are three major issues with an aStore. Because checkout is + handled by sending the user to Amazon we can't put it in a QWebView. + If we're sending the user to Amazon sending them there directly is + nicer. Also, we cannot put the aStore in a QWebView and let it open the + redirection the users default browser because the cookies with the + shopping cart won't transfer. + + Another issue with the aStore is how it handles the referral. It only + counts the referral for the items in the shopping card / the item + that directed the user to Amazon. Kindle books do not use the shopping + cart and send the user directly to Amazon for the purchase. In this + instance we would only get referral credit for the one book that the + aStore directs to Amazon that the user buys. Any other purchases we + won't get credit for. + + The last issue with the aStore is performance. Even though it's an + Amazon site it's alow. So much slower than Amazon.com that it makes + me not want to browse books using it. The look and feel are lesser + issues. So is the fact that it almost seems like the purchase is + with calibre. This can cause some support issues because we can't + do much for issues with Amazon.com purchase hiccups. + + Another option that was evaluated was the Product Advertising API. + The reasons against this are complexity. It would take a lot of work + to basically re-create Amazon.com within calibre. The Product + Advertising API is also designed with being run on a server not + in an app. The signing keys would have to be made avaliable to ever + calibre user which means bad things could be done with our account. + + The Product Advertising API also assumes the same browser for easy + shopping cart transfer to Amazon. With QWebView not working and there + not being an easy way to transfer cookies between a QWebView and the + users default browser this won't work well. + + We could create our own website on the calibre server and create an + Amazon Product Advertising API store. However, this goes back to the + complexity argument. Why spend the time recreating Amazon.com + + The final and largest issue against using the Product Advertising API + is the Efficiency Guidelines: + + "Each account used to access the Product Advertising API will be allowed + an initial usage limit of 2,000 requests per hour. Each account will + receive an additional 500 requests per hour (up to a maximum of 25,000 + requests per hour) for every $1 of shipped item revenue driven per hour + in a trailing 30-day period. Usage thresholds are recalculated daily based + on revenue performance." + + With over two million users a limit of 2,000 request per hour could + render our store unusable for no other reason than Amazon rate + limiting our traffic. + + The best (I use the term lightly here) solution is to open Amazon.com + in the users default browser and set the affiliate id as part of the url. + ''' + aff_id = {'tag': 'josbl0e-cpb-20'} + # Use Kovid's affiliate id 30% of the time. + if random.randint(1, 10) in (1, 2, 3): + aff_id['tag'] = 'calibrebs-20' + store_link = 'http://www.amazon.com/Kindle-eBooks/b/?ie=UTF&node=1286228011&ref_=%(tag)s&ref=%(tag)s&tag=%(tag)s&linkCode=ur2&camp=1789&creative=390957' % aff_id + if detail_item: + aff_id['asin'] = detail_item + store_link = 'http://www.amazon.com/dp/%(asin)s/?tag=%(tag)s' % aff_id + open_url(QUrl(store_link)) + + def search(self, query, max_results=10, timeout=60): + url = 'http://www.amazon.com/s/url=search-alias%3Ddigital-text&field-keywords=' + 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="productData"]'): + if counter <= 0: + break + + # Even though we are searching digital-text only Amazon will still + # put in results for non Kindle books (author pages). Se we need + # to explicitly check if the item is a Kindle book and ignore it + # if it isn't. + type = ''.join(data.xpath('//span[@class="format"]/text()')) + if 'kindle' not in type.lower(): + continue + + # We must have an asin otherwise we can't easily reference the + # book later. + asin_href = None + asin_a = data.xpath('div[@class="productTitle"]/a[1]') + if asin_a: + asin_href = asin_a[0].get('href', '') + m = re.search(r'/dp/(?P.+?)(/|$)', asin_href) + if m: + asin = m.group('asin') + else: + continue + else: + continue + + cover_url = '' + if asin_href: + cover_img = data.xpath('//div[@class="productImage"]/a[@href="%s"]/img/@src' % asin_href) + if cover_img: + cover_url = cover_img[0] + parts = cover_url.split('/') + bn = parts[-1] + f, _, ext = bn.rpartition('.') + if '_' in f: + bn = f.partition('_')[0]+'_SL160_.'+ext + parts[-1] = bn + cover_url = '/'.join(parts) + + title = ''.join(data.xpath('div[@class="productTitle"]/a/text()')) + author = ''.join(data.xpath('div[@class="productTitle"]/span[@class="ptBrand"]/text()')) + author = author.split('by')[-1] + price = ''.join(data.xpath('div[@class="newPrice"]/span/text()')) + + counter -= 1 + + s = SearchResult() + s.cover_url = cover_url + s.title = title.strip() + s.author = author.strip() + s.price = price.strip() + s.detail_item = asin.strip() + s.formats = 'Kindle' + + yield s + + def get_details(self, search_result, timeout): + url = 'http://amazon.com/dp/' + + br = browser() + with closing(br.open(url + search_result.detail_item, timeout=timeout)) as nf: + idata = html.fromstring(nf.read()) + if idata.xpath('boolean(//div[@class="content"]//li/b[contains(text(), "Simultaneous Device Usage")])'): + if idata.xpath('boolean(//div[@class="content"]//li[contains(., "Unlimited") and contains(b, "Simultaneous Device Usage")])'): + search_result.drm = SearchResult.DRM_UNLOCKED + else: + search_result.drm = SearchResult.DRM_UNKNOWN + else: + search_result.drm = SearchResult.DRM_LOCKED + return True + + From 5398c0fc64b68fb67cf30285cb746431aa0488e5 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 26 Apr 2011 11:17:50 +0100 Subject: [PATCH 2/9] First cut at amazon.uk store plugin. --- src/calibre/gui2/store/amazon_plugin.py | 6 +- src/calibre/gui2/store/amazon_uk_plugin.py | 188 ++------------------- 2 files changed, 15 insertions(+), 179 deletions(-) diff --git a/src/calibre/gui2/store/amazon_plugin.py b/src/calibre/gui2/store/amazon_plugin.py index a68e4611f0..55fb613288 100644 --- a/src/calibre/gui2/store/amazon_plugin.py +++ b/src/calibre/gui2/store/amazon_plugin.py @@ -116,8 +116,9 @@ class AmazonKindleStore(StorePlugin): store_link = 'http://www.amazon.com/dp/%(asin)s/?tag=%(tag)s' % aff_id open_url(QUrl(store_link)) + search_url = 'http://www.amazon.com/s/url=search-alias%3Ddigital-text&field-keywords=' def search(self, query, max_results=10, timeout=60): - url = 'http://www.amazon.com/s/url=search-alias%3Ddigital-text&field-keywords=' + urllib2.quote(query) + url = self.search_url + urllib2.quote(query) br = browser() counter = max_results @@ -179,8 +180,9 @@ class AmazonKindleStore(StorePlugin): yield s + details_url = 'http://amazon.com/dp/' def get_details(self, search_result, timeout): - url = 'http://amazon.com/dp/' + url = self.details_url br = browser() with closing(br.open(url + search_result.detail_item, timeout=timeout)) as nf: diff --git a/src/calibre/gui2/store/amazon_uk_plugin.py b/src/calibre/gui2/store/amazon_uk_plugin.py index a68e4611f0..e24ddb485e 100644 --- a/src/calibre/gui2/store/amazon_uk_plugin.py +++ b/src/calibre/gui2/store/amazon_uk_plugin.py @@ -6,192 +6,26 @@ __license__ = 'GPL 3' __copyright__ = '2011, John Schember ' __docformat__ = 'restructuredtext en' -import random -import re -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.search_result import SearchResult +from calibre.gui2.store.amazon_plugin import AmazonKindleStore -class AmazonKindleStore(StorePlugin): +class AmazonUKKindleStore(AmazonKindleStore): + + ''' + For comments on the implementation, please see amazon_plugin.py + ''' def open(self, parent=None, detail_item=None, external=False): - ''' - Amazon comes with a number of difficulties. - - QWebView has major issues with Amazon.com. The largest of - issues is it simply doesn't work on a number of pages. - - When connecting to a number parts of Amazon.com (Kindle library - for instance) QNetworkAccessManager fails to connect with a - NetworkError of 399 - ProtocolFailure. The strange thing is, - when I check QNetworkRequest.HttpStatusCodeAttribute when the - 399 error is returned the status code is 200 (Ok). However, once - the QNetworkAccessManager decides there was a NetworkError it - does not download the page from Amazon. So I can't even set the - HTML in the QWebView myself. - - There is http://bugreports.qt.nokia.com/browse/QTWEBKIT-259 an - open bug about the issue but it is not correct. We can set the - useragent (Arora does) to something else and the above issue - will persist. This http://developer.qt.nokia.com/forums/viewthread/793 - gives a bit more information about the issue but as of now (27/Feb/2011) - there is no solution or work around. - - We cannot change the The linkDelegationPolicy to allow us to avoid - QNetworkAccessManager because it only works links. Forms aren't - included so the same issue persists on any part of the site (login) - that use a form to load a new page. - - Using an aStore was evaluated but I've decided against using it. - There are three major issues with an aStore. Because checkout is - handled by sending the user to Amazon we can't put it in a QWebView. - If we're sending the user to Amazon sending them there directly is - nicer. Also, we cannot put the aStore in a QWebView and let it open the - redirection the users default browser because the cookies with the - shopping cart won't transfer. - - Another issue with the aStore is how it handles the referral. It only - counts the referral for the items in the shopping card / the item - that directed the user to Amazon. Kindle books do not use the shopping - cart and send the user directly to Amazon for the purchase. In this - instance we would only get referral credit for the one book that the - aStore directs to Amazon that the user buys. Any other purchases we - won't get credit for. - - The last issue with the aStore is performance. Even though it's an - Amazon site it's alow. So much slower than Amazon.com that it makes - me not want to browse books using it. The look and feel are lesser - issues. So is the fact that it almost seems like the purchase is - with calibre. This can cause some support issues because we can't - do much for issues with Amazon.com purchase hiccups. - - Another option that was evaluated was the Product Advertising API. - The reasons against this are complexity. It would take a lot of work - to basically re-create Amazon.com within calibre. The Product - Advertising API is also designed with being run on a server not - in an app. The signing keys would have to be made avaliable to ever - calibre user which means bad things could be done with our account. - - The Product Advertising API also assumes the same browser for easy - shopping cart transfer to Amazon. With QWebView not working and there - not being an easy way to transfer cookies between a QWebView and the - users default browser this won't work well. - - We could create our own website on the calibre server and create an - Amazon Product Advertising API store. However, this goes back to the - complexity argument. Why spend the time recreating Amazon.com - - The final and largest issue against using the Product Advertising API - is the Efficiency Guidelines: - - "Each account used to access the Product Advertising API will be allowed - an initial usage limit of 2,000 requests per hour. Each account will - receive an additional 500 requests per hour (up to a maximum of 25,000 - requests per hour) for every $1 of shipped item revenue driven per hour - in a trailing 30-day period. Usage thresholds are recalculated daily based - on revenue performance." - - With over two million users a limit of 2,000 request per hour could - render our store unusable for no other reason than Amazon rate - limiting our traffic. - - The best (I use the term lightly here) solution is to open Amazon.com - in the users default browser and set the affiliate id as part of the url. - ''' - aff_id = {'tag': 'josbl0e-cpb-20'} - # Use Kovid's affiliate id 30% of the time. - if random.randint(1, 10) in (1, 2, 3): - aff_id['tag'] = 'calibrebs-20' - store_link = 'http://www.amazon.com/Kindle-eBooks/b/?ie=UTF&node=1286228011&ref_=%(tag)s&ref=%(tag)s&tag=%(tag)s&linkCode=ur2&camp=1789&creative=390957' % aff_id + aff_id = {'tag': 'calcharles-21'} + store_link = 'http://www.amazon.co.uk/Kindle-eBooks/b/?ie=UTF&node=1286228011&ref_=%(tag)s&ref=%(tag)s&tag=%(tag)s&linkCode=ur2&camp=1789&creative=390957' % aff_id if detail_item: aff_id['asin'] = detail_item - store_link = 'http://www.amazon.com/dp/%(asin)s/?tag=%(tag)s' % aff_id + store_link = 'http://www.amazon.co.uk/dp/%(asin)s/?tag=%(tag)s' % aff_id open_url(QUrl(store_link)) - def search(self, query, max_results=10, timeout=60): - url = 'http://www.amazon.com/s/url=search-alias%3Ddigital-text&field-keywords=' + 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="productData"]'): - if counter <= 0: - break - - # Even though we are searching digital-text only Amazon will still - # put in results for non Kindle books (author pages). Se we need - # to explicitly check if the item is a Kindle book and ignore it - # if it isn't. - type = ''.join(data.xpath('//span[@class="format"]/text()')) - if 'kindle' not in type.lower(): - continue - - # We must have an asin otherwise we can't easily reference the - # book later. - asin_href = None - asin_a = data.xpath('div[@class="productTitle"]/a[1]') - if asin_a: - asin_href = asin_a[0].get('href', '') - m = re.search(r'/dp/(?P.+?)(/|$)', asin_href) - if m: - asin = m.group('asin') - else: - continue - else: - continue - - cover_url = '' - if asin_href: - cover_img = data.xpath('//div[@class="productImage"]/a[@href="%s"]/img/@src' % asin_href) - if cover_img: - cover_url = cover_img[0] - parts = cover_url.split('/') - bn = parts[-1] - f, _, ext = bn.rpartition('.') - if '_' in f: - bn = f.partition('_')[0]+'_SL160_.'+ext - parts[-1] = bn - cover_url = '/'.join(parts) - - title = ''.join(data.xpath('div[@class="productTitle"]/a/text()')) - author = ''.join(data.xpath('div[@class="productTitle"]/span[@class="ptBrand"]/text()')) - author = author.split('by')[-1] - price = ''.join(data.xpath('div[@class="newPrice"]/span/text()')) - - counter -= 1 - - s = SearchResult() - s.cover_url = cover_url - s.title = title.strip() - s.author = author.strip() - s.price = price.strip() - s.detail_item = asin.strip() - s.formats = 'Kindle' - - yield s - - def get_details(self, search_result, timeout): - url = 'http://amazon.com/dp/' - - br = browser() - with closing(br.open(url + search_result.detail_item, timeout=timeout)) as nf: - idata = html.fromstring(nf.read()) - if idata.xpath('boolean(//div[@class="content"]//li/b[contains(text(), "Simultaneous Device Usage")])'): - if idata.xpath('boolean(//div[@class="content"]//li[contains(., "Unlimited") and contains(b, "Simultaneous Device Usage")])'): - search_result.drm = SearchResult.DRM_UNLOCKED - else: - search_result.drm = SearchResult.DRM_UNKNOWN - else: - search_result.drm = SearchResult.DRM_LOCKED - return True - + search_url = 'http://www.amazon.co.uk/s/url=search-alias%3Ddigital-text&field-keywords=' + details_url = 'http://amazon.co.uk/dp/' From 335b933c2ba46d6d8e6518b47e8725be4232d039 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 26 Apr 2011 12:45:43 +0100 Subject: [PATCH 3/9] First iteration of waterstones store plugin --- src/calibre/customize/builtins.py | 8 +- .../gui2/store/waterstones_uk_plugin.py | 73 +++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 src/calibre/gui2/store/waterstones_uk_plugin.py diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 4f426d7490..1e885baf0c 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -1179,11 +1179,17 @@ class StoreSmashwordsStore(StoreBase): description = _('Your ebook. Your way.') actual_plugin = 'calibre.gui2.store.smashwords_plugin:SmashwordsStore' +class StoreWaterstonesUKStore(StoreBase): + name = 'Waterstones UK' + description = _('Feel every word') + actual_plugin = 'calibre.gui2.store.waterstones_uk_plugin:WaterstonesUKStore' + plugins += [StoreAmazonKindleStore, StoreAmazonUKKindleStore, StoreBaenWebScriptionStore, StoreBNStore, StoreBeWriteStore, StoreDieselEbooksStore, StoreEbookscomStore, StoreEHarlequinStoretore, StoreFeedbooksStore, StoreGutenbergStore, StoreKoboStore, StoreManyBooksStore, - StoreMobileReadStore, StoreOpenLibraryStore, StoreSmashwordsStore] + StoreMobileReadStore, StoreOpenLibraryStore, StoreSmashwordsStore, + StoreWaterstonesUKStore] # }}} diff --git a/src/calibre/gui2/store/waterstones_uk_plugin.py b/src/calibre/gui2/store/waterstones_uk_plugin.py new file mode 100644 index 0000000000..6c0f415891 --- /dev/null +++ b/src/calibre/gui2/store/waterstones_uk_plugin.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +import random +import re +import urllib2 +from contextlib import closing + +from lxml import html + +from PyQt4.Qt import QUrl + +from calibre import browser, url_slash_cleaner +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 WaterstonesUKStore(BasicStoreConfig, StorePlugin): + + def open(self, parent=None, detail_item=None, external=False): + url = 'http://clkuk.tradedoubler.com/click?p=51196&a=1951604&g=19333484' + + if external or self.config.get('open_external', False): + if detail_item: + url = url + detail_item + open_url(QUrl(url_slash_cleaner(url))) + else: + detail_url = None + if detail_item: + detail_url = url + 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.waterstones.com/waterstonesweb/advancedSearch.do?buttonClicked=1&format=3757&bookkeywords=' + 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[contains(@class, "results-pane")]'): + print('here') + if counter <= 0: + break + + cover_url = ''.join(data.xpath('.//div[@class="image"]/a/img/@src')) + + title = ''.join(data.xpath('./div/div/h2/a/text()')) + author = ', '.join(data.xpath('.//p[@class="byAuthor"]/a/text()')) + price = ''.join(data.xpath('.//p[@class="price"]/span[@class="priceStandard"]/text()')) + print(title, author, price) + + 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.formats = 'EPUB' + + yield s From 97300dee5b74c9135dc6a86758246b57f673ac2e Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 26 Apr 2011 13:09:08 +0100 Subject: [PATCH 4/9] Fixes for waterstone store plugin --- src/calibre/gui2/store/waterstones_uk_plugin.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/calibre/gui2/store/waterstones_uk_plugin.py b/src/calibre/gui2/store/waterstones_uk_plugin.py index 6c0f415891..a066979f25 100644 --- a/src/calibre/gui2/store/waterstones_uk_plugin.py +++ b/src/calibre/gui2/store/waterstones_uk_plugin.py @@ -6,8 +6,6 @@ __license__ = 'GPL 3' __copyright__ = '2011, John Schember ' __docformat__ = 'restructuredtext en' -import random -import re import urllib2 from contextlib import closing @@ -15,7 +13,7 @@ from lxml import html from PyQt4.Qt import QUrl -from calibre import browser, url_slash_cleaner +from calibre import browser from calibre.gui2 import open_url from calibre.gui2.store import StorePlugin from calibre.gui2.store.basic_config import BasicStoreConfig @@ -29,12 +27,12 @@ class WaterstonesUKStore(BasicStoreConfig, StorePlugin): if external or self.config.get('open_external', False): if detail_item: - url = url + detail_item - open_url(QUrl(url_slash_cleaner(url))) + url = detail_item + open_url(QUrl(url)) else: detail_url = None if detail_item: - detail_url = url + detail_item + detail_url = detail_item d = WebStoreDialog(self.gui, url, parent, detail_url) d.setWindowTitle(self.name) d.set_tags(self.config.get('tags', '')) @@ -49,12 +47,13 @@ class WaterstonesUKStore(BasicStoreConfig, StorePlugin): with closing(br.open(url, timeout=timeout)) as f: doc = html.fromstring(f.read()) for data in doc.xpath('//div[contains(@class, "results-pane")]'): - print('here') if counter <= 0: break + id = ''.join(data.xpath('./div/div/h2/a/@href')).strip() + if not id: + continue cover_url = ''.join(data.xpath('.//div[@class="image"]/a/img/@src')) - title = ''.join(data.xpath('./div/div/h2/a/text()')) author = ', '.join(data.xpath('.//p[@class="byAuthor"]/a/text()')) price = ''.join(data.xpath('.//p[@class="price"]/span[@class="priceStandard"]/text()')) @@ -68,6 +67,7 @@ class WaterstonesUKStore(BasicStoreConfig, StorePlugin): s.author = author.strip() s.price = price s.drm = SearchResult.DRM_LOCKED + s.detail_item = id s.formats = 'EPUB' yield s From 996c9e77088c199bddf61a76b3749909a79996b6 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 26 Apr 2011 14:35:54 +0100 Subject: [PATCH 5/9] Add Foyles. Fix waterstones to use deep linking. --- src/calibre/customize/builtins.py | 10 ++- src/calibre/gui2/store/foyles_uk_plugin.py | 75 +++++++++++++++++++ .../gui2/store/waterstones_uk_plugin.py | 6 +- 3 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 src/calibre/gui2/store/foyles_uk_plugin.py diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 1e885baf0c..29b9954caa 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -1184,12 +1184,16 @@ class StoreWaterstonesUKStore(StoreBase): description = _('Feel every word') actual_plugin = 'calibre.gui2.store.waterstones_uk_plugin:WaterstonesUKStore' -plugins += [StoreAmazonKindleStore, StoreAmazonUKKindleStore, - StoreBaenWebScriptionStore, StoreBNStore, +class StoreFoylesUKStore(StoreBase): + name = 'Foyles UK' + description = _('Foyles of London, online') + actual_plugin = 'calibre.gui2.store.foyles_uk_plugin:FoylesUKStore' + +plugins += [StoreAmazonKindleStore, StoreBaenWebScriptionStore, StoreBNStore, StoreBeWriteStore, StoreDieselEbooksStore, StoreEbookscomStore, StoreEHarlequinStoretore, StoreFeedbooksStore, StoreGutenbergStore, StoreKoboStore, StoreManyBooksStore, StoreMobileReadStore, StoreOpenLibraryStore, StoreSmashwordsStore, - StoreWaterstonesUKStore] + StoreAmazonUKKindleStore, StoreFoylesUKStore, StoreWaterstonesUKStore] # }}} diff --git a/src/calibre/gui2/store/foyles_uk_plugin.py b/src/calibre/gui2/store/foyles_uk_plugin.py new file mode 100644 index 0000000000..134f710ef0 --- /dev/null +++ b/src/calibre/gui2/store/foyles_uk_plugin.py @@ -0,0 +1,75 @@ +# -*- 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, url_slash_cleaner +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 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_redirect = 'http://www.foyles.co.uk' + + if external or self.config.get('open_external', False): + if detail_item: + url = url + url_redirect + detail_item + open_url(QUrl(url_slash_cleaner(url))) + else: + detail_url = None + if detail_item: + detail_url = url + url_redirect + 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.foyles.co.uk/Public/Shop/Search.aspx?fFacetId=1015&searchBy=1&quick=true&term=' + 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('//table[contains(@id, "MainContent")]/tr/td/div[contains(@class, "Item")]'): + if counter <= 0: + break + id = ''.join(data.xpath('.//a[@class="Title"]/@href')).strip() + if not id: + continue + + cover_url = ''.join(data.xpath('.//div[@class="image"]/a/img/@src')) + + title = ''.join(data.xpath('.//a[@class="Title"]/text()')) + author = ', '.join(data.xpath('.//span[@class="Author"]/text()')) + price = ''.join(data.xpath('./ul/li[@class="Strong"]/text()')) + price = price[price.rfind(' '):] + + counter -= 1 + + s = SearchResult() + s.cover_url = cover_url + s.title = title.strip() + s.author = author.strip() + s.price = price + s.detail_item = id + s.drm = SearchResult.DRM_LOCKED + s.formats = 'EPUB' + + yield s diff --git a/src/calibre/gui2/store/waterstones_uk_plugin.py b/src/calibre/gui2/store/waterstones_uk_plugin.py index a066979f25..198b855393 100644 --- a/src/calibre/gui2/store/waterstones_uk_plugin.py +++ b/src/calibre/gui2/store/waterstones_uk_plugin.py @@ -24,15 +24,17 @@ class WaterstonesUKStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): url = 'http://clkuk.tradedoubler.com/click?p=51196&a=1951604&g=19333484' + url_details = 'http://clkuk.tradedoubler.com/click?p(51196)a(1951604)g(16460516)url({0})' if external or self.config.get('open_external', False): if detail_item: - url = detail_item + url = url_details.format(detail_item) open_url(QUrl(url)) else: detail_url = None if detail_item: - detail_url = detail_item + detail_url = url_details.format(detail_item) + print(detail_url) d = WebStoreDialog(self.gui, url, parent, detail_url) d.setWindowTitle(self.name) d.set_tags(self.config.get('tags', '')) From 81bd4d1a6e1b2a63be9afc824941b1e29a8aa267 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 26 Apr 2011 14:47:20 +0100 Subject: [PATCH 6/9] Remove some print statements --- src/calibre/gui2/store/waterstones_uk_plugin.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/calibre/gui2/store/waterstones_uk_plugin.py b/src/calibre/gui2/store/waterstones_uk_plugin.py index 198b855393..6870f4cf86 100644 --- a/src/calibre/gui2/store/waterstones_uk_plugin.py +++ b/src/calibre/gui2/store/waterstones_uk_plugin.py @@ -34,7 +34,6 @@ class WaterstonesUKStore(BasicStoreConfig, StorePlugin): detail_url = None if detail_item: detail_url = url_details.format(detail_item) - print(detail_url) d = WebStoreDialog(self.gui, url, parent, detail_url) d.setWindowTitle(self.name) d.set_tags(self.config.get('tags', '')) @@ -59,7 +58,6 @@ class WaterstonesUKStore(BasicStoreConfig, StorePlugin): title = ''.join(data.xpath('./div/div/h2/a/text()')) author = ', '.join(data.xpath('.//p[@class="byAuthor"]/a/text()')) price = ''.join(data.xpath('.//p[@class="price"]/span[@class="priceStandard"]/text()')) - print(title, author, price) counter -= 1 From 2811b452fbc1e9ba9991939b7746be193f877a9e Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 26 Apr 2011 16:09:17 +0100 Subject: [PATCH 7/9] Fix links. --- src/calibre/gui2/store/amazon_uk_plugin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/store/amazon_uk_plugin.py b/src/calibre/gui2/store/amazon_uk_plugin.py index e24ddb485e..7e128dc62f 100644 --- a/src/calibre/gui2/store/amazon_uk_plugin.py +++ b/src/calibre/gui2/store/amazon_uk_plugin.py @@ -20,10 +20,11 @@ class AmazonUKKindleStore(AmazonKindleStore): def open(self, parent=None, detail_item=None, external=False): aff_id = {'tag': 'calcharles-21'} - store_link = 'http://www.amazon.co.uk/Kindle-eBooks/b/?ie=UTF&node=1286228011&ref_=%(tag)s&ref=%(tag)s&tag=%(tag)s&linkCode=ur2&camp=1789&creative=390957' % aff_id + store_link = 'http://www.amazon.co.uk/gp/redirect.html?ie=UTF8&location=http://www.amazon.co.uk/Kindle-eBooks/b?ie=UTF8&node=341689031&ref_=sa_menu_kbo2&tag=%(tag)s&linkCode=ur2&camp=1634&creative=19450' % aff_id + if detail_item: aff_id['asin'] = detail_item - store_link = 'http://www.amazon.co.uk/dp/%(asin)s/?tag=%(tag)s' % aff_id + 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)) search_url = 'http://www.amazon.co.uk/s/url=search-alias%3Ddigital-text&field-keywords=' From 7169ebe7d94d12d71c1e217ae88fee9f9ae18ead Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 26 Apr 2011 18:05:14 +0100 Subject: [PATCH 8/9] amazon.de store plugin --- src/calibre/customize/builtins.py | 10 +++++-- src/calibre/gui2/store/amazon_de_plugin.py | 31 ++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 src/calibre/gui2/store/amazon_de_plugin.py diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 29b9954caa..8973548b9d 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -1189,11 +1189,17 @@ class StoreFoylesUKStore(StoreBase): description = _('Foyles of London, online') actual_plugin = 'calibre.gui2.store.foyles_uk_plugin:FoylesUKStore' -plugins += [StoreAmazonKindleStore, StoreBaenWebScriptionStore, StoreBNStore, +class AmazonDEKindleStore(StoreBase): + name = 'Amazon DE Kindle' + description = _('Kindle eBooks') + actual_plugin = 'calibre.gui2.store.amazon_de_plugin:AmazonDEKindleStore' + +plugins += [StoreAmazonKindleStore, AmazonDEKindleStore, StoreAmazonUKKindleStore, + StoreBaenWebScriptionStore, StoreBNStore, StoreBeWriteStore, StoreDieselEbooksStore, StoreEbookscomStore, StoreEHarlequinStoretore, StoreFeedbooksStore, StoreGutenbergStore, StoreKoboStore, StoreManyBooksStore, StoreMobileReadStore, StoreOpenLibraryStore, StoreSmashwordsStore, - StoreAmazonUKKindleStore, StoreFoylesUKStore, StoreWaterstonesUKStore] + StoreFoylesUKStore, StoreWaterstonesUKStore] # }}} diff --git a/src/calibre/gui2/store/amazon_de_plugin.py b/src/calibre/gui2/store/amazon_de_plugin.py new file mode 100644 index 0000000000..94583f8987 --- /dev/null +++ b/src/calibre/gui2/store/amazon_de_plugin.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + + +from PyQt4.Qt import QUrl + +from calibre.gui2 import open_url +from calibre.gui2.store.amazon_plugin import AmazonKindleStore + +class AmazonDEKindleStore(AmazonKindleStore): + + ''' + For comments on the implementation, please see amazon_plugin.py + ''' + + def open(self, parent=None, detail_item=None, external=False): + aff_id = {'tag': 'charhale0a-21'} + store_link = 'http://www.amazon.de/gp/redirect.html?ie=UTF8&location=http://www.amazon.de/&site-redirect=de&tag=%(tag)s&linkCode=ur2&camp=1638&creative=6742' % aff_id + if detail_item: + aff_id['asin'] = detail_item + store_link = 'http://www.amazon.de/gp/redirect.html?ie=UTF8&location=http://www.amazon.de/dp/%(asin)s&site-redirect=de&tag=%(tag)s&linkCode=ur2&camp=1638&creative=6742' % aff_id + open_url(QUrl(store_link)) + + search_url = 'http://www.amazon.de/s/url=search-alias%3Ddigital-text&field-keywords=' + details_url = 'http://amazon.de/dp/' + From 061fbd44c3423f2498edf165dae8ae504d61682f Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 27 Apr 2011 10:21:15 +0100 Subject: [PATCH 9/9] 1) Fix cover problem with FoylesUKStore 2) Add PDF format and DRM verification to WaterstonesUKStore 3) Make store URL go to kindle store for amazon_de_plugin.py 4) Change amazon_plugin.py and amazon_de_plugin.py to recognize DRM words in German --- src/calibre/gui2/store/amazon_de_plugin.py | 13 +++++++++---- src/calibre/gui2/store/amazon_plugin.py | 9 +++++++-- src/calibre/gui2/store/foyles_uk_plugin.py | 5 ++++- src/calibre/gui2/store/waterstones_uk_plugin.py | 15 +++++++++++++-- 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/calibre/gui2/store/amazon_de_plugin.py b/src/calibre/gui2/store/amazon_de_plugin.py index 94583f8987..2baa90360c 100644 --- a/src/calibre/gui2/store/amazon_de_plugin.py +++ b/src/calibre/gui2/store/amazon_de_plugin.py @@ -6,7 +6,6 @@ __license__ = 'GPL 3' __copyright__ = '2011, John Schember ' __docformat__ = 'restructuredtext en' - from PyQt4.Qt import QUrl from calibre.gui2 import open_url @@ -20,12 +19,18 @@ class AmazonDEKindleStore(AmazonKindleStore): def open(self, parent=None, detail_item=None, external=False): aff_id = {'tag': 'charhale0a-21'} - store_link = 'http://www.amazon.de/gp/redirect.html?ie=UTF8&location=http://www.amazon.de/&site-redirect=de&tag=%(tag)s&linkCode=ur2&camp=1638&creative=6742' % aff_id + store_link = ('http://www.amazon.de/gp/redirect.html?ie=UTF8&site-redirect=de' + '&tag=%(tag)s&linkCode=ur2&camp=1638&creative=19454' + '&location=http://www.amazon.de/ebooks-kindle/b?node=530886031') % aff_id if detail_item: aff_id['asin'] = detail_item - store_link = 'http://www.amazon.de/gp/redirect.html?ie=UTF8&location=http://www.amazon.de/dp/%(asin)s&site-redirect=de&tag=%(tag)s&linkCode=ur2&camp=1638&creative=6742' % aff_id + store_link = ('http://www.amazon.de/gp/redirect.html?ie=UTF8' + '&location=http://www.amazon.de/dp/%(asin)s&site-redirect=de' + '&tag=%(tag)s&linkCode=ur2&camp=1638&creative=6742') % aff_id open_url(QUrl(store_link)) search_url = 'http://www.amazon.de/s/url=search-alias%3Ddigital-text&field-keywords=' - details_url = 'http://amazon.de/dp/' + details_url = 'http://amazon.de/dp/' + drm_search_text = u'Gleichzeitige Verwendung von Geräten' + drm_free_text = u'Keine Einschränkung' \ No newline at end of file diff --git a/src/calibre/gui2/store/amazon_plugin.py b/src/calibre/gui2/store/amazon_plugin.py index 55fb613288..179c6ffaef 100644 --- a/src/calibre/gui2/store/amazon_plugin.py +++ b/src/calibre/gui2/store/amazon_plugin.py @@ -181,14 +181,19 @@ class AmazonKindleStore(StorePlugin): yield s details_url = 'http://amazon.com/dp/' + drm_search_text = u'Simultaneous Device Usage' + drm_free_text = u'Unlimited' def get_details(self, search_result, timeout): 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 idata.xpath('boolean(//div[@class="content"]//li/b[contains(text(), "Simultaneous Device Usage")])'): - if idata.xpath('boolean(//div[@class="content"]//li[contains(., "Unlimited") and contains(b, "Simultaneous Device Usage")])'): + 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 diff --git a/src/calibre/gui2/store/foyles_uk_plugin.py b/src/calibre/gui2/store/foyles_uk_plugin.py index 134f710ef0..b2ad3e5de1 100644 --- a/src/calibre/gui2/store/foyles_uk_plugin.py +++ b/src/calibre/gui2/store/foyles_uk_plugin.py @@ -54,7 +54,10 @@ class FoylesUKStore(BasicStoreConfig, StorePlugin): if not id: continue - cover_url = ''.join(data.xpath('.//div[@class="image"]/a/img/@src')) + cover_url = ''.join(data.xpath('.//a[@class="Jacket"]/img/@src')) + if cover_url: + cover_url = 'http://www.foyles.co.uk' + cover_url + print(cover_url) title = ''.join(data.xpath('.//a[@class="Title"]/text()')) author = ', '.join(data.xpath('.//span[@class="Author"]/text()')) diff --git a/src/calibre/gui2/store/waterstones_uk_plugin.py b/src/calibre/gui2/store/waterstones_uk_plugin.py index 6870f4cf86..d422165c47 100644 --- a/src/calibre/gui2/store/waterstones_uk_plugin.py +++ b/src/calibre/gui2/store/waterstones_uk_plugin.py @@ -58,6 +58,9 @@ class WaterstonesUKStore(BasicStoreConfig, StorePlugin): title = ''.join(data.xpath('./div/div/h2/a/text()')) author = ', '.join(data.xpath('.//p[@class="byAuthor"]/a/text()')) price = ''.join(data.xpath('.//p[@class="price"]/span[@class="priceStandard"]/text()')) + drm = data.xpath('boolean(.//td[@headers="productFormat" and contains(., "DRM")])') + pdf = data.xpath('boolean(.//td[@headers="productFormat" and contains(., "PDF")])') + epub = data.xpath('boolean(.//td[@headers="productFormat" and contains(., "EPUB")])') counter -= 1 @@ -66,8 +69,16 @@ class WaterstonesUKStore(BasicStoreConfig, StorePlugin): s.title = title.strip() s.author = author.strip() s.price = price - s.drm = SearchResult.DRM_LOCKED + if drm: + s.drm = SearchResult.DRM_LOCKED + else: + s.drm = SearchResult.DRM_UNKNOWN s.detail_item = id - s.formats = 'EPUB' + formats = [] + if epub: + formats.append('EPUB') + if pdf: + formats.append('PDF') + s.formats = ', '.join(formats) yield s