mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 18:54:09 -04:00
Move check db integrity from preferences to library drop down menu. Fix regression that broke using title_sort in templates
This commit is contained in:
commit
4ab91285cc
@ -38,7 +38,8 @@ class SafeFormat(TemplateFormatter):
|
|||||||
|
|
||||||
def get_value(self, key, args, kwargs):
|
def get_value(self, key, args, kwargs):
|
||||||
try:
|
try:
|
||||||
key = field_metadata.search_term_to_field_key(key.lower())
|
if key != 'title_sort':
|
||||||
|
key = field_metadata.search_term_to_field_key(key.lower())
|
||||||
b = self.book.get_user_metadata(key, False)
|
b = self.book.get_user_metadata(key, False)
|
||||||
if b and b['datatype'] == 'int' and self.book.get(key, 0) == 0:
|
if b and b['datatype'] == 'int' and self.book.get(key, 0) == 0:
|
||||||
v = ''
|
v = ''
|
||||||
|
@ -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
|
||||||
|
@ -18,7 +18,7 @@ from calibre.utils.config import prefs, tweaks
|
|||||||
|
|
||||||
class Worker(Thread):
|
class Worker(Thread):
|
||||||
|
|
||||||
def __init__(self, ids, db, loc, progress, done):
|
def __init__(self, ids, db, loc, progress, done, delete_after):
|
||||||
Thread.__init__(self)
|
Thread.__init__(self)
|
||||||
self.ids = ids
|
self.ids = ids
|
||||||
self.processed = set([])
|
self.processed = set([])
|
||||||
@ -27,6 +27,7 @@ class Worker(Thread):
|
|||||||
self.error = None
|
self.error = None
|
||||||
self.progress = progress
|
self.progress = progress
|
||||||
self.done = done
|
self.done = done
|
||||||
|
self.delete_after = delete_after
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
try:
|
try:
|
||||||
@ -68,7 +69,8 @@ class Worker(Thread):
|
|||||||
self.add_formats(identical_book, paths, newdb, replace=False)
|
self.add_formats(identical_book, paths, newdb, replace=False)
|
||||||
if not added:
|
if not added:
|
||||||
newdb.import_book(mi, paths, notify=False, import_hooks=False,
|
newdb.import_book(mi, paths, notify=False, import_hooks=False,
|
||||||
apply_import_tags=tweaks['add_new_book_tags_when_importing_books'])
|
apply_import_tags=tweaks['add_new_book_tags_when_importing_books'],
|
||||||
|
preserve_uuid=self.delete_after)
|
||||||
co = self.db.conversion_options(x, 'PIPE')
|
co = self.db.conversion_options(x, 'PIPE')
|
||||||
if co is not None:
|
if co is not None:
|
||||||
newdb.set_conversion_options(x, 'PIPE', co)
|
newdb.set_conversion_options(x, 'PIPE', co)
|
||||||
@ -134,7 +136,8 @@ class CopyToLibraryAction(InterfaceAction):
|
|||||||
self.pd.set_msg(_('Copying') + ' ' + title)
|
self.pd.set_msg(_('Copying') + ' ' + title)
|
||||||
self.pd.set_value(idx)
|
self.pd.set_value(idx)
|
||||||
|
|
||||||
self.worker = Worker(ids, db, loc, Dispatcher(progress), Dispatcher(self.pd.accept))
|
self.worker = Worker(ids, db, loc, Dispatcher(progress),
|
||||||
|
Dispatcher(self.pd.accept), delete_after)
|
||||||
self.worker.start()
|
self.worker.start()
|
||||||
|
|
||||||
self.pd.exec_()
|
self.pd.exec_()
|
||||||
|
@ -1413,15 +1413,16 @@ class DeviceMixin(object): # {{{
|
|||||||
|
|
||||||
# Force a reset if the caches are not initialized
|
# Force a reset if the caches are not initialized
|
||||||
if reset or not hasattr(self, 'db_book_title_cache'):
|
if reset or not hasattr(self, 'db_book_title_cache'):
|
||||||
|
# Build a cache (map) of the library, so the search isn't On**2
|
||||||
|
self.db_book_title_cache = {}
|
||||||
|
self.db_book_uuid_cache = {}
|
||||||
# It might be possible to get here without having initialized the
|
# It might be possible to get here without having initialized the
|
||||||
# library view. In this case, simply give up
|
# library view. In this case, simply give up
|
||||||
try:
|
try:
|
||||||
db = self.library_view.model().db
|
db = self.library_view.model().db
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
# Build a cache (map) of the library, so the search isn't On**2
|
|
||||||
self.db_book_title_cache = {}
|
|
||||||
self.db_book_uuid_cache = {}
|
|
||||||
for id in db.data.iterallids():
|
for id in db.data.iterallids():
|
||||||
mi = db.get_metadata(id, index_is_id=True)
|
mi = db.get_metadata(id, index_is_id=True)
|
||||||
title = clean_string(mi.title)
|
title = clean_string(mi.title)
|
||||||
@ -1455,7 +1456,7 @@ class DeviceMixin(object): # {{{
|
|||||||
if update_metadata:
|
if update_metadata:
|
||||||
book.smart_update(self.db_book_uuid_cache[book.uuid],
|
book.smart_update(self.db_book_uuid_cache[book.uuid],
|
||||||
replace_metadata=True)
|
replace_metadata=True)
|
||||||
book.in_library = True
|
book.in_library = 'UUID'
|
||||||
# ensure that the correct application_id is set
|
# ensure that the correct application_id is set
|
||||||
book.application_id = \
|
book.application_id = \
|
||||||
self.db_book_uuid_cache[book.uuid].application_id
|
self.db_book_uuid_cache[book.uuid].application_id
|
||||||
@ -1468,21 +1469,21 @@ class DeviceMixin(object): # {{{
|
|||||||
# will match if any of the db_id, author, or author_sort
|
# will match if any of the db_id, author, or author_sort
|
||||||
# also match.
|
# also match.
|
||||||
if getattr(book, 'application_id', None) in d['db_ids']:
|
if getattr(book, 'application_id', None) in d['db_ids']:
|
||||||
book.in_library = True
|
|
||||||
# app_id already matches a db_id. No need to set it.
|
# app_id already matches a db_id. No need to set it.
|
||||||
if update_metadata:
|
if update_metadata:
|
||||||
book.smart_update(d['db_ids'][book.application_id],
|
book.smart_update(d['db_ids'][book.application_id],
|
||||||
replace_metadata=True)
|
replace_metadata=True)
|
||||||
|
book.in_library = 'APP_ID'
|
||||||
continue
|
continue
|
||||||
# Sonys know their db_id independent of the application_id
|
# Sonys know their db_id independent of the application_id
|
||||||
# in the metadata cache. Check that as well.
|
# in the metadata cache. Check that as well.
|
||||||
if getattr(book, 'db_id', None) in d['db_ids']:
|
if getattr(book, 'db_id', None) in d['db_ids']:
|
||||||
book.in_library = True
|
|
||||||
book.application_id = \
|
|
||||||
d['db_ids'][book.db_id].application_id
|
|
||||||
if update_metadata:
|
if update_metadata:
|
||||||
book.smart_update(d['db_ids'][book.db_id],
|
book.smart_update(d['db_ids'][book.db_id],
|
||||||
replace_metadata=True)
|
replace_metadata=True)
|
||||||
|
book.in_library = 'DB_ID'
|
||||||
|
book.application_id = \
|
||||||
|
d['db_ids'][book.db_id].application_id
|
||||||
continue
|
continue
|
||||||
# We now know that the application_id is not right. Set it
|
# We now know that the application_id is not right. Set it
|
||||||
# to None to prevent book_on_device from accidentally
|
# to None to prevent book_on_device from accidentally
|
||||||
@ -1494,19 +1495,19 @@ class DeviceMixin(object): # {{{
|
|||||||
# either can appear as the author
|
# either can appear as the author
|
||||||
book_authors = clean_string(authors_to_string(book.authors))
|
book_authors = clean_string(authors_to_string(book.authors))
|
||||||
if book_authors in d['authors']:
|
if book_authors in d['authors']:
|
||||||
book.in_library = True
|
|
||||||
book.application_id = \
|
|
||||||
d['authors'][book_authors].application_id
|
|
||||||
if update_metadata:
|
if update_metadata:
|
||||||
book.smart_update(d['authors'][book_authors],
|
book.smart_update(d['authors'][book_authors],
|
||||||
replace_metadata=True)
|
replace_metadata=True)
|
||||||
elif book_authors in d['author_sort']:
|
book.in_library = 'AUTHOR'
|
||||||
book.in_library = True
|
|
||||||
book.application_id = \
|
book.application_id = \
|
||||||
d['author_sort'][book_authors].application_id
|
d['authors'][book_authors].application_id
|
||||||
|
elif book_authors in d['author_sort']:
|
||||||
if update_metadata:
|
if update_metadata:
|
||||||
book.smart_update(d['author_sort'][book_authors],
|
book.smart_update(d['author_sort'][book_authors],
|
||||||
replace_metadata=True)
|
replace_metadata=True)
|
||||||
|
book.in_library = 'AUTH_SORT'
|
||||||
|
book.application_id = \
|
||||||
|
d['author_sort'][book_authors].application_id
|
||||||
else:
|
else:
|
||||||
# Book definitely not matched. Clear its application ID
|
# Book definitely not matched. Clear its application ID
|
||||||
book.application_id = None
|
book.application_id = None
|
||||||
|
@ -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')
|
||||||
|
@ -24,7 +24,7 @@ from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \
|
|||||||
REGEXP_MATCH, CoverCache, MetadataBackup
|
REGEXP_MATCH, CoverCache, MetadataBackup
|
||||||
from calibre.library.cli import parse_series_string
|
from calibre.library.cli import parse_series_string
|
||||||
from calibre import strftime, isbytestring, prepare_string_for_xml
|
from calibre import strftime, isbytestring, prepare_string_for_xml
|
||||||
from calibre.constants import filesystem_encoding
|
from calibre.constants import filesystem_encoding, DEBUG
|
||||||
from calibre.gui2.library import DEFAULT_SORT
|
from calibre.gui2.library import DEFAULT_SORT
|
||||||
|
|
||||||
def human_readable(size, precision=1):
|
def human_readable(size, precision=1):
|
||||||
@ -699,6 +699,10 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
if role == Qt.DisplayRole:
|
if role == Qt.DisplayRole:
|
||||||
return QVariant(self.headers[self.column_map[section]])
|
return QVariant(self.headers[self.column_map[section]])
|
||||||
return NONE
|
return NONE
|
||||||
|
if DEBUG and role == Qt.ToolTipRole and orientation == Qt.Vertical:
|
||||||
|
col = self.db.field_metadata['uuid']['rec_index']
|
||||||
|
return QVariant(_('This book\'s UUID is "{0}"').format(self.db.data[section][col]))
|
||||||
|
|
||||||
if role == Qt.DisplayRole: # orientation is vertical
|
if role == Qt.DisplayRole: # orientation is vertical
|
||||||
return QVariant(section+1)
|
return QVariant(section+1)
|
||||||
return NONE
|
return NONE
|
||||||
@ -1206,6 +1210,8 @@ class DeviceBooksModel(BooksModel): # {{{
|
|||||||
if tags:
|
if tags:
|
||||||
tags.sort(cmp=lambda x,y: cmp(x.lower(), y.lower()))
|
tags.sort(cmp=lambda x,y: cmp(x.lower(), y.lower()))
|
||||||
return QVariant(', '.join(tags))
|
return QVariant(', '.join(tags))
|
||||||
|
elif DEBUG and cname == 'inlibrary':
|
||||||
|
return QVariant(self.db[self.map[row]].in_library)
|
||||||
elif role == Qt.ToolTipRole and index.isValid():
|
elif role == Qt.ToolTipRole and index.isValid():
|
||||||
if self.map[row] in self.indices_to_be_deleted():
|
if self.map[row] in self.indices_to_be_deleted():
|
||||||
return QVariant(_('Marked for deletion'))
|
return QVariant(_('Marked for deletion'))
|
||||||
@ -1227,8 +1233,10 @@ class DeviceBooksModel(BooksModel): # {{{
|
|||||||
return NONE
|
return NONE
|
||||||
|
|
||||||
def headerData(self, section, orientation, role):
|
def headerData(self, section, orientation, role):
|
||||||
if role == Qt.ToolTipRole:
|
if role == Qt.ToolTipRole and orientation == Qt.Horizontal:
|
||||||
return QVariant(_('The lookup/search name is "{0}"').format(self.column_map[section]))
|
return QVariant(_('The lookup/search name is "{0}"').format(self.column_map[section]))
|
||||||
|
if DEBUG and role == Qt.ToolTipRole and orientation == Qt.Vertical:
|
||||||
|
return QVariant(_('This book\'s UUID is "{0}"').format(self.db[self.map[section]].uuid))
|
||||||
if role != Qt.DisplayRole:
|
if role != Qt.DisplayRole:
|
||||||
return NONE
|
return NONE
|
||||||
if orientation == Qt.Horizontal:
|
if orientation == Qt.Horizontal:
|
||||||
|
@ -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">
|
||||||
|
@ -836,11 +836,11 @@ class TagBrowserMixin(object): # {{{
|
|||||||
rename_func = partial(db.rename_custom_item, label=cc_label)
|
rename_func = partial(db.rename_custom_item, label=cc_label)
|
||||||
delete_func = partial(db.delete_custom_item_using_id, label=cc_label)
|
delete_func = partial(db.delete_custom_item_using_id, label=cc_label)
|
||||||
if rename_func:
|
if rename_func:
|
||||||
|
for item in to_delete:
|
||||||
|
delete_func(item)
|
||||||
for text in to_rename:
|
for text in to_rename:
|
||||||
for old_id in to_rename[text]:
|
for old_id in to_rename[text]:
|
||||||
rename_func(old_id, new_name=unicode(text))
|
rename_func(old_id, new_name=unicode(text))
|
||||||
for item in to_delete:
|
|
||||||
delete_func(item)
|
|
||||||
|
|
||||||
# Clean up everything, as information could have changed for many books.
|
# Clean up everything, as information could have changed for many books.
|
||||||
self.library_view.model().refresh()
|
self.library_view.model().refresh()
|
||||||
|
@ -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:
|
||||||
|
prints(_('You must provide the --really-do-it option to do a'
|
||||||
|
' recovery'), end='\n\n')
|
||||||
|
parser.print_help()
|
||||||
|
return 1
|
||||||
|
|
||||||
if opts.library_path is not None:
|
if opts.library_path is not None:
|
||||||
dbpath = opts.library_path
|
dbpath = opts.library_path
|
||||||
|
|
||||||
@ -1025,10 +1037,10 @@ information is the equivalent of what is shown in the tags pane.
|
|||||||
parser.add_option('-q', '--quote', default='"',
|
parser.add_option('-q', '--quote', default='"',
|
||||||
help=_('The character to put around the category value in CSV mode. '
|
help=_('The character to put around the category value in CSV mode. '
|
||||||
'Default is quotes (").'))
|
'Default is quotes (").'))
|
||||||
parser.add_option('-r', '--categories', default=None, 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=',',
|
||||||
@ -1052,8 +1064,10 @@ def command_list_categories(args, dbpath):
|
|||||||
db = LibraryDatabase2(dbpath)
|
db = LibraryDatabase2(dbpath)
|
||||||
category_data = db.get_categories()
|
category_data = db.get_categories()
|
||||||
data = []
|
data = []
|
||||||
|
report_on = [c.strip() for c in opts.report.split(',') if c.strip()]
|
||||||
categories = [k for k in category_data.keys()
|
categories = [k for k in category_data.keys()
|
||||||
if db.metadata_for_field(k)['kind'] not in ['user', 'search']]
|
if db.metadata_for_field(k)['kind'] not in ['user', 'search'] and
|
||||||
|
(not report_on or k in report_on)]
|
||||||
|
|
||||||
categories.sort(cmp=lambda x,y: cmp(x if x[0] != '#' else x[1:],
|
categories.sort(cmp=lambda x,y: cmp(x if x[0] != '#' else x[1:],
|
||||||
y if y[0] != '#' else y[1:]))
|
y if y[0] != '#' else y[1:]))
|
||||||
|
@ -681,7 +681,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
mi = self.data.get(idx, self.FIELD_MAP['all_metadata'],
|
mi = self.data.get(idx, self.FIELD_MAP['all_metadata'],
|
||||||
row_is_id = index_is_id)
|
row_is_id = index_is_id)
|
||||||
if mi is not None:
|
if mi is not None:
|
||||||
if get_cover and mi.cover is None:
|
if get_cover:
|
||||||
|
# Always get the cover, because the value can be wrong if the
|
||||||
|
# original mi was from the OPF
|
||||||
mi.cover = self.cover(idx, index_is_id=index_is_id, as_path=True)
|
mi.cover = self.cover(idx, index_is_id=index_is_id, as_path=True)
|
||||||
return mi
|
return mi
|
||||||
|
|
||||||
@ -1281,8 +1283,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
doit(self.set_series, id, mi.series, notify=False, commit=False)
|
doit(self.set_series, id, mi.series, notify=False, commit=False)
|
||||||
if mi.cover_data[1] is not None:
|
if mi.cover_data[1] is not None:
|
||||||
doit(self.set_cover, id, mi.cover_data[1]) # doesn't use commit
|
doit(self.set_cover, id, mi.cover_data[1]) # doesn't use commit
|
||||||
elif mi.cover is not None and os.access(mi.cover, os.R_OK):
|
elif mi.cover is not None:
|
||||||
doit(self.set_cover, id, lopen(mi.cover, 'rb'))
|
if os.access(mi.cover, os.R_OK):
|
||||||
|
with lopen(mi.cover, 'rb') as f:
|
||||||
|
doit(self.set_cover, id, f)
|
||||||
if mi.tags:
|
if mi.tags:
|
||||||
doit(self.set_tags, id, mi.tags, notify=False, commit=False)
|
doit(self.set_tags, id, mi.tags, notify=False, commit=False)
|
||||||
if mi.comments:
|
if mi.comments:
|
||||||
@ -1462,6 +1466,16 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
if notify:
|
if notify:
|
||||||
self.notify('metadata', [id])
|
self.notify('metadata', [id])
|
||||||
|
|
||||||
|
def set_uuid(self, id, uuid, notify=True, commit=True):
|
||||||
|
if uuid:
|
||||||
|
self.conn.execute('UPDATE books SET uuid=? WHERE id=?', (uuid, id))
|
||||||
|
self.data.set(id, self.FIELD_MAP['uuid'], uuid, row_is_id=True)
|
||||||
|
self.dirtied([id], commit=False)
|
||||||
|
if commit:
|
||||||
|
self.conn.commit()
|
||||||
|
if notify:
|
||||||
|
self.notify('metadata', [id])
|
||||||
|
|
||||||
# Convenience methods for tags_list_editor
|
# Convenience methods for tags_list_editor
|
||||||
# Note: we generally do not need to refresh_ids because library_view will
|
# Note: we generally do not need to refresh_ids because library_view will
|
||||||
# refresh everything.
|
# refresh everything.
|
||||||
@ -1485,7 +1499,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def rename_tag(self, old_id, new_name):
|
def rename_tag(self, old_id, new_name):
|
||||||
new_name = new_name.strip()
|
# It is possible that new_name is in fact a set of names. Split it on
|
||||||
|
# comma to find out. If it is, then rename the first one and append the
|
||||||
|
# rest
|
||||||
|
new_names = [t.strip() for t in new_name.strip().split(',') if t.strip()]
|
||||||
|
new_name = new_names[0]
|
||||||
|
new_names = new_names[1:]
|
||||||
|
|
||||||
|
# get the list of books that reference the tag being changed
|
||||||
|
books = self.conn.get('''SELECT book from books_tags_link
|
||||||
|
WHERE tag=?''', (old_id,))
|
||||||
|
books = [b[0] for b in books]
|
||||||
|
|
||||||
new_id = self.conn.get(
|
new_id = self.conn.get(
|
||||||
'''SELECT id from tags
|
'''SELECT id from tags
|
||||||
WHERE name=?''', (new_name,), all=False)
|
WHERE name=?''', (new_name,), all=False)
|
||||||
@ -1501,9 +1526,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
# all the changes. To get around this, we first delete any links
|
# all the changes. To get around this, we first delete any links
|
||||||
# to the new_id from books referencing the old_id, so that
|
# to the new_id from books referencing the old_id, so that
|
||||||
# renaming old_id to new_id will be unique on the book
|
# renaming old_id to new_id will be unique on the book
|
||||||
books = self.conn.get('''SELECT book from books_tags_link
|
for book_id in books:
|
||||||
WHERE tag=?''', (old_id,))
|
|
||||||
for (book_id,) in books:
|
|
||||||
self.conn.execute('''DELETE FROM books_tags_link
|
self.conn.execute('''DELETE FROM books_tags_link
|
||||||
WHERE book=? and tag=?''', (book_id, new_id))
|
WHERE book=? and tag=?''', (book_id, new_id))
|
||||||
|
|
||||||
@ -1512,7 +1535,13 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
WHERE tag=?''',(new_id, old_id,))
|
WHERE tag=?''',(new_id, old_id,))
|
||||||
# Get rid of the no-longer used publisher
|
# Get rid of the no-longer used publisher
|
||||||
self.conn.execute('DELETE FROM tags WHERE id=?', (old_id,))
|
self.conn.execute('DELETE FROM tags WHERE id=?', (old_id,))
|
||||||
self.dirty_books_referencing('tags', new_id, commit=False)
|
|
||||||
|
if new_names:
|
||||||
|
# have some left-over names to process. Add them to the book.
|
||||||
|
for book_id in books:
|
||||||
|
self.set_tags(book_id, new_names, append=True, notify=False,
|
||||||
|
commit=False)
|
||||||
|
self.dirtied(books, commit=False)
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
|
||||||
def delete_tag_using_id(self, id):
|
def delete_tag_using_id(self, id):
|
||||||
@ -2110,7 +2139,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
return None, len(ids)
|
return None, len(ids)
|
||||||
|
|
||||||
def import_book(self, mi, formats, notify=True, import_hooks=True,
|
def import_book(self, mi, formats, notify=True, import_hooks=True,
|
||||||
apply_import_tags=True):
|
apply_import_tags=True, preserve_uuid=False):
|
||||||
series_index = 1.0 if mi.series_index is None else mi.series_index
|
series_index = 1.0 if mi.series_index is None else mi.series_index
|
||||||
if apply_import_tags:
|
if apply_import_tags:
|
||||||
self._add_newbook_tag(mi)
|
self._add_newbook_tag(mi)
|
||||||
@ -2133,6 +2162,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
if mi.pubdate is None:
|
if mi.pubdate is None:
|
||||||
mi.pubdate = utcnow()
|
mi.pubdate = utcnow()
|
||||||
self.set_metadata(id, mi, ignore_errors=True)
|
self.set_metadata(id, mi, ignore_errors=True)
|
||||||
|
if preserve_uuid and mi.uuid:
|
||||||
|
self.set_uuid(id, mi.uuid, commit=False)
|
||||||
for path in formats:
|
for path in formats:
|
||||||
ext = os.path.splitext(path)[1][1:].lower()
|
ext = os.path.splitext(path)[1][1:].lower()
|
||||||
if ext == 'opf':
|
if ext == 'opf':
|
||||||
@ -2142,6 +2173,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
else:
|
else:
|
||||||
with lopen(path, 'rb') as f:
|
with lopen(path, 'rb') as f:
|
||||||
self.add_format(id, ext, f, index_is_id=True)
|
self.add_format(id, ext, f, index_is_id=True)
|
||||||
|
# Mark the book dirty, It probably already has been done by
|
||||||
|
# set_metadata, but probably isn't good enough
|
||||||
|
self.dirtied([id], commit=False)
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
self.data.refresh_ids(self, [id]) # Needed to update format list and size
|
self.data.refresh_ids(self, [id]) # Needed to update format list and size
|
||||||
if notify:
|
if notify:
|
||||||
|
@ -200,6 +200,8 @@ class Restore(Thread):
|
|||||||
def restore_book(self, book, db):
|
def restore_book(self, book, db):
|
||||||
db.create_book_entry(book['mi'], add_duplicates=True,
|
db.create_book_entry(book['mi'], add_duplicates=True,
|
||||||
force_id=book['id'])
|
force_id=book['id'])
|
||||||
|
if book['mi'].uuid:
|
||||||
|
db.set_uuid(book['id'], book['mi'].uuid, commit=False, notify=False)
|
||||||
db.conn.execute('UPDATE books SET path=? WHERE id=?', (book['path'],
|
db.conn.execute('UPDATE books SET path=? WHERE id=?', (book['path'],
|
||||||
book['id']))
|
book['id']))
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user