From 0d55d5127032c108795adb17886d1d06d279cf5b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 12 Jun 2010 07:46:25 -0600 Subject: [PATCH 01/13] ... --- resources/recipes/libero.recipe | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/recipes/libero.recipe b/resources/recipes/libero.recipe index 4354940746..f2208d01a3 100644 --- a/resources/recipes/libero.recipe +++ b/resources/recipes/libero.recipe @@ -11,11 +11,11 @@ http://www.libero-news.it/ from calibre.web.feeds.news import BasicNewsRecipe class LiberoNews(BasicNewsRecipe): - __author__ = 'Marini Gabriele' - description = 'Italian daily newspaper' + __author__ = 'Marini Gabriele' + description = 'Italian daily newspaper' - cover_url = 'http://www.ilgiornale.it/img_v1/logo.gif' - title = u'Libero' + cover_url = 'http://www.libero-news.it/images/logo.png' + title = u'Libero ' publisher = 'EDITORIALE LIBERO s.r.l 2006' category = 'News, politics, culture, economy, general interest' From ff6a9c7aacf5f3d57d658e561db3b3ac88f0543d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 12 Jun 2010 08:06:58 -0600 Subject: [PATCH 02/13] Fix #5805 (Calibre 0.7.2 - multiple edit metadata individually shows edit in bulk screen) --- src/calibre/gui2/init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py index 5f42636dad..90afbbf0a2 100644 --- a/src/calibre/gui2/init.py +++ b/src/calibre/gui2/init.py @@ -47,7 +47,7 @@ class ToolbarMixin(object): # {{{ def __init__(self): md = QMenu() md.addAction(_('Edit metadata individually'), - partial(self.edit_metadata, False)) + partial(self.edit_metadata, False, bulk=False)) md.addSeparator() md.addAction(_('Edit metadata in bulk'), partial(self.edit_metadata, False, bulk=True)) From dc33b9655421aaa99f386efe12d12bd844859c3b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 12 Jun 2010 09:03:02 -0600 Subject: [PATCH 03/13] More gui2.ui refactoring --- src/calibre/gui2/device.py | 196 +++++++++++++++++++++++++++++- src/calibre/gui2/ui.py | 243 +------------------------------------ src/calibre/gui2/update.py | 37 +++++- 3 files changed, 233 insertions(+), 243 deletions(-) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 9d7cf86dac..bae0316e14 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -3,7 +3,7 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' # Imports {{{ -import os, traceback, Queue, time, socket, cStringIO, re +import os, traceback, Queue, time, socket, cStringIO, re, sys from threading import Thread, RLock from itertools import repeat from functools import partial @@ -15,12 +15,13 @@ 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.devices.errors import UserFeedback from calibre.gui2.dialogs.choose_format import ChooseFormatDialog from calibre.utils.ipc.job import BaseJob from calibre.devices.scanner import DeviceScanner from calibre.gui2 import config, error_dialog, Dispatcher, dynamic, \ - pixmap_to_data, warning_dialog, \ - question_dialog + pixmap_to_data, warning_dialog, \ + question_dialog, info_dialog, choose_dir from calibre.ebooks.metadata import authors_to_string, authors_to_sort_string from calibre import preferred_encoding, prints from calibre.utils.filenames import ascii_filename @@ -597,10 +598,194 @@ class Emailer(Thread): # {{{ # }}} -class DeviceMixin(object): +class DeviceMixin(object): # {{{ def __init__(self): self.db_book_uuid_cache = set() + self.device_error_dialog = error_dialog(self, _('Error'), + _('Error communicating with device'), ' ') + self.device_error_dialog.setModal(Qt.NonModal) + self.device_connected = None + self.emailer = Emailer() + self.emailer.start() + self.device_manager = DeviceManager(Dispatcher(self.device_detected), + self.job_manager, Dispatcher(self.status_bar.show_message)) + self.device_manager.start() + + def connect_to_folder(self): + dir = choose_dir(self, 'Select Device Folder', + _('Select folder to open as device')) + if dir is not None: + self.device_manager.connect_to_folder(dir) + + def disconnect_from_folder(self): + self.device_manager.disconnect_folder() + + def _sync_action_triggered(self, *args): + m = getattr(self, '_sync_menu', None) + if m is not None: + m.trigger_default() + + def create_device_menu(self): + self._sync_menu = DeviceMenu(self) + self.action_sync.setMenu(self._sync_menu) + self.connect(self._sync_menu, + SIGNAL('sync(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'), + self.dispatch_sync_event) + self._sync_menu.fetch_annotations.connect(self.fetch_annotations) + self._sync_menu.connect_to_folder.connect(self.connect_to_folder) + self._sync_menu.disconnect_from_folder.connect(self.disconnect_from_folder) + if self.device_connected: + self._sync_menu.connect_to_folder_action.setEnabled(False) + if self.device_connected == 'folder': + self._sync_menu.disconnect_from_folder_action.setEnabled(True) + else: + self._sync_menu.disconnect_from_folder_action.setEnabled(False) + else: + self._sync_menu.connect_to_folder_action.setEnabled(True) + self._sync_menu.disconnect_from_folder_action.setEnabled(False) + + + + def device_job_exception(self, job): + ''' + Handle exceptions in threaded device jobs. + ''' + if isinstance(getattr(job, 'exception', None), UserFeedback): + ex = job.exception + func = {UserFeedback.ERROR:error_dialog, + UserFeedback.WARNING:warning_dialog, + UserFeedback.INFO:info_dialog}[ex.level] + return func(self, _('Failed'), ex.msg, det_msg=ex.details if + ex.details else '', show=True) + + try: + if 'Could not read 32 bytes on the control bus.' in \ + unicode(job.details): + error_dialog(self, _('Error talking to device'), + _('There was a temporary error talking to the ' + 'device. Please unplug and reconnect the device ' + 'and or reboot.')).show() + return + except: + pass + try: + prints(job.details, file=sys.stderr) + except: + pass + if not self.device_error_dialog.isVisible(): + self.device_error_dialog.setDetailedText(job.details) + self.device_error_dialog.show() + + # Device connected {{{ + def device_detected(self, connected, is_folder_device): + ''' + Called when a device is connected to the computer. + ''' + if connected: + self._sync_menu.connect_to_folder_action.setEnabled(False) + if is_folder_device: + self._sync_menu.disconnect_from_folder_action.setEnabled(True) + self.device_manager.get_device_information(\ + Dispatcher(self.info_read)) + self.set_default_thumbnail(\ + self.device_manager.device.THUMBNAIL_HEIGHT) + self.status_bar.show_message(_('Device: ')+\ + self.device_manager.device.__class__.get_gui_name()+\ + _(' detected.'), 3000) + self.device_connected = 'device' if not is_folder_device else 'folder' + self._sync_menu.enable_device_actions(True, + self.device_manager.device.card_prefix(), + self.device_manager.device) + self.location_view.model().device_connected(self.device_manager.device) + self.eject_action.setEnabled(True) + self.refresh_ondevice_info (device_connected = True, reset_only = True) + else: + self._sync_menu.connect_to_folder_action.setEnabled(True) + self._sync_menu.disconnect_from_folder_action.setEnabled(False) + self.device_connected = None + self._sync_menu.enable_device_actions(False) + self.location_view.model().update_devices() + self.vanity.setText(self.vanity_template%\ + dict(version=self.latest_version, device=' ')) + self.device_info = ' ' + if self.current_view() != self.library_view: + self.book_details.reset_info() + self.location_view.setCurrentIndex(self.location_view.model().index(0)) + self.eject_action.setEnabled(False) + self.refresh_ondevice_info (device_connected = False) + + def info_read(self, job): + ''' + Called once device information has been read. + ''' + if job.failed: + return self.device_job_exception(job) + info, cp, fs = job.result + self.location_view.model().update_devices(cp, fs) + self.device_info = _('Connected ')+info[0] + self.vanity.setText(self.vanity_template%\ + dict(version=self.latest_version, device=self.device_info)) + + self.device_manager.books(Dispatcher(self.metadata_downloaded)) + + def metadata_downloaded(self, job): + ''' + Called once metadata has been read for all books on the device. + ''' + if job.failed: + self.device_job_exception(job) + return + self.set_books_in_library(job.result, reset=True) + mainlist, cardalist, cardblist = job.result + self.memory_view.set_database(mainlist) + self.memory_view.set_editable(self.device_manager.device.CAN_SET_METADATA) + self.card_a_view.set_database(cardalist) + self.card_a_view.set_editable(self.device_manager.device.CAN_SET_METADATA) + self.card_b_view.set_database(cardblist) + self.card_b_view.set_editable(self.device_manager.device.CAN_SET_METADATA) + self.sync_news() + self.sync_catalogs() + self.refresh_ondevice_info(device_connected = True) + + def refresh_ondevice_info(self, device_connected, reset_only = False): + ''' + Force the library view to refresh, taking into consideration + books information + ''' + self.book_on_device(None, reset=True) + if reset_only: + return + self.library_view.set_device_connected(device_connected) + + # }}} + + def remove_paths(self, paths): + return self.device_manager.delete_books( + Dispatcher(self.books_deleted), paths) + + def books_deleted(self, job): + ''' + Called once deletion is done on the device + ''' + for view in (self.memory_view, self.card_a_view, self.card_b_view): + view.model().deletion_done(job, job.failed) + if job.failed: + self.device_job_exception(job) + return + + if self.delete_memory.has_key(job): + paths, model = self.delete_memory.pop(job) + self.device_manager.remove_books_from_metadata(paths, + self.booklists()) + model.paths_deleted(paths) + self.upload_booklists() + # Clear the ondevice info so it will be recomputed + self.book_on_device(None, None, reset=True) + # We want to reset all the ondevice flags in the library. Use a big + # hammer, so we don't need to worry about whether some succeeded or not + self.library_view.model().refresh() + def dispatch_sync_event(self, dest, delete, specific): rows = self.library_view.selectionModel().selectedRows() @@ -1220,3 +1405,6 @@ class DeviceMixin(object): # Correct the metadata cache on device. if self.device_manager.is_device_connected: self.device_manager.sync_booklists(None, booklists) + + # }}} + diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 5824171213..795e79a7ee 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -10,7 +10,6 @@ __docformat__ = 'restructuredtext en' '''The main GUI''' import collections, datetime, os, shutil, sys, textwrap, time -from xml.parsers.expat import ExpatError from Queue import Queue, Empty from threading import Thread from functools import partial @@ -24,12 +23,11 @@ from PyQt4.QtSvg import QSvgRenderer from calibre import prints, patheq, strftime from calibre.constants import __version__, __appname__, isfrozen, islinux, \ - iswindows, isosx, filesystem_encoding, preferred_encoding + isosx, filesystem_encoding, preferred_encoding from calibre.utils.filenames import ascii_filename from calibre.ptempfile import PersistentTemporaryFile from calibre.utils.config import prefs, dynamic from calibre.utils.ipc.server import Server -from calibre.devices.errors import UserFeedback from calibre.gui2 import warning_dialog, choose_files, error_dialog, \ question_dialog,\ pixmap_to_data, choose_dir, \ @@ -40,10 +38,10 @@ from calibre.gui2.cover_flow import CoverFlowMixin from calibre.gui2.widgets import ProgressIndicator, IMAGE_EXTENSIONS from calibre.gui2.wizard import move_library from calibre.gui2.dialogs.scheduler import Scheduler -from calibre.gui2.update import CheckForUpdates +from calibre.gui2.update import UpdateMixin from calibre.gui2.main_window import MainWindow from calibre.gui2.main_ui import Ui_MainWindow -from calibre.gui2.device import DeviceManager, DeviceMenu, DeviceMixin, Emailer +from calibre.gui2.device import DeviceMixin from calibre.gui2.jobs import JobManager, JobsDialog, JobsButton from calibre.gui2.dialogs.metadata_single import MetadataSingleDialog from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog @@ -106,7 +104,7 @@ class SystemTrayIcon(QSystemTrayIcon): # {{{ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin, - SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin): + SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin): 'The main GUI' def set_default_thumbnail(self, height): @@ -152,6 +150,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # }}} LayoutMixin.__init__(self) + DeviceMixin.__init__(self) self.restriction_count_of_books_in_view = 0 self.restriction_count_of_books_in_library = 0 @@ -160,19 +159,13 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, self.progress_indicator = ProgressIndicator(self) self.verbose = opts.verbose self.get_metadata = GetMetadata() - self.emailer = Emailer() - self.emailer.start() self.upload_memory = {} self.delete_memory = {} self.conversion_jobs = {} self.persistent_files = [] self.metadata_dialogs = [] self.default_thumbnail = None - self.device_error_dialog = error_dialog(self, _('Error'), - _('Error communicating with device'), ' ') - self.device_error_dialog.setModal(Qt.NonModal) self.tb_wrapper = textwrap.TextWrapper(width=40) - self.device_connected = None self.viewers = collections.deque() self.content_server = None self.system_tray_icon = SystemTrayIcon(QIcon(I('library.png')), self) @@ -216,17 +209,10 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, SIGNAL('activated(QSystemTrayIcon::ActivationReason)'), self.system_tray_icon_activated) - DeviceMixin.__init__(self) ####################### Start spare job server ######################## QTimer.singleShot(1000, self.add_spare_server) - ####################### Setup device detection ######################## - self.device_manager = DeviceManager(Dispatcher(self.device_detected), - self.job_manager, Dispatcher(self.status_bar.show_message)) - self.device_manager.start() - - ####################### Location View ######################## QObject.connect(self.location_view, SIGNAL('location_selected(PyQt_PyObject)'), @@ -248,12 +234,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, self.latest_version = ' ' self.vanity.setText(self.vanity_template%dict(version=' ', device=' ')) self.device_info = ' ' - if not opts.no_update_check: - self.update_checker = CheckForUpdates(self) - self.update_checker.update_found.connect(self.update_found, - type=Qt.QueuedConnection) - self.update_checker.start() - + UpdateMixin.__init__(self, opts) ####################### Status Bar ##################### self.status_bar.initialize(self.system_tray_icon) self.book_details.show_book_info.connect(self.show_book_info) @@ -342,39 +323,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, MainWindow.resizeEvent(self, ev) self.search.setMaximumWidth(self.width()-150) - def connect_to_folder(self): - dir = choose_dir(self, 'Select Device Folder', - _('Select folder to open as device')) - if dir is not None: - self.device_manager.connect_to_folder(dir) - - def disconnect_from_folder(self): - self.device_manager.disconnect_folder() - - def _sync_action_triggered(self, *args): - m = getattr(self, '_sync_menu', None) - if m is not None: - m.trigger_default() - - def create_device_menu(self): - self._sync_menu = DeviceMenu(self) - self.action_sync.setMenu(self._sync_menu) - self.connect(self._sync_menu, - SIGNAL('sync(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'), - self.dispatch_sync_event) - self._sync_menu.fetch_annotations.connect(self.fetch_annotations) - self._sync_menu.connect_to_folder.connect(self.connect_to_folder) - self._sync_menu.disconnect_from_folder.connect(self.disconnect_from_folder) - if self.device_connected: - self._sync_menu.connect_to_folder_action.setEnabled(False) - if self.device_connected == 'folder': - self._sync_menu.disconnect_from_folder_action.setEnabled(True) - else: - self._sync_menu.disconnect_from_folder_action.setEnabled(False) - else: - self._sync_menu.connect_to_folder_action.setEnabled(True) - self._sync_menu.disconnect_from_folder_action.setEnabled(False) - def add_spare_server(self, *args): self.spare_servers.append(Server(limit=int(config['worker_limit']/2.0))) @@ -457,108 +405,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, def booklists(self): return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db - ########################## Connect to device ############################## - - def save_device_view_settings(self): - model = self.location_view.model() - return - #self.memory_view.write_settings() - for x in range(model.rowCount()): - if x > 1: - if model.location_for_row(x) == 'carda': - self.card_a_view.write_settings() - elif model.location_for_row(x) == 'cardb': - self.card_b_view.write_settings() - - def device_detected(self, connected, is_folder_device): - ''' - Called when a device is connected to the computer. - ''' - if connected: - self._sync_menu.connect_to_folder_action.setEnabled(False) - if is_folder_device: - self._sync_menu.disconnect_from_folder_action.setEnabled(True) - self.device_manager.get_device_information(\ - Dispatcher(self.info_read)) - self.set_default_thumbnail(\ - self.device_manager.device.THUMBNAIL_HEIGHT) - self.status_bar.show_message(_('Device: ')+\ - self.device_manager.device.__class__.get_gui_name()+\ - _(' detected.'), 3000) - self.device_connected = 'device' if not is_folder_device else 'folder' - self._sync_menu.enable_device_actions(True, - self.device_manager.device.card_prefix(), - self.device_manager.device) - self.location_view.model().device_connected(self.device_manager.device) - self.eject_action.setEnabled(True) - self.refresh_ondevice_info (device_connected = True, reset_only = True) - else: - self._sync_menu.connect_to_folder_action.setEnabled(True) - self._sync_menu.disconnect_from_folder_action.setEnabled(False) - self.save_device_view_settings() - self.device_connected = None - self._sync_menu.enable_device_actions(False) - self.location_view.model().update_devices() - self.vanity.setText(self.vanity_template%\ - dict(version=self.latest_version, device=' ')) - self.device_info = ' ' - if self.current_view() != self.library_view: - self.book_details.reset_info() - self.location_view.setCurrentIndex(self.location_view.model().index(0)) - self.eject_action.setEnabled(False) - self.refresh_ondevice_info (device_connected = False) - - def info_read(self, job): - ''' - Called once device information has been read. - ''' - if job.failed: - return self.device_job_exception(job) - info, cp, fs = job.result - self.location_view.model().update_devices(cp, fs) - self.device_info = _('Connected ')+info[0] - self.vanity.setText(self.vanity_template%\ - dict(version=self.latest_version, device=self.device_info)) - - self.device_manager.books(Dispatcher(self.metadata_downloaded)) - - def metadata_downloaded(self, job): - ''' - Called once metadata has been read for all books on the device. - ''' - if job.failed: - if isinstance(job.exception, ExpatError): - error_dialog(self, _('Device database corrupted'), - _(''' -

The database of books on the reader is corrupted. Try the following: -

    -
  1. Unplug the reader. Wait for it to finish regenerating the database (i.e. wait till it is ready to be used). Plug it back in. Now it should work with %(app)s. If not try the next step.
  2. -
  3. Quit %(app)s. Find the file media.xml in the reader's main memory. Delete it. Unplug the reader. Wait for it to regenerate the file. Re-connect it and start %(app)s.
  4. -
- ''')%dict(app=__appname__)).exec_() - else: - self.device_job_exception(job) - return - self.set_books_in_library(job.result, reset=True) - mainlist, cardalist, cardblist = job.result - self.memory_view.set_database(mainlist) - self.memory_view.set_editable(self.device_manager.device.CAN_SET_METADATA) - self.card_a_view.set_database(cardalist) - self.card_a_view.set_editable(self.device_manager.device.CAN_SET_METADATA) - self.card_b_view.set_database(cardblist) - self.card_b_view.set_editable(self.device_manager.device.CAN_SET_METADATA) - self.sync_news() - self.sync_catalogs() - self.refresh_ondevice_info(device_connected = True) - - ############################################################################ - ### Force the library view to refresh, taking into consideration books information - def refresh_ondevice_info(self, device_connected, reset_only = False): - self.book_on_device(None, reset=True) - if reset_only: - return - self.library_view.set_device_connected(device_connected) - ############################################################################ ######################### Fetch annotations ################################ @@ -1060,31 +906,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, view.model().mark_for_deletion(job, rows) self.status_bar.show_message(_('Deleting books from device.'), 1000) - def remove_paths(self, paths): - return self.device_manager.delete_books(\ - Dispatcher(self.books_deleted), paths) - - def books_deleted(self, job): - ''' - Called once deletion is done on the device - ''' - for view in (self.memory_view, self.card_a_view, self.card_b_view): - view.model().deletion_done(job, job.failed) - if job.failed: - self.device_job_exception(job) - return - - if self.delete_memory.has_key(job): - paths, model = self.delete_memory.pop(job) - self.device_manager.remove_books_from_metadata(paths, - self.booklists()) - model.paths_deleted(paths) - self.upload_booklists() - # Clear the ondevice info so it will be recomputed - self.book_on_device(None, None, reset=True) - # We want to reset all the ondevice flags in the library. Use a big - # hammer, so we don't need to worry about whether some succeeded or not - self.library_view.model().refresh() ############################################################################ @@ -1858,35 +1679,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, self.set_number_of_books_shown() - def device_job_exception(self, job): - ''' - Handle exceptions in threaded device jobs. - ''' - if isinstance(getattr(job, 'exception', None), UserFeedback): - ex = job.exception - func = {UserFeedback.ERROR:error_dialog, - UserFeedback.WARNING:warning_dialog, - UserFeedback.INFO:info_dialog}[ex.level] - return func(self, _('Failed'), ex.msg, det_msg=ex.details if - ex.details else '', show=True) - - try: - if 'Could not read 32 bytes on the control bus.' in \ - unicode(job.details): - error_dialog(self, _('Error talking to device'), - _('There was a temporary error talking to the ' - 'device. Please unplug and reconnect the device ' - 'and or reboot.')).show() - return - except: - pass - try: - prints(job.details, file=sys.stderr) - except: - pass - if not self.device_error_dialog.isVisible(): - self.device_error_dialog.setDetailedText(job.details) - self.device_error_dialog.show() def job_exception(self, job): if not hasattr(self, '_modeless_dialogs'): @@ -2066,26 +1858,3 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, else: e.ignore() - def update_found(self, version): - os = 'windows' if iswindows else 'osx' if isosx else 'linux' - url = 'http://calibre-ebook.com/download_%s'%os - self.latest_version = '
' + _('' - 'Latest version: %s')%(url, version) - self.vanity.setText(self.vanity_template%\ - (dict(version=self.latest_version, - device=self.device_info))) - self.vanity.update() - if config.get('new_version_notification') and \ - dynamic.get('update to version %s'%version, True): - if question_dialog(self, _('Update available'), - _('%s has been updated to version %s. ' - 'See the new features. Visit the download pa' - 'ge?')%(__appname__, version)): - url = 'http://calibre-ebook.com/download_'+\ - ('windows' if iswindows else 'osx' if isosx else 'linux') - QDesktopServices.openUrl(QUrl(url)) - dynamic.set('update to version %s'%version, False) - - - diff --git a/src/calibre/gui2/update.py b/src/calibre/gui2/update.py index 92e9db1cf2..18b6b52d79 100644 --- a/src/calibre/gui2/update.py +++ b/src/calibre/gui2/update.py @@ -3,12 +3,13 @@ __copyright__ = '2008, Kovid Goyal ' import traceback -from PyQt4.QtCore import QThread, pyqtSignal +from PyQt4.Qt import QThread, pyqtSignal, QDesktopServices, QUrl, Qt import mechanize -from calibre.constants import __version__, iswindows, isosx +from calibre.constants import __appname__, __version__, iswindows, isosx from calibre import browser from calibre.utils.config import prefs +from calibre.gui2 import config, dynamic, question_dialog URL = 'http://status.calibre-ebook.com/latest' @@ -36,3 +37,35 @@ class CheckForUpdates(QThread): traceback.print_exc() self.sleep(self.INTERVAL) +def UpdateMixin(object): + + def __init__(self, opts): + if not opts.no_update_check: + self.update_checker = CheckForUpdates(self) + self.update_checker.update_found.connect(self.update_found, + type=Qt.QueuedConnection) + self.update_checker.start() + + def update_found(self, version): + os = 'windows' if iswindows else 'osx' if isosx else 'linux' + url = 'http://calibre-ebook.com/download_%s'%os + self.latest_version = '
' + _('' + 'Latest version: %s')%(url, version) + self.vanity.setText(self.vanity_template%\ + (dict(version=self.latest_version, + device=self.device_info))) + self.vanity.update() + if config.get('new_version_notification') and \ + dynamic.get('update to version %s'%version, True): + if question_dialog(self, _('Update available'), + _('%s has been updated to version %s. ' + 'See the new features. Visit the download pa' + 'ge?')%(__appname__, version)): + url = 'http://calibre-ebook.com/download_'+\ + ('windows' if iswindows else 'osx' if isosx else 'linux') + QDesktopServices.openUrl(QUrl(url)) + dynamic.set('update to version %s'%version, False) + + + From bb81c3e2ddc04ed2f8b940455aac8eeba19b625f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 12 Jun 2010 09:54:58 -0600 Subject: [PATCH 04/13] More GUI refactoring --- src/calibre/gui2/actions.py | 1197 +++++++++++++++++++++++++++++++++ src/calibre/gui2/device.py | 13 +- src/calibre/gui2/ui.py | 1240 +---------------------------------- src/calibre/gui2/update.py | 2 +- 4 files changed, 1230 insertions(+), 1222 deletions(-) create mode 100644 src/calibre/gui2/actions.py diff --git a/src/calibre/gui2/actions.py b/src/calibre/gui2/actions.py new file mode 100644 index 0000000000..cabc17cd00 --- /dev/null +++ b/src/calibre/gui2/actions.py @@ -0,0 +1,1197 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import shutil, os, datetime, sys, time +from functools import partial + +from PyQt4.Qt import QInputDialog, pyqtSignal, QModelIndex, QThread, Qt, \ + SIGNAL, QPixmap, QTimer, QDesktopServices, QUrl, QDialog + +from calibre import strftime +from calibre.ptempfile import PersistentTemporaryFile +from calibre.utils.config import prefs, dynamic +from calibre.gui2 import error_dialog, Dispatcher, gprefs, choose_files, \ + choose_dir, warning_dialog, info_dialog, question_dialog, config +from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, NavigableString +from calibre.utils.filenames import ascii_filename +from calibre.gui2.widgets import IMAGE_EXTENSIONS +from calibre.gui2.dialogs.metadata_single import MetadataSingleDialog +from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog +from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook, \ + fetch_scheduled_recipe, generate_catalog +from calibre.constants import preferred_encoding, filesystem_encoding, \ + isosx, isfrozen, islinux +from calibre.gui2.dialogs.choose_format import ChooseFormatDialog +from calibre.ebooks import BOOK_EXTENSIONS +from calibre.gui2.dialogs.confirm_delete import confirm + +class AnnotationsAction(object): # {{{ + + def fetch_annotations(self, *args): + # Generate a path_map from selected ids + def get_ids_from_selected_rows(): + rows = self.library_view.selectionModel().selectedRows() + if not rows or len(rows) < 2: + rows = xrange(self.library_view.model().rowCount(QModelIndex())) + ids = map(self.library_view.model().id, rows) + return ids + + def get_formats(id): + formats = db.formats(id, index_is_id=True) + fmts = [] + if formats: + for format in formats.split(','): + fmts.append(format.lower()) + return fmts + + def generate_annotation_paths(ids, db, device): + # Generate path templates + # Individual storage mount points scanned/resolved in driver.get_annotations() + path_map = {} + for id in ids: + mi = db.get_metadata(id, index_is_id=True) + a_path = device.create_upload_path(os.path.abspath('/'), mi, 'x.bookmark', create_dirs=False) + path_map[id] = dict(path=a_path, fmts=get_formats(id)) + return path_map + + device = self.device_manager.device + + if self.current_view() is not self.library_view: + return error_dialog(self, _('Use library only'), + _('User annotations generated from main library only'), + show=True) + db = self.library_view.model().db + + # Get the list of ids + ids = get_ids_from_selected_rows() + if not ids: + return error_dialog(self, _('No books selected'), + _('No books selected to fetch annotations from'), + show=True) + + # Map ids to paths + path_map = generate_annotation_paths(ids, db, device) + + # Dispatch to devices.kindle.driver.get_annotations() + self.device_manager.annotations(Dispatcher(self.annotations_fetched), + path_map) + + def annotations_fetched(self, job): + from calibre.devices.usbms.device import Device + from calibre.ebooks.metadata import MetaInformation + from calibre.gui2.dialogs.progress import ProgressDialog + from calibre.library.cli import do_add_format + + class Updater(QThread): + + update_progress = pyqtSignal(int) + update_done = pyqtSignal() + FINISHED_READING_PCT_THRESHOLD = 96 + + def __init__(self, parent, db, annotation_map, done_callback): + QThread.__init__(self, parent) + self.db = db + self.pd = ProgressDialog(_('Merging user annotations into database'), '', + 0, len(job.result), parent=parent) + + self.am = annotation_map + self.done_callback = done_callback + self.connect(self.pd, SIGNAL('canceled()'), self.canceled) + self.pd.setModal(True) + self.pd.show() + self.update_progress.connect(self.pd.set_value, + type=Qt.QueuedConnection) + self.update_done.connect(self.pd.hide, type=Qt.QueuedConnection) + + def generate_annotation_html(self, bookmark): + # Returns
...
+ last_read_location = bookmark.last_read_location + timestamp = datetime.datetime.utcfromtimestamp(bookmark.timestamp) + percent_read = bookmark.percent_read + + ka_soup = BeautifulSoup() + dtc = 0 + divTag = Tag(ka_soup,'div') + divTag['class'] = 'user_annotations' + + # Add the last-read location + spanTag = Tag(ka_soup, 'span') + spanTag['style'] = 'font-weight:bold' + if bookmark.book_format == 'pdf': + spanTag.insert(0,NavigableString( + _("%s
Last Page Read: %d (%d%%)") % \ + (strftime(u'%x', timestamp.timetuple()), + last_read_location, + percent_read))) + else: + spanTag.insert(0,NavigableString( + _("%s
Last Page Read: Location %d (%d%%)") % \ + (strftime(u'%x', timestamp.timetuple()), + last_read_location, + percent_read))) + + divTag.insert(dtc, spanTag) + dtc += 1 + divTag.insert(dtc, Tag(ka_soup,'br')) + dtc += 1 + + if bookmark.user_notes: + user_notes = bookmark.user_notes + annotations = [] + + # Add the annotations sorted by location + # Italicize highlighted text + for location in sorted(user_notes): + if user_notes[location]['text']: + annotations.append( + _('Location %d • %s
%s
') % \ + (user_notes[location]['displayed_location'], + user_notes[location]['type'], + user_notes[location]['text'] if \ + user_notes[location]['type'] == 'Note' else \ + '%s' % user_notes[location]['text'])) + else: + if bookmark.book_format == 'pdf': + annotations.append( + _('Page %d • %s
') % \ + (user_notes[location]['displayed_location'], + user_notes[location]['type'])) + else: + annotations.append( + _('Location %d • %s
') % \ + (user_notes[location]['displayed_location'], + user_notes[location]['type'])) + + for annotation in annotations: + divTag.insert(dtc, annotation) + dtc += 1 + + ka_soup.insert(0,divTag) + return ka_soup + + def mark_book_as_read(self,id): + read_tag = gprefs.get('catalog_epub_mobi_read_tag') + self.db.set_tags(id, [read_tag], append=True) + + def canceled(self): + self.pd.hide() + + def run(self): + ignore_tags = set(['Catalog','Clippings']) + for (i, id) in enumerate(self.am): + bm = Device.UserAnnotation(self.am[id][0],self.am[id][1]) + if bm.type == 'kindle_bookmark': + mi = self.db.get_metadata(id, index_is_id=True) + user_notes_soup = self.generate_annotation_html(bm.value) + if mi.comments: + a_offset = mi.comments.find('
') + ad_offset = mi.comments.find('
') + + if a_offset >= 0: + mi.comments = mi.comments[:a_offset] + if ad_offset >= 0: + mi.comments = mi.comments[:ad_offset] + if set(mi.tags).intersection(ignore_tags): + continue + if mi.comments: + hrTag = Tag(user_notes_soup,'hr') + hrTag['class'] = 'annotations_divider' + user_notes_soup.insert(0,hrTag) + + mi.comments += user_notes_soup.prettify() + else: + mi.comments = unicode(user_notes_soup.prettify()) + # Update library comments + self.db.set_comment(id, mi.comments) + + # Update 'read' tag except for Catalogs/Clippings + if bm.value.percent_read >= self.FINISHED_READING_PCT_THRESHOLD: + if not set(mi.tags).intersection(ignore_tags): + self.mark_book_as_read(id) + + # Add bookmark file to id + self.db.add_format_with_hooks(id, bm.value.bookmark_extension, + bm.value.path, index_is_id=True) + self.update_progress.emit(i) + elif bm.type == 'kindle_clippings': + # Find 'My Clippings' author=Kindle in database, or add + last_update = 'Last modified %s' % strftime(u'%x %X',bm.value['timestamp'].timetuple()) + mc_id = list(db.data.parse('title:"My Clippings"')) + if mc_id: + do_add_format(self.db, mc_id[0], 'TXT', bm.value['path']) + mi = self.db.get_metadata(mc_id[0], index_is_id=True) + mi.comments = last_update + self.db.set_metadata(mc_id[0], mi) + else: + mi = MetaInformation('My Clippings', authors = ['Kindle']) + mi.tags = ['Clippings'] + mi.comments = last_update + self.db.add_books([bm.value['path']], ['txt'], [mi]) + + self.update_done.emit() + self.done_callback(self.am.keys()) + + if not job.result: return + + if self.current_view() is not self.library_view: + return error_dialog(self, _('Use library only'), + _('User annotations generated from main library only'), + show=True) + db = self.library_view.model().db + + self.__annotation_updater = Updater(self, db, job.result, + Dispatcher(self.library_view.model().refresh_ids)) + self.__annotation_updater.start() + + # }}} + +class AddAction(object): # {{{ + + def add_recursive(self, single): + root = choose_dir(self, 'recursive book import root dir dialog', + 'Select root folder') + if not root: + return + from calibre.gui2.add import Adder + self._adder = Adder(self, + self.library_view.model().db, + Dispatcher(self._files_added), spare_server=self.spare_server) + self._adder.add_recursive(root, single) + + def add_recursive_single(self, *args): + ''' + Add books from the local filesystem to either the library or the device + recursively assuming one book per folder. + ''' + self.add_recursive(True) + + def add_recursive_multiple(self, *args): + ''' + Add books from the local filesystem to either the library or the device + recursively assuming multiple books per folder. + ''' + self.add_recursive(False) + + def add_empty(self, *args): + ''' + Add an empty book item to the library. This does not import any formats + from a book file. + ''' + num, ok = QInputDialog.getInt(self, _('How many empty books?'), + _('How many empty books should be added?'), 1, 1, 100) + if ok: + from calibre.ebooks.metadata import MetaInformation + for x in xrange(num): + self.library_view.model().db.import_book(MetaInformation(None), []) + self.library_view.model().books_added(num) + + def files_dropped(self, paths): + to_device = self.stack.currentIndex() != 0 + self._add_books(paths, to_device) + + def files_dropped_on_book(self, event, paths): + accept = False + if self.current_view() is not self.library_view: + return + db = self.library_view.model().db + current_idx = self.library_view.currentIndex() + if not current_idx.isValid(): return + cid = db.id(current_idx.row()) + for path in paths: + ext = os.path.splitext(path)[1].lower() + if ext: + ext = ext[1:] + if ext in IMAGE_EXTENSIONS: + pmap = QPixmap() + pmap.load(path) + if not pmap.isNull(): + accept = True + db.set_cover(cid, pmap) + elif ext in BOOK_EXTENSIONS: + db.add_format_with_hooks(cid, ext, path, index_is_id=True) + accept = True + if accept: + event.accept() + self.cover_cache.refresh([cid]) + self.library_view.model().current_changed(current_idx, current_idx) + + def __add_filesystem_book(self, paths, allow_device=True): + if isinstance(paths, basestring): + paths = [paths] + books = [path for path in map(os.path.abspath, paths) if os.access(path, + os.R_OK)] + + if books: + to_device = allow_device and self.stack.currentIndex() != 0 + self._add_books(books, to_device) + if to_device: + self.status_bar.show_message(\ + _('Uploading books to device.'), 2000) + + + def add_filesystem_book(self, paths, allow_device=True): + self._add_filesystem_book(paths, allow_device=allow_device) + + def add_books(self, *args): + ''' + Add books from the local filesystem to either the library or the device. + ''' + filters = [ + (_('Books'), BOOK_EXTENSIONS), + (_('EPUB Books'), ['epub']), + (_('LRF Books'), ['lrf']), + (_('HTML Books'), ['htm', 'html', 'xhtm', 'xhtml']), + (_('LIT Books'), ['lit']), + (_('MOBI Books'), ['mobi', 'prc', 'azw']), + (_('Topaz books'), ['tpz','azw1']), + (_('Text books'), ['txt', 'rtf']), + (_('PDF Books'), ['pdf']), + (_('Comics'), ['cbz', 'cbr', 'cbc']), + (_('Archives'), ['zip', 'rar']), + ] + to_device = self.stack.currentIndex() != 0 + if to_device: + filters = [(_('Supported books'), self.device_manager.device.FORMATS)] + + books = choose_files(self, 'add books dialog dir', 'Select books', + filters=filters) + if not books: + return + self._add_books(books, to_device) + + def _add_books(self, paths, to_device, on_card=None): + if on_card is None: + on_card = 'carda' if self.stack.currentIndex() == 2 else 'cardb' if self.stack.currentIndex() == 3 else None + if not paths: + return + from calibre.gui2.add import Adder + self.__adder_func = partial(self._files_added, on_card=on_card) + self._adder = Adder(self, + None if to_device else self.library_view.model().db, + Dispatcher(self.__adder_func), spare_server=self.spare_server) + self._adder.add(paths) + + def _files_added(self, paths=[], names=[], infos=[], on_card=None): + if paths: + self.upload_books(paths, + list(map(ascii_filename, names)), + infos, on_card=on_card) + self.status_bar.show_message( + _('Uploading books to device.'), 2000) + if getattr(self._adder, 'number_of_books_added', 0) > 0: + self.library_view.model().books_added(self._adder.number_of_books_added) + if hasattr(self, 'db_images'): + self.db_images.reset() + if getattr(self._adder, 'merged_books', False): + books = u'\n'.join([x if isinstance(x, unicode) else + x.decode(preferred_encoding, 'replace') for x in + self._adder.merged_books]) + info_dialog(self, _('Merged some books'), + _('Some duplicates were found and merged into the ' + 'following existing books:'), det_msg=books, show=True) + if getattr(self._adder, 'critical', None): + det_msg = [] + for name, log in self._adder.critical.items(): + if isinstance(name, str): + name = name.decode(filesystem_encoding, 'replace') + det_msg.append(name+'\n'+log) + + warning_dialog(self, _('Failed to read metadata'), + _('Failed to read metadata from the following')+':', + det_msg='\n\n'.join(det_msg), show=True) + + if hasattr(self._adder, 'cleanup'): + self._adder.cleanup() + self._adder = None + # }}} + +class DeleteAction(object): # {{{ + + def _get_selected_formats(self, msg): + from calibre.gui2.dialogs.select_formats import SelectFormats + fmts = self.library_view.model().db.all_formats() + d = SelectFormats([x.lower() for x in fmts], msg, parent=self) + if d.exec_() != d.Accepted: + return None + return d.selected_formats + + def _get_selected_ids(self, err_title=_('Cannot delete')): + rows = self.library_view.selectionModel().selectedRows() + if not rows or len(rows) == 0: + d = error_dialog(self, err_title, _('No book selected')) + d.exec_() + return set([]) + return set(map(self.library_view.model().id, rows)) + + def delete_selected_formats(self, *args): + ids = self._get_selected_ids() + if not ids: + return + fmts = self._get_selected_formats( + _('Choose formats to be deleted')) + if not fmts: + return + for id in ids: + for fmt in fmts: + self.library_view.model().db.remove_format(id, fmt, + index_is_id=True, notify=False) + self.library_view.model().refresh_ids(ids) + self.library_view.model().current_changed(self.library_view.currentIndex(), + self.library_view.currentIndex()) + if ids: + self.tags_view.recount() + + def delete_all_but_selected_formats(self, *args): + ids = self._get_selected_ids() + if not ids: + return + fmts = self._get_selected_formats( + '

'+_('Choose formats not to be deleted')) + if fmts is None: + return + for id in ids: + bfmts = self.library_view.model().db.formats(id, index_is_id=True) + if bfmts is None: + continue + bfmts = set([x.lower() for x in bfmts.split(',')]) + rfmts = bfmts - set(fmts) + for fmt in rfmts: + self.library_view.model().db.remove_format(id, fmt, + index_is_id=True, notify=False) + self.library_view.model().refresh_ids(ids) + self.library_view.model().current_changed(self.library_view.currentIndex(), + self.library_view.currentIndex()) + if ids: + self.tags_view.recount() + + + def delete_covers(self, *args): + ids = self._get_selected_ids() + if not ids: + return + for id in ids: + self.library_view.model().db.remove_cover(id) + self.library_view.model().refresh_ids(ids) + self.library_view.model().current_changed(self.library_view.currentIndex(), + self.library_view.currentIndex()) + + def delete_books(self, *args): + ''' + Delete selected books from device or library. + ''' + view = self.current_view() + rows = view.selectionModel().selectedRows() + if not rows or len(rows) == 0: + return + if self.stack.currentIndex() == 0: + if not confirm('

'+_('The selected books will be ' + 'permanently deleted and the files ' + 'removed from your computer. Are you sure?') + +'

', 'library_delete_books', self): + return + ci = view.currentIndex() + row = None + if ci.isValid(): + row = ci.row() + ids_deleted = view.model().delete_books(rows) + for v in (self.memory_view, self.card_a_view, self.card_b_view): + if v is None: + continue + v.model().clear_ondevice(ids_deleted) + if row is not None: + ci = view.model().index(row, 0) + if ci.isValid(): + view.setCurrentIndex(ci) + sm = view.selectionModel() + sm.select(ci, sm.Select) + else: + if not confirm('

'+_('The selected books will be ' + 'permanently deleted ' + 'from your device. Are you sure?') + +'

', 'device_delete_books', self): + return + if self.stack.currentIndex() == 1: + view = self.memory_view + elif self.stack.currentIndex() == 2: + view = self.card_a_view + else: + view = self.card_b_view + paths = view.model().paths(rows) + job = self.remove_paths(paths) + self.delete_memory[job] = (paths, view.model()) + view.model().mark_for_deletion(job, rows) + self.status_bar.show_message(_('Deleting books from device.'), 1000) + + # }}} + +class EditMetadataAction(object): # {{{ + + def download_metadata(self, checked, covers=True, set_metadata=True, + set_social_metadata=None): + rows = self.library_view.selectionModel().selectedRows() + 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] + if set_social_metadata is None: + get_social_metadata = config['get_social_metadata'] + else: + get_social_metadata = set_social_metadata + from calibre.gui2.metadata import DownloadMetadata + self._download_book_metadata = DownloadMetadata(db, ids, + get_covers=covers, set_metadata=set_metadata, + get_social_metadata=get_social_metadata) + self._download_book_metadata.start() + if set_social_metadata is not None and set_social_metadata: + x = _('social metadata') + else: + x = _('covers') if covers and not set_metadata else _('metadata') + self.progress_indicator.start( + _('Downloading %s for %d book(s)')%(x, len(ids))) + self._book_metadata_download_check = QTimer(self) + self.connect(self._book_metadata_download_check, + SIGNAL('timeout()'), self.book_metadata_download_check, + Qt.QueuedConnection) + 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: + 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 = '%s\n'%('\n'.join(details)) + warning_dialog(self, _('Failed to download some metadata'), + _('Failed to download metadata for the following:'), + det_msg=details).exec_() + else: + err = _('Failed to download metadata:') + error_dialog(self, _('Error'), err, det_msg=x.tb).exec_() + + + def edit_metadata(self, checked, bulk=None): + ''' + Edit metadata of selected books in library. + ''' + rows = self.library_view.selectionModel().selectedRows() + previous = self.library_view.currentIndex() + if not rows or len(rows) == 0: + d = error_dialog(self, _('Cannot edit metadata'), + _('No books selected')) + d.exec_() + return + + if bulk or (bulk is None and len(rows) > 1): + return self.edit_bulk_metadata(checked) + + def accepted(id): + self.library_view.model().refresh_ids([id]) + + for row in rows: + self._metadata_view_id = self.library_view.model().db.id(row.row()) + d = MetadataSingleDialog(self, row.row(), + self.library_view.model().db, + accepted_callback=accepted, + cancel_all=rows.index(row) < len(rows)-1) + self.connect(d, SIGNAL('view_format(PyQt_PyObject)'), + self.metadata_view_format) + d.exec_() + if d.cancel_all: + break + if rows: + current = self.library_view.currentIndex() + m = self.library_view.model() + m.refresh_cover_cache(map(m.id, rows)) + if self.cover_flow: + self.cover_flow.dataChanged() + m.current_changed(current, previous) + self.tags_view.recount() + + def edit_bulk_metadata(self, checked): + ''' + Edit metadata of selected books in library in bulk. + ''' + rows = [r.row() for r in \ + self.library_view.selectionModel().selectedRows()] + if not rows or len(rows) == 0: + d = error_dialog(self, _('Cannot edit metadata'), + _('No books selected')) + d.exec_() + return + if MetadataBulkDialog(self, rows, + self.library_view.model().db).changed: + self.library_view.model().resort(reset=False) + self.library_view.model().research() + self.tags_view.recount() + + # Merge books {{{ + def merge_books(self, safe_merge=False): + ''' + Merge selected books in library. + ''' + if self.stack.currentIndex() != 0: + return + rows = self.library_view.selectionModel().selectedRows() + if not rows or len(rows) == 0: + return error_dialog(self, _('Cannot merge books'), + _('No books selected'), show=True) + if len(rows) < 2: + return error_dialog(self, _('Cannot merge books'), + _('At least two books must be selected for merging'), + show=True) + dest_id, src_books, src_ids = self.books_to_merge(rows) + if safe_merge: + if not confirm('

'+_( + 'All book formats and metadata from the selected books ' + 'will be added to the first selected book.

' + 'The second and subsequently selected books will not ' + 'be deleted or changed.

' + 'Please confirm you want to proceed.') + +'

', 'merge_books_safe', self): + return + self.add_formats(dest_id, src_books) + self.merge_metadata(dest_id, src_ids) + else: + if not confirm('

'+_( + 'All book formats and metadata from the selected books will be merged ' + 'into the first selected book.

' + 'After merger the second and ' + 'subsequently selected books will be deleted.

' + 'All book formats of the first selected book will be kept ' + 'and any duplicate formats in the second and subsequently selected books ' + 'will be permanently deleted from your computer.

' + 'Are you sure you want to proceed?') + +'

', 'merge_books', self): + return + if len(rows)>5: + if not confirm('

'+_('You are about to merge more than 5 books. ' + 'Are you sure you want to proceed?') + +'

', 'merge_too_many_books', self): + return + self.add_formats(dest_id, src_books) + self.merge_metadata(dest_id, src_ids) + self.delete_books_after_merge(src_ids) + # leave the selection highlight on first selected book + dest_row = rows[0].row() + for row in rows: + if row.row() < rows[0].row(): + dest_row -= 1 + ci = self.library_view.model().index(dest_row, 0) + if ci.isValid(): + self.library_view.setCurrentIndex(ci) + + def add_formats(self, dest_id, src_books, replace=False): + for src_book in src_books: + if src_book: + fmt = os.path.splitext(src_book)[-1].replace('.', '').upper() + with open(src_book, 'rb') as f: + self.library_view.model().db.add_format(dest_id, fmt, f, index_is_id=True, + notify=False, replace=replace) + + def books_to_merge(self, rows): + src_books = [] + src_ids = [] + m = self.library_view.model() + for i, row in enumerate(rows): + id_ = m.id(row) + if i == 0: + dest_id = id_ + else: + src_ids.append(id_) + dbfmts = m.db.formats(id_, index_is_id=True) + if dbfmts: + for fmt in dbfmts.split(','): + src_books.append(m.db.format_abspath(id_, fmt, + index_is_id=True)) + return [dest_id, src_books, src_ids] + + def delete_books_after_merge(self, ids_to_delete): + self.library_view.model().delete_books_by_id(ids_to_delete) + + def merge_metadata(self, dest_id, src_ids): + db = self.library_view.model().db + dest_mi = db.get_metadata(dest_id, index_is_id=True, get_cover=True) + orig_dest_comments = dest_mi.comments + for src_id in src_ids: + src_mi = db.get_metadata(src_id, index_is_id=True, get_cover=True) + if src_mi.comments and orig_dest_comments != src_mi.comments: + if not dest_mi.comments or len(dest_mi.comments) == 0: + dest_mi.comments = src_mi.comments + else: + dest_mi.comments = unicode(dest_mi.comments) + u'\n\n' + unicode(src_mi.comments) + if src_mi.title and src_mi.title and (not dest_mi.title or + dest_mi.title == _('Unknown')): + dest_mi.title = src_mi.title + if src_mi.title and (not dest_mi.authors or dest_mi.authors[0] == + _('Unknown')): + dest_mi.authors = src_mi.authors + dest_mi.author_sort = src_mi.author_sort + if src_mi.tags: + if not dest_mi.tags: + dest_mi.tags = src_mi.tags + else: + for tag in src_mi.tags: + dest_mi.tags.append(tag) + if src_mi.cover and not dest_mi.cover: + dest_mi.cover = src_mi.cover + if not dest_mi.publisher: + dest_mi.publisher = src_mi.publisher + if not dest_mi.rating: + dest_mi.rating = src_mi.rating + if not dest_mi.series: + dest_mi.series = src_mi.series + dest_mi.series_index = src_mi.series_index + db.set_metadata(dest_id, dest_mi, ignore_errors=False) + # }}} + + # }}} + +class SaveToDiskAction(object): # {{{ + + def save_single_format_to_disk(self, checked): + self.save_to_disk(checked, False, prefs['output_format']) + + def save_specific_format_disk(self, fmt): + self.save_to_disk(False, False, fmt) + + def save_to_single_dir(self, checked): + self.save_to_disk(checked, True) + + def save_single_fmt_to_single_dir(self, *args): + self.save_to_disk(False, single_dir=True, + single_format=prefs['output_format']) + + def save_to_disk(self, checked, single_dir=False, single_format=None): + rows = self.current_view().selectionModel().selectedRows() + if not rows or len(rows) == 0: + return error_dialog(self, _('Cannot save to disk'), + _('No books selected'), show=True) + path = choose_dir(self, 'save to disk dialog', + _('Choose destination directory')) + if not path: + return + + if self.current_view() is self.library_view: + from calibre.gui2.add import Saver + from calibre.library.save_to_disk import config + opts = config().parse() + if single_format is not None: + opts.formats = single_format + # Special case for Kindle annotation files + if single_format.lower() in ['mbp','pdr','tan']: + opts.to_lowercase = False + opts.save_cover = False + opts.write_opf = False + opts.template = opts.send_template + if single_dir: + opts.template = opts.template.split('/')[-1].strip() + if not opts.template: + opts.template = '{title} - {authors}' + self._saver = Saver(self, self.library_view.model().db, + Dispatcher(self._books_saved), rows, path, opts, + spare_server=self.spare_server) + + else: + paths = self.current_view().model().paths(rows) + self.device_manager.save_books( + Dispatcher(self.books_saved), paths, path) + + + def _books_saved(self, path, failures, error): + self._saver = None + if error: + return error_dialog(self, _('Error while saving'), + _('There was an error while saving.'), + error, show=True) + if failures: + failures = [u'%s\n\t%s'% + (title, '\n\t'.join(err.splitlines())) for title, err in + failures] + + warning_dialog(self, _('Could not save some books'), + _('Could not save some books') + ', ' + + _('Click the show details button to see which ones.'), + u'\n\n'.join(failures), show=True) + QDesktopServices.openUrl(QUrl.fromLocalFile(path)) + + def books_saved(self, job): + if job.failed: + return self.device_job_exception(job) + + # }}} + +class GenerateCatalogAction(object): # {{{ + + def generate_catalog(self): + rows = self.library_view.selectionModel().selectedRows() + if not rows or len(rows) < 2: + rows = xrange(self.library_view.model().rowCount(QModelIndex())) + ids = map(self.library_view.model().id, rows) + + dbspec = None + if not ids: + return error_dialog(self, _('No books selected'), + _('No books selected to generate catalog for'), + show=True) + + # Calling gui2.tools:generate_catalog() + ret = generate_catalog(self, dbspec, ids, self.device_manager.device) + if ret is None: + return + + func, args, desc, out, sync, title = ret + + fmt = os.path.splitext(out)[1][1:].upper() + job = self.job_manager.run_job( + Dispatcher(self.catalog_generated), func, args=args, + description=desc) + job.catalog_file_path = out + job.fmt = fmt + job.catalog_sync, job.catalog_title = sync, title + self.status_bar.show_message(_('Generating %s catalog...')%fmt) + + def catalog_generated(self, job): + if job.result: + # Search terms nulled catalog results + return error_dialog(self, _('No books found'), + _("No books to catalog\nCheck exclude tags"), + show=True) + if job.failed: + return self.job_exception(job) + id = self.library_view.model().add_catalog(job.catalog_file_path, job.catalog_title) + self.library_view.model().reset() + if job.catalog_sync: + sync = dynamic.get('catalogs_to_be_synced', set([])) + sync.add(id) + dynamic.set('catalogs_to_be_synced', sync) + self.status_bar.show_message(_('Catalog generated.'), 3000) + self.sync_catalogs() + if job.fmt not in ['EPUB','MOBI']: + export_dir = choose_dir(self, _('Export Catalog Directory'), + _('Select destination for %s.%s') % (job.catalog_title, job.fmt.lower())) + if export_dir: + destination = os.path.join(export_dir, '%s.%s' % (job.catalog_title, job.fmt.lower())) + shutil.copyfile(job.catalog_file_path, destination) + + # }}} + +class FetchNewsAction(object): # {{{ + + def download_scheduled_recipe(self, arg): + func, args, desc, fmt, temp_files = \ + fetch_scheduled_recipe(arg) + job = self.job_manager.run_job( + Dispatcher(self.scheduled_recipe_fetched), func, args=args, + description=desc) + self.conversion_jobs[job] = (temp_files, fmt, arg) + self.status_bar.show_message(_('Fetching news from ')+arg['title'], 2000) + + def scheduled_recipe_fetched(self, job): + temp_files, fmt, arg = self.conversion_jobs.pop(job) + pt = temp_files[0] + if job.failed: + self.scheduler.recipe_download_failed(arg) + return self.job_exception(job) + id = self.library_view.model().add_news(pt.name, arg) + self.library_view.model().reset() + sync = dynamic.get('news_to_be_synced', set([])) + sync.add(id) + dynamic.set('news_to_be_synced', sync) + self.scheduler.recipe_downloaded(arg) + self.status_bar.show_message(arg['title'] + _(' fetched.'), 3000) + self.email_news(id) + self.sync_news() + + # }}} + +class ConvertAction(object): # {{{ + + def auto_convert(self, book_ids, on_card, format): + previous = self.library_view.currentIndex() + rows = [x.row() for x in \ + self.library_view.selectionModel().selectedRows()] + jobs, changed, bad = convert_single_ebook(self, self.library_view.model().db, book_ids, True, format) + if jobs == []: return + self.queue_convert_jobs(jobs, changed, bad, rows, previous, + self.book_auto_converted, extra_job_args=[on_card]) + + def auto_convert_mail(self, to, fmts, delete_from_library, book_ids, format): + previous = self.library_view.currentIndex() + rows = [x.row() for x in \ + self.library_view.selectionModel().selectedRows()] + jobs, changed, bad = convert_single_ebook(self, self.library_view.model().db, book_ids, True, format) + if jobs == []: return + self.queue_convert_jobs(jobs, changed, bad, rows, previous, + self.book_auto_converted_mail, + extra_job_args=[delete_from_library, to, fmts]) + + def auto_convert_news(self, book_ids, format): + previous = self.library_view.currentIndex() + rows = [x.row() for x in \ + self.library_view.selectionModel().selectedRows()] + jobs, changed, bad = convert_single_ebook(self, self.library_view.model().db, book_ids, True, format) + if jobs == []: return + self.queue_convert_jobs(jobs, changed, bad, rows, previous, + self.book_auto_converted_news) + + def auto_convert_catalogs(self, book_ids, format): + previous = self.library_view.currentIndex() + rows = [x.row() for x in \ + self.library_view.selectionModel().selectedRows()] + jobs, changed, bad = convert_single_ebook(self, self.library_view.model().db, book_ids, True, format) + if jobs == []: return + self.queue_convert_jobs(jobs, changed, bad, rows, previous, + self.book_auto_converted_catalogs) + + def get_books_for_conversion(self): + rows = [r.row() for r in \ + self.library_view.selectionModel().selectedRows()] + if not rows or len(rows) == 0: + d = error_dialog(self, _('Cannot convert'), + _('No books selected')) + d.exec_() + return None + return [self.library_view.model().db.id(r) for r in rows] + + def convert_ebook(self, checked, bulk=None): + book_ids = self.get_books_for_conversion() + if book_ids is None: return + previous = self.library_view.currentIndex() + rows = [x.row() for x in \ + self.library_view.selectionModel().selectedRows()] + num = 0 + if bulk or (bulk is None and len(book_ids) > 1): + self.__bulk_queue = convert_bulk_ebook(self, self.queue_convert_jobs, + self.library_view.model().db, book_ids, + out_format=prefs['output_format'], args=(rows, previous, + self.book_converted)) + if self.__bulk_queue is None: + return + num = len(self.__bulk_queue.book_ids) + else: + jobs, changed, bad = convert_single_ebook(self, + self.library_view.model().db, book_ids, out_format=prefs['output_format']) + self.queue_convert_jobs(jobs, changed, bad, rows, previous, + self.book_converted) + num = len(jobs) + + if num > 0: + self.status_bar.show_message(_('Starting conversion of %d book(s)') % + num, 2000) + + def queue_convert_jobs(self, jobs, changed, bad, rows, previous, + converted_func, extra_job_args=[]): + for func, args, desc, fmt, id, temp_files in jobs: + if id not in bad: + job = self.job_manager.run_job(Dispatcher(converted_func), + func, args=args, description=desc) + args = [temp_files, fmt, id]+extra_job_args + self.conversion_jobs[job] = tuple(args) + + if changed: + self.library_view.model().refresh_rows(rows) + current = self.library_view.currentIndex() + self.library_view.model().current_changed(current, previous) + + def book_auto_converted(self, job): + temp_files, fmt, book_id, on_card = self.conversion_jobs[job] + self.book_converted(job) + self.sync_to_device(on_card, False, specific_format=fmt, send_ids=[book_id], do_auto_convert=False) + + def book_auto_converted_mail(self, job): + temp_files, fmt, book_id, delete_from_library, to, fmts = self.conversion_jobs[job] + self.book_converted(job) + self.send_by_mail(to, fmts, delete_from_library, specific_format=fmt, send_ids=[book_id], do_auto_convert=False) + + def book_auto_converted_news(self, job): + temp_files, fmt, book_id = self.conversion_jobs[job] + self.book_converted(job) + self.sync_news(send_ids=[book_id], do_auto_convert=False) + + def book_auto_converted_catalogs(self, job): + temp_files, fmt, book_id = self.conversion_jobs[job] + self.book_converted(job) + self.sync_catalogs(send_ids=[book_id], do_auto_convert=False) + + def book_converted(self, job): + temp_files, fmt, book_id = self.conversion_jobs.pop(job)[:3] + try: + if job.failed: + self.job_exception(job) + return + data = open(temp_files[-1].name, 'rb') + self.library_view.model().db.add_format(book_id, \ + fmt, data, index_is_id=True) + data.close() + self.status_bar.show_message(job.description + \ + (' completed'), 2000) + finally: + for f in temp_files: + try: + if os.path.exists(f.name): + os.remove(f.name) + except: + pass + self.tags_view.recount() + if self.current_view() is self.library_view: + current = self.library_view.currentIndex() + self.library_view.model().current_changed(current, QModelIndex()) + + # }}} + +class ViewAction(object): # {{{ + + def view_format(self, row, format): + fmt_path = self.library_view.model().db.format_abspath(row, format) + if fmt_path: + self._view_file(fmt_path) + + def metadata_view_format(self, fmt): + fmt_path = self.library_view.model().db.\ + format_abspath(self._metadata_view_id, + fmt, index_is_id=True) + if fmt_path: + self._view_file(fmt_path) + + + def book_downloaded_for_viewing(self, job): + if job.failed: + self.device_job_exception(job) + return + self._view_file(job.result) + + def _launch_viewer(self, name=None, viewer='ebook-viewer', internal=True): + self.setCursor(Qt.BusyCursor) + try: + if internal: + args = [viewer] + if isosx and 'ebook' in viewer: + args.append('--raise-window') + if name is not None: + args.append(name) + self.job_manager.launch_gui_app(viewer, + kwargs=dict(args=args)) + else: + paths = os.environ.get('LD_LIBRARY_PATH', + '').split(os.pathsep) + paths = [x for x in paths if x] + if isfrozen and islinux and paths: + npaths = [x for x in paths if x != sys.frozen_path] + os.environ['LD_LIBRARY_PATH'] = os.pathsep.join(npaths) + QDesktopServices.openUrl(QUrl.fromLocalFile(name))#launch(name) + if isfrozen and islinux and paths: + os.environ['LD_LIBRARY_PATH'] = os.pathsep.join(paths) + time.sleep(2) # User feedback + finally: + self.unsetCursor() + + def _view_file(self, name): + ext = os.path.splitext(name)[1].upper().replace('.', '') + viewer = 'lrfviewer' if ext == 'LRF' else 'ebook-viewer' + internal = ext in config['internally_viewed_formats'] + self._launch_viewer(name, viewer, internal) + + def view_specific_format(self, triggered): + rows = self.library_view.selectionModel().selectedRows() + if not rows or len(rows) == 0: + d = error_dialog(self, _('Cannot view'), _('No book selected')) + d.exec_() + return + + row = rows[0].row() + formats = self.library_view.model().db.formats(row).upper().split(',') + d = ChooseFormatDialog(self, _('Choose the format to view'), formats) + if d.exec_() == QDialog.Accepted: + format = d.format() + self.view_format(row, format) + + def _view_check(self, num, max_=3): + if num <= max_: + return True + return question_dialog(self, _('Multiple Books Selected'), + _('You are attempting to open %d books. Opening too many ' + 'books at once can be slow and have a negative effect on the ' + 'responsiveness of your computer. Once started the process ' + 'cannot be stopped until complete. Do you wish to continue?' + ) % num) + + def view_folder(self, *args): + rows = self.current_view().selectionModel().selectedRows() + if not rows or len(rows) == 0: + d = error_dialog(self, _('Cannot open folder'), + _('No book selected')) + d.exec_() + return + if not self._view_check(len(rows)): + return + for row in rows: + path = self.library_view.model().db.abspath(row.row()) + QDesktopServices.openUrl(QUrl.fromLocalFile(path)) + + + def view_book(self, triggered): + rows = self.current_view().selectionModel().selectedRows() + self._view_books(rows) + + def view_specific_book(self, index): + self._view_books([index]) + + def _view_books(self, rows): + if not rows or len(rows) == 0: + self._launch_viewer() + return + + if not self._view_check(len(rows)): + return + + if self.current_view() is self.library_view: + for row in rows: + if hasattr(row, 'row'): + row = row.row() + + formats = self.library_view.model().db.formats(row) + title = self.library_view.model().db.title(row) + if not formats: + error_dialog(self, _('Cannot view'), + _('%s has no available formats.')%(title,), show=True) + continue + + formats = formats.upper().split(',') + + + in_prefs = False + for format in prefs['input_format_order']: + if format in formats: + in_prefs = True + self.view_format(row, format) + break + if not in_prefs: + self.view_format(row, formats[0]) + else: + paths = self.current_view().model().paths(rows) + for path in paths: + pt = PersistentTemporaryFile('_viewer_'+\ + os.path.splitext(path)[1]) + self.persistent_files.append(pt) + pt.close() + self.device_manager.view_book(\ + Dispatcher(self.book_downloaded_for_viewing), + path, pt.name) + + # }}} + diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index bae0316e14..cf54e6c1f3 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -10,7 +10,8 @@ from functools import partial from binascii import unhexlify from PyQt4.Qt import QMenu, QAction, QActionGroup, QIcon, SIGNAL, QPixmap, \ - Qt, pyqtSignal + Qt, pyqtSignal, QColor, QPainter +from PyQt4.QtSvg import QSvgRenderer from calibre.customize.ui import available_input_formats, available_output_formats, \ device_plugins @@ -612,6 +613,16 @@ class DeviceMixin(object): # {{{ self.job_manager, Dispatcher(self.status_bar.show_message)) self.device_manager.start() + def set_default_thumbnail(self, height): + r = QSvgRenderer(I('book.svg')) + pixmap = QPixmap(height, height) + pixmap.fill(QColor(255,255,255)) + p = QPainter(pixmap) + r.render(p) + p.end() + self.default_thumbnail = (pixmap.width(), pixmap.height(), + pixmap_to_data(pixmap)) + def connect_to_folder(self): dir = choose_dir(self, 'Select Device Folder', _('Select folder to open as device')) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 795e79a7ee..dcc1470bed 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -9,33 +9,24 @@ __docformat__ = 'restructuredtext en' '''The main GUI''' -import collections, datetime, os, shutil, sys, textwrap, time +import collections, os, sys, textwrap, time from Queue import Queue, Empty from threading import Thread -from functools import partial from PyQt4.Qt import Qt, SIGNAL, QObject, QUrl, QTimer, \ - QModelIndex, QPixmap, QColor, QPainter, QMenu, QIcon, \ + QPixmap, QMenu, QIcon, \ QDialog, QDesktopServices, \ QSystemTrayIcon, QApplication, QKeySequence, QAction, \ - QMessageBox, QHelpEvent, QInputDialog,\ - QThread, pyqtSignal -from PyQt4.QtSvg import QSvgRenderer + QMessageBox, QHelpEvent -from calibre import prints, patheq, strftime -from calibre.constants import __version__, __appname__, isfrozen, islinux, \ - isosx, filesystem_encoding, preferred_encoding -from calibre.utils.filenames import ascii_filename +from calibre import prints, patheq +from calibre.constants import __version__, __appname__, isosx from calibre.ptempfile import PersistentTemporaryFile from calibre.utils.config import prefs, dynamic from calibre.utils.ipc.server import Server -from calibre.gui2 import warning_dialog, choose_files, error_dialog, \ - question_dialog,\ - pixmap_to_data, choose_dir, \ - Dispatcher, gprefs, \ - max_available_height, config, info_dialog, \ - GetMetadata +from calibre.gui2 import error_dialog, GetMetadata, \ + Dispatcher, gprefs, max_available_height, config, info_dialog from calibre.gui2.cover_flow import CoverFlowMixin -from calibre.gui2.widgets import ProgressIndicator, IMAGE_EXTENSIONS +from calibre.gui2.widgets import ProgressIndicator from calibre.gui2.wizard import move_library from calibre.gui2.dialogs.scheduler import Scheduler from calibre.gui2.update import UpdateMixin @@ -43,22 +34,19 @@ from calibre.gui2.main_window import MainWindow from calibre.gui2.main_ui import Ui_MainWindow from calibre.gui2.device import DeviceMixin from calibre.gui2.jobs import JobManager, JobsDialog, JobsButton -from calibre.gui2.dialogs.metadata_single import MetadataSingleDialog -from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog -from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook, \ - fetch_scheduled_recipe, generate_catalog from calibre.gui2.dialogs.config import ConfigDialog -from calibre.gui2.dialogs.choose_format import ChooseFormatDialog + from calibre.gui2.dialogs.book_info import BookInfo -from calibre.ebooks import BOOK_EXTENSIONS -from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, NavigableString from calibre.library.database2 import LibraryDatabase2 from calibre.library.caches import CoverCache -from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.init import ToolbarMixin, LibraryViewMixin, LayoutMixin from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin from calibre.gui2.tag_view import TagBrowserMixin +from calibre.gui2.actions import AnnotationsAction, AddAction, DeleteAction, \ + EditMetadataAction, SaveToDiskAction, GenerateCatalogAction, FetchNewsAction, \ + ConvertAction, ViewAction + class Listener(Thread): # {{{ @@ -102,22 +90,14 @@ class SystemTrayIcon(QSystemTrayIcon): # {{{ # }}} -class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, +class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{ TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin, - SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin): + SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin, + AnnotationsAction, AddAction, DeleteAction, + EditMetadataAction, SaveToDiskAction, GenerateCatalogAction, FetchNewsAction, + ConvertAction, ViewAction): 'The main GUI' - def set_default_thumbnail(self, height): - r = QSvgRenderer(I('book.svg')) - pixmap = QPixmap(height, height) - pixmap.fill(QColor(255,255,255)) - p = QPainter(pixmap) - r.render(p) - p.end() - self.default_thumbnail = (pixmap.width(), pixmap.height(), - pixmap_to_data(pixmap)) - - self.last_time = datetime.datetime.now() def __init__(self, opts, parent=None): MainWindow.__init__(self, opts, parent) @@ -125,7 +105,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, def initialize(self, library_path, db, listener, actions): opts = self.opts - self.last_time = datetime.datetime.now() self.preferences_action, self.quit_action = actions self.library_path = library_path self.spare_servers = [] @@ -406,1180 +385,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db - ######################### Fetch annotations ################################ - - def fetch_annotations(self, *args): - # Generate a path_map from selected ids - def get_ids_from_selected_rows(): - rows = self.library_view.selectionModel().selectedRows() - if not rows or len(rows) < 2: - rows = xrange(self.library_view.model().rowCount(QModelIndex())) - ids = map(self.library_view.model().id, rows) - return ids - - def get_formats(id): - formats = db.formats(id, index_is_id=True) - fmts = [] - if formats: - for format in formats.split(','): - fmts.append(format.lower()) - return fmts - - def generate_annotation_paths(ids, db, device): - # Generate path templates - # Individual storage mount points scanned/resolved in driver.get_annotations() - path_map = {} - for id in ids: - mi = db.get_metadata(id, index_is_id=True) - a_path = device.create_upload_path(os.path.abspath('/'), mi, 'x.bookmark', create_dirs=False) - path_map[id] = dict(path=a_path, fmts=get_formats(id)) - return path_map - - device = self.device_manager.device - - if self.current_view() is not self.library_view: - return error_dialog(self, _('Use library only'), - _('User annotations generated from main library only'), - show=True) - db = self.library_view.model().db - - # Get the list of ids - ids = get_ids_from_selected_rows() - if not ids: - return error_dialog(self, _('No books selected'), - _('No books selected to fetch annotations from'), - show=True) - - # Map ids to paths - path_map = generate_annotation_paths(ids, db, device) - - # Dispatch to devices.kindle.driver.get_annotations() - self.device_manager.annotations(Dispatcher(self.annotations_fetched), - path_map) - - def annotations_fetched(self, job): - from calibre.devices.usbms.device import Device - from calibre.ebooks.metadata import MetaInformation - from calibre.gui2.dialogs.progress import ProgressDialog - from calibre.library.cli import do_add_format - - class Updater(QThread): - - update_progress = pyqtSignal(int) - update_done = pyqtSignal() - FINISHED_READING_PCT_THRESHOLD = 96 - - def __init__(self, parent, db, annotation_map, done_callback): - QThread.__init__(self, parent) - self.db = db - self.pd = ProgressDialog(_('Merging user annotations into database'), '', - 0, len(job.result), parent=parent) - - self.am = annotation_map - self.done_callback = done_callback - self.connect(self.pd, SIGNAL('canceled()'), self.canceled) - self.pd.setModal(True) - self.pd.show() - self.update_progress.connect(self.pd.set_value, - type=Qt.QueuedConnection) - self.update_done.connect(self.pd.hide, type=Qt.QueuedConnection) - - def generate_annotation_html(self, bookmark): - # Returns
...
- last_read_location = bookmark.last_read_location - timestamp = datetime.datetime.utcfromtimestamp(bookmark.timestamp) - percent_read = bookmark.percent_read - - ka_soup = BeautifulSoup() - dtc = 0 - divTag = Tag(ka_soup,'div') - divTag['class'] = 'user_annotations' - - # Add the last-read location - spanTag = Tag(ka_soup, 'span') - spanTag['style'] = 'font-weight:bold' - if bookmark.book_format == 'pdf': - spanTag.insert(0,NavigableString( - _("%s
Last Page Read: %d (%d%%)") % \ - (strftime(u'%x', timestamp.timetuple()), - last_read_location, - percent_read))) - else: - spanTag.insert(0,NavigableString( - _("%s
Last Page Read: Location %d (%d%%)") % \ - (strftime(u'%x', timestamp.timetuple()), - last_read_location, - percent_read))) - - divTag.insert(dtc, spanTag) - dtc += 1 - divTag.insert(dtc, Tag(ka_soup,'br')) - dtc += 1 - - if bookmark.user_notes: - user_notes = bookmark.user_notes - annotations = [] - - # Add the annotations sorted by location - # Italicize highlighted text - for location in sorted(user_notes): - if user_notes[location]['text']: - annotations.append( - _('Location %d • %s
%s
') % \ - (user_notes[location]['displayed_location'], - user_notes[location]['type'], - user_notes[location]['text'] if \ - user_notes[location]['type'] == 'Note' else \ - '%s' % user_notes[location]['text'])) - else: - if bookmark.book_format == 'pdf': - annotations.append( - _('Page %d • %s
') % \ - (user_notes[location]['displayed_location'], - user_notes[location]['type'])) - else: - annotations.append( - _('Location %d • %s
') % \ - (user_notes[location]['displayed_location'], - user_notes[location]['type'])) - - for annotation in annotations: - divTag.insert(dtc, annotation) - dtc += 1 - - ka_soup.insert(0,divTag) - return ka_soup - - def mark_book_as_read(self,id): - read_tag = gprefs.get('catalog_epub_mobi_read_tag') - self.db.set_tags(id, [read_tag], append=True) - - def canceled(self): - self.pd.hide() - - def run(self): - ignore_tags = set(['Catalog','Clippings']) - for (i, id) in enumerate(self.am): - bm = Device.UserAnnotation(self.am[id][0],self.am[id][1]) - if bm.type == 'kindle_bookmark': - mi = self.db.get_metadata(id, index_is_id=True) - user_notes_soup = self.generate_annotation_html(bm.value) - if mi.comments: - a_offset = mi.comments.find('
') - ad_offset = mi.comments.find('
') - - if a_offset >= 0: - mi.comments = mi.comments[:a_offset] - if ad_offset >= 0: - mi.comments = mi.comments[:ad_offset] - if set(mi.tags).intersection(ignore_tags): - continue - if mi.comments: - hrTag = Tag(user_notes_soup,'hr') - hrTag['class'] = 'annotations_divider' - user_notes_soup.insert(0,hrTag) - - mi.comments += user_notes_soup.prettify() - else: - mi.comments = unicode(user_notes_soup.prettify()) - # Update library comments - self.db.set_comment(id, mi.comments) - - # Update 'read' tag except for Catalogs/Clippings - if bm.value.percent_read >= self.FINISHED_READING_PCT_THRESHOLD: - if not set(mi.tags).intersection(ignore_tags): - self.mark_book_as_read(id) - - # Add bookmark file to id - self.db.add_format_with_hooks(id, bm.value.bookmark_extension, - bm.value.path, index_is_id=True) - self.update_progress.emit(i) - elif bm.type == 'kindle_clippings': - # Find 'My Clippings' author=Kindle in database, or add - last_update = 'Last modified %s' % strftime(u'%x %X',bm.value['timestamp'].timetuple()) - mc_id = list(db.data.parse('title:"My Clippings"')) - if mc_id: - do_add_format(self.db, mc_id[0], 'TXT', bm.value['path']) - mi = self.db.get_metadata(mc_id[0], index_is_id=True) - mi.comments = last_update - self.db.set_metadata(mc_id[0], mi) - else: - mi = MetaInformation('My Clippings', authors = ['Kindle']) - mi.tags = ['Clippings'] - mi.comments = last_update - self.db.add_books([bm.value['path']], ['txt'], [mi]) - - self.update_done.emit() - self.done_callback(self.am.keys()) - - if not job.result: return - - if self.current_view() is not self.library_view: - return error_dialog(self, _('Use library only'), - _('User annotations generated from main library only'), - show=True) - db = self.library_view.model().db - - self.__annotation_updater = Updater(self, db, job.result, - Dispatcher(self.library_view.model().refresh_ids)) - self.__annotation_updater.start() - - - ############################################################################ - - ################################# Add books ################################ - - def add_recursive(self, single): - root = choose_dir(self, 'recursive book import root dir dialog', - 'Select root folder') - if not root: - return - from calibre.gui2.add import Adder - self._adder = Adder(self, - self.library_view.model().db, - Dispatcher(self._files_added), spare_server=self.spare_server) - self._adder.add_recursive(root, single) - - def add_recursive_single(self, *args): - ''' - Add books from the local filesystem to either the library or the device - recursively assuming one book per folder. - ''' - self.add_recursive(True) - - def add_recursive_multiple(self, *args): - ''' - Add books from the local filesystem to either the library or the device - recursively assuming multiple books per folder. - ''' - self.add_recursive(False) - - def add_empty(self, *args): - ''' - Add an empty book item to the library. This does not import any formats - from a book file. - ''' - num, ok = QInputDialog.getInt(self, _('How many empty books?'), - _('How many empty books should be added?'), 1, 1, 100) - if ok: - from calibre.ebooks.metadata import MetaInformation - for x in xrange(num): - self.library_view.model().db.import_book(MetaInformation(None), []) - self.library_view.model().books_added(num) - - def files_dropped(self, paths): - to_device = self.stack.currentIndex() != 0 - self._add_books(paths, to_device) - - def files_dropped_on_book(self, event, paths): - accept = False - if self.current_view() is not self.library_view: - return - db = self.library_view.model().db - current_idx = self.library_view.currentIndex() - if not current_idx.isValid(): return - cid = db.id(current_idx.row()) - for path in paths: - ext = os.path.splitext(path)[1].lower() - if ext: - ext = ext[1:] - if ext in IMAGE_EXTENSIONS: - pmap = QPixmap() - pmap.load(path) - if not pmap.isNull(): - accept = True - db.set_cover(cid, pmap) - elif ext in BOOK_EXTENSIONS: - db.add_format_with_hooks(cid, ext, path, index_is_id=True) - accept = True - if accept: - event.accept() - self.cover_cache.refresh([cid]) - self.library_view.model().current_changed(current_idx, current_idx) - - def __add_filesystem_book(self, paths, allow_device=True): - if isinstance(paths, basestring): - paths = [paths] - books = [path for path in map(os.path.abspath, paths) if os.access(path, - os.R_OK)] - - if books: - to_device = allow_device and self.stack.currentIndex() != 0 - self._add_books(books, to_device) - if to_device: - self.status_bar.show_message(\ - _('Uploading books to device.'), 2000) - - - def add_filesystem_book(self, paths, allow_device=True): - self._add_filesystem_book(paths, allow_device=allow_device) - - def add_books(self, *args): - ''' - Add books from the local filesystem to either the library or the device. - ''' - filters = [ - (_('Books'), BOOK_EXTENSIONS), - (_('EPUB Books'), ['epub']), - (_('LRF Books'), ['lrf']), - (_('HTML Books'), ['htm', 'html', 'xhtm', 'xhtml']), - (_('LIT Books'), ['lit']), - (_('MOBI Books'), ['mobi', 'prc', 'azw']), - (_('Topaz books'), ['tpz','azw1']), - (_('Text books'), ['txt', 'rtf']), - (_('PDF Books'), ['pdf']), - (_('Comics'), ['cbz', 'cbr', 'cbc']), - (_('Archives'), ['zip', 'rar']), - ] - to_device = self.stack.currentIndex() != 0 - if to_device: - filters = [(_('Supported books'), self.device_manager.device.FORMATS)] - - books = choose_files(self, 'add books dialog dir', 'Select books', - filters=filters) - if not books: - return - self._add_books(books, to_device) - - def _add_books(self, paths, to_device, on_card=None): - if on_card is None: - on_card = 'carda' if self.stack.currentIndex() == 2 else 'cardb' if self.stack.currentIndex() == 3 else None - if not paths: - return - from calibre.gui2.add import Adder - self.__adder_func = partial(self._files_added, on_card=on_card) - self._adder = Adder(self, - None if to_device else self.library_view.model().db, - Dispatcher(self.__adder_func), spare_server=self.spare_server) - self._adder.add(paths) - - def _files_added(self, paths=[], names=[], infos=[], on_card=None): - if paths: - self.upload_books(paths, - list(map(ascii_filename, names)), - infos, on_card=on_card) - self.status_bar.show_message( - _('Uploading books to device.'), 2000) - if getattr(self._adder, 'number_of_books_added', 0) > 0: - self.library_view.model().books_added(self._adder.number_of_books_added) - if hasattr(self, 'db_images'): - self.db_images.reset() - if getattr(self._adder, 'merged_books', False): - books = u'\n'.join([x if isinstance(x, unicode) else - x.decode(preferred_encoding, 'replace') for x in - self._adder.merged_books]) - info_dialog(self, _('Merged some books'), - _('Some duplicates were found and merged into the ' - 'following existing books:'), det_msg=books, show=True) - if getattr(self._adder, 'critical', None): - det_msg = [] - for name, log in self._adder.critical.items(): - if isinstance(name, str): - name = name.decode(filesystem_encoding, 'replace') - det_msg.append(name+'\n'+log) - - warning_dialog(self, _('Failed to read metadata'), - _('Failed to read metadata from the following')+':', - det_msg='\n\n'.join(det_msg), show=True) - - if hasattr(self._adder, 'cleanup'): - self._adder.cleanup() - self._adder = None - - - ############################################################################ - - ############################### Delete books ############################### - - def _get_selected_formats(self, msg): - from calibre.gui2.dialogs.select_formats import SelectFormats - fmts = self.library_view.model().db.all_formats() - d = SelectFormats([x.lower() for x in fmts], msg, parent=self) - if d.exec_() != d.Accepted: - return None - return d.selected_formats - - def _get_selected_ids(self, err_title=_('Cannot delete')): - rows = self.library_view.selectionModel().selectedRows() - if not rows or len(rows) == 0: - d = error_dialog(self, err_title, _('No book selected')) - d.exec_() - return set([]) - return set(map(self.library_view.model().id, rows)) - - def delete_selected_formats(self, *args): - ids = self._get_selected_ids() - if not ids: - return - fmts = self._get_selected_formats( - _('Choose formats to be deleted')) - if not fmts: - return - for id in ids: - for fmt in fmts: - self.library_view.model().db.remove_format(id, fmt, - index_is_id=True, notify=False) - self.library_view.model().refresh_ids(ids) - self.library_view.model().current_changed(self.library_view.currentIndex(), - self.library_view.currentIndex()) - if ids: - self.tags_view.recount() - - def delete_all_but_selected_formats(self, *args): - ids = self._get_selected_ids() - if not ids: - return - fmts = self._get_selected_formats( - '

'+_('Choose formats not to be deleted')) - if fmts is None: - return - for id in ids: - bfmts = self.library_view.model().db.formats(id, index_is_id=True) - if bfmts is None: - continue - bfmts = set([x.lower() for x in bfmts.split(',')]) - rfmts = bfmts - set(fmts) - for fmt in rfmts: - self.library_view.model().db.remove_format(id, fmt, - index_is_id=True, notify=False) - self.library_view.model().refresh_ids(ids) - self.library_view.model().current_changed(self.library_view.currentIndex(), - self.library_view.currentIndex()) - if ids: - self.tags_view.recount() - - - def delete_covers(self, *args): - ids = self._get_selected_ids() - if not ids: - return - for id in ids: - self.library_view.model().db.remove_cover(id) - self.library_view.model().refresh_ids(ids) - self.library_view.model().current_changed(self.library_view.currentIndex(), - self.library_view.currentIndex()) - - def delete_books(self, *args): - ''' - Delete selected books from device or library. - ''' - view = self.current_view() - rows = view.selectionModel().selectedRows() - if not rows or len(rows) == 0: - return - if self.stack.currentIndex() == 0: - if not confirm('

'+_('The selected books will be ' - 'permanently deleted and the files ' - 'removed from your computer. Are you sure?') - +'

', 'library_delete_books', self): - return - ci = view.currentIndex() - row = None - if ci.isValid(): - row = ci.row() - ids_deleted = view.model().delete_books(rows) - for v in (self.memory_view, self.card_a_view, self.card_b_view): - if v is None: - continue - v.model().clear_ondevice(ids_deleted) - if row is not None: - ci = view.model().index(row, 0) - if ci.isValid(): - view.setCurrentIndex(ci) - sm = view.selectionModel() - sm.select(ci, sm.Select) - else: - if not confirm('

'+_('The selected books will be ' - 'permanently deleted ' - 'from your device. Are you sure?') - +'

', 'device_delete_books', self): - return - if self.stack.currentIndex() == 1: - view = self.memory_view - elif self.stack.currentIndex() == 2: - view = self.card_a_view - else: - view = self.card_b_view - paths = view.model().paths(rows) - job = self.remove_paths(paths) - self.delete_memory[job] = (paths, view.model()) - view.model().mark_for_deletion(job, rows) - self.status_bar.show_message(_('Deleting books from device.'), 1000) - - - ############################################################################ - - ############################### Edit metadata ############################## - - def download_metadata(self, checked, covers=True, set_metadata=True, - set_social_metadata=None): - rows = self.library_view.selectionModel().selectedRows() - 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] - if set_social_metadata is None: - get_social_metadata = config['get_social_metadata'] - else: - get_social_metadata = set_social_metadata - from calibre.gui2.metadata import DownloadMetadata - self._download_book_metadata = DownloadMetadata(db, ids, - get_covers=covers, set_metadata=set_metadata, - get_social_metadata=get_social_metadata) - self._download_book_metadata.start() - if set_social_metadata is not None and set_social_metadata: - x = _('social metadata') - else: - x = _('covers') if covers and not set_metadata else _('metadata') - self.progress_indicator.start( - _('Downloading %s for %d book(s)')%(x, len(ids))) - self._book_metadata_download_check = QTimer(self) - self.connect(self._book_metadata_download_check, - SIGNAL('timeout()'), self.book_metadata_download_check, - Qt.QueuedConnection) - 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: - 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 = '%s\n'%('\n'.join(details)) - warning_dialog(self, _('Failed to download some metadata'), - _('Failed to download metadata for the following:'), - det_msg=details).exec_() - else: - err = _('Failed to download metadata:') - error_dialog(self, _('Error'), err, det_msg=x.tb).exec_() - - - def edit_metadata(self, checked, bulk=None): - ''' - Edit metadata of selected books in library. - ''' - rows = self.library_view.selectionModel().selectedRows() - previous = self.library_view.currentIndex() - if not rows or len(rows) == 0: - d = error_dialog(self, _('Cannot edit metadata'), - _('No books selected')) - d.exec_() - return - - if bulk or (bulk is None and len(rows) > 1): - return self.edit_bulk_metadata(checked) - - def accepted(id): - self.library_view.model().refresh_ids([id]) - - for row in rows: - self._metadata_view_id = self.library_view.model().db.id(row.row()) - d = MetadataSingleDialog(self, row.row(), - self.library_view.model().db, - accepted_callback=accepted, - cancel_all=rows.index(row) < len(rows)-1) - self.connect(d, SIGNAL('view_format(PyQt_PyObject)'), - self.metadata_view_format) - d.exec_() - if d.cancel_all: - break - if rows: - current = self.library_view.currentIndex() - m = self.library_view.model() - m.refresh_cover_cache(map(m.id, rows)) - if self.cover_flow: - self.cover_flow.dataChanged() - m.current_changed(current, previous) - self.tags_view.recount() - - def edit_bulk_metadata(self, checked): - ''' - Edit metadata of selected books in library in bulk. - ''' - rows = [r.row() for r in \ - self.library_view.selectionModel().selectedRows()] - if not rows or len(rows) == 0: - d = error_dialog(self, _('Cannot edit metadata'), - _('No books selected')) - d.exec_() - return - if MetadataBulkDialog(self, rows, - self.library_view.model().db).changed: - self.library_view.model().resort(reset=False) - self.library_view.model().research() - self.tags_view.recount() - - ############################################################################ - - ############################### Merge books ############################## - def merge_books(self, safe_merge=False): - ''' - Merge selected books in library. - ''' - if self.stack.currentIndex() != 0: - return - rows = self.library_view.selectionModel().selectedRows() - if not rows or len(rows) == 0: - return error_dialog(self, _('Cannot merge books'), - _('No books selected'), show=True) - if len(rows) < 2: - return error_dialog(self, _('Cannot merge books'), - _('At least two books must be selected for merging'), - show=True) - dest_id, src_books, src_ids = self.books_to_merge(rows) - if safe_merge: - if not confirm('

'+_( - 'All book formats and metadata from the selected books ' - 'will be added to the first selected book.

' - 'The second and subsequently selected books will not ' - 'be deleted or changed.

' - 'Please confirm you want to proceed.') - +'

', 'merge_books_safe', self): - return - self.add_formats(dest_id, src_books) - self.merge_metadata(dest_id, src_ids) - else: - if not confirm('

'+_( - 'All book formats and metadata from the selected books will be merged ' - 'into the first selected book.

' - 'After merger the second and ' - 'subsequently selected books will be deleted.

' - 'All book formats of the first selected book will be kept ' - 'and any duplicate formats in the second and subsequently selected books ' - 'will be permanently deleted from your computer.

' - 'Are you sure you want to proceed?') - +'

', 'merge_books', self): - return - if len(rows)>5: - if not confirm('

'+_('You are about to merge more than 5 books. ' - 'Are you sure you want to proceed?') - +'

', 'merge_too_many_books', self): - return - self.add_formats(dest_id, src_books) - self.merge_metadata(dest_id, src_ids) - self.delete_books_after_merge(src_ids) - # leave the selection highlight on first selected book - dest_row = rows[0].row() - for row in rows: - if row.row() < rows[0].row(): - dest_row -= 1 - ci = self.library_view.model().index(dest_row, 0) - if ci.isValid(): - self.library_view.setCurrentIndex(ci) - - def add_formats(self, dest_id, src_books, replace=False): - for src_book in src_books: - if src_book: - fmt = os.path.splitext(src_book)[-1].replace('.', '').upper() - with open(src_book, 'rb') as f: - self.library_view.model().db.add_format(dest_id, fmt, f, index_is_id=True, - notify=False, replace=replace) - - def books_to_merge(self, rows): - src_books = [] - src_ids = [] - m = self.library_view.model() - for i, row in enumerate(rows): - id_ = m.id(row) - if i == 0: - dest_id = id_ - else: - src_ids.append(id_) - dbfmts = m.db.formats(id_, index_is_id=True) - if dbfmts: - for fmt in dbfmts.split(','): - src_books.append(m.db.format_abspath(id_, fmt, - index_is_id=True)) - return [dest_id, src_books, src_ids] - - def delete_books_after_merge(self, ids_to_delete): - self.library_view.model().delete_books_by_id(ids_to_delete) - - def merge_metadata(self, dest_id, src_ids): - db = self.library_view.model().db - dest_mi = db.get_metadata(dest_id, index_is_id=True, get_cover=True) - orig_dest_comments = dest_mi.comments - for src_id in src_ids: - src_mi = db.get_metadata(src_id, index_is_id=True, get_cover=True) - if src_mi.comments and orig_dest_comments != src_mi.comments: - if not dest_mi.comments or len(dest_mi.comments) == 0: - dest_mi.comments = src_mi.comments - else: - dest_mi.comments = unicode(dest_mi.comments) + u'\n\n' + unicode(src_mi.comments) - if src_mi.title and src_mi.title and (not dest_mi.title or - dest_mi.title == _('Unknown')): - dest_mi.title = src_mi.title - if src_mi.title and (not dest_mi.authors or dest_mi.authors[0] == - _('Unknown')): - dest_mi.authors = src_mi.authors - dest_mi.author_sort = src_mi.author_sort - if src_mi.tags: - if not dest_mi.tags: - dest_mi.tags = src_mi.tags - else: - for tag in src_mi.tags: - dest_mi.tags.append(tag) - if src_mi.cover and not dest_mi.cover: - dest_mi.cover = src_mi.cover - if not dest_mi.publisher: - dest_mi.publisher = src_mi.publisher - if not dest_mi.rating: - dest_mi.rating = src_mi.rating - if not dest_mi.series: - dest_mi.series = src_mi.series - dest_mi.series_index = src_mi.series_index - db.set_metadata(dest_id, dest_mi, ignore_errors=False) - - ############################################################################ - - - ############################## Save to disk ################################ - def save_single_format_to_disk(self, checked): - self.save_to_disk(checked, False, prefs['output_format']) - - def save_specific_format_disk(self, fmt): - self.save_to_disk(False, False, fmt) - - def save_to_single_dir(self, checked): - self.save_to_disk(checked, True) - - def save_single_fmt_to_single_dir(self, *args): - self.save_to_disk(False, single_dir=True, - single_format=prefs['output_format']) - - def save_to_disk(self, checked, single_dir=False, single_format=None): - rows = self.current_view().selectionModel().selectedRows() - if not rows or len(rows) == 0: - return error_dialog(self, _('Cannot save to disk'), - _('No books selected'), show=True) - path = choose_dir(self, 'save to disk dialog', - _('Choose destination directory')) - if not path: - return - - if self.current_view() is self.library_view: - from calibre.gui2.add import Saver - from calibre.library.save_to_disk import config - opts = config().parse() - if single_format is not None: - opts.formats = single_format - # Special case for Kindle annotation files - if single_format.lower() in ['mbp','pdr','tan']: - opts.to_lowercase = False - opts.save_cover = False - opts.write_opf = False - opts.template = opts.send_template - if single_dir: - opts.template = opts.template.split('/')[-1].strip() - if not opts.template: - opts.template = '{title} - {authors}' - self._saver = Saver(self, self.library_view.model().db, - Dispatcher(self._books_saved), rows, path, opts, - spare_server=self.spare_server) - - else: - paths = self.current_view().model().paths(rows) - self.device_manager.save_books( - Dispatcher(self.books_saved), paths, path) - - - def _books_saved(self, path, failures, error): - self._saver = None - if error: - return error_dialog(self, _('Error while saving'), - _('There was an error while saving.'), - error, show=True) - if failures: - failures = [u'%s\n\t%s'% - (title, '\n\t'.join(err.splitlines())) for title, err in - failures] - - warning_dialog(self, _('Could not save some books'), - _('Could not save some books') + ', ' + - _('Click the show details button to see which ones.'), - u'\n\n'.join(failures), show=True) - QDesktopServices.openUrl(QUrl.fromLocalFile(path)) - - def books_saved(self, job): - if job.failed: - return self.device_job_exception(job) - - ############################################################################ - - ############################### Generate catalog ########################### - - def generate_catalog(self): - rows = self.library_view.selectionModel().selectedRows() - if not rows or len(rows) < 2: - rows = xrange(self.library_view.model().rowCount(QModelIndex())) - ids = map(self.library_view.model().id, rows) - - dbspec = None - if not ids: - return error_dialog(self, _('No books selected'), - _('No books selected to generate catalog for'), - show=True) - - # Calling gui2.tools:generate_catalog() - ret = generate_catalog(self, dbspec, ids, self.device_manager.device) - if ret is None: - return - - func, args, desc, out, sync, title = ret - - fmt = os.path.splitext(out)[1][1:].upper() - job = self.job_manager.run_job( - Dispatcher(self.catalog_generated), func, args=args, - description=desc) - job.catalog_file_path = out - job.fmt = fmt - job.catalog_sync, job.catalog_title = sync, title - self.status_bar.show_message(_('Generating %s catalog...')%fmt) - - def catalog_generated(self, job): - if job.result: - # Search terms nulled catalog results - return error_dialog(self, _('No books found'), - _("No books to catalog\nCheck exclude tags"), - show=True) - if job.failed: - return self.job_exception(job) - id = self.library_view.model().add_catalog(job.catalog_file_path, job.catalog_title) - self.library_view.model().reset() - if job.catalog_sync: - sync = dynamic.get('catalogs_to_be_synced', set([])) - sync.add(id) - dynamic.set('catalogs_to_be_synced', sync) - self.status_bar.show_message(_('Catalog generated.'), 3000) - self.sync_catalogs() - if job.fmt not in ['EPUB','MOBI']: - export_dir = choose_dir(self, _('Export Catalog Directory'), - _('Select destination for %s.%s') % (job.catalog_title, job.fmt.lower())) - if export_dir: - destination = os.path.join(export_dir, '%s.%s' % (job.catalog_title, job.fmt.lower())) - shutil.copyfile(job.catalog_file_path, destination) - - ############################### Fetch news ################################# - - def download_scheduled_recipe(self, arg): - func, args, desc, fmt, temp_files = \ - fetch_scheduled_recipe(arg) - job = self.job_manager.run_job( - Dispatcher(self.scheduled_recipe_fetched), func, args=args, - description=desc) - self.conversion_jobs[job] = (temp_files, fmt, arg) - self.status_bar.show_message(_('Fetching news from ')+arg['title'], 2000) - - def scheduled_recipe_fetched(self, job): - temp_files, fmt, arg = self.conversion_jobs.pop(job) - pt = temp_files[0] - if job.failed: - self.scheduler.recipe_download_failed(arg) - return self.job_exception(job) - id = self.library_view.model().add_news(pt.name, arg) - self.library_view.model().reset() - sync = dynamic.get('news_to_be_synced', set([])) - sync.add(id) - dynamic.set('news_to_be_synced', sync) - self.scheduler.recipe_downloaded(arg) - self.status_bar.show_message(arg['title'] + _(' fetched.'), 3000) - self.email_news(id) - self.sync_news() - - ############################################################################ - - ############################### Convert #################################### - - def auto_convert(self, book_ids, on_card, format): - previous = self.library_view.currentIndex() - rows = [x.row() for x in \ - self.library_view.selectionModel().selectedRows()] - jobs, changed, bad = convert_single_ebook(self, self.library_view.model().db, book_ids, True, format) - if jobs == []: return - self.queue_convert_jobs(jobs, changed, bad, rows, previous, - self.book_auto_converted, extra_job_args=[on_card]) - - def auto_convert_mail(self, to, fmts, delete_from_library, book_ids, format): - previous = self.library_view.currentIndex() - rows = [x.row() for x in \ - self.library_view.selectionModel().selectedRows()] - jobs, changed, bad = convert_single_ebook(self, self.library_view.model().db, book_ids, True, format) - if jobs == []: return - self.queue_convert_jobs(jobs, changed, bad, rows, previous, - self.book_auto_converted_mail, - extra_job_args=[delete_from_library, to, fmts]) - - def auto_convert_news(self, book_ids, format): - previous = self.library_view.currentIndex() - rows = [x.row() for x in \ - self.library_view.selectionModel().selectedRows()] - jobs, changed, bad = convert_single_ebook(self, self.library_view.model().db, book_ids, True, format) - if jobs == []: return - self.queue_convert_jobs(jobs, changed, bad, rows, previous, - self.book_auto_converted_news) - - def auto_convert_catalogs(self, book_ids, format): - previous = self.library_view.currentIndex() - rows = [x.row() for x in \ - self.library_view.selectionModel().selectedRows()] - jobs, changed, bad = convert_single_ebook(self, self.library_view.model().db, book_ids, True, format) - if jobs == []: return - self.queue_convert_jobs(jobs, changed, bad, rows, previous, - self.book_auto_converted_catalogs) - - - - def get_books_for_conversion(self): - rows = [r.row() for r in \ - self.library_view.selectionModel().selectedRows()] - if not rows or len(rows) == 0: - d = error_dialog(self, _('Cannot convert'), - _('No books selected')) - d.exec_() - return None - return [self.library_view.model().db.id(r) for r in rows] - - def convert_ebook(self, checked, bulk=None): - book_ids = self.get_books_for_conversion() - if book_ids is None: return - previous = self.library_view.currentIndex() - rows = [x.row() for x in \ - self.library_view.selectionModel().selectedRows()] - num = 0 - if bulk or (bulk is None and len(book_ids) > 1): - self.__bulk_queue = convert_bulk_ebook(self, self.queue_convert_jobs, - self.library_view.model().db, book_ids, - out_format=prefs['output_format'], args=(rows, previous, - self.book_converted)) - if self.__bulk_queue is None: - return - num = len(self.__bulk_queue.book_ids) - else: - jobs, changed, bad = convert_single_ebook(self, - self.library_view.model().db, book_ids, out_format=prefs['output_format']) - self.queue_convert_jobs(jobs, changed, bad, rows, previous, - self.book_converted) - num = len(jobs) - - if num > 0: - self.status_bar.show_message(_('Starting conversion of %d book(s)') % - num, 2000) - - def queue_convert_jobs(self, jobs, changed, bad, rows, previous, - converted_func, extra_job_args=[]): - for func, args, desc, fmt, id, temp_files in jobs: - if id not in bad: - job = self.job_manager.run_job(Dispatcher(converted_func), - func, args=args, description=desc) - args = [temp_files, fmt, id]+extra_job_args - self.conversion_jobs[job] = tuple(args) - - if changed: - self.library_view.model().refresh_rows(rows) - current = self.library_view.currentIndex() - self.library_view.model().current_changed(current, previous) - - def book_auto_converted(self, job): - temp_files, fmt, book_id, on_card = self.conversion_jobs[job] - self.book_converted(job) - self.sync_to_device(on_card, False, specific_format=fmt, send_ids=[book_id], do_auto_convert=False) - - def book_auto_converted_mail(self, job): - temp_files, fmt, book_id, delete_from_library, to, fmts = self.conversion_jobs[job] - self.book_converted(job) - self.send_by_mail(to, fmts, delete_from_library, specific_format=fmt, send_ids=[book_id], do_auto_convert=False) - - def book_auto_converted_news(self, job): - temp_files, fmt, book_id = self.conversion_jobs[job] - self.book_converted(job) - self.sync_news(send_ids=[book_id], do_auto_convert=False) - - def book_auto_converted_catalogs(self, job): - temp_files, fmt, book_id = self.conversion_jobs[job] - self.book_converted(job) - self.sync_catalogs(send_ids=[book_id], do_auto_convert=False) - - def book_converted(self, job): - temp_files, fmt, book_id = self.conversion_jobs.pop(job)[:3] - try: - if job.failed: - self.job_exception(job) - return - data = open(temp_files[-1].name, 'rb') - self.library_view.model().db.add_format(book_id, \ - fmt, data, index_is_id=True) - data.close() - self.status_bar.show_message(job.description + \ - (' completed'), 2000) - finally: - for f in temp_files: - try: - if os.path.exists(f.name): - os.remove(f.name) - except: - pass - self.tags_view.recount() - if self.current_view() is self.library_view: - current = self.library_view.currentIndex() - self.library_view.model().current_changed(current, QModelIndex()) - - #############################View book###################################### - - def view_format(self, row, format): - fmt_path = self.library_view.model().db.format_abspath(row, format) - if fmt_path: - self._view_file(fmt_path) - - def metadata_view_format(self, fmt): - fmt_path = self.library_view.model().db.\ - format_abspath(self._metadata_view_id, - fmt, index_is_id=True) - if fmt_path: - self._view_file(fmt_path) - - - def book_downloaded_for_viewing(self, job): - if job.failed: - self.device_job_exception(job) - return - self._view_file(job.result) - - def _launch_viewer(self, name=None, viewer='ebook-viewer', internal=True): - self.setCursor(Qt.BusyCursor) - try: - if internal: - args = [viewer] - if isosx and 'ebook' in viewer: - args.append('--raise-window') - if name is not None: - args.append(name) - self.job_manager.launch_gui_app(viewer, - kwargs=dict(args=args)) - else: - paths = os.environ.get('LD_LIBRARY_PATH', - '').split(os.pathsep) - paths = [x for x in paths if x] - if isfrozen and islinux and paths: - npaths = [x for x in paths if x != sys.frozen_path] - os.environ['LD_LIBRARY_PATH'] = os.pathsep.join(npaths) - QDesktopServices.openUrl(QUrl.fromLocalFile(name))#launch(name) - if isfrozen and islinux and paths: - os.environ['LD_LIBRARY_PATH'] = os.pathsep.join(paths) - time.sleep(2) # User feedback - finally: - self.unsetCursor() - - def _view_file(self, name): - ext = os.path.splitext(name)[1].upper().replace('.', '') - viewer = 'lrfviewer' if ext == 'LRF' else 'ebook-viewer' - internal = ext in config['internally_viewed_formats'] - self._launch_viewer(name, viewer, internal) - - def view_specific_format(self, triggered): - rows = self.library_view.selectionModel().selectedRows() - if not rows or len(rows) == 0: - d = error_dialog(self, _('Cannot view'), _('No book selected')) - d.exec_() - return - - row = rows[0].row() - formats = self.library_view.model().db.formats(row).upper().split(',') - d = ChooseFormatDialog(self, _('Choose the format to view'), formats) - if d.exec_() == QDialog.Accepted: - format = d.format() - self.view_format(row, format) - - def _view_check(self, num, max_=3): - if num <= max_: - return True - return question_dialog(self, _('Multiple Books Selected'), - _('You are attempting to open %d books. Opening too many ' - 'books at once can be slow and have a negative effect on the ' - 'responsiveness of your computer. Once started the process ' - 'cannot be stopped until complete. Do you wish to continue?' - ) % num) - - def view_folder(self, *args): - rows = self.current_view().selectionModel().selectedRows() - if not rows or len(rows) == 0: - d = error_dialog(self, _('Cannot open folder'), - _('No book selected')) - d.exec_() - return - if not self._view_check(len(rows)): - return - for row in rows: - path = self.library_view.model().db.abspath(row.row()) - QDesktopServices.openUrl(QUrl.fromLocalFile(path)) - - - def view_book(self, triggered): - rows = self.current_view().selectionModel().selectedRows() - self._view_books(rows) - - def view_specific_book(self, index): - self._view_books([index]) - - def _view_books(self, rows): - if not rows or len(rows) == 0: - self._launch_viewer() - return - - if not self._view_check(len(rows)): - return - - if self.current_view() is self.library_view: - for row in rows: - if hasattr(row, 'row'): - row = row.row() - - formats = self.library_view.model().db.formats(row) - title = self.library_view.model().db.title(row) - if not formats: - error_dialog(self, _('Cannot view'), - _('%s has no available formats.')%(title,), show=True) - continue - - formats = formats.upper().split(',') - - - in_prefs = False - for format in prefs['input_format_order']: - if format in formats: - in_prefs = True - self.view_format(row, format) - break - if not in_prefs: - self.view_format(row, formats[0]) - else: - paths = self.current_view().model().paths(rows) - for path in paths: - pt = PersistentTemporaryFile('_viewer_'+\ - os.path.splitext(path)[1]) - self.persistent_files.append(pt) - pt.close() - self.device_manager.view_book(\ - Dispatcher(self.book_downloaded_for_viewing), - path, pt.name) - - - ############################################################################ - - ############################################################################ - - ############################### Do config ################################## - def do_config(self, *args): if self.job_manager.has_jobs(): d = error_dialog(self, _('Cannot configure'), @@ -1629,10 +434,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, self.library_view.model().count_changed() prefs['library_path'] = self.library_path - ############################################################################ - - ################################ Book info ################################# - def show_book_info(self, *args): if self.current_view() is not self.library_view: error_dialog(self, _('No detailed info available'), @@ -1643,9 +444,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, if index.isValid(): BookInfo(self, self.library_view, index).show() - ############################################################################ - - ############################################################################ def location_selected(self, location): ''' Called when a location icon is clicked (e.g. Library) @@ -1858,3 +656,5 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, else: e.ignore() + # }}} + diff --git a/src/calibre/gui2/update.py b/src/calibre/gui2/update.py index 18b6b52d79..9dcd4d9084 100644 --- a/src/calibre/gui2/update.py +++ b/src/calibre/gui2/update.py @@ -37,7 +37,7 @@ class CheckForUpdates(QThread): traceback.print_exc() self.sleep(self.INTERVAL) -def UpdateMixin(object): +class UpdateMixin(object): def __init__(self, opts): if not opts.no_update_check: From 48458ac7870ef9b36b2052f30564320d24634c55 Mon Sep 17 00:00:00 2001 From: GRiker Date: Sat, 12 Jun 2010 10:19:47 -0600 Subject: [PATCH 05/13] GwR revisions --- src/calibre/devices/apple/driver.py | 257 ++++++++-------------------- src/calibre/web/feeds/news.py | 5 - src/calibre/web/feeds/templates.py | 13 +- 3 files changed, 76 insertions(+), 199 deletions(-) diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index 3e16eccbbc..ae440a359e 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -76,7 +76,7 @@ class ITUNES(DevicePlugin): supported_platforms = ['osx','windows'] author = 'GRiker' #: The version of this plugin as a 3-tuple (major, minor, revision) - version = (0, 5, 0) + version = (0,6,0) OPEN_FEEDBACK_MESSAGE = _( 'Apple device detected, launching iTunes, please wait ...') @@ -280,7 +280,7 @@ class ITUNES(DevicePlugin): if self.report_progress is not None: self.report_progress(i+1/book_count, _('%d of %d') % (i+1, book_count)) - self._purge_orphans(cached_books) + self._purge_orphans(library_books, cached_books) elif iswindows: try: @@ -316,7 +316,7 @@ class ITUNES(DevicePlugin): if self.report_progress is not None: self.report_progress(i+1/book_count, _('%d of %d') % (i+1, book_count)) - self._purge_orphans(cached_books) + self._purge_orphans(library_books, cached_books) finally: pythoncom.CoUninitialize() @@ -324,9 +324,9 @@ class ITUNES(DevicePlugin): if self.report_progress is not None: self.report_progress(1.0, _('finished')) self.cached_books = cached_books - if DEBUG: - self._dump_booklist(booklist, 'returning from books():') - self._dump_cached_books('returning from books():') +# if DEBUG: +# self._dump_booklist(booklist, 'returning from books():') +# self._dump_cached_books('returning from books():') return booklist else: return [] @@ -463,7 +463,7 @@ class ITUNES(DevicePlugin): else: # iTunes running, but not connected iPad if DEBUG: - self.log.info(' self.ejected = True') + self.log.info(' iDevice has been ejected') self.ejected = True return False @@ -782,121 +782,6 @@ class ITUNES(DevicePlugin): # self._dump_cached_books('upload_books()') self._dump_update_list('upload_books()') - ''' - if isosx: - - for (i,file) in enumerate(files): - path = self.path_template % (metadata[i].title, metadata[i].author[0]) - - if self.manual_sync_mode: - # Delete existing from Device|Books, add to self.update_list - # for deletion from booklist[0] during add_books_to_metadata - if path in self.cached_books: - self.update_list.append(self.cached_books[path]) - if DEBUG: - self.log.info(" adding '%s' by %s to self.update_list" % - (self.cached_books[path]['title'],self.cached_books[path]['author'])) - - if DEBUG: - self.log.info( " deleting existing '%s'" % (path)) - self._remove_from_iTunes(self.cached_books[path]) - if self.manual_sync_mode: - dev_book_added = self._remove_from_device(self.cached_books[path]) - - - # Add to iTunes Library|Books - fpath = file - if getattr(file, 'orig_file_path', None) is not None: - fpath = file.orig_file_path - elif getattr(file, 'name', None) is not None: - fpath = file.name - - if isinstance(file,PersistentTemporaryFile) and self.manual_sync_mode: - if DEBUG: - self.log.info(" PTF not added to Library|Books") - else: - added = self.iTunes.add(appscript.mactypes.File(fpath)) - if DEBUG: - self.log.info(" file added to Library|Books") - - dev_book_added = None - if self.manual_sync_mode: - dev_book_added = self._add_device_book(fpath) - - thumb = None - if metadata[i].cover: - try: - # Use cover data as artwork - cover_data = open(metadata[i].cover,'rb') - added.artworks[1].data_.set(cover_data.read()) - - # Resize for thumb - width = metadata[i].thumbnail[0] - height = metadata[i].thumbnail[1] - im = PILImage.open(metadata[i].cover) - im = im.resize((width, height), PILImage.ANTIALIAS) - of = cStringIO.StringIO() - im.convert('RGB').save(of, 'JPEG') - thumb = of.getvalue() - - # Refresh the thumbnail cache - if DEBUG: - self.log.info( " refreshing cached thumb for '%s'" % metadata[i].title) - archive_path = os.path.join(self.cache_dir, "thumbs.zip") - zfw = zipfile.ZipFile(archive_path, mode='a') - thumb_path = path.rpartition('.')[0] + '.jpg' - zfw.writestr(thumb_path, thumb) - zfw.close() - except: - self.problem_titles.append("'%s' by %s" % (metadata[i].title, metadata[i].author[0])) - self.log.error("ITUNES.upload_books(): error converting '%s' to thumb for '%s'" % (metadata[i].cover,metadata[i].title)) - - # Create a new Book - this_book = Book(metadata[i].title, metadata[i].author[0]) - try: - this_book.datetime = parse_date(str(added.date_added())).timetuple() - except: - pass - this_book.db_id = None - this_book.device_collections = [] - this_book.library_id = added - this_book.path = path - this_book.size = self._get_device_book_size(fpath, added.size()) - this_book.thumbnail = thumb - this_book.iTunes_id = added - - new_booklist.append(this_book) - - # Populate the iTunes metadata - if metadata[i].comments: - added.comment.set(strip_tags.sub('',metadata[i].comments)) - added.description.set("added by calibre %s" % strftime('%Y-%m-%d %H:%M:%S')) - added.enabled.set(True) - if metadata[i].rating: - added.rating.set(metadata[i].rating*10) - added.sort_artist.set(metadata[i].author_sort.title()) - added.sort_name.set(this_book.title_sorter) - - # Set genre from metadata - # iTunes grabs the first dc:subject from the opf metadata, - # But we can manually override with first tag starting with alpha - for tag in metadata[i].tags: - if self._is_alpha(tag[0]): - added.genre.set(tag) - break - - # Add new_book to self.cached_paths - self.cached_books[this_book.path] = { - 'title': this_book.title, - 'author': this_book.author, - 'lib_book': added, - 'dev_book': dev_book_added - } - - # Report progress - if self.report_progress is not None: - self.report_progress(i+1/file_count, _('%d of %d') % (i+1, file_count)) - ''' if isosx: for (i,file) in enumerate(files): path = self.path_template % (metadata[i].title, metadata[i].author[0]) @@ -1378,6 +1263,15 @@ class ITUNES(DevicePlugin): self.log.info(" %s" % file.name) self.log.info() + def _dump_library_books(self, library_books): + ''' + ''' + if DEBUG: + self.log.info("\n library_books:") + for book in library_books: + self.log.info(" %s" % book) + self.log.info() + def _dump_update_list(self,header=None): if header: msg = '\nself.update_list called from %s' % header @@ -1590,7 +1484,7 @@ class ITUNES(DevicePlugin): self.log.info(" ignoring '%s' of type '%s'" % (book.name(), book.kind())) else: if DEBUG: - self.log.info(" adding %-30.30s [%s]" % (book.name(), book.kind())) + self.log.info(" adding %-30.30s %-30.30s [%s]" % (book.name(), book.artist(), book.kind())) device_books.append(book) elif iswindows: @@ -1619,7 +1513,7 @@ class ITUNES(DevicePlugin): self.log.info(" ignoring '%s' of type '%s'" % (book.Name, book.KindAsString)) else: if DEBUG: - self.log.info(" adding %-30.30s [%s]" % (book.Name, book.KindAsString)) + self.log.info(" adding %-30.30s %-30.30s [%s]" % (book.Name, book.Artist, book.KindAsString)) device_books.append(book) finally: @@ -1716,11 +1610,11 @@ class ITUNES(DevicePlugin): if book.location() == appscript.k.missing_value: library_orphans[path] = book if DEBUG: - self.log.info(" found calibre orphan '%s' in Library|Books" % book.name()) + self.log.info(" found iTunes PTF '%s' in Library|Books" % book.name()) library_books[path] = book if DEBUG: - self.log.info(" adding %-30.30s [%s]" % (book.name(), book.kind())) + self.log.info(" adding %-30.30s %-30.30s [%s]" % (book.name(), book.artist(), book.kind())) else: if DEBUG: self.log.info(' no Library playlists') @@ -1730,9 +1624,6 @@ class ITUNES(DevicePlugin): elif iswindows: lib = None -# try: -# pythoncom.CoInitialize() -# self.iTunes = win32com.client.Dispatch("iTunes.Application") for source in self.iTunes.sources: if source.Kind == self.Sources.index('Library'): lib = source @@ -1772,16 +1663,14 @@ class ITUNES(DevicePlugin): if not book.Location: library_orphans[path] = book if DEBUG: - self.log.info(" found calibre orphan '%s' in Library|Books" % book.Name) + self.log.info(" found iTunes PTF '%s' in Library|Books" % book.Name) library_books[path] = book if DEBUG: - self.log.info(" adding %-30.30s [%s]" % (book.Name, book.KindAsString)) + self.log.info(" adding %-30.30s %-30.30s [%s]" % (book.Name, book.Artist, book.KindAsString)) except: if DEBUG: self.log.info(" no books in library") -# finally: -# pythoncom.CoUninitialize() self.library_orphans = library_orphans return library_books @@ -1905,44 +1794,36 @@ class ITUNES(DevicePlugin): self.version[0],self.version[1],self.version[2])) self.log.info(" iTunes_media: %s" % self.iTunes_media) - def _purge_orphans(self,cached_books): + def _purge_orphans(self,library_books, cached_books): ''' - Scan self.library_orphans for any paths not on device - Remove any true orphans from iTunes - This occurs when recipes are uploaded in a previous session - and the book has since been deleted on the device + Scan library_books for any paths not on device + Remove any iTunes orphans originally added by calibre + This occurs when the user deletes a book in iBooks while disconnected ''' if DEBUG: - self.log.info(" ITUNES._purge_orphans") + self.log.info("\n ITUNES._purge_orphans") + #self._dump_library_books(library_books) #self.log.info(" cached_books:\n %s" % "\n ".join(cached_books.keys())) - orphan_paths = {} - - if isosx: - for orphan in self.library_orphans: - path = self.path_template % (self.library_orphans[orphan].name(), - self.library_orphans[orphan].artist()) - orphan_paths[path] = self.library_orphans[orphan] - - # Scan orphan_paths for paths not found in cached_books - for orphan in orphan_paths.keys(): - if orphan not in cached_books: + for book in library_books: + if isosx: + if book not in cached_books and \ + str(library_books[book].description()).startswith(self.description_prefix): if DEBUG: - self.log.info(" '%s' not found on device, removing from iTunes" % orphan) - self.iTunes.delete(orphan_paths[orphan]) - - elif iswindows: - for orphan in self.library_orphans: - path = self.path_template % (self.library_orphans[orphan].Name, - self.library_orphans[orphan].Artist) - orphan_paths[path] = self.library_orphans[orphan] - - # Scan orphan_paths for paths not found in cached_books - for orphan in orphan_paths.keys(): - if orphan not in cached_books: + self.log.info(" '%s' not found on iDevice, removing from iTunes" % book) + btr = { 'title':library_books[book].name(), + 'author':library_books[book].artist(), + 'lib_book':library_books[book]} + self._remove_from_iTunes(btr) + elif iswindows: + if book not in cached_books and \ + library_books[book].Description.startswith(self.description_prefix): if DEBUG: - self.log.info(" '%s' not found on device, removing from iTunes" % orphan) - orphan_paths[orphan].Delete() + self.log.info(" '%s' not found on iDevice, removing from iTunes" % book) + btr = { 'title':library_books[book].Name, + 'author':library_books[book].Artist, + 'lib_book':library_books[book]} + self._remove_from_iTunes(btr) def _remove_existing_copies(self,path,file,metadata): ''' @@ -2040,7 +1921,7 @@ class ITUNES(DevicePlugin): except: # We get here if there was an error with .location().path - self.log.info(" removing orphan '%s' from iTunes" % cached_book['title']) + self.log.info(" removing orphan '%s' from iTunes" % cached_book['title']) self.iTunes.delete(cached_book['lib_book']) @@ -2049,33 +1930,33 @@ class ITUNES(DevicePlugin): Assume we're wrapped in a pythoncom Windows stores the book under a common author directory, so we just delete the .epub ''' - - book = self._find_library_book(cached_book) - if book: + try: + book = cached_book['lib_book'] + path = book.Location + except: + book = self._find_library_book(cached_book) path = book.Location - storage_path = os.path.split(book.Location) - if book.Location.startswith(self.iTunes_media): - if DEBUG: - self.log.info(" removing '%s' at %s" % - (cached_book['title'], path)) - try: - os.remove(path) - except: - self.log.warning(" could not find '%s' in iTunes storage" % path) - try: - os.rmdir(storage_path[0]) - self.log.info(" removed folder '%s'" % storage_path[0]) - except: - self.log.info(" folder '%s' not found or not empty" % storage_path[0]) - # Delete from iTunes database - else: - self.log.info(" '%s' stored external to iTunes, no files deleted" % cached_book['title']) - - book.Delete() + storage_path = os.path.split(book.Location) + if book.Location.startswith(self.iTunes_media): + if DEBUG: + self.log.info(" removing '%s' at %s" % + (cached_book['title'], path)) + try: + os.remove(path) + except: + self.log.warning(" could not find '%s' in iTunes storage" % path) + try: + os.rmdir(storage_path[0]) + self.log.info(" removed folder '%s'" % storage_path[0]) + except: + self.log.info(" folder '%s' not found or not empty" % storage_path[0]) + # Delete from iTunes database else: - self.log.warning(" could not find '%s' in iTunes database" % cached_book['title']) + self.log.info(" '%s' stored external to iTunes, no files deleted" % cached_book['title']) + + book.Delete() def _update_device(self, msg='', wait=True): ''' diff --git a/src/calibre/web/feeds/news.py b/src/calibre/web/feeds/news.py index 8977f64d60..f54d5bde9d 100644 --- a/src/calibre/web/feeds/news.py +++ b/src/calibre/web/feeds/news.py @@ -801,11 +801,6 @@ class BasicNewsRecipe(Recipe): .calibre_navbar { font-family:monospace; } - hr { - border-color:gray; - border-style:solid; - border-width:thin; - } ''' diff --git a/src/calibre/web/feeds/templates.py b/src/calibre/web/feeds/templates.py index b64795b816..d23596a274 100644 --- a/src/calibre/web/feeds/templates.py +++ b/src/calibre/web/feeds/templates.py @@ -108,7 +108,7 @@ class TouchscreenNavBarTemplate(Template): navbar = DIV(CLASS('calibre_navbar', 'calibre_rescale_100', style='text-align:'+align)) if bottom: - navbar.append(HR()) + navbar.append(DIV(style="border-top:1px solid gray;border-bottom:1em solid white")) text = 'This article was downloaded by ' p = PT(text, STRONG(__appname__), A(url, href=url), style='text-align:left') p[0].tail = ' from ' @@ -136,7 +136,7 @@ class TouchscreenNavBarTemplate(Template): navbar.iterchildren(reversed=True).next().tail = ' | ' if not bottom: - navbar.append(HR()) + navbar.append(DIV(style="border-top:1px solid gray;border-bottom:1em solid white")) self.root = HTML(head, BODY(navbar)) @@ -193,6 +193,8 @@ class TouchscreenIndexTemplate(Template): div = DIV( masthead_p, PT(date, style='text-align:center'), + #DIV(style="border-color:gray;border-top-style:solid;border-width:thin"), + DIV(style="border-top:1px solid gray;border-bottom:1em solid white"), toc) self.root = HTML(head, BODY(div)) @@ -256,10 +258,9 @@ class TouchscreenFeedTemplate(Template): head.append(STYLE(extra_css, type='text/css')) body = BODY(style='page-break-before:always') div = DIV( - H2(feed.title, - CLASS('calibre_feed_title', 'calibre_rescale_160')), - CLASS('calibre_rescale_100') - ) + H2(feed.title, CLASS('calibre_feed_title', 'calibre_rescale_160')), + DIV(style="border-top:1px solid gray;border-bottom:1em solid white") + ) body.append(div) if getattr(feed, 'image', None): div.append(DIV(IMG( From 421f2ebc902b10590c033764e9401b4afa41ea6e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 12 Jun 2010 10:52:40 -0600 Subject: [PATCH 06/13] ... --- src/calibre/gui2/actions.py | 3 +++ src/calibre/gui2/ui.py | 13 +++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/calibre/gui2/actions.py b/src/calibre/gui2/actions.py index cabc17cd00..4b2e367080 100644 --- a/src/calibre/gui2/actions.py +++ b/src/calibre/gui2/actions.py @@ -251,6 +251,9 @@ class AnnotationsAction(object): # {{{ class AddAction(object): # {{{ + def __init__(self): + self._add_filesystem_book = Dispatcher(self.__add_filesystem_book) + def add_recursive(self, single): root = choose_dir(self, 'recursive book import root dir dialog', 'Select root folder') diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index dcc1470bed..682ede1978 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -13,7 +13,7 @@ import collections, os, sys, textwrap, time from Queue import Queue, Empty from threading import Thread from PyQt4.Qt import Qt, SIGNAL, QObject, QUrl, QTimer, \ - QPixmap, QMenu, QIcon, \ + QPixmap, QMenu, QIcon, pyqtSignal, \ QDialog, QDesktopServices, \ QSystemTrayIcon, QApplication, QKeySequence, QAction, \ QMessageBox, QHelpEvent @@ -24,7 +24,7 @@ from calibre.ptempfile import PersistentTemporaryFile from calibre.utils.config import prefs, dynamic from calibre.utils.ipc.server import Server from calibre.gui2 import error_dialog, GetMetadata, \ - Dispatcher, gprefs, max_available_height, config, info_dialog + gprefs, max_available_height, config, info_dialog from calibre.gui2.cover_flow import CoverFlowMixin from calibre.gui2.widgets import ProgressIndicator from calibre.gui2.wizard import move_library @@ -77,13 +77,15 @@ class Listener(Thread): # {{{ class SystemTrayIcon(QSystemTrayIcon): # {{{ + tooltip_requested = pyqtSignal(object) + def __init__(self, icon, parent): QSystemTrayIcon.__init__(self, icon, parent) def event(self, ev): if ev.type() == ev.ToolTip: evh = QHelpEvent(ev) - self.emit(SIGNAL('tooltip_requested(PyQt_PyObject)'), + self.tooltip_requested.emit( (self, evh.globalPos())) return True return QSystemTrayIcon.event(self, ev) @@ -149,8 +151,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{ self.content_server = None self.system_tray_icon = SystemTrayIcon(QIcon(I('library.png')), self) self.system_tray_icon.setToolTip('calibre') - self.connect(self.system_tray_icon, - SIGNAL('tooltip_requested(PyQt_PyObject)'), + self.system_tray_icon.tooltip_requested.connect( self.job_manager.show_tooltip) if not config['systray_icon']: self.system_tray_icon.hide() @@ -292,8 +293,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{ self.location_view.setCurrentIndex(self.location_view.model().index(0)) - self._add_filesystem_book = Dispatcher(self.__add_filesystem_book) self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection) + AddAction.__init__(self) self.read_settings() self.finalize_layout() From ea2d2139c9cf8d798c662bf395eeab576121f1a8 Mon Sep 17 00:00:00 2001 From: GRiker Date: Sat, 12 Jun 2010 11:27:10 -0600 Subject: [PATCH 07/13] GwR revisions --- src/calibre/web/feeds/templates.py | 38 +++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/src/calibre/web/feeds/templates.py b/src/calibre/web/feeds/templates.py index d23596a274..7ebf7294ae 100644 --- a/src/calibre/web/feeds/templates.py +++ b/src/calibre/web/feeds/templates.py @@ -279,17 +279,33 @@ class TouchscreenFeedTemplate(Template): if not getattr(article, 'downloaded', False): continue tr = TR() - td = TD( - A(article.title, CLASS('summary_headline','calibre_rescale_120', - href=article.url)) - ) - if article.author: - td.append(DIV(article.author, - CLASS('summary_byline', 'calibre_rescale_100'))) - if article.summary: - td.append(DIV(cutoff(article.text_summary), - CLASS('summary_text', 'calibre_rescale_100'))) - tr.append(td) + + if True: + div_td = DIV( + A(article.title, CLASS('summary_headline','calibre_rescale_120', + href=article.url)), + style="display:inline-block") + if article.author: + div_td.append(DIV(article.author, + CLASS('summary_byline', 'calibre_rescale_100'))) + if article.summary: + div_td.append(DIV(cutoff(article.text_summary), + CLASS('summary_text', 'calibre_rescale_100'))) + tr.append(TD(div_td)) + else: + td = TD( + A(article.title, CLASS('summary_headline','calibre_rescale_120', + href=article.url)) + ) + if article.author: + td.append(DIV(article.author, + CLASS('summary_byline', 'calibre_rescale_100'))) + if article.summary: + td.append(DIV(cutoff(article.text_summary), + CLASS('summary_text', 'calibre_rescale_100'))) + + tr.append(td) + toc.append(tr) div.append(toc) From 2511b5d13571a3ef8fe95705d6f6d2d95c9823f5 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 12 Jun 2010 15:35:37 -0600 Subject: [PATCH 08/13] Cover Browser: Scale text size with height of cover browser. Only show a reflection of half the cover. Also Fix #5808 (Cover browser in 0.7.2 now has reduced quality images.) --- src/calibre/gui2/cover_flow.py | 1 - src/calibre/gui2/pictureflow/pictureflow.cpp | 36 ++++++++++++++------ 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/calibre/gui2/cover_flow.py b/src/calibre/gui2/cover_flow.py index f06d912a5d..6a9709cd8b 100644 --- a/src/calibre/gui2/cover_flow.py +++ b/src/calibre/gui2/cover_flow.py @@ -83,7 +83,6 @@ if pictureflow is not None: self.setFocusPolicy(Qt.WheelFocus) self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) - self.setZoomFactor(150) def sizeHint(self): return self.minimumSize() diff --git a/src/calibre/gui2/pictureflow/pictureflow.cpp b/src/calibre/gui2/pictureflow/pictureflow.cpp index 60985a1a12..58b6cd32e0 100644 --- a/src/calibre/gui2/pictureflow/pictureflow.cpp +++ b/src/calibre/gui2/pictureflow/pictureflow.cpp @@ -85,7 +85,9 @@ typedef long PFreal; typedef unsigned short QRgb565; -#define FONT_SIZE 18 +#define REFLECTION_FACTOR 1.5 + +#define MAX(x, y) ((x > y) ? x : y) #define RGB565_RED_MASK 0xF800 #define RGB565_GREEN_MASK 0x07E0 @@ -124,6 +126,7 @@ inline PFreal floatToFixed(float val) return (PFreal)(val*PFREAL_ONE); } +// sinTable {{{ #define IANGLE_MAX 1024 #define IANGLE_MASK 1023 @@ -293,6 +296,7 @@ int main(int, char**) return 0; } #endif +// }}} inline PFreal fsin(int iangle) { @@ -315,6 +319,8 @@ struct SlideInfo PFreal cy; }; +// PicturePlowPrivate {{{ + class PictureFlowPrivate { public: @@ -369,6 +375,7 @@ private: int slideWidth; int slideHeight; + int fontSize; int zoom; int queueLength; @@ -406,6 +413,7 @@ PictureFlowPrivate::PictureFlowPrivate(PictureFlow* w, int queueLength_) slideWidth = 200; slideHeight = 200; + fontSize = 10; zoom = 100; centerIndex = 0; @@ -542,8 +550,11 @@ void PictureFlowPrivate::showSlide(int index) void PictureFlowPrivate::resize(int w, int h) { - slideHeight = int(float(h)/2.); + if (w < 10) w = 10; + if (h < 10) h = 10; + slideHeight = int(float(h)/REFLECTION_FACTOR); slideWidth = int(float(slideHeight) * 2/3.); + fontSize = MAX(int(h/20.), 12); recalc(w, h); resetSlides(); triggerRender(); @@ -592,8 +603,8 @@ static QImage prepareSurface(QImage img, int w, int h) img = img.scaled(w, h, Qt::IgnoreAspectRatio, mode); // slightly larger, to accomodate for the reflection - int hs = h * 2; - int hofs = h / 3; + int hs = int(h * REFLECTION_FACTOR); + int hofs = 0; // offscreen buffer: black is sweet QImage result(hs, w, QImage::Format_RGB16); @@ -715,13 +726,13 @@ void PictureFlowPrivate::render() QFont font = QFont(); font.setBold(true); - font.setPointSize(FONT_SIZE); + font.setPixelSize(fontSize); painter.setFont(font); painter.setPen(Qt::white); //painter.setPen(QColor(255,255,255,127)); if (centerIndex < slideCount() && centerIndex > -1) - painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2-FONT_SIZE*3), + painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2-fontSize*3), Qt::AlignCenter, slideImages->caption(centerIndex)); painter.end(); @@ -766,7 +777,7 @@ void PictureFlowPrivate::render() QFont font = QFont(); font.setBold(true); - font.setPointSize(FONT_SIZE); + font.setPixelSize(fontSize); painter.setFont(font); int leftTextIndex = (step>0) ? centerIndex : centerIndex-1; @@ -774,12 +785,12 @@ void PictureFlowPrivate::render() painter.setPen(QColor(255,255,255, (255-fade) )); if (leftTextIndex < sc && leftTextIndex > -1) - painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - FONT_SIZE*3), + painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - fontSize*3), Qt::AlignCenter, slideImages->caption(leftTextIndex)); painter.setPen(QColor(255,255,255, fade)); if (leftTextIndex+1 < sc && leftTextIndex > -2) - painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - FONT_SIZE*3), + painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - fontSize*3), Qt::AlignCenter, slideImages->caption(leftTextIndex+1)); @@ -893,7 +904,7 @@ int col1, int col2) int center = (sh*BILINEAR_STRETCH_VER/2); int dy = dist*BILINEAR_STRETCH_VER / h; #else - int center = (sh/2); + int center = sh/2; int dy = dist / h; #endif int p1 = center*PFREAL_ONE - dy/2; @@ -1110,8 +1121,9 @@ void PictureFlowPrivate::clearSurfaceCache() surfaceCache.clear(); } -// ----------------------------------------- +// }}} +// PictureFlow {{{ PictureFlow::PictureFlow(QWidget* parent, int queueLength): QWidget(parent) { d = new PictureFlowPrivate(this, queueLength); @@ -1387,3 +1399,5 @@ void PictureFlow::emitcurrentChanged(int index) { emit currentChanged(index); } int FlowImages::count() { return 0; } QImage FlowImages::image(int index) { index=0; return QImage(); } QString FlowImages::caption(int index) {index=0; return QString(); } + +// }}} From 0d66fe64cf96cd9be095e00a46b93384aa037094 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 12 Jun 2010 16:42:21 -0600 Subject: [PATCH 09/13] EPUB Output: Default cover is generated is now generated as a JPEG, reducing size by an order of magnitude. Fixes #5810 (0.7.2 creating larger epubs from RTF) --- src/calibre/ebooks/oeb/transforms/cover.py | 4 ++-- src/calibre/utils/magick_draw.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/calibre/ebooks/oeb/transforms/cover.py b/src/calibre/ebooks/oeb/transforms/cover.py index 4d41ab14b4..83c5ec93e4 100644 --- a/src/calibre/ebooks/oeb/transforms/cover.py +++ b/src/calibre/ebooks/oeb/transforms/cover.py @@ -103,8 +103,8 @@ class CoverManager(object): 32)] img_data = create_cover_page(lines, I('library.png')) id, href = self.oeb.manifest.generate('cover_image', - 'cover_image.png') - item = self.oeb.manifest.add(id, href, guess_type('t.png')[0], + 'cover_image.jpg') + item = self.oeb.manifest.add(id, href, guess_type('t.jpg')[0], data=img_data) m.clear('cover') m.add('cover', item.id) diff --git a/src/calibre/utils/magick_draw.py b/src/calibre/utils/magick_draw.py index 160f4b70a5..2a259301db 100644 --- a/src/calibre/utils/magick_draw.py +++ b/src/calibre/utils/magick_draw.py @@ -175,7 +175,7 @@ def add_borders_to_image(path_to_image, left=0, top=0, right=0, bottom=0, p.DestroyMagickWand(canvas) def create_cover_page(top_lines, logo_path, width=590, height=750, - bgcolor='white', output_format='png'): + bgcolor='white', output_format='jpg'): ans = None with p.ImageMagick(): canvas = create_canvas(width, height, bgcolor) From 7cf81e7bff74d91e3e114f4b4ca4bb559c0f6542 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 12 Jun 2010 17:08:26 -0600 Subject: [PATCH 10/13] News download: Fix prepreprocess_html method --- src/calibre/manual/news_recipe.rst | 2 +- src/calibre/web/feeds/news.py | 11 ++++++----- src/calibre/web/fetch/simple.py | 6 ++++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/calibre/manual/news_recipe.rst b/src/calibre/manual/news_recipe.rst index 14cc41d436..7e5045ea47 100644 --- a/src/calibre/manual/news_recipe.rst +++ b/src/calibre/manual/news_recipe.rst @@ -111,7 +111,7 @@ Pre/post processing of downloaded HTML .. automember:: BasicNewsRecipe.remove_javascript -.. automethod:: BasicNewsRecipe.prepreprocess_html +.. automethod:: BasicNewsRecipe.skip_ad_pages .. automethod:: BasicNewsRecipe.preprocess_html diff --git a/src/calibre/web/feeds/news.py b/src/calibre/web/feeds/news.py index f54d5bde9d..9e05babecc 100644 --- a/src/calibre/web/feeds/news.py +++ b/src/calibre/web/feeds/news.py @@ -413,18 +413,19 @@ class BasicNewsRecipe(Recipe): return url return article.get('link', None) - def prepreprocess_html(self, soup): + def skip_ad_pages(self, soup): ''' This method is called with the source of each downloaded :term:`HTML` file, before any of the cleanup attributes like remove_tags, keep_only_tags are applied. Note that preprocess_regexps will have already been applied. - It can be used to do arbitrarily powerful pre-processing on the :term:`HTML`. - It should return `soup` after processing it. + It is meant to allow the recipe to skip ad pages. If the soup represents + an ad page, return the HTML of the real page. Otherwise return + None. `soup`: A `BeautifulSoup `_ instance containing the downloaded :term:`HTML`. ''' - return soup + return None def preprocess_html(self, soup): @@ -628,7 +629,7 @@ class BasicNewsRecipe(Recipe): self.web2disk_options = web2disk_option_parser().parse_args(web2disk_cmdline)[0] for extra in ('keep_only_tags', 'remove_tags', 'preprocess_regexps', - 'prepreprocess_html', 'preprocess_html', 'remove_tags_after', + 'skip_ad_pages', 'preprocess_html', 'remove_tags_after', 'remove_tags_before', 'is_link_wanted'): setattr(self.web2disk_options, extra, getattr(self, extra)) self.web2disk_options.postprocess_html = self._postprocess_html diff --git a/src/calibre/web/fetch/simple.py b/src/calibre/web/fetch/simple.py index bde91ec0d2..b6186f785d 100644 --- a/src/calibre/web/fetch/simple.py +++ b/src/calibre/web/fetch/simple.py @@ -136,7 +136,7 @@ class RecursiveFetcher(object): self.remove_tags_before = getattr(options, 'remove_tags_before', None) self.keep_only_tags = getattr(options, 'keep_only_tags', []) self.preprocess_html_ext = getattr(options, 'preprocess_html', lambda soup: soup) - self.prepreprocess_html_ext = getattr(options, 'prepreprocess_html', lambda soup: soup) + self.prepreprocess_html_ext = getattr(options, 'skip_ad_pages', lambda soup: None) self.postprocess_html_ext= getattr(options, 'postprocess_html', None) self._is_link_wanted = getattr(options, 'is_link_wanted', default_is_link_wanted) @@ -154,7 +154,9 @@ class RecursiveFetcher(object): nmassage.append((re.compile(r'', re.DOTALL), lambda m: '')) soup = BeautifulSoup(xml_to_unicode(src, self.verbose, strip_encoding_pats=True)[0], markupMassage=nmassage) - soup = self.prepreprocess_html_ext(soup) + replace = self.prepreprocess_html_ext(soup) + if replace is not None: + soup = BeautifulSoup(xml_to_unicode(src, self.verbose, strip_encoding_pats=True)[0], markupMassage=nmassage) if self.keep_only_tags: body = Tag(soup, 'body') From 86dee77d368b9e8c0571992a438ce9e435cb11b7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 12 Jun 2010 20:28:04 -0600 Subject: [PATCH 11/13] Make the book details pane animated --- resources/recipes/the_oz.recipe | 2 +- src/calibre/gui2/status.py | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/resources/recipes/the_oz.recipe b/resources/recipes/the_oz.recipe index a55f31e63e..ccdce0acb6 100644 --- a/resources/recipes/the_oz.recipe +++ b/resources/recipes/the_oz.recipe @@ -16,7 +16,7 @@ class DailyTelegraph(BasicNewsRecipe): language = 'en_AU' oldest_article = 2 - max_articles_per_feed = 10 + max_articles_per_feed = 20 remove_javascript = True no_stylesheets = True encoding = 'utf8' diff --git a/src/calibre/gui2/status.py b/src/calibre/gui2/status.py index 50a256ef2d..06c3e9c85f 100644 --- a/src/calibre/gui2/status.py +++ b/src/calibre/gui2/status.py @@ -2,9 +2,10 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' import os, collections -from PyQt4.QtGui import QStatusBar, QLabel, QWidget, QHBoxLayout, QPixmap, \ - QSizePolicy, QScrollArea -from PyQt4.QtCore import Qt, QSize, pyqtSignal +from PyQt4.Qt import QStatusBar, QLabel, QWidget, QHBoxLayout, QPixmap, \ + QSizePolicy, QScrollArea, Qt, QSize, pyqtSignal, \ + QPropertyAnimation, QEasingCurve + from calibre import fit_image, preferred_encoding, isosx from calibre.gui2 import config @@ -50,6 +51,10 @@ class BookInfoDisplay(QWidget): def __init__(self, coverpath=I('book.svg')): QLabel.__init__(self) + self.animation = QPropertyAnimation(self, 'size', self) + self.animation.setEasingCurve(QEasingCurve(QEasingCurve.OutExpo)) + self.animation.setDuration(1000) + self.animation.setStartValue(QSize(0, 0)) self.setMaximumWidth(81) self.setMaximumHeight(108) self.default_pixmap = QPixmap(coverpath) @@ -58,6 +63,7 @@ class BookInfoDisplay(QWidget): self.setPixmap(self.default_pixmap) def do_layout(self): + self.animation.stop() pixmap = self.pixmap() pwidth, pheight = pixmap.width(), pixmap.height() width, height = fit_image(pwidth, pheight, @@ -68,11 +74,12 @@ class BookInfoDisplay(QWidget): except ZeroDivisionError: aspect_ratio = 1 self.setMaximumWidth(int(aspect_ratio*self.maximumHeight())) + self.animation.setEndValue(self.maximumSize()) def setPixmap(self, pixmap): QLabel.setPixmap(self, pixmap) self.do_layout() - + self.animation.start() def sizeHint(self): return QSize(self.maximumWidth(), self.maximumHeight()) From 7dbdf55b13f1d34a617e9442b0ecf0782e252794 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 12 Jun 2010 21:05:16 -0600 Subject: [PATCH 12/13] Support for the Samsung Galaxy --- src/calibre/devices/android/driver.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 54bd745879..0bbdf0f22c 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -30,7 +30,7 @@ class ANDROID(USBMS): 0x18d1 : { 0x4e11 : [0x0100, 0x226], 0x4e12: [0x0100, 0x226]}, # Samsung - 0x04e8 : { 0x681d : [0x0222], 0x681c : [0x0222, 0x0224]}, + 0x04e8 : { 0x681d : [0x0222, 0x0400], 0x681c : [0x0222, 0x0224]}, # Acer 0x502 : { 0x3203 : [0x0100]}, @@ -41,10 +41,12 @@ class ANDROID(USBMS): 'be used') EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(EBOOK_DIR_MAIN) - VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER', 'GT-I5700'] + VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER', + 'GT-I5700', 'SAMSUNG'] WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE', - '__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD'] - WINDOWS_CARD_A_MEM = ['ANDROID_PHONE'] + '__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', + 'PROD_GT-I9000'] + WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'PROD_GT-I9000_CARD'] OSX_MAIN_MEM = 'HTC Android Phone Media' From ac4c623c39a8551d0044ed45a22138f3969b0d48 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 12 Jun 2010 21:21:09 -0600 Subject: [PATCH 13/13] Cover cache: Resize covers larger than 600x800 in the cover cache to reduce memory consumption in the GUI --- src/calibre/library/caches.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index bb6001794a..57f9d0baaf 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -10,14 +10,14 @@ import collections, glob, os, re, itertools, functools from itertools import repeat from datetime import timedelta -from PyQt4.QtCore import QThread, QReadWriteLock -from PyQt4.QtGui import QImage +from PyQt4.Qt import QThread, QReadWriteLock, QImage, Qt from calibre.utils.config import tweaks from calibre.utils.date import parse_date, now, UNDEFINED_DATE from calibre.utils.search_query_parser import SearchQueryParser from calibre.utils.pyparsing import ParseException from calibre.ebooks.metadata import title_sort +from calibre import fit_image class CoverCache(QThread): @@ -96,6 +96,11 @@ class CoverCache(QThread): img.loadFromData(data) if img.isNull(): continue + scaled, nwidth, nheight = fit_image(img.width(), + img.height(), 600, 800) + if scaled: + img = img.scaled(nwidth, nheight, Qt.KeepAspectRatio, + Qt.SmoothTransformation) except: continue self.cache_lock.lockForWrite()