Get Books: Allow stores to customize the browser instance used for downloading book files

This commit is contained in:
Kovid Goyal 2015-01-03 09:13:54 +05:30
parent a145a210b8
commit 2d48de8cb2
9 changed files with 51 additions and 32 deletions

View File

@ -12,7 +12,6 @@ from contextlib import closing
from mechanize import MozillaCookieJar from mechanize import MozillaCookieJar
from calibre import browser from calibre import browser
from calibre.constants import __appname__, __version__
from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks import BOOK_EXTENSIONS
from calibre.gui2 import Dispatcher, gprefs from calibre.gui2 import Dispatcher, gprefs
from calibre.gui2.dialogs.message_box import MessageBox from calibre.gui2.dialogs.message_box import MessageBox
@ -52,15 +51,13 @@ def get_download_filename(response):
filename = ascii_filename(filename) filename = ascii_filename(filename)
return filename return filename
def download_file(url, cookie_file=None, filename=None): def download_file(url, cookie_file=None, filename=None, create_browser=None):
user_agent = None
if url.startswith('//'): if url.startswith('//'):
url = 'http:' + url url = 'http:' + url
if url.startswith('http://www.gutenberg.org'): try:
# Project Gutenberg returns an HTML page if the user agent is a normal br = browser() if create_browser is None else create_browser()
# browser user agent except NotImplementedError:
user_agent = '%s/%s' % (__appname__, __version__) br = browser()
br = browser(user_agent=user_agent)
if cookie_file: if cookie_file:
cj = MozillaCookieJar() cj = MozillaCookieJar()
cj.load(cookie_file) cj.load(cookie_file)
@ -78,10 +75,11 @@ def download_file(url, cookie_file=None, filename=None):
class EbookDownload(object): 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): def __call__(self, gui, cookie_file=None, url='', filename='', save_loc='', add_to_lib=True, tags=[], create_browser=None,
log=None, abort=None, notifications=None):
dfilename = '' dfilename = ''
try: try:
dfilename = self._download(cookie_file, url, filename, save_loc, add_to_lib) dfilename = self._download(cookie_file, url, filename, save_loc, add_to_lib, create_browser)
self._add(dfilename, gui, add_to_lib, tags) self._add(dfilename, gui, add_to_lib, tags)
self._save_as(dfilename, save_loc) self._save_as(dfilename, save_loc)
finally: finally:
@ -91,13 +89,13 @@ class EbookDownload(object):
except: except:
pass pass
def _download(self, cookie_file, url, filename, save_loc, add_to_lib): def _download(self, cookie_file, url, filename, save_loc, add_to_lib, create_browser):
if not url: if not url:
raise Exception(_('No file specified to download.')) raise Exception(_('No file specified to download.'))
if not save_loc and not add_to_lib: if not save_loc and not add_to_lib:
# Nothing to do. # Nothing to do.
return '' return ''
return download_file(url, cookie_file, filename) return download_file(url, cookie_file, filename, create_browser=create_browser)
def _add(self, filename, gui, add_to_lib, tags): def _add(self, filename, gui, add_to_lib, tags):
if not add_to_lib or not filename: if not add_to_lib or not filename:
@ -124,10 +122,10 @@ class EbookDownload(object):
gui_ebook_download = EbookDownload() gui_ebook_download = EbookDownload()
def start_ebook_download(callback, job_manager, gui, cookie_file=None, url='', filename='', save_loc='', add_to_lib=True, tags=[]): def start_ebook_download(callback, job_manager, gui, cookie_file=None, url='', filename='', save_loc='', add_to_lib=True, tags=[], create_browser=None):
description = _('Downloading %s') % filename.decode('utf-8', 'ignore') if filename else url.decode('utf-8', 'ignore') description = _('Downloading %s') % filename.decode('utf-8', 'ignore') if filename else url.decode('utf-8', 'ignore')
job = ThreadedJob('ebook_download', description, gui_ebook_download, ( job = ThreadedJob('ebook_download', description, gui_ebook_download, (
gui, cookie_file, url, filename, save_loc, add_to_lib, tags), {}, gui, cookie_file, url, filename, save_loc, add_to_lib, tags, create_browser), {},
callback, max_concurrent_count=2, killable=False) callback, max_concurrent_count=2, killable=False)
job_manager.run_threaded_job(job) job_manager.run_threaded_job(job)
@ -137,11 +135,11 @@ class EbookDownloadMixin(object):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
pass pass
def download_ebook(self, url='', cookie_file=None, filename='', save_loc='', add_to_lib=True, tags=[]): def download_ebook(self, url='', cookie_file=None, filename='', save_loc='', add_to_lib=True, tags=[], create_browser=None):
if tags: if tags:
if isinstance(tags, basestring): if isinstance(tags, basestring):
tags = tags.split(',') tags = tags.split(',')
start_ebook_download(Dispatcher(self.downloaded_ebook), self.job_manager, self, cookie_file, url, filename, save_loc, add_to_lib, tags) start_ebook_download(Dispatcher(self.downloaded_ebook), self.job_manager, self, cookie_file, url, filename, save_loc, add_to_lib, tags, create_browser)
self.status_bar.show_message(_('Downloading') + ' ' + filename.decode('utf-8', 'ignore') if filename else url.decode('utf-8', 'ignore'), 3000) self.status_bar.show_message(_('Downloading') + ' ' + filename.decode('utf-8', 'ignore') if filename else url.decode('utf-8', 'ignore'), 3000)
def downloaded_ebook(self, job): def downloaded_ebook(self, job):

View File

@ -8,7 +8,8 @@ __docformat__ = 'restructuredtext en'
from calibre.utils.filenames import ascii_filename from calibre.utils.filenames import ascii_filename
class StorePlugin(object): # {{{ class StorePlugin(object): # {{{
''' '''
A plugin representing an online ebook repository (store). The store can A plugin representing an online ebook repository (store). The store can
be a commercial store that sells ebooks or a source of free downloadable be a commercial store that sells ebooks or a source of free downloadable
@ -60,6 +61,18 @@ class StorePlugin(object): # {{{
config = JSONConfig('store/stores/' + ascii_filename(self.name)) config = JSONConfig('store/stores/' + ascii_filename(self.name))
self.config = config self.config = config
def create_browser(self):
'''
If the server requires special headers, such as a particular user agent
or a referrer, then implement this method in you plugin to return a
customized browser instance. See the Gutenberg plugin for an example.
Note that if you implement the open() method in your plugin and use the
WebStoreDialog class, remember to pass self.createbrowser in the
constructor of WebStoreDialog.
'''
raise NotImplementedError()
def open(self, gui, parent=None, detail_item=None, external=False): def open(self, gui, parent=None, detail_item=None, external=False):
''' '''
Open the store. Open the store.

View File

@ -32,7 +32,7 @@ class OpenSearchOPDSStore(StorePlugin):
if external or self.config.get('open_external', False): if external or self.config.get('open_external', False):
open_url(QUrl(detail_item if detail_item else self.web_url)) open_url(QUrl(detail_item if detail_item else self.web_url))
else: else:
d = WebStoreDialog(self.gui, self.web_url, parent, detail_item) d = WebStoreDialog(self.gui, self.web_url, parent, detail_item, create_browser=self.create_browser)
d.setWindowTitle(self.name) d.setWindowTitle(self.name)
d.set_tags(self.config.get('tags', '')) d.set_tags(self.config.get('tags', ''))
d.exec_() d.exec_()
@ -97,5 +97,4 @@ class OpenSearchOPDSStore(StorePlugin):
s.price = currency_code + ' ' + price s.price = currency_code + ' ' + price
s.price = s.price.strip() s.price = s.price.strip()
yield s yield s

View File

@ -122,6 +122,7 @@ class SearchThread(Thread):
res.store_name = store_name res.store_name = store_name
res.affiliate = store_plugin.base_plugin.affiliate res.affiliate = store_plugin.base_plugin.affiliate
res.plugin_author = store_plugin.base_plugin.author res.plugin_author = store_plugin.base_plugin.author
res.create_browser = store_plugin.create_browser
self.results.put((res, store_plugin)) self.results.put((res, store_plugin))
self.tasks.task_done() self.tasks.task_done()
except: except:

View File

@ -395,7 +395,7 @@ class SearchDialog(QDialog, Ui_Dialog):
fname = result.title[:60] + '.' + ext.lower() fname = result.title[:60] + '.' + ext.lower()
fname = ascii_filename(fname) fname = ascii_filename(fname)
show_download_info(result.title, parent=self) show_download_info(result.title, parent=self)
self.gui.download_ebook(result.downloads[ext], filename=fname) self.gui.download_ebook(result.downloads[ext], filename=fname, create_browser=result.create_browser)
def open_store(self, result): def open_store(self, result):
self.gui.istores[result.store_name].open(self, result.detail_item, self.open_external.isChecked()) self.gui.istores[result.store_name].open(self, result.detail_item, self.open_external.isChecked())

View File

@ -27,6 +27,7 @@ class SearchResult(object):
self.downloads = {} self.downloads = {}
self.affiliate = False self.affiliate = False
self.plugin_author = '' self.plugin_author = ''
self.create_browser = None
def __eq__(self, other): def __eq__(self, other):
return self.title == other.title and self.author == other.author and self.store_name == other.store_name and self.formats == other.formats return self.title == other.title and self.author == other.author and self.store_name == other.store_name and self.formats == other.formats

View File

@ -16,7 +16,7 @@ from contextlib import closing
from lxml import etree from lxml import etree
from calibre import browser, url_slash_cleaner from calibre import browser, url_slash_cleaner
from calibre.constants import __version__ from calibre.constants import __appname__, __version__
from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.basic_config import BasicStoreConfig
from calibre.gui2.store.opensearch_store import OpenSearchOPDSStore from calibre.gui2.store.opensearch_store import OpenSearchOPDSStore
from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.search_result import SearchResult
@ -93,6 +93,11 @@ class GutenbergStore(BasicStoreConfig, OpenSearchOPDSStore):
open_search_url = 'http://www.gutenberg.org/catalog/osd-books.xml' open_search_url = 'http://www.gutenberg.org/catalog/osd-books.xml'
web_url = web_url web_url = web_url
def create_browser(self):
from calibre import browser
user_agent = '%s/%s' % (__appname__, __version__)
return browser(user_agent=user_agent)
def search(self, query, max_results=10, timeout=60): def search(self, query, max_results=10, timeout=60):
''' '''
Gutenberg's ODPS feed is poorly implmented and has a number of issues Gutenberg's ODPS feed is poorly implmented and has a number of issues

View File

@ -26,6 +26,7 @@ class NPWebView(QWebView):
QWebView.__init__(self, *args) QWebView.__init__(self, *args)
self.gui = None self.gui = None
self.tags = '' self.tags = ''
self.create_browser = None
self._page = NPWebPage() self._page = NPWebPage()
self.setPage(self._page) self.setPage(self._page)
@ -90,10 +91,10 @@ class NPWebView(QWebView):
return return
name = choose_save_file(self, 'web-store-download-unknown', _('File is not a supported ebook type. Save to disk?'), initial_filename=filename) name = choose_save_file(self, 'web-store-download-unknown', _('File is not a supported ebook type. Save to disk?'), initial_filename=filename)
if name: if name:
self.gui.download_ebook(url, cf, name, name, False) self.gui.download_ebook(url, cf, name, name, False, create_browser=self.create_browser)
else: else:
show_download_info(filename, self) show_download_info(filename, self)
self.gui.download_ebook(url, cf, filename, tags=self.tags) self.gui.download_ebook(url, cf, filename, tags=self.tags, create_browser=self.create_browser)
def ignore_ssl_errors(self, reply, errors): def ignore_ssl_errors(self, reply, errors):
reply.ignoreSslErrors(errors) reply.ignoreSslErrors(errors)

View File

@ -13,21 +13,22 @@ from calibre.gui2.store.web_store_dialog_ui import Ui_Dialog
class WebStoreDialog(QDialog, Ui_Dialog): class WebStoreDialog(QDialog, Ui_Dialog):
def __init__(self, gui, base_url, parent=None, detail_url=None): def __init__(self, gui, base_url, parent=None, detail_url=None, create_browser=None):
QDialog.__init__(self, parent=parent) QDialog.__init__(self, parent=parent)
self.setupUi(self) self.setupUi(self)
self.gui = gui self.gui = gui
self.base_url = base_url self.base_url = base_url
self.view.set_gui(self.gui) self.view.set_gui(self.gui)
self.view.create_browser = create_browser
self.view.loadStarted.connect(self.load_started) self.view.loadStarted.connect(self.load_started)
self.view.loadProgress.connect(self.load_progress) self.view.loadProgress.connect(self.load_progress)
self.view.loadFinished.connect(self.load_finished) self.view.loadFinished.connect(self.load_finished)
self.home.clicked.connect(self.go_home) self.home.clicked.connect(self.go_home)
self.reload.clicked.connect(self.view.reload) self.reload.clicked.connect(self.view.reload)
self.back.clicked.connect(self.view.back) self.back.clicked.connect(self.view.back)
self.go_home(detail_url=detail_url) self.go_home(detail_url=detail_url)
def set_tags(self, tags): def set_tags(self, tags):
@ -35,21 +36,21 @@ class WebStoreDialog(QDialog, Ui_Dialog):
def load_started(self): def load_started(self):
self.progress.setValue(0) self.progress.setValue(0)
def load_progress(self, val): def load_progress(self, val):
self.progress.setValue(val) self.progress.setValue(val)
def load_finished(self, ok=True): def load_finished(self, ok=True):
self.progress.setValue(100) self.progress.setValue(100)
def go_home(self, checked=False, detail_url=None): def go_home(self, checked=False, detail_url=None):
if detail_url: if detail_url:
url = detail_url url = detail_url
else: else:
url = self.base_url url = self.base_url
# Reduce redundant /'s because some stores # Reduce redundant /'s because some stores
# (Feedbooks) and server frameworks (cherrypy) # (Feedbooks) and server frameworks (cherrypy)
# choke on them. # choke on them.
url = url_slash_cleaner(url) url = url_slash_cleaner(url)
self.view.load(QUrl(url)) self.view.load(QUrl(url))