Move store plugins to use a GUI wrapper plugin to keep the plugin system happy. Fix segfault with cover download thread pool.

This commit is contained in:
John Schember 2011-03-01 07:48:33 -05:00
parent 06e3186391
commit 84d8dc78c1
12 changed files with 219 additions and 203 deletions

View File

@ -581,60 +581,41 @@ class PreferencesPlugin(Plugin): # {{{
# }}}
class StorePlugin(Plugin): # {{{
class StoreBase(Plugin): # {{{
supported_platforms = ['windows', 'osx', 'linux']
author = 'John Schember'
type = _('Store')
# This needs to be changed to (0, 8, 0)
minimum_calibre_version = (0, 4, 118)
def open(self, gui, parent=None, detail_item=None, external=False):
actual_plugin = None
actual_plugin_object = None
def load_actual_plugin(self, gui):
'''
Open the store.
:param gui: The main GUI. This will be used to have the job
system start downloading an item from the store.
:param parent: The parent of the store dialog. This is used
to create modal dialogs.
:param detail_item: A plugin specific reference to an item
in the store that the user should be shown.
:param external: When False open an internal dialog with the
store. When True open the users default browser to the store's
web site. :param:`detail_item` should still be respected when external
is True.
This method must return the actual interface action plugin object.
'''
raise NotImplementedError()
def search(self, query, max_results=10, timeout=60):
'''
Searches the store for items matching query. This should
return items as a generator.
mod, cls = self.actual_plugin.split(':')
self.actual_plugin_object = getattr(__import__(mod, fromlist=['1'], level=0), cls)(gui, self.name)
return self.actual_plugin_object
def customization_help(self, gui=False):
if self.actual_plugin_object:
return self.actual_plugin_object.customization_help(gui)
else:
raise NotImplementedError()
:param query: The string query search with.
:param max_results: The maximum number of results to return.
:param timeout: The maximum amount of time in seconds to spend download the search results.
:return: :class:`calibre.gui2.store.search_result.SearchResult` objects
item_data is plugin specific and is used in :meth:`open` to open to a specifc place in the store.
'''
raise NotImplementedError()
def get_settings(self):
'''
This is only useful for plugins that implement
:attr:`config_widget` that is the only way to save
settings. This is used by plugins to get the saved
settings and apply when necessary.
:return: A dictionary filled with the settings used
by this plugin.
'''
raise NotImplementedError()
def config_widget(self):
if self.actual_plugin_object:
return self.actual_plugin_object.config_widget()
else:
raise NotImplementedError()
def save_settings(self, config_widget):
if self.actual_plugin_object:
return self.actual_plugin_object.save_settings(config_widget)
else:
raise NotImplementedError()
# }}}

View File

@ -5,7 +5,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import textwrap, os, glob, functools, re
from calibre import guess_type
from calibre.customize import FileTypePlugin, MetadataReaderPlugin, \
MetadataWriterPlugin, PreferencesPlugin, InterfaceActionBase
MetadataWriterPlugin, PreferencesPlugin, InterfaceActionBase, StoreBase
from calibre.constants import numeric_version
from calibre.ebooks.metadata.archive import ArchiveExtract, get_cbz_metadata
from calibre.ebooks.metadata.opf2 import metadata_to_opf
@ -1042,12 +1042,32 @@ plugins += [GoogleBooks]
# }}}
# Store plugins {{{
from calibre.gui2.store.amazon_plugin import AmazonKindleStore
from calibre.gui2.store.gutenberg_plugin import GutenbergStore
from calibre.gui2.store.feedbooks_plugin import FeedbooksStore
from calibre.gui2.store.manybooks_plugin import ManyBooksStore
from calibre.gui2.store.smashwords_plugin import SmashwordsStore
class StoreAmazonKindleStore(StoreBase):
name = 'Amazon Kindle'
description = _('Kindle books from Amazon')
actual_plugin = 'calibre.gui2.store.amazon_plugin:AmazonKindleStore'
plugins += [AmazonKindleStore, GutenbergStore, FeedbooksStore, ManyBooksStore, SmashwordsStore]
class StoreGutenbergStore(StoreBase):
name = 'Project Gutenberg'
description = _('The first producer of free ebooks.')
actual_plugin = 'calibre.gui2.store.gutenberg_plugin:GutenbergStore'
class StoreFeedbooksStore(StoreBase):
name = 'Feedbooks'
description = _('Read anywhere.')
actual_plugin = 'calibre.gui2.store.feedbooks_plugin:FeedbooksStore'
class StoreManyBooksStore(StoreBase):
name = 'ManyBooks'
description = _('The best ebooks at the best price: free!')
actual_plugin = 'calibre.gui2.store.manybooks_plugin:ManyBooksStore'
class StoreSmashwordsStore(StoreBase):
name = 'Smashwords'
description = _('Your ebook. Your way.')
actual_plugin = 'calibre.gui2.store.smashwords_plugin:SmashwordsStore'
plugins += [StoreAmazonKindleStore, StoreGutenbergStore, StoreFeedbooksStore, StoreManyBooksStore, StoreSmashwordsStore]
# }}}

View File

@ -8,7 +8,7 @@ from contextlib import closing
from calibre.customize import Plugin, CatalogPlugin, FileTypePlugin, \
MetadataReaderPlugin, MetadataWriterPlugin, \
InterfaceActionBase as InterfaceAction, \
PreferencesPlugin, StorePlugin
PreferencesPlugin, StoreBase
from calibre.customize.conversion import InputFormatPlugin, OutputFormatPlugin
from calibre.customize.profiles import InputProfile, OutputProfile
from calibre.customize.builtins import plugins as builtin_plugins
@ -283,7 +283,7 @@ def preferences_plugins():
def store_plugins():
customization = config['plugin_customization']
for plugin in _initialized_plugins:
if isinstance(plugin, StorePlugin):
if isinstance(plugin, StoreBase):
if not is_disabled(plugin):
plugin.site_customization = customization.get(plugin.name, '')
yield plugin

View File

@ -8,7 +8,6 @@ from functools import partial
from PyQt4.Qt import Qt, QMenu, QToolButton, QDialog, QVBoxLayout
from calibre.customize.ui import store_plugins
from calibre.gui2.actions import InterfaceAction
class StoreAction(InterfaceAction):
@ -21,14 +20,14 @@ class StoreAction(InterfaceAction):
self.store_menu = QMenu()
self.store_menu.addAction(_('Search'), self.search)
self.store_menu.addSeparator()
for x in store_plugins():
self.store_menu.addAction(x.name, partial(self.open_store, x))
for n, p in self.gui.istores.items():
self.store_menu.addAction(n, partial(self.open_store, p))
self.qaction.setMenu(self.store_menu)
def search(self):
from calibre.gui2.store.search import SearchDialog
sd = SearchDialog(self.gui, self.gui)
sd = SearchDialog(self.gui.istores, self.gui)
sd.exec_()
def open_store(self, store_plugin):
store_plugin.open(self.gui, self.gui)
store_plugin.open(self.gui)

View File

@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
__license__ = 'GPL 3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
class StorePlugin(object): # {{{
def __init__(self, gui, name):
self.gui = gui
self.name = name
self.base_plugin = None
def open(self, gui, parent=None, detail_item=None, external=False):
'''
Open the store.
:param gui: The main GUI. This will be used to have the job
system start downloading an item from the store.
:param parent: The parent of the store dialog. This is used
to create modal dialogs.
:param detail_item: A plugin specific reference to an item
in the store that the user should be shown.
:param external: When False open an internal dialog with the
store. When True open the users default browser to the store's
web site. :param:`detail_item` should still be respected when external
is True.
'''
raise NotImplementedError()
def search(self, query, max_results=10, timeout=60):
'''
Searches the store for items matching query. This should
return items as a generator.
:param query: The string query search with.
:param max_results: The maximum number of results to return.
:param timeout: The maximum amount of time in seconds to spend download the search results.
:return: :class:`calibre.gui2.store.search_result.SearchResult` objects
item_data is plugin specific and is used in :meth:`open` to open to a specifc place in the store.
'''
raise NotImplementedError()
def get_settings(self):
'''
This is only useful for plugins that implement
:attr:`config_widget` that is the only way to save
settings. This is used by plugins to get the saved
settings and apply when necessary.
:return: A dictionary filled with the settings used
by this plugin.
'''
raise NotImplementedError()
def do_genesis(self):
self.genesis()
def genesis(self):
pass
def config_widget(self):
raise NotImplementedError()
def save_settings(self, config_widget):
raise NotImplementedError()
def customization_help(self, gui=False):
raise NotImplementedError()
# }}}

View File

@ -14,15 +14,13 @@ from lxml import html
from PyQt4.Qt import QUrl
from calibre import browser
from calibre.customize import StorePlugin
from calibre.gui2 import open_url
from calibre.gui2.store import StorePlugin
from calibre.gui2.store.search_result import SearchResult
class AmazonKindleStore(StorePlugin):
name = 'Amazon Kindle'
description = _('Kindle books from Amazon')
def open(self, gui, parent=None, detail_item=None):
def open(self, parent=None, detail_item=None, external=False):
'''
Amazon comes with a number of difficulties.
@ -106,7 +104,6 @@ class AmazonKindleStore(StorePlugin):
The best (I use the term lightly here) solution is to open Amazon.com
in the users default browser and set the affiliate id as part of the url.
'''
from calibre.gui2 import open_url
aff_id = {'tag': 'josbl0e-cpb-20'}
# Use Kovid's affiliate id 30% of the time.
if random.randint(1, 10) in (1, 2, 3):

View File

@ -10,18 +10,14 @@ from contextlib import closing
from lxml import html
from calibre import browser
from calibre.customize import StorePlugin
from calibre.gui2.store.search_result import SearchResult
from calibre.gui2.store import StorePlugin
from calibre.gui2.store.basic_config import BasicStoreConfig
from calibre.gui2.store.web_store_dialog import WebStoreDialog
class FeedbooksStore(StorePlugin):
class FeedbooksStore(BasicStoreConfig, StorePlugin):
name = 'Feedbooks'
description = _('Read anywhere.')
def open(self, gui, parent=None, detail_item=None):
from calibre.gui2.store.web_store_dialog import WebStoreDialog
d = WebStoreDialog(gui, 'http://m.feedbooks.com/', parent, detail_item)
def open(self, parent=None, detail_item=None, external=False):
d = WebStoreDialog(self.gui, 'http://m.feedbooks.com/', parent, detail_item)
d.setWindowTitle(self.name)
d.set_tags(self.name + ',' + _('store'))
d = d.exec_()
@ -76,22 +72,3 @@ class FeedbooksStore(StorePlugin):
s.detail_item = id.strip()
yield s
def customization_help(self, gui=False):
return 'Customize the behavior of this store.'
def config_widget(self):
from calibre.gui2.store.basic_config_widget import BasicStoreConfigWidget
return BasicStoreConfigWidget(self)
def save_settings(self, config_widget):
from calibre.gui2.store.basic_config_widget import save_settings
save_settings(config_widget)
def get_settings(self):
from calibre.gui2 import gprefs
settings = {}
settings[self.name + '_tags'] = gprefs.get(self.name + '_tags', self.name + ', store, download')
return settings

View File

@ -10,18 +10,16 @@ from contextlib import closing
from lxml import html
from calibre import browser
from calibre.customize import StorePlugin
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 GutenbergStore(StorePlugin):
name = 'Project Gutenberg'
description = _('The first producer of free ebooks.')
class GutenbergStore(BasicStoreConfig, StorePlugin):
def open(self, gui, parent=None, detail_item=None):
def open(self, parent=None, detail_item=None, external=False):
settings = self.get_settings()
from calibre.gui2.store.web_store_dialog import WebStoreDialog
d = WebStoreDialog(gui, 'http://m.gutenberg.org/', parent, detail_item)
d = WebStoreDialog(self.gui, 'http://m.gutenberg.org/', parent, detail_item)
d.setWindowTitle(self.name)
d.set_tags(settings.get(self.name + '_tags', ''))
d = d.exec_()
@ -51,8 +49,9 @@ class GutenbergStore(StorePlugin):
continue
id = url.split('/')[-1]
heading = ''.join(url_a.xpath('text()'))
title, _, author = heading.partition('by ')
url_a = html.fromstring(html.tostring(url_a))
heading = ''.join(url_a.xpath('//text()'))
title, _, author = heading.rpartition('by ')
author = author.split('-')[0]
price = '$0.00'
@ -66,22 +65,3 @@ class GutenbergStore(StorePlugin):
s.detail_item = '/ebooks/' + id.strip()
yield s
def customization_help(self, gui=False):
return 'Customize the behavior of this store.'
def config_widget(self):
from calibre.gui2.store.basic_config_widget import BasicStoreConfigWidget
return BasicStoreConfigWidget(self)
def save_settings(self, config_widget):
from calibre.gui2.store.basic_config_widget import save_settings
save_settings(config_widget)
def get_settings(self):
from calibre.gui2 import gprefs
settings = {}
settings[self.name + '_tags'] = gprefs.get(self.name + '_tags', self.name + ', store, download')
return settings

View File

@ -11,18 +11,15 @@ from contextlib import closing
from lxml import html
from calibre import browser
from calibre.customize import StorePlugin
from calibre.gui2.store import StorePlugin
from calibre.gui2.store.basic_config import BasicStoreConfig
from calibre.gui2.store.search_result import SearchResult
class ManyBooksStore(StorePlugin):
name = 'ManyBooks'
description = _('The best ebooks at the best price: free!')
from calibre.gui2.store.web_store_dialog import WebStoreDialog
def open(self, gui, parent=None, detail_item=None):
from calibre.gui2.store.web_store_dialog import WebStoreDialog
d = WebStoreDialog(gui, 'http://manybooks.net/', parent, detail_item)
class ManyBooksStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False):
d = WebStoreDialog(self.gui, 'http://manybooks.net/', parent, detail_item)
d.setWindowTitle(self.name)
d.set_tags(self.name + ',' + _('store'))
d = d.exec_()
@ -55,8 +52,9 @@ class ManyBooksStore(StorePlugin):
id = url.split('/')[-1]
id = id.strip()
heading = ''.join(url_a.xpath('text()'))
title, _, author = heading.partition('by ')
url_a = html.fromstring(html.tostring(url_a))
heading = ''.join(url_a.xpath('//text()'))
title, _, author = heading.rpartition('by ')
author = author.split('-')[0]
price = '$0.00'
@ -78,22 +76,3 @@ class ManyBooksStore(StorePlugin):
s.detail_item = '/titles/' + id
yield s
def customization_help(self, gui=False):
return 'Customize the behavior of this store.'
def config_widget(self):
from calibre.gui2.store.basic_config_widget import BasicStoreConfigWidget
return BasicStoreConfigWidget(self)
def save_settings(self, config_widget):
from calibre.gui2.store.basic_config_widget import save_settings
save_settings(config_widget)
def get_settings(self):
from calibre.gui2 import gprefs
settings = {}
settings[self.name + '_tags'] = gprefs.get(self.name + '_tags', self.name + ', store, download')
return settings

View File

@ -16,7 +16,6 @@ from PyQt4.Qt import Qt, QAbstractItemModel, QDialog, QTimer, QVariant, \
QPushButton
from calibre import browser
from calibre.customize.ui import store_plugins
from calibre.gui2 import NONE
from calibre.gui2.store.search_ui import Ui_Dialog
from calibre.utils.icu import sort_key
@ -29,17 +28,12 @@ COVER_DOWNLOAD_THREAD_TOTAL = 2
class SearchDialog(QDialog, Ui_Dialog):
def __init__(self, gui, *args):
def __init__(self, istores, *args):
QDialog.__init__(self, *args)
self.setupUi(self)
# We pass this on to the store plugins so they can
# tell the gui's job system to start downloading an
# item.
self.gui = gui
# We keep a cache of store plugins and reference them by name.
self.store_plugins = {}
self.store_plugins = istores
self.search_pool = SearchThreadPool(SearchThread, SEARCH_THREAD_TOTAL)
# Check for results and hung threads.
self.checker = QTimer()
@ -53,12 +47,11 @@ class SearchDialog(QDialog, Ui_Dialog):
# per search basis.
stores_group_layout = QVBoxLayout()
self.stores_group.setLayout(stores_group_layout)
for x in store_plugins():
self.store_plugins[x.name] = x
cbox = QCheckBox(x.name)
for x in self.store_plugins:
cbox = QCheckBox(x)
cbox.setChecked(True)
stores_group_layout.addWidget(cbox)
setattr(self, 'store_check_' + x.name, cbox)
setattr(self, 'store_check_' + x, cbox)
stores_group_layout.addStretch()
self.search.clicked.connect(self.do_search)
@ -122,7 +115,7 @@ class SearchDialog(QDialog, Ui_Dialog):
self.checker.stop()
else:
# Stop the checker if not threads are running.
if not self.search_pool.threads_running():
if not self.search_pool.threads_running() and not self.search_pool.has_tasks():
self.checker.stop()
while self.search_pool.has_results():
@ -132,7 +125,7 @@ class SearchDialog(QDialog, Ui_Dialog):
def open_store(self, index):
result = self.results_view.model().get_result(index)
self.store_plugins[result.store_name].open(self.gui, self, result.detail_item)
self.store_plugins[result.store_name].open(self, result.detail_item)
def get_store_checks(self):
'''
@ -156,6 +149,10 @@ class SearchDialog(QDialog, Ui_Dialog):
def stores_select_none(self):
for check in self.get_store_checks():
check.setChecked(False)
def closeEvent(self, e):
self.model.closing()
QDialog.closeEvent(self, e)
class GenericDownloadThreadPool(object):
@ -305,6 +302,9 @@ class Matches(QAbstractItemModel):
self.matches = []
self.cover_pool = CoverThreadPool(CoverThread, 2)
self.cover_pool.start_threads()
def closing(self):
self.cover_pool.abort()
def clear_results(self):
self.matches = []
@ -315,7 +315,6 @@ class Matches(QAbstractItemModel):
def add_result(self, result):
self.layoutAboutToBeChanged.emit()
self.matches.append(result)
self.cover_pool.add_task(result, self.update_result)
self.layoutChanged.emit()

View File

@ -12,22 +12,19 @@ from contextlib import closing
from lxml import html
from calibre import browser
from calibre.customize import StorePlugin
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 SmashwordsStore(StorePlugin):
name = 'Smashwords'
description = _('Your ebook. Your way.')
def open(self, gui, parent=None, detail_item=None):
from calibre.gui2.store.web_store_dialog import WebStoreDialog
class SmashwordsStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False):
aff_id = 'usernone'
# Use Kovid's affiliate id 30% of the time.
if random.randint(1, 10) in (1, 2, 3):
aff_id = 'kovidgoyal'
d = WebStoreDialog(gui, 'http://www.smashwords.com/?ref=%s' % aff_id, parent, detail_item)
d = WebStoreDialog(self.gui, 'http://www.smashwords.com/?ref=%s' % aff_id, parent, detail_item)
d.setWindowTitle(self.name)
d.set_tags(self.name + ',' + _('store'))
d = d.exec_()
@ -78,22 +75,3 @@ class SmashwordsStore(StorePlugin):
s.detail_item = '/books/view/' + id.strip()
yield s
def customization_help(self, gui=False):
return 'Customize the behavior of this store.'
def config_widget(self):
from calibre.gui2.store.basic_config_widget import BasicStoreConfigWidget
return BasicStoreConfigWidget(self)
def save_settings(self, config_widget):
from calibre.gui2.store.basic_config_widget import save_settings
save_settings(config_widget)
def get_settings(self):
from calibre.gui2 import gprefs
settings = {}
settings[self.name + '_tags'] = gprefs.get(self.name + '_tags', self.name + ', store, download')
return settings

View File

@ -22,7 +22,7 @@ from calibre.ptempfile import PersistentTemporaryFile
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
from calibre.customize.ui import interface_actions, store_plugins
from calibre.gui2 import error_dialog, GetMetadata, open_local_file, \
gprefs, max_available_height, config, info_dialog, Dispatcher, \
question_dialog
@ -102,6 +102,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
self.device_connected = None
self.gui_debug = gui_debug
self.iactions = OrderedDict()
# Actions
for action in interface_actions():
if opts.ignore_plugins and action.plugin_path is not None:
continue
@ -114,11 +115,24 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
if action.plugin_path is None:
raise
continue
ac.plugin_path = action.plugin_path
ac.interface_action_base_plugin = action
self.add_iaction(ac)
# Stores
self.istores = OrderedDict()
for store in store_plugins():
if opts.ignore_plugins and store.plugin_path is not None:
continue
try:
st = self.init_istore(store)
self.add_istore(st)
except:
# Ignore errors in loading user supplied plugins
import traceback
traceback.print_exc()
if store.plugin_path is None:
raise
continue
def init_iaction(self, action):
ac = action.load_actual_plugin(self)
@ -126,6 +140,13 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
ac.interface_action_base_plugin = action
action.actual_iaction_plugin_loaded = True
return ac
def init_istore(self, store):
st = store.load_actual_plugin(self)
st.plugin_path = store.plugin_path
st.base_plugin = store
store.actual_istore_plugin_loaded = True
return st
def add_iaction(self, ac):
acmap = self.iactions
@ -134,6 +155,14 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
acmap[ac.name] = ac
else:
acmap[ac.name] = ac
def add_istore(self, st):
stmap = self.istores
if st.name in stmap:
if st.priority >= stmap[st.name].priority:
stmap[st.name] = st
else:
stmap[st.name] = st
def initialize(self, library_path, db, listener, actions, show_gui=True):
@ -155,6 +184,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
for ac in self.iactions.values():
ac.do_genesis()
for st in self.istores.values():
st.do_genesis()
MainWindowMixin.__init__(self, db)
# Jobs Button {{{