diff --git a/src/calibre/customize/__init__.py b/src/calibre/customize/__init__.py index 1f44eb4ae2..598a5af8f9 100644 --- a/src/calibre/customize/__init__.py +++ b/src/calibre/customize/__init__.py @@ -581,3 +581,23 @@ class PreferencesPlugin(Plugin): # {{{ # }}} + +class StorePlugin(Plugin): # {{{ + + supported_platforms = ['windows', 'osx', 'linux'] + author = 'John Schember' + type = _('Stores') + + def open(self, parent=None, start_item=None): + ''' + Open a dialog for displaying the store. + start_item is a refernce unique to the store + plugin and opens to the item when specified. + ''' + raise NotImplementedError() + + def search(self, query, max_results=10): + raise NotImplementedError() + +# }}} + diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 6cfe915036..54dfc73242 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -805,6 +805,10 @@ class ActionTweakEpub(InterfaceActionBase): class ActionNextMatch(InterfaceActionBase): name = 'Next Match' actual_plugin = 'calibre.gui2.actions.next_match:NextMatchAction' + +class ActionStore(InterfaceActionBase): + name = 'Store' + actual_plugin = 'calibre.gui2.actions.store:StoreAction' plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog, ActionConvert, ActionDelete, ActionEditMetadata, ActionView, @@ -812,7 +816,7 @@ plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog, ActionRestart, ActionOpenFolder, ActionConnectShare, ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks, ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary, - ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch] + ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch, ActionStore] # }}} @@ -1037,3 +1041,10 @@ from calibre.ebooks.metadata.sources.google import GoogleBooks plugins += [GoogleBooks] # }}} + +# Store plugins {{{ +from calibre.gui2.store.amazon.amazon_plugin import AmazonKindleStore + +plugins += [AmazonKindleStore] + +# }}} diff --git a/src/calibre/customize/ui.py b/src/calibre/customize/ui.py index 5f67e23d92..ce20a96068 100644 --- a/src/calibre/customize/ui.py +++ b/src/calibre/customize/ui.py @@ -8,7 +8,7 @@ from contextlib import closing from calibre.customize import Plugin, CatalogPlugin, FileTypePlugin, \ MetadataReaderPlugin, MetadataWriterPlugin, \ InterfaceActionBase as InterfaceAction, \ - PreferencesPlugin + PreferencesPlugin, StorePlugin from calibre.customize.conversion import InputFormatPlugin, OutputFormatPlugin from calibre.customize.profiles import InputProfile, OutputProfile from calibre.customize.builtins import plugins as builtin_plugins @@ -277,6 +277,17 @@ def preferences_plugins(): yield plugin # }}} +# Store Plugins # {{{ + +def store_plugins(): + customization = config['plugin_customization'] + for plugin in _initialized_plugins: + if isinstance(plugin, StorePlugin): + if not is_disabled(plugin): + plugin.site_customization = customization.get(plugin.name, '') + yield plugin +# }}} + # Metadata read/write {{{ _metadata_readers = {} _metadata_writers = {} diff --git a/src/calibre/gui2/actions/store.py b/src/calibre/gui2/actions/store.py new file mode 100644 index 0000000000..97094ad86e --- /dev/null +++ b/src/calibre/gui2/actions/store.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +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): + + name = 'Store' + action_spec = (_('Store'), None, None, None) + + def genesis(self): + self.qaction.triggered.connect(self.search) + 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)) + self.qaction.setMenu(self.store_menu) + + def search(self): + from calibre.gui2.store.search import SearchDialog + sd = SearchDialog(self.gui) + sd.exec_() + + def open_store(self, store_plugin): + store_plugin.open(self.gui) diff --git a/src/calibre/gui2/store/__init__.py b/src/calibre/gui2/store/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/calibre/gui2/store/amazon/__init__.py b/src/calibre/gui2/store/amazon/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/calibre/gui2/store/amazon/amazon_kindle_dialog.py b/src/calibre/gui2/store/amazon/amazon_kindle_dialog.py new file mode 100644 index 0000000000..3d1f206eae --- /dev/null +++ b/src/calibre/gui2/store/amazon/amazon_kindle_dialog.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +import urllib + +from PyQt4.Qt import QDialog, QUrl + +from calibre.gui2.store.amazon.amazon_kindle_dialog_ui import Ui_Dialog + +class AmazonKindleDialog(QDialog, Ui_Dialog): + + ASTORE_URL = 'http://astore.amazon.com/josbl0e-20/' + + def __init__(self, parent=None, start_item=None): + QDialog.__init__(self, parent=parent) + self.setupUi(self) + + self.view.loadStarted.connect(self.load_started) + self.view.loadProgress.connect(self.load_progress) + self.view.loadFinished.connect(self.load_finished) + self.home.clicked.connect(self.go_home) + self.reload.clicked.connect(self.go_reload) + + self.go_home(start_item=start_item) + + def load_started(self): + self.progress.setValue(0) + + def load_progress(self, val): + self.progress.setValue(val) + + def load_finished(self): + self.progress.setValue(100) + + def go_home(self, checked=False, start_item=None): + url = self.ASTORE_URL + if start_item: + url += 'detail/' + urllib.quote(start_item) + self.view.load(QUrl(url)) + + def go_reload(self, checked=False): + self.view.reload() diff --git a/src/calibre/gui2/store/amazon/amazon_kindle_dialog.ui b/src/calibre/gui2/store/amazon/amazon_kindle_dialog.ui new file mode 100644 index 0000000000..fb4692fc5c --- /dev/null +++ b/src/calibre/gui2/store/amazon/amazon_kindle_dialog.ui @@ -0,0 +1,84 @@ + + + Dialog + + + + 0 + 0 + 681 + 615 + + + + Amazon Kindle Store + + + true + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + + + + about:blank + + + + + + + + + + + Home + + + + + + + Reload + + + + + + + 0 + + + %p% + + + + + + + + QWebView + QWidget +
QtWebKit/QWebView
+
+ + NPWebView + QWebView +
web_control.h
+
+
+ + +
diff --git a/src/calibre/gui2/store/amazon/amazon_plugin.py b/src/calibre/gui2/store/amazon/amazon_plugin.py new file mode 100644 index 0000000000..e056a174d8 --- /dev/null +++ b/src/calibre/gui2/store/amazon/amazon_plugin.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +import re +import urllib2 +from contextlib import closing + +from lxml import html + +from calibre import browser +from calibre.customize import StorePlugin + +class AmazonKindleStore(StorePlugin): + + name = 'Amazon Kindle' + description = _('Buy Kindle books from Amazon') + + def open(self, parent=None, start_item=None): + from calibre.gui2.store.amazon.amazon_kindle_dialog import AmazonKindleDialog + d = AmazonKindleDialog(parent, start_item) + d = d.exec_() + + def search(self, query, max_results=10): + url = 'http://www.amazon.com/s/url=search-alias%3Ddigital-text&field-keywords=' + urllib2.quote(query) + br = browser() + + counter = max_results + with closing(br.open(url)) as f: + doc = html.fromstring(f.read()) + for data in doc.xpath('//div[@class="productData"]'): + if counter <= 0: + break + + # Even though we are searching digital-text only Amazon will still + # put in results for non Kindle books (author pages). Se we need + # to explicitly check if the item is a Kindle book and ignore it + # if it isn't. + type = ''.join(data.xpath('//span[@class="format"]/text()')) + if 'kindle' not in type.lower(): + continue + + title = ''.join(data.xpath('div[@class="productTitle"]/a/text()')) + author = ''.join(data.xpath('div[@class="productTitle"]/span[@class="ptBrand"]/text()')) + price = ''.join(data.xpath('div[@class="newPrice"]/span/text()')) + + # We must have an asin otherwise we can't easily reference the + # book later. + asin = data.xpath('div[@class="productTitle"]/a[1]') + if asin: + asin = asin[0].get('href', '') + m = re.search(r'/dp/(?P.+?)(/|$)', asin) + if m: + asin = m.group('asin') + else: + continue + + counter -= 1 + yield (title.strip(), author.strip(), price.strip(), asin.strip()) diff --git a/src/calibre/gui2/store/amazon/web_control.py b/src/calibre/gui2/store/amazon/web_control.py new file mode 100644 index 0000000000..d7f95343d2 --- /dev/null +++ b/src/calibre/gui2/store/amazon/web_control.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +from PyQt4.Qt import QWebView, QWebPage + +class NPWebView(QWebView): + + def createWindow(self, type): + if type == QWebPage.WebBrowserWindow: + return self + else: + return None + + + + \ No newline at end of file diff --git a/src/calibre/gui2/store/search.py b/src/calibre/gui2/store/search.py new file mode 100644 index 0000000000..b0cfb3c9b4 --- /dev/null +++ b/src/calibre/gui2/store/search.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +from threading import Event, Thread +from Queue import Queue + +from PyQt4.Qt import QDialog, QTimer + +from calibre.customize.ui import store_plugins +from calibre.gui2.store.search_ui import Ui_Dialog + +class SearchDialog(QDialog, Ui_Dialog): + + def __init__(self, *args): + QDialog.__init__(self, *args) + self.setupUi(self) + + self.store_plugins = {} + self.running_threads = [] + self.results = Queue() + self.abort = Event() + self.checker = QTimer() + + for x in store_plugins(): + self.store_plugins[x.name] = x + + self.search.clicked.connect(self.do_search) + self.checker.timeout.connect(self.get_results) + + def do_search(self, checked=False): + # Stop all running threads. + self.checker.stop() + self.abort.set() + self.running_threads = [] + self.results = Queue() + self.abort = Event() + for n in self.store_plugins: + t = SearchThread(unicode(self.search_edit.text()), (n, self.store_plugins[n]), self.results, self.abort) + self.running_threads.append(t) + t.start() + if self.running_threads: + self.checker.start(100) + + def get_results(self): + running = False + for t in self.running_threads: + if t.is_alive(): + running = True + if not running: + self.checker.stop() + + while not self.results.empty(): + print self.results.get_nowait() + + +class SearchThread(Thread): + + def __init__(self, query, store, results, abort): + Thread.__init__(self) + self.daemon = True + self.query = query + self.store_name = store[0] + self.store_plugin = store[1] + self.results = results + self.abort = abort + + def run(self): + if self.abort.is_set(): + return + for res in self.store_plugin.search(self.query): + self.results.put((self.store_name, res)) diff --git a/src/calibre/gui2/store/search.ui b/src/calibre/gui2/store/search.ui new file mode 100644 index 0000000000..46cac9864e --- /dev/null +++ b/src/calibre/gui2/store/search.ui @@ -0,0 +1,37 @@ + + + Dialog + + + + 0 + 0 + 616 + 545 + + + + calibre Store Search + + + true + + + + + + + + + Search + + + + + + + + + + +