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 1/5] 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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()