diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 97744db234..1c990f7e8b 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -12,6 +12,7 @@ from PyQt4.Qt import QMenu, QAction, QActionGroup, QIcon, SIGNAL, QPixmap, \ from calibre.customize.ui import available_input_formats, available_output_formats, \ device_plugins +from calibre.devices.interface import DevicePlugin from calibre.constants import iswindows from calibre.gui2.dialogs.choose_format import ChooseFormatDialog from calibre.parallel import Job @@ -27,6 +28,11 @@ from calibre.devices.errors import FreeSpaceError from calibre.utils.smtp import compose_mail, sendmail, extract_email_address, \ config as email_config +def warning(title, msg, details, parent): + from calibre.gui2.widgets import WarningDialog + WarningDialog(title, msg, details, parent).exec_() + + class DeviceJob(Job): def __init__(self, func, *args, **kwargs): @@ -541,7 +547,7 @@ class DeviceGUI(object): p.loadFromData(data) if not p.isNull(): ht = self.device_manager.device_class.THUMBNAIL_HEIGHT \ - if self.device_manager else Device.THUMBNAIL_HEIGHT + if self.device_manager else DevicePlugin.THUMBNAIL_HEIGHT p = p.scaledToHeight(ht, Qt.SmoothTransformation) return (p.width(), p.height(), pixmap_to_data(p)) @@ -616,7 +622,7 @@ class DeviceGUI(object): ids = [self.library_view.model().id(r) for r in self.library_view.selectionModel().selectedRows()] if send_ids is None else send_ids if not self.device_manager or not ids or len(ids) == 0: return - + _files, _auto_ids = self.library_view.model().get_preferred_formats_from_ids(ids, self.device_manager.device_class.settings().format_map, paths=True, set_metadata=True, @@ -626,7 +632,7 @@ class DeviceGUI(object): ids = list(set(ids).difference(_auto_ids)) else: _auto_ids = [] - + metadata = self.library_view.model().get_metadata(ids, True) ids = iter(ids) for mi in metadata: @@ -634,7 +640,7 @@ class DeviceGUI(object): if cdata: mi['cover'] = self.cover_to_thumbnail(cdata) metadata = iter(metadata) - + files = [getattr(f, 'name', None) for f in _files] bad, good, gf, names, remove_ids = [], [], [], [], [] for f in files: @@ -660,13 +666,13 @@ class DeviceGUI(object): remove = remove_ids if delete_from_library else [] self.upload_books(gf, names, good, on_card, memory=(_files, remove)) self.status_bar.showMessage(_('Sending books to device.'), 5000) - + auto = [] if _auto_ids != []: for id in _auto_ids: if specific_format == None: formats = [f.lower() for f in self.library_view.model().db.formats(id, index_is_id=True).split(',')] - formats = formats if formats != None else [] + formats = formats if formats != None else [] if list(set(formats).intersection(available_input_formats())) != [] and list(set(self.device_manager.device_class.settings().format_map).intersection(available_output_formats())) != []: auto.append(id) else: @@ -676,7 +682,7 @@ class DeviceGUI(object): auto.append(id) else: bad.append(self.library_view.model().db.title(id, index_is_id=True)) - + if auto != []: format = None for fmt in self.device_manager.device_class.settings().format_map: diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index 96a3444ec4..97e4c24d53 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -24,6 +24,7 @@ from calibre.gui2 import APP_UID, warning_dialog, choose_files, error_dialog, \ max_available_height, config, info_dialog, \ available_width, GetMetadata from calibre.gui2.cover_flow import CoverFlow, DatabaseImages, pictureflowerror +from calibre.gui2.widgets import ProgressIndicator, WarningDialog from calibre.gui2.dialogs.scheduler import Scheduler from calibre.gui2.update import CheckForUpdates from calibre.gui2.dialogs.progress import ProgressDialog @@ -74,6 +75,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): Ui_MainWindow.__init__(self) self.setupUi(self) self.setWindowTitle(__appname__) + self.progress_indicator = ProgressIndicator(self) self.verbose = opts.verbose self.get_metadata = GetMetadata() self.read_settings() @@ -169,6 +171,9 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): md.addAction(_('Edit metadata individually')) md.addSeparator() md.addAction(_('Edit metadata in bulk')) + md.addSeparator() + md.addAction(_('Download metadata and covers')) + md.addAction(_('Download only metadata')) self.metadata_menu = md self.add_menu = QMenu() self.add_menu.addAction(_('Add books from a single directory')) @@ -195,6 +200,12 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): partial(self.edit_metadata, bulk=False)) QObject.connect(md.actions()[2], SIGNAL('triggered(bool)'), partial(self.edit_metadata, bulk=True)) + QObject.connect(md.actions()[4], SIGNAL('triggered(bool)'), + partial(self.download_metadata, covers=True)) + QObject.connect(md.actions()[5], SIGNAL('triggered(bool)'), + partial(self.download_metadata, covers=False)) + + self.save_menu = QMenu() self.save_menu.addAction(_('Save to disk')) self.save_menu.addAction(_('Save to disk in a single directory')) @@ -821,6 +832,54 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): ############################################################################ ############################### Edit metadata ############################## + + def download_metadata(self, checked, covers=True): + rows = self.library_view.selectionModel().selectedRows() + previous = self.library_view.currentIndex() + if not rows or len(rows) == 0: + d = error_dialog(self, _('Cannot download metadata'), + _('No books selected')) + d.exec_() + return + db = self.library_view.model().db + ids = [db.id(row.row()) for row in rows] + from calibre.gui2.metadata import DownloadMetadata + self._download_book_metadata = DownloadMetadata(db, ids, get_covers=covers) + self._download_book_metadata.start() + self.progress_indicator.start( + _('Downloading metadata for %d book(s)')%len(ids)) + self._book_metadata_download_check = QTimer(self) + self.connect(self._book_metadata_download_check, + SIGNAL('timeout()'), self.book_metadata_download_check) + self._book_metadata_download_check.start(100) + + def book_metadata_download_check(self): + if self._download_book_metadata.is_alive(): + return + self._book_metadata_download_check.stop() + self.progress_indicator.stop() + cr = self.library_view.currentIndex().row() + x = self._download_book_metadata + self._download_book_metadata = None + if x.exception is None: + db = self.library_view.model().refresh_ids( + x.updated, cr) + if x.failures: + details = ['
  • %s: %s
  • '%(title, reason) for title, + reason in x.failures.values()] + details = '

    '%(''.join(details)) + WarningDialog(_('Failed to download some metadata'), + _('Failed to download metadata for the following:'), + details, self).exec_() + else: + err = _('Failed to download metadata:')+\ + '
    '+x.tb+'
    ' + ConversionErrorDialog(self, _('Error'), err, + show=True) + + + + def edit_metadata(self, checked, bulk=None): ''' Edit metadata of selected books in library. @@ -1081,8 +1140,9 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): #############################View book###################################### def view_format(self, row, format): - self._view_file(self.library_view.model().db.format(row, - format, as_file=True).name) + fmt_path = self.library_view.model().db.format_abspath(row, format) + if fmt_path: + self._view_file(fmt_path) def book_downloaded_for_viewing(self, job): if job.exception: diff --git a/src/calibre/gui2/metadata.py b/src/calibre/gui2/metadata.py new file mode 100644 index 0000000000..607fd6a41b --- /dev/null +++ b/src/calibre/gui2/metadata.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import with_statement + +__license__ = 'GPL v3' +__copyright__ = '2009, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +from threading import Thread +from Queue import Queue, Empty + + +from calibre.ebooks.metadata.fetch import search +from calibre.utils.config import prefs +from calibre.ebooks.metadata.library_thing import cover_from_isbn + +class Worker(Thread): + + def __init__(self): + Thread.__init__(self) + self.setDaemon(True) + self.jobs = Queue() + self.results = Queue() + + def run(self): + while True: + isbn = self.jobs.get() + if not isbn: + break + cdata, _ = cover_from_isbn(isbn) + if cdata: + self.results.put((isbn, cdata)) + + def __enter__(self): + self.start() + return self + + def __exit__(self, *args): + self.jobs.put(False) + + +class DownloadMetadata(Thread): + + def __init__(self, db, ids, get_covers): + Thread.__init__(self) + self.setDaemon(True) + self.metadata = {} + self.covers = {} + self.db = db + self.updated = set([]) + self.get_covers = get_covers + self.worker = Worker() + for id in ids: + self.metadata[id] = db.get_metadata(id, index_is_id=True) + + def run(self): + self.exception = self.tb = None + try: + self._run() + except Exception, e: + self.exception = e + import traceback + self.tb = traceback.format_exc() + + def _run(self): + self.key = prefs['isbndb_com_key'] + if not self.key: + self.key = None + self.fetched_metadata = {} + self.failures = {} + with self.worker: + for id, mi in self.metadata.items(): + args = {} + if mi.isbn: + args['isbn'] = mi.isbn + else: + if not mi.title: + self.failures[id] = \ + (str(id), _('Book has neither title nor ISBN')) + continue + args['title'] = mi.title + if mi.authors: + args['author'] = mi.authors[0] + if self.key: + args['isbndb_key'] = self.key + results, exceptions = search(**args) + if results: + fmi = results[0] + self.fetched_metadata[id] = fmi + if fmi.isbn and self.get_covers: + self.worker.jobs.put(fmi.isbn) + mi.smart_update(fmi) + else: + self.failures[id] = (mi.title, + _('No matches found for this book')) + self.commit_covers() + + self.commit_covers(True) + for id in self.fetched_metadata: + self.db.set_metadata(id, self.metadata[id]) + self.updated = set(self.fetched_metadata) + + + def commit_covers(self, all=False): + if all: + self.worker.jobs.put(False) + while True: + try: + isbn, cdata = self.worker.results.get(False) + for id, mi in self.metadata.items(): + if mi.isbn == isbn: + self.db.set_cover(id, cdata) + except Empty: + if not all or not self.worker.is_alive(): + return + +