diff --git a/src/libprs500/__init__.py b/src/libprs500/__init__.py index e5413c8ddc..8fbe6f627e 100644 --- a/src/libprs500/__init__.py +++ b/src/libprs500/__init__.py @@ -13,7 +13,7 @@ ## with this program; if not, write to the Free Software Foundation, Inc., ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ''' E-book management software''' -__version__ = "0.3.94" +__version__ = "0.3.95" __docformat__ = "epytext" __author__ = "Kovid Goyal " __appname__ = 'libprs500' diff --git a/src/libprs500/ebooks/lrf/web/convert_from.py b/src/libprs500/ebooks/lrf/web/convert_from.py index 11690b0e60..59a90f24e3 100644 --- a/src/libprs500/ebooks/lrf/web/convert_from.py +++ b/src/libprs500/ebooks/lrf/web/convert_from.py @@ -111,16 +111,14 @@ def process_profile(args, options, logger=None): profile['finalize'](profile) shutil.rmtree(tdir) - - -def main(args=sys.argv): +def main(args=sys.argv, logger=None): parser = option_parser() options, args = parser.parse_args(args) if len(args) > 2: parser.print_help() return 1 try: - process_profile(args, options) + process_profile(args, options, logger=logger) except CommandLineError, err: print >>sys.stderr, err return 0 diff --git a/src/libprs500/gui2/Makefile b/src/libprs500/gui2/Makefile index 9575097fc9..96620113f5 100644 --- a/src/libprs500/gui2/Makefile +++ b/src/libprs500/gui2/Makefile @@ -1,4 +1,4 @@ -UI = main_ui.py dialogs/metadata_single_ui.py dialogs/metadata_bulk_ui.py dialogs/jobs_ui.py +UI = main_ui.py dialogs/metadata_single_ui.py dialogs/metadata_bulk_ui.py dialogs/jobs_ui.py dialogs/conversion_error_ui.py RC = images_rc.pyc %_ui.py : %.ui diff --git a/src/libprs500/gui2/dialogs/conversion_error.py b/src/libprs500/gui2/dialogs/conversion_error.py new file mode 100644 index 0000000000..325481bf86 --- /dev/null +++ b/src/libprs500/gui2/dialogs/conversion_error.py @@ -0,0 +1,29 @@ +## Copyright (C) 2007 Kovid Goyal kovid@kovidgoyal.net +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License along +## with this program; if not, write to the Free Software Foundation, Inc., +## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +'''''' + +from libprs500.gui2.dialogs import Dialog +from libprs500.gui2.dialogs.conversion_error_ui import Ui_ConversionErrorDialog + +class ConversionErrorDialog(Dialog, Ui_ConversionErrorDialog): + + def __init__(self, window, title, html): + Ui_ConversionErrorDialog.__init__(self) + Dialog.__init__(self, window) + self.setupUi(self.dialog) + html = '' + html + '' + self.dialog.setWindowTitle(title) + self.text.setHtml(html) + self.dialog.show() \ No newline at end of file diff --git a/src/libprs500/gui2/dialogs/conversion_error.ui b/src/libprs500/gui2/dialogs/conversion_error.ui new file mode 100644 index 0000000000..38d215269d --- /dev/null +++ b/src/libprs500/gui2/dialogs/conversion_error.ui @@ -0,0 +1,51 @@ + + ConversionErrorDialog + + + + 0 + 0 + 658 + 515 + + + + ERROR + + + :/library + + + + + + + + + :/images/dialog_error.svg + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + diff --git a/src/libprs500/gui2/images.qrc b/src/libprs500/gui2/images.qrc index e968fb9e64..0d7f268364 100644 --- a/src/libprs500/gui2/images.qrc +++ b/src/libprs500/gui2/images.qrc @@ -1,6 +1,7 @@ images/book.svg + images/news.svg images/clear_left.svg images/dialog_error.svg images/dialog_warning.svg @@ -33,5 +34,8 @@ images/sd.svg images/sync.svg images/trash.svg + images/news/bbc.png + images/news/newsweek.png + images/news/nytimes.png diff --git a/src/libprs500/gui2/images/news.svg b/src/libprs500/gui2/images/news.svg new file mode 100644 index 0000000000..38016738f5 --- /dev/null +++ b/src/libprs500/gui2/images/news.svg @@ -0,0 +1,2826 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Oxygen team + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/libprs500/gui2/images/news/bbc.png b/src/libprs500/gui2/images/news/bbc.png new file mode 100644 index 0000000000..7255a175bf Binary files /dev/null and b/src/libprs500/gui2/images/news/bbc.png differ diff --git a/src/libprs500/gui2/images/news/newsweek.png b/src/libprs500/gui2/images/news/newsweek.png new file mode 100644 index 0000000000..987c946552 Binary files /dev/null and b/src/libprs500/gui2/images/news/newsweek.png differ diff --git a/src/libprs500/gui2/images/news/nytimes.png b/src/libprs500/gui2/images/news/nytimes.png new file mode 100644 index 0000000000..17282300fe Binary files /dev/null and b/src/libprs500/gui2/images/news/nytimes.png differ diff --git a/src/libprs500/gui2/images/save.svg b/src/libprs500/gui2/images/save.svg new file mode 100644 index 0000000000..af62235cfc --- /dev/null +++ b/src/libprs500/gui2/images/save.svg @@ -0,0 +1,1961 @@ + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/libprs500/gui2/jobs.py b/src/libprs500/gui2/jobs.py index d76f8663a0..43073016be 100644 --- a/src/libprs500/gui2/jobs.py +++ b/src/libprs500/gui2/jobs.py @@ -12,7 +12,7 @@ ## You should have received a copy of the GNU General Public License along ## with this program; if not, write to the Free Software Foundation, Inc., ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -import traceback, textwrap +import traceback, textwrap, logging, cStringIO from PyQt4.QtCore import QAbstractTableModel, QMutex, QObject, SIGNAL, Qt, \ QVariant, QThread, QModelIndex @@ -41,12 +41,28 @@ class Job(QThread): self.mutex = mutex self.result = None self.percent_done = 0 + self.logger = logging.getLogger('Job #'+str(id)) + self.logger.setLevel(logging.DEBUG) + self.log_dest = cStringIO.StringIO() + handler = logging.StreamHandler(self.log_dest) + handler.setLevel(logging.DEBUG) + handler.setFormatter(logging.Formatter('[%(levelname)s] %(filename)s:%(lineno)s: %(message)s')) + self.logger.addHandler(handler) + + def progress_update(self, val): + self.percent_done = val + self.emit(SIGNAL('status_update(int, int)'), self.id, int(val)) + +class DeviceJob(Job): + ''' + Jobs that involve communication with the device. Synchronous. + ''' def run(self): if self.mutex != None: self.mutex.lock() last_traceback, exception = None, None - try: + try: try: self.result = self.func(self.progress_update, *self.args, **self.kwargs) except Exception, err: @@ -57,20 +73,24 @@ class Job(QThread): self.mutex.unlock() self.emit(SIGNAL('jobdone(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'), self.id, self.description, self.result, exception, last_traceback) - - def progress_update(self, val): - self.percent_done = val - self.emit(SIGNAL('status_update(int, int)'), self.id, int(val)) - -class DeviceJob(Job): - ''' - Jobs that involve communication with the device. - ''' - def __init__(self, id, description, mutex, func, *args, **kwargs): - Job.__init__(self, id, description, mutex, func, *args, **kwargs) - +class ConversionJob(Job): + ''' Jobs that invlove conversion of content. Asynchronous. ''' + def run(self): + last_traceback, exception = None, None + try: + try: + self.kwargs['logger'] = self.logger + self.result = self.func(*self.args, **self.kwargs) + except Exception, err: + exception = err + last_traceback = traceback.format_exc() + finally: + self.emit(SIGNAL('jobdone(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'), + self.id, self.description, self.result, exception, last_traceback, self.log_dest.getvalue()) + + class JobManager(QAbstractTableModel): @@ -123,6 +143,17 @@ class JobManager(QAbstractTableModel): def has_jobs(self): return len(self.jobs.values()) > 0 + def run_conversion_job(self, slot, callable, *args, **kwargs): + desc = kwargs.pop('job_description', '') + job = self.create_job(ConversionJob, desc, None, callable, *args, **kwargs) + QObject.connect(job, SIGNAL('jobdone(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'), + self.job_done) + if slot: + QObject.connect(job, SIGNAL('jobdone(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'), + slot) + job.start() + return job.id + def run_device_job(self, slot, callable, *args, **kwargs): ''' Run a job to communicate with the device. @@ -213,7 +244,7 @@ class JobManager(QAbstractTableModel): status = 'Done' return QVariant(status) if col == 2: - p = str(job.percent_done) + r'%' + p = str(job.percent_done) + r'%' if job.percent_done > 0 else 'Unavailable' return QVariant(p) if role == Qt.DecorationRole and col == 0: return self.device_job_icon if isinstance(job, DeviceJob) else self.job_icon diff --git a/src/libprs500/gui2/main.py b/src/libprs500/gui2/main.py index fc0b2926a4..c9474ff46a 100644 --- a/src/libprs500/gui2/main.py +++ b/src/libprs500/gui2/main.py @@ -16,11 +16,14 @@ import os, sys, traceback, StringIO, textwrap from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, \ QSettings, QVariant, QSize, QThread -from PyQt4.QtGui import QPixmap, QColor, QPainter, QMenu, QIcon, QMessageBox +from PyQt4.QtGui import QPixmap, QColor, QPainter, QMenu, QIcon, QMessageBox, \ + QToolButton from PyQt4.QtSvg import QSvgRenderer from libprs500 import __version__, __appname__ +from libprs500.ptempfile import PersistentTemporaryFile from libprs500.ebooks.metadata.meta import get_metadata +from libprs500.ebooks.lrf.web.convert_from import main as web2lrf from libprs500.devices.errors import FreeSpaceError from libprs500.devices.interface import Device from libprs500.gui2 import APP_TITLE, warning_dialog, choose_files, error_dialog, \ @@ -33,6 +36,7 @@ from libprs500.gui2.jobs import JobManager from libprs500.gui2.dialogs.metadata_single import MetadataSingleDialog from libprs500.gui2.dialogs.metadata_bulk import MetadataBulkDialog from libprs500.gui2.dialogs.jobs import JobsDialog +from libprs500.gui2.dialogs.conversion_error import ConversionErrorDialog class Main(QObject, Ui_MainWindow): @@ -57,6 +61,8 @@ class Main(QObject, Ui_MainWindow): self.device_manager = None self.upload_memory = {} self.delete_memory = {} + self.conversion_jobs = {} + self.persistent_files = [] self.default_thumbnail = None self.device_error_dialog = error_dialog(self.window, 'Error communicating with device', ' ') self.device_error_dialog.setModal(Qt.NonModal) @@ -98,10 +104,19 @@ class Main(QObject, Ui_MainWindow): QObject.connect(self.action_save, SIGNAL("triggered(bool)"), self.save_to_disk) self.action_sync.setMenu(sm) self.action_edit.setMenu(md) - self.tool_bar.addAction(self.action_sync) - self.tool_bar.addAction(self.action_edit) - self.tool_bar.setContextMenuPolicy(Qt.PreventContextMenu) - + nm = QMenu() + nm.addAction(QIcon(':/images/news/bbc.png'), 'BBC') + nm.addAction(QIcon(':/images/news/newsweek.png'), 'Newsweek') + nm.addAction(QIcon(':/images/news/nytimes.png'), 'New York Times') + QObject.connect(nm.actions()[0], SIGNAL('triggered(bool)'), self.fetch_news_bbc) + QObject.connect(nm.actions()[1], SIGNAL('triggered(bool)'), self.fetch_news_newsweek) + QObject.connect(nm.actions()[2], SIGNAL('triggered(bool)'), self.fetch_news_nytimes) + self.news_menu = nm + self.action_news.setMenu(nm) + self.tool_bar.widgetForAction(self.action_news).setPopupMode(QToolButton.InstantPopup) + self.tool_bar.widgetForAction(self.action_edit).setPopupMode(QToolButton.MenuButtonPopup) + self.tool_bar.widgetForAction(self.action_sync).setPopupMode(QToolButton.MenuButtonPopup) + self.tool_bar.setContextMenuPolicy(Qt.PreventContextMenu) ####################### Library view ######################## self.library_view.set_database(self.database_path) for func, target in [ @@ -231,10 +246,16 @@ class Main(QObject, Ui_MainWindow): filters=[('Books', BOOK_EXTENSIONS)]) if not books: return + to_device = self.stack.currentIndex() != 0 + self._add_books(books, to_device) + if to_device: + self.status_bar.showMessage('Uploading books to device.', 2000) + + def _add_books(self, paths, to_device): on_card = False if self.stack.currentIndex() != 2 else True # Get format and metadata information formats, metadata, names, infos = [], [], [], [] - for book in books: + for book in paths: format = os.path.splitext(book)[1] format = format[1:] if format else None stream = open(book, 'rb') @@ -244,16 +265,16 @@ class Main(QObject, Ui_MainWindow): formats.append(format) metadata.append(mi) names.append(os.path.basename(book)) - infos.append({'title':mi.title, 'authors':mi.author, 'cover':self.default_thumbnail}) + infos.append({'title':mi.title, 'authors':mi.author, + 'cover':self.default_thumbnail, 'tags':[]}) - if self.stack.currentIndex() == 0: + if not to_device: model = self.current_view().model() - model.add_books(books, formats, metadata) + model.add_books(paths, formats, metadata) model.resort() model.research() else: - self.upload_books(books, names, infos, on_card=on_card) - self.status_bar.showMessage('Adding books to device.', 2000) + self.upload_books(paths, names, infos, on_card=on_card) def upload_books(self, files, names, metadata, on_card=False): ''' @@ -446,6 +467,39 @@ class Main(QObject, Ui_MainWindow): self.device_job_exception(id, description, exception, formatted_traceback) return + ############################################################################ + + ############################### Fetch news ################################# + + def fetch_news(self, profile, pretty): + pt = PersistentTemporaryFile(suffix='.lrf') + pt.close() + args = ['web2lrf', '-o', pt.name, profile] + id = self.job_manager.run_conversion_job(self.news_fetched, web2lrf, args=args, + job_description='Fetch news from '+pretty) + self.conversion_jobs[id] = (pt, 'lrf') + self.status_bar.showMessage('Fetching news from '+pretty, 2000) + + def news_fetched(self, id, description, result, exception, formatted_traceback, log): + pt, fmt = self.conversion_jobs.pop(id) + if exception: + self.conversion_job_exception(id, description, exception, formatted_traceback, log) + return + to_device = self.device_connected and fmt in self.device_manager.device_class.FORMATS + self._add_books([pt.name], to_device) + if to_device: + self.status_bar.showMessage('News fetched. Uploading to device.', 2000) + self.persistent_files.append(pt) + + def fetch_news_bbc(self, checked): + self.fetch_news('bbc', 'BBC') + + def fetch_news_newsweek(self, checked): + self.fetch_news('newsweek', 'Newsweek') + + def fetch_news_nytimes(self, checked): + self.fetch_news('nytimes', 'New York Times') + ############################################################################ def location_selected(self, location): ''' @@ -491,7 +545,19 @@ class Main(QObject, Ui_MainWindow): msg += self.wrap_traceback(formatted_traceback) self.device_error_dialog.setText(msg) self.device_error_dialog.show() - + + def conversion_job_exception(self, id, description, exception, formatted_traceback, log): + print >>sys.stderr, 'Error in job:', description + print >>sys.stderr, log + print >>sys.stderr, exception + print >>sys.stderr, formatted_traceback + msg = u'

%s: '%(exception.__class__.__name__,) + unicode(str(exception), 'utf8', 'replace') + u'

' + msg += u'

Failed to perform job: '+description + msg += u'

Detailed traceback:

'
+        msg += formatted_traceback + '
' + msg += '

Log:

'
+        msg += log
+        ConversionErrorDialog(self.window, 'Conversion Error', msg)
         
     
     def read_settings(self):
diff --git a/src/libprs500/gui2/main.ui b/src/libprs500/gui2/main.ui
index afc08087f2..39780763fe 100644
--- a/src/libprs500/gui2/main.ui
+++ b/src/libprs500/gui2/main.ui
@@ -319,7 +319,12 @@
    
    
    
+   
+   
+   
    
+   
+   
   
   
    
@@ -387,6 +392,14 @@
     Save to disk
    
   
+  
+   
+    :/images/news.svg
+   
+   
+    Fetch news
+   
+