From 76cd6956692bf859c82881b0c75ee85e8c4d8fd4 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Fri, 8 Oct 2010 13:04:59 +0100 Subject: [PATCH] Add 'Library maintenance' to the library dropdown menu 1) move check integrity to here 2) move check library to here 3) move mark dirty to here 4) add a stub recover_database command 5) improve help message for cli calibredb restore_database, and add the --really-do-it option --- src/calibre/gui2/actions/choose_library.py | 127 ++++++++++++++++++++- src/calibre/gui2/dialogs/check_library.py | 2 +- src/calibre/gui2/preferences/misc.py | 93 +-------------- src/calibre/gui2/preferences/misc.ui | 21 ---- src/calibre/library/cli.py | 24 +++- 5 files changed, 141 insertions(+), 126 deletions(-) diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py index 044cbcdf85..95b3f9e24d 100644 --- a/src/calibre/gui2/actions/choose_library.py +++ b/src/calibre/gui2/actions/choose_library.py @@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en' import os, shutil from functools import partial -from PyQt4.Qt import QMenu, Qt, QInputDialog +from PyQt4.Qt import QMenu, Qt, QInputDialog, QThread, pyqtSignal, QProgressDialog from calibre import isbytestring from calibre.constants import filesystem_encoding @@ -16,6 +16,7 @@ from calibre.utils.config import prefs from calibre.gui2 import gprefs, warning_dialog, Dispatcher, error_dialog, \ question_dialog, info_dialog from calibre.gui2.actions import InterfaceAction +from calibre.gui2.dialogs.check_library import CheckLibraryDialog class LibraryUsageStats(object): # {{{ @@ -75,6 +76,72 @@ class LibraryUsageStats(object): # {{{ self.write_stats() # }}} +# Check Integrity {{{ + +class VacThread(QThread): + + check_done = pyqtSignal(object, object) + callback = pyqtSignal(object, object) + + def __init__(self, parent, db): + QThread.__init__(self, parent) + self.db = db + self._parent = parent + + def run(self): + err = bad = None + try: + bad = self.db.check_integrity(self.callbackf) + except: + import traceback + err = traceback.format_exc() + self.check_done.emit(bad, err) + + def callbackf(self, progress, msg): + self.callback.emit(progress, msg) + + +class CheckIntegrity(QProgressDialog): + + def __init__(self, db, parent=None): + QProgressDialog.__init__(self, parent) + self.db = db + self.setCancelButton(None) + self.setMinimum(0) + self.setMaximum(100) + self.setWindowTitle(_('Checking database integrity')) + self.setAutoReset(False) + self.setValue(0) + + self.vthread = VacThread(self, db) + self.vthread.check_done.connect(self.check_done, + type=Qt.QueuedConnection) + self.vthread.callback.connect(self.callback, type=Qt.QueuedConnection) + self.vthread.start() + + def callback(self, progress, msg): + self.setLabelText(msg) + self.setValue(int(100*progress)) + + def check_done(self, bad, err): + if err: + error_dialog(self, _('Error'), + _('Failed to check database integrity'), + det_msg=err, show=True) + elif bad: + titles = [self.db.title(x, index_is_id=True) for x in bad] + det_msg = '\n'.join(titles) + warning_dialog(self, _('Some inconsistencies found'), + _('The following books had formats listed in the ' + 'database that are not actually available. ' + 'The entries for the formats have been removed. ' + 'You should check them manually. This can ' + 'happen if you manipulate the files in the ' + 'library folder directly.'), det_msg=det_msg, show=True) + self.reset() + +# }}} + class ChooseLibraryAction(InterfaceAction): name = 'Choose Library' @@ -117,11 +184,28 @@ class ChooseLibraryAction(InterfaceAction): self.rename_separator = self.choose_menu.addSeparator() - self.create_action(spec=(_('Library backup status...'), 'lt.png', None, - None), attr='action_backup_status') - self.action_backup_status.triggered.connect(self.backup_status, - type=Qt.QueuedConnection) - self.choose_menu.addAction(self.action_backup_status) + self.maintenance_menu = QMenu(_('Library Maintenance')) + ac = self.create_action(spec=(_('Library metadata backup status'), + 'lt.png', None, None), attr='action_backup_status') + ac.triggered.connect(self.backup_status, type=Qt.QueuedConnection) + self.maintenance_menu.addAction(ac) + ac = self.create_action(spec=(_('Start backing up metadata of all books'), + 'lt.png', None, None), attr='action_backup_metadata') + ac.triggered.connect(self.mark_dirty, type=Qt.QueuedConnection) + self.maintenance_menu.addAction(ac) + ac = self.create_action(spec=(_('Check library'), 'lt.png', + None, None), attr='action_check_library') + ac.triggered.connect(self.check_library, type=Qt.QueuedConnection) + self.maintenance_menu.addAction(ac) + ac = self.create_action(spec=(_('Check database integrity'), 'lt.png', + None, None), attr='action_check_database') + ac.triggered.connect(self.check_database, type=Qt.QueuedConnection) + self.maintenance_menu.addAction(ac) + ac = self.create_action(spec=(_('Recover database'), 'lt.png', + None, None), attr='action_restore_database') + ac.triggered.connect(self.restore_database, type=Qt.QueuedConnection) + self.maintenance_menu.addAction(ac) + self.choose_menu.addMenu(self.maintenance_menu) def library_name(self): db = self.gui.library_view.model().db @@ -234,6 +318,37 @@ class ChooseLibraryAction(InterfaceAction): _('Book metadata files remaining to be written: %s') % dirty_text, show=True) + def mark_dirty(self): + db = self.gui.library_view.model().db + db.dirtied(list(db.data.iterallids())) + info_dialog(self.gui, _('Backup metadata'), + _('Metadata will be backed up while calibre is running, at the ' + 'rate of approximately 1 book per second.'), show=True) + + def check_library(self): + db = self.gui.library_view.model().db + d = CheckLibraryDialog(self.gui.parent(), db) + d.exec_() + + def check_database(self, *args): + m = self.gui.library_view.model() + m.stop_metadata_backup() + try: + d = CheckIntegrity(m.db, self.gui) + d.exec_() + finally: + m.start_metadata_backup() + + def restore_database(self): + info_dialog(self.gui, _('Recover database'), + _( + 'This command rebuilds your calibre database from the information ' + 'stored by calibre in the OPF files.' + '

' + + 'This function is not currently available in the GUI. You can ' + 'recover your database using the \'calibredb restore_database\' ' + 'command line function.' + ), show=True) + def switch_requested(self, location): if not self.change_library_allowed(): return diff --git a/src/calibre/gui2/dialogs/check_library.py b/src/calibre/gui2/dialogs/check_library.py index 29e5a2097c..46071d3c06 100644 --- a/src/calibre/gui2/dialogs/check_library.py +++ b/src/calibre/gui2/dialogs/check_library.py @@ -32,7 +32,7 @@ class CheckLibraryDialog(QDialog): self.copy = QPushButton(_('Copy to clipboard')) self.copy.setDefault(False) self.copy.clicked.connect(self.copy_to_clipboard) - self.ok = QPushButton('&OK') + self.ok = QPushButton('&Done') self.ok.setDefault(True) self.ok.clicked.connect(self.accept) self.cancel = QPushButton('&Cancel') diff --git a/src/calibre/gui2/preferences/misc.py b/src/calibre/gui2/preferences/misc.py index c9dc25caff..330332a716 100644 --- a/src/calibre/gui2/preferences/misc.py +++ b/src/calibre/gui2/preferences/misc.py @@ -5,81 +5,14 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' -from PyQt4.Qt import QProgressDialog, QThread, Qt, pyqtSignal -from calibre.gui2.dialogs.check_library import CheckLibraryDialog from calibre.gui2.preferences import ConfigWidgetBase, test_widget from calibre.gui2.preferences.misc_ui import Ui_Form -from calibre.gui2 import error_dialog, config, warning_dialog, \ - open_local_file, info_dialog +from calibre.gui2 import error_dialog, config, open_local_file, info_dialog from calibre.constants import isosx # Check Integrity {{{ -class VacThread(QThread): - - check_done = pyqtSignal(object, object) - callback = pyqtSignal(object, object) - - def __init__(self, parent, db): - QThread.__init__(self, parent) - self.db = db - self._parent = parent - - def run(self): - err = bad = None - try: - bad = self.db.check_integrity(self.callbackf) - except: - import traceback - err = traceback.format_exc() - self.check_done.emit(bad, err) - - def callbackf(self, progress, msg): - self.callback.emit(progress, msg) - - -class CheckIntegrity(QProgressDialog): - - def __init__(self, db, parent=None): - QProgressDialog.__init__(self, parent) - self.db = db - self.setCancelButton(None) - self.setMinimum(0) - self.setMaximum(100) - self.setWindowTitle(_('Checking database integrity')) - self.setAutoReset(False) - self.setValue(0) - - self.vthread = VacThread(self, db) - self.vthread.check_done.connect(self.check_done, - type=Qt.QueuedConnection) - self.vthread.callback.connect(self.callback, type=Qt.QueuedConnection) - self.vthread.start() - - def callback(self, progress, msg): - self.setLabelText(msg) - self.setValue(int(100*progress)) - - def check_done(self, bad, err): - if err: - error_dialog(self, _('Error'), - _('Failed to check database integrity'), - det_msg=err, show=True) - elif bad: - titles = [self.db.title(x, index_is_id=True) for x in bad] - det_msg = '\n'.join(titles) - warning_dialog(self, _('Some inconsistencies found'), - _('The following books had formats listed in the ' - 'database that are not actually available. ' - 'The entries for the formats have been removed. ' - 'You should check them manually. This can ' - 'happen if you manipulate the files in the ' - 'library folder directly.'), det_msg=det_msg, show=True) - self.reset() - -# }}} - class ConfigWidget(ConfigWidgetBase, Ui_Form): def genesis(self, gui): @@ -88,39 +21,15 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): r('worker_limit', config, restart_required=True) r('enforce_cpu_limit', config, restart_required=True) self.device_detection_button.clicked.connect(self.debug_device_detection) - self.compact_button.clicked.connect(self.compact) - self.button_all_books_dirty.clicked.connect(self.mark_dirty) - self.button_check_library.clicked.connect(self.check_library) self.button_open_config_dir.clicked.connect(self.open_config_dir) self.button_osx_symlinks.clicked.connect(self.create_symlinks) self.button_osx_symlinks.setVisible(isosx) - def mark_dirty(self): - db = self.gui.library_view.model().db - db.dirtied(list(db.data.iterallids())) - info_dialog(self, _('Backup metadata'), - _('Metadata will be backed up while calibre is running, at the ' - 'rate of 30 books per minute.'), show=True) - - def check_library(self): - db = self.gui.library_view.model().db - d = CheckLibraryDialog(self.gui.parent(), db) - d.exec_() - def debug_device_detection(self, *args): from calibre.gui2.preferences.device_debug import DebugDevice d = DebugDevice(self) d.exec_() - def compact(self, *args): - m = self.gui.library_view.model() - m.stop_metadata_backup() - try: - d = CheckIntegrity(m.db, self) - d.exec_() - finally: - m.start_metadata_backup() - def open_config_dir(self, *args): from calibre.utils.config import config_dir open_local_file(config_dir) diff --git a/src/calibre/gui2/preferences/misc.ui b/src/calibre/gui2/preferences/misc.ui index dd0ca15840..c036cb971b 100644 --- a/src/calibre/gui2/preferences/misc.ui +++ b/src/calibre/gui2/preferences/misc.ui @@ -77,13 +77,6 @@ - - - - &Check database integrity - - - @@ -124,20 +117,6 @@ - - - - Back up metadata of all books - - - - - - - Check the library folders for potential problems - - - diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py index 63361d87f5..44a7368ce1 100644 --- a/src/calibre/library/cli.py +++ b/src/calibre/library/cli.py @@ -961,13 +961,19 @@ def restore_database_option_parser(): ''' %prog restore_database [options] - Restore this database from the metadata stored in OPF - files in each directory of the calibre library. This is - useful if your metadata.db file has been corrupted. + Restore this database from the metadata stored in OPF files in each + directory of the calibre library. This is useful if your metadata.db file + has been corrupted. - WARNING: This completely regenerates your database. You will - lose stored per-book conversion settings and custom recipes. + WARNING: This command completely regenerates your database. You will lose + all saved searches, user categories, plugboards, stored per-book conversion + settings, and custom recipes. Restored metadata will only be as accurate as + what is found in the OPF files. ''')) + + parser.add_option('-r', '--really-do-it', default=False, action='store_true', + help=_('Really do the recovery. The command will not run ' + 'unless this option is specified.')) return parser def command_restore_database(args, dbpath): @@ -978,6 +984,12 @@ def command_restore_database(args, dbpath): parser.print_help() return 1 + if not opts.really_do_it: + print _('You must provide the --really-do-it option to do a recovery\n') + parser.print_help() + return 1 + + return if opts.library_path is not None: dbpath = opts.library_path @@ -1028,7 +1040,7 @@ information is the equivalent of what is shown in the tags pane. parser.add_option('-r', '--categories', default='', dest='report', help=_("Comma-separated list of category lookup names.\n" "Default: all")) - parser.add_option('-w', '--line-width', default=-1, type=int, + parser.add_option('-w', '--idth', default=-1, type=int, help=_('The maximum width of a single line in the output. ' 'Defaults to detecting screen size.')) parser.add_option('-s', '--separator', default=',',