From 2cb241b205224202f01b452ed7310fca302c62cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20D=C5=82ugosz?= Date: Mon, 23 May 2011 22:56:39 +0200 Subject: [PATCH 01/10] legimi store --- src/calibre/customize/builtins.py | 11 ++++ src/calibre/gui2/store/legimi_plugin.py | 75 +++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 src/calibre/gui2/store/legimi_plugin.py diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 680e36d0f3..d9e8be00b5 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -1272,6 +1272,16 @@ class StoreKoboStore(StoreBase): headquarters = 'CA' formats = ['EPUB'] +class StoreLegimiStore(StoreBase): + name = 'Legimi' + author = u'Tomasz Długosz' + description = u'Tanie oraz darmowe ebooki, egazety i blogi w formacie EPUB, wprost na Twój e-czytnik, iPhone, iPad, Android i komputer' + actual_plugin = 'calibre.gui2.store.legimi_plugin:LegimiStore' + + drm_free_only = False + headquarters = 'PL' + formats = ['EPUB'] + class StoreManyBooksStore(StoreBase): name = 'ManyBooks' description = u'Public domain and creative commons works from many sources.' @@ -1393,6 +1403,7 @@ plugins += [ StoreGoogleBooksStore, StoreGutenbergStore, StoreKoboStore, + StoreLegimiStore, StoreManyBooksStore, StoreMobileReadStore, StoreNextoStore, diff --git a/src/calibre/gui2/store/legimi_plugin.py b/src/calibre/gui2/store/legimi_plugin.py new file mode 100644 index 0000000000..7212f0f394 --- /dev/null +++ b/src/calibre/gui2/store/legimi_plugin.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, Tomasz Długosz ' +__docformat__ = 'restructuredtext en' + +import re +import urllib +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 LegimiStore(BasicStoreConfig, StorePlugin): + + def open(self, parent=None, detail_item=None, external=False): + + url = 'http://www.legimi.com/pl/ebooks/?price=any' + detail_url = None + + if detail_item: + detail_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_() + + def search(self, query, max_results=10, timeout=60): + url = 'http://www.legimi.com/pl/ebooks/?price=any&lang=pl&search=' + urllib.quote_plus(query.encode('utf-8')) + '&sort=relevance' + + 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="list"]/ul/li'): + if counter <= 0: + break + + id = ''.join(data.xpath('.//div[@class="item_cover_container"]/a[1]/@href')) + if not id: + continue + + cover_url = ''.join(data.xpath('.//div[@class="item_cover_container"]/a/img/@src')) + title = ''.join(data.xpath('.//div[@class="item_entries"]/h2/a/text()')) + author = ''.join(data.xpath('.//div[@class="item_entries"]/span[1]/a/text()')) + price = ''.join(data.xpath('.//div[@class="item_entries"]/span[3]/text()')) + price = re.sub(r'[^0-9,]*','',price) + ' zł' + + counter -= 1 + + s = SearchResult() + s.cover_url = 'http://www.legimi.com/' + cover_url + s.title = title.strip() + s.author = author.strip() + s.price = price + s.detail_item = 'http://www.legimi.com/' + id.strip() + s.drm = SearchResult.DRM_LOCKED + s.formats = 'EPUB' + + yield s From b897f6dc119645bef217fa6191fe9e179d8b9a3a Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 24 May 2011 15:07:46 +0100 Subject: [PATCH 02/10] Changes to descriptions, addition to declined.txt --- src/calibre/customize/builtins.py | 56 ++++++++++++++--------------- src/calibre/gui2/store/declined.txt | 3 ++ 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 680e36d0f3..4c80c53d78 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -854,14 +854,14 @@ class ActionStore(InterfaceActionBase): name = 'Store' author = 'John Schember' actual_plugin = 'calibre.gui2.actions.store:StoreAction' - + def customization_help(self, gui=False): return 'Customize the behavior of the store search.' - + def config_widget(self): from calibre.gui2.store.config.store import config_widget as get_cw return get_cw() - + def save_settings(self, config_widget): from calibre.gui2.store.config.store import save_settings as save save(config_widget) @@ -1108,7 +1108,7 @@ class StoreAmazonKindleStore(StoreBase): name = 'Amazon Kindle' description = u'Kindle books from Amazon.' actual_plugin = 'calibre.gui2.store.amazon_plugin:AmazonKindleStore' - + drm_free_only = False headquarters = 'US' formats = ['KINDLE'] @@ -1118,7 +1118,7 @@ class StoreAmazonDEKindleStore(StoreBase): author = 'Charles Haley' description = u'Kindle Bücher von Amazon.' actual_plugin = 'calibre.gui2.store.amazon_de_plugin:AmazonDEKindleStore' - + drm_free_only = False headquarters = 'DE' formats = ['KINDLE'] @@ -1128,7 +1128,7 @@ class StoreAmazonUKKindleStore(StoreBase): author = 'Charles Haley' description = u'Kindle books from Amazon\'s UK web site. Also, includes French language ebooks.' actual_plugin = 'calibre.gui2.store.amazon_uk_plugin:AmazonUKKindleStore' - + drm_free_only = False headquarters = 'UK' formats = ['KINDLE'] @@ -1146,7 +1146,7 @@ class StoreBaenWebScriptionStore(StoreBase): name = 'Baen WebScription' description = u'Sci-Fi & Fantasy brought to you by Jim Baen.' actual_plugin = 'calibre.gui2.store.baen_webscription_plugin:BaenWebScriptionStore' - + drm_free_only = True headquarters = 'US' formats = ['EPUB', 'LIT', 'LRF', 'MOBI', 'RB', 'RTF', 'ZIP'] @@ -1155,7 +1155,7 @@ class StoreBNStore(StoreBase): name = 'Barnes and Noble' description = u'The world\'s largest book seller. As the ultimate destination for book lovers, Barnes & Noble.com offers an incredible array of content.' actual_plugin = 'calibre.gui2.store.bn_plugin:BNStore' - + drm_free_only = False headquarters = 'US' formats = ['NOOK'] @@ -1163,9 +1163,9 @@ class StoreBNStore(StoreBase): class StoreBeamEBooksDEStore(StoreBase): name = 'Beam EBooks DE' author = 'Charles Haley' - description = u'Der eBook Shop.' + description = u'Bei uns finden Sie: Tausende deutschsprachige eBooks; Alle eBooks ohne hartes DRM; PDF, ePub und Mobipocket Format; Sofortige Verfügbarkeit - 24 Stunden am Tag; Günstige Preise; eBooks für viele Lesegeräte, PC,Mac und Smartphones; Viele Gratis eBooks' actual_plugin = 'calibre.gui2.store.beam_ebooks_de_plugin:BeamEBooksDEStore' - + drm_free_only = True headquarters = 'DE' formats = ['EPUB', 'MOBI', 'PDF'] @@ -1174,7 +1174,7 @@ class StoreBeWriteStore(StoreBase): name = 'BeWrite Books' description = u'Publishers of fine books. Highly selective and editorially driven. Does not offer: books for children or exclusively YA, erotica, swords-and-sorcery fantasy and space-opera-style science fiction. All other genres are represented.' actual_plugin = 'calibre.gui2.store.bewrite_plugin:BeWriteStore' - + drm_free_only = True headquarters = 'US' formats = ['EPUB', 'MOBI', 'PDF'] @@ -1183,7 +1183,7 @@ class StoreDieselEbooksStore(StoreBase): name = 'Diesel eBooks' description = u'Instant access to over 2.4 million titles from hundreds of publishers including Harlequin, HarperCollins, John Wiley & Sons, McGraw-Hill, Simon & Schuster and Random House.' actual_plugin = 'calibre.gui2.store.diesel_ebooks_plugin:DieselEbooksStore' - + drm_free_only = False headquarters = 'US' formats = ['EPUB', 'PDF'] @@ -1192,7 +1192,7 @@ class StoreEbookscomStore(StoreBase): name = 'eBooks.com' description = u'Sells books in multiple electronic formats in all categories. Technical infrastructure is cutting edge, robust and scalable, with servers in the US and Europe.' actual_plugin = 'calibre.gui2.store.ebooks_com_plugin:EbookscomStore' - + drm_free_only = False headquarters = 'US' formats = ['EPUB', 'LIT', 'MOBI', 'PDF'] @@ -1200,9 +1200,9 @@ class StoreEbookscomStore(StoreBase): class StoreEPubBuyDEStore(StoreBase): name = 'EPUBBuy DE' author = 'Charles Haley' - description = u'Deutsch epub-Spezialisten.' + 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'] @@ -1211,7 +1211,7 @@ 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.' actual_plugin = 'calibre.gui2.store.eharlequin_plugin:EHarlequinStore' - + drm_free_only = False headquarters = 'CA' formats = ['EPUB', 'PDF'] @@ -1220,7 +1220,7 @@ class StoreFeedbooksStore(StoreBase): name = 'Feedbooks' description = u'Feedbooks is a cloud publishing and distribution service, connected to a large ecosystem of reading systems and social networks. Provides a variety of genres from independent and classic books.' actual_plugin = 'calibre.gui2.store.feedbooks_plugin:FeedbooksStore' - + drm_free_only = False headquarters = 'FR' formats = ['EPUB', 'MOBI', 'PDF'] @@ -1249,7 +1249,7 @@ class StoreGoogleBooksStore(StoreBase): name = 'Google Books' description = u'Google Books' actual_plugin = 'calibre.gui2.store.google_books_plugin:GoogleBooksStore' - + drm_free_only = False headquarters = 'US' formats = ['EPUB', 'PDF', 'TXT'] @@ -1258,7 +1258,7 @@ class StoreGutenbergStore(StoreBase): name = 'Project Gutenberg' description = u'The first producer of free ebooks. Free in the United States because their copyright has expired. They may not be free of copyright in other countries. Readers outside of the United States must check the copyright laws of their countries before downloading or redistributing our ebooks.' actual_plugin = 'calibre.gui2.store.gutenberg_plugin:GutenbergStore' - + drm_free_only = True headquarters = 'US' formats = ['EPUB', 'HTML', 'MOBI', 'PDB', 'TXT'] @@ -1267,7 +1267,7 @@ class StoreKoboStore(StoreBase): name = 'Kobo' description = u'With over 2.3 million eBooks to browse we have engaged readers in over 200 countries in Kobo eReading. Our eBook listings include New York Times Bestsellers, award winners, classics and more!' actual_plugin = 'calibre.gui2.store.kobo_plugin:KoboStore' - + drm_free_only = False headquarters = 'CA' formats = ['EPUB'] @@ -1276,7 +1276,7 @@ class StoreManyBooksStore(StoreBase): name = 'ManyBooks' description = u'Public domain and creative commons works from many sources.' actual_plugin = 'calibre.gui2.store.manybooks_plugin:ManyBooksStore' - + drm_free_only = True headquarters = 'US' formats = ['EPUB', 'FB2', 'JAR', 'LIT', 'LRF', 'MOBI', 'PDB', 'PDF', 'RB', 'RTF', 'TCR', 'TXT', 'ZIP'] @@ -1295,7 +1295,7 @@ class StoreNextoStore(StoreBase): author = u'Tomasz Długosz' description = u'Największy w Polsce sklep internetowy z audiobookami mp3, ebookami pdf oraz prasą do pobrania on-line.' actual_plugin = 'calibre.gui2.store.nexto_plugin:NextoStore' - + drm_free_only = False headquarters = 'PL' formats = ['EPUB', 'PDF'] @@ -1304,7 +1304,7 @@ 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.open_library_plugin:OpenLibraryStore' - + drm_free_only = True headquarters = ['US'] formats = ['DAISY', 'DJVU', 'EPUB', 'MOBI', 'PDF', 'TXT'] @@ -1313,7 +1313,7 @@ class StoreOReillyStore(StoreBase): name = 'OReilly' description = u'Programming and tech ebooks from OReilly.' actual_plugin = 'calibre.gui2.store.oreilly_plugin:OReillyStore' - + drm_free_only = True headquarters = 'US' formats = ['APK', 'DAISY', 'EPUB', 'MOBI', 'PDF'] @@ -1331,7 +1331,7 @@ class StoreSmashwordsStore(StoreBase): name = 'Smashwords' description = u'An ebook publishing and distribution platform for ebook authors, publishers and readers. Covers many genres and formats.' actual_plugin = 'calibre.gui2.store.smashwords_plugin:SmashwordsStore' - + drm_free_only = True headquarters = 'US' formats = ['EPUB', 'HTML', 'LRF', 'MOBI', 'PDB', 'RTF', 'TXT'] @@ -1341,7 +1341,7 @@ class StoreWaterstonesUKStore(StoreBase): 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'] @@ -1359,7 +1359,7 @@ class StoreWizardsTowerBooksStore(StoreBase): name = 'Wizards Tower Books' description = u'A science fiction and fantasy publisher. Concentrates mainly on making out-of-print works available once more as e-books, and helping other small presses exploit the e-book market. Also publishes a small number of limited-print-run anthologies with a view to encouraging diversity in the science fiction and fantasy field.' actual_plugin = 'calibre.gui2.store.wizards_tower_books_plugin:WizardsTowerBooksStore' - + drm_free_only = True headquarters = 'UK' formats = ['EPUB', 'MOBI'] @@ -1389,7 +1389,7 @@ plugins += [ StoreEHarlequinStore, StoreFeedbooksStore, StoreFoylesUKStore, - StoreGandalfStore, + StoreGandalfStore, StoreGoogleBooksStore, StoreGutenbergStore, StoreKoboStore, diff --git a/src/calibre/gui2/store/declined.txt b/src/calibre/gui2/store/declined.txt index 2b0e5caed2..2186303d4b 100644 --- a/src/calibre/gui2/store/declined.txt +++ b/src/calibre/gui2/store/declined.txt @@ -3,3 +3,6 @@ or asked not to be included in the store integration. * Borders (http://www.borders.com/) * WH Smith (http://www.whsmith.co.uk/) + Refused to permit signing up for the affiliate program +* Libraria Rizzoli (http://libreriarizzoli.corriere.it/). + No reply with two attempts over 2 weeks \ No newline at end of file From 50c80a8505c395c43904d0139dbaf908628c7298 Mon Sep 17 00:00:00 2001 From: John Schember Date: Tue, 24 May 2011 19:34:04 -0400 Subject: [PATCH 03/10] Store: Fix some bugs. Add basic store chooser which give information and allows enabling and disabling store plugins in a user friendly manner. --- src/calibre/customize/builtins.py | 4 +- src/calibre/gui2/actions/store.py | 7 + src/calibre/gui2/store/config/chooser.py | 18 + .../gui2/store/config/chooser/__init__.py | 0 .../config/chooser/adv_search_builder.py | 131 ++++++ .../config/chooser/adv_search_builder.ui | 416 ++++++++++++++++++ .../store/config/chooser/chooser_dialog.py | 28 ++ .../store/config/chooser/chooser_widget.py | 35 ++ .../store/config/chooser/chooser_widget.ui | 87 ++++ .../gui2/store/config/chooser/models.py | 244 ++++++++++ .../gui2/store/config/chooser/results_view.py | 31 ++ .../gui2/store/config/search/__init__.py | 0 .../config/{ => search}/search_widget.py | 2 +- .../config/{ => search}/search_widget.ui | 0 src/calibre/gui2/store/config/store.py | 2 +- src/calibre/gui2/store/mobileread/models.py | 1 + .../gui2/store/search/adv_search_builder.py | 2 +- src/calibre/gui2/store/search/search.py | 2 +- src/calibre/gui2/store/search/search.ui | 7 +- 19 files changed, 1009 insertions(+), 8 deletions(-) create mode 100644 src/calibre/gui2/store/config/chooser.py create mode 100644 src/calibre/gui2/store/config/chooser/__init__.py create mode 100644 src/calibre/gui2/store/config/chooser/adv_search_builder.py create mode 100644 src/calibre/gui2/store/config/chooser/adv_search_builder.ui create mode 100644 src/calibre/gui2/store/config/chooser/chooser_dialog.py create mode 100644 src/calibre/gui2/store/config/chooser/chooser_widget.py create mode 100644 src/calibre/gui2/store/config/chooser/chooser_widget.ui create mode 100644 src/calibre/gui2/store/config/chooser/models.py create mode 100644 src/calibre/gui2/store/config/chooser/results_view.py create mode 100644 src/calibre/gui2/store/config/search/__init__.py rename src/calibre/gui2/store/config/{ => search}/search_widget.py (96%) rename src/calibre/gui2/store/config/{ => search}/search_widget.ui (100%) diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index d9e8be00b5..5c90e5699b 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -1316,7 +1316,7 @@ class StoreOpenLibraryStore(StoreBase): actual_plugin = 'calibre.gui2.store.open_library_plugin:OpenLibraryStore' drm_free_only = True - headquarters = ['US'] + headquarters = 'US' formats = ['DAISY', 'DJVU', 'EPUB', 'MOBI', 'PDF', 'TXT'] class StoreOReillyStore(StoreBase): @@ -1381,7 +1381,7 @@ class StoreWoblinkStore(StoreBase): actual_plugin = 'calibre.gui2.store.woblink_plugin:WoblinkStore' drm_free_only = False - location = 'PL' + headquarters = 'PL' formats = ['EPUB'] plugins += [ diff --git a/src/calibre/gui2/actions/store.py b/src/calibre/gui2/actions/store.py index c8507e851c..effe470359 100644 --- a/src/calibre/gui2/actions/store.py +++ b/src/calibre/gui2/actions/store.py @@ -34,6 +34,8 @@ class StoreAction(InterfaceAction): self.store_list_menu = self.store_menu.addMenu(_('Stores')) 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)) + self.store_menu.addSeparator() + self.store_menu.addAction(_('Choose stores'), self.choose) self.qaction.setMenu(self.store_menu) def do_search(self): @@ -107,6 +109,11 @@ class StoreAction(InterfaceAction): query = 'author:"%s" title:"%s"' % (self._get_author(row), self._get_title(row)) self.search(query) + def choose(self): + from calibre.gui2.store.config.chooser.chooser_dialog import StoreChooserDialog + d = StoreChooserDialog(self.gui) + d.exec_() + def open_store(self, store_plugin): self.show_disclaimer() store_plugin.open(self.gui) diff --git a/src/calibre/gui2/store/config/chooser.py b/src/calibre/gui2/store/config/chooser.py new file mode 100644 index 0000000000..f5c40a18ae --- /dev/null +++ b/src/calibre/gui2/store/config/chooser.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +''' +Config widget access functions for enabling and disabling stores. +''' + +def config_widget(): + from calibre.gui2.store.config.chooser.chooser_widget import StoreChooserWidget + return StoreChooserWidget() + +def save_settings(config_widget): + pass diff --git a/src/calibre/gui2/store/config/chooser/__init__.py b/src/calibre/gui2/store/config/chooser/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/calibre/gui2/store/config/chooser/adv_search_builder.py b/src/calibre/gui2/store/config/chooser/adv_search_builder.py new file mode 100644 index 0000000000..7b519abcd1 --- /dev/null +++ b/src/calibre/gui2/store/config/chooser/adv_search_builder.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +import re + +from PyQt4.Qt import (QDialog, QDialogButtonBox) + +from calibre.gui2.store.config.chooser.adv_search_builder_ui import Ui_Dialog +from calibre.library.caches import CONTAINS_MATCH, EQUALS_MATCH + +class AdvSearchBuilderDialog(QDialog, Ui_Dialog): + + def __init__(self, parent): + QDialog.__init__(self, parent) + self.setupUi(self) + + self.buttonBox.accepted.connect(self.advanced_search_button_pushed) + self.tab_2_button_box.accepted.connect(self.accept) + self.tab_2_button_box.rejected.connect(self.reject) + self.clear_button.clicked.connect(self.clear_button_pushed) + self.adv_search_used = False + self.mc = '' + + self.tabWidget.setCurrentIndex(0) + self.tabWidget.currentChanged[int].connect(self.tab_changed) + self.tab_changed(0) + + def tab_changed(self, idx): + if idx == 1: + self.tab_2_button_box.button(QDialogButtonBox.Ok).setDefault(True) + else: + self.buttonBox.button(QDialogButtonBox.Ok).setDefault(True) + + def advanced_search_button_pushed(self): + self.adv_search_used = True + self.accept() + + def clear_button_pushed(self): + self.name_box.setText('') + self.description_box.setText('') + self.headquarters_box.setText('') + self.format_box.setText('') + self.enabled_combo.setIndex(0) + self.drm_combo.setIndex(0) + + def tokens(self, raw): + phrases = re.findall(r'\s*".*?"\s*', raw) + for f in phrases: + raw = raw.replace(f, ' ') + phrases = [t.strip('" ') for t in phrases] + return ['"' + self.mc + t + '"' for t in phrases + [r.strip() for r in raw.split()]] + + def search_string(self): + if self.adv_search_used: + return self.adv_search_string() + else: + return self.box_search_string() + + def adv_search_string(self): + mk = self.matchkind.currentIndex() + if mk == CONTAINS_MATCH: + self.mc = '' + elif mk == EQUALS_MATCH: + self.mc = '=' + else: + self.mc = '~' + all, any, phrase, none = map(lambda x: unicode(x.text()), + (self.all, self.any, self.phrase, self.none)) + all, any, none = map(self.tokens, (all, any, none)) + phrase = phrase.strip() + all = ' and '.join(all) + any = ' or '.join(any) + none = ' and not '.join(none) + ans = '' + if phrase: + ans += '"%s"'%phrase + if all: + ans += (' and ' if ans else '') + all + if none: + ans += (' and not ' if ans else 'not ') + none + if any: + ans += (' or ' if ans else '') + any + return ans + + def token(self): + txt = unicode(self.text.text()).strip() + if txt: + if self.negate.isChecked(): + txt = '!'+txt + tok = self.FIELDS[unicode(self.field.currentText())]+txt + if re.search(r'\s', tok): + tok = '"%s"'%tok + return tok + + def box_search_string(self): + mk = self.matchkind.currentIndex() + if mk == CONTAINS_MATCH: + self.mc = '' + elif mk == EQUALS_MATCH: + self.mc = '=' + else: + self.mc = '~' + + ans = [] + self.box_last_values = {} + name = unicode(self.name_box.text()).strip() + if name: + ans.append('name:"' + self.mc + name + '"') + description = unicode(self.description_box.text()).strip() + if description: + ans.append('description:"' + self.mc + description + '"') + headquarters = unicode(self.headquarters_box.text()).strip() + if headquarters: + ans.append('headquarters:"' + self.mc + headquarters + '"') + format = unicode(self.format_box.text()).strip() + if format: + ans.append('format:"' + self.mc + format + '"') + enabled = unicode(self.enabled_combo.currentText()).strip() + if enabled: + ans.append('enabled:' + enabled) + drm = unicode(self.drm_combo.currentText()).strip() + if drm: + ans.append('drm:' + drm) + if ans: + return ' and '.join(ans) + return '' diff --git a/src/calibre/gui2/store/config/chooser/adv_search_builder.ui b/src/calibre/gui2/store/config/chooser/adv_search_builder.ui new file mode 100644 index 0000000000..7d57321c72 --- /dev/null +++ b/src/calibre/gui2/store/config/chooser/adv_search_builder.ui @@ -0,0 +1,416 @@ + + + Dialog + + + + 0 + 0 + 752 + 472 + + + + Advanced Search + + + + :/images/search.png:/images/search.png + + + + + + &What kind of match to use: + + + matchkind + + + + + + + + Contains: the word or phrase matches anywhere in the metadata field + + + + + Equals: the word or phrase must match the entire metadata field + + + + + Regular expression: the expression must match anywhere in the metadata field + + + + + + + + 0 + + + + A&dvanced Search + + + + + + Find entries that have... + + + + + + + + &All these words: + + + all + + + + + + + + + + + + + + This exact &phrase: + + + all + + + + + + + + + + + + + + &One or more of these words: + + + all + + + + + + + + + + + + + + + But dont show entries that have... + + + + + + + + Any of these &unwanted words: + + + all + + + + + + + + + + + + + 16777215 + 30 + + + + See the <a href="http://calibre-ebook.com/user_manual/gui.html#the-search-interface">User Manual</a> for more help + + + true + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + Nam&e/Description ... + + + + + + &Name: + + + name_box + + + + + + + Enter the title. + + + + + + + &Description: + + + description_box + + + + + + + &Headquarters: + + + headquarters_box + + + + + + + + + &Clear + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Search only in specific fields: + + + + + + + + + + + + + &Format: + + + format_box + + + + + + + + + + Enabled: + + + + + + + DRM: + + + + + + + + + + + + + true + + + + + false + + + + + + + + + + + + + + true + + + + + false + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + EnLineEdit + QLineEdit +
widgets.h
+
+
+ + all + phrase + any + none + buttonBox + name_box + description_box + headquarters_box + format_box + clear_button + tab_2_button_box + tabWidget + matchkind + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + +
diff --git a/src/calibre/gui2/store/config/chooser/chooser_dialog.py b/src/calibre/gui2/store/config/chooser/chooser_dialog.py new file mode 100644 index 0000000000..c94796dc11 --- /dev/null +++ b/src/calibre/gui2/store/config/chooser/chooser_dialog.py @@ -0,0 +1,28 @@ +# -*- 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 (QDialog, QDialogButtonBox, QVBoxLayout) + +from calibre.gui2.store.config.chooser.chooser_widget import StoreChooserWidget + +class StoreChooserDialog(QDialog): + + def __init__(self, parent): + QDialog.__init__(self, parent) + + self.setWindowTitle(_('Choose stores')) + + button_box = QDialogButtonBox(QDialogButtonBox.Close) + button_box.accepted.connect(self.accept) + button_box.rejected.connect(self.reject) + v = QVBoxLayout(self) + self.config_widget = StoreChooserWidget() + v.addWidget(self.config_widget) + v.addWidget(button_box) + + self.resize(800, 600) diff --git a/src/calibre/gui2/store/config/chooser/chooser_widget.py b/src/calibre/gui2/store/config/chooser/chooser_widget.py new file mode 100644 index 0000000000..93630d69a7 --- /dev/null +++ b/src/calibre/gui2/store/config/chooser/chooser_widget.py @@ -0,0 +1,35 @@ +# -*- 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 (QWidget, QIcon, QDialog) + +from calibre.gui2.store.config.chooser.adv_search_builder import AdvSearchBuilderDialog +from calibre.gui2.store.config.chooser.chooser_widget_ui import Ui_Form + +class StoreChooserWidget(QWidget, Ui_Form): + + def __init__(self): + QWidget.__init__(self) + self.setupUi(self) + + self.adv_search_builder.setIcon(QIcon(I('search.png'))) + + self.search.clicked.connect(self.do_search) + self.adv_search_builder.clicked.connect(self.build_adv_search) + self.results_view.activated.connect(self.toggle_plugin) + + def do_search(self): + self.results_view.model().search(unicode(self.query.text())) + + def toggle_plugin(self, index): + self.results_view.model().toggle_plugin(index) + + def build_adv_search(self): + adv = AdvSearchBuilderDialog(self) + if adv.exec_() == QDialog.Accepted: + self.query.setText(adv.search_string()) diff --git a/src/calibre/gui2/store/config/chooser/chooser_widget.ui b/src/calibre/gui2/store/config/chooser/chooser_widget.ui new file mode 100644 index 0000000000..69117406b1 --- /dev/null +++ b/src/calibre/gui2/store/config/chooser/chooser_widget.ui @@ -0,0 +1,87 @@ + + + Form + + + + 0 + 0 + 610 + 553 + + + + Form + + + + + + + + Query: + + + + + + + ... + + + + + + + + + + Search + + + + + + + + + true + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + false + + + true + + + false + + + true + + + false + + + false + + + + + + + + ResultsView + QTreeView +
results_view.h
+
+
+ + +
diff --git a/src/calibre/gui2/store/config/chooser/models.py b/src/calibre/gui2/store/config/chooser/models.py new file mode 100644 index 0000000000..460b698878 --- /dev/null +++ b/src/calibre/gui2/store/config/chooser/models.py @@ -0,0 +1,244 @@ +# -*- 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 (Qt, QAbstractItemModel, QIcon, QVariant, QModelIndex) + +from calibre.gui2 import NONE +from calibre.customize.ui import is_disabled, disable_plugin, enable_plugin +from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \ + REGEXP_MATCH +from calibre.utils.icu import sort_key +from calibre.utils.search_query_parser import SearchQueryParser + + +class Matches(QAbstractItemModel): + + HEADERS = [_('Enabled'), _('Name'), _('No DRM'), _('Headquarters'), _('Formats')] + HTML_COLS = [1] + + def __init__(self, plugins): + QAbstractItemModel.__init__(self) + + self.NO_DRM_ICON = QIcon(I('ok.png')) + + self.all_matches = plugins + self.matches = plugins + self.filter = '' + self.search_filter = SearchFilter(self.all_matches) + + self.sort_col = 1 + self.sort_order = Qt.AscendingOrder + + def get_plugin(self, index): + row = index.row() + if row < len(self.matches): + return self.matches[row] + else: + return None + + def search(self, filter): + self.filter = filter.strip() + if not self.filter: + self.matches = self.all_matches + else: + try: + self.matches = list(self.search_filter.parse(self.filter)) + except: + self.matches = self.all_matches + self.layoutChanged.emit() + self.sort(self.sort_col, self.sort_order) + + def toggle_plugin(self, index): + new_index = self.createIndex(index.row(), 0) + data = QVariant(is_disabled(self.get_plugin(index))) + self.setData(new_index, data, Qt.CheckStateRole) + + def index(self, row, column, parent=QModelIndex()): + return self.createIndex(row, column) + + def parent(self, index): + if not index.isValid() or index.internalId() == 0: + return QModelIndex() + return self.createIndex(0, 0) + + def rowCount(self, *args): + return len(self.matches) + + def columnCount(self, *args): + return len(self.HEADERS) + + def headerData(self, section, orientation, role): + if role != Qt.DisplayRole: + return NONE + text = '' + if orientation == Qt.Horizontal: + if section < len(self.HEADERS): + text = self.HEADERS[section] + return QVariant(text) + else: + return QVariant(section+1) + + def data(self, index, role): + row, col = index.row(), index.column() + result = self.matches[row] + if role in (Qt.DisplayRole, Qt.EditRole): + if col == 1: + return QVariant('%s
%s' % (result.name, result.description)) + elif col == 3: + return QVariant(result.headquarters) + elif col == 4: + return QVariant(', '.join(result.formats).upper()) + elif role == Qt.DecorationRole: + if col == 2: + if result.drm_free_only: + return QVariant(self.NO_DRM_ICON) + elif role == Qt.CheckStateRole: + if col == 0: + if is_disabled(result): + return Qt.Unchecked + return Qt.Checked + elif role == Qt.ToolTipRole: + return QVariant('

%s

' % result.description) + return NONE + + def setData(self, index, data, role): + if not index.isValid(): + return False + row, col = index.row(), index.column() + if col == 0: + if data.toBool(): + enable_plugin(self.get_plugin(index)) + else: + disable_plugin(self.get_plugin(index)) + self.dataChanged.emit(self.index(index.row(), 0), self.index(index.row(), self.columnCount() - 1)) + return True + + def flags(self, index): + if index.column() == 0: + return QAbstractItemModel.flags(self, index) | Qt.ItemIsUserCheckable + return QAbstractItemModel.flags(self, index) + + def data_as_text(self, match, col): + text = '' + if col == 0: + text = 'b' if is_disabled(match) else 'a' + elif col == 1: + text = match.name + elif col == 2: + text = 'b' if match.drm else 'a' + elif col == 3: + text = match.headquarteres + return text + + def sort(self, col, order, reset=True): + self.sort_col = col + self.sort_order = order + if not self.matches: + return + descending = order == Qt.DescendingOrder + self.matches.sort(None, + lambda x: sort_key(unicode(self.data_as_text(x, col))), + descending) + if reset: + self.reset() + + +class SearchFilter(SearchQueryParser): + + USABLE_LOCATIONS = [ + 'all', + 'description', + 'drm', + 'enabled', + 'format', + 'formats', + 'headquarters', + 'name', + ] + + def __init__(self, all_plugins=[]): + SearchQueryParser.__init__(self, locations=self.USABLE_LOCATIONS) + self.srs = set(all_plugins) + + def universal_set(self): + return self.srs + + def get_matches(self, location, query): + location = location.lower().strip() + if location == 'formats': + location = 'format' + + matchkind = CONTAINS_MATCH + if len(query) > 1: + if query.startswith('\\'): + query = query[1:] + elif query.startswith('='): + matchkind = EQUALS_MATCH + query = query[1:] + elif query.startswith('~'): + matchkind = REGEXP_MATCH + query = query[1:] + if matchkind != REGEXP_MATCH: ### leave case in regexps because it can be significant e.g. \S \W \D + query = query.lower() + + if location not in self.USABLE_LOCATIONS: + return set([]) + matches = set([]) + all_locs = set(self.USABLE_LOCATIONS) - set(['all']) + locations = all_locs if location == 'all' else [location] + q = { + 'description': lambda x: x.description.lower(), + 'drm': lambda x: not x.drm_free_only, + 'enabled': lambda x: not is_disabled(x), + 'format': lambda x: ','.join(x.formats).lower(), + 'headquarters': lambda x: x.headquarters.lower(), + 'name': lambda x : x.name.lower(), + } + q['formats'] = q['format'] + for sr in self.srs: + for locvalue in locations: + accessor = q[locvalue] + if query == 'true': + if locvalue in ('drm', 'enabled'): + if accessor(sr) == True: + matches.add(sr) + elif accessor(sr) is not None: + matches.add(sr) + continue + if query == 'false': + if locvalue in ('drm', 'enabled'): + if accessor(sr) == False: + matches.add(sr) + elif accessor(sr) is None: + matches.add(sr) + continue + # this is bool, so can't match below + if locvalue in ('drm', 'enabled'): + continue + try: + ### Can't separate authors because comma is used for name sep and author sep + ### Exact match might not get what you want. For that reason, turn author + ### exactmatch searches into contains searches. + if locvalue == 'name' and matchkind == EQUALS_MATCH: + m = CONTAINS_MATCH + else: + m = matchkind + + if locvalue == 'format': + vals = accessor(sr).split(',') + else: + vals = [accessor(sr)] + if _match(query, vals, m): + matches.add(sr) + break + except ValueError: # Unicode errors + import traceback + traceback.print_exc() + return matches + + \ No newline at end of file diff --git a/src/calibre/gui2/store/config/chooser/results_view.py b/src/calibre/gui2/store/config/chooser/results_view.py new file mode 100644 index 0000000000..52d7696e4f --- /dev/null +++ b/src/calibre/gui2/store/config/chooser/results_view.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 (QTreeView, QSize) + +from calibre.customize.ui import store_plugins +from calibre.gui2.metadata.single_download import RichTextDelegate +from calibre.gui2.store.config.chooser.models import Matches + +class ResultsView(QTreeView): + + def __init__(self, *args): + QTreeView.__init__(self,*args) + + self._model = Matches([p for p in store_plugins()]) + self.setModel(self._model) + + self.setIconSize(QSize(24, 24)) + + self.rt_delegate = RichTextDelegate(self) + + for i in self._model.HTML_COLS: + self.setItemDelegateForColumn(i, self.rt_delegate) + + for i in xrange(self._model.columnCount()): + self.resizeColumnToContents(i) diff --git a/src/calibre/gui2/store/config/search/__init__.py b/src/calibre/gui2/store/config/search/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/calibre/gui2/store/config/search_widget.py b/src/calibre/gui2/store/config/search/search_widget.py similarity index 96% rename from src/calibre/gui2/store/config/search_widget.py rename to src/calibre/gui2/store/config/search/search_widget.py index 43e911a432..b2e55d2ad1 100644 --- a/src/calibre/gui2/store/config/search_widget.py +++ b/src/calibre/gui2/store/config/search/search_widget.py @@ -9,7 +9,7 @@ __docformat__ = 'restructuredtext en' from PyQt4.Qt import QWidget from calibre.gui2 import JSONConfig -from calibre.gui2.store.config.search_widget_ui import Ui_Form +from calibre.gui2.store.config.search.search_widget_ui import Ui_Form class StoreConfigWidget(QWidget, Ui_Form): diff --git a/src/calibre/gui2/store/config/search_widget.ui b/src/calibre/gui2/store/config/search/search_widget.ui similarity index 100% rename from src/calibre/gui2/store/config/search_widget.ui rename to src/calibre/gui2/store/config/search/search_widget.ui diff --git a/src/calibre/gui2/store/config/store.py b/src/calibre/gui2/store/config/store.py index ddc24870bd..852f602d08 100644 --- a/src/calibre/gui2/store/config/store.py +++ b/src/calibre/gui2/store/config/store.py @@ -11,7 +11,7 @@ Config widget access functions for configuring the store action. ''' def config_widget(): - from calibre.gui2.store.config.search_widget import StoreConfigWidget + from calibre.gui2.store.config.search.search_widget import StoreConfigWidget return StoreConfigWidget() def save_settings(config_widget): diff --git a/src/calibre/gui2/store/mobileread/models.py b/src/calibre/gui2/store/mobileread/models.py index a080affb51..297707e248 100644 --- a/src/calibre/gui2/store/mobileread/models.py +++ b/src/calibre/gui2/store/mobileread/models.py @@ -47,6 +47,7 @@ class BooksModel(QAbstractItemModel): self.books = list(self.search_filter.parse(self.filter)) except: self.books = self.all_books + self.layoutChanged.emit() self.sort(self.sort_col, self.sort_order) self.total_changed.emit(self.rowCount()) diff --git a/src/calibre/gui2/store/search/adv_search_builder.py b/src/calibre/gui2/store/search/adv_search_builder.py index 50d4d3f3f4..745e709f90 100644 --- a/src/calibre/gui2/store/search/adv_search_builder.py +++ b/src/calibre/gui2/store/search/adv_search_builder.py @@ -116,7 +116,7 @@ class AdvSearchBuilderDialog(QDialog, Ui_Dialog): if price: ans.append('price:"' + self.mc + price + '"') format = unicode(self.format_box.text()).strip() - if author: + if format: ans.append('format:"' + self.mc + format + '"') if ans: return ' and '.join(ans) diff --git a/src/calibre/gui2/store/search/search.py b/src/calibre/gui2/store/search/search.py index c7c252034d..ffc6ec097e 100644 --- a/src/calibre/gui2/store/search/search.py +++ b/src/calibre/gui2/store/search/search.py @@ -14,7 +14,7 @@ from PyQt4.Qt import (Qt, QDialog, QDialogButtonBox, QTimer, QCheckBox, from calibre.gui2 import JSONConfig, info_dialog from calibre.gui2.progress_indicator import ProgressIndicator -from calibre.gui2.store.config.search_widget import StoreConfigWidget +from calibre.gui2.store.config.search.search_widget import StoreConfigWidget from calibre.gui2.store.search.adv_search_builder import AdvSearchBuilderDialog from calibre.gui2.store.search.download_thread import SearchThreadPool, \ CacheUpdateThreadPool diff --git a/src/calibre/gui2/store/search/search.ui b/src/calibre/gui2/store/search/search.ui index 0360fa5f98..1451aa09f1 100644 --- a/src/calibre/gui2/store/search/search.ui +++ b/src/calibre/gui2/store/search/search.ui @@ -82,8 +82,8 @@ 0 0 - 102 - 129 + 125 + 127 @@ -159,6 +159,9 @@ false + + false + From b871315864d5e39a9f9e768b37d5268387decf73 Mon Sep 17 00:00:00 2001 From: John Schember Date: Tue, 24 May 2011 19:45:20 -0400 Subject: [PATCH 04/10] Store: Chooser, set sort and sort indicator. --- src/calibre/gui2/store/config/chooser/results_view.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/store/config/chooser/results_view.py b/src/calibre/gui2/store/config/chooser/results_view.py index 52d7696e4f..1c18a18d7b 100644 --- a/src/calibre/gui2/store/config/chooser/results_view.py +++ b/src/calibre/gui2/store/config/chooser/results_view.py @@ -6,7 +6,7 @@ __license__ = 'GPL 3' __copyright__ = '2011, John Schember ' __docformat__ = 'restructuredtext en' -from PyQt4.Qt import (QTreeView, QSize) +from PyQt4.Qt import (Qt, QTreeView, QSize) from calibre.customize.ui import store_plugins from calibre.gui2.metadata.single_download import RichTextDelegate @@ -29,3 +29,6 @@ class ResultsView(QTreeView): for i in xrange(self._model.columnCount()): self.resizeColumnToContents(i) + + self.model().sort(1, Qt.AscendingOrder) + self.header().setSortIndicator(self.model().sort_col, self.model().sort_order) From 41cc4be9528bfd8835e1f0b67bc6aff494d579f1 Mon Sep 17 00:00:00 2001 From: John Schember Date: Wed, 25 May 2011 06:55:52 -0400 Subject: [PATCH 05/10] Store: Reload store plugins accessible by GUI after using the store chooser. --- src/calibre/gui2/actions/store.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/calibre/gui2/actions/store.py b/src/calibre/gui2/actions/store.py index effe470359..0fd783f0a3 100644 --- a/src/calibre/gui2/actions/store.py +++ b/src/calibre/gui2/actions/store.py @@ -113,6 +113,8 @@ class StoreAction(InterfaceAction): from calibre.gui2.store.config.chooser.chooser_dialog import StoreChooserDialog d = StoreChooserDialog(self.gui) d.exec_() + self.gui.load_store_plugins() + self.load_menu() def open_store(self, store_plugin): self.show_disclaimer() From a8deb0ed7d2b349a82711f96cb0ed28a3db16156 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 25 May 2011 09:56:27 -0600 Subject: [PATCH 06/10] ... --- src/calibre/devices/android/driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index db473a755e..1cdf394c24 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -59,7 +59,7 @@ class ANDROID(USBMS): 0x0489 : { 0xc001 : [0x0226], 0xc004 : [0x0226], }, # Acer - 0x502 : { 0x3203 : [0x0100]}, + 0x502 : { 0x3203 : [0x0100, 0x224]}, # Dell 0x413c : { 0xb007 : [0x0100, 0x0224, 0x0226]}, From ce6cedf730c2c59b0cc41bcce7c29a969edf2eaf Mon Sep 17 00:00:00 2001 From: Li Fanxi Date: Thu, 26 May 2011 00:24:04 +0800 Subject: [PATCH 07/10] [Bug] Error in passing API key to douban API. --- src/calibre/ebooks/metadata/sources/douban.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/ebooks/metadata/sources/douban.py b/src/calibre/ebooks/metadata/sources/douban.py index 3c6bb7b6c7..8a95c4ed6b 100644 --- a/src/calibre/ebooks/metadata/sources/douban.py +++ b/src/calibre/ebooks/metadata/sources/douban.py @@ -211,7 +211,7 @@ class Douban(Source): 'q': q, }) if self.DOUBAN_API_KEY and self.DOUBAN_API_KEY != '': - url = url + "?apikey=" + self.DOUBAN_API_KEY + url = url + "&apikey=" + self.DOUBAN_API_KEY return url # }}} From 70d1f3c046913e90cfd649859b2a2d32528a88b9 Mon Sep 17 00:00:00 2001 From: Li Fanxi Date: Thu, 26 May 2011 00:31:47 +0800 Subject: [PATCH 08/10] [Bug] Error in passing API key to douban API when search by isbn or douban id --- src/calibre/ebooks/metadata/sources/douban.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/metadata/sources/douban.py b/src/calibre/ebooks/metadata/sources/douban.py index 8a95c4ed6b..70bf01a00e 100644 --- a/src/calibre/ebooks/metadata/sources/douban.py +++ b/src/calibre/ebooks/metadata/sources/douban.py @@ -211,7 +211,10 @@ class Douban(Source): 'q': q, }) if self.DOUBAN_API_KEY and self.DOUBAN_API_KEY != '': - url = url + "&apikey=" + self.DOUBAN_API_KEY + if t == "isbn" or t == "subject": + url = url + "?apikey=" + self.DOUBAN_API_KEY + else: + url = url + "&apikey=" + self.DOUBAN_API_KEY return url # }}} From b707ae28202b550a704c0729b040e254fcaca51e Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 25 May 2011 17:44:47 +0100 Subject: [PATCH 09/10] Addition of boolean template functions and, or, not. Change documentation to include them. Add a function classification summary to the documentation. --- src/calibre/manual/template_lang.rst | 20 +++++++++- src/calibre/utils/formatter_functions.py | 51 ++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/calibre/manual/template_lang.rst b/src/calibre/manual/template_lang.rst index 69c77e5bfd..16a90f7531 100644 --- a/src/calibre/manual/template_lang.rst +++ b/src/calibre/manual/template_lang.rst @@ -229,13 +229,14 @@ For various values of series_index, the program returns: The following functions are available in addition to those described in single-function mode. Remember from the example above that the single-function mode functions require an additional first parameter specifying the field to operate on. With the exception of the ``id`` parameter of assign, all parameters can be statements (sequences of expressions): + * ``and(value, value, ...)`` -- returns the string "1" if all values are not empty, otherwise returns the empty string. This function works well with test or first_non_empty. You can have as many values as you want. * ``add(x, y)`` -- returns x + y. Throws an exception if either x or y are not numbers. * ``assign(id, val)`` -- assigns val to id, then returns val. id must be an identifier, not an expression * ``booksize()`` -- returns the value of the |app| 'size' field. Returns '' if there are no formats. * ``cmp(x, y, lt, eq, gt)`` -- compares x and y after converting both to numbers. Returns ``lt`` if x < y. Returns ``eq`` if x == y. Otherwise returns ``gt``. * ``divide(x, y)`` -- returns x / y. Throws an exception if either x or y are not numbers. * ``field(name)`` -- returns the metadata field named by ``name``. - * ``first_non_empty(value, value, ...) -- returns the first value that is not empty. If all values are empty, then the empty value is returned. You can have as many values as you want. + * ``first_non_empty(value, value, ...)`` -- returns the first value that is not empty. If all values are empty, then the empty value is returned. You can have as many values as you want. * ``format_date(x, date_format)`` -- format_date(val, format_string) -- format the value, which must be a date field, using the format_string, returning a string. The formatting codes are:: d : the day as number without a leading zero (1 to 31) @@ -251,7 +252,9 @@ The following functions are available in addition to those described in single-f iso : the date with time and timezone. Must be the only format present. * ``eval(string)`` -- evaluates the string as a program, passing the local variables (those ``assign`` ed to). This permits using the template processor to construct complex results from local variables. + * ``not(value)`` -- returns the string "1" if the value is empty, otherwise returns the empty string. This function works well with test or first_non_empty. You can have as many values as you want. * ``multiply(x, y)`` -- returns x * y. Throws an exception if either x or y are not numbers. + * ``or(value, value, ...)`` -- returns the string "1" if any value is not empty, otherwise returns the empty string. This function works well with test or first_non_empty. You can have as many values as you want. * ``print(a, b, ...)`` -- prints the arguments to standard output. Unless you start calibre from the command line (``calibre-debug -g``), the output will go to a black hole. * ``raw_field(name)`` -- returns the metadata field named by name without applying any formatting. * ``strcat(a, b, ...)`` -- can take any number of arguments. Returns a string formed by concatenating all the arguments. @@ -259,7 +262,22 @@ The following functions are available in addition to those described in single-f * ``substr(str, start, end)`` -- returns the ``start``'th through the ``end``'th characters of ``str``. The first character in ``str`` is the zero'th character. If end is negative, then it indicates that many characters counting from the right. If end is zero, then it indicates the last character. For example, ``substr('12345', 1, 0)`` returns ``'2345'``, and ``substr('12345', 1, -1)`` returns ``'234'``. * ``subtract(x, y)`` -- returns x - y. Throws an exception if either x or y are not numbers. * ``template(x)`` -- evaluates x as a template. The evaluation is done in its own context, meaning that variables are not shared between the caller and the template evaluation. Because the `{` and `}` characters are special, you must use `[[` for the `{` character and `]]` for the '}' character; they are converted automatically. For example, ``template('[[title_sort]]') will evaluate the template ``{title_sort}`` and return its value. + +Function classification summary: + * Get values from metadata: ``field``. ``raw_field``. In some situations, ``lookup`` can be used in place of ``field``. + * Arithmetic: ``add``, ``subtract``, ``multiply``, ``divide`` + * Boolean: ``and``, ``or``, ``not``. The function ``if_empty`` is similar to ``and`` called with one argument. + * If-then-else: ``contains``, ``test`` + * Iterating over values: ``first_non_empty``, ``lookup``, ``switch`` + * List lookup: ``in_list``, ``list_item``, ``select``, + * List manipulation: ``count``, ``sublist``, ``subitems`` + * Recursion: ``eval``, ``template`` + * Relational: ``cmp`` , ``strcmp`` for strings + * String case changes: ``lowercase``, ``uppercase``, ``titlecase``, ``capitalize`` + * String manipulation: ``re``, ``shorten``, ``substr`` + * Other: ``assign``, ``booksize``, ``print``, ``format_date``, + .. _general_mode: Using general program mode diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index c53277f3ce..a3a156648f 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -594,7 +594,56 @@ class BuiltinFirstNonEmpty(BuiltinFormatterFunction): i += 1 return '' +class BuiltinAnd(BuiltinFormatterFunction): + name = 'and' + arg_count = -1 + doc = _('and(value, value, ...) -- ' + 'returns the string "1" if all values are not empty, otherwise ' + 'returns the empty string. This function works well with test or ' + 'first_non_empty. You can have as many values as you want.') + + def evaluate(self, formatter, kwargs, mi, locals, *args): + i = 0 + while i < len(args): + if not args[i]: + return '' + i += 1 + return '1' + +class BuiltinOr(BuiltinFormatterFunction): + name = 'or' + arg_count = -1 + doc = _('or(value, value, ...) -- ' + 'returns the string "1" if any value is not empty, otherwise ' + 'returns the empty string. This function works well with test or ' + 'first_non_empty. You can have as many values as you want.') + + def evaluate(self, formatter, kwargs, mi, locals, *args): + i = 0 + while i < len(args): + if args[i]: + return '1' + i += 1 + return '' + +class BuiltinNot(BuiltinFormatterFunction): + name = 'not' + arg_count = 1 + doc = _('not(value) -- ' + 'returns the string "1" if the value is empty, otherwise ' + 'returns the empty string. This function works well with test or ' + 'first_non_empty. You can have as many values as you want.') + + def evaluate(self, formatter, kwargs, mi, locals, *args): + i = 0 + while i < len(args): + if args[i]: + return '1' + i += 1 + return '' + builtin_add = BuiltinAdd() +builtin_and = BuiltinAnd() builtin_assign = BuiltinAssign() builtin_booksize = BuiltinBooksize() builtin_capitalize = BuiltinCapitalize() @@ -612,6 +661,8 @@ builtin_list_item = BuiltinListitem() builtin_lookup = BuiltinLookup() builtin_lowercase = BuiltinLowercase() builtin_multiply = BuiltinMultiply() +builtin_not = BuiltinNot() +builtin_or = BuiltinOr() builtin_print = BuiltinPrint() builtin_raw_field = BuiltinRaw_field() builtin_re = BuiltinRe() From aa30d964db0b8fbacfb963cfce3fd08cb30759cd Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 25 May 2011 18:09:41 +0100 Subject: [PATCH 10/10] Slight improvement to user device faq entry --- src/calibre/manual/faq.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst index d3784eda6f..b120fd4a1b 100644 --- a/src/calibre/manual/faq.rst +++ b/src/calibre/manual/faq.rst @@ -138,7 +138,7 @@ Follow these steps to find the problem: My device is non-standard or unusual. What can I do to connect to it? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -In addition to the :guilabel:`Connect to Folder` function found under the Connect/Share button, |app| provides a ``User Defined`` device plugin that can be used to connect to any USB device that presents that shows up as a disk drive in your operating system. See the device plugin ``Preferences -> Plugins -> Device Plugins -> User Defined`` and ``Preferences -> Miscellaneous -> Get information to setup the user defined device`` for more information. +In addition to the :guilabel:`Connect to Folder` function found under the Connect/Share button, |app| provides a ``User Defined`` device plugin that can be used to connect to any USB device that shows up as a disk drive in your operating system. Note: on windows, the device must have a drive letter for calibre to use it. See the device plugin ``Preferences -> Plugins -> Device Plugins -> User Defined`` and ``Preferences -> Miscellaneous -> Get information to setup the user defined device`` for more information. How does |app| manage collections on my SONY reader? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~