1
0
mirror of https://github.com/arsenetar/dupeguru.git synced 2026-03-12 03:31:37 +00:00

Compare commits

..

16 Commits

Author SHA1 Message Date
091cae0cc6 feat: Add confirmation dialog when canceling job
- Implement a confirmation dialog for cancellation of jobs, required
  changing from QProgressDialog to QDialog to keep cleaner.
- Update ui translation source file

Close #1033, #515
2023-01-06 00:06:55 -06:00
e30a135451 feat: Add additional scan time options
- Add option to include file existence check at end of scan, speeds up
  end of scan operation time considerably, however if user has removed
  or moved files since starting a scan there could be later errors when
  interacting with results.  Defaults to existing behavior of including
  the check, until it can be verified later dialogs and actions handle
  non-existent items better.
- Add option to ignore differences in mtime when checking hash cache.
  Option is present in advanced tab of preferences.  Closes #1022.
- Regenerate pot files for translations
2023-01-05 23:01:16 -06:00
1db93fd142 Merge pull request #1069 from eugenesan/master
Add webp image format support
2022-12-06 05:50:36 -06:00
48862b6414 Merge pull request #1036 from dktrkranz/desktopfile
Add Keywords tag to desktop file
2022-12-06 05:48:50 -06:00
Eugene San (eugenesan)
c920412856 Add webp image format support 2022-11-24 13:53:27 -07:00
4448b999ab fix: Add W503 to flake8 extend-ignore
For some reason flake8 is now throwing W503, which should be disabled by
default, adding to extend-ignore fixes it, so doing that for now.
2022-09-28 07:05:46 -05:00
af1ae33598 Merge pull request #1042 from fascox/patch-1
Update core.po for `it`
2022-09-28 06:52:52 -05:00
265d10b261 Merge pull request #1026 from muath-ye/patch-1
Update columns.po for `ar`
2022-09-28 06:46:50 -05:00
Fabio Scognamiglio
1eee3fd7e4 Update core.po
fix mispelled translation
2022-09-10 13:29:04 +02:00
Luca Falavigna
1827827fdf Add Keywords tag to desktop file 2022-08-31 14:57:16 +00:00
Muath Alsowadi
db174d4e63 Update columns.po 2022-08-07 09:32:33 +03:00
1f1dfa88dc Update version & changelog for 4.3.1 release 2022-07-07 22:06:06 -05:00
916c5204cf Update translations from transifex 2022-07-07 21:57:59 -05:00
71af825b37 Move try/except of cache db to get() and put()
- Move the try/except of cache db calls to the calls themselves.
- Add some additional information to logging statements on cache db
  exception to improve troubleshooting.
2022-07-07 21:52:22 -05:00
97f490b8b7 Fix typo in engine.py 2022-07-07 19:06:35 -05:00
d369bcddd7 Updates from investigation of #1015
- Add protection for empty hash digests in comparison of non-zero size
  files
- Bump version to 4.3.1-dev for identification
2022-07-07 19:00:09 -05:00
18 changed files with 211 additions and 112 deletions

View File

@@ -1,2 +1,2 @@
__version__ = "4.3.0" __version__ = "4.3.1"
__appname__ = "dupeGuru" __appname__ = "dupeGuru"

View File

@@ -154,6 +154,8 @@ class DupeGuru(Broadcaster):
"ignore_hardlink_matches": False, "ignore_hardlink_matches": False,
"copymove_dest_type": DestType.RELATIVE, "copymove_dest_type": DestType.RELATIVE,
"picture_cache_type": self.PICTURE_CACHE_TYPE, "picture_cache_type": self.PICTURE_CACHE_TYPE,
"include_exists_check": True,
"rehash_ignore_mtime": False,
} }
self.selected_dupes = [] self.selected_dupes = []
self.details_panel = DetailsPanel(self) self.details_panel = DetailsPanel(self)
@@ -555,7 +557,9 @@ class DupeGuru(Broadcaster):
# a workaround to make the damn thing work. # a workaround to make the damn thing work.
exepath, args = match.groups() exepath, args = match.groups()
path, exename = op.split(exepath) path, exename = op.split(exepath)
p = subprocess.Popen(exename + args, shell=True, cwd=path, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) p = subprocess.Popen(
exename + args, shell=True, cwd=path, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
)
output = p.stdout.read() output = p.stdout.read()
logging.info("Custom command %s %s: %s", exename, args, output) logging.info("Custom command %s %s: %s", exename, args, output)
else: else:
@@ -792,6 +796,7 @@ class DupeGuru(Broadcaster):
Scans folders selected in :attr:`directories` and put the results in :attr:`results` Scans folders selected in :attr:`directories` and put the results in :attr:`results`
""" """
scanner = self.SCANNER_CLASS() scanner = self.SCANNER_CLASS()
fs.filesdb.ignore_mtime = self.options["rehash_ignore_mtime"] is True
if not self.directories.has_any_file(): if not self.directories.has_any_file():
self.view.show_message(tr("The selected directories contain no scannable file.")) self.view.show_message(tr("The selected directories contain no scannable file."))
return return

View File

@@ -303,12 +303,13 @@ def getmatches_by_contents(files, bigsize=0, j=job.nulljob):
# skip hashing for zero length files # skip hashing for zero length files
result.append(Match(first, second, 100)) result.append(Match(first, second, 100))
continue continue
if first.digest_partial == second.digest_partial: # if digests are the same (and not None) then files match
if first.digest_partial == second.digest_partial and first.digest_partial is not None:
if bigsize > 0 and first.size > bigsize: if bigsize > 0 and first.size > bigsize:
if first.digest_samples == second.digest_samples: if first.digest_samples == second.digest_samples and first.digest_samples is not None:
result.append(Match(first, second, 100)) result.append(Match(first, second, 100))
else: else:
if first.digest == second.digest: if first.digest == second.digest and first.digest is not None:
result.append(Match(first, second, 100)) result.append(Match(first, second, 100))
group_count += 1 group_count += 1
j.add_progress(desc=PROGRESS_MESSAGE % (len(result), group_count)) j.add_progress(desc=PROGRESS_MESSAGE % (len(result), group_count))

View File

@@ -100,11 +100,14 @@ class FilesDB:
create_table_query = "CREATE TABLE IF NOT EXISTS files (path TEXT PRIMARY KEY, size INTEGER, mtime_ns INTEGER, entry_dt DATETIME, digest BLOB, digest_partial BLOB, digest_samples BLOB)" create_table_query = "CREATE TABLE IF NOT EXISTS files (path TEXT PRIMARY KEY, size INTEGER, mtime_ns INTEGER, entry_dt DATETIME, digest BLOB, digest_partial BLOB, digest_samples BLOB)"
drop_table_query = "DROP TABLE IF EXISTS files;" drop_table_query = "DROP TABLE IF EXISTS files;"
select_query = "SELECT {key} FROM files WHERE path=:path AND size=:size and mtime_ns=:mtime_ns" select_query = "SELECT {key} FROM files WHERE path=:path AND size=:size and mtime_ns=:mtime_ns"
select_query_ignore_mtime = "SELECT {key} FROM files WHERE path=:path AND size=:size"
insert_query = """ insert_query = """
INSERT INTO files (path, size, mtime_ns, entry_dt, {key}) VALUES (:path, :size, :mtime_ns, datetime('now'), :value) INSERT INTO files (path, size, mtime_ns, entry_dt, {key}) VALUES (:path, :size, :mtime_ns, datetime('now'), :value)
ON CONFLICT(path) DO UPDATE SET size=:size, mtime_ns=:mtime_ns, entry_dt=datetime('now'), {key}=:value; ON CONFLICT(path) DO UPDATE SET size=:size, mtime_ns=:mtime_ns, entry_dt=datetime('now'), {key}=:value;
""" """
ignore_mtime = False
def __init__(self): def __init__(self):
self.conn = None self.conn = None
self.cur = None self.cur = None
@@ -144,13 +147,20 @@ class FilesDB:
stat = path.stat() stat = path.stat()
size = stat.st_size size = stat.st_size
mtime_ns = stat.st_mtime_ns mtime_ns = stat.st_mtime_ns
try:
with self.lock: with self.lock:
self.cur.execute(self.select_query.format(key=key), {"path": str(path), "size": size, "mtime_ns": mtime_ns}) if self.ignore_mtime:
self.cur.execute(self.select_query_ignore_mtime.format(key=key), {"path": str(path), "size": size})
else:
self.cur.execute(
self.select_query.format(key=key), {"path": str(path), "size": size, "mtime_ns": mtime_ns}
)
result = self.cur.fetchone() result = self.cur.fetchone()
if result: if result:
return result[0] return result[0]
except Exception as ex:
logging.warning(f"Couldn't get {key} for {path} w/{size}, {mtime_ns}: {ex}")
return None return None
@@ -158,12 +168,14 @@ class FilesDB:
stat = path.stat() stat = path.stat()
size = stat.st_size size = stat.st_size
mtime_ns = stat.st_mtime_ns mtime_ns = stat.st_mtime_ns
try:
with self.lock: with self.lock:
self.cur.execute( self.cur.execute(
self.insert_query.format(key=key), self.insert_query.format(key=key),
{"path": str(path), "size": size, "mtime_ns": mtime_ns, "value": value}, {"path": str(path), "size": size, "mtime_ns": mtime_ns, "value": value},
) )
except Exception as ex:
logging.warning(f"Couldn't put {key} for {path} w/{size}, {mtime_ns}: {ex}")
def commit(self) -> None: def commit(self) -> None:
with self.lock: with self.lock:
@@ -265,34 +277,25 @@ class File:
self.size = nonone(stats.st_size, 0) self.size = nonone(stats.st_size, 0)
self.mtime = nonone(stats.st_mtime, 0) self.mtime = nonone(stats.st_mtime, 0)
elif field == "digest_partial": elif field == "digest_partial":
try:
self.digest_partial = filesdb.get(self.path, "digest_partial") self.digest_partial = filesdb.get(self.path, "digest_partial")
if self.digest_partial is None: if self.digest_partial is None:
self.digest_partial = self._calc_digest_partial() self.digest_partial = self._calc_digest_partial()
filesdb.put(self.path, "digest_partial", self.digest_partial) filesdb.put(self.path, "digest_partial", self.digest_partial)
except Exception as e:
logging.warning("Couldn't get digest_partial for %s: %s", self.path, e)
elif field == "digest": elif field == "digest":
try:
self.digest = filesdb.get(self.path, "digest") self.digest = filesdb.get(self.path, "digest")
if self.digest is None: if self.digest is None:
self.digest = self._calc_digest() self.digest = self._calc_digest()
filesdb.put(self.path, "digest", self.digest) filesdb.put(self.path, "digest", self.digest)
except Exception as e:
logging.warning("Couldn't get digest for %s: %s", self.path, e)
elif field == "digest_samples": elif field == "digest_samples":
size = self.size size = self.size
# Might as well hash such small files entirely. # Might as well hash such small files entirely.
if size <= MIN_FILE_SIZE: if size <= MIN_FILE_SIZE:
setattr(self, field, self.digest) setattr(self, field, self.digest)
return return
try:
self.digest_samples = filesdb.get(self.path, "digest_samples") self.digest_samples = filesdb.get(self.path, "digest_samples")
if self.digest_samples is None: if self.digest_samples is None:
self.digest_samples = self._calc_digest_samples() self.digest_samples = self._calc_digest_samples()
filesdb.put(self.path, "digest_samples", self.digest_samples) filesdb.put(self.path, "digest_samples", self.digest_samples)
except Exception as e:
logging.warning(f"Couldn't get digest_samples for {self.path}: {e}")
def _read_all_info(self, attrnames=None): def _read_all_info(self, attrnames=None):
"""Cache all possible info. """Cache all possible info.

View File

@@ -29,7 +29,7 @@ class Photo(fs.File):
__slots__ = fs.File.__slots__ + tuple(INITIAL_INFO.keys()) __slots__ = fs.File.__slots__ + tuple(INITIAL_INFO.keys())
# These extensions are supported on all platforms # These extensions are supported on all platforms
HANDLED_EXTS = {"png", "jpg", "jpeg", "gif", "bmp", "tiff", "tif"} HANDLED_EXTS = {"png", "jpg", "jpeg", "gif", "bmp", "tiff", "tif", "webp"}
def _plat_get_dimensions(self): def _plat_get_dimensions(self):
raise NotImplementedError() raise NotImplementedError()

View File

@@ -171,6 +171,7 @@ class Scanner:
matches = [m for m in matches if m.first.path not in toremove or m.second.path not in toremove] matches = [m for m in matches if m.first.path not in toremove or m.second.path not in toremove]
if not self.mix_file_kind: if not self.mix_file_kind:
matches = [m for m in matches if get_file_ext(m.first.name) == get_file_ext(m.second.name)] matches = [m for m in matches if get_file_ext(m.first.name) == get_file_ext(m.second.name)]
if self.include_exists_check:
matches = [m for m in matches if m.first.path.exists() and m.second.path.exists()] matches = [m for m in matches if m.first.path.exists() and m.second.path.exists()]
matches = [m for m in matches if not (m.first.is_ref and m.second.is_ref)] matches = [m for m in matches if not (m.first.is_ref and m.second.is_ref)]
if ignore_list: if ignore_list:
@@ -212,3 +213,4 @@ class Scanner:
large_size_threshold = 0 large_size_threshold = 0
big_file_size_threshold = 0 big_file_size_threshold = 0
word_weighting = False word_weighting = False
include_exists_check = True

View File

@@ -1,3 +1,8 @@
=== 4.3.1 (2022-07-08)
* Fix issue where cache db exceptions could prevent files being hashed (#1015)
* Add extra guard for non-zero length files without digests to prevent false duplicates
* Update Italian translations
=== 4.3.0 (2022-07-01) === 4.3.0 (2022-07-01)
* Redirect stdout from custom command to the log files (#1008) * Redirect stdout from custom command to the log files (#1008)
* Update translations * Update translations

View File

@@ -10,110 +10,110 @@ msgstr ""
#: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20
#: core\gui\problem_table.py:18 #: core\gui\problem_table.py:18
msgid "File Path" msgid "File Path"
msgstr "" msgstr "مسار الملف"
#: core\gui\problem_table.py:19 #: core\gui\problem_table.py:19
msgid "Error Message" msgid "Error Message"
msgstr "" msgstr "رسالة خطأ"
#: core\me\prioritize.py:23 #: core\me\prioritize.py:23
msgid "Duration" msgid "Duration"
msgstr "" msgstr "مدة"
#: core\me\prioritize.py:30 core\me\result_table.py:23 #: core\me\prioritize.py:30 core\me\result_table.py:23
msgid "Bitrate" msgid "Bitrate"
msgstr "" msgstr "معدل البت"
#: core\me\prioritize.py:37 #: core\me\prioritize.py:37
msgid "Samplerate" msgid "Samplerate"
msgstr "" msgstr "معدل العينة"
#: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92 #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92
#: core\se\result_table.py:19 #: core\se\result_table.py:19
msgid "Filename" msgid "Filename"
msgstr "" msgstr "اسم الملف"
#: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75
#: core\se\result_table.py:20 #: core\se\result_table.py:20
msgid "Folder" msgid "Folder"
msgstr "" msgstr "مجلد"
#: core\me\result_table.py:21 #: core\me\result_table.py:21
msgid "Size (MB)" msgid "Size (MB)"
msgstr "" msgstr "الحجم (ميغا بايت)"
#: core\me\result_table.py:22 #: core\me\result_table.py:22
msgid "Time" msgid "Time"
msgstr "" msgstr "زمن"
#: core\me\result_table.py:24 #: core\me\result_table.py:24
msgid "Sample Rate" msgid "Sample Rate"
msgstr "" msgstr "معدل العينة"
#: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65
#: core\se\result_table.py:22 #: core\se\result_table.py:22
msgid "Kind" msgid "Kind"
msgstr "" msgstr "طيب القلب"
#: core\me\result_table.py:26 core\pe\result_table.py:25 #: core\me\result_table.py:26 core\pe\result_table.py:25
#: core\prioritize.py:163 core\se\result_table.py:23 #: core\prioritize.py:163 core\se\result_table.py:23
msgid "Modification" msgid "Modification"
msgstr "" msgstr "تعديل"
#: core\me\result_table.py:27 #: core\me\result_table.py:27
msgid "Title" msgid "Title"
msgstr "" msgstr "عنوان"
#: core\me\result_table.py:28 #: core\me\result_table.py:28
msgid "Artist" msgid "Artist"
msgstr "" msgstr "فنان"
#: core\me\result_table.py:29 #: core\me\result_table.py:29
msgid "Album" msgid "Album"
msgstr "" msgstr "البوم"
#: core\me\result_table.py:30 #: core\me\result_table.py:30
msgid "Genre" msgid "Genre"
msgstr "" msgstr "النوع"
#: core\me\result_table.py:31 #: core\me\result_table.py:31
msgid "Year" msgid "Year"
msgstr "" msgstr "سنة"
#: core\me\result_table.py:32 #: core\me\result_table.py:32
msgid "Track Number" msgid "Track Number"
msgstr "" msgstr "رقم الشاحنة"
#: core\me\result_table.py:33 #: core\me\result_table.py:33
msgid "Comment" msgid "Comment"
msgstr "" msgstr "تعليق"
#: core\me\result_table.py:34 core\pe\result_table.py:26 #: core\me\result_table.py:34 core\pe\result_table.py:26
#: core\se\result_table.py:24 #: core\se\result_table.py:24
msgid "Match %" msgid "Match %"
msgstr "" msgstr "مباراة ٪"
#: core\me\result_table.py:35 core\se\result_table.py:25 #: core\me\result_table.py:35 core\se\result_table.py:25
msgid "Words Used" msgid "Words Used"
msgstr "" msgstr "الكلمات المستخدمة"
#: core\me\result_table.py:36 core\pe\result_table.py:27 #: core\me\result_table.py:36 core\pe\result_table.py:27
#: core\se\result_table.py:26 #: core\se\result_table.py:26
msgid "Dupe Count" msgid "Dupe Count"
msgstr "" msgstr "عدد المخادعين"
#: core\pe\prioritize.py:23 core\pe\result_table.py:23 #: core\pe\prioritize.py:23 core\pe\result_table.py:23
msgid "Dimensions" msgid "Dimensions"
msgstr "" msgstr "أبعاد"
#: core\pe\result_table.py:21 core\se\result_table.py:21 #: core\pe\result_table.py:21 core\se\result_table.py:21
msgid "Size (KB)" msgid "Size (KB)"
msgstr "" msgstr "الحجم (كيلو بايت)"
#: core\pe\result_table.py:24 #: core\pe\result_table.py:24
msgid "EXIF Timestamp" msgid "EXIF Timestamp"
msgstr "" msgstr "الطابع الزمني EXIF"
#: core\prioritize.py:156 #: core\prioritize.py:156
msgid "Size" msgid "Size"
msgstr "" msgstr "بحجم"

View File

@@ -36,83 +36,83 @@ msgstr ""
msgid "Sending to Trash" msgid "Sending to Trash"
msgstr "" msgstr ""
#: core\app.py:291 #: core\app.py:293
msgid "A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again." msgid "A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again."
msgstr "" msgstr ""
#: core\app.py:302 #: core\app.py:304
msgid "No duplicates found." msgid "No duplicates found."
msgstr "" msgstr ""
#: core\app.py:317 #: core\app.py:319
msgid "All marked files were copied successfully." msgid "All marked files were copied successfully."
msgstr "" msgstr ""
#: core\app.py:319 #: core\app.py:321
msgid "All marked files were moved successfully." msgid "All marked files were moved successfully."
msgstr "" msgstr ""
#: core\app.py:321 #: core\app.py:323
msgid "All marked files were deleted successfully." msgid "All marked files were deleted successfully."
msgstr "" msgstr ""
#: core\app.py:323 #: core\app.py:325
msgid "All marked files were successfully sent to Trash." msgid "All marked files were successfully sent to Trash."
msgstr "" msgstr ""
#: core\app.py:328 #: core\app.py:330
msgid "Could not load file: {}" msgid "Could not load file: {}"
msgstr "" msgstr ""
#: core\app.py:384 #: core\app.py:386
msgid "'{}' already is in the list." msgid "'{}' already is in the list."
msgstr "" msgstr ""
#: core\app.py:386 #: core\app.py:388
msgid "'{}' does not exist." msgid "'{}' does not exist."
msgstr "" msgstr ""
#: core\app.py:394 #: core\app.py:396
msgid "All selected %d matches are going to be ignored in all subsequent scans. Continue?" msgid "All selected %d matches are going to be ignored in all subsequent scans. Continue?"
msgstr "" msgstr ""
#: core\app.py:471 #: core\app.py:473
msgid "Select a directory to copy marked files to" msgid "Select a directory to copy marked files to"
msgstr "" msgstr ""
#: core\app.py:473 #: core\app.py:475
msgid "Select a directory to move marked files to" msgid "Select a directory to move marked files to"
msgstr "" msgstr ""
#: core\app.py:512 #: core\app.py:514
msgid "Select a destination for your exported CSV" msgid "Select a destination for your exported CSV"
msgstr "" msgstr ""
#: core\app.py:518 core\app.py:773 core\app.py:783 #: core\app.py:520 core\app.py:781 core\app.py:791
msgid "Couldn't write to file: {}" msgid "Couldn't write to file: {}"
msgstr "" msgstr ""
#: core\app.py:541 #: core\app.py:543
msgid "You have no custom command set up. Set it up in your preferences." msgid "You have no custom command set up. Set it up in your preferences."
msgstr "" msgstr ""
#: core\app.py:697 core\app.py:709 #: core\app.py:705 core\app.py:717
msgid "You are about to remove %d files from results. Continue?" msgid "You are about to remove %d files from results. Continue?"
msgstr "" msgstr ""
#: core\app.py:745 #: core\app.py:753
msgid "{} duplicate groups were changed by the re-prioritization." msgid "{} duplicate groups were changed by the re-prioritization."
msgstr "" msgstr ""
#: core\app.py:792 #: core\app.py:801
msgid "The selected directories contain no scannable file." msgid "The selected directories contain no scannable file."
msgstr "" msgstr ""
#: core\app.py:808 #: core\app.py:817
msgid "Collecting files to scan" msgid "Collecting files to scan"
msgstr "" msgstr ""
#: core\app.py:858 #: core\app.py:867
msgid "%s (%d discarded)" msgid "%s (%d discarded)"
msgstr "" msgstr ""

View File

@@ -150,7 +150,7 @@ msgstr "Raccolte {} cartelle da scansionare"
#: core\engine.py:27 #: core\engine.py:27
msgid "%d matches found from %d groups" msgid "%d matches found from %d groups"
msgstr "%d corrispondeze trovate da %d gruppi" msgstr "%d corrispondenze trovate da %d gruppi"
#: core\gui\deletion_options.py:71 #: core\gui\deletion_options.py:71
msgid "You are sending {} file(s) to the Trash." msgid "You are sending {} file(s) to the Trash."

View File

@@ -2,15 +2,16 @@
# Andrew Senetar <arsenetar@gmail.com>, 2022 # Andrew Senetar <arsenetar@gmail.com>, 2022
# Emanuele, 2022 # Emanuele, 2022
# Fuan <jcfrt@posteo.net>, 2022 # Fuan <jcfrt@posteo.net>, 2022
# Giovanni, 2022
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Last-Translator: Fuan <jcfrt@posteo.net>, 2022\n" "Last-Translator: Giovanni, 2022\n"
"Language-Team: Italian (https://www.transifex.com/voltaicideas/teams/116153/it/)\n" "Language-Team: Italian (https://www.transifex.com/voltaicideas/teams/116153/it/)\n"
"Language: it\n" "Language: it\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: utf-8\n" "Content-Transfer-Encoding: utf-8\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
#: qt/app.py:81 #: qt/app.py:81
msgid "Quit" msgid "Quit"
@@ -979,37 +980,40 @@ msgstr "Ignora file più grandi di"
#: qt\app.py:135 qt\app.py:293 #: qt\app.py:135 qt\app.py:293
msgid "Clear Cache" msgid "Clear Cache"
msgstr "" msgstr "Svuota cache"
#: qt\app.py:294 #: qt\app.py:294
msgid "" msgid ""
"Do you really want to clear the cache? This will remove all cached file " "Do you really want to clear the cache? This will remove all cached file "
"hashes and picture analysis." "hashes and picture analysis."
msgstr "" msgstr ""
"Vuoi davvero svuotare la cache? Ciò rimuoverà tutti gli hash dei file "
"memorizzati nella cache e le analisi delle immagini."
#: qt\app.py:299 #: qt\app.py:299
msgid "Cache cleared." msgid "Cache cleared."
msgstr "" msgstr "Cache svuotata"
#: qt\preferences_dialog.py:173 #: qt\preferences_dialog.py:173
msgid "Use dark style" msgid "Use dark style"
msgstr "" msgstr "Usa stile scuro"
#: qt\preferences_dialog.py:241 #: qt\preferences_dialog.py:241
msgid "Profile scan operation" msgid "Profile scan operation"
msgstr "" msgstr "Profila l'operazione di scansione"
#: qt\preferences_dialog.py:242 #: qt\preferences_dialog.py:242
msgid "Profile the scan operation and save logs for optimization." msgid "Profile the scan operation and save logs for optimization."
msgstr "" msgstr ""
"Profila l'operazione di scansione e salva i registri per l'ottimizzazione."
#: qt\preferences_dialog.py:246 #: qt\preferences_dialog.py:246
msgid "Logs located in: <a href=\"{}\">{}</a>" msgid "Logs located in: <a href=\"{}\">{}</a>"
msgstr "" msgstr "I log si trovano in: <a href=\"{}\">{}</a>"
#: qt\preferences_dialog.py:291 #: qt\preferences_dialog.py:291
msgid "Debug" msgid "Debug"
msgstr "" msgstr "Debug"
#: qt\about_box.py:31 #: qt\about_box.py:31
msgid "About {}" msgid "About {}"
@@ -1021,7 +1025,7 @@ msgstr "Versione {}"
#: qt\about_box.py:49 qt\about_box.py:75 #: qt\about_box.py:49 qt\about_box.py:75
msgid "Checking for updates..." msgid "Checking for updates..."
msgstr "" msgstr "Controllo degli aggiornamenti..."
#: qt\about_box.py:54 #: qt\about_box.py:54
msgid "Licensed under GPLv3" msgid "Licensed under GPLv3"
@@ -1029,11 +1033,11 @@ msgstr "Distribuito sotto licenza GPLv3"
#: qt\about_box.py:68 #: qt\about_box.py:68
msgid "No update available." msgid "No update available."
msgstr "" msgstr "Nessun aggiornamento disponibile."
#: qt\about_box.py:71 #: qt\about_box.py:71
msgid "New version {} available, download <a href=\"{}\">here</a>." msgid "New version {} available, download <a href=\"{}\">here</a>."
msgstr "" msgstr "È disponibile la nuova versione {}, scaricabile <a href=\"{}\">qui</a>."
#: qt\error_report_dialog.py:50 #: qt\error_report_dialog.py:50
msgid "Error Report" msgid "Error Report"

View File

@@ -1092,3 +1092,25 @@ msgstr ""
#: qt\search_edit.py:78 #: qt\search_edit.py:78
msgid "Search..." msgid "Search..."
msgstr "" msgstr ""
#: qt\preferences_dialog.py:219
msgid ""
"These options are for advanced users or for very specific situations, most "
"users should not have to modify these."
msgstr ""
#: qt\preferences_dialog.py:225
msgid "Include existence check after scan completion"
msgstr ""
#: qt\preferences_dialog.py:227
msgid "Ignore difference in mtime when loading cached digests"
msgstr ""
#: qt\progress_window.py:64
msgid "Cancel?"
msgstr ""
#: qt\progress_window.py:65
msgid "Are you sure you want to cancel? All progress will be lost."
msgstr ""

View File

@@ -6,3 +6,4 @@ Icon=dupeguru
Terminal=false Terminal=false
Type=Application Type=Application
Categories=Utility; Categories=Utility;
Keywords=file manager;gui;

View File

@@ -193,6 +193,8 @@ class DupeGuru(QObject):
self.model.options["scanned_tags"] = scanned_tags self.model.options["scanned_tags"] = scanned_tags
self.model.options["match_scaled"] = self.prefs.match_scaled self.model.options["match_scaled"] = self.prefs.match_scaled
self.model.options["picture_cache_type"] = self.prefs.picture_cache_type self.model.options["picture_cache_type"] = self.prefs.picture_cache_type
self.model.options["include_exists_check"] = self.prefs.include_exists_check
self.model.options["rehash_ignore_mtime"] = self.prefs.rehash_ignore_mtime
if self.details_dialog: if self.details_dialog:
self.details_dialog.update_options() self.details_dialog.update_options()

View File

@@ -161,6 +161,8 @@ class Preferences(PreferencesBase):
self.ignore_hardlink_matches = get("IgnoreHardlinkMatches", self.ignore_hardlink_matches) self.ignore_hardlink_matches = get("IgnoreHardlinkMatches", self.ignore_hardlink_matches)
self.use_regexp = get("UseRegexp", self.use_regexp) self.use_regexp = get("UseRegexp", self.use_regexp)
self.remove_empty_folders = get("RemoveEmptyFolders", self.remove_empty_folders) self.remove_empty_folders = get("RemoveEmptyFolders", self.remove_empty_folders)
self.rehash_ignore_mtime = get("RehashIgnoreMTime", self.rehash_ignore_mtime)
self.include_exists_check = get("IncludeExistsCheck", self.include_exists_check)
self.debug_mode = get("DebugMode", self.debug_mode) self.debug_mode = get("DebugMode", self.debug_mode)
self.profile_scan = get("ProfileScan", self.profile_scan) self.profile_scan = get("ProfileScan", self.profile_scan)
self.destination_type = get("DestinationType", self.destination_type) self.destination_type = get("DestinationType", self.destination_type)
@@ -231,6 +233,8 @@ class Preferences(PreferencesBase):
self.use_regexp = False self.use_regexp = False
self.ignore_hardlink_matches = False self.ignore_hardlink_matches = False
self.remove_empty_folders = False self.remove_empty_folders = False
self.rehash_ignore_mtime = False
self.include_exists_check = True
self.debug_mode = False self.debug_mode = False
self.profile_scan = False self.profile_scan = False
self.destination_type = 1 self.destination_type = 1
@@ -283,6 +287,8 @@ class Preferences(PreferencesBase):
set_("IgnoreHardlinkMatches", self.ignore_hardlink_matches) set_("IgnoreHardlinkMatches", self.ignore_hardlink_matches)
set_("UseRegexp", self.use_regexp) set_("UseRegexp", self.use_regexp)
set_("RemoveEmptyFolders", self.remove_empty_folders) set_("RemoveEmptyFolders", self.remove_empty_folders)
set_("RehashIgnoreMTime", self.rehash_ignore_mtime)
set_("IncludeExistsCheck", self.include_exists_check)
set_("DebugMode", self.debug_mode) set_("DebugMode", self.debug_mode)
set_("ProfileScan", self.profile_scan) set_("ProfileScan", self.profile_scan)
set_("DestinationType", self.destination_type) set_("DestinationType", self.destination_type)

View File

@@ -47,8 +47,9 @@ class Sections(Flag):
GENERAL = auto() GENERAL = auto()
DISPLAY = auto() DISPLAY = auto()
ADVANCED = auto()
DEBUG = auto() DEBUG = auto()
ALL = GENERAL | DISPLAY | DEBUG ALL = GENERAL | DISPLAY | ADVANCED | DEBUG
class PreferencesDialogBase(QDialog): class PreferencesDialogBase(QDialog):
@@ -213,6 +214,19 @@ use the modifier key to drag the floating window around"
details_groupbox.setLayout(self.details_groupbox_layout) details_groupbox.setLayout(self.details_groupbox_layout)
self.displayVLayout.addWidget(details_groupbox) self.displayVLayout.addWidget(details_groupbox)
def _setup_advanced_page(self):
tab_label = QLabel(
tr(
"These options are for advanced users or for very specific situations, most users should not have to modify these."
),
wordWrap=True,
)
self.advanced_vlayout.addWidget(tab_label)
self._setupAddCheckbox("include_exists_check_box", tr("Include existence check after scan completion"))
self.advanced_vlayout.addWidget(self.include_exists_check_box)
self._setupAddCheckbox("rehash_ignore_mtime_box", tr("Ignore difference in mtime when loading cached digests"))
self.advanced_vlayout.addWidget(self.rehash_ignore_mtime_box)
def _setupDebugPage(self): def _setupDebugPage(self):
self._setupAddCheckbox("debugModeBox", tr("Debug mode (restart required)")) self._setupAddCheckbox("debugModeBox", tr("Debug mode (restart required)"))
self._setupAddCheckbox("profile_scan_box", tr("Profile scan operation")) self._setupAddCheckbox("profile_scan_box", tr("Profile scan operation"))
@@ -244,16 +258,20 @@ use the modifier key to drag the floating window around"
self.tabwidget = QTabWidget() self.tabwidget = QTabWidget()
self.page_general = QWidget() self.page_general = QWidget()
self.page_display = QWidget() self.page_display = QWidget()
self.page_advanced = QWidget()
self.page_debug = QWidget() self.page_debug = QWidget()
self.widgetsVLayout = QVBoxLayout() self.widgetsVLayout = QVBoxLayout()
self.page_general.setLayout(self.widgetsVLayout) self.page_general.setLayout(self.widgetsVLayout)
self.displayVLayout = QVBoxLayout() self.displayVLayout = QVBoxLayout()
self.displayVLayout.setSpacing(5) # arbitrary value, might conflict with style self.displayVLayout.setSpacing(5) # arbitrary value, might conflict with style
self.page_display.setLayout(self.displayVLayout) self.page_display.setLayout(self.displayVLayout)
self.advanced_vlayout = QVBoxLayout()
self.page_advanced.setLayout(self.advanced_vlayout)
self.debugVLayout = QVBoxLayout() self.debugVLayout = QVBoxLayout()
self.page_debug.setLayout(self.debugVLayout) self.page_debug.setLayout(self.debugVLayout)
self._setupPreferenceWidgets() self._setupPreferenceWidgets()
self._setupDisplayPage() self._setupDisplayPage()
self._setup_advanced_page()
self._setupDebugPage() self._setupDebugPage()
# self.mainVLayout.addLayout(self.widgetsVLayout) # self.mainVLayout.addLayout(self.widgetsVLayout)
self.buttonBox = QDialogButtonBox(self) self.buttonBox = QDialogButtonBox(self)
@@ -265,9 +283,11 @@ use the modifier key to drag the floating window around"
self.layout().setSizeConstraint(QLayout.SetFixedSize) self.layout().setSizeConstraint(QLayout.SetFixedSize)
self.tabwidget.addTab(self.page_general, tr("General")) self.tabwidget.addTab(self.page_general, tr("General"))
self.tabwidget.addTab(self.page_display, tr("Display")) self.tabwidget.addTab(self.page_display, tr("Display"))
self.tabwidget.addTab(self.page_advanced, tr("Advanced"))
self.tabwidget.addTab(self.page_debug, tr("Debug")) self.tabwidget.addTab(self.page_debug, tr("Debug"))
self.displayVLayout.addStretch(0) self.displayVLayout.addStretch(0)
self.widgetsVLayout.addStretch(0) self.widgetsVLayout.addStretch(0)
self.advanced_vlayout.addStretch(0)
self.debugVLayout.addStretch(0) self.debugVLayout.addStretch(0)
def _load(self, prefs, setchecked, section): def _load(self, prefs, setchecked, section):
@@ -318,6 +338,9 @@ use the modifier key to drag the floating window around"
except KeyError: except KeyError:
selected_lang = self.supportedLanguages["en"] selected_lang = self.supportedLanguages["en"]
self.languageComboBox.setCurrentText(selected_lang) self.languageComboBox.setCurrentText(selected_lang)
if section & Sections.ADVANCED:
setchecked(self.rehash_ignore_mtime_box, prefs.rehash_ignore_mtime)
setchecked(self.include_exists_check_box, prefs.include_exists_check)
if section & Sections.DEBUG: if section & Sections.DEBUG:
setchecked(self.debugModeBox, prefs.debug_mode) setchecked(self.debugModeBox, prefs.debug_mode)
setchecked(self.profile_scan_box, prefs.profile_scan) setchecked(self.profile_scan_box, prefs.profile_scan)
@@ -334,6 +357,8 @@ use the modifier key to drag the floating window around"
prefs.use_regexp = ischecked(self.useRegexpBox) prefs.use_regexp = ischecked(self.useRegexpBox)
prefs.remove_empty_folders = ischecked(self.removeEmptyFoldersBox) prefs.remove_empty_folders = ischecked(self.removeEmptyFoldersBox)
prefs.ignore_hardlink_matches = ischecked(self.ignoreHardlinkMatches) prefs.ignore_hardlink_matches = ischecked(self.ignoreHardlinkMatches)
prefs.rehash_ignore_mtime = ischecked(self.rehash_ignore_mtime_box)
prefs.include_exists_check = ischecked(self.include_exists_check_box)
prefs.debug_mode = ischecked(self.debugModeBox) prefs.debug_mode = ischecked(self.debugModeBox)
prefs.profile_scan = ischecked(self.profile_scan_box) prefs.profile_scan = ischecked(self.profile_scan_box)
prefs.reference_bold_font = ischecked(self.reference_bold_font) prefs.reference_bold_font = ischecked(self.reference_bold_font)

View File

@@ -5,7 +5,7 @@
# http://www.gnu.org/licenses/gpl-3.0.html # http://www.gnu.org/licenses/gpl-3.0.html
from PyQt5.QtCore import Qt, QTimer from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtWidgets import QProgressDialog from PyQt5.QtWidgets import QDialog, QMessageBox, QVBoxLayout, QLabel, QProgressBar, QPushButton
from hscommon.trans import tr from hscommon.trans import tr
@@ -25,37 +25,60 @@ class ProgressWindow:
def refresh(self): # Labels def refresh(self): # Labels
if self._window is not None: if self._window is not None:
self._window.setWindowTitle(self.model.jobdesc_textfield.text) self._window.setWindowTitle(self.model.jobdesc_textfield.text)
self._window.setLabelText(self.model.progressdesc_textfield.text) self._label.setText(self.model.progressdesc_textfield.text)
def set_progress(self, last_progress): def set_progress(self, last_progress):
if self._window is not None: if self._window is not None:
if last_progress < 0: if last_progress < 0:
self._window.setRange(0, 0) self._progress_bar.setRange(0, 0)
else: else:
self._window.setRange(0, 100) self._progress_bar.setRange(0, 100)
self._window.setValue(last_progress) self._progress_bar.setValue(last_progress)
def show(self): def show(self):
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint
self._window = QProgressDialog("", tr("Cancel"), 0, 100, self.parent, flags) self._window = QDialog(self.parent, flags)
self._setup_ui()
self._window.setModal(True) self._window.setModal(True)
self._window.setAutoReset(False)
self._window.setAutoClose(False)
self._timer = QTimer(self._window) self._timer = QTimer(self._window)
self._timer.timeout.connect(self.model.pulse) self._timer.timeout.connect(self.model.pulse)
self._window.show() self._window.show()
self._window.canceled.connect(self.model.cancel)
self._timer.start(500) self._timer.start(500)
def _setup_ui(self):
self._window.setWindowTitle(tr("Cancel"))
vertical_layout = QVBoxLayout(self._window)
self._label = QLabel("", self._window)
vertical_layout.addWidget(self._label)
self._progress_bar = QProgressBar(self._window)
self._progress_bar.setRange(0, 100)
vertical_layout.addWidget(self._progress_bar)
self._cancel_button = QPushButton(tr("Cancel"), self._window)
self._cancel_button.clicked.connect(self.cancel)
vertical_layout.addWidget(self._cancel_button)
def cancel(self):
if self._window is not None:
confirm_dialog = QMessageBox(
QMessageBox.Icon.Question,
tr("Cancel?"),
tr("Are you sure you want to cancel? All progress will be lost."),
QMessageBox.StandardButton.No | QMessageBox.StandardButton.Yes,
self._window,
)
confirm_dialog.setDefaultButton(QMessageBox.StandardButton.No)
result = confirm_dialog.exec_()
if result != QMessageBox.StandardButton.Yes:
return
self.close()
def close(self): def close(self):
# it seems it is possible for close to be called without a corresponding # it seems it is possible for close to be called without a corresponding
# show, only perform a close if there is a window to close # show, only perform a close if there is a window to close
if self._window is not None: if self._window is not None:
self._timer.stop() self._timer.stop()
del self._timer del self._timer
# For some weird reason, canceled() signal is sent upon close, whether the user canceled
# or not. If we don't want a false cancellation, we have to disconnect it.
self._window.canceled.disconnect()
self._window.close() self._window.close()
self._window.setParent(None) self._window.setParent(None)
self._window = None self._window = None
self.model.cancel()

View File

@@ -19,4 +19,4 @@ deps =
exclude = .tox,env,build,cocoalib,cocoa,help,./qt/dg_rc.py,cocoa/run_template.py,./pkg exclude = .tox,env,build,cocoalib,cocoa,help,./qt/dg_rc.py,cocoa/run_template.py,./pkg
max-line-length = 120 max-line-length = 120
select = C,E,F,W,B,B950 select = C,E,F,W,B,B950
extend-ignore = E203, E501 extend-ignore = E203, E501, W503