diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 680e36d0f3..9172a5aec6 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,16 +1267,26 @@ 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'] +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.' 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 +1305,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,16 +1314,16 @@ 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'] + headquarters = 'US' formats = ['DAISY', 'DJVU', 'EPUB', 'MOBI', 'PDF', 'TXT'] 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 +1341,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 +1351,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 +1369,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'] @@ -1371,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 += [ @@ -1389,10 +1399,11 @@ plugins += [ StoreEHarlequinStore, StoreFeedbooksStore, StoreFoylesUKStore, - StoreGandalfStore, + StoreGandalfStore, StoreGoogleBooksStore, StoreGutenbergStore, StoreKoboStore, + StoreLegimiStore, StoreManyBooksStore, StoreMobileReadStore, StoreNextoStore, diff --git a/src/calibre/gui2/actions/store.py b/src/calibre/gui2/actions/store.py index c8507e851c..0fd783f0a3 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,13 @@ 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_() + self.gui.load_store_plugins() + self.load_menu() + def open_store(self, store_plugin): self.show_disclaimer() store_plugin.open(self.gui) 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..0c784f6614 --- /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 getattr(match, 'drm', True) else 'a' + elif col == 3: + text = getattr(match, 'headquarters', '') + 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 + + 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..1c18a18d7b --- /dev/null +++ b/src/calibre/gui2/store/config/chooser/results_view.py @@ -0,0 +1,34 @@ +# -*- 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, 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) + + self.model().sort(1, Qt.AscendingOrder) + self.header().setSortIndicator(self.model().sort_col, self.model().sort_order) 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 95% rename from src/calibre/gui2/store/config/search_widget.py rename to src/calibre/gui2/store/config/search/search_widget.py index 732a270940..b2e55d2ad1 100644 --- a/src/calibre/gui2/store/config/search_widget.py +++ b/src/calibre/gui2/store/config/search/search_widget.py @@ -9,25 +9,25 @@ __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): - + def __init__(self, config=None): QWidget.__init__(self) self.setupUi(self) self.config = JSONConfig('store/search') if not config else config - + # These default values should be the same as in # calibre.gui2.store.search.search:SearchDialog.load_settings # Seconds self.opt_timeout.setValue(self.config.get('timeout', 75)) self.opt_hang_time.setValue(self.config.get('hang_time', 75)) - + self.opt_max_results.setValue(self.config.get('max_results', 10)) self.opt_open_external.setChecked(self.config.get('open_external', True)) - + # Number of threads to run for each type of operation self.opt_search_thread_count.setValue(self.config.get('search_thread_count', 4)) self.opt_cache_thread_count.setValue(self.config.get('cache_thread_count', 2)) diff --git a/src/calibre/gui2/store/config/search_widget.ui b/src/calibre/gui2/store/config/search/search_widget.ui similarity index 93% rename from src/calibre/gui2/store/config/search_widget.ui rename to src/calibre/gui2/store/config/search/search_widget.ui index 0b322b6f63..a73aae3ea5 100644 --- a/src/calibre/gui2/store/config/search_widget.ui +++ b/src/calibre/gui2/store/config/search/search_widget.ui @@ -93,13 +93,13 @@ - Performance + Threads - Number of simultaneous searches + Number of search threads to use @@ -113,7 +113,7 @@ - Number of simultaneous cache updates + Number of cache update threads to use @@ -127,7 +127,7 @@ - Number of simultaneous cover downloads + Number of conver download threads to use @@ -141,7 +141,7 @@ - Number of simultaneous details downloads + Number of details threads to use 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/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 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 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 04743bd7c2..3be39e3e87 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 @@ -238,7 +238,7 @@ class SearchDialog(QDialog, Ui_Dialog): v = QVBoxLayout(d) button_box.accepted.connect(d.accept) button_box.rejected.connect(d.reject) - d.setWindowTitle(_('Customize Get Books')) + d.setWindowTitle(_('Customize get books search')) config_widget = StoreConfigWidget(self.config) v.addWidget(config_widget) v.addWidget(button_box) 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 +