merge with John's branch

This commit is contained in:
Tomasz Długosz 2011-05-22 13:30:34 +02:00
commit 62c390ecc9
14 changed files with 566 additions and 99 deletions

View File

@ -854,6 +854,17 @@ class ActionStore(InterfaceActionBase):
name = 'Store' name = 'Store'
author = 'John Schember' author = 'John Schember'
actual_plugin = 'calibre.gui2.actions.store:StoreAction' 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)
plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog, plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
ActionConvert, ActionDelete, ActionEditMetadata, ActionView, ActionConvert, ActionDelete, ActionEditMetadata, ActionView,
@ -1097,144 +1108,255 @@ class StoreAmazonKindleStore(StoreBase):
name = 'Amazon Kindle' name = 'Amazon Kindle'
description = _('Kindle books from Amazon.') description = _('Kindle books from Amazon.')
actual_plugin = 'calibre.gui2.store.amazon_plugin:AmazonKindleStore' actual_plugin = 'calibre.gui2.store.amazon_plugin:AmazonKindleStore'
drm_free_only = False
location = 'US'
formats = ['KINDLE']
class StoreAmazonDEKindleStore(StoreBase): class StoreAmazonDEKindleStore(StoreBase):
name = 'Amazon DE Kindle' name = 'Amazon DE Kindle'
description = _('Kindle books from Amazon.de.') description = _('Kindle books from Amazon.de.')
actual_plugin = 'calibre.gui2.store.amazon_de_plugin:AmazonDEKindleStore' actual_plugin = 'calibre.gui2.store.amazon_de_plugin:AmazonDEKindleStore'
drm_free_only = False
location = 'DE'
formats = ['KINDLE']
class StoreAmazonUKKindleStore(StoreBase): class StoreAmazonUKKindleStore(StoreBase):
name = 'Amazon UK Kindle' name = 'Amazon UK Kindle'
description = _('Kindle books from Amazon.uk.') description = _('Kindle books from Amazon.uk.')
actual_plugin = 'calibre.gui2.store.amazon_uk_plugin:AmazonUKKindleStore' actual_plugin = 'calibre.gui2.store.amazon_uk_plugin:AmazonUKKindleStore'
drm_free_only = False
location = 'UK'
formats = ['KINDLE']
class StoreArchiveOrgStore(StoreBase): class StoreArchiveOrgStore(StoreBase):
name = 'Archive.org' name = 'Archive.org'
description = _('Free Books : Download & Streaming : Ebook and Texts Archive : Internet Archive.') description = _('Free Books : Download & Streaming : Ebook and Texts Archive : Internet Archive.')
actual_plugin = 'calibre.gui2.store.archive_org_plugin:ArchiveOrgStore' actual_plugin = 'calibre.gui2.store.archive_org_plugin:ArchiveOrgStore'
drm_free_only = True
location = 'US'
formats = ['DAISY', 'DJVU', 'EPUB', 'MOBI', 'PDF', 'TXT']
class StoreBaenWebScriptionStore(StoreBase): class StoreBaenWebScriptionStore(StoreBase):
name = 'Baen WebScription' name = 'Baen WebScription'
description = _('Ebooks for readers.') description = _('Ebooks for readers.')
actual_plugin = 'calibre.gui2.store.baen_webscription_plugin:BaenWebScriptionStore' actual_plugin = 'calibre.gui2.store.baen_webscription_plugin:BaenWebScriptionStore'
drm_free_only = True
location = 'US'
formats = ['EPUB', 'LIT', 'LRF', 'MOBI', 'RB', 'RTF', 'ZIP']
class StoreBNStore(StoreBase): class StoreBNStore(StoreBase):
name = 'Barnes and Noble' name = 'Barnes and Noble'
description = _('Books, Textbooks, eBooks, Toys, Games and More.') description = _('Books, Textbooks, eBooks, Toys, Games and More.')
actual_plugin = 'calibre.gui2.store.bn_plugin:BNStore' actual_plugin = 'calibre.gui2.store.bn_plugin:BNStore'
drm_free_only = False
location = 'US'
formats = ['NOOK']
class StoreBeamEBooksDEStore(StoreBase): class StoreBeamEBooksDEStore(StoreBase):
name = 'Beam EBooks DE' name = 'Beam EBooks DE'
description = _('Der eBook Shop.') description = _('Der eBook Shop.')
actual_plugin = 'calibre.gui2.store.beam_ebooks_de_plugin:BeamEBooksDEStore' actual_plugin = 'calibre.gui2.store.beam_ebooks_de_plugin:BeamEBooksDEStore'
drm_free_only = False
location = 'DE'
formats = ['MOBI', 'PDF']
class StoreBeWriteStore(StoreBase): class StoreBeWriteStore(StoreBase):
name = 'BeWrite Books' name = 'BeWrite Books'
description = _('Publishers of fine books.') description = _('Publishers of fine books.')
actual_plugin = 'calibre.gui2.store.bewrite_plugin:BeWriteStore' actual_plugin = 'calibre.gui2.store.bewrite_plugin:BeWriteStore'
drm_free_only = True
location = 'US'
formats = ['EPUB', 'MOBI', 'PDF']
class StoreDieselEbooksStore(StoreBase): class StoreDieselEbooksStore(StoreBase):
name = 'Diesel eBooks' name = 'Diesel eBooks'
description = _('World Famous eBook Store.') description = _('World Famous eBook Store.')
actual_plugin = 'calibre.gui2.store.diesel_ebooks_plugin:DieselEbooksStore' actual_plugin = 'calibre.gui2.store.diesel_ebooks_plugin:DieselEbooksStore'
drm_free_only = False
location = 'US'
formats = ['EPUB', 'PDF']
class StoreEbookscomStore(StoreBase): class StoreEbookscomStore(StoreBase):
name = 'eBooks.com' name = 'eBooks.com'
description = _('The digital bookstore.') description = _('The digital bookstore.')
actual_plugin = 'calibre.gui2.store.ebooks_com_plugin:EbookscomStore' actual_plugin = 'calibre.gui2.store.ebooks_com_plugin:EbookscomStore'
drm_free_only = False
location = 'US'
formats = ['EPUB', 'LIT', 'MOBI', 'PDF']
class StoreEPubBuyDEStore(StoreBase): class StoreEPubBuyDEStore(StoreBase):
name = 'EPUBBuy DE' name = 'EPUBBuy DE'
description = _('EPUBReaders eBook Shop.') description = _('EPUBReaders eBook Shop.')
actual_plugin = 'calibre.gui2.store.epubbuy_de_plugin:EPubBuyDEStore' actual_plugin = 'calibre.gui2.store.epubbuy_de_plugin:EPubBuyDEStore'
drm_free_only = True
location = 'DE'
formats = ['EPUB']
class StoreEHarlequinStore(StoreBase): class StoreEHarlequinStore(StoreBase):
name = 'eHarlequin' name = 'eHarlequin'
description = _('Entertain, enrich, inspire.') description = _('Entertain, enrich, inspire.')
actual_plugin = 'calibre.gui2.store.eharlequin_plugin:EHarlequinStore' actual_plugin = 'calibre.gui2.store.eharlequin_plugin:EHarlequinStore'
drm_free_only = False
location = 'US'
formats = ['EPUB', 'PDF']
class StoreFeedbooksStore(StoreBase): class StoreFeedbooksStore(StoreBase):
name = 'Feedbooks' name = 'Feedbooks'
description = _('Read anywhere.') description = _('Read anywhere.')
actual_plugin = 'calibre.gui2.store.feedbooks_plugin:FeedbooksStore' actual_plugin = 'calibre.gui2.store.feedbooks_plugin:FeedbooksStore'
drm_free_only = False
location = 'FR'
formats = ['EPUB', 'MOBI', 'PDF']
class StoreFoylesUKStore(StoreBase): class StoreFoylesUKStore(StoreBase):
name = 'Foyles UK' name = 'Foyles UK'
description = _('Foyles of London, online.') description = _('Foyles of London, online.')
actual_plugin = 'calibre.gui2.store.foyles_uk_plugin:FoylesUKStore' actual_plugin = 'calibre.gui2.store.foyles_uk_plugin:FoylesUKStore'
drm_free_only = False
location = 'UK'
formats = ['EPUB', 'PDF']
class StoreGandalfStore(StoreBase): class StoreGandalfStore(StoreBase):
name = 'Gandalf' name = 'Gandalf'
author = 'Tomasz Długosz' author = 'Tomasz Długosz'
description = _('Zaczarowany świat książek') description = _('Zaczarowany świat książek')
actual_plugin = 'calibre.gui2.store.gandalf_plugin:GandalfStore' actual_plugin = 'calibre.gui2.store.gandalf_plugin:GandalfStore'
drm_free_only = False
location = 'PL'
formats = ['EPUB', 'PDF']
class StoreGoogleBooksStore(StoreBase): class StoreGoogleBooksStore(StoreBase):
name = 'Google Books' name = 'Google Books'
description = _('Google Books') description = _('Google Books')
actual_plugin = 'calibre.gui2.store.google_books_plugin:GoogleBooksStore' actual_plugin = 'calibre.gui2.store.google_books_plugin:GoogleBooksStore'
drm_free_only = False
location = 'US'
formats = ['EPUB', 'PDF', 'TXT']
class StoreGutenbergStore(StoreBase): class StoreGutenbergStore(StoreBase):
name = 'Project Gutenberg' name = 'Project Gutenberg'
description = _('The first producer of free ebooks.') description = _('The first producer of free ebooks.')
actual_plugin = 'calibre.gui2.store.gutenberg_plugin:GutenbergStore' actual_plugin = 'calibre.gui2.store.gutenberg_plugin:GutenbergStore'
drm_free_only = True
location = 'US'
formats = ['EPUB', 'HTML', 'MOBI', 'PDB', 'TXT']
class StoreKoboStore(StoreBase): class StoreKoboStore(StoreBase):
name = 'Kobo' name = 'Kobo'
description = _('eReading: anytime. anyplace.') description = _('eReading: anytime. anyplace.')
actual_plugin = 'calibre.gui2.store.kobo_plugin:KoboStore' actual_plugin = 'calibre.gui2.store.kobo_plugin:KoboStore'
drm_free_only = False
location = 'US'
formats = ['EPUB']
class StoreManyBooksStore(StoreBase): class StoreManyBooksStore(StoreBase):
name = 'ManyBooks' name = 'ManyBooks'
description = _('The best ebooks at the best price: free!') description = _('The best ebooks at the best price: free!')
actual_plugin = 'calibre.gui2.store.manybooks_plugin:ManyBooksStore' actual_plugin = 'calibre.gui2.store.manybooks_plugin:ManyBooksStore'
drm_free_only = True
location = 'US'
formats = ['EPUB', 'FB2', 'JAR', 'LIT', 'LRF', 'MOBI', 'PDB', 'PDF', 'RB', 'RTF', 'TCR', 'TXT', 'ZIP']
class StoreMobileReadStore(StoreBase): class StoreMobileReadStore(StoreBase):
name = 'MobileRead' name = 'MobileRead'
description = _('Ebooks handcrafted with the utmost care.') description = _('Ebooks handcrafted with the utmost care.')
actual_plugin = 'calibre.gui2.store.mobileread.mobileread_plugin:MobileReadStore' actual_plugin = 'calibre.gui2.store.mobileread.mobileread_plugin:MobileReadStore'
drm_free_only = True
location = 'CH'
formats = ['EPUB', 'IMP', 'LRF', 'LIT', 'MOBI', 'PDF']
class StoreNextoStore(StoreBase): class StoreNextoStore(StoreBase):
name = 'Nexto' name = 'Nexto'
author = 'Tomasz Długosz' author = 'Tomasz Długosz'
description = _('Audiobooki mp3, ebooki, prasa - księgarnia internetowa.') description = _('Audiobooki mp3, ebooki, prasa - księgarnia internetowa.')
actual_plugin = 'calibre.gui2.store.nexto_plugin:NextoStore' actual_plugin = 'calibre.gui2.store.nexto_plugin:NextoStore'
drm_free_only = False
location = 'PL'
formats = ['EPUB', 'PDF']
class StoreOpenLibraryStore(StoreBase): class StoreOpenLibraryStore(StoreBase):
name = 'Open Library' name = 'Open Library'
description = _('One web page for every book.') description = _('One web page for every book.')
actual_plugin = 'calibre.gui2.store.open_library_plugin:OpenLibraryStore' actual_plugin = 'calibre.gui2.store.open_library_plugin:OpenLibraryStore'
drm_free_only = True
location = ['US']
formats = ['DAISY', 'DJVU', 'EPUB', 'MOBI', 'PDF', 'TXT']
class StoreOReillyStore(StoreBase): class StoreOReillyStore(StoreBase):
name = 'OReilly' name = 'OReilly'
description = _('DRM-Free tech ebooks.') description = _('DRM-Free tech ebooks.')
actual_plugin = 'calibre.gui2.store.oreilly_plugin:OReillyStore' actual_plugin = 'calibre.gui2.store.oreilly_plugin:OReillyStore'
drm_free_only = True
location = 'US'
formats = ['APK', 'DAISY', 'EPUB', 'MOBI', 'PDF']
class StorePragmaticBookshelfStore(StoreBase): class StorePragmaticBookshelfStore(StoreBase):
name = 'Pragmatic Bookshelf' name = 'Pragmatic Bookshelf'
description = _('The Pragmatic Bookshelf') description = _('The Pragmatic Bookshelf')
actual_plugin = 'calibre.gui2.store.pragmatic_bookshelf_plugin:PragmaticBookshelfStore' actual_plugin = 'calibre.gui2.store.pragmatic_bookshelf_plugin:PragmaticBookshelfStore'
drm_free_only = True
location = 'US'
formats = ['EPUB', 'MOBI', 'PDF']
class StoreSmashwordsStore(StoreBase): class StoreSmashwordsStore(StoreBase):
name = 'Smashwords' name = 'Smashwords'
description = _('Your ebook. Your way.') description = _('Your ebook. Your way.')
actual_plugin = 'calibre.gui2.store.smashwords_plugin:SmashwordsStore' actual_plugin = 'calibre.gui2.store.smashwords_plugin:SmashwordsStore'
drm_free_only = True
location = 'US'
formats = ['EPUB', 'HTML', 'LRF', 'MOBI', 'PDB', 'RTF', 'TXT']
class StoreWaterstonesUKStore(StoreBase): class StoreWaterstonesUKStore(StoreBase):
name = 'Waterstones UK' name = 'Waterstones UK'
description = _('Feel every word.') description = _('Feel every word.')
actual_plugin = 'calibre.gui2.store.waterstones_uk_plugin:WaterstonesUKStore' actual_plugin = 'calibre.gui2.store.waterstones_uk_plugin:WaterstonesUKStore'
drm_free_only = False
location = 'UK'
formats = ['EPUB', 'PDF']
class StoreWeightlessBooksStore(StoreBase): class StoreWeightlessBooksStore(StoreBase):
name = 'Weightless Books' name = 'Weightless Books'
description = '(e)Books That Don\'t Weigh You Down.' description = '(e)Books That Don\'t Weigh You Down.'
actual_plugin = 'calibre.gui2.store.weightless_books_plugin:WeightlessBooksStore' actual_plugin = 'calibre.gui2.store.weightless_books_plugin:WeightlessBooksStore'
drm_free_only = True
location = 'US'
formats = ['EPUB', 'HTML', 'LIT', 'MOBI', 'PDF']
class StoreWizardsTowerBooksStore(StoreBase): class StoreWizardsTowerBooksStore(StoreBase):
name = 'Wizards Tower Books' name = 'Wizards Tower Books'
description = 'Wizard\'s Tower Press.' description = 'Wizard\'s Tower Press.'
actual_plugin = 'calibre.gui2.store.wizards_tower_books_plugin:WizardsTowerBooksStore' actual_plugin = 'calibre.gui2.store.wizards_tower_books_plugin:WizardsTowerBooksStore'
drm_free_only = True
location = 'UK'
formats = ['EPUB', 'MOBI']
class StoreWoblinkStore(StoreBase): class StoreWoblinkStore(StoreBase):
name = 'Woblink' name = 'Woblink'

View File

@ -216,9 +216,26 @@ def store_plugins():
customization = config['plugin_customization'] customization = config['plugin_customization']
for plugin in _initialized_plugins: for plugin in _initialized_plugins:
if isinstance(plugin, Store): if isinstance(plugin, Store):
if not is_disabled(plugin): plugin.site_customization = customization.get(plugin.name, '')
plugin.site_customization = customization.get(plugin.name, '') yield plugin
yield plugin
def available_store_plugins():
for plugin in store_plugins():
if not is_disabled(plugin):
yield plugin
def stores():
stores = set([])
for plugin in store_plugins():
stores.add(plugin.name)
return stores
def available_stores():
stores = set([])
for plugin in available_store_plugins():
stores.add(plugin.name)
return stores
# }}} # }}}
# Metadata read/write {{{ # Metadata read/write {{{

View File

@ -16,7 +16,7 @@ class AmazonDEKindleStore(AmazonKindleStore):
For comments on the implementation, please see amazon_plugin.py For comments on the implementation, please see amazon_plugin.py
''' '''
search_url = 'http://www.amazon.de/s/url=search-alias%3Ddigital-text&field-keywords=' search_url = 'http://www.amazon.de/s/?url=search-alias%3Ddigital-text&field-keywords='
details_url = 'http://amazon.de/dp/' details_url = 'http://amazon.de/dp/'
drm_search_text = u'Gleichzeitige Verwendung von Geräten' drm_search_text = u'Gleichzeitige Verwendung von Geräten'
drm_free_text = u'Keine Einschränkung' drm_free_text = u'Keine Einschränkung'

View File

@ -17,7 +17,7 @@ class AmazonUKKindleStore(AmazonKindleStore):
For comments on the implementation, please see amazon_plugin.py For comments on the implementation, please see amazon_plugin.py
''' '''
search_url = 'http://www.amazon.co.uk/s/url=search-alias%3Ddigital-text&field-keywords=' search_url = 'http://www.amazon.co.uk/s/?url=search-alias%3Ddigital-text&field-keywords='
details_url = 'http://amazon.co.uk/dp/' details_url = 'http://amazon.co.uk/dp/'
def open(self, parent=None, detail_item=None, external=False): def open(self, parent=None, detail_item=None, external=False):

View File

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import, print_function)
__license__ = 'GPL 3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
from PyQt4.Qt import QWidget
from calibre.gui2 import JSONConfig
from calibre.gui2.store.config.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))
self.opt_cover_thread_count.setValue(self.config.get('cover_thread_count', 2))
self.opt_details_thread_count.setValue(self.config.get('details_thread_count', 4))
def save_settings(self):
self.config['timeout'] = self.opt_timeout.value()
self.config['hang_time'] = self.opt_hang_time.value()
self.config['max_results'] = self.opt_max_results.value()
self.config['open_external'] = self.opt_open_external.isChecked()
self.config['search_thread_count'] = self.opt_search_thread_count.value()
self.config['cache_thread_count'] = self.opt_cache_thread_count.value()
self.config['cover_thread_count'] = self.opt_cover_thread_count.value()
self.config['details_thread_count'] = self.opt_details_thread_count.value()

View File

@ -0,0 +1,162 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>465</width>
<height>396</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Time</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Number of seconds to wait for a store to respond</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="opt_timeout">
<property name="minimum">
<number>1</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Number of seconds to let a store process results</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="opt_hang_time">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>99</number>
</property>
<property name="singleStep">
<number>1</number>
</property>
<property name="value">
<number>1</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Display</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Maximum number of results to show per store</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="opt_max_results">
<property name="minimum">
<number>1</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="opt_open_external">
<property name="text">
<string>Open search result in system browser</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Threads</string>
</property>
<layout class="QFormLayout" name="formLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Number of search threads to use</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="opt_search_thread_count">
<property name="minimum">
<number>1</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Number of cache update threads to use</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="opt_cache_thread_count">
<property name="minimum">
<number>1</number>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Number of conver download threads to use</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="opt_cover_thread_count">
<property name="minimum">
<number>1</number>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Number of details threads to use</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QSpinBox" name="opt_details_thread_count">
<property name="minimum">
<number>1</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import, print_function)
__license__ = 'GPL 3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
'''
Config widget access functions for configuring the store action.
'''
def config_widget():
from calibre.gui2.store.config.search_widget import StoreConfigWidget
return StoreConfigWidget()
def save_settings(config_widget):
config_widget.save_settings()

View File

@ -51,7 +51,7 @@ class GoogleBooksStore(BasicStoreConfig, StorePlugin):
title = ''.join(data.xpath('.//h3/a//text()')) title = ''.join(data.xpath('.//h3/a//text()'))
authors = data.xpath('.//span[@class="gl"]//a//text()') authors = data.xpath('.//span[@class="gl"]//a//text()')
if authors[-1].strip().lower() == 'preview': if authors[-1].strip().lower() in ('preview', 'read'):
authors = authors[:-1] authors = authors[:-1]
else: else:
continue continue

View File

@ -22,7 +22,7 @@ class GenericDownloadThreadPool(object):
at the end of the function. at the end of the function.
''' '''
def __init__(self, thread_type, thread_count): def __init__(self, thread_type, thread_count=1):
self.thread_type = thread_type self.thread_type = thread_type
self.thread_count = thread_count self.thread_count = thread_count
@ -30,6 +30,9 @@ class GenericDownloadThreadPool(object):
self.results = Queue() self.results = Queue()
self.threads = [] self.threads = []
def set_thread_count(self, thread_count):
self.thread_count = thread_count
def add_task(self): def add_task(self):
''' '''
This must be implemented in a sub class and this function This must be implemented in a sub class and this function
@ -92,8 +95,8 @@ class SearchThreadPool(GenericDownloadThreadPool):
def __init__(self, thread_count): def __init__(self, thread_count):
GenericDownloadThreadPool.__init__(self, SearchThread, thread_count) GenericDownloadThreadPool.__init__(self, SearchThread, thread_count)
def add_task(self, query, store_name, store_plugin, timeout): def add_task(self, query, store_name, store_plugin, max_results, timeout):
self.tasks.put((query, store_name, store_plugin, timeout)) self.tasks.put((query, store_name, store_plugin, max_results, timeout))
GenericDownloadThreadPool.add_task(self) GenericDownloadThreadPool.add_task(self)
@ -112,8 +115,8 @@ class SearchThread(Thread):
def run(self): def run(self):
while self._run and not self.tasks.empty(): while self._run and not self.tasks.empty():
try: try:
query, store_name, store_plugin, timeout = self.tasks.get() query, store_name, store_plugin, max_results, timeout = self.tasks.get()
for res in store_plugin.search(query, timeout=timeout): for res in store_plugin.search(query, max_results=max_results, timeout=timeout):
if not self._run: if not self._run:
return return
res.store_name = store_name res.store_name = store_name

View File

@ -9,7 +9,8 @@ __docformat__ = 'restructuredtext en'
import re import re
from operator import attrgetter from operator import attrgetter
from PyQt4.Qt import (Qt, QAbstractItemModel, QVariant, QPixmap, QModelIndex, QSize) from PyQt4.Qt import (Qt, QAbstractItemModel, QVariant, QPixmap, QModelIndex, QSize,
pyqtSignal)
from calibre.gui2 import NONE from calibre.gui2 import NONE
from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.search_result import SearchResult
@ -30,10 +31,12 @@ def comparable_price(text):
class Matches(QAbstractItemModel): class Matches(QAbstractItemModel):
total_changed = pyqtSignal(int)
HEADERS = [_('Cover'), _('Title'), _('Price'), _('DRM'), _('Store')] HEADERS = [_('Cover'), _('Title'), _('Price'), _('DRM'), _('Store')]
HTML_COLS = (1, 4) HTML_COLS = (1, 4)
def __init__(self): def __init__(self, cover_thread_count=2, detail_thread_count=4):
QAbstractItemModel.__init__(self) QAbstractItemModel.__init__(self)
self.DRM_LOCKED_ICON = QPixmap(I('drm-locked.png')).scaledToHeight(64, self.DRM_LOCKED_ICON = QPixmap(I('drm-locked.png')).scaledToHeight(64,
@ -51,8 +54,8 @@ class Matches(QAbstractItemModel):
self.matches = [] self.matches = []
self.query = '' self.query = ''
self.search_filter = SearchFilter() self.search_filter = SearchFilter()
self.cover_pool = CoverThreadPool(2) self.cover_pool = CoverThreadPool(cover_thread_count)
self.details_pool = DetailsThreadPool(4) self.details_pool = DetailsThreadPool(detail_thread_count)
self.sort_col = 2 self.sort_col = 2
self.sort_order = Qt.AscendingOrder self.sort_order = Qt.AscendingOrder
@ -69,6 +72,7 @@ class Matches(QAbstractItemModel):
self.query = '' self.query = ''
self.cover_pool.abort() self.cover_pool.abort()
self.details_pool.abort() self.details_pool.abort()
self.total_changed.emit(self.rowCount())
self.reset() self.reset()
def add_result(self, result, store_plugin): def add_result(self, result, store_plugin):
@ -101,6 +105,7 @@ class Matches(QAbstractItemModel):
self.matches = list(self.search_filter.parse(self.query)) self.matches = list(self.search_filter.parse(self.query))
else: else:
self.matches = list(self.search_filter.universal_set()) self.matches = list(self.search_filter.universal_set())
self.total_changed.emit(self.rowCount())
self.sort(self.sort_col, self.sort_order, False) self.sort(self.sort_col, self.sort_order, False)
self.layoutChanged.emit() self.layoutChanged.emit()

View File

@ -9,18 +9,17 @@ __docformat__ = 'restructuredtext en'
import re import re
from random import shuffle from random import shuffle
from PyQt4.Qt import (Qt, QDialog, QTimer, QCheckBox, QVBoxLayout, QIcon, QWidget) from PyQt4.Qt import (Qt, QDialog, QDialogButtonBox, QTimer, QCheckBox,
QVBoxLayout, QIcon, QWidget)
from calibre.gui2 import JSONConfig, info_dialog from calibre.gui2 import JSONConfig, info_dialog
from calibre.gui2.progress_indicator import ProgressIndicator from calibre.gui2.progress_indicator import ProgressIndicator
from calibre.gui2.store.config.search_widget import StoreConfigWidget
from calibre.gui2.store.search.adv_search_builder import AdvSearchBuilderDialog from calibre.gui2.store.search.adv_search_builder import AdvSearchBuilderDialog
from calibre.gui2.store.search.download_thread import SearchThreadPool, \ from calibre.gui2.store.search.download_thread import SearchThreadPool, \
CacheUpdateThreadPool CacheUpdateThreadPool
from calibre.gui2.store.search.search_ui import Ui_Dialog from calibre.gui2.store.search.search_ui import Ui_Dialog
HANG_TIME = 75000 # milliseconds seconds
TIMEOUT = 75 # seconds
class SearchDialog(QDialog, Ui_Dialog): class SearchDialog(QDialog, Ui_Dialog):
def __init__(self, istores, parent=None, query=''): def __init__(self, istores, parent=None, query=''):
@ -28,13 +27,22 @@ class SearchDialog(QDialog, Ui_Dialog):
self.setupUi(self) self.setupUi(self)
self.config = JSONConfig('store/search') self.config = JSONConfig('store/search')
self.search_edit.initialize('store_search_search') self.search_edit.initialize('store_search_search')
# Loads variables that store various settings.
# This needs to be called soon in __init__ because
# the variables it sets up are used later.
self.load_settings()
# We keep a cache of store plugins and reference them by name. # We keep a cache of store plugins and reference them by name.
self.store_plugins = istores self.store_plugins = istores
self.search_pool = SearchThreadPool(4)
self.cache_pool = CacheUpdateThreadPool(2) # Setup our worker threads.
self.search_pool = SearchThreadPool(self.search_thread_count)
self.cache_pool = CacheUpdateThreadPool(self.cache_thread_count)
self.results_view.model().cover_pool.set_thread_count(self.cover_thread_count)
self.results_view.model().details_pool.set_thread_count(self.details_thread_count)
# Check for results and hung threads. # Check for results and hung threads.
self.checker = QTimer() self.checker = QTimer()
self.progress_checker = QTimer() self.progress_checker = QTimer()
@ -42,21 +50,21 @@ class SearchDialog(QDialog, Ui_Dialog):
# Update store caches silently. # Update store caches silently.
for p in self.store_plugins.values(): for p in self.store_plugins.values():
self.cache_pool.add_task(p, 30) self.cache_pool.add_task(p, self.timeout)
# Add check boxes for each store so the user # Add check boxes for each store so the user
# can disable searching specific stores on a # can disable searching specific stores on a
# per search basis. # per search basis.
stores_check_widget = QWidget() stores_check_widget = QWidget()
stores_group_layout = QVBoxLayout() store_list_layout = QVBoxLayout()
stores_check_widget.setLayout(stores_group_layout) stores_check_widget.setLayout(store_list_layout)
for x in sorted(self.store_plugins.keys(), key=lambda x: x.lower()): for x in sorted(self.store_plugins.keys(), key=lambda x: x.lower()):
cbox = QCheckBox(x) cbox = QCheckBox(x)
cbox.setChecked(False) cbox.setChecked(False)
stores_group_layout.addWidget(cbox) store_list_layout.addWidget(cbox)
setattr(self, 'store_check_' + x, cbox) setattr(self, 'store_check_' + x, cbox)
stores_group_layout.addStretch() store_list_layout.addStretch()
self.stores_group.setWidget(stores_check_widget) self.store_list.setWidget(stores_check_widget)
# Set the search query # Set the search query
self.search_edit.setText(query) self.search_edit.setText(query)
@ -66,15 +74,18 @@ class SearchDialog(QDialog, Ui_Dialog):
self.top_layout.addWidget(self.pi) self.top_layout.addWidget(self.pi)
self.adv_search_button.setIcon(QIcon(I('search.png'))) self.adv_search_button.setIcon(QIcon(I('search.png')))
self.configure.setIcon(QIcon(I('config.png')))
self.adv_search_button.clicked.connect(self.build_adv_search) self.adv_search_button.clicked.connect(self.build_adv_search)
self.search.clicked.connect(self.do_search) self.search.clicked.connect(self.do_search)
self.checker.timeout.connect(self.get_results) self.checker.timeout.connect(self.get_results)
self.progress_checker.timeout.connect(self.check_progress) self.progress_checker.timeout.connect(self.check_progress)
self.results_view.activated.connect(self.open_store) self.results_view.activated.connect(self.open_store)
self.results_view.model().total_changed.connect(self.update_book_total)
self.select_all_stores.clicked.connect(self.stores_select_all) self.select_all_stores.clicked.connect(self.stores_select_all)
self.select_invert_stores.clicked.connect(self.stores_select_invert) self.select_invert_stores.clicked.connect(self.stores_select_invert)
self.select_none_stores.clicked.connect(self.stores_select_none) self.select_none_stores.clicked.connect(self.stores_select_none)
self.configure.clicked.connect(self.do_config)
self.finished.connect(self.dialog_closed) self.finished.connect(self.dialog_closed)
self.progress_checker.start(100) self.progress_checker.start(100)
@ -128,7 +139,7 @@ class SearchDialog(QDialog, Ui_Dialog):
# Add plugins that the user has checked to the search pool's work queue. # Add plugins that the user has checked to the search pool's work queue.
for n in store_names: for n in store_names:
if getattr(self, 'store_check_' + n).isChecked(): if getattr(self, 'store_check_' + n).isChecked():
self.search_pool.add_task(query, n, self.store_plugins[n], TIMEOUT) self.search_pool.add_task(query, n, self.store_plugins[n], self.max_results, self.timeout)
self.hang_check = 0 self.hang_check = 0
self.checker.start(100) self.checker.start(100)
self.pi.startAnimation() self.pi.startAnimation()
@ -190,7 +201,7 @@ class SearchDialog(QDialog, Ui_Dialog):
else: else:
self.resize_columns() self.resize_columns()
self.open_external.setChecked(self.config.get('open_external', True)) self.open_external.setChecked(self.should_open_external)
store_check = self.config.get('store_checked', None) store_check = self.config.get('store_checked', None)
if store_check: if store_check:
@ -202,11 +213,56 @@ class SearchDialog(QDialog, Ui_Dialog):
self.results_view.model().sort_order = self.config.get('sort_order', Qt.AscendingOrder) self.results_view.model().sort_order = self.config.get('sort_order', Qt.AscendingOrder)
self.results_view.header().setSortIndicator(self.results_view.model().sort_col, self.results_view.model().sort_order) self.results_view.header().setSortIndicator(self.results_view.model().sort_col, self.results_view.model().sort_order)
def load_settings(self):
# Seconds
self.timeout = self.config.get('timeout', 75)
# Milliseconds
self.hang_time = self.config.get('hang_time', 75) * 1000
self.max_results = self.config.get('max_results', 10)
self.should_open_external = self.config.get('open_external', True)
# Number of threads to run for each type of operation
self.search_thread_count = self.config.get('search_thread_count', 4)
self.cache_thread_count = self.config.get('cache_thread_count', 2)
self.cover_thread_count = self.config.get('cover_thread_count', 2)
self.details_thread_count = self.config.get('details_thread_count', 4)
def do_config(self):
# Save values that need to be synced between the dialog and the
# search widget.
self.config['open_external'] = self.open_external.isChecked()
d = QDialog(self)
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
v = QVBoxLayout(d)
button_box.accepted.connect(d.accept)
button_box.rejected.connect(d.reject)
d.setWindowTitle(_('Customize get books search'))
config_widget = StoreConfigWidget(self.config)
v.addWidget(config_widget)
v.addWidget(button_box)
d.exec_()
if d.result() == QDialog.Accepted:
config_widget.save_settings()
self.config_changed()
def config_changed(self):
self.load_settings()
self.open_external.setChecked(self.should_open_external)
self.search_pool.set_thread_count(self.search_thread_count)
self.cache_pool.set_thread_count(self.cache_thread_count)
self.results_view.model().cover_pool.set_thread_count(self.cover_thread_count)
self.results_view.model().details_pool.set_thread_count(self.details_thread_count)
def get_results(self): def get_results(self):
# We only want the search plugins to run # We only want the search plugins to run
# a maximum set amount of time before giving up. # a maximum set amount of time before giving up.
self.hang_check += 1 self.hang_check += 1
if self.hang_check >= HANG_TIME: if self.hang_check >= self.hang_time:
self.search_pool.abort() self.search_pool.abort()
self.checker.stop() self.checker.stop()
else: else:
@ -222,6 +278,8 @@ class SearchDialog(QDialog, Ui_Dialog):
if not self.search_pool.threads_running() and not self.results_view.model().has_results(): if not self.search_pool.threads_running() and not self.results_view.model().has_results():
info_dialog(self, _('No matches'), _('Couldn\'t find any books matching your query.'), show=True, show_copy_button=False) info_dialog(self, _('No matches'), _('Couldn\'t find any books matching your query.'), show=True, show_copy_button=False)
def update_book_total(self, total):
self.total.setText('%s' % total)
def open_store(self, index): def open_store(self, index):
result = self.results_view.model().get_result(index) result = self.results_view.model().get_result(index)

View File

@ -6,8 +6,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>937</width> <width>584</width>
<height>669</height> <height>533</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@ -20,7 +20,7 @@
<property name="sizeGripEnabled"> <property name="sizeGripEnabled">
<bool>true</bool> <bool>true</bool>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QVBoxLayout" name="verticalLayout_5">
<item> <item>
<layout class="QHBoxLayout" name="top_layout"> <layout class="QHBoxLayout" name="top_layout">
<item> <item>
@ -66,8 +66,14 @@
<string>Stores</string> <string>Stores</string>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_2"> <layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item> <item>
<widget class="QScrollArea" name="stores_group"> <widget class="QScrollArea" name="store_list">
<property name="widgetResizable"> <property name="widgetResizable">
<bool>true</bool> <bool>true</bool>
</property> </property>
@ -76,15 +82,18 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>215</width> <width>102</width>
<height>93</height> <height>129</height>
</rect> </rect>
</property> </property>
</widget> </widget>
</widget> </widget>
</item> </item>
<item> <item>
<layout class="QHBoxLayout" name="horizontalLayout_3"> <layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<item> <item>
<widget class="QPushButton" name="select_all_stores"> <widget class="QPushButton" name="select_all_stores">
<property name="text"> <property name="text">
@ -108,76 +117,104 @@
</item> </item>
</layout> </layout>
</item> </item>
</layout>
</widget>
<widget class="QWidget" name="verticalLayoutWidget">
<layout class="QVBoxLayout" name="verticalLayout_4">
<item> <item>
<widget class="QCheckBox" name="open_external"> <widget class="ResultsView" name="results_view">
<property name="toolTip"> <property name="sizePolicy">
<string>Open a selected book in the system's web browser</string> <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property> </property>
<property name="text"> <property name="minimumSize">
<string>Open in &amp;external browser</string> <size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<property name="uniformRowHeights">
<bool>false</bool>
</property>
<property name="itemsExpandable">
<bool>false</bool>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
<property name="expandsOnDoubleClick">
<bool>false</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QToolButton" name="configure">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="open_external">
<property name="toolTip">
<string>Open a selected book in the system's web browser</string>
</property>
<property name="text">
<string>Open in &amp;external browser</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout> </layout>
</widget> </widget>
<widget class="QSplitter" name="splitter_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>2</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="ResultsView" name="results_view">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<property name="uniformRowHeights">
<bool>false</bool>
</property>
<property name="itemsExpandable">
<bool>false</bool>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
<property name="expandsOnDoubleClick">
<bool>false</bool>
</property>
</widget>
</widget>
</widget>
</widget> </widget>
</item> </item>
<item> <item>
<layout class="QHBoxLayout" name="bottom_layout"> <layout class="QHBoxLayout" name="bottom_layout">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Books:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="total">
<property name="text">
<string>0</string>
</property>
</widget>
</item>
<item> <item>
<spacer name="horizontalSpacer"> <spacer name="horizontalSpacer">
<property name="orientation"> <property name="orientation">

View File

@ -23,7 +23,7 @@ from calibre.constants import __appname__, isosx
from calibre.utils.config import prefs, dynamic from calibre.utils.config import prefs, dynamic
from calibre.utils.ipc.server import Server from calibre.utils.ipc.server import Server
from calibre.library.database2 import LibraryDatabase2 from calibre.library.database2 import LibraryDatabase2
from calibre.customize.ui import interface_actions, store_plugins from calibre.customize.ui import interface_actions, available_store_plugins
from calibre.gui2 import error_dialog, GetMetadata, open_url, \ from calibre.gui2 import error_dialog, GetMetadata, open_url, \
gprefs, max_available_height, config, info_dialog, Dispatcher, \ gprefs, max_available_height, config, info_dialog, Dispatcher, \
question_dialog question_dialog
@ -144,7 +144,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
def load_store_plugins(self): def load_store_plugins(self):
self.istores = OrderedDict() self.istores = OrderedDict()
for store in store_plugins(): for store in available_store_plugins():
if self.opts.ignore_plugins and store.plugin_path is not None: if self.opts.ignore_plugins and store.plugin_path is not None:
continue continue
try: try: