mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Add a configuration dialog for Get Books
This commit is contained in:
commit
ff03988bb0
@ -607,9 +607,22 @@ class StoreBase(Plugin): # {{{
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
author = 'John Schember'
|
||||
type = _('Store')
|
||||
# Information about the store. Should be in the primary language
|
||||
# of the store. This should not be translatable when set by
|
||||
# a subclass.
|
||||
description = _('An ebook store.')
|
||||
minimum_calibre_version = (0, 8, 0)
|
||||
version = (1, 0, 1)
|
||||
|
||||
actual_plugin = None
|
||||
|
||||
# Does the store only distribute ebooks without DRM.
|
||||
drm_free_only = False
|
||||
# This is the 2 letter country code for the corporate
|
||||
# headquarters of the store.
|
||||
headquarters = ''
|
||||
# All formats the store distributes ebooks in.
|
||||
formats = []
|
||||
|
||||
def load_actual_plugin(self, gui):
|
||||
'''
|
||||
|
@ -854,6 +854,17 @@ 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)
|
||||
|
||||
plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
|
||||
ActionConvert, ActionDelete, ActionEditMetadata, ActionView,
|
||||
@ -1095,146 +1106,273 @@ plugins += [LookAndFeel, Behavior, Columns, Toolbar, Search, InputOptions,
|
||||
# Store plugins {{{
|
||||
class StoreAmazonKindleStore(StoreBase):
|
||||
name = 'Amazon Kindle'
|
||||
description = _('Kindle books from Amazon.')
|
||||
description = u'Kindle books from Amazon.'
|
||||
actual_plugin = 'calibre.gui2.store.amazon_plugin:AmazonKindleStore'
|
||||
|
||||
drm_free_only = False
|
||||
headquarters = 'US'
|
||||
formats = ['KINDLE']
|
||||
|
||||
class StoreAmazonDEKindleStore(StoreBase):
|
||||
name = 'Amazon DE Kindle'
|
||||
description = _('Kindle books from Amazon.de.')
|
||||
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']
|
||||
|
||||
class StoreAmazonUKKindleStore(StoreBase):
|
||||
name = 'Amazon UK Kindle'
|
||||
description = _('Kindle books from Amazon.uk.')
|
||||
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']
|
||||
|
||||
class StoreArchiveOrgStore(StoreBase):
|
||||
name = 'Archive.org'
|
||||
description = _('Free Books : Download & Streaming : Ebook and Texts Archive : Internet Archive.')
|
||||
description = u'An Internet library offering permanent access for researchers, historians, scholars, people with disabilities, and the general public to historical collections that exist in digital format.'
|
||||
actual_plugin = 'calibre.gui2.store.archive_org_plugin:ArchiveOrgStore'
|
||||
|
||||
drm_free_only = True
|
||||
headquarters = 'US'
|
||||
formats = ['DAISY', 'DJVU', 'EPUB', 'MOBI', 'PDF', 'TXT']
|
||||
|
||||
class StoreBaenWebScriptionStore(StoreBase):
|
||||
name = 'Baen WebScription'
|
||||
description = _('Ebooks for readers.')
|
||||
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']
|
||||
|
||||
class StoreBNStore(StoreBase):
|
||||
name = 'Barnes and Noble'
|
||||
description = _('Books, Textbooks, eBooks, Toys, Games and More.')
|
||||
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']
|
||||
|
||||
class StoreBeamEBooksDEStore(StoreBase):
|
||||
name = 'Beam EBooks DE'
|
||||
description = _('Der eBook Shop.')
|
||||
author = 'Charles Haley'
|
||||
description = u'Der eBook Shop.'
|
||||
actual_plugin = 'calibre.gui2.store.beam_ebooks_de_plugin:BeamEBooksDEStore'
|
||||
|
||||
drm_free_only = True
|
||||
headquarters = 'DE'
|
||||
formats = ['EPUB', 'MOBI', 'PDF']
|
||||
|
||||
class StoreBeWriteStore(StoreBase):
|
||||
name = 'BeWrite Books'
|
||||
description = _('Publishers of fine 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']
|
||||
|
||||
class StoreDieselEbooksStore(StoreBase):
|
||||
name = 'Diesel eBooks'
|
||||
description = _('World Famous eBook Store.')
|
||||
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']
|
||||
|
||||
class StoreEbookscomStore(StoreBase):
|
||||
name = 'eBooks.com'
|
||||
description = _('The digital bookstore.')
|
||||
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']
|
||||
|
||||
class StoreEPubBuyDEStore(StoreBase):
|
||||
name = 'EPUBBuy DE'
|
||||
description = _('EPUBReaders eBook Shop.')
|
||||
author = 'Charles Haley'
|
||||
description = u'Deutsch epub-Spezialisten.'
|
||||
actual_plugin = 'calibre.gui2.store.epubbuy_de_plugin:EPubBuyDEStore'
|
||||
|
||||
drm_free_only = True
|
||||
headquarters = 'DE'
|
||||
formats = ['EPUB']
|
||||
|
||||
class StoreEHarlequinStore(StoreBase):
|
||||
name = 'eHarlequin'
|
||||
description = _('Entertain, enrich, inspire.')
|
||||
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']
|
||||
|
||||
class StoreFeedbooksStore(StoreBase):
|
||||
name = 'Feedbooks'
|
||||
description = _('Read anywhere.')
|
||||
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']
|
||||
|
||||
class StoreFoylesUKStore(StoreBase):
|
||||
name = 'Foyles UK'
|
||||
description = _('Foyles of London, online.')
|
||||
author = 'Charles Haley'
|
||||
description = u'Foyles of London\'s ebook store. Provides extensive range covering all subjects.'
|
||||
actual_plugin = 'calibre.gui2.store.foyles_uk_plugin:FoylesUKStore'
|
||||
|
||||
drm_free_only = False
|
||||
headquarters = 'UK'
|
||||
formats = ['EPUB', 'PDF']
|
||||
|
||||
class StoreGandalfStore(StoreBase):
|
||||
name = 'Gandalf'
|
||||
author = 'Tomasz Długosz'
|
||||
description = _('Zaczarowany świat książek')
|
||||
author = u'Tomasz Długosz'
|
||||
description = u'Księgarnia internetowa Gandalf.'
|
||||
actual_plugin = 'calibre.gui2.store.gandalf_plugin:GandalfStore'
|
||||
|
||||
drm_free_only = False
|
||||
headquarters = 'PL'
|
||||
formats = ['EPUB', 'PDF']
|
||||
|
||||
class StoreGoogleBooksStore(StoreBase):
|
||||
name = 'Google Books'
|
||||
description = _('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']
|
||||
|
||||
class StoreGutenbergStore(StoreBase):
|
||||
name = 'Project Gutenberg'
|
||||
description = _('The first producer of free ebooks.')
|
||||
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']
|
||||
|
||||
class StoreKoboStore(StoreBase):
|
||||
name = 'Kobo'
|
||||
description = _('eReading: anytime. anyplace.')
|
||||
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 StoreManyBooksStore(StoreBase):
|
||||
name = 'ManyBooks'
|
||||
description = _('The best ebooks at the best price: free!')
|
||||
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']
|
||||
|
||||
class StoreMobileReadStore(StoreBase):
|
||||
name = 'MobileRead'
|
||||
description = _('Ebooks handcrafted with the utmost care.')
|
||||
description = u'Ebooks handcrafted with the utmost care.'
|
||||
actual_plugin = 'calibre.gui2.store.mobileread.mobileread_plugin:MobileReadStore'
|
||||
|
||||
drm_free_only = True
|
||||
headquarters = 'CH'
|
||||
formats = ['EPUB', 'IMP', 'LRF', 'LIT', 'MOBI', 'PDF']
|
||||
|
||||
class StoreNextoStore(StoreBase):
|
||||
name = 'Nexto'
|
||||
author = 'Tomasz Długosz'
|
||||
description = _('Audiobooki mp3, ebooki, prasa - księgarnia internetowa.')
|
||||
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']
|
||||
|
||||
class StoreOpenLibraryStore(StoreBase):
|
||||
name = 'Open Library'
|
||||
description = _('One web page for every book.')
|
||||
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']
|
||||
|
||||
class StoreOReillyStore(StoreBase):
|
||||
name = 'OReilly'
|
||||
description = _('DRM-Free tech ebooks.')
|
||||
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']
|
||||
|
||||
class StorePragmaticBookshelfStore(StoreBase):
|
||||
name = 'Pragmatic Bookshelf'
|
||||
description = _('The Pragmatic Bookshelf')
|
||||
description = u'The Pragmatic Bookshelf\'s collection of programming and tech books avaliable as ebooks.'
|
||||
actual_plugin = 'calibre.gui2.store.pragmatic_bookshelf_plugin:PragmaticBookshelfStore'
|
||||
|
||||
drm_free_only = True
|
||||
headquarters = 'US'
|
||||
formats = ['EPUB', 'MOBI', 'PDF']
|
||||
|
||||
class StoreSmashwordsStore(StoreBase):
|
||||
name = 'Smashwords'
|
||||
description = _('Your ebook. Your way.')
|
||||
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']
|
||||
|
||||
class StoreWaterstonesUKStore(StoreBase):
|
||||
name = 'Waterstones UK'
|
||||
description = _('Feel every word.')
|
||||
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']
|
||||
|
||||
class StoreWeightlessBooksStore(StoreBase):
|
||||
name = 'Weightless Books'
|
||||
description = '(e)Books That Don\'t Weigh You Down.'
|
||||
description = u'An independent DRM-free ebooksite devoted to ebooks of all sorts.'
|
||||
actual_plugin = 'calibre.gui2.store.weightless_books_plugin:WeightlessBooksStore'
|
||||
|
||||
drm_free_only = True
|
||||
headquarters = 'US'
|
||||
formats = ['EPUB', 'HTML', 'LIT', 'MOBI', 'PDF']
|
||||
|
||||
class StoreWizardsTowerBooksStore(StoreBase):
|
||||
name = 'Wizards Tower Books'
|
||||
description = 'Wizard\'s Tower Press.'
|
||||
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']
|
||||
|
||||
class StoreWoblinkStore(StoreBase):
|
||||
name = 'Woblink'
|
||||
author = 'Tomasz Długosz'
|
||||
description = u'Czytanie zdarza się wszędzie!'
|
||||
actual_plugin = 'calibre.gui2.store.woblink_plugin:WoblinkStore'
|
||||
|
||||
drm_free_only = False
|
||||
location = 'PL'
|
||||
formats = ['EPUB']
|
||||
|
||||
plugins += [
|
||||
StoreArchiveOrgStore,
|
||||
@ -1264,7 +1402,8 @@ plugins += [
|
||||
StoreSmashwordsStore,
|
||||
StoreWaterstonesUKStore,
|
||||
StoreWeightlessBooksStore,
|
||||
StoreWizardsTowerBooksStore
|
||||
StoreWizardsTowerBooksStore,
|
||||
StoreWoblinkStore
|
||||
]
|
||||
|
||||
# }}}
|
||||
|
@ -216,9 +216,26 @@ def store_plugins():
|
||||
customization = config['plugin_customization']
|
||||
for plugin in _initialized_plugins:
|
||||
if isinstance(plugin, Store):
|
||||
if not is_disabled(plugin):
|
||||
plugin.site_customization = customization.get(plugin.name, '')
|
||||
yield plugin
|
||||
plugin.site_customization = customization.get(plugin.name, '')
|
||||
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 {{{
|
||||
|
@ -16,7 +16,7 @@ class AmazonDEKindleStore(AmazonKindleStore):
|
||||
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/'
|
||||
drm_search_text = u'Gleichzeitige Verwendung von Geräten'
|
||||
drm_free_text = u'Keine Einschränkung'
|
||||
|
@ -17,7 +17,7 @@ class AmazonUKKindleStore(AmazonKindleStore):
|
||||
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/'
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
|
0
src/calibre/gui2/store/config/__init__.py
Normal file
0
src/calibre/gui2/store/config/__init__.py
Normal file
45
src/calibre/gui2/store/config/search_widget.py
Normal file
45
src/calibre/gui2/store/config/search_widget.py
Normal 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()
|
162
src/calibre/gui2/store/config/search_widget.ui
Normal file
162
src/calibre/gui2/store/config/search_widget.ui
Normal 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>Performance</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 simultaneous searches</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 simultaneous cache updates</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 simultaneous cover downloads</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 simultaneous details downloads</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>
|
18
src/calibre/gui2/store/config/store.py
Normal file
18
src/calibre/gui2/store/config/store.py
Normal 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()
|
@ -51,7 +51,7 @@ class GoogleBooksStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
title = ''.join(data.xpath('.//h3/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]
|
||||
else:
|
||||
continue
|
||||
|
@ -22,7 +22,7 @@ class GenericDownloadThreadPool(object):
|
||||
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_count = thread_count
|
||||
|
||||
@ -30,6 +30,9 @@ class GenericDownloadThreadPool(object):
|
||||
self.results = Queue()
|
||||
self.threads = []
|
||||
|
||||
def set_thread_count(self, thread_count):
|
||||
self.thread_count = thread_count
|
||||
|
||||
def add_task(self):
|
||||
'''
|
||||
This must be implemented in a sub class and this function
|
||||
@ -92,8 +95,8 @@ class SearchThreadPool(GenericDownloadThreadPool):
|
||||
def __init__(self, thread_count):
|
||||
GenericDownloadThreadPool.__init__(self, SearchThread, thread_count)
|
||||
|
||||
def add_task(self, query, store_name, store_plugin, timeout):
|
||||
self.tasks.put((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, max_results, timeout))
|
||||
GenericDownloadThreadPool.add_task(self)
|
||||
|
||||
|
||||
@ -112,8 +115,8 @@ class SearchThread(Thread):
|
||||
def run(self):
|
||||
while self._run and not self.tasks.empty():
|
||||
try:
|
||||
query, store_name, store_plugin, timeout = self.tasks.get()
|
||||
for res in store_plugin.search(query, timeout=timeout):
|
||||
query, store_name, store_plugin, max_results, timeout = self.tasks.get()
|
||||
for res in store_plugin.search(query, max_results=max_results, timeout=timeout):
|
||||
if not self._run:
|
||||
return
|
||||
res.store_name = store_name
|
||||
|
@ -9,7 +9,8 @@ __docformat__ = 'restructuredtext en'
|
||||
import re
|
||||
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.store.search_result import SearchResult
|
||||
@ -30,10 +31,12 @@ def comparable_price(text):
|
||||
|
||||
class Matches(QAbstractItemModel):
|
||||
|
||||
total_changed = pyqtSignal(int)
|
||||
|
||||
HEADERS = [_('Cover'), _('Title'), _('Price'), _('DRM'), _('Store')]
|
||||
HTML_COLS = (1, 4)
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, cover_thread_count=2, detail_thread_count=4):
|
||||
QAbstractItemModel.__init__(self)
|
||||
|
||||
self.DRM_LOCKED_ICON = QPixmap(I('drm-locked.png')).scaledToHeight(64,
|
||||
@ -51,8 +54,8 @@ class Matches(QAbstractItemModel):
|
||||
self.matches = []
|
||||
self.query = ''
|
||||
self.search_filter = SearchFilter()
|
||||
self.cover_pool = CoverThreadPool(2)
|
||||
self.details_pool = DetailsThreadPool(4)
|
||||
self.cover_pool = CoverThreadPool(cover_thread_count)
|
||||
self.details_pool = DetailsThreadPool(detail_thread_count)
|
||||
|
||||
self.sort_col = 2
|
||||
self.sort_order = Qt.AscendingOrder
|
||||
@ -69,6 +72,7 @@ class Matches(QAbstractItemModel):
|
||||
self.query = ''
|
||||
self.cover_pool.abort()
|
||||
self.details_pool.abort()
|
||||
self.total_changed.emit(self.rowCount())
|
||||
self.reset()
|
||||
|
||||
def add_result(self, result, store_plugin):
|
||||
@ -101,6 +105,7 @@ class Matches(QAbstractItemModel):
|
||||
self.matches = list(self.search_filter.parse(self.query))
|
||||
else:
|
||||
self.matches = list(self.search_filter.universal_set())
|
||||
self.total_changed.emit(self.rowCount())
|
||||
self.sort(self.sort_col, self.sort_order, False)
|
||||
self.layoutChanged.emit()
|
||||
|
||||
|
@ -9,18 +9,17 @@ __docformat__ = 'restructuredtext en'
|
||||
import re
|
||||
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.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.download_thread import SearchThreadPool, \
|
||||
CacheUpdateThreadPool
|
||||
from calibre.gui2.store.search.search_ui import Ui_Dialog
|
||||
|
||||
HANG_TIME = 75000 # milliseconds seconds
|
||||
TIMEOUT = 75 # seconds
|
||||
|
||||
class SearchDialog(QDialog, Ui_Dialog):
|
||||
|
||||
def __init__(self, istores, parent=None, query=''):
|
||||
@ -28,35 +27,44 @@ class SearchDialog(QDialog, Ui_Dialog):
|
||||
self.setupUi(self)
|
||||
|
||||
self.config = JSONConfig('store/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.
|
||||
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.
|
||||
self.checker = QTimer()
|
||||
self.progress_checker = QTimer()
|
||||
self.hang_check = 0
|
||||
|
||||
|
||||
# Update store caches silently.
|
||||
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
|
||||
# can disable searching specific stores on a
|
||||
# per search basis.
|
||||
stores_check_widget = QWidget()
|
||||
stores_group_layout = QVBoxLayout()
|
||||
stores_check_widget.setLayout(stores_group_layout)
|
||||
store_list_layout = QVBoxLayout()
|
||||
stores_check_widget.setLayout(store_list_layout)
|
||||
for x in sorted(self.store_plugins.keys(), key=lambda x: x.lower()):
|
||||
cbox = QCheckBox(x)
|
||||
cbox.setChecked(False)
|
||||
stores_group_layout.addWidget(cbox)
|
||||
store_list_layout.addWidget(cbox)
|
||||
setattr(self, 'store_check_' + x, cbox)
|
||||
stores_group_layout.addStretch()
|
||||
self.stores_group.setWidget(stores_check_widget)
|
||||
store_list_layout.addStretch()
|
||||
self.store_list.setWidget(stores_check_widget)
|
||||
|
||||
# Set the search query
|
||||
self.search_edit.setText(query)
|
||||
@ -64,17 +72,20 @@ class SearchDialog(QDialog, Ui_Dialog):
|
||||
# Create and add the progress indicator
|
||||
self.pi = ProgressIndicator(self, 24)
|
||||
self.top_layout.addWidget(self.pi)
|
||||
|
||||
|
||||
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.search.clicked.connect(self.do_search)
|
||||
self.checker.timeout.connect(self.get_results)
|
||||
self.progress_checker.timeout.connect(self.check_progress)
|
||||
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_invert_stores.clicked.connect(self.stores_select_invert)
|
||||
self.select_none_stores.clicked.connect(self.stores_select_none)
|
||||
self.configure.clicked.connect(self.do_config)
|
||||
self.finished.connect(self.dialog_closed)
|
||||
|
||||
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.
|
||||
for n in store_names:
|
||||
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.checker.start(100)
|
||||
self.pi.startAnimation()
|
||||
@ -190,23 +201,68 @@ class SearchDialog(QDialog, Ui_Dialog):
|
||||
else:
|
||||
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)
|
||||
if store_check:
|
||||
for n in store_check:
|
||||
if hasattr(self, 'store_check_' + n):
|
||||
getattr(self, 'store_check_' + n).setChecked(store_check[n])
|
||||
|
||||
|
||||
self.results_view.model().sort_col = self.config.get('sort_col', 2)
|
||||
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'))
|
||||
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):
|
||||
# We only want the search plugins to run
|
||||
# a maximum set amount of time before giving up.
|
||||
self.hang_check += 1
|
||||
if self.hang_check >= HANG_TIME:
|
||||
if self.hang_check >= self.hang_time:
|
||||
self.search_pool.abort()
|
||||
self.checker.stop()
|
||||
else:
|
||||
@ -222,13 +278,15 @@ class SearchDialog(QDialog, Ui_Dialog):
|
||||
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)
|
||||
|
||||
def update_book_total(self, total):
|
||||
self.total.setText('%s' % total)
|
||||
|
||||
def open_store(self, index):
|
||||
result = self.results_view.model().get_result(index)
|
||||
self.store_plugins[result.store_name].open(self, result.detail_item, self.open_external.isChecked())
|
||||
|
||||
def check_progress(self):
|
||||
if not self.search_pool.threads_running() and not self.results_view.model().cover_pool.threads_running() and not self.results_view.model().details_pool.threads_running():
|
||||
if not self.search_pool.threads_running() and not self.results_view.model().cover_pool.threads_running() and not self.results_view.model().details_pool.threads_running():
|
||||
self.pi.stopAnimation()
|
||||
else:
|
||||
if not self.pi.isAnimated():
|
||||
@ -262,7 +320,7 @@ class SearchDialog(QDialog, Ui_Dialog):
|
||||
self.search_pool.abort()
|
||||
self.cache_pool.abort()
|
||||
self.save_state()
|
||||
|
||||
|
||||
def exec_(self):
|
||||
if unicode(self.search_edit.text()).strip():
|
||||
self.do_search()
|
||||
|
@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>937</width>
|
||||
<height>669</height>
|
||||
<width>584</width>
|
||||
<height>533</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@ -20,7 +20,7 @@
|
||||
<property name="sizeGripEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="top_layout">
|
||||
<item>
|
||||
@ -66,8 +66,14 @@
|
||||
<string>Stores</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QScrollArea" name="stores_group">
|
||||
<widget class="QScrollArea" name="store_list">
|
||||
<property name="widgetResizable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
@ -76,15 +82,18 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>215</width>
|
||||
<height>93</height>
|
||||
<width>102</width>
|
||||
<height>129</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="select_all_stores">
|
||||
<property name="text">
|
||||
@ -108,76 +117,104 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="verticalLayoutWidget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="open_external">
|
||||
<property name="toolTip">
|
||||
<string>Open a selected book in the system's web browser</string>
|
||||
<widget class="ResultsView" name="results_view">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>1</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Open in &external browser</string>
|
||||
<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>
|
||||
</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 &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>
|
||||
</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>
|
||||
</item>
|
||||
<item>
|
||||
<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>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
|
76
src/calibre/gui2/store/woblink_plugin.py
Normal file
76
src/calibre/gui2/store/woblink_plugin.py
Normal file
@ -0,0 +1,76 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import (unicode_literals, division, absolute_import, print_function)
|
||||
|
||||
__license__ = 'GPL 3'
|
||||
__copyright__ = '2011, Tomasz Długosz <tomek3d@gmail.com>'
|
||||
__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 WoblinkStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
|
||||
url = 'http://woblink.com/publication'
|
||||
detail_url = None
|
||||
|
||||
if detail_item:
|
||||
detail_url = 'http://woblink.com' + 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://woblink.com/publication?query=' + urllib.quote_plus(query.encode('utf-8'))
|
||||
|
||||
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="book-item"]'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
id = ''.join(data.xpath('.//td[@class="w10 va-t"]/a[1]/@href'))
|
||||
if not id:
|
||||
continue
|
||||
|
||||
cover_url = ''.join(data.xpath('.//td[@class="w10 va-t"]/a[1]/img/@src'))
|
||||
title = ''.join(data.xpath('.//h3[@class="title"]/a[1]/text()'))
|
||||
author = ''.join(data.xpath('.//p[@class="author"]/a[1]/text()'))
|
||||
price = ''.join(data.xpath('.//div[@class="prices"]/p[1]/span/text()'))
|
||||
price = re.sub('PLN', ' zł', price)
|
||||
price = re.sub('\.', ',', price)
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = 'http://woblink.com' + cover_url
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = price
|
||||
s.detail_item = id.strip()
|
||||
s.drm = SearchResult.DRM_LOCKED
|
||||
s.formats = 'EPUB'
|
||||
|
||||
yield s
|
@ -23,7 +23,7 @@ from calibre.constants import __appname__, isosx
|
||||
from calibre.utils.config import prefs, dynamic
|
||||
from calibre.utils.ipc.server import Server
|
||||
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, \
|
||||
gprefs, max_available_height, config, info_dialog, Dispatcher, \
|
||||
question_dialog
|
||||
@ -144,7 +144,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||
|
||||
def load_store_plugins(self):
|
||||
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:
|
||||
continue
|
||||
try:
|
||||
|
Loading…
x
Reference in New Issue
Block a user