From 58ca9bc7d0cb8e35641573af395b35d644a227ab Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 3 Jul 2011 10:05:43 -0400 Subject: [PATCH 1/7] Store: Change class name of opensearch store to allow for easily adding more opensearch results for other feed types. Document opensearch module changes. --- src/calibre/gui2/store/opensearch_store.py | 6 ++- .../gui2/store/stores/archive_org_plugin.py | 8 ++-- .../gui2/store/stores/epubbud_plugin.py | 6 +-- .../gui2/store/stores/feedbooks_plugin.py | 6 +-- .../stores/pragmatic_bookshelf_plugin.py | 6 +-- src/calibre/utils/opensearch/__init__.py | 37 +++++++++++++++++++ 6 files changed, 55 insertions(+), 14 deletions(-) diff --git a/src/calibre/gui2/store/opensearch_store.py b/src/calibre/gui2/store/opensearch_store.py index 54fedbd002..6e8f5de7ba 100644 --- a/src/calibre/gui2/store/opensearch_store.py +++ b/src/calibre/gui2/store/opensearch_store.py @@ -22,7 +22,7 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog from calibre.utils.opensearch.description import Description from calibre.utils.opensearch.query import Query -class OpenSearchStore(StorePlugin): +class OpenSearchOPDSStore(StorePlugin): open_search_url = '' web_url = '' @@ -99,3 +99,7 @@ class OpenSearchStore(StorePlugin): yield s + +class OpenSearchOPDSDetailStore(OpenSearchOPDSStore): + + pass diff --git a/src/calibre/gui2/store/stores/archive_org_plugin.py b/src/calibre/gui2/store/stores/archive_org_plugin.py index 6972c604ce..7439056baa 100644 --- a/src/calibre/gui2/store/stores/archive_org_plugin.py +++ b/src/calibre/gui2/store/stores/archive_org_plugin.py @@ -6,12 +6,11 @@ __license__ = 'GPL 3' __copyright__ = '2011, John Schember ' __docformat__ = 'restructuredtext en' - from calibre.gui2.store.basic_config import BasicStoreConfig -from calibre.gui2.store.opensearch_store import OpenSearchStore +from calibre.gui2.store.opensearch_store import OpenSearchOPDSStore from calibre.gui2.store.search_result import SearchResult -class ArchiveOrgStore(BasicStoreConfig, OpenSearchStore): +class ArchiveOrgStore(BasicStoreConfig, OpenSearchOPDSStore): open_search_url = 'http://bookserver.archive.org/catalog/opensearch.xml' web_url = 'http://www.archive.org/details/texts' @@ -19,7 +18,7 @@ class ArchiveOrgStore(BasicStoreConfig, OpenSearchStore): # http://bookserver.archive.org/catalog/ def search(self, query, max_results=10, timeout=60): - for s in OpenSearchStore.search(self, query, max_results, timeout): + for s in OpenSearchOPDSStore.search(self, query, max_results, timeout): s.detail_item = 'http://www.archive.org/details/' + s.detail_item.split(':')[-1] s.price = '$0.00' s.drm = SearchResult.DRM_UNLOCKED @@ -33,6 +32,7 @@ class ArchiveOrgStore(BasicStoreConfig, OpenSearchStore): from calibre import browser from contextlib import closing from lxml import html + 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/stores/epubbud_plugin.py b/src/calibre/gui2/store/stores/epubbud_plugin.py index b4d642f62b..029b2b3fc9 100644 --- a/src/calibre/gui2/store/stores/epubbud_plugin.py +++ b/src/calibre/gui2/store/stores/epubbud_plugin.py @@ -7,10 +7,10 @@ __copyright__ = '2011, John Schember ' __docformat__ = 'restructuredtext en' from calibre.gui2.store.basic_config import BasicStoreConfig -from calibre.gui2.store.opensearch_store import OpenSearchStore +from calibre.gui2.store.opensearch_store import OpenSearchOPDSStore from calibre.gui2.store.search_result import SearchResult -class EpubBudStore(BasicStoreConfig, OpenSearchStore): +class EpubBudStore(BasicStoreConfig, OpenSearchOPDSStore): open_search_url = 'http://www.epubbud.com/feeds/opensearch.xml' web_url = 'http://www.epubbud.com/' @@ -18,7 +18,7 @@ class EpubBudStore(BasicStoreConfig, OpenSearchStore): # http://www.epubbud.com/feeds/catalog.atom def search(self, query, max_results=10, timeout=60): - for s in OpenSearchStore.search(self, query, max_results, timeout): + for s in OpenSearchOPDSStore.search(self, query, max_results, timeout): s.price = '$0.00' s.drm = SearchResult.DRM_UNLOCKED s.formats = 'EPUB' diff --git a/src/calibre/gui2/store/stores/feedbooks_plugin.py b/src/calibre/gui2/store/stores/feedbooks_plugin.py index 96d0a10dc7..cac44fd8df 100644 --- a/src/calibre/gui2/store/stores/feedbooks_plugin.py +++ b/src/calibre/gui2/store/stores/feedbooks_plugin.py @@ -7,10 +7,10 @@ __copyright__ = '2011, John Schember ' __docformat__ = 'restructuredtext en' from calibre.gui2.store.basic_config import BasicStoreConfig -from calibre.gui2.store.opensearch_store import OpenSearchStore +from calibre.gui2.store.opensearch_store import OpenSearchOPDSStore from calibre.gui2.store.search_result import SearchResult -class FeedbooksStore(BasicStoreConfig, OpenSearchStore): +class FeedbooksStore(BasicStoreConfig, OpenSearchOPDSStore): open_search_url = 'http://assets0.feedbooks.net/opensearch.xml?t=1253087147' web_url = 'http://feedbooks.com/' @@ -18,7 +18,7 @@ class FeedbooksStore(BasicStoreConfig, OpenSearchStore): # http://www.feedbooks.com/catalog def search(self, query, max_results=10, timeout=60): - for s in OpenSearchStore.search(self, query, max_results, timeout): + for s in OpenSearchOPDSStore.search(self, query, max_results, timeout): if s.downloads: s.drm = SearchResult.DRM_UNLOCKED s.price = '$0.00' diff --git a/src/calibre/gui2/store/stores/pragmatic_bookshelf_plugin.py b/src/calibre/gui2/store/stores/pragmatic_bookshelf_plugin.py index 671186ba87..99b94778bf 100644 --- a/src/calibre/gui2/store/stores/pragmatic_bookshelf_plugin.py +++ b/src/calibre/gui2/store/stores/pragmatic_bookshelf_plugin.py @@ -7,10 +7,10 @@ __copyright__ = '2011, John Schember ' __docformat__ = 'restructuredtext en' from calibre.gui2.store.basic_config import BasicStoreConfig -from calibre.gui2.store.opensearch_store import OpenSearchStore +from calibre.gui2.store.opensearch_store import OpenSearchOPDSStore from calibre.gui2.store.search_result import SearchResult -class PragmaticBookshelfStore(BasicStoreConfig, OpenSearchStore): +class PragmaticBookshelfStore(BasicStoreConfig, OpenSearchOPDSStore): open_search_url = 'http://pragprog.com/catalog/search-description' web_url = 'http://pragprog.com/' @@ -18,7 +18,7 @@ class PragmaticBookshelfStore(BasicStoreConfig, OpenSearchStore): # http://pragprog.com/catalog.opds def search(self, query, max_results=10, timeout=60): - for s in OpenSearchStore.search(self, query, max_results, timeout): + for s in OpenSearchOPDSStore.search(self, query, max_results, timeout): s.drm = SearchResult.DRM_UNLOCKED s.formats = 'EPUB, PDF, MOBI' yield s diff --git a/src/calibre/utils/opensearch/__init__.py b/src/calibre/utils/opensearch/__init__.py index e69de29bb2..3d0c4d8787 100644 --- a/src/calibre/utils/opensearch/__init__.py +++ b/src/calibre/utils/opensearch/__init__.py @@ -0,0 +1,37 @@ +''' +Based on the OpenSearch Python module by Ed Summers from +https://github.com/edsu/opensearch . + +This module is heavily modified and does not implement all the features from +the original. The ability for the the module to perform a search and retrieve +search results has been removed. The original module used a modified version +of the Universal feed parser from http://feedparser.org/ . The use of +FeedPaser made getting search results very slow. There is also a bug in the +modified FeedParser that causes the system to run out of file descriptors. + +Instead of fixing the modified feed parser it was decided to remove it and +manually parse the feeds in a set of type specific classes. This is much +faster and as we know in advance the feed format is simpler than using +FeedParser. Also, replacing the modified FeedParser with the newest version +of FeedParser caused some feeds to be parsed incorrectly and result in a loss +of data. + +The module was also rewritten to use lxml instead of MiniDom. + + +Usage: + +description = Description(open_search_url) +url_template = description.get_best_template() +if not url_template: + return +query = Query(url_template) + +# set up initial values. +query.searchTerms = urllib.quote_plus(search_terms) +# Note the count is ignored by some feeds. +query.count = max_results + +search_url = oquery.url() + +''' From 3e0797872c0eaa08f2a4f93927e16be87aa834ae Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 3 Jul 2011 10:59:54 -0400 Subject: [PATCH 2/7] Store: Manybooks uses opds feed (faster, more accurate, fixes covers not showing in many cases, fix formats list). Opensearch: support creating search urls from Stanza catalogs. Store: opensearch based classes don't need to quote the search terms as the opensearch module does this already. --- src/calibre/gui2/store/opensearch_store.py | 7 +- .../gui2/store/stores/manybooks_plugin.py | 144 ++++++++++-------- src/calibre/utils/opensearch/__init__.py | 2 +- src/calibre/utils/opensearch/description.py | 19 ++- 4 files changed, 96 insertions(+), 76 deletions(-) diff --git a/src/calibre/gui2/store/opensearch_store.py b/src/calibre/gui2/store/opensearch_store.py index 6e8f5de7ba..bcc92b25f1 100644 --- a/src/calibre/gui2/store/opensearch_store.py +++ b/src/calibre/gui2/store/opensearch_store.py @@ -7,7 +7,6 @@ __copyright__ = '2011, John Schember ' __docformat__ = 'restructuredtext en' import mimetypes -import urllib from contextlib import closing from lxml import etree @@ -50,7 +49,7 @@ class OpenSearchOPDSStore(StorePlugin): oquery = Query(url_template) # set up initial values - oquery.searchTerms = urllib.quote_plus(query) + oquery.searchTerms = query oquery.count = max_results url = oquery.url() @@ -99,7 +98,3 @@ class OpenSearchOPDSStore(StorePlugin): yield s - -class OpenSearchOPDSDetailStore(OpenSearchOPDSStore): - - pass diff --git a/src/calibre/gui2/store/stores/manybooks_plugin.py b/src/calibre/gui2/store/stores/manybooks_plugin.py index 829a97012f..c7dbf0a608 100644 --- a/src/calibre/gui2/store/stores/manybooks_plugin.py +++ b/src/calibre/gui2/store/stores/manybooks_plugin.py @@ -6,89 +6,101 @@ __license__ = 'GPL 3' __copyright__ = '2011, John Schember ' __docformat__ = 'restructuredtext en' -import re -import urllib +import mimetypes from contextlib import closing -from lxml import html +from lxml import etree -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 import browser from calibre.gui2.store.basic_config import BasicStoreConfig +from calibre.gui2.store.opensearch_store import OpenSearchOPDSStore from calibre.gui2.store.search_result import SearchResult -from calibre.gui2.store.web_store_dialog import WebStoreDialog +from calibre.utils.opensearch.description import Description +from calibre.utils.opensearch.query import Query -class ManyBooksStore(BasicStoreConfig, StorePlugin): +class ManyBooksStore(BasicStoreConfig, OpenSearchOPDSStore): - def open(self, parent=None, detail_item=None, external=False): - url = 'http://manybooks.net/' - - detail_url = None - if detail_item: - detail_url = url + detail_item - - if external or self.config.get('open_external', False): - open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url))) - else: - d = WebStoreDialog(self.gui, url, parent, detail_url) - d.setWindowTitle(self.name) - d.set_tags(self.config.get('tags', '')) - d.exec_() + open_search_url = 'http://www.manybooks.net/opds/' + web_url = 'http://manybooks.net' def search(self, query, max_results=10, timeout=60): - # ManyBooks website separates results for title and author. - # It also doesn't do a clear job of references authors and - # secondary titles. Google is also faster. - # Using a google search so we can search on both fields at once. - url = 'http://www.google.com/xhtml?q=site:manybooks.net+' + urllib.quote_plus(query) + ''' + Manybooks uses a very strange opds feed. The opds + main feed is structured like a stanza feed. The + search result entries give very little information + and requires you to go to a detail link. The detail + link has the wrong type specified (text/html instead + of application/atom+xml). + ''' + if not hasattr(self, 'open_search_url'): + return - br = browser() + description = Description(self.open_search_url) + url_template = description.get_best_template() + if not url_template: + return + oquery = Query(url_template) + # set up initial values + oquery.searchTerms = query + oquery.count = max_results + url = oquery.url() + counter = max_results + br = browser() with closing(br.open(url, timeout=timeout)) as f: - doc = html.fromstring(f.read()) - for data in doc.xpath('//div[@class="edewpi"]//div[@class="r ld"]'): + doc = etree.fromstring(f.read()) + for data in doc.xpath('//*[local-name() = "entry"]'): if counter <= 0: break - - url = '' - url_a = data.xpath('div[@class="jd"]/a') - if url_a: - url_a = url_a[0] - url = url_a.get('href', None) - if url: - url = url.split('u=')[-1][:-2] - if '/titles/' not in url: - continue - id = url.split('/')[-1] - id = id.strip() - - url_a = html.fromstring(html.tostring(url_a)) - heading = ''.join(url_a.xpath('//text()')) - title, _, author = heading.rpartition('by ') - author = author.split('-')[0] - price = '$0.00' - - cover_url = '' - mo = re.match('^\D+', id) - if mo: - cover_name = mo.group() - cover_name = cover_name.replace('etext', '') - cover_id = id.split('.')[0] - cover_url = 'http://www.manybooks.net/images/' + id[0] + '/' + cover_name + '/' + cover_id + '-thumb.jpg' - + counter -= 1 - + s = SearchResult() - s.cover_url = cover_url - s.title = title.strip() - s.author = author.strip() - s.price = price.strip() - s.detail_item = '/titles/' + id + + detail_links = data.xpath('./*[local-name() = "link" and @type = "text/html"]') + if not detail_links: + continue + detail_link = detail_links[0] + detail_href = detail_link.get('href') + if not detail_href: + continue + + s.detail_item = 'http://manybooks.net/titles/' + detail_href.split('tid=')[-1] + '.html' + # These can have HTML inside of them. We are going to get them again later + # just in case. + s.title = ''.join(data.xpath('./*[local-name() = "title"]//text()')).strip() + s.author = ', '.join(data.xpath('./*[local-name() = "author"]//text()')).strip() + + # Follow the detail link to get the rest of the info. + with closing(br.open(detail_href, timeout=timeout/4)) as df: + ddoc = etree.fromstring(df.read()) + ddata = ddoc.xpath('//*[local-name() = "entry"][1]') + if ddata: + ddata = ddata[0] + + # This is the real title and author info we want. We got + # it previously just in case it's not specified here for some reason. + s.title = ''.join(ddata.xpath('./*[local-name() = "title"]//text()')).strip() + s.author = ', '.join(ddata.xpath('./*[local-name() = "author"]//text()')).strip() + if s.author.startswith(','): + s.author = s.author[1:] + if s.author.endswith(','): + s.author = s.author[:-1] + + s.cover_url = ''.join(ddata.xpath('./*[local-name() = "link" and @rel = "http://opds-spec.org/thumbnail"][1]/@href')).strip() + + for link in ddata.xpath('./*[local-name() = "link" and @rel = "http://opds-spec.org/acquisition"]'): + type = link.get('type') + href = link.get('href') + if type: + ext = mimetypes.guess_extension(type) + if ext: + ext = ext[1:].upper().strip() + s.downloads[ext] = href + + s.price = '$0.00' s.drm = SearchResult.DRM_UNLOCKED - s.formts = 'EPUB, PDB (eReader, PalmDoc, zTXT, Plucker, iSilo), FB2, ZIP, AZW, MOBI, PRC, LIT, PKG, PDF, TXT, RB, RTF, LRF, TCR, JAR' + s.formats = 'EPUB, PDB (eReader, PalmDoc, zTXT, Plucker, iSilo), FB2, ZIP, AZW, MOBI, PRC, LIT, PKG, PDF, TXT, RB, RTF, LRF, TCR, JAR' yield s diff --git a/src/calibre/utils/opensearch/__init__.py b/src/calibre/utils/opensearch/__init__.py index 3d0c4d8787..62bd0e0236 100644 --- a/src/calibre/utils/opensearch/__init__.py +++ b/src/calibre/utils/opensearch/__init__.py @@ -28,7 +28,7 @@ if not url_template: query = Query(url_template) # set up initial values. -query.searchTerms = urllib.quote_plus(search_terms) +query.searchTerms = search_terms # Note the count is ignored by some feeds. query.count = max_results diff --git a/src/calibre/utils/opensearch/description.py b/src/calibre/utils/opensearch/description.py index 0b5afd8a7e..d5922d0c2b 100644 --- a/src/calibre/utils/opensearch/description.py +++ b/src/calibre/utils/opensearch/description.py @@ -40,7 +40,7 @@ class Description(object): with closing(br.open(url, timeout=15)) as f: doc = etree.fromstring(f.read()) - # version 1.1 has repeating Url elements + # version 1.1 has repeating Url elements. self.urls = [] for element in doc.xpath('//*[local-name() = "Url"]'): template = element.get('template') @@ -50,9 +50,22 @@ class Description(object): url.template = template url.type = type self.urls.append(url) + # Stanza catalogs. + for element in doc.xpath('//*[local-name() = "link"]'): + if element.get('rel') != 'search': + continue + href = element.get('href') + type = element.get('type') + if href and type: + url = URL() + url.template = href + url.type = type + self.urls.append(url) - # this is version 1.0 specific - self.url = ''.join(doc.xpath('//*[local-name() = "Url"][1]//text()')) + # this is version 1.0 specific. + self.url = '' + if not self.urls: + self.url = ''.join(doc.xpath('//*[local-name() = "Url"][1]//text()')) self.format = ''.join(doc.xpath('//*[local-name() = "Format"][1]//text()')) self.shortname = ''.join(doc.xpath('//*[local-name() = "ShortName"][1]//text()')) From 38e8eb3616b8ebe37748b2d671e7261a2ddd89fe Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 3 Jul 2011 11:08:13 -0400 Subject: [PATCH 3/7] Store: Use title and format to construct filename instead of relying on url. Makes the downloading notice look better and easier to understand what is being downloaded. --- src/calibre/gui2/store/search/search.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/store/search/search.py b/src/calibre/gui2/store/search/search.py index fd20669f09..f6fa423e23 100644 --- a/src/calibre/gui2/store/search/search.py +++ b/src/calibre/gui2/store/search/search.py @@ -349,7 +349,8 @@ class SearchDialog(QDialog, Ui_Dialog): 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]) + fname = result.title + '.' + ext.lower() + self.gui.download_ebook(result.downloads[ext], filename=fname) def open_store(self, result): self.gui.istores[result.store_name].open(self, result.detail_item, self.open_external.isChecked()) From 6df5c994d9955e7e0de057435a7703c2c378d6c1 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 3 Jul 2011 11:40:24 -0400 Subject: [PATCH 4/7] Store: Gutenberg, rewrite plugin to use gutenberg's search and allow for direct downloading. --- .../gui2/store/stores/gutenberg_plugin.py | 70 +++++++++---------- 1 file changed, 34 insertions(+), 36 deletions(-) diff --git a/src/calibre/gui2/store/stores/gutenberg_plugin.py b/src/calibre/gui2/store/stores/gutenberg_plugin.py index 85d1f3966a..ad30f2067d 100644 --- a/src/calibre/gui2/store/stores/gutenberg_plugin.py +++ b/src/calibre/gui2/store/stores/gutenberg_plugin.py @@ -6,6 +6,7 @@ __license__ = 'GPL 3' __copyright__ = '2011, John Schember ' __docformat__ = 'restructuredtext en' +import mimetypes import urllib from contextlib import closing @@ -23,70 +24,67 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog class GutenbergStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): - url = 'http://m.gutenberg.org/' - ext_url = 'http://gutenberg.org/' + url = 'http://gutenberg.org/' + + if detail_item: + detail_item = url_slash_cleaner(url + detail_item) if external or self.config.get('open_external', False): - if detail_item: - ext_url = ext_url + detail_item - open_url(QUrl(url_slash_cleaner(ext_url))) + open_url(QUrl(detail_item if detail_item else url)) else: - detail_url = None - if detail_item: - detail_url = url + detail_item - d = WebStoreDialog(self.gui, url, parent, detail_url) + d = WebStoreDialog(self.gui, url, parent, detail_item) d.setWindowTitle(self.name) d.set_tags(self.config.get('tags', '')) d.exec_() def search(self, query, max_results=10, timeout=60): - # Gutenberg's website does not allow searching both author and title. - # Using a google search so we can search on both fields at once. - url = 'http://www.google.com/xhtml?q=site:gutenberg.org+' + urllib.quote_plus(query) + url = 'http://m.gutenberg.org/ebooks/search.mobile/?default_prefix=all&sort_order=title&query=' + urllib.quote_plus(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="edewpi"]//div[@class="r ld"]'): + for data in doc.xpath('//ol[@class="results"]//li[contains(@class, "icon_title")]'): if counter <= 0: break + + id = ''.join(data.xpath('./a/@href')) + id = id.split('.mobile')[0] - url = '' - url_a = data.xpath('div[@class="jd"]/a') - if url_a: - url_a = url_a[0] - url = url_a.get('href', None) - if url: - url = url.split('u=')[-1].split('&')[0] - if '/ebooks/' not in url: - continue - id = url.split('/')[-1] - - url_a = html.fromstring(html.tostring(url_a)) - heading = ''.join(url_a.xpath('//text()')) - title, _, author = heading.rpartition('by ') - author = author.split('-')[0] - price = '$0.00' + title = ''.join(data.xpath('.//span[@class="title"]/text()')) + author = ''.join(data.xpath('.//span[@class="subtitle"]/text()')) counter -= 1 s = SearchResult() s.cover_url = '' + + s.detail_item = id.strip() s.title = title.strip() s.author = author.strip() - s.price = price.strip() - s.detail_item = '/ebooks/' + id.strip() + s.price = '$0.00' s.drm = SearchResult.DRM_UNLOCKED yield s def get_details(self, search_result, timeout): - url = 'http://m.gutenberg.org/' + url = url_slash_cleaner('http://m.gutenberg.org/' + search_result.detail_item + '.mobile') br = browser() - with closing(br.open(url + search_result.detail_item, timeout=timeout)) as nf: - idata = html.fromstring(nf.read()) - search_result.formats = ', '.join(idata.xpath('//a[@type!="application/atom+xml"]//span[@class="title"]/text()')) - return True \ No newline at end of file + with closing(br.open(url, timeout=timeout)) as nf: + doc = html.fromstring(nf.read()) + + for save_item in doc.xpath('//li[contains(@class, "icon_save")]/a'): + type = save_item.get('type') + href = save_item.get('href') + + if type: + ext = mimetypes.guess_extension(type) + if ext: + ext = ext[1:].upper().strip() + search_result.downloads[ext] = href + + search_result.formats = ', '.join(search_result.downloads.keys()) + + return True From 18c3d48a5ed4c998454644c8c31d40f32bbb66b3 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 3 Jul 2011 11:51:51 -0400 Subject: [PATCH 5/7] Store: Remove openlibrary plugin. It uses Archive.org as it's backend for digital texts. Since we filter out results that do not have downloadable files from open library we're left wit the same results given by the archive.org plugin. Thus making open library redundant and not necessary. --- src/calibre/customize/builtins.py | 9 -- .../gui2/store/stores/open_library_plugin.py | 84 ------------------- 2 files changed, 93 deletions(-) delete mode 100644 src/calibre/gui2/store/stores/open_library_plugin.py diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 4858b585ae..dcec4dbc6b 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -1387,15 +1387,6 @@ class StoreOpenBooksStore(StoreBase): drm_free_only = True headquarters = 'US' -class StoreOpenLibraryStore(StoreBase): - name = 'Open Library' - description = u'One web page for every book ever published. The goal is to be a true online library. Over 20 million records from a variety of large catalogs as well as single contributions, with more on the way.' - actual_plugin = 'calibre.gui2.store.stores.open_library_plugin:OpenLibraryStore' - - drm_free_only = True - headquarters = 'US' - formats = ['DAISY', 'DJVU', 'EPUB', 'MOBI', 'PDF', 'TXT'] - class StoreOReillyStore(StoreBase): name = 'OReilly' description = u'Programming and tech ebooks from OReilly.' diff --git a/src/calibre/gui2/store/stores/open_library_plugin.py b/src/calibre/gui2/store/stores/open_library_plugin.py deleted file mode 100644 index b95f1bf930..0000000000 --- a/src/calibre/gui2/store/stores/open_library_plugin.py +++ /dev/null @@ -1,84 +0,0 @@ -# -*- 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 OpenLibraryStore(BasicStoreConfig, StorePlugin): - - def open(self, parent=None, detail_item=None, external=False): - url = 'http://openlibrary.org/' - - 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://openlibrary.org/search?q=' + urllib2.quote(query) + '&has_fulltext=true' - - 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[@id="searchResults"]/ul[@id="siteSearch"]/li'): - if counter <= 0: - break - - # Don't include books that don't have downloadable files. - if not data.xpath('boolean(./span[@class="actions"]//span[@class="label" and contains(text(), "Read")])'): - continue - id = ''.join(data.xpath('./span[@class="bookcover"]/a/@href')) - if not id: - continue - cover_url = ''.join(data.xpath('./span[@class="bookcover"]/a/img/@src')) - - title = ''.join(data.xpath('.//h3[@class="booktitle"]/a[@class="results"]/text()')) - author = ''.join(data.xpath('.//span[@class="bookauthor"]/a/text()')) - price = '$0.00' - - counter -= 1 - - s = SearchResult() - s.cover_url = cover_url - s.title = title.strip() - s.author = author.strip() - s.price = price - s.detail_item = id.strip() - s.drm = SearchResult.DRM_UNLOCKED - - yield s - - def get_details(self, search_result, timeout): - url = 'http://openlibrary.org/' - - br = browser() - with closing(br.open(url_slash_cleaner(url + search_result.detail_item), timeout=timeout)) as nf: - idata = html.fromstring(nf.read()) - search_result.formats = ', '.join(list(set(idata.xpath('//a[contains(@title, "Download")]/text()')))) - return True From c535dbbabf2aa77480e47b852737d848ee7e96e2 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 3 Jul 2011 12:11:25 -0400 Subject: [PATCH 6/7] ... --- src/calibre/customize/builtins.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index dcec4dbc6b..82d1d2ff01 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -1505,7 +1505,6 @@ plugins += [ StoreMobileReadStore, StoreNextoStore, StoreOpenBooksStore, - StoreOpenLibraryStore, StoreOReillyStore, StorePragmaticBookshelfStore, StoreSmashwordsStore, From 33fa268fe7a88518bd1cab573639f63d179565eb Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 3 Jul 2011 12:45:31 -0400 Subject: [PATCH 7/7] Updates to quick start guide. --- resources/quick_start.epub | Bin 130640 -> 130575 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/resources/quick_start.epub b/resources/quick_start.epub index 3b289537a68d390883048ea0c099950fd46893c6..b1e793604ce19900042e954c3514aa94af5b8fed 100644 GIT binary patch delta 15289 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 Y02jcVjQ;_khW+yA(``Ne`Gz3>56j9ZrvLx| delta 15359 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