mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
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
This commit is contained in:
parent
3ad46e0018
commit
76cd695669
@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
import os, shutil
|
import os, shutil
|
||||||
from functools import partial
|
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 import isbytestring
|
||||||
from calibre.constants import filesystem_encoding
|
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, \
|
from calibre.gui2 import gprefs, warning_dialog, Dispatcher, error_dialog, \
|
||||||
question_dialog, info_dialog
|
question_dialog, info_dialog
|
||||||
from calibre.gui2.actions import InterfaceAction
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
from calibre.gui2.dialogs.check_library import CheckLibraryDialog
|
||||||
|
|
||||||
class LibraryUsageStats(object): # {{{
|
class LibraryUsageStats(object): # {{{
|
||||||
|
|
||||||
@ -75,6 +76,72 @@ class LibraryUsageStats(object): # {{{
|
|||||||
self.write_stats()
|
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):
|
class ChooseLibraryAction(InterfaceAction):
|
||||||
|
|
||||||
name = 'Choose Library'
|
name = 'Choose Library'
|
||||||
@ -117,11 +184,28 @@ class ChooseLibraryAction(InterfaceAction):
|
|||||||
|
|
||||||
self.rename_separator = self.choose_menu.addSeparator()
|
self.rename_separator = self.choose_menu.addSeparator()
|
||||||
|
|
||||||
self.create_action(spec=(_('Library backup status...'), 'lt.png', None,
|
self.maintenance_menu = QMenu(_('Library Maintenance'))
|
||||||
None), attr='action_backup_status')
|
ac = self.create_action(spec=(_('Library metadata backup status'),
|
||||||
self.action_backup_status.triggered.connect(self.backup_status,
|
'lt.png', None, None), attr='action_backup_status')
|
||||||
type=Qt.QueuedConnection)
|
ac.triggered.connect(self.backup_status, type=Qt.QueuedConnection)
|
||||||
self.choose_menu.addAction(self.action_backup_status)
|
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):
|
def library_name(self):
|
||||||
db = self.gui.library_view.model().db
|
db = self.gui.library_view.model().db
|
||||||
@ -234,6 +318,37 @@ class ChooseLibraryAction(InterfaceAction):
|
|||||||
_('Book metadata files remaining to be written: %s') % dirty_text,
|
_('Book metadata files remaining to be written: %s') % dirty_text,
|
||||||
show=True)
|
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.' + '<p>' +
|
||||||
|
'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):
|
def switch_requested(self, location):
|
||||||
if not self.change_library_allowed():
|
if not self.change_library_allowed():
|
||||||
return
|
return
|
||||||
|
@ -32,7 +32,7 @@ class CheckLibraryDialog(QDialog):
|
|||||||
self.copy = QPushButton(_('Copy to clipboard'))
|
self.copy = QPushButton(_('Copy to clipboard'))
|
||||||
self.copy.setDefault(False)
|
self.copy.setDefault(False)
|
||||||
self.copy.clicked.connect(self.copy_to_clipboard)
|
self.copy.clicked.connect(self.copy_to_clipboard)
|
||||||
self.ok = QPushButton('&OK')
|
self.ok = QPushButton('&Done')
|
||||||
self.ok.setDefault(True)
|
self.ok.setDefault(True)
|
||||||
self.ok.clicked.connect(self.accept)
|
self.ok.clicked.connect(self.accept)
|
||||||
self.cancel = QPushButton('&Cancel')
|
self.cancel = QPushButton('&Cancel')
|
||||||
|
@ -5,81 +5,14 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__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 import ConfigWidgetBase, test_widget
|
||||||
from calibre.gui2.preferences.misc_ui import Ui_Form
|
from calibre.gui2.preferences.misc_ui import Ui_Form
|
||||||
from calibre.gui2 import error_dialog, config, warning_dialog, \
|
from calibre.gui2 import error_dialog, config, open_local_file, info_dialog
|
||||||
open_local_file, info_dialog
|
|
||||||
from calibre.constants import isosx
|
from calibre.constants import isosx
|
||||||
|
|
||||||
# Check Integrity {{{
|
# 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):
|
class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||||
|
|
||||||
def genesis(self, gui):
|
def genesis(self, gui):
|
||||||
@ -88,39 +21,15 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
r('worker_limit', config, restart_required=True)
|
r('worker_limit', config, restart_required=True)
|
||||||
r('enforce_cpu_limit', config, restart_required=True)
|
r('enforce_cpu_limit', config, restart_required=True)
|
||||||
self.device_detection_button.clicked.connect(self.debug_device_detection)
|
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_open_config_dir.clicked.connect(self.open_config_dir)
|
||||||
self.button_osx_symlinks.clicked.connect(self.create_symlinks)
|
self.button_osx_symlinks.clicked.connect(self.create_symlinks)
|
||||||
self.button_osx_symlinks.setVisible(isosx)
|
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):
|
def debug_device_detection(self, *args):
|
||||||
from calibre.gui2.preferences.device_debug import DebugDevice
|
from calibre.gui2.preferences.device_debug import DebugDevice
|
||||||
d = DebugDevice(self)
|
d = DebugDevice(self)
|
||||||
d.exec_()
|
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):
|
def open_config_dir(self, *args):
|
||||||
from calibre.utils.config import config_dir
|
from calibre.utils.config import config_dir
|
||||||
open_local_file(config_dir)
|
open_local_file(config_dir)
|
||||||
|
@ -77,13 +77,6 @@
|
|||||||
</property>
|
</property>
|
||||||
</spacer>
|
</spacer>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="0" colspan="2">
|
|
||||||
<widget class="QPushButton" name="compact_button">
|
|
||||||
<property name="text">
|
|
||||||
<string>&Check database integrity</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="6" column="0">
|
<item row="6" column="0">
|
||||||
<spacer name="verticalSpacer_7">
|
<spacer name="verticalSpacer_7">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
@ -124,20 +117,6 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="10" column="0" colspan="2">
|
|
||||||
<widget class="QPushButton" name="button_all_books_dirty">
|
|
||||||
<property name="text">
|
|
||||||
<string>Back up metadata of all books</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="11" column="0" colspan="2">
|
|
||||||
<widget class="QPushButton" name="button_check_library">
|
|
||||||
<property name="text">
|
|
||||||
<string>Check the library folders for potential problems</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="20" column="0">
|
<item row="20" column="0">
|
||||||
<spacer name="verticalSpacer_9">
|
<spacer name="verticalSpacer_9">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
|
@ -961,13 +961,19 @@ def restore_database_option_parser():
|
|||||||
'''
|
'''
|
||||||
%prog restore_database [options]
|
%prog restore_database [options]
|
||||||
|
|
||||||
Restore this database from the metadata stored in OPF
|
Restore this database from the metadata stored in OPF files in each
|
||||||
files in each directory of the calibre library. This is
|
directory of the calibre library. This is useful if your metadata.db file
|
||||||
useful if your metadata.db file has been corrupted.
|
has been corrupted.
|
||||||
|
|
||||||
WARNING: This completely regenerates your database. You will
|
WARNING: This command completely regenerates your database. You will lose
|
||||||
lose stored per-book conversion settings and custom recipes.
|
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
|
return parser
|
||||||
|
|
||||||
def command_restore_database(args, dbpath):
|
def command_restore_database(args, dbpath):
|
||||||
@ -978,6 +984,12 @@ def command_restore_database(args, dbpath):
|
|||||||
parser.print_help()
|
parser.print_help()
|
||||||
return 1
|
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:
|
if opts.library_path is not None:
|
||||||
dbpath = opts.library_path
|
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',
|
parser.add_option('-r', '--categories', default='', dest='report',
|
||||||
help=_("Comma-separated list of category lookup names.\n"
|
help=_("Comma-separated list of category lookup names.\n"
|
||||||
"Default: all"))
|
"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. '
|
help=_('The maximum width of a single line in the output. '
|
||||||
'Defaults to detecting screen size.'))
|
'Defaults to detecting screen size.'))
|
||||||
parser.add_option('-s', '--separator', default=',',
|
parser.add_option('-s', '--separator', default=',',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user