mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Beginnings of store. Start of Amazon Kindle store. Start of meta store search.
This commit is contained in:
parent
b1c003f5af
commit
95397a087a
@ -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()
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
@ -805,6 +805,10 @@ class ActionTweakEpub(InterfaceActionBase):
|
|||||||
class ActionNextMatch(InterfaceActionBase):
|
class ActionNextMatch(InterfaceActionBase):
|
||||||
name = 'Next Match'
|
name = 'Next Match'
|
||||||
actual_plugin = 'calibre.gui2.actions.next_match:NextMatchAction'
|
actual_plugin = 'calibre.gui2.actions.next_match:NextMatchAction'
|
||||||
|
|
||||||
|
class ActionStore(InterfaceActionBase):
|
||||||
|
name = 'Store'
|
||||||
|
actual_plugin = 'calibre.gui2.actions.store:StoreAction'
|
||||||
|
|
||||||
plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
|
plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
|
||||||
ActionConvert, ActionDelete, ActionEditMetadata, ActionView,
|
ActionConvert, ActionDelete, ActionEditMetadata, ActionView,
|
||||||
@ -812,7 +816,7 @@ plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
|
|||||||
ActionRestart, ActionOpenFolder, ActionConnectShare,
|
ActionRestart, ActionOpenFolder, ActionConnectShare,
|
||||||
ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks,
|
ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks,
|
||||||
ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary,
|
ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary,
|
||||||
ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch]
|
ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch, ActionStore]
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
@ -1037,3 +1041,10 @@ from calibre.ebooks.metadata.sources.google import GoogleBooks
|
|||||||
plugins += [GoogleBooks]
|
plugins += [GoogleBooks]
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
# Store plugins {{{
|
||||||
|
from calibre.gui2.store.amazon.amazon_plugin import AmazonKindleStore
|
||||||
|
|
||||||
|
plugins += [AmazonKindleStore]
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
@ -8,7 +8,7 @@ from contextlib import closing
|
|||||||
from calibre.customize import Plugin, CatalogPlugin, FileTypePlugin, \
|
from calibre.customize import Plugin, CatalogPlugin, FileTypePlugin, \
|
||||||
MetadataReaderPlugin, MetadataWriterPlugin, \
|
MetadataReaderPlugin, MetadataWriterPlugin, \
|
||||||
InterfaceActionBase as InterfaceAction, \
|
InterfaceActionBase as InterfaceAction, \
|
||||||
PreferencesPlugin
|
PreferencesPlugin, StorePlugin
|
||||||
from calibre.customize.conversion import InputFormatPlugin, OutputFormatPlugin
|
from calibre.customize.conversion import InputFormatPlugin, OutputFormatPlugin
|
||||||
from calibre.customize.profiles import InputProfile, OutputProfile
|
from calibre.customize.profiles import InputProfile, OutputProfile
|
||||||
from calibre.customize.builtins import plugins as builtin_plugins
|
from calibre.customize.builtins import plugins as builtin_plugins
|
||||||
@ -277,6 +277,17 @@ def preferences_plugins():
|
|||||||
yield plugin
|
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 read/write {{{
|
||||||
_metadata_readers = {}
|
_metadata_readers = {}
|
||||||
_metadata_writers = {}
|
_metadata_writers = {}
|
||||||
|
34
src/calibre/gui2/actions/store.py
Normal file
34
src/calibre/gui2/actions/store.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
__license__ = 'GPL 3'
|
||||||
|
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||||
|
__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)
|
0
src/calibre/gui2/store/__init__.py
Normal file
0
src/calibre/gui2/store/__init__.py
Normal file
0
src/calibre/gui2/store/amazon/__init__.py
Normal file
0
src/calibre/gui2/store/amazon/__init__.py
Normal file
45
src/calibre/gui2/store/amazon/amazon_kindle_dialog.py
Normal file
45
src/calibre/gui2/store/amazon/amazon_kindle_dialog.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
__license__ = 'GPL 3'
|
||||||
|
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||||
|
__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()
|
84
src/calibre/gui2/store/amazon/amazon_kindle_dialog.ui
Normal file
84
src/calibre/gui2/store/amazon/amazon_kindle_dialog.ui
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>Dialog</class>
|
||||||
|
<widget class="QDialog" name="Dialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>681</width>
|
||||||
|
<height>615</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Amazon Kindle Store</string>
|
||||||
|
</property>
|
||||||
|
<property name="sizeGripEnabled">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<item row="0" column="0" colspan="3">
|
||||||
|
<widget class="QFrame" name="frame">
|
||||||
|
<property name="frameShape">
|
||||||
|
<enum>QFrame::StyledPanel</enum>
|
||||||
|
</property>
|
||||||
|
<property name="frameShadow">
|
||||||
|
<enum>QFrame::Raised</enum>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<property name="margin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="NPWebView" name="view">
|
||||||
|
<property name="url">
|
||||||
|
<url>
|
||||||
|
<string>about:blank</string>
|
||||||
|
</url>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QPushButton" name="home">
|
||||||
|
<property name="text">
|
||||||
|
<string>Home</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="QPushButton" name="reload">
|
||||||
|
<property name="text">
|
||||||
|
<string>Reload</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="2">
|
||||||
|
<widget class="QProgressBar" name="progress">
|
||||||
|
<property name="value">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="format">
|
||||||
|
<string>%p%</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<customwidgets>
|
||||||
|
<customwidget>
|
||||||
|
<class>QWebView</class>
|
||||||
|
<extends>QWidget</extends>
|
||||||
|
<header>QtWebKit/QWebView</header>
|
||||||
|
</customwidget>
|
||||||
|
<customwidget>
|
||||||
|
<class>NPWebView</class>
|
||||||
|
<extends>QWebView</extends>
|
||||||
|
<header>web_control.h</header>
|
||||||
|
</customwidget>
|
||||||
|
</customwidgets>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
61
src/calibre/gui2/store/amazon/amazon_plugin.py
Normal file
61
src/calibre/gui2/store/amazon/amazon_plugin.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
__license__ = 'GPL 3'
|
||||||
|
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||||
|
__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>.+?)(/|$)', asin)
|
||||||
|
if m:
|
||||||
|
asin = m.group('asin')
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
counter -= 1
|
||||||
|
yield (title.strip(), author.strip(), price.strip(), asin.strip())
|
19
src/calibre/gui2/store/amazon/web_control.py
Normal file
19
src/calibre/gui2/store/amazon/web_control.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
__license__ = 'GPL 3'
|
||||||
|
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||||
|
__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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
74
src/calibre/gui2/store/search.py
Normal file
74
src/calibre/gui2/store/search.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
__license__ = 'GPL 3'
|
||||||
|
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||||
|
__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))
|
37
src/calibre/gui2/store/search.ui
Normal file
37
src/calibre/gui2/store/search.ui
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>Dialog</class>
|
||||||
|
<widget class="QDialog" name="Dialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>616</width>
|
||||||
|
<height>545</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>calibre Store Search</string>
|
||||||
|
</property>
|
||||||
|
<property name="sizeGripEnabled">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLineEdit" name="search_edit"/>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QPushButton" name="search">
|
||||||
|
<property name="text">
|
||||||
|
<string>Search</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0" colspan="2">
|
||||||
|
<widget class="QTreeView" name="results"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
Loading…
x
Reference in New Issue
Block a user