mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 10:44: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):
|
||||
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 = ''
|
||||
|
@ -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
|
||||
|
@ -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_()
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -77,13 +77,6 @@
|
||||
</property>
|
||||
</spacer>
|
||||
</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">
|
||||
<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">
|
||||
|
@ -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()
|
||||
|
@ -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:]))
|
||||
|
@ -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:
|
||||
|
@ -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']))
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user