diff --git a/src/calibre/customize/__init__.py b/src/calibre/customize/__init__.py index 2edb7d7fd1..2c75b40494 100644 --- a/src/calibre/customize/__init__.py +++ b/src/calibre/customize/__init__.py @@ -588,7 +588,7 @@ class StorePlugin(Plugin): # {{{ author = 'John Schember' type = _('Stores') - def open(self, parent=None, start_item=None): + def open(self, gui, parent=None, start_item=None): ''' Open a dialog for displaying the store. start_item is a refernce unique to the store diff --git a/src/calibre/gui2/actions/store.py b/src/calibre/gui2/actions/store.py index 97094ad86e..9066150684 100644 --- a/src/calibre/gui2/actions/store.py +++ b/src/calibre/gui2/actions/store.py @@ -27,8 +27,8 @@ class StoreAction(InterfaceAction): def search(self): from calibre.gui2.store.search import SearchDialog - sd = SearchDialog(self.gui) + sd = SearchDialog(self.gui, self.gui) sd.exec_() def open_store(self, store_plugin): - store_plugin.open(self.gui) + store_plugin.open(self.gui, self.gui) diff --git a/src/calibre/gui2/store/amazon/amazon_kindle_dialog.py b/src/calibre/gui2/store/amazon/amazon_kindle_dialog.py index 3d1f206eae..108e1cfc40 100644 --- a/src/calibre/gui2/store/amazon/amazon_kindle_dialog.py +++ b/src/calibre/gui2/store/amazon/amazon_kindle_dialog.py @@ -14,10 +14,12 @@ class AmazonKindleDialog(QDialog, Ui_Dialog): ASTORE_URL = 'http://astore.amazon.com/josbl0e-20/' - def __init__(self, parent=None, start_item=None): + def __init__(self, gui, parent=None, start_item=None): QDialog.__init__(self, parent=parent) self.setupUi(self) + self.gui = gui + self.view.loadStarted.connect(self.load_started) self.view.loadProgress.connect(self.load_progress) self.view.loadFinished.connect(self.load_finished) diff --git a/src/calibre/gui2/store/amazon/amazon_plugin.py b/src/calibre/gui2/store/amazon/amazon_plugin.py index 1cbee5a6ee..8c63bdafa5 100644 --- a/src/calibre/gui2/store/amazon/amazon_plugin.py +++ b/src/calibre/gui2/store/amazon/amazon_plugin.py @@ -18,9 +18,9 @@ class AmazonKindleStore(StorePlugin): name = 'Amazon Kindle' description = _('Buy Kindle books from Amazon') - def open(self, parent=None, start_item=None): + def open(self, gui, parent=None, start_item=None): from calibre.gui2.store.amazon.amazon_kindle_dialog import AmazonKindleDialog - d = AmazonKindleDialog(parent, start_item) + d = AmazonKindleDialog(gui, parent, start_item) d = d.exec_() def search(self, query, max_results=10, timeout=60): diff --git a/src/calibre/gui2/store/search.py b/src/calibre/gui2/store/search.py index 443b2a2847..ac6981a25f 100644 --- a/src/calibre/gui2/store/search.py +++ b/src/calibre/gui2/store/search.py @@ -21,9 +21,11 @@ class SearchDialog(QDialog, Ui_Dialog): HANG_TIME = 75000 # milliseconds seconds TIMEOUT = 75 # seconds - def __init__(self, *args): + def __init__(self, gui, *args): QDialog.__init__(self, *args) self.setupUi(self) + + self.gui = gui self.store_plugins = {} self.running_threads = [] @@ -96,7 +98,7 @@ class SearchDialog(QDialog, Ui_Dialog): def open_store(self, index): result = self.results_view.model().get_result(index) - self.store_plugins[result.store].open(self, result.item_data) + self.store_plugins[result.store].open(self.gui, self, result.item_data) class SearchThread(Thread): diff --git a/src/calibre/gui2/store_download.py b/src/calibre/gui2/store_download.py new file mode 100644 index 0000000000..f9460df17b --- /dev/null +++ b/src/calibre/gui2/store_download.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +import cStringIO +import os +import shutil +import time +from contextlib import closing +from threading import Thread +from Queue import Queue + +from calibre import browser +from calibre.ebooks import BOOK_EXTENSIONS +from calibre.gui2 import Dispatcher +from calibre.ptempfile import PersistentTemporaryFile +from calibre.utils.ipc.job import BaseJob + +class StoreDownloadJob(BaseJob): + + def __init__(self, callback, description, job_manager, db, url='', save_as_loc='', add_to_lib=True): + BaseJob.__init__(self, description) + self.exception = None + self.job_manager = job_manager + self.db = db + self.args = (url, save_as_loc, add_to_lib) + self.tmp_file_name = '' + self.callback = callback + self.log_path = None + self._log_file = cStringIO.StringIO() + self._log_file.write(self.description.encode('utf-8') + '\n') + + @property + def log_file(self): + if self.log_path is not None: + return open(self.log_path, 'rb') + return cStringIO.StringIO(self._log_file.getvalue()) + + def start_work(self): + self.start_time = time.time() + self.job_manager.changed_queue.put(self) + + def job_done(self): + self.duration = time.time() - self.start_time + self.percent = 1 + # Dump log onto disk + lf = PersistentTemporaryFile('store_log') + lf.write(self._log_file.getvalue()) + lf.close() + self.log_path = lf.name + self._log_file.close() + self._log_file = None + + self.job_manager.changed_queue.put(self) + + def log_write(self, what): + self._log_file.write(what) + +class StoreDownloader(Thread): + + def __init__(self, job_manager): + Thread.__init__(self) + self.daemon = True + self.jobs = Queue() + self.job_manager = job_manager + self._run = True + + def stop(self): + self._run = False + self.jobs.put(None) + + def run(self): + while self._run: + try: + job = self.jobs.get() + except: + break + if job is None or not self._run: + break + + failed, exc = False, None + job.start_work() + if job.kill_on_start: + job.log_write('Aborted\n') + job.failed = failed + job.killed = True + job.job_done() + continue + + try: + self._download(job) + self._add(job) + self._save_as(job) + break + except Exception, e: + if not self._run: + return + import traceback + failed = True + exc = e + job.log_write('\nSending failed...\n') + job.log_write(traceback.format_exc()) + + if not self._run: + break + + job.failed = failed + job.exception = exc + job.job_done() + try: + job.callback(job) + except: + import traceback + traceback.print_exc() + + def _download(self, job): + url, save_loc, add_to_lib = job.args + if not url: + raise Exception(_('No file specified to download.')) + if not save_loc and not add_to_lib: + # Nothing to do. + return + + br = browser() + + basename = br.geturl(url).split('/')[-1] + ext = os.path.splitext(basename)[1][1:].lower() + if ext not in BOOK_EXTENSIONS: + raise Exception(_('Not a valid ebook format.')) + + tf = PersistentTemporaryFile(suffix=basename) + with closing(br.urlopen(url)) as f: + tf.write(f.read()) + tf.close() + job.tmp_file_name = tf.name + + def _add(self, job): + url, save_loc, add_to_lib = job.args + if not add_to_lib and job.tmp_file_name: + return + + ext = os.path.splitext(job.tmp_file_name)[1:] + + from calibre.ebooks.metadata.meta import get_metadata + with open(job.tmp_file_name) as f: + mi = get_metadata(f, ext) + + job.db.add_books([job.tmp_file_name], [ext], [mi]) + + def _save_as(self, job): + url, save_loc, add_to_lib = job.args + if not save_loc and job.tmp_fie_name: + return + + shutil.copy(job.tmp_fie_name, save_loc) + + def download_from_store(self, callback, db, url='', save_as_loc='', add_to_lib=True): + description = _('Downloading %s') % url + job = StoreDownloadJob(callback, description, job_manager, db, url, save_as_loc, add_to_lib) + self.job_manager.add_job(job) + self.jobs.put(job) + + +class StoreDownloadMixin(object): + + def __init__(self): + self.store_downloader = StoreDownloader(self.job_manager) + self.store_downloader.start() + + def download_from_store(self, url='', save_as_loc='', add_to_lib=True): + self.store_downloader.download_from_store(Dispatcher(self.downloaded_from_store), self.library_view.model().db, url, save_as_loc, add_to_lib) + self.status_bar.show_message(_('Downloading') + ' ' + url, 3000) + + def downloaded_from_store(self, job): + if job.failed: + self.job_exception(job, dialog_title=_('Failed to download book')) + return + + self.status_bar.show_message(job.description + ' ' + _('finished'), 5000) + + diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 9b9308d253..61d8676cfd 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -33,6 +33,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.store_download import StoreDownloadMixin 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, + StoreDownloadMixin ): 'The main GUI'