diff --git a/cocoa/base/ui/preferences_panel.py b/cocoa/base/ui/preferences_panel.py index df7bf585..4c4b2af9 100644 --- a/cocoa/base/ui/preferences_panel.py +++ b/cocoa/base/ui/preferences_panel.py @@ -12,7 +12,7 @@ dialogHeights = { scanTypeNames = { 'se': ["Filename", "Content", "Folders"], 'me': ["Filename", "Filename - Fields", "Filename - Fields (No Order)", "Tags", "Content", "Audio Content"], - 'pe': ["Contents", "EXIF Timestamp"], + 'pe': ["Contents", "EXIF Timestamp", "Trigger-happy mode"], } result = Window(410, dialogHeights[edition], dialogTitles[edition]) diff --git a/cocoa/inter/app_pe.py b/cocoa/inter/app_pe.py index ebaf802a..555f1cf5 100644 --- a/cocoa/inter/app_pe.py +++ b/cocoa/inter/app_pe.py @@ -331,6 +331,7 @@ class PyDupeGuru(PyDupeGuruBase): self.model.scanner.scan_type = [ ScanType.FuzzyBlock, ScanType.ExifTimestamp, + ScanType.TriggerHappyMode, ][scan_type] except IndexError: pass diff --git a/core/scanner.py b/core/scanner.py index 30f34369..4695bf60 100644 --- a/core/scanner.py +++ b/core/scanner.py @@ -33,6 +33,7 @@ class ScanType: #PE FuzzyBlock = 10 ExifTimestamp = 11 + TriggerHappyMode = 12 SCANNABLE_TAGS = ['track', 'artist', 'album', 'title', 'genre', 'year'] diff --git a/core_pe/matchexif.py b/core_pe/matchexif.py index 30b8459f..6491e534 100644 --- a/core_pe/matchexif.py +++ b/core_pe/matchexif.py @@ -10,21 +10,43 @@ from collections import defaultdict from itertools import combinations from hscommon.trans import tr +from jobprogress import job from core.engine import Match -def getmatches(files, match_scaled, j): +def group_by_timestamp(files, date_only=False, j=job.nulljob): + """Returns a mapping timestamp --> set(files). + + If ``date_only`` is ``True``, ignore the "time" part of the timestamp and consider files as + matching as soon as their date part match. + """ timestamp2pic = defaultdict(set) for picture in j.iter_with_progress(files, tr("Read EXIF of %d/%d pictures")): timestamp = picture.exif_timestamp if timestamp: + if date_only: + timestamp = timestamp[:10] timestamp2pic[timestamp].add(picture) - if '0000:00:00 00:00:00' in timestamp2pic: # very likely false matches - del timestamp2pic['0000:00:00 00:00:00'] + NULL_TS = '0000:00:00 00:00:00' + if date_only: + NULL_TS = NULL_TS[:10] + if NULL_TS in timestamp2pic: # very likely false matches + del timestamp2pic[NULL_TS] + return timestamp2pic + +def getmatches(files, match_scaled=True, date_only=False, j=job.nulljob): + """Returns a list of files with the same EXIF date. + + Reads the EXIF tag of all ``files`` and return a :class:`Match` for every pair of files having + the exact same EXIF timestamp (DateTimeOriginal). + + If ``match_scaled`` if ``False``, ignore files that don't have the same dimensions. + """ + timestamp2pic = group_by_timestamp(files, j=j) matches = [] for pictures in timestamp2pic.values(): for p1, p2 in combinations(pictures, 2): if (not match_scaled) and (p1.dimensions != p2.dimensions): continue matches.append(Match(p1, p2, 100)) - return matches \ No newline at end of file + return matches diff --git a/core_pe/scanner.py b/core_pe/scanner.py index 7aaf6b34..71c91fd6 100644 --- a/core_pe/scanner.py +++ b/core_pe/scanner.py @@ -20,7 +20,17 @@ class ScannerPE(Scanner): if self.scan_type == ScanType.FuzzyBlock: return matchblock.getmatches(files, self.cache_path, self.threshold, self.match_scaled, j) elif self.scan_type == ScanType.ExifTimestamp: - return matchexif.getmatches(files, self.match_scaled, j) + return matchexif.getmatches(files, match_scaled=self.match_scaled, j=j) + elif self.scan_type == ScanType.TriggerHappyMode: + j = j.start_subjob([1, 9]) + groups = matchexif.group_by_timestamp(files, date_only=True, j=j) + j = j.start_subjob(len(groups)) + matches = [] + for subfiles in groups.values(): + matches += matchblock.getmatches( + list(subfiles), self.cache_path, self.threshold, self.match_scaled, j + ) + return matches else: raise Exception("Invalid scan type") diff --git a/qt/pe/preferences_dialog.py b/qt/pe/preferences_dialog.py index ca15d343..2c59a114 100644 --- a/qt/pe/preferences_dialog.py +++ b/qt/pe/preferences_dialog.py @@ -20,6 +20,7 @@ tr = trget('ui') SCAN_TYPE_ORDER = [ ScanType.FuzzyBlock, ScanType.ExifTimestamp, + ScanType.TriggerHappyMode, ] class PreferencesDialog(PreferencesDialogBase): @@ -32,6 +33,7 @@ class PreferencesDialog(PreferencesDialogBase): scanTypeLabels = [ tr("Contents"), tr("EXIF Timestamp"), + tr("Trigger-happy mode"), ] self._setupScanTypeBox(scanTypeLabels) self._setupFilterHardnessBox()