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:
Kovid Goyal 2010-10-08 11:21:13 -06:00
commit 4ab91285cc
12 changed files with 225 additions and 159 deletions

View File

@ -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 = ''

View File

@ -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.' + '<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):
if not self.change_library_allowed():
return

View File

@ -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_()

View File

@ -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

View File

@ -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')

View File

@ -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:

View File

@ -5,81 +5,14 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__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)

View File

@ -77,13 +77,6 @@
</property>
</spacer>
</item>
<item row="5" column="0" colspan="2">
<widget class="QPushButton" name="compact_button">
<property name="text">
<string>&amp;Check database integrity</string>
</property>
</widget>
</item>
<item row="6" column="0">
<spacer name="verticalSpacer_7">
<property name="orientation">
@ -124,20 +117,6 @@
</property>
</widget>
</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">
<spacer name="verticalSpacer_9">
<property name="orientation">

View File

@ -836,11 +836,11 @@ class TagBrowserMixin(object): # {{{
rename_func = partial(db.rename_custom_item, label=cc_label)
delete_func = partial(db.delete_custom_item_using_id, label=cc_label)
if rename_func:
for item in to_delete:
delete_func(item)
for text in to_rename:
for old_id in to_rename[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.
self.library_view.model().refresh()

View File

@ -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:
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:
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='"',
help=_('The character to put around the category value in CSV mode. '
'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"
"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=',',
@ -1052,8 +1064,10 @@ def command_list_categories(args, dbpath):
db = LibraryDatabase2(dbpath)
category_data = db.get_categories()
data = []
report_on = [c.strip() for c in opts.report.split(',') if c.strip()]
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:],
y if y[0] != '#' else y[1:]))

View File

@ -681,7 +681,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
mi = self.data.get(idx, self.FIELD_MAP['all_metadata'],
row_is_id = index_is_id)
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)
return mi
@ -1281,8 +1283,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
doit(self.set_series, id, mi.series, notify=False, commit=False)
if mi.cover_data[1] is not None:
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):
doit(self.set_cover, id, lopen(mi.cover, 'rb'))
elif mi.cover is not None:
if os.access(mi.cover, os.R_OK):
with lopen(mi.cover, 'rb') as f:
doit(self.set_cover, id, f)
if mi.tags:
doit(self.set_tags, id, mi.tags, notify=False, commit=False)
if mi.comments:
@ -1462,6 +1466,16 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if notify:
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
# Note: we generally do not need to refresh_ids because library_view will
# refresh everything.
@ -1485,7 +1499,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return result
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(
'''SELECT id from tags
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
# to the new_id from books referencing the old_id, so that
# renaming old_id to new_id will be unique on the book
books = self.conn.get('''SELECT book from books_tags_link
WHERE tag=?''', (old_id,))
for (book_id,) in books:
for book_id in books:
self.conn.execute('''DELETE FROM books_tags_link
WHERE book=? and tag=?''', (book_id, new_id))
@ -1512,7 +1535,13 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
WHERE tag=?''',(new_id, old_id,))
# Get rid of the no-longer used publisher
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()
def delete_tag_using_id(self, id):
@ -2110,7 +2139,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return None, len(ids)
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
if apply_import_tags:
self._add_newbook_tag(mi)
@ -2133,6 +2162,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if mi.pubdate is None:
mi.pubdate = utcnow()
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:
ext = os.path.splitext(path)[1][1:].lower()
if ext == 'opf':
@ -2142,6 +2173,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
else:
with lopen(path, 'rb') as f:
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.data.refresh_ids(self, [id]) # Needed to update format list and size
if notify:

View File

@ -200,6 +200,8 @@ class Restore(Thread):
def restore_book(self, book, db):
db.create_book_entry(book['mi'], add_duplicates=True,
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'],
book['id']))