mirror of
https://github.com/arsenetar/dupeguru.git
synced 2026-01-24 23:51:38 +00:00
Compare commits
79 Commits
5a4958cff9
...
4.2.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
c408873d20
|
|||
|
bbcdfbf698
|
|||
|
8cee1a9467
|
|||
|
448d33dcb6
|
|||
|
8d414cadac
|
|||
|
f902ee889a
|
|||
|
bc89e71935
|
|||
|
17b83c8001
|
|||
|
0f845ee67a
|
|||
|
d40e32a143
|
|||
|
1bc206e62d
|
|||
|
106a0feaba
|
|||
|
984e0c4094
|
|||
|
9321e811d7
|
|||
|
a64fcbfb5c
|
|||
|
cff07a12d6
|
|||
|
|
b9c7832c4a | ||
|
b9dfeac2f3
|
|||
| efc99eee96 | |||
|
|
ff7733bb73 | ||
|
4b2fbe87ea
|
|||
| 9e4b41feb5 | |||
|
cbfa8720f1
|
|||
|
a02c5e5b9b
|
|||
|
35e6ffd6af
|
|||
|
e957f840da
|
|||
|
85e22089bd
|
|||
|
b7d68b4458
|
|||
|
8f440603ee
|
|||
|
5d8e559ca3
|
|||
|
2c11eecf97
|
|||
|
02803f738b
|
|||
|
db27e6a645
|
|||
|
c9c35cc60d
|
|||
|
880205dbc8
|
|||
|
6456e64328
|
|||
|
f6a0c0cc6d
|
|||
|
eb57d269fc
|
|||
| 34f41dc522 | |||
|
|
77460045c4 | ||
|
|
9753afba74 | ||
|
|
1ea108fc2b | ||
|
|
2f02a6010d | ||
|
b80489fd66
|
|||
|
1d60e124ee
|
|||
|
e22d7d2fc9
|
|||
|
0a0694e095
|
|||
|
3da9d5d869
|
|||
|
78fb052d77
|
|||
|
9805cba10d
|
|||
|
4c3dfe2f1f
|
|||
|
b0baa5bfd6
|
|||
| 22996ee914 | |||
|
|
31ec9c667f | ||
|
3045361243
|
|||
|
809116c764
|
|||
|
83f401595d
|
|||
|
814d145366
|
|||
|
efb76c7686
|
|||
|
47dbe805bb
|
|||
|
f11fccc889
|
|||
|
2e13c4ccb5
|
|||
|
da72ffd1fd
|
|||
|
2c9437bef4
|
|||
|
f9085386a6
|
|||
|
d576a7043c
|
|||
|
1ef5f56158
|
|||
|
f9316de244
|
|||
|
0189c29f47
|
|||
|
b4fa1d68f0
|
|||
|
16df882481
|
|||
|
58c04ff9ad
|
|||
|
6b8f85e39a
|
|||
|
2fff1a3436
|
|||
|
a685524dd5
|
|||
|
74918e2c56
|
|||
|
18895d983b
|
|||
|
fe720208ea
|
|||
| 091d9e9239 |
13
.github/FUNDING.yml
vendored
Normal file
13
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: arsenetar
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
50
.github/workflows/codeql-analysis.yml
vendored
Normal file
50
.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [master]
|
||||
schedule:
|
||||
- cron: "24 20 * * 2"
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: ["cpp", "python"]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
- if: matrix.language == 'cpp'
|
||||
name: Build Cpp
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install python3-pyqt5
|
||||
make modules
|
||||
- if: matrix.language == 'python'
|
||||
name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
# Analysis
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
32
.github/workflows/default.yml
vendored
32
.github/workflows/default.yml
vendored
@@ -4,19 +4,19 @@ name: Default CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python 3.9
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
python-version: "3.10"
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
@@ -28,10 +28,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python 3.9
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
python-version: "3.10"
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
@@ -45,16 +45,20 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
python-version: [3.6, 3.7, 3.8, 3.9]
|
||||
python-version: [3.7, 3.8, 3.9, "3.10"]
|
||||
exclude:
|
||||
- os: macos-latest
|
||||
python-version: 3.6
|
||||
python-version: 3.7
|
||||
- os: macos-latest
|
||||
python-version: 3.7
|
||||
- os: windows-latest
|
||||
python-version: 3.6
|
||||
python-version: 3.8
|
||||
- os: macos-latest
|
||||
python-version: 3.9
|
||||
- os: windows-latest
|
||||
python-version: 3.7
|
||||
- os: windows-latest
|
||||
python-version: 3.8
|
||||
- os: windows-latest
|
||||
python-version: 3.9
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -72,3 +76,9 @@ jobs:
|
||||
- name: Run tests
|
||||
run: |
|
||||
pytest core hscommon
|
||||
- name: Upload Artifacts
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: modules ${{ matrix.python-version }}
|
||||
path: ${{ github.workspace }}/**/*.so
|
||||
|
||||
123
.gitignore
vendored
123
.gitignore
vendored
@@ -1,28 +1,111 @@
|
||||
.DS_Store
|
||||
__pycache__
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.waf*
|
||||
.lock-waf*
|
||||
.tox
|
||||
/tags
|
||||
#*.pot
|
||||
|
||||
build
|
||||
dist
|
||||
env*
|
||||
/deps
|
||||
cocoa/autogen
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
/run.py
|
||||
/cocoa/*/Info.plist
|
||||
/cocoa/*/build
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env*/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
#!.vscode/tasks.json
|
||||
#!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
# dupeGuru Specific
|
||||
/qt/*_rc.py
|
||||
/help/*/conf.py
|
||||
/help/*/changelog.rst
|
||||
/transifex
|
||||
cocoa/autogen
|
||||
/cocoa/*/Info.plist
|
||||
/cocoa/*/build
|
||||
|
||||
*.pyd
|
||||
*.exe
|
||||
*.spec
|
||||
|
||||
.vscode
|
||||
*.waf*
|
||||
.lock-waf*
|
||||
/tags
|
||||
1
.sonarcloud.properties
Normal file
1
.sonarcloud.properties
Normal file
@@ -0,0 +1 @@
|
||||
sonar.python.version=3.7, 3.8, 3.9, 3.10
|
||||
27
.tx/config
27
.tx/config
@@ -1,26 +1,27 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[dupeguru-1.core]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/core.po
|
||||
source_file = locale/core.pot
|
||||
source_lang = en
|
||||
type = PO
|
||||
|
||||
[dupeguru-1.columns]
|
||||
[o:voltaicideas:p:dupeguru-1:r:columns]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/columns.po
|
||||
source_file = locale/columns.pot
|
||||
source_lang = en
|
||||
type = PO
|
||||
|
||||
[dupeguru-1.ui]
|
||||
[o:voltaicideas:p:dupeguru-1:r:core]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/core.po
|
||||
source_file = locale/core.pot
|
||||
source_lang = en
|
||||
type = PO
|
||||
|
||||
[o:voltaicideas:p:dupeguru-1:r:qtlib]
|
||||
file_filter = qtlib/locale/<lang>/LC_MESSAGES/qtlib.po
|
||||
source_file = qtlib/locale/qtlib.pot
|
||||
source_lang = en
|
||||
type = PO
|
||||
|
||||
[o:voltaicideas:p:dupeguru-1:r:ui]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/ui.po
|
||||
source_file = locale/ui.pot
|
||||
source_lang = en
|
||||
type = PO
|
||||
|
||||
[dupeguru-1.qtlib]
|
||||
file_filter = qtlib/locale/<lang>/LC_MESSAGES/qtlib.po
|
||||
source_file = qtlib/locale/qtlib.pot
|
||||
source_lang = en
|
||||
type = PO
|
||||
|
||||
10
.vscode/extensions.json
vendored
Normal file
10
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
// List of extensions which should be recommended for users of this workspace.
|
||||
"recommendations": [
|
||||
"redhat.vscode-yaml",
|
||||
"ms-python.vscode-pylance",
|
||||
"ms-python.python"
|
||||
],
|
||||
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
12
.vscode/settings.json
vendored
Normal file
12
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"python.formatting.provider": "black",
|
||||
"cSpell.words": [
|
||||
"Dupras",
|
||||
"hscommon"
|
||||
],
|
||||
"python.languageServer": "Pylance",
|
||||
"yaml.schemaStore.enable": true,
|
||||
"yaml.schemas": {
|
||||
"https://json.schemastore.org/github-workflow.json": ".github/workflows/*.yml"
|
||||
}
|
||||
}
|
||||
88
CONTRIBUTING.md
Normal file
88
CONTRIBUTING.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Contributing to dupeGuru
|
||||
|
||||
The following is a set of guidelines and information for contributing to dupeGuru.
|
||||
|
||||
#### Table of Contents
|
||||
|
||||
[Things to Know Before Starting](#things-to-know-before-starting)
|
||||
|
||||
[Ways to Contribute](#ways-to-contribute)
|
||||
* [Reporting Bugs](#reporting-bugs)
|
||||
* [Suggesting Enhancements](#suggesting-enhancements)
|
||||
* [Localization](#localization)
|
||||
* [Code Contribution](#code-contribution)
|
||||
* [Pull Requests](#pull-requests)
|
||||
|
||||
[Style Guides](#style-guides)
|
||||
* [Git Commit Messages](#git-commit-messages)
|
||||
* [Python Style Guide](#python-style-guide)
|
||||
* [Documentation Style Guide](#documentation-style-guide)
|
||||
|
||||
[Additional Notes](#additional-notes)
|
||||
* [Issue and Pull Request Labels](#issue-and-pull-request-labels)
|
||||
|
||||
## Things to Know Before Starting
|
||||
**TODO**
|
||||
## Ways to contribute
|
||||
### Reporting Bugs
|
||||
**TODO**
|
||||
### Suggesting Enhancements
|
||||
**TODO**
|
||||
### Localization
|
||||
**TODO**
|
||||
### Code Contribution
|
||||
**TODO**
|
||||
### Pull Requests
|
||||
Please follow these steps to have your contribution considered by the maintainers:
|
||||
|
||||
1. Keep Pull Request specific to one feature or bug.
|
||||
2. Follow the [style guides](#style-guides)
|
||||
3. After you submit your pull request, verify that all [status checks](https://help.github.com/articles/about-status-checks/) are passing <details><summary>What if the status checks are failing?</summary>If a status check is failing, and you believe that the failure is unrelated to your change, please leave a comment on the pull request explaining why you believe the failure is unrelated. A maintainer will re-run the status check for you. If we conclude that the failure was a false positive, then we will open an issue to track that problem with our status check suite.</details>
|
||||
|
||||
While the prerequisites above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted.
|
||||
|
||||
## Style Guides
|
||||
### Git Commit Messages
|
||||
- Use the present tense ("Add feature" not "Added feature")
|
||||
- Use the imperative mood ("Move cursor to..." not "Moves cursor to...")
|
||||
- Limit the first line to 72 characters or less
|
||||
- Reference issues and pull requests liberally after the first line
|
||||
|
||||
### Python Style Guide
|
||||
- All files are formatted with [Black](https://github.com/psf/black)
|
||||
- Follow [PEP 8](https://peps.python.org/pep-0008/) as much as practical
|
||||
- Pass [flake8](https://flake8.pycqa.org/en/latest/) linting
|
||||
- Include [PEP 484](https://peps.python.org/pep-0484/) type hints (new code)
|
||||
|
||||
### Documentation Style Guide
|
||||
**TODO**
|
||||
|
||||
## Additional Notes
|
||||
### Issue and Pull Request Labels
|
||||
This section lists and describes the various labels used with issues and pull requests. Each of the labels is listed with a search link as well.
|
||||
|
||||
#### Issue Type and Status
|
||||
| Label name | Search | Description |
|
||||
|------------|--------|-------------|
|
||||
| `enhancement` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement) | Feature requests and enhancements. |
|
||||
| `bug` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Abug) | Bug reports. |
|
||||
| `duplicate` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Aduplicate) | Issue is a duplicate of existing issue. |
|
||||
| `needs-reproduction` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Aneeds-reproduction) | A bug that has not been able to be reproduced. |
|
||||
| `needs-information` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Aneeds-information) | More information needs to be collected about these problems or feature requests (e.g. steps to reproduce). |
|
||||
| `blocked` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Ablocked) | Issue blocked by other issues. |
|
||||
| `beginner` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Abeginner) | Less complex issues for users who want to start contributing. |
|
||||
|
||||
#### Category Labels
|
||||
| Label name | Search | Description |
|
||||
|------------|--------|-------------|
|
||||
| `3rd party` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3A%223rd%20party%22) | Related to a 3rd party dependency. |
|
||||
| `crash` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Acrash) | Related to crashes (complete, or unhandled). |
|
||||
| `documentation` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Adocumentation) | Related to any documentation. |
|
||||
| `linux` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3linux) | Related to running on Linux. |
|
||||
| `mac` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Amac) | Related to running on macOS. |
|
||||
| `performance` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Aperformance) | Related to the performance. |
|
||||
| `ui` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Aui)| Related to the visual design. |
|
||||
| `windows` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Awindows) | Related to running on Windows. |
|
||||
|
||||
#### Pull Request Labels
|
||||
None at this time, if the volume of Pull Requests increase labels may be added to manage.
|
||||
6
MANIFEST.in
Normal file
6
MANIFEST.in
Normal file
@@ -0,0 +1,6 @@
|
||||
recursive-include core *.h
|
||||
recursive-include core *.m
|
||||
include run.py
|
||||
graft locale
|
||||
graft help
|
||||
graft qtlib/locale
|
||||
4
Makefile
4
Makefile
@@ -1,7 +1,7 @@
|
||||
PYTHON ?= python3
|
||||
PYTHON_VERSION_MINOR := $(shell ${PYTHON} -c "import sys; print(sys.version_info.minor)")
|
||||
PYRCC5 ?= pyrcc5
|
||||
REQ_MINOR_VERSION = 6
|
||||
REQ_MINOR_VERSION = 7
|
||||
PREFIX ?= /usr/local
|
||||
|
||||
# Window compatability via Msys2
|
||||
@@ -53,7 +53,7 @@ pyc: | env
|
||||
${VENV_PYTHON} -m compileall ${packages}
|
||||
|
||||
reqs:
|
||||
ifneq ($(shell test $(PYTHON_VERSION_MINOR) -gt $(REQ_MINOR_VERSION); echo $$?),0)
|
||||
ifneq ($(shell test $(PYTHON_VERSION_MINOR) -ge $(REQ_MINOR_VERSION); echo $$?),0)
|
||||
$(error "Python 3.${REQ_MINOR_VERSION}+ required. Aborting.")
|
||||
endif
|
||||
ifndef NO_VENV
|
||||
|
||||
@@ -36,7 +36,7 @@ For windows instructions see the [Windows Instructions](Windows.md).
|
||||
For macos instructions (qt version) see the [macOS Instructions](macos.md).
|
||||
|
||||
### Prerequisites
|
||||
* [Python 3.6+][python]
|
||||
* [Python 3.7+][python]
|
||||
* PyQt5
|
||||
|
||||
### System Setup
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Python 3.6+][python]
|
||||
- [Python 3.7+][python]
|
||||
- [Visual Studio 2019][vs] or [Visual Studio Build Tools 2019][vsBuildTools] with the Windows 10 SDK
|
||||
- [nsis][nsis] (for installer creation)
|
||||
- [msys2][msys2] (for using makefile method)
|
||||
@@ -16,7 +16,7 @@ After installing python it is recommended to update setuptools before compiling
|
||||
More details on setting up python for compiling packages on windows can be found on the [python wiki][pythonWindowsCompilers] Take note of the required vc++ versions.
|
||||
|
||||
### With build.py (preferred)
|
||||
To build with a different python version 3.6 vs 3.8 or 32 bit vs 64 bit specify that version instead of -3.8 to the `py` command below. If you want to build additional versions while keeping all virtual environments setup use a different location for each virtual environment.
|
||||
To build with a different python version 3.7 vs 3.8 or 32 bit vs 64 bit specify that version instead of -3.8 to the `py` command below. If you want to build additional versions while keeping all virtual environments setup use a different location for each virtual environment.
|
||||
|
||||
$ cd <dupeGuru directory>
|
||||
$ py -3.8 -m venv .\env
|
||||
|
||||
104
build.py
104
build.py
@@ -4,18 +4,17 @@
|
||||
# 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
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from optparse import OptionParser
|
||||
import shutil
|
||||
from multiprocessing import Pool
|
||||
|
||||
from setuptools import setup, Extension
|
||||
|
||||
from setuptools import sandbox
|
||||
from hscommon import sphinxgen
|
||||
from hscommon.build import (
|
||||
add_to_pythonpath,
|
||||
print_and_do,
|
||||
move_all,
|
||||
fix_qt_resource_file,
|
||||
)
|
||||
from hscommon import loc
|
||||
@@ -30,7 +29,8 @@ def parse_args():
|
||||
dest="clean",
|
||||
help="Clean build folder before building",
|
||||
)
|
||||
parser.add_option("--doc", action="store_true", dest="doc", help="Build only the help file")
|
||||
parser.add_option("--doc", action="store_true", dest="doc", help="Build only the help file (en)")
|
||||
parser.add_option("--alldoc", action="store_true", dest="all_doc", help="Build only the help file in all languages")
|
||||
parser.add_option("--loc", action="store_true", dest="loc", help="Build only localization")
|
||||
parser.add_option(
|
||||
"--updatepot",
|
||||
@@ -60,16 +60,16 @@ def parse_args():
|
||||
return options
|
||||
|
||||
|
||||
def build_help():
|
||||
print("Generating Help")
|
||||
current_path = op.abspath(".")
|
||||
help_basepath = op.join(current_path, "help", "en")
|
||||
help_destpath = op.join(current_path, "build", "help")
|
||||
changelog_path = op.join(current_path, "help", "changelog")
|
||||
def build_one_help(language):
|
||||
print("Generating Help in {}".format(language))
|
||||
current_path = Path(".").absolute()
|
||||
changelog_path = current_path.joinpath("help", "changelog")
|
||||
tixurl = "https://github.com/arsenetar/dupeguru/issues/{}"
|
||||
confrepl = {"language": "en"}
|
||||
changelogtmpl = op.join(current_path, "help", "changelog.tmpl")
|
||||
conftmpl = op.join(current_path, "help", "conf.tmpl")
|
||||
changelogtmpl = current_path.joinpath("help", "changelog.tmpl")
|
||||
conftmpl = current_path.joinpath("help", "conf.tmpl")
|
||||
help_basepath = current_path.joinpath("help", language)
|
||||
help_destpath = current_path.joinpath("build", "help", language)
|
||||
confrepl = {"language": language}
|
||||
sphinxgen.gen(
|
||||
help_basepath,
|
||||
help_destpath,
|
||||
@@ -81,16 +81,23 @@ def build_help():
|
||||
)
|
||||
|
||||
|
||||
def build_help():
|
||||
languages = ["en", "de", "fr", "hy", "ru", "uk"]
|
||||
# Running with Pools as for some reason sphinx seems to cross contaminate the output otherwise
|
||||
with Pool(len(languages)) as p:
|
||||
p.map(build_one_help, languages)
|
||||
|
||||
|
||||
def build_qt_localizations():
|
||||
loc.compile_all_po(op.join("qtlib", "locale"))
|
||||
loc.merge_locale_dir(op.join("qtlib", "locale"), "locale")
|
||||
loc.compile_all_po(Path("qtlib", "locale"))
|
||||
loc.merge_locale_dir(Path("qtlib", "locale"), "locale")
|
||||
|
||||
|
||||
def build_localizations():
|
||||
loc.compile_all_po("locale")
|
||||
build_qt_localizations()
|
||||
locale_dest = op.join("build", "locale")
|
||||
if op.exists(locale_dest):
|
||||
locale_dest = Path("build", "locale")
|
||||
if locale_dest.exists():
|
||||
shutil.rmtree(locale_dest)
|
||||
shutil.copytree("locale", locale_dest, ignore=shutil.ignore_patterns("*.po", "*.pot"))
|
||||
|
||||
@@ -98,57 +105,35 @@ def build_localizations():
|
||||
def build_updatepot():
|
||||
print("Building .pot files from source files")
|
||||
print("Building core.pot")
|
||||
loc.generate_pot(["core"], op.join("locale", "core.pot"), ["tr"])
|
||||
loc.generate_pot(["core"], Path("locale", "core.pot"), ["tr"])
|
||||
print("Building columns.pot")
|
||||
loc.generate_pot(["core"], op.join("locale", "columns.pot"), ["coltr"])
|
||||
loc.generate_pot(["core"], Path("locale", "columns.pot"), ["coltr"])
|
||||
print("Building ui.pot")
|
||||
# When we're not under OS X, we don't want to overwrite ui.pot because it contains Cocoa locs
|
||||
# We want to merge the generated pot with the old pot in the most preserving way possible.
|
||||
ui_packages = ["qt", op.join("cocoa", "inter")]
|
||||
loc.generate_pot(ui_packages, op.join("locale", "ui.pot"), ["tr"], merge=True)
|
||||
ui_packages = ["qt", Path("cocoa", "inter")]
|
||||
loc.generate_pot(ui_packages, Path("locale", "ui.pot"), ["tr"], merge=True)
|
||||
print("Building qtlib.pot")
|
||||
loc.generate_pot(["qtlib"], op.join("qtlib", "locale", "qtlib.pot"), ["tr"])
|
||||
loc.generate_pot(["qtlib"], Path("qtlib", "locale", "qtlib.pot"), ["tr"])
|
||||
|
||||
|
||||
def build_mergepot():
|
||||
print("Updating .po files using .pot files")
|
||||
loc.merge_pots_into_pos("locale")
|
||||
loc.merge_pots_into_pos(op.join("qtlib", "locale"))
|
||||
# loc.merge_pots_into_pos(op.join("cocoalib", "locale"))
|
||||
loc.merge_pots_into_pos(Path("qtlib", "locale"))
|
||||
# loc.merge_pots_into_pos(Path("cocoalib", "locale"))
|
||||
|
||||
|
||||
def build_normpo():
|
||||
loc.normalize_all_pos("locale")
|
||||
loc.normalize_all_pos(op.join("qtlib", "locale"))
|
||||
# loc.normalize_all_pos(op.join("cocoalib", "locale"))
|
||||
loc.normalize_all_pos(Path("qtlib", "locale"))
|
||||
# loc.normalize_all_pos(Path("cocoalib", "locale"))
|
||||
|
||||
|
||||
def build_pe_modules():
|
||||
print("Building PE Modules")
|
||||
exts = [
|
||||
Extension(
|
||||
"_block",
|
||||
[
|
||||
op.join("core", "pe", "modules", "block.c"),
|
||||
op.join("core", "pe", "modules", "common.c"),
|
||||
],
|
||||
),
|
||||
Extension(
|
||||
"_cache",
|
||||
[
|
||||
op.join("core", "pe", "modules", "cache.c"),
|
||||
op.join("core", "pe", "modules", "common.c"),
|
||||
],
|
||||
),
|
||||
]
|
||||
exts.append(Extension("_block_qt", [op.join("qt", "pe", "modules", "block.c")]))
|
||||
setup(
|
||||
script_args=["build_ext", "--inplace"],
|
||||
ext_modules=exts,
|
||||
)
|
||||
move_all("_block_qt*", op.join("qt", "pe"))
|
||||
move_all("_block*", op.join("core", "pe"))
|
||||
move_all("_cache*", op.join("core", "pe"))
|
||||
# Leverage setup.py to build modules
|
||||
sandbox.run_setup("setup.py", ["build_ext", "--inplace"])
|
||||
|
||||
|
||||
def build_normal():
|
||||
@@ -159,19 +144,22 @@ def build_normal():
|
||||
print("Building localizations")
|
||||
build_localizations()
|
||||
print("Building Qt stuff")
|
||||
print_and_do("pyrcc5 {0} > {1}".format(op.join("qt", "dg.qrc"), op.join("qt", "dg_rc.py")))
|
||||
fix_qt_resource_file(op.join("qt", "dg_rc.py"))
|
||||
print_and_do("pyrcc5 {0} > {1}".format(Path("qt", "dg.qrc"), Path("qt", "dg_rc.py")))
|
||||
fix_qt_resource_file(Path("qt", "dg_rc.py"))
|
||||
build_help()
|
||||
|
||||
|
||||
def main():
|
||||
if sys.version_info < (3, 7):
|
||||
sys.exit("Python < 3.7 is unsupported.")
|
||||
options = parse_args()
|
||||
if options.clean:
|
||||
if op.exists("build"):
|
||||
if options.clean and Path("build").exists():
|
||||
shutil.rmtree("build")
|
||||
if not op.exists("build"):
|
||||
os.mkdir("build")
|
||||
if not Path("build").exists():
|
||||
Path("build").mkdir()
|
||||
if options.doc:
|
||||
build_one_help("en")
|
||||
elif options.all_doc:
|
||||
build_help()
|
||||
elif options.loc:
|
||||
build_localizations()
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
__version__ = "4.1.1"
|
||||
__version__ = "4.2.1"
|
||||
__appname__ = "dupeGuru"
|
||||
|
||||
149
core/app.py
149
core/app.py
@@ -48,31 +48,31 @@ MSG_MANY_FILES_TO_OPEN = tr(
|
||||
|
||||
|
||||
class DestType:
|
||||
Direct = 0
|
||||
Relative = 1
|
||||
Absolute = 2
|
||||
DIRECT = 0
|
||||
RELATIVE = 1
|
||||
ABSOLUTE = 2
|
||||
|
||||
|
||||
class JobType:
|
||||
Scan = "job_scan"
|
||||
Load = "job_load"
|
||||
Move = "job_move"
|
||||
Copy = "job_copy"
|
||||
Delete = "job_delete"
|
||||
SCAN = "job_scan"
|
||||
LOAD = "job_load"
|
||||
MOVE = "job_move"
|
||||
COPY = "job_copy"
|
||||
DELETE = "job_delete"
|
||||
|
||||
|
||||
class AppMode:
|
||||
Standard = 0
|
||||
Music = 1
|
||||
Picture = 2
|
||||
STANDARD = 0
|
||||
MUSIC = 1
|
||||
PICTURE = 2
|
||||
|
||||
|
||||
JOBID2TITLE = {
|
||||
JobType.Scan: tr("Scanning for duplicates"),
|
||||
JobType.Load: tr("Loading"),
|
||||
JobType.Move: tr("Moving"),
|
||||
JobType.Copy: tr("Copying"),
|
||||
JobType.Delete: tr("Sending to Trash"),
|
||||
JobType.SCAN: tr("Scanning for duplicates"),
|
||||
JobType.LOAD: tr("Loading"),
|
||||
JobType.MOVE: tr("Moving"),
|
||||
JobType.COPY: tr("Copying"),
|
||||
JobType.DELETE: tr("Sending to Trash"),
|
||||
}
|
||||
|
||||
|
||||
@@ -132,12 +132,14 @@ class DupeGuru(Broadcaster):
|
||||
logging.debug("Debug mode enabled")
|
||||
Broadcaster.__init__(self)
|
||||
self.view = view
|
||||
self.appdata = desktop.special_folder_path(desktop.SpecialFolder.AppData, appname=self.NAME, portable=portable)
|
||||
self.appdata = desktop.special_folder_path(desktop.SpecialFolder.APPDATA, appname=self.NAME, portable=portable)
|
||||
if not op.exists(self.appdata):
|
||||
os.makedirs(self.appdata)
|
||||
self.app_mode = AppMode.Standard
|
||||
self.app_mode = AppMode.STANDARD
|
||||
self.discarded_file_count = 0
|
||||
self.exclude_list = ExcludeList()
|
||||
hash_cache_file = op.join(self.appdata, "hash_cache.db")
|
||||
fs.filesdb.connect(hash_cache_file)
|
||||
self.directories = directories.Directories(self.exclude_list)
|
||||
self.results = results.Results(self)
|
||||
self.ignore_list = IgnoreList()
|
||||
@@ -148,7 +150,7 @@ class DupeGuru(Broadcaster):
|
||||
"escape_filter_regexp": True,
|
||||
"clean_empty_dirs": False,
|
||||
"ignore_hardlink_matches": False,
|
||||
"copymove_dest_type": DestType.Relative,
|
||||
"copymove_dest_type": DestType.RELATIVE,
|
||||
"picture_cache_type": self.PICTURE_CACHE_TYPE,
|
||||
}
|
||||
self.selected_dupes = []
|
||||
@@ -169,9 +171,9 @@ class DupeGuru(Broadcaster):
|
||||
def _recreate_result_table(self):
|
||||
if self.result_table is not None:
|
||||
self.result_table.disconnect()
|
||||
if self.app_mode == AppMode.Picture:
|
||||
if self.app_mode == AppMode.PICTURE:
|
||||
self.result_table = pe.result_table.ResultTable(self)
|
||||
elif self.app_mode == AppMode.Music:
|
||||
elif self.app_mode == AppMode.MUSIC:
|
||||
self.result_table = me.result_table.ResultTable(self)
|
||||
else:
|
||||
self.result_table = se.result_table.ResultTable(self)
|
||||
@@ -184,12 +186,10 @@ class DupeGuru(Broadcaster):
|
||||
return op.join(self.appdata, cache_name)
|
||||
|
||||
def _get_dupe_sort_key(self, dupe, get_group, key, delta):
|
||||
if self.app_mode in (AppMode.Music, AppMode.Picture):
|
||||
if key == "folder_path":
|
||||
if self.app_mode in (AppMode.MUSIC, AppMode.PICTURE) and key == "folder_path":
|
||||
dupe_folder_path = getattr(dupe, "display_folder_path", dupe.folder_path)
|
||||
return str(dupe_folder_path).lower()
|
||||
if self.app_mode == AppMode.Picture:
|
||||
if delta and key == "dimensions":
|
||||
if self.app_mode == AppMode.PICTURE and delta and key == "dimensions":
|
||||
r = cmp_value(dupe, key)
|
||||
ref_value = cmp_value(get_group().ref, key)
|
||||
return get_delta_dimensions(r, ref_value)
|
||||
@@ -212,8 +212,7 @@ class DupeGuru(Broadcaster):
|
||||
return result
|
||||
|
||||
def _get_group_sort_key(self, group, key):
|
||||
if self.app_mode in (AppMode.Music, AppMode.Picture):
|
||||
if key == "folder_path":
|
||||
if self.app_mode in (AppMode.MUSIC, AppMode.PICTURE) and key == "folder_path":
|
||||
dupe_folder_path = getattr(group.ref, "display_folder_path", group.ref.folder_path)
|
||||
return str(dupe_folder_path).lower()
|
||||
if key == "percentage":
|
||||
@@ -267,7 +266,7 @@ class DupeGuru(Broadcaster):
|
||||
return None
|
||||
|
||||
def _get_export_data(self):
|
||||
columns = [col for col in self.result_table.columns.ordered_columns if col.visible and col.name != "marked"]
|
||||
columns = [col for col in self.result_table._columns.ordered_columns if col.visible and col.name != "marked"]
|
||||
colnames = [col.display for col in columns]
|
||||
rows = []
|
||||
for group_id, group in enumerate(self.results.groups):
|
||||
@@ -294,32 +293,36 @@ class DupeGuru(Broadcaster):
|
||||
self.view.show_message(msg)
|
||||
|
||||
def _job_completed(self, jobid):
|
||||
if jobid == JobType.Scan:
|
||||
if jobid == JobType.SCAN:
|
||||
self._results_changed()
|
||||
fs.filesdb.commit()
|
||||
if not self.results.groups:
|
||||
self.view.show_message(tr("No duplicates found."))
|
||||
else:
|
||||
self.view.show_results_window()
|
||||
if jobid in {JobType.Move, JobType.Delete}:
|
||||
if jobid in {JobType.MOVE, JobType.DELETE}:
|
||||
self._results_changed()
|
||||
if jobid == JobType.Load:
|
||||
if jobid == JobType.LOAD:
|
||||
self._recreate_result_table()
|
||||
self._results_changed()
|
||||
self.view.show_results_window()
|
||||
if jobid in {JobType.Copy, JobType.Move, JobType.Delete}:
|
||||
if jobid in {JobType.COPY, JobType.MOVE, JobType.DELETE}:
|
||||
if self.results.problems:
|
||||
self.problem_dialog.refresh()
|
||||
self.view.show_problem_dialog()
|
||||
else:
|
||||
msg = {
|
||||
JobType.Copy: tr("All marked files were copied successfully."),
|
||||
JobType.Move: tr("All marked files were moved successfully."),
|
||||
JobType.Delete: tr("All marked files were successfully sent to Trash."),
|
||||
}[jobid]
|
||||
if jobid == JobType.COPY:
|
||||
msg = tr("All marked files were copied successfully.")
|
||||
elif jobid == JobType.MOVE:
|
||||
msg = tr("All marked files were moved successfully.")
|
||||
elif jobid == JobType.DELETE and self.deletion_options.direct:
|
||||
msg = tr("All marked files were deleted successfully.")
|
||||
else:
|
||||
msg = tr("All marked files were successfully sent to Trash.")
|
||||
self.view.show_message(msg)
|
||||
|
||||
def _job_error(self, jobid, err):
|
||||
if jobid == JobType.Load:
|
||||
if jobid == JobType.LOAD:
|
||||
msg = tr("Could not load file: {}").format(err)
|
||||
self.view.show_message(msg)
|
||||
return False
|
||||
@@ -349,17 +352,17 @@ class DupeGuru(Broadcaster):
|
||||
|
||||
# --- Protected
|
||||
def _get_fileclasses(self):
|
||||
if self.app_mode == AppMode.Picture:
|
||||
if self.app_mode == AppMode.PICTURE:
|
||||
return [pe.photo.PLAT_SPECIFIC_PHOTO_CLASS]
|
||||
elif self.app_mode == AppMode.Music:
|
||||
elif self.app_mode == AppMode.MUSIC:
|
||||
return [me.fs.MusicFile]
|
||||
else:
|
||||
return [se.fs.File]
|
||||
|
||||
def _prioritization_categories(self):
|
||||
if self.app_mode == AppMode.Picture:
|
||||
if self.app_mode == AppMode.PICTURE:
|
||||
return pe.prioritize.all_categories()
|
||||
elif self.app_mode == AppMode.Music:
|
||||
elif self.app_mode == AppMode.MUSIC:
|
||||
return me.prioritize.all_categories()
|
||||
else:
|
||||
return prioritize.all_categories()
|
||||
@@ -393,20 +396,20 @@ class DupeGuru(Broadcaster):
|
||||
g = self.results.get_group_of_duplicate(dupe)
|
||||
for other in g:
|
||||
if other is not dupe:
|
||||
self.ignore_list.Ignore(str(other.path), str(dupe.path))
|
||||
self.ignore_list.ignore(str(other.path), str(dupe.path))
|
||||
self.remove_duplicates(dupes)
|
||||
self.ignore_list_dialog.refresh()
|
||||
|
||||
def apply_filter(self, filter):
|
||||
def apply_filter(self, result_filter):
|
||||
"""Apply a filter ``filter`` to the results so that it shows only dupe groups that match it.
|
||||
|
||||
:param str filter: filter to apply
|
||||
"""
|
||||
self.results.apply_filter(None)
|
||||
if self.options["escape_filter_regexp"]:
|
||||
filter = escape(filter, set("()[]\\.|+?^"))
|
||||
filter = escape(filter, "*", ".")
|
||||
self.results.apply_filter(filter)
|
||||
result_filter = escape(result_filter, set("()[]\\.|+?^"))
|
||||
result_filter = escape(result_filter, "*", ".")
|
||||
self.results.apply_filter(result_filter)
|
||||
self._results_changed()
|
||||
|
||||
def clean_empty_dirs(self, path):
|
||||
@@ -420,14 +423,17 @@ class DupeGuru(Broadcaster):
|
||||
except FileNotFoundError:
|
||||
pass # we don't care
|
||||
|
||||
def clear_hash_cache(self):
|
||||
fs.filesdb.clear()
|
||||
|
||||
def copy_or_move(self, dupe, copy: bool, destination: str, dest_type: DestType):
|
||||
source_path = dupe.path
|
||||
location_path = first(p for p in self.directories if dupe.path in p)
|
||||
dest_path = Path(destination)
|
||||
if dest_type in {DestType.Relative, DestType.Absolute}:
|
||||
if dest_type in {DestType.RELATIVE, DestType.ABSOLUTE}:
|
||||
# no filename, no windows drive letter
|
||||
source_base = source_path.remove_drive_letter().parent()
|
||||
if dest_type == DestType.Relative:
|
||||
if dest_type == DestType.RELATIVE:
|
||||
source_base = source_base[location_path:]
|
||||
dest_path = dest_path[source_base]
|
||||
if not dest_path.exists():
|
||||
@@ -466,7 +472,7 @@ class DupeGuru(Broadcaster):
|
||||
)
|
||||
if destination:
|
||||
desttype = self.options["copymove_dest_type"]
|
||||
jobid = JobType.Copy if copy else JobType.Move
|
||||
jobid = JobType.COPY if copy else JobType.MOVE
|
||||
self._start_job(jobid, do)
|
||||
|
||||
def delete_marked(self):
|
||||
@@ -482,7 +488,7 @@ class DupeGuru(Broadcaster):
|
||||
self.deletion_options.direct,
|
||||
]
|
||||
logging.debug("Starting deletion job with args %r", args)
|
||||
self._start_job(JobType.Delete, self._do_delete, args=args)
|
||||
self._start_job(JobType.DELETE, self._do_delete, args=args)
|
||||
|
||||
def export_to_xhtml(self):
|
||||
"""Export current results to XHTML.
|
||||
@@ -535,12 +541,12 @@ class DupeGuru(Broadcaster):
|
||||
return
|
||||
if not self.selected_dupes:
|
||||
return
|
||||
dupe = self.selected_dupes[0]
|
||||
group = self.results.get_group_of_duplicate(dupe)
|
||||
ref = group.ref
|
||||
cmd = cmd.replace("%d", str(dupe.path))
|
||||
cmd = cmd.replace("%r", str(ref.path))
|
||||
match = re.match(r'"([^"]+)"(.*)', cmd)
|
||||
dupes = self.selected_dupes
|
||||
refs = [self.results.get_group_of_duplicate(dupe).ref for dupe in dupes]
|
||||
for dupe, ref in zip(dupes, refs):
|
||||
dupe_cmd = cmd.replace("%d", str(dupe.path))
|
||||
dupe_cmd = dupe_cmd.replace("%r", str(ref.path))
|
||||
match = re.match(r'"([^"]+)"(.*)', dupe_cmd)
|
||||
if match is not None:
|
||||
# This code here is because subprocess. Popen doesn't seem to accept, under Windows,
|
||||
# executable paths with spaces in it, *even* when they're enclosed in "". So this is
|
||||
@@ -549,7 +555,7 @@ class DupeGuru(Broadcaster):
|
||||
path, exename = op.split(exepath)
|
||||
subprocess.Popen(exename + args, shell=True, cwd=path)
|
||||
else:
|
||||
subprocess.Popen(cmd, shell=True)
|
||||
subprocess.Popen(dupe_cmd, shell=True)
|
||||
|
||||
def load(self):
|
||||
"""Load directory selection and ignore list from files in appdata.
|
||||
@@ -582,7 +588,7 @@ class DupeGuru(Broadcaster):
|
||||
def do(j):
|
||||
self.results.load_from_xml(filename, self._get_file, j)
|
||||
|
||||
self._start_job(JobType.Load, do)
|
||||
self._start_job(JobType.LOAD, do)
|
||||
|
||||
def make_selected_reference(self):
|
||||
"""Promote :attr:`selected_dupes` to reference position within their respective groups.
|
||||
@@ -595,8 +601,7 @@ class DupeGuru(Broadcaster):
|
||||
changed_groups = set()
|
||||
for dupe in dupes:
|
||||
g = self.results.get_group_of_duplicate(dupe)
|
||||
if g not in changed_groups:
|
||||
if self.results.make_ref(dupe):
|
||||
if g not in changed_groups and self.results.make_ref(dupe):
|
||||
changed_groups.add(g)
|
||||
# It's not always obvious to users what this action does, so to make it a bit clearer,
|
||||
# we change our selection to the ref of all changed groups. However, we also want to keep
|
||||
@@ -647,15 +652,14 @@ class DupeGuru(Broadcaster):
|
||||
|
||||
def open_selected(self):
|
||||
"""Open :attr:`selected_dupes` with their associated application."""
|
||||
if len(self.selected_dupes) > 10:
|
||||
if not self.view.ask_yes_no(MSG_MANY_FILES_TO_OPEN):
|
||||
if len(self.selected_dupes) > 10 and not self.view.ask_yes_no(MSG_MANY_FILES_TO_OPEN):
|
||||
return
|
||||
for dupe in self.selected_dupes:
|
||||
desktop.open_path(dupe.path)
|
||||
|
||||
def purge_ignore_list(self):
|
||||
"""Remove files that don't exist from :attr:`ignore_list`."""
|
||||
self.ignore_list.Filter(lambda f, s: op.exists(f) and op.exists(s))
|
||||
self.ignore_list.filter(lambda f, s: op.exists(f) and op.exists(s))
|
||||
self.ignore_list_dialog.refresh()
|
||||
|
||||
def remove_directories(self, indexes):
|
||||
@@ -753,6 +757,9 @@ class DupeGuru(Broadcaster):
|
||||
self.exclude_list.save_to_xml(p)
|
||||
self.notify("save_session")
|
||||
|
||||
def close(self):
|
||||
fs.filesdb.close()
|
||||
|
||||
def save_as(self, filename):
|
||||
"""Save results in ``filename``.
|
||||
|
||||
@@ -786,7 +793,7 @@ class DupeGuru(Broadcaster):
|
||||
for k, v in self.options.items():
|
||||
if hasattr(scanner, k):
|
||||
setattr(scanner, k, v)
|
||||
if self.app_mode == AppMode.Picture:
|
||||
if self.app_mode == AppMode.PICTURE:
|
||||
scanner.cache_path = self._get_picture_cache_path()
|
||||
self.results.groups = []
|
||||
self._recreate_result_table()
|
||||
@@ -794,7 +801,7 @@ class DupeGuru(Broadcaster):
|
||||
|
||||
def do(j):
|
||||
j.set_progress(0, tr("Collecting files to scan"))
|
||||
if scanner.scan_type == ScanType.Folders:
|
||||
if scanner.scan_type == ScanType.FOLDERS:
|
||||
files = list(self.directories.get_folders(folderclass=se.fs.Folder, j=j))
|
||||
else:
|
||||
files = list(self.directories.get_files(fileclasses=self.fileclasses, j=j))
|
||||
@@ -804,7 +811,7 @@ class DupeGuru(Broadcaster):
|
||||
self.results.groups = scanner.get_dupe_groups(files, self.ignore_list, j)
|
||||
self.discarded_file_count = scanner.discarded_file_count
|
||||
|
||||
self._start_job(JobType.Scan, do)
|
||||
self._start_job(JobType.SCAN, do)
|
||||
|
||||
def toggle_selected_mark_state(self):
|
||||
selected = self.without_ref(self.selected_dupes)
|
||||
@@ -849,18 +856,18 @@ class DupeGuru(Broadcaster):
|
||||
|
||||
@property
|
||||
def SCANNER_CLASS(self):
|
||||
if self.app_mode == AppMode.Picture:
|
||||
if self.app_mode == AppMode.PICTURE:
|
||||
return pe.scanner.ScannerPE
|
||||
elif self.app_mode == AppMode.Music:
|
||||
elif self.app_mode == AppMode.MUSIC:
|
||||
return me.scanner.ScannerME
|
||||
else:
|
||||
return se.scanner.ScannerSE
|
||||
|
||||
@property
|
||||
def METADATA_TO_READ(self):
|
||||
if self.app_mode == AppMode.Picture:
|
||||
if self.app_mode == AppMode.PICTURE:
|
||||
return ["size", "mtime", "dimensions", "exif_timestamp"]
|
||||
elif self.app_mode == AppMode.Music:
|
||||
elif self.app_mode == AppMode.MUSIC:
|
||||
return [
|
||||
"size",
|
||||
"mtime",
|
||||
|
||||
@@ -11,6 +11,7 @@ import logging
|
||||
from hscommon.jobprogress import job
|
||||
from hscommon.path import Path
|
||||
from hscommon.util import FileOrPath
|
||||
from hscommon.trans import tr
|
||||
|
||||
from . import fs
|
||||
|
||||
@@ -30,9 +31,9 @@ class DirectoryState:
|
||||
* DirectoryState.Excluded: Don't scan this folder
|
||||
"""
|
||||
|
||||
Normal = 0
|
||||
Reference = 1
|
||||
Excluded = 2
|
||||
NORMAL = 0
|
||||
REFERENCE = 1
|
||||
EXCLUDED = 2
|
||||
|
||||
|
||||
class AlreadyThereError(Exception):
|
||||
@@ -82,50 +83,49 @@ class Directories:
|
||||
# We iterate even if we only have one item here
|
||||
for denied_path_re in self._exclude_list.compiled:
|
||||
if denied_path_re.match(str(path.name)):
|
||||
return DirectoryState.Excluded
|
||||
return DirectoryState.EXCLUDED
|
||||
# return # We still use the old logic to force state on hidden dirs
|
||||
# Override this in subclasses to specify the state of some special folders.
|
||||
if path.name.startswith("."):
|
||||
return DirectoryState.Excluded
|
||||
return DirectoryState.EXCLUDED
|
||||
|
||||
def _get_files(self, from_path, fileclasses, j):
|
||||
for root, dirs, files in os.walk(str(from_path)):
|
||||
j.check_if_cancelled()
|
||||
rootPath = Path(root)
|
||||
state = self.get_state(rootPath)
|
||||
if state == DirectoryState.Excluded:
|
||||
root_path = Path(root)
|
||||
state = self.get_state(root_path)
|
||||
if state == DirectoryState.EXCLUDED and not any(p[: len(root_path)] == root_path for p in self.states):
|
||||
# Recursively get files from folders with lots of subfolder is expensive. However, there
|
||||
# might be a subfolder in this path that is not excluded. What we want to do is to skim
|
||||
# through self.states and see if we must continue, or we can stop right here to save time
|
||||
if not any(p[: len(rootPath)] == rootPath for p in self.states):
|
||||
del dirs[:]
|
||||
try:
|
||||
if state != DirectoryState.Excluded:
|
||||
if state != DirectoryState.EXCLUDED:
|
||||
# Old logic
|
||||
if self._exclude_list is None or not self._exclude_list.mark_count:
|
||||
found_files = [fs.get_file(rootPath + f, fileclasses=fileclasses) for f in files]
|
||||
found_files = [fs.get_file(root_path + f, fileclasses=fileclasses) for f in files]
|
||||
else:
|
||||
found_files = []
|
||||
# print(f"len of files: {len(files)} {files}")
|
||||
for f in files:
|
||||
if not self._exclude_list.is_excluded(root, f):
|
||||
found_files.append(fs.get_file(rootPath + f, fileclasses=fileclasses))
|
||||
found_files.append(fs.get_file(root_path + f, fileclasses=fileclasses))
|
||||
found_files = [f for f in found_files if f is not None]
|
||||
# In some cases, directories can be considered as files by dupeGuru, which is
|
||||
# why we have this line below. In fact, there only one case: Bundle files under
|
||||
# OS X... In other situations, this forloop will do nothing.
|
||||
for d in dirs[:]:
|
||||
f = fs.get_file(rootPath + d, fileclasses=fileclasses)
|
||||
f = fs.get_file(root_path + d, fileclasses=fileclasses)
|
||||
if f is not None:
|
||||
found_files.append(f)
|
||||
dirs.remove(d)
|
||||
logging.debug(
|
||||
"Collected %d files in folder %s",
|
||||
len(found_files),
|
||||
str(rootPath),
|
||||
str(root_path),
|
||||
)
|
||||
for file in found_files:
|
||||
file.is_ref = state == DirectoryState.Reference
|
||||
file.is_ref = state == DirectoryState.REFERENCE
|
||||
yield file
|
||||
except (EnvironmentError, fs.InvalidPath):
|
||||
pass
|
||||
@@ -137,8 +137,8 @@ class Directories:
|
||||
for folder in self._get_folders(subfolder, j):
|
||||
yield folder
|
||||
state = self.get_state(from_folder.path)
|
||||
if state != DirectoryState.Excluded:
|
||||
from_folder.is_ref = state == DirectoryState.Reference
|
||||
if state != DirectoryState.EXCLUDED:
|
||||
from_folder.is_ref = state == DirectoryState.REFERENCE
|
||||
logging.debug("Yielding Folder %r state: %d", from_folder, state)
|
||||
yield from_folder
|
||||
except (EnvironmentError, fs.InvalidPath):
|
||||
@@ -183,8 +183,12 @@ class Directories:
|
||||
"""
|
||||
if fileclasses is None:
|
||||
fileclasses = [fs.File]
|
||||
file_count = 0
|
||||
for path in self._dirs:
|
||||
for file in self._get_files(path, fileclasses=fileclasses, j=j):
|
||||
file_count += 1
|
||||
if type(j) != job.NullJob:
|
||||
j.set_progress(-1, tr("Collected {} files to scan").format(file_count))
|
||||
yield file
|
||||
|
||||
def get_folders(self, folderclass=None, j=job.nulljob):
|
||||
@@ -194,9 +198,13 @@ class Directories:
|
||||
"""
|
||||
if folderclass is None:
|
||||
folderclass = fs.Folder
|
||||
folder_count = 0
|
||||
for path in self._dirs:
|
||||
from_folder = folderclass(path)
|
||||
for folder in self._get_folders(from_folder, j):
|
||||
folder_count += 1
|
||||
if type(j) != job.NullJob:
|
||||
j.set_progress(-1, tr("Collected {} folders to scan").format(folder_count))
|
||||
yield folder
|
||||
|
||||
def get_state(self, path):
|
||||
@@ -207,9 +215,9 @@ 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) or DirectoryState.NORMAL
|
||||
# Save non-default states in cache, necessary for _get_files()
|
||||
if state != DirectoryState.Normal:
|
||||
if state != DirectoryState.NORMAL:
|
||||
self.states[path] = state
|
||||
return state
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ from hscommon.jobprogress import job
|
||||
) = range(3)
|
||||
|
||||
JOB_REFRESH_RATE = 100
|
||||
PROGRESS_MESSAGE = tr("%d matches found from %d groups")
|
||||
|
||||
|
||||
def getwords(s):
|
||||
@@ -106,14 +107,14 @@ def compare_fields(first, second, flags=()):
|
||||
# We don't want to remove field directly in the list. We must work on a copy.
|
||||
second = second[:]
|
||||
for field1 in first:
|
||||
max = 0
|
||||
max_score = 0
|
||||
matched_field = None
|
||||
for field2 in second:
|
||||
r = compare(field1, field2, flags)
|
||||
if r > max:
|
||||
max = r
|
||||
if r > max_score:
|
||||
max_score = r
|
||||
matched_field = field2
|
||||
results.append(max)
|
||||
results.append(max_score)
|
||||
if matched_field:
|
||||
second.remove(matched_field)
|
||||
else:
|
||||
@@ -248,10 +249,11 @@ def getmatches(
|
||||
match_flags.append(MATCH_SIMILAR_WORDS)
|
||||
if no_field_order:
|
||||
match_flags.append(NO_FIELD_ORDER)
|
||||
j.start_job(len(word_dict), tr("0 matches found"))
|
||||
j.start_job(len(word_dict), PROGRESS_MESSAGE % (0, 0))
|
||||
compared = defaultdict(set)
|
||||
result = []
|
||||
try:
|
||||
word_count = 0
|
||||
# This whole 'popping' thing is there to avoid taking too much memory at the same time.
|
||||
while word_dict:
|
||||
items = word_dict.popitem()[1]
|
||||
@@ -266,7 +268,8 @@ def getmatches(
|
||||
result.append(m)
|
||||
if len(result) >= LIMIT:
|
||||
return result
|
||||
j.add_progress(desc=tr("%d matches found") % len(result))
|
||||
word_count += 1
|
||||
j.add_progress(desc=PROGRESS_MESSAGE % (len(result), word_count))
|
||||
except MemoryError:
|
||||
# This is the place where the memory usage is at its peak during the scan.
|
||||
# Just continue the process with an incomplete list of matches.
|
||||
@@ -285,17 +288,21 @@ def getmatches_by_contents(files, bigsize=0, j=job.nulljob):
|
||||
"""
|
||||
size2files = defaultdict(set)
|
||||
for f in files:
|
||||
if f.size:
|
||||
size2files[f.size].add(f)
|
||||
del files
|
||||
possible_matches = [files for files in size2files.values() if len(files) > 1]
|
||||
del size2files
|
||||
result = []
|
||||
j.start_job(len(possible_matches), tr("0 matches found"))
|
||||
j.start_job(len(possible_matches), PROGRESS_MESSAGE % (0, 0))
|
||||
group_count = 0
|
||||
for group in possible_matches:
|
||||
for first, second in itertools.combinations(group, 2):
|
||||
if first.is_ref and second.is_ref:
|
||||
continue # Don't spend time comparing two ref pics together.
|
||||
if first.size == 0 and second.size == 0:
|
||||
# skip md5 for zero length files
|
||||
result.append(Match(first, second, 100))
|
||||
continue
|
||||
if first.md5partial == second.md5partial:
|
||||
if bigsize > 0 and first.size > bigsize:
|
||||
if first.md5samples == second.md5samples:
|
||||
@@ -303,7 +310,8 @@ def getmatches_by_contents(files, bigsize=0, j=job.nulljob):
|
||||
else:
|
||||
if first.md5 == second.md5:
|
||||
result.append(Match(first, second, 100))
|
||||
j.add_progress(desc=tr("%d matches found") % len(result))
|
||||
group_count += 1
|
||||
j.add_progress(desc=PROGRESS_MESSAGE % (len(result), group_count))
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@@ -150,10 +150,7 @@ class ExcludeList(Markable):
|
||||
# @timer
|
||||
@memoize
|
||||
def _do_compile(self, expr):
|
||||
try:
|
||||
return re.compile(expr)
|
||||
except Exception as e:
|
||||
raise (e)
|
||||
|
||||
# @timer
|
||||
# @memoize # probably not worth memoizing this one if we memoize the above
|
||||
@@ -235,7 +232,7 @@ class ExcludeList(Markable):
|
||||
# This exception should never be ignored
|
||||
raise AlreadyThereException()
|
||||
if regex in forbidden_regexes:
|
||||
raise Exception("Forbidden (dangerous) expression.")
|
||||
raise ValueError("Forbidden (dangerous) expression.")
|
||||
|
||||
iscompilable, exception, compiled = self.compile_re(regex)
|
||||
if not iscompilable and not forced:
|
||||
@@ -510,7 +507,6 @@ if ISWINDOWS:
|
||||
def has_sep(regexp):
|
||||
return "\\" + sep in regexp
|
||||
|
||||
|
||||
else:
|
||||
|
||||
def has_sep(regexp):
|
||||
|
||||
139
core/fs.py
139
core/fs.py
@@ -14,7 +14,11 @@
|
||||
import hashlib
|
||||
from math import floor
|
||||
import logging
|
||||
import sqlite3
|
||||
from threading import Lock
|
||||
from typing import Any
|
||||
|
||||
from hscommon.path import Path
|
||||
from hscommon.util import nonone, get_file_ext
|
||||
|
||||
__all__ = [
|
||||
@@ -78,6 +82,82 @@ class OperationError(FSError):
|
||||
cls_message = "Operation on '{name}' failed."
|
||||
|
||||
|
||||
class FilesDB:
|
||||
|
||||
create_table_query = "CREATE TABLE IF NOT EXISTS files (path TEXT PRIMARY KEY, size INTEGER, mtime_ns INTEGER, entry_dt DATETIME, md5 BLOB, md5partial BLOB)"
|
||||
drop_table_query = "DROP TABLE files;"
|
||||
select_query = "SELECT {key} FROM files WHERE path=:path AND size=:size and mtime_ns=:mtime_ns"
|
||||
insert_query = """
|
||||
INSERT INTO files (path, size, mtime_ns, entry_dt, {key}) VALUES (:path, :size, :mtime_ns, datetime('now'), :value)
|
||||
ON CONFLICT(path) DO UPDATE SET size=:size, mtime_ns=:mtime_ns, entry_dt=datetime('now'), {key}=:value;
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.conn = None
|
||||
self.cur = None
|
||||
self.lock = None
|
||||
|
||||
def connect(self, path):
|
||||
# type: (str, ) -> None
|
||||
|
||||
self.conn = sqlite3.connect(path, check_same_thread=False)
|
||||
self.cur = self.conn.cursor()
|
||||
self.cur.execute(self.create_table_query)
|
||||
self.lock = Lock()
|
||||
|
||||
def clear(self):
|
||||
# type: () -> None
|
||||
|
||||
with self.lock:
|
||||
self.cur.execute(self.drop_table_query)
|
||||
self.cur.execute(self.create_table_query)
|
||||
|
||||
def get(self, path, key):
|
||||
# type: (Path, str) -> bytes
|
||||
|
||||
stat = path.stat()
|
||||
size = stat.st_size
|
||||
mtime_ns = stat.st_mtime_ns
|
||||
|
||||
with self.lock:
|
||||
self.cur.execute(self.select_query.format(key=key), {"path": str(path), "size": size, "mtime_ns": mtime_ns})
|
||||
result = self.cur.fetchone()
|
||||
|
||||
if result:
|
||||
return result[0]
|
||||
|
||||
return None
|
||||
|
||||
def put(self, path, key, value):
|
||||
# type: (Path, str, Any) -> None
|
||||
|
||||
stat = path.stat()
|
||||
size = stat.st_size
|
||||
mtime_ns = stat.st_mtime_ns
|
||||
|
||||
with self.lock:
|
||||
self.cur.execute(
|
||||
self.insert_query.format(key=key),
|
||||
{"path": str(path), "size": size, "mtime_ns": mtime_ns, "value": value},
|
||||
)
|
||||
|
||||
def commit(self):
|
||||
# type: () -> None
|
||||
|
||||
with self.lock:
|
||||
self.conn.commit()
|
||||
|
||||
def close(self):
|
||||
# type: () -> None
|
||||
|
||||
with self.lock:
|
||||
self.cur.close()
|
||||
self.conn.close()
|
||||
|
||||
|
||||
filesdb = FilesDB() # Singleton
|
||||
|
||||
|
||||
class File:
|
||||
"""Represents a file and holds metadata to be used for scanning."""
|
||||
|
||||
@@ -107,10 +187,32 @@ class File:
|
||||
result = self.INITIAL_INFO[attrname]
|
||||
return result
|
||||
|
||||
def _calc_md5(self):
|
||||
# type: () -> bytes
|
||||
|
||||
with self.path.open("rb") as fp:
|
||||
md5 = hashlib.md5()
|
||||
# The goal here is to not run out of memory on really big files. However, the chunk
|
||||
# size has to be large enough so that the python loop isn't too costly in terms of
|
||||
# CPU.
|
||||
CHUNK_SIZE = 1024 * 1024 # 1 mb
|
||||
filedata = fp.read(CHUNK_SIZE)
|
||||
while filedata:
|
||||
md5.update(filedata)
|
||||
filedata = fp.read(CHUNK_SIZE)
|
||||
return md5.digest()
|
||||
|
||||
def _calc_md5partial(self):
|
||||
# type: () -> bytes
|
||||
|
||||
# This offset is where we should start reading the file to get a partial md5
|
||||
# For audio file, it should be where audio data starts
|
||||
def _get_md5partial_offset_and_size(self):
|
||||
return (0x4000, 0x4000) # 16Kb
|
||||
offset, size = (0x4000, 0x4000)
|
||||
|
||||
with self.path.open("rb") as fp:
|
||||
fp.seek(offset)
|
||||
partialdata = fp.read(size)
|
||||
return hashlib.md5(partialdata).digest()
|
||||
|
||||
def _read_info(self, field):
|
||||
# print(f"_read_info({field}) for {self}")
|
||||
@@ -120,28 +222,20 @@ class File:
|
||||
self.mtime = nonone(stats.st_mtime, 0)
|
||||
elif field == "md5partial":
|
||||
try:
|
||||
with self.path.open("rb") as fp:
|
||||
offset, size = self._get_md5partial_offset_and_size()
|
||||
fp.seek(offset)
|
||||
partialdata = fp.read(size)
|
||||
md5 = hashlib.md5(partialdata)
|
||||
self.md5partial = md5.digest()
|
||||
except Exception:
|
||||
pass
|
||||
self.md5partial = filesdb.get(self.path, "md5partial")
|
||||
if self.md5partial is None:
|
||||
self.md5partial = self._calc_md5partial()
|
||||
filesdb.put(self.path, "md5partial", self.md5partial)
|
||||
except Exception as e:
|
||||
logging.warning("Couldn't get md5partial for %s: %s", self.path, e)
|
||||
elif field == "md5":
|
||||
try:
|
||||
with self.path.open("rb") as fp:
|
||||
md5 = hashlib.md5()
|
||||
filedata = fp.read(CHUNK_SIZE)
|
||||
while filedata:
|
||||
md5.update(filedata)
|
||||
filedata = fp.read(CHUNK_SIZE)
|
||||
# FIXME For python 3.8 and later
|
||||
# while filedata := fp.read(CHUNK_SIZE):
|
||||
# md5.update(filedata)
|
||||
self.md5 = md5.digest()
|
||||
except Exception:
|
||||
pass
|
||||
self.md5 = filesdb.get(self.path, "md5")
|
||||
if self.md5 is None:
|
||||
self.md5 = self._calc_md5()
|
||||
filesdb.put(self.path, "md5", self.md5)
|
||||
except Exception as e:
|
||||
logging.warning("Couldn't get md5 for %s: %s", self.path, e)
|
||||
elif field == "md5samples":
|
||||
try:
|
||||
with self.path.open("rb") as fp:
|
||||
@@ -168,7 +262,6 @@ class File:
|
||||
setattr(self, field, md5.digest())
|
||||
except Exception as e:
|
||||
logging.error(f"Error computing md5samples: {e}")
|
||||
pass
|
||||
|
||||
def _read_all_info(self, attrnames=None):
|
||||
"""Cache all possible info.
|
||||
|
||||
@@ -15,16 +15,21 @@ class DupeGuruGUIObject(Listener):
|
||||
self.app = app
|
||||
|
||||
def directories_changed(self):
|
||||
# Implemented in child classes
|
||||
pass
|
||||
|
||||
def dupes_selected(self):
|
||||
# Implemented in child classes
|
||||
pass
|
||||
|
||||
def marking_changed(self):
|
||||
# Implemented in child classes
|
||||
pass
|
||||
|
||||
def results_changed(self):
|
||||
# Implemented in child classes
|
||||
pass
|
||||
|
||||
def results_changed_but_keep_selection(self):
|
||||
# Implemented in child classes
|
||||
pass
|
||||
|
||||
@@ -44,5 +44,4 @@ class DetailsPanel(GUIObject, DupeGuruGUIObject):
|
||||
|
||||
# --- Event Handlers
|
||||
def dupes_selected(self):
|
||||
self._refresh()
|
||||
self.view.refresh()
|
||||
self._view_updated()
|
||||
|
||||
@@ -11,7 +11,7 @@ from hscommon.gui.tree import Tree, Node
|
||||
from ..directories import DirectoryState
|
||||
from .base import DupeGuruGUIObject
|
||||
|
||||
STATE_ORDER = [DirectoryState.Normal, DirectoryState.Reference, DirectoryState.Excluded]
|
||||
STATE_ORDER = [DirectoryState.NORMAL, DirectoryState.REFERENCE, DirectoryState.EXCLUDED]
|
||||
|
||||
|
||||
# Lazily loads children
|
||||
@@ -86,9 +86,9 @@ class DirectoryTree(Tree, DupeGuruGUIObject):
|
||||
else:
|
||||
# All selected nodes or on second-or-more level, exclude them.
|
||||
nodes = self.selected_nodes
|
||||
newstate = DirectoryState.Excluded
|
||||
if all(node.state == DirectoryState.Excluded for node in nodes):
|
||||
newstate = DirectoryState.Normal
|
||||
newstate = DirectoryState.EXCLUDED
|
||||
if all(node.state == DirectoryState.EXCLUDED for node in nodes):
|
||||
newstate = DirectoryState.NORMAL
|
||||
for node in nodes:
|
||||
node.state = newstate
|
||||
|
||||
@@ -103,5 +103,4 @@ class DirectoryTree(Tree, DupeGuruGUIObject):
|
||||
|
||||
# --- Event Handlers
|
||||
def directories_changed(self):
|
||||
self._refresh()
|
||||
self.view.refresh()
|
||||
self._view_updated()
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
# from hscommon.trans import tr
|
||||
from .exclude_list_table import ExcludeListTable
|
||||
from core.exclude import has_sep
|
||||
from os import sep
|
||||
@@ -47,10 +46,7 @@ class ExcludeListDialogCore:
|
||||
return False
|
||||
|
||||
def add(self, regex):
|
||||
try:
|
||||
self.exclude_list.add(regex)
|
||||
except Exception as e:
|
||||
raise (e)
|
||||
self.exclude_list.mark(regex)
|
||||
self.exclude_list_table.add(regex)
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ class ExcludeListTable(GUITable, DupeGuruGUIObject):
|
||||
def __init__(self, exclude_list_dialog, app):
|
||||
GUITable.__init__(self)
|
||||
DupeGuruGUIObject.__init__(self, app)
|
||||
self.columns = Columns(self)
|
||||
self._columns = Columns(self)
|
||||
self.dialog = exclude_list_dialog
|
||||
|
||||
def rename_selected(self, newname):
|
||||
|
||||
@@ -24,7 +24,7 @@ class IgnoreListDialog:
|
||||
return
|
||||
msg = tr("Do you really want to remove all %d items from the ignore list?") % len(self.ignore_list)
|
||||
if self.app.view.ask_yes_no(msg):
|
||||
self.ignore_list.Clear()
|
||||
self.ignore_list.clear()
|
||||
self.refresh()
|
||||
|
||||
def refresh(self):
|
||||
|
||||
@@ -22,7 +22,7 @@ class IgnoreListTable(GUITable):
|
||||
|
||||
def __init__(self, ignore_list_dialog):
|
||||
GUITable.__init__(self)
|
||||
self.columns = Columns(self)
|
||||
self._columns = Columns(self)
|
||||
self.view = None
|
||||
self.dialog = ignore_list_dialog
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ class ProblemTable(GUITable):
|
||||
|
||||
def __init__(self, problem_dialog):
|
||||
GUITable.__init__(self)
|
||||
self.columns = Columns(self)
|
||||
self._columns = Columns(self)
|
||||
self.dialog = problem_dialog
|
||||
|
||||
# --- Override
|
||||
|
||||
@@ -82,7 +82,7 @@ class ResultTable(GUITable, DupeGuruGUIObject):
|
||||
def __init__(self, app):
|
||||
GUITable.__init__(self)
|
||||
DupeGuruGUIObject.__init__(self, app)
|
||||
self.columns = Columns(self, prefaccess=app, savename="ResultTable")
|
||||
self._columns = Columns(self, prefaccess=app, savename="ResultTable")
|
||||
self._power_marker = False
|
||||
self._delta_values = False
|
||||
self._sort_descriptors = ("name", True)
|
||||
@@ -190,4 +190,4 @@ class ResultTable(GUITable, DupeGuruGUIObject):
|
||||
self.view.refresh()
|
||||
|
||||
def save_session(self):
|
||||
self.columns.save_columns()
|
||||
self._columns.save_columns()
|
||||
|
||||
@@ -20,8 +20,7 @@ class IgnoreList:
|
||||
|
||||
# ---Override
|
||||
def __init__(self):
|
||||
self._ignored = {}
|
||||
self._count = 0
|
||||
self.clear()
|
||||
|
||||
def __iter__(self):
|
||||
for first, seconds in self._ignored.items():
|
||||
@@ -32,7 +31,7 @@ class IgnoreList:
|
||||
return self._count
|
||||
|
||||
# ---Public
|
||||
def AreIgnored(self, first, second):
|
||||
def are_ignored(self, first, second):
|
||||
def do_check(first, second):
|
||||
try:
|
||||
matches = self._ignored[first]
|
||||
@@ -42,23 +41,23 @@ class IgnoreList:
|
||||
|
||||
return do_check(first, second) or do_check(second, first)
|
||||
|
||||
def Clear(self):
|
||||
def clear(self):
|
||||
self._ignored = {}
|
||||
self._count = 0
|
||||
|
||||
def Filter(self, func):
|
||||
def filter(self, func):
|
||||
"""Applies a filter on all ignored items, and remove all matches where func(first,second)
|
||||
doesn't return True.
|
||||
"""
|
||||
filtered = IgnoreList()
|
||||
for first, second in self:
|
||||
if func(first, second):
|
||||
filtered.Ignore(first, second)
|
||||
filtered.ignore(first, second)
|
||||
self._ignored = filtered._ignored
|
||||
self._count = filtered._count
|
||||
|
||||
def Ignore(self, first, second):
|
||||
if self.AreIgnored(first, second):
|
||||
def ignore(self, first, second):
|
||||
if self.are_ignored(first, second):
|
||||
return
|
||||
try:
|
||||
matches = self._ignored[first]
|
||||
@@ -88,8 +87,7 @@ class IgnoreList:
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
if not inner(first, second):
|
||||
if not inner(second, first):
|
||||
if not inner(first, second) and not inner(second, first):
|
||||
raise ValueError()
|
||||
|
||||
def load_from_xml(self, infile):
|
||||
@@ -110,7 +108,7 @@ class IgnoreList:
|
||||
for sfn in subfile_elems:
|
||||
subfile_path = sfn.get("path")
|
||||
if subfile_path:
|
||||
self.Ignore(file_path, subfile_path)
|
||||
self.ignore(file_path, subfile_path)
|
||||
|
||||
def save_to_xml(self, outfile):
|
||||
"""Create a XML file that can be used by load_from_xml.
|
||||
|
||||
@@ -17,9 +17,11 @@ class Markable:
|
||||
# in self.__marked, and is not affected by __inverted. Thus, self.mark while __inverted
|
||||
# is True will launch _DidUnmark.
|
||||
def _did_mark(self, o):
|
||||
# Implemented in child classes
|
||||
pass
|
||||
|
||||
def _did_unmark(self, o):
|
||||
# Implemented in child classes
|
||||
pass
|
||||
|
||||
def _get_markable_count(self):
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from hsaudiotag import auto
|
||||
import mutagen
|
||||
from hscommon.util import get_file_ext, format_size, format_time
|
||||
|
||||
from core.util import format_timestamp, format_perc, format_words, format_dupe_count
|
||||
@@ -26,6 +26,9 @@ TAG_FIELDS = {
|
||||
"comment",
|
||||
}
|
||||
|
||||
# This is a temporary workaround for migration from hsaudiotag for the can_handle method
|
||||
SUPPORTED_EXTS = {"mp3", "wma", "m4a", "m4p", "ogg", "flac", "aif", "aiff", "aifc"}
|
||||
|
||||
|
||||
class MusicFile(fs.File):
|
||||
INITIAL_INFO = fs.File.INITIAL_INFO.copy()
|
||||
@@ -50,7 +53,7 @@ class MusicFile(fs.File):
|
||||
def can_handle(cls, path):
|
||||
if not fs.File.can_handle(path):
|
||||
return False
|
||||
return get_file_ext(path.name) in auto.EXT2CLASS
|
||||
return get_file_ext(path.name) in SUPPORTED_EXTS
|
||||
|
||||
def get_display_info(self, group, delta):
|
||||
size = self.size
|
||||
@@ -95,21 +98,23 @@ class MusicFile(fs.File):
|
||||
}
|
||||
|
||||
def _get_md5partial_offset_and_size(self):
|
||||
f = auto.File(str(self.path))
|
||||
return (f.audio_offset, f.audio_size)
|
||||
# No longer calculating the offset and audio size, just whole file
|
||||
size = self.path.stat().st_size
|
||||
return (0, size)
|
||||
|
||||
def _read_info(self, field):
|
||||
fs.File._read_info(self, field)
|
||||
if field in TAG_FIELDS:
|
||||
f = auto.File(str(self.path))
|
||||
self.audiosize = f.audio_size
|
||||
self.bitrate = f.bitrate
|
||||
self.duration = f.duration
|
||||
self.samplerate = f.sample_rate
|
||||
self.artist = f.artist
|
||||
self.album = f.album
|
||||
self.title = f.title
|
||||
self.genre = f.genre
|
||||
self.comment = f.comment
|
||||
self.year = f.year
|
||||
self.track = f.track
|
||||
# The various conversions here are to make this look like the previous implementation
|
||||
file = mutagen.File(str(self.path), easy=True)
|
||||
self.audiosize = self.path.stat().st_size
|
||||
self.bitrate = file.info.bitrate / 1000
|
||||
self.duration = file.info.length
|
||||
self.samplerate = file.info.sample_rate
|
||||
self.artist = ", ".join(file.tags.get("artist") or [])
|
||||
self.album = ", ".join(file.tags.get("album") or [])
|
||||
self.title = ", ".join(file.tags.get("title") or [])
|
||||
self.genre = ", ".join(file.tags.get("genre") or [])
|
||||
self.comment = ", ".join(file.tags.get("comment") or [""])
|
||||
self.year = ", ".join(file.tags.get("date") or [])
|
||||
self.track = (file.tags.get("tracknumber") or [""])[0]
|
||||
|
||||
@@ -17,9 +17,9 @@ class ScannerME(ScannerBase):
|
||||
@staticmethod
|
||||
def get_scan_options():
|
||||
return [
|
||||
ScanOption(ScanType.Filename, tr("Filename")),
|
||||
ScanOption(ScanType.Fields, tr("Filename - Fields")),
|
||||
ScanOption(ScanType.FieldsNoOrder, tr("Filename - Fields (No Order)")),
|
||||
ScanOption(ScanType.Tag, tr("Tags")),
|
||||
ScanOption(ScanType.Contents, tr("Contents")),
|
||||
ScanOption(ScanType.FILENAME, tr("Filename")),
|
||||
ScanOption(ScanType.FIELDS, tr("Filename - Fields")),
|
||||
ScanOption(ScanType.FIELDSNOORDER, tr("Filename - Fields (No Order)")),
|
||||
ScanOption(ScanType.TAG, tr("Tags")),
|
||||
ScanOption(ScanType.CONTENTS, tr("Contents")),
|
||||
]
|
||||
|
||||
@@ -193,8 +193,8 @@ class TIFF_file:
|
||||
self.s2nfunc = s2n_intel if self.endian == INTEL_ENDIAN else s2n_motorola
|
||||
|
||||
def s2n(self, offset, length, signed=0, debug=False):
|
||||
slice = self.data[offset : offset + length]
|
||||
val = self.s2nfunc(slice)
|
||||
data_slice = self.data[offset : offset + length]
|
||||
val = self.s2nfunc(data_slice)
|
||||
# Sign extension ?
|
||||
if signed:
|
||||
msb = 1 << (8 * length - 1)
|
||||
@@ -206,7 +206,7 @@ class TIFF_file:
|
||||
"Slice for offset %d length %d: %r and value: %d",
|
||||
offset,
|
||||
length,
|
||||
slice,
|
||||
data_slice,
|
||||
val,
|
||||
)
|
||||
return val
|
||||
@@ -236,10 +236,10 @@ class TIFF_file:
|
||||
for i in range(entries):
|
||||
entry = ifd + 2 + 12 * i
|
||||
tag = self.s2n(entry, 2)
|
||||
type = self.s2n(entry + 2, 2)
|
||||
if not 1 <= type <= 10:
|
||||
entry_type = self.s2n(entry + 2, 2)
|
||||
if not 1 <= entry_type <= 10:
|
||||
continue # not handled
|
||||
typelen = [1, 1, 2, 4, 8, 1, 1, 2, 4, 8][type - 1]
|
||||
typelen = [1, 1, 2, 4, 8, 1, 1, 2, 4, 8][entry_type - 1]
|
||||
count = self.s2n(entry + 4, 4)
|
||||
if count > MAX_COUNT:
|
||||
logging.debug("Probably corrupt. Aborting.")
|
||||
@@ -247,14 +247,14 @@ class TIFF_file:
|
||||
offset = entry + 8
|
||||
if count * typelen > 4:
|
||||
offset = self.s2n(offset, 4)
|
||||
if type == 2:
|
||||
if entry_type == 2:
|
||||
# Special case: nul-terminated ASCII string
|
||||
values = str(self.data[offset : offset + count - 1], encoding="latin-1")
|
||||
else:
|
||||
values = []
|
||||
signed = type == 6 or type >= 8
|
||||
for j in range(count):
|
||||
if type in {5, 10}:
|
||||
signed = entry_type == 6 or entry_type >= 8
|
||||
for _ in range(count):
|
||||
if entry_type in {5, 10}:
|
||||
# The type is either 5 or 10
|
||||
value_j = Fraction(self.s2n(offset, 4, signed), self.s2n(offset + 4, 4, signed))
|
||||
else:
|
||||
@@ -263,7 +263,7 @@ class TIFF_file:
|
||||
values.append(value_j)
|
||||
offset = offset + typelen
|
||||
# Now "values" is either a string or an array
|
||||
a.append((tag, type, values))
|
||||
a.append((tag, entry_type, values))
|
||||
return a
|
||||
|
||||
|
||||
@@ -298,7 +298,7 @@ def get_fields(fp):
|
||||
T = TIFF_file(data)
|
||||
# There may be more than one IFD per file, but we only read the first one because others are
|
||||
# most likely thumbnails.
|
||||
main_IFD_offset = T.first_IFD()
|
||||
main_ifd_offset = T.first_IFD()
|
||||
result = {}
|
||||
|
||||
def add_tag_to_result(tag, values):
|
||||
@@ -310,8 +310,8 @@ def get_fields(fp):
|
||||
return # don't overwrite data
|
||||
result[stag] = values
|
||||
|
||||
logging.debug("IFD at offset %d", main_IFD_offset)
|
||||
IFD = T.dump_IFD(main_IFD_offset)
|
||||
logging.debug("IFD at offset %d", main_ifd_offset)
|
||||
IFD = T.dump_IFD(main_ifd_offset)
|
||||
exif_off = gps_off = 0
|
||||
for tag, type, values in IFD:
|
||||
if tag == 0x8769:
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* 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"
|
||||
@@ -17,8 +17,7 @@ static PyObject *DifferentBlockCountError;
|
||||
/* Returns a 3 sized tuple containing the mean color of 'image'.
|
||||
* image: a PIL image or crop.
|
||||
*/
|
||||
static PyObject* getblock(PyObject *image)
|
||||
{
|
||||
static PyObject *getblock(PyObject *image) {
|
||||
int i, totr, totg, totb;
|
||||
Py_ssize_t pixel_count;
|
||||
PyObject *ppixels;
|
||||
@@ -30,7 +29,7 @@ static PyObject* getblock(PyObject *image)
|
||||
}
|
||||
|
||||
pixel_count = PySequence_Length(ppixels);
|
||||
for (i=0; i<pixel_count; i++) {
|
||||
for (i = 0; i < pixel_count; i++) {
|
||||
PyObject *ppixel, *pr, *pg, *pb;
|
||||
int r, g, b;
|
||||
|
||||
@@ -65,8 +64,7 @@ static PyObject* getblock(PyObject *image)
|
||||
/* Returns the difference between the first block and the second.
|
||||
* It returns an absolute sum of the 3 differences (RGB).
|
||||
*/
|
||||
static int diff(PyObject *first, PyObject *second)
|
||||
{
|
||||
static int diff(PyObject *first, PyObject *second) {
|
||||
int r1, g1, b1, r2, b2, g2;
|
||||
PyObject *pr, *pg, *pb;
|
||||
pr = PySequence_ITEM(first, 0);
|
||||
@@ -93,7 +91,7 @@ static int diff(PyObject *first, PyObject *second)
|
||||
}
|
||||
|
||||
PyDoc_STRVAR(block_getblocks2_doc,
|
||||
"Returns a list of blocks (3 sized tuples).\n\
|
||||
"Returns a list of blocks (3 sized tuples).\n\
|
||||
\n\
|
||||
image: A PIL image to base the blocks on.\n\
|
||||
block_count_per_side: This integer determine the number of blocks the function will return.\n\
|
||||
@@ -101,8 +99,7 @@ If it is 10, for example, 100 blocks will be returns (10 width, 10 height). The
|
||||
necessarely cover square areas. The area covered by each block will be proportional to the image\n\
|
||||
itself.\n");
|
||||
|
||||
static PyObject* block_getblocks2(PyObject *self, PyObject *args)
|
||||
{
|
||||
static PyObject *block_getblocks2(PyObject *self, PyObject *args) {
|
||||
int block_count_per_side, width, height, block_width, block_height, ih;
|
||||
PyObject *image;
|
||||
PyObject *pimage_size, *pwidth, *pheight;
|
||||
@@ -128,23 +125,23 @@ static PyObject* block_getblocks2(PyObject *self, PyObject *args)
|
||||
block_width = max(width / block_count_per_side, 1);
|
||||
block_height = max(height / block_count_per_side, 1);
|
||||
|
||||
result = PyList_New(block_count_per_side * block_count_per_side);
|
||||
result = PyList_New((Py_ssize_t)block_count_per_side * block_count_per_side);
|
||||
if (result == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (ih=0; ih<block_count_per_side; ih++) {
|
||||
for (ih = 0; ih < block_count_per_side; ih++) {
|
||||
int top, bottom, iw;
|
||||
top = min(ih*block_height, height-block_height);
|
||||
top = min(ih * block_height, height - block_height);
|
||||
bottom = top + block_height;
|
||||
for (iw=0; iw<block_count_per_side; iw++) {
|
||||
for (iw = 0; iw < block_count_per_side; iw++) {
|
||||
int left, right;
|
||||
PyObject *pbox;
|
||||
PyObject *pmethodname;
|
||||
PyObject *pcrop;
|
||||
PyObject *pblock;
|
||||
|
||||
left = min(iw*block_width, width-block_width);
|
||||
left = min(iw * block_width, width - block_width);
|
||||
right = left + block_width;
|
||||
pbox = inttuple(4, left, top, right, bottom);
|
||||
pmethodname = PyUnicode_FromString("crop");
|
||||
@@ -161,7 +158,7 @@ static PyObject* block_getblocks2(PyObject *self, PyObject *args)
|
||||
Py_DECREF(result);
|
||||
return NULL;
|
||||
}
|
||||
PyList_SET_ITEM(result, ih*block_count_per_side+iw, pblock);
|
||||
PyList_SET_ITEM(result, ih * block_count_per_side + iw, pblock);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,19 +166,19 @@ static PyObject* block_getblocks2(PyObject *self, PyObject *args)
|
||||
}
|
||||
|
||||
PyDoc_STRVAR(block_avgdiff_doc,
|
||||
"Returns the average diff between first blocks and seconds.\n\
|
||||
"Returns the average diff between first blocks and seconds.\n\
|
||||
\n\
|
||||
If the result surpasses limit, limit + 1 is returned, except if less than min_iterations\n\
|
||||
iterations have been made in the blocks.\n");
|
||||
|
||||
static PyObject* block_avgdiff(PyObject *self, PyObject *args)
|
||||
{
|
||||
static PyObject *block_avgdiff(PyObject *self, PyObject *args) {
|
||||
PyObject *first, *second;
|
||||
int limit, min_iterations;
|
||||
Py_ssize_t count;
|
||||
int sum, i, result;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "OOii", &first, &second, &limit, &min_iterations)) {
|
||||
if (!PyArg_ParseTuple(args, "OOii", &first, &second, &limit,
|
||||
&min_iterations)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -196,7 +193,7 @@ static PyObject* block_avgdiff(PyObject *self, PyObject *args)
|
||||
}
|
||||
|
||||
sum = 0;
|
||||
for (i=0; i<count; i++) {
|
||||
for (i = 0; i < count; i++) {
|
||||
int iteration_count;
|
||||
PyObject *item1, *item2;
|
||||
|
||||
@@ -206,7 +203,8 @@ static PyObject* block_avgdiff(PyObject *self, PyObject *args)
|
||||
sum += diff(item1, item2);
|
||||
Py_DECREF(item1);
|
||||
Py_DECREF(item2);
|
||||
if ((sum > limit*iteration_count) && (iteration_count >= min_iterations)) {
|
||||
if ((sum > limit * iteration_count) &&
|
||||
(iteration_count >= min_iterations)) {
|
||||
return PyLong_FromLong(limit + 1);
|
||||
}
|
||||
}
|
||||
@@ -224,8 +222,7 @@ static PyMethodDef BlockMethods[] = {
|
||||
{NULL, NULL, 0, NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
static struct PyModuleDef BlockDef = {
|
||||
PyModuleDef_HEAD_INIT,
|
||||
static struct PyModuleDef BlockDef = {PyModuleDef_HEAD_INIT,
|
||||
"_block",
|
||||
NULL,
|
||||
-1,
|
||||
@@ -233,12 +230,9 @@ static struct PyModuleDef BlockDef = {
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL
|
||||
};
|
||||
NULL};
|
||||
|
||||
PyObject *
|
||||
PyInit__block(void)
|
||||
{
|
||||
PyObject *PyInit__block(void) {
|
||||
PyObject *m = PyModule_Create(&BlockDef);
|
||||
if (m == NULL) {
|
||||
return NULL;
|
||||
@@ -246,7 +240,8 @@ PyInit__block(void)
|
||||
|
||||
NoBlocksError = PyErr_NewException("_block.NoBlocksError", NULL, NULL);
|
||||
PyModule_AddObject(m, "NoBlocksError", NoBlocksError);
|
||||
DifferentBlockCountError = PyErr_NewException("_block.DifferentBlockCountError", NULL, NULL);
|
||||
DifferentBlockCountError =
|
||||
PyErr_NewException("_block.DifferentBlockCountError", NULL, NULL);
|
||||
PyModule_AddObject(m, "DifferentBlockCountError", DifferentBlockCountError);
|
||||
|
||||
return m;
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
#include "common.h"
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreGraphics/CoreGraphics.h>
|
||||
#import <ImageIO/ImageIO.h>
|
||||
|
||||
#define RADIANS( degrees ) ( degrees * M_PI / 180 )
|
||||
|
||||
|
||||
@@ -18,12 +18,12 @@ class ScannerPE(Scanner):
|
||||
@staticmethod
|
||||
def get_scan_options():
|
||||
return [
|
||||
ScanOption(ScanType.FuzzyBlock, tr("Contents")),
|
||||
ScanOption(ScanType.ExifTimestamp, tr("EXIF Timestamp")),
|
||||
ScanOption(ScanType.FUZZYBLOCK, tr("Contents")),
|
||||
ScanOption(ScanType.EXIFTIMESTAMP, tr("EXIF Timestamp")),
|
||||
]
|
||||
|
||||
def _getmatches(self, files, j):
|
||||
if self.scan_type == ScanType.FuzzyBlock:
|
||||
if self.scan_type == ScanType.FUZZYBLOCK:
|
||||
return matchblock.getmatches(
|
||||
files,
|
||||
cache_path=self.cache_path,
|
||||
@@ -31,7 +31,7 @@ class ScannerPE(Scanner):
|
||||
match_scaled=self.match_scaled,
|
||||
j=j,
|
||||
)
|
||||
elif self.scan_type == ScanType.ExifTimestamp:
|
||||
elif self.scan_type == ScanType.EXIFTIMESTAMP:
|
||||
return matchexif.getmatches(files, self.match_scaled, j)
|
||||
else:
|
||||
raise Exception("Invalid scan type")
|
||||
raise ValueError("Invalid scan type")
|
||||
|
||||
@@ -21,16 +21,16 @@ from . import engine
|
||||
|
||||
|
||||
class ScanType:
|
||||
Filename = 0
|
||||
Fields = 1
|
||||
FieldsNoOrder = 2
|
||||
Tag = 3
|
||||
Folders = 4
|
||||
Contents = 5
|
||||
FILENAME = 0
|
||||
FIELDS = 1
|
||||
FIELDSNOORDER = 2
|
||||
TAG = 3
|
||||
FOLDERS = 4
|
||||
CONTENTS = 5
|
||||
|
||||
# PE
|
||||
FuzzyBlock = 10
|
||||
ExifTimestamp = 11
|
||||
FUZZYBLOCK = 10
|
||||
EXIFTIMESTAMP = 11
|
||||
|
||||
|
||||
ScanOption = namedtuple("ScanOption", "scan_type label")
|
||||
@@ -77,16 +77,23 @@ class Scanner:
|
||||
self.discarded_file_count = 0
|
||||
|
||||
def _getmatches(self, files, j):
|
||||
if self.size_threshold or self.scan_type in {
|
||||
ScanType.Contents,
|
||||
ScanType.Folders,
|
||||
}:
|
||||
if (
|
||||
self.size_threshold
|
||||
or self.large_size_threshold
|
||||
or self.scan_type
|
||||
in {
|
||||
ScanType.CONTENTS,
|
||||
ScanType.FOLDERS,
|
||||
}
|
||||
):
|
||||
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.scan_type in {ScanType.Contents, ScanType.Folders}:
|
||||
if self.large_size_threshold:
|
||||
files = [f for f in files if f.size <= self.large_size_threshold]
|
||||
if self.scan_type in {ScanType.CONTENTS, ScanType.FOLDERS}:
|
||||
return engine.getmatches_by_contents(files, bigsize=self.big_file_size_threshold, j=j)
|
||||
else:
|
||||
j = j.start_subjob([2, 8])
|
||||
@@ -94,13 +101,13 @@ class Scanner:
|
||||
kw["match_similar_words"] = self.match_similar_words
|
||||
kw["weight_words"] = self.word_weighting
|
||||
kw["min_match_percentage"] = self.min_match_percentage
|
||||
if self.scan_type == ScanType.FieldsNoOrder:
|
||||
self.scan_type = ScanType.Fields
|
||||
if self.scan_type == ScanType.FIELDSNOORDER:
|
||||
self.scan_type = ScanType.FIELDS
|
||||
kw["no_field_order"] = True
|
||||
func = {
|
||||
ScanType.Filename: lambda f: engine.getwords(rem_file_ext(f.name)),
|
||||
ScanType.Fields: lambda f: engine.getfields(rem_file_ext(f.name)),
|
||||
ScanType.Tag: lambda f: [
|
||||
ScanType.FILENAME: lambda f: engine.getwords(rem_file_ext(f.name)),
|
||||
ScanType.FIELDS: lambda f: engine.getfields(rem_file_ext(f.name)),
|
||||
ScanType.TAG: lambda f: [
|
||||
engine.getwords(str(getattr(f, attrname)))
|
||||
for attrname in SCANNABLE_TAGS
|
||||
if attrname in self.scanned_tags
|
||||
@@ -150,7 +157,7 @@ class Scanner:
|
||||
# "duplicated duplicates if you will). Then, we also don't want mixed file kinds if the
|
||||
# option isn't enabled, we want matches for which both files exist and, lastly, we don't
|
||||
# want matches with both files as ref.
|
||||
if self.scan_type == ScanType.Folders and matches:
|
||||
if self.scan_type == ScanType.FOLDERS and matches:
|
||||
allpath = {m.first.path for m in matches}
|
||||
allpath |= {m.second.path for m in matches}
|
||||
sortedpaths = sorted(allpath)
|
||||
@@ -167,14 +174,14 @@ class Scanner:
|
||||
matches = [m for m in matches if m.first.path.exists() and m.second.path.exists()]
|
||||
matches = [m for m in matches if not (m.first.is_ref and m.second.is_ref)]
|
||||
if ignore_list:
|
||||
matches = [m for m in matches if not ignore_list.AreIgnored(str(m.first.path), str(m.second.path))]
|
||||
matches = [m for m in matches if not ignore_list.are_ignored(str(m.first.path), str(m.second.path))]
|
||||
logging.info("Grouping matches")
|
||||
groups = engine.get_groups(matches)
|
||||
if self.scan_type in {
|
||||
ScanType.Filename,
|
||||
ScanType.Fields,
|
||||
ScanType.FieldsNoOrder,
|
||||
ScanType.Tag,
|
||||
ScanType.FILENAME,
|
||||
ScanType.FIELDS,
|
||||
ScanType.FIELDSNOORDER,
|
||||
ScanType.TAG,
|
||||
}:
|
||||
matched_files = dedupe([m.first for m in matches] + [m.second for m in matches])
|
||||
self.discarded_file_count = len(matched_files) - sum(len(g) for g in groups)
|
||||
@@ -199,8 +206,9 @@ class Scanner:
|
||||
match_similar_words = False
|
||||
min_match_percentage = 80
|
||||
mix_file_kind = True
|
||||
scan_type = ScanType.Filename
|
||||
scan_type = ScanType.FILENAME
|
||||
scanned_tags = {"artist", "title"}
|
||||
size_threshold = 0
|
||||
large_size_threshold = 0
|
||||
big_file_size_threshold = 0
|
||||
word_weighting = False
|
||||
|
||||
@@ -13,7 +13,7 @@ class ScannerSE(ScannerBase):
|
||||
@staticmethod
|
||||
def get_scan_options():
|
||||
return [
|
||||
ScanOption(ScanType.Filename, tr("Filename")),
|
||||
ScanOption(ScanType.Contents, tr("Contents")),
|
||||
ScanOption(ScanType.Folders, tr("Folders")),
|
||||
ScanOption(ScanType.FILENAME, tr("Filename")),
|
||||
ScanOption(ScanType.CONTENTS, tr("Contents")),
|
||||
ScanOption(ScanType.FOLDERS, tr("Folders")),
|
||||
]
|
||||
|
||||
@@ -23,7 +23,7 @@ from ..scanner import ScanType
|
||||
|
||||
def add_fake_files_to_directories(directories, files):
|
||||
directories.get_files = lambda j=None: iter(files)
|
||||
directories._dirs.append("this is just so Scan() doesnt return 3")
|
||||
directories._dirs.append("this is just so Scan() doesn't return 3")
|
||||
|
||||
|
||||
class TestCaseDupeGuru:
|
||||
@@ -43,7 +43,7 @@ class TestCaseDupeGuru:
|
||||
dgapp.apply_filter("()[]\\.|+?^abc")
|
||||
call = dgapp.results.apply_filter.calls[1]
|
||||
eq_("\\(\\)\\[\\]\\\\\\.\\|\\+\\?\\^abc", call["filter_str"])
|
||||
dgapp.apply_filter("(*)") # In "simple mode", we want the * to behave as a wilcard
|
||||
dgapp.apply_filter("(*)") # In "simple mode", we want the * to behave as a wildcard
|
||||
call = dgapp.results.apply_filter.calls[3]
|
||||
eq_(r"\(.*\)", call["filter_str"])
|
||||
dgapp.options["escape_filter_regexp"] = False
|
||||
@@ -88,14 +88,14 @@ class TestCaseDupeGuru:
|
||||
eq_(1, len(calls))
|
||||
eq_(sourcepath, calls[0]["path"])
|
||||
|
||||
def test_Scan_with_objects_evaluating_to_false(self):
|
||||
def test_scan_with_objects_evaluating_to_false(self):
|
||||
class FakeFile(fs.File):
|
||||
def __bool__(self):
|
||||
return False
|
||||
|
||||
# At some point, any() was used in a wrong way that made Scan() wrongly return 1
|
||||
app = TestApp().app
|
||||
f1, f2 = [FakeFile("foo") for i in range(2)]
|
||||
f1, f2 = [FakeFile("foo") for _ in range(2)]
|
||||
f1.is_ref, f2.is_ref = (False, False)
|
||||
assert not (bool(f1) and bool(f2))
|
||||
add_fake_files_to_directories(app.directories, [f1, f2])
|
||||
@@ -110,7 +110,7 @@ class TestCaseDupeGuru:
|
||||
os.link(str(tmppath["myfile"]), str(tmppath["hardlink"]))
|
||||
app = TestApp().app
|
||||
app.directories.add_path(tmppath)
|
||||
app.options["scan_type"] = ScanType.Contents
|
||||
app.options["scan_type"] = ScanType.CONTENTS
|
||||
app.options["ignore_hardlink_matches"] = True
|
||||
app.start_scanning()
|
||||
eq_(len(app.results.groups), 0)
|
||||
@@ -124,7 +124,7 @@ class TestCaseDupeGuru:
|
||||
assert not dgapp.result_table.rename_selected("foo") # no crash
|
||||
|
||||
|
||||
class TestCaseDupeGuru_clean_empty_dirs:
|
||||
class TestCaseDupeGuruCleanEmptyDirs:
|
||||
@pytest.fixture
|
||||
def do_setup(self, request):
|
||||
monkeypatch = request.getfixturevalue("monkeypatch")
|
||||
@@ -184,7 +184,7 @@ class TestCaseDupeGuruWithResults:
|
||||
tmppath["bar"].mkdir()
|
||||
self.app.directories.add_path(tmppath)
|
||||
|
||||
def test_GetObjects(self, do_setup):
|
||||
def test_get_objects(self, do_setup):
|
||||
objects = self.objects
|
||||
groups = self.groups
|
||||
r = self.rtable[0]
|
||||
@@ -197,7 +197,7 @@ class TestCaseDupeGuruWithResults:
|
||||
assert r._group is groups[1]
|
||||
assert r._dupe is objects[4]
|
||||
|
||||
def test_GetObjects_after_sort(self, do_setup):
|
||||
def test_get_objects_after_sort(self, do_setup):
|
||||
objects = self.objects
|
||||
groups = self.groups[:] # we need an un-sorted reference
|
||||
self.rtable.sort("name", False)
|
||||
@@ -212,7 +212,7 @@ class TestCaseDupeGuruWithResults:
|
||||
# The first 2 dupes have been removed. The 3rd one is a ref. it stays there, in first pos.
|
||||
eq_(self.rtable.selected_indexes, [1]) # no exception
|
||||
|
||||
def test_selectResultNodePaths(self, do_setup):
|
||||
def test_select_result_node_paths(self, do_setup):
|
||||
app = self.app
|
||||
objects = self.objects
|
||||
self.rtable.select([1, 2])
|
||||
@@ -220,7 +220,7 @@ class TestCaseDupeGuruWithResults:
|
||||
assert app.selected_dupes[0] is objects[1]
|
||||
assert app.selected_dupes[1] is objects[2]
|
||||
|
||||
def test_selectResultNodePaths_with_ref(self, do_setup):
|
||||
def test_select_result_node_paths_with_ref(self, do_setup):
|
||||
app = self.app
|
||||
objects = self.objects
|
||||
self.rtable.select([1, 2, 3])
|
||||
@@ -229,7 +229,7 @@ class TestCaseDupeGuruWithResults:
|
||||
assert app.selected_dupes[1] is objects[2]
|
||||
assert app.selected_dupes[2] is self.groups[1].ref
|
||||
|
||||
def test_selectResultNodePaths_after_sort(self, do_setup):
|
||||
def test_select_result_node_paths_after_sort(self, do_setup):
|
||||
app = self.app
|
||||
objects = self.objects
|
||||
groups = self.groups[:] # To keep the old order in memory
|
||||
@@ -256,7 +256,7 @@ class TestCaseDupeGuruWithResults:
|
||||
app.remove_selected()
|
||||
eq_(self.rtable.selected_indexes, []) # no exception
|
||||
|
||||
def test_selectPowerMarkerRows_after_sort(self, do_setup):
|
||||
def test_select_powermarker_rows_after_sort(self, do_setup):
|
||||
app = self.app
|
||||
objects = self.objects
|
||||
self.rtable.power_marker = True
|
||||
@@ -295,7 +295,7 @@ class TestCaseDupeGuruWithResults:
|
||||
app.toggle_selected_mark_state()
|
||||
eq_(app.results.mark_count, 0)
|
||||
|
||||
def test_refreshDetailsWithSelected(self, do_setup):
|
||||
def test_refresh_details_with_selected(self, do_setup):
|
||||
self.rtable.select([1, 4])
|
||||
eq_(self.dpanel.row(0), ("Filename", "bar bleh", "foo bar"))
|
||||
self.dpanel.view.check_gui_calls(["refresh"])
|
||||
@@ -303,7 +303,7 @@ class TestCaseDupeGuruWithResults:
|
||||
eq_(self.dpanel.row(0), ("Filename", "---", "---"))
|
||||
self.dpanel.view.check_gui_calls(["refresh"])
|
||||
|
||||
def test_makeSelectedReference(self, do_setup):
|
||||
def test_make_selected_reference(self, do_setup):
|
||||
app = self.app
|
||||
objects = self.objects
|
||||
groups = self.groups
|
||||
@@ -312,7 +312,7 @@ class TestCaseDupeGuruWithResults:
|
||||
assert groups[0].ref is objects[1]
|
||||
assert groups[1].ref is objects[4]
|
||||
|
||||
def test_makeSelectedReference_by_selecting_two_dupes_in_the_same_group(self, do_setup):
|
||||
def test_make_selected_reference_by_selecting_two_dupes_in_the_same_group(self, do_setup):
|
||||
app = self.app
|
||||
objects = self.objects
|
||||
groups = self.groups
|
||||
@@ -322,7 +322,7 @@ class TestCaseDupeGuruWithResults:
|
||||
assert groups[0].ref is objects[1]
|
||||
assert groups[1].ref is objects[4]
|
||||
|
||||
def test_removeSelected(self, do_setup):
|
||||
def test_remove_selected(self, do_setup):
|
||||
app = self.app
|
||||
self.rtable.select([1, 4])
|
||||
app.remove_selected()
|
||||
@@ -330,7 +330,7 @@ class TestCaseDupeGuruWithResults:
|
||||
app.remove_selected()
|
||||
eq_(len(app.results.dupes), 0)
|
||||
|
||||
def test_addDirectory_simple(self, do_setup):
|
||||
def test_add_directory_simple(self, do_setup):
|
||||
# There's already a directory in self.app, so adding another once makes 2 of em
|
||||
app = self.app
|
||||
# any other path that isn't a parent or child of the already added path
|
||||
@@ -338,7 +338,7 @@ class TestCaseDupeGuruWithResults:
|
||||
app.add_directory(otherpath)
|
||||
eq_(len(app.directories), 2)
|
||||
|
||||
def test_addDirectory_already_there(self, do_setup):
|
||||
def test_add_directory_already_there(self, do_setup):
|
||||
app = self.app
|
||||
otherpath = Path(op.dirname(__file__))
|
||||
app.add_directory(otherpath)
|
||||
@@ -346,7 +346,7 @@ class TestCaseDupeGuruWithResults:
|
||||
eq_(len(app.view.messages), 1)
|
||||
assert "already" in app.view.messages[0]
|
||||
|
||||
def test_addDirectory_does_not_exist(self, do_setup):
|
||||
def test_add_directory_does_not_exist(self, do_setup):
|
||||
app = self.app
|
||||
app.add_directory("/does_not_exist")
|
||||
eq_(len(app.view.messages), 1)
|
||||
@@ -362,30 +362,30 @@ class TestCaseDupeGuruWithResults:
|
||||
# BOTH the ref and the other dupe should have been added
|
||||
eq_(len(app.ignore_list), 3)
|
||||
|
||||
def test_purgeIgnoreList(self, do_setup, tmpdir):
|
||||
def test_purge_ignorelist(self, do_setup, tmpdir):
|
||||
app = self.app
|
||||
p1 = str(tmpdir.join("file1"))
|
||||
p2 = str(tmpdir.join("file2"))
|
||||
open(p1, "w").close()
|
||||
open(p2, "w").close()
|
||||
dne = "/does_not_exist"
|
||||
app.ignore_list.Ignore(dne, p1)
|
||||
app.ignore_list.Ignore(p2, dne)
|
||||
app.ignore_list.Ignore(p1, p2)
|
||||
app.ignore_list.ignore(dne, p1)
|
||||
app.ignore_list.ignore(p2, dne)
|
||||
app.ignore_list.ignore(p1, p2)
|
||||
app.purge_ignore_list()
|
||||
eq_(1, len(app.ignore_list))
|
||||
assert app.ignore_list.AreIgnored(p1, p2)
|
||||
assert not app.ignore_list.AreIgnored(dne, p1)
|
||||
assert app.ignore_list.are_ignored(p1, p2)
|
||||
assert not app.ignore_list.are_ignored(dne, p1)
|
||||
|
||||
def test_only_unicode_is_added_to_ignore_list(self, do_setup):
|
||||
def FakeIgnore(first, second):
|
||||
def fake_ignore(first, second):
|
||||
if not isinstance(first, str):
|
||||
self.fail()
|
||||
if not isinstance(second, str):
|
||||
self.fail()
|
||||
|
||||
app = self.app
|
||||
app.ignore_list.Ignore = FakeIgnore
|
||||
app.ignore_list.ignore = fake_ignore
|
||||
self.rtable.select([4])
|
||||
app.add_selected_to_ignore_list()
|
||||
|
||||
@@ -419,7 +419,7 @@ class TestCaseDupeGuruWithResults:
|
||||
# don't crash
|
||||
|
||||
|
||||
class TestCaseDupeGuru_renameSelected:
|
||||
class TestCaseDupeGuruRenameSelected:
|
||||
@pytest.fixture
|
||||
def do_setup(self, request):
|
||||
tmpdir = request.getfixturevalue("tmpdir")
|
||||
@@ -502,7 +502,6 @@ class TestAppWithDirectoriesInTree:
|
||||
# refreshed.
|
||||
node = self.dtree[0]
|
||||
eq_(len(node), 3) # a len() call is required for subnodes to be loaded
|
||||
subnode = node[0]
|
||||
node.state = 1 # the state property is a state index
|
||||
node = self.dtree[0]
|
||||
eq_(len(node), 3)
|
||||
|
||||
@@ -151,8 +151,8 @@ class TestApp(TestAppBase):
|
||||
def __init__(self):
|
||||
def link_gui(gui):
|
||||
gui.view = self.make_logger()
|
||||
if hasattr(gui, "columns"): # tables
|
||||
gui.columns.view = self.make_logger()
|
||||
if hasattr(gui, "_columns"): # tables
|
||||
gui._columns.view = self.make_logger()
|
||||
return gui
|
||||
|
||||
TestAppBase.__init__(self)
|
||||
|
||||
@@ -73,99 +73,6 @@ class TestCasegetblock:
|
||||
eq_((meanred, meangreen, meanblue), b)
|
||||
|
||||
|
||||
# class TCdiff(unittest.TestCase):
|
||||
# def test_diff(self):
|
||||
# b1 = (10, 20, 30)
|
||||
# b2 = (1, 2, 3)
|
||||
# eq_(9 + 18 + 27, diff(b1, b2))
|
||||
#
|
||||
# def test_diff_negative(self):
|
||||
# b1 = (10, 20, 30)
|
||||
# b2 = (1, 2, 3)
|
||||
# eq_(9 + 18 + 27, diff(b2, b1))
|
||||
#
|
||||
# def test_diff_mixed_positive_and_negative(self):
|
||||
# b1 = (1, 5, 10)
|
||||
# b2 = (10, 1, 15)
|
||||
# eq_(9 + 4 + 5, diff(b1, b2))
|
||||
#
|
||||
|
||||
# class TCgetblocks(unittest.TestCase):
|
||||
# def test_empty_image(self):
|
||||
# im = empty()
|
||||
# blocks = getblocks(im, 1)
|
||||
# eq_(0, len(blocks))
|
||||
#
|
||||
# def test_one_block_image(self):
|
||||
# im = four_pixels()
|
||||
# blocks = getblocks2(im, 1)
|
||||
# eq_(1, len(blocks))
|
||||
# block = blocks[0]
|
||||
# meanred = (0xff + 0x80) // 4
|
||||
# meangreen = (0x80 + 0x40) // 4
|
||||
# meanblue = (0xff + 0x80) // 4
|
||||
# eq_((meanred, meangreen, meanblue), block)
|
||||
#
|
||||
# def test_not_enough_height_to_fit_a_block(self):
|
||||
# im = FakeImage((2, 1), [BLACK, BLACK])
|
||||
# blocks = getblocks(im, 2)
|
||||
# eq_(0, len(blocks))
|
||||
#
|
||||
# def xtest_dont_include_leftovers(self):
|
||||
# # this test is disabled because getblocks is not used and getblock in cdeffed
|
||||
# pixels = [
|
||||
# RED,(0, 0x80, 0xff), BLACK,
|
||||
# (0x80, 0, 0),(0, 0x40, 0x80), BLACK,
|
||||
# BLACK, BLACK, BLACK
|
||||
# ]
|
||||
# im = FakeImage((3, 3), pixels)
|
||||
# blocks = getblocks(im, 2)
|
||||
# block = blocks[0]
|
||||
# #Because the block is smaller than the image, only blocksize must be considered.
|
||||
# meanred = (0xff + 0x80) // 4
|
||||
# meangreen = (0x80 + 0x40) // 4
|
||||
# meanblue = (0xff + 0x80) // 4
|
||||
# eq_((meanred, meangreen, meanblue), block)
|
||||
#
|
||||
# def xtest_two_blocks(self):
|
||||
# # this test is disabled because getblocks is not used and getblock in cdeffed
|
||||
# pixels = [BLACK for i in xrange(4 * 2)]
|
||||
# pixels[0] = RED
|
||||
# pixels[1] = (0, 0x80, 0xff)
|
||||
# pixels[4] = (0x80, 0, 0)
|
||||
# pixels[5] = (0, 0x40, 0x80)
|
||||
# im = FakeImage((4, 2), pixels)
|
||||
# blocks = getblocks(im, 2)
|
||||
# eq_(2, len(blocks))
|
||||
# block = blocks[0]
|
||||
# #Because the block is smaller than the image, only blocksize must be considered.
|
||||
# meanred = (0xff + 0x80) // 4
|
||||
# meangreen = (0x80 + 0x40) // 4
|
||||
# meanblue = (0xff + 0x80) // 4
|
||||
# eq_((meanred, meangreen, meanblue), block)
|
||||
# eq_(BLACK, blocks[1])
|
||||
#
|
||||
# def test_four_blocks(self):
|
||||
# pixels = [BLACK for i in xrange(4 * 4)]
|
||||
# pixels[0] = RED
|
||||
# pixels[1] = (0, 0x80, 0xff)
|
||||
# pixels[4] = (0x80, 0, 0)
|
||||
# pixels[5] = (0, 0x40, 0x80)
|
||||
# im = FakeImage((4, 4), pixels)
|
||||
# blocks = getblocks2(im, 2)
|
||||
# eq_(4, len(blocks))
|
||||
# block = blocks[0]
|
||||
# #Because the block is smaller than the image, only blocksize must be considered.
|
||||
# meanred = (0xff + 0x80) // 4
|
||||
# meangreen = (0x80 + 0x40) // 4
|
||||
# meanblue = (0xff + 0x80) // 4
|
||||
# eq_((meanred, meangreen, meanblue), block)
|
||||
# eq_(BLACK, blocks[1])
|
||||
# eq_(BLACK, blocks[2])
|
||||
# eq_(BLACK, blocks[3])
|
||||
#
|
||||
|
||||
|
||||
class TestCasegetblocks2:
|
||||
def test_empty_image(self):
|
||||
im = empty()
|
||||
@@ -270,8 +177,8 @@ class TestCaseavgdiff:
|
||||
def test_return_at_least_1_at_the_slightest_difference(self):
|
||||
ref = (0, 0, 0)
|
||||
b1 = (1, 0, 0)
|
||||
blocks1 = [ref for i in range(250)]
|
||||
blocks2 = [ref for i in range(250)]
|
||||
blocks1 = [ref for _ in range(250)]
|
||||
blocks2 = [ref for _ in range(250)]
|
||||
blocks2[0] = b1
|
||||
eq_(1, my_avgdiff(blocks1, blocks2))
|
||||
|
||||
@@ -280,41 +187,3 @@ class TestCaseavgdiff:
|
||||
blocks1 = [ref, ref]
|
||||
blocks2 = [ref, ref]
|
||||
eq_(0, my_avgdiff(blocks1, blocks2))
|
||||
|
||||
|
||||
# class TCmaxdiff(unittest.TestCase):
|
||||
# def test_empty(self):
|
||||
# self.assertRaises(NoBlocksError, maxdiff,[],[])
|
||||
#
|
||||
# def test_two_blocks(self):
|
||||
# b1 = (5, 10, 15)
|
||||
# b2 = (255, 250, 245)
|
||||
# b3 = (0, 0, 0)
|
||||
# b4 = (255, 0, 255)
|
||||
# blocks1 = [b1, b2]
|
||||
# blocks2 = [b3, b4]
|
||||
# expected1 = 5 + 10 + 15
|
||||
# expected2 = 0 + 250 + 10
|
||||
# expected = max(expected1, expected2)
|
||||
# eq_(expected, maxdiff(blocks1, blocks2))
|
||||
#
|
||||
# def test_blocks_not_the_same_size(self):
|
||||
# b = (0, 0, 0)
|
||||
# self.assertRaises(DifferentBlockCountError, maxdiff,[b, b],[b])
|
||||
#
|
||||
# def test_first_arg_is_empty_but_not_second(self):
|
||||
# #Don't return 0 (as when the 2 lists are empty), raise!
|
||||
# b = (0, 0, 0)
|
||||
# self.assertRaises(DifferentBlockCountError, maxdiff,[],[b])
|
||||
#
|
||||
# def test_limit(self):
|
||||
# b1 = (5, 10, 15)
|
||||
# b2 = (255, 250, 245)
|
||||
# b3 = (0, 0, 0)
|
||||
# b4 = (255, 0, 255)
|
||||
# blocks1 = [b1, b2]
|
||||
# blocks2 = [b3, b4]
|
||||
# expected1 = 5 + 10 + 15
|
||||
# expected2 = 0 + 250 + 10
|
||||
# eq_(expected1, maxdiff(blocks1, blocks2, expected1 - 1))
|
||||
#
|
||||
|
||||
@@ -17,7 +17,7 @@ except ImportError:
|
||||
skip("Can't import the cache module, probably hasn't been compiled.")
|
||||
|
||||
|
||||
class TestCasecolors_to_string:
|
||||
class TestCaseColorsToString:
|
||||
def test_no_color(self):
|
||||
eq_("", colors_to_string([]))
|
||||
|
||||
@@ -30,7 +30,7 @@ class TestCasecolors_to_string:
|
||||
eq_("000102030405", colors_to_string([(0, 1, 2), (3, 4, 5)]))
|
||||
|
||||
|
||||
class TestCasestring_to_colors:
|
||||
class TestCaseStringToColors:
|
||||
def test_empty(self):
|
||||
eq_([], string_to_colors(""))
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ def test_add_path():
|
||||
assert p in d
|
||||
|
||||
|
||||
def test_AddPath_when_path_is_already_there():
|
||||
def test_add_path_when_path_is_already_there():
|
||||
d = Directories()
|
||||
p = testpath["onefile"]
|
||||
d.add_path(p)
|
||||
@@ -112,7 +112,7 @@ def test_add_path_containing_paths_already_there():
|
||||
eq_(d[0], testpath)
|
||||
|
||||
|
||||
def test_AddPath_non_latin(tmpdir):
|
||||
def test_add_path_non_latin(tmpdir):
|
||||
p = Path(str(tmpdir))
|
||||
to_add = p["unicode\u201a"]
|
||||
os.mkdir(str(to_add))
|
||||
@@ -140,20 +140,20 @@ def test_states():
|
||||
d = Directories()
|
||||
p = testpath["onefile"]
|
||||
d.add_path(p)
|
||||
eq_(DirectoryState.Normal, d.get_state(p))
|
||||
d.set_state(p, DirectoryState.Reference)
|
||||
eq_(DirectoryState.Reference, d.get_state(p))
|
||||
eq_(DirectoryState.Reference, d.get_state(p["dir1"]))
|
||||
eq_(DirectoryState.NORMAL, d.get_state(p))
|
||||
d.set_state(p, DirectoryState.REFERENCE)
|
||||
eq_(DirectoryState.REFERENCE, d.get_state(p))
|
||||
eq_(DirectoryState.REFERENCE, d.get_state(p["dir1"]))
|
||||
eq_(1, len(d.states))
|
||||
eq_(p, list(d.states.keys())[0])
|
||||
eq_(DirectoryState.Reference, d.states[p])
|
||||
eq_(DirectoryState.REFERENCE, d.states[p])
|
||||
|
||||
|
||||
def test_get_state_with_path_not_there():
|
||||
# When the path's not there, just return DirectoryState.Normal
|
||||
d = Directories()
|
||||
d.add_path(testpath["onefile"])
|
||||
eq_(d.get_state(testpath), DirectoryState.Normal)
|
||||
eq_(d.get_state(testpath), DirectoryState.NORMAL)
|
||||
|
||||
|
||||
def test_states_overwritten_when_larger_directory_eat_smaller_ones():
|
||||
@@ -162,20 +162,20 @@ def test_states_overwritten_when_larger_directory_eat_smaller_ones():
|
||||
d = Directories()
|
||||
p = testpath["onefile"]
|
||||
d.add_path(p)
|
||||
d.set_state(p, DirectoryState.Excluded)
|
||||
d.set_state(p, DirectoryState.EXCLUDED)
|
||||
d.add_path(testpath)
|
||||
d.set_state(testpath, DirectoryState.Reference)
|
||||
eq_(d.get_state(p), DirectoryState.Reference)
|
||||
eq_(d.get_state(p["dir1"]), DirectoryState.Reference)
|
||||
eq_(d.get_state(testpath), DirectoryState.Reference)
|
||||
d.set_state(testpath, DirectoryState.REFERENCE)
|
||||
eq_(d.get_state(p), DirectoryState.REFERENCE)
|
||||
eq_(d.get_state(p["dir1"]), DirectoryState.REFERENCE)
|
||||
eq_(d.get_state(testpath), DirectoryState.REFERENCE)
|
||||
|
||||
|
||||
def test_get_files():
|
||||
d = Directories()
|
||||
p = testpath["fs"]
|
||||
d.add_path(p)
|
||||
d.set_state(p["dir1"], DirectoryState.Reference)
|
||||
d.set_state(p["dir2"], DirectoryState.Excluded)
|
||||
d.set_state(p["dir1"], DirectoryState.REFERENCE)
|
||||
d.set_state(p["dir2"], DirectoryState.EXCLUDED)
|
||||
files = list(d.get_files())
|
||||
eq_(5, len(files))
|
||||
for f in files:
|
||||
@@ -204,8 +204,8 @@ def test_get_folders():
|
||||
d = Directories()
|
||||
p = testpath["fs"]
|
||||
d.add_path(p)
|
||||
d.set_state(p["dir1"], DirectoryState.Reference)
|
||||
d.set_state(p["dir2"], DirectoryState.Excluded)
|
||||
d.set_state(p["dir1"], DirectoryState.REFERENCE)
|
||||
d.set_state(p["dir2"], DirectoryState.EXCLUDED)
|
||||
folders = list(d.get_folders())
|
||||
eq_(len(folders), 3)
|
||||
ref = [f for f in folders if f.is_ref]
|
||||
@@ -220,7 +220,7 @@ def test_get_files_with_inherited_exclusion():
|
||||
d = Directories()
|
||||
p = testpath["onefile"]
|
||||
d.add_path(p)
|
||||
d.set_state(p, DirectoryState.Excluded)
|
||||
d.set_state(p, DirectoryState.EXCLUDED)
|
||||
eq_([], list(d.get_files()))
|
||||
|
||||
|
||||
@@ -233,14 +233,14 @@ def test_save_and_load(tmpdir):
|
||||
p2.mkdir()
|
||||
d1.add_path(p1)
|
||||
d1.add_path(p2)
|
||||
d1.set_state(p1, DirectoryState.Reference)
|
||||
d1.set_state(p1["dir1"], DirectoryState.Excluded)
|
||||
d1.set_state(p1, DirectoryState.REFERENCE)
|
||||
d1.set_state(p1["dir1"], DirectoryState.EXCLUDED)
|
||||
tmpxml = str(tmpdir.join("directories_testunit.xml"))
|
||||
d1.save_to_file(tmpxml)
|
||||
d2.load_from_file(tmpxml)
|
||||
eq_(2, len(d2))
|
||||
eq_(DirectoryState.Reference, d2.get_state(p1))
|
||||
eq_(DirectoryState.Excluded, d2.get_state(p1["dir1"]))
|
||||
eq_(DirectoryState.REFERENCE, d2.get_state(p1))
|
||||
eq_(DirectoryState.EXCLUDED, d2.get_state(p1["dir1"]))
|
||||
|
||||
|
||||
def test_invalid_path():
|
||||
@@ -258,7 +258,7 @@ def test_set_state_on_invalid_path():
|
||||
Path(
|
||||
"foobar",
|
||||
),
|
||||
DirectoryState.Normal,
|
||||
DirectoryState.NORMAL,
|
||||
)
|
||||
except LookupError:
|
||||
assert False
|
||||
@@ -287,7 +287,7 @@ def test_unicode_save(tmpdir):
|
||||
p1.mkdir()
|
||||
p1["foo\xe9"].mkdir()
|
||||
d.add_path(p1)
|
||||
d.set_state(p1["foo\xe9"], DirectoryState.Excluded)
|
||||
d.set_state(p1["foo\xe9"], DirectoryState.EXCLUDED)
|
||||
tmpxml = str(tmpdir.join("directories_testunit.xml"))
|
||||
try:
|
||||
d.save_to_file(tmpxml)
|
||||
@@ -321,10 +321,10 @@ def test_get_state_returns_excluded_by_default_for_hidden_directories(tmpdir):
|
||||
hidden_dir_path = p[".foo"]
|
||||
p[".foo"].mkdir()
|
||||
d.add_path(p)
|
||||
eq_(d.get_state(hidden_dir_path), DirectoryState.Excluded)
|
||||
eq_(d.get_state(hidden_dir_path), DirectoryState.EXCLUDED)
|
||||
# But it can be overriden
|
||||
d.set_state(hidden_dir_path, DirectoryState.Normal)
|
||||
eq_(d.get_state(hidden_dir_path), DirectoryState.Normal)
|
||||
d.set_state(hidden_dir_path, DirectoryState.NORMAL)
|
||||
eq_(d.get_state(hidden_dir_path), DirectoryState.NORMAL)
|
||||
|
||||
|
||||
def test_default_path_state_override(tmpdir):
|
||||
@@ -332,7 +332,7 @@ def test_default_path_state_override(tmpdir):
|
||||
class MyDirectories(Directories):
|
||||
def _default_state_for_path(self, path):
|
||||
if "foobar" in path:
|
||||
return DirectoryState.Excluded
|
||||
return DirectoryState.EXCLUDED
|
||||
|
||||
d = MyDirectories()
|
||||
p1 = Path(str(tmpdir))
|
||||
@@ -341,12 +341,12 @@ def test_default_path_state_override(tmpdir):
|
||||
p1["foobaz"].mkdir()
|
||||
p1["foobaz/somefile"].open("w").close()
|
||||
d.add_path(p1)
|
||||
eq_(d.get_state(p1["foobaz"]), DirectoryState.Normal)
|
||||
eq_(d.get_state(p1["foobar"]), DirectoryState.Excluded)
|
||||
eq_(d.get_state(p1["foobaz"]), DirectoryState.NORMAL)
|
||||
eq_(d.get_state(p1["foobar"]), DirectoryState.EXCLUDED)
|
||||
eq_(len(list(d.get_files())), 1) # only the 'foobaz' file is there
|
||||
# However, the default state can be changed
|
||||
d.set_state(p1["foobar"], DirectoryState.Normal)
|
||||
eq_(d.get_state(p1["foobar"]), DirectoryState.Normal)
|
||||
d.set_state(p1["foobar"], DirectoryState.NORMAL)
|
||||
eq_(d.get_state(p1["foobar"]), DirectoryState.NORMAL)
|
||||
eq_(len(list(d.get_files())), 2)
|
||||
|
||||
|
||||
@@ -375,11 +375,11 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
|
||||
p1["$Recycle.Bin"].mkdir()
|
||||
p1["$Recycle.Bin"]["subdir"].mkdir()
|
||||
self.d.add_path(p1)
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]), DirectoryState.Excluded)
|
||||
# By default, subdirs should be excluded too, but this can be overriden separately
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.Excluded)
|
||||
self.d.set_state(p1["$Recycle.Bin"]["subdir"], DirectoryState.Normal)
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.Normal)
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]), DirectoryState.EXCLUDED)
|
||||
# By default, subdirs should be excluded too, but this can be overridden separately
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.EXCLUDED)
|
||||
self.d.set_state(p1["$Recycle.Bin"]["subdir"], DirectoryState.NORMAL)
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.NORMAL)
|
||||
|
||||
def test_exclude_refined(self, tmpdir):
|
||||
regex1 = r"^\$Recycle\.Bin$"
|
||||
@@ -398,16 +398,16 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
|
||||
self.d.add_path(p1["$Recycle.Bin"])
|
||||
|
||||
# Filter should set the default state to Excluded
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]), DirectoryState.Excluded)
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]), DirectoryState.EXCLUDED)
|
||||
# The subdir should inherit its parent state
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.Excluded)
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdar"]), DirectoryState.Excluded)
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.EXCLUDED)
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdar"]), DirectoryState.EXCLUDED)
|
||||
# Override a child path's state
|
||||
self.d.set_state(p1["$Recycle.Bin"]["subdir"], DirectoryState.Normal)
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.Normal)
|
||||
self.d.set_state(p1["$Recycle.Bin"]["subdir"], DirectoryState.NORMAL)
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.NORMAL)
|
||||
# Parent should keep its default state, and the other child too
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]), DirectoryState.Excluded)
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdar"]), DirectoryState.Excluded)
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]), DirectoryState.EXCLUDED)
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdar"]), DirectoryState.EXCLUDED)
|
||||
# print(f"get_folders(): {[x for x in self.d.get_folders()]}")
|
||||
|
||||
# only the 2 files directly under the Normal directory
|
||||
@@ -419,8 +419,8 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
|
||||
assert "somesubdirfile.png" in files
|
||||
assert "unwanted_subdirfile.gif" in files
|
||||
# Overriding the parent should enable all children
|
||||
self.d.set_state(p1["$Recycle.Bin"], DirectoryState.Normal)
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdar"]), DirectoryState.Normal)
|
||||
self.d.set_state(p1["$Recycle.Bin"], DirectoryState.NORMAL)
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdar"]), DirectoryState.NORMAL)
|
||||
# all files there
|
||||
files = self.get_files_and_expect_num_result(6)
|
||||
assert "somefile.png" in files
|
||||
@@ -444,7 +444,7 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
|
||||
assert self.d._exclude_list.error(regex3) is None
|
||||
# print(f"get_folders(): {[x for x in self.d.get_folders()]}")
|
||||
# Directory shouldn't change its state here, unless explicitely done by user
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.Normal)
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.NORMAL)
|
||||
files = self.get_files_and_expect_num_result(5)
|
||||
assert "unwanted_subdirfile.gif" not in files
|
||||
assert "unwanted_subdarfile.png" in files
|
||||
@@ -454,14 +454,14 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
|
||||
self.d._exclude_list.rename(regex3, regex4)
|
||||
assert self.d._exclude_list.error(regex4) is None
|
||||
p1["$Recycle.Bin"]["subdar"]["file_ending_with_subdir"].open("w").close()
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.Excluded)
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.EXCLUDED)
|
||||
files = self.get_files_and_expect_num_result(4)
|
||||
assert "file_ending_with_subdir" not in files
|
||||
assert "somesubdarfile.jpeg" in files
|
||||
assert "somesubdirfile.png" not in files
|
||||
assert "unwanted_subdirfile.gif" not in files
|
||||
self.d.set_state(p1["$Recycle.Bin"]["subdir"], DirectoryState.Normal)
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.Normal)
|
||||
self.d.set_state(p1["$Recycle.Bin"]["subdir"], DirectoryState.NORMAL)
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.NORMAL)
|
||||
# print(f"get_folders(): {[x for x in self.d.get_folders()]}")
|
||||
files = self.get_files_and_expect_num_result(6)
|
||||
assert "file_ending_with_subdir" not in files
|
||||
@@ -471,7 +471,7 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
|
||||
regex5 = r".*subdir.*"
|
||||
self.d._exclude_list.rename(regex4, regex5)
|
||||
# Files containing substring should be filtered
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.Normal)
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.NORMAL)
|
||||
# The path should not match, only the filename, the "subdir" in the directory name shouldn't matter
|
||||
p1["$Recycle.Bin"]["subdir"]["file_which_shouldnt_match"].open("w").close()
|
||||
files = self.get_files_and_expect_num_result(5)
|
||||
@@ -493,7 +493,7 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
|
||||
assert self.d._exclude_list.error(regex6) is None
|
||||
assert regex6 in self.d._exclude_list
|
||||
# This still should not be affected
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.Normal)
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.NORMAL)
|
||||
files = self.get_files_and_expect_num_result(5)
|
||||
# These files are under the "/subdir" directory
|
||||
assert "somesubdirfile.png" not in files
|
||||
@@ -518,7 +518,7 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
|
||||
self.d._exclude_list.add(regex3)
|
||||
self.d._exclude_list.mark(regex3)
|
||||
# print(f"get_folders(): {[x for x in self.d.get_folders()]}")
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["思叫物語"]), DirectoryState.Excluded)
|
||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["思叫物語"]), DirectoryState.EXCLUDED)
|
||||
files = self.get_files_and_expect_num_result(2)
|
||||
assert "過去白濁物語~]_カラー.jpg" not in files
|
||||
assert "なししろ会う前" not in files
|
||||
@@ -527,7 +527,7 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
|
||||
regex4 = r".*物語$"
|
||||
self.d._exclude_list.rename(regex3, regex4)
|
||||
assert self.d._exclude_list.error(regex4) is None
|
||||
self.d.set_state(p1["$Recycle.Bin"]["思叫物語"], DirectoryState.Normal)
|
||||
self.d.set_state(p1["$Recycle.Bin"]["思叫物語"], DirectoryState.NORMAL)
|
||||
files = self.get_files_and_expect_num_result(5)
|
||||
assert "過去白濁物語~]_カラー.jpg" in files
|
||||
assert "なししろ会う前" in files
|
||||
@@ -546,8 +546,8 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
|
||||
p1["foobar"][".hidden_dir"][".hidden_subfile.png"].open("w").close()
|
||||
self.d.add_path(p1["foobar"])
|
||||
# It should not inherit its parent's state originally
|
||||
eq_(self.d.get_state(p1["foobar"][".hidden_dir"]), DirectoryState.Excluded)
|
||||
self.d.set_state(p1["foobar"][".hidden_dir"], DirectoryState.Normal)
|
||||
eq_(self.d.get_state(p1["foobar"][".hidden_dir"]), DirectoryState.EXCLUDED)
|
||||
self.d.set_state(p1["foobar"][".hidden_dir"], DirectoryState.NORMAL)
|
||||
# The files should still be filtered
|
||||
files = self.get_files_and_expect_num_result(1)
|
||||
eq_(len(self.d._exclude_list.compiled_paths), 0)
|
||||
|
||||
@@ -103,10 +103,9 @@ class TestCasegetfields:
|
||||
expected = [["a", "bc", "def"]]
|
||||
actual = getfields(" - a bc def")
|
||||
eq_(expected, actual)
|
||||
expected = [["bc", "def"]]
|
||||
|
||||
|
||||
class TestCaseunpack_fields:
|
||||
class TestCaseUnpackFields:
|
||||
def test_with_fields(self):
|
||||
expected = ["a", "b", "c", "d", "e", "f"]
|
||||
actual = unpack_fields([["a"], ["b", "c"], ["d", "e", "f"]])
|
||||
@@ -218,24 +217,24 @@ class TestCaseWordCompareWithFields:
|
||||
eq_([["c", "d", "f"], ["a", "b"]], second)
|
||||
|
||||
|
||||
class TestCasebuild_word_dict:
|
||||
class TestCaseBuildWordDict:
|
||||
def test_with_standard_words(self):
|
||||
itemList = [NamedObject("foo bar", True)]
|
||||
itemList.append(NamedObject("bar baz", True))
|
||||
itemList.append(NamedObject("baz bleh foo", True))
|
||||
d = build_word_dict(itemList)
|
||||
item_list = [NamedObject("foo bar", True)]
|
||||
item_list.append(NamedObject("bar baz", True))
|
||||
item_list.append(NamedObject("baz bleh foo", True))
|
||||
d = build_word_dict(item_list)
|
||||
eq_(4, len(d))
|
||||
eq_(2, len(d["foo"]))
|
||||
assert itemList[0] in d["foo"]
|
||||
assert itemList[2] in d["foo"]
|
||||
assert item_list[0] in d["foo"]
|
||||
assert item_list[2] in d["foo"]
|
||||
eq_(2, len(d["bar"]))
|
||||
assert itemList[0] in d["bar"]
|
||||
assert itemList[1] in d["bar"]
|
||||
assert item_list[0] in d["bar"]
|
||||
assert item_list[1] in d["bar"]
|
||||
eq_(2, len(d["baz"]))
|
||||
assert itemList[1] in d["baz"]
|
||||
assert itemList[2] in d["baz"]
|
||||
assert item_list[1] in d["baz"]
|
||||
assert item_list[2] in d["baz"]
|
||||
eq_(1, len(d["bleh"]))
|
||||
assert itemList[2] in d["bleh"]
|
||||
assert item_list[2] in d["bleh"]
|
||||
|
||||
def test_unpack_fields(self):
|
||||
o = NamedObject("")
|
||||
@@ -269,7 +268,7 @@ class TestCasebuild_word_dict:
|
||||
eq_(100, self.log[1])
|
||||
|
||||
|
||||
class TestCasemerge_similar_words:
|
||||
class TestCaseMergeSimilarWords:
|
||||
def test_some_similar_words(self):
|
||||
d = {
|
||||
"foobar": set([1]),
|
||||
@@ -281,11 +280,11 @@ class TestCasemerge_similar_words:
|
||||
eq_(3, len(d["foobar"]))
|
||||
|
||||
|
||||
class TestCasereduce_common_words:
|
||||
class TestCaseReduceCommonWords:
|
||||
def test_typical(self):
|
||||
d = {
|
||||
"foo": set([NamedObject("foo bar", True) for i in range(50)]),
|
||||
"bar": set([NamedObject("foo bar", True) for i in range(49)]),
|
||||
"foo": set([NamedObject("foo bar", True) for _ in range(50)]),
|
||||
"bar": set([NamedObject("foo bar", True) for _ in range(49)]),
|
||||
}
|
||||
reduce_common_words(d, 50)
|
||||
assert "foo" not in d
|
||||
@@ -293,7 +292,7 @@ class TestCasereduce_common_words:
|
||||
|
||||
def test_dont_remove_objects_with_only_common_words(self):
|
||||
d = {
|
||||
"common": set([NamedObject("common uncommon", True) for i in range(50)] + [NamedObject("common", True)]),
|
||||
"common": set([NamedObject("common uncommon", True) for _ in range(50)] + [NamedObject("common", True)]),
|
||||
"uncommon": set([NamedObject("common uncommon", True)]),
|
||||
}
|
||||
reduce_common_words(d, 50)
|
||||
@@ -302,20 +301,20 @@ class TestCasereduce_common_words:
|
||||
|
||||
def test_values_still_are_set_instances(self):
|
||||
d = {
|
||||
"common": set([NamedObject("common uncommon", True) for i in range(50)] + [NamedObject("common", True)]),
|
||||
"common": set([NamedObject("common uncommon", True) for _ in range(50)] + [NamedObject("common", True)]),
|
||||
"uncommon": set([NamedObject("common uncommon", True)]),
|
||||
}
|
||||
reduce_common_words(d, 50)
|
||||
assert isinstance(d["common"], set)
|
||||
assert isinstance(d["uncommon"], set)
|
||||
|
||||
def test_dont_raise_KeyError_when_a_word_has_been_removed(self):
|
||||
def test_dont_raise_keyerror_when_a_word_has_been_removed(self):
|
||||
# If a word has been removed by the reduce, an object in a subsequent common word that
|
||||
# contains the word that has been removed would cause a KeyError.
|
||||
d = {
|
||||
"foo": set([NamedObject("foo bar baz", True) for i in range(50)]),
|
||||
"bar": set([NamedObject("foo bar baz", True) for i in range(50)]),
|
||||
"baz": set([NamedObject("foo bar baz", True) for i in range(49)]),
|
||||
"foo": set([NamedObject("foo bar baz", True) for _ in range(50)]),
|
||||
"bar": set([NamedObject("foo bar baz", True) for _ in range(50)]),
|
||||
"baz": set([NamedObject("foo bar baz", True) for _ in range(49)]),
|
||||
}
|
||||
try:
|
||||
reduce_common_words(d, 50)
|
||||
@@ -329,7 +328,7 @@ class TestCasereduce_common_words:
|
||||
o.words = [["foo", "bar"], ["baz"]]
|
||||
return o
|
||||
|
||||
d = {"foo": set([create_it() for i in range(50)])}
|
||||
d = {"foo": set([create_it() for _ in range(50)])}
|
||||
try:
|
||||
reduce_common_words(d, 50)
|
||||
except TypeError:
|
||||
@@ -342,9 +341,9 @@ class TestCasereduce_common_words:
|
||||
# would not stay in 'bar' because 'foo' is not a common word anymore.
|
||||
only_common = NamedObject("foo bar", True)
|
||||
d = {
|
||||
"foo": set([NamedObject("foo bar baz", True) for i in range(49)] + [only_common]),
|
||||
"bar": set([NamedObject("foo bar baz", True) for i in range(49)] + [only_common]),
|
||||
"baz": set([NamedObject("foo bar baz", True) for i in range(49)]),
|
||||
"foo": set([NamedObject("foo bar baz", True) for _ in range(49)] + [only_common]),
|
||||
"bar": set([NamedObject("foo bar baz", True) for _ in range(49)] + [only_common]),
|
||||
"baz": set([NamedObject("foo bar baz", True) for _ in range(49)]),
|
||||
}
|
||||
reduce_common_words(d, 50)
|
||||
eq_(1, len(d["foo"]))
|
||||
@@ -352,7 +351,7 @@ class TestCasereduce_common_words:
|
||||
eq_(49, len(d["baz"]))
|
||||
|
||||
|
||||
class TestCaseget_match:
|
||||
class TestCaseGetMatch:
|
||||
def test_simple(self):
|
||||
o1 = NamedObject("foo bar", True)
|
||||
o2 = NamedObject("bar bleh", True)
|
||||
@@ -381,12 +380,12 @@ class TestCaseGetMatches:
|
||||
eq_(getmatches([]), [])
|
||||
|
||||
def test_simple(self):
|
||||
itemList = [
|
||||
item_list = [
|
||||
NamedObject("foo bar"),
|
||||
NamedObject("bar bleh"),
|
||||
NamedObject("a b c foo"),
|
||||
]
|
||||
r = getmatches(itemList)
|
||||
r = getmatches(item_list)
|
||||
eq_(2, len(r))
|
||||
m = first(m for m in r if m.percentage == 50) # "foo bar" and "bar bleh"
|
||||
assert_match(m, "foo bar", "bar bleh")
|
||||
@@ -394,40 +393,40 @@ class TestCaseGetMatches:
|
||||
assert_match(m, "foo bar", "a b c foo")
|
||||
|
||||
def test_null_and_unrelated_objects(self):
|
||||
itemList = [
|
||||
item_list = [
|
||||
NamedObject("foo bar"),
|
||||
NamedObject("bar bleh"),
|
||||
NamedObject(""),
|
||||
NamedObject("unrelated object"),
|
||||
]
|
||||
r = getmatches(itemList)
|
||||
r = getmatches(item_list)
|
||||
eq_(len(r), 1)
|
||||
m = r[0]
|
||||
eq_(m.percentage, 50)
|
||||
assert_match(m, "foo bar", "bar bleh")
|
||||
|
||||
def test_twice_the_same_word(self):
|
||||
itemList = [NamedObject("foo foo bar"), NamedObject("bar bleh")]
|
||||
r = getmatches(itemList)
|
||||
item_list = [NamedObject("foo foo bar"), NamedObject("bar bleh")]
|
||||
r = getmatches(item_list)
|
||||
eq_(1, len(r))
|
||||
|
||||
def test_twice_the_same_word_when_preworded(self):
|
||||
itemList = [NamedObject("foo foo bar", True), NamedObject("bar bleh", True)]
|
||||
r = getmatches(itemList)
|
||||
item_list = [NamedObject("foo foo bar", True), NamedObject("bar bleh", True)]
|
||||
r = getmatches(item_list)
|
||||
eq_(1, len(r))
|
||||
|
||||
def test_two_words_match(self):
|
||||
itemList = [NamedObject("foo bar"), NamedObject("foo bar bleh")]
|
||||
r = getmatches(itemList)
|
||||
item_list = [NamedObject("foo bar"), NamedObject("foo bar bleh")]
|
||||
r = getmatches(item_list)
|
||||
eq_(1, len(r))
|
||||
|
||||
def test_match_files_with_only_common_words(self):
|
||||
# If a word occurs more than 50 times, it is excluded from the matching process
|
||||
# The problem with the common_word_threshold is that the files containing only common
|
||||
# words will never be matched together. We *should* match them.
|
||||
# This test assumes that the common word threashold const is 50
|
||||
itemList = [NamedObject("foo") for i in range(50)]
|
||||
r = getmatches(itemList)
|
||||
# This test assumes that the common word threshold const is 50
|
||||
item_list = [NamedObject("foo") for _ in range(50)]
|
||||
r = getmatches(item_list)
|
||||
eq_(1225, len(r))
|
||||
|
||||
def test_use_words_already_there_if_there(self):
|
||||
@@ -450,28 +449,28 @@ class TestCaseGetMatches:
|
||||
eq_(100, self.log[-1])
|
||||
|
||||
def test_weight_words(self):
|
||||
itemList = [NamedObject("foo bar"), NamedObject("bar bleh")]
|
||||
m = getmatches(itemList, weight_words=True)[0]
|
||||
item_list = [NamedObject("foo bar"), NamedObject("bar bleh")]
|
||||
m = getmatches(item_list, weight_words=True)[0]
|
||||
eq_(int((6.0 / 13.0) * 100), m.percentage)
|
||||
|
||||
def test_similar_word(self):
|
||||
itemList = [NamedObject("foobar"), NamedObject("foobars")]
|
||||
eq_(len(getmatches(itemList, match_similar_words=True)), 1)
|
||||
eq_(getmatches(itemList, match_similar_words=True)[0].percentage, 100)
|
||||
itemList = [NamedObject("foobar"), NamedObject("foo")]
|
||||
eq_(len(getmatches(itemList, match_similar_words=True)), 0) # too far
|
||||
itemList = [NamedObject("bizkit"), NamedObject("bizket")]
|
||||
eq_(len(getmatches(itemList, match_similar_words=True)), 1)
|
||||
itemList = [NamedObject("foobar"), NamedObject("foosbar")]
|
||||
eq_(len(getmatches(itemList, match_similar_words=True)), 1)
|
||||
item_list = [NamedObject("foobar"), NamedObject("foobars")]
|
||||
eq_(len(getmatches(item_list, match_similar_words=True)), 1)
|
||||
eq_(getmatches(item_list, match_similar_words=True)[0].percentage, 100)
|
||||
item_list = [NamedObject("foobar"), NamedObject("foo")]
|
||||
eq_(len(getmatches(item_list, match_similar_words=True)), 0) # too far
|
||||
item_list = [NamedObject("bizkit"), NamedObject("bizket")]
|
||||
eq_(len(getmatches(item_list, match_similar_words=True)), 1)
|
||||
item_list = [NamedObject("foobar"), NamedObject("foosbar")]
|
||||
eq_(len(getmatches(item_list, match_similar_words=True)), 1)
|
||||
|
||||
def test_single_object_with_similar_words(self):
|
||||
itemList = [NamedObject("foo foos")]
|
||||
eq_(len(getmatches(itemList, match_similar_words=True)), 0)
|
||||
item_list = [NamedObject("foo foos")]
|
||||
eq_(len(getmatches(item_list, match_similar_words=True)), 0)
|
||||
|
||||
def test_double_words_get_counted_only_once(self):
|
||||
itemList = [NamedObject("foo bar foo bleh"), NamedObject("foo bar bleh bar")]
|
||||
m = getmatches(itemList)[0]
|
||||
item_list = [NamedObject("foo bar foo bleh"), NamedObject("foo bar bleh bar")]
|
||||
m = getmatches(item_list)[0]
|
||||
eq_(75, m.percentage)
|
||||
|
||||
def test_with_fields(self):
|
||||
@@ -491,13 +490,13 @@ class TestCaseGetMatches:
|
||||
eq_(m.percentage, 50)
|
||||
|
||||
def test_only_match_similar_when_the_option_is_set(self):
|
||||
itemList = [NamedObject("foobar"), NamedObject("foobars")]
|
||||
eq_(len(getmatches(itemList, match_similar_words=False)), 0)
|
||||
item_list = [NamedObject("foobar"), NamedObject("foobars")]
|
||||
eq_(len(getmatches(item_list, match_similar_words=False)), 0)
|
||||
|
||||
def test_dont_recurse_do_match(self):
|
||||
# with nosetests, the stack is increased. The number has to be high enough not to be failing falsely
|
||||
sys.setrecursionlimit(200)
|
||||
files = [NamedObject("foo bar") for i in range(201)]
|
||||
files = [NamedObject("foo bar") for _ in range(201)]
|
||||
try:
|
||||
getmatches(files)
|
||||
except RuntimeError:
|
||||
@@ -506,35 +505,31 @@ class TestCaseGetMatches:
|
||||
sys.setrecursionlimit(1000)
|
||||
|
||||
def test_min_match_percentage(self):
|
||||
itemList = [
|
||||
item_list = [
|
||||
NamedObject("foo bar"),
|
||||
NamedObject("bar bleh"),
|
||||
NamedObject("a b c foo"),
|
||||
]
|
||||
r = getmatches(itemList, min_match_percentage=50)
|
||||
r = getmatches(item_list, min_match_percentage=50)
|
||||
eq_(1, len(r)) # Only "foo bar" / "bar bleh" should match
|
||||
|
||||
def test_MemoryError(self, monkeypatch):
|
||||
def test_memory_error(self, monkeypatch):
|
||||
@log_calls
|
||||
def mocked_match(first, second, flags):
|
||||
if len(mocked_match.calls) > 42:
|
||||
raise MemoryError()
|
||||
return Match(first, second, 0)
|
||||
|
||||
objects = [NamedObject() for i in range(10)] # results in 45 matches
|
||||
objects = [NamedObject() for _ in range(10)] # results in 45 matches
|
||||
monkeypatch.setattr(engine, "get_match", mocked_match)
|
||||
try:
|
||||
r = getmatches(objects)
|
||||
except MemoryError:
|
||||
self.fail("MemorryError must be handled")
|
||||
self.fail("MemoryError must be handled")
|
||||
eq_(42, len(r))
|
||||
|
||||
|
||||
class TestCaseGetMatchesByContents:
|
||||
def test_dont_compare_empty_files(self):
|
||||
o1, o2 = no(size=0), no(size=0)
|
||||
assert not getmatches_by_contents([o1, o2])
|
||||
|
||||
def test_big_file_partial_hashes(self):
|
||||
smallsize = 1
|
||||
bigsize = 100 * 1024 * 1024 # 100MB
|
||||
@@ -563,7 +558,7 @@ class TestCaseGetMatchesByContents:
|
||||
|
||||
|
||||
class TestCaseGroup:
|
||||
def test_empy(self):
|
||||
def test_empty(self):
|
||||
g = Group()
|
||||
eq_(None, g.ref)
|
||||
eq_([], g.dupes)
|
||||
@@ -802,14 +797,14 @@ class TestCaseGroup:
|
||||
eq_(0, len(g.candidates))
|
||||
|
||||
|
||||
class TestCaseget_groups:
|
||||
class TestCaseGetGroups:
|
||||
def test_empty(self):
|
||||
r = get_groups([])
|
||||
eq_([], r)
|
||||
|
||||
def test_simple(self):
|
||||
itemList = [NamedObject("foo bar"), NamedObject("bar bleh")]
|
||||
matches = getmatches(itemList)
|
||||
item_list = [NamedObject("foo bar"), NamedObject("bar bleh")]
|
||||
matches = getmatches(item_list)
|
||||
m = matches[0]
|
||||
r = get_groups(matches)
|
||||
eq_(1, len(r))
|
||||
@@ -819,15 +814,15 @@ class TestCaseget_groups:
|
||||
|
||||
def test_group_with_multiple_matches(self):
|
||||
# This results in 3 matches
|
||||
itemList = [NamedObject("foo"), NamedObject("foo"), NamedObject("foo")]
|
||||
matches = getmatches(itemList)
|
||||
item_list = [NamedObject("foo"), NamedObject("foo"), NamedObject("foo")]
|
||||
matches = getmatches(item_list)
|
||||
r = get_groups(matches)
|
||||
eq_(1, len(r))
|
||||
g = r[0]
|
||||
eq_(3, len(g))
|
||||
|
||||
def test_must_choose_a_group(self):
|
||||
itemList = [
|
||||
item_list = [
|
||||
NamedObject("a b"),
|
||||
NamedObject("a b"),
|
||||
NamedObject("b c"),
|
||||
@@ -836,13 +831,13 @@ class TestCaseget_groups:
|
||||
]
|
||||
# There will be 2 groups here: group "a b" and group "c d"
|
||||
# "b c" can go either of them, but not both.
|
||||
matches = getmatches(itemList)
|
||||
matches = getmatches(item_list)
|
||||
r = get_groups(matches)
|
||||
eq_(2, len(r))
|
||||
eq_(5, len(r[0]) + len(r[1]))
|
||||
|
||||
def test_should_all_go_in_the_same_group(self):
|
||||
itemList = [
|
||||
item_list = [
|
||||
NamedObject("a b"),
|
||||
NamedObject("a b"),
|
||||
NamedObject("a b"),
|
||||
@@ -850,7 +845,7 @@ class TestCaseget_groups:
|
||||
]
|
||||
# There will be 2 groups here: group "a b" and group "c d"
|
||||
# "b c" can fit in both, but it must be in only one of them
|
||||
matches = getmatches(itemList)
|
||||
matches = getmatches(item_list)
|
||||
r = get_groups(matches)
|
||||
eq_(1, len(r))
|
||||
|
||||
@@ -869,8 +864,8 @@ class TestCaseget_groups:
|
||||
assert o3 in g
|
||||
|
||||
def test_four_sized_group(self):
|
||||
itemList = [NamedObject("foobar") for i in range(4)]
|
||||
m = getmatches(itemList)
|
||||
item_list = [NamedObject("foobar") for _ in range(4)]
|
||||
m = getmatches(item_list)
|
||||
r = get_groups(m)
|
||||
eq_(1, len(r))
|
||||
eq_(4, len(r[0]))
|
||||
|
||||
@@ -5,12 +5,8 @@
|
||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import io
|
||||
|
||||
# import os.path as op
|
||||
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
# from pytest import raises
|
||||
from hscommon.testutil import eq_
|
||||
from hscommon.plat import ISWINDOWS
|
||||
|
||||
@@ -144,11 +140,7 @@ class TestCaseListEmpty:
|
||||
def test_force_add_not_compilable(self):
|
||||
"""Used when loading from XML for example"""
|
||||
regex = r"one))"
|
||||
try:
|
||||
self.exclude_list.add(regex, forced=True)
|
||||
except Exception as e:
|
||||
# Should not get an exception here unless it's a duplicate regex
|
||||
raise e
|
||||
marked = self.exclude_list.mark(regex)
|
||||
eq_(marked, False) # can't be marked since not compilable
|
||||
eq_(len(self.exclude_list), 1)
|
||||
@@ -232,7 +224,6 @@ class TestCaseListEmpty:
|
||||
found = True
|
||||
if not found:
|
||||
raise (Exception(f"Default RE {re} not found in compiled list."))
|
||||
continue
|
||||
eq_(len(default_regexes), len(self.exclude_list.compiled))
|
||||
|
||||
|
||||
|
||||
@@ -16,54 +16,54 @@ from ..ignore import IgnoreList
|
||||
def test_empty():
|
||||
il = IgnoreList()
|
||||
eq_(0, len(il))
|
||||
assert not il.AreIgnored("foo", "bar")
|
||||
assert not il.are_ignored("foo", "bar")
|
||||
|
||||
|
||||
def test_simple():
|
||||
il = IgnoreList()
|
||||
il.Ignore("foo", "bar")
|
||||
assert il.AreIgnored("foo", "bar")
|
||||
assert il.AreIgnored("bar", "foo")
|
||||
assert not il.AreIgnored("foo", "bleh")
|
||||
assert not il.AreIgnored("bleh", "bar")
|
||||
il.ignore("foo", "bar")
|
||||
assert il.are_ignored("foo", "bar")
|
||||
assert il.are_ignored("bar", "foo")
|
||||
assert not il.are_ignored("foo", "bleh")
|
||||
assert not il.are_ignored("bleh", "bar")
|
||||
eq_(1, len(il))
|
||||
|
||||
|
||||
def test_multiple():
|
||||
il = IgnoreList()
|
||||
il.Ignore("foo", "bar")
|
||||
il.Ignore("foo", "bleh")
|
||||
il.Ignore("bleh", "bar")
|
||||
il.Ignore("aybabtu", "bleh")
|
||||
assert il.AreIgnored("foo", "bar")
|
||||
assert il.AreIgnored("bar", "foo")
|
||||
assert il.AreIgnored("foo", "bleh")
|
||||
assert il.AreIgnored("bleh", "bar")
|
||||
assert not il.AreIgnored("aybabtu", "bar")
|
||||
il.ignore("foo", "bar")
|
||||
il.ignore("foo", "bleh")
|
||||
il.ignore("bleh", "bar")
|
||||
il.ignore("aybabtu", "bleh")
|
||||
assert il.are_ignored("foo", "bar")
|
||||
assert il.are_ignored("bar", "foo")
|
||||
assert il.are_ignored("foo", "bleh")
|
||||
assert il.are_ignored("bleh", "bar")
|
||||
assert not il.are_ignored("aybabtu", "bar")
|
||||
eq_(4, len(il))
|
||||
|
||||
|
||||
def test_clear():
|
||||
il = IgnoreList()
|
||||
il.Ignore("foo", "bar")
|
||||
il.Clear()
|
||||
assert not il.AreIgnored("foo", "bar")
|
||||
assert not il.AreIgnored("bar", "foo")
|
||||
il.ignore("foo", "bar")
|
||||
il.clear()
|
||||
assert not il.are_ignored("foo", "bar")
|
||||
assert not il.are_ignored("bar", "foo")
|
||||
eq_(0, len(il))
|
||||
|
||||
|
||||
def test_add_same_twice():
|
||||
il = IgnoreList()
|
||||
il.Ignore("foo", "bar")
|
||||
il.Ignore("bar", "foo")
|
||||
il.ignore("foo", "bar")
|
||||
il.ignore("bar", "foo")
|
||||
eq_(1, len(il))
|
||||
|
||||
|
||||
def test_save_to_xml():
|
||||
il = IgnoreList()
|
||||
il.Ignore("foo", "bar")
|
||||
il.Ignore("foo", "bleh")
|
||||
il.Ignore("bleh", "bar")
|
||||
il.ignore("foo", "bar")
|
||||
il.ignore("foo", "bleh")
|
||||
il.ignore("bleh", "bar")
|
||||
f = io.BytesIO()
|
||||
il.save_to_xml(f)
|
||||
f.seek(0)
|
||||
@@ -77,22 +77,22 @@ def test_save_to_xml():
|
||||
eq_(len(subchildren), 3)
|
||||
|
||||
|
||||
def test_SaveThenLoad():
|
||||
def test_save_then_load():
|
||||
il = IgnoreList()
|
||||
il.Ignore("foo", "bar")
|
||||
il.Ignore("foo", "bleh")
|
||||
il.Ignore("bleh", "bar")
|
||||
il.Ignore("\u00e9", "bar")
|
||||
il.ignore("foo", "bar")
|
||||
il.ignore("foo", "bleh")
|
||||
il.ignore("bleh", "bar")
|
||||
il.ignore("\u00e9", "bar")
|
||||
f = io.BytesIO()
|
||||
il.save_to_xml(f)
|
||||
f.seek(0)
|
||||
il = IgnoreList()
|
||||
il.load_from_xml(f)
|
||||
eq_(4, len(il))
|
||||
assert il.AreIgnored("\u00e9", "bar")
|
||||
assert il.are_ignored("\u00e9", "bar")
|
||||
|
||||
|
||||
def test_LoadXML_with_empty_file_tags():
|
||||
def test_load_xml_with_empty_file_tags():
|
||||
f = io.BytesIO()
|
||||
f.write(b'<?xml version="1.0" encoding="utf-8"?><ignore_list><file><file/></file></ignore_list>')
|
||||
f.seek(0)
|
||||
@@ -101,18 +101,18 @@ def test_LoadXML_with_empty_file_tags():
|
||||
eq_(0, len(il))
|
||||
|
||||
|
||||
def test_AreIgnore_works_when_a_child_is_a_key_somewhere_else():
|
||||
def test_are_ignore_works_when_a_child_is_a_key_somewhere_else():
|
||||
il = IgnoreList()
|
||||
il.Ignore("foo", "bar")
|
||||
il.Ignore("bar", "baz")
|
||||
assert il.AreIgnored("bar", "foo")
|
||||
il.ignore("foo", "bar")
|
||||
il.ignore("bar", "baz")
|
||||
assert il.are_ignored("bar", "foo")
|
||||
|
||||
|
||||
def test_no_dupes_when_a_child_is_a_key_somewhere_else():
|
||||
il = IgnoreList()
|
||||
il.Ignore("foo", "bar")
|
||||
il.Ignore("bar", "baz")
|
||||
il.Ignore("bar", "foo")
|
||||
il.ignore("foo", "bar")
|
||||
il.ignore("bar", "baz")
|
||||
il.ignore("bar", "foo")
|
||||
eq_(2, len(il))
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ def test_iterate():
|
||||
il = IgnoreList()
|
||||
expected = [("foo", "bar"), ("bar", "baz"), ("foo", "baz")]
|
||||
for i in expected:
|
||||
il.Ignore(i[0], i[1])
|
||||
il.ignore(i[0], i[1])
|
||||
for i in il:
|
||||
expected.remove(i) # No exception should be raised
|
||||
assert not expected # expected should be empty
|
||||
@@ -129,18 +129,18 @@ def test_iterate():
|
||||
|
||||
def test_filter():
|
||||
il = IgnoreList()
|
||||
il.Ignore("foo", "bar")
|
||||
il.Ignore("bar", "baz")
|
||||
il.Ignore("foo", "baz")
|
||||
il.Filter(lambda f, s: f == "bar")
|
||||
il.ignore("foo", "bar")
|
||||
il.ignore("bar", "baz")
|
||||
il.ignore("foo", "baz")
|
||||
il.filter(lambda f, s: f == "bar")
|
||||
eq_(1, len(il))
|
||||
assert not il.AreIgnored("foo", "bar")
|
||||
assert il.AreIgnored("bar", "baz")
|
||||
assert not il.are_ignored("foo", "bar")
|
||||
assert il.are_ignored("bar", "baz")
|
||||
|
||||
|
||||
def test_save_with_non_ascii_items():
|
||||
il = IgnoreList()
|
||||
il.Ignore("\xac", "\xbf")
|
||||
il.ignore("\xac", "\xbf")
|
||||
f = io.BytesIO()
|
||||
try:
|
||||
il.save_to_xml(f)
|
||||
@@ -151,29 +151,29 @@ def test_save_with_non_ascii_items():
|
||||
def test_len():
|
||||
il = IgnoreList()
|
||||
eq_(0, len(il))
|
||||
il.Ignore("foo", "bar")
|
||||
il.ignore("foo", "bar")
|
||||
eq_(1, len(il))
|
||||
|
||||
|
||||
def test_nonzero():
|
||||
il = IgnoreList()
|
||||
assert not il
|
||||
il.Ignore("foo", "bar")
|
||||
il.ignore("foo", "bar")
|
||||
assert il
|
||||
|
||||
|
||||
def test_remove():
|
||||
il = IgnoreList()
|
||||
il.Ignore("foo", "bar")
|
||||
il.Ignore("foo", "baz")
|
||||
il.ignore("foo", "bar")
|
||||
il.ignore("foo", "baz")
|
||||
il.remove("bar", "foo")
|
||||
eq_(len(il), 1)
|
||||
assert not il.AreIgnored("foo", "bar")
|
||||
assert not il.are_ignored("foo", "bar")
|
||||
|
||||
|
||||
def test_remove_non_existant():
|
||||
il = IgnoreList()
|
||||
il.Ignore("foo", "bar")
|
||||
il.Ignore("foo", "baz")
|
||||
il.ignore("foo", "bar")
|
||||
il.ignore("foo", "baz")
|
||||
with raises(ValueError):
|
||||
il.remove("foo", "bleh")
|
||||
|
||||
@@ -402,7 +402,7 @@ class TestCaseResultsMarkings:
|
||||
self.results.make_ref(d)
|
||||
eq_("0 / 3 (0.00 B / 3.00 B) duplicates marked.", self.results.stat_line)
|
||||
|
||||
def test_SaveXML(self):
|
||||
def test_save_xml(self):
|
||||
self.results.mark(self.objects[1])
|
||||
self.results.mark_invert()
|
||||
f = io.BytesIO()
|
||||
@@ -419,7 +419,7 @@ class TestCaseResultsMarkings:
|
||||
eq_("n", d1.get("marked"))
|
||||
eq_("y", d2.get("marked"))
|
||||
|
||||
def test_LoadXML(self):
|
||||
def test_load_xml(self):
|
||||
def get_file(path):
|
||||
return [f for f in self.objects if str(f.path) == path][0]
|
||||
|
||||
@@ -485,7 +485,7 @@ class TestCaseResultsXML:
|
||||
eq_("ibabtu", d1.get("words"))
|
||||
eq_("ibabtu", d2.get("words"))
|
||||
|
||||
def test_LoadXML(self):
|
||||
def test_load_xml(self):
|
||||
def get_file(path):
|
||||
return [f for f in self.objects if str(f.path) == path][0]
|
||||
|
||||
@@ -517,7 +517,7 @@ class TestCaseResultsXML:
|
||||
eq_(["ibabtu"], g2[0].words)
|
||||
eq_(["ibabtu"], g2[1].words)
|
||||
|
||||
def test_LoadXML_with_filename(self, tmpdir):
|
||||
def test_load_xml_with_filename(self, tmpdir):
|
||||
def get_file(path):
|
||||
return [f for f in self.objects if str(f.path) == path][0]
|
||||
|
||||
@@ -529,7 +529,7 @@ class TestCaseResultsXML:
|
||||
r.load_from_xml(filename, get_file)
|
||||
eq_(2, len(r.groups))
|
||||
|
||||
def test_LoadXML_with_some_files_that_dont_exist_anymore(self):
|
||||
def test_load_xml_with_some_files_that_dont_exist_anymore(self):
|
||||
def get_file(path):
|
||||
if path.endswith("ibabtu 2"):
|
||||
return None
|
||||
@@ -545,7 +545,7 @@ class TestCaseResultsXML:
|
||||
eq_(1, len(r.groups))
|
||||
eq_(3, len(r.groups[0]))
|
||||
|
||||
def test_LoadXML_missing_attributes_and_bogus_elements(self):
|
||||
def test_load_xml_missing_attributes_and_bogus_elements(self):
|
||||
def get_file(path):
|
||||
return [f for f in self.objects if str(f.path) == path][0]
|
||||
|
||||
|
||||
@@ -52,10 +52,12 @@ def test_empty(fake_fileexists):
|
||||
def test_default_settings(fake_fileexists):
|
||||
s = Scanner()
|
||||
eq_(s.min_match_percentage, 80)
|
||||
eq_(s.scan_type, ScanType.Filename)
|
||||
eq_(s.scan_type, ScanType.FILENAME)
|
||||
eq_(s.mix_file_kind, True)
|
||||
eq_(s.word_weighting, False)
|
||||
eq_(s.match_similar_words, False)
|
||||
eq_(s.size_threshold, 0)
|
||||
eq_(s.large_size_threshold, 0)
|
||||
eq_(s.big_file_size_threshold, 0)
|
||||
|
||||
|
||||
@@ -98,7 +100,7 @@ def test_trim_all_ref_groups(fake_fileexists):
|
||||
eq_(s.discarded_file_count, 0)
|
||||
|
||||
|
||||
def test_priorize(fake_fileexists):
|
||||
def test_prioritize(fake_fileexists):
|
||||
s = Scanner()
|
||||
f = [
|
||||
no("foo", path="p1"),
|
||||
@@ -119,7 +121,7 @@ def test_priorize(fake_fileexists):
|
||||
|
||||
def test_content_scan(fake_fileexists):
|
||||
s = Scanner()
|
||||
s.scan_type = ScanType.Contents
|
||||
s.scan_type = ScanType.CONTENTS
|
||||
f = [no("foo"), no("bar"), no("bleh")]
|
||||
f[0].md5 = f[0].md5partial = f[0].md5samples = "foobar"
|
||||
f[1].md5 = f[1].md5partial = f[1].md5samples = "foobar"
|
||||
@@ -133,18 +135,62 @@ def test_content_scan(fake_fileexists):
|
||||
def test_content_scan_compare_sizes_first(fake_fileexists):
|
||||
class MyFile(no):
|
||||
@property
|
||||
def md5(file):
|
||||
def md5(self):
|
||||
raise AssertionError()
|
||||
|
||||
s = Scanner()
|
||||
s.scan_type = ScanType.Contents
|
||||
s.scan_type = ScanType.CONTENTS
|
||||
f = [MyFile("foo", 1), MyFile("bar", 2)]
|
||||
eq_(len(s.get_dupe_groups(f)), 0)
|
||||
|
||||
|
||||
def test_ignore_file_size(fake_fileexists):
|
||||
s = Scanner()
|
||||
s.scan_type = ScanType.CONTENTS
|
||||
small_size = 10 # 10KB
|
||||
s.size_threshold = 0
|
||||
large_size = 100 * 1024 * 1024 # 100MB
|
||||
s.large_size_threshold = 0
|
||||
f = [
|
||||
no("smallignore1", small_size - 1),
|
||||
no("smallignore2", small_size - 1),
|
||||
no("small1", small_size),
|
||||
no("small2", small_size),
|
||||
no("large1", large_size),
|
||||
no("large2", large_size),
|
||||
no("largeignore1", large_size + 1),
|
||||
no("largeignore2", large_size + 1),
|
||||
]
|
||||
f[0].md5 = f[0].md5partial = f[0].md5samples = "smallignore"
|
||||
f[1].md5 = f[1].md5partial = f[1].md5samples = "smallignore"
|
||||
f[2].md5 = f[2].md5partial = f[2].md5samples = "small"
|
||||
f[3].md5 = f[3].md5partial = f[3].md5samples = "small"
|
||||
f[4].md5 = f[4].md5partial = f[4].md5samples = "large"
|
||||
f[5].md5 = f[5].md5partial = f[5].md5samples = "large"
|
||||
f[6].md5 = f[6].md5partial = f[6].md5samples = "largeignore"
|
||||
f[7].md5 = f[7].md5partial = f[7].md5samples = "largeignore"
|
||||
|
||||
r = s.get_dupe_groups(f)
|
||||
# No ignores
|
||||
eq_(len(r), 4)
|
||||
# Ignore smaller
|
||||
s.size_threshold = small_size
|
||||
r = s.get_dupe_groups(f)
|
||||
eq_(len(r), 3)
|
||||
# Ignore larger
|
||||
s.size_threshold = 0
|
||||
s.large_size_threshold = large_size
|
||||
r = s.get_dupe_groups(f)
|
||||
eq_(len(r), 3)
|
||||
# Ignore both
|
||||
s.size_threshold = small_size
|
||||
r = s.get_dupe_groups(f)
|
||||
eq_(len(r), 2)
|
||||
|
||||
|
||||
def test_big_file_partial_hashes(fake_fileexists):
|
||||
s = Scanner()
|
||||
s.scan_type = ScanType.Contents
|
||||
s.scan_type = ScanType.CONTENTS
|
||||
|
||||
smallsize = 1
|
||||
bigsize = 100 * 1024 * 1024 # 100MB
|
||||
@@ -173,7 +219,7 @@ def test_big_file_partial_hashes(fake_fileexists):
|
||||
|
||||
def test_min_match_perc_doesnt_matter_for_content_scan(fake_fileexists):
|
||||
s = Scanner()
|
||||
s.scan_type = ScanType.Contents
|
||||
s.scan_type = ScanType.CONTENTS
|
||||
f = [no("foo"), no("bar"), no("bleh")]
|
||||
f[0].md5 = f[0].md5partial = f[0].md5samples = "foobar"
|
||||
f[1].md5 = f[1].md5partial = f[1].md5samples = "foobar"
|
||||
@@ -190,7 +236,7 @@ def test_min_match_perc_doesnt_matter_for_content_scan(fake_fileexists):
|
||||
|
||||
def test_content_scan_doesnt_put_md5_in_words_at_the_end(fake_fileexists):
|
||||
s = Scanner()
|
||||
s.scan_type = ScanType.Contents
|
||||
s.scan_type = ScanType.CONTENTS
|
||||
f = [no("foo"), no("bar")]
|
||||
f[0].md5 = f[0].md5partial = f[0].md5samples = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
|
||||
f[1].md5 = f[1].md5partial = f[1].md5samples = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
|
||||
@@ -256,7 +302,7 @@ def test_similar_words(fake_fileexists):
|
||||
|
||||
def test_fields(fake_fileexists):
|
||||
s = Scanner()
|
||||
s.scan_type = ScanType.Fields
|
||||
s.scan_type = ScanType.FIELDS
|
||||
f = [no("The White Stripes - Little Ghost"), no("The White Stripes - Little Acorn")]
|
||||
r = s.get_dupe_groups(f)
|
||||
eq_(len(r), 0)
|
||||
@@ -264,7 +310,7 @@ def test_fields(fake_fileexists):
|
||||
|
||||
def test_fields_no_order(fake_fileexists):
|
||||
s = Scanner()
|
||||
s.scan_type = ScanType.FieldsNoOrder
|
||||
s.scan_type = ScanType.FIELDSNOORDER
|
||||
f = [no("The White Stripes - Little Ghost"), no("Little Ghost - The White Stripes")]
|
||||
r = s.get_dupe_groups(f)
|
||||
eq_(len(r), 1)
|
||||
@@ -272,7 +318,7 @@ def test_fields_no_order(fake_fileexists):
|
||||
|
||||
def test_tag_scan(fake_fileexists):
|
||||
s = Scanner()
|
||||
s.scan_type = ScanType.Tag
|
||||
s.scan_type = ScanType.TAG
|
||||
o1 = no("foo")
|
||||
o2 = no("bar")
|
||||
o1.artist = "The White Stripes"
|
||||
@@ -285,7 +331,7 @@ def test_tag_scan(fake_fileexists):
|
||||
|
||||
def test_tag_with_album_scan(fake_fileexists):
|
||||
s = Scanner()
|
||||
s.scan_type = ScanType.Tag
|
||||
s.scan_type = ScanType.TAG
|
||||
s.scanned_tags = set(["artist", "album", "title"])
|
||||
o1 = no("foo")
|
||||
o2 = no("bar")
|
||||
@@ -305,7 +351,7 @@ def test_tag_with_album_scan(fake_fileexists):
|
||||
|
||||
def test_that_dash_in_tags_dont_create_new_fields(fake_fileexists):
|
||||
s = Scanner()
|
||||
s.scan_type = ScanType.Tag
|
||||
s.scan_type = ScanType.TAG
|
||||
s.scanned_tags = set(["artist", "album", "title"])
|
||||
s.min_match_percentage = 50
|
||||
o1 = no("foo")
|
||||
@@ -322,7 +368,7 @@ def test_that_dash_in_tags_dont_create_new_fields(fake_fileexists):
|
||||
|
||||
def test_tag_scan_with_different_scanned(fake_fileexists):
|
||||
s = Scanner()
|
||||
s.scan_type = ScanType.Tag
|
||||
s.scan_type = ScanType.TAG
|
||||
s.scanned_tags = set(["track", "year"])
|
||||
o1 = no("foo")
|
||||
o2 = no("bar")
|
||||
@@ -340,7 +386,7 @@ def test_tag_scan_with_different_scanned(fake_fileexists):
|
||||
|
||||
def test_tag_scan_only_scans_existing_tags(fake_fileexists):
|
||||
s = Scanner()
|
||||
s.scan_type = ScanType.Tag
|
||||
s.scan_type = ScanType.TAG
|
||||
s.scanned_tags = set(["artist", "foo"])
|
||||
o1 = no("foo")
|
||||
o2 = no("bar")
|
||||
@@ -354,7 +400,7 @@ def test_tag_scan_only_scans_existing_tags(fake_fileexists):
|
||||
|
||||
def test_tag_scan_converts_to_str(fake_fileexists):
|
||||
s = Scanner()
|
||||
s.scan_type = ScanType.Tag
|
||||
s.scan_type = ScanType.TAG
|
||||
s.scanned_tags = set(["track"])
|
||||
o1 = no("foo")
|
||||
o2 = no("bar")
|
||||
@@ -369,7 +415,7 @@ def test_tag_scan_converts_to_str(fake_fileexists):
|
||||
|
||||
def test_tag_scan_non_ascii(fake_fileexists):
|
||||
s = Scanner()
|
||||
s.scan_type = ScanType.Tag
|
||||
s.scan_type = ScanType.TAG
|
||||
s.scanned_tags = set(["title"])
|
||||
o1 = no("foo")
|
||||
o2 = no("bar")
|
||||
@@ -391,8 +437,8 @@ def test_ignore_list(fake_fileexists):
|
||||
f2.path = Path("dir2/foobar")
|
||||
f3.path = Path("dir3/foobar")
|
||||
ignore_list = IgnoreList()
|
||||
ignore_list.Ignore(str(f1.path), str(f2.path))
|
||||
ignore_list.Ignore(str(f1.path), str(f3.path))
|
||||
ignore_list.ignore(str(f1.path), str(f2.path))
|
||||
ignore_list.ignore(str(f1.path), str(f3.path))
|
||||
r = s.get_dupe_groups([f1, f2, f3], ignore_list=ignore_list)
|
||||
eq_(len(r), 1)
|
||||
g = r[0]
|
||||
@@ -415,8 +461,8 @@ def test_ignore_list_checks_for_unicode(fake_fileexists):
|
||||
f2.path = Path("foo2\u00e9")
|
||||
f3.path = Path("foo3\u00e9")
|
||||
ignore_list = IgnoreList()
|
||||
ignore_list.Ignore(str(f1.path), str(f2.path))
|
||||
ignore_list.Ignore(str(f1.path), str(f3.path))
|
||||
ignore_list.ignore(str(f1.path), str(f2.path))
|
||||
ignore_list.ignore(str(f1.path), str(f3.path))
|
||||
r = s.get_dupe_groups([f1, f2, f3], ignore_list=ignore_list)
|
||||
eq_(len(r), 1)
|
||||
g = r[0]
|
||||
@@ -520,7 +566,7 @@ def test_dont_group_files_that_dont_exist(tmpdir):
|
||||
# In this test, we have to delete one of the files between the get_matches() part and the
|
||||
# get_groups() part.
|
||||
s = Scanner()
|
||||
s.scan_type = ScanType.Contents
|
||||
s.scan_type = ScanType.CONTENTS
|
||||
p = Path(str(tmpdir))
|
||||
p["file1"].open("w").write("foo")
|
||||
p["file2"].open("w").write("foo")
|
||||
@@ -539,7 +585,7 @@ def test_folder_scan_exclude_subfolder_matches(fake_fileexists):
|
||||
# when doing a Folders scan type, don't include matches for folders whose parent folder already
|
||||
# match.
|
||||
s = Scanner()
|
||||
s.scan_type = ScanType.Folders
|
||||
s.scan_type = ScanType.FOLDERS
|
||||
topf1 = no("top folder 1", size=42)
|
||||
topf1.md5 = topf1.md5partial = topf1.md5samples = b"some_md5_1"
|
||||
topf1.path = Path("/topf1")
|
||||
@@ -574,7 +620,7 @@ def test_dont_count_ref_files_as_discarded(fake_fileexists):
|
||||
# However, this causes problems in "discarded" counting and we make sure here that we don't
|
||||
# report discarded matches in exact duplicate scans.
|
||||
s = Scanner()
|
||||
s.scan_type = ScanType.Contents
|
||||
s.scan_type = ScanType.CONTENTS
|
||||
o1 = no("foo", path="p1")
|
||||
o2 = no("foo", path="p2")
|
||||
o3 = no("foo", path="p3")
|
||||
@@ -587,8 +633,8 @@ def test_dont_count_ref_files_as_discarded(fake_fileexists):
|
||||
eq_(s.discarded_file_count, 0)
|
||||
|
||||
|
||||
def test_priorize_me(fake_fileexists):
|
||||
# in ScannerME, bitrate goes first (right after is_ref) in priorization
|
||||
def test_prioritize_me(fake_fileexists):
|
||||
# in ScannerME, bitrate goes first (right after is_ref) in prioritization
|
||||
s = ScannerME()
|
||||
o1, o2 = no("foo", path="p1"), no("foo", path="p2")
|
||||
o1.bitrate = 1
|
||||
|
||||
@@ -1,3 +1,42 @@
|
||||
=== 4.2.1 (2022-03-25)
|
||||
* Default to English on unsupported system language (#976)
|
||||
* Fix image viewer zoom datatype issue (#978)
|
||||
* Fix errors from window change event (#937, #980)
|
||||
* Fix deprecation warning from SQLite
|
||||
* Enforce minimum Windows version in installer (#983)
|
||||
* Fix help path for local files
|
||||
* Drop python 3.6 support
|
||||
* VS Code project settings added, yaml validation for GitHub actions
|
||||
|
||||
=== 4.2.0 (2021-01-24)
|
||||
|
||||
* Add Malay and Turkish
|
||||
* Add dark style for windows (#900)
|
||||
* Add caching md5 file hashes (#942)
|
||||
* Add feature to partially hash large files, with user adjustable preference (#908)
|
||||
* Add portable mode (store settings next to executable)
|
||||
* Add file association for .dupeguru files on windows
|
||||
* Add ability to pass .dupeguru file to load on startup (#902)
|
||||
* Add ability to reveal in explorer/finder (#895)
|
||||
* Switch audio tag processing from hsaudiotag to mutagen (#440)
|
||||
* Add ability to use Qt dialogs instead of native OS dialogs for some file selection operations
|
||||
* Add OS and Python details to error dialog to assist in troubleshooting
|
||||
* Add preference to ignore large files with threshold (#430)
|
||||
* Fix error on close from DetailsPanel (#857, #873)
|
||||
* Change reference background color (#894, #898)
|
||||
* Remove stripping of unicode characters when matching names (#879)
|
||||
* Fix exception when deleting in delta view (#863, #905)
|
||||
* Fix dupes only view not updating after re-prioritize results (#757, #910, #911)
|
||||
* Fix ability to drag'n'drop file/folder with certain characters in name (#897)
|
||||
* Fix window position opening partially offscreen (#653)
|
||||
* Fix TypeError is photo mode (#551)
|
||||
* Change message for when files are deleted directly (#904)
|
||||
* Add more feedback during scan (#700)
|
||||
* Add Python version check to build.py (#589)
|
||||
* General code cleanups
|
||||
* Improvements to using standardized build tooling
|
||||
* Moved CI/CD to github actions, added codeql, SonarCloud
|
||||
|
||||
=== 4.1.1 (2021-03-21)
|
||||
|
||||
* Add Japanese
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Häufig gestellte Fragen
|
||||
==========================
|
||||
|
||||
.. topic:: What is |appname|?
|
||||
.. topic:: What is dupeGuru?
|
||||
|
||||
.. only:: edition_se
|
||||
|
||||
@@ -25,7 +25,7 @@ Häufig gestellte Fragen
|
||||
|
||||
.. topic:: Was sind die Demo-Einschränkungen von dupeGuru?
|
||||
|
||||
Keine, |appname| ist `Fairware <http://open.hardcoded.net/about/>`_.
|
||||
Keine, dupeGuru ist `Fairware <http://open.hardcoded.net/about/>`_.
|
||||
|
||||
.. topic:: Die Markierungsbox einer Datei, die ich löschen möchte, ist deaktiviert. Was muss ich tun?
|
||||
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
|appname| Hilfe
|
||||
dupeGuru Hilfe
|
||||
===============
|
||||
|
||||
.. only:: edition_se
|
||||
|
||||
Dieses Dokument ist auch auf `Englisch <http://www.hardcoded.net/dupeguru/help/en/>`__ und `Französisch <http://www.hardcoded.net/dupeguru/help/fr/>`__ verfügbar.
|
||||
|
||||
.. only:: edition_me
|
||||
|
||||
Dieses Dokument ist auch auf `Englisch <http://www.hardcoded.net/dupeguru/help/en/>`__ und `Französisch <http://www.hardcoded.net/dupeguru_me/help/fr/>`__ verfügbar.
|
||||
|
||||
.. only:: edition_pe
|
||||
|
||||
Dieses Dokument ist auch auf `Englisch <http://www.hardcoded.net/dupeguru/help/en/>`__ und `Französisch <http://www.hardcoded.net/dupeguru_pe/help/fr/>`__ verfügbar.
|
||||
Dieses Dokument ist auch auf `Englisch <http://dupeguru.voltaicideas.net/help/en/>`__ und `Französisch <http://dupeguru.voltaicideas.net/help/fr/>`__ verfügbar.
|
||||
|
||||
.. only:: edition_se or edition_me
|
||||
|
||||
|appname| ist ein Tool zum Auffinden von Duplikaten auf Ihrem Computer. Es kann entweder Dateinamen oder Inhalte scannen. Der Dateiname-Scan stellt einen lockeren Suchalgorithmus zur Verfügung, der sogar Duplikate findet, die nicht den exakten selben Namen haben.
|
||||
dupeGuru ist ein Tool zum Auffinden von Duplikaten auf Ihrem Computer. Es kann entweder Dateinamen oder Inhalte scannen. Der Dateiname-Scan stellt einen lockeren Suchalgorithmus zur Verfügung, der sogar Duplikate findet, die nicht den exakten selben Namen haben.
|
||||
|
||||
.. only:: edition_pe
|
||||
|
||||
@@ -23,7 +15,7 @@
|
||||
|
||||
Obwohl dupeGuru auch leicht ohne Dokumentation genutzt werden kann, ist es sinnvoll die Hilfe zu lesen. Wenn Sie nach einer Führung für den ersten Duplikatscan suchen, werfen Sie einen Blick auf die :doc:`Schnellstart <quick_start>` Sektion
|
||||
|
||||
Es ist eine gute Idee |appname| aktuell zu halten. Sie können die neueste Version auf der `homepage`_ finden.
|
||||
Es ist eine gute Idee dupeGuru aktuell zu halten. Sie können die neueste Version auf der http://dupeguru.voltaicideas.net finden.
|
||||
|
||||
Inhalte:
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ a community around this project.
|
||||
|
||||
So, whatever your skills, if you're interested in contributing to dupeGuru, please do so. Normally,
|
||||
this documentation should be enough to get you started, but if it isn't, then **please**,
|
||||
`let me know`_ because it's a problem that I'm committed to fix. If there's any situation where you'd
|
||||
open a discussion at https://github.com/arsenetar/dupeguru/discussions. If there's any situation where you'd
|
||||
wish to contribute but some doubt you're having prevent you from going forward, please contact me.
|
||||
I'd much prefer to spend the time figuring out with you whether (and how) you can contribute than
|
||||
taking the chance of missing that opportunity.
|
||||
@@ -82,10 +82,9 @@ agree on what should be added to the documentation.
|
||||
dupeGuru. For more information about how to do that, you can refer to the `translator guide`_.
|
||||
|
||||
.. _been open source: https://www.hardcoded.net/articles/free-as-in-speech-fair-as-in-trade
|
||||
.. _let me know: mailto:hsoft@hardcoded.net
|
||||
.. _Source code repository: https://github.com/hsoft/dupeguru
|
||||
.. _Issue Tracker: https://github.com/hsoft/dupeguru/issues
|
||||
.. _Issue labels meaning: https://github.com/hsoft/dupeguru/wiki/issue-labels
|
||||
.. _Source code repository: https://github.com/arsenetar/dupeguru
|
||||
.. _Issue Tracker: https://github.com/arsenetar/issues
|
||||
.. _Issue labels meaning: https://github.com/arsenetar/wiki/issue-labels
|
||||
.. _Sphinx: http://sphinx-doc.org/
|
||||
.. _reST: http://en.wikipedia.org/wiki/ReStructuredText
|
||||
.. _translator guide: https://github.com/hsoft/dupeguru/wiki/Translator-Guide
|
||||
.. _translator guide: https://github.com/arsenetar/wiki/Translator-Guide
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
hscommon.jobprogress.qt
|
||||
=======================
|
||||
|
||||
.. automodule:: hscommon.jobprogress.qt
|
||||
|
||||
.. autosummary::
|
||||
|
||||
Progress
|
||||
|
||||
.. autoclass:: Progress
|
||||
:members:
|
||||
|
||||
@@ -151,8 +151,6 @@ delete files" option that is offered to you when you activate Send to Trash. Thi
|
||||
files to the Trash, but delete them immediately. In some cases, for example on network storage
|
||||
(NAS), this has been known to work when normal deletion didn't.
|
||||
|
||||
If this fail, `HS forums`_ might be of some help.
|
||||
|
||||
Why is Picture mode's contents scan so slow?
|
||||
--------------------------------------------
|
||||
|
||||
@@ -178,7 +176,6 @@ Preferences are stored elsewhere:
|
||||
* Linux: ``~/.config/Hardcoded Software/dupeGuru.conf``
|
||||
* Mac OS X: In the built-in ``defaults`` system, as ``com.hardcoded-software.dupeguru``
|
||||
|
||||
.. _HS forums: https://forum.hardcoded.net/
|
||||
.. _Github: https://github.com/hsoft/dupeguru
|
||||
.. _open an issue: https://github.com/hsoft/dupeguru/wiki/issue-labels
|
||||
.. _Github: https://github.com/arsenetar/dupeguru
|
||||
.. _open an issue: https://github.com/arsenetar/dupeguru/wiki/issue-labels
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@ dupeGuru help
|
||||
|
||||
This help document is also available in these languages:
|
||||
|
||||
* `French <http://www.hardcoded.net/dupeguru/help/fr>`__
|
||||
* `German <http://www.hardcoded.net/dupeguru/help/de>`__
|
||||
* `Armenian <http://www.hardcoded.net/dupeguru/help/hy>`__
|
||||
* `Russian <http://www.hardcoded.net/dupeguru/help/ru>`__
|
||||
* `Ukrainian <http://www.hardcoded.net/dupeguru/help/uk>`__
|
||||
* `French <http://dupeguru.voltaicideas.net/help/fr>`__
|
||||
* `German <http://dupeguru.voltaicideas.net/help/de>`__
|
||||
* `Armenian <http://dupeguru.voltaicideas.net/help/hy>`__
|
||||
* `Russian <http://dupeguru.voltaicideas.net/help/ru>`__
|
||||
* `Ukrainian <http://dupeguru.voltaicideas.net/help/uk>`__
|
||||
|
||||
dupeGuru is a tool to find duplicate files on your computer. It has three
|
||||
modes, Standard, Music and Picture, with each mode having its own scan types
|
||||
@@ -42,4 +42,4 @@ Indices and tables
|
||||
* :ref:`genindex`
|
||||
* :ref:`search`
|
||||
|
||||
.. _homepage: https://www.hardcoded.net/dupeguru
|
||||
.. _homepage: https://dupeguru.voltaicideas.net/
|
||||
|
||||
@@ -3,7 +3,7 @@ Foire aux questions
|
||||
|
||||
.. contents::
|
||||
|
||||
Qu'est-ce que |appname|?
|
||||
Qu'est-ce que dupeGuru?
|
||||
------------------------
|
||||
|
||||
.. only:: edition_se
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
Aide |appname|
|
||||
Aide dupeGuru
|
||||
===============
|
||||
|
||||
.. only:: edition_se
|
||||
|
||||
Ce document est aussi disponible en `anglais <http://www.hardcoded.net/dupeguru/help/en/>`__, en `allemand <http://www.hardcoded.net/dupeguru/help/de/>`__ et en `arménien <http://www.hardcoded.net/dupeguru/help/hy/>`__.
|
||||
|
||||
.. only:: edition_me
|
||||
|
||||
Ce document est aussi disponible en `anglais <http://www.hardcoded.net/dupeguru_me/help/en/>`__, en `allemand <http://www.hardcoded.net/dupeguru_me/help/de/>`__ et en `arménien <http://www.hardcoded.net/dupeguru_me/help/hy/>`__.
|
||||
|
||||
.. only:: edition_pe
|
||||
|
||||
Ce document est aussi disponible en `anglais <http://www.hardcoded.net/dupeguru_pe/help/en/>`__, en `allemand <http://www.hardcoded.net/dupeguru_pe/help/de/>`__ et en `arménien <http://www.hardcoded.net/dupeguru_pe/help/hy/>`__.
|
||||
Ce document est aussi disponible en `anglais <http://dupeguru.voltaicideas.net/help/en/>`__, en `allemand <http://dupeguru.voltaicideas.net/help/de/>`__ et en `arménien <http://dupeguru.voltaicideas.net/help/hy/>`__.
|
||||
|
||||
.. only:: edition_se or edition_me
|
||||
|
||||
|appname| est un outil pour trouver des doublons parmi vos fichiers. Il peut comparer soit les noms de fichiers, soit le contenu. Le comparateur de nom de fichier peut trouver des doublons même si les noms ne sont pas exactement pareils.
|
||||
dupeGuru est un outil pour trouver des doublons parmi vos fichiers. Il peut comparer soit les noms de fichiers, soit le contenu. Le comparateur de nom de fichier peut trouver des doublons même si les noms ne sont pas exactement pareils.
|
||||
|
||||
.. only:: edition_pe
|
||||
|
||||
@@ -23,7 +15,7 @@ Aide |appname|
|
||||
|
||||
Bien que dupeGuru puisse être utilisé sans lire l'aide, une telle lecture vous permettra de bien comprendre comment l'application fonctionne. Pour un guide rapide pour une première utilisation, référez vous à la section :doc:`Démarrage Rapide <quick_start>`.
|
||||
|
||||
C'est toujours une bonne idée de garder |appname| à jour. Vous pouvez télécharger la dernière version sur sa `page web`_.
|
||||
C'est toujours une bonne idée de garder dupeGuru à jour. Vous pouvez télécharger la dernière version sur sa http://dupeguru.voltaicideas.net.
|
||||
|
||||
Contents:
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Հաճախ Տրվող Հարցեր
|
||||
==========================
|
||||
|
||||
.. topic:: Ի՞նչ է |appname|-ը:
|
||||
.. topic:: Ի՞նչ է dupeGuru-ը:
|
||||
|
||||
.. only:: edition_se
|
||||
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
|appname| help
|
||||
dupeGuru help
|
||||
===============
|
||||
|
||||
.. only:: edition_se
|
||||
|
||||
Այս փաստաթուղթը հասանելի է նաև՝ `Ֆրանսերեն <http://www.hardcoded.net/dupeguru/help/fr/>`__ և `Գերմաներեն <http://www.hardcoded.net/dupeguru/help/de/>`__.
|
||||
|
||||
.. only:: edition_me
|
||||
|
||||
Այս փաստաթուղթը հասանելի է նաև՝ `Ֆրանսերեն <http://www.hardcoded.net/dupeguru_me/help/fr/>`__ և `Գերմաներեն <http://www.hardcoded.net/dupeguru_me/help/de/>`__.
|
||||
|
||||
.. only:: edition_pe
|
||||
|
||||
Այս փաստաթուղթը հասանելի է նաև՝ `Ֆրանսերեն <http://www.hardcoded.net/dupeguru_pe/help/fr/>`__ և `Գերմաներեն <http://www.hardcoded.net/dupeguru_pe/help/de/>`__.
|
||||
Այս փաստաթուղթը հասանելի է նաև՝ `Ֆրանսերեն <http://dupeguru.voltaicideas.net/help/fr/>`__ և `Գերմաներեն <http://dupeguru.voltaicideas.net/help/de/>`__.
|
||||
|
||||
.. only:: edition_se or edition_me
|
||||
|
||||
|appname| ծրագիր է՝ գտնելու կրկնօրինակ ունեցող ֆայլեր Ձեր համակարգչում: Այն կարող է անգամ ստուգել ֆայլի անունները կան բովանդակությունը: Ֆայլի անվան ստուգման հնարավորությունները ոչ ճշգրիտ համընկման ալգորիթմով, որը կարող է գտնել ֆայլի անվան կրկնօրինակներ, անգամ եթե դրանք նույնը չեն:
|
||||
dupeGuru ծրագիր է՝ գտնելու կրկնօրինակ ունեցող ֆայլեր Ձեր համակարգչում: Այն կարող է անգամ ստուգել ֆայլի անունները կան բովանդակությունը: Ֆայլի անվան ստուգման հնարավորությունները ոչ ճշգրիտ համընկման ալգորիթմով, որը կարող է գտնել ֆայլի անվան կրկնօրինակներ, անգամ եթե դրանք նույնը չեն:
|
||||
|
||||
.. only:: edition_pe
|
||||
|
||||
@@ -23,7 +15,7 @@
|
||||
|
||||
Չնայած dupeGuru-ն կարող է հեշտությամբ օգտագործվել առանց օգնության, այնուհանդերձ եթե կարդաք այս ֆայլը, այն մեծապես կօգնի Ձեզ ընկալելու ծրագրի աշխատանքը: Եթե Դուք նայում եք ձեռնարկը կրկնօրինակների առաջին ստուգման համար, ապա կարող եք ընտրել :doc:`Արագ Սկիզբ <quick_start>` հատվածը:
|
||||
|
||||
Շատ լավ միտք է պահելու |appname| թարմացված: Կարող եք բեռնել վեբ կայքի համապատասխան էջից `homepage`_:
|
||||
Շատ լավ միտք է պահելու dupeGuru թարմացված: Կարող եք բեռնել վեբ կայքի համապատասխան էջից http://dupeguru.voltaicideas.net:
|
||||
|
||||
Պարունակությունը.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Часто задаваемые вопросы
|
||||
==========================
|
||||
|
||||
.. topic:: Что такое |appname|?
|
||||
.. topic:: Что такое dupeGuru?
|
||||
|
||||
.. only:: edition_se
|
||||
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
|appname| help
|
||||
dupeGuru help
|
||||
===============
|
||||
|
||||
.. only:: edition_se
|
||||
|
||||
Этот документ также доступна на `французском <http://www.hardcoded.net/dupeguru/help/fr/>`__, `немецком <http://www.hardcoded.net/dupeguru/help/de/>`__ и `армянский <http://www.hardcoded.net/dupeguru/help/hy/>`__.
|
||||
|
||||
.. only:: edition_me
|
||||
|
||||
Этот документ также доступна на `французском <http://www.hardcoded.net/dupeguru_me/help/fr/>`__, `немецкий <http://www.hardcoded.net/dupeguru_me/help/de/>`__ и `армянский <http://www.hardcoded.net/dupeguru_me/help/hy/>`__.
|
||||
|
||||
.. only:: edition_pe
|
||||
|
||||
Этот документ также доступна на `французском <http://www.hardcoded.net/dupeguru_pe/help/fr/>`__, `немецкий <http://www.hardcoded.net/dupeguru_pe/help/de/>`__ и `армянский <http://www.hardcoded.net/dupeguru_pe/help/hy/>`__.
|
||||
Этот документ также доступна на `французском <http://dupeguru.voltaicideas.net/help/fr/>`__, `немецком <http://dupeguru.voltaicideas.net/help/de/>`__ и `армянский <http://dupeguru.voltaicideas.net/help/hy/>`__.
|
||||
|
||||
.. only:: edition_se or edition_me
|
||||
|
||||
|appname| есть инструмент для поиска дубликатов файлов на вашем компьютере. Он может сканировать либо имен файлов или содержимого.Имя файла функций сканирования нечеткого соответствия алгоритма, который позволяет найти одинаковые имена файлов, даже если они не совсем то же самое.
|
||||
dupeGuru есть инструмент для поиска дубликатов файлов на вашем компьютере. Он может сканировать либо имен файлов или содержимого.Имя файла функций сканирования нечеткого соответствия алгоритма, который позволяет найти одинаковые имена файлов, даже если они не совсем то же самое.
|
||||
|
||||
.. only:: edition_pe
|
||||
|
||||
@@ -23,7 +13,7 @@
|
||||
|
||||
Хотя dupeGuru может быть легко использована без документации, чтение этого файла поможет вам освоить его. Если вы ищете руководство для вашей первой дублировать сканирования, вы можете взглянуть на раздел :doc:`Быстрый <quick_start>` Начало.
|
||||
|
||||
Это хорошая идея, чтобы сохранить |appname| обновлен. Вы можете скачать последнюю версию на своей `homepage`_.
|
||||
Это хорошая идея, чтобы сохранить dupeGuru обновлен. Вы можете скачать последнюю версию на своей http://dupeguru.voltaicideas.net.
|
||||
Содержание:
|
||||
|
||||
.. toctree::
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Часті питання
|
||||
==========================
|
||||
|
||||
.. topic:: Що таке |appname|?
|
||||
.. topic:: Що таке dupeGuru?
|
||||
|
||||
.. only:: edition_se
|
||||
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
|appname| help
|
||||
dupeGuru help
|
||||
===============
|
||||
|
||||
.. only:: edition_se
|
||||
|
||||
Цей документ також доступна на `французькому <http://www.hardcoded.net/dupeguru/help/fr/>`__, `німецький <http://www.hardcoded.net/dupeguru/help/de/>`__ і `Вірменський <http://www.hardcoded.net/dupeguru/help/hy/>`__.
|
||||
|
||||
.. only:: edition_me
|
||||
|
||||
Цей документ також доступна на `французькому <http://www.hardcoded.net/dupeguru_me/help/fr/>`__, `німецький <http://www.hardcoded.net/dupeguru_me/help/de/>`__ і `Вірменський <http://www.hardcoded.net/dupeguru_me/help/hy/>`__.
|
||||
|
||||
.. only:: edition_pe
|
||||
|
||||
Цей документ також доступна на `французькому <http://www.hardcoded.net/dupeguru_pe/help/fr/>`__, `німецький <http://www.hardcoded.net/dupeguru_pe/help/de/>`__ і `Вірменський <http://www.hardcoded.net/dupeguru_pe/help/hy/>`__.
|
||||
Цей документ також доступна на `французькому <http://dupeguru.voltaicideas.net/help/fr/>`__, `німецький <http://dupeguru.voltaicideas.net/help/de/>`__ і `Вірменський <http://dupeguru.voltaicideas.net/help/hy/>`__.
|
||||
|
||||
.. only:: edition_se or edition_me
|
||||
|
||||
|appname| це інструмент для пошуку дублікатів файлів на вашому комп'ютері. Він може сканувати або імен файлів або вмісту. Файл функцій сканування нечіткого відповідності алгоритму, який дозволяє знайти однакові імена файлів, навіть якщо вони не зовсім те ж саме.
|
||||
dupeGuru це інструмент для пошуку дублікатів файлів на вашому комп'ютері. Він може сканувати або імен файлів або вмісту. Файл функцій сканування нечіткого відповідності алгоритму, який дозволяє знайти однакові імена файлів, навіть якщо вони не зовсім те ж саме.
|
||||
|
||||
.. only:: edition_pe
|
||||
|
||||
@@ -23,7 +15,7 @@
|
||||
|
||||
Хоча dupeGuru може бути легко використана без документації, читання цього файлу допоможе вам освоїти його. Якщо ви шукаєте керівництво для вашої першої дублювати сканування, ви можете поглянути на: :doc:`Quick Start <quick_start>`
|
||||
|
||||
Це гарна ідея, щоб зберегти |appname| оновлено. Ви можете завантажити останню версію на своєму `homepage`_.
|
||||
Це гарна ідея, щоб зберегти dupeGuru оновлено. Ви можете завантажити останню версію на своєму http://dupeguru.voltaicideas.net.
|
||||
|
||||
Contents:
|
||||
|
||||
|
||||
@@ -336,7 +336,6 @@ def read_changelog_file(filename):
|
||||
with open(filename, "rt", encoding="utf-8") as fp:
|
||||
contents = fp.read()
|
||||
splitted = re_changelog_header.split(contents)[1:] # the first item is empty
|
||||
# splitted = [version1, date1, desc1, version2, date2, ...]
|
||||
result = []
|
||||
for version, date_str, description in iter_by_three(iter(splitted)):
|
||||
date = datetime.strptime(date_str, "%Y-%m-%d").date()
|
||||
@@ -399,8 +398,8 @@ def create_osx_app_structure(
|
||||
# `resources`: A list of paths of files or folders going in the "Resources" folder.
|
||||
# `frameworks`: Same as above for "Frameworks".
|
||||
# `symlink_resources`: If True, will symlink resources into the structure instead of copying them.
|
||||
app = OSXAppStructure(dest, infoplist)
|
||||
app.create()
|
||||
app = OSXAppStructure(dest)
|
||||
app.create(infoplist)
|
||||
app.copy_executable(executable)
|
||||
app.copy_resources(*resources, use_symlinks=symlink_resources)
|
||||
app.copy_frameworks(*frameworks)
|
||||
|
||||
@@ -13,8 +13,8 @@ import traceback
|
||||
# Taken from http://bzimmer.ziclix.com/2008/12/17/python-thread-dumps/
|
||||
def stacktraces():
|
||||
code = []
|
||||
for threadId, stack in sys._current_frames().items():
|
||||
code.append("\n# ThreadID: %s" % threadId)
|
||||
for thread_id, stack in sys._current_frames().items():
|
||||
code.append("\n# ThreadID: %s" % thread_id)
|
||||
for filename, lineno, name, line in traceback.extract_stack(stack):
|
||||
code.append('File: "%s", line %d, in %s' % (filename, lineno, name))
|
||||
if line:
|
||||
|
||||
@@ -11,8 +11,8 @@ import logging
|
||||
|
||||
|
||||
class SpecialFolder:
|
||||
AppData = 1
|
||||
Cache = 2
|
||||
APPDATA = 1
|
||||
CACHE = 2
|
||||
|
||||
|
||||
def open_url(url):
|
||||
@@ -55,7 +55,7 @@ try:
|
||||
_reveal_path = proxy.revealPath_
|
||||
|
||||
def _special_folder_path(special_folder, appname=None, portable=False):
|
||||
if special_folder == SpecialFolder.Cache:
|
||||
if special_folder == SpecialFolder.CACHE:
|
||||
base = proxy.getCachePath()
|
||||
else:
|
||||
base = proxy.getAppdataPath()
|
||||
@@ -63,14 +63,14 @@ try:
|
||||
appname = proxy.bundleInfo_("CFBundleName")
|
||||
return op.join(base, appname)
|
||||
|
||||
|
||||
except ImportError:
|
||||
try:
|
||||
from PyQt5.QtCore import QUrl, QStandardPaths
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
from qtlib.util import getAppData
|
||||
from qtlib.util import get_appdata
|
||||
from core.util import executable_folder
|
||||
from hscommon.plat import ISWINDOWS
|
||||
from hscommon.plat import ISWINDOWS, ISOSX
|
||||
import subprocess
|
||||
|
||||
def _open_url(url):
|
||||
QDesktopServices.openUrl(QUrl(url))
|
||||
@@ -80,16 +80,21 @@ except ImportError:
|
||||
QDesktopServices.openUrl(url)
|
||||
|
||||
def _reveal_path(path):
|
||||
if ISWINDOWS:
|
||||
subprocess.run(["explorer", "/select,", op.abspath(path)])
|
||||
elif ISOSX:
|
||||
subprocess.run(["open", "-R", op.abspath(path)])
|
||||
else:
|
||||
_open_path(op.dirname(str(path)))
|
||||
|
||||
def _special_folder_path(special_folder, appname=None, portable=False):
|
||||
if special_folder == SpecialFolder.Cache:
|
||||
if special_folder == SpecialFolder.CACHE:
|
||||
if ISWINDOWS and portable:
|
||||
folder = op.join(executable_folder(), "cache")
|
||||
else:
|
||||
folder = QStandardPaths.standardLocations(QStandardPaths.CacheLocation)[0]
|
||||
else:
|
||||
folder = getAppData(portable)
|
||||
folder = get_appdata(portable)
|
||||
return folder
|
||||
|
||||
except ImportError:
|
||||
@@ -98,9 +103,11 @@ except ImportError:
|
||||
logging.warning("Can't setup desktop functions!")
|
||||
|
||||
def _open_path(path):
|
||||
# Dummy for tests
|
||||
pass
|
||||
|
||||
def _reveal_path(path):
|
||||
# Dummy for tests
|
||||
pass
|
||||
|
||||
def _special_folder_path(special_folder, appname=None, portable=False):
|
||||
|
||||
@@ -139,31 +139,34 @@ class Job:
|
||||
self._progress = progress
|
||||
if self._progress > self._currmax:
|
||||
self._progress = self._currmax
|
||||
if self._progress < 0:
|
||||
self._progress = 0
|
||||
self._do_update(desc)
|
||||
|
||||
|
||||
class NullJob:
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Null job does nothing
|
||||
pass
|
||||
|
||||
def add_progress(self, *args, **kwargs):
|
||||
# Null job does nothing
|
||||
pass
|
||||
|
||||
def check_if_cancelled(self):
|
||||
# Null job does nothing
|
||||
pass
|
||||
|
||||
def iter_with_progress(self, sequence, *args, **kwargs):
|
||||
return iter(sequence)
|
||||
|
||||
def start_job(self, *args, **kwargs):
|
||||
# Null job does nothing
|
||||
pass
|
||||
|
||||
def start_subjob(self, *args, **kwargs):
|
||||
return NullJob()
|
||||
|
||||
def set_progress(self, *args, **kwargs):
|
||||
# Null job does nothing
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2009-09-14
|
||||
# Copyright 2011 Hardcoded Software (http://www.hardcoded.net)
|
||||
#
|
||||
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, Qt, QTimer
|
||||
from PyQt5.QtWidgets import QProgressDialog
|
||||
|
||||
from . import performer
|
||||
|
||||
|
||||
class Progress(QProgressDialog, performer.ThreadedJobPerformer):
|
||||
finished = pyqtSignal(["QString"])
|
||||
|
||||
def __init__(self, parent):
|
||||
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint
|
||||
QProgressDialog.__init__(self, "", "Cancel", 0, 100, parent, flags)
|
||||
self.setModal(True)
|
||||
self.setAutoReset(False)
|
||||
self.setAutoClose(False)
|
||||
self._timer = QTimer()
|
||||
self._jobid = ""
|
||||
self._timer.timeout.connect(self.updateProgress)
|
||||
|
||||
def updateProgress(self):
|
||||
# the values might change before setValue happens
|
||||
last_progress = self.last_progress
|
||||
last_desc = self.last_desc
|
||||
if not self._job_running or last_progress is None:
|
||||
self._timer.stop()
|
||||
self.close()
|
||||
if not self.job_cancelled:
|
||||
self.finished.emit(self._jobid)
|
||||
return
|
||||
if self.wasCanceled():
|
||||
self.job_cancelled = True
|
||||
return
|
||||
if last_desc:
|
||||
self.setLabelText(last_desc)
|
||||
self.setValue(last_progress)
|
||||
|
||||
def run(self, jobid, title, target, args=()):
|
||||
self._jobid = jobid
|
||||
self.reset()
|
||||
self.setLabelText("")
|
||||
self.run_threaded(target, args)
|
||||
self.setWindowTitle(title)
|
||||
self.show()
|
||||
self._timer.start(500)
|
||||
@@ -21,6 +21,8 @@ PO2COCOA = {
|
||||
|
||||
COCOA2PO = {v: k for k, v in PO2COCOA.items()}
|
||||
|
||||
STRING_EXT = ".strings"
|
||||
|
||||
|
||||
def get_langs(folder):
|
||||
return [name for name in os.listdir(folder) if op.isdir(op.join(folder, name))]
|
||||
@@ -152,7 +154,7 @@ def strings2pot(target, dest):
|
||||
|
||||
|
||||
def allstrings2pot(lprojpath, dest, excludes=None):
|
||||
allstrings = files_with_ext(lprojpath, ".strings")
|
||||
allstrings = files_with_ext(lprojpath, STRING_EXT)
|
||||
if excludes:
|
||||
allstrings = [p for p in allstrings if op.splitext(op.basename(p))[0] not in excludes]
|
||||
for strings_path in allstrings:
|
||||
@@ -210,7 +212,7 @@ def generate_cocoa_strings_from_code(code_folder, dest_folder):
|
||||
def generate_cocoa_strings_from_xib(xib_folder):
|
||||
xibs = [op.join(xib_folder, fn) for fn in os.listdir(xib_folder) if fn.endswith(".xib")]
|
||||
for xib in xibs:
|
||||
dest = xib.replace(".xib", ".strings")
|
||||
dest = xib.replace(".xib", STRING_EXT)
|
||||
print_and_do("ibtool {} --generate-strings-file {}".format(xib, dest))
|
||||
print_and_do("iconv -f utf-16 -t utf-8 {0} | tee {0}".format(dest))
|
||||
|
||||
@@ -226,6 +228,6 @@ def localize_stringsfile(stringsfile, dest_root_folder):
|
||||
|
||||
|
||||
def localize_all_stringsfiles(src_folder, dest_root_folder):
|
||||
stringsfiles = [op.join(src_folder, fn) for fn in os.listdir(src_folder) if fn.endswith(".strings")]
|
||||
stringsfiles = [op.join(src_folder, fn) for fn in os.listdir(src_folder) if fn.endswith(STRING_EXT)]
|
||||
for path in stringsfiles:
|
||||
localize_stringsfile(path, dest_root_folder)
|
||||
|
||||
@@ -167,10 +167,10 @@ def getFilesForName(name):
|
||||
# check for glob chars
|
||||
if containsAny(name, "*?[]"):
|
||||
files = glob.glob(name)
|
||||
list = []
|
||||
file_list = []
|
||||
for file in files:
|
||||
list.extend(getFilesForName(file))
|
||||
return list
|
||||
file_list.extend(getFilesForName(file))
|
||||
return file_list
|
||||
|
||||
# try to find module or package
|
||||
name = _get_modpkg_path(name)
|
||||
@@ -179,9 +179,9 @@ def getFilesForName(name):
|
||||
|
||||
if os.path.isdir(name):
|
||||
# find all python files in directory
|
||||
list = []
|
||||
os.walk(name, _visit_pyfiles, list)
|
||||
return list
|
||||
file_list = []
|
||||
os.walk(name, _visit_pyfiles, file_list)
|
||||
return file_list
|
||||
elif os.path.exists(name):
|
||||
# a single file
|
||||
return [name]
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import os.path as op
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
from .build import read_changelog_file, filereplace
|
||||
@@ -48,9 +48,9 @@ def gen(
|
||||
if confrepl is None:
|
||||
confrepl = {}
|
||||
if confpath is None:
|
||||
confpath = op.join(basepath, "conf.tmpl")
|
||||
confpath = Path(basepath, "conf.tmpl")
|
||||
if changelogtmpl is None:
|
||||
changelogtmpl = op.join(basepath, "changelog.tmpl")
|
||||
changelogtmpl = Path(basepath, "changelog.tmpl")
|
||||
changelog = read_changelog_file(changelogpath)
|
||||
tix = tixgen(tixurl)
|
||||
rendered_logs = []
|
||||
@@ -62,13 +62,13 @@ def gen(
|
||||
rendered = CHANGELOG_FORMAT.format(version=log["version"], date=log["date_str"], description=description)
|
||||
rendered_logs.append(rendered)
|
||||
confrepl["version"] = changelog[0]["version"]
|
||||
changelog_out = op.join(basepath, "changelog.rst")
|
||||
changelog_out = Path(basepath, "changelog.rst")
|
||||
filereplace(changelogtmpl, changelog_out, changelog="\n".join(rendered_logs))
|
||||
if op.exists(confpath):
|
||||
conf_out = op.join(basepath, "conf.py")
|
||||
if Path(confpath).exists():
|
||||
conf_out = Path(basepath, "conf.py")
|
||||
filereplace(confpath, conf_out, **confrepl)
|
||||
# Call the sphinx_build function, which is the same as doing sphinx-build from cli
|
||||
try:
|
||||
sphinx_build([basepath, destpath])
|
||||
sphinx_build([str(basepath), str(destpath)])
|
||||
except SystemExit:
|
||||
print("Sphinx called sys.exit(), but we're cancelling it because we don't actually want to exit")
|
||||
|
||||
@@ -45,7 +45,7 @@ class _ActualThread(threading.Thread):
|
||||
self._lock = threading.Lock()
|
||||
self._run = True
|
||||
self.lastrowid = -1
|
||||
self.setDaemon(True)
|
||||
self.daemon = True
|
||||
self.start()
|
||||
|
||||
def _query(self, query):
|
||||
|
||||
@@ -19,7 +19,7 @@ from ..path import Path
|
||||
from ..testutil import eq_
|
||||
|
||||
|
||||
class TestCase_GetConflictedName:
|
||||
class TestCaseGetConflictedName:
|
||||
def test_simple(self):
|
||||
name = get_conflicted_name(["bar"], "bar")
|
||||
eq_("[000] bar", name)
|
||||
@@ -46,7 +46,7 @@ class TestCase_GetConflictedName:
|
||||
eq_("[000] bar", name)
|
||||
|
||||
|
||||
class TestCase_GetUnconflictedName:
|
||||
class TestCaseGetUnconflictedName:
|
||||
def test_main(self):
|
||||
eq_("foobar", get_unconflicted_name("[000] foobar"))
|
||||
eq_("foobar", get_unconflicted_name("[9999] foobar"))
|
||||
@@ -56,7 +56,7 @@ class TestCase_GetUnconflictedName:
|
||||
eq_("foo [000] bar", get_unconflicted_name("foo [000] bar"))
|
||||
|
||||
|
||||
class TestCase_IsConflicted:
|
||||
class TestCaseIsConflicted:
|
||||
def test_main(self):
|
||||
assert is_conflicted("[000] foobar")
|
||||
assert is_conflicted("[9999] foobar")
|
||||
@@ -66,7 +66,7 @@ class TestCase_IsConflicted:
|
||||
assert not is_conflicted("foo [000] bar")
|
||||
|
||||
|
||||
class TestCase_move_copy:
|
||||
class TestCaseMoveCopy:
|
||||
@pytest.fixture
|
||||
def do_setup(self, request):
|
||||
tmpdir = request.getfixturevalue("tmpdir")
|
||||
|
||||
@@ -51,7 +51,7 @@ def test_init_with_tuple_and_list(force_ossep):
|
||||
|
||||
def test_init_with_invalid_value(force_ossep):
|
||||
try:
|
||||
path = Path(42) # noqa: F841
|
||||
Path(42)
|
||||
assert False
|
||||
except TypeError:
|
||||
pass
|
||||
@@ -142,8 +142,6 @@ def test_path_slice(force_ossep):
|
||||
eq_((), foobar[:foobar])
|
||||
abcd = Path("a/b/c/d")
|
||||
a = Path("a")
|
||||
b = Path("b") # noqa: #F841
|
||||
c = Path("c") # noqa: #F841
|
||||
d = Path("d")
|
||||
z = Path("z")
|
||||
eq_("b/c", abcd[a:d])
|
||||
@@ -216,7 +214,7 @@ def test_str_repr_of_mix_between_non_ascii_str_and_unicode(force_ossep):
|
||||
eq_("foo\u00e9/bar".encode(sys.getfilesystemencoding()), p.tobytes())
|
||||
|
||||
|
||||
def test_Path_of_a_Path_returns_self(force_ossep):
|
||||
def test_path_of_a_path_returns_self(force_ossep):
|
||||
# if Path() is called with a path as value, just return value.
|
||||
p = Path("foo/bar")
|
||||
assert Path(p) is p
|
||||
|
||||
@@ -91,7 +91,7 @@ def test_make_sure_theres_no_messup_between_queries():
|
||||
threads = []
|
||||
for i in range(1, 101):
|
||||
t = threading.Thread(target=run, args=(i,))
|
||||
t.start
|
||||
t.start()
|
||||
threads.append(t)
|
||||
while threads:
|
||||
time.sleep(0.1)
|
||||
|
||||
@@ -19,6 +19,7 @@ class TestRow(Row):
|
||||
self._index = index
|
||||
|
||||
def load(self):
|
||||
# Does nothing for test
|
||||
pass
|
||||
|
||||
def save(self):
|
||||
@@ -75,14 +76,17 @@ def test_allow_edit_when_attr_is_property_with_fset():
|
||||
class TestRow(Row):
|
||||
@property
|
||||
def foo(self):
|
||||
# property only for existence checks
|
||||
pass
|
||||
|
||||
@property
|
||||
def bar(self):
|
||||
# property only for existence checks
|
||||
pass
|
||||
|
||||
@bar.setter
|
||||
def bar(self, value):
|
||||
# setter only for existence checks
|
||||
pass
|
||||
|
||||
row = TestRow(Table())
|
||||
@@ -97,10 +101,12 @@ def test_can_edit_prop_has_priority_over_fset_checks():
|
||||
class TestRow(Row):
|
||||
@property
|
||||
def bar(self):
|
||||
# property only for existence checks
|
||||
pass
|
||||
|
||||
@bar.setter
|
||||
def bar(self, value):
|
||||
# setter only for existence checks
|
||||
pass
|
||||
|
||||
can_edit_bar = False
|
||||
|
||||
@@ -236,49 +236,8 @@ def test_multi_replace():
|
||||
|
||||
# --- Files
|
||||
|
||||
# These test cases needed https://github.com/hsoft/pytest-monkeyplus/ which appears to not be compatible with latest
|
||||
# pytest, looking at where this is used only appears to be in hscommon.localize_all_stringfiles at top level.
|
||||
# Right now this repo does not seem to utilize any of that functionality so going to leave these tests out for now.
|
||||
# TODO decide if fixing these tests is worth it or not.
|
||||
|
||||
# class TestCase_modified_after:
|
||||
# def test_first_is_modified_after(self, monkeyplus):
|
||||
# monkeyplus.patch_osstat("first", st_mtime=42)
|
||||
# monkeyplus.patch_osstat("second", st_mtime=41)
|
||||
# assert modified_after("first", "second")
|
||||
|
||||
# def test_second_is_modified_after(self, monkeyplus):
|
||||
# monkeyplus.patch_osstat("first", st_mtime=42)
|
||||
# monkeyplus.patch_osstat("second", st_mtime=43)
|
||||
# assert not modified_after("first", "second")
|
||||
|
||||
# def test_same_mtime(self, monkeyplus):
|
||||
# monkeyplus.patch_osstat("first", st_mtime=42)
|
||||
# monkeyplus.patch_osstat("second", st_mtime=42)
|
||||
# assert not modified_after("first", "second")
|
||||
|
||||
# def test_first_file_does_not_exist(self, monkeyplus):
|
||||
# # when the first file doesn't exist, we return False
|
||||
# monkeyplus.patch_osstat("second", st_mtime=42)
|
||||
# assert not modified_after("does_not_exist", "second") # no crash
|
||||
|
||||
# def test_second_file_does_not_exist(self, monkeyplus):
|
||||
# # when the second file doesn't exist, we return True
|
||||
# monkeyplus.patch_osstat("first", st_mtime=42)
|
||||
# assert modified_after("first", "does_not_exist") # no crash
|
||||
|
||||
# def test_first_file_is_none(self, monkeyplus):
|
||||
# # when the first file is None, we return False
|
||||
# monkeyplus.patch_osstat("second", st_mtime=42)
|
||||
# assert not modified_after(None, "second") # no crash
|
||||
|
||||
# def test_second_file_is_none(self, monkeyplus):
|
||||
# # when the second file is None, we return True
|
||||
# monkeyplus.patch_osstat("first", st_mtime=42)
|
||||
# assert modified_after("first", None) # no crash
|
||||
|
||||
|
||||
class TestCase_delete_if_empty:
|
||||
class TestCaseDeleteIfEmpty:
|
||||
def test_is_empty(self, tmpdir):
|
||||
testpath = Path(str(tmpdir))
|
||||
assert delete_if_empty(testpath)
|
||||
@@ -330,9 +289,11 @@ class TestCase_delete_if_empty:
|
||||
delete_if_empty(Path(str(tmpdir))) # no crash
|
||||
|
||||
|
||||
class TestCase_open_if_filename:
|
||||
class TestCaseOpenIfFilename:
|
||||
FILE_NAME = "test.txt"
|
||||
|
||||
def test_file_name(self, tmpdir):
|
||||
filepath = str(tmpdir.join("test.txt"))
|
||||
filepath = str(tmpdir.join(self.FILE_NAME))
|
||||
open(filepath, "wb").write(b"test_data")
|
||||
file, close = open_if_filename(filepath)
|
||||
assert close
|
||||
@@ -348,16 +309,18 @@ class TestCase_open_if_filename:
|
||||
eq_("test_data", file.read())
|
||||
|
||||
def test_mode_is_passed_to_open(self, tmpdir):
|
||||
filepath = str(tmpdir.join("test.txt"))
|
||||
filepath = str(tmpdir.join(self.FILE_NAME))
|
||||
open(filepath, "w").close()
|
||||
file, close = open_if_filename(filepath, "a")
|
||||
eq_("a", file.mode)
|
||||
file.close()
|
||||
|
||||
|
||||
class TestCase_FileOrPath:
|
||||
class TestCaseFileOrPath:
|
||||
FILE_NAME = "test.txt"
|
||||
|
||||
def test_path(self, tmpdir):
|
||||
filepath = str(tmpdir.join("test.txt"))
|
||||
filepath = str(tmpdir.join(self.FILE_NAME))
|
||||
open(filepath, "wb").write(b"test_data")
|
||||
with FileOrPath(filepath) as fp:
|
||||
eq_(b"test_data", fp.read())
|
||||
@@ -370,7 +333,7 @@ class TestCase_FileOrPath:
|
||||
eq_("test_data", fp.read())
|
||||
|
||||
def test_mode_is_passed_to_open(self, tmpdir):
|
||||
filepath = str(tmpdir.join("test.txt"))
|
||||
filepath = str(tmpdir.join(self.FILE_NAME))
|
||||
open(filepath, "w").close()
|
||||
with FileOrPath(filepath, "a") as fp:
|
||||
eq_("a", fp.mode)
|
||||
|
||||
@@ -230,8 +230,8 @@ def log_calls(func):
|
||||
"""
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
unifiedArgs = _unify_args(func, args, kwargs)
|
||||
wrapper.calls.append(unifiedArgs)
|
||||
unified_args = _unify_args(func, args, kwargs)
|
||||
wrapper.calls.append(unified_args)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
wrapper.calls = []
|
||||
|
||||
@@ -58,6 +58,7 @@ def get_locale_name(lang):
|
||||
"it": "it_IT",
|
||||
"ja": "ja_JP",
|
||||
"ko": "ko_KR",
|
||||
"ms": "ms_MY",
|
||||
"nl": "nl_NL",
|
||||
"pl_PL": "pl_PL",
|
||||
"pt_BR": "pt_BR",
|
||||
@@ -131,11 +132,11 @@ def install_gettext_trans(base_folder, lang):
|
||||
def install_gettext_trans_under_cocoa():
|
||||
from cocoa import proxy
|
||||
|
||||
resFolder = proxy.getResourcePath()
|
||||
baseFolder = op.join(resFolder, "locale")
|
||||
currentLang = proxy.systemLang()
|
||||
install_gettext_trans(baseFolder, currentLang)
|
||||
localename = get_locale_name(currentLang)
|
||||
res_folder = proxy.getResourcePath()
|
||||
base_folder = op.join(res_folder, "locale")
|
||||
current_lang = proxy.systemLang()
|
||||
install_gettext_trans(base_folder, current_lang)
|
||||
localename = get_locale_name(current_lang)
|
||||
if localename is not None:
|
||||
locale.setlocale(locale.LC_ALL, localename)
|
||||
|
||||
@@ -149,7 +150,9 @@ def install_gettext_trans_under_qt(base_folder, lang=None):
|
||||
if not lang:
|
||||
lang = str(QLocale.system().name())[:2]
|
||||
localename = get_locale_name(lang)
|
||||
if localename is not None:
|
||||
if localename is None:
|
||||
lang = "en"
|
||||
localename = get_locale_name(lang)
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, localename)
|
||||
except locale.Error:
|
||||
|
||||
@@ -177,13 +177,13 @@ def pluralize(number, word, decimals=0, plural_word=None):
|
||||
``plural_word``: If the plural rule for word is more complex than adding a 's', specify a plural
|
||||
"""
|
||||
number = round(number, decimals)
|
||||
format = "%%1.%df %%s" % decimals
|
||||
plural_format = "%%1.%df %%s" % decimals
|
||||
if number > 1:
|
||||
if plural_word is None:
|
||||
word += "s"
|
||||
else:
|
||||
word = plural_word
|
||||
return format % (number, word)
|
||||
return plural_format % (number, word)
|
||||
|
||||
|
||||
def format_time(seconds, with_hours=True):
|
||||
@@ -226,7 +226,7 @@ def format_time_decimal(seconds):
|
||||
|
||||
|
||||
SIZE_DESC = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
|
||||
SIZE_VALS = tuple(1024 ** i for i in range(1, 9))
|
||||
SIZE_VALS = tuple(1024**i for i in range(1, 9))
|
||||
|
||||
|
||||
def format_size(size, decimal=0, forcepower=-1, showdesc=True):
|
||||
@@ -252,16 +252,16 @@ def format_size(size, decimal=0, forcepower=-1, showdesc=True):
|
||||
div = SIZE_VALS[i - 1]
|
||||
else:
|
||||
div = 1
|
||||
format = "%%%d.%df" % (decimal, decimal)
|
||||
size_format = "%%%d.%df" % (decimal, decimal)
|
||||
negative = size < 0
|
||||
divided_size = (0.0 + abs(size)) / div
|
||||
if decimal == 0:
|
||||
divided_size = ceil(divided_size)
|
||||
else:
|
||||
divided_size = ceil(divided_size * (10 ** decimal)) / (10 ** decimal)
|
||||
divided_size = ceil(divided_size * (10**decimal)) / (10**decimal)
|
||||
if negative:
|
||||
divided_size *= -1
|
||||
result = format % divided_size
|
||||
result = size_format % divided_size
|
||||
if showdesc:
|
||||
result += " " + SIZE_DESC[i]
|
||||
return result
|
||||
@@ -292,7 +292,7 @@ def multi_replace(s, replace_from, replace_to=""):
|
||||
the same length as ``replace_from``, it will be transformed into a list.
|
||||
"""
|
||||
if isinstance(replace_to, str) and (len(replace_from) != len(replace_to)):
|
||||
replace_to = [replace_to for r in replace_from]
|
||||
replace_to = [replace_to for _ in replace_from]
|
||||
if len(replace_from) != len(replace_to):
|
||||
raise ValueError("len(replace_from) must be equal to len(replace_to)")
|
||||
replace = list(zip(replace_from, replace_to))
|
||||
|
||||
@@ -36,7 +36,7 @@ msgstr ""
|
||||
msgid "Sending to Trash"
|
||||
msgstr ""
|
||||
|
||||
#: core\app.py:290
|
||||
#: core\app.py:289
|
||||
msgid "A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again."
|
||||
msgstr ""
|
||||
|
||||
@@ -48,76 +48,84 @@ msgstr ""
|
||||
msgid "All marked files were copied successfully."
|
||||
msgstr ""
|
||||
|
||||
#: core\app.py:316
|
||||
#: core\app.py:317
|
||||
msgid "All marked files were moved successfully."
|
||||
msgstr ""
|
||||
|
||||
#: core\app.py:317
|
||||
#: core\app.py:319
|
||||
msgid "All marked files were deleted successfully."
|
||||
msgstr ""
|
||||
|
||||
#: core\app.py:321
|
||||
msgid "All marked files were successfully sent to Trash."
|
||||
msgstr ""
|
||||
|
||||
#: core\app.py:323
|
||||
#: core\app.py:326
|
||||
msgid "Could not load file: {}"
|
||||
msgstr ""
|
||||
|
||||
#: core\app.py:379
|
||||
#: core\app.py:382
|
||||
msgid "'{}' already is in the list."
|
||||
msgstr ""
|
||||
|
||||
#: core\app.py:381
|
||||
#: core\app.py:384
|
||||
msgid "'{}' does not exist."
|
||||
msgstr ""
|
||||
|
||||
#: core\app.py:389
|
||||
#: core\app.py:392
|
||||
msgid "All selected %d matches are going to be ignored in all subsequent scans. Continue?"
|
||||
msgstr ""
|
||||
|
||||
#: core\app.py:463
|
||||
#: core\app.py:469
|
||||
msgid "Select a directory to copy marked files to"
|
||||
msgstr ""
|
||||
|
||||
#: core\app.py:465
|
||||
#: core\app.py:471
|
||||
msgid "Select a directory to move marked files to"
|
||||
msgstr ""
|
||||
|
||||
#: core\app.py:504
|
||||
#: core\app.py:510
|
||||
msgid "Select a destination for your exported CSV"
|
||||
msgstr ""
|
||||
|
||||
#: core\app.py:510 core\app.py:764 core\app.py:774
|
||||
#: core\app.py:516 core\app.py:771 core\app.py:781
|
||||
msgid "Couldn't write to file: {}"
|
||||
msgstr ""
|
||||
|
||||
#: core\app.py:533
|
||||
#: core\app.py:539
|
||||
msgid "You have no custom command set up. Set it up in your preferences."
|
||||
msgstr ""
|
||||
|
||||
#: core\app.py:691 core\app.py:703
|
||||
#: core\app.py:695 core\app.py:707
|
||||
msgid "You are about to remove %d files from results. Continue?"
|
||||
msgstr ""
|
||||
|
||||
#: core\app.py:739
|
||||
#: core\app.py:743
|
||||
msgid "{} duplicate groups were changed by the re-prioritization."
|
||||
msgstr ""
|
||||
|
||||
#: core\app.py:783
|
||||
#: core\app.py:790
|
||||
msgid "The selected directories contain no scannable file."
|
||||
msgstr ""
|
||||
|
||||
#: core\app.py:796
|
||||
#: core\app.py:803
|
||||
msgid "Collecting files to scan"
|
||||
msgstr ""
|
||||
|
||||
#: core\app.py:843
|
||||
#: core\app.py:850
|
||||
msgid "%s (%d discarded)"
|
||||
msgstr ""
|
||||
|
||||
#: core\engine.py:251 core\engine.py:294
|
||||
msgid "0 matches found"
|
||||
#: core\directories.py:191
|
||||
msgid "Collected {} files to scan"
|
||||
msgstr ""
|
||||
|
||||
#: core\engine.py:269 core\engine.py:306
|
||||
msgid "%d matches found"
|
||||
#: core\directories.py:207
|
||||
msgid "Collected {} folders to scan"
|
||||
msgstr ""
|
||||
|
||||
#: core\engine.py:27
|
||||
msgid "%d matches found from %d groups"
|
||||
msgstr ""
|
||||
|
||||
#: core\gui\deletion_options.py:71
|
||||
@@ -220,15 +228,15 @@ msgstr ""
|
||||
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 ""
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ msgstr "Kopíruji"
|
||||
msgid "Sending to Trash"
|
||||
msgstr "Vyhazuji do koše"
|
||||
|
||||
#: core\app.py:308
|
||||
#: 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."
|
||||
@@ -55,35 +55,39 @@ msgstr ""
|
||||
"Předchozí akce stále nebyla ukončena. Novou zatím nemůžete spustit. Počkejte"
|
||||
" pár sekund a zkuste to znovu."
|
||||
|
||||
#: core\app.py:318
|
||||
#: core\app.py:300
|
||||
msgid "No duplicates found."
|
||||
msgstr "Nebyli nalezeny žádné duplicity."
|
||||
|
||||
#: core\app.py:333
|
||||
#: core\app.py:315
|
||||
msgid "All marked files were copied successfully."
|
||||
msgstr "Všechny označené soubory byly úspěšně zkopírovány."
|
||||
|
||||
#: core\app.py:334
|
||||
#: core\app.py:317
|
||||
msgid "All marked files were moved successfully."
|
||||
msgstr "Všechny označené soubory byly úspěšně přesunuty."
|
||||
|
||||
#: core\app.py:335
|
||||
#: core\app.py:319
|
||||
msgid "All marked files were deleted successfully."
|
||||
msgstr ""
|
||||
|
||||
#: core\app.py:321
|
||||
msgid "All marked files were successfully sent to Trash."
|
||||
msgstr "Všechny označené soubory byly úspěšně odeslány do koše."
|
||||
|
||||
#: core\app.py:343
|
||||
#: core\app.py:326
|
||||
msgid "Could not load file: {}"
|
||||
msgstr "Soubor se nepodařilo načíst: {}"
|
||||
|
||||
#: core\app.py:399
|
||||
#: core\app.py:382
|
||||
msgid "'{}' already is in the list."
|
||||
msgstr "'{}' již je v seznamu."
|
||||
|
||||
#: core\app.py:401
|
||||
#: core\app.py:384
|
||||
msgid "'{}' does not exist."
|
||||
msgstr "'{}' neexistuje."
|
||||
|
||||
#: core\app.py:410
|
||||
#: core\app.py:392
|
||||
msgid ""
|
||||
"All selected %d matches are going to be ignored in all subsequent scans. "
|
||||
"Continue?"
|
||||
@@ -91,60 +95,64 @@ msgstr ""
|
||||
"Všech %d vybraných shod bude v následujících hledáních ignorováno. "
|
||||
"Pokračovat?"
|
||||
|
||||
#: core\app.py:486
|
||||
#: core\app.py:469
|
||||
msgid "Select a directory to copy marked files to"
|
||||
msgstr "Vyberte adresář, do kterého chcete zkopírovat označené soubory"
|
||||
|
||||
#: core\app.py:487
|
||||
#: core\app.py:471
|
||||
msgid "Select a directory to move marked files to"
|
||||
msgstr "Vyberte adresář, kam chcete přesunout označené soubory"
|
||||
|
||||
#: core\app.py:527
|
||||
#: core\app.py:510
|
||||
msgid "Select a destination for your exported CSV"
|
||||
msgstr "Vyberte cíl pro exportovaný soubor CSV"
|
||||
|
||||
#: core\app.py:534 core\app.py:801 core\app.py:811
|
||||
#: core\app.py:516 core\app.py:771 core\app.py:781
|
||||
msgid "Couldn't write to file: {}"
|
||||
msgstr "Nelze zapisovat do souboru: {}"
|
||||
|
||||
#: core\app.py:559
|
||||
#: core\app.py:539
|
||||
msgid "You have no custom command set up. Set it up in your preferences."
|
||||
msgstr ""
|
||||
"Nedefinoval jste žádný uživatelský příkaz. Nadefinujete ho v předvolbách."
|
||||
|
||||
#: core\app.py:727 core\app.py:740
|
||||
#: core\app.py:695 core\app.py:707
|
||||
msgid "You are about to remove %d files from results. Continue?"
|
||||
msgstr "Chystáte se z výsledků odstranit %d souborů. Pokračovat?"
|
||||
|
||||
#: core\app.py:774
|
||||
#: core\app.py:743
|
||||
msgid "{} duplicate groups were changed by the re-prioritization."
|
||||
msgstr "{} duplicitní skupiny byly změněny změně priorit."
|
||||
|
||||
#: core\app.py:821
|
||||
#: core\app.py:790
|
||||
msgid "The selected directories contain no scannable file."
|
||||
msgstr "Vybrané adresáře neobsahují žádné soubory vhodné k prohledávání."
|
||||
|
||||
#: core\app.py:835
|
||||
#: core\app.py:803
|
||||
msgid "Collecting files to scan"
|
||||
msgstr "Shromažďuji prohlížené soubory"
|
||||
|
||||
#: core\app.py:891
|
||||
#: core\app.py:850
|
||||
msgid "%s (%d discarded)"
|
||||
msgstr "%s (%d vyřazeno)"
|
||||
|
||||
#: core\engine.py:244 core\engine.py:288
|
||||
msgid "0 matches found"
|
||||
msgstr "Nalezeno 0 shod"
|
||||
#: core\directories.py:191
|
||||
msgid "Collected {} files to scan"
|
||||
msgstr ""
|
||||
|
||||
#: core\engine.py:262 core\engine.py:296
|
||||
msgid "%d matches found"
|
||||
msgstr "Nalezeno %d shod"
|
||||
#: core\directories.py:207
|
||||
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 "Posíláte-{} soubory do koše."
|
||||
|
||||
#: core\gui\exclude_list_table.py:15
|
||||
#: core\gui\exclude_list_table.py:14
|
||||
msgid "Regular Expressions"
|
||||
msgstr "Regulární výrazy"
|
||||
|
||||
@@ -176,15 +184,15 @@ msgstr "Obsah"
|
||||
msgid "Analyzed %d/%d pictures"
|
||||
msgstr "Analyzováno %d/%d snímků"
|
||||
|
||||
#: core\pe\matchblock.py:181
|
||||
#: core\pe\matchblock.py:177
|
||||
msgid "Performed %d/%d chunk matches"
|
||||
msgstr "Provedeno %d/%d porovnání bloků"
|
||||
|
||||
#: core\pe\matchblock.py:191
|
||||
#: core\pe\matchblock.py:185
|
||||
msgid "Preparing for matching"
|
||||
msgstr "Připravuji porovnávání"
|
||||
|
||||
#: core\pe\matchblock.py:244
|
||||
#: core\pe\matchblock.py:234
|
||||
msgid "Verified %d/%d matches"
|
||||
msgstr "Ověřeno %d/%d shod"
|
||||
|
||||
@@ -232,23 +240,23 @@ msgstr "Nejnovější"
|
||||
msgid "Oldest"
|
||||
msgstr "Nejstarší"
|
||||
|
||||
#: core\results.py:142
|
||||
#: core\results.py:134
|
||||
msgid "%d / %d (%s / %s) duplicates marked."
|
||||
msgstr "%d / %d (%s / %s) duplicit označeno."
|
||||
|
||||
#: core\results.py:149
|
||||
#: core\results.py:141
|
||||
msgid " filter: %s"
|
||||
msgstr " filtr: %s"
|
||||
|
||||
#: core\scanner.py:85
|
||||
#: core\scanner.py:90
|
||||
msgid "Read size of %d/%d files"
|
||||
msgstr "Read size of %d/%d files"
|
||||
|
||||
#: core\scanner.py:109
|
||||
#: core\scanner.py:116
|
||||
msgid "Read metadata of %d/%d files"
|
||||
msgstr "Načtena metadata %d/%d souborů"
|
||||
|
||||
#: core\scanner.py:147
|
||||
#: core\scanner.py:154
|
||||
msgid "Almost done! Fiddling with results..."
|
||||
msgstr "Skoro hotovo! Fidlování s výsledky..."
|
||||
|
||||
|
||||
@@ -937,3 +937,43 @@ msgstr "Všeobecné"
|
||||
#: qt\preferences_dialog.py:286
|
||||
msgid "Display"
|
||||
msgstr "Zobrazit"
|
||||
|
||||
#: qt\se\preferences_dialog.py:70
|
||||
msgid "Partially hash files bigger than"
|
||||
msgstr ""
|
||||
|
||||
#: qt\se\preferences_dialog.py:80
|
||||
msgid "MB"
|
||||
msgstr ""
|
||||
|
||||
#: qt\preferences_dialog.py:163
|
||||
msgid "Use native OS dialogs"
|
||||
msgstr ""
|
||||
|
||||
#: qt\preferences_dialog.py:166
|
||||
msgid ""
|
||||
"For actions such as file/folder selection use the OS native dialogs.\n"
|
||||
"Some native dialogs have limited functionality."
|
||||
msgstr ""
|
||||
|
||||
#: qt\se\preferences_dialog.py:68
|
||||
msgid "Ignore files larger than"
|
||||
msgstr ""
|
||||
|
||||
#: qt\app.py:135 qt\app.py:293
|
||||
msgid "Clear Cache"
|
||||
msgstr ""
|
||||
|
||||
#: qt\app.py:294
|
||||
msgid ""
|
||||
"Do you really want to clear the cache? This will remove all cached file "
|
||||
"hashes and picture analysis."
|
||||
msgstr ""
|
||||
|
||||
#: qt\app.py:299
|
||||
msgid "Cache cleared."
|
||||
msgstr ""
|
||||
|
||||
#: qt\preferences_dialog.py:173
|
||||
msgid "Use dark style"
|
||||
msgstr ""
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# Translators:
|
||||
# Andrew Senetar <arsenetar@gmail.com>, 2021
|
||||
# Fuan <jcfrt@posteo.net>, 2021
|
||||
# Robert M, 2021
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\n"
|
||||
"Last-Translator: Robert M, 2021\n"
|
||||
"Language-Team: German (https://www.transifex.com/voltaicideas/teams/116153/de/)\n"
|
||||
"Language: de\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -47,7 +48,7 @@ msgstr "Kopiere"
|
||||
msgid "Sending to Trash"
|
||||
msgstr "Verschiebe in den Papierkorb"
|
||||
|
||||
#: core\app.py:308
|
||||
#: 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."
|
||||
@@ -55,36 +56,40 @@ msgstr ""
|
||||
"Eine vorherige Aktion ist noch in der Bearbeitung. Sie können noch keine "
|
||||
"Neue starten. Warten Sie einige Sekunden und versuchen es erneut."
|
||||
|
||||
#: core\app.py:318
|
||||
#: core\app.py:300
|
||||
msgid "No duplicates found."
|
||||
msgstr "Keine Duplikate gefunden."
|
||||
|
||||
#: core\app.py:333
|
||||
#: core\app.py:315
|
||||
msgid "All marked files were copied successfully."
|
||||
msgstr "Alle markierten Dateien wurden erfolgreich kopiert."
|
||||
|
||||
#: core\app.py:334
|
||||
#: core\app.py:317
|
||||
msgid "All marked files were moved successfully."
|
||||
msgstr "Alle markierten Dateien wurden erfolgreich verschoben."
|
||||
|
||||
#: core\app.py:335
|
||||
#: core\app.py:319
|
||||
msgid "All marked files were deleted successfully."
|
||||
msgstr "Alle markierten Dateien wurden erfolgreich gelöscht."
|
||||
|
||||
#: core\app.py:321
|
||||
msgid "All marked files were successfully sent to Trash."
|
||||
msgstr ""
|
||||
"Alle markierten Dateien wurden erfolgreich in den Papierkorb verschoben."
|
||||
|
||||
#: core\app.py:343
|
||||
#: core\app.py:326
|
||||
msgid "Could not load file: {}"
|
||||
msgstr "Konnte Datei {} nicht laden."
|
||||
|
||||
#: core\app.py:399
|
||||
#: core\app.py:382
|
||||
msgid "'{}' already is in the list."
|
||||
msgstr "'{}' ist bereits in der Liste."
|
||||
|
||||
#: core\app.py:401
|
||||
#: core\app.py:384
|
||||
msgid "'{}' does not exist."
|
||||
msgstr "'{}' existiert nicht."
|
||||
|
||||
#: core\app.py:410
|
||||
#: core\app.py:392
|
||||
msgid ""
|
||||
"All selected %d matches are going to be ignored in all subsequent scans. "
|
||||
"Continue?"
|
||||
@@ -92,65 +97,69 @@ msgstr ""
|
||||
"Alle %d ausgewählten Dateien werden in zukünftigen Scans ignoriert. "
|
||||
"Fortfahren?"
|
||||
|
||||
#: core\app.py:486
|
||||
#: core\app.py:469
|
||||
msgid "Select a directory to copy marked files to"
|
||||
msgstr ""
|
||||
"Wählen Sie ein Verzeichnis aus, in das markierte Dateien kopiert werden "
|
||||
"sollen"
|
||||
|
||||
#: core\app.py:487
|
||||
#: core\app.py:471
|
||||
msgid "Select a directory to move marked files to"
|
||||
msgstr ""
|
||||
"Wählen Sie ein Verzeichnis aus, in das markierte Dateien verschoben werden "
|
||||
"sollen"
|
||||
|
||||
#: core\app.py:527
|
||||
#: core\app.py:510
|
||||
msgid "Select a destination for your exported CSV"
|
||||
msgstr "Zielverzeichnis für den CSV Export angeben"
|
||||
|
||||
#: core\app.py:534 core\app.py:801 core\app.py:811
|
||||
#: core\app.py:516 core\app.py:771 core\app.py:781
|
||||
msgid "Couldn't write to file: {}"
|
||||
msgstr "Konnte Datei {} nicht schreiben."
|
||||
|
||||
#: core\app.py:559
|
||||
#: core\app.py:539
|
||||
msgid "You have no custom command set up. Set it up in your preferences."
|
||||
msgstr ""
|
||||
"Sie haben noch keinen Befehl erstellt. Bitte dies in den Einstellungen vornehmen.\n"
|
||||
"Bsp.: \"C:\\Program Files\\Diff\\Diff.exe\" \"%d\" \"%r\""
|
||||
|
||||
#: core\app.py:727 core\app.py:740
|
||||
#: core\app.py:695 core\app.py:707
|
||||
msgid "You are about to remove %d files from results. Continue?"
|
||||
msgstr "%d Dateien werden aus der Ergebnisliste entfernt. Fortfahren?"
|
||||
|
||||
#: core\app.py:774
|
||||
#: core\app.py:743
|
||||
msgid "{} duplicate groups were changed by the re-prioritization."
|
||||
msgstr "{} Duplikat-Gruppen wurden durch die Neu-Priorisierung geändert."
|
||||
|
||||
#: core\app.py:821
|
||||
#: core\app.py:790
|
||||
msgid "The selected directories contain no scannable file."
|
||||
msgstr "Ausgewählte Ordner enthalten keine scannbaren Dateien."
|
||||
|
||||
#: core\app.py:835
|
||||
#: core\app.py:803
|
||||
msgid "Collecting files to scan"
|
||||
msgstr "Sammle zu scannende Dateien..."
|
||||
|
||||
#: core\app.py:891
|
||||
#: core\app.py:850
|
||||
msgid "%s (%d discarded)"
|
||||
msgstr "%s (%d verworfen)"
|
||||
|
||||
#: core\engine.py:244 core\engine.py:288
|
||||
msgid "0 matches found"
|
||||
msgstr "0 Übereinstimmungen gefunden"
|
||||
#: core\directories.py:191
|
||||
msgid "Collected {} files to scan"
|
||||
msgstr "{} Dateien für Scan gesammelt"
|
||||
|
||||
#: core\engine.py:262 core\engine.py:296
|
||||
msgid "%d matches found"
|
||||
msgstr "%d Übereinstimmungen gefunden"
|
||||
#: core\directories.py:207
|
||||
msgid "Collected {} folders to scan"
|
||||
msgstr "{} Ordner für Scan gesammelt"
|
||||
|
||||
#: core\gui\deletion_options.py:73
|
||||
#: core\engine.py:27
|
||||
msgid "%d matches found from %d groups"
|
||||
msgstr "%d Treffer in %d Gruppen gefunden"
|
||||
|
||||
#: core\gui\deletion_options.py:71
|
||||
msgid "You are sending {} file(s) to the Trash."
|
||||
msgstr "Verschiebe {} Datei(en) in den Papierkorb."
|
||||
|
||||
#: core\gui\exclude_list_table.py:15
|
||||
#: core\gui\exclude_list_table.py:14
|
||||
msgid "Regular Expressions"
|
||||
msgstr "Reguläre Ausdrücke"
|
||||
|
||||
@@ -182,15 +191,15 @@ msgstr "Inhalt"
|
||||
msgid "Analyzed %d/%d pictures"
|
||||
msgstr "Analysiere Bild %d/%d"
|
||||
|
||||
#: core\pe\matchblock.py:181
|
||||
#: core\pe\matchblock.py:177
|
||||
msgid "Performed %d/%d chunk matches"
|
||||
msgstr "%d/%d Chunk-Matches ausgeführt"
|
||||
|
||||
#: core\pe\matchblock.py:191
|
||||
#: core\pe\matchblock.py:185
|
||||
msgid "Preparing for matching"
|
||||
msgstr "Bereite Matching vor"
|
||||
|
||||
#: core\pe\matchblock.py:244
|
||||
#: core\pe\matchblock.py:234
|
||||
msgid "Verified %d/%d matches"
|
||||
msgstr "%d/%d verifizierte Übereinstimmungen"
|
||||
|
||||
@@ -238,23 +247,23 @@ msgstr "Neuste"
|
||||
msgid "Oldest"
|
||||
msgstr "Älterste"
|
||||
|
||||
#: core\results.py:142
|
||||
#: core\results.py:134
|
||||
msgid "%d / %d (%s / %s) duplicates marked."
|
||||
msgstr "%d / %d (%s / %s) Duplikate markiert."
|
||||
|
||||
#: core\results.py:149
|
||||
#: core\results.py:141
|
||||
msgid " filter: %s"
|
||||
msgstr " Filter: %s"
|
||||
|
||||
#: core\scanner.py:85
|
||||
#: core\scanner.py:90
|
||||
msgid "Read size of %d/%d files"
|
||||
msgstr "Lese Größe von %d/%d Dateien"
|
||||
|
||||
#: core\scanner.py:109
|
||||
#: core\scanner.py:116
|
||||
msgid "Read metadata of %d/%d files"
|
||||
msgstr "Lese Metadaten von %d/%d Dateien"
|
||||
|
||||
#: core\scanner.py:147
|
||||
#: core\scanner.py:154
|
||||
msgid "Almost done! Fiddling with results..."
|
||||
msgstr "Fast fertig! Arrangiere Ergebnisse..."
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# Translators:
|
||||
# Andrew Senetar <arsenetar@gmail.com>, 2021
|
||||
# Fuan <jcfrt@posteo.net>, 2021
|
||||
# Robert M, 2021
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\n"
|
||||
"Last-Translator: Robert M, 2021\n"
|
||||
"Language-Team: German (https://www.transifex.com/voltaicideas/teams/116153/de/)\n"
|
||||
"Language: de\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -946,3 +947,45 @@ msgstr "Allgemeines"
|
||||
#: qt\preferences_dialog.py:286
|
||||
msgid "Display"
|
||||
msgstr "Anzeige"
|
||||
|
||||
#: qt\se\preferences_dialog.py:70
|
||||
msgid "Partially hash files bigger than"
|
||||
msgstr "Dateien partiell hashen die größer sind als"
|
||||
|
||||
#: qt\se\preferences_dialog.py:80
|
||||
msgid "MB"
|
||||
msgstr "MB"
|
||||
|
||||
#: qt\preferences_dialog.py:163
|
||||
msgid "Use native OS dialogs"
|
||||
msgstr "Benutzer System-eigene Dialoge"
|
||||
|
||||
#: qt\preferences_dialog.py:166
|
||||
msgid ""
|
||||
"For actions such as file/folder selection use the OS native dialogs.\n"
|
||||
"Some native dialogs have limited functionality."
|
||||
msgstr ""
|
||||
"Benutzer System-eigene Dialoge für Aktionen wie Datei/Ordern-Auswahl\n"
|
||||
"Manche System-eigene Dialoge sind in ihren Funktionen limitiert."
|
||||
|
||||
#: qt\se\preferences_dialog.py:68
|
||||
msgid "Ignore files larger than"
|
||||
msgstr "Ignoriere Dateien größer als"
|
||||
|
||||
#: qt\app.py:135 qt\app.py:293
|
||||
msgid "Clear Cache"
|
||||
msgstr ""
|
||||
|
||||
#: qt\app.py:294
|
||||
msgid ""
|
||||
"Do you really want to clear the cache? This will remove all cached file "
|
||||
"hashes and picture analysis."
|
||||
msgstr ""
|
||||
|
||||
#: qt\app.py:299
|
||||
msgid "Cache cleared."
|
||||
msgstr ""
|
||||
|
||||
#: qt\preferences_dialog.py:173
|
||||
msgid "Use dark style"
|
||||
msgstr ""
|
||||
|
||||
@@ -48,7 +48,7 @@ msgstr "Αντιγραφή"
|
||||
msgid "Sending to Trash"
|
||||
msgstr "Αποστολή στα σκουπίδια"
|
||||
|
||||
#: core\app.py:308
|
||||
#: 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."
|
||||
@@ -56,94 +56,102 @@ msgstr ""
|
||||
"Μια προηγούμενη ενέργεια είναι σε εξέλιξη. Δεν μπορείτε να ξεκινήσετε "
|
||||
"καινούργια ακόμα. Περιμένετε λίγα δευτερόλεπτα, έπειτα προσπαθήστε ξανά."
|
||||
|
||||
#: core\app.py:318
|
||||
#: core\app.py:300
|
||||
msgid "No duplicates found."
|
||||
msgstr "Δεν βρέθηκαν διπλότυπα."
|
||||
|
||||
#: core\app.py:333
|
||||
#: core\app.py:315
|
||||
msgid "All marked files were copied successfully."
|
||||
msgstr "Όλα τα επιλεγμένα αρχεία αντιγράφηκαν επιτυχώς."
|
||||
|
||||
#: core\app.py:334
|
||||
#: core\app.py:317
|
||||
msgid "All marked files were moved successfully."
|
||||
msgstr "Όλα τα επιλεγμένα αρχεία μετακινήθηκαν επιτυχώς."
|
||||
|
||||
#: core\app.py:335
|
||||
#: core\app.py:319
|
||||
msgid "All marked files were deleted successfully."
|
||||
msgstr ""
|
||||
|
||||
#: core\app.py:321
|
||||
msgid "All marked files were successfully sent to Trash."
|
||||
msgstr "Όλα τα επιλεγμένα αρχεία στάλθηκαν με επιτυχία στον κάδο."
|
||||
|
||||
#: core\app.py:343
|
||||
#: core\app.py:326
|
||||
msgid "Could not load file: {}"
|
||||
msgstr "Δεν ήταν δυνατή η φόρτωση του αρχείου: {}"
|
||||
|
||||
#: core\app.py:399
|
||||
#: core\app.py:382
|
||||
msgid "'{}' already is in the list."
|
||||
msgstr "'{}' υπάρχει ήδη στη λίστα."
|
||||
|
||||
#: core\app.py:401
|
||||
#: core\app.py:384
|
||||
msgid "'{}' does not exist."
|
||||
msgstr "'{}' δεν υπάρχει."
|
||||
|
||||
#: core\app.py:410
|
||||
#: core\app.py:392
|
||||
msgid ""
|
||||
"All selected %d matches are going to be ignored in all subsequent scans. "
|
||||
"Continue?"
|
||||
msgstr ""
|
||||
"Όλα τα επιλεγμένα %d στοιχεία θα αγνοηθούν σε μελλοντικές σαρώσεις.Συνέχεια;"
|
||||
|
||||
#: core\app.py:486
|
||||
#: core\app.py:469
|
||||
msgid "Select a directory to copy marked files to"
|
||||
msgstr "Επιλέξτε έναν κατάλογο για να αντιγράψετε επισημασμένα αρχεία."
|
||||
|
||||
#: core\app.py:487
|
||||
#: core\app.py:471
|
||||
msgid "Select a directory to move marked files to"
|
||||
msgstr "Επιλέξτε έναν κατάλογο για να μετακινήσετε τα επισημασμένα αρχεία."
|
||||
|
||||
#: core\app.py:527
|
||||
#: core\app.py:510
|
||||
msgid "Select a destination for your exported CSV"
|
||||
msgstr "Επιλέξτε έναν προορισμό για το εξαγόμενο CSV σας"
|
||||
|
||||
#: core\app.py:534 core\app.py:801 core\app.py:811
|
||||
#: core\app.py:516 core\app.py:771 core\app.py:781
|
||||
msgid "Couldn't write to file: {}"
|
||||
msgstr "Δεν ήταν δυνατή η εγγραφή στο αρχείο: {}"
|
||||
|
||||
#: core\app.py:559
|
||||
#: core\app.py:539
|
||||
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:695 core\app.py:707
|
||||
msgid "You are about to remove %d files from results. Continue?"
|
||||
msgstr "Πρόκειται να αφαιρέσετε %d αρχεία από τα αποτελέσματα. Συνέχεια;"
|
||||
|
||||
#: core\app.py:774
|
||||
#: core\app.py:743
|
||||
msgid "{} duplicate groups were changed by the re-prioritization."
|
||||
msgstr "{} ομάδες διπλοτύπων άλλαξαν από το επαναπροσδιορισμό."
|
||||
|
||||
#: core\app.py:821
|
||||
#: core\app.py:790
|
||||
msgid "The selected directories contain no scannable file."
|
||||
msgstr "Οι επιλεγμένοι φάκελοι δεν περιέχουν σαρώσιμα αρχεία."
|
||||
|
||||
#: core\app.py:835
|
||||
#: core\app.py:803
|
||||
msgid "Collecting files to scan"
|
||||
msgstr "Συλλογή αρχείων για σάρωση"
|
||||
|
||||
#: core\app.py:891
|
||||
#: core\app.py:850
|
||||
msgid "%s (%d discarded)"
|
||||
msgstr "%s (%d απορρίφθηκαν)"
|
||||
|
||||
#: core\engine.py:244 core\engine.py:288
|
||||
msgid "0 matches found"
|
||||
msgstr "0 διπλότυπα βρέθηκαν"
|
||||
#: core\directories.py:191
|
||||
msgid "Collected {} files to scan"
|
||||
msgstr ""
|
||||
|
||||
#: core\engine.py:262 core\engine.py:296
|
||||
msgid "%d matches found"
|
||||
msgstr "Βρέθηκαν %d διπλότυπα"
|
||||
#: core\directories.py:207
|
||||
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 "Κανονικές εκφράσεις"
|
||||
|
||||
@@ -175,15 +183,15 @@ msgstr "Περιεχόμενα"
|
||||
msgid "Analyzed %d/%d pictures"
|
||||
msgstr "Ανάλυση %d/%d εικόνων"
|
||||
|
||||
#: core\pe\matchblock.py:181
|
||||
#: core\pe\matchblock.py:177
|
||||
msgid "Performed %d/%d chunk matches"
|
||||
msgstr "Εκτέλεση %d/%d μερικής ταυτοποίησης"
|
||||
|
||||
#: 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 "Πιστοποίηση %d/%d ταυτόσημων"
|
||||
|
||||
@@ -231,23 +239,23 @@ msgstr "Νεώτερο"
|
||||
msgid "Oldest"
|
||||
msgstr "Παλαιότερο"
|
||||
|
||||
#: core\results.py:142
|
||||
#: core\results.py:134
|
||||
msgid "%d / %d (%s / %s) duplicates marked."
|
||||
msgstr "%d / %d (%s / %s) επιλεγμένα διπλότυπα."
|
||||
|
||||
#: core\results.py:149
|
||||
#: core\results.py:141
|
||||
msgid " filter: %s"
|
||||
msgstr " φίλτρο: %s"
|
||||
|
||||
#: core\scanner.py:85
|
||||
#: core\scanner.py:90
|
||||
msgid "Read size of %d/%d files"
|
||||
msgstr "Ανάγνωση μεγέθους %d/%d αρχείων"
|
||||
|
||||
#: core\scanner.py:109
|
||||
#: core\scanner.py:116
|
||||
msgid "Read metadata of %d/%d files"
|
||||
msgstr "Ανάγνωση μεταδεδομένων των %d/%d αρχείων"
|
||||
|
||||
#: core\scanner.py:147
|
||||
#: core\scanner.py:154
|
||||
msgid "Almost done! Fiddling with results..."
|
||||
msgstr "Σχεδόν τελείωσα! Παιχνίδι με αποτελέσματα ..."
|
||||
|
||||
|
||||
@@ -954,3 +954,43 @@ 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 ""
|
||||
|
||||
@@ -47,7 +47,7 @@ msgstr "Copiando"
|
||||
msgid "Sending to Trash"
|
||||
msgstr "Enviando a la Papelera"
|
||||
|
||||
#: core\app.py:308
|
||||
#: 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."
|
||||
@@ -55,36 +55,40 @@ msgstr ""
|
||||
"Una acción previa sigue ejecutándose. No puede abrir una nueva todavía. "
|
||||
"Espere unos segundos y vuelva a intentarlo."
|
||||
|
||||
#: core\app.py:318
|
||||
#: core\app.py:300
|
||||
msgid "No duplicates found."
|
||||
msgstr "No se han encontrado duplicados."
|
||||
|
||||
#: core\app.py:333
|
||||
#: core\app.py:315
|
||||
msgid "All marked files were copied successfully."
|
||||
msgstr ""
|
||||
"Todos los ficheros seleccionados han sido copiados satisfactoriamente."
|
||||
|
||||
#: core\app.py:334
|
||||
#: core\app.py:317
|
||||
msgid "All marked files were moved successfully."
|
||||
msgstr "Todos los ficheros seleccionados se han movidos satisfactoriamente."
|
||||
|
||||
#: core\app.py:335
|
||||
#: core\app.py:319
|
||||
msgid "All marked files were deleted successfully."
|
||||
msgstr ""
|
||||
|
||||
#: core\app.py:321
|
||||
msgid "All marked files were successfully sent to Trash."
|
||||
msgstr "Todo los ficheros marcados se han enviado a la papelera exitosamente."
|
||||
|
||||
#: core\app.py:343
|
||||
#: core\app.py:326
|
||||
msgid "Could not load file: {}"
|
||||
msgstr "No se pudo cargar el archivo: {}"
|
||||
|
||||
#: core\app.py:399
|
||||
#: core\app.py:382
|
||||
msgid "'{}' already is in the list."
|
||||
msgstr "'{}' ya está en la lista."
|
||||
|
||||
#: core\app.py:401
|
||||
#: core\app.py:384
|
||||
msgid "'{}' does not exist."
|
||||
msgstr "'{}' no existe."
|
||||
|
||||
#: core\app.py:410
|
||||
#: core\app.py:392
|
||||
msgid ""
|
||||
"All selected %d matches are going to be ignored in all subsequent scans. "
|
||||
"Continue?"
|
||||
@@ -92,59 +96,63 @@ msgstr ""
|
||||
"Todas las %d coincidencias seleccionadas van a ser ignoradas en las "
|
||||
"subsiguientes exploraciones. ¿Continuar?"
|
||||
|
||||
#: core\app.py:486
|
||||
#: core\app.py:469
|
||||
msgid "Select a directory to copy marked files to"
|
||||
msgstr "Seleccione un directorio donde desee copiar los archivos marcados"
|
||||
|
||||
#: core\app.py:487
|
||||
#: core\app.py:471
|
||||
msgid "Select a directory to move marked files to"
|
||||
msgstr "Seleccione un directorio al que desee mover los archivos marcados"
|
||||
|
||||
#: core\app.py:527
|
||||
#: core\app.py:510
|
||||
msgid "Select a destination for your exported CSV"
|
||||
msgstr "Seleccionar un destino para el CSV seleccionado"
|
||||
|
||||
#: core\app.py:534 core\app.py:801 core\app.py:811
|
||||
#: core\app.py:516 core\app.py:771 core\app.py:781
|
||||
msgid "Couldn't write to file: {}"
|
||||
msgstr "No se pudo escribir en el archivo: {}"
|
||||
|
||||
#: core\app.py:559
|
||||
#: core\app.py:539
|
||||
msgid "You have no custom command set up. Set it up in your preferences."
|
||||
msgstr "No hay comandos configurados. Establézcalos en sus preferencias."
|
||||
|
||||
#: core\app.py:727 core\app.py:740
|
||||
#: core\app.py:695 core\app.py:707
|
||||
msgid "You are about to remove %d files from results. Continue?"
|
||||
msgstr "Está a punto de eliminar %d ficheros de resultados. ¿Continuar?"
|
||||
|
||||
#: core\app.py:774
|
||||
#: core\app.py:743
|
||||
msgid "{} duplicate groups were changed by the re-prioritization."
|
||||
msgstr "{} grupos de duplicados han sido cambiados por la re-priorización"
|
||||
|
||||
#: core\app.py:821
|
||||
#: core\app.py:790
|
||||
msgid "The selected directories contain no scannable file."
|
||||
msgstr "Las carpetas seleccionadas no contienen ficheros para explorar."
|
||||
|
||||
#: core\app.py:835
|
||||
#: core\app.py:803
|
||||
msgid "Collecting files to scan"
|
||||
msgstr "Recopilando ficheros a explorar"
|
||||
|
||||
#: core\app.py:891
|
||||
#: core\app.py:850
|
||||
msgid "%s (%d discarded)"
|
||||
msgstr "%s (%d descartados)"
|
||||
|
||||
#: core\engine.py:244 core\engine.py:288
|
||||
msgid "0 matches found"
|
||||
msgstr "0 coincidencias"
|
||||
#: core\directories.py:191
|
||||
msgid "Collected {} files to scan"
|
||||
msgstr ""
|
||||
|
||||
#: core\engine.py:262 core\engine.py:296
|
||||
msgid "%d matches found"
|
||||
msgstr "%d coincidencias encontradas"
|
||||
#: core\directories.py:207
|
||||
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 "Enviando {} fichero(s) a la Papelera"
|
||||
|
||||
#: core\gui\exclude_list_table.py:15
|
||||
#: core\gui\exclude_list_table.py:14
|
||||
msgid "Regular Expressions"
|
||||
msgstr "Expresiones regulares"
|
||||
|
||||
@@ -177,15 +185,15 @@ msgstr "Contenido"
|
||||
msgid "Analyzed %d/%d pictures"
|
||||
msgstr "Analizadas %d/%d imágenes"
|
||||
|
||||
#: core\pe\matchblock.py:181
|
||||
#: core\pe\matchblock.py:177
|
||||
msgid "Performed %d/%d chunk matches"
|
||||
msgstr "Realizado %d/%d trozos coincidentes"
|
||||
|
||||
#: core\pe\matchblock.py:191
|
||||
#: core\pe\matchblock.py:185
|
||||
msgid "Preparing for matching"
|
||||
msgstr "Preparando para coincidencias"
|
||||
|
||||
#: core\pe\matchblock.py:244
|
||||
#: core\pe\matchblock.py:234
|
||||
msgid "Verified %d/%d matches"
|
||||
msgstr "Verificadas %d/%d coincidencias"
|
||||
|
||||
@@ -233,23 +241,23 @@ msgstr "El más nuevo"
|
||||
msgid "Oldest"
|
||||
msgstr "El más antiguo"
|
||||
|
||||
#: core\results.py:142
|
||||
#: core\results.py:134
|
||||
msgid "%d / %d (%s / %s) duplicates marked."
|
||||
msgstr "%d / %d (%s / %s) duplicados marcados."
|
||||
|
||||
#: core\results.py:149
|
||||
#: core\results.py:141
|
||||
msgid " filter: %s"
|
||||
msgstr "filtro: %s"
|
||||
|
||||
#: core\scanner.py:85
|
||||
#: core\scanner.py:90
|
||||
msgid "Read size of %d/%d files"
|
||||
msgstr "Tamaño de lectura de %d/%d ficheros"
|
||||
|
||||
#: core\scanner.py:109
|
||||
#: core\scanner.py:116
|
||||
msgid "Read metadata of %d/%d files"
|
||||
msgstr "Leyendo metadatos de %d/%d ficheros"
|
||||
|
||||
#: core\scanner.py:147
|
||||
#: core\scanner.py:154
|
||||
msgid "Almost done! Fiddling with results..."
|
||||
msgstr "¡Casi termino! Jugando con los resultados..."
|
||||
|
||||
|
||||
@@ -947,3 +947,43 @@ msgstr "General"
|
||||
#: qt\preferences_dialog.py:286
|
||||
msgid "Display"
|
||||
msgstr "Visualización"
|
||||
|
||||
#: qt\se\preferences_dialog.py:70
|
||||
msgid "Partially hash files bigger than"
|
||||
msgstr ""
|
||||
|
||||
#: qt\se\preferences_dialog.py:80
|
||||
msgid "MB"
|
||||
msgstr ""
|
||||
|
||||
#: qt\preferences_dialog.py:163
|
||||
msgid "Use native OS dialogs"
|
||||
msgstr ""
|
||||
|
||||
#: qt\preferences_dialog.py:166
|
||||
msgid ""
|
||||
"For actions such as file/folder selection use the OS native dialogs.\n"
|
||||
"Some native dialogs have limited functionality."
|
||||
msgstr ""
|
||||
|
||||
#: qt\se\preferences_dialog.py:68
|
||||
msgid "Ignore files larger than"
|
||||
msgstr ""
|
||||
|
||||
#: qt\app.py:135 qt\app.py:293
|
||||
msgid "Clear Cache"
|
||||
msgstr ""
|
||||
|
||||
#: qt\app.py:294
|
||||
msgid ""
|
||||
"Do you really want to clear the cache? This will remove all cached file "
|
||||
"hashes and picture analysis."
|
||||
msgstr ""
|
||||
|
||||
#: qt\app.py:299
|
||||
msgid "Cache cleared."
|
||||
msgstr ""
|
||||
|
||||
#: qt\preferences_dialog.py:173
|
||||
msgid "Use dark style"
|
||||
msgstr ""
|
||||
|
||||
@@ -47,7 +47,7 @@ msgstr "Copie en cours"
|
||||
msgid "Sending to Trash"
|
||||
msgstr "Envoi de fichiers à la corbeille"
|
||||
|
||||
#: core\app.py:308
|
||||
#: 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."
|
||||
@@ -55,95 +55,103 @@ msgstr ""
|
||||
"Une action précédente est encore en cours. Attendez quelques secondes avant "
|
||||
"d'en repartir une nouvelle."
|
||||
|
||||
#: core\app.py:318
|
||||
#: core\app.py:300
|
||||
msgid "No duplicates found."
|
||||
msgstr "Aucun doublon trouvé."
|
||||
|
||||
#: core\app.py:333
|
||||
#: core\app.py:315
|
||||
msgid "All marked files were copied successfully."
|
||||
msgstr "Tous les fichiers marqués ont été copiés correctement."
|
||||
|
||||
#: core\app.py:334
|
||||
#: core\app.py:317
|
||||
msgid "All marked files were moved successfully."
|
||||
msgstr "Tous les fichiers marqués ont été déplacés correctement."
|
||||
|
||||
#: core\app.py:335
|
||||
#: core\app.py:319
|
||||
msgid "All marked files were deleted successfully."
|
||||
msgstr ""
|
||||
|
||||
#: core\app.py:321
|
||||
msgid "All marked files were successfully sent to Trash."
|
||||
msgstr ""
|
||||
"Tous les fichiers marqués ont été correctement envoyés à la corbeille."
|
||||
|
||||
#: core\app.py:343
|
||||
#: core\app.py:326
|
||||
msgid "Could not load file: {}"
|
||||
msgstr "Impossible d'ouvrir le fichier: {}"
|
||||
|
||||
#: core\app.py:399
|
||||
#: core\app.py:382
|
||||
msgid "'{}' already is in the list."
|
||||
msgstr "'{}' est déjà dans la liste."
|
||||
|
||||
#: core\app.py:401
|
||||
#: core\app.py:384
|
||||
msgid "'{}' does not exist."
|
||||
msgstr "'{}' n'existe pas."
|
||||
|
||||
#: core\app.py:410
|
||||
#: core\app.py:392
|
||||
msgid ""
|
||||
"All selected %d matches are going to be ignored in all subsequent scans. "
|
||||
"Continue?"
|
||||
msgstr "%d fichiers seront ignorés des prochains scans. Continuer?"
|
||||
|
||||
#: core\app.py:486
|
||||
#: core\app.py:469
|
||||
msgid "Select a directory to copy marked files to"
|
||||
msgstr "Sélectionnez un dossier vers lequel copier les fichiers marqués."
|
||||
|
||||
#: core\app.py:487
|
||||
#: core\app.py:471
|
||||
msgid "Select a directory to move marked files to"
|
||||
msgstr "Sélectionnez un dossier vers lequel déplacer les fichiers marqués."
|
||||
|
||||
#: core\app.py:527
|
||||
#: core\app.py:510
|
||||
msgid "Select a destination for your exported CSV"
|
||||
msgstr "Choisissez une destination pour votre exportation CSV"
|
||||
|
||||
#: core\app.py:534 core\app.py:801 core\app.py:811
|
||||
#: core\app.py:516 core\app.py:771 core\app.py:781
|
||||
msgid "Couldn't write to file: {}"
|
||||
msgstr "Impossible d'écrire le fichier: {}"
|
||||
|
||||
#: core\app.py:559
|
||||
#: core\app.py:539
|
||||
msgid "You have no custom command set up. Set it up in your preferences."
|
||||
msgstr ""
|
||||
"Vous n'avez pas de commande personnalisée. Ajoutez-la dans vos préférences."
|
||||
|
||||
#: core\app.py:727 core\app.py:740
|
||||
#: core\app.py:695 core\app.py:707
|
||||
msgid "You are about to remove %d files from results. Continue?"
|
||||
msgstr "%d fichiers seront retirés des résultats. Continuer?"
|
||||
|
||||
#: core\app.py:774
|
||||
#: core\app.py:743
|
||||
msgid "{} duplicate groups were changed by the re-prioritization."
|
||||
msgstr "{} groupes de doublons ont été modifiés par la re-prioritisation."
|
||||
|
||||
#: core\app.py:821
|
||||
#: core\app.py:790
|
||||
msgid "The selected directories contain no scannable file."
|
||||
msgstr "Les dossiers sélectionnés ne contiennent pas de fichiers valides."
|
||||
|
||||
#: core\app.py:835
|
||||
#: core\app.py:803
|
||||
msgid "Collecting files to scan"
|
||||
msgstr "Collecte des fichiers à scanner"
|
||||
|
||||
#: core\app.py:891
|
||||
#: core\app.py:850
|
||||
msgid "%s (%d discarded)"
|
||||
msgstr "%s (%d hors-groupe)"
|
||||
|
||||
#: core\engine.py:244 core\engine.py:288
|
||||
msgid "0 matches found"
|
||||
msgstr "0 paires trouvées"
|
||||
#: core\directories.py:191
|
||||
msgid "Collected {} files to scan"
|
||||
msgstr ""
|
||||
|
||||
#: core\engine.py:262 core\engine.py:296
|
||||
msgid "%d matches found"
|
||||
msgstr "%d paires trouvées"
|
||||
#: core\directories.py:207
|
||||
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 "Vous envoyez {} fichier(s) à la corbeille."
|
||||
|
||||
#: core\gui\exclude_list_table.py:15
|
||||
#: core\gui\exclude_list_table.py:14
|
||||
msgid "Regular Expressions"
|
||||
msgstr "Expressions régulières"
|
||||
|
||||
@@ -177,15 +185,15 @@ msgstr "Contenu"
|
||||
msgid "Analyzed %d/%d pictures"
|
||||
msgstr "Analyzé %d/%d images"
|
||||
|
||||
#: core\pe\matchblock.py:181
|
||||
#: core\pe\matchblock.py:177
|
||||
msgid "Performed %d/%d chunk matches"
|
||||
msgstr "%d/%d blocs d'images comparés"
|
||||
|
||||
#: core\pe\matchblock.py:191
|
||||
#: core\pe\matchblock.py:185
|
||||
msgid "Preparing for matching"
|
||||
msgstr "Préparation pour la comparaison"
|
||||
|
||||
#: core\pe\matchblock.py:244
|
||||
#: core\pe\matchblock.py:234
|
||||
msgid "Verified %d/%d matches"
|
||||
msgstr "Vérifié %d/%d paires"
|
||||
|
||||
@@ -233,23 +241,23 @@ msgstr "Plus récent"
|
||||
msgid "Oldest"
|
||||
msgstr "Moins récent"
|
||||
|
||||
#: core\results.py:142
|
||||
#: core\results.py:134
|
||||
msgid "%d / %d (%s / %s) duplicates marked."
|
||||
msgstr "%d / %d (%s / %s) doublons marqués."
|
||||
|
||||
#: core\results.py:149
|
||||
#: core\results.py:141
|
||||
msgid " filter: %s"
|
||||
msgstr " filtre: %s"
|
||||
|
||||
#: core\scanner.py:85
|
||||
#: core\scanner.py:90
|
||||
msgid "Read size of %d/%d files"
|
||||
msgstr "Lu la taille de %d/%d fichiers"
|
||||
|
||||
#: core\scanner.py:109
|
||||
#: core\scanner.py:116
|
||||
msgid "Read metadata of %d/%d files"
|
||||
msgstr "Lu les métadonnées de %d/%d fichiers"
|
||||
|
||||
#: core\scanner.py:147
|
||||
#: core\scanner.py:154
|
||||
msgid "Almost done! Fiddling with results..."
|
||||
msgstr "Bientôt terminé! Bidouille des résultats..."
|
||||
|
||||
|
||||
@@ -942,3 +942,43 @@ msgstr "Général"
|
||||
#: qt\preferences_dialog.py:286
|
||||
msgid "Display"
|
||||
msgstr "Affichage"
|
||||
|
||||
#: qt\se\preferences_dialog.py:70
|
||||
msgid "Partially hash files bigger than"
|
||||
msgstr ""
|
||||
|
||||
#: qt\se\preferences_dialog.py:80
|
||||
msgid "MB"
|
||||
msgstr ""
|
||||
|
||||
#: qt\preferences_dialog.py:163
|
||||
msgid "Use native OS dialogs"
|
||||
msgstr ""
|
||||
|
||||
#: qt\preferences_dialog.py:166
|
||||
msgid ""
|
||||
"For actions such as file/folder selection use the OS native dialogs.\n"
|
||||
"Some native dialogs have limited functionality."
|
||||
msgstr ""
|
||||
|
||||
#: qt\se\preferences_dialog.py:68
|
||||
msgid "Ignore files larger than"
|
||||
msgstr ""
|
||||
|
||||
#: qt\app.py:135 qt\app.py:293
|
||||
msgid "Clear Cache"
|
||||
msgstr ""
|
||||
|
||||
#: qt\app.py:294
|
||||
msgid ""
|
||||
"Do you really want to clear the cache? This will remove all cached file "
|
||||
"hashes and picture analysis."
|
||||
msgstr ""
|
||||
|
||||
#: qt\app.py:299
|
||||
msgid "Cache cleared."
|
||||
msgstr ""
|
||||
|
||||
#: qt\preferences_dialog.py:173
|
||||
msgid "Use dark style"
|
||||
msgstr ""
|
||||
|
||||
@@ -48,7 +48,7 @@ msgstr "Պատճենվում է"
|
||||
msgid "Sending to Trash"
|
||||
msgstr "Ուղարկվում է Աղբարկղ"
|
||||
|
||||
#: core\app.py:308
|
||||
#: 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."
|
||||
@@ -56,95 +56,103 @@ msgstr ""
|
||||
"Նախորդ գործողությունը դեռևս ձեռադրում է այստեղ: Չեք կարող սկսել մեկ ուրիշը: "
|
||||
"Սպասեք մի քանի վայրկյան և կրկին փորձեք:"
|
||||
|
||||
#: core\app.py:318
|
||||
#: core\app.py:300
|
||||
msgid "No duplicates found."
|
||||
msgstr "Կրկնօրինակներ չկան:"
|
||||
|
||||
#: core\app.py:333
|
||||
#: core\app.py:315
|
||||
msgid "All marked files were copied successfully."
|
||||
msgstr "Բոլոր նշված ֆայլերը հաջողությամբ պատճենվել են:"
|
||||
|
||||
#: core\app.py:334
|
||||
#: core\app.py:317
|
||||
msgid "All marked files were moved successfully."
|
||||
msgstr "Բոլոր նշված ֆայլերը հաջողությամբ տեղափոխվել են:"
|
||||
|
||||
#: core\app.py:335
|
||||
#: core\app.py:319
|
||||
msgid "All marked files were deleted successfully."
|
||||
msgstr ""
|
||||
|
||||
#: core\app.py:321
|
||||
msgid "All marked files were successfully sent to Trash."
|
||||
msgstr "Բոլոր նշված ֆայլերը հաջողությամբ Ջնջվել են:"
|
||||
|
||||
#: core\app.py:343
|
||||
#: core\app.py:326
|
||||
msgid "Could not load file: {}"
|
||||
msgstr "Հնարավոր չէ բեռնել ֆայլը: {}"
|
||||
|
||||
#: core\app.py:399
|
||||
#: core\app.py:382
|
||||
msgid "'{}' already is in the list."
|
||||
msgstr "'{}'-ը արդեն առկա է ցանկում:"
|
||||
|
||||
#: core\app.py:401
|
||||
#: core\app.py:384
|
||||
msgid "'{}' does not exist."
|
||||
msgstr "'{}'-ը գոյություն չունի:"
|
||||
|
||||
#: core\app.py:410
|
||||
#: core\app.py:392
|
||||
msgid ""
|
||||
"All selected %d matches are going to be ignored in all subsequent scans. "
|
||||
"Continue?"
|
||||
msgstr ""
|
||||
"Ընտրված %d համընկնումները կանտեսվեն հետագա բոլոր ստուգումներից: Շարունակե՞լ:"
|
||||
|
||||
#: core\app.py:486
|
||||
#: core\app.py:469
|
||||
msgid "Select a directory to copy marked files to"
|
||||
msgstr "Ընտրեք գրացուցակ, որտեղ ցանկանում եք պատճենել նշված ֆայլերը"
|
||||
|
||||
#: core\app.py:487
|
||||
#: core\app.py:471
|
||||
msgid "Select a directory to move marked files to"
|
||||
msgstr ""
|
||||
"Խնդրում ենք ընտրել գրացուցակ, որտեղ ցանկանում եք տեղափոխել նշված ֆայլերը"
|
||||
|
||||
#: core\app.py:527
|
||||
#: core\app.py:510
|
||||
msgid "Select a destination for your exported CSV"
|
||||
msgstr "Ընտրեք նպատակակետ ձեր արտահանված CSV- ի համար"
|
||||
|
||||
#: core\app.py:534 core\app.py:801 core\app.py:811
|
||||
#: core\app.py:516 core\app.py:771 core\app.py:781
|
||||
msgid "Couldn't write to file: {}"
|
||||
msgstr "Չէր կարող գրել է ֆայլը: {}"
|
||||
|
||||
#: core\app.py:559
|
||||
#: core\app.py:539
|
||||
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:695 core\app.py:707
|
||||
msgid "You are about to remove %d files from results. Continue?"
|
||||
msgstr "Դուք պատրաստվում եք ջնջելու %d ֆայլեր: Շարունակե՞լ:"
|
||||
|
||||
#: core\app.py:774
|
||||
#: core\app.py:743
|
||||
msgid "{} duplicate groups were changed by the re-prioritization."
|
||||
msgstr "{} կրկնօրինակ խմբերը փոխվել են առաջնահերթության կարգով:"
|
||||
|
||||
#: core\app.py:821
|
||||
#: core\app.py:790
|
||||
msgid "The selected directories contain no scannable file."
|
||||
msgstr "Ընտրված թղթապանակները պարունակում են չստուգվող ֆայլ:"
|
||||
|
||||
#: core\app.py:835
|
||||
#: core\app.py:803
|
||||
msgid "Collecting files to scan"
|
||||
msgstr "Հավաքվում են ֆայլեր՝ ստուգելու համար"
|
||||
|
||||
#: core\app.py:891
|
||||
#: core\app.py:850
|
||||
msgid "%s (%d discarded)"
|
||||
msgstr "%s (%d անպիտան)"
|
||||
|
||||
#: core\engine.py:244 core\engine.py:288
|
||||
msgid "0 matches found"
|
||||
msgstr "0 համընկնում է գտնվել"
|
||||
#: core\directories.py:191
|
||||
msgid "Collected {} files to scan"
|
||||
msgstr ""
|
||||
|
||||
#: core\engine.py:262 core\engine.py:296
|
||||
msgid "%d matches found"
|
||||
msgstr "%d համընկնում է գտնվել"
|
||||
#: core\directories.py:207
|
||||
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 "Կանոնավոր արտահայտություններ"
|
||||
|
||||
@@ -176,15 +184,15 @@ msgstr "Բովանդակություն"
|
||||
msgid "Analyzed %d/%d pictures"
|
||||
msgstr "Ստուգվում է %d/%d նկարները"
|
||||
|
||||
#: core\pe\matchblock.py:181
|
||||
#: core\pe\matchblock.py:177
|
||||
msgid "Performed %d/%d chunk matches"
|
||||
msgstr "Կատարվում է %d/%d տվյալի համընկնում"
|
||||
|
||||
#: 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 "Ստուգում է %d/%d համընկնումները"
|
||||
|
||||
@@ -232,23 +240,23 @@ msgstr "Նորագույնը"
|
||||
msgid "Oldest"
|
||||
msgstr "Ամենահինը"
|
||||
|
||||
#: core\results.py:142
|
||||
#: core\results.py:134
|
||||
msgid "%d / %d (%s / %s) duplicates marked."
|
||||
msgstr "%d / %d (%s / %s) նշված կրկնօրինակներ:"
|
||||
|
||||
#: core\results.py:149
|
||||
#: core\results.py:141
|
||||
msgid " filter: %s"
|
||||
msgstr "ֆիլտր. %s"
|
||||
|
||||
#: core\scanner.py:85
|
||||
#: core\scanner.py:90
|
||||
msgid "Read size of %d/%d files"
|
||||
msgstr "Կարդալ %d/%d ֆայլերի չափը"
|
||||
|
||||
#: core\scanner.py:109
|
||||
#: core\scanner.py:116
|
||||
msgid "Read metadata of %d/%d files"
|
||||
msgstr "Կարդալ %d/%d ֆայլերի մետատվյալները"
|
||||
|
||||
#: core\scanner.py:147
|
||||
#: core\scanner.py:154
|
||||
msgid "Almost done! Fiddling with results..."
|
||||
msgstr "Գրեթե արված է! Արդյունքների կազմակերպում..."
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# Translators:
|
||||
# Andrew Senetar <arsenetar@gmail.com>, 2021
|
||||
# Fuan <jcfrt@posteo.net>, 2021
|
||||
# Emanuele, 2021
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\n"
|
||||
"Last-Translator: Emanuele, 2021\n"
|
||||
"Language-Team: Italian (https://www.transifex.com/voltaicideas/teams/116153/it/)\n"
|
||||
"Language: it\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -48,7 +49,7 @@ msgstr "Copia in corso"
|
||||
msgid "Sending to Trash"
|
||||
msgstr "Spostamento nel cestino"
|
||||
|
||||
#: core\app.py:308
|
||||
#: 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."
|
||||
@@ -56,35 +57,39 @@ msgstr ""
|
||||
"Un'azione precedente è ancora in corso. Non puoi cominciarne una nuova. "
|
||||
"Aspetta qualche secondo e quindi riprova."
|
||||
|
||||
#: core\app.py:318
|
||||
#: core\app.py:300
|
||||
msgid "No duplicates found."
|
||||
msgstr "Non sono stati trovati dei duplicati."
|
||||
|
||||
#: core\app.py:333
|
||||
#: core\app.py:315
|
||||
msgid "All marked files were copied successfully."
|
||||
msgstr "Tutti i file marcati sono stati copiati correttamente."
|
||||
|
||||
#: core\app.py:334
|
||||
#: core\app.py:317
|
||||
msgid "All marked files were moved successfully."
|
||||
msgstr "Tutti i file marcati sono stati spostati correttamente."
|
||||
|
||||
#: core\app.py:335
|
||||
#: core\app.py:319
|
||||
msgid "All marked files were deleted successfully."
|
||||
msgstr "Tutti i file marcati sono stati cancellati correttamente."
|
||||
|
||||
#: core\app.py:321
|
||||
msgid "All marked files were successfully sent to Trash."
|
||||
msgstr "Tutti i file marcati sono stati spostati nel cestino."
|
||||
|
||||
#: core\app.py:343
|
||||
#: core\app.py:326
|
||||
msgid "Could not load file: {}"
|
||||
msgstr "Impossibile caricare il file: {}"
|
||||
|
||||
#: core\app.py:399
|
||||
#: core\app.py:382
|
||||
msgid "'{}' already is in the list."
|
||||
msgstr "'{}' è già nella lista."
|
||||
|
||||
#: core\app.py:401
|
||||
#: core\app.py:384
|
||||
msgid "'{}' does not exist."
|
||||
msgstr "'{}' non esiste."
|
||||
|
||||
#: core\app.py:410
|
||||
#: core\app.py:392
|
||||
msgid ""
|
||||
"All selected %d matches are going to be ignored in all subsequent scans. "
|
||||
"Continue?"
|
||||
@@ -92,62 +97,66 @@ msgstr ""
|
||||
"Tutti i %d elementi che coincidono verranno ignorati in tutte le scansioni "
|
||||
"successive. Continuare?"
|
||||
|
||||
#: core\app.py:486
|
||||
#: core\app.py:469
|
||||
msgid "Select a directory to copy marked files to"
|
||||
msgstr "Seleziona una directory in cui desideri copiare i file contrassegnati"
|
||||
|
||||
#: core\app.py:487
|
||||
#: core\app.py:471
|
||||
msgid "Select a directory to move marked files to"
|
||||
msgstr ""
|
||||
"Seleziona una directory in cui desideri spostare i file contrassegnati"
|
||||
|
||||
#: core\app.py:527
|
||||
#: core\app.py:510
|
||||
msgid "Select a destination for your exported CSV"
|
||||
msgstr "Seleziona una destinazione per il file CSV"
|
||||
|
||||
#: core\app.py:534 core\app.py:801 core\app.py:811
|
||||
#: core\app.py:516 core\app.py:771 core\app.py:781
|
||||
msgid "Couldn't write to file: {}"
|
||||
msgstr "Impossibile modificare il file: {}"
|
||||
|
||||
#: core\app.py:559
|
||||
#: core\app.py:539
|
||||
msgid "You have no custom command set up. Set it up in your preferences."
|
||||
msgstr ""
|
||||
"Non hai impostato nessun comando personalizzato. Impostalo nelle tue "
|
||||
"preferenze."
|
||||
|
||||
#: core\app.py:727 core\app.py:740
|
||||
#: core\app.py:695 core\app.py:707
|
||||
msgid "You are about to remove %d files from results. Continue?"
|
||||
msgstr "Stai per rimuovere %d file dai risultati. Continuare?"
|
||||
|
||||
#: core\app.py:774
|
||||
#: core\app.py:743
|
||||
msgid "{} duplicate groups were changed by the re-prioritization."
|
||||
msgstr "{} gruppi duplicati sono stati cambiati dalla nuova priorirità"
|
||||
|
||||
#: core\app.py:821
|
||||
#: core\app.py:790
|
||||
msgid "The selected directories contain no scannable file."
|
||||
msgstr "Le cartelle selezionate non contengono file da scansionare."
|
||||
|
||||
#: core\app.py:835
|
||||
#: core\app.py:803
|
||||
msgid "Collecting files to scan"
|
||||
msgstr "Raccolta file da scansionare"
|
||||
|
||||
#: core\app.py:891
|
||||
#: core\app.py:850
|
||||
msgid "%s (%d discarded)"
|
||||
msgstr "%s (%d scartati)"
|
||||
|
||||
#: core\engine.py:244 core\engine.py:288
|
||||
msgid "0 matches found"
|
||||
msgstr "Nessun duplicato trovato"
|
||||
#: core\directories.py:191
|
||||
msgid "Collected {} files to scan"
|
||||
msgstr "Raccolti {} file da scansionare"
|
||||
|
||||
#: core\engine.py:262 core\engine.py:296
|
||||
msgid "%d matches found"
|
||||
msgstr "Trovato/i %d duplicato/i"
|
||||
#: core\directories.py:207
|
||||
msgid "Collected {} folders to scan"
|
||||
msgstr "Raccolte {} cartelle da scansionare"
|
||||
|
||||
#: core\gui\deletion_options.py:73
|
||||
#: core\engine.py:27
|
||||
msgid "%d matches found from %d groups"
|
||||
msgstr "%d corrispondeze trovate da %d gruppi"
|
||||
|
||||
#: core\gui\deletion_options.py:71
|
||||
msgid "You are sending {} file(s) to the Trash."
|
||||
msgstr "Stai spostando {} file al Cestino."
|
||||
|
||||
#: core\gui\exclude_list_table.py:15
|
||||
#: core\gui\exclude_list_table.py:14
|
||||
msgid "Regular Expressions"
|
||||
msgstr "Espressioni regolari"
|
||||
|
||||
@@ -181,15 +190,15 @@ msgstr "Contenuti"
|
||||
msgid "Analyzed %d/%d pictures"
|
||||
msgstr "Analizzate %d/%d immagini"
|
||||
|
||||
#: core\pe\matchblock.py:181
|
||||
#: core\pe\matchblock.py:177
|
||||
msgid "Performed %d/%d chunk matches"
|
||||
msgstr "Effettuate %d/%d comparazioni sui sottogruppi di immagini"
|
||||
|
||||
#: core\pe\matchblock.py:191
|
||||
#: core\pe\matchblock.py:185
|
||||
msgid "Preparing for matching"
|
||||
msgstr "Preparazione per la comparazione"
|
||||
|
||||
#: core\pe\matchblock.py:244
|
||||
#: core\pe\matchblock.py:234
|
||||
msgid "Verified %d/%d matches"
|
||||
msgstr "Verificate %d/%d somiglianze"
|
||||
|
||||
@@ -237,23 +246,23 @@ msgstr "Il più nuovo"
|
||||
msgid "Oldest"
|
||||
msgstr "Il più vecchio"
|
||||
|
||||
#: core\results.py:142
|
||||
#: core\results.py:134
|
||||
msgid "%d / %d (%s / %s) duplicates marked."
|
||||
msgstr "%d / %d (%s / %s) duplicati marcati."
|
||||
|
||||
#: core\results.py:149
|
||||
#: core\results.py:141
|
||||
msgid " filter: %s"
|
||||
msgstr " filtro: %s"
|
||||
|
||||
#: core\scanner.py:85
|
||||
#: core\scanner.py:90
|
||||
msgid "Read size of %d/%d files"
|
||||
msgstr "Lettura dimensione di %d/%d file"
|
||||
|
||||
#: core\scanner.py:109
|
||||
#: core\scanner.py:116
|
||||
msgid "Read metadata of %d/%d files"
|
||||
msgstr "Lettura metadata di %d/%d files"
|
||||
|
||||
#: core\scanner.py:147
|
||||
#: core\scanner.py:154
|
||||
msgid "Almost done! Fiddling with results..."
|
||||
msgstr "Quasi finito! Sto organizzando i risultati..."
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# Translators:
|
||||
# Andrew Senetar <arsenetar@gmail.com>, 2021
|
||||
# Fuan <jcfrt@posteo.net>, 2021
|
||||
# Emanuele, 2021
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\n"
|
||||
"Last-Translator: Emanuele, 2021\n"
|
||||
"Language-Team: Italian (https://www.transifex.com/voltaicideas/teams/116153/it/)\n"
|
||||
"Language: it\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -951,3 +952,45 @@ msgstr "Generale"
|
||||
#: qt\preferences_dialog.py:286
|
||||
msgid "Display"
|
||||
msgstr "Schermo"
|
||||
|
||||
#: qt\se\preferences_dialog.py:70
|
||||
msgid "Partially hash files bigger than"
|
||||
msgstr "Calcola hash parziale di file più grandi di"
|
||||
|
||||
#: qt\se\preferences_dialog.py:80
|
||||
msgid "MB"
|
||||
msgstr "MB"
|
||||
|
||||
#: qt\preferences_dialog.py:163
|
||||
msgid "Use native OS dialogs"
|
||||
msgstr "Usa le finestre di dialogo native del Sistema Operativo"
|
||||
|
||||
#: qt\preferences_dialog.py:166
|
||||
msgid ""
|
||||
"For actions such as file/folder selection use the OS native dialogs.\n"
|
||||
"Some native dialogs have limited functionality."
|
||||
msgstr ""
|
||||
"Per azioni come selezione di file/cartelle usa le finestre di dialogo native del Sistema Operativo.\n"
|
||||
"Alcune finestre di dialogo native hanno funzionalità limitate."
|
||||
|
||||
#: qt\se\preferences_dialog.py:68
|
||||
msgid "Ignore files larger than"
|
||||
msgstr "Ignora file più grandi di"
|
||||
|
||||
#: qt\app.py:135 qt\app.py:293
|
||||
msgid "Clear Cache"
|
||||
msgstr ""
|
||||
|
||||
#: qt\app.py:294
|
||||
msgid ""
|
||||
"Do you really want to clear the cache? This will remove all cached file "
|
||||
"hashes and picture analysis."
|
||||
msgstr ""
|
||||
|
||||
#: qt\app.py:299
|
||||
msgid "Cache cleared."
|
||||
msgstr ""
|
||||
|
||||
#: qt\preferences_dialog.py:173
|
||||
msgid "Use dark style"
|
||||
msgstr ""
|
||||
|
||||
@@ -44,99 +44,107 @@ msgstr "コピー中"
|
||||
msgid "Sending to Trash"
|
||||
msgstr "ごみ箱に送信します"
|
||||
|
||||
#: core\app.py:308
|
||||
#: 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:318
|
||||
#: core\app.py:300
|
||||
msgid "No duplicates found."
|
||||
msgstr "重複は見つかりませんでした。"
|
||||
|
||||
#: core\app.py:333
|
||||
#: core\app.py:315
|
||||
msgid "All marked files were copied successfully."
|
||||
msgstr "マークされたファイルはすべて正常にコピーされました。"
|
||||
|
||||
#: core\app.py:334
|
||||
#: core\app.py:317
|
||||
msgid "All marked files were moved successfully."
|
||||
msgstr "マークされたファイルはすべて正常に移動されました。"
|
||||
|
||||
#: core\app.py:335
|
||||
#: core\app.py:319
|
||||
msgid "All marked files were deleted successfully."
|
||||
msgstr ""
|
||||
|
||||
#: core\app.py:321
|
||||
msgid "All marked files were successfully sent to Trash."
|
||||
msgstr "マークされたファイルはすべてごみ箱に正常に送信されました。"
|
||||
|
||||
#: core\app.py:343
|
||||
#: core\app.py:326
|
||||
msgid "Could not load file: {}"
|
||||
msgstr "ファイルを読み込めませんでした:{}"
|
||||
|
||||
#: core\app.py:399
|
||||
#: core\app.py:382
|
||||
msgid "'{}' already is in the list."
|
||||
msgstr "「{}」既にリストに含まれています。"
|
||||
|
||||
#: core\app.py:401
|
||||
#: core\app.py:384
|
||||
msgid "'{}' does not exist."
|
||||
msgstr "'{}' 存在しません。"
|
||||
|
||||
#: core\app.py:410
|
||||
#: core\app.py:392
|
||||
msgid ""
|
||||
"All selected %d matches are going to be ignored in all subsequent scans. "
|
||||
"Continue?"
|
||||
msgstr "選択した%d個の一致は、以降のすべてのスキャンで無視されます。 継続する?"
|
||||
|
||||
#: core\app.py:486
|
||||
#: core\app.py:469
|
||||
msgid "Select a directory to copy marked files to"
|
||||
msgstr "マークされたファイルをコピーするディレクトリを選択してください"
|
||||
|
||||
#: core\app.py:487
|
||||
#: core\app.py:471
|
||||
msgid "Select a directory to move marked files to"
|
||||
msgstr "マークされたファイルを移動するディレクトリを選択してください"
|
||||
|
||||
#: core\app.py:527
|
||||
#: core\app.py:510
|
||||
msgid "Select a destination for your exported CSV"
|
||||
msgstr "エクスポートしたCSVの宛先を選択します。"
|
||||
|
||||
#: core\app.py:534 core\app.py:801 core\app.py:811
|
||||
#: core\app.py:516 core\app.py:771 core\app.py:781
|
||||
msgid "Couldn't write to file: {}"
|
||||
msgstr "ファイルに書き込めませんでした:{}"
|
||||
|
||||
#: core\app.py:559
|
||||
#: core\app.py:539
|
||||
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:695 core\app.py:707
|
||||
msgid "You are about to remove %d files from results. Continue?"
|
||||
msgstr "結果から%d個のファイルを削除しようとしています。 継続する?"
|
||||
|
||||
#: core\app.py:774
|
||||
#: core\app.py:743
|
||||
msgid "{} duplicate groups were changed by the re-prioritization."
|
||||
msgstr "{}重複するグループは、再優先順位付けによって変更されました。"
|
||||
|
||||
#: core\app.py:821
|
||||
#: core\app.py:790
|
||||
msgid "The selected directories contain no scannable file."
|
||||
msgstr "選択したディレクトリにはスキャン可能なファイルが含まれていません。"
|
||||
|
||||
#: core\app.py:835
|
||||
#: core\app.py:803
|
||||
msgid "Collecting files to scan"
|
||||
msgstr "スキャンするファイルを収集しています"
|
||||
|
||||
#: core\app.py:891
|
||||
#: core\app.py:850
|
||||
msgid "%s (%d discarded)"
|
||||
msgstr "%s (%d 廃棄)"
|
||||
|
||||
#: core\engine.py:244 core\engine.py:288
|
||||
msgid "0 matches found"
|
||||
msgstr "一致するものが見つかりません"
|
||||
#: core\directories.py:191
|
||||
msgid "Collected {} files to scan"
|
||||
msgstr ""
|
||||
|
||||
#: core\engine.py:262 core\engine.py:296
|
||||
msgid "%d matches found"
|
||||
msgstr "%d の一致が見つかりました"
|
||||
#: core\directories.py:207
|
||||
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 "正規表現"
|
||||
|
||||
@@ -168,15 +176,15 @@ msgstr "内容"
|
||||
msgid "Analyzed %d/%d pictures"
|
||||
msgstr "%d/%d 枚の写真を分析しました"
|
||||
|
||||
#: core\pe\matchblock.py:181
|
||||
#: core\pe\matchblock.py:177
|
||||
msgid "Performed %d/%d chunk matches"
|
||||
msgstr "チャンクマッチを%d/%d回実行しました"
|
||||
|
||||
#: 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 "%d/%d件の一致を確認"
|
||||
|
||||
@@ -224,23 +232,23 @@ msgstr "最新"
|
||||
msgid "Oldest"
|
||||
msgstr "最古"
|
||||
|
||||
#: core\results.py:142
|
||||
#: core\results.py:134
|
||||
msgid "%d / %d (%s / %s) duplicates marked."
|
||||
msgstr "%d / %d (%s / %s)マークされた重複。"
|
||||
|
||||
#: core\results.py:149
|
||||
#: core\results.py:141
|
||||
msgid " filter: %s"
|
||||
msgstr "フィルタ: %s"
|
||||
|
||||
#: core\scanner.py:85
|
||||
#: core\scanner.py:90
|
||||
msgid "Read size of %d/%d files"
|
||||
msgstr "%d/%dファイルのサイズを読み取った"
|
||||
|
||||
#: core\scanner.py:109
|
||||
#: core\scanner.py:116
|
||||
msgid "Read metadata of %d/%d files"
|
||||
msgstr "%d/%dファイルのメタデータを読み取った"
|
||||
|
||||
#: core\scanner.py:147
|
||||
#: core\scanner.py:154
|
||||
msgid "Almost done! Fiddling with results..."
|
||||
msgstr "ほぼ完了しました! 結果をいじっています..."
|
||||
|
||||
|
||||
@@ -925,3 +925,43 @@ 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 ""
|
||||
|
||||
@@ -45,99 +45,107 @@ msgstr "복사중"
|
||||
msgid "Sending to Trash"
|
||||
msgstr "휴지통으로 보내기"
|
||||
|
||||
#: core\app.py:308
|
||||
#: 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:318
|
||||
#: core\app.py:300
|
||||
msgid "No duplicates found."
|
||||
msgstr "중복 파일이 없습니다."
|
||||
|
||||
#: core\app.py:333
|
||||
#: core\app.py:315
|
||||
msgid "All marked files were copied successfully."
|
||||
msgstr "표시된 모든 파일이 성공적으로 복사되었습니다."
|
||||
|
||||
#: core\app.py:334
|
||||
#: core\app.py:317
|
||||
msgid "All marked files were moved successfully."
|
||||
msgstr "표시된 모든 파일이 성공적으로 이동되었습니다."
|
||||
|
||||
#: core\app.py:335
|
||||
#: core\app.py:319
|
||||
msgid "All marked files were deleted successfully."
|
||||
msgstr ""
|
||||
|
||||
#: core\app.py:321
|
||||
msgid "All marked files were successfully sent to Trash."
|
||||
msgstr "표시된 모든 파일이 성공적으로 휴지통으로 전송되었습니다."
|
||||
|
||||
#: core\app.py:343
|
||||
#: core\app.py:326
|
||||
msgid "Could not load file: {}"
|
||||
msgstr "파일을로드 할 수 없습니다 : {}"
|
||||
|
||||
#: core\app.py:399
|
||||
#: core\app.py:382
|
||||
msgid "'{}' already is in the list."
|
||||
msgstr "'{}' 는 이미 목록에 있습니다."
|
||||
|
||||
#: core\app.py:401
|
||||
#: core\app.py:384
|
||||
msgid "'{}' does not exist."
|
||||
msgstr "'{}' 가 존재하지 않습니다."
|
||||
|
||||
#: core\app.py:410
|
||||
#: core\app.py:392
|
||||
msgid ""
|
||||
"All selected %d matches are going to be ignored in all subsequent scans. "
|
||||
"Continue?"
|
||||
msgstr "선택된 %d개의 일치 항목은 모든 후속 검색에서 무시됩니다. 계속하다?"
|
||||
|
||||
#: core\app.py:486
|
||||
#: core\app.py:469
|
||||
msgid "Select a directory to copy marked files to"
|
||||
msgstr "표시된 파일을 복사 할 디렉토리를 선택하십시오"
|
||||
|
||||
#: core\app.py:487
|
||||
#: core\app.py:471
|
||||
msgid "Select a directory to move marked files to"
|
||||
msgstr "표시된 파일을 이동할 디렉토리를 선택하십시오"
|
||||
|
||||
#: core\app.py:527
|
||||
#: core\app.py:510
|
||||
msgid "Select a destination for your exported CSV"
|
||||
msgstr "내 보낸 CSV의 대상을 선택하십시오"
|
||||
|
||||
#: core\app.py:534 core\app.py:801 core\app.py:811
|
||||
#: core\app.py:516 core\app.py:771 core\app.py:781
|
||||
msgid "Couldn't write to file: {}"
|
||||
msgstr "파일에 쓸 수 없습니다 : {}"
|
||||
|
||||
#: core\app.py:559
|
||||
#: core\app.py:539
|
||||
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:695 core\app.py:707
|
||||
msgid "You are about to remove %d files from results. Continue?"
|
||||
msgstr "결과에서 %d 개의 파일을 제거하려고합니다. 계속하다?"
|
||||
|
||||
#: core\app.py:774
|
||||
#: core\app.py:743
|
||||
msgid "{} duplicate groups were changed by the re-prioritization."
|
||||
msgstr "{} 개의 중복 그룹이 우선 순위 재 지정으로 변경되었습니다."
|
||||
|
||||
#: core\app.py:821
|
||||
#: core\app.py:790
|
||||
msgid "The selected directories contain no scannable file."
|
||||
msgstr "선택한 디렉토리에 스캔 가능한 파일이 없습니다."
|
||||
|
||||
#: core\app.py:835
|
||||
#: core\app.py:803
|
||||
msgid "Collecting files to scan"
|
||||
msgstr "스캔 할 파일 수집"
|
||||
|
||||
#: core\app.py:891
|
||||
#: core\app.py:850
|
||||
msgid "%s (%d discarded)"
|
||||
msgstr "%s (%d 폐기)"
|
||||
|
||||
#: core\engine.py:244 core\engine.py:288
|
||||
msgid "0 matches found"
|
||||
msgstr "일치하는 항목이 없습니다"
|
||||
#: core\directories.py:191
|
||||
msgid "Collected {} files to scan"
|
||||
msgstr ""
|
||||
|
||||
#: core\engine.py:262 core\engine.py:296
|
||||
msgid "%d matches found"
|
||||
msgstr "%d개의 일치 항목을 찾았습니다."
|
||||
#: core\directories.py:207
|
||||
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 "정규식"
|
||||
|
||||
@@ -169,15 +177,15 @@ msgstr "내용"
|
||||
msgid "Analyzed %d/%d pictures"
|
||||
msgstr "%d/%d 사진 분석"
|
||||
|
||||
#: core\pe\matchblock.py:181
|
||||
#: core\pe\matchblock.py:177
|
||||
msgid "Performed %d/%d chunk matches"
|
||||
msgstr "%d/%d 청크 매치 수행"
|
||||
|
||||
#: 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 "%d/%d 일치 확인"
|
||||
|
||||
@@ -225,23 +233,23 @@ msgstr "최신"
|
||||
msgid "Oldest"
|
||||
msgstr "가장 오래된"
|
||||
|
||||
#: core\results.py:142
|
||||
#: core\results.py:134
|
||||
msgid "%d / %d (%s / %s) duplicates marked."
|
||||
msgstr "%d / %d (%s / %s) 개의 중복이 표시되었습니다."
|
||||
|
||||
#: core\results.py:149
|
||||
#: core\results.py:141
|
||||
msgid " filter: %s"
|
||||
msgstr "필터: %s"
|
||||
|
||||
#: core\scanner.py:85
|
||||
#: core\scanner.py:90
|
||||
msgid "Read size of %d/%d files"
|
||||
msgstr "%d/%d 개의 파일을 읽을 수 있습니다."
|
||||
|
||||
#: core\scanner.py:109
|
||||
#: core\scanner.py:116
|
||||
msgid "Read metadata of %d/%d files"
|
||||
msgstr "%d/%d 개 파일의 메타 데이터를 읽었습니다."
|
||||
|
||||
#: core\scanner.py:147
|
||||
#: core\scanner.py:154
|
||||
msgid "Almost done! Fiddling with results..."
|
||||
msgstr "거의 완료되었습니다! 결과를 만지작 거리는 중..."
|
||||
|
||||
|
||||
@@ -927,3 +927,43 @@ 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 ""
|
||||
|
||||
122
locale/ms/LC_MESSAGES/columns.po
Normal file
122
locale/ms/LC_MESSAGES/columns.po
Normal file
@@ -0,0 +1,122 @@
|
||||
# Translators:
|
||||
# Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi (MNH48) <admin@mnh48.moe>, 2021
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Last-Translator: Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi (MNH48) <admin@mnh48.moe>, 2021\n"
|
||||
"Language-Team: Malay (https://www.transifex.com/voltaicideas/teams/116153/ms/)\n"
|
||||
"Language: ms\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: utf-8\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20
|
||||
#: core\gui\problem_table.py:18
|
||||
msgid "File Path"
|
||||
msgstr "Laluan Fail"
|
||||
|
||||
#: core\gui\problem_table.py:19
|
||||
msgid "Error Message"
|
||||
msgstr "Mesej Ralat"
|
||||
|
||||
#: core\me\prioritize.py:23
|
||||
msgid "Duration"
|
||||
msgstr "Tempoh"
|
||||
|
||||
#: core\me\prioritize.py:30 core\me\result_table.py:23
|
||||
msgid "Bitrate"
|
||||
msgstr "Kadar Bit"
|
||||
|
||||
#: core\me\prioritize.py:37
|
||||
msgid "Samplerate"
|
||||
msgstr "Kadar Sampel"
|
||||
|
||||
#: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92
|
||||
#: core\se\result_table.py:19
|
||||
msgid "Filename"
|
||||
msgstr "Nama Fail"
|
||||
|
||||
#: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75
|
||||
#: core\se\result_table.py:20
|
||||
msgid "Folder"
|
||||
msgstr "Folder"
|
||||
|
||||
#: core\me\result_table.py:21
|
||||
msgid "Size (MB)"
|
||||
msgstr "Saiz (MB)"
|
||||
|
||||
#: core\me\result_table.py:22
|
||||
msgid "Time"
|
||||
msgstr "Masa"
|
||||
|
||||
#: core\me\result_table.py:24
|
||||
msgid "Sample Rate"
|
||||
msgstr "Kadar Sampel"
|
||||
|
||||
#: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65
|
||||
#: core\se\result_table.py:22
|
||||
msgid "Kind"
|
||||
msgstr "Jenis"
|
||||
|
||||
#: core\me\result_table.py:26 core\pe\result_table.py:25
|
||||
#: core\prioritize.py:163 core\se\result_table.py:23
|
||||
msgid "Modification"
|
||||
msgstr "Pengubahsuaian"
|
||||
|
||||
#: core\me\result_table.py:27
|
||||
msgid "Title"
|
||||
msgstr "Tajuk"
|
||||
|
||||
#: core\me\result_table.py:28
|
||||
msgid "Artist"
|
||||
msgstr "Artis"
|
||||
|
||||
#: core\me\result_table.py:29
|
||||
msgid "Album"
|
||||
msgstr "Album"
|
||||
|
||||
#: core\me\result_table.py:30
|
||||
msgid "Genre"
|
||||
msgstr "Genre"
|
||||
|
||||
#: core\me\result_table.py:31
|
||||
msgid "Year"
|
||||
msgstr "Tahun"
|
||||
|
||||
#: core\me\result_table.py:32
|
||||
msgid "Track Number"
|
||||
msgstr "Nombor Runut"
|
||||
|
||||
#: core\me\result_table.py:33
|
||||
msgid "Comment"
|
||||
msgstr "Komen"
|
||||
|
||||
#: core\me\result_table.py:34 core\pe\result_table.py:26
|
||||
#: core\se\result_table.py:24
|
||||
msgid "Match %"
|
||||
msgstr "% Padanan"
|
||||
|
||||
#: core\me\result_table.py:35 core\se\result_table.py:25
|
||||
msgid "Words Used"
|
||||
msgstr "Perkataan Diguna"
|
||||
|
||||
#: core\me\result_table.py:36 core\pe\result_table.py:27
|
||||
#: core\se\result_table.py:26
|
||||
msgid "Dupe Count"
|
||||
msgstr "Jumlah Duplikasi"
|
||||
|
||||
#: core\pe\prioritize.py:23 core\pe\result_table.py:23
|
||||
msgid "Dimensions"
|
||||
msgstr "Dimensi"
|
||||
|
||||
#: core\pe\result_table.py:21 core\se\result_table.py:21
|
||||
msgid "Size (KB)"
|
||||
msgstr "Saiz (KB)"
|
||||
|
||||
#: core\pe\result_table.py:24
|
||||
msgid "EXIF Timestamp"
|
||||
msgstr "Cap Masa EXIF"
|
||||
|
||||
#: core\prioritize.py:156
|
||||
msgid "Size"
|
||||
msgstr "Saiz"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user