From 2e9c1d6d55beb7bbb1101b8bf084d63f9007af49 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 18 Oct 2010 13:14:59 +0100 Subject: [PATCH] Enhancement #7207 - Add possibility of deleting items in check_library --- src/calibre/gui2/dialogs/check_library.py | 62 +++++++++++++++++++---- src/calibre/library/check_library.py | 58 ++++++++++----------- src/calibre/library/cli.py | 4 +- 3 files changed, 79 insertions(+), 45 deletions(-) diff --git a/src/calibre/gui2/dialogs/check_library.py b/src/calibre/gui2/dialogs/check_library.py index 46071d3c06..741a42893d 100644 --- a/src/calibre/gui2/dialogs/check_library.py +++ b/src/calibre/gui2/dialogs/check_library.py @@ -3,11 +3,15 @@ __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __docformat__ = 'restructuredtext en' __license__ = 'GPL v3' +import os + from PyQt4.Qt import QDialog, QVBoxLayout, QHBoxLayout, QTreeWidget, QLabel, \ 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.database2 import delete_file class Item(QTreeWidgetItem): pass @@ -24,23 +28,28 @@ class CheckLibraryDialog(QDialog): self.setLayout(self._layout) self.log = QTreeWidget(self) + self.log.itemChanged.connect(self.item_changed) self._layout.addWidget(self.log) - self.check = QPushButton(_('Run the check')) + self.check = QPushButton(_('&Run the check')) self.check.setDefault(False) 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.clicked.connect(self.copy_to_clipboard) self.ok = QPushButton('&Done') self.ok.setDefault(True) 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.setDefault(False) self.cancel.clicked.connect(self.reject) self.bbox = QDialogButtonBox(self) self.bbox.addButton(self.copy, 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.ok, QDialogButtonBox.AcceptRole) @@ -83,35 +92,66 @@ class CheckLibraryDialog(QDialog): plaintext = [] def builder(tree, checker, check): - attr = check[0] + attr, h, checkable = check list = getattr(checker, attr, None) if list is None: return - h = check[1] tl = Item([h]) for problem in list: 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(1, problem[1]) - p = ', '.join(problem[2]) - it.setText(2, p) 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) t = self.log t.clear() - t.setColumnCount(3); - t.setHeaderLabels([_('Name'), _('Path from library'), _('Additional Information')]) + t.setColumnCount(2); + t.setHeaderLabels([_('Name'), _('Path from library')]) + self.all_items = [] for check in CHECKS: builder(t, checker, check) t.setColumnWidth(0, 200) t.setColumnWidth(1, 400) - + self.delete.setEnabled(False) self.text_results = '\n'.join(plaintext) + def item_changed(self, item, column): + print 'item_changed' + for it in self.all_items: + if it.checkState(1): + self.delete.setEnabled(True) + return + + def delete_marked(self): + print 'delete marked' + if not confirm('
'+_('The marked files and folders will be ' + 'permanently deleted. Are you sure?') + +'
', '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: + print 'failed to delete', os.path.join(self.db.library_path ,unicode(it.text(1))) + self.run_the_check() + def copy_to_clipboard(self): QApplication.clipboard().setText(self.text_results) diff --git a/src/calibre/library/check_library.py b/src/calibre/library/check_library.py index a6f3d40131..85f3d4747c 100644 --- a/src/calibre/library/check_library.py +++ b/src/calibre/library/check_library.py @@ -14,14 +14,14 @@ from calibre.ebooks import BOOK_EXTENSIONS EBOOK_EXTENSIONS = frozenset(BOOK_EXTENSIONS) NORMALS = frozenset(['metadata.opf', 'cover.jpg']) -CHECKS = [('invalid_titles', _('Invalid titles')), - ('extra_titles', _('Extra titles')), - ('invalid_authors', _('Invalid authors')), - ('extra_authors', _('Extra authors')), - ('missing_formats', _('Missing book formats')), - ('extra_formats', _('Extra book formats')), - ('extra_files', _('Unknown files in books')), - ('failed_folders', _('Folders raising exception')) +CHECKS = [('invalid_titles', _('Invalid titles'), True), + ('extra_titles', _('Extra titles'), True), + ('invalid_authors', _('Invalid authors'), True), + ('extra_authors', _('Extra authors'), True), + ('missing_formats', _('Missing book formats'), False), + ('extra_formats', _('Extra book formats'), True), + ('extra_files', _('Unknown files in books'), True), + ('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.db_id_regexp = re.compile(r'^.* \((\d+)\)$') - self.bad_ext_pat = re.compile(r'[^a-z0-9]+') self.dirs = [] self.book_dirs = [] @@ -78,7 +77,7 @@ class CheckLibrary(object): auth_path = os.path.join(lib, auth_dir) # First check: author must be a directory if not os.path.isdir(auth_path): - self.invalid_authors.append((auth_dir, auth_dir, [])) + self.invalid_authors.append((auth_dir, auth_dir)) continue self.potential_authors[auth_dir] = {} @@ -93,7 +92,7 @@ class CheckLibrary(object): m = self.db_id_regexp.search(title_dir) # Second check: title must have an ID and must be a directory 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 id = m.group(1) @@ -101,12 +100,12 @@ class CheckLibrary(object): if self.is_case_sensitive: if int(id) not in self.all_ids or \ db_path not in self.all_dbpaths: - self.extra_titles.append((title_dir, db_path, [])) + self.extra_titles.append((title_dir, db_path)) continue else: if int(id) not in self.all_ids or \ 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 # Record the book to check its formats @@ -115,7 +114,7 @@ class CheckLibrary(object): # Fourth check: author directories that contain no 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: try: @@ -132,9 +131,7 @@ class CheckLibrary(object): ext = ext[1:].lower() if ext in EBOOK_EXTENSIONS: return True - if self.bad_ext_pat.search(ext) is not None: - return False - return True + return False def process_book(self, lib, book_info): (db_path, title_dir, book_id) = book_info @@ -148,18 +145,18 @@ class CheckLibrary(object): if self.is_case_sensitive: unknowns = frozenset(filenames-formats-NORMALS) # Check: any books that aren't formats or normally there? - if unknowns: - self.extra_files.append((title_dir, db_path, unknowns)) + for u in unknowns: + self.extra_files.append((title_dir, os.path.join(db_path, u))) # Check: any book formats that should be there? missing = book_formats - formats - if missing: - self.missing_formats.append((title_dir, db_path, missing)) + for m in missing: + self.missing_formats.append((title_dir, os.path.join(db_path, m))) # Check: any book formats that shouldn't be there? extra = formats - book_formats - NORMALS - if extra: - self.extra_formats.append((title_dir, db_path, extra)) + for e in extra: + self.extra_formats.append((title_dir, os.path.join(db_path, e))) else: def lc_map(fnames, fset): m = {} @@ -171,19 +168,16 @@ class CheckLibrary(object): formats_lc = frozenset([f.lower() for f in formats]) unknowns = frozenset(filenames_lc-formats_lc-NORMALS) # Check: any books that aren't formats or normally there? - if unknowns: - self.extra_files.append((title_dir, db_path, - lc_map(filenames, unknowns))) + for f in lc_map(filenames, unknowns): + self.extra_files.append((title_dir, os.path.join(db_path, f))) book_formats_lc = frozenset([f.lower() for f in book_formats]) # Check: any book formats that should be there? missing = book_formats_lc - formats_lc - if missing: - self.missing_formats.append((title_dir, db_path, - lc_map(book_formats, missing))) + for m in lc_map(book_formats, missing): + self.missing_formats.append((title_dir, os.path.join(db_path, m))) # Check: any book formats that shouldn't be there? extra = formats_lc - book_formats_lc - NORMALS - if extra: - self.extra_formats.append((title_dir, db_path, - lc_map(formats, extra))) + for e in lc_map(formats, extra): + self.extra_formats.append((title_dir, os.path.join(db_path, e))) diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py index 08fd0fbf86..7d3fb329e0 100644 --- a/src/calibre/library/cli.py +++ b/src/calibre/library/cli.py @@ -943,11 +943,11 @@ def command_check_library(args, dbpath): return if opts.csv: for i in list: - print check[1] + ',' + i[0] + ',' + i[1] + ',' + '|'.join(i[2]) + print check[1] + ',' + i[0] + ',' + i[1] else: print check[1] 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) checker = CheckLibrary(dbpath, db)