mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-07 10:14:46 -04:00
Initial import of John's store code
This commit is contained in:
commit
01653cf9ad
BIN
resources/images/store.png
Normal file
BIN
resources/images/store.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
@ -5,7 +5,9 @@ __docformat__ = 'restructuredtext en'
|
||||
|
||||
import uuid, sys, os, re, logging, time, random, \
|
||||
__builtin__, warnings, multiprocessing
|
||||
from contextlib import closing
|
||||
from urllib import getproxies
|
||||
from urllib2 import unquote as urllib2_unquote
|
||||
__builtin__.__dict__['dynamic_property'] = lambda(func): func(None)
|
||||
from htmlentitydefs import name2codepoint
|
||||
from math import floor
|
||||
@ -290,6 +292,9 @@ def get_parsed_proxy(typ='http', debug=True):
|
||||
prints('Using http proxy', str(ans))
|
||||
return ans
|
||||
|
||||
USER_AGENT = 'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.13) Gecko/20101210 Gentoo Firefox/3.6.13'
|
||||
USER_AGENT_MOBILE = 'Mozilla/5.0 (Windows; U; Windows CE 5.1; rv:1.8.1a3) Gecko/20060610 Minimo/0.016'
|
||||
|
||||
def random_user_agent():
|
||||
choices = [
|
||||
'Mozilla/5.0 (Windows NT 5.2; rv:2.0.1) Gecko/20100101 Firefox/4.0.1',
|
||||
@ -305,7 +310,6 @@ def random_user_agent():
|
||||
#return choices[-1]
|
||||
return choices[random.randint(0, len(choices)-1)]
|
||||
|
||||
|
||||
def browser(honor_time=True, max_time=2, mobile_browser=False, user_agent=None):
|
||||
'''
|
||||
Create a mechanize browser for web scraping. The browser handles cookies,
|
||||
@ -319,8 +323,7 @@ def browser(honor_time=True, max_time=2, mobile_browser=False, user_agent=None):
|
||||
opener.set_handle_refresh(True, max_time=max_time, honor_time=honor_time)
|
||||
opener.set_handle_robots(False)
|
||||
if user_agent is None:
|
||||
user_agent = ' Mozilla/5.0 (Windows; U; Windows CE 5.1; rv:1.8.1a3) Gecko/20060610 Minimo/0.016' if mobile_browser else \
|
||||
'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.13) Gecko/20101210 Gentoo Firefox/3.6.13'
|
||||
user_agent = USER_AGENT_MOBILE if mobile_browser else USER_AGENT
|
||||
opener.addheaders = [('User-agent', user_agent)]
|
||||
http_proxy = get_proxies().get('http', None)
|
||||
if http_proxy:
|
||||
@ -537,7 +540,46 @@ def as_unicode(obj, enc=preferred_encoding):
|
||||
obj = repr(obj)
|
||||
return force_unicode(obj, enc=enc)
|
||||
|
||||
def url_slash_cleaner(url):
|
||||
'''
|
||||
Removes redundant /'s from url's.
|
||||
'''
|
||||
return re.sub(r'(?<!:)/{2,}', '/', url)
|
||||
|
||||
def get_download_filename(url, cookie_file=None):
|
||||
filename = ''
|
||||
|
||||
br = browser()
|
||||
if cookie_file:
|
||||
from mechanize import MozillaCookieJar
|
||||
cj = MozillaCookieJar()
|
||||
cj.load(cookie_file)
|
||||
br.set_cookiejar(cj)
|
||||
|
||||
try:
|
||||
with closing(br.open(url)) as r:
|
||||
disposition = r.info().get('Content-disposition', '')
|
||||
for p in disposition.split(';'):
|
||||
if 'filename' in p:
|
||||
if '*=' in disposition:
|
||||
parts = disposition.split('*=')[-1]
|
||||
filename = parts.split('\'')[-1]
|
||||
else:
|
||||
filename = disposition.split('=')[-1]
|
||||
if filename[0] in ('\'', '"'):
|
||||
filename = filename[1:]
|
||||
if filename[-1] in ('\'', '"'):
|
||||
filename = filename[:-1]
|
||||
filename = urllib2_unquote(filename)
|
||||
break
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if not filename:
|
||||
filename = r.geturl().split('/')[-1]
|
||||
|
||||
return filename
|
||||
|
||||
def human_readable(size):
|
||||
""" Convert a size in bytes into a human readable form """
|
||||
|
@ -602,3 +602,35 @@ class PreferencesPlugin(Plugin): # {{{
|
||||
|
||||
# }}}
|
||||
|
||||
class StoreBase(Plugin): # {{{
|
||||
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
author = 'John Schember'
|
||||
type = _('Store')
|
||||
|
||||
actual_plugin = None
|
||||
|
||||
def load_actual_plugin(self, gui):
|
||||
'''
|
||||
This method must return the actual interface action plugin object.
|
||||
'''
|
||||
mod, cls = self.actual_plugin.split(':')
|
||||
self.actual_plugin_object = getattr(importlib.import_module(mod), cls)(gui, self.name)
|
||||
return self.actual_plugin_object
|
||||
|
||||
def customization_help(self, gui=False):
|
||||
if getattr(self, 'actual_plugin_object', None) is not None:
|
||||
return self.actual_plugin_object.customization_help(gui)
|
||||
raise NotImplementedError()
|
||||
|
||||
def config_widget(self):
|
||||
if getattr(self, 'actual_plugin_object', None) is not None:
|
||||
return self.actual_plugin_object.config_widget()
|
||||
raise NotImplementedError()
|
||||
|
||||
def save_settings(self, config_widget):
|
||||
if getattr(self, 'actual_plugin_object', None) is not None:
|
||||
return self.actual_plugin_object.save_settings(config_widget)
|
||||
raise NotImplementedError()
|
||||
|
||||
# }}}
|
||||
|
@ -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
|
||||
@ -854,13 +854,18 @@ class ActionNextMatch(InterfaceActionBase):
|
||||
name = 'Next Match'
|
||||
actual_plugin = 'calibre.gui2.actions.next_match:NextMatchAction'
|
||||
|
||||
class ActionStore(InterfaceActionBase):
|
||||
name = 'Store'
|
||||
author = 'John Schember'
|
||||
actual_plugin = 'calibre.gui2.actions.store:StoreAction'
|
||||
|
||||
plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
|
||||
ActionConvert, ActionDelete, ActionEditMetadata, ActionView,
|
||||
ActionFetchNews, ActionSaveToDisk, ActionShowBookDetails,
|
||||
ActionRestart, ActionOpenFolder, ActionConnectShare,
|
||||
ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks,
|
||||
ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary,
|
||||
ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch]
|
||||
ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch, ActionStore]
|
||||
|
||||
# }}}
|
||||
|
||||
@ -1093,4 +1098,81 @@ if test_eight_code:
|
||||
|
||||
#}}}
|
||||
|
||||
# Store plugins {{{
|
||||
class StoreAmazonKindleStore(StoreBase):
|
||||
name = 'Amazon Kindle'
|
||||
description = _('Kindle books from Amazon')
|
||||
actual_plugin = 'calibre.gui2.store.amazon_plugin:AmazonKindleStore'
|
||||
|
||||
class StoreBaenWebScriptionStore(StoreBase):
|
||||
name = 'Baen WebScription'
|
||||
description = _('Ebooks for readers.')
|
||||
actual_plugin = 'calibre.gui2.store.baen_webscription_plugin:BaenWebScriptionStore'
|
||||
|
||||
class StoreBNStore(StoreBase):
|
||||
name = 'Barnes and Noble'
|
||||
description = _('Books, Textbooks, eBooks, Toys, Games and More.')
|
||||
actual_plugin = 'calibre.gui2.store.bn_plugin:BNStore'
|
||||
|
||||
class StoreBeWriteStore(StoreBase):
|
||||
name = 'BeWrite Books'
|
||||
description = _('Publishers of fine books.')
|
||||
actual_plugin = 'calibre.gui2.store.bewrite_plugin:BeWriteStore'
|
||||
|
||||
class StoreDieselEbooksStore(StoreBase):
|
||||
name = 'Diesel eBooks'
|
||||
description = _('World Famous eBook Store.')
|
||||
actual_plugin = 'calibre.gui2.store.diesel_ebooks_plugin:DieselEbooksStore'
|
||||
|
||||
class StoreEbookscomStore(StoreBase):
|
||||
name = 'eBooks.com'
|
||||
description = _('The digital bookstore.')
|
||||
actual_plugin = 'calibre.gui2.store.ebooks_com_plugin:EbookscomStore'
|
||||
|
||||
class StoreEHarlequinStoretore(StoreBase):
|
||||
name = 'eHarlequin'
|
||||
description = _('entertain, enrich, inspire.')
|
||||
actual_plugin = 'calibre.gui2.store.eharlequin_plugin:EHarlequinStore'
|
||||
|
||||
class StoreFeedbooksStore(StoreBase):
|
||||
name = 'Feedbooks'
|
||||
description = _('Read anywhere.')
|
||||
actual_plugin = 'calibre.gui2.store.feedbooks_plugin:FeedbooksStore'
|
||||
|
||||
class StoreGutenbergStore(StoreBase):
|
||||
name = 'Project Gutenberg'
|
||||
description = _('The first producer of free ebooks.')
|
||||
actual_plugin = 'calibre.gui2.store.gutenberg_plugin:GutenbergStore'
|
||||
|
||||
class StoreKoboStore(StoreBase):
|
||||
name = 'Kobo'
|
||||
description = _('eReading: anytime. anyplace.')
|
||||
actual_plugin = 'calibre.gui2.store.kobo_plugin:KoboStore'
|
||||
|
||||
class StoreManyBooksStore(StoreBase):
|
||||
name = 'ManyBooks'
|
||||
description = _('The best ebooks at the best price: free!')
|
||||
actual_plugin = 'calibre.gui2.store.manybooks_plugin:ManyBooksStore'
|
||||
|
||||
class StoreMobileReadStore(StoreBase):
|
||||
name = 'MobileRead'
|
||||
description = _('Ebooks handcrafted with the utmost care')
|
||||
actual_plugin = 'calibre.gui2.store.mobileread_plugin:MobileReadStore'
|
||||
|
||||
class StoreOpenLibraryStore(StoreBase):
|
||||
name = 'Open Library'
|
||||
description = _('One web page for every book.')
|
||||
actual_plugin = 'calibre.gui2.store.open_library_plugin:OpenLibraryStore'
|
||||
|
||||
class StoreSmashwordsStore(StoreBase):
|
||||
name = 'Smashwords'
|
||||
description = _('Your ebook. Your way.')
|
||||
actual_plugin = 'calibre.gui2.store.smashwords_plugin:SmashwordsStore'
|
||||
|
||||
plugins += [StoreAmazonKindleStore, StoreBaenWebScriptionStore, StoreBNStore,
|
||||
StoreBeWriteStore, StoreDieselEbooksStore, StoreEbookscomStore,
|
||||
StoreEHarlequinStoretore,
|
||||
StoreFeedbooksStore, StoreGutenbergStore, StoreKoboStore, StoreManyBooksStore,
|
||||
StoreMobileReadStore, StoreOpenLibraryStore, StoreSmashwordsStore]
|
||||
|
||||
# }}}
|
||||
|
@ -7,7 +7,8 @@ import os, shutil, traceback, functools, sys
|
||||
from calibre.customize import (CatalogPlugin, FileTypePlugin, PluginNotFound,
|
||||
MetadataReaderPlugin, MetadataWriterPlugin,
|
||||
InterfaceActionBase as InterfaceAction,
|
||||
PreferencesPlugin, platform, InvalidPlugin)
|
||||
PreferencesPlugin, platform, InvalidPlugin,
|
||||
StoreBase as Store)
|
||||
from calibre.customize.conversion import InputFormatPlugin, OutputFormatPlugin
|
||||
from calibre.customize.zipplugin import loader
|
||||
from calibre.customize.profiles import InputProfile, OutputProfile
|
||||
@ -244,6 +245,17 @@ def preferences_plugins():
|
||||
yield plugin
|
||||
# }}}
|
||||
|
||||
# Store Plugins # {{{
|
||||
|
||||
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
|
||||
# }}}
|
||||
|
||||
# Metadata read/write {{{
|
||||
_metadata_readers = {}
|
||||
_metadata_writers = {}
|
||||
|
39
src/calibre/gui2/actions/store.py
Normal file
39
src/calibre/gui2/actions/store.py
Normal file
@ -0,0 +1,39 @@
|
||||
# -*- 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 functools import partial
|
||||
|
||||
from PyQt4.Qt import Qt, QMenu, QToolButton, QDialog, QVBoxLayout
|
||||
|
||||
from calibre.gui2.actions import InterfaceAction
|
||||
|
||||
class StoreAction(InterfaceAction):
|
||||
|
||||
name = 'Store'
|
||||
action_spec = (_('Store'), 'store.png', None, None)
|
||||
|
||||
def genesis(self):
|
||||
self.qaction.triggered.connect(self.search)
|
||||
self.store_menu = QMenu()
|
||||
self.load_menu()
|
||||
|
||||
def load_menu(self):
|
||||
self.store_menu.clear()
|
||||
self.store_menu.addAction(_('Search'), self.search)
|
||||
self.store_menu.addSeparator()
|
||||
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.istores, self.gui)
|
||||
sd.exec_()
|
||||
|
||||
def open_store(self, store_plugin):
|
||||
store_plugin.open(self.gui)
|
106
src/calibre/gui2/ebook_download.py
Normal file
106
src/calibre/gui2/ebook_download.py
Normal file
@ -0,0 +1,106 @@
|
||||
# -*- 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'
|
||||
|
||||
import os
|
||||
import shutil
|
||||
from contextlib import closing
|
||||
from mechanize import MozillaCookieJar
|
||||
|
||||
from calibre import browser, get_download_filename
|
||||
from calibre.ebooks import BOOK_EXTENSIONS
|
||||
from calibre.gui2 import Dispatcher
|
||||
from calibre.gui2.threaded_jobs import ThreadedJob
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
|
||||
class EbookDownload(object):
|
||||
|
||||
def __call__(self, gui, cookie_file=None, url='', filename='', save_loc='', add_to_lib=True, tags=[], log=None, abort=None, notifications=None):
|
||||
dfilename = ''
|
||||
try:
|
||||
dfilename = self._download(cookie_file, url, filename, save_loc, add_to_lib)
|
||||
self._add(dfilename, gui, add_to_lib, tags)
|
||||
self._save_as(dfilename, save_loc)
|
||||
except Exception as e:
|
||||
raise e
|
||||
finally:
|
||||
try:
|
||||
if dfilename:
|
||||
os.remove(dfilename)
|
||||
except:
|
||||
pass
|
||||
|
||||
def _download(self, cookie_file, url, filename, save_loc, add_to_lib):
|
||||
dfilename = ''
|
||||
|
||||
if not url:
|
||||
raise Exception(_('No file specified to download.'))
|
||||
if not save_loc and not add_to_lib:
|
||||
# Nothing to do.
|
||||
return dfilename
|
||||
|
||||
if not filename:
|
||||
filename = get_download_filename(url, cookie_file)
|
||||
|
||||
br = browser()
|
||||
if cookie_file:
|
||||
cj = MozillaCookieJar()
|
||||
cj.load(cookie_file)
|
||||
br.set_cookiejar(cj)
|
||||
with closing(br.open(url)) as r:
|
||||
tf = PersistentTemporaryFile(suffix=filename)
|
||||
tf.write(r.read())
|
||||
dfilename = tf.name
|
||||
|
||||
return dfilename
|
||||
|
||||
def _add(self, filename, gui, add_to_lib, tags):
|
||||
if not add_to_lib or not filename:
|
||||
return
|
||||
ext = os.path.splitext(filename)[1][1:].lower()
|
||||
if ext not in BOOK_EXTENSIONS:
|
||||
raise Exception(_('Not a support ebook format.'))
|
||||
|
||||
from calibre.ebooks.metadata.meta import get_metadata
|
||||
with open(filename) as f:
|
||||
mi = get_metadata(f, ext)
|
||||
mi.tags.extend(tags)
|
||||
|
||||
id = gui.library_view.model().db.create_book_entry(mi)
|
||||
gui.library_view.model().db.add_format_with_hooks(id, ext.upper(), filename, index_is_id=True)
|
||||
gui.library_view.model().books_added(1)
|
||||
gui.library_view.model().count_changed()
|
||||
|
||||
def _save_as(self, dfilename, save_loc):
|
||||
if not save_loc or not dfilename:
|
||||
return
|
||||
shutil.copy(dfilename, save_loc)
|
||||
|
||||
|
||||
gui_ebook_download = EbookDownload()
|
||||
|
||||
def start_ebook_download(callback, job_manager, gui, cookie_file=None, url='', filename='', save_loc='', add_to_lib=True, tags=[]):
|
||||
description = _('Downloading %s') % filename if filename else url
|
||||
job = ThreadedJob('ebook_download', description, gui_ebook_download, (gui, cookie_file, url, filename, save_loc, add_to_lib, tags), {}, callback, max_concurrent_count=2, killable=False)
|
||||
job_manager.run_threaded_job(job)
|
||||
|
||||
|
||||
class EbookDownloadMixin(object):
|
||||
|
||||
def download_ebook(self, url='', cookie_file=None, filename='', save_loc='', add_to_lib=True, tags=[]):
|
||||
if tags:
|
||||
if isinstance(tags, basestring):
|
||||
tags = tags.split(',')
|
||||
start_ebook_download(Dispatcher(self.downloaded_ebook), self.job_manager, self, cookie_file, url, filename, save_loc, add_to_lib, tags)
|
||||
self.status_bar.show_message(_('Downloading') + ' ' + filename if filename else url, 3000)
|
||||
|
||||
def downloaded_ebook(self, job):
|
||||
if job.failed:
|
||||
self.job_exception(job, dialog_title=_('Failed to download ebook'))
|
||||
return
|
||||
|
||||
self.status_bar.show_message(job.description + ' ' + _('finished'), 5000)
|
@ -218,6 +218,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
self.search.search.connect(self.find)
|
||||
self.next_button.clicked.connect(self.find_next)
|
||||
self.previous_button.clicked.connect(self.find_previous)
|
||||
self.changed_signal.connect(self.reload_store_plugins)
|
||||
|
||||
def find(self, query):
|
||||
idx = self._plugin_model.find(query)
|
||||
@ -344,6 +345,11 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
plugin.name + _(' cannot be removed. It is a '
|
||||
'builtin plugin. Try disabling it instead.')).exec_()
|
||||
|
||||
def reload_store_plugins(self):
|
||||
self.gui.load_store_plugins()
|
||||
if self.gui.iactions.has_key('Store'):
|
||||
self.gui.iactions['Store'].load_menu()
|
||||
|
||||
def check_for_add_to_toolbars(self, plugin):
|
||||
from calibre.gui2.preferences.toolbar import ConfigWidget
|
||||
from calibre.customize import InterfaceActionBase
|
||||
@ -376,6 +382,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
installed_actions.append(plugin_action.name)
|
||||
gprefs['action-layout-'+key] = tuple(installed_actions)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from PyQt4.Qt import QApplication
|
||||
app = QApplication([])
|
||||
|
139
src/calibre/gui2/store/__init__.py
Normal file
139
src/calibre/gui2/store/__init__.py
Normal file
@ -0,0 +1,139 @@
|
||||
# -*- 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'
|
||||
|
||||
class StorePlugin(object): # {{{
|
||||
'''
|
||||
A plugin representing an online ebook repository (store). The store can
|
||||
be a comercial store that sells ebooks or a source of free downloadable
|
||||
ebooks.
|
||||
|
||||
Note that this class is the base class for these plugins, however, to
|
||||
integrate the plugin with calibre's plugin system, you have to make a
|
||||
wrapper class that references the actual plugin. See the
|
||||
:mod:`calibre.customize.builtins` module for examples.
|
||||
|
||||
If two :class:`StorePlugin` objects have the same name, the one with higher
|
||||
priority takes precedence.
|
||||
|
||||
Sub-classes must implement :meth:`open`, and :meth:`search`.
|
||||
|
||||
Regarding :meth:`open`. Most stores only make themselves available
|
||||
though a web site thus most store plugins will open using
|
||||
:class:`calibre.gui2.store.web_store_dialog.WebStoreDialog`. This will
|
||||
open a modal window and display the store website in a QWebView.
|
||||
|
||||
Sub-classes should implement and use the :meth:`genesis` if they require
|
||||
plugin specific initialization. They should not override or otherwise
|
||||
reimplement :meth:`__init__`.
|
||||
|
||||
Once initialized, this plugin has access to the main calibre GUI via the
|
||||
:attr:`gui` member. You can access other plugins by name, for example::
|
||||
|
||||
self.gui.istores['Amazon Kindle']
|
||||
|
||||
Plugin authors can use affiliate programs within their plugin. The
|
||||
distribution of money earned from a store plugin is 70/30. 70% going
|
||||
to the pluin author / maintainer and 30% going to the calibre project.
|
||||
|
||||
The easiest way to handle affiliate money payouts is to randomly select
|
||||
between the author's affiliate id and calibre's affiliate id so that
|
||||
70% of the time the author's id is used.
|
||||
'''
|
||||
|
||||
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.
|
||||
|
||||
Don't be lazy with the search! Load as much data as possible in the
|
||||
:class:`calibre.gui2.store.search_result.SearchResult` object. If you have to parse
|
||||
multiple pages to get all of the data then do so. However, if data (such as cover_url)
|
||||
isn't available because the store does not display cover images then it's okay to
|
||||
ignore it.
|
||||
|
||||
Also, by default search results can only include ebooks. A plugin can offer users
|
||||
an option to include physical books in the search results but this must be
|
||||
disabled by default.
|
||||
|
||||
If a store doesn't provide search on it's own use something like a site specific
|
||||
google search to get search results for this funtion.
|
||||
|
||||
: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):
|
||||
'''
|
||||
Plugin specific initialization.
|
||||
'''
|
||||
pass
|
||||
|
||||
def config_widget(self):
|
||||
'''
|
||||
See :class:`calibre.customize.Plugin` for details.
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
def save_settings(self, config_widget):
|
||||
'''
|
||||
See :class:`calibre.customize.Plugin` for details.
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
def customization_help(self, gui=False):
|
||||
'''
|
||||
See :class:`calibre.customize.Plugin` for details.
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
# }}}
|
172
src/calibre/gui2/store/amazon_plugin.py
Normal file
172
src/calibre/gui2/store/amazon_plugin.py
Normal file
@ -0,0 +1,172 @@
|
||||
# -*- 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'
|
||||
|
||||
import random
|
||||
import re
|
||||
import urllib2
|
||||
from contextlib import closing
|
||||
|
||||
from lxml import html
|
||||
|
||||
from PyQt4.Qt import QUrl
|
||||
|
||||
from calibre import browser
|
||||
from calibre.gui2 import open_url
|
||||
from calibre.gui2.store import StorePlugin
|
||||
from calibre.gui2.store.search_result import SearchResult
|
||||
|
||||
class AmazonKindleStore(StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
'''
|
||||
Amazon comes with a number of difficulties.
|
||||
|
||||
QWebView has major issues with Amazon.com. The largest of
|
||||
issues is it simply doesn't work on a number of pages.
|
||||
|
||||
When connecting to a number parts of Amazon.com (Kindle library
|
||||
for instance) QNetworkAccessManager fails to connect with a
|
||||
NetworkError of 399 - ProtocolFailure. The strange thing is,
|
||||
when I check QNetworkRequest.HttpStatusCodeAttribute when the
|
||||
399 error is returned the status code is 200 (Ok). However, once
|
||||
the QNetworkAccessManager decides there was a NetworkError it
|
||||
does not download the page from Amazon. So I can't even set the
|
||||
HTML in the QWebView myself.
|
||||
|
||||
There is http://bugreports.qt.nokia.com/browse/QTWEBKIT-259 an
|
||||
open bug about the issue but it is not correct. We can set the
|
||||
useragent (Arora does) to something else and the above issue
|
||||
will persist. This http://developer.qt.nokia.com/forums/viewthread/793
|
||||
gives a bit more information about the issue but as of now (27/Feb/2011)
|
||||
there is no solution or work around.
|
||||
|
||||
We cannot change the The linkDelegationPolicy to allow us to avoid
|
||||
QNetworkAccessManager because it only works links. Forms aren't
|
||||
included so the same issue persists on any part of the site (login)
|
||||
that use a form to load a new page.
|
||||
|
||||
Using an aStore was evaluated but I've decided against using it.
|
||||
There are three major issues with an aStore. Because checkout is
|
||||
handled by sending the user to Amazon we can't put it in a QWebView.
|
||||
If we're sending the user to Amazon sending them there directly is
|
||||
nicer. Also, we cannot put the aStore in a QWebView and let it open the
|
||||
redirection the users default browser because the cookies with the
|
||||
shopping cart won't transfer.
|
||||
|
||||
Another issue with the aStore is how it handles the referral. It only
|
||||
counts the referral for the items in the shopping card / the item
|
||||
that directed the user to Amazon. Kindle books do not use the shopping
|
||||
cart and send the user directly to Amazon for the purchase. In this
|
||||
instance we would only get referral credit for the one book that the
|
||||
aStore directs to Amazon that the user buys. Any other purchases we
|
||||
won't get credit for.
|
||||
|
||||
The last issue with the aStore is performance. Even though it's an
|
||||
Amazon site it's alow. So much slower than Amazon.com that it makes
|
||||
me not want to browse books using it. The look and feel are lesser
|
||||
issues. So is the fact that it almost seems like the purchase is
|
||||
with calibre. This can cause some support issues because we can't
|
||||
do much for issues with Amazon.com purchase hiccups.
|
||||
|
||||
Another option that was evaluated was the Product Advertising API.
|
||||
The reasons against this are complexity. It would take a lot of work
|
||||
to basically re-create Amazon.com within calibre. The Product
|
||||
Advertising API is also designed with being run on a server not
|
||||
in an app. The signing keys would have to be made avaliable to ever
|
||||
calibre user which means bad things could be done with our account.
|
||||
|
||||
The Product Advertising API also assumes the same browser for easy
|
||||
shopping cart transfer to Amazon. With QWebView not working and there
|
||||
not being an easy way to transfer cookies between a QWebView and the
|
||||
users default browser this won't work well.
|
||||
|
||||
We could create our own website on the calibre server and create an
|
||||
Amazon Product Advertising API store. However, this goes back to the
|
||||
complexity argument. Why spend the time recreating Amazon.com
|
||||
|
||||
The final and largest issue against using the Product Advertising API
|
||||
is the Efficiency Guidelines:
|
||||
|
||||
"Each account used to access the Product Advertising API will be allowed
|
||||
an initial usage limit of 2,000 requests per hour. Each account will
|
||||
receive an additional 500 requests per hour (up to a maximum of 25,000
|
||||
requests per hour) for every $1 of shipped item revenue driven per hour
|
||||
in a trailing 30-day period. Usage thresholds are recalculated daily based
|
||||
on revenue performance."
|
||||
|
||||
With over two million users a limit of 2,000 request per hour could
|
||||
render our store unusable for no other reason than Amazon rate
|
||||
limiting our traffic.
|
||||
|
||||
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.
|
||||
'''
|
||||
aff_id = {'tag': 'josbl0e-cpb-20'}
|
||||
# Use Kovid's affiliate id 30% of the time.
|
||||
if random.randint(1, 10) in (1, 2, 3):
|
||||
aff_id['tag'] = 'calibrebs-20'
|
||||
store_link = 'http://www.amazon.com/Kindle-eBooks/b/?ie=UTF&node=1286228011&ref_=%(tag)s&ref=%(tag)s&tag=%(tag)s&linkCode=ur2&camp=1789&creative=390957' % aff_id
|
||||
if detail_item:
|
||||
aff_id['asin'] = detail_item
|
||||
store_link = 'http://www.amazon.com/dp/%(asin)s/?tag=%(tag)s' % aff_id
|
||||
open_url(QUrl(store_link))
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
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, timeout=timeout)) 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
|
||||
|
||||
# We must have an asin otherwise we can't easily reference the
|
||||
# book later.
|
||||
asin_href = None
|
||||
asin_a = data.xpath('div[@class="productTitle"]/a[1]')
|
||||
if asin_a:
|
||||
asin_href = asin_a[0].get('href', '')
|
||||
m = re.search(r'/dp/(?P<asin>.+?)(/|$)', asin_href)
|
||||
if m:
|
||||
asin = m.group('asin')
|
||||
else:
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
|
||||
cover_url = ''
|
||||
if asin_href:
|
||||
cover_img = data.xpath('//div[@class="productImage"]/a[@href="%s"]/img/@src' % asin_href)
|
||||
if cover_img:
|
||||
cover_url = cover_img[0]
|
||||
|
||||
title = ''.join(data.xpath('div[@class="productTitle"]/a/text()'))
|
||||
author = ''.join(data.xpath('div[@class="productTitle"]/span[@class="ptBrand"]/text()'))
|
||||
author = author.split('by')[-1]
|
||||
price = ''.join(data.xpath('div[@class="newPrice"]/span/text()'))
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = price.strip()
|
||||
s.detail_item = asin.strip()
|
||||
|
||||
yield s
|
89
src/calibre/gui2/store/baen_webscription_plugin.py
Normal file
89
src/calibre/gui2/store/baen_webscription_plugin.py
Normal file
@ -0,0 +1,89 @@
|
||||
# -*- 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'
|
||||
|
||||
import re
|
||||
import urllib2
|
||||
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 BaenWebScriptionStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
settings = self.get_settings()
|
||||
url = 'http://www.webscription.net/'
|
||||
|
||||
if external or settings.get(self.name + '_open_external', False):
|
||||
if detail_item:
|
||||
url = url + detail_item
|
||||
open_url(QUrl(url_slash_cleaner(url)))
|
||||
else:
|
||||
detail_url = None
|
||||
if detail_item:
|
||||
detail_url = url + detail_item
|
||||
d = WebStoreDialog(self.gui, url, parent, detail_url)
|
||||
d.setWindowTitle(self.name)
|
||||
d.set_tags(settings.get(self.name + '_tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
url = 'http://www.webscription.net/searchadv.aspx?IsSubmit=true&SearchTerm=' + urllib2.quote(query)
|
||||
|
||||
br = browser()
|
||||
|
||||
counter = max_results
|
||||
with closing(br.open(url, timeout=timeout)) as f:
|
||||
doc = html.fromstring(f.read())
|
||||
for data in doc.xpath('//table/tr/td/img[@src="skins/Skin_1/images/matchingproducts.gif"]/..//tr'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
id = ''.join(data.xpath('./td[1]/a/@href'))
|
||||
if not id:
|
||||
continue
|
||||
|
||||
title = ''.join(data.xpath('./td[1]/a/text()'))
|
||||
|
||||
author = ''
|
||||
cover_url = ''
|
||||
price = ''
|
||||
|
||||
with closing(br.open('http://www.webscription.net/' + id.strip(), timeout=timeout/4)) as nf:
|
||||
idata = html.fromstring(nf.read())
|
||||
author = ''.join(idata.xpath('//span[@class="ProductNameText"]/../b/text()'))
|
||||
author = author.split('by ')[-1]
|
||||
price = ''.join(idata.xpath('//span[@class="variantprice"]/text()'))
|
||||
a, b, price = price.partition('$')
|
||||
price = b + price
|
||||
|
||||
pnum = ''
|
||||
mo = re.search(r'p-(?P<num>\d+)-', id.strip())
|
||||
if mo:
|
||||
pnum = mo.group('num')
|
||||
if pnum:
|
||||
cover_url = 'http://www.webscription.net/' + ''.join(idata.xpath('//img[@id="ProductPic%s"]/@src' % pnum))
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = price
|
||||
s.detail_item = id.strip()
|
||||
|
||||
yield s
|
52
src/calibre/gui2/store/basic_config.py
Normal file
52
src/calibre/gui2/store/basic_config.py
Normal file
@ -0,0 +1,52 @@
|
||||
# -*- 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 gprefs
|
||||
from calibre.gui2.store.basic_config_widget_ui import Ui_Form
|
||||
|
||||
def save_settings(config_widget):
|
||||
gprefs[config_widget.store.name + '_open_external'] = config_widget.open_external.isChecked()
|
||||
tags = unicode(config_widget.tags.text())
|
||||
gprefs[config_widget.store.name + '_tags'] = tags
|
||||
|
||||
class BasicStoreConfigWidget(QWidget, Ui_Form):
|
||||
|
||||
def __init__(self, store):
|
||||
QWidget.__init__(self)
|
||||
self.setupUi(self)
|
||||
|
||||
self.store = store
|
||||
|
||||
self.load_setings()
|
||||
|
||||
def load_setings(self):
|
||||
settings = self.store.get_settings()
|
||||
|
||||
self.open_external.setChecked(settings.get(self.store.name + '_open_external'))
|
||||
self.tags.setText(settings.get(self.store.name + '_tags', ''))
|
||||
|
||||
class BasicStoreConfig(object):
|
||||
|
||||
def customization_help(self, gui=False):
|
||||
return 'Customize the behavior of this store.'
|
||||
|
||||
def config_widget(self):
|
||||
return BasicStoreConfigWidget(self)
|
||||
|
||||
def save_settings(self, config_widget):
|
||||
save_settings(config_widget)
|
||||
|
||||
def get_settings(self):
|
||||
settings = {}
|
||||
|
||||
settings[self.name + '_open_external'] = gprefs.get(self.name + '_open_external', False)
|
||||
settings[self.name + '_tags'] = gprefs.get(self.name + '_tags', self.name + ', store, download')
|
||||
|
||||
return settings
|
38
src/calibre/gui2/store/basic_config_widget.ui
Normal file
38
src/calibre/gui2/store/basic_config_widget.ui
Normal file
@ -0,0 +1,38 @@
|
||||
<?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>460</width>
|
||||
<height>69</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Added Tags:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="tags"/>
|
||||
</item>
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="open_external">
|
||||
<property name="text">
|
||||
<string>Open store in external web browswer</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
81
src/calibre/gui2/store/bewrite_plugin.py
Normal file
81
src/calibre/gui2/store/bewrite_plugin.py
Normal file
@ -0,0 +1,81 @@
|
||||
# -*- 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'
|
||||
|
||||
import re
|
||||
import urllib2
|
||||
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 BeWriteStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
settings = self.get_settings()
|
||||
url = 'http://www.bewrite.net/mm5/merchant.mvc?Screen=SFNT'
|
||||
|
||||
if external or settings.get(self.name + '_open_external', False):
|
||||
if detail_item:
|
||||
url = url + detail_item
|
||||
open_url(QUrl(url_slash_cleaner(url)))
|
||||
else:
|
||||
detail_url = None
|
||||
if detail_item:
|
||||
detail_url = url + detail_item
|
||||
d = WebStoreDialog(self.gui, url, parent, detail_url)
|
||||
d.setWindowTitle(self.name)
|
||||
d.set_tags(settings.get(self.name + '_tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
url = 'http://www.bewrite.net/mm5/merchant.mvc?Search_Code=B&Screen=SRCH&Search=' + urllib2.quote(query)
|
||||
|
||||
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[@id="content"]//table/tr[position() > 1]'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
id = ''.join(data.xpath('.//a/@href'))
|
||||
if not id:
|
||||
continue
|
||||
|
||||
heading = ''.join(data.xpath('./td[2]//text()'))
|
||||
title, q, author = heading.partition('by ')
|
||||
cover_url = ''
|
||||
price = ''
|
||||
|
||||
with closing(br.open(id.strip(), timeout=timeout/4)) as nf:
|
||||
idata = html.fromstring(nf.read())
|
||||
price = ''.join(idata.xpath('//div[@id="content"]//td[contains(text(), "ePub")]/text()'))
|
||||
price = '$' + price.split('$')[-1]
|
||||
cover_img = idata.xpath('//div[@id="content"]//img[1]/@src')
|
||||
if cover_img:
|
||||
cover_url = 'http://www.bewrite.net/mm5/' + cover_img[0]
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url.strip()
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = price.strip()
|
||||
s.detail_item = id.strip()
|
||||
|
||||
yield s
|
82
src/calibre/gui2/store/bn_plugin.py
Normal file
82
src/calibre/gui2/store/bn_plugin.py
Normal file
@ -0,0 +1,82 @@
|
||||
# -*- 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'
|
||||
|
||||
import random
|
||||
import re
|
||||
import urllib2
|
||||
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 BNStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
settings = self.get_settings()
|
||||
|
||||
pub_id = '21000000000352219'
|
||||
# Use Kovid's affiliate id 30% of the time.
|
||||
if random.randint(1, 10) in (1, 2, 3):
|
||||
pub_id = '21000000000352583'
|
||||
|
||||
url = 'http://gan.doubleclick.net/gan_click?lid=41000000028437369&pubid=' + pub_id
|
||||
|
||||
if detail_item:
|
||||
mo = re.search(r'(?<=/)(?P<isbn>\d+)(?=/|$)', detail_item)
|
||||
if mo:
|
||||
isbn = mo.group('isbn')
|
||||
detail_item = 'http://gan.doubleclick.net/gan_click?lid=41000000012871747&pid=' + isbn + '&adurl=' + detail_item + '&pubid=' + pub_id
|
||||
|
||||
if external or settings.get(self.name + '_open_external', False):
|
||||
open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url)))
|
||||
else:
|
||||
d = WebStoreDialog(self.gui, url, parent, detail_item)
|
||||
d.setWindowTitle(self.name)
|
||||
d.set_tags(settings.get(self.name + '_tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
url = 'http://productsearch.barnesandnoble.com/search/results.aspx?STORE=EBOOK&SZE=%s&WRD=' % max_results
|
||||
url += urllib2.quote(query)
|
||||
|
||||
br = browser()
|
||||
|
||||
counter = max_results
|
||||
with closing(br.open(url, timeout=timeout)) as f:
|
||||
doc = html.fromstring(f.read())
|
||||
for data in doc.xpath('//ul[contains(@class, "wgt-search-results-display")]/li[contains(@class, "search-result-item") and contains(@class, "nook-result-item")]'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
id = ''.join(data.xpath('.//div[contains(@class, "wgt-product-image-module")]/a/@href'))
|
||||
if not id:
|
||||
continue
|
||||
cover_url = ''.join(data.xpath('.//div[contains(@class, "wgt-product-image-module")]/a/img/@src'))
|
||||
|
||||
title = ''.join(data.xpath('.//span[@class="product-title"]/a/text()'))
|
||||
author = ', '.join(data.xpath('.//span[@class="contributers-line"]/a/text()'))
|
||||
price = ''.join(data.xpath('.//span[contains(@class, "onlinePriceValue2")]/text()'))
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = price
|
||||
s.detail_item = id.strip()
|
||||
|
||||
yield s
|
87
src/calibre/gui2/store/diesel_ebooks_plugin.py
Normal file
87
src/calibre/gui2/store/diesel_ebooks_plugin.py
Normal file
@ -0,0 +1,87 @@
|
||||
# -*- 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'
|
||||
|
||||
import random
|
||||
import urllib2
|
||||
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 DieselEbooksStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
settings = self.get_settings()
|
||||
url = 'http://www.diesel-ebooks.com/'
|
||||
|
||||
aff_id = '?aid=2049'
|
||||
# Use Kovid's affiliate id 30% of the time.
|
||||
if random.randint(1, 10) in (1, 2, 3):
|
||||
aff_id = '?aid=2053'
|
||||
|
||||
detail_url = None
|
||||
if detail_item:
|
||||
detail_url = url + detail_item + aff_id
|
||||
url = url + aff_id
|
||||
|
||||
if external or settings.get(self.name + '_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(settings.get(self.name + '_tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
url = 'http://www.diesel-ebooks.com/index.php?page=seek&id[m]=&id[c]=scope%253Dinventory&id[q]=' + urllib2.quote(query)
|
||||
|
||||
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="item clearfix"]'):
|
||||
data = html.fromstring(html.tostring(data))
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
id = ''.join(data.xpath('div[@class="cover"]/a/@href'))
|
||||
if not id or '/item/' not in id:
|
||||
continue
|
||||
a, b, id = id.partition('/item/')
|
||||
|
||||
cover_url = ''.join(data.xpath('div[@class="cover"]//img/@src'))
|
||||
if cover_url.startswith('/'):
|
||||
cover_url = cover_url[1:]
|
||||
cover_url = 'http://www.diesel-ebooks.com/' + cover_url
|
||||
|
||||
title = ''.join(data.xpath('.//div[@class="content"]//h2/text()'))
|
||||
author = ''.join(data.xpath('//div[@class="content"]//div[@class="author"]/a/text()'))
|
||||
price = ''
|
||||
price_elem = data.xpath('//td[@class="price"]/text()')
|
||||
if price_elem:
|
||||
price = price_elem[0]
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = price.strip()
|
||||
s.detail_item = '/item/' + id.strip()
|
||||
|
||||
yield s
|
95
src/calibre/gui2/store/ebooks_com_plugin.py
Normal file
95
src/calibre/gui2/store/ebooks_com_plugin.py
Normal file
@ -0,0 +1,95 @@
|
||||
# -*- 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'
|
||||
|
||||
import random
|
||||
import urllib2
|
||||
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 EbookscomStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
settings = self.get_settings()
|
||||
|
||||
m_url = 'http://www.dpbolvw.net/'
|
||||
h_click = 'click-4879827-10364500'
|
||||
d_click = 'click-4879827-10281551'
|
||||
# Use Kovid's affiliate id 30% of the time.
|
||||
if random.randint(1, 10) in (1, 2, 3):
|
||||
h_click = 'click-4913808-10364500'
|
||||
d_click = 'click-4913808-10281551'
|
||||
|
||||
url = m_url + h_click
|
||||
detail_url = None
|
||||
if detail_item:
|
||||
detail_url = m_url + d_click + detail_item
|
||||
|
||||
if external or settings.get(self.name + '_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(settings.get(self.name + '_tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
url = 'http://www.ebooks.com/SearchApp/SearchResults.net?term=' + urllib2.quote(query)
|
||||
|
||||
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_a" or @class="book_b"]'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
id = ''.join(data.xpath('.//a[1]/@href'))
|
||||
id = id.split('=')[-1]
|
||||
if not id:
|
||||
continue
|
||||
|
||||
price = ''
|
||||
with closing(br.open('http://www.ebooks.com/ebooks/book_display.asp?IID=' + id.strip(), timeout=timeout)) as fp:
|
||||
pdoc = html.fromstring(fp.read())
|
||||
pdata = pdoc.xpath('//table[@class="price"]/tr/td/text()')
|
||||
if len(pdata) >= 2:
|
||||
price = pdata[1]
|
||||
if not price:
|
||||
continue
|
||||
|
||||
cover_url = ''.join(data.xpath('.//img[1]/@src'))
|
||||
|
||||
title = ''
|
||||
author = ''
|
||||
heading_a = data.xpath('.//a[1]/text()')
|
||||
if heading_a:
|
||||
title = heading_a[0]
|
||||
if len(heading_a) >= 2:
|
||||
author = heading_a[1]
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = price.strip()
|
||||
s.detail_item = '?url=http://www.ebooks.com/cj.asp?IID=' + id.strip() + '&cjsku=' + id.strip()
|
||||
|
||||
yield s
|
80
src/calibre/gui2/store/eharlequin_plugin.py
Normal file
80
src/calibre/gui2/store/eharlequin_plugin.py
Normal file
@ -0,0 +1,80 @@
|
||||
# -*- 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'
|
||||
|
||||
import random
|
||||
import urllib2
|
||||
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 EHarlequinStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
settings = self.get_settings()
|
||||
|
||||
m_url = 'http://www.dpbolvw.net/'
|
||||
h_click = 'click-4879827-534091'
|
||||
d_click = 'click-4879827-10375439'
|
||||
# Use Kovid's affiliate id 30% of the time.
|
||||
if random.randint(1, 10) in (1, 2, 3):
|
||||
h_click = 'click-4913808-534091'
|
||||
d_click = 'click-4913808-10375439'
|
||||
|
||||
url = m_url + h_click
|
||||
detail_url = None
|
||||
if detail_item:
|
||||
detail_url = m_url + d_click + detail_item
|
||||
|
||||
if external or settings.get(self.name + '_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(settings.get(self.name + '_tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
url = 'http://ebooks.eharlequin.com/BANGSearch.dll?Type=FullText&FullTextField=All&FullTextCriteria=' + urllib2.quote(query)
|
||||
|
||||
br = browser()
|
||||
|
||||
counter = max_results
|
||||
with closing(br.open(url, timeout=timeout)) as f:
|
||||
doc = html.fromstring(f.read())
|
||||
for data in doc.xpath('//table[not(.//@class="sidelink")]/tr[.//ul[@id="details"]]'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
id = ''.join(data.xpath('.//ul[@id="details"]/li[@id="title-results"]/a/@href'))
|
||||
if not id:
|
||||
continue
|
||||
|
||||
title = ''.join(data.xpath('.//ul[@id="details"]/li[@id="title-results"]/a/text()'))
|
||||
author = ''.join(data.xpath('.//ul[@id="details"]/li[@id="author"][1]//a/text()'))
|
||||
price = ''.join(data.xpath('.//div[@class="ourprice"]/font/text()'))
|
||||
cover_url = ''.join(data.xpath('.//a[@href="%s"]/img/@src' % id))
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = price.strip()
|
||||
s.detail_item = '?url=http://ebooks.eharlequin.com/' + id.strip()
|
||||
|
||||
yield s
|
92
src/calibre/gui2/store/feedbooks_plugin.py
Normal file
92
src/calibre/gui2/store/feedbooks_plugin.py
Normal file
@ -0,0 +1,92 @@
|
||||
# -*- 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'
|
||||
|
||||
import urllib2
|
||||
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 FeedbooksStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
settings = self.get_settings()
|
||||
url = 'http://m.feedbooks.com/'
|
||||
ext_url = 'http://feedbooks.com/'
|
||||
|
||||
if external or settings.get(self.name + '_open_external', False):
|
||||
if detail_item:
|
||||
ext_url = ext_url + detail_item
|
||||
open_url(QUrl(url_slash_cleaner(ext_url)))
|
||||
else:
|
||||
detail_url = None
|
||||
if detail_item:
|
||||
detail_url = url + detail_item
|
||||
d = WebStoreDialog(self.gui, url, parent, detail_url)
|
||||
d.setWindowTitle(self.name)
|
||||
d.set_tags(settings.get(self.name + '_tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
url = 'http://m.feedbooks.com/search?query=' + urllib2.quote(query)
|
||||
|
||||
br = browser()
|
||||
|
||||
counter = max_results
|
||||
with closing(br.open(url, timeout=timeout)) as f:
|
||||
doc = html.fromstring(f.read())
|
||||
for data in doc.xpath('//ul[@class="m-list"]//li'):
|
||||
if counter <= 0:
|
||||
break
|
||||
data = html.fromstring(html.tostring(data))
|
||||
|
||||
id = ''
|
||||
id_a = data.xpath('//a[@class="buy"]')
|
||||
if id_a:
|
||||
id = id_a[0].get('href', None)
|
||||
id = id.split('/')[-2]
|
||||
id = '/item/' + id
|
||||
else:
|
||||
id_a = data.xpath('//a[@class="download"]')
|
||||
if id_a:
|
||||
id = id_a[0].get('href', None)
|
||||
id = id.split('/')[-1]
|
||||
id = id.split('.')[0]
|
||||
id = '/book/' + id
|
||||
if not id:
|
||||
continue
|
||||
|
||||
title = ''.join(data.xpath('//h5//a/text()'))
|
||||
author = ''.join(data.xpath('//h6//a/text()'))
|
||||
price = ''.join(data.xpath('//a[@class="buy"]/text()'))
|
||||
if not price:
|
||||
price = '$0.00'
|
||||
cover_url = ''
|
||||
cover_url_img = data.xpath('//img')
|
||||
if cover_url_img:
|
||||
cover_url = cover_url_img[0].get('src')
|
||||
cover_url.split('?')[0]
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = price.replace(' ', '').strip()
|
||||
s.detail_item = id.strip()
|
||||
|
||||
yield s
|
83
src/calibre/gui2/store/gutenberg_plugin.py
Normal file
83
src/calibre/gui2/store/gutenberg_plugin.py
Normal file
@ -0,0 +1,83 @@
|
||||
# -*- 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'
|
||||
|
||||
import urllib2
|
||||
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 GutenbergStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
settings = self.get_settings()
|
||||
url = 'http://m.gutenberg.org/'
|
||||
ext_url = 'http://gutenberg.org/'
|
||||
|
||||
if external or settings.get(self.name + '_open_external', False):
|
||||
if detail_item:
|
||||
ext_url = ext_url + detail_item
|
||||
open_url(QUrl(url_slash_cleaner(ext_url)))
|
||||
else:
|
||||
detail_url = None
|
||||
if detail_item:
|
||||
detail_url = url + detail_item
|
||||
d = WebStoreDialog(self.gui, url, parent, detail_url)
|
||||
d.setWindowTitle(self.name)
|
||||
d.set_tags(settings.get(self.name + '_tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
# Gutenberg's website does not allow searching both author and title.
|
||||
# Using a google search so we can search on both fields at once.
|
||||
url = 'http://www.google.com/xhtml?q=site:gutenberg.org+' + urllib2.quote(query)
|
||||
|
||||
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="edewpi"]//div[@class="r ld"]'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
url = ''
|
||||
url_a = data.xpath('div[@class="jd"]/a')
|
||||
if url_a:
|
||||
url_a = url_a[0]
|
||||
url = url_a.get('href', None)
|
||||
if url:
|
||||
url = url.split('u=')[-1].split('&')[0]
|
||||
if '/ebooks/' not in url:
|
||||
continue
|
||||
id = url.split('/')[-1]
|
||||
|
||||
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'
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = ''
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = price.strip()
|
||||
s.detail_item = '/ebooks/' + id.strip()
|
||||
|
||||
yield s
|
84
src/calibre/gui2/store/kobo_plugin.py
Normal file
84
src/calibre/gui2/store/kobo_plugin.py
Normal file
@ -0,0 +1,84 @@
|
||||
# -*- 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'
|
||||
|
||||
import random
|
||||
import urllib2
|
||||
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 KoboStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
settings = self.get_settings()
|
||||
|
||||
m_url = 'http://www.dpbolvw.net/'
|
||||
h_click = 'click-4879827-10762497'
|
||||
d_click = 'click-4879827-10772898'
|
||||
# Use Kovid's affiliate id 30% of the time.
|
||||
if random.randint(1, 10) in (1, 2, 3):
|
||||
h_click = 'click-4913808-10762497'
|
||||
d_click = 'click-4913808-10772898'
|
||||
|
||||
url = m_url + h_click
|
||||
detail_url = None
|
||||
if detail_item:
|
||||
detail_url = m_url + d_click + detail_item
|
||||
|
||||
if external or settings.get(self.name + '_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(settings.get(self.name + '_tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
url = 'http://www.kobobooks.com/search/search.html?q=' + urllib2.quote(query)
|
||||
|
||||
br = browser()
|
||||
|
||||
counter = max_results
|
||||
with closing(br.open(url, timeout=timeout)) as f:
|
||||
doc = html.fromstring(f.read())
|
||||
for data in doc.xpath('//ul[@class="SCShortCoverList"]/li'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
id = ''.join(data.xpath('.//div[@class="SearchImageContainer"]/a[1]/@href'))
|
||||
if not id:
|
||||
continue
|
||||
|
||||
price = ''.join(data.xpath('.//span[@class="SCOurPrice"]/strong/text()'))
|
||||
if not price:
|
||||
price = '$0.00'
|
||||
|
||||
cover_url = ''.join(data.xpath('.//div[@class="SearchImageContainer"]//img[1]/@src'))
|
||||
|
||||
title = ''.join(data.xpath('.//div[@class="SCItemHeader"]/h1/a[1]/text()'))
|
||||
author = ''.join(data.xpath('.//div[@class="SCItemSummary"]/span/a[1]/text()'))
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = price.strip()
|
||||
s.detail_item = '?url=http://www.kobobooks.com/' + id.strip()
|
||||
|
||||
yield s
|
93
src/calibre/gui2/store/manybooks_plugin.py
Normal file
93
src/calibre/gui2/store/manybooks_plugin.py
Normal file
@ -0,0 +1,93 @@
|
||||
# -*- 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'
|
||||
|
||||
import re
|
||||
import urllib2
|
||||
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 ManyBooksStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
settings = self.get_settings()
|
||||
url = 'http://manybooks.net/'
|
||||
|
||||
detail_url = None
|
||||
if detail_item:
|
||||
detail_url = url + detail_item
|
||||
|
||||
if external or settings.get(self.name + '_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(settings.get(self.name + '_tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
# ManyBooks website separates results for title and author.
|
||||
# It also doesn't do a clear job of references authors and
|
||||
# secondary titles. Google is also faster.
|
||||
# Using a google search so we can search on both fields at once.
|
||||
url = 'http://www.google.com/xhtml?q=site:manybooks.net+' + urllib2.quote(query)
|
||||
|
||||
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="edewpi"]//div[@class="r ld"]'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
url = ''
|
||||
url_a = data.xpath('div[@class="jd"]/a')
|
||||
if url_a:
|
||||
url_a = url_a[0]
|
||||
url = url_a.get('href', None)
|
||||
if url:
|
||||
url = url.split('u=')[-1][:-2]
|
||||
if '/titles/' not in url:
|
||||
continue
|
||||
id = url.split('/')[-1]
|
||||
id = id.strip()
|
||||
|
||||
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'
|
||||
|
||||
cover_url = ''
|
||||
mo = re.match('^\D+', id)
|
||||
if mo:
|
||||
cover_name = mo.group()
|
||||
cover_name = cover_name.replace('etext', '')
|
||||
cover_id = id.split('.')[0]
|
||||
cover_url = 'http://manybooks_images.s3.amazonaws.com/original_covers/' + id[0] + '/' + cover_name + '/' + cover_id + '-thumb.jpg'
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = price.strip()
|
||||
s.detail_item = '/titles/' + id
|
||||
|
||||
yield s
|
304
src/calibre/gui2/store/mobileread_plugin.py
Normal file
304
src/calibre/gui2/store/mobileread_plugin.py
Normal file
@ -0,0 +1,304 @@
|
||||
# -*- 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'
|
||||
|
||||
import difflib
|
||||
import heapq
|
||||
import time
|
||||
from contextlib import closing
|
||||
from threading import RLock
|
||||
|
||||
from lxml import html
|
||||
|
||||
from PyQt4.Qt import Qt, QUrl, QDialog, QAbstractItemModel, QModelIndex, QVariant, \
|
||||
pyqtSignal
|
||||
|
||||
from calibre import browser
|
||||
from calibre.gui2 import open_url, NONE
|
||||
from calibre.gui2.store import StorePlugin
|
||||
from calibre.gui2.store.basic_config import BasicStoreConfig
|
||||
from calibre.gui2.store.mobileread_store_dialog_ui import Ui_Dialog
|
||||
from calibre.gui2.store.search_result import SearchResult
|
||||
from calibre.gui2.store.web_store_dialog import WebStoreDialog
|
||||
from calibre.utils.config import DynamicConfig
|
||||
from calibre.utils.icu import sort_key
|
||||
|
||||
class MobileReadStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def genesis(self):
|
||||
self.config = DynamicConfig('store_' + self.name)
|
||||
self.rlock = RLock()
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
settings = self.get_settings()
|
||||
url = 'http://www.mobileread.com/'
|
||||
|
||||
if external or settings.get(self.name + '_open_external', False):
|
||||
open_url(QUrl(detail_item if detail_item else url))
|
||||
else:
|
||||
if detail_item:
|
||||
d = WebStoreDialog(self.gui, url, parent, detail_item)
|
||||
d.setWindowTitle(self.name)
|
||||
d.set_tags(settings.get(self.name + '_tags', ''))
|
||||
d.exec_()
|
||||
else:
|
||||
d = MobeReadStoreDialog(self, parent)
|
||||
d.setWindowTitle(self.name)
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
books = self.get_book_list(timeout=timeout)
|
||||
|
||||
query = query.lower()
|
||||
query_parts = query.split(' ')
|
||||
matches = []
|
||||
s = difflib.SequenceMatcher()
|
||||
for x in books:
|
||||
ratio = 0
|
||||
t_string = '%s %s' % (x.author.lower(), x.title.lower())
|
||||
query_matches = []
|
||||
for q in query_parts:
|
||||
if q in t_string:
|
||||
query_matches.append(q)
|
||||
for q in query_matches:
|
||||
s.set_seq2(q)
|
||||
for p in t_string.split(' '):
|
||||
s.set_seq1(p)
|
||||
ratio += s.ratio()
|
||||
if ratio > 0:
|
||||
matches.append((ratio, x))
|
||||
|
||||
# Move the best scorers to head of list.
|
||||
matches = heapq.nlargest(max_results, matches)
|
||||
for score, book in matches:
|
||||
book.price = '$0.00'
|
||||
yield book
|
||||
|
||||
def update_book_list(self, timeout=10):
|
||||
with self.rlock:
|
||||
url = 'http://www.mobileread.com/forums/ebooks.php?do=getlist&type=html'
|
||||
|
||||
last_download = self.config.get(self.name + '_last_download', None)
|
||||
# Don't update the book list if our cache is less than one week old.
|
||||
if last_download and (time.time() - last_download) < 604800:
|
||||
return
|
||||
|
||||
# Download the book list HTML file from MobileRead.
|
||||
br = browser()
|
||||
raw_data = None
|
||||
with closing(br.open(url, timeout=timeout)) as f:
|
||||
raw_data = f.read()
|
||||
|
||||
if not raw_data:
|
||||
return
|
||||
|
||||
# Turn books listed in the HTML file into BookRef's.
|
||||
books = []
|
||||
try:
|
||||
data = html.fromstring(raw_data)
|
||||
for book_data in data.xpath('//ul/li'):
|
||||
book = BookRef()
|
||||
book.detail_item = ''.join(book_data.xpath('.//a/@href'))
|
||||
book.format = ''.join(book_data.xpath('.//i/text()'))
|
||||
book.format = book.format.strip()
|
||||
|
||||
text = ''.join(book_data.xpath('.//a/text()'))
|
||||
if ':' in text:
|
||||
book.author, q, text = text.partition(':')
|
||||
book.author = book.author.strip()
|
||||
book.title = text.strip()
|
||||
books.append(book)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Save the book list and it's create time.
|
||||
if books:
|
||||
self.config[self.name + '_last_download'] = time.time()
|
||||
self.config[self.name + '_book_list'] = books
|
||||
|
||||
def get_book_list(self, timeout=10):
|
||||
self.update_book_list(timeout=timeout)
|
||||
return self.config.get(self.name + '_book_list', [])
|
||||
|
||||
|
||||
class BookRef(SearchResult):
|
||||
|
||||
def __init__(self):
|
||||
SearchResult.__init__(self)
|
||||
|
||||
self.format = ''
|
||||
|
||||
|
||||
class MobeReadStoreDialog(QDialog, Ui_Dialog):
|
||||
|
||||
def __init__(self, plugin, *args):
|
||||
QDialog.__init__(self, *args)
|
||||
self.setupUi(self)
|
||||
|
||||
self.plugin = plugin
|
||||
|
||||
self.model = BooksModel()
|
||||
self.results_view.setModel(self.model)
|
||||
self.results_view.model().set_books(self.plugin.get_book_list())
|
||||
self.total.setText('%s' % self.model.rowCount())
|
||||
|
||||
self.results_view.activated.connect(self.open_store)
|
||||
self.search_query.textChanged.connect(self.model.set_filter)
|
||||
self.results_view.model().total_changed.connect(self.total.setText)
|
||||
self.finished.connect(self.dialog_closed)
|
||||
|
||||
self.restore_state()
|
||||
|
||||
def open_store(self, index):
|
||||
result = self.results_view.model().get_book(index)
|
||||
if result:
|
||||
self.plugin.open(self, result.detail_item)
|
||||
|
||||
def restore_state(self):
|
||||
geometry = self.plugin.config['store_mobileread_dialog_geometry']
|
||||
if geometry:
|
||||
self.restoreGeometry(geometry)
|
||||
|
||||
results_cwidth = self.plugin.config['store_mobileread_dialog_results_view_column_width']
|
||||
if results_cwidth:
|
||||
for i, x in enumerate(results_cwidth):
|
||||
if i >= self.results_view.model().columnCount():
|
||||
break
|
||||
self.results_view.setColumnWidth(i, x)
|
||||
else:
|
||||
for i in xrange(self.results_view.model().columnCount()):
|
||||
self.results_view.resizeColumnToContents(i)
|
||||
|
||||
self.results_view.model().sort_col = self.plugin.config.get('store_mobileread_dialog_sort_col', 0)
|
||||
self.results_view.model().sort_order = self.plugin.config.get('store_mobileread_dialog_sort_order', Qt.AscendingOrder)
|
||||
self.results_view.model().sort(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 save_state(self):
|
||||
self.plugin.config['store_mobileread_dialog_geometry'] = self.saveGeometry()
|
||||
self.plugin.config['store_mobileread_dialog_results_view_column_width'] = [self.results_view.columnWidth(i) for i in range(self.model.columnCount())]
|
||||
self.plugin.config['store_mobileread_dialog_sort_col'] = self.results_view.model().sort_col
|
||||
self.plugin.config['store_mobileread_dialog_sort_order'] = self.results_view.model().sort_order
|
||||
|
||||
def dialog_closed(self, result):
|
||||
self.save_state()
|
||||
|
||||
|
||||
class BooksModel(QAbstractItemModel):
|
||||
|
||||
total_changed = pyqtSignal(unicode)
|
||||
|
||||
HEADERS = [_('Title'), _('Author(s)'), _('Format')]
|
||||
|
||||
def __init__(self):
|
||||
QAbstractItemModel.__init__(self)
|
||||
self.books = []
|
||||
self.all_books = []
|
||||
self.filter = ''
|
||||
self.sort_col = 0
|
||||
self.sort_order = Qt.AscendingOrder
|
||||
|
||||
def set_books(self, books):
|
||||
self.books = books
|
||||
self.all_books = books
|
||||
|
||||
self.sort(self.sort_col, self.sort_order)
|
||||
|
||||
def get_book(self, index):
|
||||
row = index.row()
|
||||
if row < len(self.books):
|
||||
return self.books[row]
|
||||
else:
|
||||
return None
|
||||
|
||||
def set_filter(self, filter):
|
||||
#self.layoutAboutToBeChanged.emit()
|
||||
self.beginResetModel()
|
||||
|
||||
self.filter = unicode(filter)
|
||||
self.books = []
|
||||
if self.filter:
|
||||
for b in self.all_books:
|
||||
test = '%s %s %s' % (b.title, b.author, b.format)
|
||||
test = test.lower()
|
||||
include = True
|
||||
for item in self.filter.split(' '):
|
||||
item = item.lower()
|
||||
if item not in test:
|
||||
include = False
|
||||
break
|
||||
if include:
|
||||
self.books.append(b)
|
||||
else:
|
||||
self.books = self.all_books
|
||||
|
||||
self.sort(self.sort_col, self.sort_order, reset=False)
|
||||
self.total_changed.emit('%s' % self.rowCount())
|
||||
|
||||
self.endResetModel()
|
||||
#self.layoutChanged.emit()
|
||||
|
||||
def index(self, row, column, parent=QModelIndex()):
|
||||
return self.createIndex(row, column)
|
||||
|
||||
def parent(self, index):
|
||||
if not index.isValid() or index.internalId() == 0:
|
||||
return QModelIndex()
|
||||
return self.createIndex(0, 0)
|
||||
|
||||
def rowCount(self, *args):
|
||||
return len(self.books)
|
||||
|
||||
def columnCount(self, *args):
|
||||
return len(self.HEADERS)
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
if role != Qt.DisplayRole:
|
||||
return NONE
|
||||
text = ''
|
||||
if orientation == Qt.Horizontal:
|
||||
if section < len(self.HEADERS):
|
||||
text = self.HEADERS[section]
|
||||
return QVariant(text)
|
||||
else:
|
||||
return QVariant(section+1)
|
||||
|
||||
def data(self, index, role):
|
||||
row, col = index.row(), index.column()
|
||||
result = self.books[row]
|
||||
if role == Qt.DisplayRole:
|
||||
if col == 0:
|
||||
return QVariant(result.title)
|
||||
elif col == 1:
|
||||
return QVariant(result.author)
|
||||
elif col == 2:
|
||||
return QVariant(result.format)
|
||||
return NONE
|
||||
|
||||
def data_as_text(self, result, col):
|
||||
text = ''
|
||||
if col == 0:
|
||||
text = result.title
|
||||
elif col == 1:
|
||||
text = result.author
|
||||
elif col == 2:
|
||||
text = result.format
|
||||
return text
|
||||
|
||||
def sort(self, col, order, reset=True):
|
||||
self.sort_col = col
|
||||
self.sort_order = order
|
||||
|
||||
if not self.books:
|
||||
return
|
||||
descending = order == Qt.DescendingOrder
|
||||
self.books.sort(None,
|
||||
lambda x: sort_key(unicode(self.data_as_text(x, col))),
|
||||
descending)
|
||||
if reset:
|
||||
self.reset()
|
||||
|
112
src/calibre/gui2/store/mobileread_store_dialog.ui
Normal file
112
src/calibre/gui2/store/mobileread_store_dialog.ui
Normal file
@ -0,0 +1,112 @@
|
||||
<?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>691</width>
|
||||
<height>614</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Dialog</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Search:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="search_query"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTreeView" name="results_view">
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="rootIsDecorated">
|
||||
<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>
|
||||
<attribute name="headerCascadingSectionResizes">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<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">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>308</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="close_button">
|
||||
<property name="text">
|
||||
<string>Close</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>close_button</sender>
|
||||
<signal>clicked()</signal>
|
||||
<receiver>Dialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>440</x>
|
||||
<y>432</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>245</x>
|
||||
<y>230</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
72
src/calibre/gui2/store/open_library_plugin.py
Normal file
72
src/calibre/gui2/store/open_library_plugin.py
Normal file
@ -0,0 +1,72 @@
|
||||
# -*- 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'
|
||||
|
||||
import urllib2
|
||||
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 OpenLibraryStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
settings = self.get_settings()
|
||||
url = 'http://openlibrary.org/'
|
||||
|
||||
if external or settings.get(self.name + '_open_external', False):
|
||||
if detail_item:
|
||||
url = url + detail_item
|
||||
open_url(QUrl(url_slash_cleaner(url)))
|
||||
else:
|
||||
detail_url = None
|
||||
if detail_item:
|
||||
detail_url = url + detail_item
|
||||
d = WebStoreDialog(self.gui, url, parent, detail_url)
|
||||
d.setWindowTitle(self.name)
|
||||
d.set_tags(settings.get(self.name + '_tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
url = 'http://openlibrary.org/search?q=' + urllib2.quote(query) + '&has_fulltext=true'
|
||||
|
||||
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[@id="searchResults"]/ul[@id="siteSearch"]/li'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
id = ''.join(data.xpath('./span[@class="bookcover"]/a/@href'))
|
||||
if not id:
|
||||
continue
|
||||
cover_url = ''.join(data.xpath('./span[@class="bookcover"]/a/img/@src'))
|
||||
|
||||
title = ''.join(data.xpath('.//h3[@class="booktitle"]/a[@class="results"]/text()'))
|
||||
author = ''.join(data.xpath('.//span[@class="bookauthor"]/a/text()'))
|
||||
price = '$0.00'
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = price
|
||||
s.detail_item = id.strip()
|
||||
|
||||
yield s
|
453
src/calibre/gui2/store/search.py
Normal file
453
src/calibre/gui2/store/search.py
Normal file
@ -0,0 +1,453 @@
|
||||
# -*- 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'
|
||||
|
||||
import re
|
||||
import time
|
||||
from contextlib import closing
|
||||
from random import shuffle
|
||||
from threading import Thread
|
||||
from Queue import Queue
|
||||
|
||||
from PyQt4.Qt import Qt, QAbstractItemModel, QDialog, QTimer, QVariant, \
|
||||
QModelIndex, QPixmap, QSize, QCheckBox, QVBoxLayout, QHBoxLayout, \
|
||||
QPushButton, QString, QByteArray
|
||||
|
||||
from calibre import browser
|
||||
from calibre.gui2 import NONE
|
||||
from calibre.gui2.progress_indicator import ProgressIndicator
|
||||
from calibre.gui2.store.search_ui import Ui_Dialog
|
||||
from calibre.utils.config import DynamicConfig
|
||||
from calibre.utils.icu import sort_key
|
||||
from calibre.utils.magick.draw import thumbnail
|
||||
|
||||
HANG_TIME = 75000 # milliseconds seconds
|
||||
TIMEOUT = 75 # seconds
|
||||
SEARCH_THREAD_TOTAL = 4
|
||||
COVER_DOWNLOAD_THREAD_TOTAL = 2
|
||||
|
||||
class SearchDialog(QDialog, Ui_Dialog):
|
||||
|
||||
def __init__(self, istores, *args):
|
||||
QDialog.__init__(self, *args)
|
||||
self.setupUi(self)
|
||||
|
||||
self.config = DynamicConfig('store_search')
|
||||
|
||||
# We keep a cache of store plugins and reference them by name.
|
||||
self.store_plugins = istores
|
||||
self.search_pool = SearchThreadPool(SearchThread, SEARCH_THREAD_TOTAL)
|
||||
# Check for results and hung threads.
|
||||
self.checker = QTimer()
|
||||
self.hang_check = 0
|
||||
|
||||
self.model = Matches()
|
||||
self.results_view.setModel(self.model)
|
||||
|
||||
# Add check boxes for each store so the user
|
||||
# can disable searching specific stores on a
|
||||
# per search basis.
|
||||
stores_group_layout = QVBoxLayout()
|
||||
self.stores_group.setLayout(stores_group_layout)
|
||||
for x in self.store_plugins:
|
||||
cbox = QCheckBox(x)
|
||||
cbox.setChecked(True)
|
||||
stores_group_layout.addWidget(cbox)
|
||||
setattr(self, 'store_check_' + x, cbox)
|
||||
stores_group_layout.addStretch()
|
||||
|
||||
# Create and add the progress indicator
|
||||
self.pi = ProgressIndicator(self, 24)
|
||||
self.bottom_layout.insertWidget(0, self.pi)
|
||||
|
||||
self.search.clicked.connect(self.do_search)
|
||||
self.checker.timeout.connect(self.get_results)
|
||||
self.results_view.activated.connect(self.open_store)
|
||||
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.finished.connect(self.dialog_closed)
|
||||
|
||||
self.restore_state()
|
||||
|
||||
def resize_columns(self):
|
||||
total = 600
|
||||
# Cover
|
||||
self.results_view.setColumnWidth(0, 85)
|
||||
total = total - 85
|
||||
# Title
|
||||
self.results_view.setColumnWidth(1,int(total*.35))
|
||||
# Author
|
||||
self.results_view.setColumnWidth(2,int(total*.35))
|
||||
# Price
|
||||
self.results_view.setColumnWidth(3, int(total*.10))
|
||||
# Store
|
||||
self.results_view.setColumnWidth(4, int(total*.20))
|
||||
|
||||
def do_search(self, checked=False):
|
||||
# Stop all running threads.
|
||||
self.checker.stop()
|
||||
self.search_pool.abort()
|
||||
# Clear the visible results.
|
||||
self.results_view.model().clear_results()
|
||||
|
||||
# Don't start a search if there is nothing to search for.
|
||||
query = unicode(self.search_edit.text())
|
||||
if not query.strip():
|
||||
return
|
||||
|
||||
# Plugins are in alphebetic order. Randomize the
|
||||
# order of plugin names. This way plugins closer
|
||||
# to a don't have an unfair advantage over
|
||||
# plugins further from a.
|
||||
store_names = self.store_plugins.keys()
|
||||
if not store_names:
|
||||
return
|
||||
shuffle(store_names)
|
||||
# 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)
|
||||
if self.search_pool.has_tasks():
|
||||
self.hang_check = 0
|
||||
self.checker.start(100)
|
||||
self.search_pool.start_threads()
|
||||
self.pi.startAnimation()
|
||||
|
||||
def save_state(self):
|
||||
self.config['store_search_geometry'] = self.saveGeometry()
|
||||
self.config['store_search_store_splitter_state'] = self.store_splitter.saveState()
|
||||
self.config['store_search_results_view_column_width'] = [self.results_view.columnWidth(i) for i in range(self.model.columnCount())]
|
||||
|
||||
store_check = {}
|
||||
for n in self.store_plugins:
|
||||
store_check[n] = getattr(self, 'store_check_' + n).isChecked()
|
||||
self.config['store_search_store_checked'] = store_check
|
||||
|
||||
def restore_state(self):
|
||||
geometry = self.config['store_search_geometry']
|
||||
if geometry:
|
||||
self.restoreGeometry(geometry)
|
||||
|
||||
splitter_state = self.config['store_search_store_splitter_state']
|
||||
if splitter_state:
|
||||
self.store_splitter.restoreState(splitter_state)
|
||||
|
||||
results_cwidth = self.config['store_search_results_view_column_width']
|
||||
if results_cwidth:
|
||||
for i, x in enumerate(results_cwidth):
|
||||
if i >= self.model.columnCount():
|
||||
break
|
||||
self.results_view.setColumnWidth(i, x)
|
||||
else:
|
||||
self.resize_columns()
|
||||
|
||||
store_check = self.config['store_search_store_checked']
|
||||
if store_check:
|
||||
for n in store_check:
|
||||
if hasattr(self, 'store_check_' + n):
|
||||
getattr(self, 'store_check_' + n).setChecked(store_check[n])
|
||||
|
||||
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:
|
||||
self.search_pool.abort()
|
||||
self.checker.stop()
|
||||
self.pi.stopAnimation()
|
||||
else:
|
||||
# Stop the checker if not threads are running.
|
||||
if not self.search_pool.threads_running() and not self.search_pool.has_tasks():
|
||||
self.checker.stop()
|
||||
self.pi.stopAnimation()
|
||||
|
||||
while self.search_pool.has_results():
|
||||
res = self.search_pool.get_result()
|
||||
if res:
|
||||
self.results_view.model().add_result(res)
|
||||
|
||||
def open_store(self, index):
|
||||
result = self.results_view.model().get_result(index)
|
||||
self.store_plugins[result.store_name].open(self, result.detail_item)
|
||||
|
||||
def get_store_checks(self):
|
||||
'''
|
||||
Returns a list of QCheckBox's for each store.
|
||||
'''
|
||||
checks = []
|
||||
for x in self.store_plugins:
|
||||
check = getattr(self, 'store_check_' + x, None)
|
||||
if check:
|
||||
checks.append(check)
|
||||
return checks
|
||||
|
||||
def stores_select_all(self):
|
||||
for check in self.get_store_checks():
|
||||
check.setChecked(True)
|
||||
|
||||
def stores_select_invert(self):
|
||||
for check in self.get_store_checks():
|
||||
check.setChecked(not check.isChecked())
|
||||
|
||||
def stores_select_none(self):
|
||||
for check in self.get_store_checks():
|
||||
check.setChecked(False)
|
||||
|
||||
def dialog_closed(self, result):
|
||||
self.model.closing()
|
||||
self.search_pool.abort()
|
||||
self.save_state()
|
||||
|
||||
|
||||
class GenericDownloadThreadPool(object):
|
||||
'''
|
||||
add_task must be implemented in a subclass.
|
||||
'''
|
||||
|
||||
def __init__(self, thread_type, thread_count):
|
||||
self.thread_type = thread_type
|
||||
self.thread_count = thread_count
|
||||
|
||||
self.tasks = Queue()
|
||||
self.results = Queue()
|
||||
self.threads = []
|
||||
|
||||
def add_task(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def start_threads(self):
|
||||
for i in range(self.thread_count):
|
||||
t = self.thread_type(self.tasks, self.results)
|
||||
self.threads.append(t)
|
||||
t.start()
|
||||
|
||||
def abort(self):
|
||||
self.tasks = Queue()
|
||||
self.results = Queue()
|
||||
for t in self.threads:
|
||||
t.abort()
|
||||
self.threads = []
|
||||
|
||||
def has_tasks(self):
|
||||
return not self.tasks.empty()
|
||||
|
||||
def get_result(self):
|
||||
return self.results.get()
|
||||
|
||||
def get_result_no_wait(self):
|
||||
return self.results.get_nowait()
|
||||
|
||||
def result_count(self):
|
||||
return len(self.results)
|
||||
|
||||
def has_results(self):
|
||||
return not self.results.empty()
|
||||
|
||||
def threads_running(self):
|
||||
for t in self.threads:
|
||||
if t.is_alive():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class SearchThreadPool(GenericDownloadThreadPool):
|
||||
'''
|
||||
Threads will run until there is no work or
|
||||
abort is called. Create and start new threads
|
||||
using start_threads(). Reset by calling abort().
|
||||
|
||||
Example:
|
||||
sp = SearchThreadPool(SearchThread, 3)
|
||||
add tasks using add_task(...)
|
||||
sp.start_threads()
|
||||
all threads have finished.
|
||||
sp.abort()
|
||||
add tasks using add_task(...)
|
||||
sp.start_threads()
|
||||
'''
|
||||
|
||||
def add_task(self, query, store_name, store_plugin, timeout):
|
||||
self.tasks.put((query, store_name, store_plugin, timeout))
|
||||
|
||||
|
||||
class SearchThread(Thread):
|
||||
|
||||
def __init__(self, tasks, results):
|
||||
Thread.__init__(self)
|
||||
self.daemon = True
|
||||
self.tasks = tasks
|
||||
self.results = results
|
||||
self._run = True
|
||||
|
||||
def abort(self):
|
||||
self._run = False
|
||||
|
||||
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):
|
||||
if not self._run:
|
||||
return
|
||||
res.store_name = store_name
|
||||
self.results.put(res)
|
||||
self.tasks.task_done()
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
class CoverThreadPool(GenericDownloadThreadPool):
|
||||
'''
|
||||
Once started all threads run until abort is called.
|
||||
'''
|
||||
|
||||
def add_task(self, search_result, update_callback, timeout=5):
|
||||
self.tasks.put((search_result, update_callback, timeout))
|
||||
|
||||
|
||||
class CoverThread(Thread):
|
||||
|
||||
def __init__(self, tasks, results):
|
||||
Thread.__init__(self)
|
||||
self.daemon = True
|
||||
self.tasks = tasks
|
||||
self.results = results
|
||||
self._run = True
|
||||
|
||||
self.br = browser()
|
||||
|
||||
def abort(self):
|
||||
self._run = False
|
||||
|
||||
def run(self):
|
||||
while self._run:
|
||||
try:
|
||||
time.sleep(.1)
|
||||
while not self.tasks.empty():
|
||||
if not self._run:
|
||||
break
|
||||
result, callback, timeout = self.tasks.get()
|
||||
if result and result.cover_url:
|
||||
with closing(self.br.open(result.cover_url, timeout=timeout)) as f:
|
||||
result.cover_data = f.read()
|
||||
result.cover_data = thumbnail(result.cover_data, 64, 64)[2]
|
||||
callback()
|
||||
self.tasks.task_done()
|
||||
except:
|
||||
continue
|
||||
|
||||
|
||||
class Matches(QAbstractItemModel):
|
||||
|
||||
HEADERS = [_('Cover'), _('Title'), _('Author(s)'), _('Price'), _('Store')]
|
||||
|
||||
def __init__(self):
|
||||
QAbstractItemModel.__init__(self)
|
||||
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 = []
|
||||
self.cover_pool.abort()
|
||||
self.cover_pool.start_threads()
|
||||
self.reset()
|
||||
|
||||
def add_result(self, result):
|
||||
self.layoutAboutToBeChanged.emit()
|
||||
self.matches.append(result)
|
||||
self.cover_pool.add_task(result, self.update_result)
|
||||
self.layoutChanged.emit()
|
||||
|
||||
def get_result(self, index):
|
||||
row = index.row()
|
||||
if row < len(self.matches):
|
||||
return self.matches[row]
|
||||
else:
|
||||
return None
|
||||
|
||||
def update_result(self):
|
||||
self.layoutAboutToBeChanged.emit()
|
||||
self.layoutChanged.emit()
|
||||
|
||||
def index(self, row, column, parent=QModelIndex()):
|
||||
return self.createIndex(row, column)
|
||||
|
||||
def parent(self, index):
|
||||
if not index.isValid() or index.internalId() == 0:
|
||||
return QModelIndex()
|
||||
return self.createIndex(0, 0)
|
||||
|
||||
def rowCount(self, *args):
|
||||
return len(self.matches)
|
||||
|
||||
def columnCount(self, *args):
|
||||
return len(self.HEADERS)
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
if role != Qt.DisplayRole:
|
||||
return NONE
|
||||
text = ''
|
||||
if orientation == Qt.Horizontal:
|
||||
if section < len(self.HEADERS):
|
||||
text = self.HEADERS[section]
|
||||
return QVariant(text)
|
||||
else:
|
||||
return QVariant(section+1)
|
||||
|
||||
def data(self, index, role):
|
||||
row, col = index.row(), index.column()
|
||||
result = self.matches[row]
|
||||
if role == Qt.DisplayRole:
|
||||
if col == 1:
|
||||
return QVariant(result.title)
|
||||
elif col == 2:
|
||||
return QVariant(result.author)
|
||||
elif col == 3:
|
||||
return QVariant(result.price)
|
||||
elif col == 4:
|
||||
return QVariant(result.store_name)
|
||||
return NONE
|
||||
elif role == Qt.DecorationRole:
|
||||
if col == 0 and result.cover_data:
|
||||
p = QPixmap()
|
||||
p.loadFromData(result.cover_data)
|
||||
return QVariant(p)
|
||||
elif role == Qt.SizeHintRole:
|
||||
return QSize(64, 64)
|
||||
return NONE
|
||||
|
||||
def data_as_text(self, result, col):
|
||||
text = ''
|
||||
if col == 1:
|
||||
text = result.title
|
||||
elif col == 2:
|
||||
text = result.author
|
||||
elif col == 3:
|
||||
text = result.price
|
||||
if len(text) < 3 or text[-3] not in ('.', ','):
|
||||
text += '00'
|
||||
text = re.sub(r'\D', '', text)
|
||||
text = text.rjust(6, '0')
|
||||
elif col == 4:
|
||||
text = result.store_name
|
||||
return text
|
||||
|
||||
def sort(self, col, order, reset=True):
|
||||
if not self.matches:
|
||||
return
|
||||
descending = order == Qt.DescendingOrder
|
||||
self.matches.sort(None,
|
||||
lambda x: sort_key(unicode(self.data_as_text(x, col))),
|
||||
descending)
|
||||
if reset:
|
||||
self.reset()
|
||||
|
196
src/calibre/gui2/store/search.ui
Normal file
196
src/calibre/gui2/store/search.ui
Normal file
@ -0,0 +1,196 @@
|
||||
<?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>937</width>
|
||||
<height>669</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>calibre Store Search</string>
|
||||
</property>
|
||||
<property name="sizeGripEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Query:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="search_edit"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="search">
|
||||
<property name="text">
|
||||
<string>Search</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSplitter" name="store_splitter">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Stores</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QScrollArea" name="stores_group">
|
||||
<property name="widgetResizable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QWidget" name="scrollAreaWidgetContents">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>215</width>
|
||||
<height>116</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="QPushButton" name="select_all_stores">
|
||||
<property name="text">
|
||||
<string>All</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="select_invert_stores">
|
||||
<property name="text">
|
||||
<string>Invert</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="select_none_stores">
|
||||
<property name="text">
|
||||
<string>None</string>
|
||||
</property>
|
||||
</widget>
|
||||
</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="QTreeView" 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>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="close">
|
||||
<property name="text">
|
||||
<string>Close</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>close</sender>
|
||||
<signal>clicked()</signal>
|
||||
<receiver>Dialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>526</x>
|
||||
<y>525</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>307</x>
|
||||
<y>272</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
18
src/calibre/gui2/store/search_result.py
Normal file
18
src/calibre/gui2/store/search_result.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'
|
||||
|
||||
class SearchResult(object):
|
||||
|
||||
def __init__(self):
|
||||
self.store_name = ''
|
||||
self.cover_url = ''
|
||||
self.cover_data = None
|
||||
self.title = ''
|
||||
self.author = ''
|
||||
self.price = ''
|
||||
self.detail_item = ''
|
94
src/calibre/gui2/store/smashwords_plugin.py
Normal file
94
src/calibre/gui2/store/smashwords_plugin.py
Normal file
@ -0,0 +1,94 @@
|
||||
# -*- 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'
|
||||
|
||||
import random
|
||||
import re
|
||||
import urllib2
|
||||
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 SmashwordsStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
settings = self.get_settings()
|
||||
url = 'http://www.smashwords.com/'
|
||||
|
||||
aff_id = '?ref=usernone'
|
||||
# Use Kovid's affiliate id 30% of the time.
|
||||
if random.randint(1, 10) in (1, 2, 3):
|
||||
aff_id = '?ref=kovidgoyal'
|
||||
|
||||
detail_url = None
|
||||
if detail_item:
|
||||
detail_url = url + detail_item + aff_id
|
||||
url = url + aff_id
|
||||
|
||||
if external or settings.get(self.name + '_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(settings.get(self.name + '_tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
url = 'http://www.smashwords.com/books/search?query=' + urllib2.quote(query)
|
||||
|
||||
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[@id="pageCenterContent2"]//div[@class="bookCoverImg"]'):
|
||||
if counter <= 0:
|
||||
break
|
||||
data = html.fromstring(html.tostring(data))
|
||||
|
||||
id = None
|
||||
id_a = data.xpath('//a[@class="bookTitle"]')
|
||||
if id_a:
|
||||
id = id_a[0].get('href', None)
|
||||
if id:
|
||||
id = id.split('/')[-1]
|
||||
if not id:
|
||||
continue
|
||||
|
||||
cover_url = ''
|
||||
c_url = data.get('style', None)
|
||||
if c_url:
|
||||
mo = re.search(r'http://[^\'"]+', c_url)
|
||||
if mo:
|
||||
cover_url = mo.group()
|
||||
|
||||
title = ''.join(data.xpath('//a[@class="bookTitle"]/text()'))
|
||||
subnote = ''.join(data.xpath('//span[@class="subnote"]/text()'))
|
||||
author = ''.join(data.xpath('//span[@class="subnote"]/a/text()'))
|
||||
price = subnote.partition('$')[2]
|
||||
price = price.split(u'\xa0')[0]
|
||||
price = '$' + price
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = price.strip()
|
||||
s.detail_item = '/books/view/' + id.strip()
|
||||
|
||||
yield s
|
112
src/calibre/gui2/store/web_control.py
Normal file
112
src/calibre/gui2/store/web_control.py
Normal file
@ -0,0 +1,112 @@
|
||||
# -*- 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'
|
||||
|
||||
import os
|
||||
from urlparse import urlparse
|
||||
|
||||
from PyQt4.Qt import QWebView, QWebPage, QNetworkCookieJar, QNetworkRequest, QString, \
|
||||
QFileDialog, QNetworkProxy
|
||||
|
||||
from calibre import USER_AGENT, get_proxies, get_download_filename
|
||||
from calibre.ebooks import BOOK_EXTENSIONS
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
|
||||
class NPWebView(QWebView):
|
||||
|
||||
def __init__(self, *args):
|
||||
QWebView.__init__(self, *args)
|
||||
self.gui = None
|
||||
self.tags = ''
|
||||
|
||||
self.setPage(NPWebPage())
|
||||
self.page().networkAccessManager().setCookieJar(QNetworkCookieJar())
|
||||
|
||||
http_proxy = get_proxies().get('http', None)
|
||||
if http_proxy:
|
||||
proxy_parts = urlparse(http_proxy)
|
||||
proxy = QNetworkProxy()
|
||||
proxy.setType(QNetworkProxy.HttpProxy)
|
||||
proxy.setUser(proxy_parts.username)
|
||||
proxy.setPassword(proxy_parts.password)
|
||||
proxy.setHostName(proxy_parts.hostname)
|
||||
proxy.setPort(proxy_parts.port)
|
||||
self.page().networkAccessManager().setProxy(proxy)
|
||||
|
||||
self.page().setForwardUnsupportedContent(True)
|
||||
self.page().unsupportedContent.connect(self.start_download)
|
||||
self.page().downloadRequested.connect(self.start_download)
|
||||
self.page().networkAccessManager().sslErrors.connect(self.ignore_ssl_errors)
|
||||
|
||||
def createWindow(self, type):
|
||||
if type == QWebPage.WebBrowserWindow:
|
||||
return self
|
||||
else:
|
||||
return None
|
||||
|
||||
def set_gui(self, gui):
|
||||
self.gui = gui
|
||||
|
||||
def set_tags(self, tags):
|
||||
self.tags = tags
|
||||
|
||||
def start_download(self, request):
|
||||
if not self.gui:
|
||||
return
|
||||
|
||||
url = unicode(request.url().toString())
|
||||
cf = self.get_cookies()
|
||||
|
||||
filename = get_download_filename(url, cf)
|
||||
ext = os.path.splitext(filename)[1][1:].lower()
|
||||
if ext not in BOOK_EXTENSIONS:
|
||||
home = os.path.expanduser('~')
|
||||
name = QFileDialog.getSaveFileName(self,
|
||||
_('File is not a supported ebook type. Save to disk?'),
|
||||
os.path.join(home, filename),
|
||||
'*.*')
|
||||
if name:
|
||||
self.gui.download_ebook(url, cf, name, name, False)
|
||||
else:
|
||||
self.gui.download_ebook(url, cf, filename, tags=self.tags)
|
||||
|
||||
def ignore_ssl_errors(self, reply, errors):
|
||||
reply.ignoreSslErrors(errors)
|
||||
|
||||
def get_cookies(self):
|
||||
'''
|
||||
Writes QNetworkCookies to Mozilla cookie .txt file.
|
||||
|
||||
:return: The file path to the cookie file.
|
||||
'''
|
||||
cf = PersistentTemporaryFile(suffix='.txt')
|
||||
|
||||
cf.write('# Netscape HTTP Cookie File\n\n')
|
||||
|
||||
for c in self.page().networkAccessManager().cookieJar().allCookies():
|
||||
cookie = []
|
||||
domain = unicode(c.domain())
|
||||
|
||||
cookie.append(domain)
|
||||
cookie.append('TRUE' if domain.startswith('.') else 'FALSE')
|
||||
cookie.append(unicode(c.path()))
|
||||
cookie.append('TRUE' if c.isSecure() else 'FALSE')
|
||||
cookie.append(unicode(c.expirationDate().toTime_t()))
|
||||
cookie.append(unicode(c.name()))
|
||||
cookie.append(unicode(c.value()))
|
||||
|
||||
cf.write('\t'.join(cookie))
|
||||
cf.write('\n')
|
||||
|
||||
cf.close()
|
||||
return cf.name
|
||||
|
||||
|
||||
class NPWebPage(QWebPage):
|
||||
|
||||
def userAgentForUrl(self, url):
|
||||
return USER_AGENT
|
55
src/calibre/gui2/store/web_store_dialog.py
Normal file
55
src/calibre/gui2/store/web_store_dialog.py
Normal file
@ -0,0 +1,55 @@
|
||||
# -*- 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 QDialog, QUrl
|
||||
|
||||
from calibre import url_slash_cleaner
|
||||
from calibre.gui2.store.web_store_dialog_ui import Ui_Dialog
|
||||
|
||||
class WebStoreDialog(QDialog, Ui_Dialog):
|
||||
|
||||
def __init__(self, gui, base_url, parent=None, detail_url=None):
|
||||
QDialog.__init__(self, parent=parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self.gui = gui
|
||||
self.base_url = base_url
|
||||
|
||||
self.view.set_gui(self.gui)
|
||||
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.view.reload)
|
||||
self.back.clicked.connect(self.view.back)
|
||||
|
||||
self.go_home(detail_url=detail_url)
|
||||
|
||||
def set_tags(self, tags):
|
||||
self.view.set_tags(tags)
|
||||
|
||||
def load_started(self):
|
||||
self.progress.setValue(0)
|
||||
|
||||
def load_progress(self, val):
|
||||
self.progress.setValue(val)
|
||||
|
||||
def load_finished(self, ok=True):
|
||||
self.progress.setValue(100)
|
||||
|
||||
def go_home(self, checked=False, detail_url=None):
|
||||
if detail_url:
|
||||
url = detail_url
|
||||
else:
|
||||
url = self.base_url
|
||||
|
||||
# Reduce redundant /'s because some stores
|
||||
# (Feedbooks) and server frameworks (cherrypy)
|
||||
# choke on them.
|
||||
url = url_slash_cleaner(url)
|
||||
self.view.load(QUrl(url))
|
115
src/calibre/gui2/store/web_store_dialog.ui
Normal file
115
src/calibre/gui2/store/web_store_dialog.ui
Normal file
@ -0,0 +1,115 @@
|
||||
<?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>962</width>
|
||||
<height>656</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="sizeGripEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0" colspan="5">
|
||||
<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="3">
|
||||
<widget class="QProgressBar" name="progress">
|
||||
<property name="value">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="format">
|
||||
<string>%p%</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QPushButton" name="back">
|
||||
<property name="text">
|
||||
<string>Back</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="4">
|
||||
<widget class="QPushButton" name="close">
|
||||
<property name="text">
|
||||
<string>Close</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>
|
||||
<connection>
|
||||
<sender>close</sender>
|
||||
<signal>clicked()</signal>
|
||||
<receiver>Dialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>917</x>
|
||||
<y>635</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>480</x>
|
||||
<y>327</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
@ -38,7 +38,7 @@ class ThreadedJob(BaseJob):
|
||||
|
||||
:func: The function that actually does the work. This function *must*
|
||||
accept at least three keyword arguments: abort, log and notifications. abort is
|
||||
An Event object. func should periodically check abort.is_set(0 and if
|
||||
An Event object. func should periodically check abort.is_set() and if
|
||||
it is True, it should stop processing as soon as possible. notifications
|
||||
is a Queue. func should put progress notifications into it in the form
|
||||
of a tuple (frac, msg). frac is a number between 0 and 1 indicating
|
||||
|
@ -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
|
||||
from calibre.customize.ui import interface_actions, store_plugins
|
||||
from calibre.gui2 import error_dialog, GetMetadata, open_url, \
|
||||
gprefs, max_available_height, config, info_dialog, Dispatcher, \
|
||||
question_dialog
|
||||
@ -34,6 +34,7 @@ from calibre.gui2.main_window import MainWindow
|
||||
from calibre.gui2.layout import MainWindowMixin
|
||||
from calibre.gui2.device import DeviceMixin
|
||||
from calibre.gui2.email import EmailMixin
|
||||
from calibre.gui2.ebook_download import EbookDownloadMixin
|
||||
from calibre.gui2.jobs import JobManager, JobsDialog, JobsButton
|
||||
from calibre.gui2.init import LibraryViewMixin, LayoutMixin
|
||||
from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin
|
||||
@ -89,7 +90,8 @@ class SystemTrayIcon(QSystemTrayIcon): # {{{
|
||||
|
||||
class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||
TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin,
|
||||
SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin
|
||||
SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin,
|
||||
EbookDownloadMixin
|
||||
):
|
||||
'The main GUI'
|
||||
|
||||
@ -100,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
|
||||
@ -112,11 +115,10 @@ 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)
|
||||
self.load_store_plugins()
|
||||
|
||||
def init_iaction(self, action):
|
||||
ac = action.load_actual_plugin(self)
|
||||
@ -133,6 +135,37 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||
else:
|
||||
acmap[ac.name] = ac
|
||||
|
||||
def load_store_plugins(self):
|
||||
self.istores = OrderedDict()
|
||||
for store in store_plugins():
|
||||
if self.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_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_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):
|
||||
opts = self.opts
|
||||
@ -154,6 +187,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||
for ac in self.iactions.values():
|
||||
ac.do_genesis()
|
||||
self.donate_action = QAction(QIcon(I('donate.png')), _('&Donate to support calibre'), self)
|
||||
for st in self.istores.values():
|
||||
st.do_genesis()
|
||||
MainWindowMixin.__init__(self, db)
|
||||
|
||||
# Jobs Button {{{
|
||||
@ -165,6 +200,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||
|
||||
LayoutMixin.__init__(self)
|
||||
EmailMixin.__init__(self)
|
||||
EbookDownloadMixin.__init__(self)
|
||||
DeviceMixin.__init__(self)
|
||||
|
||||
self.progress_indicator = ProgressIndicator(self)
|
||||
|
Loading…
x
Reference in New Issue
Block a user