1
0
mirror of https://github.com/arsenetar/dupeguru.git synced 2026-01-24 23:51:38 +00:00

Compare commits

..

58 Commits

Author SHA1 Message Date
9e4b41feb5 Fix BASE_PATH for frozen macOS app 2022-03-09 06:50:41 -06:00
cbfa8720f1 Update imports for objc module 2022-03-09 05:01:12 -06:00
a02c5e5b9b Add built modules as artifacts 2022-03-04 01:14:01 -06:00
35e6ffd6af Fix macOS packaging issue 2022-02-09 22:33:41 -06:00
e957f840da Fix python version check in makefile, close #971 2022-02-09 21:59:35 -06:00
85e22089bd Black formatting changes 2022-02-09 21:49:51 -06:00
b7d68b4458 Update debian control template depends 2022-02-09 21:45:45 -06:00
8f440603ee Add Python 3.10 to tox.ini 2022-01-25 10:39:52 -06:00
5d8e559ca3 Fix issue introduced in fix for #900 2022-01-25 10:39:08 -06:00
2c11eecf97 Update version and changelog to 4.2.0 2022-01-24 22:28:40 -06:00
02803f738b Update translation files including Malay 2022-01-24 21:05:33 -06:00
db27e6a645 Add Malay to language selection 2022-01-24 21:02:57 -06:00
c9c35cc60d Add translation source file for dark style change. 2022-01-24 19:33:42 -06:00
880205dbc8 Fix python 3.10 in default action 2022-01-24 19:30:42 -06:00
6456e64328 Update python versions for CI/CD
- Update python versions for Default action
- Set python versions for sonarcloud
2022-01-24 19:27:29 -06:00
f6a0c0cc6d Add initial dark style for use in Windows
- Other platforms can achieve this with the OS theme so not enabled for them at this time.
- Adds preference in display options to use dark style, default is false.
2022-01-24 19:14:30 -06:00
eb57d269fc Update translation source files 2021-11-23 21:11:30 -06:00
34f41dc522 Merge pull request #942 from Dobatymo/hash-cache
Implement hash cache for md5 hash based on sqlite
2021-11-23 21:08:22 -06:00
Dobatymo
77460045c4 clean up abstraction 2021-10-29 15:24:47 +08:00
Dobatymo
9753afba74 change FilesDB to singleton class
move hash calculation back in to Files class
clear cache now clears hash cache in addition to picture cache
2021-10-29 15:12:40 +08:00
Dobatymo
1ea108fc2b changed cache filename 2021-10-29 15:12:40 +08:00
Dobatymo
2f02a6010d implement hash cache for md5 hash based on sqlite 2021-10-29 15:12:40 +08:00
b80489fd66 Update translation source files 2021-09-15 20:15:09 -05:00
1d60e124ee Update invoke_custom_command to run for all selected items 2021-09-02 20:48:25 -05:00
e22d7d2fc9 Remove filtering of 0 size files in engine
Files size is already able to be filtered at a higher level, some users
may decide to see zero length files. Fix #321.
2021-08-28 18:16:22 -05:00
0a0694e095 Expand fix for #630 to fix #551 2021-08-28 17:29:25 -05:00
3da9d5d869 Update documentation files, add multi-language doc build
- Update links in documentation, and some errors
- Remove non-existent page
- Update build to build all languages with --alldoc flag
- Fix one minor debugging change introduced in package.py
2021-08-28 17:07:18 -05:00
78fb052d77 Add more progress details to getmatches, ref #700 2021-08-28 04:58:22 -05:00
9805cba10d Use different message for direct delete success, close #904 2021-08-28 04:27:34 -05:00
4c3dfe2f1f Provide more feedback during scans
- Add output for number of collected files / folders
- Update to allow indeterminate progress bar
- Remove unused hscommon\jobprogress\qt.py
2021-08-28 04:05:07 -05:00
b0baa5bfd6 Add windows position handling at open, fix #653
- Move offscreen windows back on screen
- Restore maximized state without impacting resored size
- Fullscreen comes back on primary screen, needs further work to support
  restore on other screens
2021-08-27 23:26:19 -05:00
22996ee914 Merge pull request #935 from chchia/master
resize preference dialog file size box
2021-08-27 21:57:03 -05:00
chchia
31ec9c667f resize preference dialog file size box 2021-08-28 10:28:06 +08:00
3045361243 Add preference to ignore large files, close #430 2021-08-27 05:35:54 -05:00
809116c764 Fix CodeQL Alerts
- Cast int to Py_ssize_t for multiplication
2021-08-26 03:43:31 -05:00
83f401595d Minor Updates
- Cleanup extension modules in setup.py to use correct namespaces
- Update build.py to leverage setup.py for modules
- Roll mutagen required version back to 1.44.0 to support more distros
- Change build.py and sphinxgen.py to use pathlib
- Remove hsaudiotag from package list for debian and arch
2021-08-26 03:29:24 -05:00
814d145366 Updates to setup files
- Include additional non-python files in MANIFEST.in (package_data in
  setup.cfg was not including the files)
- Update requirements in setup.cfg
2021-08-25 04:10:38 -05:00
efb76c7686 Add OS and Python Information to error dialog 2021-08-25 02:05:18 -05:00
47dbe805bb More cleanup and fixed a flake8 build issue 2021-08-25 01:11:24 -05:00
f11fccc889 More cleanups
- Cleanup columns.py and tables
- Other misc cleanups
- Remove text_field.py from qtlib as it is not used
- Remove unused variables from image_viewer method
2021-08-25 00:46:33 -05:00
2e13c4ccb5 Update internationalization files 2021-08-24 03:54:54 -05:00
da72ffd1fd Add ability to use non-native dialog for directories
- Add preference for native dialogs
- Add non-native directory selection to allow selecting multiple folders
  fixes #874 when using non-native.
2021-08-24 03:52:43 -05:00
2c9437bef4 Fix #897 2021-08-24 03:13:03 -05:00
f9085386a6 First pass code cleanup in qt/qtlib 2021-08-24 00:12:23 -05:00
d576a7043c Code cleanups in core and other affected files 2021-08-21 18:02:02 -05:00
1ef5f56158 Code cleanups in hscommon & external effects 2021-08-21 16:56:27 -05:00
f9316de244 Code cleanups in hscommon\tests 2021-08-21 16:25:33 -05:00
0189c29f47 Misc cleanups in core/tests 2021-08-21 03:52:09 -05:00
b4fa1d68f0 Add check for python version to build.py, close #589 2021-08-20 23:49:20 -05:00
16df882481 Update requirements.txt for previous change 2021-08-19 00:17:46 -05:00
58c04ff9ad Switch from hsaudiotag to mutagen, close #440
- This opens up the ability to support more tags and audio information
- Also makes progress on #333
2021-08-19 00:14:26 -05:00
6b8f85e39a Reveal in Explorer / Finder, close #895 2021-08-18 20:51:45 -05:00
2fff1a3436 Add ablity to load results at start, closes #902
- Add ablility to load .dupguru file at start by passing as first argument
- Add file association to .dupeguru file in windows at install
2021-08-18 19:24:14 -05:00
a685524dd5 Add files for more standardized build tools
- Prior investigation into linux packaging (not using pyinstaller) suggested
having setuptools files could make packaging easier and automatable
- Add setup.cfg and setup.py as initial starting point
- Add MANIFEST.in (at least temporarily)

Currently with the python build module this almost works for main application.
It does not include all the extra data files right now.
2021-08-18 04:12:38 -05:00
74918e2c56 Attempt to fix apt-get failure 2021-08-18 03:07:47 -05:00
18895d983b Fix syntax error in codeql-analysis.yml 2021-08-18 03:04:44 -05:00
fe720208ea Add minimum custom build for codeql cpp 2021-08-18 02:49:20 -05:00
091d9e9239 Create codeql-analysis.yml
Test out codeql
2021-08-18 02:33:40 -05:00
178 changed files with 5804 additions and 3130 deletions

50
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,50 @@
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '24 20 * * 2'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'cpp', 'python' ]
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
- if: matrix.language == 'cpp'
name: Build Cpp
run: |
sudo apt-get update
sudo apt-get install python3-pyqt5
make modules
- if: matrix.language == 'python'
name: Autobuild
uses: github/codeql-action/autobuild@v1
# Analysis
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -13,10 +13,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.9 - name: Set up Python 3.10
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: 3.9 python-version: '3.10'
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
@@ -28,10 +28,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.9 - name: Set up Python 3.10
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: 3.9 python-version: '3.10'
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
@@ -45,16 +45,20 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
python-version: [3.6, 3.7, 3.8, 3.9] python-version: [3.6, 3.7, 3.8, 3.9, '3.10']
exclude: exclude:
- os: macos-latest - os: macos-latest
python-version: 3.6 python-version: 3.6
- os: macos-latest - os: macos-latest
python-version: 3.7 python-version: 3.7
- os: macos-latest
python-version: 3.8
- os: windows-latest - os: windows-latest
python-version: 3.6 python-version: 3.6
- os: windows-latest - os: windows-latest
python-version: 3.7 python-version: 3.7
- os: windows-latest
python-version: 3.8
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -72,3 +76,9 @@ jobs:
- name: Run tests - name: Run tests
run: | run: |
pytest core hscommon pytest core hscommon
- name: Upload Artifacts
if: matrix.os == 'ubuntu-latest'
uses: actions/upload-artifact@v3
with:
name: modules ${{ matrix.python-version }}
path: ${{ github.workspace }}/**/*.so

2
.gitignore vendored
View File

@@ -1,11 +1,13 @@
.DS_Store .DS_Store
__pycache__ __pycache__
*.egg-info
*.so *.so
*.mo *.mo
*.waf* *.waf*
.lock-waf* .lock-waf*
.tox .tox
/tags /tags
*.eggs
build build
dist dist

1
.sonarcloud.properties Normal file
View File

@@ -0,0 +1 @@
sonar.python.version=3.6, 3.7, 3.8, 3.9, 3.10

6
MANIFEST.in Normal file
View File

@@ -0,0 +1,6 @@
recursive-include core *.h
recursive-include core *.m
include run.py
graft locale
graft help
graft qtlib/locale

View File

@@ -53,7 +53,7 @@ pyc: | env
${VENV_PYTHON} -m compileall ${packages} ${VENV_PYTHON} -m compileall ${packages}
reqs: reqs:
ifneq ($(shell test $(PYTHON_VERSION_MINOR) -gt $(REQ_MINOR_VERSION); echo $$?),0) ifneq ($(shell test $(PYTHON_VERSION_MINOR) -ge $(REQ_MINOR_VERSION); echo $$?),0)
$(error "Python 3.${REQ_MINOR_VERSION}+ required. Aborting.") $(error "Python 3.${REQ_MINOR_VERSION}+ required. Aborting.")
endif endif
ifndef NO_VENV ifndef NO_VENV

106
build.py
View File

@@ -4,18 +4,17 @@
# which should be included with this package. The terms are also available at # which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html # http://www.gnu.org/licenses/gpl-3.0.html
import os from pathlib import Path
import os.path as op import sys
from optparse import OptionParser from optparse import OptionParser
import shutil import shutil
from multiprocessing import Pool
from setuptools import setup, Extension from setuptools import sandbox
from hscommon import sphinxgen from hscommon import sphinxgen
from hscommon.build import ( from hscommon.build import (
add_to_pythonpath, add_to_pythonpath,
print_and_do, print_and_do,
move_all,
fix_qt_resource_file, fix_qt_resource_file,
) )
from hscommon import loc from hscommon import loc
@@ -30,7 +29,8 @@ def parse_args():
dest="clean", dest="clean",
help="Clean build folder before building", help="Clean build folder before building",
) )
parser.add_option("--doc", action="store_true", dest="doc", help="Build only the help file") parser.add_option("--doc", action="store_true", dest="doc", help="Build only the help file (en)")
parser.add_option("--alldoc", action="store_true", dest="all_doc", help="Build only the help file in all languages")
parser.add_option("--loc", action="store_true", dest="loc", help="Build only localization") parser.add_option("--loc", action="store_true", dest="loc", help="Build only localization")
parser.add_option( parser.add_option(
"--updatepot", "--updatepot",
@@ -60,16 +60,16 @@ def parse_args():
return options return options
def build_help(): def build_one_help(language):
print("Generating Help") print("Generating Help in {}".format(language))
current_path = op.abspath(".") current_path = Path(".").absolute()
help_basepath = op.join(current_path, "help", "en") changelog_path = current_path.joinpath("help", "changelog")
help_destpath = op.join(current_path, "build", "help")
changelog_path = op.join(current_path, "help", "changelog")
tixurl = "https://github.com/arsenetar/dupeguru/issues/{}" tixurl = "https://github.com/arsenetar/dupeguru/issues/{}"
confrepl = {"language": "en"} changelogtmpl = current_path.joinpath("help", "changelog.tmpl")
changelogtmpl = op.join(current_path, "help", "changelog.tmpl") conftmpl = current_path.joinpath("help", "conf.tmpl")
conftmpl = op.join(current_path, "help", "conf.tmpl") help_basepath = current_path.joinpath("help", language)
help_destpath = current_path.joinpath("build", "help", language)
confrepl = {"language": language}
sphinxgen.gen( sphinxgen.gen(
help_basepath, help_basepath,
help_destpath, help_destpath,
@@ -81,16 +81,23 @@ def build_help():
) )
def build_help():
languages = ["en", "de", "fr", "hy", "ru", "uk"]
# Running with Pools as for some reason sphinx seems to cross contaminate the output otherwise
with Pool(len(languages)) as p:
p.map(build_one_help, languages)
def build_qt_localizations(): def build_qt_localizations():
loc.compile_all_po(op.join("qtlib", "locale")) loc.compile_all_po(Path("qtlib", "locale"))
loc.merge_locale_dir(op.join("qtlib", "locale"), "locale") loc.merge_locale_dir(Path("qtlib", "locale"), "locale")
def build_localizations(): def build_localizations():
loc.compile_all_po("locale") loc.compile_all_po("locale")
build_qt_localizations() build_qt_localizations()
locale_dest = op.join("build", "locale") locale_dest = Path("build", "locale")
if op.exists(locale_dest): if locale_dest.exists():
shutil.rmtree(locale_dest) shutil.rmtree(locale_dest)
shutil.copytree("locale", locale_dest, ignore=shutil.ignore_patterns("*.po", "*.pot")) shutil.copytree("locale", locale_dest, ignore=shutil.ignore_patterns("*.po", "*.pot"))
@@ -98,57 +105,35 @@ def build_localizations():
def build_updatepot(): def build_updatepot():
print("Building .pot files from source files") print("Building .pot files from source files")
print("Building core.pot") print("Building core.pot")
loc.generate_pot(["core"], op.join("locale", "core.pot"), ["tr"]) loc.generate_pot(["core"], Path("locale", "core.pot"), ["tr"])
print("Building columns.pot") print("Building columns.pot")
loc.generate_pot(["core"], op.join("locale", "columns.pot"), ["coltr"]) loc.generate_pot(["core"], Path("locale", "columns.pot"), ["coltr"])
print("Building ui.pot") print("Building ui.pot")
# When we're not under OS X, we don't want to overwrite ui.pot because it contains Cocoa locs # When we're not under OS X, we don't want to overwrite ui.pot because it contains Cocoa locs
# We want to merge the generated pot with the old pot in the most preserving way possible. # We want to merge the generated pot with the old pot in the most preserving way possible.
ui_packages = ["qt", op.join("cocoa", "inter")] ui_packages = ["qt", Path("cocoa", "inter")]
loc.generate_pot(ui_packages, op.join("locale", "ui.pot"), ["tr"], merge=True) loc.generate_pot(ui_packages, Path("locale", "ui.pot"), ["tr"], merge=True)
print("Building qtlib.pot") print("Building qtlib.pot")
loc.generate_pot(["qtlib"], op.join("qtlib", "locale", "qtlib.pot"), ["tr"]) loc.generate_pot(["qtlib"], Path("qtlib", "locale", "qtlib.pot"), ["tr"])
def build_mergepot(): def build_mergepot():
print("Updating .po files using .pot files") print("Updating .po files using .pot files")
loc.merge_pots_into_pos("locale") loc.merge_pots_into_pos("locale")
loc.merge_pots_into_pos(op.join("qtlib", "locale")) loc.merge_pots_into_pos(Path("qtlib", "locale"))
# loc.merge_pots_into_pos(op.join("cocoalib", "locale")) # loc.merge_pots_into_pos(Path("cocoalib", "locale"))
def build_normpo(): def build_normpo():
loc.normalize_all_pos("locale") loc.normalize_all_pos("locale")
loc.normalize_all_pos(op.join("qtlib", "locale")) loc.normalize_all_pos(Path("qtlib", "locale"))
# loc.normalize_all_pos(op.join("cocoalib", "locale")) # loc.normalize_all_pos(Path("cocoalib", "locale"))
def build_pe_modules(): def build_pe_modules():
print("Building PE Modules") print("Building PE Modules")
exts = [ # Leverage setup.py to build modules
Extension( sandbox.run_setup("setup.py", ["build_ext", "--inplace"])
"_block",
[
op.join("core", "pe", "modules", "block.c"),
op.join("core", "pe", "modules", "common.c"),
],
),
Extension(
"_cache",
[
op.join("core", "pe", "modules", "cache.c"),
op.join("core", "pe", "modules", "common.c"),
],
),
]
exts.append(Extension("_block_qt", [op.join("qt", "pe", "modules", "block.c")]))
setup(
script_args=["build_ext", "--inplace"],
ext_modules=exts,
)
move_all("_block_qt*", op.join("qt", "pe"))
move_all("_block*", op.join("core", "pe"))
move_all("_cache*", op.join("core", "pe"))
def build_normal(): def build_normal():
@@ -159,19 +144,22 @@ def build_normal():
print("Building localizations") print("Building localizations")
build_localizations() build_localizations()
print("Building Qt stuff") print("Building Qt stuff")
print_and_do("pyrcc5 {0} > {1}".format(op.join("qt", "dg.qrc"), op.join("qt", "dg_rc.py"))) print_and_do("pyrcc5 {0} > {1}".format(Path("qt", "dg.qrc"), Path("qt", "dg_rc.py")))
fix_qt_resource_file(op.join("qt", "dg_rc.py")) fix_qt_resource_file(Path("qt", "dg_rc.py"))
build_help() build_help()
def main(): def main():
if sys.version_info < (3, 6):
sys.exit("Python < 3.6 is unsupported.")
options = parse_args() options = parse_args()
if options.clean: if options.clean and Path("build").exists():
if op.exists("build"): shutil.rmtree("build")
shutil.rmtree("build") if not Path("build").exists():
if not op.exists("build"): Path("build").mkdir()
os.mkdir("build")
if options.doc: if options.doc:
build_one_help("en")
elif options.all_doc:
build_help() build_help()
elif options.loc: elif options.loc:
build_localizations() build_localizations()

View File

@@ -1,2 +1,2 @@
__version__ = "4.1.1" __version__ = "4.2.0"
__appname__ = "dupeGuru" __appname__ = "dupeGuru"

View File

@@ -48,31 +48,31 @@ MSG_MANY_FILES_TO_OPEN = tr(
class DestType: class DestType:
Direct = 0 DIRECT = 0
Relative = 1 RELATIVE = 1
Absolute = 2 ABSOLUTE = 2
class JobType: class JobType:
Scan = "job_scan" SCAN = "job_scan"
Load = "job_load" LOAD = "job_load"
Move = "job_move" MOVE = "job_move"
Copy = "job_copy" COPY = "job_copy"
Delete = "job_delete" DELETE = "job_delete"
class AppMode: class AppMode:
Standard = 0 STANDARD = 0
Music = 1 MUSIC = 1
Picture = 2 PICTURE = 2
JOBID2TITLE = { JOBID2TITLE = {
JobType.Scan: tr("Scanning for duplicates"), JobType.SCAN: tr("Scanning for duplicates"),
JobType.Load: tr("Loading"), JobType.LOAD: tr("Loading"),
JobType.Move: tr("Moving"), JobType.MOVE: tr("Moving"),
JobType.Copy: tr("Copying"), JobType.COPY: tr("Copying"),
JobType.Delete: tr("Sending to Trash"), JobType.DELETE: tr("Sending to Trash"),
} }
@@ -132,12 +132,14 @@ class DupeGuru(Broadcaster):
logging.debug("Debug mode enabled") logging.debug("Debug mode enabled")
Broadcaster.__init__(self) Broadcaster.__init__(self)
self.view = view self.view = view
self.appdata = desktop.special_folder_path(desktop.SpecialFolder.AppData, appname=self.NAME, portable=portable) self.appdata = desktop.special_folder_path(desktop.SpecialFolder.APPDATA, appname=self.NAME, portable=portable)
if not op.exists(self.appdata): if not op.exists(self.appdata):
os.makedirs(self.appdata) os.makedirs(self.appdata)
self.app_mode = AppMode.Standard self.app_mode = AppMode.STANDARD
self.discarded_file_count = 0 self.discarded_file_count = 0
self.exclude_list = ExcludeList() self.exclude_list = ExcludeList()
hash_cache_file = op.join(self.appdata, "hash_cache.db")
fs.filesdb.connect(hash_cache_file)
self.directories = directories.Directories(self.exclude_list) self.directories = directories.Directories(self.exclude_list)
self.results = results.Results(self) self.results = results.Results(self)
self.ignore_list = IgnoreList() self.ignore_list = IgnoreList()
@@ -148,7 +150,7 @@ class DupeGuru(Broadcaster):
"escape_filter_regexp": True, "escape_filter_regexp": True,
"clean_empty_dirs": False, "clean_empty_dirs": False,
"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,
} }
self.selected_dupes = [] self.selected_dupes = []
@@ -169,9 +171,9 @@ class DupeGuru(Broadcaster):
def _recreate_result_table(self): def _recreate_result_table(self):
if self.result_table is not None: if self.result_table is not None:
self.result_table.disconnect() self.result_table.disconnect()
if self.app_mode == AppMode.Picture: if self.app_mode == AppMode.PICTURE:
self.result_table = pe.result_table.ResultTable(self) self.result_table = pe.result_table.ResultTable(self)
elif self.app_mode == AppMode.Music: elif self.app_mode == AppMode.MUSIC:
self.result_table = me.result_table.ResultTable(self) self.result_table = me.result_table.ResultTable(self)
else: else:
self.result_table = se.result_table.ResultTable(self) self.result_table = se.result_table.ResultTable(self)
@@ -184,15 +186,13 @@ class DupeGuru(Broadcaster):
return op.join(self.appdata, cache_name) return op.join(self.appdata, cache_name)
def _get_dupe_sort_key(self, dupe, get_group, key, delta): def _get_dupe_sort_key(self, dupe, get_group, key, delta):
if self.app_mode in (AppMode.Music, AppMode.Picture): if self.app_mode in (AppMode.MUSIC, AppMode.PICTURE) and key == "folder_path":
if key == "folder_path": dupe_folder_path = getattr(dupe, "display_folder_path", dupe.folder_path)
dupe_folder_path = getattr(dupe, "display_folder_path", dupe.folder_path) return str(dupe_folder_path).lower()
return str(dupe_folder_path).lower() if self.app_mode == AppMode.PICTURE and delta and key == "dimensions":
if self.app_mode == AppMode.Picture: r = cmp_value(dupe, key)
if delta and key == "dimensions": ref_value = cmp_value(get_group().ref, key)
r = cmp_value(dupe, key) return get_delta_dimensions(r, ref_value)
ref_value = cmp_value(get_group().ref, key)
return get_delta_dimensions(r, ref_value)
if key == "marked": if key == "marked":
return self.results.is_marked(dupe) return self.results.is_marked(dupe)
if key == "percentage": if key == "percentage":
@@ -212,10 +212,9 @@ class DupeGuru(Broadcaster):
return result return result
def _get_group_sort_key(self, group, key): def _get_group_sort_key(self, group, key):
if self.app_mode in (AppMode.Music, AppMode.Picture): if self.app_mode in (AppMode.MUSIC, AppMode.PICTURE) and key == "folder_path":
if key == "folder_path": dupe_folder_path = getattr(group.ref, "display_folder_path", group.ref.folder_path)
dupe_folder_path = getattr(group.ref, "display_folder_path", group.ref.folder_path) return str(dupe_folder_path).lower()
return str(dupe_folder_path).lower()
if key == "percentage": if key == "percentage":
return group.percentage return group.percentage
if key == "dupe_count": if key == "dupe_count":
@@ -267,7 +266,7 @@ class DupeGuru(Broadcaster):
return None return None
def _get_export_data(self): def _get_export_data(self):
columns = [col for col in self.result_table.columns.ordered_columns if col.visible and col.name != "marked"] columns = [col for col in self.result_table._columns.ordered_columns if col.visible and col.name != "marked"]
colnames = [col.display for col in columns] colnames = [col.display for col in columns]
rows = [] rows = []
for group_id, group in enumerate(self.results.groups): for group_id, group in enumerate(self.results.groups):
@@ -294,32 +293,36 @@ class DupeGuru(Broadcaster):
self.view.show_message(msg) self.view.show_message(msg)
def _job_completed(self, jobid): def _job_completed(self, jobid):
if jobid == JobType.Scan: if jobid == JobType.SCAN:
self._results_changed() self._results_changed()
fs.filesdb.commit()
if not self.results.groups: if not self.results.groups:
self.view.show_message(tr("No duplicates found.")) self.view.show_message(tr("No duplicates found."))
else: else:
self.view.show_results_window() self.view.show_results_window()
if jobid in {JobType.Move, JobType.Delete}: if jobid in {JobType.MOVE, JobType.DELETE}:
self._results_changed() self._results_changed()
if jobid == JobType.Load: if jobid == JobType.LOAD:
self._recreate_result_table() self._recreate_result_table()
self._results_changed() self._results_changed()
self.view.show_results_window() self.view.show_results_window()
if jobid in {JobType.Copy, JobType.Move, JobType.Delete}: if jobid in {JobType.COPY, JobType.MOVE, JobType.DELETE}:
if self.results.problems: if self.results.problems:
self.problem_dialog.refresh() self.problem_dialog.refresh()
self.view.show_problem_dialog() self.view.show_problem_dialog()
else: else:
msg = { if jobid == JobType.COPY:
JobType.Copy: tr("All marked files were copied successfully."), msg = tr("All marked files were copied successfully.")
JobType.Move: tr("All marked files were moved successfully."), elif jobid == JobType.MOVE:
JobType.Delete: tr("All marked files were successfully sent to Trash."), msg = tr("All marked files were moved successfully.")
}[jobid] elif jobid == JobType.DELETE and self.deletion_options.direct:
msg = tr("All marked files were deleted successfully.")
else:
msg = tr("All marked files were successfully sent to Trash.")
self.view.show_message(msg) self.view.show_message(msg)
def _job_error(self, jobid, err): def _job_error(self, jobid, err):
if jobid == JobType.Load: if jobid == JobType.LOAD:
msg = tr("Could not load file: {}").format(err) msg = tr("Could not load file: {}").format(err)
self.view.show_message(msg) self.view.show_message(msg)
return False return False
@@ -349,17 +352,17 @@ class DupeGuru(Broadcaster):
# --- Protected # --- Protected
def _get_fileclasses(self): def _get_fileclasses(self):
if self.app_mode == AppMode.Picture: if self.app_mode == AppMode.PICTURE:
return [pe.photo.PLAT_SPECIFIC_PHOTO_CLASS] return [pe.photo.PLAT_SPECIFIC_PHOTO_CLASS]
elif self.app_mode == AppMode.Music: elif self.app_mode == AppMode.MUSIC:
return [me.fs.MusicFile] return [me.fs.MusicFile]
else: else:
return [se.fs.File] return [se.fs.File]
def _prioritization_categories(self): def _prioritization_categories(self):
if self.app_mode == AppMode.Picture: if self.app_mode == AppMode.PICTURE:
return pe.prioritize.all_categories() return pe.prioritize.all_categories()
elif self.app_mode == AppMode.Music: elif self.app_mode == AppMode.MUSIC:
return me.prioritize.all_categories() return me.prioritize.all_categories()
else: else:
return prioritize.all_categories() return prioritize.all_categories()
@@ -393,20 +396,20 @@ class DupeGuru(Broadcaster):
g = self.results.get_group_of_duplicate(dupe) g = self.results.get_group_of_duplicate(dupe)
for other in g: for other in g:
if other is not dupe: if other is not dupe:
self.ignore_list.Ignore(str(other.path), str(dupe.path)) self.ignore_list.ignore(str(other.path), str(dupe.path))
self.remove_duplicates(dupes) self.remove_duplicates(dupes)
self.ignore_list_dialog.refresh() self.ignore_list_dialog.refresh()
def apply_filter(self, filter): def apply_filter(self, result_filter):
"""Apply a filter ``filter`` to the results so that it shows only dupe groups that match it. """Apply a filter ``filter`` to the results so that it shows only dupe groups that match it.
:param str filter: filter to apply :param str filter: filter to apply
""" """
self.results.apply_filter(None) self.results.apply_filter(None)
if self.options["escape_filter_regexp"]: if self.options["escape_filter_regexp"]:
filter = escape(filter, set("()[]\\.|+?^")) result_filter = escape(result_filter, set("()[]\\.|+?^"))
filter = escape(filter, "*", ".") result_filter = escape(result_filter, "*", ".")
self.results.apply_filter(filter) self.results.apply_filter(result_filter)
self._results_changed() self._results_changed()
def clean_empty_dirs(self, path): def clean_empty_dirs(self, path):
@@ -420,14 +423,17 @@ class DupeGuru(Broadcaster):
except FileNotFoundError: except FileNotFoundError:
pass # we don't care pass # we don't care
def clear_hash_cache(self):
fs.filesdb.clear()
def copy_or_move(self, dupe, copy: bool, destination: str, dest_type: DestType): def copy_or_move(self, dupe, copy: bool, destination: str, dest_type: DestType):
source_path = dupe.path source_path = dupe.path
location_path = first(p for p in self.directories if dupe.path in p) location_path = first(p for p in self.directories if dupe.path in p)
dest_path = Path(destination) dest_path = Path(destination)
if dest_type in {DestType.Relative, DestType.Absolute}: if dest_type in {DestType.RELATIVE, DestType.ABSOLUTE}:
# no filename, no windows drive letter # no filename, no windows drive letter
source_base = source_path.remove_drive_letter().parent() source_base = source_path.remove_drive_letter().parent()
if dest_type == DestType.Relative: if dest_type == DestType.RELATIVE:
source_base = source_base[location_path:] source_base = source_base[location_path:]
dest_path = dest_path[source_base] dest_path = dest_path[source_base]
if not dest_path.exists(): if not dest_path.exists():
@@ -466,7 +472,7 @@ class DupeGuru(Broadcaster):
) )
if destination: if destination:
desttype = self.options["copymove_dest_type"] desttype = self.options["copymove_dest_type"]
jobid = JobType.Copy if copy else JobType.Move jobid = JobType.COPY if copy else JobType.MOVE
self._start_job(jobid, do) self._start_job(jobid, do)
def delete_marked(self): def delete_marked(self):
@@ -482,7 +488,7 @@ class DupeGuru(Broadcaster):
self.deletion_options.direct, self.deletion_options.direct,
] ]
logging.debug("Starting deletion job with args %r", args) logging.debug("Starting deletion job with args %r", args)
self._start_job(JobType.Delete, self._do_delete, args=args) self._start_job(JobType.DELETE, self._do_delete, args=args)
def export_to_xhtml(self): def export_to_xhtml(self):
"""Export current results to XHTML. """Export current results to XHTML.
@@ -535,21 +541,21 @@ class DupeGuru(Broadcaster):
return return
if not self.selected_dupes: if not self.selected_dupes:
return return
dupe = self.selected_dupes[0] dupes = self.selected_dupes
group = self.results.get_group_of_duplicate(dupe) refs = [self.results.get_group_of_duplicate(dupe).ref for dupe in dupes]
ref = group.ref for dupe, ref in zip(dupes, refs):
cmd = cmd.replace("%d", str(dupe.path)) dupe_cmd = cmd.replace("%d", str(dupe.path))
cmd = cmd.replace("%r", str(ref.path)) dupe_cmd = dupe_cmd.replace("%r", str(ref.path))
match = re.match(r'"([^"]+)"(.*)', cmd) match = re.match(r'"([^"]+)"(.*)', dupe_cmd)
if match is not None: if match is not None:
# This code here is because subprocess. Popen doesn't seem to accept, under Windows, # This code here is because subprocess. Popen doesn't seem to accept, under Windows,
# executable paths with spaces in it, *even* when they're enclosed in "". So this is # executable paths with spaces in it, *even* when they're enclosed in "". So this is
# 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)
subprocess.Popen(exename + args, shell=True, cwd=path) subprocess.Popen(exename + args, shell=True, cwd=path)
else: else:
subprocess.Popen(cmd, shell=True) subprocess.Popen(dupe_cmd, shell=True)
def load(self): def load(self):
"""Load directory selection and ignore list from files in appdata. """Load directory selection and ignore list from files in appdata.
@@ -582,7 +588,7 @@ class DupeGuru(Broadcaster):
def do(j): def do(j):
self.results.load_from_xml(filename, self._get_file, j) self.results.load_from_xml(filename, self._get_file, j)
self._start_job(JobType.Load, do) self._start_job(JobType.LOAD, do)
def make_selected_reference(self): def make_selected_reference(self):
"""Promote :attr:`selected_dupes` to reference position within their respective groups. """Promote :attr:`selected_dupes` to reference position within their respective groups.
@@ -595,9 +601,8 @@ class DupeGuru(Broadcaster):
changed_groups = set() changed_groups = set()
for dupe in dupes: for dupe in dupes:
g = self.results.get_group_of_duplicate(dupe) g = self.results.get_group_of_duplicate(dupe)
if g not in changed_groups: if g not in changed_groups and self.results.make_ref(dupe):
if self.results.make_ref(dupe): changed_groups.add(g)
changed_groups.add(g)
# It's not always obvious to users what this action does, so to make it a bit clearer, # It's not always obvious to users what this action does, so to make it a bit clearer,
# we change our selection to the ref of all changed groups. However, we also want to keep # we change our selection to the ref of all changed groups. However, we also want to keep
# the files that were ref before and weren't changed by the action. In effect, what this # the files that were ref before and weren't changed by the action. In effect, what this
@@ -647,15 +652,14 @@ class DupeGuru(Broadcaster):
def open_selected(self): def open_selected(self):
"""Open :attr:`selected_dupes` with their associated application.""" """Open :attr:`selected_dupes` with their associated application."""
if len(self.selected_dupes) > 10: if len(self.selected_dupes) > 10 and not self.view.ask_yes_no(MSG_MANY_FILES_TO_OPEN):
if not self.view.ask_yes_no(MSG_MANY_FILES_TO_OPEN): return
return
for dupe in self.selected_dupes: for dupe in self.selected_dupes:
desktop.open_path(dupe.path) desktop.open_path(dupe.path)
def purge_ignore_list(self): def purge_ignore_list(self):
"""Remove files that don't exist from :attr:`ignore_list`.""" """Remove files that don't exist from :attr:`ignore_list`."""
self.ignore_list.Filter(lambda f, s: op.exists(f) and op.exists(s)) self.ignore_list.filter(lambda f, s: op.exists(f) and op.exists(s))
self.ignore_list_dialog.refresh() self.ignore_list_dialog.refresh()
def remove_directories(self, indexes): def remove_directories(self, indexes):
@@ -753,6 +757,9 @@ class DupeGuru(Broadcaster):
self.exclude_list.save_to_xml(p) self.exclude_list.save_to_xml(p)
self.notify("save_session") self.notify("save_session")
def close(self):
fs.filesdb.close()
def save_as(self, filename): def save_as(self, filename):
"""Save results in ``filename``. """Save results in ``filename``.
@@ -786,7 +793,7 @@ class DupeGuru(Broadcaster):
for k, v in self.options.items(): for k, v in self.options.items():
if hasattr(scanner, k): if hasattr(scanner, k):
setattr(scanner, k, v) setattr(scanner, k, v)
if self.app_mode == AppMode.Picture: if self.app_mode == AppMode.PICTURE:
scanner.cache_path = self._get_picture_cache_path() scanner.cache_path = self._get_picture_cache_path()
self.results.groups = [] self.results.groups = []
self._recreate_result_table() self._recreate_result_table()
@@ -794,7 +801,7 @@ class DupeGuru(Broadcaster):
def do(j): def do(j):
j.set_progress(0, tr("Collecting files to scan")) j.set_progress(0, tr("Collecting files to scan"))
if scanner.scan_type == ScanType.Folders: if scanner.scan_type == ScanType.FOLDERS:
files = list(self.directories.get_folders(folderclass=se.fs.Folder, j=j)) files = list(self.directories.get_folders(folderclass=se.fs.Folder, j=j))
else: else:
files = list(self.directories.get_files(fileclasses=self.fileclasses, j=j)) files = list(self.directories.get_files(fileclasses=self.fileclasses, j=j))
@@ -804,7 +811,7 @@ class DupeGuru(Broadcaster):
self.results.groups = 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 self.discarded_file_count = scanner.discarded_file_count
self._start_job(JobType.Scan, do) self._start_job(JobType.SCAN, do)
def toggle_selected_mark_state(self): def toggle_selected_mark_state(self):
selected = self.without_ref(self.selected_dupes) selected = self.without_ref(self.selected_dupes)
@@ -849,18 +856,18 @@ class DupeGuru(Broadcaster):
@property @property
def SCANNER_CLASS(self): def SCANNER_CLASS(self):
if self.app_mode == AppMode.Picture: if self.app_mode == AppMode.PICTURE:
return pe.scanner.ScannerPE return pe.scanner.ScannerPE
elif self.app_mode == AppMode.Music: elif self.app_mode == AppMode.MUSIC:
return me.scanner.ScannerME return me.scanner.ScannerME
else: else:
return se.scanner.ScannerSE return se.scanner.ScannerSE
@property @property
def METADATA_TO_READ(self): def METADATA_TO_READ(self):
if self.app_mode == AppMode.Picture: if self.app_mode == AppMode.PICTURE:
return ["size", "mtime", "dimensions", "exif_timestamp"] return ["size", "mtime", "dimensions", "exif_timestamp"]
elif self.app_mode == AppMode.Music: elif self.app_mode == AppMode.MUSIC:
return [ return [
"size", "size",
"mtime", "mtime",

View File

@@ -11,6 +11,7 @@ import logging
from hscommon.jobprogress import job from hscommon.jobprogress import job
from hscommon.path import Path from hscommon.path import Path
from hscommon.util import FileOrPath from hscommon.util import FileOrPath
from hscommon.trans import tr
from . import fs from . import fs
@@ -30,9 +31,9 @@ class DirectoryState:
* DirectoryState.Excluded: Don't scan this folder * DirectoryState.Excluded: Don't scan this folder
""" """
Normal = 0 NORMAL = 0
Reference = 1 REFERENCE = 1
Excluded = 2 EXCLUDED = 2
class AlreadyThereError(Exception): class AlreadyThereError(Exception):
@@ -82,50 +83,49 @@ class Directories:
# We iterate even if we only have one item here # We iterate even if we only have one item here
for denied_path_re in self._exclude_list.compiled: for denied_path_re in self._exclude_list.compiled:
if denied_path_re.match(str(path.name)): if denied_path_re.match(str(path.name)):
return DirectoryState.Excluded return DirectoryState.EXCLUDED
# return # We still use the old logic to force state on hidden dirs # return # We still use the old logic to force state on hidden dirs
# Override this in subclasses to specify the state of some special folders. # Override this in subclasses to specify the state of some special folders.
if path.name.startswith("."): if path.name.startswith("."):
return DirectoryState.Excluded return DirectoryState.EXCLUDED
def _get_files(self, from_path, fileclasses, j): def _get_files(self, from_path, fileclasses, j):
for root, dirs, files in os.walk(str(from_path)): for root, dirs, files in os.walk(str(from_path)):
j.check_if_cancelled() j.check_if_cancelled()
rootPath = Path(root) root_path = Path(root)
state = self.get_state(rootPath) state = self.get_state(root_path)
if state == DirectoryState.Excluded: if state == DirectoryState.EXCLUDED and not any(p[: len(root_path)] == root_path for p in self.states):
# Recursively get files from folders with lots of subfolder is expensive. However, there # Recursively get files from folders with lots of subfolder is expensive. However, there
# might be a subfolder in this path that is not excluded. What we want to do is to skim # might be a subfolder in this path that is not excluded. What we want to do is to skim
# through self.states and see if we must continue, or we can stop right here to save time # through self.states and see if we must continue, or we can stop right here to save time
if not any(p[: len(rootPath)] == rootPath for p in self.states): del dirs[:]
del dirs[:]
try: try:
if state != DirectoryState.Excluded: if state != DirectoryState.EXCLUDED:
# Old logic # Old logic
if self._exclude_list is None or not self._exclude_list.mark_count: if self._exclude_list is None or not self._exclude_list.mark_count:
found_files = [fs.get_file(rootPath + f, fileclasses=fileclasses) for f in files] found_files = [fs.get_file(root_path + f, fileclasses=fileclasses) for f in files]
else: else:
found_files = [] found_files = []
# print(f"len of files: {len(files)} {files}") # print(f"len of files: {len(files)} {files}")
for f in files: for f in files:
if not self._exclude_list.is_excluded(root, f): if not self._exclude_list.is_excluded(root, f):
found_files.append(fs.get_file(rootPath + f, fileclasses=fileclasses)) found_files.append(fs.get_file(root_path + f, fileclasses=fileclasses))
found_files = [f for f in found_files if f is not None] found_files = [f for f in found_files if f is not None]
# In some cases, directories can be considered as files by dupeGuru, which is # In some cases, directories can be considered as files by dupeGuru, which is
# why we have this line below. In fact, there only one case: Bundle files under # why we have this line below. In fact, there only one case: Bundle files under
# OS X... In other situations, this forloop will do nothing. # OS X... In other situations, this forloop will do nothing.
for d in dirs[:]: for d in dirs[:]:
f = fs.get_file(rootPath + d, fileclasses=fileclasses) f = fs.get_file(root_path + d, fileclasses=fileclasses)
if f is not None: if f is not None:
found_files.append(f) found_files.append(f)
dirs.remove(d) dirs.remove(d)
logging.debug( logging.debug(
"Collected %d files in folder %s", "Collected %d files in folder %s",
len(found_files), len(found_files),
str(rootPath), str(root_path),
) )
for file in found_files: for file in found_files:
file.is_ref = state == DirectoryState.Reference file.is_ref = state == DirectoryState.REFERENCE
yield file yield file
except (EnvironmentError, fs.InvalidPath): except (EnvironmentError, fs.InvalidPath):
pass pass
@@ -137,8 +137,8 @@ class Directories:
for folder in self._get_folders(subfolder, j): for folder in self._get_folders(subfolder, j):
yield folder yield folder
state = self.get_state(from_folder.path) state = self.get_state(from_folder.path)
if state != DirectoryState.Excluded: if state != DirectoryState.EXCLUDED:
from_folder.is_ref = state == DirectoryState.Reference from_folder.is_ref = state == DirectoryState.REFERENCE
logging.debug("Yielding Folder %r state: %d", from_folder, state) logging.debug("Yielding Folder %r state: %d", from_folder, state)
yield from_folder yield from_folder
except (EnvironmentError, fs.InvalidPath): except (EnvironmentError, fs.InvalidPath):
@@ -183,8 +183,12 @@ class Directories:
""" """
if fileclasses is None: if fileclasses is None:
fileclasses = [fs.File] fileclasses = [fs.File]
file_count = 0
for path in self._dirs: for path in self._dirs:
for file in self._get_files(path, fileclasses=fileclasses, j=j): for file in self._get_files(path, fileclasses=fileclasses, j=j):
file_count += 1
if type(j) != job.NullJob:
j.set_progress(-1, tr("Collected {} files to scan").format(file_count))
yield file yield file
def get_folders(self, folderclass=None, j=job.nulljob): def get_folders(self, folderclass=None, j=job.nulljob):
@@ -194,9 +198,13 @@ class Directories:
""" """
if folderclass is None: if folderclass is None:
folderclass = fs.Folder folderclass = fs.Folder
folder_count = 0
for path in self._dirs: for path in self._dirs:
from_folder = folderclass(path) from_folder = folderclass(path)
for folder in self._get_folders(from_folder, j): for folder in self._get_folders(from_folder, j):
folder_count += 1
if type(j) != job.NullJob:
j.set_progress(-1, tr("Collected {} folders to scan").format(folder_count))
yield folder yield folder
def get_state(self, path): def get_state(self, path):
@@ -207,9 +215,9 @@ class Directories:
# direct match? easy result. # direct match? easy result.
if path in self.states: if path in self.states:
return self.states[path] return self.states[path]
state = self._default_state_for_path(path) or DirectoryState.Normal state = self._default_state_for_path(path) or DirectoryState.NORMAL
# Save non-default states in cache, necessary for _get_files() # Save non-default states in cache, necessary for _get_files()
if state != DirectoryState.Normal: if state != DirectoryState.NORMAL:
self.states[path] = state self.states[path] = state
return state return state

View File

@@ -24,6 +24,7 @@ from hscommon.jobprogress import job
) = range(3) ) = range(3)
JOB_REFRESH_RATE = 100 JOB_REFRESH_RATE = 100
PROGRESS_MESSAGE = tr("%d matches found from %d groups")
def getwords(s): def getwords(s):
@@ -106,14 +107,14 @@ def compare_fields(first, second, flags=()):
# We don't want to remove field directly in the list. We must work on a copy. # We don't want to remove field directly in the list. We must work on a copy.
second = second[:] second = second[:]
for field1 in first: for field1 in first:
max = 0 max_score = 0
matched_field = None matched_field = None
for field2 in second: for field2 in second:
r = compare(field1, field2, flags) r = compare(field1, field2, flags)
if r > max: if r > max_score:
max = r max_score = r
matched_field = field2 matched_field = field2
results.append(max) results.append(max_score)
if matched_field: if matched_field:
second.remove(matched_field) second.remove(matched_field)
else: else:
@@ -248,10 +249,11 @@ def getmatches(
match_flags.append(MATCH_SIMILAR_WORDS) match_flags.append(MATCH_SIMILAR_WORDS)
if no_field_order: if no_field_order:
match_flags.append(NO_FIELD_ORDER) match_flags.append(NO_FIELD_ORDER)
j.start_job(len(word_dict), tr("0 matches found")) j.start_job(len(word_dict), PROGRESS_MESSAGE % (0, 0))
compared = defaultdict(set) compared = defaultdict(set)
result = [] result = []
try: try:
word_count = 0
# This whole 'popping' thing is there to avoid taking too much memory at the same time. # This whole 'popping' thing is there to avoid taking too much memory at the same time.
while word_dict: while word_dict:
items = word_dict.popitem()[1] items = word_dict.popitem()[1]
@@ -266,7 +268,8 @@ def getmatches(
result.append(m) result.append(m)
if len(result) >= LIMIT: if len(result) >= LIMIT:
return result return result
j.add_progress(desc=tr("%d matches found") % len(result)) word_count += 1
j.add_progress(desc=PROGRESS_MESSAGE % (len(result), word_count))
except MemoryError: except MemoryError:
# This is the place where the memory usage is at its peak during the scan. # This is the place where the memory usage is at its peak during the scan.
# Just continue the process with an incomplete list of matches. # Just continue the process with an incomplete list of matches.
@@ -285,17 +288,21 @@ def getmatches_by_contents(files, bigsize=0, j=job.nulljob):
""" """
size2files = defaultdict(set) size2files = defaultdict(set)
for f in files: for f in files:
if f.size: size2files[f.size].add(f)
size2files[f.size].add(f)
del files del files
possible_matches = [files for files in size2files.values() if len(files) > 1] possible_matches = [files for files in size2files.values() if len(files) > 1]
del size2files del size2files
result = [] result = []
j.start_job(len(possible_matches), tr("0 matches found")) j.start_job(len(possible_matches), PROGRESS_MESSAGE % (0, 0))
group_count = 0
for group in possible_matches: for group in possible_matches:
for first, second in itertools.combinations(group, 2): for first, second in itertools.combinations(group, 2):
if first.is_ref and second.is_ref: if first.is_ref and second.is_ref:
continue # Don't spend time comparing two ref pics together. continue # Don't spend time comparing two ref pics together.
if first.size == 0 and second.size == 0:
# skip md5 for zero length files
result.append(Match(first, second, 100))
continue
if first.md5partial == second.md5partial: if first.md5partial == second.md5partial:
if bigsize > 0 and first.size > bigsize: if bigsize > 0 and first.size > bigsize:
if first.md5samples == second.md5samples: if first.md5samples == second.md5samples:
@@ -303,7 +310,8 @@ def getmatches_by_contents(files, bigsize=0, j=job.nulljob):
else: else:
if first.md5 == second.md5: if first.md5 == second.md5:
result.append(Match(first, second, 100)) result.append(Match(first, second, 100))
j.add_progress(desc=tr("%d matches found") % len(result)) group_count += 1
j.add_progress(desc=PROGRESS_MESSAGE % (len(result), group_count))
return result return result

View File

@@ -150,10 +150,7 @@ class ExcludeList(Markable):
# @timer # @timer
@memoize @memoize
def _do_compile(self, expr): def _do_compile(self, expr):
try: return re.compile(expr)
return re.compile(expr)
except Exception as e:
raise (e)
# @timer # @timer
# @memoize # probably not worth memoizing this one if we memoize the above # @memoize # probably not worth memoizing this one if we memoize the above
@@ -235,7 +232,7 @@ class ExcludeList(Markable):
# This exception should never be ignored # This exception should never be ignored
raise AlreadyThereException() raise AlreadyThereException()
if regex in forbidden_regexes: if regex in forbidden_regexes:
raise Exception("Forbidden (dangerous) expression.") raise ValueError("Forbidden (dangerous) expression.")
iscompilable, exception, compiled = self.compile_re(regex) iscompilable, exception, compiled = self.compile_re(regex)
if not iscompilable and not forced: if not iscompilable and not forced:
@@ -510,7 +507,6 @@ if ISWINDOWS:
def has_sep(regexp): def has_sep(regexp):
return "\\" + sep in regexp return "\\" + sep in regexp
else: else:
def has_sep(regexp): def has_sep(regexp):

View File

@@ -14,7 +14,11 @@
import hashlib import hashlib
from math import floor from math import floor
import logging import logging
import sqlite3
from threading import Lock
from typing import Any
from hscommon.path import Path
from hscommon.util import nonone, get_file_ext from hscommon.util import nonone, get_file_ext
__all__ = [ __all__ = [
@@ -78,6 +82,82 @@ class OperationError(FSError):
cls_message = "Operation on '{name}' failed." cls_message = "Operation on '{name}' failed."
class FilesDB:
create_table_query = "CREATE TABLE IF NOT EXISTS files (path TEXT PRIMARY KEY, size INTEGER, mtime_ns INTEGER, entry_dt DATETIME, md5 BLOB, md5partial BLOB)"
drop_table_query = "DROP TABLE files;"
select_query = "SELECT {key} FROM files WHERE path=:path AND size=:size and mtime_ns=:mtime_ns"
insert_query = """
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;
"""
def __init__(self):
self.conn = None
self.cur = None
self.lock = None
def connect(self, path):
# type: (str, ) -> None
self.conn = sqlite3.connect(path, check_same_thread=False)
self.cur = self.conn.cursor()
self.cur.execute(self.create_table_query)
self.lock = Lock()
def clear(self):
# type: () -> None
with self.lock:
self.cur.execute(self.drop_table_query)
self.cur.execute(self.create_table_query)
def get(self, path, key):
# type: (Path, str) -> bytes
stat = path.stat()
size = stat.st_size
mtime_ns = stat.st_mtime_ns
with self.lock:
self.cur.execute(self.select_query.format(key=key), {"path": str(path), "size": size, "mtime_ns": mtime_ns})
result = self.cur.fetchone()
if result:
return result[0]
return None
def put(self, path, key, value):
# type: (Path, str, Any) -> None
stat = path.stat()
size = stat.st_size
mtime_ns = stat.st_mtime_ns
with self.lock:
self.cur.execute(
self.insert_query.format(key=key),
{"path": str(path), "size": size, "mtime_ns": mtime_ns, "value": value},
)
def commit(self):
# type: () -> None
with self.lock:
self.conn.commit()
def close(self):
# type: () -> None
with self.lock:
self.cur.close()
self.conn.close()
filesdb = FilesDB() # Singleton
class File: class File:
"""Represents a file and holds metadata to be used for scanning.""" """Represents a file and holds metadata to be used for scanning."""
@@ -107,10 +187,32 @@ class File:
result = self.INITIAL_INFO[attrname] result = self.INITIAL_INFO[attrname]
return result return result
# This offset is where we should start reading the file to get a partial md5 def _calc_md5(self):
# For audio file, it should be where audio data starts # type: () -> bytes
def _get_md5partial_offset_and_size(self):
return (0x4000, 0x4000) # 16Kb with self.path.open("rb") as fp:
md5 = hashlib.md5()
# The goal here is to not run out of memory on really big files. However, the chunk
# size has to be large enough so that the python loop isn't too costly in terms of
# CPU.
CHUNK_SIZE = 1024 * 1024 # 1 mb
filedata = fp.read(CHUNK_SIZE)
while filedata:
md5.update(filedata)
filedata = fp.read(CHUNK_SIZE)
return md5.digest()
def _calc_md5partial(self):
# type: () -> bytes
# This offset is where we should start reading the file to get a partial md5
# For audio file, it should be where audio data starts
offset, size = (0x4000, 0x4000)
with self.path.open("rb") as fp:
fp.seek(offset)
partialdata = fp.read(size)
return hashlib.md5(partialdata).digest()
def _read_info(self, field): def _read_info(self, field):
# print(f"_read_info({field}) for {self}") # print(f"_read_info({field}) for {self}")
@@ -120,28 +222,20 @@ class File:
self.mtime = nonone(stats.st_mtime, 0) self.mtime = nonone(stats.st_mtime, 0)
elif field == "md5partial": elif field == "md5partial":
try: try:
with self.path.open("rb") as fp: self.md5partial = filesdb.get(self.path, "md5partial")
offset, size = self._get_md5partial_offset_and_size() if self.md5partial is None:
fp.seek(offset) self.md5partial = self._calc_md5partial()
partialdata = fp.read(size) filesdb.put(self.path, "md5partial", self.md5partial)
md5 = hashlib.md5(partialdata) except Exception as e:
self.md5partial = md5.digest() logging.warning("Couldn't get md5partial for %s: %s", self.path, e)
except Exception:
pass
elif field == "md5": elif field == "md5":
try: try:
with self.path.open("rb") as fp: self.md5 = filesdb.get(self.path, "md5")
md5 = hashlib.md5() if self.md5 is None:
filedata = fp.read(CHUNK_SIZE) self.md5 = self._calc_md5()
while filedata: filesdb.put(self.path, "md5", self.md5)
md5.update(filedata) except Exception as e:
filedata = fp.read(CHUNK_SIZE) logging.warning("Couldn't get md5 for %s: %s", self.path, e)
# FIXME For python 3.8 and later
# while filedata := fp.read(CHUNK_SIZE):
# md5.update(filedata)
self.md5 = md5.digest()
except Exception:
pass
elif field == "md5samples": elif field == "md5samples":
try: try:
with self.path.open("rb") as fp: with self.path.open("rb") as fp:
@@ -168,7 +262,6 @@ class File:
setattr(self, field, md5.digest()) setattr(self, field, md5.digest())
except Exception as e: except Exception as e:
logging.error(f"Error computing md5samples: {e}") logging.error(f"Error computing md5samples: {e}")
pass
def _read_all_info(self, attrnames=None): def _read_all_info(self, attrnames=None):
"""Cache all possible info. """Cache all possible info.

View File

@@ -15,16 +15,21 @@ class DupeGuruGUIObject(Listener):
self.app = app self.app = app
def directories_changed(self): def directories_changed(self):
# Implemented in child classes
pass pass
def dupes_selected(self): def dupes_selected(self):
# Implemented in child classes
pass pass
def marking_changed(self): def marking_changed(self):
# Implemented in child classes
pass pass
def results_changed(self): def results_changed(self):
# Implemented in child classes
pass pass
def results_changed_but_keep_selection(self): def results_changed_but_keep_selection(self):
# Implemented in child classes
pass pass

View File

@@ -44,5 +44,4 @@ class DetailsPanel(GUIObject, DupeGuruGUIObject):
# --- Event Handlers # --- Event Handlers
def dupes_selected(self): def dupes_selected(self):
self._refresh() self._view_updated()
self.view.refresh()

View File

@@ -11,7 +11,7 @@ from hscommon.gui.tree import Tree, Node
from ..directories import DirectoryState from ..directories import DirectoryState
from .base import DupeGuruGUIObject from .base import DupeGuruGUIObject
STATE_ORDER = [DirectoryState.Normal, DirectoryState.Reference, DirectoryState.Excluded] STATE_ORDER = [DirectoryState.NORMAL, DirectoryState.REFERENCE, DirectoryState.EXCLUDED]
# Lazily loads children # Lazily loads children
@@ -86,9 +86,9 @@ class DirectoryTree(Tree, DupeGuruGUIObject):
else: else:
# All selected nodes or on second-or-more level, exclude them. # All selected nodes or on second-or-more level, exclude them.
nodes = self.selected_nodes nodes = self.selected_nodes
newstate = DirectoryState.Excluded newstate = DirectoryState.EXCLUDED
if all(node.state == DirectoryState.Excluded for node in nodes): if all(node.state == DirectoryState.EXCLUDED for node in nodes):
newstate = DirectoryState.Normal newstate = DirectoryState.NORMAL
for node in nodes: for node in nodes:
node.state = newstate node.state = newstate
@@ -103,5 +103,4 @@ class DirectoryTree(Tree, DupeGuruGUIObject):
# --- Event Handlers # --- Event Handlers
def directories_changed(self): def directories_changed(self):
self._refresh() self._view_updated()
self.view.refresh()

View File

@@ -5,7 +5,6 @@
# which should be included with this package. The terms are also available at # which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html # http://www.gnu.org/licenses/gpl-3.0.html
# from hscommon.trans import tr
from .exclude_list_table import ExcludeListTable from .exclude_list_table import ExcludeListTable
from core.exclude import has_sep from core.exclude import has_sep
from os import sep from os import sep
@@ -47,10 +46,7 @@ class ExcludeListDialogCore:
return False return False
def add(self, regex): def add(self, regex):
try: self.exclude_list.add(regex)
self.exclude_list.add(regex)
except Exception as e:
raise (e)
self.exclude_list.mark(regex) self.exclude_list.mark(regex)
self.exclude_list_table.add(regex) self.exclude_list_table.add(regex)

View File

@@ -16,7 +16,7 @@ class ExcludeListTable(GUITable, DupeGuruGUIObject):
def __init__(self, exclude_list_dialog, app): def __init__(self, exclude_list_dialog, app):
GUITable.__init__(self) GUITable.__init__(self)
DupeGuruGUIObject.__init__(self, app) DupeGuruGUIObject.__init__(self, app)
self.columns = Columns(self) self._columns = Columns(self)
self.dialog = exclude_list_dialog self.dialog = exclude_list_dialog
def rename_selected(self, newname): def rename_selected(self, newname):

View File

@@ -24,7 +24,7 @@ class IgnoreListDialog:
return return
msg = tr("Do you really want to remove all %d items from the ignore list?") % len(self.ignore_list) msg = tr("Do you really want to remove all %d items from the ignore list?") % len(self.ignore_list)
if self.app.view.ask_yes_no(msg): if self.app.view.ask_yes_no(msg):
self.ignore_list.Clear() self.ignore_list.clear()
self.refresh() self.refresh()
def refresh(self): def refresh(self):

View File

@@ -22,7 +22,7 @@ class IgnoreListTable(GUITable):
def __init__(self, ignore_list_dialog): def __init__(self, ignore_list_dialog):
GUITable.__init__(self) GUITable.__init__(self)
self.columns = Columns(self) self._columns = Columns(self)
self.view = None self.view = None
self.dialog = ignore_list_dialog self.dialog = ignore_list_dialog

View File

@@ -21,7 +21,7 @@ class ProblemTable(GUITable):
def __init__(self, problem_dialog): def __init__(self, problem_dialog):
GUITable.__init__(self) GUITable.__init__(self)
self.columns = Columns(self) self._columns = Columns(self)
self.dialog = problem_dialog self.dialog = problem_dialog
# --- Override # --- Override

View File

@@ -82,7 +82,7 @@ class ResultTable(GUITable, DupeGuruGUIObject):
def __init__(self, app): def __init__(self, app):
GUITable.__init__(self) GUITable.__init__(self)
DupeGuruGUIObject.__init__(self, app) DupeGuruGUIObject.__init__(self, app)
self.columns = Columns(self, prefaccess=app, savename="ResultTable") self._columns = Columns(self, prefaccess=app, savename="ResultTable")
self._power_marker = False self._power_marker = False
self._delta_values = False self._delta_values = False
self._sort_descriptors = ("name", True) self._sort_descriptors = ("name", True)
@@ -190,4 +190,4 @@ class ResultTable(GUITable, DupeGuruGUIObject):
self.view.refresh() self.view.refresh()
def save_session(self): def save_session(self):
self.columns.save_columns() self._columns.save_columns()

View File

@@ -20,8 +20,7 @@ class IgnoreList:
# ---Override # ---Override
def __init__(self): def __init__(self):
self._ignored = {} self.clear()
self._count = 0
def __iter__(self): def __iter__(self):
for first, seconds in self._ignored.items(): for first, seconds in self._ignored.items():
@@ -32,7 +31,7 @@ class IgnoreList:
return self._count return self._count
# ---Public # ---Public
def AreIgnored(self, first, second): def are_ignored(self, first, second):
def do_check(first, second): def do_check(first, second):
try: try:
matches = self._ignored[first] matches = self._ignored[first]
@@ -42,23 +41,23 @@ class IgnoreList:
return do_check(first, second) or do_check(second, first) return do_check(first, second) or do_check(second, first)
def Clear(self): def clear(self):
self._ignored = {} self._ignored = {}
self._count = 0 self._count = 0
def Filter(self, func): def filter(self, func):
"""Applies a filter on all ignored items, and remove all matches where func(first,second) """Applies a filter on all ignored items, and remove all matches where func(first,second)
doesn't return True. doesn't return True.
""" """
filtered = IgnoreList() filtered = IgnoreList()
for first, second in self: for first, second in self:
if func(first, second): if func(first, second):
filtered.Ignore(first, second) filtered.ignore(first, second)
self._ignored = filtered._ignored self._ignored = filtered._ignored
self._count = filtered._count self._count = filtered._count
def Ignore(self, first, second): def ignore(self, first, second):
if self.AreIgnored(first, second): if self.are_ignored(first, second):
return return
try: try:
matches = self._ignored[first] matches = self._ignored[first]
@@ -88,9 +87,8 @@ class IgnoreList:
except KeyError: except KeyError:
return False return False
if not inner(first, second): if not inner(first, second) and not inner(second, first):
if not inner(second, first): raise ValueError()
raise ValueError()
def load_from_xml(self, infile): def load_from_xml(self, infile):
"""Loads the ignore list from a XML created with save_to_xml. """Loads the ignore list from a XML created with save_to_xml.
@@ -110,7 +108,7 @@ class IgnoreList:
for sfn in subfile_elems: for sfn in subfile_elems:
subfile_path = sfn.get("path") subfile_path = sfn.get("path")
if subfile_path: if subfile_path:
self.Ignore(file_path, subfile_path) self.ignore(file_path, subfile_path)
def save_to_xml(self, outfile): def save_to_xml(self, outfile):
"""Create a XML file that can be used by load_from_xml. """Create a XML file that can be used by load_from_xml.

View File

@@ -17,9 +17,11 @@ class Markable:
# in self.__marked, and is not affected by __inverted. Thus, self.mark while __inverted # in self.__marked, and is not affected by __inverted. Thus, self.mark while __inverted
# is True will launch _DidUnmark. # is True will launch _DidUnmark.
def _did_mark(self, o): def _did_mark(self, o):
# Implemented in child classes
pass pass
def _did_unmark(self, o): def _did_unmark(self, o):
# Implemented in child classes
pass pass
def _get_markable_count(self): def _get_markable_count(self):

View File

@@ -6,7 +6,7 @@
# which should be included with this package. The terms are also available at # which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html # http://www.gnu.org/licenses/gpl-3.0.html
from hsaudiotag import auto import mutagen
from hscommon.util import get_file_ext, format_size, format_time from hscommon.util import get_file_ext, format_size, format_time
from core.util import format_timestamp, format_perc, format_words, format_dupe_count from core.util import format_timestamp, format_perc, format_words, format_dupe_count
@@ -26,6 +26,9 @@ TAG_FIELDS = {
"comment", "comment",
} }
# This is a temporary workaround for migration from hsaudiotag for the can_handle method
SUPPORTED_EXTS = {"mp3", "wma", "m4a", "m4p", "ogg", "flac", "aif", "aiff", "aifc"}
class MusicFile(fs.File): class MusicFile(fs.File):
INITIAL_INFO = fs.File.INITIAL_INFO.copy() INITIAL_INFO = fs.File.INITIAL_INFO.copy()
@@ -50,7 +53,7 @@ class MusicFile(fs.File):
def can_handle(cls, path): def can_handle(cls, path):
if not fs.File.can_handle(path): if not fs.File.can_handle(path):
return False return False
return get_file_ext(path.name) in auto.EXT2CLASS return get_file_ext(path.name) in SUPPORTED_EXTS
def get_display_info(self, group, delta): def get_display_info(self, group, delta):
size = self.size size = self.size
@@ -95,21 +98,23 @@ class MusicFile(fs.File):
} }
def _get_md5partial_offset_and_size(self): def _get_md5partial_offset_and_size(self):
f = auto.File(str(self.path)) # No longer calculating the offset and audio size, just whole file
return (f.audio_offset, f.audio_size) size = self.path.stat().st_size
return (0, size)
def _read_info(self, field): def _read_info(self, field):
fs.File._read_info(self, field) fs.File._read_info(self, field)
if field in TAG_FIELDS: if field in TAG_FIELDS:
f = auto.File(str(self.path)) # The various conversions here are to make this look like the previous implementation
self.audiosize = f.audio_size file = mutagen.File(str(self.path), easy=True)
self.bitrate = f.bitrate self.audiosize = self.path.stat().st_size
self.duration = f.duration self.bitrate = file.info.bitrate / 1000
self.samplerate = f.sample_rate self.duration = file.info.length
self.artist = f.artist self.samplerate = file.info.sample_rate
self.album = f.album self.artist = ", ".join(file.tags.get("artist") or [])
self.title = f.title self.album = ", ".join(file.tags.get("album") or [])
self.genre = f.genre self.title = ", ".join(file.tags.get("title") or [])
self.comment = f.comment self.genre = ", ".join(file.tags.get("genre") or [])
self.year = f.year self.comment = ", ".join(file.tags.get("comment") or [""])
self.track = f.track self.year = ", ".join(file.tags.get("date") or [])
self.track = (file.tags.get("tracknumber") or [""])[0]

View File

@@ -17,9 +17,9 @@ class ScannerME(ScannerBase):
@staticmethod @staticmethod
def get_scan_options(): def get_scan_options():
return [ return [
ScanOption(ScanType.Filename, tr("Filename")), ScanOption(ScanType.FILENAME, tr("Filename")),
ScanOption(ScanType.Fields, tr("Filename - Fields")), ScanOption(ScanType.FIELDS, tr("Filename - Fields")),
ScanOption(ScanType.FieldsNoOrder, tr("Filename - Fields (No Order)")), ScanOption(ScanType.FIELDSNOORDER, tr("Filename - Fields (No Order)")),
ScanOption(ScanType.Tag, tr("Tags")), ScanOption(ScanType.TAG, tr("Tags")),
ScanOption(ScanType.Contents, tr("Contents")), ScanOption(ScanType.CONTENTS, tr("Contents")),
] ]

View File

@@ -193,8 +193,8 @@ class TIFF_file:
self.s2nfunc = s2n_intel if self.endian == INTEL_ENDIAN else s2n_motorola self.s2nfunc = s2n_intel if self.endian == INTEL_ENDIAN else s2n_motorola
def s2n(self, offset, length, signed=0, debug=False): def s2n(self, offset, length, signed=0, debug=False):
slice = self.data[offset : offset + length] data_slice = self.data[offset : offset + length]
val = self.s2nfunc(slice) val = self.s2nfunc(data_slice)
# Sign extension ? # Sign extension ?
if signed: if signed:
msb = 1 << (8 * length - 1) msb = 1 << (8 * length - 1)
@@ -206,7 +206,7 @@ class TIFF_file:
"Slice for offset %d length %d: %r and value: %d", "Slice for offset %d length %d: %r and value: %d",
offset, offset,
length, length,
slice, data_slice,
val, val,
) )
return val return val
@@ -236,10 +236,10 @@ class TIFF_file:
for i in range(entries): for i in range(entries):
entry = ifd + 2 + 12 * i entry = ifd + 2 + 12 * i
tag = self.s2n(entry, 2) tag = self.s2n(entry, 2)
type = self.s2n(entry + 2, 2) entry_type = self.s2n(entry + 2, 2)
if not 1 <= type <= 10: if not 1 <= entry_type <= 10:
continue # not handled continue # not handled
typelen = [1, 1, 2, 4, 8, 1, 1, 2, 4, 8][type - 1] typelen = [1, 1, 2, 4, 8, 1, 1, 2, 4, 8][entry_type - 1]
count = self.s2n(entry + 4, 4) count = self.s2n(entry + 4, 4)
if count > MAX_COUNT: if count > MAX_COUNT:
logging.debug("Probably corrupt. Aborting.") logging.debug("Probably corrupt. Aborting.")
@@ -247,14 +247,14 @@ class TIFF_file:
offset = entry + 8 offset = entry + 8
if count * typelen > 4: if count * typelen > 4:
offset = self.s2n(offset, 4) offset = self.s2n(offset, 4)
if type == 2: if entry_type == 2:
# Special case: nul-terminated ASCII string # Special case: nul-terminated ASCII string
values = str(self.data[offset : offset + count - 1], encoding="latin-1") values = str(self.data[offset : offset + count - 1], encoding="latin-1")
else: else:
values = [] values = []
signed = type == 6 or type >= 8 signed = entry_type == 6 or entry_type >= 8
for j in range(count): for _ in range(count):
if type in {5, 10}: if entry_type in {5, 10}:
# The type is either 5 or 10 # The type is either 5 or 10
value_j = Fraction(self.s2n(offset, 4, signed), self.s2n(offset + 4, 4, signed)) value_j = Fraction(self.s2n(offset, 4, signed), self.s2n(offset + 4, 4, signed))
else: else:
@@ -263,7 +263,7 @@ class TIFF_file:
values.append(value_j) values.append(value_j)
offset = offset + typelen offset = offset + typelen
# Now "values" is either a string or an array # Now "values" is either a string or an array
a.append((tag, type, values)) a.append((tag, entry_type, values))
return a return a
@@ -298,7 +298,7 @@ def get_fields(fp):
T = TIFF_file(data) T = TIFF_file(data)
# There may be more than one IFD per file, but we only read the first one because others are # There may be more than one IFD per file, but we only read the first one because others are
# most likely thumbnails. # most likely thumbnails.
main_IFD_offset = T.first_IFD() main_ifd_offset = T.first_IFD()
result = {} result = {}
def add_tag_to_result(tag, values): def add_tag_to_result(tag, values):
@@ -310,8 +310,8 @@ def get_fields(fp):
return # don't overwrite data return # don't overwrite data
result[stag] = values result[stag] = values
logging.debug("IFD at offset %d", main_IFD_offset) logging.debug("IFD at offset %d", main_ifd_offset)
IFD = T.dump_IFD(main_IFD_offset) IFD = T.dump_IFD(main_ifd_offset)
exif_off = gps_off = 0 exif_off = gps_off = 0
for tag, type, values in IFD: for tag, type, values in IFD:
if tag == 0x8769: if tag == 0x8769:

View File

@@ -2,9 +2,9 @@
* Created On: 2010-01-30 * Created On: 2010-01-30
* Copyright 2014 Hardcoded Software (http://www.hardcoded.net) * Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
* *
* This software is licensed under the "BSD" License as described in the "LICENSE" file, * This software is licensed under the "BSD" License as described in the
* which should be included with this package. The terms are also available at * "LICENSE" file, which should be included with this package. The terms are
* http://www.hardcoded.net/licenses/bsd_license * also available at http://www.hardcoded.net/licenses/bsd_license
*/ */
#include "common.h" #include "common.h"
@@ -14,86 +14,84 @@ static PyObject *NoBlocksError;
/* avgdiff/maxdiff has been called with 2 block lists of different size. */ /* avgdiff/maxdiff has been called with 2 block lists of different size. */
static PyObject *DifferentBlockCountError; static PyObject *DifferentBlockCountError;
/* Returns a 3 sized tuple containing the mean color of 'image'. /* Returns a 3 sized tuple containing the mean color of 'image'.
* image: a PIL image or crop. * image: a PIL image or crop.
*/ */
static PyObject* getblock(PyObject *image) static PyObject *getblock(PyObject *image) {
{ int i, totr, totg, totb;
int i, totr, totg, totb; Py_ssize_t pixel_count;
Py_ssize_t pixel_count; PyObject *ppixels;
PyObject *ppixels;
totr = totg = totb = 0;
totr = totg = totb = 0; ppixels = PyObject_CallMethod(image, "getdata", NULL);
ppixels = PyObject_CallMethod(image, "getdata", NULL); if (ppixels == NULL) {
if (ppixels == NULL) { return NULL;
return NULL; }
}
pixel_count = PySequence_Length(ppixels);
pixel_count = PySequence_Length(ppixels); for (i = 0; i < pixel_count; i++) {
for (i=0; i<pixel_count; i++) { PyObject *ppixel, *pr, *pg, *pb;
PyObject *ppixel, *pr, *pg, *pb; int r, g, b;
int r, g, b;
ppixel = PySequence_ITEM(ppixels, i);
ppixel = PySequence_ITEM(ppixels, i); pr = PySequence_ITEM(ppixel, 0);
pr = PySequence_ITEM(ppixel, 0); pg = PySequence_ITEM(ppixel, 1);
pg = PySequence_ITEM(ppixel, 1); pb = PySequence_ITEM(ppixel, 2);
pb = PySequence_ITEM(ppixel, 2); Py_DECREF(ppixel);
Py_DECREF(ppixel); r = PyLong_AsLong(pr);
r = PyLong_AsLong(pr); g = PyLong_AsLong(pg);
g = PyLong_AsLong(pg); b = PyLong_AsLong(pb);
b = PyLong_AsLong(pb); Py_DECREF(pr);
Py_DECREF(pr); Py_DECREF(pg);
Py_DECREF(pg); Py_DECREF(pb);
Py_DECREF(pb);
totr += r;
totr += r; totg += g;
totg += g; totb += b;
totb += b; }
}
Py_DECREF(ppixels);
Py_DECREF(ppixels);
if (pixel_count) {
if (pixel_count) { totr /= pixel_count;
totr /= pixel_count; totg /= pixel_count;
totg /= pixel_count; totb /= pixel_count;
totb /= pixel_count; }
}
return inttuple(3, totr, totg, totb);
return inttuple(3, totr, totg, totb);
} }
/* Returns the difference between the first block and the second. /* Returns the difference between the first block and the second.
* It returns an absolute sum of the 3 differences (RGB). * It returns an absolute sum of the 3 differences (RGB).
*/ */
static int diff(PyObject *first, PyObject *second) static int diff(PyObject *first, PyObject *second) {
{ int r1, g1, b1, r2, b2, g2;
int r1, g1, b1, r2, b2, g2; PyObject *pr, *pg, *pb;
PyObject *pr, *pg, *pb; pr = PySequence_ITEM(first, 0);
pr = PySequence_ITEM(first, 0); pg = PySequence_ITEM(first, 1);
pg = PySequence_ITEM(first, 1); pb = PySequence_ITEM(first, 2);
pb = PySequence_ITEM(first, 2); r1 = PyLong_AsLong(pr);
r1 = PyLong_AsLong(pr); g1 = PyLong_AsLong(pg);
g1 = PyLong_AsLong(pg); b1 = PyLong_AsLong(pb);
b1 = PyLong_AsLong(pb); Py_DECREF(pr);
Py_DECREF(pr); Py_DECREF(pg);
Py_DECREF(pg); Py_DECREF(pb);
Py_DECREF(pb);
pr = PySequence_ITEM(second, 0);
pr = PySequence_ITEM(second, 0); pg = PySequence_ITEM(second, 1);
pg = PySequence_ITEM(second, 1); pb = PySequence_ITEM(second, 2);
pb = PySequence_ITEM(second, 2); r2 = PyLong_AsLong(pr);
r2 = PyLong_AsLong(pr); g2 = PyLong_AsLong(pg);
g2 = PyLong_AsLong(pg); b2 = PyLong_AsLong(pb);
b2 = PyLong_AsLong(pb); Py_DECREF(pr);
Py_DECREF(pr); Py_DECREF(pg);
Py_DECREF(pg); Py_DECREF(pb);
Py_DECREF(pb);
return abs(r1 - r2) + abs(g1 - g2) + abs(b1 - b2);
return abs(r1 - r2) + abs(g1 - g2) + abs(b1 - b2);
} }
PyDoc_STRVAR(block_getblocks2_doc, PyDoc_STRVAR(block_getblocks2_doc,
"Returns a list of blocks (3 sized tuples).\n\ "Returns a list of blocks (3 sized tuples).\n\
\n\ \n\
image: A PIL image to base the blocks on.\n\ image: A PIL image to base the blocks on.\n\
block_count_per_side: This integer determine the number of blocks the function will return.\n\ block_count_per_side: This integer determine the number of blocks the function will return.\n\
@@ -101,153 +99,150 @@ If it is 10, for example, 100 blocks will be returns (10 width, 10 height). The
necessarely cover square areas. The area covered by each block will be proportional to the image\n\ necessarely cover square areas. The area covered by each block will be proportional to the image\n\
itself.\n"); itself.\n");
static PyObject* block_getblocks2(PyObject *self, PyObject *args) static PyObject *block_getblocks2(PyObject *self, PyObject *args) {
{ int block_count_per_side, width, height, block_width, block_height, ih;
int block_count_per_side, width, height, block_width, block_height, ih; PyObject *image;
PyObject *image; PyObject *pimage_size, *pwidth, *pheight;
PyObject *pimage_size, *pwidth, *pheight; PyObject *result;
PyObject *result;
if (!PyArg_ParseTuple(args, "Oi", &image, &block_count_per_side)) {
if (!PyArg_ParseTuple(args, "Oi", &image, &block_count_per_side)) { return NULL;
}
pimage_size = PyObject_GetAttrString(image, "size");
pwidth = PySequence_ITEM(pimage_size, 0);
pheight = PySequence_ITEM(pimage_size, 1);
width = PyLong_AsLong(pwidth);
height = PyLong_AsLong(pheight);
Py_DECREF(pimage_size);
Py_DECREF(pwidth);
Py_DECREF(pheight);
if (!(width && height)) {
return PyList_New(0);
}
block_width = max(width / block_count_per_side, 1);
block_height = max(height / block_count_per_side, 1);
result = PyList_New((Py_ssize_t)block_count_per_side * block_count_per_side);
if (result == NULL) {
return NULL;
}
for (ih = 0; ih < block_count_per_side; ih++) {
int top, bottom, iw;
top = min(ih * block_height, height - block_height);
bottom = top + block_height;
for (iw = 0; iw < block_count_per_side; iw++) {
int left, right;
PyObject *pbox;
PyObject *pmethodname;
PyObject *pcrop;
PyObject *pblock;
left = min(iw * block_width, width - block_width);
right = left + block_width;
pbox = inttuple(4, left, top, right, bottom);
pmethodname = PyUnicode_FromString("crop");
pcrop = PyObject_CallMethodObjArgs(image, pmethodname, pbox, NULL);
Py_DECREF(pmethodname);
Py_DECREF(pbox);
if (pcrop == NULL) {
Py_DECREF(result);
return NULL; return NULL;
} }
pblock = getblock(pcrop);
pimage_size = PyObject_GetAttrString(image, "size"); Py_DECREF(pcrop);
pwidth = PySequence_ITEM(pimage_size, 0); if (pblock == NULL) {
pheight = PySequence_ITEM(pimage_size, 1); Py_DECREF(result);
width = PyLong_AsLong(pwidth);
height = PyLong_AsLong(pheight);
Py_DECREF(pimage_size);
Py_DECREF(pwidth);
Py_DECREF(pheight);
if (!(width && height)) {
return PyList_New(0);
}
block_width = max(width / block_count_per_side, 1);
block_height = max(height / block_count_per_side, 1);
result = PyList_New(block_count_per_side * block_count_per_side);
if (result == NULL) {
return NULL; return NULL;
}
PyList_SET_ITEM(result, ih * block_count_per_side + iw, pblock);
} }
}
for (ih=0; ih<block_count_per_side; ih++) {
int top, bottom, iw; return result;
top = min(ih*block_height, height-block_height);
bottom = top + block_height;
for (iw=0; iw<block_count_per_side; iw++) {
int left, right;
PyObject *pbox;
PyObject *pmethodname;
PyObject *pcrop;
PyObject *pblock;
left = min(iw*block_width, width-block_width);
right = left + block_width;
pbox = inttuple(4, left, top, right, bottom);
pmethodname = PyUnicode_FromString("crop");
pcrop = PyObject_CallMethodObjArgs(image, pmethodname, pbox, NULL);
Py_DECREF(pmethodname);
Py_DECREF(pbox);
if (pcrop == NULL) {
Py_DECREF(result);
return NULL;
}
pblock = getblock(pcrop);
Py_DECREF(pcrop);
if (pblock == NULL) {
Py_DECREF(result);
return NULL;
}
PyList_SET_ITEM(result, ih*block_count_per_side+iw, pblock);
}
}
return result;
} }
PyDoc_STRVAR(block_avgdiff_doc, PyDoc_STRVAR(block_avgdiff_doc,
"Returns the average diff between first blocks and seconds.\n\ "Returns the average diff between first blocks and seconds.\n\
\n\ \n\
If the result surpasses limit, limit + 1 is returned, except if less than min_iterations\n\ If the result surpasses limit, limit + 1 is returned, except if less than min_iterations\n\
iterations have been made in the blocks.\n"); iterations have been made in the blocks.\n");
static PyObject* block_avgdiff(PyObject *self, PyObject *args) static PyObject *block_avgdiff(PyObject *self, PyObject *args) {
{ PyObject *first, *second;
PyObject *first, *second; int limit, min_iterations;
int limit, min_iterations; Py_ssize_t count;
Py_ssize_t count; int sum, i, result;
int sum, i, result;
if (!PyArg_ParseTuple(args, "OOii", &first, &second, &limit,
if (!PyArg_ParseTuple(args, "OOii", &first, &second, &limit, &min_iterations)) { &min_iterations)) {
return NULL; return NULL;
}
count = PySequence_Length(first);
if (count != PySequence_Length(second)) {
PyErr_SetString(DifferentBlockCountError, "");
return NULL;
}
if (!count) {
PyErr_SetString(NoBlocksError, "");
return NULL;
}
sum = 0;
for (i = 0; i < count; i++) {
int iteration_count;
PyObject *item1, *item2;
iteration_count = i + 1;
item1 = PySequence_ITEM(first, i);
item2 = PySequence_ITEM(second, i);
sum += diff(item1, item2);
Py_DECREF(item1);
Py_DECREF(item2);
if ((sum > limit * iteration_count) &&
(iteration_count >= min_iterations)) {
return PyLong_FromLong(limit + 1);
} }
}
count = PySequence_Length(first);
if (count != PySequence_Length(second)) { result = sum / count;
PyErr_SetString(DifferentBlockCountError, ""); if (!result && sum) {
return NULL; result = 1;
} }
if (!count) { return PyLong_FromLong(result);
PyErr_SetString(NoBlocksError, "");
return NULL;
}
sum = 0;
for (i=0; i<count; i++) {
int iteration_count;
PyObject *item1, *item2;
iteration_count = i + 1;
item1 = PySequence_ITEM(first, i);
item2 = PySequence_ITEM(second, i);
sum += diff(item1, item2);
Py_DECREF(item1);
Py_DECREF(item2);
if ((sum > limit*iteration_count) && (iteration_count >= min_iterations)) {
return PyLong_FromLong(limit + 1);
}
}
result = sum / count;
if (!result && sum) {
result = 1;
}
return PyLong_FromLong(result);
} }
static PyMethodDef BlockMethods[] = { static PyMethodDef BlockMethods[] = {
{"getblocks2", block_getblocks2, METH_VARARGS, block_getblocks2_doc}, {"getblocks2", block_getblocks2, METH_VARARGS, block_getblocks2_doc},
{"avgdiff", block_avgdiff, METH_VARARGS, block_avgdiff_doc}, {"avgdiff", block_avgdiff, METH_VARARGS, block_avgdiff_doc},
{NULL, NULL, 0, NULL} /* Sentinel */ {NULL, NULL, 0, NULL} /* Sentinel */
}; };
static struct PyModuleDef BlockDef = { static struct PyModuleDef BlockDef = {PyModuleDef_HEAD_INIT,
PyModuleDef_HEAD_INIT, "_block",
"_block", NULL,
NULL, -1,
-1, BlockMethods,
BlockMethods, NULL,
NULL, NULL,
NULL, NULL,
NULL, NULL};
NULL
};
PyObject * PyObject *PyInit__block(void) {
PyInit__block(void) PyObject *m = PyModule_Create(&BlockDef);
{ if (m == NULL) {
PyObject *m = PyModule_Create(&BlockDef); return NULL;
if (m == NULL) { }
return NULL;
}
NoBlocksError = PyErr_NewException("_block.NoBlocksError", NULL, NULL);
PyModule_AddObject(m, "NoBlocksError", NoBlocksError);
DifferentBlockCountError = PyErr_NewException("_block.DifferentBlockCountError", NULL, NULL);
PyModule_AddObject(m, "DifferentBlockCountError", DifferentBlockCountError);
return m; NoBlocksError = PyErr_NewException("_block.NoBlocksError", NULL, NULL);
PyModule_AddObject(m, "NoBlocksError", NoBlocksError);
DifferentBlockCountError =
PyErr_NewException("_block.DifferentBlockCountError", NULL, NULL);
PyModule_AddObject(m, "DifferentBlockCountError", DifferentBlockCountError);
return m;
} }

View File

@@ -10,6 +10,8 @@
#include "common.h" #include "common.h"
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
#import <CoreGraphics/CoreGraphics.h>
#import <ImageIO/ImageIO.h>
#define RADIANS( degrees ) ( degrees * M_PI / 180 ) #define RADIANS( degrees ) ( degrees * M_PI / 180 )

View File

@@ -18,12 +18,12 @@ class ScannerPE(Scanner):
@staticmethod @staticmethod
def get_scan_options(): def get_scan_options():
return [ return [
ScanOption(ScanType.FuzzyBlock, tr("Contents")), ScanOption(ScanType.FUZZYBLOCK, tr("Contents")),
ScanOption(ScanType.ExifTimestamp, tr("EXIF Timestamp")), ScanOption(ScanType.EXIFTIMESTAMP, tr("EXIF Timestamp")),
] ]
def _getmatches(self, files, j): def _getmatches(self, files, j):
if self.scan_type == ScanType.FuzzyBlock: if self.scan_type == ScanType.FUZZYBLOCK:
return matchblock.getmatches( return matchblock.getmatches(
files, files,
cache_path=self.cache_path, cache_path=self.cache_path,
@@ -31,7 +31,7 @@ class ScannerPE(Scanner):
match_scaled=self.match_scaled, match_scaled=self.match_scaled,
j=j, j=j,
) )
elif self.scan_type == ScanType.ExifTimestamp: elif self.scan_type == ScanType.EXIFTIMESTAMP:
return matchexif.getmatches(files, self.match_scaled, j) return matchexif.getmatches(files, self.match_scaled, j)
else: else:
raise Exception("Invalid scan type") raise ValueError("Invalid scan type")

View File

@@ -21,16 +21,16 @@ from . import engine
class ScanType: class ScanType:
Filename = 0 FILENAME = 0
Fields = 1 FIELDS = 1
FieldsNoOrder = 2 FIELDSNOORDER = 2
Tag = 3 TAG = 3
Folders = 4 FOLDERS = 4
Contents = 5 CONTENTS = 5
# PE # PE
FuzzyBlock = 10 FUZZYBLOCK = 10
ExifTimestamp = 11 EXIFTIMESTAMP = 11
ScanOption = namedtuple("ScanOption", "scan_type label") ScanOption = namedtuple("ScanOption", "scan_type label")
@@ -77,16 +77,23 @@ class Scanner:
self.discarded_file_count = 0 self.discarded_file_count = 0
def _getmatches(self, files, j): def _getmatches(self, files, j):
if self.size_threshold or self.scan_type in { if (
ScanType.Contents, self.size_threshold
ScanType.Folders, or self.large_size_threshold
}: or self.scan_type
in {
ScanType.CONTENTS,
ScanType.FOLDERS,
}
):
j = j.start_subjob([2, 8]) j = j.start_subjob([2, 8])
for f in j.iter_with_progress(files, tr("Read size of %d/%d files")): for f in j.iter_with_progress(files, tr("Read size of %d/%d files")):
f.size # pre-read, makes a smoother progress if read here (especially for bundles) f.size # pre-read, makes a smoother progress if read here (especially for bundles)
if self.size_threshold: if self.size_threshold:
files = [f for f in files if f.size >= self.size_threshold] files = [f for f in files if f.size >= self.size_threshold]
if self.scan_type in {ScanType.Contents, ScanType.Folders}: if self.large_size_threshold:
files = [f for f in files if f.size <= self.large_size_threshold]
if self.scan_type in {ScanType.CONTENTS, ScanType.FOLDERS}:
return engine.getmatches_by_contents(files, bigsize=self.big_file_size_threshold, j=j) return engine.getmatches_by_contents(files, bigsize=self.big_file_size_threshold, j=j)
else: else:
j = j.start_subjob([2, 8]) j = j.start_subjob([2, 8])
@@ -94,13 +101,13 @@ class Scanner:
kw["match_similar_words"] = self.match_similar_words kw["match_similar_words"] = self.match_similar_words
kw["weight_words"] = self.word_weighting kw["weight_words"] = self.word_weighting
kw["min_match_percentage"] = self.min_match_percentage kw["min_match_percentage"] = self.min_match_percentage
if self.scan_type == ScanType.FieldsNoOrder: if self.scan_type == ScanType.FIELDSNOORDER:
self.scan_type = ScanType.Fields self.scan_type = ScanType.FIELDS
kw["no_field_order"] = True kw["no_field_order"] = True
func = { func = {
ScanType.Filename: lambda f: engine.getwords(rem_file_ext(f.name)), ScanType.FILENAME: lambda f: engine.getwords(rem_file_ext(f.name)),
ScanType.Fields: lambda f: engine.getfields(rem_file_ext(f.name)), ScanType.FIELDS: lambda f: engine.getfields(rem_file_ext(f.name)),
ScanType.Tag: lambda f: [ ScanType.TAG: lambda f: [
engine.getwords(str(getattr(f, attrname))) engine.getwords(str(getattr(f, attrname)))
for attrname in SCANNABLE_TAGS for attrname in SCANNABLE_TAGS
if attrname in self.scanned_tags if attrname in self.scanned_tags
@@ -150,7 +157,7 @@ class Scanner:
# "duplicated duplicates if you will). Then, we also don't want mixed file kinds if the # "duplicated duplicates if you will). Then, we also don't want mixed file kinds if the
# option isn't enabled, we want matches for which both files exist and, lastly, we don't # option isn't enabled, we want matches for which both files exist and, lastly, we don't
# want matches with both files as ref. # want matches with both files as ref.
if self.scan_type == ScanType.Folders and matches: if self.scan_type == ScanType.FOLDERS and matches:
allpath = {m.first.path for m in matches} allpath = {m.first.path for m in matches}
allpath |= {m.second.path for m in matches} allpath |= {m.second.path for m in matches}
sortedpaths = sorted(allpath) sortedpaths = sorted(allpath)
@@ -167,14 +174,14 @@ class Scanner:
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:
matches = [m for m in matches if not ignore_list.AreIgnored(str(m.first.path), str(m.second.path))] matches = [m for m in matches if not ignore_list.are_ignored(str(m.first.path), str(m.second.path))]
logging.info("Grouping matches") logging.info("Grouping matches")
groups = engine.get_groups(matches) groups = engine.get_groups(matches)
if self.scan_type in { if self.scan_type in {
ScanType.Filename, ScanType.FILENAME,
ScanType.Fields, ScanType.FIELDS,
ScanType.FieldsNoOrder, ScanType.FIELDSNOORDER,
ScanType.Tag, ScanType.TAG,
}: }:
matched_files = dedupe([m.first for m in matches] + [m.second for m in matches]) matched_files = dedupe([m.first for m in matches] + [m.second for m in matches])
self.discarded_file_count = len(matched_files) - sum(len(g) for g in groups) self.discarded_file_count = len(matched_files) - sum(len(g) for g in groups)
@@ -199,8 +206,9 @@ class Scanner:
match_similar_words = False match_similar_words = False
min_match_percentage = 80 min_match_percentage = 80
mix_file_kind = True mix_file_kind = True
scan_type = ScanType.Filename scan_type = ScanType.FILENAME
scanned_tags = {"artist", "title"} scanned_tags = {"artist", "title"}
size_threshold = 0 size_threshold = 0
large_size_threshold = 0
big_file_size_threshold = 0 big_file_size_threshold = 0
word_weighting = False word_weighting = False

View File

@@ -13,7 +13,7 @@ class ScannerSE(ScannerBase):
@staticmethod @staticmethod
def get_scan_options(): def get_scan_options():
return [ return [
ScanOption(ScanType.Filename, tr("Filename")), ScanOption(ScanType.FILENAME, tr("Filename")),
ScanOption(ScanType.Contents, tr("Contents")), ScanOption(ScanType.CONTENTS, tr("Contents")),
ScanOption(ScanType.Folders, tr("Folders")), ScanOption(ScanType.FOLDERS, tr("Folders")),
] ]

View File

@@ -23,7 +23,7 @@ from ..scanner import ScanType
def add_fake_files_to_directories(directories, files): def add_fake_files_to_directories(directories, files):
directories.get_files = lambda j=None: iter(files) directories.get_files = lambda j=None: iter(files)
directories._dirs.append("this is just so Scan() doesnt return 3") directories._dirs.append("this is just so Scan() doesn't return 3")
class TestCaseDupeGuru: class TestCaseDupeGuru:
@@ -43,7 +43,7 @@ class TestCaseDupeGuru:
dgapp.apply_filter("()[]\\.|+?^abc") dgapp.apply_filter("()[]\\.|+?^abc")
call = dgapp.results.apply_filter.calls[1] call = dgapp.results.apply_filter.calls[1]
eq_("\\(\\)\\[\\]\\\\\\.\\|\\+\\?\\^abc", call["filter_str"]) eq_("\\(\\)\\[\\]\\\\\\.\\|\\+\\?\\^abc", call["filter_str"])
dgapp.apply_filter("(*)") # In "simple mode", we want the * to behave as a wilcard dgapp.apply_filter("(*)") # In "simple mode", we want the * to behave as a wildcard
call = dgapp.results.apply_filter.calls[3] call = dgapp.results.apply_filter.calls[3]
eq_(r"\(.*\)", call["filter_str"]) eq_(r"\(.*\)", call["filter_str"])
dgapp.options["escape_filter_regexp"] = False dgapp.options["escape_filter_regexp"] = False
@@ -88,14 +88,14 @@ class TestCaseDupeGuru:
eq_(1, len(calls)) eq_(1, len(calls))
eq_(sourcepath, calls[0]["path"]) eq_(sourcepath, calls[0]["path"])
def test_Scan_with_objects_evaluating_to_false(self): def test_scan_with_objects_evaluating_to_false(self):
class FakeFile(fs.File): class FakeFile(fs.File):
def __bool__(self): def __bool__(self):
return False return False
# At some point, any() was used in a wrong way that made Scan() wrongly return 1 # At some point, any() was used in a wrong way that made Scan() wrongly return 1
app = TestApp().app app = TestApp().app
f1, f2 = [FakeFile("foo") for i in range(2)] f1, f2 = [FakeFile("foo") for _ in range(2)]
f1.is_ref, f2.is_ref = (False, False) f1.is_ref, f2.is_ref = (False, False)
assert not (bool(f1) and bool(f2)) assert not (bool(f1) and bool(f2))
add_fake_files_to_directories(app.directories, [f1, f2]) add_fake_files_to_directories(app.directories, [f1, f2])
@@ -110,7 +110,7 @@ class TestCaseDupeGuru:
os.link(str(tmppath["myfile"]), str(tmppath["hardlink"])) os.link(str(tmppath["myfile"]), str(tmppath["hardlink"]))
app = TestApp().app app = TestApp().app
app.directories.add_path(tmppath) app.directories.add_path(tmppath)
app.options["scan_type"] = ScanType.Contents app.options["scan_type"] = ScanType.CONTENTS
app.options["ignore_hardlink_matches"] = True app.options["ignore_hardlink_matches"] = True
app.start_scanning() app.start_scanning()
eq_(len(app.results.groups), 0) eq_(len(app.results.groups), 0)
@@ -124,7 +124,7 @@ class TestCaseDupeGuru:
assert not dgapp.result_table.rename_selected("foo") # no crash assert not dgapp.result_table.rename_selected("foo") # no crash
class TestCaseDupeGuru_clean_empty_dirs: class TestCaseDupeGuruCleanEmptyDirs:
@pytest.fixture @pytest.fixture
def do_setup(self, request): def do_setup(self, request):
monkeypatch = request.getfixturevalue("monkeypatch") monkeypatch = request.getfixturevalue("monkeypatch")
@@ -184,7 +184,7 @@ class TestCaseDupeGuruWithResults:
tmppath["bar"].mkdir() tmppath["bar"].mkdir()
self.app.directories.add_path(tmppath) self.app.directories.add_path(tmppath)
def test_GetObjects(self, do_setup): def test_get_objects(self, do_setup):
objects = self.objects objects = self.objects
groups = self.groups groups = self.groups
r = self.rtable[0] r = self.rtable[0]
@@ -197,7 +197,7 @@ class TestCaseDupeGuruWithResults:
assert r._group is groups[1] assert r._group is groups[1]
assert r._dupe is objects[4] assert r._dupe is objects[4]
def test_GetObjects_after_sort(self, do_setup): def test_get_objects_after_sort(self, do_setup):
objects = self.objects objects = self.objects
groups = self.groups[:] # we need an un-sorted reference groups = self.groups[:] # we need an un-sorted reference
self.rtable.sort("name", False) self.rtable.sort("name", False)
@@ -212,7 +212,7 @@ class TestCaseDupeGuruWithResults:
# The first 2 dupes have been removed. The 3rd one is a ref. it stays there, in first pos. # The first 2 dupes have been removed. The 3rd one is a ref. it stays there, in first pos.
eq_(self.rtable.selected_indexes, [1]) # no exception eq_(self.rtable.selected_indexes, [1]) # no exception
def test_selectResultNodePaths(self, do_setup): def test_select_result_node_paths(self, do_setup):
app = self.app app = self.app
objects = self.objects objects = self.objects
self.rtable.select([1, 2]) self.rtable.select([1, 2])
@@ -220,7 +220,7 @@ class TestCaseDupeGuruWithResults:
assert app.selected_dupes[0] is objects[1] assert app.selected_dupes[0] is objects[1]
assert app.selected_dupes[1] is objects[2] assert app.selected_dupes[1] is objects[2]
def test_selectResultNodePaths_with_ref(self, do_setup): def test_select_result_node_paths_with_ref(self, do_setup):
app = self.app app = self.app
objects = self.objects objects = self.objects
self.rtable.select([1, 2, 3]) self.rtable.select([1, 2, 3])
@@ -229,7 +229,7 @@ class TestCaseDupeGuruWithResults:
assert app.selected_dupes[1] is objects[2] assert app.selected_dupes[1] is objects[2]
assert app.selected_dupes[2] is self.groups[1].ref assert app.selected_dupes[2] is self.groups[1].ref
def test_selectResultNodePaths_after_sort(self, do_setup): def test_select_result_node_paths_after_sort(self, do_setup):
app = self.app app = self.app
objects = self.objects objects = self.objects
groups = self.groups[:] # To keep the old order in memory groups = self.groups[:] # To keep the old order in memory
@@ -256,7 +256,7 @@ class TestCaseDupeGuruWithResults:
app.remove_selected() app.remove_selected()
eq_(self.rtable.selected_indexes, []) # no exception eq_(self.rtable.selected_indexes, []) # no exception
def test_selectPowerMarkerRows_after_sort(self, do_setup): def test_select_powermarker_rows_after_sort(self, do_setup):
app = self.app app = self.app
objects = self.objects objects = self.objects
self.rtable.power_marker = True self.rtable.power_marker = True
@@ -295,7 +295,7 @@ class TestCaseDupeGuruWithResults:
app.toggle_selected_mark_state() app.toggle_selected_mark_state()
eq_(app.results.mark_count, 0) eq_(app.results.mark_count, 0)
def test_refreshDetailsWithSelected(self, do_setup): def test_refresh_details_with_selected(self, do_setup):
self.rtable.select([1, 4]) self.rtable.select([1, 4])
eq_(self.dpanel.row(0), ("Filename", "bar bleh", "foo bar")) eq_(self.dpanel.row(0), ("Filename", "bar bleh", "foo bar"))
self.dpanel.view.check_gui_calls(["refresh"]) self.dpanel.view.check_gui_calls(["refresh"])
@@ -303,7 +303,7 @@ class TestCaseDupeGuruWithResults:
eq_(self.dpanel.row(0), ("Filename", "---", "---")) eq_(self.dpanel.row(0), ("Filename", "---", "---"))
self.dpanel.view.check_gui_calls(["refresh"]) self.dpanel.view.check_gui_calls(["refresh"])
def test_makeSelectedReference(self, do_setup): def test_make_selected_reference(self, do_setup):
app = self.app app = self.app
objects = self.objects objects = self.objects
groups = self.groups groups = self.groups
@@ -312,7 +312,7 @@ class TestCaseDupeGuruWithResults:
assert groups[0].ref is objects[1] assert groups[0].ref is objects[1]
assert groups[1].ref is objects[4] assert groups[1].ref is objects[4]
def test_makeSelectedReference_by_selecting_two_dupes_in_the_same_group(self, do_setup): def test_make_selected_reference_by_selecting_two_dupes_in_the_same_group(self, do_setup):
app = self.app app = self.app
objects = self.objects objects = self.objects
groups = self.groups groups = self.groups
@@ -322,7 +322,7 @@ class TestCaseDupeGuruWithResults:
assert groups[0].ref is objects[1] assert groups[0].ref is objects[1]
assert groups[1].ref is objects[4] assert groups[1].ref is objects[4]
def test_removeSelected(self, do_setup): def test_remove_selected(self, do_setup):
app = self.app app = self.app
self.rtable.select([1, 4]) self.rtable.select([1, 4])
app.remove_selected() app.remove_selected()
@@ -330,7 +330,7 @@ class TestCaseDupeGuruWithResults:
app.remove_selected() app.remove_selected()
eq_(len(app.results.dupes), 0) eq_(len(app.results.dupes), 0)
def test_addDirectory_simple(self, do_setup): def test_add_directory_simple(self, do_setup):
# There's already a directory in self.app, so adding another once makes 2 of em # There's already a directory in self.app, so adding another once makes 2 of em
app = self.app app = self.app
# any other path that isn't a parent or child of the already added path # any other path that isn't a parent or child of the already added path
@@ -338,7 +338,7 @@ class TestCaseDupeGuruWithResults:
app.add_directory(otherpath) app.add_directory(otherpath)
eq_(len(app.directories), 2) eq_(len(app.directories), 2)
def test_addDirectory_already_there(self, do_setup): def test_add_directory_already_there(self, do_setup):
app = self.app app = self.app
otherpath = Path(op.dirname(__file__)) otherpath = Path(op.dirname(__file__))
app.add_directory(otherpath) app.add_directory(otherpath)
@@ -346,7 +346,7 @@ class TestCaseDupeGuruWithResults:
eq_(len(app.view.messages), 1) eq_(len(app.view.messages), 1)
assert "already" in app.view.messages[0] assert "already" in app.view.messages[0]
def test_addDirectory_does_not_exist(self, do_setup): def test_add_directory_does_not_exist(self, do_setup):
app = self.app app = self.app
app.add_directory("/does_not_exist") app.add_directory("/does_not_exist")
eq_(len(app.view.messages), 1) eq_(len(app.view.messages), 1)
@@ -362,30 +362,30 @@ class TestCaseDupeGuruWithResults:
# BOTH the ref and the other dupe should have been added # BOTH the ref and the other dupe should have been added
eq_(len(app.ignore_list), 3) eq_(len(app.ignore_list), 3)
def test_purgeIgnoreList(self, do_setup, tmpdir): def test_purge_ignorelist(self, do_setup, tmpdir):
app = self.app app = self.app
p1 = str(tmpdir.join("file1")) p1 = str(tmpdir.join("file1"))
p2 = str(tmpdir.join("file2")) p2 = str(tmpdir.join("file2"))
open(p1, "w").close() open(p1, "w").close()
open(p2, "w").close() open(p2, "w").close()
dne = "/does_not_exist" dne = "/does_not_exist"
app.ignore_list.Ignore(dne, p1) app.ignore_list.ignore(dne, p1)
app.ignore_list.Ignore(p2, dne) app.ignore_list.ignore(p2, dne)
app.ignore_list.Ignore(p1, p2) app.ignore_list.ignore(p1, p2)
app.purge_ignore_list() app.purge_ignore_list()
eq_(1, len(app.ignore_list)) eq_(1, len(app.ignore_list))
assert app.ignore_list.AreIgnored(p1, p2) assert app.ignore_list.are_ignored(p1, p2)
assert not app.ignore_list.AreIgnored(dne, p1) assert not app.ignore_list.are_ignored(dne, p1)
def test_only_unicode_is_added_to_ignore_list(self, do_setup): def test_only_unicode_is_added_to_ignore_list(self, do_setup):
def FakeIgnore(first, second): def fake_ignore(first, second):
if not isinstance(first, str): if not isinstance(first, str):
self.fail() self.fail()
if not isinstance(second, str): if not isinstance(second, str):
self.fail() self.fail()
app = self.app app = self.app
app.ignore_list.Ignore = FakeIgnore app.ignore_list.ignore = fake_ignore
self.rtable.select([4]) self.rtable.select([4])
app.add_selected_to_ignore_list() app.add_selected_to_ignore_list()
@@ -419,7 +419,7 @@ class TestCaseDupeGuruWithResults:
# don't crash # don't crash
class TestCaseDupeGuru_renameSelected: class TestCaseDupeGuruRenameSelected:
@pytest.fixture @pytest.fixture
def do_setup(self, request): def do_setup(self, request):
tmpdir = request.getfixturevalue("tmpdir") tmpdir = request.getfixturevalue("tmpdir")
@@ -502,7 +502,6 @@ class TestAppWithDirectoriesInTree:
# refreshed. # refreshed.
node = self.dtree[0] node = self.dtree[0]
eq_(len(node), 3) # a len() call is required for subnodes to be loaded eq_(len(node), 3) # a len() call is required for subnodes to be loaded
subnode = node[0]
node.state = 1 # the state property is a state index node.state = 1 # the state property is a state index
node = self.dtree[0] node = self.dtree[0]
eq_(len(node), 3) eq_(len(node), 3)

View File

@@ -151,8 +151,8 @@ class TestApp(TestAppBase):
def __init__(self): def __init__(self):
def link_gui(gui): def link_gui(gui):
gui.view = self.make_logger() gui.view = self.make_logger()
if hasattr(gui, "columns"): # tables if hasattr(gui, "_columns"): # tables
gui.columns.view = self.make_logger() gui._columns.view = self.make_logger()
return gui return gui
TestAppBase.__init__(self) TestAppBase.__init__(self)

View File

@@ -73,99 +73,6 @@ class TestCasegetblock:
eq_((meanred, meangreen, meanblue), b) eq_((meanred, meangreen, meanblue), b)
# class TCdiff(unittest.TestCase):
# def test_diff(self):
# b1 = (10, 20, 30)
# b2 = (1, 2, 3)
# eq_(9 + 18 + 27, diff(b1, b2))
#
# def test_diff_negative(self):
# b1 = (10, 20, 30)
# b2 = (1, 2, 3)
# eq_(9 + 18 + 27, diff(b2, b1))
#
# def test_diff_mixed_positive_and_negative(self):
# b1 = (1, 5, 10)
# b2 = (10, 1, 15)
# eq_(9 + 4 + 5, diff(b1, b2))
#
# class TCgetblocks(unittest.TestCase):
# def test_empty_image(self):
# im = empty()
# blocks = getblocks(im, 1)
# eq_(0, len(blocks))
#
# def test_one_block_image(self):
# im = four_pixels()
# blocks = getblocks2(im, 1)
# eq_(1, len(blocks))
# block = blocks[0]
# meanred = (0xff + 0x80) // 4
# meangreen = (0x80 + 0x40) // 4
# meanblue = (0xff + 0x80) // 4
# eq_((meanred, meangreen, meanblue), block)
#
# def test_not_enough_height_to_fit_a_block(self):
# im = FakeImage((2, 1), [BLACK, BLACK])
# blocks = getblocks(im, 2)
# eq_(0, len(blocks))
#
# def xtest_dont_include_leftovers(self):
# # this test is disabled because getblocks is not used and getblock in cdeffed
# pixels = [
# RED,(0, 0x80, 0xff), BLACK,
# (0x80, 0, 0),(0, 0x40, 0x80), BLACK,
# BLACK, BLACK, BLACK
# ]
# im = FakeImage((3, 3), pixels)
# blocks = getblocks(im, 2)
# block = blocks[0]
# #Because the block is smaller than the image, only blocksize must be considered.
# meanred = (0xff + 0x80) // 4
# meangreen = (0x80 + 0x40) // 4
# meanblue = (0xff + 0x80) // 4
# eq_((meanred, meangreen, meanblue), block)
#
# def xtest_two_blocks(self):
# # this test is disabled because getblocks is not used and getblock in cdeffed
# pixels = [BLACK for i in xrange(4 * 2)]
# pixels[0] = RED
# pixels[1] = (0, 0x80, 0xff)
# pixels[4] = (0x80, 0, 0)
# pixels[5] = (0, 0x40, 0x80)
# im = FakeImage((4, 2), pixels)
# blocks = getblocks(im, 2)
# eq_(2, len(blocks))
# block = blocks[0]
# #Because the block is smaller than the image, only blocksize must be considered.
# meanred = (0xff + 0x80) // 4
# meangreen = (0x80 + 0x40) // 4
# meanblue = (0xff + 0x80) // 4
# eq_((meanred, meangreen, meanblue), block)
# eq_(BLACK, blocks[1])
#
# def test_four_blocks(self):
# pixels = [BLACK for i in xrange(4 * 4)]
# pixels[0] = RED
# pixels[1] = (0, 0x80, 0xff)
# pixels[4] = (0x80, 0, 0)
# pixels[5] = (0, 0x40, 0x80)
# im = FakeImage((4, 4), pixels)
# blocks = getblocks2(im, 2)
# eq_(4, len(blocks))
# block = blocks[0]
# #Because the block is smaller than the image, only blocksize must be considered.
# meanred = (0xff + 0x80) // 4
# meangreen = (0x80 + 0x40) // 4
# meanblue = (0xff + 0x80) // 4
# eq_((meanred, meangreen, meanblue), block)
# eq_(BLACK, blocks[1])
# eq_(BLACK, blocks[2])
# eq_(BLACK, blocks[3])
#
class TestCasegetblocks2: class TestCasegetblocks2:
def test_empty_image(self): def test_empty_image(self):
im = empty() im = empty()
@@ -270,8 +177,8 @@ class TestCaseavgdiff:
def test_return_at_least_1_at_the_slightest_difference(self): def test_return_at_least_1_at_the_slightest_difference(self):
ref = (0, 0, 0) ref = (0, 0, 0)
b1 = (1, 0, 0) b1 = (1, 0, 0)
blocks1 = [ref for i in range(250)] blocks1 = [ref for _ in range(250)]
blocks2 = [ref for i in range(250)] blocks2 = [ref for _ in range(250)]
blocks2[0] = b1 blocks2[0] = b1
eq_(1, my_avgdiff(blocks1, blocks2)) eq_(1, my_avgdiff(blocks1, blocks2))
@@ -280,41 +187,3 @@ class TestCaseavgdiff:
blocks1 = [ref, ref] blocks1 = [ref, ref]
blocks2 = [ref, ref] blocks2 = [ref, ref]
eq_(0, my_avgdiff(blocks1, blocks2)) eq_(0, my_avgdiff(blocks1, blocks2))
# class TCmaxdiff(unittest.TestCase):
# def test_empty(self):
# self.assertRaises(NoBlocksError, maxdiff,[],[])
#
# def test_two_blocks(self):
# b1 = (5, 10, 15)
# b2 = (255, 250, 245)
# b3 = (0, 0, 0)
# b4 = (255, 0, 255)
# blocks1 = [b1, b2]
# blocks2 = [b3, b4]
# expected1 = 5 + 10 + 15
# expected2 = 0 + 250 + 10
# expected = max(expected1, expected2)
# eq_(expected, maxdiff(blocks1, blocks2))
#
# def test_blocks_not_the_same_size(self):
# b = (0, 0, 0)
# self.assertRaises(DifferentBlockCountError, maxdiff,[b, b],[b])
#
# def test_first_arg_is_empty_but_not_second(self):
# #Don't return 0 (as when the 2 lists are empty), raise!
# b = (0, 0, 0)
# self.assertRaises(DifferentBlockCountError, maxdiff,[],[b])
#
# def test_limit(self):
# b1 = (5, 10, 15)
# b2 = (255, 250, 245)
# b3 = (0, 0, 0)
# b4 = (255, 0, 255)
# blocks1 = [b1, b2]
# blocks2 = [b3, b4]
# expected1 = 5 + 10 + 15
# expected2 = 0 + 250 + 10
# eq_(expected1, maxdiff(blocks1, blocks2, expected1 - 1))
#

View File

@@ -17,7 +17,7 @@ except ImportError:
skip("Can't import the cache module, probably hasn't been compiled.") skip("Can't import the cache module, probably hasn't been compiled.")
class TestCasecolors_to_string: class TestCaseColorsToString:
def test_no_color(self): def test_no_color(self):
eq_("", colors_to_string([])) eq_("", colors_to_string([]))
@@ -30,7 +30,7 @@ class TestCasecolors_to_string:
eq_("000102030405", colors_to_string([(0, 1, 2), (3, 4, 5)])) eq_("000102030405", colors_to_string([(0, 1, 2), (3, 4, 5)]))
class TestCasestring_to_colors: class TestCaseStringToColors:
def test_empty(self): def test_empty(self):
eq_([], string_to_colors("")) eq_([], string_to_colors(""))

View File

@@ -92,7 +92,7 @@ def test_add_path():
assert p in d assert p in d
def test_AddPath_when_path_is_already_there(): def test_add_path_when_path_is_already_there():
d = Directories() d = Directories()
p = testpath["onefile"] p = testpath["onefile"]
d.add_path(p) d.add_path(p)
@@ -112,7 +112,7 @@ def test_add_path_containing_paths_already_there():
eq_(d[0], testpath) eq_(d[0], testpath)
def test_AddPath_non_latin(tmpdir): def test_add_path_non_latin(tmpdir):
p = Path(str(tmpdir)) p = Path(str(tmpdir))
to_add = p["unicode\u201a"] to_add = p["unicode\u201a"]
os.mkdir(str(to_add)) os.mkdir(str(to_add))
@@ -140,20 +140,20 @@ def test_states():
d = Directories() d = Directories()
p = testpath["onefile"] p = testpath["onefile"]
d.add_path(p) d.add_path(p)
eq_(DirectoryState.Normal, d.get_state(p)) eq_(DirectoryState.NORMAL, d.get_state(p))
d.set_state(p, DirectoryState.Reference) d.set_state(p, DirectoryState.REFERENCE)
eq_(DirectoryState.Reference, d.get_state(p)) eq_(DirectoryState.REFERENCE, d.get_state(p))
eq_(DirectoryState.Reference, d.get_state(p["dir1"])) eq_(DirectoryState.REFERENCE, d.get_state(p["dir1"]))
eq_(1, len(d.states)) eq_(1, len(d.states))
eq_(p, list(d.states.keys())[0]) eq_(p, list(d.states.keys())[0])
eq_(DirectoryState.Reference, d.states[p]) eq_(DirectoryState.REFERENCE, d.states[p])
def test_get_state_with_path_not_there(): def test_get_state_with_path_not_there():
# When the path's not there, just return DirectoryState.Normal # When the path's not there, just return DirectoryState.Normal
d = Directories() d = Directories()
d.add_path(testpath["onefile"]) d.add_path(testpath["onefile"])
eq_(d.get_state(testpath), DirectoryState.Normal) eq_(d.get_state(testpath), DirectoryState.NORMAL)
def test_states_overwritten_when_larger_directory_eat_smaller_ones(): def test_states_overwritten_when_larger_directory_eat_smaller_ones():
@@ -162,20 +162,20 @@ def test_states_overwritten_when_larger_directory_eat_smaller_ones():
d = Directories() d = Directories()
p = testpath["onefile"] p = testpath["onefile"]
d.add_path(p) d.add_path(p)
d.set_state(p, DirectoryState.Excluded) d.set_state(p, DirectoryState.EXCLUDED)
d.add_path(testpath) d.add_path(testpath)
d.set_state(testpath, DirectoryState.Reference) d.set_state(testpath, DirectoryState.REFERENCE)
eq_(d.get_state(p), DirectoryState.Reference) eq_(d.get_state(p), DirectoryState.REFERENCE)
eq_(d.get_state(p["dir1"]), DirectoryState.Reference) eq_(d.get_state(p["dir1"]), DirectoryState.REFERENCE)
eq_(d.get_state(testpath), DirectoryState.Reference) eq_(d.get_state(testpath), DirectoryState.REFERENCE)
def test_get_files(): def test_get_files():
d = Directories() d = Directories()
p = testpath["fs"] p = testpath["fs"]
d.add_path(p) d.add_path(p)
d.set_state(p["dir1"], DirectoryState.Reference) d.set_state(p["dir1"], DirectoryState.REFERENCE)
d.set_state(p["dir2"], DirectoryState.Excluded) d.set_state(p["dir2"], DirectoryState.EXCLUDED)
files = list(d.get_files()) files = list(d.get_files())
eq_(5, len(files)) eq_(5, len(files))
for f in files: for f in files:
@@ -204,8 +204,8 @@ def test_get_folders():
d = Directories() d = Directories()
p = testpath["fs"] p = testpath["fs"]
d.add_path(p) d.add_path(p)
d.set_state(p["dir1"], DirectoryState.Reference) d.set_state(p["dir1"], DirectoryState.REFERENCE)
d.set_state(p["dir2"], DirectoryState.Excluded) d.set_state(p["dir2"], DirectoryState.EXCLUDED)
folders = list(d.get_folders()) folders = list(d.get_folders())
eq_(len(folders), 3) eq_(len(folders), 3)
ref = [f for f in folders if f.is_ref] ref = [f for f in folders if f.is_ref]
@@ -220,7 +220,7 @@ def test_get_files_with_inherited_exclusion():
d = Directories() d = Directories()
p = testpath["onefile"] p = testpath["onefile"]
d.add_path(p) d.add_path(p)
d.set_state(p, DirectoryState.Excluded) d.set_state(p, DirectoryState.EXCLUDED)
eq_([], list(d.get_files())) eq_([], list(d.get_files()))
@@ -233,14 +233,14 @@ def test_save_and_load(tmpdir):
p2.mkdir() p2.mkdir()
d1.add_path(p1) d1.add_path(p1)
d1.add_path(p2) d1.add_path(p2)
d1.set_state(p1, DirectoryState.Reference) d1.set_state(p1, DirectoryState.REFERENCE)
d1.set_state(p1["dir1"], DirectoryState.Excluded) d1.set_state(p1["dir1"], DirectoryState.EXCLUDED)
tmpxml = str(tmpdir.join("directories_testunit.xml")) tmpxml = str(tmpdir.join("directories_testunit.xml"))
d1.save_to_file(tmpxml) d1.save_to_file(tmpxml)
d2.load_from_file(tmpxml) d2.load_from_file(tmpxml)
eq_(2, len(d2)) eq_(2, len(d2))
eq_(DirectoryState.Reference, d2.get_state(p1)) eq_(DirectoryState.REFERENCE, d2.get_state(p1))
eq_(DirectoryState.Excluded, d2.get_state(p1["dir1"])) eq_(DirectoryState.EXCLUDED, d2.get_state(p1["dir1"]))
def test_invalid_path(): def test_invalid_path():
@@ -258,7 +258,7 @@ def test_set_state_on_invalid_path():
Path( Path(
"foobar", "foobar",
), ),
DirectoryState.Normal, DirectoryState.NORMAL,
) )
except LookupError: except LookupError:
assert False assert False
@@ -287,7 +287,7 @@ def test_unicode_save(tmpdir):
p1.mkdir() p1.mkdir()
p1["foo\xe9"].mkdir() p1["foo\xe9"].mkdir()
d.add_path(p1) d.add_path(p1)
d.set_state(p1["foo\xe9"], DirectoryState.Excluded) d.set_state(p1["foo\xe9"], DirectoryState.EXCLUDED)
tmpxml = str(tmpdir.join("directories_testunit.xml")) tmpxml = str(tmpdir.join("directories_testunit.xml"))
try: try:
d.save_to_file(tmpxml) d.save_to_file(tmpxml)
@@ -321,10 +321,10 @@ def test_get_state_returns_excluded_by_default_for_hidden_directories(tmpdir):
hidden_dir_path = p[".foo"] hidden_dir_path = p[".foo"]
p[".foo"].mkdir() p[".foo"].mkdir()
d.add_path(p) d.add_path(p)
eq_(d.get_state(hidden_dir_path), DirectoryState.Excluded) eq_(d.get_state(hidden_dir_path), DirectoryState.EXCLUDED)
# But it can be overriden # But it can be overriden
d.set_state(hidden_dir_path, DirectoryState.Normal) d.set_state(hidden_dir_path, DirectoryState.NORMAL)
eq_(d.get_state(hidden_dir_path), DirectoryState.Normal) eq_(d.get_state(hidden_dir_path), DirectoryState.NORMAL)
def test_default_path_state_override(tmpdir): def test_default_path_state_override(tmpdir):
@@ -332,7 +332,7 @@ def test_default_path_state_override(tmpdir):
class MyDirectories(Directories): class MyDirectories(Directories):
def _default_state_for_path(self, path): def _default_state_for_path(self, path):
if "foobar" in path: if "foobar" in path:
return DirectoryState.Excluded return DirectoryState.EXCLUDED
d = MyDirectories() d = MyDirectories()
p1 = Path(str(tmpdir)) p1 = Path(str(tmpdir))
@@ -341,12 +341,12 @@ def test_default_path_state_override(tmpdir):
p1["foobaz"].mkdir() p1["foobaz"].mkdir()
p1["foobaz/somefile"].open("w").close() p1["foobaz/somefile"].open("w").close()
d.add_path(p1) d.add_path(p1)
eq_(d.get_state(p1["foobaz"]), DirectoryState.Normal) eq_(d.get_state(p1["foobaz"]), DirectoryState.NORMAL)
eq_(d.get_state(p1["foobar"]), DirectoryState.Excluded) eq_(d.get_state(p1["foobar"]), DirectoryState.EXCLUDED)
eq_(len(list(d.get_files())), 1) # only the 'foobaz' file is there eq_(len(list(d.get_files())), 1) # only the 'foobaz' file is there
# However, the default state can be changed # However, the default state can be changed
d.set_state(p1["foobar"], DirectoryState.Normal) d.set_state(p1["foobar"], DirectoryState.NORMAL)
eq_(d.get_state(p1["foobar"]), DirectoryState.Normal) eq_(d.get_state(p1["foobar"]), DirectoryState.NORMAL)
eq_(len(list(d.get_files())), 2) eq_(len(list(d.get_files())), 2)
@@ -375,11 +375,11 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
p1["$Recycle.Bin"].mkdir() p1["$Recycle.Bin"].mkdir()
p1["$Recycle.Bin"]["subdir"].mkdir() p1["$Recycle.Bin"]["subdir"].mkdir()
self.d.add_path(p1) self.d.add_path(p1)
eq_(self.d.get_state(p1["$Recycle.Bin"]), DirectoryState.Excluded) eq_(self.d.get_state(p1["$Recycle.Bin"]), DirectoryState.EXCLUDED)
# By default, subdirs should be excluded too, but this can be overriden separately # By default, subdirs should be excluded too, but this can be overridden separately
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.Excluded) eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.EXCLUDED)
self.d.set_state(p1["$Recycle.Bin"]["subdir"], DirectoryState.Normal) self.d.set_state(p1["$Recycle.Bin"]["subdir"], DirectoryState.NORMAL)
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.Normal) eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.NORMAL)
def test_exclude_refined(self, tmpdir): def test_exclude_refined(self, tmpdir):
regex1 = r"^\$Recycle\.Bin$" regex1 = r"^\$Recycle\.Bin$"
@@ -398,16 +398,16 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
self.d.add_path(p1["$Recycle.Bin"]) self.d.add_path(p1["$Recycle.Bin"])
# Filter should set the default state to Excluded # Filter should set the default state to Excluded
eq_(self.d.get_state(p1["$Recycle.Bin"]), DirectoryState.Excluded) eq_(self.d.get_state(p1["$Recycle.Bin"]), DirectoryState.EXCLUDED)
# The subdir should inherit its parent state # The subdir should inherit its parent state
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.Excluded) eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.EXCLUDED)
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdar"]), DirectoryState.Excluded) eq_(self.d.get_state(p1["$Recycle.Bin"]["subdar"]), DirectoryState.EXCLUDED)
# Override a child path's state # Override a child path's state
self.d.set_state(p1["$Recycle.Bin"]["subdir"], DirectoryState.Normal) self.d.set_state(p1["$Recycle.Bin"]["subdir"], DirectoryState.NORMAL)
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.Normal) eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.NORMAL)
# Parent should keep its default state, and the other child too # Parent should keep its default state, and the other child too
eq_(self.d.get_state(p1["$Recycle.Bin"]), DirectoryState.Excluded) eq_(self.d.get_state(p1["$Recycle.Bin"]), DirectoryState.EXCLUDED)
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdar"]), DirectoryState.Excluded) eq_(self.d.get_state(p1["$Recycle.Bin"]["subdar"]), DirectoryState.EXCLUDED)
# print(f"get_folders(): {[x for x in self.d.get_folders()]}") # print(f"get_folders(): {[x for x in self.d.get_folders()]}")
# only the 2 files directly under the Normal directory # only the 2 files directly under the Normal directory
@@ -419,8 +419,8 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
assert "somesubdirfile.png" in files assert "somesubdirfile.png" in files
assert "unwanted_subdirfile.gif" in files assert "unwanted_subdirfile.gif" in files
# Overriding the parent should enable all children # Overriding the parent should enable all children
self.d.set_state(p1["$Recycle.Bin"], DirectoryState.Normal) self.d.set_state(p1["$Recycle.Bin"], DirectoryState.NORMAL)
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdar"]), DirectoryState.Normal) eq_(self.d.get_state(p1["$Recycle.Bin"]["subdar"]), DirectoryState.NORMAL)
# all files there # all files there
files = self.get_files_and_expect_num_result(6) files = self.get_files_and_expect_num_result(6)
assert "somefile.png" in files assert "somefile.png" in files
@@ -444,7 +444,7 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
assert self.d._exclude_list.error(regex3) is None assert self.d._exclude_list.error(regex3) is None
# print(f"get_folders(): {[x for x in self.d.get_folders()]}") # print(f"get_folders(): {[x for x in self.d.get_folders()]}")
# Directory shouldn't change its state here, unless explicitely done by user # Directory shouldn't change its state here, unless explicitely done by user
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.Normal) eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.NORMAL)
files = self.get_files_and_expect_num_result(5) files = self.get_files_and_expect_num_result(5)
assert "unwanted_subdirfile.gif" not in files assert "unwanted_subdirfile.gif" not in files
assert "unwanted_subdarfile.png" in files assert "unwanted_subdarfile.png" in files
@@ -454,14 +454,14 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
self.d._exclude_list.rename(regex3, regex4) self.d._exclude_list.rename(regex3, regex4)
assert self.d._exclude_list.error(regex4) is None assert self.d._exclude_list.error(regex4) is None
p1["$Recycle.Bin"]["subdar"]["file_ending_with_subdir"].open("w").close() p1["$Recycle.Bin"]["subdar"]["file_ending_with_subdir"].open("w").close()
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.Excluded) eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.EXCLUDED)
files = self.get_files_and_expect_num_result(4) files = self.get_files_and_expect_num_result(4)
assert "file_ending_with_subdir" not in files assert "file_ending_with_subdir" not in files
assert "somesubdarfile.jpeg" in files assert "somesubdarfile.jpeg" in files
assert "somesubdirfile.png" not in files assert "somesubdirfile.png" not in files
assert "unwanted_subdirfile.gif" not in files assert "unwanted_subdirfile.gif" not in files
self.d.set_state(p1["$Recycle.Bin"]["subdir"], DirectoryState.Normal) self.d.set_state(p1["$Recycle.Bin"]["subdir"], DirectoryState.NORMAL)
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.Normal) eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.NORMAL)
# print(f"get_folders(): {[x for x in self.d.get_folders()]}") # print(f"get_folders(): {[x for x in self.d.get_folders()]}")
files = self.get_files_and_expect_num_result(6) files = self.get_files_and_expect_num_result(6)
assert "file_ending_with_subdir" not in files assert "file_ending_with_subdir" not in files
@@ -471,7 +471,7 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
regex5 = r".*subdir.*" regex5 = r".*subdir.*"
self.d._exclude_list.rename(regex4, regex5) self.d._exclude_list.rename(regex4, regex5)
# Files containing substring should be filtered # Files containing substring should be filtered
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.Normal) eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.NORMAL)
# The path should not match, only the filename, the "subdir" in the directory name shouldn't matter # The path should not match, only the filename, the "subdir" in the directory name shouldn't matter
p1["$Recycle.Bin"]["subdir"]["file_which_shouldnt_match"].open("w").close() p1["$Recycle.Bin"]["subdir"]["file_which_shouldnt_match"].open("w").close()
files = self.get_files_and_expect_num_result(5) files = self.get_files_and_expect_num_result(5)
@@ -493,7 +493,7 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
assert self.d._exclude_list.error(regex6) is None assert self.d._exclude_list.error(regex6) is None
assert regex6 in self.d._exclude_list assert regex6 in self.d._exclude_list
# This still should not be affected # This still should not be affected
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.Normal) eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.NORMAL)
files = self.get_files_and_expect_num_result(5) files = self.get_files_and_expect_num_result(5)
# These files are under the "/subdir" directory # These files are under the "/subdir" directory
assert "somesubdirfile.png" not in files assert "somesubdirfile.png" not in files
@@ -518,7 +518,7 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
self.d._exclude_list.add(regex3) self.d._exclude_list.add(regex3)
self.d._exclude_list.mark(regex3) self.d._exclude_list.mark(regex3)
# print(f"get_folders(): {[x for x in self.d.get_folders()]}") # print(f"get_folders(): {[x for x in self.d.get_folders()]}")
eq_(self.d.get_state(p1["$Recycle.Bin"]["思叫物語"]), DirectoryState.Excluded) eq_(self.d.get_state(p1["$Recycle.Bin"]["思叫物語"]), DirectoryState.EXCLUDED)
files = self.get_files_and_expect_num_result(2) files = self.get_files_and_expect_num_result(2)
assert "過去白濁物語~]_カラー.jpg" not in files assert "過去白濁物語~]_カラー.jpg" not in files
assert "なししろ会う前" not in files assert "なししろ会う前" not in files
@@ -527,7 +527,7 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
regex4 = r".*物語$" regex4 = r".*物語$"
self.d._exclude_list.rename(regex3, regex4) self.d._exclude_list.rename(regex3, regex4)
assert self.d._exclude_list.error(regex4) is None assert self.d._exclude_list.error(regex4) is None
self.d.set_state(p1["$Recycle.Bin"]["思叫物語"], DirectoryState.Normal) self.d.set_state(p1["$Recycle.Bin"]["思叫物語"], DirectoryState.NORMAL)
files = self.get_files_and_expect_num_result(5) files = self.get_files_and_expect_num_result(5)
assert "過去白濁物語~]_カラー.jpg" in files assert "過去白濁物語~]_カラー.jpg" in files
assert "なししろ会う前" in files assert "なししろ会う前" in files
@@ -546,8 +546,8 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
p1["foobar"][".hidden_dir"][".hidden_subfile.png"].open("w").close() p1["foobar"][".hidden_dir"][".hidden_subfile.png"].open("w").close()
self.d.add_path(p1["foobar"]) self.d.add_path(p1["foobar"])
# It should not inherit its parent's state originally # It should not inherit its parent's state originally
eq_(self.d.get_state(p1["foobar"][".hidden_dir"]), DirectoryState.Excluded) eq_(self.d.get_state(p1["foobar"][".hidden_dir"]), DirectoryState.EXCLUDED)
self.d.set_state(p1["foobar"][".hidden_dir"], DirectoryState.Normal) self.d.set_state(p1["foobar"][".hidden_dir"], DirectoryState.NORMAL)
# The files should still be filtered # The files should still be filtered
files = self.get_files_and_expect_num_result(1) files = self.get_files_and_expect_num_result(1)
eq_(len(self.d._exclude_list.compiled_paths), 0) eq_(len(self.d._exclude_list.compiled_paths), 0)

View File

@@ -103,10 +103,9 @@ class TestCasegetfields:
expected = [["a", "bc", "def"]] expected = [["a", "bc", "def"]]
actual = getfields(" - a bc def") actual = getfields(" - a bc def")
eq_(expected, actual) eq_(expected, actual)
expected = [["bc", "def"]]
class TestCaseunpack_fields: class TestCaseUnpackFields:
def test_with_fields(self): def test_with_fields(self):
expected = ["a", "b", "c", "d", "e", "f"] expected = ["a", "b", "c", "d", "e", "f"]
actual = unpack_fields([["a"], ["b", "c"], ["d", "e", "f"]]) actual = unpack_fields([["a"], ["b", "c"], ["d", "e", "f"]])
@@ -218,24 +217,24 @@ class TestCaseWordCompareWithFields:
eq_([["c", "d", "f"], ["a", "b"]], second) eq_([["c", "d", "f"], ["a", "b"]], second)
class TestCasebuild_word_dict: class TestCaseBuildWordDict:
def test_with_standard_words(self): def test_with_standard_words(self):
itemList = [NamedObject("foo bar", True)] item_list = [NamedObject("foo bar", True)]
itemList.append(NamedObject("bar baz", True)) item_list.append(NamedObject("bar baz", True))
itemList.append(NamedObject("baz bleh foo", True)) item_list.append(NamedObject("baz bleh foo", True))
d = build_word_dict(itemList) d = build_word_dict(item_list)
eq_(4, len(d)) eq_(4, len(d))
eq_(2, len(d["foo"])) eq_(2, len(d["foo"]))
assert itemList[0] in d["foo"] assert item_list[0] in d["foo"]
assert itemList[2] in d["foo"] assert item_list[2] in d["foo"]
eq_(2, len(d["bar"])) eq_(2, len(d["bar"]))
assert itemList[0] in d["bar"] assert item_list[0] in d["bar"]
assert itemList[1] in d["bar"] assert item_list[1] in d["bar"]
eq_(2, len(d["baz"])) eq_(2, len(d["baz"]))
assert itemList[1] in d["baz"] assert item_list[1] in d["baz"]
assert itemList[2] in d["baz"] assert item_list[2] in d["baz"]
eq_(1, len(d["bleh"])) eq_(1, len(d["bleh"]))
assert itemList[2] in d["bleh"] assert item_list[2] in d["bleh"]
def test_unpack_fields(self): def test_unpack_fields(self):
o = NamedObject("") o = NamedObject("")
@@ -269,7 +268,7 @@ class TestCasebuild_word_dict:
eq_(100, self.log[1]) eq_(100, self.log[1])
class TestCasemerge_similar_words: class TestCaseMergeSimilarWords:
def test_some_similar_words(self): def test_some_similar_words(self):
d = { d = {
"foobar": set([1]), "foobar": set([1]),
@@ -281,11 +280,11 @@ class TestCasemerge_similar_words:
eq_(3, len(d["foobar"])) eq_(3, len(d["foobar"]))
class TestCasereduce_common_words: class TestCaseReduceCommonWords:
def test_typical(self): def test_typical(self):
d = { d = {
"foo": set([NamedObject("foo bar", True) for i in range(50)]), "foo": set([NamedObject("foo bar", True) for _ in range(50)]),
"bar": set([NamedObject("foo bar", True) for i in range(49)]), "bar": set([NamedObject("foo bar", True) for _ in range(49)]),
} }
reduce_common_words(d, 50) reduce_common_words(d, 50)
assert "foo" not in d assert "foo" not in d
@@ -293,7 +292,7 @@ class TestCasereduce_common_words:
def test_dont_remove_objects_with_only_common_words(self): def test_dont_remove_objects_with_only_common_words(self):
d = { d = {
"common": set([NamedObject("common uncommon", True) for i in range(50)] + [NamedObject("common", True)]), "common": set([NamedObject("common uncommon", True) for _ in range(50)] + [NamedObject("common", True)]),
"uncommon": set([NamedObject("common uncommon", True)]), "uncommon": set([NamedObject("common uncommon", True)]),
} }
reduce_common_words(d, 50) reduce_common_words(d, 50)
@@ -302,20 +301,20 @@ class TestCasereduce_common_words:
def test_values_still_are_set_instances(self): def test_values_still_are_set_instances(self):
d = { d = {
"common": set([NamedObject("common uncommon", True) for i in range(50)] + [NamedObject("common", True)]), "common": set([NamedObject("common uncommon", True) for _ in range(50)] + [NamedObject("common", True)]),
"uncommon": set([NamedObject("common uncommon", True)]), "uncommon": set([NamedObject("common uncommon", True)]),
} }
reduce_common_words(d, 50) reduce_common_words(d, 50)
assert isinstance(d["common"], set) assert isinstance(d["common"], set)
assert isinstance(d["uncommon"], set) assert isinstance(d["uncommon"], set)
def test_dont_raise_KeyError_when_a_word_has_been_removed(self): def test_dont_raise_keyerror_when_a_word_has_been_removed(self):
# If a word has been removed by the reduce, an object in a subsequent common word that # If a word has been removed by the reduce, an object in a subsequent common word that
# contains the word that has been removed would cause a KeyError. # contains the word that has been removed would cause a KeyError.
d = { d = {
"foo": set([NamedObject("foo bar baz", True) for i in range(50)]), "foo": set([NamedObject("foo bar baz", True) for _ in range(50)]),
"bar": set([NamedObject("foo bar baz", True) for i in range(50)]), "bar": set([NamedObject("foo bar baz", True) for _ in range(50)]),
"baz": set([NamedObject("foo bar baz", True) for i in range(49)]), "baz": set([NamedObject("foo bar baz", True) for _ in range(49)]),
} }
try: try:
reduce_common_words(d, 50) reduce_common_words(d, 50)
@@ -329,7 +328,7 @@ class TestCasereduce_common_words:
o.words = [["foo", "bar"], ["baz"]] o.words = [["foo", "bar"], ["baz"]]
return o return o
d = {"foo": set([create_it() for i in range(50)])} d = {"foo": set([create_it() for _ in range(50)])}
try: try:
reduce_common_words(d, 50) reduce_common_words(d, 50)
except TypeError: except TypeError:
@@ -342,9 +341,9 @@ class TestCasereduce_common_words:
# would not stay in 'bar' because 'foo' is not a common word anymore. # would not stay in 'bar' because 'foo' is not a common word anymore.
only_common = NamedObject("foo bar", True) only_common = NamedObject("foo bar", True)
d = { d = {
"foo": set([NamedObject("foo bar baz", True) for i in range(49)] + [only_common]), "foo": set([NamedObject("foo bar baz", True) for _ in range(49)] + [only_common]),
"bar": set([NamedObject("foo bar baz", True) for i in range(49)] + [only_common]), "bar": set([NamedObject("foo bar baz", True) for _ in range(49)] + [only_common]),
"baz": set([NamedObject("foo bar baz", True) for i in range(49)]), "baz": set([NamedObject("foo bar baz", True) for _ in range(49)]),
} }
reduce_common_words(d, 50) reduce_common_words(d, 50)
eq_(1, len(d["foo"])) eq_(1, len(d["foo"]))
@@ -352,7 +351,7 @@ class TestCasereduce_common_words:
eq_(49, len(d["baz"])) eq_(49, len(d["baz"]))
class TestCaseget_match: class TestCaseGetMatch:
def test_simple(self): def test_simple(self):
o1 = NamedObject("foo bar", True) o1 = NamedObject("foo bar", True)
o2 = NamedObject("bar bleh", True) o2 = NamedObject("bar bleh", True)
@@ -381,12 +380,12 @@ class TestCaseGetMatches:
eq_(getmatches([]), []) eq_(getmatches([]), [])
def test_simple(self): def test_simple(self):
itemList = [ item_list = [
NamedObject("foo bar"), NamedObject("foo bar"),
NamedObject("bar bleh"), NamedObject("bar bleh"),
NamedObject("a b c foo"), NamedObject("a b c foo"),
] ]
r = getmatches(itemList) r = getmatches(item_list)
eq_(2, len(r)) eq_(2, len(r))
m = first(m for m in r if m.percentage == 50) # "foo bar" and "bar bleh" m = first(m for m in r if m.percentage == 50) # "foo bar" and "bar bleh"
assert_match(m, "foo bar", "bar bleh") assert_match(m, "foo bar", "bar bleh")
@@ -394,40 +393,40 @@ class TestCaseGetMatches:
assert_match(m, "foo bar", "a b c foo") assert_match(m, "foo bar", "a b c foo")
def test_null_and_unrelated_objects(self): def test_null_and_unrelated_objects(self):
itemList = [ item_list = [
NamedObject("foo bar"), NamedObject("foo bar"),
NamedObject("bar bleh"), NamedObject("bar bleh"),
NamedObject(""), NamedObject(""),
NamedObject("unrelated object"), NamedObject("unrelated object"),
] ]
r = getmatches(itemList) r = getmatches(item_list)
eq_(len(r), 1) eq_(len(r), 1)
m = r[0] m = r[0]
eq_(m.percentage, 50) eq_(m.percentage, 50)
assert_match(m, "foo bar", "bar bleh") assert_match(m, "foo bar", "bar bleh")
def test_twice_the_same_word(self): def test_twice_the_same_word(self):
itemList = [NamedObject("foo foo bar"), NamedObject("bar bleh")] item_list = [NamedObject("foo foo bar"), NamedObject("bar bleh")]
r = getmatches(itemList) r = getmatches(item_list)
eq_(1, len(r)) eq_(1, len(r))
def test_twice_the_same_word_when_preworded(self): def test_twice_the_same_word_when_preworded(self):
itemList = [NamedObject("foo foo bar", True), NamedObject("bar bleh", True)] item_list = [NamedObject("foo foo bar", True), NamedObject("bar bleh", True)]
r = getmatches(itemList) r = getmatches(item_list)
eq_(1, len(r)) eq_(1, len(r))
def test_two_words_match(self): def test_two_words_match(self):
itemList = [NamedObject("foo bar"), NamedObject("foo bar bleh")] item_list = [NamedObject("foo bar"), NamedObject("foo bar bleh")]
r = getmatches(itemList) r = getmatches(item_list)
eq_(1, len(r)) eq_(1, len(r))
def test_match_files_with_only_common_words(self): def test_match_files_with_only_common_words(self):
# If a word occurs more than 50 times, it is excluded from the matching process # If a word occurs more than 50 times, it is excluded from the matching process
# The problem with the common_word_threshold is that the files containing only common # The problem with the common_word_threshold is that the files containing only common
# words will never be matched together. We *should* match them. # words will never be matched together. We *should* match them.
# This test assumes that the common word threashold const is 50 # This test assumes that the common word threshold const is 50
itemList = [NamedObject("foo") for i in range(50)] item_list = [NamedObject("foo") for _ in range(50)]
r = getmatches(itemList) r = getmatches(item_list)
eq_(1225, len(r)) eq_(1225, len(r))
def test_use_words_already_there_if_there(self): def test_use_words_already_there_if_there(self):
@@ -450,28 +449,28 @@ class TestCaseGetMatches:
eq_(100, self.log[-1]) eq_(100, self.log[-1])
def test_weight_words(self): def test_weight_words(self):
itemList = [NamedObject("foo bar"), NamedObject("bar bleh")] item_list = [NamedObject("foo bar"), NamedObject("bar bleh")]
m = getmatches(itemList, weight_words=True)[0] m = getmatches(item_list, weight_words=True)[0]
eq_(int((6.0 / 13.0) * 100), m.percentage) eq_(int((6.0 / 13.0) * 100), m.percentage)
def test_similar_word(self): def test_similar_word(self):
itemList = [NamedObject("foobar"), NamedObject("foobars")] item_list = [NamedObject("foobar"), NamedObject("foobars")]
eq_(len(getmatches(itemList, match_similar_words=True)), 1) eq_(len(getmatches(item_list, match_similar_words=True)), 1)
eq_(getmatches(itemList, match_similar_words=True)[0].percentage, 100) eq_(getmatches(item_list, match_similar_words=True)[0].percentage, 100)
itemList = [NamedObject("foobar"), NamedObject("foo")] item_list = [NamedObject("foobar"), NamedObject("foo")]
eq_(len(getmatches(itemList, match_similar_words=True)), 0) # too far eq_(len(getmatches(item_list, match_similar_words=True)), 0) # too far
itemList = [NamedObject("bizkit"), NamedObject("bizket")] item_list = [NamedObject("bizkit"), NamedObject("bizket")]
eq_(len(getmatches(itemList, match_similar_words=True)), 1) eq_(len(getmatches(item_list, match_similar_words=True)), 1)
itemList = [NamedObject("foobar"), NamedObject("foosbar")] item_list = [NamedObject("foobar"), NamedObject("foosbar")]
eq_(len(getmatches(itemList, match_similar_words=True)), 1) eq_(len(getmatches(item_list, match_similar_words=True)), 1)
def test_single_object_with_similar_words(self): def test_single_object_with_similar_words(self):
itemList = [NamedObject("foo foos")] item_list = [NamedObject("foo foos")]
eq_(len(getmatches(itemList, match_similar_words=True)), 0) eq_(len(getmatches(item_list, match_similar_words=True)), 0)
def test_double_words_get_counted_only_once(self): def test_double_words_get_counted_only_once(self):
itemList = [NamedObject("foo bar foo bleh"), NamedObject("foo bar bleh bar")] item_list = [NamedObject("foo bar foo bleh"), NamedObject("foo bar bleh bar")]
m = getmatches(itemList)[0] m = getmatches(item_list)[0]
eq_(75, m.percentage) eq_(75, m.percentage)
def test_with_fields(self): def test_with_fields(self):
@@ -491,13 +490,13 @@ class TestCaseGetMatches:
eq_(m.percentage, 50) eq_(m.percentage, 50)
def test_only_match_similar_when_the_option_is_set(self): def test_only_match_similar_when_the_option_is_set(self):
itemList = [NamedObject("foobar"), NamedObject("foobars")] item_list = [NamedObject("foobar"), NamedObject("foobars")]
eq_(len(getmatches(itemList, match_similar_words=False)), 0) eq_(len(getmatches(item_list, match_similar_words=False)), 0)
def test_dont_recurse_do_match(self): def test_dont_recurse_do_match(self):
# with nosetests, the stack is increased. The number has to be high enough not to be failing falsely # with nosetests, the stack is increased. The number has to be high enough not to be failing falsely
sys.setrecursionlimit(200) sys.setrecursionlimit(200)
files = [NamedObject("foo bar") for i in range(201)] files = [NamedObject("foo bar") for _ in range(201)]
try: try:
getmatches(files) getmatches(files)
except RuntimeError: except RuntimeError:
@@ -506,35 +505,31 @@ class TestCaseGetMatches:
sys.setrecursionlimit(1000) sys.setrecursionlimit(1000)
def test_min_match_percentage(self): def test_min_match_percentage(self):
itemList = [ item_list = [
NamedObject("foo bar"), NamedObject("foo bar"),
NamedObject("bar bleh"), NamedObject("bar bleh"),
NamedObject("a b c foo"), NamedObject("a b c foo"),
] ]
r = getmatches(itemList, min_match_percentage=50) r = getmatches(item_list, min_match_percentage=50)
eq_(1, len(r)) # Only "foo bar" / "bar bleh" should match eq_(1, len(r)) # Only "foo bar" / "bar bleh" should match
def test_MemoryError(self, monkeypatch): def test_memory_error(self, monkeypatch):
@log_calls @log_calls
def mocked_match(first, second, flags): def mocked_match(first, second, flags):
if len(mocked_match.calls) > 42: if len(mocked_match.calls) > 42:
raise MemoryError() raise MemoryError()
return Match(first, second, 0) return Match(first, second, 0)
objects = [NamedObject() for i in range(10)] # results in 45 matches objects = [NamedObject() for _ in range(10)] # results in 45 matches
monkeypatch.setattr(engine, "get_match", mocked_match) monkeypatch.setattr(engine, "get_match", mocked_match)
try: try:
r = getmatches(objects) r = getmatches(objects)
except MemoryError: except MemoryError:
self.fail("MemorryError must be handled") self.fail("MemoryError must be handled")
eq_(42, len(r)) eq_(42, len(r))
class TestCaseGetMatchesByContents: class TestCaseGetMatchesByContents:
def test_dont_compare_empty_files(self):
o1, o2 = no(size=0), no(size=0)
assert not getmatches_by_contents([o1, o2])
def test_big_file_partial_hashes(self): def test_big_file_partial_hashes(self):
smallsize = 1 smallsize = 1
bigsize = 100 * 1024 * 1024 # 100MB bigsize = 100 * 1024 * 1024 # 100MB
@@ -563,7 +558,7 @@ class TestCaseGetMatchesByContents:
class TestCaseGroup: class TestCaseGroup:
def test_empy(self): def test_empty(self):
g = Group() g = Group()
eq_(None, g.ref) eq_(None, g.ref)
eq_([], g.dupes) eq_([], g.dupes)
@@ -802,14 +797,14 @@ class TestCaseGroup:
eq_(0, len(g.candidates)) eq_(0, len(g.candidates))
class TestCaseget_groups: class TestCaseGetGroups:
def test_empty(self): def test_empty(self):
r = get_groups([]) r = get_groups([])
eq_([], r) eq_([], r)
def test_simple(self): def test_simple(self):
itemList = [NamedObject("foo bar"), NamedObject("bar bleh")] item_list = [NamedObject("foo bar"), NamedObject("bar bleh")]
matches = getmatches(itemList) matches = getmatches(item_list)
m = matches[0] m = matches[0]
r = get_groups(matches) r = get_groups(matches)
eq_(1, len(r)) eq_(1, len(r))
@@ -819,15 +814,15 @@ class TestCaseget_groups:
def test_group_with_multiple_matches(self): def test_group_with_multiple_matches(self):
# This results in 3 matches # This results in 3 matches
itemList = [NamedObject("foo"), NamedObject("foo"), NamedObject("foo")] item_list = [NamedObject("foo"), NamedObject("foo"), NamedObject("foo")]
matches = getmatches(itemList) matches = getmatches(item_list)
r = get_groups(matches) r = get_groups(matches)
eq_(1, len(r)) eq_(1, len(r))
g = r[0] g = r[0]
eq_(3, len(g)) eq_(3, len(g))
def test_must_choose_a_group(self): def test_must_choose_a_group(self):
itemList = [ item_list = [
NamedObject("a b"), NamedObject("a b"),
NamedObject("a b"), NamedObject("a b"),
NamedObject("b c"), NamedObject("b c"),
@@ -836,13 +831,13 @@ class TestCaseget_groups:
] ]
# There will be 2 groups here: group "a b" and group "c d" # There will be 2 groups here: group "a b" and group "c d"
# "b c" can go either of them, but not both. # "b c" can go either of them, but not both.
matches = getmatches(itemList) matches = getmatches(item_list)
r = get_groups(matches) r = get_groups(matches)
eq_(2, len(r)) eq_(2, len(r))
eq_(5, len(r[0]) + len(r[1])) eq_(5, len(r[0]) + len(r[1]))
def test_should_all_go_in_the_same_group(self): def test_should_all_go_in_the_same_group(self):
itemList = [ item_list = [
NamedObject("a b"), NamedObject("a b"),
NamedObject("a b"), NamedObject("a b"),
NamedObject("a b"), NamedObject("a b"),
@@ -850,7 +845,7 @@ class TestCaseget_groups:
] ]
# There will be 2 groups here: group "a b" and group "c d" # There will be 2 groups here: group "a b" and group "c d"
# "b c" can fit in both, but it must be in only one of them # "b c" can fit in both, but it must be in only one of them
matches = getmatches(itemList) matches = getmatches(item_list)
r = get_groups(matches) r = get_groups(matches)
eq_(1, len(r)) eq_(1, len(r))
@@ -869,8 +864,8 @@ class TestCaseget_groups:
assert o3 in g assert o3 in g
def test_four_sized_group(self): def test_four_sized_group(self):
itemList = [NamedObject("foobar") for i in range(4)] item_list = [NamedObject("foobar") for _ in range(4)]
m = getmatches(itemList) m = getmatches(item_list)
r = get_groups(m) r = get_groups(m)
eq_(1, len(r)) eq_(1, len(r))
eq_(4, len(r[0])) eq_(4, len(r[0]))

View File

@@ -5,12 +5,8 @@
# http://www.gnu.org/licenses/gpl-3.0.html # http://www.gnu.org/licenses/gpl-3.0.html
import io import io
# import os.path as op
from xml.etree import ElementTree as ET from xml.etree import ElementTree as ET
# from pytest import raises
from hscommon.testutil import eq_ from hscommon.testutil import eq_
from hscommon.plat import ISWINDOWS from hscommon.plat import ISWINDOWS
@@ -144,11 +140,7 @@ class TestCaseListEmpty:
def test_force_add_not_compilable(self): def test_force_add_not_compilable(self):
"""Used when loading from XML for example""" """Used when loading from XML for example"""
regex = r"one))" regex = r"one))"
try: self.exclude_list.add(regex, forced=True)
self.exclude_list.add(regex, forced=True)
except Exception as e:
# Should not get an exception here unless it's a duplicate regex
raise e
marked = self.exclude_list.mark(regex) marked = self.exclude_list.mark(regex)
eq_(marked, False) # can't be marked since not compilable eq_(marked, False) # can't be marked since not compilable
eq_(len(self.exclude_list), 1) eq_(len(self.exclude_list), 1)
@@ -232,7 +224,6 @@ class TestCaseListEmpty:
found = True found = True
if not found: if not found:
raise (Exception(f"Default RE {re} not found in compiled list.")) raise (Exception(f"Default RE {re} not found in compiled list."))
continue
eq_(len(default_regexes), len(self.exclude_list.compiled)) eq_(len(default_regexes), len(self.exclude_list.compiled))

View File

@@ -16,54 +16,54 @@ from ..ignore import IgnoreList
def test_empty(): def test_empty():
il = IgnoreList() il = IgnoreList()
eq_(0, len(il)) eq_(0, len(il))
assert not il.AreIgnored("foo", "bar") assert not il.are_ignored("foo", "bar")
def test_simple(): def test_simple():
il = IgnoreList() il = IgnoreList()
il.Ignore("foo", "bar") il.ignore("foo", "bar")
assert il.AreIgnored("foo", "bar") assert il.are_ignored("foo", "bar")
assert il.AreIgnored("bar", "foo") assert il.are_ignored("bar", "foo")
assert not il.AreIgnored("foo", "bleh") assert not il.are_ignored("foo", "bleh")
assert not il.AreIgnored("bleh", "bar") assert not il.are_ignored("bleh", "bar")
eq_(1, len(il)) eq_(1, len(il))
def test_multiple(): def test_multiple():
il = IgnoreList() il = IgnoreList()
il.Ignore("foo", "bar") il.ignore("foo", "bar")
il.Ignore("foo", "bleh") il.ignore("foo", "bleh")
il.Ignore("bleh", "bar") il.ignore("bleh", "bar")
il.Ignore("aybabtu", "bleh") il.ignore("aybabtu", "bleh")
assert il.AreIgnored("foo", "bar") assert il.are_ignored("foo", "bar")
assert il.AreIgnored("bar", "foo") assert il.are_ignored("bar", "foo")
assert il.AreIgnored("foo", "bleh") assert il.are_ignored("foo", "bleh")
assert il.AreIgnored("bleh", "bar") assert il.are_ignored("bleh", "bar")
assert not il.AreIgnored("aybabtu", "bar") assert not il.are_ignored("aybabtu", "bar")
eq_(4, len(il)) eq_(4, len(il))
def test_clear(): def test_clear():
il = IgnoreList() il = IgnoreList()
il.Ignore("foo", "bar") il.ignore("foo", "bar")
il.Clear() il.clear()
assert not il.AreIgnored("foo", "bar") assert not il.are_ignored("foo", "bar")
assert not il.AreIgnored("bar", "foo") assert not il.are_ignored("bar", "foo")
eq_(0, len(il)) eq_(0, len(il))
def test_add_same_twice(): def test_add_same_twice():
il = IgnoreList() il = IgnoreList()
il.Ignore("foo", "bar") il.ignore("foo", "bar")
il.Ignore("bar", "foo") il.ignore("bar", "foo")
eq_(1, len(il)) eq_(1, len(il))
def test_save_to_xml(): def test_save_to_xml():
il = IgnoreList() il = IgnoreList()
il.Ignore("foo", "bar") il.ignore("foo", "bar")
il.Ignore("foo", "bleh") il.ignore("foo", "bleh")
il.Ignore("bleh", "bar") il.ignore("bleh", "bar")
f = io.BytesIO() f = io.BytesIO()
il.save_to_xml(f) il.save_to_xml(f)
f.seek(0) f.seek(0)
@@ -77,22 +77,22 @@ def test_save_to_xml():
eq_(len(subchildren), 3) eq_(len(subchildren), 3)
def test_SaveThenLoad(): def test_save_then_load():
il = IgnoreList() il = IgnoreList()
il.Ignore("foo", "bar") il.ignore("foo", "bar")
il.Ignore("foo", "bleh") il.ignore("foo", "bleh")
il.Ignore("bleh", "bar") il.ignore("bleh", "bar")
il.Ignore("\u00e9", "bar") il.ignore("\u00e9", "bar")
f = io.BytesIO() f = io.BytesIO()
il.save_to_xml(f) il.save_to_xml(f)
f.seek(0) f.seek(0)
il = IgnoreList() il = IgnoreList()
il.load_from_xml(f) il.load_from_xml(f)
eq_(4, len(il)) eq_(4, len(il))
assert il.AreIgnored("\u00e9", "bar") assert il.are_ignored("\u00e9", "bar")
def test_LoadXML_with_empty_file_tags(): def test_load_xml_with_empty_file_tags():
f = io.BytesIO() f = io.BytesIO()
f.write(b'<?xml version="1.0" encoding="utf-8"?><ignore_list><file><file/></file></ignore_list>') f.write(b'<?xml version="1.0" encoding="utf-8"?><ignore_list><file><file/></file></ignore_list>')
f.seek(0) f.seek(0)
@@ -101,18 +101,18 @@ def test_LoadXML_with_empty_file_tags():
eq_(0, len(il)) eq_(0, len(il))
def test_AreIgnore_works_when_a_child_is_a_key_somewhere_else(): def test_are_ignore_works_when_a_child_is_a_key_somewhere_else():
il = IgnoreList() il = IgnoreList()
il.Ignore("foo", "bar") il.ignore("foo", "bar")
il.Ignore("bar", "baz") il.ignore("bar", "baz")
assert il.AreIgnored("bar", "foo") assert il.are_ignored("bar", "foo")
def test_no_dupes_when_a_child_is_a_key_somewhere_else(): def test_no_dupes_when_a_child_is_a_key_somewhere_else():
il = IgnoreList() il = IgnoreList()
il.Ignore("foo", "bar") il.ignore("foo", "bar")
il.Ignore("bar", "baz") il.ignore("bar", "baz")
il.Ignore("bar", "foo") il.ignore("bar", "foo")
eq_(2, len(il)) eq_(2, len(il))
@@ -121,7 +121,7 @@ def test_iterate():
il = IgnoreList() il = IgnoreList()
expected = [("foo", "bar"), ("bar", "baz"), ("foo", "baz")] expected = [("foo", "bar"), ("bar", "baz"), ("foo", "baz")]
for i in expected: for i in expected:
il.Ignore(i[0], i[1]) il.ignore(i[0], i[1])
for i in il: for i in il:
expected.remove(i) # No exception should be raised expected.remove(i) # No exception should be raised
assert not expected # expected should be empty assert not expected # expected should be empty
@@ -129,18 +129,18 @@ def test_iterate():
def test_filter(): def test_filter():
il = IgnoreList() il = IgnoreList()
il.Ignore("foo", "bar") il.ignore("foo", "bar")
il.Ignore("bar", "baz") il.ignore("bar", "baz")
il.Ignore("foo", "baz") il.ignore("foo", "baz")
il.Filter(lambda f, s: f == "bar") il.filter(lambda f, s: f == "bar")
eq_(1, len(il)) eq_(1, len(il))
assert not il.AreIgnored("foo", "bar") assert not il.are_ignored("foo", "bar")
assert il.AreIgnored("bar", "baz") assert il.are_ignored("bar", "baz")
def test_save_with_non_ascii_items(): def test_save_with_non_ascii_items():
il = IgnoreList() il = IgnoreList()
il.Ignore("\xac", "\xbf") il.ignore("\xac", "\xbf")
f = io.BytesIO() f = io.BytesIO()
try: try:
il.save_to_xml(f) il.save_to_xml(f)
@@ -151,29 +151,29 @@ def test_save_with_non_ascii_items():
def test_len(): def test_len():
il = IgnoreList() il = IgnoreList()
eq_(0, len(il)) eq_(0, len(il))
il.Ignore("foo", "bar") il.ignore("foo", "bar")
eq_(1, len(il)) eq_(1, len(il))
def test_nonzero(): def test_nonzero():
il = IgnoreList() il = IgnoreList()
assert not il assert not il
il.Ignore("foo", "bar") il.ignore("foo", "bar")
assert il assert il
def test_remove(): def test_remove():
il = IgnoreList() il = IgnoreList()
il.Ignore("foo", "bar") il.ignore("foo", "bar")
il.Ignore("foo", "baz") il.ignore("foo", "baz")
il.remove("bar", "foo") il.remove("bar", "foo")
eq_(len(il), 1) eq_(len(il), 1)
assert not il.AreIgnored("foo", "bar") assert not il.are_ignored("foo", "bar")
def test_remove_non_existant(): def test_remove_non_existant():
il = IgnoreList() il = IgnoreList()
il.Ignore("foo", "bar") il.ignore("foo", "bar")
il.Ignore("foo", "baz") il.ignore("foo", "baz")
with raises(ValueError): with raises(ValueError):
il.remove("foo", "bleh") il.remove("foo", "bleh")

View File

@@ -402,7 +402,7 @@ class TestCaseResultsMarkings:
self.results.make_ref(d) self.results.make_ref(d)
eq_("0 / 3 (0.00 B / 3.00 B) duplicates marked.", self.results.stat_line) eq_("0 / 3 (0.00 B / 3.00 B) duplicates marked.", self.results.stat_line)
def test_SaveXML(self): def test_save_xml(self):
self.results.mark(self.objects[1]) self.results.mark(self.objects[1])
self.results.mark_invert() self.results.mark_invert()
f = io.BytesIO() f = io.BytesIO()
@@ -419,7 +419,7 @@ class TestCaseResultsMarkings:
eq_("n", d1.get("marked")) eq_("n", d1.get("marked"))
eq_("y", d2.get("marked")) eq_("y", d2.get("marked"))
def test_LoadXML(self): def test_load_xml(self):
def get_file(path): def get_file(path):
return [f for f in self.objects if str(f.path) == path][0] return [f for f in self.objects if str(f.path) == path][0]
@@ -485,7 +485,7 @@ class TestCaseResultsXML:
eq_("ibabtu", d1.get("words")) eq_("ibabtu", d1.get("words"))
eq_("ibabtu", d2.get("words")) eq_("ibabtu", d2.get("words"))
def test_LoadXML(self): def test_load_xml(self):
def get_file(path): def get_file(path):
return [f for f in self.objects if str(f.path) == path][0] return [f for f in self.objects if str(f.path) == path][0]
@@ -517,7 +517,7 @@ class TestCaseResultsXML:
eq_(["ibabtu"], g2[0].words) eq_(["ibabtu"], g2[0].words)
eq_(["ibabtu"], g2[1].words) eq_(["ibabtu"], g2[1].words)
def test_LoadXML_with_filename(self, tmpdir): def test_load_xml_with_filename(self, tmpdir):
def get_file(path): def get_file(path):
return [f for f in self.objects if str(f.path) == path][0] return [f for f in self.objects if str(f.path) == path][0]
@@ -529,7 +529,7 @@ class TestCaseResultsXML:
r.load_from_xml(filename, get_file) r.load_from_xml(filename, get_file)
eq_(2, len(r.groups)) eq_(2, len(r.groups))
def test_LoadXML_with_some_files_that_dont_exist_anymore(self): def test_load_xml_with_some_files_that_dont_exist_anymore(self):
def get_file(path): def get_file(path):
if path.endswith("ibabtu 2"): if path.endswith("ibabtu 2"):
return None return None
@@ -545,7 +545,7 @@ class TestCaseResultsXML:
eq_(1, len(r.groups)) eq_(1, len(r.groups))
eq_(3, len(r.groups[0])) eq_(3, len(r.groups[0]))
def test_LoadXML_missing_attributes_and_bogus_elements(self): def test_load_xml_missing_attributes_and_bogus_elements(self):
def get_file(path): def get_file(path):
return [f for f in self.objects if str(f.path) == path][0] return [f for f in self.objects if str(f.path) == path][0]

View File

@@ -52,10 +52,12 @@ def test_empty(fake_fileexists):
def test_default_settings(fake_fileexists): def test_default_settings(fake_fileexists):
s = Scanner() s = Scanner()
eq_(s.min_match_percentage, 80) eq_(s.min_match_percentage, 80)
eq_(s.scan_type, ScanType.Filename) eq_(s.scan_type, ScanType.FILENAME)
eq_(s.mix_file_kind, True) eq_(s.mix_file_kind, True)
eq_(s.word_weighting, False) eq_(s.word_weighting, False)
eq_(s.match_similar_words, False) eq_(s.match_similar_words, False)
eq_(s.size_threshold, 0)
eq_(s.large_size_threshold, 0)
eq_(s.big_file_size_threshold, 0) eq_(s.big_file_size_threshold, 0)
@@ -98,7 +100,7 @@ def test_trim_all_ref_groups(fake_fileexists):
eq_(s.discarded_file_count, 0) eq_(s.discarded_file_count, 0)
def test_priorize(fake_fileexists): def test_prioritize(fake_fileexists):
s = Scanner() s = Scanner()
f = [ f = [
no("foo", path="p1"), no("foo", path="p1"),
@@ -119,7 +121,7 @@ def test_priorize(fake_fileexists):
def test_content_scan(fake_fileexists): def test_content_scan(fake_fileexists):
s = Scanner() s = Scanner()
s.scan_type = ScanType.Contents s.scan_type = ScanType.CONTENTS
f = [no("foo"), no("bar"), no("bleh")] f = [no("foo"), no("bar"), no("bleh")]
f[0].md5 = f[0].md5partial = f[0].md5samples = "foobar" f[0].md5 = f[0].md5partial = f[0].md5samples = "foobar"
f[1].md5 = f[1].md5partial = f[1].md5samples = "foobar" f[1].md5 = f[1].md5partial = f[1].md5samples = "foobar"
@@ -133,18 +135,62 @@ def test_content_scan(fake_fileexists):
def test_content_scan_compare_sizes_first(fake_fileexists): def test_content_scan_compare_sizes_first(fake_fileexists):
class MyFile(no): class MyFile(no):
@property @property
def md5(file): def md5(self):
raise AssertionError() raise AssertionError()
s = Scanner() s = Scanner()
s.scan_type = ScanType.Contents s.scan_type = ScanType.CONTENTS
f = [MyFile("foo", 1), MyFile("bar", 2)] f = [MyFile("foo", 1), MyFile("bar", 2)]
eq_(len(s.get_dupe_groups(f)), 0) eq_(len(s.get_dupe_groups(f)), 0)
def test_ignore_file_size(fake_fileexists):
s = Scanner()
s.scan_type = ScanType.CONTENTS
small_size = 10 # 10KB
s.size_threshold = 0
large_size = 100 * 1024 * 1024 # 100MB
s.large_size_threshold = 0
f = [
no("smallignore1", small_size - 1),
no("smallignore2", small_size - 1),
no("small1", small_size),
no("small2", small_size),
no("large1", large_size),
no("large2", large_size),
no("largeignore1", large_size + 1),
no("largeignore2", large_size + 1),
]
f[0].md5 = f[0].md5partial = f[0].md5samples = "smallignore"
f[1].md5 = f[1].md5partial = f[1].md5samples = "smallignore"
f[2].md5 = f[2].md5partial = f[2].md5samples = "small"
f[3].md5 = f[3].md5partial = f[3].md5samples = "small"
f[4].md5 = f[4].md5partial = f[4].md5samples = "large"
f[5].md5 = f[5].md5partial = f[5].md5samples = "large"
f[6].md5 = f[6].md5partial = f[6].md5samples = "largeignore"
f[7].md5 = f[7].md5partial = f[7].md5samples = "largeignore"
r = s.get_dupe_groups(f)
# No ignores
eq_(len(r), 4)
# Ignore smaller
s.size_threshold = small_size
r = s.get_dupe_groups(f)
eq_(len(r), 3)
# Ignore larger
s.size_threshold = 0
s.large_size_threshold = large_size
r = s.get_dupe_groups(f)
eq_(len(r), 3)
# Ignore both
s.size_threshold = small_size
r = s.get_dupe_groups(f)
eq_(len(r), 2)
def test_big_file_partial_hashes(fake_fileexists): def test_big_file_partial_hashes(fake_fileexists):
s = Scanner() s = Scanner()
s.scan_type = ScanType.Contents s.scan_type = ScanType.CONTENTS
smallsize = 1 smallsize = 1
bigsize = 100 * 1024 * 1024 # 100MB bigsize = 100 * 1024 * 1024 # 100MB
@@ -173,7 +219,7 @@ def test_big_file_partial_hashes(fake_fileexists):
def test_min_match_perc_doesnt_matter_for_content_scan(fake_fileexists): def test_min_match_perc_doesnt_matter_for_content_scan(fake_fileexists):
s = Scanner() s = Scanner()
s.scan_type = ScanType.Contents s.scan_type = ScanType.CONTENTS
f = [no("foo"), no("bar"), no("bleh")] f = [no("foo"), no("bar"), no("bleh")]
f[0].md5 = f[0].md5partial = f[0].md5samples = "foobar" f[0].md5 = f[0].md5partial = f[0].md5samples = "foobar"
f[1].md5 = f[1].md5partial = f[1].md5samples = "foobar" f[1].md5 = f[1].md5partial = f[1].md5samples = "foobar"
@@ -190,7 +236,7 @@ def test_min_match_perc_doesnt_matter_for_content_scan(fake_fileexists):
def test_content_scan_doesnt_put_md5_in_words_at_the_end(fake_fileexists): def test_content_scan_doesnt_put_md5_in_words_at_the_end(fake_fileexists):
s = Scanner() s = Scanner()
s.scan_type = ScanType.Contents s.scan_type = ScanType.CONTENTS
f = [no("foo"), no("bar")] f = [no("foo"), no("bar")]
f[0].md5 = f[0].md5partial = f[0].md5samples = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" f[0].md5 = f[0].md5partial = f[0].md5samples = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
f[1].md5 = f[1].md5partial = f[1].md5samples = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" f[1].md5 = f[1].md5partial = f[1].md5samples = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
@@ -256,7 +302,7 @@ def test_similar_words(fake_fileexists):
def test_fields(fake_fileexists): def test_fields(fake_fileexists):
s = Scanner() s = Scanner()
s.scan_type = ScanType.Fields s.scan_type = ScanType.FIELDS
f = [no("The White Stripes - Little Ghost"), no("The White Stripes - Little Acorn")] f = [no("The White Stripes - Little Ghost"), no("The White Stripes - Little Acorn")]
r = s.get_dupe_groups(f) r = s.get_dupe_groups(f)
eq_(len(r), 0) eq_(len(r), 0)
@@ -264,7 +310,7 @@ def test_fields(fake_fileexists):
def test_fields_no_order(fake_fileexists): def test_fields_no_order(fake_fileexists):
s = Scanner() s = Scanner()
s.scan_type = ScanType.FieldsNoOrder s.scan_type = ScanType.FIELDSNOORDER
f = [no("The White Stripes - Little Ghost"), no("Little Ghost - The White Stripes")] f = [no("The White Stripes - Little Ghost"), no("Little Ghost - The White Stripes")]
r = s.get_dupe_groups(f) r = s.get_dupe_groups(f)
eq_(len(r), 1) eq_(len(r), 1)
@@ -272,7 +318,7 @@ def test_fields_no_order(fake_fileexists):
def test_tag_scan(fake_fileexists): def test_tag_scan(fake_fileexists):
s = Scanner() s = Scanner()
s.scan_type = ScanType.Tag s.scan_type = ScanType.TAG
o1 = no("foo") o1 = no("foo")
o2 = no("bar") o2 = no("bar")
o1.artist = "The White Stripes" o1.artist = "The White Stripes"
@@ -285,7 +331,7 @@ def test_tag_scan(fake_fileexists):
def test_tag_with_album_scan(fake_fileexists): def test_tag_with_album_scan(fake_fileexists):
s = Scanner() s = Scanner()
s.scan_type = ScanType.Tag s.scan_type = ScanType.TAG
s.scanned_tags = set(["artist", "album", "title"]) s.scanned_tags = set(["artist", "album", "title"])
o1 = no("foo") o1 = no("foo")
o2 = no("bar") o2 = no("bar")
@@ -305,7 +351,7 @@ def test_tag_with_album_scan(fake_fileexists):
def test_that_dash_in_tags_dont_create_new_fields(fake_fileexists): def test_that_dash_in_tags_dont_create_new_fields(fake_fileexists):
s = Scanner() s = Scanner()
s.scan_type = ScanType.Tag s.scan_type = ScanType.TAG
s.scanned_tags = set(["artist", "album", "title"]) s.scanned_tags = set(["artist", "album", "title"])
s.min_match_percentage = 50 s.min_match_percentage = 50
o1 = no("foo") o1 = no("foo")
@@ -322,7 +368,7 @@ def test_that_dash_in_tags_dont_create_new_fields(fake_fileexists):
def test_tag_scan_with_different_scanned(fake_fileexists): def test_tag_scan_with_different_scanned(fake_fileexists):
s = Scanner() s = Scanner()
s.scan_type = ScanType.Tag s.scan_type = ScanType.TAG
s.scanned_tags = set(["track", "year"]) s.scanned_tags = set(["track", "year"])
o1 = no("foo") o1 = no("foo")
o2 = no("bar") o2 = no("bar")
@@ -340,7 +386,7 @@ def test_tag_scan_with_different_scanned(fake_fileexists):
def test_tag_scan_only_scans_existing_tags(fake_fileexists): def test_tag_scan_only_scans_existing_tags(fake_fileexists):
s = Scanner() s = Scanner()
s.scan_type = ScanType.Tag s.scan_type = ScanType.TAG
s.scanned_tags = set(["artist", "foo"]) s.scanned_tags = set(["artist", "foo"])
o1 = no("foo") o1 = no("foo")
o2 = no("bar") o2 = no("bar")
@@ -354,7 +400,7 @@ def test_tag_scan_only_scans_existing_tags(fake_fileexists):
def test_tag_scan_converts_to_str(fake_fileexists): def test_tag_scan_converts_to_str(fake_fileexists):
s = Scanner() s = Scanner()
s.scan_type = ScanType.Tag s.scan_type = ScanType.TAG
s.scanned_tags = set(["track"]) s.scanned_tags = set(["track"])
o1 = no("foo") o1 = no("foo")
o2 = no("bar") o2 = no("bar")
@@ -369,7 +415,7 @@ def test_tag_scan_converts_to_str(fake_fileexists):
def test_tag_scan_non_ascii(fake_fileexists): def test_tag_scan_non_ascii(fake_fileexists):
s = Scanner() s = Scanner()
s.scan_type = ScanType.Tag s.scan_type = ScanType.TAG
s.scanned_tags = set(["title"]) s.scanned_tags = set(["title"])
o1 = no("foo") o1 = no("foo")
o2 = no("bar") o2 = no("bar")
@@ -391,8 +437,8 @@ def test_ignore_list(fake_fileexists):
f2.path = Path("dir2/foobar") f2.path = Path("dir2/foobar")
f3.path = Path("dir3/foobar") f3.path = Path("dir3/foobar")
ignore_list = IgnoreList() ignore_list = IgnoreList()
ignore_list.Ignore(str(f1.path), str(f2.path)) ignore_list.ignore(str(f1.path), str(f2.path))
ignore_list.Ignore(str(f1.path), str(f3.path)) ignore_list.ignore(str(f1.path), str(f3.path))
r = s.get_dupe_groups([f1, f2, f3], ignore_list=ignore_list) r = s.get_dupe_groups([f1, f2, f3], ignore_list=ignore_list)
eq_(len(r), 1) eq_(len(r), 1)
g = r[0] g = r[0]
@@ -415,8 +461,8 @@ def test_ignore_list_checks_for_unicode(fake_fileexists):
f2.path = Path("foo2\u00e9") f2.path = Path("foo2\u00e9")
f3.path = Path("foo3\u00e9") f3.path = Path("foo3\u00e9")
ignore_list = IgnoreList() ignore_list = IgnoreList()
ignore_list.Ignore(str(f1.path), str(f2.path)) ignore_list.ignore(str(f1.path), str(f2.path))
ignore_list.Ignore(str(f1.path), str(f3.path)) ignore_list.ignore(str(f1.path), str(f3.path))
r = s.get_dupe_groups([f1, f2, f3], ignore_list=ignore_list) r = s.get_dupe_groups([f1, f2, f3], ignore_list=ignore_list)
eq_(len(r), 1) eq_(len(r), 1)
g = r[0] g = r[0]
@@ -520,7 +566,7 @@ def test_dont_group_files_that_dont_exist(tmpdir):
# In this test, we have to delete one of the files between the get_matches() part and the # In this test, we have to delete one of the files between the get_matches() part and the
# get_groups() part. # get_groups() part.
s = Scanner() s = Scanner()
s.scan_type = ScanType.Contents s.scan_type = ScanType.CONTENTS
p = Path(str(tmpdir)) p = Path(str(tmpdir))
p["file1"].open("w").write("foo") p["file1"].open("w").write("foo")
p["file2"].open("w").write("foo") p["file2"].open("w").write("foo")
@@ -539,7 +585,7 @@ def test_folder_scan_exclude_subfolder_matches(fake_fileexists):
# when doing a Folders scan type, don't include matches for folders whose parent folder already # when doing a Folders scan type, don't include matches for folders whose parent folder already
# match. # match.
s = Scanner() s = Scanner()
s.scan_type = ScanType.Folders s.scan_type = ScanType.FOLDERS
topf1 = no("top folder 1", size=42) topf1 = no("top folder 1", size=42)
topf1.md5 = topf1.md5partial = topf1.md5samples = b"some_md5_1" topf1.md5 = topf1.md5partial = topf1.md5samples = b"some_md5_1"
topf1.path = Path("/topf1") topf1.path = Path("/topf1")
@@ -574,7 +620,7 @@ def test_dont_count_ref_files_as_discarded(fake_fileexists):
# However, this causes problems in "discarded" counting and we make sure here that we don't # However, this causes problems in "discarded" counting and we make sure here that we don't
# report discarded matches in exact duplicate scans. # report discarded matches in exact duplicate scans.
s = Scanner() s = Scanner()
s.scan_type = ScanType.Contents s.scan_type = ScanType.CONTENTS
o1 = no("foo", path="p1") o1 = no("foo", path="p1")
o2 = no("foo", path="p2") o2 = no("foo", path="p2")
o3 = no("foo", path="p3") o3 = no("foo", path="p3")
@@ -587,8 +633,8 @@ def test_dont_count_ref_files_as_discarded(fake_fileexists):
eq_(s.discarded_file_count, 0) eq_(s.discarded_file_count, 0)
def test_priorize_me(fake_fileexists): def test_prioritize_me(fake_fileexists):
# in ScannerME, bitrate goes first (right after is_ref) in priorization # in ScannerME, bitrate goes first (right after is_ref) in prioritization
s = ScannerME() s = ScannerME()
o1, o2 = no("foo", path="p1"), no("foo", path="p2") o1, o2 = no("foo", path="p1"), no("foo", path="p2")
o1.bitrate = 1 o1.bitrate = 1

View File

@@ -1,3 +1,32 @@
=== 4.2.0 (2021-01-24)
* Add Malay and Turkish
* Add dark style for windows (#900)
* Add caching md5 file hashes (#942)
* Add feature to partially hash large files, with user adjustable preference (#908)
* Add portable mode (store settings next to executable)
* Add file association for .dupeguru files on windows
* Add ability to pass .dupeguru file to load on startup (#902)
* Add ability to reveal in explorer/finder (#895)
* Switch audio tag processing from hsaudiotag to mutagen (#440)
* Add ability to use Qt dialogs instead of native OS dialogs for some file selection operations
* Add OS and Python details to error dialog to assist in troubleshooting
* Add preference to ignore large files with threshold (#430)
* Fix error on close from DetailsPanel (#857, #873)
* Change reference background color (#894, #898)
* Remove stripping of unicode characters when matching names (#879)
* Fix exception when deleting in delta view (#863, #905)
* Fix dupes only view not updating after re-prioritize results (#757, #910, #911)
* Fix ability to drag'n'drop file/folder with certain characters in name (#897)
* Fix window position opening partially offscreen (#653)
* Fix TypeError is photo mode (#551)
* Change message for when files are deleted directly (#904)
* Add more feedback during scan (#700)
* Add Python version check to build.py (#589)
* General code cleanups
* Improvements to using standardized build tooling
* Moved CI/CD to github actions, added codeql, SonarCloud
=== 4.1.1 (2021-03-21) === 4.1.1 (2021-03-21)
* Add Japanese * Add Japanese

View File

@@ -1,7 +1,7 @@
Häufig gestellte Fragen Häufig gestellte Fragen
========================== ==========================
.. topic:: What is |appname|? .. topic:: What is dupeGuru?
.. only:: edition_se .. only:: edition_se
@@ -25,7 +25,7 @@ Häufig gestellte Fragen
.. topic:: Was sind die Demo-Einschränkungen von dupeGuru? .. topic:: Was sind die Demo-Einschränkungen von dupeGuru?
Keine, |appname| ist `Fairware <http://open.hardcoded.net/about/>`_. Keine, dupeGuru ist `Fairware <http://open.hardcoded.net/about/>`_.
.. topic:: Die Markierungsbox einer Datei, die ich löschen möchte, ist deaktiviert. Was muss ich tun? .. topic:: Die Markierungsbox einer Datei, die ich löschen möchte, ist deaktiviert. Was muss ich tun?

View File

@@ -1,21 +1,13 @@
|appname| Hilfe dupeGuru Hilfe
=============== ===============
.. only:: edition_se .. only:: edition_se
Dieses Dokument ist auch auf `Englisch <http://www.hardcoded.net/dupeguru/help/en/>`__ und `Französisch <http://www.hardcoded.net/dupeguru/help/fr/>`__ verfügbar. Dieses Dokument ist auch auf `Englisch <http://dupeguru.voltaicideas.net/help/en/>`__ und `Französisch <http://dupeguru.voltaicideas.net/help/fr/>`__ verfügbar.
.. only:: edition_me
Dieses Dokument ist auch auf `Englisch <http://www.hardcoded.net/dupeguru/help/en/>`__ und `Französisch <http://www.hardcoded.net/dupeguru_me/help/fr/>`__ verfügbar.
.. only:: edition_pe
Dieses Dokument ist auch auf `Englisch <http://www.hardcoded.net/dupeguru/help/en/>`__ und `Französisch <http://www.hardcoded.net/dupeguru_pe/help/fr/>`__ verfügbar.
.. only:: edition_se or edition_me .. only:: edition_se or edition_me
|appname| ist ein Tool zum Auffinden von Duplikaten auf Ihrem Computer. Es kann entweder Dateinamen oder Inhalte scannen. Der Dateiname-Scan stellt einen lockeren Suchalgorithmus zur Verfügung, der sogar Duplikate findet, die nicht den exakten selben Namen haben. dupeGuru ist ein Tool zum Auffinden von Duplikaten auf Ihrem Computer. Es kann entweder Dateinamen oder Inhalte scannen. Der Dateiname-Scan stellt einen lockeren Suchalgorithmus zur Verfügung, der sogar Duplikate findet, die nicht den exakten selben Namen haben.
.. only:: edition_pe .. only:: edition_pe
@@ -23,7 +15,7 @@
Obwohl dupeGuru auch leicht ohne Dokumentation genutzt werden kann, ist es sinnvoll die Hilfe zu lesen. Wenn Sie nach einer Führung für den ersten Duplikatscan suchen, werfen Sie einen Blick auf die :doc:`Schnellstart <quick_start>` Sektion Obwohl dupeGuru auch leicht ohne Dokumentation genutzt werden kann, ist es sinnvoll die Hilfe zu lesen. Wenn Sie nach einer Führung für den ersten Duplikatscan suchen, werfen Sie einen Blick auf die :doc:`Schnellstart <quick_start>` Sektion
Es ist eine gute Idee |appname| aktuell zu halten. Sie können die neueste Version auf der `homepage`_ finden. Es ist eine gute Idee dupeGuru aktuell zu halten. Sie können die neueste Version auf der http://dupeguru.voltaicideas.net finden.
Inhalte: Inhalte:

View File

@@ -83,9 +83,9 @@ dupeGuru. For more information about how to do that, you can refer to the `trans
.. _been open source: https://www.hardcoded.net/articles/free-as-in-speech-fair-as-in-trade .. _been open source: https://www.hardcoded.net/articles/free-as-in-speech-fair-as-in-trade
.. _let me know: mailto:hsoft@hardcoded.net .. _let me know: mailto:hsoft@hardcoded.net
.. _Source code repository: https://github.com/hsoft/dupeguru .. _Source code repository: https://github.com/arsenetar/dupeguru
.. _Issue Tracker: https://github.com/hsoft/dupeguru/issues .. _Issue Tracker: https://github.com/hsoft/arsenetar/issues
.. _Issue labels meaning: https://github.com/hsoft/dupeguru/wiki/issue-labels .. _Issue labels meaning: https://github.com/hsoft/arsenetar/wiki/issue-labels
.. _Sphinx: http://sphinx-doc.org/ .. _Sphinx: http://sphinx-doc.org/
.. _reST: http://en.wikipedia.org/wiki/ReStructuredText .. _reST: http://en.wikipedia.org/wiki/ReStructuredText
.. _translator guide: https://github.com/hsoft/dupeguru/wiki/Translator-Guide .. _translator guide: https://github.com/hsoft/arsenetar/wiki/Translator-Guide

View File

@@ -1,12 +0,0 @@
hscommon.jobprogress.qt
=======================
.. automodule:: hscommon.jobprogress.qt
.. autosummary::
Progress
.. autoclass:: Progress
:members:

View File

@@ -151,8 +151,6 @@ delete files" option that is offered to you when you activate Send to Trash. Thi
files to the Trash, but delete them immediately. In some cases, for example on network storage files to the Trash, but delete them immediately. In some cases, for example on network storage
(NAS), this has been known to work when normal deletion didn't. (NAS), this has been known to work when normal deletion didn't.
If this fail, `HS forums`_ might be of some help.
Why is Picture mode's contents scan so slow? Why is Picture mode's contents scan so slow?
-------------------------------------------- --------------------------------------------
@@ -178,7 +176,6 @@ Preferences are stored elsewhere:
* Linux: ``~/.config/Hardcoded Software/dupeGuru.conf`` * Linux: ``~/.config/Hardcoded Software/dupeGuru.conf``
* Mac OS X: In the built-in ``defaults`` system, as ``com.hardcoded-software.dupeguru`` * Mac OS X: In the built-in ``defaults`` system, as ``com.hardcoded-software.dupeguru``
.. _HS forums: https://forum.hardcoded.net/ .. _Github: https://github.com/arsenetar/dupeguru
.. _Github: https://github.com/hsoft/dupeguru .. _open an issue: https://github.com/arsenetar/dupeguru/wiki/issue-labels
.. _open an issue: https://github.com/hsoft/dupeguru/wiki/issue-labels

View File

@@ -3,11 +3,11 @@ dupeGuru help
This help document is also available in these languages: This help document is also available in these languages:
* `French <http://www.hardcoded.net/dupeguru/help/fr>`__ * `French <http://dupeguru.voltaicideas.net/help/fr>`__
* `German <http://www.hardcoded.net/dupeguru/help/de>`__ * `German <http://dupeguru.voltaicideas.net/help/de>`__
* `Armenian <http://www.hardcoded.net/dupeguru/help/hy>`__ * `Armenian <http://dupeguru.voltaicideas.net/help/hy>`__
* `Russian <http://www.hardcoded.net/dupeguru/help/ru>`__ * `Russian <http://dupeguru.voltaicideas.net/help/ru>`__
* `Ukrainian <http://www.hardcoded.net/dupeguru/help/uk>`__ * `Ukrainian <http://dupeguru.voltaicideas.net/help/uk>`__
dupeGuru is a tool to find duplicate files on your computer. It has three dupeGuru is a tool to find duplicate files on your computer. It has three
modes, Standard, Music and Picture, with each mode having its own scan types modes, Standard, Music and Picture, with each mode having its own scan types
@@ -42,4 +42,4 @@ Indices and tables
* :ref:`genindex` * :ref:`genindex`
* :ref:`search` * :ref:`search`
.. _homepage: https://www.hardcoded.net/dupeguru .. _homepage: https://dupeguru.voltaicideas.net/

View File

@@ -3,7 +3,7 @@ Foire aux questions
.. contents:: .. contents::
Qu'est-ce que |appname|? Qu'est-ce que dupeGuru?
------------------------ ------------------------
.. only:: edition_se .. only:: edition_se

View File

@@ -1,21 +1,13 @@
Aide |appname| Aide dupeGuru
=============== ===============
.. only:: edition_se .. only:: edition_se
Ce document est aussi disponible en `anglais <http://www.hardcoded.net/dupeguru/help/en/>`__, en `allemand <http://www.hardcoded.net/dupeguru/help/de/>`__ et en `arménien <http://www.hardcoded.net/dupeguru/help/hy/>`__. Ce document est aussi disponible en `anglais <http://dupeguru.voltaicideas.net/help/en/>`__, en `allemand <http://dupeguru.voltaicideas.net/help/de/>`__ et en `arménien <http://dupeguru.voltaicideas.net/help/hy/>`__.
.. only:: edition_me
Ce document est aussi disponible en `anglais <http://www.hardcoded.net/dupeguru_me/help/en/>`__, en `allemand <http://www.hardcoded.net/dupeguru_me/help/de/>`__ et en `arménien <http://www.hardcoded.net/dupeguru_me/help/hy/>`__.
.. only:: edition_pe
Ce document est aussi disponible en `anglais <http://www.hardcoded.net/dupeguru_pe/help/en/>`__, en `allemand <http://www.hardcoded.net/dupeguru_pe/help/de/>`__ et en `arménien <http://www.hardcoded.net/dupeguru_pe/help/hy/>`__.
.. only:: edition_se or edition_me .. only:: edition_se or edition_me
|appname| est un outil pour trouver des doublons parmi vos fichiers. Il peut comparer soit les noms de fichiers, soit le contenu. Le comparateur de nom de fichier peut trouver des doublons même si les noms ne sont pas exactement pareils. dupeGuru est un outil pour trouver des doublons parmi vos fichiers. Il peut comparer soit les noms de fichiers, soit le contenu. Le comparateur de nom de fichier peut trouver des doublons même si les noms ne sont pas exactement pareils.
.. only:: edition_pe .. only:: edition_pe
@@ -23,7 +15,7 @@ Aide |appname|
Bien que dupeGuru puisse être utilisé sans lire l'aide, une telle lecture vous permettra de bien comprendre comment l'application fonctionne. Pour un guide rapide pour une première utilisation, référez vous à la section :doc:`Démarrage Rapide <quick_start>`. Bien que dupeGuru puisse être utilisé sans lire l'aide, une telle lecture vous permettra de bien comprendre comment l'application fonctionne. Pour un guide rapide pour une première utilisation, référez vous à la section :doc:`Démarrage Rapide <quick_start>`.
C'est toujours une bonne idée de garder |appname| à jour. Vous pouvez télécharger la dernière version sur sa `page web`_. C'est toujours une bonne idée de garder dupeGuru à jour. Vous pouvez télécharger la dernière version sur sa http://dupeguru.voltaicideas.net.
Contents: Contents:

View File

@@ -1,7 +1,7 @@
Հաճախ Տրվող Հարցեր Հաճախ Տրվող Հարցեր
========================== ==========================
.. topic:: Ի՞նչ է |appname|-ը: .. topic:: Ի՞նչ է dupeGuru-ը:
.. only:: edition_se .. only:: edition_se

View File

@@ -1,21 +1,13 @@
|appname| help dupeGuru help
=============== ===============
.. only:: edition_se .. only:: edition_se
Այս փաստաթուղթը հասանելի է նաև՝ `Ֆրանսերեն <http://www.hardcoded.net/dupeguru/help/fr/>`__ և `Գերմաներեն <http://www.hardcoded.net/dupeguru/help/de/>`__. Այս փաստաթուղթը հասանելի է նաև՝ `Ֆրանսերեն <http://dupeguru.voltaicideas.net/help/fr/>`__ և `Գերմաներեն <http://dupeguru.voltaicideas.net/help/de/>`__.
.. only:: edition_me
Այս փաստաթուղթը հասանելի է նաև՝ `Ֆրանսերեն <http://www.hardcoded.net/dupeguru_me/help/fr/>`__ և `Գերմաներեն <http://www.hardcoded.net/dupeguru_me/help/de/>`__.
.. only:: edition_pe
Այս փաստաթուղթը հասանելի է նաև՝ `Ֆրանսերեն <http://www.hardcoded.net/dupeguru_pe/help/fr/>`__ և `Գերմաներեն <http://www.hardcoded.net/dupeguru_pe/help/de/>`__.
.. only:: edition_se or edition_me .. only:: edition_se or edition_me
|appname| ծրագիր է՝ գտնելու կրկնօրինակ ունեցող ֆայլեր Ձեր համակարգչում: Այն կարող է անգամ ստուգել ֆայլի անունները կան բովանդակությունը: Ֆայլի անվան ստուգման հնարավորությունները ոչ ճշգրիտ համընկման ալգորիթմով, որը կարող է գտնել ֆայլի անվան կրկնօրինակներ, անգամ եթե դրանք նույնը չեն: dupeGuru ծրագիր է՝ գտնելու կրկնօրինակ ունեցող ֆայլեր Ձեր համակարգչում: Այն կարող է անգամ ստուգել ֆայլի անունները կան բովանդակությունը: Ֆայլի անվան ստուգման հնարավորությունները ոչ ճշգրիտ համընկման ալգորիթմով, որը կարող է գտնել ֆայլի անվան կրկնօրինակներ, անգամ եթե դրանք նույնը չեն:
.. only:: edition_pe .. only:: edition_pe
@@ -23,7 +15,7 @@
Չնայած dupeGuru-ն կարող է հեշտությամբ օգտագործվել առանց օգնության, այնուհանդերձ եթե կարդաք այս ֆայլը, այն մեծապես կօգնի Ձեզ ընկալելու ծրագրի աշխատանքը: Եթե Դուք նայում եք ձեռնարկը կրկնօրինակների առաջին ստուգման համար, ապա կարող եք ընտրել :doc:`Արագ Սկիզբ <quick_start>` հատվածը: Չնայած dupeGuru-ն կարող է հեշտությամբ օգտագործվել առանց օգնության, այնուհանդերձ եթե կարդաք այս ֆայլը, այն մեծապես կօգնի Ձեզ ընկալելու ծրագրի աշխատանքը: Եթե Դուք նայում եք ձեռնարկը կրկնօրինակների առաջին ստուգման համար, ապա կարող եք ընտրել :doc:`Արագ Սկիզբ <quick_start>` հատվածը:
Շատ լավ միտք է պահելու |appname| թարմացված: Կարող եք բեռնել վեբ կայքի համապատասխան էջից `homepage`_: Շատ լավ միտք է պահելու dupeGuru թարմացված: Կարող եք բեռնել վեբ կայքի համապատասխան էջից http://dupeguru.voltaicideas.net:
Պարունակությունը. Պարունակությունը.

View File

@@ -1,7 +1,7 @@
Часто задаваемые вопросы Часто задаваемые вопросы
========================== ==========================
.. topic:: Что такое |appname|? .. topic:: Что такое dupeGuru?
.. only:: edition_se .. only:: edition_se

View File

@@ -1,21 +1,11 @@
|appname| help dupeGuru help
=============== ===============
.. only:: edition_se Этот документ также доступна на `французском <http://dupeguru.voltaicideas.net/help/fr/>`__, `немецком <http://dupeguru.voltaicideas.net/help/de/>`__ и `армянский <http://dupeguru.voltaicideas.net/help/hy/>`__.
Этот документ также доступна на `французском <http://www.hardcoded.net/dupeguru/help/fr/>`__, `немецком <http://www.hardcoded.net/dupeguru/help/de/>`__ и `армянский <http://www.hardcoded.net/dupeguru/help/hy/>`__.
.. only:: edition_me
Этот документ также доступна на `французском <http://www.hardcoded.net/dupeguru_me/help/fr/>`__, `немецкий <http://www.hardcoded.net/dupeguru_me/help/de/>`__ и `армянский <http://www.hardcoded.net/dupeguru_me/help/hy/>`__.
.. only:: edition_pe
Этот документ также доступна на `французском <http://www.hardcoded.net/dupeguru_pe/help/fr/>`__, `немецкий <http://www.hardcoded.net/dupeguru_pe/help/de/>`__ и `армянский <http://www.hardcoded.net/dupeguru_pe/help/hy/>`__.
.. only:: edition_se or edition_me .. only:: edition_se or edition_me
|appname| есть инструмент для поиска дубликатов файлов на вашем компьютере. Он может сканировать либо имен файлов или содержимого.Имя файла функций сканирования нечеткого соответствия алгоритма, который позволяет найти одинаковые имена файлов, даже если они не совсем то же самое. dupeGuru есть инструмент для поиска дубликатов файлов на вашем компьютере. Он может сканировать либо имен файлов или содержимого.Имя файла функций сканирования нечеткого соответствия алгоритма, который позволяет найти одинаковые имена файлов, даже если они не совсем то же самое.
.. only:: edition_pe .. only:: edition_pe
@@ -23,7 +13,7 @@
Хотя dupeGuru может быть легко использована без документации, чтение этого файла поможет вам освоить его. Если вы ищете руководство для вашей первой дублировать сканирования, вы можете взглянуть на раздел :doc:`Быстрый <quick_start>` Начало. Хотя dupeGuru может быть легко использована без документации, чтение этого файла поможет вам освоить его. Если вы ищете руководство для вашей первой дублировать сканирования, вы можете взглянуть на раздел :doc:`Быстрый <quick_start>` Начало.
Это хорошая идея, чтобы сохранить |appname| обновлен. Вы можете скачать последнюю версию на своей `homepage`_. Это хорошая идея, чтобы сохранить dupeGuru обновлен. Вы можете скачать последнюю версию на своей http://dupeguru.voltaicideas.net.
Содержание: Содержание:
.. toctree:: .. toctree::

View File

@@ -1,7 +1,7 @@
Часті питання Часті питання
========================== ==========================
.. topic:: Що таке |appname|? .. topic:: Що таке dupeGuru?
.. only:: edition_se .. only:: edition_se

View File

@@ -1,21 +1,13 @@
|appname| help dupeGuru help
=============== ===============
.. only:: edition_se .. only:: edition_se
Цей документ також доступна на `французькому <http://www.hardcoded.net/dupeguru/help/fr/>`__, `німецький <http://www.hardcoded.net/dupeguru/help/de/>`__ і `Вірменський <http://www.hardcoded.net/dupeguru/help/hy/>`__. Цей документ також доступна на `французькому <http://dupeguru.voltaicideas.net/help/fr/>`__, `німецький <http://dupeguru.voltaicideas.net/help/de/>`__ і `Вірменський <http://dupeguru.voltaicideas.net/help/hy/>`__.
.. only:: edition_me
Цей документ також доступна на `французькому <http://www.hardcoded.net/dupeguru_me/help/fr/>`__, `німецький <http://www.hardcoded.net/dupeguru_me/help/de/>`__ і `Вірменський <http://www.hardcoded.net/dupeguru_me/help/hy/>`__.
.. only:: edition_pe
Цей документ також доступна на `французькому <http://www.hardcoded.net/dupeguru_pe/help/fr/>`__, `німецький <http://www.hardcoded.net/dupeguru_pe/help/de/>`__ і `Вірменський <http://www.hardcoded.net/dupeguru_pe/help/hy/>`__.
.. only:: edition_se or edition_me .. only:: edition_se or edition_me
|appname| це інструмент для пошуку дублікатів файлів на вашому комп'ютері. Він може сканувати або імен файлів або вмісту. Файл функцій сканування нечіткого відповідності алгоритму, який дозволяє знайти однакові імена файлів, навіть якщо вони не зовсім те ж саме. dupeGuru це інструмент для пошуку дублікатів файлів на вашому комп'ютері. Він може сканувати або імен файлів або вмісту. Файл функцій сканування нечіткого відповідності алгоритму, який дозволяє знайти однакові імена файлів, навіть якщо вони не зовсім те ж саме.
.. only:: edition_pe .. only:: edition_pe
@@ -23,7 +15,7 @@
Хоча dupeGuru може бути легко використана без документації, читання цього файлу допоможе вам освоїти його. Якщо ви шукаєте керівництво для вашої першої дублювати сканування, ви можете поглянути на: :doc:`Quick Start <quick_start>` Хоча dupeGuru може бути легко використана без документації, читання цього файлу допоможе вам освоїти його. Якщо ви шукаєте керівництво для вашої першої дублювати сканування, ви можете поглянути на: :doc:`Quick Start <quick_start>`
Це гарна ідея, щоб зберегти |appname| оновлено. Ви можете завантажити останню версію на своєму `homepage`_. Це гарна ідея, щоб зберегти dupeGuru оновлено. Ви можете завантажити останню версію на своєму http://dupeguru.voltaicideas.net.
Contents: Contents:

View File

@@ -336,7 +336,6 @@ def read_changelog_file(filename):
with open(filename, "rt", encoding="utf-8") as fp: with open(filename, "rt", encoding="utf-8") as fp:
contents = fp.read() contents = fp.read()
splitted = re_changelog_header.split(contents)[1:] # the first item is empty splitted = re_changelog_header.split(contents)[1:] # the first item is empty
# splitted = [version1, date1, desc1, version2, date2, ...]
result = [] result = []
for version, date_str, description in iter_by_three(iter(splitted)): for version, date_str, description in iter_by_three(iter(splitted)):
date = datetime.strptime(date_str, "%Y-%m-%d").date() date = datetime.strptime(date_str, "%Y-%m-%d").date()
@@ -399,8 +398,8 @@ def create_osx_app_structure(
# `resources`: A list of paths of files or folders going in the "Resources" folder. # `resources`: A list of paths of files or folders going in the "Resources" folder.
# `frameworks`: Same as above for "Frameworks". # `frameworks`: Same as above for "Frameworks".
# `symlink_resources`: If True, will symlink resources into the structure instead of copying them. # `symlink_resources`: If True, will symlink resources into the structure instead of copying them.
app = OSXAppStructure(dest, infoplist) app = OSXAppStructure(dest)
app.create() app.create(infoplist)
app.copy_executable(executable) app.copy_executable(executable)
app.copy_resources(*resources, use_symlinks=symlink_resources) app.copy_resources(*resources, use_symlinks=symlink_resources)
app.copy_frameworks(*frameworks) app.copy_frameworks(*frameworks)

View File

@@ -13,8 +13,8 @@ import traceback
# Taken from http://bzimmer.ziclix.com/2008/12/17/python-thread-dumps/ # Taken from http://bzimmer.ziclix.com/2008/12/17/python-thread-dumps/
def stacktraces(): def stacktraces():
code = [] code = []
for threadId, stack in sys._current_frames().items(): for thread_id, stack in sys._current_frames().items():
code.append("\n# ThreadID: %s" % threadId) code.append("\n# ThreadID: %s" % thread_id)
for filename, lineno, name, line in traceback.extract_stack(stack): for filename, lineno, name, line in traceback.extract_stack(stack):
code.append('File: "%s", line %d, in %s' % (filename, lineno, name)) code.append('File: "%s", line %d, in %s' % (filename, lineno, name))
if line: if line:

View File

@@ -11,8 +11,8 @@ import logging
class SpecialFolder: class SpecialFolder:
AppData = 1 APPDATA = 1
Cache = 2 CACHE = 2
def open_url(url): def open_url(url):
@@ -55,7 +55,7 @@ try:
_reveal_path = proxy.revealPath_ _reveal_path = proxy.revealPath_
def _special_folder_path(special_folder, appname=None, portable=False): def _special_folder_path(special_folder, appname=None, portable=False):
if special_folder == SpecialFolder.Cache: if special_folder == SpecialFolder.CACHE:
base = proxy.getCachePath() base = proxy.getCachePath()
else: else:
base = proxy.getAppdataPath() base = proxy.getAppdataPath()
@@ -63,14 +63,14 @@ try:
appname = proxy.bundleInfo_("CFBundleName") appname = proxy.bundleInfo_("CFBundleName")
return op.join(base, appname) return op.join(base, appname)
except ImportError: except ImportError:
try: try:
from PyQt5.QtCore import QUrl, QStandardPaths from PyQt5.QtCore import QUrl, QStandardPaths
from PyQt5.QtGui import QDesktopServices from PyQt5.QtGui import QDesktopServices
from qtlib.util import getAppData from qtlib.util import get_appdata
from core.util import executable_folder from core.util import executable_folder
from hscommon.plat import ISWINDOWS from hscommon.plat import ISWINDOWS, ISOSX
import subprocess
def _open_url(url): def _open_url(url):
QDesktopServices.openUrl(QUrl(url)) QDesktopServices.openUrl(QUrl(url))
@@ -80,16 +80,21 @@ except ImportError:
QDesktopServices.openUrl(url) QDesktopServices.openUrl(url)
def _reveal_path(path): def _reveal_path(path):
_open_path(op.dirname(str(path))) if ISWINDOWS:
subprocess.run(["explorer", "/select,", op.abspath(path)])
elif ISOSX:
subprocess.run(["open", "-R", op.abspath(path)])
else:
_open_path(op.dirname(str(path)))
def _special_folder_path(special_folder, appname=None, portable=False): def _special_folder_path(special_folder, appname=None, portable=False):
if special_folder == SpecialFolder.Cache: if special_folder == SpecialFolder.CACHE:
if ISWINDOWS and portable: if ISWINDOWS and portable:
folder = op.join(executable_folder(), "cache") folder = op.join(executable_folder(), "cache")
else: else:
folder = QStandardPaths.standardLocations(QStandardPaths.CacheLocation)[0] folder = QStandardPaths.standardLocations(QStandardPaths.CacheLocation)[0]
else: else:
folder = getAppData(portable) folder = get_appdata(portable)
return folder return folder
except ImportError: except ImportError:
@@ -98,9 +103,11 @@ except ImportError:
logging.warning("Can't setup desktop functions!") logging.warning("Can't setup desktop functions!")
def _open_path(path): def _open_path(path):
# Dummy for tests
pass pass
def _reveal_path(path): def _reveal_path(path):
# Dummy for tests
pass pass
def _special_folder_path(special_folder, appname=None, portable=False): def _special_folder_path(special_folder, appname=None, portable=False):

View File

@@ -139,31 +139,34 @@ class Job:
self._progress = progress self._progress = progress
if self._progress > self._currmax: if self._progress > self._currmax:
self._progress = self._currmax self._progress = self._currmax
if self._progress < 0:
self._progress = 0
self._do_update(desc) self._do_update(desc)
class NullJob: class NullJob:
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# Null job does nothing
pass pass
def add_progress(self, *args, **kwargs): def add_progress(self, *args, **kwargs):
# Null job does nothing
pass pass
def check_if_cancelled(self): def check_if_cancelled(self):
# Null job does nothing
pass pass
def iter_with_progress(self, sequence, *args, **kwargs): def iter_with_progress(self, sequence, *args, **kwargs):
return iter(sequence) return iter(sequence)
def start_job(self, *args, **kwargs): def start_job(self, *args, **kwargs):
# Null job does nothing
pass pass
def start_subjob(self, *args, **kwargs): def start_subjob(self, *args, **kwargs):
return NullJob() return NullJob()
def set_progress(self, *args, **kwargs): def set_progress(self, *args, **kwargs):
# Null job does nothing
pass pass

View File

@@ -1,52 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2009-09-14
# Copyright 2011 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 PyQt5.QtCore import pyqtSignal, Qt, QTimer
from PyQt5.QtWidgets import QProgressDialog
from . import performer
class Progress(QProgressDialog, performer.ThreadedJobPerformer):
finished = pyqtSignal(["QString"])
def __init__(self, parent):
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint
QProgressDialog.__init__(self, "", "Cancel", 0, 100, parent, flags)
self.setModal(True)
self.setAutoReset(False)
self.setAutoClose(False)
self._timer = QTimer()
self._jobid = ""
self._timer.timeout.connect(self.updateProgress)
def updateProgress(self):
# the values might change before setValue happens
last_progress = self.last_progress
last_desc = self.last_desc
if not self._job_running or last_progress is None:
self._timer.stop()
self.close()
if not self.job_cancelled:
self.finished.emit(self._jobid)
return
if self.wasCanceled():
self.job_cancelled = True
return
if last_desc:
self.setLabelText(last_desc)
self.setValue(last_progress)
def run(self, jobid, title, target, args=()):
self._jobid = jobid
self.reset()
self.setLabelText("")
self.run_threaded(target, args)
self.setWindowTitle(title)
self.show()
self._timer.start(500)

View File

@@ -21,6 +21,8 @@ PO2COCOA = {
COCOA2PO = {v: k for k, v in PO2COCOA.items()} COCOA2PO = {v: k for k, v in PO2COCOA.items()}
STRING_EXT = ".strings"
def get_langs(folder): def get_langs(folder):
return [name for name in os.listdir(folder) if op.isdir(op.join(folder, name))] return [name for name in os.listdir(folder) if op.isdir(op.join(folder, name))]
@@ -152,7 +154,7 @@ def strings2pot(target, dest):
def allstrings2pot(lprojpath, dest, excludes=None): def allstrings2pot(lprojpath, dest, excludes=None):
allstrings = files_with_ext(lprojpath, ".strings") allstrings = files_with_ext(lprojpath, STRING_EXT)
if excludes: if excludes:
allstrings = [p for p in allstrings if op.splitext(op.basename(p))[0] not in excludes] allstrings = [p for p in allstrings if op.splitext(op.basename(p))[0] not in excludes]
for strings_path in allstrings: for strings_path in allstrings:
@@ -210,7 +212,7 @@ def generate_cocoa_strings_from_code(code_folder, dest_folder):
def generate_cocoa_strings_from_xib(xib_folder): def generate_cocoa_strings_from_xib(xib_folder):
xibs = [op.join(xib_folder, fn) for fn in os.listdir(xib_folder) if fn.endswith(".xib")] xibs = [op.join(xib_folder, fn) for fn in os.listdir(xib_folder) if fn.endswith(".xib")]
for xib in xibs: for xib in xibs:
dest = xib.replace(".xib", ".strings") dest = xib.replace(".xib", STRING_EXT)
print_and_do("ibtool {} --generate-strings-file {}".format(xib, dest)) print_and_do("ibtool {} --generate-strings-file {}".format(xib, dest))
print_and_do("iconv -f utf-16 -t utf-8 {0} | tee {0}".format(dest)) print_and_do("iconv -f utf-16 -t utf-8 {0} | tee {0}".format(dest))
@@ -226,6 +228,6 @@ def localize_stringsfile(stringsfile, dest_root_folder):
def localize_all_stringsfiles(src_folder, dest_root_folder): def localize_all_stringsfiles(src_folder, dest_root_folder):
stringsfiles = [op.join(src_folder, fn) for fn in os.listdir(src_folder) if fn.endswith(".strings")] stringsfiles = [op.join(src_folder, fn) for fn in os.listdir(src_folder) if fn.endswith(STRING_EXT)]
for path in stringsfiles: for path in stringsfiles:
localize_stringsfile(path, dest_root_folder) localize_stringsfile(path, dest_root_folder)

View File

@@ -167,10 +167,10 @@ def getFilesForName(name):
# check for glob chars # check for glob chars
if containsAny(name, "*?[]"): if containsAny(name, "*?[]"):
files = glob.glob(name) files = glob.glob(name)
list = [] file_list = []
for file in files: for file in files:
list.extend(getFilesForName(file)) file_list.extend(getFilesForName(file))
return list return file_list
# try to find module or package # try to find module or package
name = _get_modpkg_path(name) name = _get_modpkg_path(name)
@@ -179,9 +179,9 @@ def getFilesForName(name):
if os.path.isdir(name): if os.path.isdir(name):
# find all python files in directory # find all python files in directory
list = [] file_list = []
os.walk(name, _visit_pyfiles, list) os.walk(name, _visit_pyfiles, file_list)
return list return file_list
elif os.path.exists(name): elif os.path.exists(name):
# a single file # a single file
return [name] return [name]

View File

@@ -4,7 +4,7 @@
# which should be included with this package. The terms are also available at # which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html # http://www.gnu.org/licenses/gpl-3.0.html
import os.path as op from pathlib import Path
import re import re
from .build import read_changelog_file, filereplace from .build import read_changelog_file, filereplace
@@ -48,9 +48,9 @@ def gen(
if confrepl is None: if confrepl is None:
confrepl = {} confrepl = {}
if confpath is None: if confpath is None:
confpath = op.join(basepath, "conf.tmpl") confpath = Path(basepath, "conf.tmpl")
if changelogtmpl is None: if changelogtmpl is None:
changelogtmpl = op.join(basepath, "changelog.tmpl") changelogtmpl = Path(basepath, "changelog.tmpl")
changelog = read_changelog_file(changelogpath) changelog = read_changelog_file(changelogpath)
tix = tixgen(tixurl) tix = tixgen(tixurl)
rendered_logs = [] rendered_logs = []
@@ -62,13 +62,13 @@ def gen(
rendered = CHANGELOG_FORMAT.format(version=log["version"], date=log["date_str"], description=description) rendered = CHANGELOG_FORMAT.format(version=log["version"], date=log["date_str"], description=description)
rendered_logs.append(rendered) rendered_logs.append(rendered)
confrepl["version"] = changelog[0]["version"] confrepl["version"] = changelog[0]["version"]
changelog_out = op.join(basepath, "changelog.rst") changelog_out = Path(basepath, "changelog.rst")
filereplace(changelogtmpl, changelog_out, changelog="\n".join(rendered_logs)) filereplace(changelogtmpl, changelog_out, changelog="\n".join(rendered_logs))
if op.exists(confpath): if Path(confpath).exists():
conf_out = op.join(basepath, "conf.py") conf_out = Path(basepath, "conf.py")
filereplace(confpath, conf_out, **confrepl) filereplace(confpath, conf_out, **confrepl)
# Call the sphinx_build function, which is the same as doing sphinx-build from cli # Call the sphinx_build function, which is the same as doing sphinx-build from cli
try: try:
sphinx_build([basepath, destpath]) sphinx_build([str(basepath), str(destpath)])
except SystemExit: except SystemExit:
print("Sphinx called sys.exit(), but we're cancelling it because we don't actually want to exit") print("Sphinx called sys.exit(), but we're cancelling it because we don't actually want to exit")

View File

@@ -19,7 +19,7 @@ from ..path import Path
from ..testutil import eq_ from ..testutil import eq_
class TestCase_GetConflictedName: class TestCaseGetConflictedName:
def test_simple(self): def test_simple(self):
name = get_conflicted_name(["bar"], "bar") name = get_conflicted_name(["bar"], "bar")
eq_("[000] bar", name) eq_("[000] bar", name)
@@ -46,7 +46,7 @@ class TestCase_GetConflictedName:
eq_("[000] bar", name) eq_("[000] bar", name)
class TestCase_GetUnconflictedName: class TestCaseGetUnconflictedName:
def test_main(self): def test_main(self):
eq_("foobar", get_unconflicted_name("[000] foobar")) eq_("foobar", get_unconflicted_name("[000] foobar"))
eq_("foobar", get_unconflicted_name("[9999] foobar")) eq_("foobar", get_unconflicted_name("[9999] foobar"))
@@ -56,7 +56,7 @@ class TestCase_GetUnconflictedName:
eq_("foo [000] bar", get_unconflicted_name("foo [000] bar")) eq_("foo [000] bar", get_unconflicted_name("foo [000] bar"))
class TestCase_IsConflicted: class TestCaseIsConflicted:
def test_main(self): def test_main(self):
assert is_conflicted("[000] foobar") assert is_conflicted("[000] foobar")
assert is_conflicted("[9999] foobar") assert is_conflicted("[9999] foobar")
@@ -66,7 +66,7 @@ class TestCase_IsConflicted:
assert not is_conflicted("foo [000] bar") assert not is_conflicted("foo [000] bar")
class TestCase_move_copy: class TestCaseMoveCopy:
@pytest.fixture @pytest.fixture
def do_setup(self, request): def do_setup(self, request):
tmpdir = request.getfixturevalue("tmpdir") tmpdir = request.getfixturevalue("tmpdir")

View File

@@ -51,7 +51,7 @@ def test_init_with_tuple_and_list(force_ossep):
def test_init_with_invalid_value(force_ossep): def test_init_with_invalid_value(force_ossep):
try: try:
path = Path(42) # noqa: F841 Path(42)
assert False assert False
except TypeError: except TypeError:
pass pass
@@ -142,8 +142,6 @@ def test_path_slice(force_ossep):
eq_((), foobar[:foobar]) eq_((), foobar[:foobar])
abcd = Path("a/b/c/d") abcd = Path("a/b/c/d")
a = Path("a") a = Path("a")
b = Path("b") # noqa: #F841
c = Path("c") # noqa: #F841
d = Path("d") d = Path("d")
z = Path("z") z = Path("z")
eq_("b/c", abcd[a:d]) eq_("b/c", abcd[a:d])
@@ -216,7 +214,7 @@ def test_str_repr_of_mix_between_non_ascii_str_and_unicode(force_ossep):
eq_("foo\u00e9/bar".encode(sys.getfilesystemencoding()), p.tobytes()) eq_("foo\u00e9/bar".encode(sys.getfilesystemencoding()), p.tobytes())
def test_Path_of_a_Path_returns_self(force_ossep): def test_path_of_a_path_returns_self(force_ossep):
# if Path() is called with a path as value, just return value. # if Path() is called with a path as value, just return value.
p = Path("foo/bar") p = Path("foo/bar")
assert Path(p) is p assert Path(p) is p

View File

@@ -91,7 +91,7 @@ def test_make_sure_theres_no_messup_between_queries():
threads = [] threads = []
for i in range(1, 101): for i in range(1, 101):
t = threading.Thread(target=run, args=(i,)) t = threading.Thread(target=run, args=(i,))
t.start t.start()
threads.append(t) threads.append(t)
while threads: while threads:
time.sleep(0.1) time.sleep(0.1)

View File

@@ -19,6 +19,7 @@ class TestRow(Row):
self._index = index self._index = index
def load(self): def load(self):
# Does nothing for test
pass pass
def save(self): def save(self):
@@ -75,14 +76,17 @@ def test_allow_edit_when_attr_is_property_with_fset():
class TestRow(Row): class TestRow(Row):
@property @property
def foo(self): def foo(self):
# property only for existence checks
pass pass
@property @property
def bar(self): def bar(self):
# property only for existence checks
pass pass
@bar.setter @bar.setter
def bar(self, value): def bar(self, value):
# setter only for existence checks
pass pass
row = TestRow(Table()) row = TestRow(Table())
@@ -97,10 +101,12 @@ def test_can_edit_prop_has_priority_over_fset_checks():
class TestRow(Row): class TestRow(Row):
@property @property
def bar(self): def bar(self):
# property only for existence checks
pass pass
@bar.setter @bar.setter
def bar(self, value): def bar(self, value):
# setter only for existence checks
pass pass
can_edit_bar = False can_edit_bar = False

View File

@@ -236,49 +236,8 @@ def test_multi_replace():
# --- Files # --- Files
# These test cases needed https://github.com/hsoft/pytest-monkeyplus/ which appears to not be compatible with latest
# pytest, looking at where this is used only appears to be in hscommon.localize_all_stringfiles at top level.
# Right now this repo does not seem to utilize any of that functionality so going to leave these tests out for now.
# TODO decide if fixing these tests is worth it or not.
# class TestCase_modified_after: class TestCaseDeleteIfEmpty:
# def test_first_is_modified_after(self, monkeyplus):
# monkeyplus.patch_osstat("first", st_mtime=42)
# monkeyplus.patch_osstat("second", st_mtime=41)
# assert modified_after("first", "second")
# def test_second_is_modified_after(self, monkeyplus):
# monkeyplus.patch_osstat("first", st_mtime=42)
# monkeyplus.patch_osstat("second", st_mtime=43)
# assert not modified_after("first", "second")
# def test_same_mtime(self, monkeyplus):
# monkeyplus.patch_osstat("first", st_mtime=42)
# monkeyplus.patch_osstat("second", st_mtime=42)
# assert not modified_after("first", "second")
# def test_first_file_does_not_exist(self, monkeyplus):
# # when the first file doesn't exist, we return False
# monkeyplus.patch_osstat("second", st_mtime=42)
# assert not modified_after("does_not_exist", "second") # no crash
# def test_second_file_does_not_exist(self, monkeyplus):
# # when the second file doesn't exist, we return True
# monkeyplus.patch_osstat("first", st_mtime=42)
# assert modified_after("first", "does_not_exist") # no crash
# def test_first_file_is_none(self, monkeyplus):
# # when the first file is None, we return False
# monkeyplus.patch_osstat("second", st_mtime=42)
# assert not modified_after(None, "second") # no crash
# def test_second_file_is_none(self, monkeyplus):
# # when the second file is None, we return True
# monkeyplus.patch_osstat("first", st_mtime=42)
# assert modified_after("first", None) # no crash
class TestCase_delete_if_empty:
def test_is_empty(self, tmpdir): def test_is_empty(self, tmpdir):
testpath = Path(str(tmpdir)) testpath = Path(str(tmpdir))
assert delete_if_empty(testpath) assert delete_if_empty(testpath)
@@ -330,9 +289,11 @@ class TestCase_delete_if_empty:
delete_if_empty(Path(str(tmpdir))) # no crash delete_if_empty(Path(str(tmpdir))) # no crash
class TestCase_open_if_filename: class TestCaseOpenIfFilename:
FILE_NAME = "test.txt"
def test_file_name(self, tmpdir): def test_file_name(self, tmpdir):
filepath = str(tmpdir.join("test.txt")) filepath = str(tmpdir.join(self.FILE_NAME))
open(filepath, "wb").write(b"test_data") open(filepath, "wb").write(b"test_data")
file, close = open_if_filename(filepath) file, close = open_if_filename(filepath)
assert close assert close
@@ -348,16 +309,18 @@ class TestCase_open_if_filename:
eq_("test_data", file.read()) eq_("test_data", file.read())
def test_mode_is_passed_to_open(self, tmpdir): def test_mode_is_passed_to_open(self, tmpdir):
filepath = str(tmpdir.join("test.txt")) filepath = str(tmpdir.join(self.FILE_NAME))
open(filepath, "w").close() open(filepath, "w").close()
file, close = open_if_filename(filepath, "a") file, close = open_if_filename(filepath, "a")
eq_("a", file.mode) eq_("a", file.mode)
file.close() file.close()
class TestCase_FileOrPath: class TestCaseFileOrPath:
FILE_NAME = "test.txt"
def test_path(self, tmpdir): def test_path(self, tmpdir):
filepath = str(tmpdir.join("test.txt")) filepath = str(tmpdir.join(self.FILE_NAME))
open(filepath, "wb").write(b"test_data") open(filepath, "wb").write(b"test_data")
with FileOrPath(filepath) as fp: with FileOrPath(filepath) as fp:
eq_(b"test_data", fp.read()) eq_(b"test_data", fp.read())
@@ -370,7 +333,7 @@ class TestCase_FileOrPath:
eq_("test_data", fp.read()) eq_("test_data", fp.read())
def test_mode_is_passed_to_open(self, tmpdir): def test_mode_is_passed_to_open(self, tmpdir):
filepath = str(tmpdir.join("test.txt")) filepath = str(tmpdir.join(self.FILE_NAME))
open(filepath, "w").close() open(filepath, "w").close()
with FileOrPath(filepath, "a") as fp: with FileOrPath(filepath, "a") as fp:
eq_("a", fp.mode) eq_("a", fp.mode)

View File

@@ -230,8 +230,8 @@ def log_calls(func):
""" """
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
unifiedArgs = _unify_args(func, args, kwargs) unified_args = _unify_args(func, args, kwargs)
wrapper.calls.append(unifiedArgs) wrapper.calls.append(unified_args)
return func(*args, **kwargs) return func(*args, **kwargs)
wrapper.calls = [] wrapper.calls = []

View File

@@ -58,6 +58,7 @@ def get_locale_name(lang):
"it": "it_IT", "it": "it_IT",
"ja": "ja_JP", "ja": "ja_JP",
"ko": "ko_KR", "ko": "ko_KR",
"ms": "ms_MY",
"nl": "nl_NL", "nl": "nl_NL",
"pl_PL": "pl_PL", "pl_PL": "pl_PL",
"pt_BR": "pt_BR", "pt_BR": "pt_BR",
@@ -131,11 +132,11 @@ def install_gettext_trans(base_folder, lang):
def install_gettext_trans_under_cocoa(): def install_gettext_trans_under_cocoa():
from cocoa import proxy from cocoa import proxy
resFolder = proxy.getResourcePath() res_folder = proxy.getResourcePath()
baseFolder = op.join(resFolder, "locale") base_folder = op.join(res_folder, "locale")
currentLang = proxy.systemLang() current_lang = proxy.systemLang()
install_gettext_trans(baseFolder, currentLang) install_gettext_trans(base_folder, current_lang)
localename = get_locale_name(currentLang) localename = get_locale_name(current_lang)
if localename is not None: if localename is not None:
locale.setlocale(locale.LC_ALL, localename) locale.setlocale(locale.LC_ALL, localename)

View File

@@ -177,13 +177,13 @@ def pluralize(number, word, decimals=0, plural_word=None):
``plural_word``: If the plural rule for word is more complex than adding a 's', specify a plural ``plural_word``: If the plural rule for word is more complex than adding a 's', specify a plural
""" """
number = round(number, decimals) number = round(number, decimals)
format = "%%1.%df %%s" % decimals plural_format = "%%1.%df %%s" % decimals
if number > 1: if number > 1:
if plural_word is None: if plural_word is None:
word += "s" word += "s"
else: else:
word = plural_word word = plural_word
return format % (number, word) return plural_format % (number, word)
def format_time(seconds, with_hours=True): def format_time(seconds, with_hours=True):
@@ -226,7 +226,7 @@ def format_time_decimal(seconds):
SIZE_DESC = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB") SIZE_DESC = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
SIZE_VALS = tuple(1024 ** i for i in range(1, 9)) SIZE_VALS = tuple(1024**i for i in range(1, 9))
def format_size(size, decimal=0, forcepower=-1, showdesc=True): def format_size(size, decimal=0, forcepower=-1, showdesc=True):
@@ -252,16 +252,16 @@ def format_size(size, decimal=0, forcepower=-1, showdesc=True):
div = SIZE_VALS[i - 1] div = SIZE_VALS[i - 1]
else: else:
div = 1 div = 1
format = "%%%d.%df" % (decimal, decimal) size_format = "%%%d.%df" % (decimal, decimal)
negative = size < 0 negative = size < 0
divided_size = (0.0 + abs(size)) / div divided_size = (0.0 + abs(size)) / div
if decimal == 0: if decimal == 0:
divided_size = ceil(divided_size) divided_size = ceil(divided_size)
else: else:
divided_size = ceil(divided_size * (10 ** decimal)) / (10 ** decimal) divided_size = ceil(divided_size * (10**decimal)) / (10**decimal)
if negative: if negative:
divided_size *= -1 divided_size *= -1
result = format % divided_size result = size_format % divided_size
if showdesc: if showdesc:
result += " " + SIZE_DESC[i] result += " " + SIZE_DESC[i]
return result return result
@@ -292,7 +292,7 @@ def multi_replace(s, replace_from, replace_to=""):
the same length as ``replace_from``, it will be transformed into a list. the same length as ``replace_from``, it will be transformed into a list.
""" """
if isinstance(replace_to, str) and (len(replace_from) != len(replace_to)): if isinstance(replace_to, str) and (len(replace_from) != len(replace_to)):
replace_to = [replace_to for r in replace_from] replace_to = [replace_to for _ in replace_from]
if len(replace_from) != len(replace_to): if len(replace_from) != len(replace_to):
raise ValueError("len(replace_from) must be equal to len(replace_to)") raise ValueError("len(replace_from) must be equal to len(replace_to)")
replace = list(zip(replace_from, replace_to)) replace = list(zip(replace_from, replace_to))

View File

@@ -36,7 +36,7 @@ msgstr ""
msgid "Sending to Trash" msgid "Sending to Trash"
msgstr "" msgstr ""
#: core\app.py:290 #: core\app.py:289
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 ""
@@ -48,76 +48,84 @@ msgstr ""
msgid "All marked files were copied successfully." msgid "All marked files were copied successfully."
msgstr "" msgstr ""
#: core\app.py:316 #: core\app.py:317
msgid "All marked files were moved successfully." msgid "All marked files were moved successfully."
msgstr "" msgstr ""
#: core\app.py:317 #: core\app.py:319
msgid "All marked files were deleted successfully."
msgstr ""
#: core\app.py:321
msgid "All marked files were successfully sent to Trash." msgid "All marked files were successfully sent to Trash."
msgstr "" msgstr ""
#: core\app.py:323 #: core\app.py:326
msgid "Could not load file: {}" msgid "Could not load file: {}"
msgstr "" msgstr ""
#: core\app.py:379 #: core\app.py:382
msgid "'{}' already is in the list." msgid "'{}' already is in the list."
msgstr "" msgstr ""
#: core\app.py:381 #: core\app.py:384
msgid "'{}' does not exist." msgid "'{}' does not exist."
msgstr "" msgstr ""
#: core\app.py:389 #: core\app.py:392
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:463 #: core\app.py:469
msgid "Select a directory to copy marked files to" msgid "Select a directory to copy marked files to"
msgstr "" msgstr ""
#: core\app.py:465 #: core\app.py:471
msgid "Select a directory to move marked files to" msgid "Select a directory to move marked files to"
msgstr "" msgstr ""
#: core\app.py:504 #: core\app.py:510
msgid "Select a destination for your exported CSV" msgid "Select a destination for your exported CSV"
msgstr "" msgstr ""
#: core\app.py:510 core\app.py:764 core\app.py:774 #: core\app.py:516 core\app.py:771 core\app.py:781
msgid "Couldn't write to file: {}" msgid "Couldn't write to file: {}"
msgstr "" msgstr ""
#: core\app.py:533 #: core\app.py:539
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:691 core\app.py:703 #: core\app.py:695 core\app.py:707
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:739 #: core\app.py:743
msgid "{} duplicate groups were changed by the re-prioritization." msgid "{} duplicate groups were changed by the re-prioritization."
msgstr "" msgstr ""
#: core\app.py:783 #: core\app.py:790
msgid "The selected directories contain no scannable file." msgid "The selected directories contain no scannable file."
msgstr "" msgstr ""
#: core\app.py:796 #: core\app.py:803
msgid "Collecting files to scan" msgid "Collecting files to scan"
msgstr "" msgstr ""
#: core\app.py:843 #: core\app.py:850
msgid "%s (%d discarded)" msgid "%s (%d discarded)"
msgstr "" msgstr ""
#: core\engine.py:251 core\engine.py:294 #: core\directories.py:191
msgid "0 matches found" msgid "Collected {} files to scan"
msgstr "" msgstr ""
#: core\engine.py:269 core\engine.py:306 #: core\directories.py:207
msgid "%d matches found" msgid "Collected {} folders to scan"
msgstr ""
#: core\engine.py:27
msgid "%d matches found from %d groups"
msgstr "" msgstr ""
#: core\gui\deletion_options.py:71 #: core\gui\deletion_options.py:71
@@ -220,15 +228,15 @@ msgstr ""
msgid " filter: %s" msgid " filter: %s"
msgstr "" msgstr ""
#: core\scanner.py:85 #: core\scanner.py:90
msgid "Read size of %d/%d files" msgid "Read size of %d/%d files"
msgstr "" msgstr ""
#: core\scanner.py:109 #: core\scanner.py:116
msgid "Read metadata of %d/%d files" msgid "Read metadata of %d/%d files"
msgstr "" msgstr ""
#: core\scanner.py:147 #: core\scanner.py:154
msgid "Almost done! Fiddling with results..." msgid "Almost done! Fiddling with results..."
msgstr "" msgstr ""

View File

@@ -47,7 +47,7 @@ msgstr "Kopíruji"
msgid "Sending to Trash" msgid "Sending to Trash"
msgstr "Vyhazuji do koše" msgstr "Vyhazuji do koše"
#: core\app.py:308 #: core\app.py:289
msgid "" msgid ""
"A previous action is still hanging in there. You can't start a new one yet. " "A previous action is still hanging in there. You can't start a new one yet. "
"Wait a few seconds, then try again." "Wait a few seconds, then try again."
@@ -55,35 +55,39 @@ msgstr ""
"Předchozí akce stále nebyla ukončena. Novou zatím nemůžete spustit. Počkejte" "Předchozí akce stále nebyla ukončena. Novou zatím nemůžete spustit. Počkejte"
" pár sekund a zkuste to znovu." " pár sekund a zkuste to znovu."
#: core\app.py:318 #: core\app.py:300
msgid "No duplicates found." msgid "No duplicates found."
msgstr "Nebyli nalezeny žádné duplicity." msgstr "Nebyli nalezeny žádné duplicity."
#: core\app.py:333 #: core\app.py:315
msgid "All marked files were copied successfully." msgid "All marked files were copied successfully."
msgstr "Všechny označené soubory byly úspěšně zkopírovány." msgstr "Všechny označené soubory byly úspěšně zkopírovány."
#: core\app.py:334 #: core\app.py:317
msgid "All marked files were moved successfully." msgid "All marked files were moved successfully."
msgstr "Všechny označené soubory byly úspěšně přesunuty." msgstr "Všechny označené soubory byly úspěšně přesunuty."
#: core\app.py:335 #: core\app.py:319
msgid "All marked files were deleted successfully."
msgstr ""
#: core\app.py:321
msgid "All marked files were successfully sent to Trash." msgid "All marked files were successfully sent to Trash."
msgstr "Všechny označené soubory byly úspěšně odeslány do koše." msgstr "Všechny označené soubory byly úspěšně odeslány do koše."
#: core\app.py:343 #: core\app.py:326
msgid "Could not load file: {}" msgid "Could not load file: {}"
msgstr "Soubor se nepodařilo načíst: {}" msgstr "Soubor se nepodařilo načíst: {}"
#: core\app.py:399 #: core\app.py:382
msgid "'{}' already is in the list." msgid "'{}' already is in the list."
msgstr "'{}' již je v seznamu." msgstr "'{}' již je v seznamu."
#: core\app.py:401 #: core\app.py:384
msgid "'{}' does not exist." msgid "'{}' does not exist."
msgstr "'{}' neexistuje." msgstr "'{}' neexistuje."
#: core\app.py:410 #: core\app.py:392
msgid "" msgid ""
"All selected %d matches are going to be ignored in all subsequent scans. " "All selected %d matches are going to be ignored in all subsequent scans. "
"Continue?" "Continue?"
@@ -91,60 +95,64 @@ msgstr ""
"Všech %d vybraných shod bude v následujících hledáních ignorováno. " "Všech %d vybraných shod bude v následujících hledáních ignorováno. "
"Pokračovat?" "Pokračovat?"
#: core\app.py:486 #: core\app.py:469
msgid "Select a directory to copy marked files to" msgid "Select a directory to copy marked files to"
msgstr "Vyberte adresář, do kterého chcete zkopírovat označené soubory" msgstr "Vyberte adresář, do kterého chcete zkopírovat označené soubory"
#: core\app.py:487 #: core\app.py:471
msgid "Select a directory to move marked files to" msgid "Select a directory to move marked files to"
msgstr "Vyberte adresář, kam chcete přesunout označené soubory" msgstr "Vyberte adresář, kam chcete přesunout označené soubory"
#: core\app.py:527 #: core\app.py:510
msgid "Select a destination for your exported CSV" msgid "Select a destination for your exported CSV"
msgstr "Vyberte cíl pro exportovaný soubor CSV" msgstr "Vyberte cíl pro exportovaný soubor CSV"
#: core\app.py:534 core\app.py:801 core\app.py:811 #: core\app.py:516 core\app.py:771 core\app.py:781
msgid "Couldn't write to file: {}" msgid "Couldn't write to file: {}"
msgstr "Nelze zapisovat do souboru: {}" msgstr "Nelze zapisovat do souboru: {}"
#: core\app.py:559 #: core\app.py:539
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 ""
"Nedefinoval jste žádný uživatelský příkaz. Nadefinujete ho v předvolbách." "Nedefinoval jste žádný uživatelský příkaz. Nadefinujete ho v předvolbách."
#: core\app.py:727 core\app.py:740 #: core\app.py:695 core\app.py:707
msgid "You are about to remove %d files from results. Continue?" msgid "You are about to remove %d files from results. Continue?"
msgstr "Chystáte se z výsledků odstranit %d souborů. Pokračovat?" msgstr "Chystáte se z výsledků odstranit %d souborů. Pokračovat?"
#: core\app.py:774 #: core\app.py:743
msgid "{} duplicate groups were changed by the re-prioritization." msgid "{} duplicate groups were changed by the re-prioritization."
msgstr "{} duplicitní skupiny byly změněny změně priorit." msgstr "{} duplicitní skupiny byly změněny změně priorit."
#: core\app.py:821 #: core\app.py:790
msgid "The selected directories contain no scannable file." msgid "The selected directories contain no scannable file."
msgstr "Vybrané adresáře neobsahují žádné soubory vhodné k prohledávání." msgstr "Vybrané adresáře neobsahují žádné soubory vhodné k prohledávání."
#: core\app.py:835 #: core\app.py:803
msgid "Collecting files to scan" msgid "Collecting files to scan"
msgstr "Shromažďuji prohlížené soubory" msgstr "Shromažďuji prohlížené soubory"
#: core\app.py:891 #: core\app.py:850
msgid "%s (%d discarded)" msgid "%s (%d discarded)"
msgstr "%s (%d vyřazeno)" msgstr "%s (%d vyřazeno)"
#: core\engine.py:244 core\engine.py:288 #: core\directories.py:191
msgid "0 matches found" msgid "Collected {} files to scan"
msgstr "Nalezeno 0 shod" msgstr ""
#: core\engine.py:262 core\engine.py:296 #: core\directories.py:207
msgid "%d matches found" msgid "Collected {} folders to scan"
msgstr "Nalezeno %d shod" msgstr ""
#: core\gui\deletion_options.py:73 #: core\engine.py:27
msgid "%d matches found from %d groups"
msgstr ""
#: core\gui\deletion_options.py:71
msgid "You are sending {} file(s) to the Trash." msgid "You are sending {} file(s) to the Trash."
msgstr "Posíláte-{} soubory do koše." msgstr "Posíláte-{} soubory do koše."
#: core\gui\exclude_list_table.py:15 #: core\gui\exclude_list_table.py:14
msgid "Regular Expressions" msgid "Regular Expressions"
msgstr "Regulární výrazy" msgstr "Regulární výrazy"
@@ -176,15 +184,15 @@ msgstr "Obsah"
msgid "Analyzed %d/%d pictures" msgid "Analyzed %d/%d pictures"
msgstr "Analyzováno %d/%d snímků" msgstr "Analyzováno %d/%d snímků"
#: core\pe\matchblock.py:181 #: core\pe\matchblock.py:177
msgid "Performed %d/%d chunk matches" msgid "Performed %d/%d chunk matches"
msgstr "Provedeno %d/%d porovnání bloků" msgstr "Provedeno %d/%d porovnání bloků"
#: core\pe\matchblock.py:191 #: core\pe\matchblock.py:185
msgid "Preparing for matching" msgid "Preparing for matching"
msgstr "Připravuji porovnávání" msgstr "Připravuji porovnávání"
#: core\pe\matchblock.py:244 #: core\pe\matchblock.py:234
msgid "Verified %d/%d matches" msgid "Verified %d/%d matches"
msgstr "Ověřeno %d/%d shod" msgstr "Ověřeno %d/%d shod"
@@ -232,23 +240,23 @@ msgstr "Nejnovější"
msgid "Oldest" msgid "Oldest"
msgstr "Nejstarší" msgstr "Nejstarší"
#: core\results.py:142 #: core\results.py:134
msgid "%d / %d (%s / %s) duplicates marked." msgid "%d / %d (%s / %s) duplicates marked."
msgstr "%d / %d (%s / %s) duplicit označeno." msgstr "%d / %d (%s / %s) duplicit označeno."
#: core\results.py:149 #: core\results.py:141
msgid " filter: %s" msgid " filter: %s"
msgstr " filtr: %s" msgstr " filtr: %s"
#: core\scanner.py:85 #: core\scanner.py:90
msgid "Read size of %d/%d files" msgid "Read size of %d/%d files"
msgstr "Read size of %d/%d files" msgstr "Read size of %d/%d files"
#: core\scanner.py:109 #: core\scanner.py:116
msgid "Read metadata of %d/%d files" msgid "Read metadata of %d/%d files"
msgstr "Načtena metadata %d/%d souborů" msgstr "Načtena metadata %d/%d souborů"
#: core\scanner.py:147 #: core\scanner.py:154
msgid "Almost done! Fiddling with results..." msgid "Almost done! Fiddling with results..."
msgstr "Skoro hotovo! Fidlování s výsledky..." msgstr "Skoro hotovo! Fidlování s výsledky..."

View File

@@ -937,3 +937,43 @@ msgstr "Všeobecné"
#: qt\preferences_dialog.py:286 #: qt\preferences_dialog.py:286
msgid "Display" msgid "Display"
msgstr "Zobrazit" msgstr "Zobrazit"
#: qt\se\preferences_dialog.py:70
msgid "Partially hash files bigger than"
msgstr ""
#: qt\se\preferences_dialog.py:80
msgid "MB"
msgstr ""
#: qt\preferences_dialog.py:163
msgid "Use native OS dialogs"
msgstr ""
#: qt\preferences_dialog.py:166
msgid ""
"For actions such as file/folder selection use the OS native dialogs.\n"
"Some native dialogs have limited functionality."
msgstr ""
#: qt\se\preferences_dialog.py:68
msgid "Ignore files larger than"
msgstr ""
#: qt\app.py:135 qt\app.py:293
msgid "Clear Cache"
msgstr ""
#: qt\app.py:294
msgid ""
"Do you really want to clear the cache? This will remove all cached file "
"hashes and picture analysis."
msgstr ""
#: qt\app.py:299
msgid "Cache cleared."
msgstr ""
#: qt\preferences_dialog.py:173
msgid "Use dark style"
msgstr ""

View File

@@ -1,10 +1,11 @@
# Translators: # Translators:
# Andrew Senetar <arsenetar@gmail.com>, 2021 # Andrew Senetar <arsenetar@gmail.com>, 2021
# Fuan <jcfrt@posteo.net>, 2021 # Fuan <jcfrt@posteo.net>, 2021
# Robert M, 2021
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\n" "Last-Translator: Robert M, 2021\n"
"Language-Team: German (https://www.transifex.com/voltaicideas/teams/116153/de/)\n" "Language-Team: German (https://www.transifex.com/voltaicideas/teams/116153/de/)\n"
"Language: de\n" "Language: de\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@@ -47,7 +48,7 @@ msgstr "Kopiere"
msgid "Sending to Trash" msgid "Sending to Trash"
msgstr "Verschiebe in den Papierkorb" msgstr "Verschiebe in den Papierkorb"
#: core\app.py:308 #: core\app.py:289
msgid "" msgid ""
"A previous action is still hanging in there. You can't start a new one yet. " "A previous action is still hanging in there. You can't start a new one yet. "
"Wait a few seconds, then try again." "Wait a few seconds, then try again."
@@ -55,36 +56,40 @@ msgstr ""
"Eine vorherige Aktion ist noch in der Bearbeitung. Sie können noch keine " "Eine vorherige Aktion ist noch in der Bearbeitung. Sie können noch keine "
"Neue starten. Warten Sie einige Sekunden und versuchen es erneut." "Neue starten. Warten Sie einige Sekunden und versuchen es erneut."
#: core\app.py:318 #: core\app.py:300
msgid "No duplicates found." msgid "No duplicates found."
msgstr "Keine Duplikate gefunden." msgstr "Keine Duplikate gefunden."
#: core\app.py:333 #: core\app.py:315
msgid "All marked files were copied successfully." msgid "All marked files were copied successfully."
msgstr "Alle markierten Dateien wurden erfolgreich kopiert." msgstr "Alle markierten Dateien wurden erfolgreich kopiert."
#: core\app.py:334 #: core\app.py:317
msgid "All marked files were moved successfully." msgid "All marked files were moved successfully."
msgstr "Alle markierten Dateien wurden erfolgreich verschoben." msgstr "Alle markierten Dateien wurden erfolgreich verschoben."
#: core\app.py:335 #: core\app.py:319
msgid "All marked files were deleted successfully."
msgstr "Alle markierten Dateien wurden erfolgreich gelöscht."
#: core\app.py:321
msgid "All marked files were successfully sent to Trash." msgid "All marked files were successfully sent to Trash."
msgstr "" msgstr ""
"Alle markierten Dateien wurden erfolgreich in den Papierkorb verschoben." "Alle markierten Dateien wurden erfolgreich in den Papierkorb verschoben."
#: core\app.py:343 #: core\app.py:326
msgid "Could not load file: {}" msgid "Could not load file: {}"
msgstr "Konnte Datei {} nicht laden." msgstr "Konnte Datei {} nicht laden."
#: core\app.py:399 #: core\app.py:382
msgid "'{}' already is in the list." msgid "'{}' already is in the list."
msgstr "'{}' ist bereits in der Liste." msgstr "'{}' ist bereits in der Liste."
#: core\app.py:401 #: core\app.py:384
msgid "'{}' does not exist." msgid "'{}' does not exist."
msgstr "'{}' existiert nicht." msgstr "'{}' existiert nicht."
#: core\app.py:410 #: core\app.py:392
msgid "" msgid ""
"All selected %d matches are going to be ignored in all subsequent scans. " "All selected %d matches are going to be ignored in all subsequent scans. "
"Continue?" "Continue?"
@@ -92,65 +97,69 @@ msgstr ""
"Alle %d ausgewählten Dateien werden in zukünftigen Scans ignoriert. " "Alle %d ausgewählten Dateien werden in zukünftigen Scans ignoriert. "
"Fortfahren?" "Fortfahren?"
#: core\app.py:486 #: core\app.py:469
msgid "Select a directory to copy marked files to" msgid "Select a directory to copy marked files to"
msgstr "" msgstr ""
"Wählen Sie ein Verzeichnis aus, in das markierte Dateien kopiert werden " "Wählen Sie ein Verzeichnis aus, in das markierte Dateien kopiert werden "
"sollen" "sollen"
#: core\app.py:487 #: core\app.py:471
msgid "Select a directory to move marked files to" msgid "Select a directory to move marked files to"
msgstr "" msgstr ""
"Wählen Sie ein Verzeichnis aus, in das markierte Dateien verschoben werden " "Wählen Sie ein Verzeichnis aus, in das markierte Dateien verschoben werden "
"sollen" "sollen"
#: core\app.py:527 #: core\app.py:510
msgid "Select a destination for your exported CSV" msgid "Select a destination for your exported CSV"
msgstr "Zielverzeichnis für den CSV Export angeben" msgstr "Zielverzeichnis für den CSV Export angeben"
#: core\app.py:534 core\app.py:801 core\app.py:811 #: core\app.py:516 core\app.py:771 core\app.py:781
msgid "Couldn't write to file: {}" msgid "Couldn't write to file: {}"
msgstr "Konnte Datei {} nicht schreiben." msgstr "Konnte Datei {} nicht schreiben."
#: core\app.py:559 #: core\app.py:539
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 ""
"Sie haben noch keinen Befehl erstellt. Bitte dies in den Einstellungen vornehmen.\n" "Sie haben noch keinen Befehl erstellt. Bitte dies in den Einstellungen vornehmen.\n"
"Bsp.: \"C:\\Program Files\\Diff\\Diff.exe\" \"%d\" \"%r\"" "Bsp.: \"C:\\Program Files\\Diff\\Diff.exe\" \"%d\" \"%r\""
#: core\app.py:727 core\app.py:740 #: core\app.py:695 core\app.py:707
msgid "You are about to remove %d files from results. Continue?" msgid "You are about to remove %d files from results. Continue?"
msgstr "%d Dateien werden aus der Ergebnisliste entfernt. Fortfahren?" msgstr "%d Dateien werden aus der Ergebnisliste entfernt. Fortfahren?"
#: core\app.py:774 #: core\app.py:743
msgid "{} duplicate groups were changed by the re-prioritization." msgid "{} duplicate groups were changed by the re-prioritization."
msgstr "{} Duplikat-Gruppen wurden durch die Neu-Priorisierung geändert." msgstr "{} Duplikat-Gruppen wurden durch die Neu-Priorisierung geändert."
#: core\app.py:821 #: core\app.py:790
msgid "The selected directories contain no scannable file." msgid "The selected directories contain no scannable file."
msgstr "Ausgewählte Ordner enthalten keine scannbaren Dateien." msgstr "Ausgewählte Ordner enthalten keine scannbaren Dateien."
#: core\app.py:835 #: core\app.py:803
msgid "Collecting files to scan" msgid "Collecting files to scan"
msgstr "Sammle zu scannende Dateien..." msgstr "Sammle zu scannende Dateien..."
#: core\app.py:891 #: core\app.py:850
msgid "%s (%d discarded)" msgid "%s (%d discarded)"
msgstr "%s (%d verworfen)" msgstr "%s (%d verworfen)"
#: core\engine.py:244 core\engine.py:288 #: core\directories.py:191
msgid "0 matches found" msgid "Collected {} files to scan"
msgstr "0 Übereinstimmungen gefunden" msgstr "{} Dateien für Scan gesammelt"
#: core\engine.py:262 core\engine.py:296 #: core\directories.py:207
msgid "%d matches found" msgid "Collected {} folders to scan"
msgstr "%d Übereinstimmungen gefunden" msgstr "{} Ordner für Scan gesammelt"
#: core\gui\deletion_options.py:73 #: core\engine.py:27
msgid "%d matches found from %d groups"
msgstr "%d Treffer in %d Gruppen gefunden"
#: core\gui\deletion_options.py:71
msgid "You are sending {} file(s) to the Trash." msgid "You are sending {} file(s) to the Trash."
msgstr "Verschiebe {} Datei(en) in den Papierkorb." msgstr "Verschiebe {} Datei(en) in den Papierkorb."
#: core\gui\exclude_list_table.py:15 #: core\gui\exclude_list_table.py:14
msgid "Regular Expressions" msgid "Regular Expressions"
msgstr "Reguläre Ausdrücke" msgstr "Reguläre Ausdrücke"
@@ -182,15 +191,15 @@ msgstr "Inhalt"
msgid "Analyzed %d/%d pictures" msgid "Analyzed %d/%d pictures"
msgstr "Analysiere Bild %d/%d" msgstr "Analysiere Bild %d/%d"
#: core\pe\matchblock.py:181 #: core\pe\matchblock.py:177
msgid "Performed %d/%d chunk matches" msgid "Performed %d/%d chunk matches"
msgstr "%d/%d Chunk-Matches ausgeführt" msgstr "%d/%d Chunk-Matches ausgeführt"
#: core\pe\matchblock.py:191 #: core\pe\matchblock.py:185
msgid "Preparing for matching" msgid "Preparing for matching"
msgstr "Bereite Matching vor" msgstr "Bereite Matching vor"
#: core\pe\matchblock.py:244 #: core\pe\matchblock.py:234
msgid "Verified %d/%d matches" msgid "Verified %d/%d matches"
msgstr "%d/%d verifizierte Übereinstimmungen" msgstr "%d/%d verifizierte Übereinstimmungen"
@@ -238,23 +247,23 @@ msgstr "Neuste"
msgid "Oldest" msgid "Oldest"
msgstr "Älterste" msgstr "Älterste"
#: core\results.py:142 #: core\results.py:134
msgid "%d / %d (%s / %s) duplicates marked." msgid "%d / %d (%s / %s) duplicates marked."
msgstr "%d / %d (%s / %s) Duplikate markiert." msgstr "%d / %d (%s / %s) Duplikate markiert."
#: core\results.py:149 #: core\results.py:141
msgid " filter: %s" msgid " filter: %s"
msgstr " Filter: %s" msgstr " Filter: %s"
#: core\scanner.py:85 #: core\scanner.py:90
msgid "Read size of %d/%d files" msgid "Read size of %d/%d files"
msgstr "Lese Größe von %d/%d Dateien" msgstr "Lese Größe von %d/%d Dateien"
#: core\scanner.py:109 #: core\scanner.py:116
msgid "Read metadata of %d/%d files" msgid "Read metadata of %d/%d files"
msgstr "Lese Metadaten von %d/%d Dateien" msgstr "Lese Metadaten von %d/%d Dateien"
#: core\scanner.py:147 #: core\scanner.py:154
msgid "Almost done! Fiddling with results..." msgid "Almost done! Fiddling with results..."
msgstr "Fast fertig! Arrangiere Ergebnisse..." msgstr "Fast fertig! Arrangiere Ergebnisse..."

View File

@@ -1,10 +1,11 @@
# Translators: # Translators:
# Andrew Senetar <arsenetar@gmail.com>, 2021 # Andrew Senetar <arsenetar@gmail.com>, 2021
# Fuan <jcfrt@posteo.net>, 2021 # Fuan <jcfrt@posteo.net>, 2021
# Robert M, 2021
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\n" "Last-Translator: Robert M, 2021\n"
"Language-Team: German (https://www.transifex.com/voltaicideas/teams/116153/de/)\n" "Language-Team: German (https://www.transifex.com/voltaicideas/teams/116153/de/)\n"
"Language: de\n" "Language: de\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@@ -946,3 +947,45 @@ msgstr "Allgemeines"
#: qt\preferences_dialog.py:286 #: qt\preferences_dialog.py:286
msgid "Display" msgid "Display"
msgstr "Anzeige" msgstr "Anzeige"
#: qt\se\preferences_dialog.py:70
msgid "Partially hash files bigger than"
msgstr "Dateien partiell hashen die größer sind als"
#: qt\se\preferences_dialog.py:80
msgid "MB"
msgstr "MB"
#: qt\preferences_dialog.py:163
msgid "Use native OS dialogs"
msgstr "Benutzer System-eigene Dialoge"
#: qt\preferences_dialog.py:166
msgid ""
"For actions such as file/folder selection use the OS native dialogs.\n"
"Some native dialogs have limited functionality."
msgstr ""
"Benutzer System-eigene Dialoge für Aktionen wie Datei/Ordern-Auswahl\n"
"Manche System-eigene Dialoge sind in ihren Funktionen limitiert."
#: qt\se\preferences_dialog.py:68
msgid "Ignore files larger than"
msgstr "Ignoriere Dateien größer als"
#: qt\app.py:135 qt\app.py:293
msgid "Clear Cache"
msgstr ""
#: qt\app.py:294
msgid ""
"Do you really want to clear the cache? This will remove all cached file "
"hashes and picture analysis."
msgstr ""
#: qt\app.py:299
msgid "Cache cleared."
msgstr ""
#: qt\preferences_dialog.py:173
msgid "Use dark style"
msgstr ""

View File

@@ -48,7 +48,7 @@ msgstr "Αντιγραφή"
msgid "Sending to Trash" msgid "Sending to Trash"
msgstr "Αποστολή στα σκουπίδια" msgstr "Αποστολή στα σκουπίδια"
#: core\app.py:308 #: core\app.py:289
msgid "" msgid ""
"A previous action is still hanging in there. You can't start a new one yet. " "A previous action is still hanging in there. You can't start a new one yet. "
"Wait a few seconds, then try again." "Wait a few seconds, then try again."
@@ -56,94 +56,102 @@ msgstr ""
"Μια προηγούμενη ενέργεια είναι σε εξέλιξη. Δεν μπορείτε να ξεκινήσετε " "Μια προηγούμενη ενέργεια είναι σε εξέλιξη. Δεν μπορείτε να ξεκινήσετε "
"καινούργια ακόμα. Περιμένετε λίγα δευτερόλεπτα, έπειτα προσπαθήστε ξανά." "καινούργια ακόμα. Περιμένετε λίγα δευτερόλεπτα, έπειτα προσπαθήστε ξανά."
#: core\app.py:318 #: core\app.py:300
msgid "No duplicates found." msgid "No duplicates found."
msgstr "Δεν βρέθηκαν διπλότυπα." msgstr "Δεν βρέθηκαν διπλότυπα."
#: core\app.py:333 #: core\app.py:315
msgid "All marked files were copied successfully." msgid "All marked files were copied successfully."
msgstr "Όλα τα επιλεγμένα αρχεία αντιγράφηκαν επιτυχώς." msgstr "Όλα τα επιλεγμένα αρχεία αντιγράφηκαν επιτυχώς."
#: core\app.py:334 #: core\app.py:317
msgid "All marked files were moved successfully." msgid "All marked files were moved successfully."
msgstr "Όλα τα επιλεγμένα αρχεία μετακινήθηκαν επιτυχώς." msgstr "Όλα τα επιλεγμένα αρχεία μετακινήθηκαν επιτυχώς."
#: core\app.py:335 #: core\app.py:319
msgid "All marked files were deleted successfully."
msgstr ""
#: core\app.py:321
msgid "All marked files were successfully sent to Trash." msgid "All marked files were successfully sent to Trash."
msgstr "Όλα τα επιλεγμένα αρχεία στάλθηκαν με επιτυχία στον κάδο." msgstr "Όλα τα επιλεγμένα αρχεία στάλθηκαν με επιτυχία στον κάδο."
#: core\app.py:343 #: core\app.py:326
msgid "Could not load file: {}" msgid "Could not load file: {}"
msgstr "Δεν ήταν δυνατή η φόρτωση του αρχείου: {}" msgstr "Δεν ήταν δυνατή η φόρτωση του αρχείου: {}"
#: core\app.py:399 #: core\app.py:382
msgid "'{}' already is in the list." msgid "'{}' already is in the list."
msgstr "'{}' υπάρχει ήδη στη λίστα." msgstr "'{}' υπάρχει ήδη στη λίστα."
#: core\app.py:401 #: core\app.py:384
msgid "'{}' does not exist." msgid "'{}' does not exist."
msgstr "'{}' δεν υπάρχει." msgstr "'{}' δεν υπάρχει."
#: core\app.py:410 #: core\app.py:392
msgid "" msgid ""
"All selected %d matches are going to be ignored in all subsequent scans. " "All selected %d matches are going to be ignored in all subsequent scans. "
"Continue?" "Continue?"
msgstr "" msgstr ""
"Όλα τα επιλεγμένα %d στοιχεία θα αγνοηθούν σε μελλοντικές σαρώσεις.Συνέχεια;" "Όλα τα επιλεγμένα %d στοιχεία θα αγνοηθούν σε μελλοντικές σαρώσεις.Συνέχεια;"
#: core\app.py:486 #: core\app.py:469
msgid "Select a directory to copy marked files to" msgid "Select a directory to copy marked files to"
msgstr "Επιλέξτε έναν κατάλογο για να αντιγράψετε επισημασμένα αρχεία." msgstr "Επιλέξτε έναν κατάλογο για να αντιγράψετε επισημασμένα αρχεία."
#: core\app.py:487 #: core\app.py:471
msgid "Select a directory to move marked files to" msgid "Select a directory to move marked files to"
msgstr "Επιλέξτε έναν κατάλογο για να μετακινήσετε τα επισημασμένα αρχεία." msgstr "Επιλέξτε έναν κατάλογο για να μετακινήσετε τα επισημασμένα αρχεία."
#: core\app.py:527 #: core\app.py:510
msgid "Select a destination for your exported CSV" msgid "Select a destination for your exported CSV"
msgstr "Επιλέξτε έναν προορισμό για το εξαγόμενο CSV σας" msgstr "Επιλέξτε έναν προορισμό για το εξαγόμενο CSV σας"
#: core\app.py:534 core\app.py:801 core\app.py:811 #: core\app.py:516 core\app.py:771 core\app.py:781
msgid "Couldn't write to file: {}" msgid "Couldn't write to file: {}"
msgstr "Δεν ήταν δυνατή η εγγραφή στο αρχείο: {}" msgstr "Δεν ήταν δυνατή η εγγραφή στο αρχείο: {}"
#: core\app.py:559 #: core\app.py:539
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:727 core\app.py:740 #: core\app.py:695 core\app.py:707
msgid "You are about to remove %d files from results. Continue?" msgid "You are about to remove %d files from results. Continue?"
msgstr "Πρόκειται να αφαιρέσετε %d αρχεία από τα αποτελέσματα. Συνέχεια;" msgstr "Πρόκειται να αφαιρέσετε %d αρχεία από τα αποτελέσματα. Συνέχεια;"
#: core\app.py:774 #: core\app.py:743
msgid "{} duplicate groups were changed by the re-prioritization." msgid "{} duplicate groups were changed by the re-prioritization."
msgstr "{} ομάδες διπλοτύπων άλλαξαν από το επαναπροσδιορισμό." msgstr "{} ομάδες διπλοτύπων άλλαξαν από το επαναπροσδιορισμό."
#: core\app.py:821 #: core\app.py:790
msgid "The selected directories contain no scannable file." msgid "The selected directories contain no scannable file."
msgstr "Οι επιλεγμένοι φάκελοι δεν περιέχουν σαρώσιμα αρχεία." msgstr "Οι επιλεγμένοι φάκελοι δεν περιέχουν σαρώσιμα αρχεία."
#: core\app.py:835 #: core\app.py:803
msgid "Collecting files to scan" msgid "Collecting files to scan"
msgstr "Συλλογή αρχείων για σάρωση" msgstr "Συλλογή αρχείων για σάρωση"
#: core\app.py:891 #: core\app.py:850
msgid "%s (%d discarded)" msgid "%s (%d discarded)"
msgstr "%s (%d απορρίφθηκαν)" msgstr "%s (%d απορρίφθηκαν)"
#: core\engine.py:244 core\engine.py:288 #: core\directories.py:191
msgid "0 matches found" msgid "Collected {} files to scan"
msgstr "0 διπλότυπα βρέθηκαν" msgstr ""
#: core\engine.py:262 core\engine.py:296 #: core\directories.py:207
msgid "%d matches found" msgid "Collected {} folders to scan"
msgstr "Βρέθηκαν %d διπλότυπα" msgstr ""
#: core\gui\deletion_options.py:73 #: core\engine.py:27
msgid "%d matches found from %d groups"
msgstr ""
#: core\gui\deletion_options.py:71
msgid "You are sending {} file(s) to the Trash." msgid "You are sending {} file(s) to the Trash."
msgstr "Στέλνετε {} αρχεία στα σκουπίδια." msgstr "Στέλνετε {} αρχεία στα σκουπίδια."
#: core\gui\exclude_list_table.py:15 #: core\gui\exclude_list_table.py:14
msgid "Regular Expressions" msgid "Regular Expressions"
msgstr "Κανονικές εκφράσεις" msgstr "Κανονικές εκφράσεις"
@@ -175,15 +183,15 @@ msgstr "Περιεχόμενα"
msgid "Analyzed %d/%d pictures" msgid "Analyzed %d/%d pictures"
msgstr "Ανάλυση %d/%d εικόνων" msgstr "Ανάλυση %d/%d εικόνων"
#: core\pe\matchblock.py:181 #: core\pe\matchblock.py:177
msgid "Performed %d/%d chunk matches" msgid "Performed %d/%d chunk matches"
msgstr "Εκτέλεση %d/%d μερικής ταυτοποίησης" msgstr "Εκτέλεση %d/%d μερικής ταυτοποίησης"
#: core\pe\matchblock.py:191 #: core\pe\matchblock.py:185
msgid "Preparing for matching" msgid "Preparing for matching"
msgstr "Προετοιμασία για σύγκριση" msgstr "Προετοιμασία για σύγκριση"
#: core\pe\matchblock.py:244 #: core\pe\matchblock.py:234
msgid "Verified %d/%d matches" msgid "Verified %d/%d matches"
msgstr "Πιστοποίηση %d/%d ταυτόσημων" msgstr "Πιστοποίηση %d/%d ταυτόσημων"
@@ -231,23 +239,23 @@ msgstr "Νεώτερο"
msgid "Oldest" msgid "Oldest"
msgstr "Παλαιότερο" msgstr "Παλαιότερο"
#: core\results.py:142 #: core\results.py:134
msgid "%d / %d (%s / %s) duplicates marked." msgid "%d / %d (%s / %s) duplicates marked."
msgstr "%d / %d (%s / %s) επιλεγμένα διπλότυπα." msgstr "%d / %d (%s / %s) επιλεγμένα διπλότυπα."
#: core\results.py:149 #: core\results.py:141
msgid " filter: %s" msgid " filter: %s"
msgstr " φίλτρο: %s" msgstr " φίλτρο: %s"
#: core\scanner.py:85 #: core\scanner.py:90
msgid "Read size of %d/%d files" msgid "Read size of %d/%d files"
msgstr "Ανάγνωση μεγέθους %d/%d αρχείων" msgstr "Ανάγνωση μεγέθους %d/%d αρχείων"
#: core\scanner.py:109 #: core\scanner.py:116
msgid "Read metadata of %d/%d files" msgid "Read metadata of %d/%d files"
msgstr "Ανάγνωση μεταδεδομένων των %d/%d αρχείων" msgstr "Ανάγνωση μεταδεδομένων των %d/%d αρχείων"
#: core\scanner.py:147 #: core\scanner.py:154
msgid "Almost done! Fiddling with results..." msgid "Almost done! Fiddling with results..."
msgstr "Σχεδόν τελείωσα! Παιχνίδι με αποτελέσματα ..." msgstr "Σχεδόν τελείωσα! Παιχνίδι με αποτελέσματα ..."

View File

@@ -954,3 +954,43 @@ msgstr "Γενικός"
#: qt\preferences_dialog.py:286 #: qt\preferences_dialog.py:286
msgid "Display" msgid "Display"
msgstr "Απεικόνιση" msgstr "Απεικόνιση"
#: qt\se\preferences_dialog.py:70
msgid "Partially hash files bigger than"
msgstr ""
#: qt\se\preferences_dialog.py:80
msgid "MB"
msgstr ""
#: qt\preferences_dialog.py:163
msgid "Use native OS dialogs"
msgstr ""
#: qt\preferences_dialog.py:166
msgid ""
"For actions such as file/folder selection use the OS native dialogs.\n"
"Some native dialogs have limited functionality."
msgstr ""
#: qt\se\preferences_dialog.py:68
msgid "Ignore files larger than"
msgstr ""
#: qt\app.py:135 qt\app.py:293
msgid "Clear Cache"
msgstr ""
#: qt\app.py:294
msgid ""
"Do you really want to clear the cache? This will remove all cached file "
"hashes and picture analysis."
msgstr ""
#: qt\app.py:299
msgid "Cache cleared."
msgstr ""
#: qt\preferences_dialog.py:173
msgid "Use dark style"
msgstr ""

View File

@@ -47,7 +47,7 @@ msgstr "Copiando"
msgid "Sending to Trash" msgid "Sending to Trash"
msgstr "Enviando a la Papelera" msgstr "Enviando a la Papelera"
#: core\app.py:308 #: core\app.py:289
msgid "" msgid ""
"A previous action is still hanging in there. You can't start a new one yet. " "A previous action is still hanging in there. You can't start a new one yet. "
"Wait a few seconds, then try again." "Wait a few seconds, then try again."
@@ -55,36 +55,40 @@ msgstr ""
"Una acción previa sigue ejecutándose. No puede abrir una nueva todavía. " "Una acción previa sigue ejecutándose. No puede abrir una nueva todavía. "
"Espere unos segundos y vuelva a intentarlo." "Espere unos segundos y vuelva a intentarlo."
#: core\app.py:318 #: core\app.py:300
msgid "No duplicates found." msgid "No duplicates found."
msgstr "No se han encontrado duplicados." msgstr "No se han encontrado duplicados."
#: core\app.py:333 #: core\app.py:315
msgid "All marked files were copied successfully." msgid "All marked files were copied successfully."
msgstr "" msgstr ""
"Todos los ficheros seleccionados han sido copiados satisfactoriamente." "Todos los ficheros seleccionados han sido copiados satisfactoriamente."
#: core\app.py:334 #: core\app.py:317
msgid "All marked files were moved successfully." msgid "All marked files were moved successfully."
msgstr "Todos los ficheros seleccionados se han movidos satisfactoriamente." msgstr "Todos los ficheros seleccionados se han movidos satisfactoriamente."
#: core\app.py:335 #: core\app.py:319
msgid "All marked files were deleted successfully."
msgstr ""
#: core\app.py:321
msgid "All marked files were successfully sent to Trash." msgid "All marked files were successfully sent to Trash."
msgstr "Todo los ficheros marcados se han enviado a la papelera exitosamente." msgstr "Todo los ficheros marcados se han enviado a la papelera exitosamente."
#: core\app.py:343 #: core\app.py:326
msgid "Could not load file: {}" msgid "Could not load file: {}"
msgstr "No se pudo cargar el archivo: {}" msgstr "No se pudo cargar el archivo: {}"
#: core\app.py:399 #: core\app.py:382
msgid "'{}' already is in the list." msgid "'{}' already is in the list."
msgstr "'{}' ya está en la lista." msgstr "'{}' ya está en la lista."
#: core\app.py:401 #: core\app.py:384
msgid "'{}' does not exist." msgid "'{}' does not exist."
msgstr "'{}' no existe." msgstr "'{}' no existe."
#: core\app.py:410 #: core\app.py:392
msgid "" msgid ""
"All selected %d matches are going to be ignored in all subsequent scans. " "All selected %d matches are going to be ignored in all subsequent scans. "
"Continue?" "Continue?"
@@ -92,59 +96,63 @@ msgstr ""
"Todas las %d coincidencias seleccionadas van a ser ignoradas en las " "Todas las %d coincidencias seleccionadas van a ser ignoradas en las "
"subsiguientes exploraciones. ¿Continuar?" "subsiguientes exploraciones. ¿Continuar?"
#: core\app.py:486 #: core\app.py:469
msgid "Select a directory to copy marked files to" msgid "Select a directory to copy marked files to"
msgstr "Seleccione un directorio donde desee copiar los archivos marcados" msgstr "Seleccione un directorio donde desee copiar los archivos marcados"
#: core\app.py:487 #: core\app.py:471
msgid "Select a directory to move marked files to" msgid "Select a directory to move marked files to"
msgstr "Seleccione un directorio al que desee mover los archivos marcados" msgstr "Seleccione un directorio al que desee mover los archivos marcados"
#: core\app.py:527 #: core\app.py:510
msgid "Select a destination for your exported CSV" msgid "Select a destination for your exported CSV"
msgstr "Seleccionar un destino para el CSV seleccionado" msgstr "Seleccionar un destino para el CSV seleccionado"
#: core\app.py:534 core\app.py:801 core\app.py:811 #: core\app.py:516 core\app.py:771 core\app.py:781
msgid "Couldn't write to file: {}" msgid "Couldn't write to file: {}"
msgstr "No se pudo escribir en el archivo: {}" msgstr "No se pudo escribir en el archivo: {}"
#: core\app.py:559 #: core\app.py:539
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 "No hay comandos configurados. Establézcalos en sus preferencias." msgstr "No hay comandos configurados. Establézcalos en sus preferencias."
#: core\app.py:727 core\app.py:740 #: core\app.py:695 core\app.py:707
msgid "You are about to remove %d files from results. Continue?" msgid "You are about to remove %d files from results. Continue?"
msgstr "Está a punto de eliminar %d ficheros de resultados. ¿Continuar?" msgstr "Está a punto de eliminar %d ficheros de resultados. ¿Continuar?"
#: core\app.py:774 #: core\app.py:743
msgid "{} duplicate groups were changed by the re-prioritization." msgid "{} duplicate groups were changed by the re-prioritization."
msgstr "{} grupos de duplicados han sido cambiados por la re-priorización" msgstr "{} grupos de duplicados han sido cambiados por la re-priorización"
#: core\app.py:821 #: core\app.py:790
msgid "The selected directories contain no scannable file." msgid "The selected directories contain no scannable file."
msgstr "Las carpetas seleccionadas no contienen ficheros para explorar." msgstr "Las carpetas seleccionadas no contienen ficheros para explorar."
#: core\app.py:835 #: core\app.py:803
msgid "Collecting files to scan" msgid "Collecting files to scan"
msgstr "Recopilando ficheros a explorar" msgstr "Recopilando ficheros a explorar"
#: core\app.py:891 #: core\app.py:850
msgid "%s (%d discarded)" msgid "%s (%d discarded)"
msgstr "%s (%d descartados)" msgstr "%s (%d descartados)"
#: core\engine.py:244 core\engine.py:288 #: core\directories.py:191
msgid "0 matches found" msgid "Collected {} files to scan"
msgstr "0 coincidencias" msgstr ""
#: core\engine.py:262 core\engine.py:296 #: core\directories.py:207
msgid "%d matches found" msgid "Collected {} folders to scan"
msgstr "%d coincidencias encontradas" msgstr ""
#: core\gui\deletion_options.py:73 #: core\engine.py:27
msgid "%d matches found from %d groups"
msgstr ""
#: core\gui\deletion_options.py:71
msgid "You are sending {} file(s) to the Trash." msgid "You are sending {} file(s) to the Trash."
msgstr "Enviando {} fichero(s) a la Papelera" msgstr "Enviando {} fichero(s) a la Papelera"
#: core\gui\exclude_list_table.py:15 #: core\gui\exclude_list_table.py:14
msgid "Regular Expressions" msgid "Regular Expressions"
msgstr "Expresiones regulares" msgstr "Expresiones regulares"
@@ -177,15 +185,15 @@ msgstr "Contenido"
msgid "Analyzed %d/%d pictures" msgid "Analyzed %d/%d pictures"
msgstr "Analizadas %d/%d imágenes" msgstr "Analizadas %d/%d imágenes"
#: core\pe\matchblock.py:181 #: core\pe\matchblock.py:177
msgid "Performed %d/%d chunk matches" msgid "Performed %d/%d chunk matches"
msgstr "Realizado %d/%d trozos coincidentes" msgstr "Realizado %d/%d trozos coincidentes"
#: core\pe\matchblock.py:191 #: core\pe\matchblock.py:185
msgid "Preparing for matching" msgid "Preparing for matching"
msgstr "Preparando para coincidencias" msgstr "Preparando para coincidencias"
#: core\pe\matchblock.py:244 #: core\pe\matchblock.py:234
msgid "Verified %d/%d matches" msgid "Verified %d/%d matches"
msgstr "Verificadas %d/%d coincidencias" msgstr "Verificadas %d/%d coincidencias"
@@ -233,23 +241,23 @@ msgstr "El más nuevo"
msgid "Oldest" msgid "Oldest"
msgstr "El más antiguo" msgstr "El más antiguo"
#: core\results.py:142 #: core\results.py:134
msgid "%d / %d (%s / %s) duplicates marked." msgid "%d / %d (%s / %s) duplicates marked."
msgstr "%d / %d (%s / %s) duplicados marcados." msgstr "%d / %d (%s / %s) duplicados marcados."
#: core\results.py:149 #: core\results.py:141
msgid " filter: %s" msgid " filter: %s"
msgstr "filtro: %s" msgstr "filtro: %s"
#: core\scanner.py:85 #: core\scanner.py:90
msgid "Read size of %d/%d files" msgid "Read size of %d/%d files"
msgstr "Tamaño de lectura de %d/%d ficheros" msgstr "Tamaño de lectura de %d/%d ficheros"
#: core\scanner.py:109 #: core\scanner.py:116
msgid "Read metadata of %d/%d files" msgid "Read metadata of %d/%d files"
msgstr "Leyendo metadatos de %d/%d ficheros" msgstr "Leyendo metadatos de %d/%d ficheros"
#: core\scanner.py:147 #: core\scanner.py:154
msgid "Almost done! Fiddling with results..." msgid "Almost done! Fiddling with results..."
msgstr "¡Casi termino! Jugando con los resultados..." msgstr "¡Casi termino! Jugando con los resultados..."

View File

@@ -947,3 +947,43 @@ msgstr "General"
#: qt\preferences_dialog.py:286 #: qt\preferences_dialog.py:286
msgid "Display" msgid "Display"
msgstr "Visualización" msgstr "Visualización"
#: qt\se\preferences_dialog.py:70
msgid "Partially hash files bigger than"
msgstr ""
#: qt\se\preferences_dialog.py:80
msgid "MB"
msgstr ""
#: qt\preferences_dialog.py:163
msgid "Use native OS dialogs"
msgstr ""
#: qt\preferences_dialog.py:166
msgid ""
"For actions such as file/folder selection use the OS native dialogs.\n"
"Some native dialogs have limited functionality."
msgstr ""
#: qt\se\preferences_dialog.py:68
msgid "Ignore files larger than"
msgstr ""
#: qt\app.py:135 qt\app.py:293
msgid "Clear Cache"
msgstr ""
#: qt\app.py:294
msgid ""
"Do you really want to clear the cache? This will remove all cached file "
"hashes and picture analysis."
msgstr ""
#: qt\app.py:299
msgid "Cache cleared."
msgstr ""
#: qt\preferences_dialog.py:173
msgid "Use dark style"
msgstr ""

View File

@@ -47,7 +47,7 @@ msgstr "Copie en cours"
msgid "Sending to Trash" msgid "Sending to Trash"
msgstr "Envoi de fichiers à la corbeille" msgstr "Envoi de fichiers à la corbeille"
#: core\app.py:308 #: core\app.py:289
msgid "" msgid ""
"A previous action is still hanging in there. You can't start a new one yet. " "A previous action is still hanging in there. You can't start a new one yet. "
"Wait a few seconds, then try again." "Wait a few seconds, then try again."
@@ -55,95 +55,103 @@ msgstr ""
"Une action précédente est encore en cours. Attendez quelques secondes avant " "Une action précédente est encore en cours. Attendez quelques secondes avant "
"d'en repartir une nouvelle." "d'en repartir une nouvelle."
#: core\app.py:318 #: core\app.py:300
msgid "No duplicates found." msgid "No duplicates found."
msgstr "Aucun doublon trouvé." msgstr "Aucun doublon trouvé."
#: core\app.py:333 #: core\app.py:315
msgid "All marked files were copied successfully." msgid "All marked files were copied successfully."
msgstr "Tous les fichiers marqués ont été copiés correctement." msgstr "Tous les fichiers marqués ont été copiés correctement."
#: core\app.py:334 #: core\app.py:317
msgid "All marked files were moved successfully." msgid "All marked files were moved successfully."
msgstr "Tous les fichiers marqués ont été déplacés correctement." msgstr "Tous les fichiers marqués ont été déplacés correctement."
#: core\app.py:335 #: core\app.py:319
msgid "All marked files were deleted successfully."
msgstr ""
#: core\app.py:321
msgid "All marked files were successfully sent to Trash." msgid "All marked files were successfully sent to Trash."
msgstr "" msgstr ""
"Tous les fichiers marqués ont été correctement envoyés à la corbeille." "Tous les fichiers marqués ont été correctement envoyés à la corbeille."
#: core\app.py:343 #: core\app.py:326
msgid "Could not load file: {}" msgid "Could not load file: {}"
msgstr "Impossible d'ouvrir le fichier: {}" msgstr "Impossible d'ouvrir le fichier: {}"
#: core\app.py:399 #: core\app.py:382
msgid "'{}' already is in the list." msgid "'{}' already is in the list."
msgstr "'{}' est déjà dans la liste." msgstr "'{}' est déjà dans la liste."
#: core\app.py:401 #: core\app.py:384
msgid "'{}' does not exist." msgid "'{}' does not exist."
msgstr "'{}' n'existe pas." msgstr "'{}' n'existe pas."
#: core\app.py:410 #: core\app.py:392
msgid "" msgid ""
"All selected %d matches are going to be ignored in all subsequent scans. " "All selected %d matches are going to be ignored in all subsequent scans. "
"Continue?" "Continue?"
msgstr "%d fichiers seront ignorés des prochains scans. Continuer?" msgstr "%d fichiers seront ignorés des prochains scans. Continuer?"
#: core\app.py:486 #: core\app.py:469
msgid "Select a directory to copy marked files to" msgid "Select a directory to copy marked files to"
msgstr "Sélectionnez un dossier vers lequel copier les fichiers marqués." msgstr "Sélectionnez un dossier vers lequel copier les fichiers marqués."
#: core\app.py:487 #: core\app.py:471
msgid "Select a directory to move marked files to" msgid "Select a directory to move marked files to"
msgstr "Sélectionnez un dossier vers lequel déplacer les fichiers marqués." msgstr "Sélectionnez un dossier vers lequel déplacer les fichiers marqués."
#: core\app.py:527 #: core\app.py:510
msgid "Select a destination for your exported CSV" msgid "Select a destination for your exported CSV"
msgstr "Choisissez une destination pour votre exportation CSV" msgstr "Choisissez une destination pour votre exportation CSV"
#: core\app.py:534 core\app.py:801 core\app.py:811 #: core\app.py:516 core\app.py:771 core\app.py:781
msgid "Couldn't write to file: {}" msgid "Couldn't write to file: {}"
msgstr "Impossible d'écrire le fichier: {}" msgstr "Impossible d'écrire le fichier: {}"
#: core\app.py:559 #: core\app.py:539
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 ""
"Vous n'avez pas de commande personnalisée. Ajoutez-la dans vos préférences." "Vous n'avez pas de commande personnalisée. Ajoutez-la dans vos préférences."
#: core\app.py:727 core\app.py:740 #: core\app.py:695 core\app.py:707
msgid "You are about to remove %d files from results. Continue?" msgid "You are about to remove %d files from results. Continue?"
msgstr "%d fichiers seront retirés des résultats. Continuer?" msgstr "%d fichiers seront retirés des résultats. Continuer?"
#: core\app.py:774 #: core\app.py:743
msgid "{} duplicate groups were changed by the re-prioritization." msgid "{} duplicate groups were changed by the re-prioritization."
msgstr "{} groupes de doublons ont été modifiés par la re-prioritisation." msgstr "{} groupes de doublons ont été modifiés par la re-prioritisation."
#: core\app.py:821 #: core\app.py:790
msgid "The selected directories contain no scannable file." msgid "The selected directories contain no scannable file."
msgstr "Les dossiers sélectionnés ne contiennent pas de fichiers valides." msgstr "Les dossiers sélectionnés ne contiennent pas de fichiers valides."
#: core\app.py:835 #: core\app.py:803
msgid "Collecting files to scan" msgid "Collecting files to scan"
msgstr "Collecte des fichiers à scanner" msgstr "Collecte des fichiers à scanner"
#: core\app.py:891 #: core\app.py:850
msgid "%s (%d discarded)" msgid "%s (%d discarded)"
msgstr "%s (%d hors-groupe)" msgstr "%s (%d hors-groupe)"
#: core\engine.py:244 core\engine.py:288 #: core\directories.py:191
msgid "0 matches found" msgid "Collected {} files to scan"
msgstr "0 paires trouvées" msgstr ""
#: core\engine.py:262 core\engine.py:296 #: core\directories.py:207
msgid "%d matches found" msgid "Collected {} folders to scan"
msgstr "%d paires trouvées" msgstr ""
#: core\gui\deletion_options.py:73 #: core\engine.py:27
msgid "%d matches found from %d groups"
msgstr ""
#: core\gui\deletion_options.py:71
msgid "You are sending {} file(s) to the Trash." msgid "You are sending {} file(s) to the Trash."
msgstr "Vous envoyez {} fichier(s) à la corbeille." msgstr "Vous envoyez {} fichier(s) à la corbeille."
#: core\gui\exclude_list_table.py:15 #: core\gui\exclude_list_table.py:14
msgid "Regular Expressions" msgid "Regular Expressions"
msgstr "Expressions régulières" msgstr "Expressions régulières"
@@ -177,15 +185,15 @@ msgstr "Contenu"
msgid "Analyzed %d/%d pictures" msgid "Analyzed %d/%d pictures"
msgstr "Analyzé %d/%d images" msgstr "Analyzé %d/%d images"
#: core\pe\matchblock.py:181 #: core\pe\matchblock.py:177
msgid "Performed %d/%d chunk matches" msgid "Performed %d/%d chunk matches"
msgstr "%d/%d blocs d'images comparés" msgstr "%d/%d blocs d'images comparés"
#: core\pe\matchblock.py:191 #: core\pe\matchblock.py:185
msgid "Preparing for matching" msgid "Preparing for matching"
msgstr "Préparation pour la comparaison" msgstr "Préparation pour la comparaison"
#: core\pe\matchblock.py:244 #: core\pe\matchblock.py:234
msgid "Verified %d/%d matches" msgid "Verified %d/%d matches"
msgstr "Vérifié %d/%d paires" msgstr "Vérifié %d/%d paires"
@@ -233,23 +241,23 @@ msgstr "Plus récent"
msgid "Oldest" msgid "Oldest"
msgstr "Moins récent" msgstr "Moins récent"
#: core\results.py:142 #: core\results.py:134
msgid "%d / %d (%s / %s) duplicates marked." msgid "%d / %d (%s / %s) duplicates marked."
msgstr "%d / %d (%s / %s) doublons marqués." msgstr "%d / %d (%s / %s) doublons marqués."
#: core\results.py:149 #: core\results.py:141
msgid " filter: %s" msgid " filter: %s"
msgstr " filtre: %s" msgstr " filtre: %s"
#: core\scanner.py:85 #: core\scanner.py:90
msgid "Read size of %d/%d files" msgid "Read size of %d/%d files"
msgstr "Lu la taille de %d/%d fichiers" msgstr "Lu la taille de %d/%d fichiers"
#: core\scanner.py:109 #: core\scanner.py:116
msgid "Read metadata of %d/%d files" msgid "Read metadata of %d/%d files"
msgstr "Lu les métadonnées de %d/%d fichiers" msgstr "Lu les métadonnées de %d/%d fichiers"
#: core\scanner.py:147 #: core\scanner.py:154
msgid "Almost done! Fiddling with results..." msgid "Almost done! Fiddling with results..."
msgstr "Bientôt terminé! Bidouille des résultats..." msgstr "Bientôt terminé! Bidouille des résultats..."

View File

@@ -942,3 +942,43 @@ msgstr "Général"
#: qt\preferences_dialog.py:286 #: qt\preferences_dialog.py:286
msgid "Display" msgid "Display"
msgstr "Affichage" msgstr "Affichage"
#: qt\se\preferences_dialog.py:70
msgid "Partially hash files bigger than"
msgstr ""
#: qt\se\preferences_dialog.py:80
msgid "MB"
msgstr ""
#: qt\preferences_dialog.py:163
msgid "Use native OS dialogs"
msgstr ""
#: qt\preferences_dialog.py:166
msgid ""
"For actions such as file/folder selection use the OS native dialogs.\n"
"Some native dialogs have limited functionality."
msgstr ""
#: qt\se\preferences_dialog.py:68
msgid "Ignore files larger than"
msgstr ""
#: qt\app.py:135 qt\app.py:293
msgid "Clear Cache"
msgstr ""
#: qt\app.py:294
msgid ""
"Do you really want to clear the cache? This will remove all cached file "
"hashes and picture analysis."
msgstr ""
#: qt\app.py:299
msgid "Cache cleared."
msgstr ""
#: qt\preferences_dialog.py:173
msgid "Use dark style"
msgstr ""

View File

@@ -48,7 +48,7 @@ msgstr "Պատճենվում է"
msgid "Sending to Trash" msgid "Sending to Trash"
msgstr "Ուղարկվում է Աղբարկղ" msgstr "Ուղարկվում է Աղբարկղ"
#: core\app.py:308 #: core\app.py:289
msgid "" msgid ""
"A previous action is still hanging in there. You can't start a new one yet. " "A previous action is still hanging in there. You can't start a new one yet. "
"Wait a few seconds, then try again." "Wait a few seconds, then try again."
@@ -56,95 +56,103 @@ msgstr ""
"Նախորդ գործողությունը դեռևս ձեռադրում է այստեղ: Չեք կարող սկսել մեկ ուրիշը: " "Նախորդ գործողությունը դեռևս ձեռադրում է այստեղ: Չեք կարող սկսել մեկ ուրիշը: "
"Սպասեք մի քանի վայրկյան և կրկին փորձեք:" "Սպասեք մի քանի վայրկյան և կրկին փորձեք:"
#: core\app.py:318 #: core\app.py:300
msgid "No duplicates found." msgid "No duplicates found."
msgstr "Կրկնօրինակներ չկան:" msgstr "Կրկնօրինակներ չկան:"
#: core\app.py:333 #: core\app.py:315
msgid "All marked files were copied successfully." msgid "All marked files were copied successfully."
msgstr "Բոլոր նշված ֆայլերը հաջողությամբ պատճենվել են:" msgstr "Բոլոր նշված ֆայլերը հաջողությամբ պատճենվել են:"
#: core\app.py:334 #: core\app.py:317
msgid "All marked files were moved successfully." msgid "All marked files were moved successfully."
msgstr "Բոլոր նշված ֆայլերը հաջողությամբ տեղափոխվել են:" msgstr "Բոլոր նշված ֆայլերը հաջողությամբ տեղափոխվել են:"
#: core\app.py:335 #: core\app.py:319
msgid "All marked files were deleted successfully."
msgstr ""
#: core\app.py:321
msgid "All marked files were successfully sent to Trash." msgid "All marked files were successfully sent to Trash."
msgstr "Բոլոր նշված ֆայլերը հաջողությամբ Ջնջվել են:" msgstr "Բոլոր նշված ֆայլերը հաջողությամբ Ջնջվել են:"
#: core\app.py:343 #: core\app.py:326
msgid "Could not load file: {}" msgid "Could not load file: {}"
msgstr "Հնարավոր չէ բեռնել ֆայլը: {}" msgstr "Հնարավոր չէ բեռնել ֆայլը: {}"
#: core\app.py:399 #: core\app.py:382
msgid "'{}' already is in the list." msgid "'{}' already is in the list."
msgstr "'{}'-ը արդեն առկա է ցանկում:" msgstr "'{}'-ը արդեն առկա է ցանկում:"
#: core\app.py:401 #: core\app.py:384
msgid "'{}' does not exist." msgid "'{}' does not exist."
msgstr "'{}'-ը գոյություն չունի:" msgstr "'{}'-ը գոյություն չունի:"
#: core\app.py:410 #: core\app.py:392
msgid "" msgid ""
"All selected %d matches are going to be ignored in all subsequent scans. " "All selected %d matches are going to be ignored in all subsequent scans. "
"Continue?" "Continue?"
msgstr "" msgstr ""
"Ընտրված %d համընկնումները կանտեսվեն հետագա բոլոր ստուգումներից: Շարունակե՞լ:" "Ընտրված %d համընկնումները կանտեսվեն հետագա բոլոր ստուգումներից: Շարունակե՞լ:"
#: core\app.py:486 #: core\app.py:469
msgid "Select a directory to copy marked files to" msgid "Select a directory to copy marked files to"
msgstr "Ընտրեք գրացուցակ, որտեղ ցանկանում եք պատճենել նշված ֆայլերը" msgstr "Ընտրեք գրացուցակ, որտեղ ցանկանում եք պատճենել նշված ֆայլերը"
#: core\app.py:487 #: core\app.py:471
msgid "Select a directory to move marked files to" msgid "Select a directory to move marked files to"
msgstr "" msgstr ""
"Խնդրում ենք ընտրել գրացուցակ, որտեղ ցանկանում եք տեղափոխել նշված ֆայլերը" "Խնդրում ենք ընտրել գրացուցակ, որտեղ ցանկանում եք տեղափոխել նշված ֆայլերը"
#: core\app.py:527 #: core\app.py:510
msgid "Select a destination for your exported CSV" msgid "Select a destination for your exported CSV"
msgstr "Ընտրեք նպատակակետ ձեր արտահանված CSV- ի համար" msgstr "Ընտրեք նպատակակետ ձեր արտահանված CSV- ի համար"
#: core\app.py:534 core\app.py:801 core\app.py:811 #: core\app.py:516 core\app.py:771 core\app.py:781
msgid "Couldn't write to file: {}" msgid "Couldn't write to file: {}"
msgstr "Չէր կարող գրել է ֆայլը: {}" msgstr "Չէր կարող գրել է ֆայլը: {}"
#: core\app.py:559 #: core\app.py:539
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:727 core\app.py:740 #: core\app.py:695 core\app.py:707
msgid "You are about to remove %d files from results. Continue?" msgid "You are about to remove %d files from results. Continue?"
msgstr "Դուք պատրաստվում եք ջնջելու %d ֆայլեր: Շարունակե՞լ:" msgstr "Դուք պատրաստվում եք ջնջելու %d ֆայլեր: Շարունակե՞լ:"
#: core\app.py:774 #: core\app.py:743
msgid "{} duplicate groups were changed by the re-prioritization." msgid "{} duplicate groups were changed by the re-prioritization."
msgstr "{} կրկնօրինակ խմբերը փոխվել են առաջնահերթության կարգով:" msgstr "{} կրկնօրինակ խմբերը փոխվել են առաջնահերթության կարգով:"
#: core\app.py:821 #: core\app.py:790
msgid "The selected directories contain no scannable file." msgid "The selected directories contain no scannable file."
msgstr "Ընտրված թղթապանակները պարունակում են չստուգվող ֆայլ:" msgstr "Ընտրված թղթապանակները պարունակում են չստուգվող ֆայլ:"
#: core\app.py:835 #: core\app.py:803
msgid "Collecting files to scan" msgid "Collecting files to scan"
msgstr "Հավաքվում են ֆայլեր՝ ստուգելու համար" msgstr "Հավաքվում են ֆայլեր՝ ստուգելու համար"
#: core\app.py:891 #: core\app.py:850
msgid "%s (%d discarded)" msgid "%s (%d discarded)"
msgstr "%s (%d անպիտան)" msgstr "%s (%d անպիտան)"
#: core\engine.py:244 core\engine.py:288 #: core\directories.py:191
msgid "0 matches found" msgid "Collected {} files to scan"
msgstr "0 համընկնում է գտնվել" msgstr ""
#: core\engine.py:262 core\engine.py:296 #: core\directories.py:207
msgid "%d matches found" msgid "Collected {} folders to scan"
msgstr "%d համընկնում է գտնվել" msgstr ""
#: core\gui\deletion_options.py:73 #: core\engine.py:27
msgid "%d matches found from %d groups"
msgstr ""
#: core\gui\deletion_options.py:71
msgid "You are sending {} file(s) to the Trash." msgid "You are sending {} file(s) to the Trash."
msgstr "Դուք {} ֆայլ եք ուղարկում աղբարկղ:" msgstr "Դուք {} ֆայլ եք ուղարկում աղբարկղ:"
#: core\gui\exclude_list_table.py:15 #: core\gui\exclude_list_table.py:14
msgid "Regular Expressions" msgid "Regular Expressions"
msgstr "Կանոնավոր արտահայտություններ" msgstr "Կանոնավոր արտահայտություններ"
@@ -176,15 +184,15 @@ msgstr "Բովանդակություն"
msgid "Analyzed %d/%d pictures" msgid "Analyzed %d/%d pictures"
msgstr "Ստուգվում է %d/%d նկարները" msgstr "Ստուգվում է %d/%d նկարները"
#: core\pe\matchblock.py:181 #: core\pe\matchblock.py:177
msgid "Performed %d/%d chunk matches" msgid "Performed %d/%d chunk matches"
msgstr "Կատարվում է %d/%d տվյալի համընկնում" msgstr "Կատարվում է %d/%d տվյալի համընկնում"
#: core\pe\matchblock.py:191 #: core\pe\matchblock.py:185
msgid "Preparing for matching" msgid "Preparing for matching"
msgstr "Նախապատրաստեցվում է համընկնումը" msgstr "Նախապատրաստեցվում է համընկնումը"
#: core\pe\matchblock.py:244 #: core\pe\matchblock.py:234
msgid "Verified %d/%d matches" msgid "Verified %d/%d matches"
msgstr "Ստուգում է %d/%d համընկնումները" msgstr "Ստուգում է %d/%d համընկնումները"
@@ -232,23 +240,23 @@ msgstr "Նորագույնը"
msgid "Oldest" msgid "Oldest"
msgstr "Ամենահինը" msgstr "Ամենահինը"
#: core\results.py:142 #: core\results.py:134
msgid "%d / %d (%s / %s) duplicates marked." msgid "%d / %d (%s / %s) duplicates marked."
msgstr "%d / %d (%s / %s) նշված կրկնօրինակներ:" msgstr "%d / %d (%s / %s) նշված կրկնօրինակներ:"
#: core\results.py:149 #: core\results.py:141
msgid " filter: %s" msgid " filter: %s"
msgstr "ֆիլտր. %s" msgstr "ֆիլտր. %s"
#: core\scanner.py:85 #: core\scanner.py:90
msgid "Read size of %d/%d files" msgid "Read size of %d/%d files"
msgstr "Կարդալ %d/%d ֆայլերի չափը" msgstr "Կարդալ %d/%d ֆայլերի չափը"
#: core\scanner.py:109 #: core\scanner.py:116
msgid "Read metadata of %d/%d files" msgid "Read metadata of %d/%d files"
msgstr "Կարդալ %d/%d ֆայլերի մետատվյալները" msgstr "Կարդալ %d/%d ֆայլերի մետատվյալները"
#: core\scanner.py:147 #: core\scanner.py:154
msgid "Almost done! Fiddling with results..." msgid "Almost done! Fiddling with results..."
msgstr "Գրեթե արված է! Արդյունքների կազմակերպում..." msgstr "Գրեթե արված է! Արդյունքների կազմակերպում..."

View File

@@ -1,10 +1,11 @@
# Translators: # Translators:
# Andrew Senetar <arsenetar@gmail.com>, 2021 # Andrew Senetar <arsenetar@gmail.com>, 2021
# Fuan <jcfrt@posteo.net>, 2021 # Fuan <jcfrt@posteo.net>, 2021
# Emanuele, 2021
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\n" "Last-Translator: Emanuele, 2021\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"
@@ -48,7 +49,7 @@ msgstr "Copia in corso"
msgid "Sending to Trash" msgid "Sending to Trash"
msgstr "Spostamento nel cestino" msgstr "Spostamento nel cestino"
#: core\app.py:308 #: core\app.py:289
msgid "" msgid ""
"A previous action is still hanging in there. You can't start a new one yet. " "A previous action is still hanging in there. You can't start a new one yet. "
"Wait a few seconds, then try again." "Wait a few seconds, then try again."
@@ -56,35 +57,39 @@ msgstr ""
"Un'azione precedente è ancora in corso. Non puoi cominciarne una nuova. " "Un'azione precedente è ancora in corso. Non puoi cominciarne una nuova. "
"Aspetta qualche secondo e quindi riprova." "Aspetta qualche secondo e quindi riprova."
#: core\app.py:318 #: core\app.py:300
msgid "No duplicates found." msgid "No duplicates found."
msgstr "Non sono stati trovati dei duplicati." msgstr "Non sono stati trovati dei duplicati."
#: core\app.py:333 #: core\app.py:315
msgid "All marked files were copied successfully." msgid "All marked files were copied successfully."
msgstr "Tutti i file marcati sono stati copiati correttamente." msgstr "Tutti i file marcati sono stati copiati correttamente."
#: core\app.py:334 #: core\app.py:317
msgid "All marked files were moved successfully." msgid "All marked files were moved successfully."
msgstr "Tutti i file marcati sono stati spostati correttamente." msgstr "Tutti i file marcati sono stati spostati correttamente."
#: core\app.py:335 #: core\app.py:319
msgid "All marked files were deleted successfully."
msgstr "Tutti i file marcati sono stati cancellati correttamente."
#: core\app.py:321
msgid "All marked files were successfully sent to Trash." msgid "All marked files were successfully sent to Trash."
msgstr "Tutti i file marcati sono stati spostati nel cestino." msgstr "Tutti i file marcati sono stati spostati nel cestino."
#: core\app.py:343 #: core\app.py:326
msgid "Could not load file: {}" msgid "Could not load file: {}"
msgstr "Impossibile caricare il file: {}" msgstr "Impossibile caricare il file: {}"
#: core\app.py:399 #: core\app.py:382
msgid "'{}' already is in the list." msgid "'{}' already is in the list."
msgstr "'{}' è già nella lista." msgstr "'{}' è già nella lista."
#: core\app.py:401 #: core\app.py:384
msgid "'{}' does not exist." msgid "'{}' does not exist."
msgstr "'{}' non esiste." msgstr "'{}' non esiste."
#: core\app.py:410 #: core\app.py:392
msgid "" msgid ""
"All selected %d matches are going to be ignored in all subsequent scans. " "All selected %d matches are going to be ignored in all subsequent scans. "
"Continue?" "Continue?"
@@ -92,62 +97,66 @@ msgstr ""
"Tutti i %d elementi che coincidono verranno ignorati in tutte le scansioni " "Tutti i %d elementi che coincidono verranno ignorati in tutte le scansioni "
"successive. Continuare?" "successive. Continuare?"
#: core\app.py:486 #: core\app.py:469
msgid "Select a directory to copy marked files to" msgid "Select a directory to copy marked files to"
msgstr "Seleziona una directory in cui desideri copiare i file contrassegnati" msgstr "Seleziona una directory in cui desideri copiare i file contrassegnati"
#: core\app.py:487 #: core\app.py:471
msgid "Select a directory to move marked files to" msgid "Select a directory to move marked files to"
msgstr "" msgstr ""
"Seleziona una directory in cui desideri spostare i file contrassegnati" "Seleziona una directory in cui desideri spostare i file contrassegnati"
#: core\app.py:527 #: core\app.py:510
msgid "Select a destination for your exported CSV" msgid "Select a destination for your exported CSV"
msgstr "Seleziona una destinazione per il file CSV" msgstr "Seleziona una destinazione per il file CSV"
#: core\app.py:534 core\app.py:801 core\app.py:811 #: core\app.py:516 core\app.py:771 core\app.py:781
msgid "Couldn't write to file: {}" msgid "Couldn't write to file: {}"
msgstr "Impossibile modificare il file: {}" msgstr "Impossibile modificare il file: {}"
#: core\app.py:559 #: core\app.py:539
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 ""
"Non hai impostato nessun comando personalizzato. Impostalo nelle tue " "Non hai impostato nessun comando personalizzato. Impostalo nelle tue "
"preferenze." "preferenze."
#: core\app.py:727 core\app.py:740 #: core\app.py:695 core\app.py:707
msgid "You are about to remove %d files from results. Continue?" msgid "You are about to remove %d files from results. Continue?"
msgstr "Stai per rimuovere %d file dai risultati. Continuare?" msgstr "Stai per rimuovere %d file dai risultati. Continuare?"
#: core\app.py:774 #: core\app.py:743
msgid "{} duplicate groups were changed by the re-prioritization." msgid "{} duplicate groups were changed by the re-prioritization."
msgstr "{} gruppi duplicati sono stati cambiati dalla nuova priorirità" msgstr "{} gruppi duplicati sono stati cambiati dalla nuova priorirità"
#: core\app.py:821 #: core\app.py:790
msgid "The selected directories contain no scannable file." msgid "The selected directories contain no scannable file."
msgstr "Le cartelle selezionate non contengono file da scansionare." msgstr "Le cartelle selezionate non contengono file da scansionare."
#: core\app.py:835 #: core\app.py:803
msgid "Collecting files to scan" msgid "Collecting files to scan"
msgstr "Raccolta file da scansionare" msgstr "Raccolta file da scansionare"
#: core\app.py:891 #: core\app.py:850
msgid "%s (%d discarded)" msgid "%s (%d discarded)"
msgstr "%s (%d scartati)" msgstr "%s (%d scartati)"
#: core\engine.py:244 core\engine.py:288 #: core\directories.py:191
msgid "0 matches found" msgid "Collected {} files to scan"
msgstr "Nessun duplicato trovato" msgstr "Raccolti {} file da scansionare"
#: core\engine.py:262 core\engine.py:296 #: core\directories.py:207
msgid "%d matches found" msgid "Collected {} folders to scan"
msgstr "Trovato/i %d duplicato/i" msgstr "Raccolte {} cartelle da scansionare"
#: core\gui\deletion_options.py:73 #: core\engine.py:27
msgid "%d matches found from %d groups"
msgstr "%d corrispondeze trovate da %d gruppi"
#: core\gui\deletion_options.py:71
msgid "You are sending {} file(s) to the Trash." msgid "You are sending {} file(s) to the Trash."
msgstr "Stai spostando {} file al Cestino." msgstr "Stai spostando {} file al Cestino."
#: core\gui\exclude_list_table.py:15 #: core\gui\exclude_list_table.py:14
msgid "Regular Expressions" msgid "Regular Expressions"
msgstr "Espressioni regolari" msgstr "Espressioni regolari"
@@ -181,15 +190,15 @@ msgstr "Contenuti"
msgid "Analyzed %d/%d pictures" msgid "Analyzed %d/%d pictures"
msgstr "Analizzate %d/%d immagini" msgstr "Analizzate %d/%d immagini"
#: core\pe\matchblock.py:181 #: core\pe\matchblock.py:177
msgid "Performed %d/%d chunk matches" msgid "Performed %d/%d chunk matches"
msgstr "Effettuate %d/%d comparazioni sui sottogruppi di immagini" msgstr "Effettuate %d/%d comparazioni sui sottogruppi di immagini"
#: core\pe\matchblock.py:191 #: core\pe\matchblock.py:185
msgid "Preparing for matching" msgid "Preparing for matching"
msgstr "Preparazione per la comparazione" msgstr "Preparazione per la comparazione"
#: core\pe\matchblock.py:244 #: core\pe\matchblock.py:234
msgid "Verified %d/%d matches" msgid "Verified %d/%d matches"
msgstr "Verificate %d/%d somiglianze" msgstr "Verificate %d/%d somiglianze"
@@ -237,23 +246,23 @@ msgstr "Il più nuovo"
msgid "Oldest" msgid "Oldest"
msgstr "Il più vecchio" msgstr "Il più vecchio"
#: core\results.py:142 #: core\results.py:134
msgid "%d / %d (%s / %s) duplicates marked." msgid "%d / %d (%s / %s) duplicates marked."
msgstr "%d / %d (%s / %s) duplicati marcati." msgstr "%d / %d (%s / %s) duplicati marcati."
#: core\results.py:149 #: core\results.py:141
msgid " filter: %s" msgid " filter: %s"
msgstr " filtro: %s" msgstr " filtro: %s"
#: core\scanner.py:85 #: core\scanner.py:90
msgid "Read size of %d/%d files" msgid "Read size of %d/%d files"
msgstr "Lettura dimensione di %d/%d file" msgstr "Lettura dimensione di %d/%d file"
#: core\scanner.py:109 #: core\scanner.py:116
msgid "Read metadata of %d/%d files" msgid "Read metadata of %d/%d files"
msgstr "Lettura metadata di %d/%d files" msgstr "Lettura metadata di %d/%d files"
#: core\scanner.py:147 #: core\scanner.py:154
msgid "Almost done! Fiddling with results..." msgid "Almost done! Fiddling with results..."
msgstr "Quasi finito! Sto organizzando i risultati..." msgstr "Quasi finito! Sto organizzando i risultati..."

View File

@@ -1,10 +1,11 @@
# Translators: # Translators:
# Andrew Senetar <arsenetar@gmail.com>, 2021 # Andrew Senetar <arsenetar@gmail.com>, 2021
# Fuan <jcfrt@posteo.net>, 2021 # Fuan <jcfrt@posteo.net>, 2021
# Emanuele, 2021
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\n" "Last-Translator: Emanuele, 2021\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"
@@ -951,3 +952,45 @@ msgstr "Generale"
#: qt\preferences_dialog.py:286 #: qt\preferences_dialog.py:286
msgid "Display" msgid "Display"
msgstr "Schermo" msgstr "Schermo"
#: qt\se\preferences_dialog.py:70
msgid "Partially hash files bigger than"
msgstr "Calcola hash parziale di file più grandi di"
#: qt\se\preferences_dialog.py:80
msgid "MB"
msgstr "MB"
#: qt\preferences_dialog.py:163
msgid "Use native OS dialogs"
msgstr "Usa le finestre di dialogo native del Sistema Operativo"
#: qt\preferences_dialog.py:166
msgid ""
"For actions such as file/folder selection use the OS native dialogs.\n"
"Some native dialogs have limited functionality."
msgstr ""
"Per azioni come selezione di file/cartelle usa le finestre di dialogo native del Sistema Operativo.\n"
"Alcune finestre di dialogo native hanno funzionalità limitate."
#: qt\se\preferences_dialog.py:68
msgid "Ignore files larger than"
msgstr "Ignora file più grandi di"
#: qt\app.py:135 qt\app.py:293
msgid "Clear Cache"
msgstr ""
#: qt\app.py:294
msgid ""
"Do you really want to clear the cache? This will remove all cached file "
"hashes and picture analysis."
msgstr ""
#: qt\app.py:299
msgid "Cache cleared."
msgstr ""
#: qt\preferences_dialog.py:173
msgid "Use dark style"
msgstr ""

View File

@@ -44,99 +44,107 @@ msgstr "コピー中"
msgid "Sending to Trash" msgid "Sending to Trash"
msgstr "ごみ箱に送信します" msgstr "ごみ箱に送信します"
#: core\app.py:308 #: core\app.py:289
msgid "" msgid ""
"A previous action is still hanging in there. You can't start a new one yet. " "A previous action is still hanging in there. You can't start a new one yet. "
"Wait a few seconds, then try again." "Wait a few seconds, then try again."
msgstr "前のアクションはまだそこにぶら下がっています。 まだ新しいものを始めることはできません。 数秒待ってから、再試行してください。" msgstr "前のアクションはまだそこにぶら下がっています。 まだ新しいものを始めることはできません。 数秒待ってから、再試行してください。"
#: core\app.py:318 #: core\app.py:300
msgid "No duplicates found." msgid "No duplicates found."
msgstr "重複は見つかりませんでした。" msgstr "重複は見つかりませんでした。"
#: core\app.py:333 #: core\app.py:315
msgid "All marked files were copied successfully." msgid "All marked files were copied successfully."
msgstr "マークされたファイルはすべて正常にコピーされました。" msgstr "マークされたファイルはすべて正常にコピーされました。"
#: core\app.py:334 #: core\app.py:317
msgid "All marked files were moved successfully." msgid "All marked files were moved successfully."
msgstr "マークされたファイルはすべて正常に移動されました。" msgstr "マークされたファイルはすべて正常に移動されました。"
#: core\app.py:335 #: core\app.py:319
msgid "All marked files were deleted successfully."
msgstr ""
#: core\app.py:321
msgid "All marked files were successfully sent to Trash." msgid "All marked files were successfully sent to Trash."
msgstr "マークされたファイルはすべてごみ箱に正常に送信されました。" msgstr "マークされたファイルはすべてごみ箱に正常に送信されました。"
#: core\app.py:343 #: core\app.py:326
msgid "Could not load file: {}" msgid "Could not load file: {}"
msgstr "ファイルを読み込めませんでした:{}" msgstr "ファイルを読み込めませんでした:{}"
#: core\app.py:399 #: core\app.py:382
msgid "'{}' already is in the list." msgid "'{}' already is in the list."
msgstr "「{}」既にリストに含まれています。" msgstr "「{}」既にリストに含まれています。"
#: core\app.py:401 #: core\app.py:384
msgid "'{}' does not exist." msgid "'{}' does not exist."
msgstr "'{}' 存在しません。" msgstr "'{}' 存在しません。"
#: core\app.py:410 #: core\app.py:392
msgid "" msgid ""
"All selected %d matches are going to be ignored in all subsequent scans. " "All selected %d matches are going to be ignored in all subsequent scans. "
"Continue?" "Continue?"
msgstr "選択した%d個の一致は、以降のすべてのスキャンで無視されます。 継続する?" msgstr "選択した%d個の一致は、以降のすべてのスキャンで無視されます。 継続する?"
#: core\app.py:486 #: core\app.py:469
msgid "Select a directory to copy marked files to" msgid "Select a directory to copy marked files to"
msgstr "マークされたファイルをコピーするディレクトリを選択してください" msgstr "マークされたファイルをコピーするディレクトリを選択してください"
#: core\app.py:487 #: core\app.py:471
msgid "Select a directory to move marked files to" msgid "Select a directory to move marked files to"
msgstr "マークされたファイルを移動するディレクトリを選択してください" msgstr "マークされたファイルを移動するディレクトリを選択してください"
#: core\app.py:527 #: core\app.py:510
msgid "Select a destination for your exported CSV" msgid "Select a destination for your exported CSV"
msgstr "エクスポートしたCSVの宛先を選択します。" msgstr "エクスポートしたCSVの宛先を選択します。"
#: core\app.py:534 core\app.py:801 core\app.py:811 #: core\app.py:516 core\app.py:771 core\app.py:781
msgid "Couldn't write to file: {}" msgid "Couldn't write to file: {}"
msgstr "ファイルに書き込めませんでした:{}" msgstr "ファイルに書き込めませんでした:{}"
#: core\app.py:559 #: core\app.py:539
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:727 core\app.py:740 #: core\app.py:695 core\app.py:707
msgid "You are about to remove %d files from results. Continue?" msgid "You are about to remove %d files from results. Continue?"
msgstr "結果から%d個のファイルを削除しようとしています。 継続する?" msgstr "結果から%d個のファイルを削除しようとしています。 継続する?"
#: core\app.py:774 #: core\app.py:743
msgid "{} duplicate groups were changed by the re-prioritization." msgid "{} duplicate groups were changed by the re-prioritization."
msgstr "{}重複するグループは、再優先順位付けによって変更されました。" msgstr "{}重複するグループは、再優先順位付けによって変更されました。"
#: core\app.py:821 #: core\app.py:790
msgid "The selected directories contain no scannable file." msgid "The selected directories contain no scannable file."
msgstr "選択したディレクトリにはスキャン可能なファイルが含まれていません。" msgstr "選択したディレクトリにはスキャン可能なファイルが含まれていません。"
#: core\app.py:835 #: core\app.py:803
msgid "Collecting files to scan" msgid "Collecting files to scan"
msgstr "スキャンするファイルを収集しています" msgstr "スキャンするファイルを収集しています"
#: core\app.py:891 #: core\app.py:850
msgid "%s (%d discarded)" msgid "%s (%d discarded)"
msgstr "%s (%d 廃棄)" msgstr "%s (%d 廃棄)"
#: core\engine.py:244 core\engine.py:288 #: core\directories.py:191
msgid "0 matches found" msgid "Collected {} files to scan"
msgstr "一致するものが見つかりません" msgstr ""
#: core\engine.py:262 core\engine.py:296 #: core\directories.py:207
msgid "%d matches found" msgid "Collected {} folders to scan"
msgstr "%d の一致が見つかりました" msgstr ""
#: core\gui\deletion_options.py:73 #: core\engine.py:27
msgid "%d matches found from %d groups"
msgstr ""
#: core\gui\deletion_options.py:71
msgid "You are sending {} file(s) to the Trash." msgid "You are sending {} file(s) to the Trash."
msgstr "{}個のファイルをゴミ箱に送信しています" msgstr "{}個のファイルをゴミ箱に送信しています"
#: core\gui\exclude_list_table.py:15 #: core\gui\exclude_list_table.py:14
msgid "Regular Expressions" msgid "Regular Expressions"
msgstr "正規表現" msgstr "正規表現"
@@ -168,15 +176,15 @@ msgstr "内容"
msgid "Analyzed %d/%d pictures" msgid "Analyzed %d/%d pictures"
msgstr "%d/%d 枚の写真を分析しました" msgstr "%d/%d 枚の写真を分析しました"
#: core\pe\matchblock.py:181 #: core\pe\matchblock.py:177
msgid "Performed %d/%d chunk matches" msgid "Performed %d/%d chunk matches"
msgstr "チャンクマッチを%d/%d回実行しました" msgstr "チャンクマッチを%d/%d回実行しました"
#: core\pe\matchblock.py:191 #: core\pe\matchblock.py:185
msgid "Preparing for matching" msgid "Preparing for matching"
msgstr "マッチングの準備" msgstr "マッチングの準備"
#: core\pe\matchblock.py:244 #: core\pe\matchblock.py:234
msgid "Verified %d/%d matches" msgid "Verified %d/%d matches"
msgstr "%d/%d件の一致を確認" msgstr "%d/%d件の一致を確認"
@@ -224,23 +232,23 @@ msgstr "最新"
msgid "Oldest" msgid "Oldest"
msgstr "最古" msgstr "最古"
#: core\results.py:142 #: core\results.py:134
msgid "%d / %d (%s / %s) duplicates marked." msgid "%d / %d (%s / %s) duplicates marked."
msgstr "%d / %d (%s / %s)マークされた重複。" msgstr "%d / %d (%s / %s)マークされた重複。"
#: core\results.py:149 #: core\results.py:141
msgid " filter: %s" msgid " filter: %s"
msgstr "フィルタ: %s" msgstr "フィルタ: %s"
#: core\scanner.py:85 #: core\scanner.py:90
msgid "Read size of %d/%d files" msgid "Read size of %d/%d files"
msgstr "%d/%dファイルのサイズを読み取った" msgstr "%d/%dファイルのサイズを読み取った"
#: core\scanner.py:109 #: core\scanner.py:116
msgid "Read metadata of %d/%d files" msgid "Read metadata of %d/%d files"
msgstr "%d/%dファイルのメタデータを読み取った" msgstr "%d/%dファイルのメタデータを読み取った"
#: core\scanner.py:147 #: core\scanner.py:154
msgid "Almost done! Fiddling with results..." msgid "Almost done! Fiddling with results..."
msgstr "ほぼ完了しました! 結果をいじっています..." msgstr "ほぼ完了しました! 結果をいじっています..."

View File

@@ -925,3 +925,43 @@ msgstr "一般"
#: qt\preferences_dialog.py:286 #: qt\preferences_dialog.py:286
msgid "Display" msgid "Display"
msgstr "表示" msgstr "表示"
#: qt\se\preferences_dialog.py:70
msgid "Partially hash files bigger than"
msgstr ""
#: qt\se\preferences_dialog.py:80
msgid "MB"
msgstr ""
#: qt\preferences_dialog.py:163
msgid "Use native OS dialogs"
msgstr ""
#: qt\preferences_dialog.py:166
msgid ""
"For actions such as file/folder selection use the OS native dialogs.\n"
"Some native dialogs have limited functionality."
msgstr ""
#: qt\se\preferences_dialog.py:68
msgid "Ignore files larger than"
msgstr ""
#: qt\app.py:135 qt\app.py:293
msgid "Clear Cache"
msgstr ""
#: qt\app.py:294
msgid ""
"Do you really want to clear the cache? This will remove all cached file "
"hashes and picture analysis."
msgstr ""
#: qt\app.py:299
msgid "Cache cleared."
msgstr ""
#: qt\preferences_dialog.py:173
msgid "Use dark style"
msgstr ""

View File

@@ -45,99 +45,107 @@ msgstr "복사중"
msgid "Sending to Trash" msgid "Sending to Trash"
msgstr "휴지통으로 보내기" msgstr "휴지통으로 보내기"
#: core\app.py:308 #: core\app.py:289
msgid "" msgid ""
"A previous action is still hanging in there. You can't start a new one yet. " "A previous action is still hanging in there. You can't start a new one yet. "
"Wait a few seconds, then try again." "Wait a few seconds, then try again."
msgstr "이전 작업이 여전히 거기에 걸려 있습니다. 아직 새로운 것을 시작할 수 없습니다. 몇 초 후에 다시 시도하십시오." msgstr "이전 작업이 여전히 거기에 걸려 있습니다. 아직 새로운 것을 시작할 수 없습니다. 몇 초 후에 다시 시도하십시오."
#: core\app.py:318 #: core\app.py:300
msgid "No duplicates found." msgid "No duplicates found."
msgstr "중복 파일이 없습니다." msgstr "중복 파일이 없습니다."
#: core\app.py:333 #: core\app.py:315
msgid "All marked files were copied successfully." msgid "All marked files were copied successfully."
msgstr "표시된 모든 파일이 성공적으로 복사되었습니다." msgstr "표시된 모든 파일이 성공적으로 복사되었습니다."
#: core\app.py:334 #: core\app.py:317
msgid "All marked files were moved successfully." msgid "All marked files were moved successfully."
msgstr "표시된 모든 파일이 성공적으로 이동되었습니다." msgstr "표시된 모든 파일이 성공적으로 이동되었습니다."
#: core\app.py:335 #: core\app.py:319
msgid "All marked files were deleted successfully."
msgstr ""
#: core\app.py:321
msgid "All marked files were successfully sent to Trash." msgid "All marked files were successfully sent to Trash."
msgstr "표시된 모든 파일이 성공적으로 휴지통으로 전송되었습니다." msgstr "표시된 모든 파일이 성공적으로 휴지통으로 전송되었습니다."
#: core\app.py:343 #: core\app.py:326
msgid "Could not load file: {}" msgid "Could not load file: {}"
msgstr "파일을로드 할 수 없습니다 : {}" msgstr "파일을로드 할 수 없습니다 : {}"
#: core\app.py:399 #: core\app.py:382
msgid "'{}' already is in the list." msgid "'{}' already is in the list."
msgstr "'{}' 는 이미 목록에 있습니다." msgstr "'{}' 는 이미 목록에 있습니다."
#: core\app.py:401 #: core\app.py:384
msgid "'{}' does not exist." msgid "'{}' does not exist."
msgstr "'{}' 가 존재하지 않습니다." msgstr "'{}' 가 존재하지 않습니다."
#: core\app.py:410 #: core\app.py:392
msgid "" msgid ""
"All selected %d matches are going to be ignored in all subsequent scans. " "All selected %d matches are going to be ignored in all subsequent scans. "
"Continue?" "Continue?"
msgstr "선택된 %d개의 일치 항목은 모든 후속 검색에서 무시됩니다. 계속하다?" msgstr "선택된 %d개의 일치 항목은 모든 후속 검색에서 무시됩니다. 계속하다?"
#: core\app.py:486 #: core\app.py:469
msgid "Select a directory to copy marked files to" msgid "Select a directory to copy marked files to"
msgstr "표시된 파일을 복사 할 디렉토리를 선택하십시오" msgstr "표시된 파일을 복사 할 디렉토리를 선택하십시오"
#: core\app.py:487 #: core\app.py:471
msgid "Select a directory to move marked files to" msgid "Select a directory to move marked files to"
msgstr "표시된 파일을 이동할 디렉토리를 선택하십시오" msgstr "표시된 파일을 이동할 디렉토리를 선택하십시오"
#: core\app.py:527 #: core\app.py:510
msgid "Select a destination for your exported CSV" msgid "Select a destination for your exported CSV"
msgstr "내 보낸 CSV의 대상을 선택하십시오" msgstr "내 보낸 CSV의 대상을 선택하십시오"
#: core\app.py:534 core\app.py:801 core\app.py:811 #: core\app.py:516 core\app.py:771 core\app.py:781
msgid "Couldn't write to file: {}" msgid "Couldn't write to file: {}"
msgstr "파일에 쓸 수 없습니다 : {}" msgstr "파일에 쓸 수 없습니다 : {}"
#: core\app.py:559 #: core\app.py:539
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:727 core\app.py:740 #: core\app.py:695 core\app.py:707
msgid "You are about to remove %d files from results. Continue?" msgid "You are about to remove %d files from results. Continue?"
msgstr "결과에서 %d 개의 파일을 제거하려고합니다. 계속하다?" msgstr "결과에서 %d 개의 파일을 제거하려고합니다. 계속하다?"
#: core\app.py:774 #: core\app.py:743
msgid "{} duplicate groups were changed by the re-prioritization." msgid "{} duplicate groups were changed by the re-prioritization."
msgstr "{} 개의 중복 그룹이 우선 순위 재 지정으로 변경되었습니다." msgstr "{} 개의 중복 그룹이 우선 순위 재 지정으로 변경되었습니다."
#: core\app.py:821 #: core\app.py:790
msgid "The selected directories contain no scannable file." msgid "The selected directories contain no scannable file."
msgstr "선택한 디렉토리에 스캔 가능한 파일이 없습니다." msgstr "선택한 디렉토리에 스캔 가능한 파일이 없습니다."
#: core\app.py:835 #: core\app.py:803
msgid "Collecting files to scan" msgid "Collecting files to scan"
msgstr "스캔 할 파일 수집" msgstr "스캔 할 파일 수집"
#: core\app.py:891 #: core\app.py:850
msgid "%s (%d discarded)" msgid "%s (%d discarded)"
msgstr "%s (%d 폐기)" msgstr "%s (%d 폐기)"
#: core\engine.py:244 core\engine.py:288 #: core\directories.py:191
msgid "0 matches found" msgid "Collected {} files to scan"
msgstr "일치하는 항목이 없습니다" msgstr ""
#: core\engine.py:262 core\engine.py:296 #: core\directories.py:207
msgid "%d matches found" msgid "Collected {} folders to scan"
msgstr "%d개의 일치 항목을 찾았습니다." msgstr ""
#: core\gui\deletion_options.py:73 #: core\engine.py:27
msgid "%d matches found from %d groups"
msgstr ""
#: core\gui\deletion_options.py:71
msgid "You are sending {} file(s) to the Trash." msgid "You are sending {} file(s) to the Trash."
msgstr "{}개의 파일을 휴지통으로 보내고 있습니다." msgstr "{}개의 파일을 휴지통으로 보내고 있습니다."
#: core\gui\exclude_list_table.py:15 #: core\gui\exclude_list_table.py:14
msgid "Regular Expressions" msgid "Regular Expressions"
msgstr "정규식" msgstr "정규식"
@@ -169,15 +177,15 @@ msgstr "내용"
msgid "Analyzed %d/%d pictures" msgid "Analyzed %d/%d pictures"
msgstr "%d/%d 사진 분석" msgstr "%d/%d 사진 분석"
#: core\pe\matchblock.py:181 #: core\pe\matchblock.py:177
msgid "Performed %d/%d chunk matches" msgid "Performed %d/%d chunk matches"
msgstr "%d/%d 청크 매치 수행" msgstr "%d/%d 청크 매치 수행"
#: core\pe\matchblock.py:191 #: core\pe\matchblock.py:185
msgid "Preparing for matching" msgid "Preparing for matching"
msgstr "매칭 준비" msgstr "매칭 준비"
#: core\pe\matchblock.py:244 #: core\pe\matchblock.py:234
msgid "Verified %d/%d matches" msgid "Verified %d/%d matches"
msgstr "%d/%d 일치 확인" msgstr "%d/%d 일치 확인"
@@ -225,23 +233,23 @@ msgstr "최신"
msgid "Oldest" msgid "Oldest"
msgstr "가장 오래된" msgstr "가장 오래된"
#: core\results.py:142 #: core\results.py:134
msgid "%d / %d (%s / %s) duplicates marked." msgid "%d / %d (%s / %s) duplicates marked."
msgstr "%d / %d (%s / %s) 개의 중복이 표시되었습니다." msgstr "%d / %d (%s / %s) 개의 중복이 표시되었습니다."
#: core\results.py:149 #: core\results.py:141
msgid " filter: %s" msgid " filter: %s"
msgstr "필터: %s" msgstr "필터: %s"
#: core\scanner.py:85 #: core\scanner.py:90
msgid "Read size of %d/%d files" msgid "Read size of %d/%d files"
msgstr "%d/%d 개의 파일을 읽을 수 있습니다." msgstr "%d/%d 개의 파일을 읽을 수 있습니다."
#: core\scanner.py:109 #: core\scanner.py:116
msgid "Read metadata of %d/%d files" msgid "Read metadata of %d/%d files"
msgstr "%d/%d 개 파일의 메타 데이터를 읽었습니다." msgstr "%d/%d 개 파일의 메타 데이터를 읽었습니다."
#: core\scanner.py:147 #: core\scanner.py:154
msgid "Almost done! Fiddling with results..." msgid "Almost done! Fiddling with results..."
msgstr "거의 완료되었습니다! 결과를 만지작 거리는 중..." msgstr "거의 완료되었습니다! 결과를 만지작 거리는 중..."

View File

@@ -927,3 +927,43 @@ msgstr "일반"
#: qt\preferences_dialog.py:286 #: qt\preferences_dialog.py:286
msgid "Display" msgid "Display"
msgstr "디스플레이" msgstr "디스플레이"
#: qt\se\preferences_dialog.py:70
msgid "Partially hash files bigger than"
msgstr ""
#: qt\se\preferences_dialog.py:80
msgid "MB"
msgstr ""
#: qt\preferences_dialog.py:163
msgid "Use native OS dialogs"
msgstr ""
#: qt\preferences_dialog.py:166
msgid ""
"For actions such as file/folder selection use the OS native dialogs.\n"
"Some native dialogs have limited functionality."
msgstr ""
#: qt\se\preferences_dialog.py:68
msgid "Ignore files larger than"
msgstr ""
#: qt\app.py:135 qt\app.py:293
msgid "Clear Cache"
msgstr ""
#: qt\app.py:294
msgid ""
"Do you really want to clear the cache? This will remove all cached file "
"hashes and picture analysis."
msgstr ""
#: qt\app.py:299
msgid "Cache cleared."
msgstr ""
#: qt\preferences_dialog.py:173
msgid "Use dark style"
msgstr ""

View File

@@ -0,0 +1,122 @@
# Translators:
# Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi (MNH48) <admin@mnh48.moe>, 2021
#
msgid ""
msgstr ""
"Last-Translator: Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi (MNH48) <admin@mnh48.moe>, 2021\n"
"Language-Team: Malay (https://www.transifex.com/voltaicideas/teams/116153/ms/)\n"
"Language: ms\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: utf-8\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20
#: core\gui\problem_table.py:18
msgid "File Path"
msgstr "Laluan Fail"
#: core\gui\problem_table.py:19
msgid "Error Message"
msgstr "Mesej Ralat"
#: core\me\prioritize.py:23
msgid "Duration"
msgstr "Tempoh"
#: core\me\prioritize.py:30 core\me\result_table.py:23
msgid "Bitrate"
msgstr "Kadar Bit"
#: core\me\prioritize.py:37
msgid "Samplerate"
msgstr "Kadar Sampel"
#: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92
#: core\se\result_table.py:19
msgid "Filename"
msgstr "Nama Fail"
#: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75
#: core\se\result_table.py:20
msgid "Folder"
msgstr "Folder"
#: core\me\result_table.py:21
msgid "Size (MB)"
msgstr "Saiz (MB)"
#: core\me\result_table.py:22
msgid "Time"
msgstr "Masa"
#: core\me\result_table.py:24
msgid "Sample Rate"
msgstr "Kadar Sampel"
#: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65
#: core\se\result_table.py:22
msgid "Kind"
msgstr "Jenis"
#: core\me\result_table.py:26 core\pe\result_table.py:25
#: core\prioritize.py:163 core\se\result_table.py:23
msgid "Modification"
msgstr "Pengubahsuaian"
#: core\me\result_table.py:27
msgid "Title"
msgstr "Tajuk"
#: core\me\result_table.py:28
msgid "Artist"
msgstr "Artis"
#: core\me\result_table.py:29
msgid "Album"
msgstr "Album"
#: core\me\result_table.py:30
msgid "Genre"
msgstr "Genre"
#: core\me\result_table.py:31
msgid "Year"
msgstr "Tahun"
#: core\me\result_table.py:32
msgid "Track Number"
msgstr "Nombor Runut"
#: core\me\result_table.py:33
msgid "Comment"
msgstr "Komen"
#: core\me\result_table.py:34 core\pe\result_table.py:26
#: core\se\result_table.py:24
msgid "Match %"
msgstr "% Padanan"
#: core\me\result_table.py:35 core\se\result_table.py:25
msgid "Words Used"
msgstr "Perkataan Diguna"
#: core\me\result_table.py:36 core\pe\result_table.py:27
#: core\se\result_table.py:26
msgid "Dupe Count"
msgstr "Jumlah Duplikasi"
#: core\pe\prioritize.py:23 core\pe\result_table.py:23
msgid "Dimensions"
msgstr "Dimensi"
#: core\pe\result_table.py:21 core\se\result_table.py:21
msgid "Size (KB)"
msgstr "Saiz (KB)"
#: core\pe\result_table.py:24
msgid "EXIF Timestamp"
msgstr "Cap Masa EXIF"
#: core\prioritize.py:156
msgid "Size"
msgstr "Saiz"

View File

@@ -0,0 +1,267 @@
# Translators:
# Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi (MNH48) <admin@mnh48.moe>, 2021
#
msgid ""
msgstr ""
"Last-Translator: Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi (MNH48) <admin@mnh48.moe>, 2021\n"
"Language-Team: Malay (https://www.transifex.com/voltaicideas/teams/116153/ms/)\n"
"Language: ms\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: utf-8\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: core\app.py:42
msgid "There are no marked duplicates. Nothing has been done."
msgstr "Tiada duplikasi yang ditandai. Tiada apa yang dilakukan."
#: core\app.py:43
msgid "There are no selected duplicates. Nothing has been done."
msgstr "Tiada duplikasi yang dipilih. Tiada apa yang dilakukan."
#: core\app.py:44
msgid ""
"You're about to open many files at once. Depending on what those files are "
"opened with, doing so can create quite a mess. Continue?"
msgstr ""
"Anda bakal membuka banyak fail serentak. Bergantung kepada apa yang "
"digunakan untuk membuka fail tersebut, ia mungkin menyebabkan sepah. Ingin "
"teruskan?"
#: core\app.py:71
msgid "Scanning for duplicates"
msgstr "Mengimbas untuk duplikasi"
#: core\app.py:72
msgid "Loading"
msgstr "Memuatkan"
#: core\app.py:73
msgid "Moving"
msgstr "Memindahkan"
#: core\app.py:74
msgid "Copying"
msgstr "Menyalinkan"
#: core\app.py:75
msgid "Sending to Trash"
msgstr "Menghantarkan ke Tong Sampah"
#: core\app.py:289
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 ""
"Tindakan sebelum ini masih tergantung. Anda tidak boleh mulakan yang baharu "
"lagi. Tunggu beberapa saat, kemudian cuba lagi."
#: core\app.py:300
msgid "No duplicates found."
msgstr "Tiada duplikasi dijumpai."
#: core\app.py:315
msgid "All marked files were copied successfully."
msgstr "Semua fail yang ditandai telah berjaya disalin."
#: core\app.py:317
msgid "All marked files were moved successfully."
msgstr "Semua fail yang ditandai telah berjaya dipindah."
#: core\app.py:319
msgid "All marked files were deleted successfully."
msgstr "Semua fail yang ditandai telah berjaya dipadam."
#: core\app.py:321
msgid "All marked files were successfully sent to Trash."
msgstr "Semua fail yang ditandai telah berjaya dihantar ke Tong Sampah."
#: core\app.py:326
msgid "Could not load file: {}"
msgstr "Tidak mampu memuatkan fail: {}"
#: core\app.py:382
msgid "'{}' already is in the list."
msgstr "'{}' sudah ada dalam senarai."
#: core\app.py:384
msgid "'{}' does not exist."
msgstr "'{}' tidak wujud."
#: core\app.py:392
msgid ""
"All selected %d matches are going to be ignored in all subsequent scans. "
"Continue?"
msgstr ""
"Kesemua %d padanan yang dipilih akan diabaikan dalam semua imbasan "
"terkemudian. Ingin teruskan?"
#: core\app.py:469
msgid "Select a directory to copy marked files to"
msgstr "Pilih direktori dituju untuk salin fail yang ditandai"
#: core\app.py:471
msgid "Select a directory to move marked files to"
msgstr "Pilih direktori dituju untuk pindah fail yang ditandai"
#: core\app.py:510
msgid "Select a destination for your exported CSV"
msgstr "Pilih tempat tujuan untuk eksport CSV anda"
#: core\app.py:516 core\app.py:771 core\app.py:781
msgid "Couldn't write to file: {}"
msgstr "Tidak mampu menulis ke fail: {}"
#: core\app.py:539
msgid "You have no custom command set up. Set it up in your preferences."
msgstr ""
"Anda tidak ada perintah tersuai ditetapkan. Tetapkannya melalui menu "
"keutamaan anda."
#: core\app.py:695 core\app.py:707
msgid "You are about to remove %d files from results. Continue?"
msgstr "Anda bakal mengalih keluar %d fail dari keputusan. Ingin teruskan?"
#: core\app.py:743
msgid "{} duplicate groups were changed by the re-prioritization."
msgstr "{} kumpulan duplikasi telah diubah oleh pengutamaan semula."
#: core\app.py:790
msgid "The selected directories contain no scannable file."
msgstr "Direktori yang dipilih tidak mempunyai fail yang boleh diimbas."
#: core\app.py:803
msgid "Collecting files to scan"
msgstr "Mengumpulkan fail untuk diimbas"
#: core\app.py:850
msgid "%s (%d discarded)"
msgstr "%s (%d dibuang)"
#: core\directories.py:191
msgid "Collected {} files to scan"
msgstr "{} fail dikumpulkan untuk diimbas"
#: core\directories.py:207
msgid "Collected {} folders to scan"
msgstr "{} folder dikumpulkan untuk diimbas"
#: core\engine.py:27
msgid "%d matches found from %d groups"
msgstr "%d padanan dijumpai dari %d kumpulan"
#: core\gui\deletion_options.py:71
msgid "You are sending {} file(s) to the Trash."
msgstr "Anda menghantar {} fail ke Tong Sampah."
#: core\gui\exclude_list_table.py:14
msgid "Regular Expressions"
msgstr "Ungkapan Nalar"
#: core\gui\ignore_list_dialog.py:25
msgid "Do you really want to remove all %d items from the ignore list?"
msgstr ""
"Adakah anda pasti anda ingin alih keluar kesemua %d item dari senarai abai?"
#: core\me\scanner.py:20 core\se\scanner.py:16
msgid "Filename"
msgstr "Nama Fail"
#: core\me\scanner.py:21
msgid "Filename - Fields"
msgstr "Nama Fail - Medan"
#: core\me\scanner.py:22
msgid "Filename - Fields (No Order)"
msgstr "Nama Fail - Medan (Tiada Tertib)"
#: core\me\scanner.py:23
msgid "Tags"
msgstr "Tag"
#: core\me\scanner.py:24 core\pe\scanner.py:21 core\se\scanner.py:17
msgid "Contents"
msgstr "Kandungan"
#: core\pe\matchblock.py:72
msgid "Analyzed %d/%d pictures"
msgstr "%d / %d gambar dianalisis"
#: core\pe\matchblock.py:177
msgid "Performed %d/%d chunk matches"
msgstr "%d / %d padanan ketulan dilaksanakan"
#: core\pe\matchblock.py:185
msgid "Preparing for matching"
msgstr "Membuat persediaan untuk pemadanan"
#: core\pe\matchblock.py:234
msgid "Verified %d/%d matches"
msgstr "%d / %d padanan disahkan"
#: core\pe\matchexif.py:19
msgid "Read EXIF of %d/%d pictures"
msgstr "EXIF bagi %d / %d gambar dibaca"
#: core\pe\scanner.py:22
msgid "EXIF Timestamp"
msgstr "Cap masa EXIF"
#: core\prioritize.py:70
msgid "None"
msgstr "Tiada"
#: core\prioritize.py:100
msgid "Ends with number"
msgstr "Tamat dengan nombor"
#: core\prioritize.py:101
msgid "Doesn't end with number"
msgstr "Tidak tamat dengan nombor"
#: core\prioritize.py:102
msgid "Longest"
msgstr "Terpanjang"
#: core\prioritize.py:103
msgid "Shortest"
msgstr "Terpendek"
#: core\prioritize.py:140
msgid "Highest"
msgstr "Tertinggi"
#: core\prioritize.py:140
msgid "Lowest"
msgstr "Terendah"
#: core\prioritize.py:169
msgid "Newest"
msgstr "Terbaru"
#: core\prioritize.py:169
msgid "Oldest"
msgstr "Terlama"
#: core\results.py:134
msgid "%d / %d (%s / %s) duplicates marked."
msgstr "%d / %d (%s / %s) duplikasi ditandai."
#: core\results.py:141
msgid " filter: %s"
msgstr "penapis: %s"
#: core\scanner.py:90
msgid "Read size of %d/%d files"
msgstr "Saiz bagi %d / %d gambar dibaca"
#: core\scanner.py:116
msgid "Read metadata of %d/%d files"
msgstr "Metadata bagi %d / %d gambar dibaca"
#: core\scanner.py:154
msgid "Almost done! Fiddling with results..."
msgstr "Hampir selesai! Menyusun keputusan..."
#: core\se\scanner.py:18
msgid "Folders"
msgstr "Folder"

990
locale/ms/LC_MESSAGES/ui.po Normal file
View File

@@ -0,0 +1,990 @@
# Translators:
# Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi (MNH48) <admin@mnh48.moe>, 2021
#
msgid ""
msgstr ""
"Last-Translator: Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi (MNH48) <admin@mnh48.moe>, 2021\n"
"Language-Team: Malay (https://www.transifex.com/voltaicideas/teams/116153/ms/)\n"
"Language: ms\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: utf-8\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: qt/app.py:81
msgid "Quit"
msgstr "Keluar"
#: qt/app.py:82 qt/preferences_dialog.py:116
#: cocoa/en.lproj/Localizable.strings:0
msgid "Options"
msgstr "Pilihan"
#: qt/app.py:83 qt/ignore_list_dialog.py:32
#: cocoa/en.lproj/Localizable.strings:0
msgid "Ignore List"
msgstr "Senarai Abai"
#: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0
msgid "Clear Picture Cache"
msgstr "Kosongkan Cache Gambar"
#: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0
msgid "dupeGuru Help"
msgstr "Bantuan dupeGuru"
#: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0
msgid "About dupeGuru"
msgstr "Mengenai dupeGuru"
#: qt/app.py:87
msgid "Open Debug Log"
msgstr "Buka Log Nyahpepijat"
#: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0
msgid "Do you really want to remove all your cached picture analysis?"
msgstr ""
"Adakah anda pasti anda ingin alih keluar kesemua analisis gambar cache anda?"
#: qt/app.py:184
msgid "Picture cache cleared."
msgstr "Cache gambar dikosongkan."
#: qt/app.py:251
msgid "{} file (*.{})"
msgstr "{} fail (*.{})"
#: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0
msgid "Deletion Options"
msgstr "Pilihan Pemadaman"
#: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0
msgid "Link deleted files"
msgstr "Pautkan fail dipadam"
#: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0
msgid ""
"After having deleted a duplicate, place a link targeting the reference file "
"to replace the deleted file."
msgstr ""
"Setelah duplikasi dipadam, letak pautan menuju fail rujukan untuk "
"menggantikan fail dipadam."
#: qt/deletion_options.py:44
msgid "Hardlink"
msgstr "Pautan Keras"
#: qt/deletion_options.py:44
msgid "Symlink"
msgstr "Pautan Bersimbol"
#: qt/deletion_options.py:48
msgid " (unsupported)"
msgstr " (tidak disokong)"
#: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0
msgid "Directly delete files"
msgstr "Padam fail secara terus"
#: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0
msgid ""
"Instead of sending files to trash, delete them directly. This option is "
"usually used as a workaround when the normal deletion method doesn't work."
msgstr ""
"Padam fail secara terus dan bukannya hantar fail ke tong sampah. Pilihan ini"
" selalunya digunakan sebagai penyelesaian apabila kaedah pemadaman biasa "
"tidak berjaya."
#: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0
msgid "Proceed"
msgstr "Teruskan"
#: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0
msgid "Cancel"
msgstr "Batal"
#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0
msgid "Attribute"
msgstr "Atribut"
#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0
msgid "Selected"
msgstr "Dipilih"
#: qt/details_table.py:16 qt/directories_model.py:24
#: cocoa/en.lproj/Localizable.strings:0
msgid "Reference"
msgstr "Rujukan"
#: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0
msgid "Load Results..."
msgstr "Muatkan Keputusan..."
#: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0
msgid "Results Window"
msgstr "Tetingkap Keputusan"
#: qt/directories_dialog.py:66
msgid "Add Folder..."
msgstr "Tambah Folder..."
#: qt/directories_dialog.py:74 qt/result_window.py:100
#: cocoa/en.lproj/Localizable.strings:0
msgid "File"
msgstr "Fail"
#: qt/directories_dialog.py:76 qt/result_window.py:108
msgid "View"
msgstr "Lihat"
#: qt/directories_dialog.py:78 qt/result_window.py:110
#: cocoa/en.lproj/Localizable.strings:0
msgid "Help"
msgstr "Bantuan"
#: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0
msgid "Load Recent Results"
msgstr "Muatkan Keputusan Baru-baru Ini"
#: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0
msgid "Application Mode:"
msgstr "Mod Aplikasi:"
#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0
msgid "Music"
msgstr "Muzik"
#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0
msgid "Picture"
msgstr "Gambar"
#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0
msgid "Standard"
msgstr "Piawai"
#: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0
msgid "Scan Type:"
msgstr "Jenis Imbasan:"
#: qt/directories_dialog.py:135
msgid "More Options"
msgstr "Pilihan Lanjutan"
#: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0
msgid "Select folders to scan and press \"Scan\"."
msgstr "Pilih folder untuk imbas dan tekan \"Imbas\"."
#: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0
msgid "Load Results"
msgstr "Muatkan Keputusan"
#: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0
msgid "Scan"
msgstr "Imbas"
#: qt/directories_dialog.py:230
msgid "Unsaved results"
msgstr "Keputusan belum disimpan"
#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0
msgid "You have unsaved results, do you really want to quit?"
msgstr ""
"Anda mempunyai keputusan yang belum disimpan, adakah anda pasti anda ingin "
"keluar?"
#: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0
msgid "Select a folder to add to the scanning list"
msgstr "Pilih folder untuk tambah ke senarai imbasan"
#: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0
msgid "Select a results file to load"
msgstr "Pilih fail keputusan untuk dimuatkan"
#: qt/directories_dialog.py:267
msgid "All Files (*.*)"
msgstr "Semua Fail (*.*)"
#: qt/directories_dialog.py:267 qt/result_window.py:311
msgid "dupeGuru Results (*.dupeguru)"
msgstr "Keputusan dupeGuru (*.dupeguru)"
#: qt/directories_dialog.py:278
msgid "Start a new scan"
msgstr "Mulakan imbasan baharu"
#: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0
msgid "You have unsaved results, do you really want to continue?"
msgstr ""
"Anda mempunyai keputusan yang belum disimpan, adakah anda pasti anda ingin "
"teruskan?"
#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0
msgid "Name"
msgstr "Nama"
#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0
msgid "State"
msgstr "Keadaan"
#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0
msgid "Excluded"
msgstr "Dikecualikan"
#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0
msgid "Normal"
msgstr "Biasa"
#: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0
msgid "Remove Selected"
msgstr "Alih Keluar yang Dipilih"
#: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0
msgid "Clear"
msgstr "Kosongkan"
#: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61
#: cocoa/en.lproj/Localizable.strings:0
msgid "Close"
msgstr "Tutup"
#: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24
#: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18
#: cocoa/en.lproj/Localizable.strings:0
msgid "Details"
msgstr "Maklumat"
#: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0
msgid "Tags to scan:"
msgstr "Tag untuk diimbas:"
#: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0
msgid "Track"
msgstr "Runut"
#: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0
msgid "Artist"
msgstr "Artis"
#: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0
msgid "Album"
msgstr "Album"
#: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0
msgid "Title"
msgstr "Tajuk"
#: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0
msgid "Genre"
msgstr "Genre"
#: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0
msgid "Year"
msgstr "Tahun"
#: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30
#: cocoa/en.lproj/Localizable.strings:0
msgid "Word weighting"
msgstr "Pemberatan perkataan"
#: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32
#: cocoa/en.lproj/Localizable.strings:0
msgid "Match similar words"
msgstr "Padan perkataan serupa"
#: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21
#: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0
msgid "Can mix file kind"
msgstr "Boleh campur jenis fail"
#: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23
#: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0
msgid "Use regular expressions when filtering"
msgstr "Guna ungkapan nalar ketika menapis"
#: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25
#: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0
msgid "Remove empty folders on delete or move"
msgstr "Alih keluar folder kosong semasa pemadaman atau pemindahan"
#: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27
#: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0
msgid "Ignore duplicates hardlinking to the same file"
msgstr "Abaikan duplikasi yang paut keras ke fail yang sama"
#: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29
#: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0
msgid "Debug mode (restart required)"
msgstr "Mod nyahpepijat (perlu mula semula)"
#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0
msgid "Match pictures of different dimensions"
msgstr "Padan gambar dengan dimensi berlainan"
#: qt/preferences_dialog.py:43
msgid "Filter Hardness:"
msgstr "Kekuatan Penapis:"
#: qt/preferences_dialog.py:69
msgid "More Results"
msgstr "Lebihkan Keputusan"
#: qt/preferences_dialog.py:74
msgid "Fewer Results"
msgstr "Kurang Keputusan"
#: qt/preferences_dialog.py:81
msgid "Font size:"
msgstr "Saiz fon:"
#: qt/preferences_dialog.py:85
msgid "Language:"
msgstr "Bahasa:"
#: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0
msgid "Copy and Move:"
msgstr "Salin dan Pindah:"
#: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0
msgid "Right in destination"
msgstr "Dalam tempat tujuan"
#: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0
msgid "Recreate relative path"
msgstr "Cipta semula laluan relatif"
#: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0
msgid "Recreate absolute path"
msgstr "Cipta semula laluan mutlak"
#: qt/preferences_dialog.py:99
msgid "Custom Command (arguments: %d for dupe, %r for ref):"
msgstr "Perintah Tersuai (argumen: %d untuk duplikasi, %r untuk rujukan):"
#: qt/preferences_dialog.py:174
msgid "dupeGuru has to restart for language changes to take effect."
msgstr "dupeGuru perlu mula semula untuk menerima kesan perubahan bahasa."
#: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0
msgid "Re-Prioritize duplicates"
msgstr "Pengutamaan semula duplikasi"
#: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0
msgid ""
"Add criteria to the right box and click OK to send the dupes that correspond"
" the best to these criteria to their respective group's reference position. "
"Read the help file for more information."
msgstr ""
"Tambah kriteria di kotak kanan dan klik OK untuk hantar duplikasi yang "
"paling sepadan dengan kriteria tersebut ke kedudukan rujukan kumpulan "
"masing-masing. Baca fail bantuan untuk maklumat lanjut."
#: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0
msgid "Problems!"
msgstr "Masalah!"
#: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0
msgid ""
"There were problems processing some (or all) of the files. The cause of "
"these problems are described in the table below. Those files were not "
"removed from your results."
msgstr ""
"Terdapat masalah semasa memproses sesetengah (atau kesemua) fail. Penyebab "
"masalah ini diterangkan dalam jadual di bawah. Fail tersebut tidak dialih "
"keluar dari keputusan anda."
#: qt/problem_dialog.py:56
msgid "Reveal Selected"
msgstr "Dedahkan yang Dipilih"
#: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167
#: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0
msgid "Actions"
msgstr "Tindakan"
#: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0
msgid "Show Dupes Only"
msgstr "Tunjuk Duplikasi Sahaja"
#: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0
msgid "Show Delta Values"
msgstr "Tunjuk Nilai Delta"
#: qt/result_window.py:60
msgid "Send Marked to Recycle Bin..."
msgstr "Hantar yang Ditandai ke Tong Sampah..."
#: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0
msgid "Move Marked to..."
msgstr "Pindah yang Ditandai ke..."
#: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0
msgid "Copy Marked to..."
msgstr "Salin yang Ditandai ke..."
#: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0
msgid "Remove Marked from Results"
msgstr "Alih Keluar yang Ditandai dari Keputusan"
#: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0
msgid "Re-Prioritize Results..."
msgstr "Pengutamaan Semula Keputusan..."
#: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0
msgid "Remove Selected from Results"
msgstr "Alih Keluar yang Dipilih dari Keputusan"
#: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0
msgid "Add Selected to Ignore List"
msgstr "Tambah yang Dipilih ke Senarai Abai"
#: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0
msgid "Make Selected into Reference"
msgstr "Jadikan yang Dipilih menjadi Rujukan"
#: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0
msgid "Open Selected with Default Application"
msgstr "Buka yang Dipilih dengan Aplikasi Lalai"
#: qt/result_window.py:80
msgid "Open Containing Folder of Selected"
msgstr "Buka Folder yang Mengandungi yang Dipilih"
#: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0
msgid "Rename Selected"
msgstr "Namakan Semula yang Dipilih"
#: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0
msgid "Mark All"
msgstr "Tanda Semua"
#: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0
msgid "Mark None"
msgstr "Tanda Kosong"
#: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0
msgid "Invert Marking"
msgstr "Terbalikkan Penandaan"
#: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0
msgid "Mark Selected"
msgstr "Tanda yang Dipilih"
#: qt/result_window.py:87
msgid "Export To HTML"
msgstr "Eksport ke HTML"
#: qt/result_window.py:88
msgid "Export To CSV"
msgstr "Eksport ke CSV"
#: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0
msgid "Save Results..."
msgstr "Simpan Keputusan..."
#: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0
msgid "Invoke Custom Command"
msgstr "Guna Perintah Tersuai"
#: qt/result_window.py:102
msgid "Mark"
msgstr "Tanda"
#: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0
msgid "Columns"
msgstr "Lajur"
#: qt/result_window.py:163
msgid "Reset to Defaults"
msgstr "Tetap Semula ke Lalai"
#: qt/result_window.py:185
msgid "{} Results"
msgstr "{} Keputusan"
#: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0
msgid "Dupes Only"
msgstr "Duplikasi Sahaja"
#: qt/result_window.py:194
msgid "Delta Values"
msgstr "Nilai Delta"
#: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0
msgid "Select a file to save your results to"
msgstr "Pilih fail untuk simpan keputusan anda"
#: qt/se/preferences_dialog.py:41
msgid "Ignore files smaller than"
msgstr "Abaikan fail lebih kecil dari"
#: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0
msgid "KB"
msgstr "KB"
#: cocoa/en.lproj/Localizable.strings:0
msgid "%@ Results"
msgstr "%@ Keputusan"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Action"
msgstr "Tindakan"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Add New Folder..."
msgstr "Tambah Folder Baharu..."
#: cocoa/en.lproj/Localizable.strings:0
msgid "Advanced"
msgstr "Lanjutan"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Automatically check for updates"
msgstr "Periksa kemas kini secara automatik"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Basic"
msgstr "Asas"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Bring All to Front"
msgstr "Bawa Semua ke Hadapan"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Check for update..."
msgstr "Periksa kemas kini..."
#: cocoa/en.lproj/Localizable.strings:0
msgid "Close Window"
msgstr "Tutup Tetingkap"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Copy"
msgstr "Salin"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Custom command (arguments: %d for dupe, %r for ref):"
msgstr "Perintah tersuai (argumen: %d untuk duplikasi, %r untuk rujukan):"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Cut"
msgstr "Potong"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Delta"
msgstr "Delta"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Details of Selected File"
msgstr "Maklumat Fail yang Dipilih"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Details Panel"
msgstr "Panel Maklumat"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Directories"
msgstr "Direktori"
#: cocoa/en.lproj/Localizable.strings:0
msgid "dupeGuru"
msgstr "dupeGuru"
#: cocoa/en.lproj/Localizable.strings:0
msgid "dupeGuru Preferences"
msgstr "Keutamaan dupeGuru"
#: cocoa/en.lproj/Localizable.strings:0
msgid "dupeGuru Results"
msgstr "Keputusan dupeGuru"
#: cocoa/en.lproj/Localizable.strings:0
msgid "dupeGuru Website"
msgstr "Laman Sesawang dupeGuru"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Edit"
msgstr "Sunting"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Export Results to CSV"
msgstr "Eksport Keputusan ke CSV"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Export Results to XHTML"
msgstr "Eksport Keputusan ke XHTML"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Fewer results"
msgstr "Kurangkan keputusan"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Filter"
msgstr "Penapis"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Filter hardness:"
msgstr "Kekuatan penapisan:"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Filter Results..."
msgstr "Tapis Keputusan..."
#: cocoa/en.lproj/Localizable.strings:0
msgid "Folder Selection Window"
msgstr "Tetingkap Pemilihan Folder"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Font Size:"
msgstr "Saiz Fon:"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Hide dupeGuru"
msgstr "Sembunyikan dupeGuru"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Hide Others"
msgstr "Sembunyikan yang Lain"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Ignore files smaller than:"
msgstr "Abaikan fail lebih kecil dari:"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Load from file..."
msgstr "Muatkan dari fail..."
#: cocoa/en.lproj/Localizable.strings:0
msgid "Minimize"
msgstr "Meminimumkan"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Mode"
msgstr "Mod"
#: cocoa/en.lproj/Localizable.strings:0
msgid "More results"
msgstr "Lebihkan keputusan"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Ok"
msgstr "Ok"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Paste"
msgstr "Tampal"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Preferences..."
msgstr "Keutamaan..."
#: cocoa/en.lproj/Localizable.strings:0
msgid "Quick Look"
msgstr "Lihat Segera"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Quit dupeGuru"
msgstr "Keluar dupeGuru"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Reset to Default"
msgstr "Tetap Semula ke Lalai"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Reset To Defaults"
msgstr "Tetap Semula ke Lalai"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Reveal"
msgstr "Dedah"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Reveal Selected in Finder"
msgstr "Dedah yang Dipilih dalam Pencari"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Select All"
msgstr "Pilih Semua"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Send Marked to Trash..."
msgstr "Hantar yang Ditandai ke Tong Sampah..."
#: cocoa/en.lproj/Localizable.strings:0
msgid "Services"
msgstr "Perkhidmatan"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Show All"
msgstr "Tunjuk Semua"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Start Duplicate Scan"
msgstr "Mulakan Imbasan Duplikasi"
#: cocoa/en.lproj/Localizable.strings:0
msgid "The name '%@' already exists."
msgstr "Nama '%@' sudah wujud."
#: cocoa/en.lproj/Localizable.strings:0
msgid "Window"
msgstr "Tetingkap"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Zoom"
msgstr "Zum"
#: qt\app.py:158
msgid "Exclusion Filters"
msgstr "Penapis Pengecualian"
#: qt\directories_dialog.py:91
msgid "Scan Results"
msgstr "Keputusan Imbasan"
#: qt\directories_dialog.py:95
msgid "Load Directories..."
msgstr "Muatkan Direktori..."
#: qt\directories_dialog.py:96
msgid "Save Directories..."
msgstr "Simpan Direktori..."
#: qt\directories_dialog.py:337
msgid "Select a directories file to load"
msgstr "Pilih fail direktori untuk dimuatkan"
#: qt\directories_dialog.py:338
msgid "dupeGuru Results (*.dupegurudirs)"
msgstr "Keputusan dupeGuru (*.dupegurudirs)"
#: qt\directories_dialog.py:347
msgid "Select a file to save your directories to"
msgstr "Pilih fail untuk simpan direktori anda"
#: qt\directories_dialog.py:348
msgid "dupeGuru Directories (*.dupegurudirs)"
msgstr "Direktori dupeGuru (*.dupegurudirs)"
#: qt\exclude_list_dialog.py:44
msgid "Add"
msgstr "Tambah"
#: qt\exclude_list_dialog.py:46
msgid "Restore defaults"
msgstr "Tetap semula lalai"
#: qt\exclude_list_dialog.py:47
msgid "Test string"
msgstr "Cuba rentetan"
#: qt\exclude_list_dialog.py:83
msgid "Type a python regular expression here..."
msgstr "Taipkan ungkapan nalar python di sini..."
#: qt\exclude_list_dialog.py:85
msgid "Type a file system path or filename here..."
msgstr "Taipkan laluan sistem fail atau nama fail di sini..."
#: qt\exclude_list_dialog.py:152
msgid ""
"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happen to match one of the regular expressions.<br>For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\n"
"<li>2. Regular expressions with no path separator in them will be compared to the full path to the file.</li><br>\n"
"Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:<br><code>.*My\\sPictures\\\\.*\\.png</code><br><br>You can test the regular expression with the test string feature by pasting a fake path in it:<br><code>C:\\\\User\\My Pictures\\test.png</code><br><br>\n"
"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>"
msgstr ""
"Ungkapan nalar python (sensitif huruf) ini akan menapis keluar fail ketika imbasan.<br>Direktori juga akan ada <strong>keadaan lalai</strong>sendiri ditetapkan kepada Dikecualikan dalam tab Direktori jika nama tersebut sepadan dengan salah satu ungkapan nalar.<br>Untuk setiap fail yang terhimpun, dua percubaan akan dilaksanakan bagi setiap satu fail tersebut untuk menentukan sama ada fail tersebut perlu ditapis keluar:<br><li>1. Ungkapan nalar tanpa pemisah laluan di dalamnya akan dibandingkan dengan nama fail sahaja.</li>\n"
"<li>2. Ungkapan nalar tanpa pemisah laluan di dalamnya akan dibandingkan dengan laluan penuh ke fail.</li><br>\n"
"Contoh: jika anda ingin tapis keluar fail .PNG dari direktori \"My Pictures\" sahaja:<br><code>.*My\\sPictures\\\\.*\\.png</code><br><br>Anda boleh cuba ungkapan nalar dengan fungsi cuba rentetan dengan menampal laluan palsu di dalamnya:<br><code>C:\\\\User\\My Pictures\\test.png</code><br><br>\n"
"Ungkapan nalar yang terpadan akan ditonjolkan.<br>Sekiranya ada sekurang-kurangnya satu tonjolan, laluan yang dicuba akan diabaikan ketika imbasan.<br><br>Direktori dan fail yang bermula dengan tanda titik '.' ditapis keluar secara lalainya.<br><br>"
#: qt\exclude_list_table.py:36
msgid "Compilation error: "
msgstr "Ralat pengkompilan:"
#: qt\pe\image_viewer.py:56
msgid "Increase zoom"
msgstr "Naikkan zum"
#: qt\pe\image_viewer.py:66
msgid "Decrease zoom"
msgstr "Turunkan zum"
#: qt\pe\image_viewer.py:71
msgid "Ctrl+/"
msgstr "Ctrl+/"
#: qt\pe\image_viewer.py:76
msgid "Normal size"
msgstr "Saiz biasa"
#: qt\pe\image_viewer.py:81
msgid "Ctrl+*"
msgstr "Ctrl+*"
#: qt\pe\image_viewer.py:86
msgid "Best fit"
msgstr "Suaian terbaik"
#: qt\pe\preferences_dialog.py:49
msgid "Picture cache mode:"
msgstr "Mod cache gambar:"
#: qt\pe\preferences_dialog.py:56
msgid "Override theme icons in viewer toolbar"
msgstr "Mengataskan ikon tema dalam bar alat pemidang"
#: qt\pe\preferences_dialog.py:58
msgid ""
"Use our own internal icons instead of those provided by the theme engine"
msgstr ""
"Guna ikon dalaman kami sendiri menggantikan apa yang disediakan oleh enjin "
"tema"
#: qt\pe\preferences_dialog.py:66
msgid "Show scrollbars in image viewers"
msgstr "Tunjuk bar tatal dalam pemidang imej"
#: qt\pe\preferences_dialog.py:68
msgid ""
"When the image displayed doesn't fit the viewport, show scrollbars to span "
"the view around"
msgstr ""
"Apabila imej yang dipaparkan tidak muat dalam port pandang, tunjuk bar tatal"
" untuk menggerakkan pemidang"
#: qt\preferences_dialog.py:156
msgid "Use default position for tab bar (requires restart)"
msgstr "Guna kedudukan lalai untuk bar tab (perlu mula semula)"
#: qt\preferences_dialog.py:158
msgid ""
"Place the tab bar below the main menu instead of next to it\n"
"On MacOS, the tab bar will fill up the window's width instead."
msgstr ""
"Letak bar tab di bawah menu utama dan bukannya di sebelahnya\n"
"Di MacOS, bar tab akan mengisi lebar tetingkap."
#: qt\preferences_dialog.py:172
msgid "Use bold font for references"
msgstr "Guna fon tebal untuk rujukan"
#: qt\preferences_dialog.py:176
msgid "Reference foreground color:"
msgstr "Warna latar depan rujukan:"
#: qt\preferences_dialog.py:179
msgid "Reference background color:"
msgstr "Warna latar belakang rujukan:"
#: qt\preferences_dialog.py:182 qt\preferences_dialog.py:216
msgid "Delta foreground color:"
msgstr "Warna latar depan delta:"
#: qt\preferences_dialog.py:195
msgid "Show the title bar and can be docked"
msgstr "Tunjuk bar tajuk dan boleh dilimbungkan"
#: qt\preferences_dialog.py:197
msgid ""
"While the title bar is hidden, use the modifier key to drag the floating "
"window around"
msgstr ""
"Apabila bar tajuk disembunyikan, guna kekunci pengubah suai untuk seret "
"tetingkap terapung"
#: qt\preferences_dialog.py:199
msgid "The title bar can only be disabled while the window is docked"
msgstr "Bar tajuk hanya boleh dilumpuhkan ketika tetingkap dilimbungkan"
#: qt\preferences_dialog.py:202
msgid "Vertical title bar"
msgstr "Bar tajuk menegak"
#: qt\preferences_dialog.py:204
msgid ""
"Change the title bar from horizontal on top, to vertical on the left side"
msgstr ""
"Ubah bar tajuk daripada melintang di atas, kepada menegak di sisi kiri"
#: qt\tabbed_window.py:44
msgid "Show tab bar"
msgstr "Tunjuk bar tab"
#: qt\exclude_list_dialog.py:152
msgid ""
"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\n"
"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>\n"
"Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:<br><code>.*My\\sPictures\\\\.*\\.png</code><br><br>You can test the regular expression with the \"test string\" button after pasting a fake path in the test field:<br><code>C:\\\\User\\My Pictures\\test.png</code><br><br>\n"
"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>"
msgstr ""
"Ungkapan nalar python (sensitif huruf) ini akan menapis keluar fail ketika imbasan.<br>Direktori juga akan ada <strong>keadaan lalai</strong> sendiri ditetapkan kepada Dikecualikan dalam tab Direktori jika nama tersebut sepadan dengan salah satu daripada ungkapan nalar yang dipilih.<br>Untuk setiap fail yang terhimpun, dua percubaan akan dilaksanakan untuk menentukan sama ada fail tersebut perlu diabaikan sepenuhnya:<br><li>1. Ungkapan nalar tanpa pemisah laluan di dalamnya akan dibandingkan dengan nama fail sahaja.</li>\n"
"<li>2. Ungkapan nalar dengan sekurang-kurangnya satu pemisah laluan di dalamnya akan dibandingkan dengan laluan penuh ke fail.</li><br>\n"
"Contoh: jika anda ingin tapis keluar fail .PNG dari direktori \"My Pictures\" sahaja:<br><code>.*My\\sPictures\\\\.*\\.png</code><br><br>Anda boleh cuba ungkapan nalar dengan butang \"cuba rentetan\" selepas menampal laluan palsi dalam medan percubaan:<br><code>C:\\\\User\\My Pictures\\test.png</code><br><br>\n"
"Ungkapan nalar yang terpadan akan ditonjolkan.<br>Sekiranya ada sekurang-kurangnya satu tonjolan, laluan atau nama fail yang dicuba akan diabaikan ketika imbasan.<br><br>Direktori dan fail yang bermula dengan tanda titik '.' ditapis keluar secara lalainya.<br><br>"
#: qt\app.py:256
msgid "Results"
msgstr "Keputusan"
#: qt\preferences_dialog.py:150
msgid "General Interface"
msgstr "Antara Muka Am"
#: qt\preferences_dialog.py:176
msgid "Result Table"
msgstr "Jadual Keputusan"
#: qt\preferences_dialog.py:205
msgid "Details Window"
msgstr "Tetingkap Maklumat"
#: qt\preferences_dialog.py:285
msgid "General"
msgstr "Am"
#: qt\preferences_dialog.py:286
msgid "Display"
msgstr "Paparan"
#: qt\se\preferences_dialog.py:70
msgid "Partially hash files bigger than"
msgstr "Fail cincang separa lebih besar dari"
#: qt\se\preferences_dialog.py:80
msgid "MB"
msgstr "MB"
#: qt\preferences_dialog.py:163
msgid "Use native OS dialogs"
msgstr "Guna dialog OS natif"
#: qt\preferences_dialog.py:166
msgid ""
"For actions such as file/folder selection use the OS native dialogs.\n"
"Some native dialogs have limited functionality."
msgstr ""
"Gunakan dialog natif OS untuk tindakan seperti pemilihan fail/folder.\n"
"Sesetengah dialog natif mempunyai kefungsian yang terhad."
#: qt\se\preferences_dialog.py:68
msgid "Ignore files larger than"
msgstr "Abaikan fail lebih besar dari"
#: qt\app.py:135 qt\app.py:293
msgid "Clear Cache"
msgstr "Kosongkan Cache"
#: qt\app.py:294
msgid ""
"Do you really want to clear the cache? This will remove all cached file "
"hashes and picture analysis."
msgstr ""
"Adakah anda pasti anda ingin kosongkan cache? Ini akan alih keluar semua "
"cincang fail dan analisis gambar yang tercache."
#: qt\app.py:299
msgid "Cache cleared."
msgstr "Cache dikosongkan."
#: qt\preferences_dialog.py:173
msgid "Use dark style"
msgstr ""

View File

@@ -49,7 +49,7 @@ msgstr "Kopiëren"
msgid "Sending to Trash" msgid "Sending to Trash"
msgstr "Naar de prullebak verplaatsen" msgstr "Naar de prullebak verplaatsen"
#: core\app.py:308 #: core\app.py:289
msgid "" msgid ""
"A previous action is still hanging in there. You can't start a new one yet. " "A previous action is still hanging in there. You can't start a new one yet. "
"Wait a few seconds, then try again." "Wait a few seconds, then try again."
@@ -57,35 +57,39 @@ msgstr ""
"Er is nog een vorige actie bezig. Je kan nu nog geen nieuwe actie starten. " "Er is nog een vorige actie bezig. Je kan nu nog geen nieuwe actie starten. "
"Wacht een paar seconden en probeer het opnieuw" "Wacht een paar seconden en probeer het opnieuw"
#: core\app.py:318 #: core\app.py:300
msgid "No duplicates found." msgid "No duplicates found."
msgstr "Geen dubbelingen gevonden" msgstr "Geen dubbelingen gevonden"
#: core\app.py:333 #: core\app.py:315
msgid "All marked files were copied successfully." msgid "All marked files were copied successfully."
msgstr "Alle gemarkeerde bestanden zijn succesvol gekopieerd." msgstr "Alle gemarkeerde bestanden zijn succesvol gekopieerd."
#: core\app.py:334 #: core\app.py:317
msgid "All marked files were moved successfully." msgid "All marked files were moved successfully."
msgstr "Alle gemarkeerde bestanden zijn succesvol verplaatst." msgstr "Alle gemarkeerde bestanden zijn succesvol verplaatst."
#: core\app.py:335 #: core\app.py:319
msgid "All marked files were deleted successfully."
msgstr ""
#: core\app.py:321
msgid "All marked files were successfully sent to Trash." msgid "All marked files were successfully sent to Trash."
msgstr "Alle gemarkeerde bestanden zijn met succes in de prullenbak gedaan." msgstr "Alle gemarkeerde bestanden zijn met succes in de prullenbak gedaan."
#: core\app.py:343 #: core\app.py:326
msgid "Could not load file: {}" msgid "Could not load file: {}"
msgstr "Kan bestand niet laden: {}" msgstr "Kan bestand niet laden: {}"
#: core\app.py:399 #: core\app.py:382
msgid "'{}' already is in the list." msgid "'{}' already is in the list."
msgstr "'{}' staat al in de lijst." msgstr "'{}' staat al in de lijst."
#: core\app.py:401 #: core\app.py:384
msgid "'{}' does not exist." msgid "'{}' does not exist."
msgstr "'{}' bestaat niet." msgstr "'{}' bestaat niet."
#: core\app.py:410 #: core\app.py:392
msgid "" msgid ""
"All selected %d matches are going to be ignored in all subsequent scans. " "All selected %d matches are going to be ignored in all subsequent scans. "
"Continue?" "Continue?"
@@ -93,68 +97,72 @@ msgstr ""
"Alle geselecteerde %d overeenkomsten zullen in toekomstige onderzoeken " "Alle geselecteerde %d overeenkomsten zullen in toekomstige onderzoeken "
"worden overgslagen. Doorgaan?" "worden overgslagen. Doorgaan?"
#: core\app.py:486 #: core\app.py:469
msgid "Select a directory to copy marked files to" msgid "Select a directory to copy marked files to"
msgstr "" msgstr ""
"Selecteer een map waar u de gemarkeerde bestanden naartoe wilt kopiëren" "Selecteer een map waar u de gemarkeerde bestanden naartoe wilt kopiëren"
#: core\app.py:487 #: core\app.py:471
msgid "Select a directory to move marked files to" msgid "Select a directory to move marked files to"
msgstr "" msgstr ""
"Selecteer een map waar u de gemarkeerde bestanden naartoe wilt verplaatsen" "Selecteer een map waar u de gemarkeerde bestanden naartoe wilt verplaatsen"
#: core\app.py:527 #: core\app.py:510
msgid "Select a destination for your exported CSV" msgid "Select a destination for your exported CSV"
msgstr "Selecteer een locatie voor de CSV export" msgstr "Selecteer een locatie voor de CSV export"
#: core\app.py:534 core\app.py:801 core\app.py:811 #: core\app.py:516 core\app.py:771 core\app.py:781
msgid "Couldn't write to file: {}" msgid "Couldn't write to file: {}"
msgstr "Kan niet schrijven naar bestand: {}" msgstr "Kan niet schrijven naar bestand: {}"
#: core\app.py:559 #: core\app.py:539
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 ""
"Er is nog geen \"aangepaste opdracht\" ingericht. Je kan dit doen bij de " "Er is nog geen \"aangepaste opdracht\" ingericht. Je kan dit doen bij de "
"voorkeuren." "voorkeuren."
#: core\app.py:727 core\app.py:740 #: core\app.py:695 core\app.py:707
msgid "You are about to remove %d files from results. Continue?" msgid "You are about to remove %d files from results. Continue?"
msgstr "" msgstr ""
"Je staat op het punt om %d bestanden te verwijderen uit de resultaten. " "Je staat op het punt om %d bestanden te verwijderen uit de resultaten. "
"Doorgaan?" "Doorgaan?"
#: core\app.py:774 #: core\app.py:743
msgid "{} duplicate groups were changed by the re-prioritization." msgid "{} duplicate groups were changed by the re-prioritization."
msgstr "" msgstr ""
"{} dubbelingen groepen waren veranderd door de prioriteits verschuiving." "{} dubbelingen groepen waren veranderd door de prioriteits verschuiving."
#: core\app.py:821 #: core\app.py:790
msgid "The selected directories contain no scannable file." msgid "The selected directories contain no scannable file."
msgstr "" msgstr ""
"De geselecteerde folders bevatten geen bestanden die onderzocht kunnen " "De geselecteerde folders bevatten geen bestanden die onderzocht kunnen "
"worden." "worden."
#: core\app.py:835 #: core\app.py:803
msgid "Collecting files to scan" msgid "Collecting files to scan"
msgstr "Bestanden aan het verzamelen om te onderzoeken" msgstr "Bestanden aan het verzamelen om te onderzoeken"
#: core\app.py:891 #: core\app.py:850
msgid "%s (%d discarded)" msgid "%s (%d discarded)"
msgstr "%s (%d weggelaten)" msgstr "%s (%d weggelaten)"
#: core\engine.py:244 core\engine.py:288 #: core\directories.py:191
msgid "0 matches found" msgid "Collected {} files to scan"
msgstr "0 overeenkomsten gevonden" msgstr ""
#: core\engine.py:262 core\engine.py:296 #: core\directories.py:207
msgid "%d matches found" msgid "Collected {} folders to scan"
msgstr "%d overeenkomsten gevonden" msgstr ""
#: core\gui\deletion_options.py:73 #: core\engine.py:27
msgid "%d matches found from %d groups"
msgstr ""
#: core\gui\deletion_options.py:71
msgid "You are sending {} file(s) to the Trash." msgid "You are sending {} file(s) to the Trash."
msgstr "Je verplaatst {} bestand(en) naar de prullenbak" msgstr "Je verplaatst {} bestand(en) naar de prullenbak"
#: core\gui\exclude_list_table.py:15 #: core\gui\exclude_list_table.py:14
msgid "Regular Expressions" msgid "Regular Expressions"
msgstr "Normale Uitdrukkingen" msgstr "Normale Uitdrukkingen"
@@ -187,15 +195,15 @@ msgstr "Inhoud"
msgid "Analyzed %d/%d pictures" msgid "Analyzed %d/%d pictures"
msgstr "%d van de %d afbeeldingen aan het analyseren" msgstr "%d van de %d afbeeldingen aan het analyseren"
#: core\pe\matchblock.py:181 #: core\pe\matchblock.py:177
msgid "Performed %d/%d chunk matches" msgid "Performed %d/%d chunk matches"
msgstr "%d van de %d bulk overeenkomsten uitgevoerd" msgstr "%d van de %d bulk overeenkomsten uitgevoerd"
#: core\pe\matchblock.py:191 #: core\pe\matchblock.py:185
msgid "Preparing for matching" msgid "Preparing for matching"
msgstr "Voorbereiden voor dubbelingen bepaling" msgstr "Voorbereiden voor dubbelingen bepaling"
#: core\pe\matchblock.py:244 #: core\pe\matchblock.py:234
msgid "Verified %d/%d matches" msgid "Verified %d/%d matches"
msgstr "%d van de %d overeenkomsten nagekeken" msgstr "%d van de %d overeenkomsten nagekeken"
@@ -243,23 +251,23 @@ msgstr "nieuwste"
msgid "Oldest" msgid "Oldest"
msgstr "oudste" msgstr "oudste"
#: core\results.py:142 #: core\results.py:134
msgid "%d / %d (%s / %s) duplicates marked." msgid "%d / %d (%s / %s) duplicates marked."
msgstr "%d / %d (%s /%s) dubbelingen gemarkeerd" msgstr "%d / %d (%s /%s) dubbelingen gemarkeerd"
#: core\results.py:149 #: core\results.py:141
msgid " filter: %s" msgid " filter: %s"
msgstr "filter: %s" msgstr "filter: %s"
#: core\scanner.py:85 #: core\scanner.py:90
msgid "Read size of %d/%d files" msgid "Read size of %d/%d files"
msgstr "Bestandsgrootte van %d/%d bestanden aan het lezen." msgstr "Bestandsgrootte van %d/%d bestanden aan het lezen."
#: core\scanner.py:109 #: core\scanner.py:116
msgid "Read metadata of %d/%d files" msgid "Read metadata of %d/%d files"
msgstr "Metadata van %d/%d bestanden gelezen" msgstr "Metadata van %d/%d bestanden gelezen"
#: core\scanner.py:147 #: core\scanner.py:154
msgid "Almost done! Fiddling with results..." msgid "Almost done! Fiddling with results..."
msgstr "Bijna klaar! Gehannes met resultaten..." msgstr "Bijna klaar! Gehannes met resultaten..."

View File

@@ -945,3 +945,43 @@ msgstr "Algemeen"
#: qt\preferences_dialog.py:286 #: qt\preferences_dialog.py:286
msgid "Display" msgid "Display"
msgstr "Scherm" msgstr "Scherm"
#: qt\se\preferences_dialog.py:70
msgid "Partially hash files bigger than"
msgstr ""
#: qt\se\preferences_dialog.py:80
msgid "MB"
msgstr ""
#: qt\preferences_dialog.py:163
msgid "Use native OS dialogs"
msgstr ""
#: qt\preferences_dialog.py:166
msgid ""
"For actions such as file/folder selection use the OS native dialogs.\n"
"Some native dialogs have limited functionality."
msgstr ""
#: qt\se\preferences_dialog.py:68
msgid "Ignore files larger than"
msgstr ""
#: qt\app.py:135 qt\app.py:293
msgid "Clear Cache"
msgstr ""
#: qt\app.py:294
msgid ""
"Do you really want to clear the cache? This will remove all cached file "
"hashes and picture analysis."
msgstr ""
#: qt\app.py:299
msgid "Cache cleared."
msgstr ""
#: qt\preferences_dialog.py:173
msgid "Use dark style"
msgstr ""

View File

@@ -48,7 +48,7 @@ msgstr "Kopiowanie"
msgid "Sending to Trash" msgid "Sending to Trash"
msgstr "Wysyłam do kosza" msgstr "Wysyłam do kosza"
#: core\app.py:308 #: core\app.py:289
msgid "" msgid ""
"A previous action is still hanging in there. You can't start a new one yet. " "A previous action is still hanging in there. You can't start a new one yet. "
"Wait a few seconds, then try again." "Wait a few seconds, then try again."
@@ -56,35 +56,39 @@ msgstr ""
"Wciąż wisi tam poprzednia akcja. Nie możesz jeszcze rozpocząć nowego. " "Wciąż wisi tam poprzednia akcja. Nie możesz jeszcze rozpocząć nowego. "
"Poczekaj kilka sekund i spróbuj ponownie." "Poczekaj kilka sekund i spróbuj ponownie."
#: core\app.py:318 #: core\app.py:300
msgid "No duplicates found." msgid "No duplicates found."
msgstr "Nie znaleziono duplikatów." msgstr "Nie znaleziono duplikatów."
#: core\app.py:333 #: core\app.py:315
msgid "All marked files were copied successfully." msgid "All marked files were copied successfully."
msgstr "Wszystkie zaznaczone pliki zostały pomyślnie skopiowane." msgstr "Wszystkie zaznaczone pliki zostały pomyślnie skopiowane."
#: core\app.py:334 #: core\app.py:317
msgid "All marked files were moved successfully." msgid "All marked files were moved successfully."
msgstr "Wszystkie zaznaczone pliki zostały pomyślnie przeniesione." msgstr "Wszystkie zaznaczone pliki zostały pomyślnie przeniesione."
#: core\app.py:335 #: core\app.py:319
msgid "All marked files were deleted successfully."
msgstr ""
#: core\app.py:321
msgid "All marked files were successfully sent to Trash." msgid "All marked files were successfully sent to Trash."
msgstr "Wszystkie zaznaczone pliki zostały pomyślnie wysłane do kosza." msgstr "Wszystkie zaznaczone pliki zostały pomyślnie wysłane do kosza."
#: core\app.py:343 #: core\app.py:326
msgid "Could not load file: {}" msgid "Could not load file: {}"
msgstr "Nie udało się załadować pliku: {}" msgstr "Nie udało się załadować pliku: {}"
#: core\app.py:399 #: core\app.py:382
msgid "'{}' already is in the list." msgid "'{}' already is in the list."
msgstr "'{}' jest już na liście." msgstr "'{}' jest już na liście."
#: core\app.py:401 #: core\app.py:384
msgid "'{}' does not exist." msgid "'{}' does not exist."
msgstr "'{}' nie istnieje." msgstr "'{}' nie istnieje."
#: core\app.py:410 #: core\app.py:392
msgid "" msgid ""
"All selected %d matches are going to be ignored in all subsequent scans. " "All selected %d matches are going to be ignored in all subsequent scans. "
"Continue?" "Continue?"
@@ -92,61 +96,65 @@ msgstr ""
"Wszystkie zaznaczone %d duplikaty będą ignorowane w kolejnych skanach. " "Wszystkie zaznaczone %d duplikaty będą ignorowane w kolejnych skanach. "
"Kontynuować?" "Kontynuować?"
#: core\app.py:486 #: core\app.py:469
msgid "Select a directory to copy marked files to" msgid "Select a directory to copy marked files to"
msgstr "Wybierz katalog, do którego chcesz skopiować zaznaczone pliki" msgstr "Wybierz katalog, do którego chcesz skopiować zaznaczone pliki"
#: core\app.py:487 #: core\app.py:471
msgid "Select a directory to move marked files to" msgid "Select a directory to move marked files to"
msgstr "Wybierz katalog, do którego chcesz przenieść zaznaczone pliki" msgstr "Wybierz katalog, do którego chcesz przenieść zaznaczone pliki"
#: core\app.py:527 #: core\app.py:510
msgid "Select a destination for your exported CSV" msgid "Select a destination for your exported CSV"
msgstr "Wybierz miejsce docelowe dla eksportowanego pliku CSV" msgstr "Wybierz miejsce docelowe dla eksportowanego pliku CSV"
#: core\app.py:534 core\app.py:801 core\app.py:811 #: core\app.py:516 core\app.py:771 core\app.py:781
msgid "Couldn't write to file: {}" msgid "Couldn't write to file: {}"
msgstr "Nie udało się zapisać do pliku: {}" msgstr "Nie udało się zapisać do pliku: {}"
#: core\app.py:559 #: core\app.py:539
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 ""
"Nie masz skonfigurowanego polecenia niestandardowego. Ustaw to w swoich " "Nie masz skonfigurowanego polecenia niestandardowego. Ustaw to w swoich "
"preferencjach." "preferencjach."
#: core\app.py:727 core\app.py:740 #: core\app.py:695 core\app.py:707
msgid "You are about to remove %d files from results. Continue?" msgid "You are about to remove %d files from results. Continue?"
msgstr "Zamierzasz usunąć %d plików z wyników. Kontyntynuj?" msgstr "Zamierzasz usunąć %d plików z wyników. Kontyntynuj?"
#: core\app.py:774 #: core\app.py:743
msgid "{} duplicate groups were changed by the re-prioritization." msgid "{} duplicate groups were changed by the re-prioritization."
msgstr "{} zduplikowanych grup zmieniono przez ponowne ustalenie priorytetów." msgstr "{} zduplikowanych grup zmieniono przez ponowne ustalenie priorytetów."
#: core\app.py:821 #: core\app.py:790
msgid "The selected directories contain no scannable file." msgid "The selected directories contain no scannable file."
msgstr "Wybrane katalogi nie zawierają plik skanowalną." msgstr "Wybrane katalogi nie zawierają plik skanowalną."
#: core\app.py:835 #: core\app.py:803
msgid "Collecting files to scan" msgid "Collecting files to scan"
msgstr "Zbieranie plików do skanowania" msgstr "Zbieranie plików do skanowania"
#: core\app.py:891 #: core\app.py:850
msgid "%s (%d discarded)" msgid "%s (%d discarded)"
msgstr "%s(%d odrzucone)" msgstr "%s(%d odrzucone)"
#: core\engine.py:244 core\engine.py:288 #: core\directories.py:191
msgid "0 matches found" msgid "Collected {} files to scan"
msgstr "Znaleziono 0 pasujących wyników" msgstr ""
#: core\engine.py:262 core\engine.py:296 #: core\directories.py:207
msgid "%d matches found" msgid "Collected {} folders to scan"
msgstr "Znaleziono %d pasujących wyników" msgstr ""
#: core\gui\deletion_options.py:73 #: core\engine.py:27
msgid "%d matches found from %d groups"
msgstr ""
#: core\gui\deletion_options.py:71
msgid "You are sending {} file(s) to the Trash." msgid "You are sending {} file(s) to the Trash."
msgstr "Wysyłasz {} plików do Kosza" msgstr "Wysyłasz {} plików do Kosza"
#: core\gui\exclude_list_table.py:15 #: core\gui\exclude_list_table.py:14
msgid "Regular Expressions" msgid "Regular Expressions"
msgstr "Wyrażenia regularne" msgstr "Wyrażenia regularne"
@@ -178,15 +186,15 @@ msgstr "Treść"
msgid "Analyzed %d/%d pictures" msgid "Analyzed %d/%d pictures"
msgstr "Analizowane %d/%d zdjęć" msgstr "Analizowane %d/%d zdjęć"
#: core\pe\matchblock.py:181 #: core\pe\matchblock.py:177
msgid "Performed %d/%d chunk matches" msgid "Performed %d/%d chunk matches"
msgstr "Wykonano %d/%d dopasowań fragmentów" msgstr "Wykonano %d/%d dopasowań fragmentów"
#: core\pe\matchblock.py:191 #: core\pe\matchblock.py:185
msgid "Preparing for matching" msgid "Preparing for matching"
msgstr "Przygotowanie do dopasowania" msgstr "Przygotowanie do dopasowania"
#: core\pe\matchblock.py:244 #: core\pe\matchblock.py:234
msgid "Verified %d/%d matches" msgid "Verified %d/%d matches"
msgstr "Zweryfikowane %d/%d meczów" msgstr "Zweryfikowane %d/%d meczów"
@@ -234,23 +242,23 @@ msgstr "Najnowsza"
msgid "Oldest" msgid "Oldest"
msgstr "Najstarszy" msgstr "Najstarszy"
#: core\results.py:142 #: core\results.py:134
msgid "%d / %d (%s / %s) duplicates marked." msgid "%d / %d (%s / %s) duplicates marked."
msgstr "%d / %d (%s / %s) duplikaty oznakowane." msgstr "%d / %d (%s / %s) duplikaty oznakowane."
#: core\results.py:149 #: core\results.py:141
msgid " filter: %s" msgid " filter: %s"
msgstr " filtr: %s" msgstr " filtr: %s"
#: core\scanner.py:85 #: core\scanner.py:90
msgid "Read size of %d/%d files" msgid "Read size of %d/%d files"
msgstr "Odczytaj rozmiar %d/%d plików" msgstr "Odczytaj rozmiar %d/%d plików"
#: core\scanner.py:109 #: core\scanner.py:116
msgid "Read metadata of %d/%d files" msgid "Read metadata of %d/%d files"
msgstr "Przeczytaj metadane %d/%d plików" msgstr "Przeczytaj metadane %d/%d plików"
#: core\scanner.py:147 #: core\scanner.py:154
msgid "Almost done! Fiddling with results..." msgid "Almost done! Fiddling with results..."
msgstr "Prawie skończone! Porządkowanie wyników..." msgstr "Prawie skończone! Porządkowanie wyników..."

View File

@@ -943,3 +943,43 @@ msgstr "Generał"
#: qt\preferences_dialog.py:286 #: qt\preferences_dialog.py:286
msgid "Display" msgid "Display"
msgstr "Pokaz" msgstr "Pokaz"
#: qt\se\preferences_dialog.py:70
msgid "Partially hash files bigger than"
msgstr ""
#: qt\se\preferences_dialog.py:80
msgid "MB"
msgstr ""
#: qt\preferences_dialog.py:163
msgid "Use native OS dialogs"
msgstr ""
#: qt\preferences_dialog.py:166
msgid ""
"For actions such as file/folder selection use the OS native dialogs.\n"
"Some native dialogs have limited functionality."
msgstr ""
#: qt\se\preferences_dialog.py:68
msgid "Ignore files larger than"
msgstr ""
#: qt\app.py:135 qt\app.py:293
msgid "Clear Cache"
msgstr ""
#: qt\app.py:294
msgid ""
"Do you really want to clear the cache? This will remove all cached file "
"hashes and picture analysis."
msgstr ""
#: qt\app.py:299
msgid "Cache cleared."
msgstr ""
#: qt\preferences_dialog.py:173
msgid "Use dark style"
msgstr ""

View File

@@ -47,7 +47,7 @@ msgstr "Copiando"
msgid "Sending to Trash" msgid "Sending to Trash"
msgstr "Movendo para o Lixo" msgstr "Movendo para o Lixo"
#: core\app.py:308 #: core\app.py:289
msgid "" msgid ""
"A previous action is still hanging in there. You can't start a new one yet. " "A previous action is still hanging in there. You can't start a new one yet. "
"Wait a few seconds, then try again." "Wait a few seconds, then try again."
@@ -55,94 +55,102 @@ msgstr ""
"Ainda há uma ação em andamento. Não é possível iniciar outra agora. Espere " "Ainda há uma ação em andamento. Não é possível iniciar outra agora. Espere "
"alguns segundos e tente novamente." "alguns segundos e tente novamente."
#: core\app.py:318 #: core\app.py:300
msgid "No duplicates found." msgid "No duplicates found."
msgstr "Nenhuma duplicata encontrada." msgstr "Nenhuma duplicata encontrada."
#: core\app.py:333 #: core\app.py:315
msgid "All marked files were copied successfully." msgid "All marked files were copied successfully."
msgstr "Todos os arquivos marcados foram copiados corretamente." msgstr "Todos os arquivos marcados foram copiados corretamente."
#: core\app.py:334 #: core\app.py:317
msgid "All marked files were moved successfully." msgid "All marked files were moved successfully."
msgstr "Todos os arquivos marcados foram relocados corretamente." msgstr "Todos os arquivos marcados foram relocados corretamente."
#: core\app.py:335 #: core\app.py:319
msgid "All marked files were deleted successfully."
msgstr ""
#: core\app.py:321
msgid "All marked files were successfully sent to Trash." msgid "All marked files were successfully sent to Trash."
msgstr "Todos os arquivos marcados foram movidos para o Lixo corretamente." msgstr "Todos os arquivos marcados foram movidos para o Lixo corretamente."
#: core\app.py:343 #: core\app.py:326
msgid "Could not load file: {}" msgid "Could not load file: {}"
msgstr "Não foi possível carregar o arquivo: {}" msgstr "Não foi possível carregar o arquivo: {}"
#: core\app.py:399 #: core\app.py:382
msgid "'{}' already is in the list." msgid "'{}' already is in the list."
msgstr "{} já está na lista." msgstr "{} já está na lista."
#: core\app.py:401 #: core\app.py:384
msgid "'{}' does not exist." msgid "'{}' does not exist."
msgstr "{} não existe." msgstr "{} não existe."
#: core\app.py:410 #: core\app.py:392
msgid "" msgid ""
"All selected %d matches are going to be ignored in all subsequent scans. " "All selected %d matches are going to be ignored in all subsequent scans. "
"Continue?" "Continue?"
msgstr "Excluir %d duplicata(s) selecionada(s) de escaneamentos posteriores?" msgstr "Excluir %d duplicata(s) selecionada(s) de escaneamentos posteriores?"
#: core\app.py:486 #: core\app.py:469
msgid "Select a directory to copy marked files to" msgid "Select a directory to copy marked files to"
msgstr "Selecione um diretório onde deseja copiar os arquivos marcados" msgstr "Selecione um diretório onde deseja copiar os arquivos marcados"
#: core\app.py:487 #: core\app.py:471
msgid "Select a directory to move marked files to" msgid "Select a directory to move marked files to"
msgstr "Selecione um diretório para onde deseja mover os arquivos marcados" msgstr "Selecione um diretório para onde deseja mover os arquivos marcados"
#: core\app.py:527 #: core\app.py:510
msgid "Select a destination for your exported CSV" msgid "Select a destination for your exported CSV"
msgstr "Selecione uma pasta para o CSV exportado" msgstr "Selecione uma pasta para o CSV exportado"
#: core\app.py:534 core\app.py:801 core\app.py:811 #: core\app.py:516 core\app.py:771 core\app.py:781
msgid "Couldn't write to file: {}" msgid "Couldn't write to file: {}"
msgstr "Não foi possível gravar no arquivo: {}" msgstr "Não foi possível gravar no arquivo: {}"
#: core\app.py:559 #: core\app.py:539
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 ""
"Você não possui nenhum comando personalizado. Crie um nas preferências." "Você não possui nenhum comando personalizado. Crie um nas preferências."
#: core\app.py:727 core\app.py:740 #: core\app.py:695 core\app.py:707
msgid "You are about to remove %d files from results. Continue?" msgid "You are about to remove %d files from results. Continue?"
msgstr "Remover %d arquivo(s) dos resultados?" msgstr "Remover %d arquivo(s) dos resultados?"
#: core\app.py:774 #: core\app.py:743
msgid "{} duplicate groups were changed by the re-prioritization." msgid "{} duplicate groups were changed by the re-prioritization."
msgstr "{} grupos de duplicatas alterados ao repriorizar." msgstr "{} grupos de duplicatas alterados ao repriorizar."
#: core\app.py:821 #: core\app.py:790
msgid "The selected directories contain no scannable file." msgid "The selected directories contain no scannable file."
msgstr "As pastas selecionadas não contém arquivos escaneáveis." msgstr "As pastas selecionadas não contém arquivos escaneáveis."
#: core\app.py:835 #: core\app.py:803
msgid "Collecting files to scan" msgid "Collecting files to scan"
msgstr "Juntando arquivos para escanear" msgstr "Juntando arquivos para escanear"
#: core\app.py:891 #: core\app.py:850
msgid "%s (%d discarded)" msgid "%s (%d discarded)"
msgstr "%s (%d rejeitado(s))" msgstr "%s (%d rejeitado(s))"
#: core\engine.py:244 core\engine.py:288 #: core\directories.py:191
msgid "0 matches found" msgid "Collected {} files to scan"
msgstr "0 resultados encontrados" msgstr ""
#: core\engine.py:262 core\engine.py:296 #: core\directories.py:207
msgid "%d matches found" msgid "Collected {} folders to scan"
msgstr "%d resultados encontrados" msgstr ""
#: core\gui\deletion_options.py:73 #: core\engine.py:27
msgid "%d matches found from %d groups"
msgstr ""
#: core\gui\deletion_options.py:71
msgid "You are sending {} file(s) to the Trash." msgid "You are sending {} file(s) to the Trash."
msgstr "Você está movendo {} arquivo(s) para o Lixo." msgstr "Você está movendo {} arquivo(s) para o Lixo."
#: core\gui\exclude_list_table.py:15 #: core\gui\exclude_list_table.py:14
msgid "Regular Expressions" msgid "Regular Expressions"
msgstr "Expressões regulares" msgstr "Expressões regulares"
@@ -174,15 +182,15 @@ msgstr "Conteúdo"
msgid "Analyzed %d/%d pictures" msgid "Analyzed %d/%d pictures"
msgstr "%d/%d fotos analizadas" msgstr "%d/%d fotos analizadas"
#: core\pe\matchblock.py:181 #: core\pe\matchblock.py:177
msgid "Performed %d/%d chunk matches" msgid "Performed %d/%d chunk matches"
msgstr "%d/%d resultados em blocos executados" msgstr "%d/%d resultados em blocos executados"
#: core\pe\matchblock.py:191 #: core\pe\matchblock.py:185
msgid "Preparing for matching" msgid "Preparing for matching"
msgstr "Preparando para comparação" msgstr "Preparando para comparação"
#: core\pe\matchblock.py:244 #: core\pe\matchblock.py:234
msgid "Verified %d/%d matches" msgid "Verified %d/%d matches"
msgstr "%d/%d resultados verificados" msgstr "%d/%d resultados verificados"
@@ -230,23 +238,23 @@ msgstr "Mais recente"
msgid "Oldest" msgid "Oldest"
msgstr "Mais antigo" msgstr "Mais antigo"
#: core\results.py:142 #: core\results.py:134
msgid "%d / %d (%s / %s) duplicates marked." msgid "%d / %d (%s / %s) duplicates marked."
msgstr "%d / %d (%s / %s) duplicatas marcadas." msgstr "%d / %d (%s / %s) duplicatas marcadas."
#: core\results.py:149 #: core\results.py:141
msgid " filter: %s" msgid " filter: %s"
msgstr " filtro: %s" msgstr " filtro: %s"
#: core\scanner.py:85 #: core\scanner.py:90
msgid "Read size of %d/%d files" msgid "Read size of %d/%d files"
msgstr "Tamanho lido em %d/%d arquivos" msgstr "Tamanho lido em %d/%d arquivos"
#: core\scanner.py:109 #: core\scanner.py:116
msgid "Read metadata of %d/%d files" msgid "Read metadata of %d/%d files"
msgstr "Metadados lidos em %d/%d arquivos" msgstr "Metadados lidos em %d/%d arquivos"
#: core\scanner.py:147 #: core\scanner.py:154
msgid "Almost done! Fiddling with results..." msgid "Almost done! Fiddling with results..."
msgstr "Quase pronto! Mexendo nos resultados ..." msgstr "Quase pronto! Mexendo nos resultados ..."

View File

@@ -943,3 +943,43 @@ msgstr "Geral"
#: qt\preferences_dialog.py:286 #: qt\preferences_dialog.py:286
msgid "Display" msgid "Display"
msgstr "Exibição" msgstr "Exibição"
#: qt\se\preferences_dialog.py:70
msgid "Partially hash files bigger than"
msgstr ""
#: qt\se\preferences_dialog.py:80
msgid "MB"
msgstr ""
#: qt\preferences_dialog.py:163
msgid "Use native OS dialogs"
msgstr ""
#: qt\preferences_dialog.py:166
msgid ""
"For actions such as file/folder selection use the OS native dialogs.\n"
"Some native dialogs have limited functionality."
msgstr ""
#: qt\se\preferences_dialog.py:68
msgid "Ignore files larger than"
msgstr ""
#: qt\app.py:135 qt\app.py:293
msgid "Clear Cache"
msgstr ""
#: qt\app.py:294
msgid ""
"Do you really want to clear the cache? This will remove all cached file "
"hashes and picture analysis."
msgstr ""
#: qt\app.py:299
msgid "Cache cleared."
msgstr ""
#: qt\preferences_dialog.py:173
msgid "Use dark style"
msgstr ""

Some files were not shown because too many files have changed in this diff Show More