Compare commits

...

67 Commits

Author SHA1 Message Date
Andrew Senetar 3a97ba941a
ci: Merge artifacts
- Merge the resulting artifacts
- Use only the .so files from build
2024-05-11 01:21:58 -07:00
Andrew Senetar e3bcf9d686
chore: Update VS Code configuration 2024-05-11 00:12:19 -07:00
Andrew Senetar a81069be61
fix: Photo matching fixes
- Correct bad query introduced in rotation matching
- Promote get_orientation from "private" on photo class
- Fix prepare_pictures to only generate the needed blocks, add check for missing blocks when rotation matchin is true
- Fix cache test inputs to match schema
2024-05-11 00:11:27 -07:00
Andrew Senetar 08154815d0
ci: Upgrade to latest actions & python versions 2024-02-19 14:39:04 -08:00
Andrew Senetar a95a9db08b
ci: Fix version for tx-push worklflow 2024-02-19 14:22:55 -08:00
Andrew Senetar 3d866cec9a
ci: Add action to push translation source to transifex 2024-02-19 14:17:40 -08:00
Andrew Senetar 253dfd897c
chore: Pull existing language translations from Transifex 2024-02-19 13:35:35 -08:00
Andrew Senetar 6e87f53f91
chore: Regenerate translation source files 2024-02-19 13:26:07 -08:00
Andrew Senetar 95e04c4d82
ci: Update .pre-commit-config.yaml
Update .pre-commit-config.yaml to use the latest versions of black,
flake8, etc.
2024-02-19 11:35:39 -08:00
Andrew Senetar e3a612a704
fix: Correct change that broke black formmating 2024-02-19 10:43:39 -08:00
Andrew Senetar 53d5ac06bf
fix: Linting Errors & VS Code config
- Add additional settings to VS Code for formatter changes in plugins
- Fix black formatting
- Fix flake8 errors due to long lines
- Fix flake8 errors due to type comparisons
2024-02-19 10:32:13 -08:00
Andrew Senetar 13dd00c798
fix: Replace use of `imp` with `importlib`
Original PR and information found at #1187
2024-02-19 09:48:54 -08:00
Luca Falavigna 9f22835f73
Use errno.EISDIR and errno.EACCESS instead of hardcoding values (#1196) 2024-02-19 09:38:24 -06:00
Bruno Cabral 85a4557525
match all orientations (#1127)
* match all orientations

* use rotation as option

---------

Co-authored-by: Andrew Senetar <arsenetar@gmail.com>
Co-authored-by: Luke <byunghun.hyun26@gmail.com>
2024-02-19 09:19:33 -06:00
Andrew Senetar 70d956b4f8
Merge pull request #1197 from dktrkranz/Hurd
Use isolation_level=None mode for GNU Hurd
2024-02-19 09:15:10 -06:00
Luca Falavigna 007404f46a Use isolation_level=None mode for GNU Hurd 2024-02-12 18:02:13 +01:00
Andrew Senetar 4385b50825
Merge pull request #1143 from cebtenzzre/fix-tox
Update python versions in tox.ini
2023-12-01 22:08:42 -06:00
Andrew Senetar 4ef1d24351
Merge pull request #1142 from cebtenzzre/fix-requirements
Update requirements.txt
2023-12-01 22:08:05 -06:00
Andrew Senetar 03be82c0b0
Merge pull request #1144 from Cebtenzzre/fix-arch-package
Do not package send2trash on Arch Linux
2023-07-17 21:54:22 -05:00
Cebtenzzre 332b814c00 Do not package send2trash on Arch Linux
send2trash is provided by the python-send2trash package in the 'extra'
repository.
2023-07-16 19:04:11 -04:00
Cebtenzzre f56bef67e1 Update python versions in tox.ini
Python 3.6 support was already dropped in commit b9dfeac2 ("Drop Python
3.6 Support"). Don't attempt to run tests with it if it is installed.

Python 3.11 is currently supported, so add it to the list.
2023-07-16 18:49:36 -04:00
Cebtenzzre 8160fe4fcc Update requirements.txt
sphinx 7.0.0 was released on April 29th.
2023-07-16 18:38:07 -04:00
Andrew Senetar 9ad84ade29
Merge pull request #1130 from lukehyun/master
RE: Rewrote some of the korean translation to be more understandable
2023-06-08 20:10:33 -05:00
Andrew Senetar 18f32fda19
chore(translations): Synchronize translations 2023-06-08 19:14:57 -05:00
Andrew Senetar 99ec4e0f27
fix: Minor cleanups and fixes
- Update NullJob to subclass Job
- Remove unnecessary size pre-read in _getMatches() as file sizes are
  already loaded during file scan via stat call
- Skip ref check if contents scan as the scan already prevents this from
  happening, some of the other scans do things differently and need to
  be reviewed before removing this post step completely
- Add guard on partial hashing to just hash the whole file if smaller
  than the offset and size and use the value for both the partial digest
  and digest
2023-06-08 01:14:52 -05:00
Luke fe0e4bef91 RE: Rewrote some of the korean translation to be more understandable
I have updated my fork and moved my changes from before.
2023-06-01 11:40:20 +10:00
Andrew Senetar 322d29a996
Merge pull request #1121 from arsenetar/as/upgrade-deps
feat: Upgrade dependencies
2023-04-27 02:35:48 -05:00
Andrew Senetar c5a71f61b8
feat: Upgrade dependencies
Upgrade required dependency versions for most dependencies.  Add maximum
version to most dependencies as well.
2023-04-27 02:25:22 -05:00
Andrew Senetar 10405ad063
fix(build): Clean prior qt/dg_rc.py file before build
Since calls without pyqrcc5 may result in a broken file, clean prior
qt/gg_rc.py file before calling pyqrcc5.  This makes troubleshooting
pyqrcc5 issues more straightforward.

Fix #1103
2023-04-27 01:36:32 -05:00
Andrew Senetar a257dbf0d5
fix(win): Shorten file description
Fix #1119
2023-04-27 01:22:06 -05:00
Daniel Chalmers 7a4506ece3
Github -> GitHub (#1115)
Co-authored-by: Andrew Senetar <arsenetar@gmail.com>
2023-04-27 00:54:39 -05:00
Andrew Senetar aade6593ac
feat: Update translations from transifex 2023-04-27 00:49:03 -05:00
Andrew Senetar 6d8b86b7eb
fix(core): Remove old directory state logic
- Remove code forcing the exclusion of `.` directories by default, the
  new default exclusion filters do this by default
- Change default state code to always return a value
2023-02-27 17:58:15 -06:00
Andrew Senetar e41c91623c
Merge pull request #1049 from Dobatymo/colors-bytes
serialize/deserialize colors to/from bytes instead of strings
2023-01-26 21:24:20 -06:00
Andrew Senetar 46521c8af1
feat: Add migration for picture cache db
- Add migration (just delete db and change to new schema) for picture
  cache following the same sort of strategy as the file digest cache
- Rename mtime column to mtime_ns to match file cache for consistency
2023-01-13 00:05:47 -06:00
Andrew Senetar 549eb7f153
chore: Add vscode launch.json 2023-01-12 23:51:05 -06:00
Andrew Senetar 8125e3ec97
chore: Add rulers to vscode settings, format 2023-01-12 23:30:35 -06:00
Andrew Senetar 8c5e18b980
Merge remote-tracking branch 'upstream/master' into colors-bytes 2023-01-12 00:14:17 -06:00
Andrew Senetar d81759f77f
fix: Specify maximum python version for deb
Specify maximum supported python version so attempts to install are met
with better errors.
2023-01-11 23:53:02 -06:00
Andrew Senetar c57042fdd2
fix: Resolve issue with mock object for core test
Last change introduced a new method on the fs.File object that the test
object did not have.  Add similar method to test object.
2023-01-11 23:20:40 -06:00
Andrew Senetar 057be0294a
fix: Prevent exception during existence check
- Add "safe" existence check to files which catches OSErrors that may
  occur when trying to stat files
- Use "safe" existence check during final existence check
2023-01-11 23:07:06 -06:00
Andrew Senetar 81daddd072
refactor: Improve digest cache db method performance
- Remove lock on read operations, only needed for write operations
- Change to use context manager for sqlite connection
- Remove long lived cursor object and use short lived cursors instead

Fixes #1080
2023-01-11 00:58:29 -06:00
Andrew Senetar 1e651a1603
Merge pull request #1089 from arsenetar/as/pre-commit
feat: Add pre-commit, include python 3.11 in tests
2023-01-09 23:18:13 -06:00
Andrew Senetar 78f4145910
chore: Remove unused qtlib.pot file 2023-01-09 23:02:19 -06:00
Andrew Senetar 46d1afb566
chore: Apply whitespace fixes from hooks
- Remove trailing whitespace
- Correct single newline at end of files (skip for json)
- Update to formatting in a few places due to black
2023-01-09 22:58:08 -06:00
Andrew Senetar a5e31f15f0
Merge pull request #1088 from arsenetar/as/remove-shelve
feat: Remove shelve picture cache
2023-01-09 22:48:37 -06:00
Andrew Senetar 0cf6c9a1a2
ci: Update to include python 3.11 & pre-commit 2023-01-09 22:44:10 -06:00
Andrew Senetar 6db2fa2be6
fix: Correct flake8 config
- Add exclude pattern for flake8 when running with pre-commit as it does
  not fully honor the exclude paths.
- Cleanup exclude paths for flake8 in tox.ini
- Re-enable line length check and correct three affected files
2023-01-09 22:35:12 -06:00
Andrew Senetar 2dd2a801cc
feat: Add pre-commit and commitlint 2023-01-09 21:53:22 -06:00
Andrew Senetar 83f5e80427
feat: Remove shelve picture cache
- Remove shelve picture cache as it has had a fair number of historical
  issues.  Original issue for which it was added should be long
  resolved.  Additionally this allows additional consolidation of the
  various cache code and potentially dbs in the future.
- Remove all related preferences and related code for changing cache
  backend between sqlite and shelve.
2023-01-06 00:35:23 -06:00
Andrew Senetar 091cae0cc6
feat: Add confirmation dialog when canceling job
- Implement a confirmation dialog for cancellation of jobs, required
  changing from QProgressDialog to QDialog to keep cleaner.
- Update ui translation source file

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

View File

@ -4,71 +4,42 @@ name: Default CI/CD
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
lint:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.10
uses: actions/setup-python@v2
- uses: actions/checkout@v4
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt -r requirements-extra.txt
- name: Lint with flake8
run: |
flake8 .
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.10
uses: actions/setup-python@v2
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt -r requirements-extra.txt
- name: Check format with black
run: |
black .
python-version: "3.12"
- uses: pre-commit/action@v3.0.1
test:
needs: [lint, format]
needs: [pre-commit]
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: [3.7, 3.8, 3.9, "3.10"]
exclude:
- os: macos-latest
python-version: 3.7
- os: macos-latest
python-version: 3.8
- os: macos-latest
python-version: 3.9
os: [ubuntu-latest]
python-version: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12"]
include:
- os: windows-latest
python-version: 3.7
- os: windows-latest
python-version: 3.8
- os: windows-latest
python-version: 3.9
python-version: "3.12"
- os: macos-latest
python-version: "3.12"
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools
pip install -r requirements.txt -r requirements-extra.txt
- name: Build python modules
run: |
@ -78,7 +49,17 @@ jobs:
pytest core hscommon
- name: Upload Artifacts
if: matrix.os == 'ubuntu-latest'
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: modules ${{ matrix.python-version }}
path: ${{ github.workspace }}/**/*.so
path: build/**/*.so
merge-artifacts:
needs: [test]
runs-on: ubuntu-latest
steps:
- name: Merge Artifacts
uses: actions/upload-artifact/merge@v4
with:
name: modules
pattern: modules*
delete-merged: true

26
.github/workflows/tx-push.yml vendored Normal file
View File

@ -0,0 +1,26 @@
# Push translation source to Transifex
name: Transifex Sync
on:
push:
branches:
- master
paths:
- locale/*.pot
env:
TX_VERSION: "v1.6.10"
jobs:
push-source:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get Transifex Client
run: |
curl -o- https://raw.githubusercontent.com/transifex/cli/master/install.sh | bash -s -- $TX_VERSION
- name: Update & Push Translation Sources
env:
TX_TOKEN: ${{ secrets.TX_TOKEN }}
run: |
./tx push -s --use-git-timestamps

6
.gitignore vendored
View File

@ -87,8 +87,8 @@ cython_debug/
# Visual Studio Code
.vscode/*
!.vscode/settings.json
#!.vscode/tasks.json
#!.vscode/launch.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
@ -108,4 +108,4 @@ cocoa/autogen
*.waf*
.lock-waf*
/tags
/tags

24
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,24 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: check-yaml
- id: check-toml
- id: end-of-file-fixer
exclude: ".*.json"
- id: trailing-whitespace
- repo: https://github.com/psf/black
rev: 24.2.0
hooks:
- id: black
- repo: https://github.com/PyCQA/flake8
rev: 7.0.0
hooks:
- id: flake8
exclude: ^(.tox|env|build|dist|help|qt/dg_rc.py|pkg).*
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
rev: v9.11.0
hooks:
- id: commitlint
stages: [commit-msg]
additional_dependencies: ["@commitlint/config-conventional"]

View File

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

View File

@ -18,4 +18,3 @@ file_filter = locale/<lang>/LC_MESSAGES/ui.po
source_file = locale/ui.pot
source_lang = en
type = PO

View File

@ -3,8 +3,10 @@
"recommendations": [
"redhat.vscode-yaml",
"ms-python.vscode-pylance",
"ms-python.python"
"ms-python.python",
"ms-python.black-formatter",
],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
// List of extensions recommended by VS Code that should not be recommended for
// users of this workspace.
"unwantedRecommendations": []
}

17
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,17 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "DupuGuru",
"type": "debugpy",
"request": "launch",
"program": "run.py",
"console": "integratedTerminal",
"subProcess": true,
"justMyCode": false
},
]
}

13
.vscode/settings.json vendored
View File

@ -1,12 +1,17 @@
{
"python.formatting.provider": "black",
"cSpell.words": [
"Dupras",
"hscommon"
],
"editor.rulers": [
88,
120
],
"python.languageServer": "Pylance",
"yaml.schemaStore.enable": true,
"yaml.schemas": {
"https://json.schemastore.org/github-workflow.json": ".github/workflows/*.yml"
}
"[python]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "ms-python.black-formatter"
},
"python.testing.pytestEnabled": true
}

View File

@ -619,4 +619,3 @@ Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS

View File

@ -4,7 +4,7 @@ PYRCC5 ?= pyrcc5
REQ_MINOR_VERSION = 7
PREFIX ?= /usr/local
# Window compatability via Msys2
# Window compatability via Msys2
# - venv creates Scripts instead of bin
# - compile generates .pyd instead of .so
# - venv with --sytem-site-packages has issues on windows as well...
@ -12,7 +12,7 @@ PREFIX ?= /usr/local
ifeq ($(shell ${PYTHON} -c "import platform; print(platform.system())"), Windows)
BIN = Scripts
SO = *.pyd
VENV_OPTIONS =
VENV_OPTIONS =
else
BIN = bin
SO = *.so
@ -43,7 +43,7 @@ mofiles = $(patsubst %.po,%.mo,$(pofiles))
vpath %.po $(localedirs)
vpath %.mo $(localedirs)
all: | env i18n modules qt/dg_rc.py
all: | env i18n modules qt/dg_rc.py
@echo "Build complete! You can run dupeGuru with 'make run'"
run:
@ -82,7 +82,7 @@ qt/dg_rc.py: qt/dg.qrc
i18n: $(mofiles)
%.mo: %.po
msgfmt -o $@ $<
msgfmt -o $@ $<
modules: | env
$(VENV_PYTHON) build.py --modules

View File

@ -63,7 +63,7 @@ dupeGuru comes with a makefile that can be used to build and run:
$ python run.py
### Generating Debian/Ubuntu package
To generate packages the extra requirements in requirements-extra.txt must be installed, the
To generate packages the extra requirements in requirements-extra.txt must be installed, the
steps are as follows:
$ cd <dupeGuru directory>

View File

@ -29,7 +29,7 @@ To build with a different python version 3.7 vs 3.8 or 32 bit vs 64 bit specify
It is possible to build dupeGuru with the makefile on windows using a compatable POSIX environment. The following steps have been tested using [msys2][msys2]. Before running make:
1. Install msys2 or other POSIX environment
2. Install PyQt5 globally via pip
3. Use the respective console for msys2 it is `msys2 msys`
3. Use the respective console for msys2 it is `msys2 msys`
Then the following execution of the makefile should work. Pass the correct value for PYTHON to the makefile if not on the path as python3.

View File

@ -129,6 +129,7 @@ def build_normal():
print("Building localizations")
build_localizations()
print("Building Qt stuff")
Path("qt", "dg_rc.py").unlink(missing_ok=True)
print_and_do("pyrcc5 {} > {}".format(Path("qt", "dg.qrc"), Path("qt", "dg_rc.py")))
fix_qt_resource_file(Path("qt", "dg_rc.py"))
build_help()

17
commitlint.config.js Normal file
View File

@ -0,0 +1,17 @@
const Configuration = {
/*
* Resolve and load @commitlint/config-conventional from node_modules.
* Referenced packages must be installed
*/
extends: ['@commitlint/config-conventional'],
/*
* Any rules defined here will override rules from @commitlint/config-conventional
*/
rules: {
'header-max-length': [2, 'always', 72],
'subject-case': [2, 'always', 'sentence-case'],
'scope-enum': [2, 'always'],
},
};
module.exports = Configuration;

View File

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

View File

@ -126,8 +126,6 @@ class DupeGuru(Broadcaster):
NAME = PROMPT_NAME = "dupeGuru"
PICTURE_CACHE_TYPE = "sqlite" # set to 'shelve' for a ShelveCache
def __init__(self, view, portable=False):
if view.get_default(DEBUG_MODE_PREFERENCE):
logging.getLogger().setLevel(logging.DEBUG)
@ -153,7 +151,8 @@ class DupeGuru(Broadcaster):
"clean_empty_dirs": False,
"ignore_hardlink_matches": False,
"copymove_dest_type": DestType.RELATIVE,
"picture_cache_type": self.PICTURE_CACHE_TYPE,
"include_exists_check": True,
"rehash_ignore_mtime": False,
}
self.selected_dupes = []
self.details_panel = DetailsPanel(self)
@ -183,8 +182,7 @@ class DupeGuru(Broadcaster):
self.view.create_results_window()
def _get_picture_cache_path(self):
cache_type = self.options["picture_cache_type"]
cache_name = "cached_pictures.shelve" if cache_type == "shelve" else "cached_pictures.db"
cache_name = "cached_pictures.db"
return op.join(self.appdata, cache_name)
def _get_dupe_sort_key(self, dupe, get_group, key, delta):
@ -555,7 +553,9 @@ class DupeGuru(Broadcaster):
# a workaround to make the damn thing work.
exepath, args = match.groups()
path, exename = op.split(exepath)
p = subprocess.Popen(exename + args, shell=True, cwd=path, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
p = subprocess.Popen(
exename + args, shell=True, cwd=path, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
)
output = p.stdout.read()
logging.info("Custom command %s %s: %s", exename, args, output)
else:
@ -792,6 +792,7 @@ class DupeGuru(Broadcaster):
Scans folders selected in :attr:`directories` and put the results in :attr:`results`
"""
scanner = self.SCANNER_CLASS()
fs.filesdb.ignore_mtime = self.options["rehash_ignore_mtime"] is True
if not self.directories.has_any_file():
self.view.show_message(tr("The selected directories contain no scannable file."))
return

View File

@ -84,10 +84,11 @@ class Directories:
for denied_path_re in self._exclude_list.compiled:
if denied_path_re.match(str(path.name)):
return DirectoryState.EXCLUDED
# return # We still use the old logic to force state on hidden dirs
return DirectoryState.NORMAL
# Override this in subclasses to specify the state of some special folders.
if path.name.startswith("."):
return DirectoryState.EXCLUDED
return DirectoryState.NORMAL
def _get_files(self, from_path, fileclasses, j):
try:
@ -186,7 +187,7 @@ class Directories:
for path in self._dirs:
for file in self._get_files(path, fileclasses=fileclasses, j=j):
file_count += 1
if type(j) != job.NullJob:
if not isinstance(j, job.NullJob):
j.set_progress(-1, tr("Collected {} files to scan").format(file_count))
yield file
@ -202,7 +203,7 @@ class Directories:
from_folder = folderclass(path)
for folder in self._get_folders(from_folder, j):
folder_count += 1
if type(j) != job.NullJob:
if not isinstance(j, job.NullJob):
j.set_progress(-1, tr("Collected {} folders to scan").format(folder_count))
yield folder
@ -214,7 +215,7 @@ class Directories:
# direct match? easy result.
if path in self.states:
return self.states[path]
state = self._default_state_for_path(path) or DirectoryState.NORMAL
state = self._default_state_for_path(path)
# Save non-default states in cache, necessary for _get_files()
if state != DirectoryState.NORMAL:
self.states[path] = state

View File

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

View File

@ -16,6 +16,7 @@ import os
from math import floor
import logging
import sqlite3
from sys import platform
from threading import Lock
from typing import Any, AnyStr, Union, Callable
@ -54,6 +55,9 @@ CHUNK_SIZE = 1024 * 1024 # 1 MiB
# Minimum size below which partial hashing is not used
MIN_FILE_SIZE = 3 * CHUNK_SIZE # 3MiB, because we take 3 samples
# Partial hashing offset and size
PARTIAL_OFFSET_SIZE = (0x4000, 0x4000)
class FSError(Exception):
cls_message = "An error has occured on '{name}' in '{parent}'"
@ -97,60 +101,76 @@ class FilesDB:
schema_version = 1
schema_version_description = "Changed from md5 to xxhash if available."
create_table_query = "CREATE TABLE IF NOT EXISTS files (path TEXT PRIMARY KEY, size INTEGER, mtime_ns INTEGER, entry_dt DATETIME, digest BLOB, digest_partial BLOB, digest_samples BLOB)"
create_table_query = """CREATE TABLE IF NOT EXISTS files (path TEXT PRIMARY KEY, size INTEGER, mtime_ns INTEGER,
entry_dt DATETIME, digest BLOB, digest_partial BLOB, digest_samples BLOB)"""
drop_table_query = "DROP TABLE IF EXISTS files;"
select_query = "SELECT {key} FROM files WHERE path=:path AND size=:size and mtime_ns=:mtime_ns"
select_query_ignore_mtime = "SELECT {key} FROM files WHERE path=:path AND size=:size"
insert_query = """
INSERT INTO files (path, size, mtime_ns, entry_dt, {key}) VALUES (:path, :size, :mtime_ns, datetime('now'), :value)
INSERT INTO files (path, size, mtime_ns, entry_dt, {key})
VALUES (:path, :size, :mtime_ns, datetime('now'), :value)
ON CONFLICT(path) DO UPDATE SET size=:size, mtime_ns=:mtime_ns, entry_dt=datetime('now'), {key}=:value;
"""
ignore_mtime = False
def __init__(self):
self.conn = None
self.cur = None
self.lock = None
def connect(self, path: Union[AnyStr, os.PathLike]) -> None:
self.conn = sqlite3.connect(path, check_same_thread=False)
self.cur = self.conn.cursor()
if platform.startswith("gnu0"):
self.conn = sqlite3.connect(path, check_same_thread=False, isolation_level=None)
else:
self.conn = sqlite3.connect(path, check_same_thread=False)
self.lock = Lock()
self._check_upgrade()
def _check_upgrade(self) -> None:
with self.lock:
has_schema = self.cur.execute(
with self.lock, self.conn as conn:
has_schema = conn.execute(
"SELECT NAME FROM sqlite_master WHERE type='table' AND name='schema_version'"
).fetchall()
version = None
if has_schema:
version = self.cur.execute("SELECT version FROM schema_version ORDER BY version DESC").fetchone()[0]
version = conn.execute("SELECT version FROM schema_version ORDER BY version DESC").fetchone()[0]
else:
self.cur.execute("CREATE TABLE schema_version (version int PRIMARY KEY, description TEXT)")
conn.execute("CREATE TABLE schema_version (version int PRIMARY KEY, description TEXT)")
if version != self.schema_version:
self.cur.execute(self.drop_table_query)
self.cur.execute(
conn.execute(self.drop_table_query)
conn.execute(
"INSERT OR REPLACE INTO schema_version VALUES (:version, :description)",
{"version": self.schema_version, "description": self.schema_version_description},
)
self.cur.execute(self.create_table_query)
self.conn.commit()
conn.execute(self.create_table_query)
def clear(self) -> None:
with self.lock:
self.cur.execute(self.drop_table_query)
self.cur.execute(self.create_table_query)
with self.lock, self.conn as conn:
conn.execute(self.drop_table_query)
conn.execute(self.create_table_query)
def get(self, path: Path, key: str) -> Union[bytes, None]:
stat = path.stat()
size = stat.st_size
mtime_ns = stat.st_mtime_ns
try:
with self.conn as conn:
if self.ignore_mtime:
cursor = conn.execute(
self.select_query_ignore_mtime.format(key=key), {"path": str(path), "size": size}
)
else:
cursor = conn.execute(
self.select_query.format(key=key),
{"path": str(path), "size": size, "mtime_ns": mtime_ns},
)
result = cursor.fetchone()
cursor.close()
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]
if result:
return result[0]
except Exception as ex:
logging.warning(f"Couldn't get {key} for {path} w/{size}, {mtime_ns}: {ex}")
return None
@ -158,12 +178,14 @@ class FilesDB:
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},
)
try:
with self.lock, self.conn as conn:
conn.execute(
self.insert_query.format(key=key),
{"path": str(path), "size": size, "mtime_ns": mtime_ns, "value": value},
)
except Exception as ex:
logging.warning(f"Couldn't put {key} for {path} w/{size}, {mtime_ns}: {ex}")
def commit(self) -> None:
with self.lock:
@ -171,7 +193,6 @@ class FilesDB:
def close(self) -> None:
with self.lock:
self.cur.close()
self.conn.close()
@ -185,7 +206,7 @@ class File:
# Slots for File make us save quite a bit of memory. In a memory test I've made with a lot of
# files, I saved 35% memory usage with "unread" files (no _read_info() call) and gains become
# even greater when we take into account read attributes (70%!). Yeah, it's worth it.
__slots__ = ("path", "is_ref", "words") + tuple(INITIAL_INFO.keys())
__slots__ = ("path", "unicode_path", "is_ref", "words") + tuple(INITIAL_INFO.keys())
def __init__(self, path):
for attrname in self.INITIAL_INFO:
@ -196,6 +217,8 @@ class File:
self.mtime = nonone(path.stat().st_mtime, 0)
else:
self.path = path
if self.path:
self.unicode_path = str(self.path)
def __repr__(self):
return f"<{self.__class__.__name__} {str(self.path)}>"
@ -229,14 +252,9 @@ class File:
def _calc_digest_partial(self):
# type: () -> bytes
# This offset is where we should start reading the file to get a partial hash
# For audio file, it should be where audio data starts
offset, size = (0x4000, 0x4000)
with self.path.open("rb") as fp:
fp.seek(offset)
partial_data = fp.read(size)
fp.seek(PARTIAL_OFFSET_SIZE[0])
partial_data = fp.read(PARTIAL_OFFSET_SIZE[1])
return hasher(partial_data).digest()
def _calc_digest_samples(self) -> bytes:
@ -265,34 +283,29 @@ class File:
self.size = nonone(stats.st_size, 0)
self.mtime = nonone(stats.st_mtime, 0)
elif field == "digest_partial":
try:
self.digest_partial = filesdb.get(self.path, "digest_partial")
if self.digest_partial is None:
self.digest_partial = filesdb.get(self.path, "digest_partial")
if self.digest_partial is None:
# If file is smaller than partial requirements just use the full digest
if self.size < PARTIAL_OFFSET_SIZE[0] + PARTIAL_OFFSET_SIZE[1]:
self.digest_partial = self.digest
else:
self.digest_partial = self._calc_digest_partial()
filesdb.put(self.path, "digest_partial", self.digest_partial)
except Exception as e:
logging.warning("Couldn't get digest_partial for %s: %s", self.path, e)
filesdb.put(self.path, "digest_partial", self.digest_partial)
elif field == "digest":
try:
self.digest = filesdb.get(self.path, "digest")
if self.digest is None:
self.digest = self._calc_digest()
filesdb.put(self.path, "digest", self.digest)
except Exception as e:
logging.warning("Couldn't get digest for %s: %s", self.path, e)
self.digest = filesdb.get(self.path, "digest")
if self.digest is None:
self.digest = self._calc_digest()
filesdb.put(self.path, "digest", self.digest)
elif field == "digest_samples":
size = self.size
# Might as well hash such small files entirely.
if size <= MIN_FILE_SIZE:
setattr(self, field, self.digest)
self.digest_samples = self.digest
return
try:
self.digest_samples = filesdb.get(self.path, "digest_samples")
if self.digest_samples is None:
self.digest_samples = self._calc_digest_samples()
filesdb.put(self.path, "digest_samples", self.digest_samples)
except Exception as e:
logging.warning(f"Couldn't get digest_samples for {self.path}: {e}")
self.digest_samples = filesdb.get(self.path, "digest_samples")
if self.digest_samples is None:
self.digest_samples = self._calc_digest_samples()
filesdb.put(self.path, "digest_samples", self.digest_samples)
def _read_all_info(self, attrnames=None):
"""Cache all possible info.
@ -310,6 +323,14 @@ class File:
"""Returns whether this file wrapper class can handle ``path``."""
return not path.is_symlink() and path.is_file()
def exists(self) -> bool:
"""Safely check if the underlying file exists, treat error as non-existent"""
try:
return self.path.exists()
except OSError as ex:
logging.warning(f"Checking {self.path} raised: {ex}")
return False
def rename(self, newname):
if newname == self.name:
return

View File

@ -4,24 +4,13 @@
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from core.pe._cache import string_to_colors # noqa
from core.pe._cache import bytes_to_colors # noqa
def colors_to_string(colors):
"""Transform the 3 sized tuples 'colors' into a hex string.
def colors_to_bytes(colors):
"""Transform the 3 sized tuples 'colors' into a bytes string.
[(0,100,255)] --> 0064ff
[(1,2,3),(4,5,6)] --> 010203040506
[(0,100,255)] --> b'\x00d\xff'
[(1,2,3),(4,5,6)] --> b'\x01\x02\x03\x04\x05\x06'
"""
return "".join("{:02x}{:02x}{:02x}".format(r, g, b) for r, g, b in colors)
# This function is an important bottleneck of dupeGuru PE. It has been converted to C.
# def string_to_colors(s):
# """Transform the string 's' in a list of 3 sized tuples.
# """
# result = []
# for i in xrange(0, len(s), 6):
# number = int(s[i:i+6], 16)
# result.append((number >> 16, (number >> 8) & 0xff, number & 0xff))
# return result
return b"".join(map(bytes, colors))

View File

@ -2,5 +2,5 @@ from typing import Union, Tuple, List
_block = Tuple[int, int, int]
def colors_to_string(colors: List[_block]) -> str: ... # noqa: E302
def string_to_colors(s: str) -> Union[List[_block], None]: ...
def colors_to_bytes(colors: List[_block]) -> bytes: ... # noqa: E302
def bytes_to_colors(s: bytes) -> Union[List[_block], None]: ...

View File

@ -1,141 +0,0 @@
# Copyright 2016 Virgil Dupras
#
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
import os
import os.path as op
import shelve
import tempfile
from collections import namedtuple
from core.pe.cache import string_to_colors, colors_to_string
def wrap_path(path):
return f"path:{path}"
def unwrap_path(key):
return key[5:]
def wrap_id(path):
return f"id:{path}"
def unwrap_id(key):
return int(key[3:])
CacheRow = namedtuple("CacheRow", "id path blocks mtime")
class ShelveCache:
"""A class to cache picture blocks in a shelve backend."""
def __init__(self, db=None, readonly=False):
self.istmp = db is None
if self.istmp:
self.dtmp = tempfile.mkdtemp()
self.ftmp = db = op.join(self.dtmp, "tmpdb")
flag = "r" if readonly else "c"
self.shelve = shelve.open(db, flag)
self.maxid = self._compute_maxid()
def __contains__(self, key):
return wrap_path(key) in self.shelve
def __delitem__(self, key):
row = self.shelve[wrap_path(key)]
del self.shelve[wrap_path(key)]
del self.shelve[wrap_id(row.id)]
def __getitem__(self, key):
if isinstance(key, int):
skey = self.shelve[wrap_id(key)]
else:
skey = wrap_path(key)
return string_to_colors(self.shelve[skey].blocks)
def __iter__(self):
return (unwrap_path(k) for k in self.shelve if k.startswith("path:"))
def __len__(self):
return sum(1 for k in self.shelve if k.startswith("path:"))
def __setitem__(self, path_str, blocks):
blocks = colors_to_string(blocks)
if op.exists(path_str):
mtime = int(os.stat(path_str).st_mtime)
else:
mtime = 0
if path_str in self:
rowid = self.shelve[wrap_path(path_str)].id
else:
rowid = self._get_new_id()
row = CacheRow(rowid, path_str, blocks, mtime)
self.shelve[wrap_path(path_str)] = row
self.shelve[wrap_id(rowid)] = wrap_path(path_str)
def _compute_maxid(self):
return max((unwrap_id(k) for k in self.shelve if k.startswith("id:")), default=1)
def _get_new_id(self):
self.maxid += 1
return self.maxid
def clear(self):
self.shelve.clear()
def close(self):
if self.shelve is not None:
self.shelve.close()
if self.istmp:
os.remove(self.ftmp)
os.rmdir(self.dtmp)
self.shelve = None
def filter(self, func):
to_delete = [key for key in self if not func(key)]
for key in to_delete:
del self[key]
def get_id(self, path):
if path in self:
return self.shelve[wrap_path(path)].id
else:
raise ValueError(path)
def get_multiple(self, rowids):
for rowid in rowids:
try:
skey = self.shelve[wrap_id(rowid)]
except KeyError:
continue
yield (rowid, string_to_colors(self.shelve[skey].blocks))
def purge_outdated(self):
"""Go through the cache and purge outdated records.
A record is outdated if the picture doesn't exist or if its mtime is greater than the one in
the db.
"""
todelete = []
for path in self:
row = self.shelve[wrap_path(path)]
if row.mtime and op.exists(path):
picture_mtime = os.stat(path).st_mtime
if int(picture_mtime) <= row.mtime:
# not outdated
continue
todelete.append(path)
for path in todelete:
try:
del self[path]
except KeyError:
# I have no idea why a KeyError sometimes happen, but it does, as we can see in
# #402 and #439. I don't think it hurts to silently ignore the error, so that's
# what we do
pass

View File

@ -9,12 +9,24 @@ import os.path as op
import logging
import sqlite3 as sqlite
from core.pe.cache import string_to_colors, colors_to_string
from core.pe.cache import bytes_to_colors, colors_to_bytes
class SqliteCache:
"""A class to cache picture blocks in a sqlite backend."""
schema_version = 2
schema_version_description = "Added blocks for all 8 orientations."
create_table_query = (
"CREATE TABLE IF NOT EXISTS "
"pictures(path TEXT, mtime_ns INTEGER, blocks BLOB, blocks2 BLOB, blocks3 BLOB, "
"blocks4 BLOB, blocks5 BLOB, blocks6 BLOB, blocks7 BLOB, blocks8 BLOB)"
)
create_index_query = "CREATE INDEX IF NOT EXISTS idx_path on pictures (path)"
drop_table_query = "DROP TABLE IF EXISTS pictures"
drop_index_query = "DROP INDEX IF EXISTS idx_path"
def __init__(self, db=":memory:", readonly=False):
# readonly is not used in the sqlite version of the cache
self.dbname = db
@ -35,12 +47,20 @@ class SqliteCache:
# Optimized
def __getitem__(self, key):
if isinstance(key, int):
sql = "select blocks from pictures where rowid = ?"
sql = (
"select blocks, blocks2, blocks3, blocks4, blocks5, blocks6, blocks7, blocks8 "
"from pictures "
"where rowid = ?"
)
else:
sql = "select blocks from pictures where path = ?"
result = self.con.execute(sql, [key]).fetchone()
if result:
result = string_to_colors(result[0])
sql = (
"select blocks, blocks2, blocks3, blocks4, blocks5, blocks6, blocks7, blocks8 "
"from pictures "
"where path = ?"
)
blocks = self.con.execute(sql, [key]).fetchone()
if blocks:
result = [bytes_to_colors(block) for block in blocks]
return result
else:
raise KeyError(key)
@ -56,35 +76,33 @@ class SqliteCache:
return result[0][0]
def __setitem__(self, path_str, blocks):
blocks = colors_to_string(blocks)
blocks = [colors_to_bytes(block) for block in blocks]
if op.exists(path_str):
mtime = int(os.stat(path_str).st_mtime)
else:
mtime = 0
if path_str in self:
sql = "update pictures set blocks = ?, mtime = ? where path = ?"
sql = (
"update pictures set blocks = ?, blocks2 = ?, blocks3 = ?, blocks4 = ?, blocks5 = ?, blocks6 = ?, "
"blocks7 = ?, blocks8 = ?, mtime_ns = ?"
"where path = ?"
)
else:
sql = "insert into pictures(blocks,mtime,path) values(?,?,?)"
sql = (
"insert into pictures(blocks,blocks2,blocks3,blocks4,blocks5,blocks6,blocks7,blocks8,mtime_ns,path) "
"values(?,?,?,?,?,?,?,?,?,?)"
)
try:
self.con.execute(sql, [blocks, mtime, path_str])
self.con.execute(sql, blocks + [mtime, path_str])
except sqlite.OperationalError:
logging.warning("Picture cache could not set value for key %r", path_str)
except sqlite.DatabaseError as e:
logging.warning("DatabaseError while setting value for key %r: %s", path_str, str(e))
def _create_con(self, second_try=False):
def create_tables():
logging.debug("Creating picture cache tables.")
self.con.execute("drop table if exists pictures")
self.con.execute("drop index if exists idx_path")
self.con.execute("create table pictures(path TEXT, mtime INTEGER, blocks TEXT)")
self.con.execute("create index idx_path on pictures (path)")
self.con = sqlite.connect(self.dbname, isolation_level=None)
try:
self.con.execute("select path, mtime, blocks from pictures where 1=2")
except sqlite.OperationalError: # new db
create_tables()
self.con = sqlite.connect(self.dbname, isolation_level=None)
self._check_upgrade()
except sqlite.DatabaseError as e: # corrupted db
if second_try:
raise # Something really strange is happening
@ -93,6 +111,25 @@ class SqliteCache:
os.remove(self.dbname)
self._create_con(second_try=True)
def _check_upgrade(self) -> None:
with self.con as conn:
has_schema = conn.execute(
"SELECT NAME FROM sqlite_master WHERE type='table' AND name='schema_version'"
).fetchall()
version = None
if has_schema:
version = conn.execute("SELECT version FROM schema_version ORDER BY version DESC").fetchone()[0]
else:
conn.execute("CREATE TABLE schema_version (version int PRIMARY KEY, description TEXT)")
if version != self.schema_version:
conn.execute(self.drop_table_query)
conn.execute(
"INSERT OR REPLACE INTO schema_version VALUES (:version, :description)",
{"version": self.schema_version, "description": self.schema_version_description},
)
conn.execute(self.create_table_query)
conn.execute(self.create_index_query)
def clear(self):
self.close()
if self.dbname != ":memory:":
@ -118,9 +155,28 @@ class SqliteCache:
raise ValueError(path)
def get_multiple(self, rowids):
sql = "select rowid, blocks from pictures where rowid in (%s)" % ",".join(map(str, rowids))
ids = ",".join(map(str, rowids))
sql = (
"select rowid, blocks, blocks2, blocks3, blocks4, blocks5, blocks6, blocks7, blocks8 "
f"from pictures where rowid in ({ids})"
)
cur = self.con.execute(sql)
return ((rowid, string_to_colors(blocks)) for rowid, blocks in cur)
return (
(
rowid,
[
bytes_to_colors(blocks),
bytes_to_colors(blocks2),
bytes_to_colors(blocks3),
bytes_to_colors(blocks4),
bytes_to_colors(blocks5),
bytes_to_colors(blocks6),
bytes_to_colors(blocks7),
bytes_to_colors(blocks8),
],
)
for rowid, blocks, blocks2, blocks3, blocks4, blocks5, blocks6, blocks7, blocks8 in cur
)
def purge_outdated(self):
"""Go through the cache and purge outdated records.
@ -129,12 +185,12 @@ class SqliteCache:
the db.
"""
todelete = []
sql = "select rowid, path, mtime from pictures"
sql = "select rowid, path, mtime_ns from pictures"
cur = self.con.execute(sql)
for rowid, path_str, mtime in cur:
if mtime and op.exists(path_str):
for rowid, path_str, mtime_ns in cur:
if mtime_ns and op.exists(path_str):
picture_mtime = os.stat(path_str).st_mtime
if int(picture_mtime) <= mtime:
if int(picture_mtime) <= mtime_ns:
# not outdated
continue
todelete.append(rowid)

View File

@ -16,6 +16,7 @@ from hscommon.jobprogress import job
from core.engine import Match
from core.pe.block import avgdiff, DifferentBlockCountError, NoBlocksError
from core.pe.cache_sqlite import SqliteCache
# OPTIMIZATION NOTES:
# The bottleneck of the matching phase is CPU, which is why we use multiprocessing. However, another
@ -27,7 +28,7 @@ from core.pe.block import avgdiff, DifferentBlockCountError, NoBlocksError
# to files in other chunks. So chunkifying doesn't save us any actual comparison, but the advantage
# is that instead of reading blocks from disk number_of_files**2 times, we read it
# number_of_files*number_of_chunks times.
# Determining the right chunk size is tricky, bceause if it's too big, too many blocks will be in
# Determining the right chunk size is tricky, because if it's too big, too many blocks will be in
# memory at the same time and we might end up with memory trashing, which is awfully slow. So,
# because our *real* bottleneck is CPU, the chunk size must simply be enough so that the CPU isn't
# starved by Disk IOs.
@ -50,17 +51,10 @@ except Exception:
def get_cache(cache_path, readonly=False):
if cache_path.endswith("shelve"):
from core.pe.cache_shelve import ShelveCache
return ShelveCache(cache_path, readonly=readonly)
else:
from core.pe.cache_sqlite import SqliteCache
return SqliteCache(cache_path, readonly=readonly)
return SqliteCache(cache_path, readonly=readonly)
def prepare_pictures(pictures, cache_path, with_dimensions, j=job.nulljob):
def prepare_pictures(pictures, cache_path, with_dimensions, match_rotated, j=job.nulljob):
# The MemoryError handlers in there use logging without first caring about whether or not
# there is enough memory left to carry on the operation because it is assumed that the
# MemoryError happens when trying to read an image file, which is freed from memory by the
@ -78,13 +72,18 @@ def prepare_pictures(pictures, cache_path, with_dimensions, j=job.nulljob):
# entry in iPhoto library.
logging.warning("We have a picture with a null path here")
continue
picture.unicode_path = str(picture.path)
logging.debug("Analyzing picture at %s", picture.unicode_path)
if with_dimensions:
picture.dimensions # pre-read dimensions
try:
if picture.unicode_path not in cache:
blocks = picture.get_blocks(BLOCK_COUNT_PER_SIDE)
if picture.unicode_path not in cache or (
match_rotated and any(block == [] for block in cache[picture.unicode_path])
):
if match_rotated:
blocks = [picture.get_blocks(BLOCK_COUNT_PER_SIDE, orientation) for orientation in range(1, 9)]
else:
blocks = [[]] * 8
blocks[max(picture.get_orientation() - 1, 0)] = picture.get_blocks(BLOCK_COUNT_PER_SIDE)
cache[picture.unicode_path] = blocks
prepared.append(picture)
except (OSError, ValueError) as e:
@ -125,13 +124,13 @@ def get_match(first, second, percentage):
return Match(first, second, percentage)
def async_compare(ref_ids, other_ids, dbname, threshold, picinfo):
def async_compare(ref_ids, other_ids, dbname, threshold, picinfo, match_rotated=False):
# The list of ids in ref_ids have to be compared to the list of ids in other_ids. other_ids
# can be None. In this case, ref_ids has to be compared with itself
# picinfo is a dictionary {pic_id: (dimensions, is_ref)}
cache = get_cache(dbname, readonly=True)
limit = 100 - threshold
ref_pairs = list(cache.get_multiple(ref_ids))
ref_pairs = list(cache.get_multiple(ref_ids)) # (rowid, [b, b2, ..., b8])
if other_ids is not None:
other_pairs = list(cache.get_multiple(other_ids))
comparisons_to_do = [(r, o) for r in ref_pairs for o in other_pairs]
@ -144,22 +143,35 @@ def async_compare(ref_ids, other_ids, dbname, threshold, picinfo):
if ref_is_ref and other_is_ref:
continue
if ref_dimensions != other_dimensions:
continue
try:
diff = avgdiff(ref_blocks, other_blocks, limit, MIN_ITERATIONS)
percentage = 100 - diff
except (DifferentBlockCountError, NoBlocksError):
percentage = 0
if percentage >= threshold:
results.append((ref_id, other_id, percentage))
if match_rotated:
rotated_ref_dimensions = (ref_dimensions[1], ref_dimensions[0])
if rotated_ref_dimensions != other_dimensions:
continue
else:
continue
orientation_range = 1
if match_rotated:
orientation_range = 8
for orientation_ref in range(orientation_range):
try:
diff = avgdiff(ref_blocks[orientation_ref], other_blocks[0], limit, MIN_ITERATIONS)
percentage = 100 - diff
except (DifferentBlockCountError, NoBlocksError):
percentage = 0
if percentage >= threshold:
results.append((ref_id, other_id, percentage))
break
cache.close()
return results
def getmatches(pictures, cache_path, threshold, match_scaled=False, j=job.nulljob):
def getmatches(pictures, cache_path, threshold, match_scaled=False, match_rotated=False, j=job.nulljob):
def get_picinfo(p):
if match_scaled:
return (None, p.is_ref)
return ((None, None), p.is_ref)
else:
return (p.dimensions, p.is_ref)
@ -181,7 +193,7 @@ def getmatches(pictures, cache_path, threshold, match_scaled=False, j=job.nulljo
j.set_progress(comparison_count, progress_msg)
j = j.start_subjob([3, 7])
pictures = prepare_pictures(pictures, cache_path, with_dimensions=not match_scaled, j=j)
pictures = prepare_pictures(pictures, cache_path, not match_scaled, match_rotated, j=j)
j = j.start_subjob([9, 1], tr("Preparing for matching"))
cache = get_cache(cache_path)
id2picture = {}
@ -211,7 +223,7 @@ def getmatches(pictures, cache_path, threshold, match_scaled=False, j=job.nulljo
picinfo.update({p.cache_id: get_picinfo(p) for p in other_chunk})
else:
other_ids = None
args = (ref_ids, other_ids, cache_path, threshold, picinfo)
args = (ref_ids, other_ids, cache_path, threshold, picinfo, match_rotated)
async_results.append(pool.apply_async(async_compare, args))
collect_results()
collect_results(collect_all=True)

View File

@ -245,4 +245,4 @@ PyObject *PyInit__block(void) {
PyModule_AddObject(m, "DifferentBlockCountError", DifferentBlockCountError);
return m;
}
}

View File

@ -2,8 +2,8 @@
* Created On: 2010-02-04
* Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
*
* This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
* which should be included with this package. The terms are also available at
* This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
* which should be included with this package. The terms are also available at
* http://www.gnu.org/licenses/gpl-3.0.html
**/
@ -22,7 +22,7 @@ pystring2cfstring(PyObject *pystring)
UInt8 *s;
CFIndex size;
CFStringRef result;
if (PyUnicode_Check(pystring)) {
encoded = PyUnicode_AsUTF8String(pystring);
if (encoded == NULL) {
@ -32,7 +32,7 @@ pystring2cfstring(PyObject *pystring)
encoded = pystring;
Py_INCREF(encoded);
}
s = (UInt8*)PyBytes_AS_STRING(encoded);
size = PyBytes_GET_SIZE(encoded);
result = CFStringCreateWithBytes(NULL, s, size, kCFStringEncodingUTF8, FALSE);
@ -50,20 +50,20 @@ static PyObject* block_osx_get_image_size(PyObject *self, PyObject *args)
long width, height;
PyObject *pwidth, *pheight;
PyObject *result;
width = 0;
height = 0;
if (!PyArg_ParseTuple(args, "O", &path)) {
return NULL;
}
image_path = pystring2cfstring(path);
if (image_path == NULL) {
return PyErr_NoMemory();
}
image_url = CFURLCreateWithFileSystemPath(NULL, image_path, kCFURLPOSIXPathStyle, FALSE);
CFRelease(image_path);
source = CGImageSourceCreateWithURL(image_url, NULL);
CFRelease(image_url);
if (source != NULL) {
@ -75,7 +75,7 @@ static PyObject* block_osx_get_image_size(PyObject *self, PyObject *args)
}
CFRelease(source);
}
pwidth = PyLong_FromLong(width);
if (pwidth == NULL) {
return NULL;
@ -91,19 +91,19 @@ static PyObject* block_osx_get_image_size(PyObject *self, PyObject *args)
}
static CGContextRef
MyCreateBitmapContext(int width, int height)
MyCreateBitmapContext(int width, int height)
{
CGContextRef context = NULL;
CGColorSpaceRef colorSpace;
void *bitmapData;
int bitmapByteCount;
int bitmapBytesPerRow;
bitmapBytesPerRow = (width * 4);
bitmapByteCount = (bitmapBytesPerRow * height);
colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
// calloc() must be used to allocate bitmapData here because the buffer has to be zeroed.
// If it's not zeroes, when images with transparency are drawn in the context, this buffer
// will stay with undefined pixels, which means that two pictures with the same pixels will
@ -113,7 +113,7 @@ MyCreateBitmapContext(int width, int height)
fprintf(stderr, "Memory not allocated!");
return NULL;
}
context = CGBitmapContextCreate(bitmapData, width, height, 8, bitmapBytesPerRow, colorSpace,
(CGBitmapInfo)kCGImageAlphaNoneSkipLast);
if (context== NULL) {
@ -128,7 +128,7 @@ MyCreateBitmapContext(int width, int height)
static PyObject* getblock(unsigned char *imageData, int imageWidth, int imageHeight, int boxX, int boxY, int boxW, int boxH)
{
int i,j, totalR, totalG, totalB;
totalR = totalG = totalB = 0;
for(i=boxY; i<boxY+boxH; i++) {
for(j=boxX; j<boxX+boxW; j++) {
@ -142,7 +142,7 @@ static PyObject* getblock(unsigned char *imageData, int imageWidth, int imageHei
totalR /= pixelCount;
totalG /= pixelCount;
totalB /= pixelCount;
return inttuple(3, totalR, totalG, totalB);
}
@ -155,27 +155,27 @@ static PyObject* block_osx_getblocks(PyObject *self, PyObject *args)
CGImageRef image;
size_t width, height, image_width, image_height;
int block_count, block_width, block_height, orientation, i;
if (!PyArg_ParseTuple(args, "Oii", &path, &block_count, &orientation)) {
return NULL;
}
if (PySequence_Length(path) == 0) {
PyErr_SetString(PyExc_ValueError, "empty path");
return NULL;
}
if ((orientation > 8) || (orientation < 0)) {
orientation = 0; // simplifies checks later since we can only have values in 0-8
}
image_path = pystring2cfstring(path);
if (image_path == NULL) {
return PyErr_NoMemory();
}
image_url = CFURLCreateWithFileSystemPath(NULL, image_path, kCFURLPOSIXPathStyle, FALSE);
CFRelease(image_path);
source = CGImageSourceCreateWithURL(image_url, NULL);
CFRelease(image_url);
if (source == NULL) {
@ -187,8 +187,8 @@ static PyObject* block_osx_getblocks(PyObject *self, PyObject *args)
CFRelease(source);
return PyErr_NoMemory();
}
width = image_width = CGImageGetWidth(image);
height = image_height = CGImageGetHeight(image);
if (orientation >= 5) {
@ -196,9 +196,9 @@ static PyObject* block_osx_getblocks(PyObject *self, PyObject *args)
width = image_height;
height = image_width;
}
CGContextRef context = MyCreateBitmapContext(width, height);
if (orientation == 2) {
// Flip X
CGContextTranslateCTM(context, width, 0);
@ -207,7 +207,7 @@ static PyObject* block_osx_getblocks(PyObject *self, PyObject *args)
else if (orientation == 3) {
// Rot 180
CGContextTranslateCTM(context, width, height);
CGContextRotateCTM(context, RADIANS(180));
CGContextRotateCTM(context, RADIANS(180));
}
else if (orientation == 4) {
// Flip Y
@ -242,21 +242,21 @@ static PyObject* block_osx_getblocks(PyObject *self, PyObject *args)
CGContextDrawImage(context, myBoundingBox, image);
unsigned char *bitmapData = CGBitmapContextGetData(context);
CGContextRelease(context);
CGImageRelease(image);
CFRelease(source);
if (bitmapData == NULL) {
return PyErr_NoMemory();
}
block_width = max(width/block_count, 1);
block_height = max(height/block_count, 1);
result = PyList_New(block_count * block_count);
if (result == NULL) {
return NULL;
}
for(i=0; i<block_count; i++) {
int j, top;
top = min(i*block_height, height-block_height);
@ -271,8 +271,8 @@ static PyObject* block_osx_getblocks(PyObject *self, PyObject *args)
PyList_SET_ITEM(result, i*block_count+j, block);
}
}
free(bitmapData);
free(bitmapData);
return result;
}
@ -302,4 +302,4 @@ PyInit__block_osx(void)
return NULL;
}
return m;
}
}

View File

@ -2,94 +2,68 @@
* Created On: 2010-01-30
* Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
*
* This software is licensed under the "BSD" License as described in the "LICENSE" file,
* which should be included with this package. The terms are also available at
* http://www.hardcoded.net/licenses/bsd_license
* This software is licensed under the "BSD" License as described in the
* "LICENSE" file, which should be included with this package. The terms are
* also available at http://www.hardcoded.net/licenses/bsd_license
*/
#include "common.h"
/* I know that there strtol out there, but it requires a pointer to
* a char, which would in turn require me to buffer my chars around,
* making the whole process slower.
*/
static long
xchar_to_long(char c)
{
if ((c >= 48) && (c <= 57)) { /* 0-9 */
return c - 48;
}
else if ((c >= 65) && (c <= 70)) { /* A-F */
return c - 55;
}
else if ((c >= 97) && (c <= 102)) { /* a-f */
return c - 87;
}
return 0;
}
static PyObject *cache_bytes_to_colors(PyObject *self, PyObject *args) {
char *y;
Py_ssize_t char_count, i, color_count;
PyObject *result;
unsigned long r, g, b;
Py_ssize_t ci;
PyObject *color_tuple;
static PyObject*
cache_string_to_colors(PyObject *self, PyObject *args)
{
char *s;
Py_ssize_t char_count, color_count, i;
PyObject *result;
if (!PyArg_ParseTuple(args, "s#", &s, &char_count)) {
return NULL;
if (!PyArg_ParseTuple(args, "y#", &y, &char_count)) {
return NULL;
}
color_count = char_count / 3;
result = PyList_New(color_count);
if (result == NULL) {
return NULL;
}
for (i = 0; i < color_count; i++) {
ci = i * 3;
r = (unsigned char)y[ci];
g = (unsigned char)y[ci + 1];
b = (unsigned char)y[ci + 2];
color_tuple = inttuple(3, r, g, b);
if (color_tuple == NULL) {
Py_DECREF(result);
return NULL;
}
color_count = (char_count / 6);
result = PyList_New(color_count);
if (result == NULL) {
return NULL;
}
for (i=0; i<color_count; i++) {
long r, g, b;
Py_ssize_t ci;
PyObject *color_tuple;
ci = i * 6;
r = (xchar_to_long(s[ci]) << 4) + xchar_to_long(s[ci+1]);
g = (xchar_to_long(s[ci+2]) << 4) + xchar_to_long(s[ci+3]);
b = (xchar_to_long(s[ci+4]) << 4) + xchar_to_long(s[ci+5]);
color_tuple = inttuple(3, r, g, b);
if (color_tuple == NULL) {
Py_DECREF(result);
return NULL;
}
PyList_SET_ITEM(result, i, color_tuple);
}
return result;
PyList_SET_ITEM(result, i, color_tuple);
}
return result;
}
static PyMethodDef CacheMethods[] = {
{"string_to_colors", cache_string_to_colors, METH_VARARGS,
"Transform the string 's' in a list of 3 sized tuples."},
{NULL, NULL, 0, NULL} /* Sentinel */
{"bytes_to_colors", cache_bytes_to_colors, METH_VARARGS,
"Transform the bytes 's' into a list of 3 sized tuples."},
{NULL, NULL, 0, NULL} /* Sentinel */
};
static struct PyModuleDef CacheDef = {
PyModuleDef_HEAD_INIT,
"_cache",
NULL,
-1,
CacheMethods,
NULL,
NULL,
NULL,
NULL
};
static struct PyModuleDef CacheDef = {PyModuleDef_HEAD_INIT,
"_cache",
NULL,
-1,
CacheMethods,
NULL,
NULL,
NULL,
NULL};
PyObject *
PyInit__cache(void)
{
PyObject *m = PyModule_Create(&CacheDef);
if (m == NULL) {
return NULL;
}
return m;
}
PyObject *PyInit__cache(void) {
PyObject *m = PyModule_Create(&CacheDef);
if (m == NULL) {
return NULL;
}
return m;
}

View File

@ -2,8 +2,8 @@
* Created On: 2010-02-04
* Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
*
* This software is licensed under the "BSD" License as described in the "LICENSE" file,
* which should be included with this package. The terms are also available at
* This software is licensed under the "BSD" License as described in the "LICENSE" file,
* which should be included with this package. The terms are also available at
* http://www.hardcoded.net/licenses/bsd_license
*/
@ -27,19 +27,19 @@ PyObject* inttuple(int n, ...)
PyObject *pnumber;
PyObject *result;
va_list numbers;
va_start(numbers, n);
result = PyTuple_New(n);
for (i=0; i<n; i++) {
pnumber = PyLong_FromLong(va_arg(numbers, long));
pnumber = PyLong_FromUnsignedLong(va_arg(numbers, long));
if (pnumber == NULL) {
Py_DECREF(result);
return NULL;
}
PyTuple_SET_ITEM(result, i, pnumber);
}
va_end(numbers);
return result;
}

View File

@ -2,8 +2,8 @@
* Created On: 2010-02-04
* Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
*
* This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
* which should be included with this package. The terms are also available at
* This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
* which should be included with this package. The terms are also available at
* http://www.gnu.org/licenses/gpl-3.0.html
*/
@ -17,4 +17,4 @@ int min(int a, int b);
#endif
/* Create a tuple out of an array of integers. */
PyObject* inttuple(int n, ...);
PyObject* inttuple(int n, ...);

View File

@ -29,7 +29,7 @@ class Photo(fs.File):
__slots__ = fs.File.__slots__ + tuple(INITIAL_INFO.keys())
# These extensions are supported on all platforms
HANDLED_EXTS = {"png", "jpg", "jpeg", "gif", "bmp", "tiff", "tif"}
HANDLED_EXTS = {"png", "jpg", "jpeg", "gif", "bmp", "tiff", "tif", "webp"}
def _plat_get_dimensions(self):
raise NotImplementedError()
@ -37,7 +37,7 @@ class Photo(fs.File):
def _plat_get_blocks(self, block_count_per_side, orientation):
raise NotImplementedError()
def _get_orientation(self):
def get_orientation(self):
if not hasattr(self, "_cached_orientation"):
try:
with self.path.open("rb") as fp:
@ -95,10 +95,13 @@ class Photo(fs.File):
fs.File._read_info(self, field)
if field == "dimensions":
self.dimensions = self._plat_get_dimensions()
if self._get_orientation() in {5, 6, 7, 8}:
if self.get_orientation() in {5, 6, 7, 8}:
self.dimensions = (self.dimensions[1], self.dimensions[0])
elif field == "exif_timestamp":
self.exif_timestamp = self._get_exif_timestamp()
def get_blocks(self, block_count_per_side):
return self._plat_get_blocks(block_count_per_side, self._get_orientation())
def get_blocks(self, block_count_per_side, orientation: int = None):
if orientation is None:
return self._plat_get_blocks(block_count_per_side, self.get_orientation())
else:
return self._plat_get_blocks(block_count_per_side, orientation)

View File

@ -14,6 +14,7 @@ from core.pe import matchblock, matchexif
class ScannerPE(Scanner):
cache_path = None
match_scaled = False
match_rotated = False
@staticmethod
def get_scan_options():
@ -29,6 +30,7 @@ class ScannerPE(Scanner):
cache_path=self.cache_path,
threshold=self.min_match_percentage,
match_scaled=self.match_scaled,
match_rotated=self.match_rotated,
j=j,
)
elif self.scan_type == ScanType.EXIFTIMESTAMP:

View File

@ -10,6 +10,7 @@ import logging
import re
import os
import os.path as op
from errno import EISDIR, EACCES
from xml.etree import ElementTree as ET
from hscommon.jobprogress.job import nulljob
@ -376,8 +377,8 @@ class Results(Markable):
do_write(outfile)
except OSError as e:
# If our OSError is because dest is already a directory, we want to handle that. 21 is
# the code we get on OS X and Linux, 13 is what we get on Windows.
if e.errno in {21, 13}:
# the code we get on OS X and Linux (EISDIR), 13 is what we get on Windows (EACCES).
if e.errno in (EISDIR, EACCES):
p = str(outfile)
dirname, basename = op.split(p)
otherfiles = os.listdir(dirname)

View File

@ -87,8 +87,6 @@ class Scanner:
}
):
j = j.start_subjob([2, 8])
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)
if self.size_threshold:
files = [f for f in files if f.size >= self.size_threshold]
if self.large_size_threshold:
@ -171,8 +169,11 @@ class Scanner:
matches = [m for m in matches if m.first.path not in toremove or m.second.path not in toremove]
if not self.mix_file_kind:
matches = [m for m in matches if get_file_ext(m.first.name) == get_file_ext(m.second.name)]
matches = [m for m in matches if 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)]
if self.include_exists_check:
matches = [m for m in matches if m.first.exists() and m.second.exists()]
# Contents already handles ref checks, other scan types might not catch during scan
if self.scan_type != ScanType.CONTENTS:
matches = [m for m in matches if not (m.first.is_ref and m.second.is_ref)]
if ignore_list:
matches = [m for m in matches if not ignore_list.are_ignored(str(m.first.path), str(m.second.path))]
logging.info("Grouping matches")
@ -212,3 +213,4 @@ class Scanner:
large_size_threshold = 0
big_file_size_threshold = 0
word_weighting = False
include_exists_check = True

View File

@ -10,41 +10,41 @@ from pytest import raises, skip
from hscommon.testutil import eq_
try:
from core.pe.cache import colors_to_string, string_to_colors
from core.pe.cache import colors_to_bytes, bytes_to_colors
from core.pe.cache_sqlite import SqliteCache
from core.pe.cache_shelve import ShelveCache
except ImportError:
skip("Can't import the cache module, probably hasn't been compiled.")
class TestCaseColorsToString:
def test_no_color(self):
eq_("", colors_to_string([]))
eq_(b"", colors_to_bytes([]))
def test_single_color(self):
eq_("000000", colors_to_string([(0, 0, 0)]))
eq_("010101", colors_to_string([(1, 1, 1)]))
eq_("0a141e", colors_to_string([(10, 20, 30)]))
eq_(b"\x00\x00\x00", colors_to_bytes([(0, 0, 0)]))
eq_(b"\x01\x01\x01", colors_to_bytes([(1, 1, 1)]))
eq_(b"\x0a\x14\x1e", colors_to_bytes([(10, 20, 30)]))
def test_two_colors(self):
eq_("000102030405", colors_to_string([(0, 1, 2), (3, 4, 5)]))
eq_(b"\x00\x01\x02\x03\x04\x05", colors_to_bytes([(0, 1, 2), (3, 4, 5)]))
class TestCaseStringToColors:
def test_empty(self):
eq_([], string_to_colors(""))
eq_([], bytes_to_colors(b""))
def test_single_color(self):
eq_([(0, 0, 0)], string_to_colors("000000"))
eq_([(2, 3, 4)], string_to_colors("020304"))
eq_([(10, 20, 30)], string_to_colors("0a141e"))
eq_([(0, 0, 0)], bytes_to_colors(b"\x00\x00\x00"))
eq_([(2, 3, 4)], bytes_to_colors(b"\x02\x03\x04"))
eq_([(10, 20, 30)], bytes_to_colors(b"\x0a\x14\x1e"))
def test_two_colors(self):
eq_([(10, 20, 30), (40, 50, 60)], string_to_colors("0a141e28323c"))
eq_([(10, 20, 30), (40, 50, 60)], bytes_to_colors(b"\x0a\x14\x1e\x28\x32\x3c"))
def test_incomplete_color(self):
# don't return anything if it's not a complete color
eq_([], string_to_colors("102"))
eq_([], bytes_to_colors(b"\x01"))
eq_([(1, 2, 3)], bytes_to_colors(b"\x01\x02\x03\x04"))
class BaseTestCaseCache:
@ -59,13 +59,13 @@ class BaseTestCaseCache:
def test_set_then_retrieve_blocks(self):
c = self.get_cache()
b = [(0, 0, 0), (1, 2, 3)]
b = [[(0, 0, 0), (1, 2, 3)]] * 8
c["foo"] = b
eq_(b, c["foo"])
def test_delitem(self):
c = self.get_cache()
c["foo"] = ""
c["foo"] = [[]] * 8
del c["foo"]
assert "foo" not in c
with raises(KeyError):
@ -74,16 +74,16 @@ class BaseTestCaseCache:
def test_persistance(self, tmpdir):
DBNAME = tmpdir.join("hstest.db")
c = self.get_cache(str(DBNAME))
c["foo"] = [(1, 2, 3)]
c["foo"] = [[(1, 2, 3)]] * 8
del c
c = self.get_cache(str(DBNAME))
eq_([(1, 2, 3)], c["foo"])
eq_([[(1, 2, 3)]] * 8, c["foo"])
def test_filter(self):
c = self.get_cache()
c["foo"] = ""
c["bar"] = ""
c["baz"] = ""
c["foo"] = [[]] * 8
c["bar"] = [[]] * 8
c["baz"] = [[]] * 8
c.filter(lambda p: p != "bar") # only 'bar' is removed
eq_(2, len(c))
assert "foo" in c
@ -92,9 +92,9 @@ class BaseTestCaseCache:
def test_clear(self):
c = self.get_cache()
c["foo"] = ""
c["bar"] = ""
c["baz"] = ""
c["foo"] = [[]] * 8
c["bar"] = [[]] * 8
c["baz"] = [[]] * 8
c.clear()
eq_(0, len(c))
assert "foo" not in c
@ -104,7 +104,7 @@ class BaseTestCaseCache:
def test_by_id(self):
# it's possible to use the cache by referring to the files by their row_id
c = self.get_cache()
b = [(0, 0, 0), (1, 2, 3)]
b = [[(0, 0, 0), (1, 2, 3)]] * 8
c["foo"] = b
foo_id = c.get_id("foo")
eq_(c[foo_id], b)
@ -127,15 +127,10 @@ class TestCaseSqliteCache(BaseTestCaseCache):
fp.write("invalid sqlite content")
fp.close()
c = self.get_cache(dbname) # should not raise a DatabaseError
c["foo"] = [(1, 2, 3)]
c["foo"] = [[(1, 2, 3)]] * 8
del c
c = self.get_cache(dbname)
eq_(c["foo"], [(1, 2, 3)])
class TestCaseShelveCache(BaseTestCaseCache):
def get_cache(self, dbname=None):
return ShelveCache(dbname)
eq_(c["foo"], [[(1, 2, 3)]] * 8)
class TestCaseCacheSQLEscape:
@ -157,7 +152,7 @@ class TestCaseCacheSQLEscape:
def test_delitem(self):
c = self.get_cache()
c["foo'bar"] = []
c["foo'bar"] = [[]] * 8
try:
del c["foo'bar"]
except KeyError:

View File

@ -326,6 +326,7 @@ def test_default_path_state_override(tmpdir):
def _default_state_for_path(self, path):
if "foobar" in path.parts:
return DirectoryState.EXCLUDED
return DirectoryState.NORMAL
d = MyDirectories()
p1 = Path(str(tmpdir))

View File

@ -71,7 +71,10 @@ class TestCasegetwords:
def test_unicode(self):
eq_(["e", "c", "0", "a", "o", "u", "e", "u"], getwords("é ç 0 à ö û è ¤ ù"))
eq_(["02", "君のこころは輝いてるかい?", "国木田花丸", "solo", "ver"], getwords("02 君のこころは輝いてるかい? 国木田花丸 Solo Ver"))
eq_(
["02", "君のこころは輝いてるかい?", "国木田花丸", "solo", "ver"],
getwords("02 君のこころは輝いてるかい? 国木田花丸 Solo Ver"),
)
def test_splitter_chars(self):
eq_(

View File

@ -17,6 +17,7 @@ from core.scanner import Scanner, ScanType
from core.me.scanner import ScannerME
# TODO update this to be able to inherit from fs.File
class NamedObject:
def __init__(self, name="foobar", size=1, path=None):
if path is None:
@ -31,6 +32,9 @@ class NamedObject:
def __repr__(self):
return "<NamedObject {!r} {!r}>".format(self.name, self.path)
def exists(self):
return self.path.exists()
no = NamedObject
@ -238,12 +242,12 @@ def test_content_scan_doesnt_put_digest_in_words_at_the_end(fake_fileexists):
s = Scanner()
s.scan_type = ScanType.CONTENTS
f = [no("foo"), no("bar")]
f[0].digest = f[0].digest_partial = f[
0
].digest_samples = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
f[1].digest = f[1].digest_partial = f[
1
].digest_samples = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
f[0].digest = f[0].digest_partial = f[0].digest_samples = (
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
)
f[1].digest = f[1].digest_partial = f[1].digest_samples = (
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
)
r = s.get_dupe_groups(f)
# FIXME looks like we are missing something here?
r[0]

View File

@ -1,3 +1,8 @@
=== 4.3.1 (2022-07-08)
* Fix issue where cache db exceptions could prevent files being hashed (#1015)
* Add extra guard for non-zero length files without digests to prevent false duplicates
* Update Italian translations
=== 4.3.0 (2022-07-01)
* Redirect stdout from custom command to the log files (#1008)
* Update translations
@ -52,7 +57,7 @@
=== 4.1.1 (2021-03-21)
* Add Japanese
* Add Japanese
* Update internationalization and translations to be up to date with current UI.
* Minor translation and UI language updates
* Fix language selection issues on Windows (#760)
@ -414,7 +419,7 @@
=== 2.6.1 (2009-03-27)
* **Fixed** an occasional crash caused by permission issues.
* **Fixed** a bug where the "X discarded" notice would show a too large number of discarded
* **Fixed** a bug where the "X discarded" notice would show a too large number of discarded
duplicates.
=== 2.6.0 (2008-09-10)
@ -448,14 +453,14 @@
* **Added** the "Remove empty folders" option.
* **Fixed** results load/save issues.
* **Fixed** occasional status bar inaccuracies when the results are filtered.
=== 2.5.0 (2007-09-15)
* **Added** post scan filtering.
* **Fixed** issues with the rename feature under Windows
* **Fixed** some user interface annoyances under Windows
=== 2.4.8 (2007-04-14)
@ -471,7 +476,7 @@
* **Added** Re-orderable columns. In fact, I re-added the feature which was lost in the C# conversion in 2.4.0 (Windows).
* **Changed** the behavior of the scanning engine when setting the hardness to 100. It will now only match files that have their words in the same order.
* **Fixed** a bug with all the Delete/Move/Copy actions with certain kinds of files.
* **Fixed** a bug with all the Delete/Move/Copy actions with certain kinds of files.
=== 2.4.5 (2007-01-11)
@ -509,7 +514,7 @@
=== 2.3.4 (2006-11-07)
* **Improved** speed and memory usage of the scanning engine, again. Does it mean there was a lot of improvements to be made? Nah...
* **Improved** speed and memory usage of the scanning engine, again. Does it mean there was a lot of improvements to be made? Nah...
=== 2.3.3 (2006-11-02)
@ -567,7 +572,7 @@
=== 2.2.3 (2006-06-15)
* **Improved** duplicate scanning speed.
* **Added** a warning that a file couldn't be renamed if a file with the same name already exists.
* **Added** a warning that a file couldn't be renamed if a file with the same name already exists.
=== 2.2.2 (2006-06-07)
@ -611,9 +616,9 @@
=== 2.0.0 (2006-03-17)
* Complete rewrite.
* Complete rewrite.
* Now runs on Mac OS X.
=== 1.0.0 (2004-09-24)
* Initial release.
* Initial release.

View File

@ -71,7 +71,7 @@ Häufig gestellte Fragen
* Klicken Sie **Markieren --> Alle Markieren**.
.. only:: edition_me
.. topic:: Ich möchte alle Stücke markieren, die mehr als 3 Sekunden von ihrer Referenz verschieden sind. Was kann ich tun?
* Aktivieren Sie den :doc:`Nur Duplikate <results>` Modus.
@ -83,7 +83,7 @@ Häufig gestellte Fragen
* Klicken Sie auf **Entferne Ausgewählte von den Ergebnissen**.
.. topic:: Ich möchte meine Stücke mit der höchsten Bitrate zur Referenz machen. Was kann ich tun?
* Aktivieren Sie den :doc:`Nur Duplikate <results>` Modus.
* Aktivieren Sie den **Deltawerte** Modus.
* Klicken Sie auf die "Bitrate" Spalte, um nach Bitrate zu sortieren.
@ -92,9 +92,9 @@ Häufig gestellte Fragen
* Klicken Sie auf **Mache Ausgewählte zur Referenz**.
.. topic:: Ich möchte nicht das [live] und [remix] Versionen meiner Stücke als Duplikate erkannt werden. Was kann ich tun?
Ist Ihre Vergleichsschwelle niedrig genug, werden möglicherweise die live und remix Versionen in der Ergebnisliste landen. Das kann nicht verhindert werden, aber es gibt die Möglichkeit die Ergebnisse nach dem Scan zu entfernen, mittels dem Filter. Möchten Sie jedes Stück mit irgendetwas in eckigen Klammern [] im Dateinamen entfernen, so:
* **Windows**: Klicken Sie auf **Aktionen --> Filter anwenden**, geben "[*]" ein und klicken OK.
* **Mac OS X**: Geben Sie "[*]" in das "Filter" Feld der Werkzeugleiste ein.
* Klicken Sie auf **Markieren --> Alle Markieren**.

View File

@ -16,7 +16,7 @@ Jeder Ordner kann in einem von 3 Zuständen sein:
* **Referenz:** Duplikate in diesem Ordner können **nicht** gelöscht werden. Dateien dieses Ordners können sich nur in der **Referenz** Position einer Duplikatgruppe befinden. Ist mehr als eine Datei des Referenzordners in derselben Duplikatgruppe, so wird nur Eine behalten. Die Anderen werden aus der Gruppe entfernt.
* **Ausgeschlossen:** Dateien in diesem Verzeichnis sind nicht im Scan eingeschlossen.
Der Standardzustand eines Ordners ist natürlich **Normal**. Sie können den **Referenz** Zustand für Ordner nutzen, in denen auf keinen Fall eine Datei gelöscht werden soll.
Der Standardzustand eines Ordners ist natürlich **Normal**. Sie können den **Referenz** Zustand für Ordner nutzen, in denen auf keinen Fall eine Datei gelöscht werden soll.
Wenn sie einen Zustand für ein Verzeichnis setzen, erben alle Unterordner automatisch diesen Zustand, es sei denn Sie ändern den Zustand der Unterordner explizit.

View File

@ -21,7 +21,7 @@ Inhalte:
.. toctree::
:maxdepth: 2
quick_start
folders
preferences

View File

@ -4,9 +4,9 @@ Einstellungen
.. only:: edition_se
**Scan Typ:** Diese Option bestimmt nach welcher Eigenschaft die Dateien in einem Duplikate Scan verglichen werden. Wenn Sie **Dateiname** auswählen, wird dupeGuru jeden Dateinamen Wort für Wort vergleichen und, abhängig von den unteren Einstellungen, feststellen ob genügend Wörter übereinstimmen, um 2 Dateien als Duplikate zu betrachten. Wenn Sie **Inhalt** wählen, werden nur Dateien mit dem exakt gleichen Inhalt zusammenpassen.
Der **Ordner** Scan Typ ist etwas speziell. Wird er ausgewählt, scannt dupeGuru nach doppelten Ordnern anstelle von Dateien. Um festzustellen ob 2 Ordner identisch sind, werden alle Datein im Ordner gescannt und wenn die Inhalte aller Dateien der Ordner übereinstimmen, werden die Ordner als Duplikate erkannt.
**Filterempfindlichkeit:** Wenn Sie den **Dateiname** Scan Typ wählen, bestimmt diese Option wie ähnlich 2 Dateinamen für dupeGuru sein müssen, um Duplikate zu sein. Ist die Empfindlichkeit zum Beispiel 80, müssen 80% der Worte der 2 Dateinamen übereinstimmen. Um den Übereinstimmungsanteil herauszufinden, zählt dupeGuru zuerst die Gesamtzahl der Wörter **beider** Dateinamen, dann werden die gleichen Wörter gezählt (jedes Wort zählt als 2) und durch die Gesamtzahl der Wörter dividiert. Ist das Resultat größer oder gleich der Filterempfindlichkeit, haben wir ein Duplikat. Zum Beispiel, "a b c d" und "c d e" haben einen Übereinstimmungsanteil von 57 (4 gleiche Wörter, insgesamt 7 Wörter).
.. only:: edition_me
@ -33,7 +33,7 @@ Einstellungen
.. only:: edition_pe
**Scan Typ:** Diese option bestimmt, welcher Scan Typ bei Ihren Bildern angewendet wird. Der **Inhalte** Scan Typ vergleicht den Inhalt der Bilder auf eine ungenaue Art und Weise (so werden nicht nur exakte Duplikate gefunden, sondern auch Ähnliche). Der **EXIF Zeitstempel** Scan Typ schaut auf die EXIF Metadaten der Bilder (wenn vorhanden) und erkennt Bilder die den Selben haben. Er ist viel schneller als der Inhalte Scan. **Warnung:** Veränderte Bilder behalten oft den selben EXIF Zeitstempel, also achten Sie auf Falschpositive bei der Nutzung dieses Scans.
**Filterempfindlichkeit:** *Nur Inhalte Scan.* Je höher diese Einstellung, desto strenger ist der Filter (Mit anderen Worten, desto weniger Ergebnisse erhalten Sie). Die meisten Bilder der selben Qualität stimmen zu 100% überein, selbst wenn das Format anders ist (PNG und JPG zum Beispiel). Wie auch immer, wenn ein PNG mit einem JPG niederiger Qualität übereinstimmen soll, muss die Filterempfindlichkeit kleiner als 100 sein. Die Voreinstellung, 95, ist eine gute Wahl.
**Bilder unterschiedlicher Abmessung gleich:** Wird diese Box gewählt, dürfen Bilder unterschiedlicher Abmessung in einer Duplikategruppe sein..
@ -57,7 +57,7 @@ Auf jeden Fall behandelt dupeGuru Namenskonflikte indem es dem Ziel-Dateinamen e
**Eigener Befehl:** Diese Einstellung bestimmt den Befehl der durch "Führe eigenen Befehl aus" ausgeführt wird. Sie können jede externe Anwendung durch diese Aktion aufrufen. Dies ist zum Beispiel hilfreich, wenn Sie eine gute diff-Anwendung installiert haben.
Das Format des Befehls ist das Selbe wie in einer Befehlszeile, außer das 2 Platzhalter vorhanden sind: **%d** und **%r**. Diese Platzhalter werden durch den Pfad des markierten Duplikates (%d) und dem Pfad der Duplikatereferenz ersetzt (%r).
Wenn der Pfad Ihrer ausführbaren Datei Leerzeichen enthält, so schließen sie ihn bitte mit "" Zeichen ein. Sie sollten auch Platzhalter mit den Zitatzeichen einschließen, denn es ist möglich, das die Pfade der Duplikate und Referenzen ebenfalls Leerzeichen enthalten. Hier ist ein Beispiel eines eigenen Befehls::
"C:\Program Files\SuperDiffProg\SuperDiffProg.exe" "%d" "%r"

View File

@ -22,4 +22,4 @@ criterion is used and so on and so on. For example, if your arguments are "Size
"Filename (Doesn't end with a number)", the reference file that will be picked in a group will be
the biggest file, and if two or more files have the same size, the one that has a filename that
doesn't end with a number will be used. When all criteria result in ties, the order in which dupes
previously were in the group will be used.
previously were in the group will be used.

View File

@ -98,4 +98,4 @@ Aktionen Menü
* **Ausgewählte umbenennen:** Fragt nach einem neuen Namen und benennt die ausgewählte Datei um.
.. todo:: Add Move and iPhoto/iTunes warning
.. todo:: Add "Deletion Options" section.
.. todo:: Add "Deletion Options" section.

View File

@ -24,7 +24,7 @@ Development process
* `Issue Tracker`_
* `Issue labels meaning`_
dupeGuru's source code is on Github and thus managed in a Git repository. At all times, you should
dupeGuru's source code is on GitHub and thus managed in a Git repository. At all times, you should
be able to build from source a fresh checkout of the ``master`` branch using instructions from the
``README.md`` file at the root of this project. If you can't, it's a bug. Please report it.
@ -61,7 +61,7 @@ It's the same thing with feature requests. Description of a feature request, whe
already been given to how such a feature would fit in the current design, are precious to developers
and help them figure out a clear roadmap for the project.
So, even if you're not a developer, you can always open a Github account and create/comment issues.
So, even if you're not a developer, you can always open a GitHub account and create/comment issues.
Your contribution will be much appreciated.
**Documentation**. This is a bit trickier because dupeGuru's documentation is written with a rather

View File

@ -2,12 +2,12 @@ core.engine
===========
.. automodule:: core.engine
.. autoclass:: Match
.. autoclass:: Group
:members:
.. autofunction:: build_word_dict
.. autofunction:: compare
.. autofunction:: compare_fields
@ -16,7 +16,7 @@ core.engine
.. autofunction:: get_groups
.. autofunction:: merge_similar_words
.. autofunction:: reduce_common_words
.. _fields:
Fields

View File

@ -6,5 +6,5 @@ core.gui
.. toctree::
:maxdepth: 2
deletion_options

View File

@ -3,7 +3,7 @@ core
.. toctree::
:maxdepth: 2
app
fs
engine

View File

@ -4,9 +4,9 @@ hscommon.gui.base
.. automodule:: hscommon.gui.base
.. autosummary::
GUIObject
.. autoclass:: GUIObject
:members:
:private-members:

View File

@ -4,22 +4,22 @@ hscommon.gui.column
.. automodule:: hscommon.gui.column
.. autosummary::
Columns
Column
ColumnsView
PrefAccessInterface
.. autoclass:: Columns
:members:
:private-members:
.. autoclass:: Column
:members:
:private-members:
.. autoclass:: ColumnsView
:members:
.. autoclass:: PrefAccessInterface
:members:

View File

@ -4,15 +4,14 @@ hscommon.gui.progress_window
.. automodule:: hscommon.gui.progress_window
.. autosummary::
ProgressWindow
ProgressWindowView
.. autoclass:: ProgressWindow
:members:
:private-members:
.. autoclass:: ProgressWindowView
:members:
:private-members:

View File

@ -4,23 +4,23 @@ hscommon.gui.selectable_list
.. automodule:: hscommon.gui.selectable_list
.. autosummary::
Selectable
SelectableList
GUISelectableList
GUISelectableListView
.. autoclass:: Selectable
:members:
:private-members:
.. autoclass:: SelectableList
:members:
:private-members:
.. autoclass:: GUISelectableList
:members:
:private-members:
.. autoclass:: GUISelectableListView
:members:

View File

@ -2,18 +2,18 @@ hscommon.gui.table
==================
.. automodule:: hscommon.gui.table
.. autosummary::
Table
Row
GUITable
GUITableView
.. autoclass:: Table
:members:
:private-members:
.. autoclass:: Row
:members:
:private-members:
@ -21,6 +21,6 @@ hscommon.gui.table
.. autoclass:: GUITable
:members:
:private-members:
.. autoclass:: GUITableView
:members:

View File

@ -4,10 +4,10 @@ hscommon.gui.text_field
.. automodule:: hscommon.gui.text_field
.. autosummary::
TextField
TextFieldView
.. autoclass:: TextField
:members:
:private-members:

View File

@ -2,17 +2,16 @@ hscommon.gui.tree
=================
.. automodule:: hscommon.gui.tree
.. autosummary::
Tree
Node
.. autoclass:: Tree
:members:
:private-members:
.. autoclass:: Node
:members:
:private-members:

View File

@ -4,7 +4,7 @@ hscommon
.. toctree::
:maxdepth: 2
:glob:
build
conflict
desktop
@ -13,4 +13,3 @@ hscommon
util
jobprogress/*
gui/*

View File

@ -4,14 +4,13 @@ hscommon.jobprogress.job
.. automodule:: hscommon.jobprogress.job
.. autosummary::
Job
NullJob
.. autoclass:: Job
:members:
:private-members:
.. autoclass:: NullJob
:members:

View File

@ -4,9 +4,8 @@ hscommon.jobprogress.performer
.. automodule:: hscommon.jobprogress.performer
.. autosummary::
ThreadedJobPerformer
.. autoclass:: ThreadedJobPerformer
:members:

View File

@ -69,6 +69,6 @@ API
.. toctree::
:maxdepth: 2
core/index
hscommon/index

View File

@ -30,8 +30,8 @@ that makes sure that you will **always** keep at least one member of the duplica
How can I report a bug a suggest a feature?
-------------------------------------------
dupeGuru is hosted on `Github`_ and it's also where issues are tracked. The best way to report a
bug or suggest a feature is to sign up on Github and `open an issue`_.
dupeGuru is hosted on `GitHub`_ and it's also where issues are tracked. The best way to report a
bug or suggest a feature is to sign up on GitHub and `open an issue`_.
The mark box of a file I want to delete is disabled. What must I do?
--------------------------------------------------------------------
@ -176,6 +176,5 @@ Preferences are stored elsewhere:
* Linux: ``~/.config/Hardcoded Software/dupeGuru.conf``
* Mac OS X: In the built-in ``defaults`` system, as ``com.hardcoded-software.dupeguru``
.. _Github: https://github.com/arsenetar/dupeguru
.. _GitHub: https://github.com/arsenetar/dupeguru
.. _open an issue: https://github.com/arsenetar/dupeguru/wiki/issue-labels

View File

@ -24,7 +24,7 @@ Contents:
.. toctree::
:maxdepth: 2
contribute
quick_start
folders

View File

@ -14,6 +14,10 @@ Preferences
If you check this box, pictures of different dimensions will be allowed in the same
duplicate group.
**Match pictures of different rotations:**
If you check this box, pictures of different rotations will be allowed in the same
duplicate group.
.. _filter-hardness:
**Filter Hardness:**
@ -67,11 +71,11 @@ filename if the filename already exists in the destination.
The format of the command is the same as what you would write in the command line, except that there
are 2 placeholders: **%d** and **%r**. These placeholders will be replaced by the path of the
selected dupe (%d) and the path of the selected dupe's reference file (%r).
If the path to your executable contains space characters, you should enclose it in "" quotes. You
should also enclose placeholders in quotes because it's very possible that paths to dupes and refs
will contain spaces. Here's an example custom command::
"C:\Program Files\SuperDiffProg\SuperDiffProg.exe" "%d" "%r"
.. _inode: http://en.wikipedia.org/wiki/Inode

View File

@ -22,4 +22,4 @@ criterion is used and so on and so on. For example, if your arguments are "Size
"Filename (Doesn't end with a number)", the reference file that will be picked in a group will be
the biggest file, and if two or more files have the same size, the one that has a filename that
doesn't end with a number will be used. When all criteria result in ties, the order in which dupes
previously were in the group will be used.
previously were in the group will be used.

View File

@ -180,7 +180,7 @@ any of them.
the file's path. If the original file is deleted or moved, the link is broken. A hardlink is a
link to the file *itself*. That link is as good as a "real" file. Only when *all* hardlinks to a
file are deleted is the file itself deleted.
On OSX and Linux, this feature is supported fully, but under Windows, it's a bit complicated.
Windows XP doesn't support it, but Vista and up support it. However, for the feature to work,
dupeGuru has to run with administrative privileges.

View File

@ -51,7 +51,7 @@ Tour groupe de doublons contient au moins un fichier dit "référence" et ce fic
effacé. Par contre, ce que vous pouvez faire c'est de le remplacer par un autre fichier du groupe.
Pour ce faire, sélectionnez un fichier du groupe et cliquez sur l'action **Transformer sélectionnés
en références**.
Notez que si le fichier référence du groupe vient d'un dossier qui a été défini comme dossier
référence, ce fichier ne peut pas être déplacé de sa position de référence du groupe.
@ -71,7 +71,7 @@ doublons. Example: Nous avons 3 fichiers, A, B et C. Nous les comparons en utili
de filtre. La comparaison détermine que A est un double de B, A est un double C, mais que B n'est
**pas** un double de C. dupeGuru a ici un problème. Il ne peut pas créer un groupe avec A, B et C.
Il décide donc de jeter C hors du groupe. C'est de là que vient la notice '(X hors-groupe)'.
Cette notice veut dire que si jamais vous effacez tout les doubles contenus dans vos résultats et
que vous faites un nouveau scan, vous pourriez avoir de nouveaux résultats.

View File

@ -3,7 +3,7 @@ Sélection de dossiers
La première fenêtre qui apparaît lorsque dupeGuru démarre est la fenêtre de sélection de dossiers à scanner. Elle détermine la liste des dossiers qui seront scannés lorsque vous cliquerez sur **Scan**.
Pour ajouter un dossier, cliquez sur le bouton **+**. Si vous avez ajouté des dossiers dans le passé, un menu vous permettra de rapidement choisir un de ceux ci. Autrement, il vous sera demandé d'indiquer le dossier à ajouter.
Pour ajouter un dossier, cliquez sur le bouton **+**. Si vous avez ajouté des dossiers dans le passé, un menu vous permettra de rapidement choisir un de ceux ci. Autrement, il vous sera demandé d'indiquer le dossier à ajouter.
Vous pouvez aussi utiliser le drag & drop pour ajouter des dossiers à la liste.
@ -26,14 +26,14 @@ Le type d'un dossier s'applique à ses sous-dossiers, excepté si un sous-dossie
Bibliothèques iPhoto et Aperture
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
dupeGuru PE supporte iPhoto et Aperture, ce qui veut dire qu'il sait comment lire le contenu de
ces bibliothèques et comment communiquer avec ces applications pour correctement supprimer des
photos de celles-ci. Pour utiliser cette fonctionnalité, vous devez ajouter iPhoto et/ou
Aperture avec les boutons spéciaux "Ajouter librairie iPhoto" et "Ajouter librairie Aperture",
qui apparaissent quand on clique sur le petit "+". Les dossiers ajoutés seront alors
correctement interprétés par dupeGuru.
Quand une photo est supprimée d'iPhoto, elle est envoyée dans la corbeille d'iPhoto.
Quand une photo est supprimée d'Aperture, il n'est malheureusement pas possible de l'envoyer
@ -45,13 +45,13 @@ Le type d'un dossier s'applique à ses sous-dossiers, excepté si un sous-dossie
Bibliothèques iTunes
^^^^^^^^^^^^^^^^^^^^
dupeGuru ME supporte iTunes, ce qui veut dire qu'il sait comment lire le contenu de sa
bibliothèque et comment communiquer avec iTunes pour correctement supprimer des chansons de sa
bibliothèque. Pour utiliser cette fonctionnalité, vous devez ajouter iTunes avec le bouton
spécial "Ajouter librairie iTunes", qui apparait quand on clique sur le petit "+". Le dossier
ajouté sera alors correctement interprété par dupeGuru.
Quand une chanson est supprimée d'iTunes, elle est envoyée à la corebeille du système, comme un
fichier normal. La différence ici, c'est qu'après la suppression, iTunes est correctement mis au
fait de cette suppression et retire sa référence à cette chanson de sa bibliothèque.

View File

@ -21,7 +21,7 @@ Contents:
.. toctree::
:maxdepth: 2
quick_start
folders
preferences

View File

@ -4,9 +4,9 @@ Préférences
.. only:: edition_se
**Type de scan:** Cette option détermine quels aspects du fichier doit être comparé. Un scan par **Nom de fichier** compare les noms de fichiers mot-à-mot et, dépendant des autres préférences ci-dessous, déterminera si les noms se ressemblent assez pour être considérés comme doublons. Un scan par **Contenu** trouvera les doublons qui ont exactement le même contenu.
Le scan **Dossiers** est spécial. Si vous le sélectionnez, dupeGuru cherchera des doublons de *dossiers* plutôt que des doublons de fichiers. Pour déterminer si deux dossiers sont des doublons, dupeGuru regarde le contenu de tous les fichiers dans les dossiers, et si **tous** sont les mêmes, les dossiers sont considérés comme des doublons.
**Seuil du filtre:** Pour les scan de type **Nom de fichier**, cette option détermine le degré de similtude nécessaire afin de considérer deux noms comme doublons. Avec un seuil de 80, 80% des mots doivent être égaux. Pour déterminer ce pourcentage, dupeGuru compte le nombre de mots total des deux noms, puis compte le nombre de mots égaux, puis fait la division des deux. Un résultat égalisant ou dépassant le seuil sera considéré comme un doublon. Exemple: "a b c d" et "c d e" ont un pourcentage de 57 (4 mots égaux, 7 au total).
.. only:: edition_me
@ -33,7 +33,7 @@ Préférences
.. only:: edition_pe
**Type de scan:** Détermine le type de scan qui sera fait sur vos images. Le type **Contenu** compare le contenu des images de façon "fuzzy", rendant possible de trouver non seulement les doublons exactes, mais aussi les similaires. Le type **EXIF Timestamp** compare les métadonnées EXIF des images (si existantes) et détermine si le "timestamp" (moment de prise de la photo) est pareille. C'est beaucoup plus rapide que le scan par Contenu. **Attention:** Les photos modifiées gardent souvent le même timestamp, donc faites attention aux faux doublons si vous utilisez cette méthode.
**Seuil du filtre:** *Scan par Contenu seulement.* Plus il est élevé, plus les images doivent être similaires pour être considérées comme des doublons. Le défaut de 95% permet quelques petites différence, comme par exemple une différence de qualité ou bien une légère modification des couleurs.
**Comparer les images de tailles différentes:** Le nom dit tout. Sans cette option, les images de tailles différentes ne sont pas comparées.
@ -58,6 +58,6 @@ Dans tous les cas, dupeGuru résout les conflits de noms de fichier en ajoutant
Le format de la ligne de commande est la même que celle que vous écrireriez manuellement, excepté pour les arguments, **%d** et **%r**. L'endroit où vous placez ces deux arguments sera remplacé par le chemin du fichier sélectionné (%d) et le chemin de son fichier référence dans le groupe (%r).
Si le chemin de votre executable contient un espace, vous devez le placer entre guillemets "". Vous devriez aussi placer vos arguments %d et %r entre guillemets parce qu'il est très possible d'avoir des chemins de fichier contenant des espaces. Voici un exemple de commande personnelle::
Si le chemin de votre executable contient un espace, vous devez le placer entre guillemets "". Vous devriez aussi placer vos arguments %d et %r entre guillemets parce qu'il est très possible d'avoir des chemins de fichier contenant des espaces. Voici un exemple de commande personnelle::
"C:\Program Files\SuperDiffProg\SuperDiffProg.exe" "%d" "%r"

View File

@ -22,4 +22,4 @@ criterion is used and so on and so on. For example, if your arguments are "Size
"Filename (Doesn't end with a number)", the reference file that will be picked in a group will be
the biggest file, and if two or more files have the same size, the one that has a filename that
doesn't end with a number will be used. When all criteria result in ties, the order in which dupes
previously were in the group will be used.
previously were in the group will be used.

View File

@ -131,7 +131,7 @@ s'exécute. La plupart du temps, ces options n'ont pas a être activées.
liens (`symlink`_ ou `hardlink`_) vers leur fichiers de référence respectifs. Un symlink est un
lien symbolique (qui devient caduque si l'original est supprimé) et un hardlink est un lien direct
au contenu du fichier (même si l'original est supprimé, le lien reste valide).
Sur OS X et Linux, cette fonction est supportée pleinement, mais sur Windows, c'est un peu
compliqué. Windows XP ne le supporte pas, mais Vista oui. De plus, cette fonction ne peut être
utilisée que si dupeGuru roule avec les privilèges administratifs. Ouaip, Windows c'est la joie.

View File

@ -25,7 +25,7 @@
.. topic:: Ո՞րոն եք dupeGuru-ի լիցենզիայի սահմանափակումները:
Փորձնական եղանակում, Դուք կարող եք միայն կատարել գործողություններ 10 կրկնօրինակների հետ միաժամանակ: Ծրագրի
Փորձնական եղանակում, Դուք կարող եք միայն կատարել գործողություններ 10 կրկնօրինակների հետ միաժամանակ: Ծրագրի
`Անվճար տարբերակում <http://open.hardcoded.net/about/>`_ mode, այնուհանդերձ չկան էական սահմանափակումներ:
.. topic::Ջնջելու համար նշելու դաշտի պատուհանը ակտիվ չէ: Ի՞նչ անել:
@ -72,7 +72,7 @@
* Սեղմեք **Նշել --> Նշել բոլորը**:
.. only:: edition_me
.. topic:: Ես ցանկանում եմ հեռացնել բոլոր երգերը, որոնք 3 վայրկյանից հեռու են իրենց հղման ֆայլից: Ի՞նչ կարող եմ ես անել:
* Միացնել :doc:`Միայն Սխալները <results>` եղանակում:
@ -84,7 +84,7 @@
* Սեղմեք **Ջնջել ընտրվածը արդյունքներից**:
.. topic:: Ես ցանկանում եմ դարձնել իմ բարձրագույն բիթրեյթ ունեցող երգերը հղման ֆայլեր: Ի՞նչ կարող եմ ես անել:
* Միացնել :doc:`Միայն Սխալները <results>` եղանակում:
* Միացնել **Դելտա նշանակությունները** եղանակը:
* Սեղմեք "Բիթրեյթը" սյանը՝ դասավորելու համար արդյունքները ըստ բիթրեյթի:
@ -93,12 +93,12 @@
* Սեղմեք **Դարձնել ընտրվածը հղում**:
.. topic:: Ես չեմ ցանկանում [live] և [remix] տարբերակները իմ երգերի՝ հաշված որպես կրկնօրինակ: Ինչպե՞ս դա անել:
Եթե Ձեր համեմատության սահմանը բավականին ցածր է, հնարավոր է Դուք ավարտվեք կենդանի և ռեմիքս տարբերակներով Ձեր երգերի արդյունեքներում: Դուք ոչինչ չեք կարող անել դրա համար, բայց կա ինչ-որ եղանակ՝ դրանք ստուգման արդյունքներից ջնջելու համար: Եթե օրինակի համար, Դուք ցանկանում եք ջնջել ամեն մի երգ, որը գտնվում է գծիկների միջև []:.
Եթե Ձեր համեմատության սահմանը բավականին ցածր է, հնարավոր է Դուք ավարտվեք կենդանի և ռեմիքս տարբերակներով Ձեր երգերի արդյունեքներում: Դուք ոչինչ չեք կարող անել դրա համար, բայց կա ինչ-որ եղանակ՝ դրանք ստուգման արդյունքներից ջնջելու համար: Եթե օրինակի համար, Դուք ցանկանում եք ջնջել ամեն մի երգ, որը գտնվում է գծիկների միջև []:.
* **Windows**. Սեղմեք **Գործողություններ --> Կիրառել ֆիլտրը**, ապա տեսակը "[*]", ապա սեղմեք ԼԱՎ:
* **Mac OS X**. Տեսակը "[*]" "Ֆիլտր" դաշտում՝ գործիքաշերտի:
* Սեղմեք **Նշել --> Նշել բոլորը**:
* Սեղմեք **Գործողություններ --> Ջնջել ընտրվածը արդյունքներից**.
* Սեղմեք **Գործողություններ --> Ջնջել ընտրվածը արդյունքներից**.
.. topic:: Ես փորձում եմ կրկնօրինակները ուղարկել Աղբարկղ, բայց dupeGuru-ն ինձ ասում է, որ չես կարող: Ինչու՞: Ի՞նչ կարող եմ ես անել:
@ -112,4 +112,4 @@
Եթե այս ամենը ձախողվի, `կապնվեք HS աջակցության թիմի հետ <http://www.hardcoded.net/support>`_, մենք կփորձեք օգնել Ձեզ:
.. todo:: This FAQ qestion is outdated, see english version.
.. todo:: This FAQ qestion is outdated, see english version.

View File

@ -21,7 +21,7 @@
.. toctree::
:maxdepth: 2
quick_start
folders
preferences

View File

@ -4,9 +4,9 @@
.. only:: edition_se
**Ստուգելու տեսակը.** Այս ընտրանքը որոշում է, թե ֆայլերի որ ասպեկտը կհամեմատվի կրկնօրինակված ստուգման հետ: Եթե Դուք ընտրեք **Ֆայլի անունը**, ապա dupeGuru-ն կհամեմատի յուրաքանչյուրը բառ-առ-բառ և կախված է հետևյալ այլ ընտրանքներից, այն կորոշի արդյոք բավական են համընկնող բառերը դիտելու համար 2 ֆայլերի կրկնօրինակները: Եթե ընտրեք միայն **Բովանդակությունը**, ապա նույնատիպ ֆայլերը նույն բովանդակությամբ կհամընկնեն:
**Թղթապանակներ.** ստուգելու հատուկ տեսակ է: Երբ ընտրեք սա, dupeGuru-ն կստուգի կրկնօրինակ *թղթապանակները*՝ կրկնօրինակ ֆայլերի փոխարեն: Որոշելու համար արդյոք անկախ երկու թղթապանակները կրկնօրինակ են, կստուգվեն թղթապանակների ամբողջ պարունակությունը և եթե **բոլոր** ֆայլերի բովանդակությունը համընկնի, ապա թղթապանակները կորոշվեն որպես կրկնօրինակներ:
**Ֆիլտրի խստությունը.** Եթե Դուք ընտրեք **Ֆայլի անունը** ստուգելու տեսակը, այս ընտրանքը կորոշի, թե ինչքանով նման պետք է լինեն ֆայլերի անունները, որ dupeGuru-ն ճանաչի դրանք որպես կրկնօրինակներ: Եթե ֆիլտրը առավել խիստ է, օրինակ՝ 80, ապա դա նշանակում է, որ երկու ֆայլերի անունների բառերի 80%-ը պետք է համընկնի: Որոշելու համար համընկնման տոկոսը, dupeGuru-ն նախ հաշվում է բառերի ընդհանուր քիանակը **երկու** ֆայլերի անուններում, ապա հաշվում է համընկնումների քանակը (ամեն բառ համընկնում է 2-ի հաշվին) և բաժանում ընդհանուր գտնված բառերի համընկնումների միջև: Եթե արդյունքը բարձր է կամ հավասար ֆիլտրի խստությանը, ապա մենք ունեք կրկնօրինակի համընկնում: Օրինակ՝ "a b c d" և "c d e" ունեն համընկնման տոկոս, որը հավասար է 57-ի (4 բառ են համընկնում, 7 ընդհանուր բառից):
.. only:: edition_me
@ -33,7 +33,7 @@
.. only:: edition_pe
**Ստուգելու եղանակը.** Այս ընտրանքը որոշում է ստուգելու եղանակը, որը կկիրառվի նկարների նկատմամբ: **Պարունակությունը** ստուգելու եղանակը համեմատում է ակտուալ նկարների բովանդակությունը ոչ ճշգրիտ եղանակով (հնարավորություն տալով գտնելու ոչ միայն անմիջապես կրկնօրինակները, այլ նաև նմանատիպ այլ ֆայլերը): **EXIF Timestamp** ստուգելու եղանակը նայում է նկարի EXIF մետատվյալը (եթե այն կա) և համընկնող նկարները, որոնք որ նույնն են: Սա ավելի արագ է, քան բովանդակությամբ ստուգելը: **Զգուշացում.** Փոփոխված նկարները սովորաբար պահում են նույն EXIF timestamp-ը, ուստի նախ նայեք արդյունքները, ապա գործեք:
**Ֆիլտրի խստությունը.** *Ստուգում է միայն բովանդակությունը:* Այս ընտարնքի բարձրագույն նիշը, բնորոշում է ֆիլտրի "խստությունը" (Այլ կերպ ասաց, արդյունքը ավելի քիչ է լինում): Նույն որակի նկարներից շատերը երբեմն համընկնում են 100%-ով՝ անգամ եթե տեսակը ուրիշ է (PNG և JPG օրինակի համար): Այնուհանդերձ, եթե ցանկանում եք, որ PNG-ն համապատասխանի ցածր որակի JPG-ին, պետք է նշեք ֆիլտրի խստությունը 100-ից ցածր: Ծրագրայինը 95 է:
**Տարբեր չափերով նկարների համապատասխանեցում.** Եթե ընտրեք սա, տարբեր չափերի նկարները կթույլատրվեն կրկնօրինակվող նույն խմբում:
@ -57,7 +57,7 @@
**Ընտրված հրամանը.** Այս կարգավորումը որոշում է հրամանը, որը կկանչվի "Կանչել Ընտրված հրամանը" գործողությամբ: Կարող եք կանչել ցանկացած արտաքին ծրագիր՝ այս գործողությամբ: Սա կարող է օգտակար լինել եթե օրինակ փոխարենը ունեք տվյալների փոխանցման լավ ծրագիր:
Հրամանի տեսակը նույնն է, ինչ Դուք կգրեք Հրամանի տողում, բացառությամբ որտեղ կան 2 լրացումներ. **%d** և **%r**: Այս լրացումները կվերագրվեն ընտրված զոհի (%d) ճանապարհով և ընտրված զոհի հղման ֆայլով (%r):
Եթե կատարելի ֆայլի ճանապարհը պարունակում է բացատներ, ապա պետք է փակեք այն "" չակերտներով: Նաև պետք է փակեք լրացումները չակերտներով, որովհետև շատ հնարավոր է, որ զոհի ճանապարհները և հղումները կպարունակեն բացատներ: Ահա ընտրված հրամանի օրինակ՝ ::
"C:\Program Files\SuperDiffProg\SuperDiffProg.exe" "%d" "%r"

View File

@ -12,4 +12,3 @@
* Եթե համոզված եք, որ կրկնօրինակը արդյունքներում կա, ապա սեղմեք **Խմբագրել-->Նշել բոլորը**, և ապա **Գործողություններ-->Ուղարկել Նշվածը Աղբարկղ**:
Սա միայն բազային ստուգում է: Կան բազմաթիվ կարգավորումներ, որոնք հնարավորություն են տալիս նշելու տարբեր արդյունքներ և մի քանի եղանակներ արդյունքների փոփոխման: Մանրամասների համար կարդացեք Օգնության ֆայլը:

View File

@ -1,7 +1,7 @@
Վերաառաջնայնության կրկնօրինակներ
================================
dupeGuru-ը փորձում է որոշել, թե որ կրկնօրինակները պետք է գնան յուրաքանչյուր խմբի դիրքում,
dupeGuru-ը փորձում է որոշել, թե որ կրկնօրինակները պետք է գնան յուրաքանչյուր խմբի դիրքում,
բայց երբեմն սխալ է ստանում: Շատ դեպքերում, խելամիտ դասավորումը "Դելտա նշանակության"
և "Միայն սխալները" ընտրանքների ավելացնելով "Դարձնել ընտրվածը հղում" գործողության խորամանկություն է, բայց
երբեմն, պահանջվում են ավելի լավ ընտրանքներ: Ահա այստեղ է, որ վերաառաջնայնավորման պատուհանը բացվում է:
@ -23,4 +23,3 @@ dupeGuru-ը փորձում է որոշել, թե որ կրկնօրինակներ
մեծագույն ֆայլը և եթե երկու կամ ավելի ֆայլեր ունեն նույն չափը, ապա մեկը ունի ֆայլի անուն, որը
չի ավարտվում թվով, կօգտագործվի: Երբ փաստարկի արդյունքը կապված է, կարգը, որի սխալները
նախկինում էին, խումբը պետք է օգտագործվի:

View File

@ -98,4 +98,4 @@ dupeGuru-ն աջակցում է հետստուգման ֆիլտրում։ Սրա
* **Անվանափոխել ընտրվածը.** Ձեզ հարցում կկատարվի նոր անվան համար, ապա ընտրված ֆայլը կանվանափոխվի։
.. todo:: Add Move and iPhoto/iTunes warning
.. todo:: Add "Deletion Options" section.
.. todo:: Add "Deletion Options" section.

View File

@ -72,7 +72,7 @@
* Нажмите на **Отметить -> Отметить все**.
.. only:: edition_me
.. topic:: Я хочу, чтобы удалить все песни, которые более чем на 3 секунды от своей ссылке на файл. Что я могу сделать?
* Включить: документ: `обманутые Только <результаты>` режим.
@ -84,7 +84,7 @@
* Нажмите на **Удалить выбранные из результатов**.
.. topic:: Я хочу, чтобы мой высокий битрейт файлов песни ссылки. Что я могу сделать?
* Включить: документ: `обманутые Только <результаты>` режиме
* Включить **Значения делта** режим.
* Нажмите на кнопку "Битрейт" колонку для сортировки результатов по битрейт.
@ -93,9 +93,9 @@
* Нажмите на **Сделать выбранной ссылки**.
.. topic:: Я не хочу [жить] и [ремикс] версии моих песен считаться дубликатами. Как мне это сделать?
Если ваше сравнение порог достаточно низким, вы, вероятно, в конечном итоге с живой и ремикс версии ваших песен в своих результатах. Там вы ничего не можете сделать, чтобы предотвратить это, но есть кое-что можно сделать, чтобы легко удалить их со своего результаты после сканирования: после сканирования, фильтрации. Если, например, вы хотите удалить все песни с чем-либо в квадратных скобках []:
* **Windows**: Нажмите на **Действия -> Применить фильтр**, а затем введите "[*]", нажмите кнопку ОК.
* **Mac OS X**: Тип "[*]" в "Фильтр" поле на панели инструментов.
* Нажмите на Отметить **-> Отметить все**.

View File

@ -18,7 +18,7 @@
.. toctree::
:maxdepth: 2
quick_start
folders
preferences

View File

@ -4,9 +4,9 @@
.. only:: edition_se
**Тип сканирования:** Этот параметр определяет, какой аспект файлы будут сравниваться в дубликат сканирования. Если выбрать **Имя файла**, dupeGuru будем сравнивать каждое имена файлов слово за слово, и, в зависимости от других параметров ниже, он будет определять, достаточно ли слов соответствие рассмотреть 2 файлов дубликатов. Если выбрать **Содержимое**, только файлы с точно такой же контент будет матч.
**Папки** типа сканирования немного особенным. Когда вы выбираете его, dupeGuru проведет поиск дубликатов *папки* вместо того, чтобы дубликатов файлов. Для определения того, две папки, дублируют друг друга, все файлы, содержащиеся в папках будут проверяться, и если содержание **все** файлы в матче папки, папки будут считаться дубликатами.
**Фильтра Твердость:** Если вы выбрали **Имя файла** типа сканирования, эта опция определяет, как похожи два имени должно быть для dupeGuru рассматривать их дубликатов. Если фильтр твердости, например 80, то это означает, что 80% слов из двух имен файлов должны совпадать. Для определения соответствия процент, dupeGuru первой подсчитывает общее количество слов в **обоих** файла, то подсчитать количество слов соответствия (каждое слово соответствия считаются 2), а затем разделите количество слов соответствия на общее число слов. Если результат больше или равно фильтр твердость, у нас есть дубликаты матча. Например, "ABCD" и "CDE" имеют соответствующий процент 57 (4 слова соответствия, 7 всего слов).
.. only:: edition_me
@ -35,7 +35,7 @@
**Тип сканирования:** Этот параметр определяет тип сканирования, которые будут сделаны на ваши картины.**Сканирования** Содержание типа сравнивает фактическое содержание фотографий нечеткие пути (что делает его можно найти не только точными копиями, но и подобные).**EXIF Timestamp** тип сканирования смотрит на метаданные EXIF с фото (если он существует) и соответствует фотографии, которые имеют такой же. Это намного быстрее, чем сканирование содержимого. **Внимание:** Измененные фотографии часто держат же метка EXIF, так что следите за ложных срабатываний, когда вы используете, что тип сканирования.
**Фильтра Твердость:** *Содержание тип сканирования только.* Чем больше этот параметр, "тяжелее" является фильтром (Другими словами, тем меньше результатов Вы получите). Большинство фотографий одного и того же матча качества на 100%, даже если формат отличается (PNG и JPG, например.). Однако, если вы хотите, чтобы соответствовать PNG с более низким качеством JPG, вам придется установить фильтром твердость ниже, чем 100.По умолчанию, 95, это сладкое место.
**Совпадения рисунки разных размеров:** Если вы установите этот флажок, фотографии разных размеров будет разрешен в том же дубликат группы.
**Можно смешивать файл вида:** Если вы установите этот флажок, дублировать группам разрешается есть файлы с различными расширениями. Если вы не проверить его, ну, они не являются!
@ -57,7 +57,7 @@
**Специальной команды:** Это предпочтение определяет команду, которая будет вызываться "Вызов специальной команды" действия. Вы можете ссылаться ни на какие внешние приложения через это действие. Это может быть полезно, если, например, у вас есть хорошее приложение сравниваете установлены.
Формат команды такой же, как то, что вы должны написать в командной строке, за исключением того, что Есть 2 заполнителей: **%d** and **%r**. Эти заполнители будут заменены на путь выбран обманут (%d) и путь к ссылке на файл выбранного обмануть (%r).
Если путь к исполняемому содержит пробелы, необходимо заключить его в "" кавычки. Вы также должны приложить заполнителей в кавычки, потому что это очень возможно, что путь к обманутых и ссылки будут содержать пробелы. Вот пример пользовательской команды:
"C:\Program Files\SuperDiffProg\SuperDiffProg.exe" "%d" "%r"

View File

@ -11,4 +11,4 @@
* Если файл ложных дубликатов, выделите ее и нажмите **Действия -> Удалить выбранные из результатов**.
* Если вы уверены, что нет ложных дубликатов в результатах, нажмите на **Изменить -> Отметить Все**, а затем **Действия -> Отправить Помечено в Корзину**.
Это только основные сканирования. Есть много настройки вы можете сделать, чтобы получить разные результаты и несколько методов изучения и изменения ваших результатов. Чтобы узнать о них, только что прочитал остальную часть этого файла справки.
Это только основные сканирования. Есть много настройки вы можете сделать, чтобы получить разные результаты и несколько методов изучения и изменения ваших результатов. Чтобы узнать о них, только что прочитал остальную часть этого файла справки.

View File

@ -22,4 +22,4 @@ subargument в приведенном ниже списке, а затем на
"Имя файла (Не оканчивается на номер)", ссылке на файл, который будет выбран в группе будет
крупнейших файл, а если два или несколько файлов имеют одинаковый размер, который имеет имя файла с
не заканчивается номер будет использоваться. Когда все критерии привести к связи, порядок, в котором обманутые
ранее были в группе будет использоваться.
ранее были в группе будет использоваться.

View File

@ -98,4 +98,4 @@ dupeGuru поддерживает после сканирования, филь
* **Переименования выбрано:** Запрашивает новое имя, а затем переименовать выбранный файл.
.. todo:: Add Move and iPhoto/iTunes warning
.. todo:: Add "Deletion Options" section.
.. todo:: Add "Deletion Options" section.

View File

@ -72,7 +72,7 @@
* Натисніть на Марка **-> Позначити всі**.
.. only:: edition_me
.. topic:: Я хочу, щоб видалити всі пісні, які більш ніж на 3 секунди від своєї посиланням на файл. Що я можу зробити?
* Включити :doc:`ошукані Тільки <results>` режимі.
@ -84,7 +84,7 @@
* Натисніть на **Видалити вибрані з результатів**.
.. topic:: Я хочу, щоб мій високий бітрейт файлів пісні посилання. Що я можу зробити?
* Включити :doc:`ошукані Тільки <results>` режимі.
* Включити **Значення Delta** режимі.
* Натисніть на "Бітрейт" колонку для сортування результатів по бітрейт.
@ -93,9 +93,9 @@
* Натисніть на **Зробити вибраної посилання**.
.. topic:: Я не хочу [жити] і [ремікс] версії моїх пісень вважатися дублікатами. Як мені це зробити?
Якщо ваше порівняння поріг досить низьким, ви, ймовірно, в кінцевому підсумку з живою і ремікс версії ваших пісень у своїх результатах. Там ви нічого не можете зробити, щоб запобігти цьому, але є дещо можна зробити, щоб легко видалити їх зі свого результати після сканування: після сканування, фільтрації. Якщо, наприклад, ви хочете видалити всі пісні з чим-небудь у квадратних дужках []:
* **Windows**: Натисніть на **Дії -> Застосувати фільтр**, а потім введіть "[*]", натисніть кнопку ОК.
* **Mac OS X**: Тип "[*]" в "Фільтр" поле на панелі інструментів.
* Натисніть на Марка **-> Позначити всі**.
@ -114,4 +114,3 @@
Якщо все це не так, `контакт УГ підтримки <http://www.hardcoded.net/support>`_, ми зрозуміти це.
.. todo:: This FAQ qestion is outdated, see english version.

View File

@ -13,7 +13,7 @@
dupeGuru Picture Edition (PE для стислості) являє собою інструмент для пошуку дублікатів фотографій на вашому комп'ютері. Не тільки він може знайти точні відповідності, але він також може знайти дублікати серед фотографій різного роду (PNG, JPG, GIF і т.д..) І якість.
Хоча dupeGuru може бути легко використана без документації, читання цього файлу допоможе вам освоїти його. Якщо ви шукаєте керівництво для вашої першої дублювати сканування, ви можете поглянути на: :doc:`Quick Start <quick_start>`
Хоча dupeGuru може бути легко використана без документації, читання цього файлу допоможе вам освоїти його. Якщо ви шукаєте керівництво для вашої першої дублювати сканування, ви можете поглянути на: :doc:`Quick Start <quick_start>`
Це гарна ідея, щоб зберегти dupeGuru оновлено. Ви можете завантажити останню версію на своєму http://dupeguru.voltaicideas.net.
@ -21,7 +21,7 @@ Contents:
.. toctree::
:maxdepth: 2
quick_start
folders
preferences

View File

@ -4,9 +4,9 @@
.. only:: edition_se
**Тип сканування:** Цей параметр визначає, який аспект файли будуть порівнюватися в дублікат сканування. Якщо вибрати **Файл** , dupeGuru будемо порівнювати кожне імена файлів слово за слово, і, залежно від інших параметрів нижче, він буде визначати, чи достатньо слів відповідність розглянути 2 файлів дублікатів. Якщо вибрати **Вміст**, тільки файли з точно такою ж контент буде матч.
**Папки** типу сканування трохи особливим. Коли ви обираєте його, dupeGuru проведе пошук дублікатів *папки* замість того, щоб дублікатів файлів. Для визначення того, дві папки, дублюють один одного, всі файли, що містяться в папках будуть перевірятися, і якщо вміст **всі** файли в матчі папки, папки будуть вважатися дублікатами.
**Фільтра Твердість:** Якщо ви вибрали **Папки** Файл типу сканування, ця опція визначає, як схожі два імені повинно бути для dupeGuru розглядати їх дублікатів. Якщо фільтр твердості, наприклад 80, то це означає, що 80% слів з ​​двох імен файлів повинні збігатися. Для визначення відповідності відсоток, dupeGuru перший підраховує загальну кількість слів в **обох** файлу, то підрахувати кількість слів відповідності (кожне слово відповідності вважаються 2), а потім розділіть кількість слів відповідності на загальне число слів. Якщо результат більше або дорівнює фільтр твердість, у нас є дублікати матчу. Наприклад, "ABCD" і "CDE" мають відповідний відсоток 57 (4 слова відповідності, 7 всього слів).
.. only:: edition_me
@ -33,7 +33,7 @@
.. only:: edition_pe
**Тип сканування:** Цей параметр визначає тип сканування, які будуть зроблені на ваші картини. **Сканування** Зміст типу порівнює фактичний зміст фотографій нечіткі шляху (що робить його можна знайти не тільки точними копіями, але і подібні). **EXIF Timestamp** тип сканування дивиться на метадані EXIF з фото (якщо він існує) і відповідає фотографії, які мають такий же. Це набагато швидше, ніж сканування вмісту. **Увага:** Змінені фотографії часто тримають ж мітка EXIF, так що слідкуйте за помилкових спрацьовувань, коли ви використовуєте, що тип сканування.
**Фільтра Твердість:** *Вміст тип сканування тільки*. Чим більше цей параметр, "важче" є фільтром (Іншими словами, тим менше результатів Ви отримаєте). Більшість фотографій одного й того ж матчу якості на 100%, навіть якщо формат відрізняється (PNG і JPG, наприклад.). Однак, якщо ви хочете, щоб відповідати PNG з більш низькою якістю JPG, вам доведеться встановити фільтром твердість нижче, ніж 100. За замовчуванням, 95, це солодке місце.
**Матч малюнки різних розмірів:** Якщо ви встановите цей прапорець, фотографії різних розмірів буде дозволений в тому ж дублікат групи.
@ -60,5 +60,5 @@
Формат команди такий же, як те, що ви повинні написати в командному рядку, за винятком того, що Є 2 заповнювачів: **%d** and **%r**. Ці наповнювачі будуть замінені на шлях вибраний обдурити (% г) і шлях до заслання на файл вибраного обдурити (%r).
Якщо шлях до виконуваного містить прогалини, необхідно укласти його в "" лапки. Ви також повинні докласти заповнювачів в лапки, бо це дуже можливо, що шлях до обдурених і посилання будуть містити пробіли. Ось приклад користувальницької команди::
"C:\Program Files\SuperDiffProg\SuperDiffProg.exe" "%d" "%r"

View File

@ -11,4 +11,4 @@
* Якщо файл помилкових дублікатів, виділіть її та натисніть **Дії -> Видалити вибрані з результатів**.
* Якщо ви впевнені, що немає помилкових дублікатів в результатах, натисніть на **Редагувати -> Позначити Всі**, а потім **Дії -> Отправить Позначено до кошику**.
Це тільки основні сканування. Є багато налаштування ви можете зробити, щоб отримати різні результати і кілька методів вивчення та зміни ваших результатів. Щоб дізнатися про них, щойно прочитав решту цього файлу довідки.
Це тільки основні сканування. Є багато налаштування ви можете зробити, щоб отримати різні результати і кілька методів вивчення та зміни ваших результатів. Щоб дізнатися про них, щойно прочитав решту цього файлу довідки.

View File

@ -22,4 +22,4 @@ subargument в наведеному нижче списку, а потім на
"Файл (Не закінчується на номер)", заслання на файл, який буде обраний у групі буде
найбільших файл, а якщо два або декілька файлів мають однаковий розмір, який має ім'я файлу з
не закінчується номер буде використовуватися. Коли всі критерії привести до зв'язку, порядок, в якому ошукані
раніше були в групі буде використовуватися.
раніше були в групі буде використовуватися.

View File

@ -98,4 +98,4 @@ dupeGuru підтримує після сканування, фільтраці
* **Перейменування обрано:** Запит нове ім'я, а потім перейменувати вибраний файл.
.. todo:: Add Move and iPhoto/iTunes warning
.. todo:: Add "Deletion Options" section.
.. todo:: Add "Deletion Options" section.

View File

@ -7,4 +7,4 @@ Redistribution and use in source and binary forms, with or without modification,
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -14,6 +14,7 @@ import re
import os
import shutil
from errno import EISDIR, EACCES
from pathlib import Path
from typing import Callable, List
@ -75,10 +76,8 @@ def smart_copy(source_path: Path, dest_path: Path) -> None:
try:
_smart_move_or_copy(shutil.copy, source_path, dest_path)
except OSError as e:
if e.errno in {
21,
13,
}: # it's a directory, code is 21 on OS X / Linux and 13 on Windows
# It's a directory, code is 21 on OS X / Linux (EISDIR) and 13 on Windows (EACCES)
if e.errno in (EISDIR, EACCES):
_smart_move_or_copy(shutil.copytree, source_path, dest_path)
else:
raise

View File

@ -7,7 +7,7 @@
# http://www.gnu.org/licenses/gpl-3.0.html
from typing import Any, Callable, Generator, Iterator, List, Union
from typing import Any, Callable, Generator, List, Union
class JobCancelled(Exception):
@ -148,7 +148,7 @@ class Job:
self._do_update(desc)
class NullJob:
class NullJob(Job):
def __init__(self, *args, **kwargs) -> None:
# Null job does nothing
pass
@ -161,9 +161,6 @@ class NullJob:
# Null job does nothing
pass
def iter_with_progress(self, sequence, *args, **kwargs) -> Iterator:
return iter(sequence)
def start_job(self, *args, **kwargs) -> None:
# Null job does nothing
pass

View File

@ -15,7 +15,8 @@
#
import os
import imp
import importlib.machinery
import importlib.util
import sys
import glob
import token
@ -110,7 +111,7 @@ def _visit_pyfiles(list, dirname, names):
# get extension for python source files
if "_py_ext" not in globals():
global _py_ext
_py_ext = [triple[0] for triple in imp.get_suffixes() if triple[2] == imp.PY_SOURCE][0]
_py_ext = importlib.machinery.SOURCE_SUFFIXES[0]
# don't recurse into CVS directories
if "CVS" in names:
@ -120,45 +121,6 @@ def _visit_pyfiles(list, dirname, names):
list.extend([os.path.join(dirname, file) for file in names if os.path.splitext(file)[1] == _py_ext])
def _get_modpkg_path(dotted_name, pathlist=None):
"""Get the filesystem path for a module or a package.
Return the file system path to a file for a module, and to a directory for
a package. Return None if the name is not found, or is a builtin or
extension module.
"""
# split off top-most name
parts = dotted_name.split(".", 1)
if len(parts) > 1:
# we have a dotted path, import top-level package
try:
file, pathname, description = imp.find_module(parts[0], pathlist)
if file:
file.close()
except ImportError:
return None
# check if it's indeed a package
if description[2] == imp.PKG_DIRECTORY:
# recursively handle the remaining name parts
pathname = _get_modpkg_path(parts[1], [pathname])
else:
pathname = None
else:
# plain name
try:
file, pathname, description = imp.find_module(dotted_name, pathlist)
if file:
file.close()
if description[2] not in [imp.PY_SOURCE, imp.PKG_DIRECTORY]:
pathname = None
except ImportError:
pathname = None
return pathname
def getFilesForName(name):
"""Get a list of module files for a filename, a module or package name,
or a directory.
@ -173,7 +135,11 @@ def getFilesForName(name):
return file_list
# try to find module or package
name = _get_modpkg_path(name)
try:
spec = importlib.util.find_spec(name)
name = spec.origin
except ImportError:
name = None
if not name:
return []

View File

@ -41,7 +41,8 @@ def trget(domain: str) -> Callable[[str], str]:
def set_tr(
new_tr: Callable[[str, Union[str, None]], str], new_trget: Union[Callable[[str], Callable[[str], str]], None] = None
new_tr: Callable[[str, Union[str, None]], str],
new_trget: Union[Callable[[str], Callable[[str], str]], None] = None,
) -> None:
global _trfunc, _trget
_trfunc = new_tr

View File

@ -1,7 +1,10 @@
# Translators:
# Andrew Senetar <arsenetar@gmail.com>, 2022
#
msgid ""
msgstr ""
"Language-Team: Arabic (https://www.transifex.com/voltaicideas/teams/116153/ar/)\n"
"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2022\n"
"Language-Team: Arabic (https://app.transifex.com/voltaicideas/teams/116153/ar/)\n"
"Language: ar\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: utf-8\n"
@ -10,110 +13,110 @@ msgstr ""
#: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20
#: core\gui\problem_table.py:18
msgid "File Path"
msgstr ""
msgstr "مسار الملف"
#: core\gui\problem_table.py:19
msgid "Error Message"
msgstr ""
msgstr "رسالة خطأ"
#: core\me\prioritize.py:23
msgid "Duration"
msgstr ""
msgstr "مدة"
#: core\me\prioritize.py:30 core\me\result_table.py:23
msgid "Bitrate"
msgstr ""
msgstr "معدل البت"
#: core\me\prioritize.py:37
msgid "Samplerate"
msgstr ""
msgstr "معدل العينة"
#: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92
#: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:94
#: core\se\result_table.py:19
msgid "Filename"
msgstr ""
msgstr "اسم الملف"
#: 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 ""
msgstr "مجلد"
#: core\me\result_table.py:21
msgid "Size (MB)"
msgstr ""
msgstr "الحجم (ميغا بايت)"
#: core\me\result_table.py:22
msgid "Time"
msgstr ""
msgstr "زمن"
#: core\me\result_table.py:24
msgid "Sample Rate"
msgstr ""
msgstr "معدل العينة"
#: 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 ""
msgstr "طيب القلب"
#: core\me\result_table.py:26 core\pe\result_table.py:25
#: core\prioritize.py:163 core\se\result_table.py:23
#: core\prioritize.py:165 core\se\result_table.py:23
msgid "Modification"
msgstr ""
msgstr "تعديل"
#: core\me\result_table.py:27
msgid "Title"
msgstr ""
msgstr "عنوان"
#: core\me\result_table.py:28
msgid "Artist"
msgstr ""
msgstr "فنان"
#: core\me\result_table.py:29
msgid "Album"
msgstr ""
msgstr "البوم"
#: core\me\result_table.py:30
msgid "Genre"
msgstr ""
msgstr "النوع"
#: core\me\result_table.py:31
msgid "Year"
msgstr ""
msgstr "سنة"
#: core\me\result_table.py:32
msgid "Track Number"
msgstr ""
msgstr "رقم الشاحنة"
#: core\me\result_table.py:33
msgid "Comment"
msgstr ""
msgstr "تعليق"
#: core\me\result_table.py:34 core\pe\result_table.py:26
#: core\se\result_table.py:24
msgid "Match %"
msgstr ""
msgstr "مباراة ٪"
#: core\me\result_table.py:35 core\se\result_table.py:25
msgid "Words Used"
msgstr ""
msgstr "الكلمات المستخدمة"
#: core\me\result_table.py:36 core\pe\result_table.py:27
#: core\se\result_table.py:26
msgid "Dupe Count"
msgstr ""
msgstr "عدد المخادعين"
#: core\pe\prioritize.py:23 core\pe\result_table.py:23
msgid "Dimensions"
msgstr ""
msgstr "أبعاد"
#: core\pe\result_table.py:21 core\se\result_table.py:21
msgid "Size (KB)"
msgstr ""
msgstr "الحجم (كيلو بايت)"
#: core\pe\result_table.py:24
msgid "EXIF Timestamp"
msgstr ""
msgstr "الطابع الزمني EXIF"
#: core\prioritize.py:156
#: core\prioritize.py:158
msgid "Size"
msgstr ""
msgstr "بحجم"

View File

@ -1,139 +1,150 @@
# Translators:
# Andrew Senetar <arsenetar@gmail.com>, 2022
#
msgid ""
msgstr ""
"Language-Team: Arabic (https://www.transifex.com/voltaicideas/teams/116153/ar/)\n"
"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2022\n"
"Language-Team: Arabic (https://app.transifex.com/voltaicideas/teams/116153/ar/)\n"
"Language: ar\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: utf-8\n"
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
#: core\app.py:42
#: core\app.py:44
msgid "There are no marked duplicates. Nothing has been done."
msgstr ""
#: core\app.py:43
#: core\app.py:45
msgid "There are no selected duplicates. Nothing has been done."
msgstr ""
#: core\app.py:44
#: core\app.py:46
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 ""
#: core\app.py:71
#: core\app.py:73
msgid "Scanning for duplicates"
msgstr ""
#: core\app.py:72
#: core\app.py:74
msgid "Loading"
msgstr ""
#: core\app.py:73
#: core\app.py:75
msgid "Moving"
msgstr ""
#: core\app.py:74
#: core\app.py:76
msgid "Copying"
msgstr ""
#: core\app.py:75
#: core\app.py:77
msgid "Sending to Trash"
msgstr ""
#: core\app.py:308
#: core\app.py:293
msgid ""
"A previous action is still hanging in there. You can't start a new one yet. "
"Wait a few seconds, then try again."
msgstr ""
#: core\app.py:318
#: core\app.py:304
msgid "No duplicates found."
msgstr ""
#: core\app.py:333
#: core\app.py:319
msgid "All marked files were copied successfully."
msgstr ""
#: core\app.py:334
#: core\app.py:321
msgid "All marked files were moved successfully."
msgstr ""
#: core\app.py:335
#: core\app.py:323
msgid "All marked files were deleted successfully."
msgstr ""
#: core\app.py:325
msgid "All marked files were successfully sent to Trash."
msgstr ""
#: core\app.py:343
#: core\app.py:330
msgid "Could not load file: {}"
msgstr ""
#: core\app.py:399
#: core\app.py:386
msgid "'{}' already is in the list."
msgstr ""
#: core\app.py:401
#: core\app.py:388
msgid "'{}' does not exist."
msgstr ""
#: core\app.py:410
#: core\app.py:396
msgid ""
"All selected %d matches are going to be ignored in all subsequent scans. "
"Continue?"
msgstr ""
#: core\app.py:486
#: core\app.py:473
msgid "Select a directory to copy marked files to"
msgstr ""
#: core\app.py:487
#: core\app.py:475
msgid "Select a directory to move marked files to"
msgstr ""
#: core\app.py:527
#: core\app.py:514
msgid "Select a destination for your exported CSV"
msgstr ""
#: core\app.py:534 core\app.py:801 core\app.py:811
#: core\app.py:520 core\app.py:781 core\app.py:791
msgid "Couldn't write to file: {}"
msgstr ""
#: core\app.py:559
#: core\app.py:543
msgid "You have no custom command set up. Set it up in your preferences."
msgstr ""
#: core\app.py:727 core\app.py:740
#: core\app.py:705 core\app.py:717
msgid "You are about to remove %d files from results. Continue?"
msgstr ""
#: core\app.py:774
#: core\app.py:753
msgid "{} duplicate groups were changed by the re-prioritization."
msgstr ""
#: core\app.py:821
#: core\app.py:801
msgid "The selected directories contain no scannable file."
msgstr ""
#: core\app.py:835
#: core\app.py:817
msgid "Collecting files to scan"
msgstr ""
#: core\app.py:891
#: core\app.py:867
msgid "%s (%d discarded)"
msgstr ""
#: core\engine.py:244 core\engine.py:288
msgid "0 matches found"
#: core\directories.py:190
msgid "Collected {} files to scan"
msgstr ""
#: core\engine.py:262 core\engine.py:296
msgid "%d matches found"
#: core\directories.py:206
msgid "Collected {} folders to scan"
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."
msgstr ""
#: core\gui\exclude_list_table.py:15
#: core\gui\exclude_list_table.py:14
msgid "Regular Expressions"
msgstr ""
@ -143,7 +154,7 @@ msgstr ""
#: core\me\scanner.py:20 core\se\scanner.py:16
msgid "Filename"
msgstr ""
msgstr "اسم الملف"
#: core\me\scanner.py:21
msgid "Filename - Fields"
@ -165,15 +176,15 @@ msgstr ""
msgid "Analyzed %d/%d pictures"
msgstr ""
#: core\pe\matchblock.py:181
#: core\pe\matchblock.py:177
msgid "Performed %d/%d chunk matches"
msgstr ""
#: core\pe\matchblock.py:191
#: core\pe\matchblock.py:185
msgid "Preparing for matching"
msgstr ""
#: core\pe\matchblock.py:244
#: core\pe\matchblock.py:234
msgid "Verified %d/%d matches"
msgstr ""
@ -183,61 +194,61 @@ msgstr ""
#: core\pe\scanner.py:22
msgid "EXIF Timestamp"
msgstr ""
msgstr "الطابع الزمني EXIF"
#: core\prioritize.py:70
msgid "None"
msgstr ""
#: core\prioritize.py:100
#: core\prioritize.py:102
msgid "Ends with number"
msgstr ""
#: core\prioritize.py:101
#: core\prioritize.py:103
msgid "Doesn't end with number"
msgstr ""
#: core\prioritize.py:102
#: core\prioritize.py:104
msgid "Longest"
msgstr ""
#: core\prioritize.py:103
#: core\prioritize.py:105
msgid "Shortest"
msgstr ""
#: core\prioritize.py:140
#: core\prioritize.py:142
msgid "Highest"
msgstr ""
#: core\prioritize.py:140
#: core\prioritize.py:142
msgid "Lowest"
msgstr ""
#: core\prioritize.py:169
#: core\prioritize.py:171
msgid "Newest"
msgstr ""
#: core\prioritize.py:169
#: core\prioritize.py:171
msgid "Oldest"
msgstr ""
#: core\results.py:142
#: core\results.py:134
msgid "%d / %d (%s / %s) duplicates marked."
msgstr ""
#: core\results.py:149
#: core\results.py:141
msgid " filter: %s"
msgstr ""
#: core\scanner.py:85
#: core\scanner.py:90
msgid "Read size of %d/%d files"
msgstr ""
#: core\scanner.py:109
#: core\scanner.py:116
msgid "Read metadata of %d/%d files"
msgstr ""
#: core\scanner.py:147
#: core\scanner.py:154
msgid "Almost done! Fiddling with results..."
msgstr ""

View File

@ -1,7 +1,10 @@
# Translators:
# Andrew Senetar <arsenetar@gmail.com>, 2022
#
msgid ""
msgstr ""
"Language-Team: Arabic (https://www.transifex.com/voltaicideas/teams/116153/ar/)\n"
"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2022\n"
"Language-Team: Arabic (https://app.transifex.com/voltaicideas/teams/116153/ar/)\n"
"Language: ar\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: utf-8\n"
@ -249,23 +252,23 @@ msgstr ""
#: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0
msgid "Artist"
msgstr ""
msgstr "فنان"
#: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0
msgid "Album"
msgstr ""
msgstr "البوم"
#: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0
msgid "Title"
msgstr ""
msgstr "عنوان"
#: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0
msgid "Genre"
msgstr ""
msgstr "النوع"
#: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0
msgid "Year"
msgstr ""
msgstr "سنة"
#: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30
#: cocoa/en.lproj/Localizable.strings:0
@ -908,3 +911,204 @@ msgstr ""
#: qt\preferences_dialog.py:286
msgid "Display"
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 ""
#: qt\preferences_dialog.py:241
msgid "Profile scan operation"
msgstr ""
#: qt\preferences_dialog.py:242
msgid "Profile the scan operation and save logs for optimization."
msgstr ""
#: qt\preferences_dialog.py:246
msgid "Logs located in: <a href=\"{}\">{}</a>"
msgstr ""
#: qt\preferences_dialog.py:291
msgid "Debug"
msgstr ""
#: qt\about_box.py:31
msgid "About {}"
msgstr ""
#: qt\about_box.py:47
msgid "Version {}"
msgstr ""
#: qt\about_box.py:49 qt\about_box.py:75
msgid "Checking for updates..."
msgstr ""
#: qt\about_box.py:54
msgid "Licensed under GPLv3"
msgstr ""
#: qt\about_box.py:68
msgid "No update available."
msgstr ""
#: qt\about_box.py:71
msgid "New version {} available, download <a href=\"{}\">here</a>."
msgstr ""
#: qt\error_report_dialog.py:50
msgid "Error Report"
msgstr ""
#: qt\error_report_dialog.py:54
msgid "Something went wrong. How about reporting the error?"
msgstr ""
#: qt\error_report_dialog.py:60
msgid ""
"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n"
"\n"
"Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n"
"\n"
"What usually really helps is if you add a description of how you got the error. Thanks!\n"
"\n"
"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application."
msgstr ""
#: qt\error_report_dialog.py:80
msgid "Go to Github"
msgstr ""
#: qt\preferences.py:24
msgid "Czech"
msgstr ""
#: qt\preferences.py:25
msgid "German"
msgstr ""
#: qt\preferences.py:26
msgid "Greek"
msgstr ""
#: qt\preferences.py:27
msgid "English"
msgstr ""
#: qt\preferences.py:28
msgid "Spanish"
msgstr ""
#: qt\preferences.py:29
msgid "French"
msgstr ""
#: qt\preferences.py:30
msgid "Armenian"
msgstr ""
#: qt\preferences.py:31
msgid "Italian"
msgstr ""
#: qt\preferences.py:32
msgid "Japanese"
msgstr ""
#: qt\preferences.py:33
msgid "Korean"
msgstr ""
#: qt\preferences.py:34
msgid "Malay"
msgstr ""
#: qt\preferences.py:35
msgid "Dutch"
msgstr ""
#: qt\preferences.py:36
msgid "Polish"
msgstr ""
#: qt\preferences.py:37
msgid "Brazilian"
msgstr ""
#: qt\preferences.py:38
msgid "Russian"
msgstr ""
#: qt\preferences.py:39
msgid "Turkish"
msgstr ""
#: qt\preferences.py:40
msgid "Ukrainian"
msgstr ""
#: qt\preferences.py:41
msgid "Vietnamese"
msgstr ""
#: qt\preferences.py:42
msgid "Chinese (Simplified)"
msgstr ""
#: qt\recent.py:54
msgid "Clear List"
msgstr ""
#: qt\search_edit.py:78
msgid "Search..."
msgstr ""
#: qt\preferences_dialog.py:219
msgid ""
"These options are for advanced users or for very specific situations, most "
"users should not have to modify these."
msgstr ""
#: qt\preferences_dialog.py:225
msgid "Include existence check after scan completion"
msgstr ""
#: qt\preferences_dialog.py:227
msgid "Ignore difference in mtime when loading cached digests"
msgstr ""

View File

@ -114,4 +114,3 @@ msgstr ""
#: core\prioritize.py:158
msgid "Size"
msgstr ""

View File

@ -36,91 +36,91 @@ msgstr ""
msgid "Sending to Trash"
msgstr ""
#: core\app.py:291
#: 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 ""
#: core\app.py:302
#: core\app.py:300
msgid "No duplicates found."
msgstr ""
#: core\app.py:317
#: core\app.py:315
msgid "All marked files were copied successfully."
msgstr ""
#: core\app.py:319
#: core\app.py:317
msgid "All marked files were moved successfully."
msgstr ""
#: core\app.py:321
#: core\app.py:319
msgid "All marked files were deleted successfully."
msgstr ""
#: core\app.py:323
#: core\app.py:321
msgid "All marked files were successfully sent to Trash."
msgstr ""
#: core\app.py:328
#: core\app.py:326
msgid "Could not load file: {}"
msgstr ""
#: core\app.py:384
#: core\app.py:382
msgid "'{}' already is in the list."
msgstr ""
#: core\app.py:386
#: core\app.py:384
msgid "'{}' does not exist."
msgstr ""
#: core\app.py:394
#: core\app.py:392
msgid "All selected %d matches are going to be ignored in all subsequent scans. Continue?"
msgstr ""
#: core\app.py:471
#: core\app.py:469
msgid "Select a directory to copy marked files to"
msgstr ""
#: core\app.py:473
#: core\app.py:471
msgid "Select a directory to move marked files to"
msgstr ""
#: core\app.py:512
#: core\app.py:510
msgid "Select a destination for your exported CSV"
msgstr ""
#: core\app.py:518 core\app.py:773 core\app.py:783
#: core\app.py:516 core\app.py:777 core\app.py:787
msgid "Couldn't write to file: {}"
msgstr ""
#: core\app.py:541
#: core\app.py:539
msgid "You have no custom command set up. Set it up in your preferences."
msgstr ""
#: core\app.py:697 core\app.py:709
#: core\app.py:701 core\app.py:713
msgid "You are about to remove %d files from results. Continue?"
msgstr ""
#: core\app.py:745
#: core\app.py:749
msgid "{} duplicate groups were changed by the re-prioritization."
msgstr ""
#: core\app.py:792
#: core\app.py:797
msgid "The selected directories contain no scannable file."
msgstr ""
#: core\app.py:808
#: core\app.py:813
msgid "Collecting files to scan"
msgstr ""
#: core\app.py:858
#: core\app.py:863
msgid "%s (%d discarded)"
msgstr ""
#: core\directories.py:190
#: core\directories.py:191
msgid "Collected {} files to scan"
msgstr ""
#: core\directories.py:206
#: core\directories.py:207
msgid "Collected {} folders to scan"
msgstr ""
@ -156,23 +156,23 @@ msgstr ""
msgid "Tags"
msgstr ""
#: core\me\scanner.py:24 core\pe\scanner.py:21 core\se\scanner.py:17
#: core\me\scanner.py:24 core\pe\scanner.py:22 core\se\scanner.py:17
msgid "Contents"
msgstr ""
#: core\pe\matchblock.py:72
#: core\pe\matchblock.py:66
msgid "Analyzed %d/%d pictures"
msgstr ""
#: core\pe\matchblock.py:177
#: core\pe\matchblock.py:183
msgid "Performed %d/%d chunk matches"
msgstr ""
#: core\pe\matchblock.py:185
#: core\pe\matchblock.py:191
msgid "Preparing for matching"
msgstr ""
#: core\pe\matchblock.py:234
#: core\pe\matchblock.py:240
msgid "Verified %d/%d matches"
msgstr ""
@ -180,7 +180,7 @@ msgstr ""
msgid "Read EXIF of %d/%d pictures"
msgstr ""
#: core\pe\scanner.py:22
#: core\pe\scanner.py:23
msgid "EXIF Timestamp"
msgstr ""
@ -220,27 +220,22 @@ msgstr ""
msgid "Oldest"
msgstr ""
#: core\results.py:134
#: core\results.py:135
msgid "%d / %d (%s / %s) duplicates marked."
msgstr ""
#: core\results.py:141
#: core\results.py:142
msgid " filter: %s"
msgstr ""
#: core\scanner.py:90
msgid "Read size of %d/%d files"
msgstr ""
#: core\scanner.py:116
#: core\scanner.py:114
msgid "Read metadata of %d/%d files"
msgstr ""
#: core\scanner.py:154
#: core\scanner.py:152
msgid "Almost done! Fiddling with results..."
msgstr ""
#: core\se\scanner.py:18
msgid "Folders"
msgstr ""

View File

@ -5,21 +5,21 @@
msgid ""
msgstr ""
"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\n"
"Language-Team: Czech (https://www.transifex.com/voltaicideas/teams/116153/cs/)\n"
"Language-Team: Czech (https://app.transifex.com/voltaicideas/teams/116153/cs/)\n"
"Language: cs\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: utf-8\n"
"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n"
#: core\app.py:42
#: core\app.py:44
msgid "There are no marked duplicates. Nothing has been done."
msgstr "Neexistují žádné označené duplikáty. Nic se nestalo."
#: core\app.py:43
#: core\app.py:45
msgid "There are no selected duplicates. Nothing has been done."
msgstr "Nejsou k dispozici žádné vybrané duplikáty. Nic se nestalo."
#: core\app.py:44
#: core\app.py:46
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?"
@ -27,23 +27,23 @@ msgstr ""
"Chystáte se otevřít více souborů najednou. V závislosti na tom, s čím jsou "
"tyto soubory otevřeny, to může způsobit docela nepořádek. Pokračovat?"
#: core\app.py:71
#: core\app.py:73
msgid "Scanning for duplicates"
msgstr "Vyhledávám duplicity"
#: core\app.py:72
#: core\app.py:74
msgid "Loading"
msgstr "Nahrávám"
#: core\app.py:73
#: core\app.py:75
msgid "Moving"
msgstr "Přesouvám"
#: core\app.py:74
#: core\app.py:76
msgid "Copying"
msgstr "Kopíruji"
#: core\app.py:75
#: core\app.py:77
msgid "Sending to Trash"
msgstr "Vyhazuji do koše"
@ -107,7 +107,7 @@ msgstr "Vyberte adresář, kam chcete přesunout označené soubory"
msgid "Select a destination for your exported CSV"
msgstr "Vyberte cíl pro exportovaný soubor CSV"
#: core\app.py:516 core\app.py:771 core\app.py:781
#: core\app.py:516 core\app.py:777 core\app.py:787
msgid "Couldn't write to file: {}"
msgstr "Nelze zapisovat do souboru: {}"
@ -116,23 +116,23 @@ msgid "You have no custom command set up. Set it up in your preferences."
msgstr ""
"Nedefinoval jste žádný uživatelský příkaz. Nadefinujete ho v předvolbách."
#: core\app.py:695 core\app.py:707
#: core\app.py:701 core\app.py:713
msgid "You are about to remove %d files from results. Continue?"
msgstr "Chystáte se z výsledků odstranit %d souborů. Pokračovat?"
#: core\app.py:743
#: core\app.py:749
msgid "{} duplicate groups were changed by the re-prioritization."
msgstr "{} duplicitní skupiny byly změněny změně priorit."
#: core\app.py:790
#: core\app.py:797
msgid "The selected directories contain no scannable file."
msgstr "Vybrané adresáře neobsahují žádné soubory vhodné k prohledávání."
#: core\app.py:803
#: core\app.py:813
msgid "Collecting files to scan"
msgstr "Shromažďuji prohlížené soubory"
#: core\app.py:850
#: core\app.py:863
msgid "%s (%d discarded)"
msgstr "%s (%d vyřazeno)"
@ -176,23 +176,23 @@ msgstr "Název souboru - pole (bez objednávky)"
msgid "Tags"
msgstr "Tagy"
#: core\me\scanner.py:24 core\pe\scanner.py:21 core\se\scanner.py:17
#: core\me\scanner.py:24 core\pe\scanner.py:22 core\se\scanner.py:17
msgid "Contents"
msgstr "Obsah"
#: core\pe\matchblock.py:72
#: core\pe\matchblock.py:66
msgid "Analyzed %d/%d pictures"
msgstr "Analyzováno %d/%d snímků"
#: core\pe\matchblock.py:177
#: core\pe\matchblock.py:183
msgid "Performed %d/%d chunk matches"
msgstr "Provedeno %d/%d porovnání bloků"
#: core\pe\matchblock.py:185
#: core\pe\matchblock.py:191
msgid "Preparing for matching"
msgstr "Připravuji porovnávání"
#: core\pe\matchblock.py:234
#: core\pe\matchblock.py:240
msgid "Verified %d/%d matches"
msgstr "Ověřeno %d/%d shod"
@ -200,7 +200,7 @@ msgstr "Ověřeno %d/%d shod"
msgid "Read EXIF of %d/%d pictures"
msgstr "Přečetl EXIF %d/%d obrázků"
#: core\pe\scanner.py:22
#: core\pe\scanner.py:23
msgid "EXIF Timestamp"
msgstr "Časové razítko EXIF"
@ -208,55 +208,51 @@ msgstr "Časové razítko EXIF"
msgid "None"
msgstr "Zádný"
#: core\prioritize.py:100
#: core\prioritize.py:102
msgid "Ends with number"
msgstr "Končí číslem"
#: core\prioritize.py:101
#: core\prioritize.py:103
msgid "Doesn't end with number"
msgstr "Nekončí číslem"
#: core\prioritize.py:102
#: core\prioritize.py:104
msgid "Longest"
msgstr "Nejdelší"
#: core\prioritize.py:103
#: core\prioritize.py:105
msgid "Shortest"
msgstr "Nejkratší"
#: core\prioritize.py:140
#: core\prioritize.py:142
msgid "Highest"
msgstr "Nejvyšší"
#: core\prioritize.py:140
#: core\prioritize.py:142
msgid "Lowest"
msgstr "Nejnižší"
#: core\prioritize.py:169
#: core\prioritize.py:171
msgid "Newest"
msgstr "Nejnovější"
#: core\prioritize.py:169
#: core\prioritize.py:171
msgid "Oldest"
msgstr "Nejstarší"
#: core\results.py:134
#: core\results.py:135
msgid "%d / %d (%s / %s) duplicates marked."
msgstr "%d / %d (%s / %s) duplicit označeno."
#: core\results.py:141
#: core\results.py:142
msgid " filter: %s"
msgstr " filtr: %s"
#: core\scanner.py:90
msgid "Read size of %d/%d files"
msgstr "Read size of %d/%d files"
#: core\scanner.py:116
#: core\scanner.py:114
msgid "Read metadata of %d/%d files"
msgstr "Načtena metadata %d/%d souborů"
#: core\scanner.py:154
#: core\scanner.py:152
msgid "Almost done! Fiddling with results..."
msgstr "Skoro hotovo! Fidlování s výsledky..."

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