diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py index 0d08218790..3b96c98a7b 100644 --- a/src/calibre/ebooks/metadata/book/base.py +++ b/src/calibre/ebooks/metadata/book/base.py @@ -38,7 +38,8 @@ class SafeFormat(TemplateFormatter): def get_value(self, key, args, kwargs): 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) if b and b['datatype'] == 'int' and self.book.get(key, 0) == 0: v = '' 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/actions/copy_to_library.py b/src/calibre/gui2/actions/copy_to_library.py
index 513026f757..47f7904841 100644
--- a/src/calibre/gui2/actions/copy_to_library.py
+++ b/src/calibre/gui2/actions/copy_to_library.py
@@ -18,7 +18,7 @@ from calibre.utils.config import prefs, tweaks
class Worker(Thread):
- def __init__(self, ids, db, loc, progress, done):
+ def __init__(self, ids, db, loc, progress, done, delete_after):
Thread.__init__(self)
self.ids = ids
self.processed = set([])
@@ -27,6 +27,7 @@ class Worker(Thread):
self.error = None
self.progress = progress
self.done = done
+ self.delete_after = delete_after
def run(self):
try:
@@ -68,7 +69,8 @@ class Worker(Thread):
self.add_formats(identical_book, paths, newdb, replace=False)
if not added:
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')
if co is not None:
newdb.set_conversion_options(x, 'PIPE', co)
@@ -134,7 +136,8 @@ class CopyToLibraryAction(InterfaceAction):
self.pd.set_msg(_('Copying') + ' ' + title)
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.pd.exec_()
diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py
index 01f9347f67..bc9f5cf671 100644
--- a/src/calibre/gui2/device.py
+++ b/src/calibre/gui2/device.py
@@ -1413,15 +1413,16 @@ class DeviceMixin(object): # {{{
# Force a reset if the caches are not initialized
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
# library view. In this case, simply give up
try:
db = self.library_view.model().db
except:
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():
mi = db.get_metadata(id, index_is_id=True)
title = clean_string(mi.title)
@@ -1455,7 +1456,7 @@ class DeviceMixin(object): # {{{
if update_metadata:
book.smart_update(self.db_book_uuid_cache[book.uuid],
replace_metadata=True)
- book.in_library = True
+ book.in_library = 'UUID'
# ensure that the correct application_id is set
book.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
# also match.
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.
if update_metadata:
book.smart_update(d['db_ids'][book.application_id],
replace_metadata=True)
+ book.in_library = 'APP_ID'
continue
# Sonys know their db_id independent of the application_id
# in the metadata cache. Check that as well.
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:
book.smart_update(d['db_ids'][book.db_id],
replace_metadata=True)
+ book.in_library = 'DB_ID'
+ book.application_id = \
+ d['db_ids'][book.db_id].application_id
continue
# We now know that the application_id is not right. Set it
# to None to prevent book_on_device from accidentally
@@ -1494,19 +1495,19 @@ class DeviceMixin(object): # {{{
# either can appear as the author
book_authors = clean_string(authors_to_string(book.authors))
if book_authors in d['authors']:
- book.in_library = True
- book.application_id = \
- d['authors'][book_authors].application_id
if update_metadata:
book.smart_update(d['authors'][book_authors],
replace_metadata=True)
- elif book_authors in d['author_sort']:
- book.in_library = True
+ book.in_library = 'AUTHOR'
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:
book.smart_update(d['author_sort'][book_authors],
replace_metadata=True)
+ book.in_library = 'AUTH_SORT'
+ book.application_id = \
+ d['author_sort'][book_authors].application_id
else:
# Book definitely not matched. Clear its application ID
book.application_id = None
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/library/models.py b/src/calibre/gui2/library/models.py
index 53e0982211..2946985342 100644
--- a/src/calibre/gui2/library/models.py
+++ b/src/calibre/gui2/library/models.py
@@ -24,7 +24,7 @@ from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \
REGEXP_MATCH, CoverCache, MetadataBackup
from calibre.library.cli import parse_series_string
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
def human_readable(size, precision=1):
@@ -699,6 +699,10 @@ class BooksModel(QAbstractTableModel): # {{{
if role == Qt.DisplayRole:
return QVariant(self.headers[self.column_map[section]])
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
return QVariant(section+1)
return NONE
@@ -1206,6 +1210,8 @@ class DeviceBooksModel(BooksModel): # {{{
if tags:
tags.sort(cmp=lambda x,y: cmp(x.lower(), y.lower()))
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():
if self.map[row] in self.indices_to_be_deleted():
return QVariant(_('Marked for deletion'))
@@ -1227,8 +1233,10 @@ class DeviceBooksModel(BooksModel): # {{{
return NONE
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]))
+ 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:
return NONE
if orientation == Qt.Horizontal:
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