mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Fix #7207 ("Library Check" to also clean up and delete files/folders.)
This commit is contained in:
commit
a9b64899aa
@ -3,11 +3,16 @@ __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
|||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
from PyQt4.Qt import QDialog, QVBoxLayout, QHBoxLayout, QTreeWidget, QLabel, \
|
from PyQt4.Qt import QDialog, QVBoxLayout, QHBoxLayout, QTreeWidget, QLabel, \
|
||||||
QPushButton, QDialogButtonBox, QApplication, QTreeWidgetItem, \
|
QPushButton, QDialogButtonBox, QApplication, QTreeWidgetItem, \
|
||||||
QLineEdit
|
QLineEdit, Qt
|
||||||
|
|
||||||
|
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||||
from calibre.library.check_library import CheckLibrary, CHECKS
|
from calibre.library.check_library import CheckLibrary, CHECKS
|
||||||
|
from calibre.library.database2 import delete_file
|
||||||
|
from calibre import prints
|
||||||
|
|
||||||
class Item(QTreeWidgetItem):
|
class Item(QTreeWidgetItem):
|
||||||
pass
|
pass
|
||||||
@ -24,23 +29,28 @@ class CheckLibraryDialog(QDialog):
|
|||||||
self.setLayout(self._layout)
|
self.setLayout(self._layout)
|
||||||
|
|
||||||
self.log = QTreeWidget(self)
|
self.log = QTreeWidget(self)
|
||||||
|
self.log.itemChanged.connect(self.item_changed)
|
||||||
self._layout.addWidget(self.log)
|
self._layout.addWidget(self.log)
|
||||||
|
|
||||||
self.check = QPushButton(_('Run the check'))
|
self.check = QPushButton(_('&Run the check'))
|
||||||
self.check.setDefault(False)
|
self.check.setDefault(False)
|
||||||
self.check.clicked.connect(self.run_the_check)
|
self.check.clicked.connect(self.run_the_check)
|
||||||
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('&Done')
|
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.delete = QPushButton('Delete &marked')
|
||||||
|
self.delete.setDefault(False)
|
||||||
|
self.delete.clicked.connect(self.delete_marked)
|
||||||
self.cancel = QPushButton('&Cancel')
|
self.cancel = QPushButton('&Cancel')
|
||||||
self.cancel.setDefault(False)
|
self.cancel.setDefault(False)
|
||||||
self.cancel.clicked.connect(self.reject)
|
self.cancel.clicked.connect(self.reject)
|
||||||
self.bbox = QDialogButtonBox(self)
|
self.bbox = QDialogButtonBox(self)
|
||||||
self.bbox.addButton(self.copy, QDialogButtonBox.ActionRole)
|
self.bbox.addButton(self.copy, QDialogButtonBox.ActionRole)
|
||||||
self.bbox.addButton(self.check, QDialogButtonBox.ActionRole)
|
self.bbox.addButton(self.check, QDialogButtonBox.ActionRole)
|
||||||
|
self.bbox.addButton(self.delete, QDialogButtonBox.ActionRole)
|
||||||
self.bbox.addButton(self.cancel, QDialogButtonBox.RejectRole)
|
self.bbox.addButton(self.cancel, QDialogButtonBox.RejectRole)
|
||||||
self.bbox.addButton(self.ok, QDialogButtonBox.AcceptRole)
|
self.bbox.addButton(self.ok, QDialogButtonBox.AcceptRole)
|
||||||
|
|
||||||
@ -83,35 +93,66 @@ class CheckLibraryDialog(QDialog):
|
|||||||
plaintext = []
|
plaintext = []
|
||||||
|
|
||||||
def builder(tree, checker, check):
|
def builder(tree, checker, check):
|
||||||
attr = check[0]
|
attr, h, checkable = check
|
||||||
list = getattr(checker, attr, None)
|
list = getattr(checker, attr, None)
|
||||||
if list is None:
|
if list is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
h = check[1]
|
|
||||||
tl = Item([h])
|
tl = Item([h])
|
||||||
for problem in list:
|
for problem in list:
|
||||||
it = Item()
|
it = Item()
|
||||||
|
if checkable:
|
||||||
|
it.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable)
|
||||||
|
it.setCheckState(1, False)
|
||||||
|
else:
|
||||||
|
it.setFlags(Qt.ItemIsEnabled)
|
||||||
it.setText(0, problem[0])
|
it.setText(0, problem[0])
|
||||||
it.setText(1, problem[1])
|
it.setText(1, problem[1])
|
||||||
p = ', '.join(problem[2])
|
|
||||||
it.setText(2, p)
|
|
||||||
tl.addChild(it)
|
tl.addChild(it)
|
||||||
plaintext.append(','.join([h, problem[0], problem[1], p]))
|
self.all_items.append(it)
|
||||||
|
plaintext.append(','.join([h, problem[0], problem[1]]))
|
||||||
tree.addTopLevelItem(tl)
|
tree.addTopLevelItem(tl)
|
||||||
|
|
||||||
t = self.log
|
t = self.log
|
||||||
t.clear()
|
t.clear()
|
||||||
t.setColumnCount(3);
|
t.setColumnCount(2);
|
||||||
t.setHeaderLabels([_('Name'), _('Path from library'), _('Additional Information')])
|
t.setHeaderLabels([_('Name'), _('Path from library')])
|
||||||
|
self.all_items = []
|
||||||
for check in CHECKS:
|
for check in CHECKS:
|
||||||
builder(t, checker, check)
|
builder(t, checker, check)
|
||||||
|
|
||||||
t.setColumnWidth(0, 200)
|
t.setColumnWidth(0, 200)
|
||||||
t.setColumnWidth(1, 400)
|
t.setColumnWidth(1, 400)
|
||||||
|
self.delete.setEnabled(False)
|
||||||
self.text_results = '\n'.join(plaintext)
|
self.text_results = '\n'.join(plaintext)
|
||||||
|
|
||||||
|
def item_changed(self, item, column):
|
||||||
|
for it in self.all_items:
|
||||||
|
if it.checkState(1):
|
||||||
|
self.delete.setEnabled(True)
|
||||||
|
return
|
||||||
|
|
||||||
|
def delete_marked(self):
|
||||||
|
if not confirm('<p>'+_('The marked files and folders will be '
|
||||||
|
'<b>permanently deleted</b>. Are you sure?')
|
||||||
|
+'</p>', 'check_library_editor_delete', self):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Sort the paths in reverse length order so that we can be sure that
|
||||||
|
# if an item is in another item, the sub-item will be deleted first.
|
||||||
|
items = sorted(self.all_items,
|
||||||
|
key=lambda x: len(x.text(1)),
|
||||||
|
reverse=True)
|
||||||
|
for it in items:
|
||||||
|
if it.checkState(1):
|
||||||
|
try:
|
||||||
|
delete_file(os.path.join(self.db.library_path, unicode(it.text(1))))
|
||||||
|
except:
|
||||||
|
prints('failed to delete',
|
||||||
|
os.path.join(self.db.library_path,
|
||||||
|
unicode(it.text(1))))
|
||||||
|
self.run_the_check()
|
||||||
|
|
||||||
def copy_to_clipboard(self):
|
def copy_to_clipboard(self):
|
||||||
QApplication.clipboard().setText(self.text_results)
|
QApplication.clipboard().setText(self.text_results)
|
||||||
|
|
||||||
|
@ -14,14 +14,14 @@ from calibre.ebooks import BOOK_EXTENSIONS
|
|||||||
EBOOK_EXTENSIONS = frozenset(BOOK_EXTENSIONS)
|
EBOOK_EXTENSIONS = frozenset(BOOK_EXTENSIONS)
|
||||||
NORMALS = frozenset(['metadata.opf', 'cover.jpg'])
|
NORMALS = frozenset(['metadata.opf', 'cover.jpg'])
|
||||||
|
|
||||||
CHECKS = [('invalid_titles', _('Invalid titles')),
|
CHECKS = [('invalid_titles', _('Invalid titles'), True),
|
||||||
('extra_titles', _('Extra titles')),
|
('extra_titles', _('Extra titles'), True),
|
||||||
('invalid_authors', _('Invalid authors')),
|
('invalid_authors', _('Invalid authors'), True),
|
||||||
('extra_authors', _('Extra authors')),
|
('extra_authors', _('Extra authors'), True),
|
||||||
('missing_formats', _('Missing book formats')),
|
('missing_formats', _('Missing book formats'), False),
|
||||||
('extra_formats', _('Extra book formats')),
|
('extra_formats', _('Extra book formats'), True),
|
||||||
('extra_files', _('Unknown files in books')),
|
('extra_files', _('Unknown files in books'), True),
|
||||||
('failed_folders', _('Folders raising exception'))
|
('failed_folders', _('Folders raising exception'), False)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -41,7 +41,6 @@ class CheckLibrary(object):
|
|||||||
self.all_lc_dbpaths = frozenset([f.lower() for f in self.all_dbpaths])
|
self.all_lc_dbpaths = frozenset([f.lower() for f in self.all_dbpaths])
|
||||||
|
|
||||||
self.db_id_regexp = re.compile(r'^.* \((\d+)\)$')
|
self.db_id_regexp = re.compile(r'^.* \((\d+)\)$')
|
||||||
self.bad_ext_pat = re.compile(r'[^a-z0-9]+')
|
|
||||||
|
|
||||||
self.dirs = []
|
self.dirs = []
|
||||||
self.book_dirs = []
|
self.book_dirs = []
|
||||||
@ -78,7 +77,7 @@ class CheckLibrary(object):
|
|||||||
auth_path = os.path.join(lib, auth_dir)
|
auth_path = os.path.join(lib, auth_dir)
|
||||||
# First check: author must be a directory
|
# First check: author must be a directory
|
||||||
if not os.path.isdir(auth_path):
|
if not os.path.isdir(auth_path):
|
||||||
self.invalid_authors.append((auth_dir, auth_dir, []))
|
self.invalid_authors.append((auth_dir, auth_dir))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self.potential_authors[auth_dir] = {}
|
self.potential_authors[auth_dir] = {}
|
||||||
@ -93,7 +92,7 @@ class CheckLibrary(object):
|
|||||||
m = self.db_id_regexp.search(title_dir)
|
m = self.db_id_regexp.search(title_dir)
|
||||||
# Second check: title must have an ID and must be a directory
|
# Second check: title must have an ID and must be a directory
|
||||||
if m is None or not os.path.isdir(title_path):
|
if m is None or not os.path.isdir(title_path):
|
||||||
self.invalid_titles.append((auth_dir, db_path, [title_dir]))
|
self.invalid_titles.append((auth_dir, db_path))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
id = m.group(1)
|
id = m.group(1)
|
||||||
@ -101,12 +100,12 @@ class CheckLibrary(object):
|
|||||||
if self.is_case_sensitive:
|
if self.is_case_sensitive:
|
||||||
if int(id) not in self.all_ids or \
|
if int(id) not in self.all_ids or \
|
||||||
db_path not in self.all_dbpaths:
|
db_path not in self.all_dbpaths:
|
||||||
self.extra_titles.append((title_dir, db_path, []))
|
self.extra_titles.append((title_dir, db_path))
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
if int(id) not in self.all_ids or \
|
if int(id) not in self.all_ids or \
|
||||||
db_path.lower() not in self.all_lc_dbpaths:
|
db_path.lower() not in self.all_lc_dbpaths:
|
||||||
self.extra_titles.append((title_dir, db_path, []))
|
self.extra_titles.append((title_dir, db_path))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Record the book to check its formats
|
# Record the book to check its formats
|
||||||
@ -115,7 +114,7 @@ class CheckLibrary(object):
|
|||||||
|
|
||||||
# Fourth check: author directories that contain no titles
|
# Fourth check: author directories that contain no titles
|
||||||
if not found_titles:
|
if not found_titles:
|
||||||
self.extra_authors.append((auth_dir, auth_dir, []))
|
self.extra_authors.append((auth_dir, auth_dir))
|
||||||
|
|
||||||
for x in self.book_dirs:
|
for x in self.book_dirs:
|
||||||
try:
|
try:
|
||||||
@ -132,9 +131,7 @@ class CheckLibrary(object):
|
|||||||
ext = ext[1:].lower()
|
ext = ext[1:].lower()
|
||||||
if ext in EBOOK_EXTENSIONS:
|
if ext in EBOOK_EXTENSIONS:
|
||||||
return True
|
return True
|
||||||
if self.bad_ext_pat.search(ext) is not None:
|
|
||||||
return False
|
return False
|
||||||
return True
|
|
||||||
|
|
||||||
def process_book(self, lib, book_info):
|
def process_book(self, lib, book_info):
|
||||||
(db_path, title_dir, book_id) = book_info
|
(db_path, title_dir, book_id) = book_info
|
||||||
@ -148,18 +145,18 @@ class CheckLibrary(object):
|
|||||||
if self.is_case_sensitive:
|
if self.is_case_sensitive:
|
||||||
unknowns = frozenset(filenames-formats-NORMALS)
|
unknowns = frozenset(filenames-formats-NORMALS)
|
||||||
# Check: any books that aren't formats or normally there?
|
# Check: any books that aren't formats or normally there?
|
||||||
if unknowns:
|
for u in unknowns:
|
||||||
self.extra_files.append((title_dir, db_path, unknowns))
|
self.extra_files.append((title_dir, os.path.join(db_path, u)))
|
||||||
|
|
||||||
# Check: any book formats that should be there?
|
# Check: any book formats that should be there?
|
||||||
missing = book_formats - formats
|
missing = book_formats - formats
|
||||||
if missing:
|
for m in missing:
|
||||||
self.missing_formats.append((title_dir, db_path, missing))
|
self.missing_formats.append((title_dir, os.path.join(db_path, m)))
|
||||||
|
|
||||||
# Check: any book formats that shouldn't be there?
|
# Check: any book formats that shouldn't be there?
|
||||||
extra = formats - book_formats - NORMALS
|
extra = formats - book_formats - NORMALS
|
||||||
if extra:
|
for e in extra:
|
||||||
self.extra_formats.append((title_dir, db_path, extra))
|
self.extra_formats.append((title_dir, os.path.join(db_path, e)))
|
||||||
else:
|
else:
|
||||||
def lc_map(fnames, fset):
|
def lc_map(fnames, fset):
|
||||||
m = {}
|
m = {}
|
||||||
@ -171,19 +168,16 @@ class CheckLibrary(object):
|
|||||||
formats_lc = frozenset([f.lower() for f in formats])
|
formats_lc = frozenset([f.lower() for f in formats])
|
||||||
unknowns = frozenset(filenames_lc-formats_lc-NORMALS)
|
unknowns = frozenset(filenames_lc-formats_lc-NORMALS)
|
||||||
# Check: any books that aren't formats or normally there?
|
# Check: any books that aren't formats or normally there?
|
||||||
if unknowns:
|
for f in lc_map(filenames, unknowns):
|
||||||
self.extra_files.append((title_dir, db_path,
|
self.extra_files.append((title_dir, os.path.join(db_path, f)))
|
||||||
lc_map(filenames, unknowns)))
|
|
||||||
|
|
||||||
book_formats_lc = frozenset([f.lower() for f in book_formats])
|
book_formats_lc = frozenset([f.lower() for f in book_formats])
|
||||||
# Check: any book formats that should be there?
|
# Check: any book formats that should be there?
|
||||||
missing = book_formats_lc - formats_lc
|
missing = book_formats_lc - formats_lc
|
||||||
if missing:
|
for m in lc_map(book_formats, missing):
|
||||||
self.missing_formats.append((title_dir, db_path,
|
self.missing_formats.append((title_dir, os.path.join(db_path, m)))
|
||||||
lc_map(book_formats, missing)))
|
|
||||||
|
|
||||||
# Check: any book formats that shouldn't be there?
|
# Check: any book formats that shouldn't be there?
|
||||||
extra = formats_lc - book_formats_lc - NORMALS
|
extra = formats_lc - book_formats_lc - NORMALS
|
||||||
if extra:
|
for e in lc_map(formats, extra):
|
||||||
self.extra_formats.append((title_dir, db_path,
|
self.extra_formats.append((title_dir, os.path.join(db_path, e)))
|
||||||
lc_map(formats, extra)))
|
|
||||||
|
@ -943,11 +943,11 @@ def command_check_library(args, dbpath):
|
|||||||
return
|
return
|
||||||
if opts.csv:
|
if opts.csv:
|
||||||
for i in list:
|
for i in list:
|
||||||
print check[1] + ',' + i[0] + ',' + i[1] + ',' + '|'.join(i[2])
|
print check[1] + ',' + i[0] + ',' + i[1]
|
||||||
else:
|
else:
|
||||||
print check[1]
|
print check[1]
|
||||||
for i in list:
|
for i in list:
|
||||||
print ' %-30.30s - %-30.30s - %s'%(i[0], i[1], ', '.join(i[2]))
|
print ' %-40.40s - %-40.40s'%(i[0], i[1])
|
||||||
|
|
||||||
db = LibraryDatabase2(dbpath)
|
db = LibraryDatabase2(dbpath)
|
||||||
checker = CheckLibrary(dbpath, db)
|
checker = CheckLibrary(dbpath, db)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user