diff --git a/core/app.py b/core/app.py index 9ba88703..da0618e7 100644 --- a/core/app.py +++ b/core/app.py @@ -23,8 +23,9 @@ from hscommon.trans import tr from hscommon.plat import ISWINDOWS from hscommon import desktop -from . import directories, results, scanner, export, fs +from . import directories, results, export, fs from .ignore import IgnoreList +from .scanner import ScanType, Scanner from .gui.deletion_options import DeletionOptions from .gui.details_panel import DetailsPanel from .gui.directory_tree import DirectoryTree @@ -113,7 +114,7 @@ class DupeGuru(Broadcaster): """Holds everything together. Instantiated once per running application, it holds a reference to every high-level object - whose reference needs to be held: :class:`~core.results.Results`, :class:`Scanner`, + whose reference needs to be held: :class:`~core.results.Results`, :class:`~core.directories.Directories`, :mod:`core.gui` instances, etc.. It also hosts high level methods and acts as a coordinator for all those elements. This is why @@ -154,7 +155,7 @@ class DupeGuru(Broadcaster): # select_dest_file(prompt: str, ext: str) --> str PROMPT_NAME = "dupeGuru" - SCANNER_CLASS = scanner.Scanner + SCANNER_CLASS = Scanner def __init__(self, view): if view.get_default(DEBUG_MODE_PREFERENCE): @@ -165,10 +166,13 @@ class DupeGuru(Broadcaster): self.appdata = desktop.special_folder_path(desktop.SpecialFolder.AppData, appname=self.NAME) if not op.exists(self.appdata): os.makedirs(self.appdata) + self.discarded_file_count = 0 self.directories = directories.Directories() self.results = results.Results(self) self.ignore_list = IgnoreList() - self.scanner = self.SCANNER_CLASS() + # In addition to "app-level" options, this dictionary also holds options that will be + # sent to the scanner. They don't have default values because those defaults values are + # defined in the scanner class. self.options = { 'escape_filter_regexp': True, 'clean_empty_dirs': False, @@ -360,7 +364,7 @@ class DupeGuru(Broadcaster): self.view.show_message(tr("'{}' does not exist.").format(d)) def add_selected_to_ignore_list(self): - """Adds :attr:`selected_dupes` to :attr:`scanner`'s ignore list. + """Adds :attr:`selected_dupes` to :attr:`ignore_list`. """ dupes = self.without_ref(self.selected_dupes) if not dupes: @@ -731,22 +735,29 @@ class DupeGuru(Broadcaster): Scans folders selected in :attr:`directories` and put the results in :attr:`results` """ + scanner = self.SCANNER_CLASS() + if not self.directories.has_any_file(): + self.view.show_message(tr("The selected directories contain no scannable file.")) + return + # Send relevant options down to the scanner instance + for k, v in self.options.items(): + if hasattr(scanner, k): + setattr(scanner, k, v) + self.results.groups = [] + self._results_changed() + def do(j): j.set_progress(0, tr("Collecting files to scan")) - if self.scanner.scan_type == scanner.ScanType.Folders: + if scanner.scan_type == ScanType.Folders: files = list(self.directories.get_folders(j)) else: files = list(self.directories.get_files(j)) if self.options['ignore_hardlink_matches']: files = self._remove_hardlink_dupes(files) logging.info('Scanning %d files' % len(files)) - self.results.groups = self.scanner.get_dupe_groups(files, self.ignore_list, j) + self.results.groups = scanner.get_dupe_groups(files, self.ignore_list, j) + self.discarded_file_count = scanner.discarded_file_count - if not self.directories.has_any_file(): - self.view.show_message(tr("The selected directories contain no scannable file.")) - return - self.results.groups = [] - self._results_changed() self._start_job(JobType.Scan, do) def toggle_selected_mark_state(self): @@ -783,7 +794,7 @@ class DupeGuru(Broadcaster): @property def stat_line(self): result = self.results.stat_line - if self.scanner.discarded_file_count: - result = tr("%s (%d discarded)") % (result, self.scanner.discarded_file_count) + if self.discarded_file_count: + result = tr("%s (%d discarded)") % (result, self.discarded_file_count) return result diff --git a/core/scanner.py b/core/scanner.py index 434bb671..2397a084 100644 --- a/core/scanner.py +++ b/core/scanner.py @@ -124,7 +124,8 @@ class Scanner: return True return len(dupe.path) > len(ref.path) - def get_scan_options(self): + @staticmethod + def get_scan_options(): """Returns a list of scanning options for this scanner. Returns a list of ``ScanOption``. diff --git a/core/tests/app_test.py b/core/tests/app_test.py index 36dd9076..f3b1acb1 100644 --- a/core/tests/app_test.py +++ b/core/tests/app_test.py @@ -105,7 +105,7 @@ class TestCaseDupeGuru: os.link(str(tmppath['myfile']), str(tmppath['hardlink'])) app = TestApp().app app.directories.add_path(tmppath) - app.scanner.scan_type = ScanType.Contents + app.options['scan_type'] = ScanType.Contents app.options['ignore_hardlink_matches'] = True app.start_scanning() eq_(len(app.results.groups), 0) diff --git a/core_me/scanner.py b/core_me/scanner.py index 2e42170b..69fc2e21 100644 --- a/core_me/scanner.py +++ b/core_me/scanner.py @@ -13,7 +13,8 @@ class ScannerME(ScannerBase): def _key_func(dupe): return (-dupe.bitrate, -dupe.size) - def get_scan_options(self): + @staticmethod + def get_scan_options(): return [ ScanOption(ScanType.Filename, tr("Filename")), ScanOption(ScanType.Fields, tr("Filename - Fields")), diff --git a/core_pe/scanner.py b/core_pe/scanner.py index 6fb4eb90..44c3a5e4 100644 --- a/core_pe/scanner.py +++ b/core_pe/scanner.py @@ -16,7 +16,8 @@ class ScannerPE(Scanner): match_scaled = False threshold = 75 - def get_scan_options(self): + @staticmethod + def get_scan_options(): return [ ScanOption(ScanType.FuzzyBlock, tr("Contents")), ScanOption(ScanType.ExifTimestamp, tr("EXIF Timestamp")), diff --git a/core_se/scanner.py b/core_se/scanner.py index aab8f8dd..f3f8938b 100644 --- a/core_se/scanner.py +++ b/core_se/scanner.py @@ -9,7 +9,8 @@ from hscommon.trans import tr from core.scanner import Scanner as ScannerBase, ScanOption, ScanType class ScannerSE(ScannerBase): - def get_scan_options(self): + @staticmethod + def get_scan_options(): return [ ScanOption(ScanType.Filename, tr("Filename")), ScanOption(ScanType.Contents, tr("Contents")), diff --git a/qt/base/app.py b/qt/base/app.py index e0860df6..3f41ded8 100644 --- a/qt/base/app.py +++ b/qt/base/app.py @@ -89,7 +89,7 @@ class DupeGuru(QObject): createActions(ACTIONS, self) def _update_options(self): - self.model.scanner.mix_file_kind = self.prefs.mix_file_kind + self.model.options['mix_file_kind'] = self.prefs.mix_file_kind self.model.options['escape_filter_regexp'] = not self.prefs.use_regexp self.model.options['clean_empty_dirs'] = self.prefs.remove_empty_folders self.model.options['ignore_hardlink_matches'] = self.prefs.ignore_hardlink_matches diff --git a/qt/base/directories_dialog.py b/qt/base/directories_dialog.py index 28a22c5c..3a721180 100644 --- a/qt/base/directories_dialog.py +++ b/qt/base/directories_dialog.py @@ -28,7 +28,7 @@ class DirectoriesDialog(QMainWindow): self.lastAddedFolder = platform.INITIAL_FOLDER_IN_DIALOGS self.recentFolders = Recent(self.app, 'recentFolders') self._setupUi() - SCAN_TYPE_ORDER = [so.scan_type for so in self.app.model.scanner.get_scan_options()] + SCAN_TYPE_ORDER = [so.scan_type for so in self.app.model.SCANNER_CLASS.get_scan_options()] scan_type_index = SCAN_TYPE_ORDER.index(self.app.prefs.scan_type) self.scanTypeComboBox.setCurrentIndex(scan_type_index) self.directoriesModel = DirectoriesModel(self.app.model.directory_tree, view=self.treeView) @@ -130,7 +130,7 @@ class DirectoriesDialog(QMainWindow): self.scanTypeComboBox = QComboBox(self) self.scanTypeComboBox.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)) self.scanTypeComboBox.setMaximumWidth(400) - for scan_option in self.app.model.scanner.get_scan_options(): + for scan_option in self.app.model.SCANNER_CLASS.get_scan_options(): self.scanTypeComboBox.addItem(scan_option.label) hl.addWidget(self.scanTypeComboBox) self.showPreferencesButton = QPushButton(tr("Options"), self.centralwidget) @@ -240,7 +240,7 @@ class DirectoriesDialog(QMainWindow): self.app.model.start_scanning() def scanTypeChanged(self, index): - scan_options = self.app.model.scanner.get_scan_options() + scan_options = self.app.model.SCANNER_CLASS.get_scan_options() self.app.prefs.scan_type = scan_options[index].scan_type self.app._update_options() diff --git a/qt/me/app.py b/qt/me/app.py index 4d39af59..3158274f 100644 --- a/qt/me/app.py +++ b/qt/me/app.py @@ -1,9 +1,7 @@ -# Created By: Virgil Dupras -# Created On: 2009-05-21 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html from core_me import __appname__ @@ -20,18 +18,18 @@ class DupeGuru(DupeGuruBase): EDITION = 'me' LOGO_NAME = 'logo_me' NAME = __appname__ - + DETAILS_DIALOG_CLASS = DetailsDialog RESULT_MODEL_CLASS = ResultsModel PREFERENCES_CLASS = Preferences PREFERENCES_DIALOG_CLASS = PreferencesDialog - + def _update_options(self): DupeGuruBase._update_options(self) - self.model.scanner.min_match_percentage = self.prefs.filter_hardness - self.model.scanner.scan_type = self.prefs.scan_type - self.model.scanner.word_weighting = self.prefs.word_weighting - self.model.scanner.match_similar_words = self.prefs.match_similar + self.model.options['min_match_percentage'] = self.prefs.filter_hardness + self.model.options['scan_type'] = self.prefs.scan_type + self.model.options['word_weighting'] = self.prefs.word_weighting + self.model.options['match_similar_words'] = self.prefs.match_similar scanned_tags = set() if self.prefs.scan_tag_track: scanned_tags.add('track') @@ -45,5 +43,5 @@ class DupeGuru(DupeGuruBase): scanned_tags.add('genre') if self.prefs.scan_tag_year: scanned_tags.add('year') - self.model.scanner.scanned_tags = scanned_tags - + self.model.options['scanned_tags'] = scanned_tags + diff --git a/qt/pe/app.py b/qt/pe/app.py index 305eccc3..2f02f8f7 100644 --- a/qt/pe/app.py +++ b/qt/pe/app.py @@ -1,9 +1,7 @@ -# Created By: Virgil Dupras -# Created On: 2009-04-25 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) +# +# This software is licensed under the "GPLv3" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html import logging @@ -34,7 +32,7 @@ class File(PhotoBase): except EnvironmentError: logging.warning("Could not read image '%s'", str(self.path)) return (0, 0) - + def _plat_get_blocks(self, block_count_per_side, orientation): image = QImage(str(self.path)) image = image.convertToFormat(QImage.Format_RGB888) @@ -63,30 +61,30 @@ class File(PhotoBase): t.rotate(270) image = image.transformed(t) return getblocks(image, block_count_per_side) - + class DupeGuru(DupeGuruBase): MODELCLASS = DupeGuruModel EDITION = 'pe' LOGO_NAME = 'logo_pe' NAME = __appname__ - + DETAILS_DIALOG_CLASS = DetailsDialog RESULT_WINDOW_CLASS = ResultWindow RESULT_MODEL_CLASS = ResultsModel PREFERENCES_CLASS = Preferences PREFERENCES_DIALOG_CLASS = PreferencesDialog - + def _setup(self): self.model.directories.fileclasses = [File] DupeGuruBase._setup(self) self.directories_dialog.menuFile.insertAction( self.directories_dialog.actionLoadResults, self.resultWindow.actionClearPictureCache ) - + def _update_options(self): DupeGuruBase._update_options(self) - self.model.scanner.scan_type = self.prefs.scan_type - self.model.scanner.match_scaled = self.prefs.match_scaled - self.model.scanner.threshold = self.prefs.filter_hardness - + self.model.options['scan_type'] = self.prefs.scan_type + self.model.options['match_scaled'] = self.prefs.match_scaled + self.model.options['threshold'] = self.prefs.filter_hardness + diff --git a/qt/se/app.py b/qt/se/app.py index d2389a97..43005a70 100644 --- a/qt/se/app.py +++ b/qt/se/app.py @@ -1,6 +1,4 @@ -# Created By: Virgil Dupras -# Created On: 2009-05-24 -# Copyright 2015 Hardcoded Software (http://www.hardcoded.net) +# Copyright 2016 Hardcoded Software (http://www.hardcoded.net) # # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, # which should be included with this package. The terms are also available at @@ -43,10 +41,10 @@ class DupeGuru(DupeGuruBase): def _update_options(self): DupeGuruBase._update_options(self) - self.model.scanner.min_match_percentage = self.prefs.filter_hardness - self.model.scanner.scan_type = self.prefs.scan_type - self.model.scanner.word_weighting = self.prefs.word_weighting - self.model.scanner.match_similar_words = self.prefs.match_similar + self.model.options['min_match_percentage'] = self.prefs.filter_hardness + self.model.options['scan_type'] = self.prefs.scan_type + self.model.options['word_weighting'] = self.prefs.word_weighting + self.model.options['match_similar_words'] = self.prefs.match_similar threshold = self.prefs.small_file_threshold if self.prefs.ignore_small_files else 0 - self.model.scanner.size_threshold = threshold * 1024 # threshold is in KB. the scanner wants bytes + self.model.options['size_threshold'] = threshold * 1024 # threshold is in KB. the scanner wants bytes