diff --git a/core/app.py b/core/app.py index 204dd913..abcf163d 100644 --- a/core/app.py +++ b/core/app.py @@ -9,7 +9,6 @@ import os.path as op import logging import subprocess import re -import time import shutil from send2trash import send2trash @@ -18,14 +17,26 @@ from hscommon.notify import Broadcaster from hscommon.path import Path from hscommon.conflict import smart_move, smart_copy from hscommon.gui.progress_window import ProgressWindow -from hscommon.util import delete_if_empty, first, escape, nonone, format_time_decimal, allsame +from hscommon.util import delete_if_empty, first, escape, nonone, allsame from hscommon.trans import tr -from hscommon.plat import ISWINDOWS from hscommon import desktop -from . import directories, results, export, fs +import core_se.fs +import core_se.result_table +import core_se.scanner +import core_me.fs +import core_me.prioritize +import core_me.result_table +import core_me.scanner +import core_pe.photo +import core_pe.prioritize +import core_pe.result_table +import core_pe.scanner +from core_pe.photo import get_delta_dimensions +from .util import cmp_value, fix_surrogate_encoding +from . import directories, results, export, fs, prioritize from .ignore import IgnoreList -from .scanner import ScanType, Scanner +from .scanner import ScanType from .gui.deletion_options import DeletionOptions from .gui.details_panel import DetailsPanel from .gui.directory_tree import DirectoryTree @@ -67,53 +78,6 @@ JOBID2TITLE = { JobType.Copy: tr("Copying"), JobType.Delete: tr("Sending to Trash"), } -if ISWINDOWS: - JOBID2TITLE[JobType.Delete] = tr("Sending files to the recycle bin") - -def format_timestamp(t, delta): - if delta: - return format_time_decimal(t) - else: - if t > 0: - return time.strftime('%Y/%m/%d %H:%M:%S', time.localtime(t)) - else: - return '---' - -def format_words(w): - def do_format(w): - if isinstance(w, list): - return '(%s)' % ', '.join(do_format(item) for item in w) - else: - return w.replace('\n', ' ') - - return ', '.join(do_format(item) for item in w) - -def format_perc(p): - return "%0.0f" % p - -def format_dupe_count(c): - return str(c) if c else '---' - -def cmp_value(dupe, attrname): - value = getattr(dupe, attrname, '') - return value.lower() if isinstance(value, str) else value - -def fix_surrogate_encoding(s, encoding='utf-8'): - # ref #210. It's possible to end up with file paths that, while correct unicode strings, are - # decoded with the 'surrogateescape' option, which make the string unencodable to utf-8. We fix - # these strings here by trying to encode them and, if it fails, we do an encode/decode dance - # to remove the problematic characters. This dance is *lossy* but there's not much we can do - # because if we end up with this type of string, it means that we don't know the encoding of the - # underlying filesystem that brought them. Don't use this for strings you're going to re-use in - # fs-related functions because you're going to lose your path (it's going to change). Use this - # if you need to export the path somewhere else, outside of the unicode realm. - # See http://lucumr.pocoo.org/2013/7/2/the-updated-guide-to-unicode/ - try: - s.encode(encoding) - except UnicodeEncodeError: - return s.encode(encoding, 'replace').decode(encoding) - else: - return s class DupeGuru(Broadcaster): """Holds everything together. @@ -160,8 +124,7 @@ class DupeGuru(Broadcaster): # select_dest_folder(prompt: str) --> str # select_dest_file(prompt: str, ext: str) --> str - PROMPT_NAME = "dupeGuru" - SCANNER_CLASS = Scanner + NAME = PROMPT_NAME = "dupeGuru" def __init__(self, view): if view.get_default(DEBUG_MODE_PREFERENCE): @@ -185,6 +148,7 @@ class DupeGuru(Broadcaster): 'clean_empty_dirs': False, 'ignore_hardlink_matches': False, 'copymove_dest_type': DestType.Relative, + 'cache_path': op.join(self.appdata, 'cached_pictures.db'), } self.selected_dupes = [] self.details_panel = DetailsPanel(self) @@ -199,15 +163,25 @@ class DupeGuru(Broadcaster): for child in children: child.connect() - #--- Virtual - def _prioritization_categories(self): - raise NotImplementedError() - - def _create_result_table(self): - raise NotImplementedError() - #--- Private + def _create_result_table(self): + if self.app_mode == AppMode.Picture: + return core_pe.result_table.ResultTable(self) + elif self.app_mode == AppMode.Music: + return core_me.result_table.ResultTable(self) + else: + return core_se.result_table.ResultTable(self) + def _get_dupe_sort_key(self, dupe, get_group, key, delta): + if self.app_mode in (AppMode.Music, AppMode.Picture): + if key == 'folder_path': + dupe_folder_path = getattr(dupe, 'display_folder_path', dupe.folder_path) + return str(dupe_folder_path).lower() + if self.app_mode == AppMode.Picture: + if delta and key == 'dimensions': + r = cmp_value(dupe, key) + ref_value = cmp_value(get_group().ref, key) + return get_delta_dimensions(r, ref_value) if key == 'marked': return self.results.is_marked(dupe) if key == 'percentage': @@ -227,6 +201,10 @@ class DupeGuru(Broadcaster): return result def _get_group_sort_key(self, group, key): + if self.app_mode in (AppMode.Music, AppMode.Picture): + if key == 'folder_path': + dupe_folder_path = getattr(group.ref, 'display_folder_path', group.ref.folder_path) + return str(dupe_folder_path).lower() if key == 'percentage': return group.percentage if key == 'dupe_count': @@ -354,6 +332,15 @@ class DupeGuru(Broadcaster): self.selected_dupes = dupes self.notify('dupes_selected') + #--- Protected + def _prioritization_categories(self): + if self.app_mode == AppMode.Picture: + return core_pe.prioritize.all_categories() + elif self.app_mode == AppMode.Music: + return core_me.prioritize.all_categories() + else: + return prioritize.all_categories() + #--- Public def add_directory(self, d): """Adds folder ``d`` to :attr:`directories`. @@ -767,7 +754,7 @@ class DupeGuru(Broadcaster): def do(j): j.set_progress(0, tr("Collecting files to scan")) if scanner.scan_type == ScanType.Folders: - files = list(self.directories.get_folders(folderclass=self.folderclass, j=j)) + files = list(self.directories.get_folders(folderclass=core_se.fs.folder, j=j)) else: files = list(self.directories.get_files(fileclasses=self.fileclasses, j=j)) if self.options['ignore_hardlink_matches']: @@ -816,3 +803,33 @@ class DupeGuru(Broadcaster): result = tr("%s (%d discarded)") % (result, self.discarded_file_count) return result + @property + def fileclasses(self): + if self.app_mode == AppMode.Picture: + return [core_pe.photo.PLAT_SPECIFIC_PHOTO_CLASS] + elif self.app_mode == AppMode.Music: + return [core_me.fs.MusicFile] + else: + return [core_se.fs.File] + + @property + def SCANNER_CLASS(self): + if self.app_mode == AppMode.Picture: + return core_pe.scanner.ScannerPE + elif self.app_mode == AppMode.Music: + return core_me.scanner.ScannerME + else: + return core_se.scanner.ScannerSE + + @property + def METADATA_TO_READ(self): + if self.app_mode == AppMode.Picture: + return ['size', 'mtime', 'dimensions', 'exif_timestamp'] + elif self.app_mode == AppMode.Music: + return [ + 'size', 'mtime', 'duration', 'bitrate', 'samplerate', 'title', 'artist', + 'album', 'genre', 'year', 'track', 'comment' + ] + else: + return ['size', 'mtime'] + diff --git a/core/util.py b/core/util.py new file mode 100644 index 00000000..036e46f6 --- /dev/null +++ b/core/util.py @@ -0,0 +1,56 @@ +# 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 time + +from hscommon.util import format_time_decimal + +def format_timestamp(t, delta): + if delta: + return format_time_decimal(t) + else: + if t > 0: + return time.strftime('%Y/%m/%d %H:%M:%S', time.localtime(t)) + else: + return '---' + +def format_words(w): + def do_format(w): + if isinstance(w, list): + return '(%s)' % ', '.join(do_format(item) for item in w) + else: + return w.replace('\n', ' ') + + return ', '.join(do_format(item) for item in w) + +def format_perc(p): + return "%0.0f" % p + +def format_dupe_count(c): + return str(c) if c else '---' + +def cmp_value(dupe, attrname): + value = getattr(dupe, attrname, '') + return value.lower() if isinstance(value, str) else value + +def fix_surrogate_encoding(s, encoding='utf-8'): + # ref #210. It's possible to end up with file paths that, while correct unicode strings, are + # decoded with the 'surrogateescape' option, which make the string unencodable to utf-8. We fix + # these strings here by trying to encode them and, if it fails, we do an encode/decode dance + # to remove the problematic characters. This dance is *lossy* but there's not much we can do + # because if we end up with this type of string, it means that we don't know the encoding of the + # underlying filesystem that brought them. Don't use this for strings you're going to re-use in + # fs-related functions because you're going to lose your path (it's going to change). Use this + # if you need to export the path somewhere else, outside of the unicode realm. + # See http://lucumr.pocoo.org/2013/7/2/the-updated-guide-to-unicode/ + try: + s.encode(encoding) + except UnicodeEncodeError: + return s.encode(encoding, 'replace').decode(encoding) + else: + return s + + diff --git a/core_me/fs.py b/core_me/fs.py index 1be57252..eb060128 100644 --- a/core_me/fs.py +++ b/core_me/fs.py @@ -9,7 +9,7 @@ from hsaudiotag import auto from hscommon.util import get_file_ext, format_size, format_time -from core.app import format_timestamp, format_perc, format_words, format_dupe_count +from core.util import format_timestamp, format_perc, format_words, format_dupe_count from core import fs TAG_FIELDS = { diff --git a/core_pe/photo.py b/core_pe/photo.py index 5f76dadf..fdaea94e 100644 --- a/core_pe/photo.py +++ b/core_pe/photo.py @@ -7,7 +7,7 @@ import logging from hscommon.util import get_file_ext, format_size -from core.app import format_timestamp, format_perc, format_dupe_count +from core.util import format_timestamp, format_perc, format_dupe_count from core import fs from . import exif diff --git a/core_se/app.py b/core_se/app.py deleted file mode 100644 index a72377bf..00000000 --- a/core_se/app.py +++ /dev/null @@ -1,95 +0,0 @@ -# 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 os.path as op - -from core.app import DupeGuru as DupeGuruBase, AppMode, cmp_value -from core import prioritize -import core_me.fs -import core_me.prioritize -import core_me.result_table -import core_me.scanner -import core_pe.photo -import core_pe.prioritize -import core_pe.result_table -import core_pe.scanner -from core_pe.photo import get_delta_dimensions -from . import __appname__, fs, scanner -from .result_table import ResultTable - -class DupeGuru(DupeGuruBase): - NAME = __appname__ - - def __init__(self, view): - DupeGuruBase.__init__(self, view) - self.folderclass = fs.Folder - self.options['cache_path'] = op.join(self.appdata, 'cached_pictures.db') - - def _get_dupe_sort_key(self, dupe, get_group, key, delta): - if self.app_mode in (AppMode.Music, AppMode.Picture): - if key == 'folder_path': - dupe_folder_path = getattr(dupe, 'display_folder_path', dupe.folder_path) - return str(dupe_folder_path).lower() - if self.app_mode == AppMode.Picture: - if delta and key == 'dimensions': - r = cmp_value(dupe, key) - ref_value = cmp_value(get_group().ref, key) - return get_delta_dimensions(r, ref_value) - return DupeGuruBase._get_dupe_sort_key(self, dupe, get_group, key, delta) - - def _get_group_sort_key(self, group, key): - if self.app_mode in (AppMode.Music, AppMode.Picture): - if key == 'folder_path': - dupe_folder_path = getattr(group.ref, 'display_folder_path', group.ref.folder_path) - return str(dupe_folder_path).lower() - return DupeGuruBase._get_group_sort_key(self, group, key) - - def _prioritization_categories(self): - if self.app_mode == AppMode.Picture: - return core_pe.prioritize.all_categories() - elif self.app_mode == AppMode.Music: - return core_me.prioritize.all_categories() - else: - return prioritize.all_categories() - - def _create_result_table(self): - if self.app_mode == AppMode.Picture: - return core_pe.result_table.ResultTable(self) - elif self.app_mode == AppMode.Music: - return core_me.result_table.ResultTable(self) - else: - return ResultTable(self) - - @property - def fileclasses(self): - if self.app_mode == AppMode.Picture: - return [core_pe.photo.PLAT_SPECIFIC_PHOTO_CLASS] - elif self.app_mode == AppMode.Music: - return [core_me.fs.MusicFile] - else: - return [fs.File] - - @property - def SCANNER_CLASS(self): - if self.app_mode == AppMode.Picture: - return core_pe.scanner.ScannerPE - elif self.app_mode == AppMode.Music: - return core_me.scanner.ScannerME - else: - return scanner.ScannerSE - - @property - def METADATA_TO_READ(self): - if self.app_mode == AppMode.Picture: - return ['size', 'mtime', 'dimensions', 'exif_timestamp'] - elif self.app_mode == AppMode.Music: - return [ - 'size', 'mtime', 'duration', 'bitrate', 'samplerate', 'title', 'artist', - 'album', 'genre', 'year', 'track', 'comment' - ] - else: - return ['size', 'mtime'] - diff --git a/core_se/fs.py b/core_se/fs.py index 6b15cdfa..8e691251 100644 --- a/core_se/fs.py +++ b/core_se/fs.py @@ -1,15 +1,15 @@ # Created By: Virgil Dupras # Created On: 2013-07-14 # 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 +# +# 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 hscommon.util import format_size from core import fs -from core.app import format_timestamp, format_perc, format_words, format_dupe_count +from core.util import format_timestamp, format_perc, format_words, format_dupe_count def get_display_info(dupe, group, delta): size = dupe.size @@ -39,9 +39,9 @@ def get_display_info(dupe, group, delta): class File(fs.File): def get_display_info(self, group, delta): return get_display_info(self, group, delta) - + class Folder(fs.Folder): def get_display_info(self, group, delta): return get_display_info(self, group, delta) - + diff --git a/qt/base/app.py b/qt/base/app.py index bc6bd73d..514a622b 100644 --- a/qt/base/app.py +++ b/qt/base/app.py @@ -19,8 +19,7 @@ from qtlib.recent import Recent from qtlib.util import createActions from qtlib.progress_window import ProgressWindow -from core.app import AppMode -from core_se.app import DupeGuru as DupeGuruModel +from core.app import AppMode, DupeGuru as DupeGuruModel import core_pe.photo from . import platform from .preferences import Preferences