mirror of
https://github.com/arsenetar/dupeguru.git
synced 2026-01-25 16:11:39 +00:00
Compare commits
82 Commits
06eca11f0b
...
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 | |||
|
5a4958cff9
|
|||
|
be10b462fc
|
|||
|
d62b13bcdb
|
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:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ master ]
|
branches: [master]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master ]
|
branches: [master]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Set up Python 3.9
|
- name: Set up Python 3.10
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.9
|
python-version: "3.10"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
@@ -28,10 +28,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Set up Python 3.9
|
- name: Set up Python 3.10
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.9
|
python-version: "3.10"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
@@ -45,16 +45,20 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
python-version: [3.6, 3.7, 3.8, 3.9]
|
python-version: [3.7, 3.8, 3.9, "3.10"]
|
||||||
exclude:
|
exclude:
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
python-version: 3.6
|
python-version: 3.7
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
python-version: 3.7
|
python-version: 3.8
|
||||||
- os: windows-latest
|
- os: macos-latest
|
||||||
python-version: 3.6
|
python-version: 3.9
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
python-version: 3.7
|
python-version: 3.7
|
||||||
|
- os: windows-latest
|
||||||
|
python-version: 3.8
|
||||||
|
- os: windows-latest
|
||||||
|
python-version: 3.9
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
@@ -72,3 +76,9 @@ jobs:
|
|||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: |
|
run: |
|
||||||
pytest core hscommon
|
pytest core hscommon
|
||||||
|
- name: Upload Artifacts
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: modules ${{ matrix.python-version }}
|
||||||
|
path: ${{ github.workspace }}/**/*.so
|
||||||
|
|||||||
123
.gitignore
vendored
123
.gitignore
vendored
@@ -1,28 +1,111 @@
|
|||||||
.DS_Store
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
*.so
|
*.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
|
*.mo
|
||||||
*.waf*
|
#*.pot
|
||||||
.lock-waf*
|
|
||||||
.tox
|
|
||||||
/tags
|
|
||||||
|
|
||||||
build
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||||
dist
|
__pypackages__/
|
||||||
env*
|
|
||||||
/deps
|
|
||||||
cocoa/autogen
|
|
||||||
|
|
||||||
/run.py
|
# Environments
|
||||||
/cocoa/*/Info.plist
|
.env
|
||||||
/cocoa/*/build
|
.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
|
/qt/*_rc.py
|
||||||
/help/*/conf.py
|
/help/*/conf.py
|
||||||
/help/*/changelog.rst
|
/help/*/changelog.rst
|
||||||
/transifex
|
cocoa/autogen
|
||||||
|
/cocoa/*/Info.plist
|
||||||
|
/cocoa/*/build
|
||||||
|
|
||||||
*.pyd
|
*.waf*
|
||||||
*.exe
|
.lock-waf*
|
||||||
*.spec
|
/tags
|
||||||
|
|
||||||
.vscode
|
|
||||||
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
.travis.yml
27
.travis.yml
@@ -1,27 +0,0 @@
|
|||||||
sudo: false
|
|
||||||
language: python
|
|
||||||
install:
|
|
||||||
- pip3 install -r requirements.txt -r requirements-extra.txt
|
|
||||||
script: tox
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- os: "linux"
|
|
||||||
dist: "xenial"
|
|
||||||
python: "3.6"
|
|
||||||
- os: "linux"
|
|
||||||
dist: "xenial"
|
|
||||||
python: "3.7"
|
|
||||||
- os: "linux"
|
|
||||||
dist: "focal"
|
|
||||||
python: "3.8"
|
|
||||||
- os: "linux"
|
|
||||||
dist: "focal"
|
|
||||||
python: "3.9"
|
|
||||||
- os: "windows"
|
|
||||||
language: shell
|
|
||||||
python: "3.9"
|
|
||||||
env: "PATH=/c/python39:/c/python39/Scripts:$PATH"
|
|
||||||
before_install:
|
|
||||||
- choco install python --version=3.9.6
|
|
||||||
- cp /c/python39/python.exe /c/python39/python3.exe
|
|
||||||
script: tox -e py39
|
|
||||||
27
.tx/config
27
.tx/config
@@ -1,26 +1,27 @@
|
|||||||
[main]
|
[main]
|
||||||
host = https://www.transifex.com
|
host = https://www.transifex.com
|
||||||
|
|
||||||
[dupeguru-1.core]
|
[o:voltaicideas:p:dupeguru-1:r:columns]
|
||||||
file_filter = locale/<lang>/LC_MESSAGES/core.po
|
|
||||||
source_file = locale/core.pot
|
|
||||||
source_lang = en
|
|
||||||
type = PO
|
|
||||||
|
|
||||||
[dupeguru-1.columns]
|
|
||||||
file_filter = locale/<lang>/LC_MESSAGES/columns.po
|
file_filter = locale/<lang>/LC_MESSAGES/columns.po
|
||||||
source_file = locale/columns.pot
|
source_file = locale/columns.pot
|
||||||
source_lang = en
|
source_lang = en
|
||||||
type = PO
|
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
|
file_filter = locale/<lang>/LC_MESSAGES/ui.po
|
||||||
source_file = locale/ui.pot
|
source_file = locale/ui.pot
|
||||||
source_lang = en
|
source_lang = en
|
||||||
type = PO
|
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 ?= python3
|
||||||
PYTHON_VERSION_MINOR := $(shell ${PYTHON} -c "import sys; print(sys.version_info.minor)")
|
PYTHON_VERSION_MINOR := $(shell ${PYTHON} -c "import sys; print(sys.version_info.minor)")
|
||||||
PYRCC5 ?= pyrcc5
|
PYRCC5 ?= pyrcc5
|
||||||
REQ_MINOR_VERSION = 6
|
REQ_MINOR_VERSION = 7
|
||||||
PREFIX ?= /usr/local
|
PREFIX ?= /usr/local
|
||||||
|
|
||||||
# Window compatability via Msys2
|
# Window compatability via Msys2
|
||||||
@@ -53,7 +53,7 @@ pyc: | env
|
|||||||
${VENV_PYTHON} -m compileall ${packages}
|
${VENV_PYTHON} -m compileall ${packages}
|
||||||
|
|
||||||
reqs:
|
reqs:
|
||||||
ifneq ($(shell test $(PYTHON_VERSION_MINOR) -gt $(REQ_MINOR_VERSION); echo $$?),0)
|
ifneq ($(shell test $(PYTHON_VERSION_MINOR) -ge $(REQ_MINOR_VERSION); echo $$?),0)
|
||||||
$(error "Python 3.${REQ_MINOR_VERSION}+ required. Aborting.")
|
$(error "Python 3.${REQ_MINOR_VERSION}+ required. Aborting.")
|
||||||
endif
|
endif
|
||||||
ifndef NO_VENV
|
ifndef NO_VENV
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ For windows instructions see the [Windows Instructions](Windows.md).
|
|||||||
For macos instructions (qt version) see the [macOS Instructions](macos.md).
|
For macos instructions (qt version) see the [macOS Instructions](macos.md).
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
* [Python 3.6+][python]
|
* [Python 3.7+][python]
|
||||||
* PyQt5
|
* PyQt5
|
||||||
|
|
||||||
### System Setup
|
### System Setup
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
### Prerequisites
|
### 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
|
- [Visual Studio 2019][vs] or [Visual Studio Build Tools 2019][vsBuildTools] with the Windows 10 SDK
|
||||||
- [nsis][nsis] (for installer creation)
|
- [nsis][nsis] (for installer creation)
|
||||||
- [msys2][msys2] (for using makefile method)
|
- [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.
|
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)
|
### 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>
|
$ cd <dupeGuru directory>
|
||||||
$ py -3.8 -m venv .\env
|
$ 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
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
import os
|
from pathlib import Path
|
||||||
import os.path as op
|
import sys
|
||||||
from optparse import OptionParser
|
from optparse import OptionParser
|
||||||
import shutil
|
import shutil
|
||||||
|
from multiprocessing import Pool
|
||||||
|
|
||||||
from setuptools import setup, Extension
|
from setuptools import sandbox
|
||||||
|
|
||||||
from hscommon import sphinxgen
|
from hscommon import sphinxgen
|
||||||
from hscommon.build import (
|
from hscommon.build import (
|
||||||
add_to_pythonpath,
|
add_to_pythonpath,
|
||||||
print_and_do,
|
print_and_do,
|
||||||
move_all,
|
|
||||||
fix_qt_resource_file,
|
fix_qt_resource_file,
|
||||||
)
|
)
|
||||||
from hscommon import loc
|
from hscommon import loc
|
||||||
@@ -30,7 +29,8 @@ def parse_args():
|
|||||||
dest="clean",
|
dest="clean",
|
||||||
help="Clean build folder before building",
|
help="Clean build folder before building",
|
||||||
)
|
)
|
||||||
parser.add_option("--doc", action="store_true", dest="doc", help="Build only the help file")
|
parser.add_option("--doc", action="store_true", dest="doc", help="Build only the help file (en)")
|
||||||
|
parser.add_option("--alldoc", action="store_true", dest="all_doc", help="Build only the help file in all languages")
|
||||||
parser.add_option("--loc", action="store_true", dest="loc", help="Build only localization")
|
parser.add_option("--loc", action="store_true", dest="loc", help="Build only localization")
|
||||||
parser.add_option(
|
parser.add_option(
|
||||||
"--updatepot",
|
"--updatepot",
|
||||||
@@ -60,16 +60,16 @@ def parse_args():
|
|||||||
return options
|
return options
|
||||||
|
|
||||||
|
|
||||||
def build_help():
|
def build_one_help(language):
|
||||||
print("Generating Help")
|
print("Generating Help in {}".format(language))
|
||||||
current_path = op.abspath(".")
|
current_path = Path(".").absolute()
|
||||||
help_basepath = op.join(current_path, "help", "en")
|
changelog_path = current_path.joinpath("help", "changelog")
|
||||||
help_destpath = op.join(current_path, "build", "help")
|
|
||||||
changelog_path = op.join(current_path, "help", "changelog")
|
|
||||||
tixurl = "https://github.com/arsenetar/dupeguru/issues/{}"
|
tixurl = "https://github.com/arsenetar/dupeguru/issues/{}"
|
||||||
confrepl = {"language": "en"}
|
changelogtmpl = current_path.joinpath("help", "changelog.tmpl")
|
||||||
changelogtmpl = op.join(current_path, "help", "changelog.tmpl")
|
conftmpl = current_path.joinpath("help", "conf.tmpl")
|
||||||
conftmpl = op.join(current_path, "help", "conf.tmpl")
|
help_basepath = current_path.joinpath("help", language)
|
||||||
|
help_destpath = current_path.joinpath("build", "help", language)
|
||||||
|
confrepl = {"language": language}
|
||||||
sphinxgen.gen(
|
sphinxgen.gen(
|
||||||
help_basepath,
|
help_basepath,
|
||||||
help_destpath,
|
help_destpath,
|
||||||
@@ -81,16 +81,23 @@ def build_help():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_help():
|
||||||
|
languages = ["en", "de", "fr", "hy", "ru", "uk"]
|
||||||
|
# Running with Pools as for some reason sphinx seems to cross contaminate the output otherwise
|
||||||
|
with Pool(len(languages)) as p:
|
||||||
|
p.map(build_one_help, languages)
|
||||||
|
|
||||||
|
|
||||||
def build_qt_localizations():
|
def build_qt_localizations():
|
||||||
loc.compile_all_po(op.join("qtlib", "locale"))
|
loc.compile_all_po(Path("qtlib", "locale"))
|
||||||
loc.merge_locale_dir(op.join("qtlib", "locale"), "locale")
|
loc.merge_locale_dir(Path("qtlib", "locale"), "locale")
|
||||||
|
|
||||||
|
|
||||||
def build_localizations():
|
def build_localizations():
|
||||||
loc.compile_all_po("locale")
|
loc.compile_all_po("locale")
|
||||||
build_qt_localizations()
|
build_qt_localizations()
|
||||||
locale_dest = op.join("build", "locale")
|
locale_dest = Path("build", "locale")
|
||||||
if op.exists(locale_dest):
|
if locale_dest.exists():
|
||||||
shutil.rmtree(locale_dest)
|
shutil.rmtree(locale_dest)
|
||||||
shutil.copytree("locale", locale_dest, ignore=shutil.ignore_patterns("*.po", "*.pot"))
|
shutil.copytree("locale", locale_dest, ignore=shutil.ignore_patterns("*.po", "*.pot"))
|
||||||
|
|
||||||
@@ -98,57 +105,35 @@ def build_localizations():
|
|||||||
def build_updatepot():
|
def build_updatepot():
|
||||||
print("Building .pot files from source files")
|
print("Building .pot files from source files")
|
||||||
print("Building core.pot")
|
print("Building core.pot")
|
||||||
loc.generate_pot(["core"], op.join("locale", "core.pot"), ["tr"])
|
loc.generate_pot(["core"], Path("locale", "core.pot"), ["tr"])
|
||||||
print("Building columns.pot")
|
print("Building columns.pot")
|
||||||
loc.generate_pot(["core"], op.join("locale", "columns.pot"), ["coltr"])
|
loc.generate_pot(["core"], Path("locale", "columns.pot"), ["coltr"])
|
||||||
print("Building ui.pot")
|
print("Building ui.pot")
|
||||||
# When we're not under OS X, we don't want to overwrite ui.pot because it contains Cocoa locs
|
# When we're not under OS X, we don't want to overwrite ui.pot because it contains Cocoa locs
|
||||||
# We want to merge the generated pot with the old pot in the most preserving way possible.
|
# We want to merge the generated pot with the old pot in the most preserving way possible.
|
||||||
ui_packages = ["qt", op.join("cocoa", "inter")]
|
ui_packages = ["qt", Path("cocoa", "inter")]
|
||||||
loc.generate_pot(ui_packages, op.join("locale", "ui.pot"), ["tr"], merge=True)
|
loc.generate_pot(ui_packages, Path("locale", "ui.pot"), ["tr"], merge=True)
|
||||||
print("Building qtlib.pot")
|
print("Building qtlib.pot")
|
||||||
loc.generate_pot(["qtlib"], op.join("qtlib", "locale", "qtlib.pot"), ["tr"])
|
loc.generate_pot(["qtlib"], Path("qtlib", "locale", "qtlib.pot"), ["tr"])
|
||||||
|
|
||||||
|
|
||||||
def build_mergepot():
|
def build_mergepot():
|
||||||
print("Updating .po files using .pot files")
|
print("Updating .po files using .pot files")
|
||||||
loc.merge_pots_into_pos("locale")
|
loc.merge_pots_into_pos("locale")
|
||||||
loc.merge_pots_into_pos(op.join("qtlib", "locale"))
|
loc.merge_pots_into_pos(Path("qtlib", "locale"))
|
||||||
# loc.merge_pots_into_pos(op.join("cocoalib", "locale"))
|
# loc.merge_pots_into_pos(Path("cocoalib", "locale"))
|
||||||
|
|
||||||
|
|
||||||
def build_normpo():
|
def build_normpo():
|
||||||
loc.normalize_all_pos("locale")
|
loc.normalize_all_pos("locale")
|
||||||
loc.normalize_all_pos(op.join("qtlib", "locale"))
|
loc.normalize_all_pos(Path("qtlib", "locale"))
|
||||||
# loc.normalize_all_pos(op.join("cocoalib", "locale"))
|
# loc.normalize_all_pos(Path("cocoalib", "locale"))
|
||||||
|
|
||||||
|
|
||||||
def build_pe_modules():
|
def build_pe_modules():
|
||||||
print("Building PE Modules")
|
print("Building PE Modules")
|
||||||
exts = [
|
# Leverage setup.py to build modules
|
||||||
Extension(
|
sandbox.run_setup("setup.py", ["build_ext", "--inplace"])
|
||||||
"_block",
|
|
||||||
[
|
|
||||||
op.join("core", "pe", "modules", "block.c"),
|
|
||||||
op.join("core", "pe", "modules", "common.c"),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Extension(
|
|
||||||
"_cache",
|
|
||||||
[
|
|
||||||
op.join("core", "pe", "modules", "cache.c"),
|
|
||||||
op.join("core", "pe", "modules", "common.c"),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
exts.append(Extension("_block_qt", [op.join("qt", "pe", "modules", "block.c")]))
|
|
||||||
setup(
|
|
||||||
script_args=["build_ext", "--inplace"],
|
|
||||||
ext_modules=exts,
|
|
||||||
)
|
|
||||||
move_all("_block_qt*", op.join("qt", "pe"))
|
|
||||||
move_all("_block*", op.join("core", "pe"))
|
|
||||||
move_all("_cache*", op.join("core", "pe"))
|
|
||||||
|
|
||||||
|
|
||||||
def build_normal():
|
def build_normal():
|
||||||
@@ -159,19 +144,22 @@ def build_normal():
|
|||||||
print("Building localizations")
|
print("Building localizations")
|
||||||
build_localizations()
|
build_localizations()
|
||||||
print("Building Qt stuff")
|
print("Building Qt stuff")
|
||||||
print_and_do("pyrcc5 {0} > {1}".format(op.join("qt", "dg.qrc"), op.join("qt", "dg_rc.py")))
|
print_and_do("pyrcc5 {0} > {1}".format(Path("qt", "dg.qrc"), Path("qt", "dg_rc.py")))
|
||||||
fix_qt_resource_file(op.join("qt", "dg_rc.py"))
|
fix_qt_resource_file(Path("qt", "dg_rc.py"))
|
||||||
build_help()
|
build_help()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
if sys.version_info < (3, 7):
|
||||||
|
sys.exit("Python < 3.7 is unsupported.")
|
||||||
options = parse_args()
|
options = parse_args()
|
||||||
if options.clean:
|
if options.clean and Path("build").exists():
|
||||||
if op.exists("build"):
|
|
||||||
shutil.rmtree("build")
|
shutil.rmtree("build")
|
||||||
if not op.exists("build"):
|
if not Path("build").exists():
|
||||||
os.mkdir("build")
|
Path("build").mkdir()
|
||||||
if options.doc:
|
if options.doc:
|
||||||
|
build_one_help("en")
|
||||||
|
elif options.all_doc:
|
||||||
build_help()
|
build_help()
|
||||||
elif options.loc:
|
elif options.loc:
|
||||||
build_localizations()
|
build_localizations()
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
__version__ = "4.1.1"
|
__version__ = "4.2.1"
|
||||||
__appname__ = "dupeGuru"
|
__appname__ = "dupeGuru"
|
||||||
|
|||||||
151
core/app.py
151
core/app.py
@@ -48,31 +48,31 @@ MSG_MANY_FILES_TO_OPEN = tr(
|
|||||||
|
|
||||||
|
|
||||||
class DestType:
|
class DestType:
|
||||||
Direct = 0
|
DIRECT = 0
|
||||||
Relative = 1
|
RELATIVE = 1
|
||||||
Absolute = 2
|
ABSOLUTE = 2
|
||||||
|
|
||||||
|
|
||||||
class JobType:
|
class JobType:
|
||||||
Scan = "job_scan"
|
SCAN = "job_scan"
|
||||||
Load = "job_load"
|
LOAD = "job_load"
|
||||||
Move = "job_move"
|
MOVE = "job_move"
|
||||||
Copy = "job_copy"
|
COPY = "job_copy"
|
||||||
Delete = "job_delete"
|
DELETE = "job_delete"
|
||||||
|
|
||||||
|
|
||||||
class AppMode:
|
class AppMode:
|
||||||
Standard = 0
|
STANDARD = 0
|
||||||
Music = 1
|
MUSIC = 1
|
||||||
Picture = 2
|
PICTURE = 2
|
||||||
|
|
||||||
|
|
||||||
JOBID2TITLE = {
|
JOBID2TITLE = {
|
||||||
JobType.Scan: tr("Scanning for duplicates"),
|
JobType.SCAN: tr("Scanning for duplicates"),
|
||||||
JobType.Load: tr("Loading"),
|
JobType.LOAD: tr("Loading"),
|
||||||
JobType.Move: tr("Moving"),
|
JobType.MOVE: tr("Moving"),
|
||||||
JobType.Copy: tr("Copying"),
|
JobType.COPY: tr("Copying"),
|
||||||
JobType.Delete: tr("Sending to Trash"),
|
JobType.DELETE: tr("Sending to Trash"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -126,18 +126,20 @@ class DupeGuru(Broadcaster):
|
|||||||
|
|
||||||
PICTURE_CACHE_TYPE = "sqlite" # set to 'shelve' for a ShelveCache
|
PICTURE_CACHE_TYPE = "sqlite" # set to 'shelve' for a ShelveCache
|
||||||
|
|
||||||
def __init__(self, view):
|
def __init__(self, view, portable=False):
|
||||||
if view.get_default(DEBUG_MODE_PREFERENCE):
|
if view.get_default(DEBUG_MODE_PREFERENCE):
|
||||||
logging.getLogger().setLevel(logging.DEBUG)
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
logging.debug("Debug mode enabled")
|
logging.debug("Debug mode enabled")
|
||||||
Broadcaster.__init__(self)
|
Broadcaster.__init__(self)
|
||||||
self.view = view
|
self.view = view
|
||||||
self.appdata = desktop.special_folder_path(desktop.SpecialFolder.AppData, appname=self.NAME)
|
self.appdata = desktop.special_folder_path(desktop.SpecialFolder.APPDATA, appname=self.NAME, portable=portable)
|
||||||
if not op.exists(self.appdata):
|
if not op.exists(self.appdata):
|
||||||
os.makedirs(self.appdata)
|
os.makedirs(self.appdata)
|
||||||
self.app_mode = AppMode.Standard
|
self.app_mode = AppMode.STANDARD
|
||||||
self.discarded_file_count = 0
|
self.discarded_file_count = 0
|
||||||
self.exclude_list = ExcludeList()
|
self.exclude_list = ExcludeList()
|
||||||
|
hash_cache_file = op.join(self.appdata, "hash_cache.db")
|
||||||
|
fs.filesdb.connect(hash_cache_file)
|
||||||
self.directories = directories.Directories(self.exclude_list)
|
self.directories = directories.Directories(self.exclude_list)
|
||||||
self.results = results.Results(self)
|
self.results = results.Results(self)
|
||||||
self.ignore_list = IgnoreList()
|
self.ignore_list = IgnoreList()
|
||||||
@@ -148,7 +150,7 @@ class DupeGuru(Broadcaster):
|
|||||||
"escape_filter_regexp": True,
|
"escape_filter_regexp": True,
|
||||||
"clean_empty_dirs": False,
|
"clean_empty_dirs": False,
|
||||||
"ignore_hardlink_matches": False,
|
"ignore_hardlink_matches": False,
|
||||||
"copymove_dest_type": DestType.Relative,
|
"copymove_dest_type": DestType.RELATIVE,
|
||||||
"picture_cache_type": self.PICTURE_CACHE_TYPE,
|
"picture_cache_type": self.PICTURE_CACHE_TYPE,
|
||||||
}
|
}
|
||||||
self.selected_dupes = []
|
self.selected_dupes = []
|
||||||
@@ -169,9 +171,9 @@ class DupeGuru(Broadcaster):
|
|||||||
def _recreate_result_table(self):
|
def _recreate_result_table(self):
|
||||||
if self.result_table is not None:
|
if self.result_table is not None:
|
||||||
self.result_table.disconnect()
|
self.result_table.disconnect()
|
||||||
if self.app_mode == AppMode.Picture:
|
if self.app_mode == AppMode.PICTURE:
|
||||||
self.result_table = pe.result_table.ResultTable(self)
|
self.result_table = pe.result_table.ResultTable(self)
|
||||||
elif self.app_mode == AppMode.Music:
|
elif self.app_mode == AppMode.MUSIC:
|
||||||
self.result_table = me.result_table.ResultTable(self)
|
self.result_table = me.result_table.ResultTable(self)
|
||||||
else:
|
else:
|
||||||
self.result_table = se.result_table.ResultTable(self)
|
self.result_table = se.result_table.ResultTable(self)
|
||||||
@@ -184,12 +186,10 @@ class DupeGuru(Broadcaster):
|
|||||||
return op.join(self.appdata, cache_name)
|
return op.join(self.appdata, cache_name)
|
||||||
|
|
||||||
def _get_dupe_sort_key(self, dupe, get_group, key, delta):
|
def _get_dupe_sort_key(self, dupe, get_group, key, delta):
|
||||||
if self.app_mode in (AppMode.Music, AppMode.Picture):
|
if self.app_mode in (AppMode.MUSIC, AppMode.PICTURE) and key == "folder_path":
|
||||||
if key == "folder_path":
|
|
||||||
dupe_folder_path = getattr(dupe, "display_folder_path", dupe.folder_path)
|
dupe_folder_path = getattr(dupe, "display_folder_path", dupe.folder_path)
|
||||||
return str(dupe_folder_path).lower()
|
return str(dupe_folder_path).lower()
|
||||||
if self.app_mode == AppMode.Picture:
|
if self.app_mode == AppMode.PICTURE and delta and key == "dimensions":
|
||||||
if delta and key == "dimensions":
|
|
||||||
r = cmp_value(dupe, key)
|
r = cmp_value(dupe, key)
|
||||||
ref_value = cmp_value(get_group().ref, key)
|
ref_value = cmp_value(get_group().ref, key)
|
||||||
return get_delta_dimensions(r, ref_value)
|
return get_delta_dimensions(r, ref_value)
|
||||||
@@ -212,8 +212,7 @@ class DupeGuru(Broadcaster):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def _get_group_sort_key(self, group, key):
|
def _get_group_sort_key(self, group, key):
|
||||||
if self.app_mode in (AppMode.Music, AppMode.Picture):
|
if self.app_mode in (AppMode.MUSIC, AppMode.PICTURE) and key == "folder_path":
|
||||||
if key == "folder_path":
|
|
||||||
dupe_folder_path = getattr(group.ref, "display_folder_path", group.ref.folder_path)
|
dupe_folder_path = getattr(group.ref, "display_folder_path", group.ref.folder_path)
|
||||||
return str(dupe_folder_path).lower()
|
return str(dupe_folder_path).lower()
|
||||||
if key == "percentage":
|
if key == "percentage":
|
||||||
@@ -267,7 +266,7 @@ class DupeGuru(Broadcaster):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def _get_export_data(self):
|
def _get_export_data(self):
|
||||||
columns = [col for col in self.result_table.columns.ordered_columns if col.visible and col.name != "marked"]
|
columns = [col for col in self.result_table._columns.ordered_columns if col.visible and col.name != "marked"]
|
||||||
colnames = [col.display for col in columns]
|
colnames = [col.display for col in columns]
|
||||||
rows = []
|
rows = []
|
||||||
for group_id, group in enumerate(self.results.groups):
|
for group_id, group in enumerate(self.results.groups):
|
||||||
@@ -294,32 +293,36 @@ class DupeGuru(Broadcaster):
|
|||||||
self.view.show_message(msg)
|
self.view.show_message(msg)
|
||||||
|
|
||||||
def _job_completed(self, jobid):
|
def _job_completed(self, jobid):
|
||||||
if jobid == JobType.Scan:
|
if jobid == JobType.SCAN:
|
||||||
self._results_changed()
|
self._results_changed()
|
||||||
|
fs.filesdb.commit()
|
||||||
if not self.results.groups:
|
if not self.results.groups:
|
||||||
self.view.show_message(tr("No duplicates found."))
|
self.view.show_message(tr("No duplicates found."))
|
||||||
else:
|
else:
|
||||||
self.view.show_results_window()
|
self.view.show_results_window()
|
||||||
if jobid in {JobType.Move, JobType.Delete}:
|
if jobid in {JobType.MOVE, JobType.DELETE}:
|
||||||
self._results_changed()
|
self._results_changed()
|
||||||
if jobid == JobType.Load:
|
if jobid == JobType.LOAD:
|
||||||
self._recreate_result_table()
|
self._recreate_result_table()
|
||||||
self._results_changed()
|
self._results_changed()
|
||||||
self.view.show_results_window()
|
self.view.show_results_window()
|
||||||
if jobid in {JobType.Copy, JobType.Move, JobType.Delete}:
|
if jobid in {JobType.COPY, JobType.MOVE, JobType.DELETE}:
|
||||||
if self.results.problems:
|
if self.results.problems:
|
||||||
self.problem_dialog.refresh()
|
self.problem_dialog.refresh()
|
||||||
self.view.show_problem_dialog()
|
self.view.show_problem_dialog()
|
||||||
else:
|
else:
|
||||||
msg = {
|
if jobid == JobType.COPY:
|
||||||
JobType.Copy: tr("All marked files were copied successfully."),
|
msg = tr("All marked files were copied successfully.")
|
||||||
JobType.Move: tr("All marked files were moved successfully."),
|
elif jobid == JobType.MOVE:
|
||||||
JobType.Delete: tr("All marked files were successfully sent to Trash."),
|
msg = tr("All marked files were moved successfully.")
|
||||||
}[jobid]
|
elif jobid == JobType.DELETE and self.deletion_options.direct:
|
||||||
|
msg = tr("All marked files were deleted successfully.")
|
||||||
|
else:
|
||||||
|
msg = tr("All marked files were successfully sent to Trash.")
|
||||||
self.view.show_message(msg)
|
self.view.show_message(msg)
|
||||||
|
|
||||||
def _job_error(self, jobid, err):
|
def _job_error(self, jobid, err):
|
||||||
if jobid == JobType.Load:
|
if jobid == JobType.LOAD:
|
||||||
msg = tr("Could not load file: {}").format(err)
|
msg = tr("Could not load file: {}").format(err)
|
||||||
self.view.show_message(msg)
|
self.view.show_message(msg)
|
||||||
return False
|
return False
|
||||||
@@ -349,17 +352,17 @@ class DupeGuru(Broadcaster):
|
|||||||
|
|
||||||
# --- Protected
|
# --- Protected
|
||||||
def _get_fileclasses(self):
|
def _get_fileclasses(self):
|
||||||
if self.app_mode == AppMode.Picture:
|
if self.app_mode == AppMode.PICTURE:
|
||||||
return [pe.photo.PLAT_SPECIFIC_PHOTO_CLASS]
|
return [pe.photo.PLAT_SPECIFIC_PHOTO_CLASS]
|
||||||
elif self.app_mode == AppMode.Music:
|
elif self.app_mode == AppMode.MUSIC:
|
||||||
return [me.fs.MusicFile]
|
return [me.fs.MusicFile]
|
||||||
else:
|
else:
|
||||||
return [se.fs.File]
|
return [se.fs.File]
|
||||||
|
|
||||||
def _prioritization_categories(self):
|
def _prioritization_categories(self):
|
||||||
if self.app_mode == AppMode.Picture:
|
if self.app_mode == AppMode.PICTURE:
|
||||||
return pe.prioritize.all_categories()
|
return pe.prioritize.all_categories()
|
||||||
elif self.app_mode == AppMode.Music:
|
elif self.app_mode == AppMode.MUSIC:
|
||||||
return me.prioritize.all_categories()
|
return me.prioritize.all_categories()
|
||||||
else:
|
else:
|
||||||
return prioritize.all_categories()
|
return prioritize.all_categories()
|
||||||
@@ -393,20 +396,20 @@ class DupeGuru(Broadcaster):
|
|||||||
g = self.results.get_group_of_duplicate(dupe)
|
g = self.results.get_group_of_duplicate(dupe)
|
||||||
for other in g:
|
for other in g:
|
||||||
if other is not dupe:
|
if other is not dupe:
|
||||||
self.ignore_list.Ignore(str(other.path), str(dupe.path))
|
self.ignore_list.ignore(str(other.path), str(dupe.path))
|
||||||
self.remove_duplicates(dupes)
|
self.remove_duplicates(dupes)
|
||||||
self.ignore_list_dialog.refresh()
|
self.ignore_list_dialog.refresh()
|
||||||
|
|
||||||
def apply_filter(self, filter):
|
def apply_filter(self, result_filter):
|
||||||
"""Apply a filter ``filter`` to the results so that it shows only dupe groups that match it.
|
"""Apply a filter ``filter`` to the results so that it shows only dupe groups that match it.
|
||||||
|
|
||||||
:param str filter: filter to apply
|
:param str filter: filter to apply
|
||||||
"""
|
"""
|
||||||
self.results.apply_filter(None)
|
self.results.apply_filter(None)
|
||||||
if self.options["escape_filter_regexp"]:
|
if self.options["escape_filter_regexp"]:
|
||||||
filter = escape(filter, set("()[]\\.|+?^"))
|
result_filter = escape(result_filter, set("()[]\\.|+?^"))
|
||||||
filter = escape(filter, "*", ".")
|
result_filter = escape(result_filter, "*", ".")
|
||||||
self.results.apply_filter(filter)
|
self.results.apply_filter(result_filter)
|
||||||
self._results_changed()
|
self._results_changed()
|
||||||
|
|
||||||
def clean_empty_dirs(self, path):
|
def clean_empty_dirs(self, path):
|
||||||
@@ -420,14 +423,17 @@ class DupeGuru(Broadcaster):
|
|||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
pass # we don't care
|
pass # we don't care
|
||||||
|
|
||||||
|
def clear_hash_cache(self):
|
||||||
|
fs.filesdb.clear()
|
||||||
|
|
||||||
def copy_or_move(self, dupe, copy: bool, destination: str, dest_type: DestType):
|
def copy_or_move(self, dupe, copy: bool, destination: str, dest_type: DestType):
|
||||||
source_path = dupe.path
|
source_path = dupe.path
|
||||||
location_path = first(p for p in self.directories if dupe.path in p)
|
location_path = first(p for p in self.directories if dupe.path in p)
|
||||||
dest_path = Path(destination)
|
dest_path = Path(destination)
|
||||||
if dest_type in {DestType.Relative, DestType.Absolute}:
|
if dest_type in {DestType.RELATIVE, DestType.ABSOLUTE}:
|
||||||
# no filename, no windows drive letter
|
# no filename, no windows drive letter
|
||||||
source_base = source_path.remove_drive_letter().parent()
|
source_base = source_path.remove_drive_letter().parent()
|
||||||
if dest_type == DestType.Relative:
|
if dest_type == DestType.RELATIVE:
|
||||||
source_base = source_base[location_path:]
|
source_base = source_base[location_path:]
|
||||||
dest_path = dest_path[source_base]
|
dest_path = dest_path[source_base]
|
||||||
if not dest_path.exists():
|
if not dest_path.exists():
|
||||||
@@ -466,7 +472,7 @@ class DupeGuru(Broadcaster):
|
|||||||
)
|
)
|
||||||
if destination:
|
if destination:
|
||||||
desttype = self.options["copymove_dest_type"]
|
desttype = self.options["copymove_dest_type"]
|
||||||
jobid = JobType.Copy if copy else JobType.Move
|
jobid = JobType.COPY if copy else JobType.MOVE
|
||||||
self._start_job(jobid, do)
|
self._start_job(jobid, do)
|
||||||
|
|
||||||
def delete_marked(self):
|
def delete_marked(self):
|
||||||
@@ -482,7 +488,7 @@ class DupeGuru(Broadcaster):
|
|||||||
self.deletion_options.direct,
|
self.deletion_options.direct,
|
||||||
]
|
]
|
||||||
logging.debug("Starting deletion job with args %r", args)
|
logging.debug("Starting deletion job with args %r", args)
|
||||||
self._start_job(JobType.Delete, self._do_delete, args=args)
|
self._start_job(JobType.DELETE, self._do_delete, args=args)
|
||||||
|
|
||||||
def export_to_xhtml(self):
|
def export_to_xhtml(self):
|
||||||
"""Export current results to XHTML.
|
"""Export current results to XHTML.
|
||||||
@@ -535,12 +541,12 @@ class DupeGuru(Broadcaster):
|
|||||||
return
|
return
|
||||||
if not self.selected_dupes:
|
if not self.selected_dupes:
|
||||||
return
|
return
|
||||||
dupe = self.selected_dupes[0]
|
dupes = self.selected_dupes
|
||||||
group = self.results.get_group_of_duplicate(dupe)
|
refs = [self.results.get_group_of_duplicate(dupe).ref for dupe in dupes]
|
||||||
ref = group.ref
|
for dupe, ref in zip(dupes, refs):
|
||||||
cmd = cmd.replace("%d", str(dupe.path))
|
dupe_cmd = cmd.replace("%d", str(dupe.path))
|
||||||
cmd = cmd.replace("%r", str(ref.path))
|
dupe_cmd = dupe_cmd.replace("%r", str(ref.path))
|
||||||
match = re.match(r'"([^"]+)"(.*)', cmd)
|
match = re.match(r'"([^"]+)"(.*)', dupe_cmd)
|
||||||
if match is not None:
|
if match is not None:
|
||||||
# This code here is because subprocess. Popen doesn't seem to accept, under Windows,
|
# This code here is because subprocess. Popen doesn't seem to accept, under Windows,
|
||||||
# executable paths with spaces in it, *even* when they're enclosed in "". So this is
|
# executable paths with spaces in it, *even* when they're enclosed in "". So this is
|
||||||
@@ -549,7 +555,7 @@ class DupeGuru(Broadcaster):
|
|||||||
path, exename = op.split(exepath)
|
path, exename = op.split(exepath)
|
||||||
subprocess.Popen(exename + args, shell=True, cwd=path)
|
subprocess.Popen(exename + args, shell=True, cwd=path)
|
||||||
else:
|
else:
|
||||||
subprocess.Popen(cmd, shell=True)
|
subprocess.Popen(dupe_cmd, shell=True)
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
"""Load directory selection and ignore list from files in appdata.
|
"""Load directory selection and ignore list from files in appdata.
|
||||||
@@ -582,7 +588,7 @@ class DupeGuru(Broadcaster):
|
|||||||
def do(j):
|
def do(j):
|
||||||
self.results.load_from_xml(filename, self._get_file, j)
|
self.results.load_from_xml(filename, self._get_file, j)
|
||||||
|
|
||||||
self._start_job(JobType.Load, do)
|
self._start_job(JobType.LOAD, do)
|
||||||
|
|
||||||
def make_selected_reference(self):
|
def make_selected_reference(self):
|
||||||
"""Promote :attr:`selected_dupes` to reference position within their respective groups.
|
"""Promote :attr:`selected_dupes` to reference position within their respective groups.
|
||||||
@@ -595,8 +601,7 @@ class DupeGuru(Broadcaster):
|
|||||||
changed_groups = set()
|
changed_groups = set()
|
||||||
for dupe in dupes:
|
for dupe in dupes:
|
||||||
g = self.results.get_group_of_duplicate(dupe)
|
g = self.results.get_group_of_duplicate(dupe)
|
||||||
if g not in changed_groups:
|
if g not in changed_groups and self.results.make_ref(dupe):
|
||||||
if self.results.make_ref(dupe):
|
|
||||||
changed_groups.add(g)
|
changed_groups.add(g)
|
||||||
# It's not always obvious to users what this action does, so to make it a bit clearer,
|
# It's not always obvious to users what this action does, so to make it a bit clearer,
|
||||||
# we change our selection to the ref of all changed groups. However, we also want to keep
|
# we change our selection to the ref of all changed groups. However, we also want to keep
|
||||||
@@ -647,15 +652,14 @@ class DupeGuru(Broadcaster):
|
|||||||
|
|
||||||
def open_selected(self):
|
def open_selected(self):
|
||||||
"""Open :attr:`selected_dupes` with their associated application."""
|
"""Open :attr:`selected_dupes` with their associated application."""
|
||||||
if len(self.selected_dupes) > 10:
|
if len(self.selected_dupes) > 10 and not self.view.ask_yes_no(MSG_MANY_FILES_TO_OPEN):
|
||||||
if not self.view.ask_yes_no(MSG_MANY_FILES_TO_OPEN):
|
|
||||||
return
|
return
|
||||||
for dupe in self.selected_dupes:
|
for dupe in self.selected_dupes:
|
||||||
desktop.open_path(dupe.path)
|
desktop.open_path(dupe.path)
|
||||||
|
|
||||||
def purge_ignore_list(self):
|
def purge_ignore_list(self):
|
||||||
"""Remove files that don't exist from :attr:`ignore_list`."""
|
"""Remove files that don't exist from :attr:`ignore_list`."""
|
||||||
self.ignore_list.Filter(lambda f, s: op.exists(f) and op.exists(s))
|
self.ignore_list.filter(lambda f, s: op.exists(f) and op.exists(s))
|
||||||
self.ignore_list_dialog.refresh()
|
self.ignore_list_dialog.refresh()
|
||||||
|
|
||||||
def remove_directories(self, indexes):
|
def remove_directories(self, indexes):
|
||||||
@@ -753,6 +757,9 @@ class DupeGuru(Broadcaster):
|
|||||||
self.exclude_list.save_to_xml(p)
|
self.exclude_list.save_to_xml(p)
|
||||||
self.notify("save_session")
|
self.notify("save_session")
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
fs.filesdb.close()
|
||||||
|
|
||||||
def save_as(self, filename):
|
def save_as(self, filename):
|
||||||
"""Save results in ``filename``.
|
"""Save results in ``filename``.
|
||||||
|
|
||||||
@@ -786,7 +793,7 @@ class DupeGuru(Broadcaster):
|
|||||||
for k, v in self.options.items():
|
for k, v in self.options.items():
|
||||||
if hasattr(scanner, k):
|
if hasattr(scanner, k):
|
||||||
setattr(scanner, k, v)
|
setattr(scanner, k, v)
|
||||||
if self.app_mode == AppMode.Picture:
|
if self.app_mode == AppMode.PICTURE:
|
||||||
scanner.cache_path = self._get_picture_cache_path()
|
scanner.cache_path = self._get_picture_cache_path()
|
||||||
self.results.groups = []
|
self.results.groups = []
|
||||||
self._recreate_result_table()
|
self._recreate_result_table()
|
||||||
@@ -794,7 +801,7 @@ class DupeGuru(Broadcaster):
|
|||||||
|
|
||||||
def do(j):
|
def do(j):
|
||||||
j.set_progress(0, tr("Collecting files to scan"))
|
j.set_progress(0, tr("Collecting files to scan"))
|
||||||
if scanner.scan_type == ScanType.Folders:
|
if scanner.scan_type == ScanType.FOLDERS:
|
||||||
files = list(self.directories.get_folders(folderclass=se.fs.Folder, j=j))
|
files = list(self.directories.get_folders(folderclass=se.fs.Folder, j=j))
|
||||||
else:
|
else:
|
||||||
files = list(self.directories.get_files(fileclasses=self.fileclasses, j=j))
|
files = list(self.directories.get_files(fileclasses=self.fileclasses, j=j))
|
||||||
@@ -804,7 +811,7 @@ class DupeGuru(Broadcaster):
|
|||||||
self.results.groups = scanner.get_dupe_groups(files, self.ignore_list, j)
|
self.results.groups = scanner.get_dupe_groups(files, self.ignore_list, j)
|
||||||
self.discarded_file_count = scanner.discarded_file_count
|
self.discarded_file_count = scanner.discarded_file_count
|
||||||
|
|
||||||
self._start_job(JobType.Scan, do)
|
self._start_job(JobType.SCAN, do)
|
||||||
|
|
||||||
def toggle_selected_mark_state(self):
|
def toggle_selected_mark_state(self):
|
||||||
selected = self.without_ref(self.selected_dupes)
|
selected = self.without_ref(self.selected_dupes)
|
||||||
@@ -849,18 +856,18 @@ class DupeGuru(Broadcaster):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def SCANNER_CLASS(self):
|
def SCANNER_CLASS(self):
|
||||||
if self.app_mode == AppMode.Picture:
|
if self.app_mode == AppMode.PICTURE:
|
||||||
return pe.scanner.ScannerPE
|
return pe.scanner.ScannerPE
|
||||||
elif self.app_mode == AppMode.Music:
|
elif self.app_mode == AppMode.MUSIC:
|
||||||
return me.scanner.ScannerME
|
return me.scanner.ScannerME
|
||||||
else:
|
else:
|
||||||
return se.scanner.ScannerSE
|
return se.scanner.ScannerSE
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def METADATA_TO_READ(self):
|
def METADATA_TO_READ(self):
|
||||||
if self.app_mode == AppMode.Picture:
|
if self.app_mode == AppMode.PICTURE:
|
||||||
return ["size", "mtime", "dimensions", "exif_timestamp"]
|
return ["size", "mtime", "dimensions", "exif_timestamp"]
|
||||||
elif self.app_mode == AppMode.Music:
|
elif self.app_mode == AppMode.MUSIC:
|
||||||
return [
|
return [
|
||||||
"size",
|
"size",
|
||||||
"mtime",
|
"mtime",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import logging
|
|||||||
from hscommon.jobprogress import job
|
from hscommon.jobprogress import job
|
||||||
from hscommon.path import Path
|
from hscommon.path import Path
|
||||||
from hscommon.util import FileOrPath
|
from hscommon.util import FileOrPath
|
||||||
|
from hscommon.trans import tr
|
||||||
|
|
||||||
from . import fs
|
from . import fs
|
||||||
|
|
||||||
@@ -30,9 +31,9 @@ class DirectoryState:
|
|||||||
* DirectoryState.Excluded: Don't scan this folder
|
* DirectoryState.Excluded: Don't scan this folder
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Normal = 0
|
NORMAL = 0
|
||||||
Reference = 1
|
REFERENCE = 1
|
||||||
Excluded = 2
|
EXCLUDED = 2
|
||||||
|
|
||||||
|
|
||||||
class AlreadyThereError(Exception):
|
class AlreadyThereError(Exception):
|
||||||
@@ -82,50 +83,49 @@ class Directories:
|
|||||||
# We iterate even if we only have one item here
|
# We iterate even if we only have one item here
|
||||||
for denied_path_re in self._exclude_list.compiled:
|
for denied_path_re in self._exclude_list.compiled:
|
||||||
if denied_path_re.match(str(path.name)):
|
if denied_path_re.match(str(path.name)):
|
||||||
return DirectoryState.Excluded
|
return DirectoryState.EXCLUDED
|
||||||
# return # We still use the old logic to force state on hidden dirs
|
# return # We still use the old logic to force state on hidden dirs
|
||||||
# Override this in subclasses to specify the state of some special folders.
|
# Override this in subclasses to specify the state of some special folders.
|
||||||
if path.name.startswith("."):
|
if path.name.startswith("."):
|
||||||
return DirectoryState.Excluded
|
return DirectoryState.EXCLUDED
|
||||||
|
|
||||||
def _get_files(self, from_path, fileclasses, j):
|
def _get_files(self, from_path, fileclasses, j):
|
||||||
for root, dirs, files in os.walk(str(from_path)):
|
for root, dirs, files in os.walk(str(from_path)):
|
||||||
j.check_if_cancelled()
|
j.check_if_cancelled()
|
||||||
rootPath = Path(root)
|
root_path = Path(root)
|
||||||
state = self.get_state(rootPath)
|
state = self.get_state(root_path)
|
||||||
if state == DirectoryState.Excluded:
|
if state == DirectoryState.EXCLUDED and not any(p[: len(root_path)] == root_path for p in self.states):
|
||||||
# Recursively get files from folders with lots of subfolder is expensive. However, there
|
# Recursively get files from folders with lots of subfolder is expensive. However, there
|
||||||
# might be a subfolder in this path that is not excluded. What we want to do is to skim
|
# might be a subfolder in this path that is not excluded. What we want to do is to skim
|
||||||
# through self.states and see if we must continue, or we can stop right here to save time
|
# through self.states and see if we must continue, or we can stop right here to save time
|
||||||
if not any(p[: len(rootPath)] == rootPath for p in self.states):
|
|
||||||
del dirs[:]
|
del dirs[:]
|
||||||
try:
|
try:
|
||||||
if state != DirectoryState.Excluded:
|
if state != DirectoryState.EXCLUDED:
|
||||||
# Old logic
|
# Old logic
|
||||||
if self._exclude_list is None or not self._exclude_list.mark_count:
|
if self._exclude_list is None or not self._exclude_list.mark_count:
|
||||||
found_files = [fs.get_file(rootPath + f, fileclasses=fileclasses) for f in files]
|
found_files = [fs.get_file(root_path + f, fileclasses=fileclasses) for f in files]
|
||||||
else:
|
else:
|
||||||
found_files = []
|
found_files = []
|
||||||
# print(f"len of files: {len(files)} {files}")
|
# print(f"len of files: {len(files)} {files}")
|
||||||
for f in files:
|
for f in files:
|
||||||
if not self._exclude_list.is_excluded(root, f):
|
if not self._exclude_list.is_excluded(root, f):
|
||||||
found_files.append(fs.get_file(rootPath + f, fileclasses=fileclasses))
|
found_files.append(fs.get_file(root_path + f, fileclasses=fileclasses))
|
||||||
found_files = [f for f in found_files if f is not None]
|
found_files = [f for f in found_files if f is not None]
|
||||||
# In some cases, directories can be considered as files by dupeGuru, which is
|
# In some cases, directories can be considered as files by dupeGuru, which is
|
||||||
# why we have this line below. In fact, there only one case: Bundle files under
|
# why we have this line below. In fact, there only one case: Bundle files under
|
||||||
# OS X... In other situations, this forloop will do nothing.
|
# OS X... In other situations, this forloop will do nothing.
|
||||||
for d in dirs[:]:
|
for d in dirs[:]:
|
||||||
f = fs.get_file(rootPath + d, fileclasses=fileclasses)
|
f = fs.get_file(root_path + d, fileclasses=fileclasses)
|
||||||
if f is not None:
|
if f is not None:
|
||||||
found_files.append(f)
|
found_files.append(f)
|
||||||
dirs.remove(d)
|
dirs.remove(d)
|
||||||
logging.debug(
|
logging.debug(
|
||||||
"Collected %d files in folder %s",
|
"Collected %d files in folder %s",
|
||||||
len(found_files),
|
len(found_files),
|
||||||
str(rootPath),
|
str(root_path),
|
||||||
)
|
)
|
||||||
for file in found_files:
|
for file in found_files:
|
||||||
file.is_ref = state == DirectoryState.Reference
|
file.is_ref = state == DirectoryState.REFERENCE
|
||||||
yield file
|
yield file
|
||||||
except (EnvironmentError, fs.InvalidPath):
|
except (EnvironmentError, fs.InvalidPath):
|
||||||
pass
|
pass
|
||||||
@@ -137,8 +137,8 @@ class Directories:
|
|||||||
for folder in self._get_folders(subfolder, j):
|
for folder in self._get_folders(subfolder, j):
|
||||||
yield folder
|
yield folder
|
||||||
state = self.get_state(from_folder.path)
|
state = self.get_state(from_folder.path)
|
||||||
if state != DirectoryState.Excluded:
|
if state != DirectoryState.EXCLUDED:
|
||||||
from_folder.is_ref = state == DirectoryState.Reference
|
from_folder.is_ref = state == DirectoryState.REFERENCE
|
||||||
logging.debug("Yielding Folder %r state: %d", from_folder, state)
|
logging.debug("Yielding Folder %r state: %d", from_folder, state)
|
||||||
yield from_folder
|
yield from_folder
|
||||||
except (EnvironmentError, fs.InvalidPath):
|
except (EnvironmentError, fs.InvalidPath):
|
||||||
@@ -183,8 +183,12 @@ class Directories:
|
|||||||
"""
|
"""
|
||||||
if fileclasses is None:
|
if fileclasses is None:
|
||||||
fileclasses = [fs.File]
|
fileclasses = [fs.File]
|
||||||
|
file_count = 0
|
||||||
for path in self._dirs:
|
for path in self._dirs:
|
||||||
for file in self._get_files(path, fileclasses=fileclasses, j=j):
|
for file in self._get_files(path, fileclasses=fileclasses, j=j):
|
||||||
|
file_count += 1
|
||||||
|
if type(j) != job.NullJob:
|
||||||
|
j.set_progress(-1, tr("Collected {} files to scan").format(file_count))
|
||||||
yield file
|
yield file
|
||||||
|
|
||||||
def get_folders(self, folderclass=None, j=job.nulljob):
|
def get_folders(self, folderclass=None, j=job.nulljob):
|
||||||
@@ -194,9 +198,13 @@ class Directories:
|
|||||||
"""
|
"""
|
||||||
if folderclass is None:
|
if folderclass is None:
|
||||||
folderclass = fs.Folder
|
folderclass = fs.Folder
|
||||||
|
folder_count = 0
|
||||||
for path in self._dirs:
|
for path in self._dirs:
|
||||||
from_folder = folderclass(path)
|
from_folder = folderclass(path)
|
||||||
for folder in self._get_folders(from_folder, j):
|
for folder in self._get_folders(from_folder, j):
|
||||||
|
folder_count += 1
|
||||||
|
if type(j) != job.NullJob:
|
||||||
|
j.set_progress(-1, tr("Collected {} folders to scan").format(folder_count))
|
||||||
yield folder
|
yield folder
|
||||||
|
|
||||||
def get_state(self, path):
|
def get_state(self, path):
|
||||||
@@ -207,9 +215,9 @@ class Directories:
|
|||||||
# direct match? easy result.
|
# direct match? easy result.
|
||||||
if path in self.states:
|
if path in self.states:
|
||||||
return self.states[path]
|
return self.states[path]
|
||||||
state = self._default_state_for_path(path) or DirectoryState.Normal
|
state = self._default_state_for_path(path) or DirectoryState.NORMAL
|
||||||
# Save non-default states in cache, necessary for _get_files()
|
# Save non-default states in cache, necessary for _get_files()
|
||||||
if state != DirectoryState.Normal:
|
if state != DirectoryState.NORMAL:
|
||||||
self.states[path] = state
|
self.states[path] = state
|
||||||
return state
|
return state
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from hscommon.jobprogress import job
|
|||||||
) = range(3)
|
) = range(3)
|
||||||
|
|
||||||
JOB_REFRESH_RATE = 100
|
JOB_REFRESH_RATE = 100
|
||||||
|
PROGRESS_MESSAGE = tr("%d matches found from %d groups")
|
||||||
|
|
||||||
|
|
||||||
def getwords(s):
|
def getwords(s):
|
||||||
@@ -106,14 +107,14 @@ def compare_fields(first, second, flags=()):
|
|||||||
# We don't want to remove field directly in the list. We must work on a copy.
|
# We don't want to remove field directly in the list. We must work on a copy.
|
||||||
second = second[:]
|
second = second[:]
|
||||||
for field1 in first:
|
for field1 in first:
|
||||||
max = 0
|
max_score = 0
|
||||||
matched_field = None
|
matched_field = None
|
||||||
for field2 in second:
|
for field2 in second:
|
||||||
r = compare(field1, field2, flags)
|
r = compare(field1, field2, flags)
|
||||||
if r > max:
|
if r > max_score:
|
||||||
max = r
|
max_score = r
|
||||||
matched_field = field2
|
matched_field = field2
|
||||||
results.append(max)
|
results.append(max_score)
|
||||||
if matched_field:
|
if matched_field:
|
||||||
second.remove(matched_field)
|
second.remove(matched_field)
|
||||||
else:
|
else:
|
||||||
@@ -248,10 +249,11 @@ def getmatches(
|
|||||||
match_flags.append(MATCH_SIMILAR_WORDS)
|
match_flags.append(MATCH_SIMILAR_WORDS)
|
||||||
if no_field_order:
|
if no_field_order:
|
||||||
match_flags.append(NO_FIELD_ORDER)
|
match_flags.append(NO_FIELD_ORDER)
|
||||||
j.start_job(len(word_dict), tr("0 matches found"))
|
j.start_job(len(word_dict), PROGRESS_MESSAGE % (0, 0))
|
||||||
compared = defaultdict(set)
|
compared = defaultdict(set)
|
||||||
result = []
|
result = []
|
||||||
try:
|
try:
|
||||||
|
word_count = 0
|
||||||
# This whole 'popping' thing is there to avoid taking too much memory at the same time.
|
# This whole 'popping' thing is there to avoid taking too much memory at the same time.
|
||||||
while word_dict:
|
while word_dict:
|
||||||
items = word_dict.popitem()[1]
|
items = word_dict.popitem()[1]
|
||||||
@@ -266,7 +268,8 @@ def getmatches(
|
|||||||
result.append(m)
|
result.append(m)
|
||||||
if len(result) >= LIMIT:
|
if len(result) >= LIMIT:
|
||||||
return result
|
return result
|
||||||
j.add_progress(desc=tr("%d matches found") % len(result))
|
word_count += 1
|
||||||
|
j.add_progress(desc=PROGRESS_MESSAGE % (len(result), word_count))
|
||||||
except MemoryError:
|
except MemoryError:
|
||||||
# This is the place where the memory usage is at its peak during the scan.
|
# This is the place where the memory usage is at its peak during the scan.
|
||||||
# Just continue the process with an incomplete list of matches.
|
# Just continue the process with an incomplete list of matches.
|
||||||
@@ -285,17 +288,21 @@ def getmatches_by_contents(files, bigsize=0, j=job.nulljob):
|
|||||||
"""
|
"""
|
||||||
size2files = defaultdict(set)
|
size2files = defaultdict(set)
|
||||||
for f in files:
|
for f in files:
|
||||||
if f.size:
|
|
||||||
size2files[f.size].add(f)
|
size2files[f.size].add(f)
|
||||||
del files
|
del files
|
||||||
possible_matches = [files for files in size2files.values() if len(files) > 1]
|
possible_matches = [files for files in size2files.values() if len(files) > 1]
|
||||||
del size2files
|
del size2files
|
||||||
result = []
|
result = []
|
||||||
j.start_job(len(possible_matches), tr("0 matches found"))
|
j.start_job(len(possible_matches), PROGRESS_MESSAGE % (0, 0))
|
||||||
|
group_count = 0
|
||||||
for group in possible_matches:
|
for group in possible_matches:
|
||||||
for first, second in itertools.combinations(group, 2):
|
for first, second in itertools.combinations(group, 2):
|
||||||
if first.is_ref and second.is_ref:
|
if first.is_ref and second.is_ref:
|
||||||
continue # Don't spend time comparing two ref pics together.
|
continue # Don't spend time comparing two ref pics together.
|
||||||
|
if first.size == 0 and second.size == 0:
|
||||||
|
# skip md5 for zero length files
|
||||||
|
result.append(Match(first, second, 100))
|
||||||
|
continue
|
||||||
if first.md5partial == second.md5partial:
|
if first.md5partial == second.md5partial:
|
||||||
if bigsize > 0 and first.size > bigsize:
|
if bigsize > 0 and first.size > bigsize:
|
||||||
if first.md5samples == second.md5samples:
|
if first.md5samples == second.md5samples:
|
||||||
@@ -303,7 +310,8 @@ def getmatches_by_contents(files, bigsize=0, j=job.nulljob):
|
|||||||
else:
|
else:
|
||||||
if first.md5 == second.md5:
|
if first.md5 == second.md5:
|
||||||
result.append(Match(first, second, 100))
|
result.append(Match(first, second, 100))
|
||||||
j.add_progress(desc=tr("%d matches found") % len(result))
|
group_count += 1
|
||||||
|
j.add_progress(desc=PROGRESS_MESSAGE % (len(result), group_count))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -150,10 +150,7 @@ class ExcludeList(Markable):
|
|||||||
# @timer
|
# @timer
|
||||||
@memoize
|
@memoize
|
||||||
def _do_compile(self, expr):
|
def _do_compile(self, expr):
|
||||||
try:
|
|
||||||
return re.compile(expr)
|
return re.compile(expr)
|
||||||
except Exception as e:
|
|
||||||
raise (e)
|
|
||||||
|
|
||||||
# @timer
|
# @timer
|
||||||
# @memoize # probably not worth memoizing this one if we memoize the above
|
# @memoize # probably not worth memoizing this one if we memoize the above
|
||||||
@@ -235,7 +232,7 @@ class ExcludeList(Markable):
|
|||||||
# This exception should never be ignored
|
# This exception should never be ignored
|
||||||
raise AlreadyThereException()
|
raise AlreadyThereException()
|
||||||
if regex in forbidden_regexes:
|
if regex in forbidden_regexes:
|
||||||
raise Exception("Forbidden (dangerous) expression.")
|
raise ValueError("Forbidden (dangerous) expression.")
|
||||||
|
|
||||||
iscompilable, exception, compiled = self.compile_re(regex)
|
iscompilable, exception, compiled = self.compile_re(regex)
|
||||||
if not iscompilable and not forced:
|
if not iscompilable and not forced:
|
||||||
@@ -510,7 +507,6 @@ if ISWINDOWS:
|
|||||||
def has_sep(regexp):
|
def has_sep(regexp):
|
||||||
return "\\" + sep in regexp
|
return "\\" + sep in regexp
|
||||||
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
def has_sep(regexp):
|
def has_sep(regexp):
|
||||||
|
|||||||
139
core/fs.py
139
core/fs.py
@@ -14,7 +14,11 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
from math import floor
|
from math import floor
|
||||||
import logging
|
import logging
|
||||||
|
import sqlite3
|
||||||
|
from threading import Lock
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from hscommon.path import Path
|
||||||
from hscommon.util import nonone, get_file_ext
|
from hscommon.util import nonone, get_file_ext
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -78,6 +82,82 @@ class OperationError(FSError):
|
|||||||
cls_message = "Operation on '{name}' failed."
|
cls_message = "Operation on '{name}' failed."
|
||||||
|
|
||||||
|
|
||||||
|
class FilesDB:
|
||||||
|
|
||||||
|
create_table_query = "CREATE TABLE IF NOT EXISTS files (path TEXT PRIMARY KEY, size INTEGER, mtime_ns INTEGER, entry_dt DATETIME, md5 BLOB, md5partial BLOB)"
|
||||||
|
drop_table_query = "DROP TABLE files;"
|
||||||
|
select_query = "SELECT {key} FROM files WHERE path=:path AND size=:size and mtime_ns=:mtime_ns"
|
||||||
|
insert_query = """
|
||||||
|
INSERT INTO files (path, size, mtime_ns, entry_dt, {key}) VALUES (:path, :size, :mtime_ns, datetime('now'), :value)
|
||||||
|
ON CONFLICT(path) DO UPDATE SET size=:size, mtime_ns=:mtime_ns, entry_dt=datetime('now'), {key}=:value;
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.conn = None
|
||||||
|
self.cur = None
|
||||||
|
self.lock = None
|
||||||
|
|
||||||
|
def connect(self, path):
|
||||||
|
# type: (str, ) -> None
|
||||||
|
|
||||||
|
self.conn = sqlite3.connect(path, check_same_thread=False)
|
||||||
|
self.cur = self.conn.cursor()
|
||||||
|
self.cur.execute(self.create_table_query)
|
||||||
|
self.lock = Lock()
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
# type: () -> None
|
||||||
|
|
||||||
|
with self.lock:
|
||||||
|
self.cur.execute(self.drop_table_query)
|
||||||
|
self.cur.execute(self.create_table_query)
|
||||||
|
|
||||||
|
def get(self, path, key):
|
||||||
|
# type: (Path, str) -> bytes
|
||||||
|
|
||||||
|
stat = path.stat()
|
||||||
|
size = stat.st_size
|
||||||
|
mtime_ns = stat.st_mtime_ns
|
||||||
|
|
||||||
|
with self.lock:
|
||||||
|
self.cur.execute(self.select_query.format(key=key), {"path": str(path), "size": size, "mtime_ns": mtime_ns})
|
||||||
|
result = self.cur.fetchone()
|
||||||
|
|
||||||
|
if result:
|
||||||
|
return result[0]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def put(self, path, key, value):
|
||||||
|
# type: (Path, str, Any) -> None
|
||||||
|
|
||||||
|
stat = path.stat()
|
||||||
|
size = stat.st_size
|
||||||
|
mtime_ns = stat.st_mtime_ns
|
||||||
|
|
||||||
|
with self.lock:
|
||||||
|
self.cur.execute(
|
||||||
|
self.insert_query.format(key=key),
|
||||||
|
{"path": str(path), "size": size, "mtime_ns": mtime_ns, "value": value},
|
||||||
|
)
|
||||||
|
|
||||||
|
def commit(self):
|
||||||
|
# type: () -> None
|
||||||
|
|
||||||
|
with self.lock:
|
||||||
|
self.conn.commit()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
# type: () -> None
|
||||||
|
|
||||||
|
with self.lock:
|
||||||
|
self.cur.close()
|
||||||
|
self.conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
filesdb = FilesDB() # Singleton
|
||||||
|
|
||||||
|
|
||||||
class File:
|
class File:
|
||||||
"""Represents a file and holds metadata to be used for scanning."""
|
"""Represents a file and holds metadata to be used for scanning."""
|
||||||
|
|
||||||
@@ -107,10 +187,32 @@ class File:
|
|||||||
result = self.INITIAL_INFO[attrname]
|
result = self.INITIAL_INFO[attrname]
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
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
|
# 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
|
# For audio file, it should be where audio data starts
|
||||||
def _get_md5partial_offset_and_size(self):
|
offset, size = (0x4000, 0x4000)
|
||||||
return (0x4000, 0x4000) # 16Kb
|
|
||||||
|
with self.path.open("rb") as fp:
|
||||||
|
fp.seek(offset)
|
||||||
|
partialdata = fp.read(size)
|
||||||
|
return hashlib.md5(partialdata).digest()
|
||||||
|
|
||||||
def _read_info(self, field):
|
def _read_info(self, field):
|
||||||
# print(f"_read_info({field}) for {self}")
|
# print(f"_read_info({field}) for {self}")
|
||||||
@@ -120,28 +222,20 @@ class File:
|
|||||||
self.mtime = nonone(stats.st_mtime, 0)
|
self.mtime = nonone(stats.st_mtime, 0)
|
||||||
elif field == "md5partial":
|
elif field == "md5partial":
|
||||||
try:
|
try:
|
||||||
with self.path.open("rb") as fp:
|
self.md5partial = filesdb.get(self.path, "md5partial")
|
||||||
offset, size = self._get_md5partial_offset_and_size()
|
if self.md5partial is None:
|
||||||
fp.seek(offset)
|
self.md5partial = self._calc_md5partial()
|
||||||
partialdata = fp.read(size)
|
filesdb.put(self.path, "md5partial", self.md5partial)
|
||||||
md5 = hashlib.md5(partialdata)
|
except Exception as e:
|
||||||
self.md5partial = md5.digest()
|
logging.warning("Couldn't get md5partial for %s: %s", self.path, e)
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
elif field == "md5":
|
elif field == "md5":
|
||||||
try:
|
try:
|
||||||
with self.path.open("rb") as fp:
|
self.md5 = filesdb.get(self.path, "md5")
|
||||||
md5 = hashlib.md5()
|
if self.md5 is None:
|
||||||
filedata = fp.read(CHUNK_SIZE)
|
self.md5 = self._calc_md5()
|
||||||
while filedata:
|
filesdb.put(self.path, "md5", self.md5)
|
||||||
md5.update(filedata)
|
except Exception as e:
|
||||||
filedata = fp.read(CHUNK_SIZE)
|
logging.warning("Couldn't get md5 for %s: %s", self.path, e)
|
||||||
# FIXME For python 3.8 and later
|
|
||||||
# while filedata := fp.read(CHUNK_SIZE):
|
|
||||||
# md5.update(filedata)
|
|
||||||
self.md5 = md5.digest()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
elif field == "md5samples":
|
elif field == "md5samples":
|
||||||
try:
|
try:
|
||||||
with self.path.open("rb") as fp:
|
with self.path.open("rb") as fp:
|
||||||
@@ -168,7 +262,6 @@ class File:
|
|||||||
setattr(self, field, md5.digest())
|
setattr(self, field, md5.digest())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error computing md5samples: {e}")
|
logging.error(f"Error computing md5samples: {e}")
|
||||||
pass
|
|
||||||
|
|
||||||
def _read_all_info(self, attrnames=None):
|
def _read_all_info(self, attrnames=None):
|
||||||
"""Cache all possible info.
|
"""Cache all possible info.
|
||||||
|
|||||||
@@ -15,16 +15,21 @@ class DupeGuruGUIObject(Listener):
|
|||||||
self.app = app
|
self.app = app
|
||||||
|
|
||||||
def directories_changed(self):
|
def directories_changed(self):
|
||||||
|
# Implemented in child classes
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def dupes_selected(self):
|
def dupes_selected(self):
|
||||||
|
# Implemented in child classes
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def marking_changed(self):
|
def marking_changed(self):
|
||||||
|
# Implemented in child classes
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def results_changed(self):
|
def results_changed(self):
|
||||||
|
# Implemented in child classes
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def results_changed_but_keep_selection(self):
|
def results_changed_but_keep_selection(self):
|
||||||
|
# Implemented in child classes
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -44,5 +44,4 @@ class DetailsPanel(GUIObject, DupeGuruGUIObject):
|
|||||||
|
|
||||||
# --- Event Handlers
|
# --- Event Handlers
|
||||||
def dupes_selected(self):
|
def dupes_selected(self):
|
||||||
self._refresh()
|
self._view_updated()
|
||||||
self.view.refresh()
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from hscommon.gui.tree import Tree, Node
|
|||||||
from ..directories import DirectoryState
|
from ..directories import DirectoryState
|
||||||
from .base import DupeGuruGUIObject
|
from .base import DupeGuruGUIObject
|
||||||
|
|
||||||
STATE_ORDER = [DirectoryState.Normal, DirectoryState.Reference, DirectoryState.Excluded]
|
STATE_ORDER = [DirectoryState.NORMAL, DirectoryState.REFERENCE, DirectoryState.EXCLUDED]
|
||||||
|
|
||||||
|
|
||||||
# Lazily loads children
|
# Lazily loads children
|
||||||
@@ -86,9 +86,9 @@ class DirectoryTree(Tree, DupeGuruGUIObject):
|
|||||||
else:
|
else:
|
||||||
# All selected nodes or on second-or-more level, exclude them.
|
# All selected nodes or on second-or-more level, exclude them.
|
||||||
nodes = self.selected_nodes
|
nodes = self.selected_nodes
|
||||||
newstate = DirectoryState.Excluded
|
newstate = DirectoryState.EXCLUDED
|
||||||
if all(node.state == DirectoryState.Excluded for node in nodes):
|
if all(node.state == DirectoryState.EXCLUDED for node in nodes):
|
||||||
newstate = DirectoryState.Normal
|
newstate = DirectoryState.NORMAL
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
node.state = newstate
|
node.state = newstate
|
||||||
|
|
||||||
@@ -103,5 +103,4 @@ class DirectoryTree(Tree, DupeGuruGUIObject):
|
|||||||
|
|
||||||
# --- Event Handlers
|
# --- Event Handlers
|
||||||
def directories_changed(self):
|
def directories_changed(self):
|
||||||
self._refresh()
|
self._view_updated()
|
||||||
self.view.refresh()
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
# from hscommon.trans import tr
|
|
||||||
from .exclude_list_table import ExcludeListTable
|
from .exclude_list_table import ExcludeListTable
|
||||||
from core.exclude import has_sep
|
from core.exclude import has_sep
|
||||||
from os import sep
|
from os import sep
|
||||||
@@ -47,10 +46,7 @@ class ExcludeListDialogCore:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def add(self, regex):
|
def add(self, regex):
|
||||||
try:
|
|
||||||
self.exclude_list.add(regex)
|
self.exclude_list.add(regex)
|
||||||
except Exception as e:
|
|
||||||
raise (e)
|
|
||||||
self.exclude_list.mark(regex)
|
self.exclude_list.mark(regex)
|
||||||
self.exclude_list_table.add(regex)
|
self.exclude_list_table.add(regex)
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class ExcludeListTable(GUITable, DupeGuruGUIObject):
|
|||||||
def __init__(self, exclude_list_dialog, app):
|
def __init__(self, exclude_list_dialog, app):
|
||||||
GUITable.__init__(self)
|
GUITable.__init__(self)
|
||||||
DupeGuruGUIObject.__init__(self, app)
|
DupeGuruGUIObject.__init__(self, app)
|
||||||
self.columns = Columns(self)
|
self._columns = Columns(self)
|
||||||
self.dialog = exclude_list_dialog
|
self.dialog = exclude_list_dialog
|
||||||
|
|
||||||
def rename_selected(self, newname):
|
def rename_selected(self, newname):
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class IgnoreListDialog:
|
|||||||
return
|
return
|
||||||
msg = tr("Do you really want to remove all %d items from the ignore list?") % len(self.ignore_list)
|
msg = tr("Do you really want to remove all %d items from the ignore list?") % len(self.ignore_list)
|
||||||
if self.app.view.ask_yes_no(msg):
|
if self.app.view.ask_yes_no(msg):
|
||||||
self.ignore_list.Clear()
|
self.ignore_list.clear()
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class IgnoreListTable(GUITable):
|
|||||||
|
|
||||||
def __init__(self, ignore_list_dialog):
|
def __init__(self, ignore_list_dialog):
|
||||||
GUITable.__init__(self)
|
GUITable.__init__(self)
|
||||||
self.columns = Columns(self)
|
self._columns = Columns(self)
|
||||||
self.view = None
|
self.view = None
|
||||||
self.dialog = ignore_list_dialog
|
self.dialog = ignore_list_dialog
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class ProblemTable(GUITable):
|
|||||||
|
|
||||||
def __init__(self, problem_dialog):
|
def __init__(self, problem_dialog):
|
||||||
GUITable.__init__(self)
|
GUITable.__init__(self)
|
||||||
self.columns = Columns(self)
|
self._columns = Columns(self)
|
||||||
self.dialog = problem_dialog
|
self.dialog = problem_dialog
|
||||||
|
|
||||||
# --- Override
|
# --- Override
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ class ResultTable(GUITable, DupeGuruGUIObject):
|
|||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
GUITable.__init__(self)
|
GUITable.__init__(self)
|
||||||
DupeGuruGUIObject.__init__(self, app)
|
DupeGuruGUIObject.__init__(self, app)
|
||||||
self.columns = Columns(self, prefaccess=app, savename="ResultTable")
|
self._columns = Columns(self, prefaccess=app, savename="ResultTable")
|
||||||
self._power_marker = False
|
self._power_marker = False
|
||||||
self._delta_values = False
|
self._delta_values = False
|
||||||
self._sort_descriptors = ("name", True)
|
self._sort_descriptors = ("name", True)
|
||||||
@@ -190,4 +190,4 @@ class ResultTable(GUITable, DupeGuruGUIObject):
|
|||||||
self.view.refresh()
|
self.view.refresh()
|
||||||
|
|
||||||
def save_session(self):
|
def save_session(self):
|
||||||
self.columns.save_columns()
|
self._columns.save_columns()
|
||||||
|
|||||||
@@ -20,8 +20,7 @@ class IgnoreList:
|
|||||||
|
|
||||||
# ---Override
|
# ---Override
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._ignored = {}
|
self.clear()
|
||||||
self._count = 0
|
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
for first, seconds in self._ignored.items():
|
for first, seconds in self._ignored.items():
|
||||||
@@ -32,7 +31,7 @@ class IgnoreList:
|
|||||||
return self._count
|
return self._count
|
||||||
|
|
||||||
# ---Public
|
# ---Public
|
||||||
def AreIgnored(self, first, second):
|
def are_ignored(self, first, second):
|
||||||
def do_check(first, second):
|
def do_check(first, second):
|
||||||
try:
|
try:
|
||||||
matches = self._ignored[first]
|
matches = self._ignored[first]
|
||||||
@@ -42,23 +41,23 @@ class IgnoreList:
|
|||||||
|
|
||||||
return do_check(first, second) or do_check(second, first)
|
return do_check(first, second) or do_check(second, first)
|
||||||
|
|
||||||
def Clear(self):
|
def clear(self):
|
||||||
self._ignored = {}
|
self._ignored = {}
|
||||||
self._count = 0
|
self._count = 0
|
||||||
|
|
||||||
def Filter(self, func):
|
def filter(self, func):
|
||||||
"""Applies a filter on all ignored items, and remove all matches where func(first,second)
|
"""Applies a filter on all ignored items, and remove all matches where func(first,second)
|
||||||
doesn't return True.
|
doesn't return True.
|
||||||
"""
|
"""
|
||||||
filtered = IgnoreList()
|
filtered = IgnoreList()
|
||||||
for first, second in self:
|
for first, second in self:
|
||||||
if func(first, second):
|
if func(first, second):
|
||||||
filtered.Ignore(first, second)
|
filtered.ignore(first, second)
|
||||||
self._ignored = filtered._ignored
|
self._ignored = filtered._ignored
|
||||||
self._count = filtered._count
|
self._count = filtered._count
|
||||||
|
|
||||||
def Ignore(self, first, second):
|
def ignore(self, first, second):
|
||||||
if self.AreIgnored(first, second):
|
if self.are_ignored(first, second):
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
matches = self._ignored[first]
|
matches = self._ignored[first]
|
||||||
@@ -88,8 +87,7 @@ class IgnoreList:
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not inner(first, second):
|
if not inner(first, second) and not inner(second, first):
|
||||||
if not inner(second, first):
|
|
||||||
raise ValueError()
|
raise ValueError()
|
||||||
|
|
||||||
def load_from_xml(self, infile):
|
def load_from_xml(self, infile):
|
||||||
@@ -110,7 +108,7 @@ class IgnoreList:
|
|||||||
for sfn in subfile_elems:
|
for sfn in subfile_elems:
|
||||||
subfile_path = sfn.get("path")
|
subfile_path = sfn.get("path")
|
||||||
if subfile_path:
|
if subfile_path:
|
||||||
self.Ignore(file_path, subfile_path)
|
self.ignore(file_path, subfile_path)
|
||||||
|
|
||||||
def save_to_xml(self, outfile):
|
def save_to_xml(self, outfile):
|
||||||
"""Create a XML file that can be used by load_from_xml.
|
"""Create a XML file that can be used by load_from_xml.
|
||||||
|
|||||||
@@ -17,9 +17,11 @@ class Markable:
|
|||||||
# in self.__marked, and is not affected by __inverted. Thus, self.mark while __inverted
|
# in self.__marked, and is not affected by __inverted. Thus, self.mark while __inverted
|
||||||
# is True will launch _DidUnmark.
|
# is True will launch _DidUnmark.
|
||||||
def _did_mark(self, o):
|
def _did_mark(self, o):
|
||||||
|
# Implemented in child classes
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _did_unmark(self, o):
|
def _did_unmark(self, o):
|
||||||
|
# Implemented in child classes
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _get_markable_count(self):
|
def _get_markable_count(self):
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
from hsaudiotag import auto
|
import mutagen
|
||||||
from hscommon.util import get_file_ext, format_size, format_time
|
from hscommon.util import get_file_ext, format_size, format_time
|
||||||
|
|
||||||
from core.util import format_timestamp, format_perc, format_words, format_dupe_count
|
from core.util import format_timestamp, format_perc, format_words, format_dupe_count
|
||||||
@@ -26,6 +26,9 @@ TAG_FIELDS = {
|
|||||||
"comment",
|
"comment",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# This is a temporary workaround for migration from hsaudiotag for the can_handle method
|
||||||
|
SUPPORTED_EXTS = {"mp3", "wma", "m4a", "m4p", "ogg", "flac", "aif", "aiff", "aifc"}
|
||||||
|
|
||||||
|
|
||||||
class MusicFile(fs.File):
|
class MusicFile(fs.File):
|
||||||
INITIAL_INFO = fs.File.INITIAL_INFO.copy()
|
INITIAL_INFO = fs.File.INITIAL_INFO.copy()
|
||||||
@@ -50,7 +53,7 @@ class MusicFile(fs.File):
|
|||||||
def can_handle(cls, path):
|
def can_handle(cls, path):
|
||||||
if not fs.File.can_handle(path):
|
if not fs.File.can_handle(path):
|
||||||
return False
|
return False
|
||||||
return get_file_ext(path.name) in auto.EXT2CLASS
|
return get_file_ext(path.name) in SUPPORTED_EXTS
|
||||||
|
|
||||||
def get_display_info(self, group, delta):
|
def get_display_info(self, group, delta):
|
||||||
size = self.size
|
size = self.size
|
||||||
@@ -95,21 +98,23 @@ class MusicFile(fs.File):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _get_md5partial_offset_and_size(self):
|
def _get_md5partial_offset_and_size(self):
|
||||||
f = auto.File(str(self.path))
|
# No longer calculating the offset and audio size, just whole file
|
||||||
return (f.audio_offset, f.audio_size)
|
size = self.path.stat().st_size
|
||||||
|
return (0, size)
|
||||||
|
|
||||||
def _read_info(self, field):
|
def _read_info(self, field):
|
||||||
fs.File._read_info(self, field)
|
fs.File._read_info(self, field)
|
||||||
if field in TAG_FIELDS:
|
if field in TAG_FIELDS:
|
||||||
f = auto.File(str(self.path))
|
# The various conversions here are to make this look like the previous implementation
|
||||||
self.audiosize = f.audio_size
|
file = mutagen.File(str(self.path), easy=True)
|
||||||
self.bitrate = f.bitrate
|
self.audiosize = self.path.stat().st_size
|
||||||
self.duration = f.duration
|
self.bitrate = file.info.bitrate / 1000
|
||||||
self.samplerate = f.sample_rate
|
self.duration = file.info.length
|
||||||
self.artist = f.artist
|
self.samplerate = file.info.sample_rate
|
||||||
self.album = f.album
|
self.artist = ", ".join(file.tags.get("artist") or [])
|
||||||
self.title = f.title
|
self.album = ", ".join(file.tags.get("album") or [])
|
||||||
self.genre = f.genre
|
self.title = ", ".join(file.tags.get("title") or [])
|
||||||
self.comment = f.comment
|
self.genre = ", ".join(file.tags.get("genre") or [])
|
||||||
self.year = f.year
|
self.comment = ", ".join(file.tags.get("comment") or [""])
|
||||||
self.track = f.track
|
self.year = ", ".join(file.tags.get("date") or [])
|
||||||
|
self.track = (file.tags.get("tracknumber") or [""])[0]
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ class ScannerME(ScannerBase):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_scan_options():
|
def get_scan_options():
|
||||||
return [
|
return [
|
||||||
ScanOption(ScanType.Filename, tr("Filename")),
|
ScanOption(ScanType.FILENAME, tr("Filename")),
|
||||||
ScanOption(ScanType.Fields, tr("Filename - Fields")),
|
ScanOption(ScanType.FIELDS, tr("Filename - Fields")),
|
||||||
ScanOption(ScanType.FieldsNoOrder, tr("Filename - Fields (No Order)")),
|
ScanOption(ScanType.FIELDSNOORDER, tr("Filename - Fields (No Order)")),
|
||||||
ScanOption(ScanType.Tag, tr("Tags")),
|
ScanOption(ScanType.TAG, tr("Tags")),
|
||||||
ScanOption(ScanType.Contents, tr("Contents")),
|
ScanOption(ScanType.CONTENTS, tr("Contents")),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -193,8 +193,8 @@ class TIFF_file:
|
|||||||
self.s2nfunc = s2n_intel if self.endian == INTEL_ENDIAN else s2n_motorola
|
self.s2nfunc = s2n_intel if self.endian == INTEL_ENDIAN else s2n_motorola
|
||||||
|
|
||||||
def s2n(self, offset, length, signed=0, debug=False):
|
def s2n(self, offset, length, signed=0, debug=False):
|
||||||
slice = self.data[offset : offset + length]
|
data_slice = self.data[offset : offset + length]
|
||||||
val = self.s2nfunc(slice)
|
val = self.s2nfunc(data_slice)
|
||||||
# Sign extension ?
|
# Sign extension ?
|
||||||
if signed:
|
if signed:
|
||||||
msb = 1 << (8 * length - 1)
|
msb = 1 << (8 * length - 1)
|
||||||
@@ -206,7 +206,7 @@ class TIFF_file:
|
|||||||
"Slice for offset %d length %d: %r and value: %d",
|
"Slice for offset %d length %d: %r and value: %d",
|
||||||
offset,
|
offset,
|
||||||
length,
|
length,
|
||||||
slice,
|
data_slice,
|
||||||
val,
|
val,
|
||||||
)
|
)
|
||||||
return val
|
return val
|
||||||
@@ -236,10 +236,10 @@ class TIFF_file:
|
|||||||
for i in range(entries):
|
for i in range(entries):
|
||||||
entry = ifd + 2 + 12 * i
|
entry = ifd + 2 + 12 * i
|
||||||
tag = self.s2n(entry, 2)
|
tag = self.s2n(entry, 2)
|
||||||
type = self.s2n(entry + 2, 2)
|
entry_type = self.s2n(entry + 2, 2)
|
||||||
if not 1 <= type <= 10:
|
if not 1 <= entry_type <= 10:
|
||||||
continue # not handled
|
continue # not handled
|
||||||
typelen = [1, 1, 2, 4, 8, 1, 1, 2, 4, 8][type - 1]
|
typelen = [1, 1, 2, 4, 8, 1, 1, 2, 4, 8][entry_type - 1]
|
||||||
count = self.s2n(entry + 4, 4)
|
count = self.s2n(entry + 4, 4)
|
||||||
if count > MAX_COUNT:
|
if count > MAX_COUNT:
|
||||||
logging.debug("Probably corrupt. Aborting.")
|
logging.debug("Probably corrupt. Aborting.")
|
||||||
@@ -247,14 +247,14 @@ class TIFF_file:
|
|||||||
offset = entry + 8
|
offset = entry + 8
|
||||||
if count * typelen > 4:
|
if count * typelen > 4:
|
||||||
offset = self.s2n(offset, 4)
|
offset = self.s2n(offset, 4)
|
||||||
if type == 2:
|
if entry_type == 2:
|
||||||
# Special case: nul-terminated ASCII string
|
# Special case: nul-terminated ASCII string
|
||||||
values = str(self.data[offset : offset + count - 1], encoding="latin-1")
|
values = str(self.data[offset : offset + count - 1], encoding="latin-1")
|
||||||
else:
|
else:
|
||||||
values = []
|
values = []
|
||||||
signed = type == 6 or type >= 8
|
signed = entry_type == 6 or entry_type >= 8
|
||||||
for j in range(count):
|
for _ in range(count):
|
||||||
if type in {5, 10}:
|
if entry_type in {5, 10}:
|
||||||
# The type is either 5 or 10
|
# The type is either 5 or 10
|
||||||
value_j = Fraction(self.s2n(offset, 4, signed), self.s2n(offset + 4, 4, signed))
|
value_j = Fraction(self.s2n(offset, 4, signed), self.s2n(offset + 4, 4, signed))
|
||||||
else:
|
else:
|
||||||
@@ -263,7 +263,7 @@ class TIFF_file:
|
|||||||
values.append(value_j)
|
values.append(value_j)
|
||||||
offset = offset + typelen
|
offset = offset + typelen
|
||||||
# Now "values" is either a string or an array
|
# Now "values" is either a string or an array
|
||||||
a.append((tag, type, values))
|
a.append((tag, entry_type, values))
|
||||||
return a
|
return a
|
||||||
|
|
||||||
|
|
||||||
@@ -298,7 +298,7 @@ def get_fields(fp):
|
|||||||
T = TIFF_file(data)
|
T = TIFF_file(data)
|
||||||
# There may be more than one IFD per file, but we only read the first one because others are
|
# There may be more than one IFD per file, but we only read the first one because others are
|
||||||
# most likely thumbnails.
|
# most likely thumbnails.
|
||||||
main_IFD_offset = T.first_IFD()
|
main_ifd_offset = T.first_IFD()
|
||||||
result = {}
|
result = {}
|
||||||
|
|
||||||
def add_tag_to_result(tag, values):
|
def add_tag_to_result(tag, values):
|
||||||
@@ -310,8 +310,8 @@ def get_fields(fp):
|
|||||||
return # don't overwrite data
|
return # don't overwrite data
|
||||||
result[stag] = values
|
result[stag] = values
|
||||||
|
|
||||||
logging.debug("IFD at offset %d", main_IFD_offset)
|
logging.debug("IFD at offset %d", main_ifd_offset)
|
||||||
IFD = T.dump_IFD(main_IFD_offset)
|
IFD = T.dump_IFD(main_ifd_offset)
|
||||||
exif_off = gps_off = 0
|
exif_off = gps_off = 0
|
||||||
for tag, type, values in IFD:
|
for tag, type, values in IFD:
|
||||||
if tag == 0x8769:
|
if tag == 0x8769:
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
* Created On: 2010-01-30
|
* Created On: 2010-01-30
|
||||||
* Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
* Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
*
|
*
|
||||||
* This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
* This software is licensed under the "BSD" License as described in the
|
||||||
* which should be included with this package. The terms are also available at
|
* "LICENSE" file, which should be included with this package. The terms are
|
||||||
* http://www.hardcoded.net/licenses/bsd_license
|
* also available at http://www.hardcoded.net/licenses/bsd_license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
@@ -17,8 +17,7 @@ static PyObject *DifferentBlockCountError;
|
|||||||
/* Returns a 3 sized tuple containing the mean color of 'image'.
|
/* Returns a 3 sized tuple containing the mean color of 'image'.
|
||||||
* image: a PIL image or crop.
|
* image: a PIL image or crop.
|
||||||
*/
|
*/
|
||||||
static PyObject* getblock(PyObject *image)
|
static PyObject *getblock(PyObject *image) {
|
||||||
{
|
|
||||||
int i, totr, totg, totb;
|
int i, totr, totg, totb;
|
||||||
Py_ssize_t pixel_count;
|
Py_ssize_t pixel_count;
|
||||||
PyObject *ppixels;
|
PyObject *ppixels;
|
||||||
@@ -30,7 +29,7 @@ static PyObject* getblock(PyObject *image)
|
|||||||
}
|
}
|
||||||
|
|
||||||
pixel_count = PySequence_Length(ppixels);
|
pixel_count = PySequence_Length(ppixels);
|
||||||
for (i=0; i<pixel_count; i++) {
|
for (i = 0; i < pixel_count; i++) {
|
||||||
PyObject *ppixel, *pr, *pg, *pb;
|
PyObject *ppixel, *pr, *pg, *pb;
|
||||||
int r, g, b;
|
int r, g, b;
|
||||||
|
|
||||||
@@ -65,8 +64,7 @@ static PyObject* getblock(PyObject *image)
|
|||||||
/* Returns the difference between the first block and the second.
|
/* Returns the difference between the first block and the second.
|
||||||
* It returns an absolute sum of the 3 differences (RGB).
|
* It returns an absolute sum of the 3 differences (RGB).
|
||||||
*/
|
*/
|
||||||
static int diff(PyObject *first, PyObject *second)
|
static int diff(PyObject *first, PyObject *second) {
|
||||||
{
|
|
||||||
int r1, g1, b1, r2, b2, g2;
|
int r1, g1, b1, r2, b2, g2;
|
||||||
PyObject *pr, *pg, *pb;
|
PyObject *pr, *pg, *pb;
|
||||||
pr = PySequence_ITEM(first, 0);
|
pr = PySequence_ITEM(first, 0);
|
||||||
@@ -93,7 +91,7 @@ static int diff(PyObject *first, PyObject *second)
|
|||||||
}
|
}
|
||||||
|
|
||||||
PyDoc_STRVAR(block_getblocks2_doc,
|
PyDoc_STRVAR(block_getblocks2_doc,
|
||||||
"Returns a list of blocks (3 sized tuples).\n\
|
"Returns a list of blocks (3 sized tuples).\n\
|
||||||
\n\
|
\n\
|
||||||
image: A PIL image to base the blocks on.\n\
|
image: A PIL image to base the blocks on.\n\
|
||||||
block_count_per_side: This integer determine the number of blocks the function will return.\n\
|
block_count_per_side: This integer determine the number of blocks the function will return.\n\
|
||||||
@@ -101,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\
|
necessarely cover square areas. The area covered by each block will be proportional to the image\n\
|
||||||
itself.\n");
|
itself.\n");
|
||||||
|
|
||||||
static PyObject* block_getblocks2(PyObject *self, PyObject *args)
|
static PyObject *block_getblocks2(PyObject *self, PyObject *args) {
|
||||||
{
|
|
||||||
int block_count_per_side, width, height, block_width, block_height, ih;
|
int block_count_per_side, width, height, block_width, block_height, ih;
|
||||||
PyObject *image;
|
PyObject *image;
|
||||||
PyObject *pimage_size, *pwidth, *pheight;
|
PyObject *pimage_size, *pwidth, *pheight;
|
||||||
@@ -128,23 +125,23 @@ static PyObject* block_getblocks2(PyObject *self, PyObject *args)
|
|||||||
block_width = max(width / block_count_per_side, 1);
|
block_width = max(width / block_count_per_side, 1);
|
||||||
block_height = max(height / 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) {
|
if (result == NULL) {
|
||||||
return 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;
|
int top, bottom, iw;
|
||||||
top = min(ih*block_height, height-block_height);
|
top = min(ih * block_height, height - block_height);
|
||||||
bottom = top + 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;
|
int left, right;
|
||||||
PyObject *pbox;
|
PyObject *pbox;
|
||||||
PyObject *pmethodname;
|
PyObject *pmethodname;
|
||||||
PyObject *pcrop;
|
PyObject *pcrop;
|
||||||
PyObject *pblock;
|
PyObject *pblock;
|
||||||
|
|
||||||
left = min(iw*block_width, width-block_width);
|
left = min(iw * block_width, width - block_width);
|
||||||
right = left + block_width;
|
right = left + block_width;
|
||||||
pbox = inttuple(4, left, top, right, bottom);
|
pbox = inttuple(4, left, top, right, bottom);
|
||||||
pmethodname = PyUnicode_FromString("crop");
|
pmethodname = PyUnicode_FromString("crop");
|
||||||
@@ -161,7 +158,7 @@ static PyObject* block_getblocks2(PyObject *self, PyObject *args)
|
|||||||
Py_DECREF(result);
|
Py_DECREF(result);
|
||||||
return NULL;
|
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,
|
PyDoc_STRVAR(block_avgdiff_doc,
|
||||||
"Returns the average diff between first blocks and seconds.\n\
|
"Returns the average diff between first blocks and seconds.\n\
|
||||||
\n\
|
\n\
|
||||||
If the result surpasses limit, limit + 1 is returned, except if less than min_iterations\n\
|
If the result surpasses limit, limit + 1 is returned, except if less than min_iterations\n\
|
||||||
iterations have been made in the blocks.\n");
|
iterations have been made in the blocks.\n");
|
||||||
|
|
||||||
static PyObject* block_avgdiff(PyObject *self, PyObject *args)
|
static PyObject *block_avgdiff(PyObject *self, PyObject *args) {
|
||||||
{
|
|
||||||
PyObject *first, *second;
|
PyObject *first, *second;
|
||||||
int limit, min_iterations;
|
int limit, min_iterations;
|
||||||
Py_ssize_t count;
|
Py_ssize_t count;
|
||||||
int sum, i, result;
|
int sum, i, result;
|
||||||
|
|
||||||
if (!PyArg_ParseTuple(args, "OOii", &first, &second, &limit, &min_iterations)) {
|
if (!PyArg_ParseTuple(args, "OOii", &first, &second, &limit,
|
||||||
|
&min_iterations)) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,7 +193,7 @@ static PyObject* block_avgdiff(PyObject *self, PyObject *args)
|
|||||||
}
|
}
|
||||||
|
|
||||||
sum = 0;
|
sum = 0;
|
||||||
for (i=0; i<count; i++) {
|
for (i = 0; i < count; i++) {
|
||||||
int iteration_count;
|
int iteration_count;
|
||||||
PyObject *item1, *item2;
|
PyObject *item1, *item2;
|
||||||
|
|
||||||
@@ -206,7 +203,8 @@ static PyObject* block_avgdiff(PyObject *self, PyObject *args)
|
|||||||
sum += diff(item1, item2);
|
sum += diff(item1, item2);
|
||||||
Py_DECREF(item1);
|
Py_DECREF(item1);
|
||||||
Py_DECREF(item2);
|
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);
|
return PyLong_FromLong(limit + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -224,8 +222,7 @@ static PyMethodDef BlockMethods[] = {
|
|||||||
{NULL, NULL, 0, NULL} /* Sentinel */
|
{NULL, NULL, 0, NULL} /* Sentinel */
|
||||||
};
|
};
|
||||||
|
|
||||||
static struct PyModuleDef BlockDef = {
|
static struct PyModuleDef BlockDef = {PyModuleDef_HEAD_INIT,
|
||||||
PyModuleDef_HEAD_INIT,
|
|
||||||
"_block",
|
"_block",
|
||||||
NULL,
|
NULL,
|
||||||
-1,
|
-1,
|
||||||
@@ -233,12 +230,9 @@ static struct PyModuleDef BlockDef = {
|
|||||||
NULL,
|
NULL,
|
||||||
NULL,
|
NULL,
|
||||||
NULL,
|
NULL,
|
||||||
NULL
|
NULL};
|
||||||
};
|
|
||||||
|
|
||||||
PyObject *
|
PyObject *PyInit__block(void) {
|
||||||
PyInit__block(void)
|
|
||||||
{
|
|
||||||
PyObject *m = PyModule_Create(&BlockDef);
|
PyObject *m = PyModule_Create(&BlockDef);
|
||||||
if (m == NULL) {
|
if (m == NULL) {
|
||||||
return NULL;
|
return NULL;
|
||||||
@@ -246,7 +240,8 @@ PyInit__block(void)
|
|||||||
|
|
||||||
NoBlocksError = PyErr_NewException("_block.NoBlocksError", NULL, NULL);
|
NoBlocksError = PyErr_NewException("_block.NoBlocksError", NULL, NULL);
|
||||||
PyModule_AddObject(m, "NoBlocksError", NoBlocksError);
|
PyModule_AddObject(m, "NoBlocksError", NoBlocksError);
|
||||||
DifferentBlockCountError = PyErr_NewException("_block.DifferentBlockCountError", NULL, NULL);
|
DifferentBlockCountError =
|
||||||
|
PyErr_NewException("_block.DifferentBlockCountError", NULL, NULL);
|
||||||
PyModule_AddObject(m, "DifferentBlockCountError", DifferentBlockCountError);
|
PyModule_AddObject(m, "DifferentBlockCountError", DifferentBlockCountError);
|
||||||
|
|
||||||
return m;
|
return m;
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
#include "common.h"
|
#include "common.h"
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <CoreGraphics/CoreGraphics.h>
|
||||||
|
#import <ImageIO/ImageIO.h>
|
||||||
|
|
||||||
#define RADIANS( degrees ) ( degrees * M_PI / 180 )
|
#define RADIANS( degrees ) ( degrees * M_PI / 180 )
|
||||||
|
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ class ScannerPE(Scanner):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_scan_options():
|
def get_scan_options():
|
||||||
return [
|
return [
|
||||||
ScanOption(ScanType.FuzzyBlock, tr("Contents")),
|
ScanOption(ScanType.FUZZYBLOCK, tr("Contents")),
|
||||||
ScanOption(ScanType.ExifTimestamp, tr("EXIF Timestamp")),
|
ScanOption(ScanType.EXIFTIMESTAMP, tr("EXIF Timestamp")),
|
||||||
]
|
]
|
||||||
|
|
||||||
def _getmatches(self, files, j):
|
def _getmatches(self, files, j):
|
||||||
if self.scan_type == ScanType.FuzzyBlock:
|
if self.scan_type == ScanType.FUZZYBLOCK:
|
||||||
return matchblock.getmatches(
|
return matchblock.getmatches(
|
||||||
files,
|
files,
|
||||||
cache_path=self.cache_path,
|
cache_path=self.cache_path,
|
||||||
@@ -31,7 +31,7 @@ class ScannerPE(Scanner):
|
|||||||
match_scaled=self.match_scaled,
|
match_scaled=self.match_scaled,
|
||||||
j=j,
|
j=j,
|
||||||
)
|
)
|
||||||
elif self.scan_type == ScanType.ExifTimestamp:
|
elif self.scan_type == ScanType.EXIFTIMESTAMP:
|
||||||
return matchexif.getmatches(files, self.match_scaled, j)
|
return matchexif.getmatches(files, self.match_scaled, j)
|
||||||
else:
|
else:
|
||||||
raise Exception("Invalid scan type")
|
raise ValueError("Invalid scan type")
|
||||||
|
|||||||
@@ -21,16 +21,16 @@ from . import engine
|
|||||||
|
|
||||||
|
|
||||||
class ScanType:
|
class ScanType:
|
||||||
Filename = 0
|
FILENAME = 0
|
||||||
Fields = 1
|
FIELDS = 1
|
||||||
FieldsNoOrder = 2
|
FIELDSNOORDER = 2
|
||||||
Tag = 3
|
TAG = 3
|
||||||
Folders = 4
|
FOLDERS = 4
|
||||||
Contents = 5
|
CONTENTS = 5
|
||||||
|
|
||||||
# PE
|
# PE
|
||||||
FuzzyBlock = 10
|
FUZZYBLOCK = 10
|
||||||
ExifTimestamp = 11
|
EXIFTIMESTAMP = 11
|
||||||
|
|
||||||
|
|
||||||
ScanOption = namedtuple("ScanOption", "scan_type label")
|
ScanOption = namedtuple("ScanOption", "scan_type label")
|
||||||
@@ -77,16 +77,23 @@ class Scanner:
|
|||||||
self.discarded_file_count = 0
|
self.discarded_file_count = 0
|
||||||
|
|
||||||
def _getmatches(self, files, j):
|
def _getmatches(self, files, j):
|
||||||
if self.size_threshold or self.scan_type in {
|
if (
|
||||||
ScanType.Contents,
|
self.size_threshold
|
||||||
ScanType.Folders,
|
or self.large_size_threshold
|
||||||
}:
|
or self.scan_type
|
||||||
|
in {
|
||||||
|
ScanType.CONTENTS,
|
||||||
|
ScanType.FOLDERS,
|
||||||
|
}
|
||||||
|
):
|
||||||
j = j.start_subjob([2, 8])
|
j = j.start_subjob([2, 8])
|
||||||
for f in j.iter_with_progress(files, tr("Read size of %d/%d files")):
|
for f in j.iter_with_progress(files, tr("Read size of %d/%d files")):
|
||||||
f.size # pre-read, makes a smoother progress if read here (especially for bundles)
|
f.size # pre-read, makes a smoother progress if read here (especially for bundles)
|
||||||
if self.size_threshold:
|
if self.size_threshold:
|
||||||
files = [f for f in files if f.size >= self.size_threshold]
|
files = [f for f in files if f.size >= self.size_threshold]
|
||||||
if self.scan_type in {ScanType.Contents, ScanType.Folders}:
|
if self.large_size_threshold:
|
||||||
|
files = [f for f in files if f.size <= self.large_size_threshold]
|
||||||
|
if self.scan_type in {ScanType.CONTENTS, ScanType.FOLDERS}:
|
||||||
return engine.getmatches_by_contents(files, bigsize=self.big_file_size_threshold, j=j)
|
return engine.getmatches_by_contents(files, bigsize=self.big_file_size_threshold, j=j)
|
||||||
else:
|
else:
|
||||||
j = j.start_subjob([2, 8])
|
j = j.start_subjob([2, 8])
|
||||||
@@ -94,13 +101,13 @@ class Scanner:
|
|||||||
kw["match_similar_words"] = self.match_similar_words
|
kw["match_similar_words"] = self.match_similar_words
|
||||||
kw["weight_words"] = self.word_weighting
|
kw["weight_words"] = self.word_weighting
|
||||||
kw["min_match_percentage"] = self.min_match_percentage
|
kw["min_match_percentage"] = self.min_match_percentage
|
||||||
if self.scan_type == ScanType.FieldsNoOrder:
|
if self.scan_type == ScanType.FIELDSNOORDER:
|
||||||
self.scan_type = ScanType.Fields
|
self.scan_type = ScanType.FIELDS
|
||||||
kw["no_field_order"] = True
|
kw["no_field_order"] = True
|
||||||
func = {
|
func = {
|
||||||
ScanType.Filename: lambda f: engine.getwords(rem_file_ext(f.name)),
|
ScanType.FILENAME: lambda f: engine.getwords(rem_file_ext(f.name)),
|
||||||
ScanType.Fields: lambda f: engine.getfields(rem_file_ext(f.name)),
|
ScanType.FIELDS: lambda f: engine.getfields(rem_file_ext(f.name)),
|
||||||
ScanType.Tag: lambda f: [
|
ScanType.TAG: lambda f: [
|
||||||
engine.getwords(str(getattr(f, attrname)))
|
engine.getwords(str(getattr(f, attrname)))
|
||||||
for attrname in SCANNABLE_TAGS
|
for attrname in SCANNABLE_TAGS
|
||||||
if attrname in self.scanned_tags
|
if attrname in self.scanned_tags
|
||||||
@@ -150,7 +157,7 @@ class Scanner:
|
|||||||
# "duplicated duplicates if you will). Then, we also don't want mixed file kinds if the
|
# "duplicated duplicates if you will). Then, we also don't want mixed file kinds if the
|
||||||
# option isn't enabled, we want matches for which both files exist and, lastly, we don't
|
# option isn't enabled, we want matches for which both files exist and, lastly, we don't
|
||||||
# want matches with both files as ref.
|
# want matches with both files as ref.
|
||||||
if self.scan_type == ScanType.Folders and matches:
|
if self.scan_type == ScanType.FOLDERS and matches:
|
||||||
allpath = {m.first.path for m in matches}
|
allpath = {m.first.path for m in matches}
|
||||||
allpath |= {m.second.path for m in matches}
|
allpath |= {m.second.path for m in matches}
|
||||||
sortedpaths = sorted(allpath)
|
sortedpaths = sorted(allpath)
|
||||||
@@ -167,14 +174,14 @@ class Scanner:
|
|||||||
matches = [m for m in matches if m.first.path.exists() and m.second.path.exists()]
|
matches = [m for m in matches if m.first.path.exists() and m.second.path.exists()]
|
||||||
matches = [m for m in matches if not (m.first.is_ref and m.second.is_ref)]
|
matches = [m for m in matches if not (m.first.is_ref and m.second.is_ref)]
|
||||||
if ignore_list:
|
if ignore_list:
|
||||||
matches = [m for m in matches if not ignore_list.AreIgnored(str(m.first.path), str(m.second.path))]
|
matches = [m for m in matches if not ignore_list.are_ignored(str(m.first.path), str(m.second.path))]
|
||||||
logging.info("Grouping matches")
|
logging.info("Grouping matches")
|
||||||
groups = engine.get_groups(matches)
|
groups = engine.get_groups(matches)
|
||||||
if self.scan_type in {
|
if self.scan_type in {
|
||||||
ScanType.Filename,
|
ScanType.FILENAME,
|
||||||
ScanType.Fields,
|
ScanType.FIELDS,
|
||||||
ScanType.FieldsNoOrder,
|
ScanType.FIELDSNOORDER,
|
||||||
ScanType.Tag,
|
ScanType.TAG,
|
||||||
}:
|
}:
|
||||||
matched_files = dedupe([m.first for m in matches] + [m.second for m in matches])
|
matched_files = dedupe([m.first for m in matches] + [m.second for m in matches])
|
||||||
self.discarded_file_count = len(matched_files) - sum(len(g) for g in groups)
|
self.discarded_file_count = len(matched_files) - sum(len(g) for g in groups)
|
||||||
@@ -199,8 +206,9 @@ class Scanner:
|
|||||||
match_similar_words = False
|
match_similar_words = False
|
||||||
min_match_percentage = 80
|
min_match_percentage = 80
|
||||||
mix_file_kind = True
|
mix_file_kind = True
|
||||||
scan_type = ScanType.Filename
|
scan_type = ScanType.FILENAME
|
||||||
scanned_tags = {"artist", "title"}
|
scanned_tags = {"artist", "title"}
|
||||||
size_threshold = 0
|
size_threshold = 0
|
||||||
|
large_size_threshold = 0
|
||||||
big_file_size_threshold = 0
|
big_file_size_threshold = 0
|
||||||
word_weighting = False
|
word_weighting = False
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class ScannerSE(ScannerBase):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_scan_options():
|
def get_scan_options():
|
||||||
return [
|
return [
|
||||||
ScanOption(ScanType.Filename, tr("Filename")),
|
ScanOption(ScanType.FILENAME, tr("Filename")),
|
||||||
ScanOption(ScanType.Contents, tr("Contents")),
|
ScanOption(ScanType.CONTENTS, tr("Contents")),
|
||||||
ScanOption(ScanType.Folders, tr("Folders")),
|
ScanOption(ScanType.FOLDERS, tr("Folders")),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from ..scanner import ScanType
|
|||||||
|
|
||||||
def add_fake_files_to_directories(directories, files):
|
def add_fake_files_to_directories(directories, files):
|
||||||
directories.get_files = lambda j=None: iter(files)
|
directories.get_files = lambda j=None: iter(files)
|
||||||
directories._dirs.append("this is just so Scan() doesnt return 3")
|
directories._dirs.append("this is just so Scan() doesn't return 3")
|
||||||
|
|
||||||
|
|
||||||
class TestCaseDupeGuru:
|
class TestCaseDupeGuru:
|
||||||
@@ -43,7 +43,7 @@ class TestCaseDupeGuru:
|
|||||||
dgapp.apply_filter("()[]\\.|+?^abc")
|
dgapp.apply_filter("()[]\\.|+?^abc")
|
||||||
call = dgapp.results.apply_filter.calls[1]
|
call = dgapp.results.apply_filter.calls[1]
|
||||||
eq_("\\(\\)\\[\\]\\\\\\.\\|\\+\\?\\^abc", call["filter_str"])
|
eq_("\\(\\)\\[\\]\\\\\\.\\|\\+\\?\\^abc", call["filter_str"])
|
||||||
dgapp.apply_filter("(*)") # In "simple mode", we want the * to behave as a wilcard
|
dgapp.apply_filter("(*)") # In "simple mode", we want the * to behave as a wildcard
|
||||||
call = dgapp.results.apply_filter.calls[3]
|
call = dgapp.results.apply_filter.calls[3]
|
||||||
eq_(r"\(.*\)", call["filter_str"])
|
eq_(r"\(.*\)", call["filter_str"])
|
||||||
dgapp.options["escape_filter_regexp"] = False
|
dgapp.options["escape_filter_regexp"] = False
|
||||||
@@ -88,14 +88,14 @@ class TestCaseDupeGuru:
|
|||||||
eq_(1, len(calls))
|
eq_(1, len(calls))
|
||||||
eq_(sourcepath, calls[0]["path"])
|
eq_(sourcepath, calls[0]["path"])
|
||||||
|
|
||||||
def test_Scan_with_objects_evaluating_to_false(self):
|
def test_scan_with_objects_evaluating_to_false(self):
|
||||||
class FakeFile(fs.File):
|
class FakeFile(fs.File):
|
||||||
def __bool__(self):
|
def __bool__(self):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# At some point, any() was used in a wrong way that made Scan() wrongly return 1
|
# At some point, any() was used in a wrong way that made Scan() wrongly return 1
|
||||||
app = TestApp().app
|
app = TestApp().app
|
||||||
f1, f2 = [FakeFile("foo") for i in range(2)]
|
f1, f2 = [FakeFile("foo") for _ in range(2)]
|
||||||
f1.is_ref, f2.is_ref = (False, False)
|
f1.is_ref, f2.is_ref = (False, False)
|
||||||
assert not (bool(f1) and bool(f2))
|
assert not (bool(f1) and bool(f2))
|
||||||
add_fake_files_to_directories(app.directories, [f1, f2])
|
add_fake_files_to_directories(app.directories, [f1, f2])
|
||||||
@@ -110,7 +110,7 @@ class TestCaseDupeGuru:
|
|||||||
os.link(str(tmppath["myfile"]), str(tmppath["hardlink"]))
|
os.link(str(tmppath["myfile"]), str(tmppath["hardlink"]))
|
||||||
app = TestApp().app
|
app = TestApp().app
|
||||||
app.directories.add_path(tmppath)
|
app.directories.add_path(tmppath)
|
||||||
app.options["scan_type"] = ScanType.Contents
|
app.options["scan_type"] = ScanType.CONTENTS
|
||||||
app.options["ignore_hardlink_matches"] = True
|
app.options["ignore_hardlink_matches"] = True
|
||||||
app.start_scanning()
|
app.start_scanning()
|
||||||
eq_(len(app.results.groups), 0)
|
eq_(len(app.results.groups), 0)
|
||||||
@@ -124,7 +124,7 @@ class TestCaseDupeGuru:
|
|||||||
assert not dgapp.result_table.rename_selected("foo") # no crash
|
assert not dgapp.result_table.rename_selected("foo") # no crash
|
||||||
|
|
||||||
|
|
||||||
class TestCaseDupeGuru_clean_empty_dirs:
|
class TestCaseDupeGuruCleanEmptyDirs:
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def do_setup(self, request):
|
def do_setup(self, request):
|
||||||
monkeypatch = request.getfixturevalue("monkeypatch")
|
monkeypatch = request.getfixturevalue("monkeypatch")
|
||||||
@@ -184,7 +184,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
tmppath["bar"].mkdir()
|
tmppath["bar"].mkdir()
|
||||||
self.app.directories.add_path(tmppath)
|
self.app.directories.add_path(tmppath)
|
||||||
|
|
||||||
def test_GetObjects(self, do_setup):
|
def test_get_objects(self, do_setup):
|
||||||
objects = self.objects
|
objects = self.objects
|
||||||
groups = self.groups
|
groups = self.groups
|
||||||
r = self.rtable[0]
|
r = self.rtable[0]
|
||||||
@@ -197,7 +197,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
assert r._group is groups[1]
|
assert r._group is groups[1]
|
||||||
assert r._dupe is objects[4]
|
assert r._dupe is objects[4]
|
||||||
|
|
||||||
def test_GetObjects_after_sort(self, do_setup):
|
def test_get_objects_after_sort(self, do_setup):
|
||||||
objects = self.objects
|
objects = self.objects
|
||||||
groups = self.groups[:] # we need an un-sorted reference
|
groups = self.groups[:] # we need an un-sorted reference
|
||||||
self.rtable.sort("name", False)
|
self.rtable.sort("name", False)
|
||||||
@@ -212,7 +212,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
# The first 2 dupes have been removed. The 3rd one is a ref. it stays there, in first pos.
|
# The first 2 dupes have been removed. The 3rd one is a ref. it stays there, in first pos.
|
||||||
eq_(self.rtable.selected_indexes, [1]) # no exception
|
eq_(self.rtable.selected_indexes, [1]) # no exception
|
||||||
|
|
||||||
def test_selectResultNodePaths(self, do_setup):
|
def test_select_result_node_paths(self, do_setup):
|
||||||
app = self.app
|
app = self.app
|
||||||
objects = self.objects
|
objects = self.objects
|
||||||
self.rtable.select([1, 2])
|
self.rtable.select([1, 2])
|
||||||
@@ -220,7 +220,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
assert app.selected_dupes[0] is objects[1]
|
assert app.selected_dupes[0] is objects[1]
|
||||||
assert app.selected_dupes[1] is objects[2]
|
assert app.selected_dupes[1] is objects[2]
|
||||||
|
|
||||||
def test_selectResultNodePaths_with_ref(self, do_setup):
|
def test_select_result_node_paths_with_ref(self, do_setup):
|
||||||
app = self.app
|
app = self.app
|
||||||
objects = self.objects
|
objects = self.objects
|
||||||
self.rtable.select([1, 2, 3])
|
self.rtable.select([1, 2, 3])
|
||||||
@@ -229,7 +229,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
assert app.selected_dupes[1] is objects[2]
|
assert app.selected_dupes[1] is objects[2]
|
||||||
assert app.selected_dupes[2] is self.groups[1].ref
|
assert app.selected_dupes[2] is self.groups[1].ref
|
||||||
|
|
||||||
def test_selectResultNodePaths_after_sort(self, do_setup):
|
def test_select_result_node_paths_after_sort(self, do_setup):
|
||||||
app = self.app
|
app = self.app
|
||||||
objects = self.objects
|
objects = self.objects
|
||||||
groups = self.groups[:] # To keep the old order in memory
|
groups = self.groups[:] # To keep the old order in memory
|
||||||
@@ -256,7 +256,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
app.remove_selected()
|
app.remove_selected()
|
||||||
eq_(self.rtable.selected_indexes, []) # no exception
|
eq_(self.rtable.selected_indexes, []) # no exception
|
||||||
|
|
||||||
def test_selectPowerMarkerRows_after_sort(self, do_setup):
|
def test_select_powermarker_rows_after_sort(self, do_setup):
|
||||||
app = self.app
|
app = self.app
|
||||||
objects = self.objects
|
objects = self.objects
|
||||||
self.rtable.power_marker = True
|
self.rtable.power_marker = True
|
||||||
@@ -295,7 +295,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
app.toggle_selected_mark_state()
|
app.toggle_selected_mark_state()
|
||||||
eq_(app.results.mark_count, 0)
|
eq_(app.results.mark_count, 0)
|
||||||
|
|
||||||
def test_refreshDetailsWithSelected(self, do_setup):
|
def test_refresh_details_with_selected(self, do_setup):
|
||||||
self.rtable.select([1, 4])
|
self.rtable.select([1, 4])
|
||||||
eq_(self.dpanel.row(0), ("Filename", "bar bleh", "foo bar"))
|
eq_(self.dpanel.row(0), ("Filename", "bar bleh", "foo bar"))
|
||||||
self.dpanel.view.check_gui_calls(["refresh"])
|
self.dpanel.view.check_gui_calls(["refresh"])
|
||||||
@@ -303,7 +303,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
eq_(self.dpanel.row(0), ("Filename", "---", "---"))
|
eq_(self.dpanel.row(0), ("Filename", "---", "---"))
|
||||||
self.dpanel.view.check_gui_calls(["refresh"])
|
self.dpanel.view.check_gui_calls(["refresh"])
|
||||||
|
|
||||||
def test_makeSelectedReference(self, do_setup):
|
def test_make_selected_reference(self, do_setup):
|
||||||
app = self.app
|
app = self.app
|
||||||
objects = self.objects
|
objects = self.objects
|
||||||
groups = self.groups
|
groups = self.groups
|
||||||
@@ -312,7 +312,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
assert groups[0].ref is objects[1]
|
assert groups[0].ref is objects[1]
|
||||||
assert groups[1].ref is objects[4]
|
assert groups[1].ref is objects[4]
|
||||||
|
|
||||||
def test_makeSelectedReference_by_selecting_two_dupes_in_the_same_group(self, do_setup):
|
def test_make_selected_reference_by_selecting_two_dupes_in_the_same_group(self, do_setup):
|
||||||
app = self.app
|
app = self.app
|
||||||
objects = self.objects
|
objects = self.objects
|
||||||
groups = self.groups
|
groups = self.groups
|
||||||
@@ -322,7 +322,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
assert groups[0].ref is objects[1]
|
assert groups[0].ref is objects[1]
|
||||||
assert groups[1].ref is objects[4]
|
assert groups[1].ref is objects[4]
|
||||||
|
|
||||||
def test_removeSelected(self, do_setup):
|
def test_remove_selected(self, do_setup):
|
||||||
app = self.app
|
app = self.app
|
||||||
self.rtable.select([1, 4])
|
self.rtable.select([1, 4])
|
||||||
app.remove_selected()
|
app.remove_selected()
|
||||||
@@ -330,7 +330,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
app.remove_selected()
|
app.remove_selected()
|
||||||
eq_(len(app.results.dupes), 0)
|
eq_(len(app.results.dupes), 0)
|
||||||
|
|
||||||
def test_addDirectory_simple(self, do_setup):
|
def test_add_directory_simple(self, do_setup):
|
||||||
# There's already a directory in self.app, so adding another once makes 2 of em
|
# There's already a directory in self.app, so adding another once makes 2 of em
|
||||||
app = self.app
|
app = self.app
|
||||||
# any other path that isn't a parent or child of the already added path
|
# any other path that isn't a parent or child of the already added path
|
||||||
@@ -338,7 +338,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
app.add_directory(otherpath)
|
app.add_directory(otherpath)
|
||||||
eq_(len(app.directories), 2)
|
eq_(len(app.directories), 2)
|
||||||
|
|
||||||
def test_addDirectory_already_there(self, do_setup):
|
def test_add_directory_already_there(self, do_setup):
|
||||||
app = self.app
|
app = self.app
|
||||||
otherpath = Path(op.dirname(__file__))
|
otherpath = Path(op.dirname(__file__))
|
||||||
app.add_directory(otherpath)
|
app.add_directory(otherpath)
|
||||||
@@ -346,7 +346,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
eq_(len(app.view.messages), 1)
|
eq_(len(app.view.messages), 1)
|
||||||
assert "already" in app.view.messages[0]
|
assert "already" in app.view.messages[0]
|
||||||
|
|
||||||
def test_addDirectory_does_not_exist(self, do_setup):
|
def test_add_directory_does_not_exist(self, do_setup):
|
||||||
app = self.app
|
app = self.app
|
||||||
app.add_directory("/does_not_exist")
|
app.add_directory("/does_not_exist")
|
||||||
eq_(len(app.view.messages), 1)
|
eq_(len(app.view.messages), 1)
|
||||||
@@ -362,30 +362,30 @@ class TestCaseDupeGuruWithResults:
|
|||||||
# BOTH the ref and the other dupe should have been added
|
# BOTH the ref and the other dupe should have been added
|
||||||
eq_(len(app.ignore_list), 3)
|
eq_(len(app.ignore_list), 3)
|
||||||
|
|
||||||
def test_purgeIgnoreList(self, do_setup, tmpdir):
|
def test_purge_ignorelist(self, do_setup, tmpdir):
|
||||||
app = self.app
|
app = self.app
|
||||||
p1 = str(tmpdir.join("file1"))
|
p1 = str(tmpdir.join("file1"))
|
||||||
p2 = str(tmpdir.join("file2"))
|
p2 = str(tmpdir.join("file2"))
|
||||||
open(p1, "w").close()
|
open(p1, "w").close()
|
||||||
open(p2, "w").close()
|
open(p2, "w").close()
|
||||||
dne = "/does_not_exist"
|
dne = "/does_not_exist"
|
||||||
app.ignore_list.Ignore(dne, p1)
|
app.ignore_list.ignore(dne, p1)
|
||||||
app.ignore_list.Ignore(p2, dne)
|
app.ignore_list.ignore(p2, dne)
|
||||||
app.ignore_list.Ignore(p1, p2)
|
app.ignore_list.ignore(p1, p2)
|
||||||
app.purge_ignore_list()
|
app.purge_ignore_list()
|
||||||
eq_(1, len(app.ignore_list))
|
eq_(1, len(app.ignore_list))
|
||||||
assert app.ignore_list.AreIgnored(p1, p2)
|
assert app.ignore_list.are_ignored(p1, p2)
|
||||||
assert not app.ignore_list.AreIgnored(dne, p1)
|
assert not app.ignore_list.are_ignored(dne, p1)
|
||||||
|
|
||||||
def test_only_unicode_is_added_to_ignore_list(self, do_setup):
|
def test_only_unicode_is_added_to_ignore_list(self, do_setup):
|
||||||
def FakeIgnore(first, second):
|
def fake_ignore(first, second):
|
||||||
if not isinstance(first, str):
|
if not isinstance(first, str):
|
||||||
self.fail()
|
self.fail()
|
||||||
if not isinstance(second, str):
|
if not isinstance(second, str):
|
||||||
self.fail()
|
self.fail()
|
||||||
|
|
||||||
app = self.app
|
app = self.app
|
||||||
app.ignore_list.Ignore = FakeIgnore
|
app.ignore_list.ignore = fake_ignore
|
||||||
self.rtable.select([4])
|
self.rtable.select([4])
|
||||||
app.add_selected_to_ignore_list()
|
app.add_selected_to_ignore_list()
|
||||||
|
|
||||||
@@ -419,7 +419,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
# don't crash
|
# don't crash
|
||||||
|
|
||||||
|
|
||||||
class TestCaseDupeGuru_renameSelected:
|
class TestCaseDupeGuruRenameSelected:
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def do_setup(self, request):
|
def do_setup(self, request):
|
||||||
tmpdir = request.getfixturevalue("tmpdir")
|
tmpdir = request.getfixturevalue("tmpdir")
|
||||||
@@ -502,7 +502,6 @@ class TestAppWithDirectoriesInTree:
|
|||||||
# refreshed.
|
# refreshed.
|
||||||
node = self.dtree[0]
|
node = self.dtree[0]
|
||||||
eq_(len(node), 3) # a len() call is required for subnodes to be loaded
|
eq_(len(node), 3) # a len() call is required for subnodes to be loaded
|
||||||
subnode = node[0]
|
|
||||||
node.state = 1 # the state property is a state index
|
node.state = 1 # the state property is a state index
|
||||||
node = self.dtree[0]
|
node = self.dtree[0]
|
||||||
eq_(len(node), 3)
|
eq_(len(node), 3)
|
||||||
|
|||||||
@@ -151,8 +151,8 @@ class TestApp(TestAppBase):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
def link_gui(gui):
|
def link_gui(gui):
|
||||||
gui.view = self.make_logger()
|
gui.view = self.make_logger()
|
||||||
if hasattr(gui, "columns"): # tables
|
if hasattr(gui, "_columns"): # tables
|
||||||
gui.columns.view = self.make_logger()
|
gui._columns.view = self.make_logger()
|
||||||
return gui
|
return gui
|
||||||
|
|
||||||
TestAppBase.__init__(self)
|
TestAppBase.__init__(self)
|
||||||
|
|||||||
@@ -73,99 +73,6 @@ class TestCasegetblock:
|
|||||||
eq_((meanred, meangreen, meanblue), b)
|
eq_((meanred, meangreen, meanblue), b)
|
||||||
|
|
||||||
|
|
||||||
# class TCdiff(unittest.TestCase):
|
|
||||||
# def test_diff(self):
|
|
||||||
# b1 = (10, 20, 30)
|
|
||||||
# b2 = (1, 2, 3)
|
|
||||||
# eq_(9 + 18 + 27, diff(b1, b2))
|
|
||||||
#
|
|
||||||
# def test_diff_negative(self):
|
|
||||||
# b1 = (10, 20, 30)
|
|
||||||
# b2 = (1, 2, 3)
|
|
||||||
# eq_(9 + 18 + 27, diff(b2, b1))
|
|
||||||
#
|
|
||||||
# def test_diff_mixed_positive_and_negative(self):
|
|
||||||
# b1 = (1, 5, 10)
|
|
||||||
# b2 = (10, 1, 15)
|
|
||||||
# eq_(9 + 4 + 5, diff(b1, b2))
|
|
||||||
#
|
|
||||||
|
|
||||||
# class TCgetblocks(unittest.TestCase):
|
|
||||||
# def test_empty_image(self):
|
|
||||||
# im = empty()
|
|
||||||
# blocks = getblocks(im, 1)
|
|
||||||
# eq_(0, len(blocks))
|
|
||||||
#
|
|
||||||
# def test_one_block_image(self):
|
|
||||||
# im = four_pixels()
|
|
||||||
# blocks = getblocks2(im, 1)
|
|
||||||
# eq_(1, len(blocks))
|
|
||||||
# block = blocks[0]
|
|
||||||
# meanred = (0xff + 0x80) // 4
|
|
||||||
# meangreen = (0x80 + 0x40) // 4
|
|
||||||
# meanblue = (0xff + 0x80) // 4
|
|
||||||
# eq_((meanred, meangreen, meanblue), block)
|
|
||||||
#
|
|
||||||
# def test_not_enough_height_to_fit_a_block(self):
|
|
||||||
# im = FakeImage((2, 1), [BLACK, BLACK])
|
|
||||||
# blocks = getblocks(im, 2)
|
|
||||||
# eq_(0, len(blocks))
|
|
||||||
#
|
|
||||||
# def xtest_dont_include_leftovers(self):
|
|
||||||
# # this test is disabled because getblocks is not used and getblock in cdeffed
|
|
||||||
# pixels = [
|
|
||||||
# RED,(0, 0x80, 0xff), BLACK,
|
|
||||||
# (0x80, 0, 0),(0, 0x40, 0x80), BLACK,
|
|
||||||
# BLACK, BLACK, BLACK
|
|
||||||
# ]
|
|
||||||
# im = FakeImage((3, 3), pixels)
|
|
||||||
# blocks = getblocks(im, 2)
|
|
||||||
# block = blocks[0]
|
|
||||||
# #Because the block is smaller than the image, only blocksize must be considered.
|
|
||||||
# meanred = (0xff + 0x80) // 4
|
|
||||||
# meangreen = (0x80 + 0x40) // 4
|
|
||||||
# meanblue = (0xff + 0x80) // 4
|
|
||||||
# eq_((meanred, meangreen, meanblue), block)
|
|
||||||
#
|
|
||||||
# def xtest_two_blocks(self):
|
|
||||||
# # this test is disabled because getblocks is not used and getblock in cdeffed
|
|
||||||
# pixels = [BLACK for i in xrange(4 * 2)]
|
|
||||||
# pixels[0] = RED
|
|
||||||
# pixels[1] = (0, 0x80, 0xff)
|
|
||||||
# pixels[4] = (0x80, 0, 0)
|
|
||||||
# pixels[5] = (0, 0x40, 0x80)
|
|
||||||
# im = FakeImage((4, 2), pixels)
|
|
||||||
# blocks = getblocks(im, 2)
|
|
||||||
# eq_(2, len(blocks))
|
|
||||||
# block = blocks[0]
|
|
||||||
# #Because the block is smaller than the image, only blocksize must be considered.
|
|
||||||
# meanred = (0xff + 0x80) // 4
|
|
||||||
# meangreen = (0x80 + 0x40) // 4
|
|
||||||
# meanblue = (0xff + 0x80) // 4
|
|
||||||
# eq_((meanred, meangreen, meanblue), block)
|
|
||||||
# eq_(BLACK, blocks[1])
|
|
||||||
#
|
|
||||||
# def test_four_blocks(self):
|
|
||||||
# pixels = [BLACK for i in xrange(4 * 4)]
|
|
||||||
# pixels[0] = RED
|
|
||||||
# pixels[1] = (0, 0x80, 0xff)
|
|
||||||
# pixels[4] = (0x80, 0, 0)
|
|
||||||
# pixels[5] = (0, 0x40, 0x80)
|
|
||||||
# im = FakeImage((4, 4), pixels)
|
|
||||||
# blocks = getblocks2(im, 2)
|
|
||||||
# eq_(4, len(blocks))
|
|
||||||
# block = blocks[0]
|
|
||||||
# #Because the block is smaller than the image, only blocksize must be considered.
|
|
||||||
# meanred = (0xff + 0x80) // 4
|
|
||||||
# meangreen = (0x80 + 0x40) // 4
|
|
||||||
# meanblue = (0xff + 0x80) // 4
|
|
||||||
# eq_((meanred, meangreen, meanblue), block)
|
|
||||||
# eq_(BLACK, blocks[1])
|
|
||||||
# eq_(BLACK, blocks[2])
|
|
||||||
# eq_(BLACK, blocks[3])
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
class TestCasegetblocks2:
|
class TestCasegetblocks2:
|
||||||
def test_empty_image(self):
|
def test_empty_image(self):
|
||||||
im = empty()
|
im = empty()
|
||||||
@@ -270,8 +177,8 @@ class TestCaseavgdiff:
|
|||||||
def test_return_at_least_1_at_the_slightest_difference(self):
|
def test_return_at_least_1_at_the_slightest_difference(self):
|
||||||
ref = (0, 0, 0)
|
ref = (0, 0, 0)
|
||||||
b1 = (1, 0, 0)
|
b1 = (1, 0, 0)
|
||||||
blocks1 = [ref for i in range(250)]
|
blocks1 = [ref for _ in range(250)]
|
||||||
blocks2 = [ref for i in range(250)]
|
blocks2 = [ref for _ in range(250)]
|
||||||
blocks2[0] = b1
|
blocks2[0] = b1
|
||||||
eq_(1, my_avgdiff(blocks1, blocks2))
|
eq_(1, my_avgdiff(blocks1, blocks2))
|
||||||
|
|
||||||
@@ -280,41 +187,3 @@ class TestCaseavgdiff:
|
|||||||
blocks1 = [ref, ref]
|
blocks1 = [ref, ref]
|
||||||
blocks2 = [ref, ref]
|
blocks2 = [ref, ref]
|
||||||
eq_(0, my_avgdiff(blocks1, blocks2))
|
eq_(0, my_avgdiff(blocks1, blocks2))
|
||||||
|
|
||||||
|
|
||||||
# class TCmaxdiff(unittest.TestCase):
|
|
||||||
# def test_empty(self):
|
|
||||||
# self.assertRaises(NoBlocksError, maxdiff,[],[])
|
|
||||||
#
|
|
||||||
# def test_two_blocks(self):
|
|
||||||
# b1 = (5, 10, 15)
|
|
||||||
# b2 = (255, 250, 245)
|
|
||||||
# b3 = (0, 0, 0)
|
|
||||||
# b4 = (255, 0, 255)
|
|
||||||
# blocks1 = [b1, b2]
|
|
||||||
# blocks2 = [b3, b4]
|
|
||||||
# expected1 = 5 + 10 + 15
|
|
||||||
# expected2 = 0 + 250 + 10
|
|
||||||
# expected = max(expected1, expected2)
|
|
||||||
# eq_(expected, maxdiff(blocks1, blocks2))
|
|
||||||
#
|
|
||||||
# def test_blocks_not_the_same_size(self):
|
|
||||||
# b = (0, 0, 0)
|
|
||||||
# self.assertRaises(DifferentBlockCountError, maxdiff,[b, b],[b])
|
|
||||||
#
|
|
||||||
# def test_first_arg_is_empty_but_not_second(self):
|
|
||||||
# #Don't return 0 (as when the 2 lists are empty), raise!
|
|
||||||
# b = (0, 0, 0)
|
|
||||||
# self.assertRaises(DifferentBlockCountError, maxdiff,[],[b])
|
|
||||||
#
|
|
||||||
# def test_limit(self):
|
|
||||||
# b1 = (5, 10, 15)
|
|
||||||
# b2 = (255, 250, 245)
|
|
||||||
# b3 = (0, 0, 0)
|
|
||||||
# b4 = (255, 0, 255)
|
|
||||||
# blocks1 = [b1, b2]
|
|
||||||
# blocks2 = [b3, b4]
|
|
||||||
# expected1 = 5 + 10 + 15
|
|
||||||
# expected2 = 0 + 250 + 10
|
|
||||||
# eq_(expected1, maxdiff(blocks1, blocks2, expected1 - 1))
|
|
||||||
#
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ except ImportError:
|
|||||||
skip("Can't import the cache module, probably hasn't been compiled.")
|
skip("Can't import the cache module, probably hasn't been compiled.")
|
||||||
|
|
||||||
|
|
||||||
class TestCasecolors_to_string:
|
class TestCaseColorsToString:
|
||||||
def test_no_color(self):
|
def test_no_color(self):
|
||||||
eq_("", colors_to_string([]))
|
eq_("", colors_to_string([]))
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ class TestCasecolors_to_string:
|
|||||||
eq_("000102030405", colors_to_string([(0, 1, 2), (3, 4, 5)]))
|
eq_("000102030405", colors_to_string([(0, 1, 2), (3, 4, 5)]))
|
||||||
|
|
||||||
|
|
||||||
class TestCasestring_to_colors:
|
class TestCaseStringToColors:
|
||||||
def test_empty(self):
|
def test_empty(self):
|
||||||
eq_([], string_to_colors(""))
|
eq_([], string_to_colors(""))
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ def test_add_path():
|
|||||||
assert p in d
|
assert p in d
|
||||||
|
|
||||||
|
|
||||||
def test_AddPath_when_path_is_already_there():
|
def test_add_path_when_path_is_already_there():
|
||||||
d = Directories()
|
d = Directories()
|
||||||
p = testpath["onefile"]
|
p = testpath["onefile"]
|
||||||
d.add_path(p)
|
d.add_path(p)
|
||||||
@@ -112,7 +112,7 @@ def test_add_path_containing_paths_already_there():
|
|||||||
eq_(d[0], testpath)
|
eq_(d[0], testpath)
|
||||||
|
|
||||||
|
|
||||||
def test_AddPath_non_latin(tmpdir):
|
def test_add_path_non_latin(tmpdir):
|
||||||
p = Path(str(tmpdir))
|
p = Path(str(tmpdir))
|
||||||
to_add = p["unicode\u201a"]
|
to_add = p["unicode\u201a"]
|
||||||
os.mkdir(str(to_add))
|
os.mkdir(str(to_add))
|
||||||
@@ -140,20 +140,20 @@ def test_states():
|
|||||||
d = Directories()
|
d = Directories()
|
||||||
p = testpath["onefile"]
|
p = testpath["onefile"]
|
||||||
d.add_path(p)
|
d.add_path(p)
|
||||||
eq_(DirectoryState.Normal, d.get_state(p))
|
eq_(DirectoryState.NORMAL, d.get_state(p))
|
||||||
d.set_state(p, DirectoryState.Reference)
|
d.set_state(p, DirectoryState.REFERENCE)
|
||||||
eq_(DirectoryState.Reference, d.get_state(p))
|
eq_(DirectoryState.REFERENCE, d.get_state(p))
|
||||||
eq_(DirectoryState.Reference, d.get_state(p["dir1"]))
|
eq_(DirectoryState.REFERENCE, d.get_state(p["dir1"]))
|
||||||
eq_(1, len(d.states))
|
eq_(1, len(d.states))
|
||||||
eq_(p, list(d.states.keys())[0])
|
eq_(p, list(d.states.keys())[0])
|
||||||
eq_(DirectoryState.Reference, d.states[p])
|
eq_(DirectoryState.REFERENCE, d.states[p])
|
||||||
|
|
||||||
|
|
||||||
def test_get_state_with_path_not_there():
|
def test_get_state_with_path_not_there():
|
||||||
# When the path's not there, just return DirectoryState.Normal
|
# When the path's not there, just return DirectoryState.Normal
|
||||||
d = Directories()
|
d = Directories()
|
||||||
d.add_path(testpath["onefile"])
|
d.add_path(testpath["onefile"])
|
||||||
eq_(d.get_state(testpath), DirectoryState.Normal)
|
eq_(d.get_state(testpath), DirectoryState.NORMAL)
|
||||||
|
|
||||||
|
|
||||||
def test_states_overwritten_when_larger_directory_eat_smaller_ones():
|
def test_states_overwritten_when_larger_directory_eat_smaller_ones():
|
||||||
@@ -162,20 +162,20 @@ def test_states_overwritten_when_larger_directory_eat_smaller_ones():
|
|||||||
d = Directories()
|
d = Directories()
|
||||||
p = testpath["onefile"]
|
p = testpath["onefile"]
|
||||||
d.add_path(p)
|
d.add_path(p)
|
||||||
d.set_state(p, DirectoryState.Excluded)
|
d.set_state(p, DirectoryState.EXCLUDED)
|
||||||
d.add_path(testpath)
|
d.add_path(testpath)
|
||||||
d.set_state(testpath, DirectoryState.Reference)
|
d.set_state(testpath, DirectoryState.REFERENCE)
|
||||||
eq_(d.get_state(p), DirectoryState.Reference)
|
eq_(d.get_state(p), DirectoryState.REFERENCE)
|
||||||
eq_(d.get_state(p["dir1"]), DirectoryState.Reference)
|
eq_(d.get_state(p["dir1"]), DirectoryState.REFERENCE)
|
||||||
eq_(d.get_state(testpath), DirectoryState.Reference)
|
eq_(d.get_state(testpath), DirectoryState.REFERENCE)
|
||||||
|
|
||||||
|
|
||||||
def test_get_files():
|
def test_get_files():
|
||||||
d = Directories()
|
d = Directories()
|
||||||
p = testpath["fs"]
|
p = testpath["fs"]
|
||||||
d.add_path(p)
|
d.add_path(p)
|
||||||
d.set_state(p["dir1"], DirectoryState.Reference)
|
d.set_state(p["dir1"], DirectoryState.REFERENCE)
|
||||||
d.set_state(p["dir2"], DirectoryState.Excluded)
|
d.set_state(p["dir2"], DirectoryState.EXCLUDED)
|
||||||
files = list(d.get_files())
|
files = list(d.get_files())
|
||||||
eq_(5, len(files))
|
eq_(5, len(files))
|
||||||
for f in files:
|
for f in files:
|
||||||
@@ -204,8 +204,8 @@ def test_get_folders():
|
|||||||
d = Directories()
|
d = Directories()
|
||||||
p = testpath["fs"]
|
p = testpath["fs"]
|
||||||
d.add_path(p)
|
d.add_path(p)
|
||||||
d.set_state(p["dir1"], DirectoryState.Reference)
|
d.set_state(p["dir1"], DirectoryState.REFERENCE)
|
||||||
d.set_state(p["dir2"], DirectoryState.Excluded)
|
d.set_state(p["dir2"], DirectoryState.EXCLUDED)
|
||||||
folders = list(d.get_folders())
|
folders = list(d.get_folders())
|
||||||
eq_(len(folders), 3)
|
eq_(len(folders), 3)
|
||||||
ref = [f for f in folders if f.is_ref]
|
ref = [f for f in folders if f.is_ref]
|
||||||
@@ -220,7 +220,7 @@ def test_get_files_with_inherited_exclusion():
|
|||||||
d = Directories()
|
d = Directories()
|
||||||
p = testpath["onefile"]
|
p = testpath["onefile"]
|
||||||
d.add_path(p)
|
d.add_path(p)
|
||||||
d.set_state(p, DirectoryState.Excluded)
|
d.set_state(p, DirectoryState.EXCLUDED)
|
||||||
eq_([], list(d.get_files()))
|
eq_([], list(d.get_files()))
|
||||||
|
|
||||||
|
|
||||||
@@ -233,14 +233,14 @@ def test_save_and_load(tmpdir):
|
|||||||
p2.mkdir()
|
p2.mkdir()
|
||||||
d1.add_path(p1)
|
d1.add_path(p1)
|
||||||
d1.add_path(p2)
|
d1.add_path(p2)
|
||||||
d1.set_state(p1, DirectoryState.Reference)
|
d1.set_state(p1, DirectoryState.REFERENCE)
|
||||||
d1.set_state(p1["dir1"], DirectoryState.Excluded)
|
d1.set_state(p1["dir1"], DirectoryState.EXCLUDED)
|
||||||
tmpxml = str(tmpdir.join("directories_testunit.xml"))
|
tmpxml = str(tmpdir.join("directories_testunit.xml"))
|
||||||
d1.save_to_file(tmpxml)
|
d1.save_to_file(tmpxml)
|
||||||
d2.load_from_file(tmpxml)
|
d2.load_from_file(tmpxml)
|
||||||
eq_(2, len(d2))
|
eq_(2, len(d2))
|
||||||
eq_(DirectoryState.Reference, d2.get_state(p1))
|
eq_(DirectoryState.REFERENCE, d2.get_state(p1))
|
||||||
eq_(DirectoryState.Excluded, d2.get_state(p1["dir1"]))
|
eq_(DirectoryState.EXCLUDED, d2.get_state(p1["dir1"]))
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_path():
|
def test_invalid_path():
|
||||||
@@ -258,7 +258,7 @@ def test_set_state_on_invalid_path():
|
|||||||
Path(
|
Path(
|
||||||
"foobar",
|
"foobar",
|
||||||
),
|
),
|
||||||
DirectoryState.Normal,
|
DirectoryState.NORMAL,
|
||||||
)
|
)
|
||||||
except LookupError:
|
except LookupError:
|
||||||
assert False
|
assert False
|
||||||
@@ -287,7 +287,7 @@ def test_unicode_save(tmpdir):
|
|||||||
p1.mkdir()
|
p1.mkdir()
|
||||||
p1["foo\xe9"].mkdir()
|
p1["foo\xe9"].mkdir()
|
||||||
d.add_path(p1)
|
d.add_path(p1)
|
||||||
d.set_state(p1["foo\xe9"], DirectoryState.Excluded)
|
d.set_state(p1["foo\xe9"], DirectoryState.EXCLUDED)
|
||||||
tmpxml = str(tmpdir.join("directories_testunit.xml"))
|
tmpxml = str(tmpdir.join("directories_testunit.xml"))
|
||||||
try:
|
try:
|
||||||
d.save_to_file(tmpxml)
|
d.save_to_file(tmpxml)
|
||||||
@@ -321,10 +321,10 @@ def test_get_state_returns_excluded_by_default_for_hidden_directories(tmpdir):
|
|||||||
hidden_dir_path = p[".foo"]
|
hidden_dir_path = p[".foo"]
|
||||||
p[".foo"].mkdir()
|
p[".foo"].mkdir()
|
||||||
d.add_path(p)
|
d.add_path(p)
|
||||||
eq_(d.get_state(hidden_dir_path), DirectoryState.Excluded)
|
eq_(d.get_state(hidden_dir_path), DirectoryState.EXCLUDED)
|
||||||
# But it can be overriden
|
# But it can be overriden
|
||||||
d.set_state(hidden_dir_path, DirectoryState.Normal)
|
d.set_state(hidden_dir_path, DirectoryState.NORMAL)
|
||||||
eq_(d.get_state(hidden_dir_path), DirectoryState.Normal)
|
eq_(d.get_state(hidden_dir_path), DirectoryState.NORMAL)
|
||||||
|
|
||||||
|
|
||||||
def test_default_path_state_override(tmpdir):
|
def test_default_path_state_override(tmpdir):
|
||||||
@@ -332,7 +332,7 @@ def test_default_path_state_override(tmpdir):
|
|||||||
class MyDirectories(Directories):
|
class MyDirectories(Directories):
|
||||||
def _default_state_for_path(self, path):
|
def _default_state_for_path(self, path):
|
||||||
if "foobar" in path:
|
if "foobar" in path:
|
||||||
return DirectoryState.Excluded
|
return DirectoryState.EXCLUDED
|
||||||
|
|
||||||
d = MyDirectories()
|
d = MyDirectories()
|
||||||
p1 = Path(str(tmpdir))
|
p1 = Path(str(tmpdir))
|
||||||
@@ -341,12 +341,12 @@ def test_default_path_state_override(tmpdir):
|
|||||||
p1["foobaz"].mkdir()
|
p1["foobaz"].mkdir()
|
||||||
p1["foobaz/somefile"].open("w").close()
|
p1["foobaz/somefile"].open("w").close()
|
||||||
d.add_path(p1)
|
d.add_path(p1)
|
||||||
eq_(d.get_state(p1["foobaz"]), DirectoryState.Normal)
|
eq_(d.get_state(p1["foobaz"]), DirectoryState.NORMAL)
|
||||||
eq_(d.get_state(p1["foobar"]), DirectoryState.Excluded)
|
eq_(d.get_state(p1["foobar"]), DirectoryState.EXCLUDED)
|
||||||
eq_(len(list(d.get_files())), 1) # only the 'foobaz' file is there
|
eq_(len(list(d.get_files())), 1) # only the 'foobaz' file is there
|
||||||
# However, the default state can be changed
|
# However, the default state can be changed
|
||||||
d.set_state(p1["foobar"], DirectoryState.Normal)
|
d.set_state(p1["foobar"], DirectoryState.NORMAL)
|
||||||
eq_(d.get_state(p1["foobar"]), DirectoryState.Normal)
|
eq_(d.get_state(p1["foobar"]), DirectoryState.NORMAL)
|
||||||
eq_(len(list(d.get_files())), 2)
|
eq_(len(list(d.get_files())), 2)
|
||||||
|
|
||||||
|
|
||||||
@@ -375,11 +375,11 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
|
|||||||
p1["$Recycle.Bin"].mkdir()
|
p1["$Recycle.Bin"].mkdir()
|
||||||
p1["$Recycle.Bin"]["subdir"].mkdir()
|
p1["$Recycle.Bin"]["subdir"].mkdir()
|
||||||
self.d.add_path(p1)
|
self.d.add_path(p1)
|
||||||
eq_(self.d.get_state(p1["$Recycle.Bin"]), DirectoryState.Excluded)
|
eq_(self.d.get_state(p1["$Recycle.Bin"]), DirectoryState.EXCLUDED)
|
||||||
# By default, subdirs should be excluded too, but this can be overriden separately
|
# By default, subdirs should be excluded too, but this can be overridden separately
|
||||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.Excluded)
|
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.EXCLUDED)
|
||||||
self.d.set_state(p1["$Recycle.Bin"]["subdir"], DirectoryState.Normal)
|
self.d.set_state(p1["$Recycle.Bin"]["subdir"], DirectoryState.NORMAL)
|
||||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.Normal)
|
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.NORMAL)
|
||||||
|
|
||||||
def test_exclude_refined(self, tmpdir):
|
def test_exclude_refined(self, tmpdir):
|
||||||
regex1 = r"^\$Recycle\.Bin$"
|
regex1 = r"^\$Recycle\.Bin$"
|
||||||
@@ -398,16 +398,16 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
|
|||||||
self.d.add_path(p1["$Recycle.Bin"])
|
self.d.add_path(p1["$Recycle.Bin"])
|
||||||
|
|
||||||
# Filter should set the default state to Excluded
|
# Filter should set the default state to Excluded
|
||||||
eq_(self.d.get_state(p1["$Recycle.Bin"]), DirectoryState.Excluded)
|
eq_(self.d.get_state(p1["$Recycle.Bin"]), DirectoryState.EXCLUDED)
|
||||||
# The subdir should inherit its parent state
|
# The subdir should inherit its parent state
|
||||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.Excluded)
|
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.EXCLUDED)
|
||||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdar"]), DirectoryState.Excluded)
|
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdar"]), DirectoryState.EXCLUDED)
|
||||||
# Override a child path's state
|
# Override a child path's state
|
||||||
self.d.set_state(p1["$Recycle.Bin"]["subdir"], DirectoryState.Normal)
|
self.d.set_state(p1["$Recycle.Bin"]["subdir"], DirectoryState.NORMAL)
|
||||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.Normal)
|
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.NORMAL)
|
||||||
# Parent should keep its default state, and the other child too
|
# Parent should keep its default state, and the other child too
|
||||||
eq_(self.d.get_state(p1["$Recycle.Bin"]), DirectoryState.Excluded)
|
eq_(self.d.get_state(p1["$Recycle.Bin"]), DirectoryState.EXCLUDED)
|
||||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdar"]), DirectoryState.Excluded)
|
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdar"]), DirectoryState.EXCLUDED)
|
||||||
# print(f"get_folders(): {[x for x in self.d.get_folders()]}")
|
# print(f"get_folders(): {[x for x in self.d.get_folders()]}")
|
||||||
|
|
||||||
# only the 2 files directly under the Normal directory
|
# only the 2 files directly under the Normal directory
|
||||||
@@ -419,8 +419,8 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
|
|||||||
assert "somesubdirfile.png" in files
|
assert "somesubdirfile.png" in files
|
||||||
assert "unwanted_subdirfile.gif" in files
|
assert "unwanted_subdirfile.gif" in files
|
||||||
# Overriding the parent should enable all children
|
# Overriding the parent should enable all children
|
||||||
self.d.set_state(p1["$Recycle.Bin"], DirectoryState.Normal)
|
self.d.set_state(p1["$Recycle.Bin"], DirectoryState.NORMAL)
|
||||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdar"]), DirectoryState.Normal)
|
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdar"]), DirectoryState.NORMAL)
|
||||||
# all files there
|
# all files there
|
||||||
files = self.get_files_and_expect_num_result(6)
|
files = self.get_files_and_expect_num_result(6)
|
||||||
assert "somefile.png" in files
|
assert "somefile.png" in files
|
||||||
@@ -444,7 +444,7 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
|
|||||||
assert self.d._exclude_list.error(regex3) is None
|
assert self.d._exclude_list.error(regex3) is None
|
||||||
# print(f"get_folders(): {[x for x in self.d.get_folders()]}")
|
# print(f"get_folders(): {[x for x in self.d.get_folders()]}")
|
||||||
# Directory shouldn't change its state here, unless explicitely done by user
|
# Directory shouldn't change its state here, unless explicitely done by user
|
||||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.Normal)
|
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.NORMAL)
|
||||||
files = self.get_files_and_expect_num_result(5)
|
files = self.get_files_and_expect_num_result(5)
|
||||||
assert "unwanted_subdirfile.gif" not in files
|
assert "unwanted_subdirfile.gif" not in files
|
||||||
assert "unwanted_subdarfile.png" in files
|
assert "unwanted_subdarfile.png" in files
|
||||||
@@ -454,14 +454,14 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
|
|||||||
self.d._exclude_list.rename(regex3, regex4)
|
self.d._exclude_list.rename(regex3, regex4)
|
||||||
assert self.d._exclude_list.error(regex4) is None
|
assert self.d._exclude_list.error(regex4) is None
|
||||||
p1["$Recycle.Bin"]["subdar"]["file_ending_with_subdir"].open("w").close()
|
p1["$Recycle.Bin"]["subdar"]["file_ending_with_subdir"].open("w").close()
|
||||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.Excluded)
|
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.EXCLUDED)
|
||||||
files = self.get_files_and_expect_num_result(4)
|
files = self.get_files_and_expect_num_result(4)
|
||||||
assert "file_ending_with_subdir" not in files
|
assert "file_ending_with_subdir" not in files
|
||||||
assert "somesubdarfile.jpeg" in files
|
assert "somesubdarfile.jpeg" in files
|
||||||
assert "somesubdirfile.png" not in files
|
assert "somesubdirfile.png" not in files
|
||||||
assert "unwanted_subdirfile.gif" not in files
|
assert "unwanted_subdirfile.gif" not in files
|
||||||
self.d.set_state(p1["$Recycle.Bin"]["subdir"], DirectoryState.Normal)
|
self.d.set_state(p1["$Recycle.Bin"]["subdir"], DirectoryState.NORMAL)
|
||||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.Normal)
|
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.NORMAL)
|
||||||
# print(f"get_folders(): {[x for x in self.d.get_folders()]}")
|
# print(f"get_folders(): {[x for x in self.d.get_folders()]}")
|
||||||
files = self.get_files_and_expect_num_result(6)
|
files = self.get_files_and_expect_num_result(6)
|
||||||
assert "file_ending_with_subdir" not in files
|
assert "file_ending_with_subdir" not in files
|
||||||
@@ -471,7 +471,7 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
|
|||||||
regex5 = r".*subdir.*"
|
regex5 = r".*subdir.*"
|
||||||
self.d._exclude_list.rename(regex4, regex5)
|
self.d._exclude_list.rename(regex4, regex5)
|
||||||
# Files containing substring should be filtered
|
# Files containing substring should be filtered
|
||||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.Normal)
|
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.NORMAL)
|
||||||
# The path should not match, only the filename, the "subdir" in the directory name shouldn't matter
|
# The path should not match, only the filename, the "subdir" in the directory name shouldn't matter
|
||||||
p1["$Recycle.Bin"]["subdir"]["file_which_shouldnt_match"].open("w").close()
|
p1["$Recycle.Bin"]["subdir"]["file_which_shouldnt_match"].open("w").close()
|
||||||
files = self.get_files_and_expect_num_result(5)
|
files = self.get_files_and_expect_num_result(5)
|
||||||
@@ -493,7 +493,7 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
|
|||||||
assert self.d._exclude_list.error(regex6) is None
|
assert self.d._exclude_list.error(regex6) is None
|
||||||
assert regex6 in self.d._exclude_list
|
assert regex6 in self.d._exclude_list
|
||||||
# This still should not be affected
|
# This still should not be affected
|
||||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.Normal)
|
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.NORMAL)
|
||||||
files = self.get_files_and_expect_num_result(5)
|
files = self.get_files_and_expect_num_result(5)
|
||||||
# These files are under the "/subdir" directory
|
# These files are under the "/subdir" directory
|
||||||
assert "somesubdirfile.png" not in files
|
assert "somesubdirfile.png" not in files
|
||||||
@@ -518,7 +518,7 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
|
|||||||
self.d._exclude_list.add(regex3)
|
self.d._exclude_list.add(regex3)
|
||||||
self.d._exclude_list.mark(regex3)
|
self.d._exclude_list.mark(regex3)
|
||||||
# print(f"get_folders(): {[x for x in self.d.get_folders()]}")
|
# print(f"get_folders(): {[x for x in self.d.get_folders()]}")
|
||||||
eq_(self.d.get_state(p1["$Recycle.Bin"]["思叫物語"]), DirectoryState.Excluded)
|
eq_(self.d.get_state(p1["$Recycle.Bin"]["思叫物語"]), DirectoryState.EXCLUDED)
|
||||||
files = self.get_files_and_expect_num_result(2)
|
files = self.get_files_and_expect_num_result(2)
|
||||||
assert "過去白濁物語~]_カラー.jpg" not in files
|
assert "過去白濁物語~]_カラー.jpg" not in files
|
||||||
assert "なししろ会う前" not in files
|
assert "なししろ会う前" not in files
|
||||||
@@ -527,7 +527,7 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
|
|||||||
regex4 = r".*物語$"
|
regex4 = r".*物語$"
|
||||||
self.d._exclude_list.rename(regex3, regex4)
|
self.d._exclude_list.rename(regex3, regex4)
|
||||||
assert self.d._exclude_list.error(regex4) is None
|
assert self.d._exclude_list.error(regex4) is None
|
||||||
self.d.set_state(p1["$Recycle.Bin"]["思叫物語"], DirectoryState.Normal)
|
self.d.set_state(p1["$Recycle.Bin"]["思叫物語"], DirectoryState.NORMAL)
|
||||||
files = self.get_files_and_expect_num_result(5)
|
files = self.get_files_and_expect_num_result(5)
|
||||||
assert "過去白濁物語~]_カラー.jpg" in files
|
assert "過去白濁物語~]_カラー.jpg" in files
|
||||||
assert "なししろ会う前" in files
|
assert "なししろ会う前" in files
|
||||||
@@ -546,8 +546,8 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
|
|||||||
p1["foobar"][".hidden_dir"][".hidden_subfile.png"].open("w").close()
|
p1["foobar"][".hidden_dir"][".hidden_subfile.png"].open("w").close()
|
||||||
self.d.add_path(p1["foobar"])
|
self.d.add_path(p1["foobar"])
|
||||||
# It should not inherit its parent's state originally
|
# It should not inherit its parent's state originally
|
||||||
eq_(self.d.get_state(p1["foobar"][".hidden_dir"]), DirectoryState.Excluded)
|
eq_(self.d.get_state(p1["foobar"][".hidden_dir"]), DirectoryState.EXCLUDED)
|
||||||
self.d.set_state(p1["foobar"][".hidden_dir"], DirectoryState.Normal)
|
self.d.set_state(p1["foobar"][".hidden_dir"], DirectoryState.NORMAL)
|
||||||
# The files should still be filtered
|
# The files should still be filtered
|
||||||
files = self.get_files_and_expect_num_result(1)
|
files = self.get_files_and_expect_num_result(1)
|
||||||
eq_(len(self.d._exclude_list.compiled_paths), 0)
|
eq_(len(self.d._exclude_list.compiled_paths), 0)
|
||||||
|
|||||||
@@ -103,10 +103,9 @@ class TestCasegetfields:
|
|||||||
expected = [["a", "bc", "def"]]
|
expected = [["a", "bc", "def"]]
|
||||||
actual = getfields(" - a bc def")
|
actual = getfields(" - a bc def")
|
||||||
eq_(expected, actual)
|
eq_(expected, actual)
|
||||||
expected = [["bc", "def"]]
|
|
||||||
|
|
||||||
|
|
||||||
class TestCaseunpack_fields:
|
class TestCaseUnpackFields:
|
||||||
def test_with_fields(self):
|
def test_with_fields(self):
|
||||||
expected = ["a", "b", "c", "d", "e", "f"]
|
expected = ["a", "b", "c", "d", "e", "f"]
|
||||||
actual = unpack_fields([["a"], ["b", "c"], ["d", "e", "f"]])
|
actual = unpack_fields([["a"], ["b", "c"], ["d", "e", "f"]])
|
||||||
@@ -218,24 +217,24 @@ class TestCaseWordCompareWithFields:
|
|||||||
eq_([["c", "d", "f"], ["a", "b"]], second)
|
eq_([["c", "d", "f"], ["a", "b"]], second)
|
||||||
|
|
||||||
|
|
||||||
class TestCasebuild_word_dict:
|
class TestCaseBuildWordDict:
|
||||||
def test_with_standard_words(self):
|
def test_with_standard_words(self):
|
||||||
itemList = [NamedObject("foo bar", True)]
|
item_list = [NamedObject("foo bar", True)]
|
||||||
itemList.append(NamedObject("bar baz", True))
|
item_list.append(NamedObject("bar baz", True))
|
||||||
itemList.append(NamedObject("baz bleh foo", True))
|
item_list.append(NamedObject("baz bleh foo", True))
|
||||||
d = build_word_dict(itemList)
|
d = build_word_dict(item_list)
|
||||||
eq_(4, len(d))
|
eq_(4, len(d))
|
||||||
eq_(2, len(d["foo"]))
|
eq_(2, len(d["foo"]))
|
||||||
assert itemList[0] in d["foo"]
|
assert item_list[0] in d["foo"]
|
||||||
assert itemList[2] in d["foo"]
|
assert item_list[2] in d["foo"]
|
||||||
eq_(2, len(d["bar"]))
|
eq_(2, len(d["bar"]))
|
||||||
assert itemList[0] in d["bar"]
|
assert item_list[0] in d["bar"]
|
||||||
assert itemList[1] in d["bar"]
|
assert item_list[1] in d["bar"]
|
||||||
eq_(2, len(d["baz"]))
|
eq_(2, len(d["baz"]))
|
||||||
assert itemList[1] in d["baz"]
|
assert item_list[1] in d["baz"]
|
||||||
assert itemList[2] in d["baz"]
|
assert item_list[2] in d["baz"]
|
||||||
eq_(1, len(d["bleh"]))
|
eq_(1, len(d["bleh"]))
|
||||||
assert itemList[2] in d["bleh"]
|
assert item_list[2] in d["bleh"]
|
||||||
|
|
||||||
def test_unpack_fields(self):
|
def test_unpack_fields(self):
|
||||||
o = NamedObject("")
|
o = NamedObject("")
|
||||||
@@ -269,7 +268,7 @@ class TestCasebuild_word_dict:
|
|||||||
eq_(100, self.log[1])
|
eq_(100, self.log[1])
|
||||||
|
|
||||||
|
|
||||||
class TestCasemerge_similar_words:
|
class TestCaseMergeSimilarWords:
|
||||||
def test_some_similar_words(self):
|
def test_some_similar_words(self):
|
||||||
d = {
|
d = {
|
||||||
"foobar": set([1]),
|
"foobar": set([1]),
|
||||||
@@ -281,11 +280,11 @@ class TestCasemerge_similar_words:
|
|||||||
eq_(3, len(d["foobar"]))
|
eq_(3, len(d["foobar"]))
|
||||||
|
|
||||||
|
|
||||||
class TestCasereduce_common_words:
|
class TestCaseReduceCommonWords:
|
||||||
def test_typical(self):
|
def test_typical(self):
|
||||||
d = {
|
d = {
|
||||||
"foo": set([NamedObject("foo bar", True) for i in range(50)]),
|
"foo": set([NamedObject("foo bar", True) for _ in range(50)]),
|
||||||
"bar": set([NamedObject("foo bar", True) for i in range(49)]),
|
"bar": set([NamedObject("foo bar", True) for _ in range(49)]),
|
||||||
}
|
}
|
||||||
reduce_common_words(d, 50)
|
reduce_common_words(d, 50)
|
||||||
assert "foo" not in d
|
assert "foo" not in d
|
||||||
@@ -293,7 +292,7 @@ class TestCasereduce_common_words:
|
|||||||
|
|
||||||
def test_dont_remove_objects_with_only_common_words(self):
|
def test_dont_remove_objects_with_only_common_words(self):
|
||||||
d = {
|
d = {
|
||||||
"common": set([NamedObject("common uncommon", True) for i in range(50)] + [NamedObject("common", True)]),
|
"common": set([NamedObject("common uncommon", True) for _ in range(50)] + [NamedObject("common", True)]),
|
||||||
"uncommon": set([NamedObject("common uncommon", True)]),
|
"uncommon": set([NamedObject("common uncommon", True)]),
|
||||||
}
|
}
|
||||||
reduce_common_words(d, 50)
|
reduce_common_words(d, 50)
|
||||||
@@ -302,20 +301,20 @@ class TestCasereduce_common_words:
|
|||||||
|
|
||||||
def test_values_still_are_set_instances(self):
|
def test_values_still_are_set_instances(self):
|
||||||
d = {
|
d = {
|
||||||
"common": set([NamedObject("common uncommon", True) for i in range(50)] + [NamedObject("common", True)]),
|
"common": set([NamedObject("common uncommon", True) for _ in range(50)] + [NamedObject("common", True)]),
|
||||||
"uncommon": set([NamedObject("common uncommon", True)]),
|
"uncommon": set([NamedObject("common uncommon", True)]),
|
||||||
}
|
}
|
||||||
reduce_common_words(d, 50)
|
reduce_common_words(d, 50)
|
||||||
assert isinstance(d["common"], set)
|
assert isinstance(d["common"], set)
|
||||||
assert isinstance(d["uncommon"], set)
|
assert isinstance(d["uncommon"], set)
|
||||||
|
|
||||||
def test_dont_raise_KeyError_when_a_word_has_been_removed(self):
|
def test_dont_raise_keyerror_when_a_word_has_been_removed(self):
|
||||||
# If a word has been removed by the reduce, an object in a subsequent common word that
|
# If a word has been removed by the reduce, an object in a subsequent common word that
|
||||||
# contains the word that has been removed would cause a KeyError.
|
# contains the word that has been removed would cause a KeyError.
|
||||||
d = {
|
d = {
|
||||||
"foo": set([NamedObject("foo bar baz", True) for i in range(50)]),
|
"foo": set([NamedObject("foo bar baz", True) for _ in range(50)]),
|
||||||
"bar": set([NamedObject("foo bar baz", True) for i in range(50)]),
|
"bar": set([NamedObject("foo bar baz", True) for _ in range(50)]),
|
||||||
"baz": set([NamedObject("foo bar baz", True) for i in range(49)]),
|
"baz": set([NamedObject("foo bar baz", True) for _ in range(49)]),
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
reduce_common_words(d, 50)
|
reduce_common_words(d, 50)
|
||||||
@@ -329,7 +328,7 @@ class TestCasereduce_common_words:
|
|||||||
o.words = [["foo", "bar"], ["baz"]]
|
o.words = [["foo", "bar"], ["baz"]]
|
||||||
return o
|
return o
|
||||||
|
|
||||||
d = {"foo": set([create_it() for i in range(50)])}
|
d = {"foo": set([create_it() for _ in range(50)])}
|
||||||
try:
|
try:
|
||||||
reduce_common_words(d, 50)
|
reduce_common_words(d, 50)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
@@ -342,9 +341,9 @@ class TestCasereduce_common_words:
|
|||||||
# would not stay in 'bar' because 'foo' is not a common word anymore.
|
# would not stay in 'bar' because 'foo' is not a common word anymore.
|
||||||
only_common = NamedObject("foo bar", True)
|
only_common = NamedObject("foo bar", True)
|
||||||
d = {
|
d = {
|
||||||
"foo": set([NamedObject("foo bar baz", True) for i in range(49)] + [only_common]),
|
"foo": set([NamedObject("foo bar baz", True) for _ in range(49)] + [only_common]),
|
||||||
"bar": set([NamedObject("foo bar baz", True) for i in range(49)] + [only_common]),
|
"bar": set([NamedObject("foo bar baz", True) for _ in range(49)] + [only_common]),
|
||||||
"baz": set([NamedObject("foo bar baz", True) for i in range(49)]),
|
"baz": set([NamedObject("foo bar baz", True) for _ in range(49)]),
|
||||||
}
|
}
|
||||||
reduce_common_words(d, 50)
|
reduce_common_words(d, 50)
|
||||||
eq_(1, len(d["foo"]))
|
eq_(1, len(d["foo"]))
|
||||||
@@ -352,7 +351,7 @@ class TestCasereduce_common_words:
|
|||||||
eq_(49, len(d["baz"]))
|
eq_(49, len(d["baz"]))
|
||||||
|
|
||||||
|
|
||||||
class TestCaseget_match:
|
class TestCaseGetMatch:
|
||||||
def test_simple(self):
|
def test_simple(self):
|
||||||
o1 = NamedObject("foo bar", True)
|
o1 = NamedObject("foo bar", True)
|
||||||
o2 = NamedObject("bar bleh", True)
|
o2 = NamedObject("bar bleh", True)
|
||||||
@@ -381,12 +380,12 @@ class TestCaseGetMatches:
|
|||||||
eq_(getmatches([]), [])
|
eq_(getmatches([]), [])
|
||||||
|
|
||||||
def test_simple(self):
|
def test_simple(self):
|
||||||
itemList = [
|
item_list = [
|
||||||
NamedObject("foo bar"),
|
NamedObject("foo bar"),
|
||||||
NamedObject("bar bleh"),
|
NamedObject("bar bleh"),
|
||||||
NamedObject("a b c foo"),
|
NamedObject("a b c foo"),
|
||||||
]
|
]
|
||||||
r = getmatches(itemList)
|
r = getmatches(item_list)
|
||||||
eq_(2, len(r))
|
eq_(2, len(r))
|
||||||
m = first(m for m in r if m.percentage == 50) # "foo bar" and "bar bleh"
|
m = first(m for m in r if m.percentage == 50) # "foo bar" and "bar bleh"
|
||||||
assert_match(m, "foo bar", "bar bleh")
|
assert_match(m, "foo bar", "bar bleh")
|
||||||
@@ -394,40 +393,40 @@ class TestCaseGetMatches:
|
|||||||
assert_match(m, "foo bar", "a b c foo")
|
assert_match(m, "foo bar", "a b c foo")
|
||||||
|
|
||||||
def test_null_and_unrelated_objects(self):
|
def test_null_and_unrelated_objects(self):
|
||||||
itemList = [
|
item_list = [
|
||||||
NamedObject("foo bar"),
|
NamedObject("foo bar"),
|
||||||
NamedObject("bar bleh"),
|
NamedObject("bar bleh"),
|
||||||
NamedObject(""),
|
NamedObject(""),
|
||||||
NamedObject("unrelated object"),
|
NamedObject("unrelated object"),
|
||||||
]
|
]
|
||||||
r = getmatches(itemList)
|
r = getmatches(item_list)
|
||||||
eq_(len(r), 1)
|
eq_(len(r), 1)
|
||||||
m = r[0]
|
m = r[0]
|
||||||
eq_(m.percentage, 50)
|
eq_(m.percentage, 50)
|
||||||
assert_match(m, "foo bar", "bar bleh")
|
assert_match(m, "foo bar", "bar bleh")
|
||||||
|
|
||||||
def test_twice_the_same_word(self):
|
def test_twice_the_same_word(self):
|
||||||
itemList = [NamedObject("foo foo bar"), NamedObject("bar bleh")]
|
item_list = [NamedObject("foo foo bar"), NamedObject("bar bleh")]
|
||||||
r = getmatches(itemList)
|
r = getmatches(item_list)
|
||||||
eq_(1, len(r))
|
eq_(1, len(r))
|
||||||
|
|
||||||
def test_twice_the_same_word_when_preworded(self):
|
def test_twice_the_same_word_when_preworded(self):
|
||||||
itemList = [NamedObject("foo foo bar", True), NamedObject("bar bleh", True)]
|
item_list = [NamedObject("foo foo bar", True), NamedObject("bar bleh", True)]
|
||||||
r = getmatches(itemList)
|
r = getmatches(item_list)
|
||||||
eq_(1, len(r))
|
eq_(1, len(r))
|
||||||
|
|
||||||
def test_two_words_match(self):
|
def test_two_words_match(self):
|
||||||
itemList = [NamedObject("foo bar"), NamedObject("foo bar bleh")]
|
item_list = [NamedObject("foo bar"), NamedObject("foo bar bleh")]
|
||||||
r = getmatches(itemList)
|
r = getmatches(item_list)
|
||||||
eq_(1, len(r))
|
eq_(1, len(r))
|
||||||
|
|
||||||
def test_match_files_with_only_common_words(self):
|
def test_match_files_with_only_common_words(self):
|
||||||
# If a word occurs more than 50 times, it is excluded from the matching process
|
# If a word occurs more than 50 times, it is excluded from the matching process
|
||||||
# The problem with the common_word_threshold is that the files containing only common
|
# The problem with the common_word_threshold is that the files containing only common
|
||||||
# words will never be matched together. We *should* match them.
|
# words will never be matched together. We *should* match them.
|
||||||
# This test assumes that the common word threashold const is 50
|
# This test assumes that the common word threshold const is 50
|
||||||
itemList = [NamedObject("foo") for i in range(50)]
|
item_list = [NamedObject("foo") for _ in range(50)]
|
||||||
r = getmatches(itemList)
|
r = getmatches(item_list)
|
||||||
eq_(1225, len(r))
|
eq_(1225, len(r))
|
||||||
|
|
||||||
def test_use_words_already_there_if_there(self):
|
def test_use_words_already_there_if_there(self):
|
||||||
@@ -450,28 +449,28 @@ class TestCaseGetMatches:
|
|||||||
eq_(100, self.log[-1])
|
eq_(100, self.log[-1])
|
||||||
|
|
||||||
def test_weight_words(self):
|
def test_weight_words(self):
|
||||||
itemList = [NamedObject("foo bar"), NamedObject("bar bleh")]
|
item_list = [NamedObject("foo bar"), NamedObject("bar bleh")]
|
||||||
m = getmatches(itemList, weight_words=True)[0]
|
m = getmatches(item_list, weight_words=True)[0]
|
||||||
eq_(int((6.0 / 13.0) * 100), m.percentage)
|
eq_(int((6.0 / 13.0) * 100), m.percentage)
|
||||||
|
|
||||||
def test_similar_word(self):
|
def test_similar_word(self):
|
||||||
itemList = [NamedObject("foobar"), NamedObject("foobars")]
|
item_list = [NamedObject("foobar"), NamedObject("foobars")]
|
||||||
eq_(len(getmatches(itemList, match_similar_words=True)), 1)
|
eq_(len(getmatches(item_list, match_similar_words=True)), 1)
|
||||||
eq_(getmatches(itemList, match_similar_words=True)[0].percentage, 100)
|
eq_(getmatches(item_list, match_similar_words=True)[0].percentage, 100)
|
||||||
itemList = [NamedObject("foobar"), NamedObject("foo")]
|
item_list = [NamedObject("foobar"), NamedObject("foo")]
|
||||||
eq_(len(getmatches(itemList, match_similar_words=True)), 0) # too far
|
eq_(len(getmatches(item_list, match_similar_words=True)), 0) # too far
|
||||||
itemList = [NamedObject("bizkit"), NamedObject("bizket")]
|
item_list = [NamedObject("bizkit"), NamedObject("bizket")]
|
||||||
eq_(len(getmatches(itemList, match_similar_words=True)), 1)
|
eq_(len(getmatches(item_list, match_similar_words=True)), 1)
|
||||||
itemList = [NamedObject("foobar"), NamedObject("foosbar")]
|
item_list = [NamedObject("foobar"), NamedObject("foosbar")]
|
||||||
eq_(len(getmatches(itemList, match_similar_words=True)), 1)
|
eq_(len(getmatches(item_list, match_similar_words=True)), 1)
|
||||||
|
|
||||||
def test_single_object_with_similar_words(self):
|
def test_single_object_with_similar_words(self):
|
||||||
itemList = [NamedObject("foo foos")]
|
item_list = [NamedObject("foo foos")]
|
||||||
eq_(len(getmatches(itemList, match_similar_words=True)), 0)
|
eq_(len(getmatches(item_list, match_similar_words=True)), 0)
|
||||||
|
|
||||||
def test_double_words_get_counted_only_once(self):
|
def test_double_words_get_counted_only_once(self):
|
||||||
itemList = [NamedObject("foo bar foo bleh"), NamedObject("foo bar bleh bar")]
|
item_list = [NamedObject("foo bar foo bleh"), NamedObject("foo bar bleh bar")]
|
||||||
m = getmatches(itemList)[0]
|
m = getmatches(item_list)[0]
|
||||||
eq_(75, m.percentage)
|
eq_(75, m.percentage)
|
||||||
|
|
||||||
def test_with_fields(self):
|
def test_with_fields(self):
|
||||||
@@ -491,13 +490,13 @@ class TestCaseGetMatches:
|
|||||||
eq_(m.percentage, 50)
|
eq_(m.percentage, 50)
|
||||||
|
|
||||||
def test_only_match_similar_when_the_option_is_set(self):
|
def test_only_match_similar_when_the_option_is_set(self):
|
||||||
itemList = [NamedObject("foobar"), NamedObject("foobars")]
|
item_list = [NamedObject("foobar"), NamedObject("foobars")]
|
||||||
eq_(len(getmatches(itemList, match_similar_words=False)), 0)
|
eq_(len(getmatches(item_list, match_similar_words=False)), 0)
|
||||||
|
|
||||||
def test_dont_recurse_do_match(self):
|
def test_dont_recurse_do_match(self):
|
||||||
# with nosetests, the stack is increased. The number has to be high enough not to be failing falsely
|
# with nosetests, the stack is increased. The number has to be high enough not to be failing falsely
|
||||||
sys.setrecursionlimit(200)
|
sys.setrecursionlimit(200)
|
||||||
files = [NamedObject("foo bar") for i in range(201)]
|
files = [NamedObject("foo bar") for _ in range(201)]
|
||||||
try:
|
try:
|
||||||
getmatches(files)
|
getmatches(files)
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
@@ -506,35 +505,31 @@ class TestCaseGetMatches:
|
|||||||
sys.setrecursionlimit(1000)
|
sys.setrecursionlimit(1000)
|
||||||
|
|
||||||
def test_min_match_percentage(self):
|
def test_min_match_percentage(self):
|
||||||
itemList = [
|
item_list = [
|
||||||
NamedObject("foo bar"),
|
NamedObject("foo bar"),
|
||||||
NamedObject("bar bleh"),
|
NamedObject("bar bleh"),
|
||||||
NamedObject("a b c foo"),
|
NamedObject("a b c foo"),
|
||||||
]
|
]
|
||||||
r = getmatches(itemList, min_match_percentage=50)
|
r = getmatches(item_list, min_match_percentage=50)
|
||||||
eq_(1, len(r)) # Only "foo bar" / "bar bleh" should match
|
eq_(1, len(r)) # Only "foo bar" / "bar bleh" should match
|
||||||
|
|
||||||
def test_MemoryError(self, monkeypatch):
|
def test_memory_error(self, monkeypatch):
|
||||||
@log_calls
|
@log_calls
|
||||||
def mocked_match(first, second, flags):
|
def mocked_match(first, second, flags):
|
||||||
if len(mocked_match.calls) > 42:
|
if len(mocked_match.calls) > 42:
|
||||||
raise MemoryError()
|
raise MemoryError()
|
||||||
return Match(first, second, 0)
|
return Match(first, second, 0)
|
||||||
|
|
||||||
objects = [NamedObject() for i in range(10)] # results in 45 matches
|
objects = [NamedObject() for _ in range(10)] # results in 45 matches
|
||||||
monkeypatch.setattr(engine, "get_match", mocked_match)
|
monkeypatch.setattr(engine, "get_match", mocked_match)
|
||||||
try:
|
try:
|
||||||
r = getmatches(objects)
|
r = getmatches(objects)
|
||||||
except MemoryError:
|
except MemoryError:
|
||||||
self.fail("MemorryError must be handled")
|
self.fail("MemoryError must be handled")
|
||||||
eq_(42, len(r))
|
eq_(42, len(r))
|
||||||
|
|
||||||
|
|
||||||
class TestCaseGetMatchesByContents:
|
class TestCaseGetMatchesByContents:
|
||||||
def test_dont_compare_empty_files(self):
|
|
||||||
o1, o2 = no(size=0), no(size=0)
|
|
||||||
assert not getmatches_by_contents([o1, o2])
|
|
||||||
|
|
||||||
def test_big_file_partial_hashes(self):
|
def test_big_file_partial_hashes(self):
|
||||||
smallsize = 1
|
smallsize = 1
|
||||||
bigsize = 100 * 1024 * 1024 # 100MB
|
bigsize = 100 * 1024 * 1024 # 100MB
|
||||||
@@ -563,7 +558,7 @@ class TestCaseGetMatchesByContents:
|
|||||||
|
|
||||||
|
|
||||||
class TestCaseGroup:
|
class TestCaseGroup:
|
||||||
def test_empy(self):
|
def test_empty(self):
|
||||||
g = Group()
|
g = Group()
|
||||||
eq_(None, g.ref)
|
eq_(None, g.ref)
|
||||||
eq_([], g.dupes)
|
eq_([], g.dupes)
|
||||||
@@ -802,14 +797,14 @@ class TestCaseGroup:
|
|||||||
eq_(0, len(g.candidates))
|
eq_(0, len(g.candidates))
|
||||||
|
|
||||||
|
|
||||||
class TestCaseget_groups:
|
class TestCaseGetGroups:
|
||||||
def test_empty(self):
|
def test_empty(self):
|
||||||
r = get_groups([])
|
r = get_groups([])
|
||||||
eq_([], r)
|
eq_([], r)
|
||||||
|
|
||||||
def test_simple(self):
|
def test_simple(self):
|
||||||
itemList = [NamedObject("foo bar"), NamedObject("bar bleh")]
|
item_list = [NamedObject("foo bar"), NamedObject("bar bleh")]
|
||||||
matches = getmatches(itemList)
|
matches = getmatches(item_list)
|
||||||
m = matches[0]
|
m = matches[0]
|
||||||
r = get_groups(matches)
|
r = get_groups(matches)
|
||||||
eq_(1, len(r))
|
eq_(1, len(r))
|
||||||
@@ -819,15 +814,15 @@ class TestCaseget_groups:
|
|||||||
|
|
||||||
def test_group_with_multiple_matches(self):
|
def test_group_with_multiple_matches(self):
|
||||||
# This results in 3 matches
|
# This results in 3 matches
|
||||||
itemList = [NamedObject("foo"), NamedObject("foo"), NamedObject("foo")]
|
item_list = [NamedObject("foo"), NamedObject("foo"), NamedObject("foo")]
|
||||||
matches = getmatches(itemList)
|
matches = getmatches(item_list)
|
||||||
r = get_groups(matches)
|
r = get_groups(matches)
|
||||||
eq_(1, len(r))
|
eq_(1, len(r))
|
||||||
g = r[0]
|
g = r[0]
|
||||||
eq_(3, len(g))
|
eq_(3, len(g))
|
||||||
|
|
||||||
def test_must_choose_a_group(self):
|
def test_must_choose_a_group(self):
|
||||||
itemList = [
|
item_list = [
|
||||||
NamedObject("a b"),
|
NamedObject("a b"),
|
||||||
NamedObject("a b"),
|
NamedObject("a b"),
|
||||||
NamedObject("b c"),
|
NamedObject("b c"),
|
||||||
@@ -836,13 +831,13 @@ class TestCaseget_groups:
|
|||||||
]
|
]
|
||||||
# There will be 2 groups here: group "a b" and group "c d"
|
# There will be 2 groups here: group "a b" and group "c d"
|
||||||
# "b c" can go either of them, but not both.
|
# "b c" can go either of them, but not both.
|
||||||
matches = getmatches(itemList)
|
matches = getmatches(item_list)
|
||||||
r = get_groups(matches)
|
r = get_groups(matches)
|
||||||
eq_(2, len(r))
|
eq_(2, len(r))
|
||||||
eq_(5, len(r[0]) + len(r[1]))
|
eq_(5, len(r[0]) + len(r[1]))
|
||||||
|
|
||||||
def test_should_all_go_in_the_same_group(self):
|
def test_should_all_go_in_the_same_group(self):
|
||||||
itemList = [
|
item_list = [
|
||||||
NamedObject("a b"),
|
NamedObject("a b"),
|
||||||
NamedObject("a b"),
|
NamedObject("a b"),
|
||||||
NamedObject("a b"),
|
NamedObject("a b"),
|
||||||
@@ -850,7 +845,7 @@ class TestCaseget_groups:
|
|||||||
]
|
]
|
||||||
# There will be 2 groups here: group "a b" and group "c d"
|
# There will be 2 groups here: group "a b" and group "c d"
|
||||||
# "b c" can fit in both, but it must be in only one of them
|
# "b c" can fit in both, but it must be in only one of them
|
||||||
matches = getmatches(itemList)
|
matches = getmatches(item_list)
|
||||||
r = get_groups(matches)
|
r = get_groups(matches)
|
||||||
eq_(1, len(r))
|
eq_(1, len(r))
|
||||||
|
|
||||||
@@ -869,8 +864,8 @@ class TestCaseget_groups:
|
|||||||
assert o3 in g
|
assert o3 in g
|
||||||
|
|
||||||
def test_four_sized_group(self):
|
def test_four_sized_group(self):
|
||||||
itemList = [NamedObject("foobar") for i in range(4)]
|
item_list = [NamedObject("foobar") for _ in range(4)]
|
||||||
m = getmatches(itemList)
|
m = getmatches(item_list)
|
||||||
r = get_groups(m)
|
r = get_groups(m)
|
||||||
eq_(1, len(r))
|
eq_(1, len(r))
|
||||||
eq_(4, len(r[0]))
|
eq_(4, len(r[0]))
|
||||||
|
|||||||
@@ -5,12 +5,8 @@
|
|||||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
|
||||||
# import os.path as op
|
|
||||||
|
|
||||||
from xml.etree import ElementTree as ET
|
from xml.etree import ElementTree as ET
|
||||||
|
|
||||||
# from pytest import raises
|
|
||||||
from hscommon.testutil import eq_
|
from hscommon.testutil import eq_
|
||||||
from hscommon.plat import ISWINDOWS
|
from hscommon.plat import ISWINDOWS
|
||||||
|
|
||||||
@@ -144,11 +140,7 @@ class TestCaseListEmpty:
|
|||||||
def test_force_add_not_compilable(self):
|
def test_force_add_not_compilable(self):
|
||||||
"""Used when loading from XML for example"""
|
"""Used when loading from XML for example"""
|
||||||
regex = r"one))"
|
regex = r"one))"
|
||||||
try:
|
|
||||||
self.exclude_list.add(regex, forced=True)
|
self.exclude_list.add(regex, forced=True)
|
||||||
except Exception as e:
|
|
||||||
# Should not get an exception here unless it's a duplicate regex
|
|
||||||
raise e
|
|
||||||
marked = self.exclude_list.mark(regex)
|
marked = self.exclude_list.mark(regex)
|
||||||
eq_(marked, False) # can't be marked since not compilable
|
eq_(marked, False) # can't be marked since not compilable
|
||||||
eq_(len(self.exclude_list), 1)
|
eq_(len(self.exclude_list), 1)
|
||||||
@@ -232,7 +224,6 @@ class TestCaseListEmpty:
|
|||||||
found = True
|
found = True
|
||||||
if not found:
|
if not found:
|
||||||
raise (Exception(f"Default RE {re} not found in compiled list."))
|
raise (Exception(f"Default RE {re} not found in compiled list."))
|
||||||
continue
|
|
||||||
eq_(len(default_regexes), len(self.exclude_list.compiled))
|
eq_(len(default_regexes), len(self.exclude_list.compiled))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,54 +16,54 @@ from ..ignore import IgnoreList
|
|||||||
def test_empty():
|
def test_empty():
|
||||||
il = IgnoreList()
|
il = IgnoreList()
|
||||||
eq_(0, len(il))
|
eq_(0, len(il))
|
||||||
assert not il.AreIgnored("foo", "bar")
|
assert not il.are_ignored("foo", "bar")
|
||||||
|
|
||||||
|
|
||||||
def test_simple():
|
def test_simple():
|
||||||
il = IgnoreList()
|
il = IgnoreList()
|
||||||
il.Ignore("foo", "bar")
|
il.ignore("foo", "bar")
|
||||||
assert il.AreIgnored("foo", "bar")
|
assert il.are_ignored("foo", "bar")
|
||||||
assert il.AreIgnored("bar", "foo")
|
assert il.are_ignored("bar", "foo")
|
||||||
assert not il.AreIgnored("foo", "bleh")
|
assert not il.are_ignored("foo", "bleh")
|
||||||
assert not il.AreIgnored("bleh", "bar")
|
assert not il.are_ignored("bleh", "bar")
|
||||||
eq_(1, len(il))
|
eq_(1, len(il))
|
||||||
|
|
||||||
|
|
||||||
def test_multiple():
|
def test_multiple():
|
||||||
il = IgnoreList()
|
il = IgnoreList()
|
||||||
il.Ignore("foo", "bar")
|
il.ignore("foo", "bar")
|
||||||
il.Ignore("foo", "bleh")
|
il.ignore("foo", "bleh")
|
||||||
il.Ignore("bleh", "bar")
|
il.ignore("bleh", "bar")
|
||||||
il.Ignore("aybabtu", "bleh")
|
il.ignore("aybabtu", "bleh")
|
||||||
assert il.AreIgnored("foo", "bar")
|
assert il.are_ignored("foo", "bar")
|
||||||
assert il.AreIgnored("bar", "foo")
|
assert il.are_ignored("bar", "foo")
|
||||||
assert il.AreIgnored("foo", "bleh")
|
assert il.are_ignored("foo", "bleh")
|
||||||
assert il.AreIgnored("bleh", "bar")
|
assert il.are_ignored("bleh", "bar")
|
||||||
assert not il.AreIgnored("aybabtu", "bar")
|
assert not il.are_ignored("aybabtu", "bar")
|
||||||
eq_(4, len(il))
|
eq_(4, len(il))
|
||||||
|
|
||||||
|
|
||||||
def test_clear():
|
def test_clear():
|
||||||
il = IgnoreList()
|
il = IgnoreList()
|
||||||
il.Ignore("foo", "bar")
|
il.ignore("foo", "bar")
|
||||||
il.Clear()
|
il.clear()
|
||||||
assert not il.AreIgnored("foo", "bar")
|
assert not il.are_ignored("foo", "bar")
|
||||||
assert not il.AreIgnored("bar", "foo")
|
assert not il.are_ignored("bar", "foo")
|
||||||
eq_(0, len(il))
|
eq_(0, len(il))
|
||||||
|
|
||||||
|
|
||||||
def test_add_same_twice():
|
def test_add_same_twice():
|
||||||
il = IgnoreList()
|
il = IgnoreList()
|
||||||
il.Ignore("foo", "bar")
|
il.ignore("foo", "bar")
|
||||||
il.Ignore("bar", "foo")
|
il.ignore("bar", "foo")
|
||||||
eq_(1, len(il))
|
eq_(1, len(il))
|
||||||
|
|
||||||
|
|
||||||
def test_save_to_xml():
|
def test_save_to_xml():
|
||||||
il = IgnoreList()
|
il = IgnoreList()
|
||||||
il.Ignore("foo", "bar")
|
il.ignore("foo", "bar")
|
||||||
il.Ignore("foo", "bleh")
|
il.ignore("foo", "bleh")
|
||||||
il.Ignore("bleh", "bar")
|
il.ignore("bleh", "bar")
|
||||||
f = io.BytesIO()
|
f = io.BytesIO()
|
||||||
il.save_to_xml(f)
|
il.save_to_xml(f)
|
||||||
f.seek(0)
|
f.seek(0)
|
||||||
@@ -77,22 +77,22 @@ def test_save_to_xml():
|
|||||||
eq_(len(subchildren), 3)
|
eq_(len(subchildren), 3)
|
||||||
|
|
||||||
|
|
||||||
def test_SaveThenLoad():
|
def test_save_then_load():
|
||||||
il = IgnoreList()
|
il = IgnoreList()
|
||||||
il.Ignore("foo", "bar")
|
il.ignore("foo", "bar")
|
||||||
il.Ignore("foo", "bleh")
|
il.ignore("foo", "bleh")
|
||||||
il.Ignore("bleh", "bar")
|
il.ignore("bleh", "bar")
|
||||||
il.Ignore("\u00e9", "bar")
|
il.ignore("\u00e9", "bar")
|
||||||
f = io.BytesIO()
|
f = io.BytesIO()
|
||||||
il.save_to_xml(f)
|
il.save_to_xml(f)
|
||||||
f.seek(0)
|
f.seek(0)
|
||||||
il = IgnoreList()
|
il = IgnoreList()
|
||||||
il.load_from_xml(f)
|
il.load_from_xml(f)
|
||||||
eq_(4, len(il))
|
eq_(4, len(il))
|
||||||
assert il.AreIgnored("\u00e9", "bar")
|
assert il.are_ignored("\u00e9", "bar")
|
||||||
|
|
||||||
|
|
||||||
def test_LoadXML_with_empty_file_tags():
|
def test_load_xml_with_empty_file_tags():
|
||||||
f = io.BytesIO()
|
f = io.BytesIO()
|
||||||
f.write(b'<?xml version="1.0" encoding="utf-8"?><ignore_list><file><file/></file></ignore_list>')
|
f.write(b'<?xml version="1.0" encoding="utf-8"?><ignore_list><file><file/></file></ignore_list>')
|
||||||
f.seek(0)
|
f.seek(0)
|
||||||
@@ -101,18 +101,18 @@ def test_LoadXML_with_empty_file_tags():
|
|||||||
eq_(0, len(il))
|
eq_(0, len(il))
|
||||||
|
|
||||||
|
|
||||||
def test_AreIgnore_works_when_a_child_is_a_key_somewhere_else():
|
def test_are_ignore_works_when_a_child_is_a_key_somewhere_else():
|
||||||
il = IgnoreList()
|
il = IgnoreList()
|
||||||
il.Ignore("foo", "bar")
|
il.ignore("foo", "bar")
|
||||||
il.Ignore("bar", "baz")
|
il.ignore("bar", "baz")
|
||||||
assert il.AreIgnored("bar", "foo")
|
assert il.are_ignored("bar", "foo")
|
||||||
|
|
||||||
|
|
||||||
def test_no_dupes_when_a_child_is_a_key_somewhere_else():
|
def test_no_dupes_when_a_child_is_a_key_somewhere_else():
|
||||||
il = IgnoreList()
|
il = IgnoreList()
|
||||||
il.Ignore("foo", "bar")
|
il.ignore("foo", "bar")
|
||||||
il.Ignore("bar", "baz")
|
il.ignore("bar", "baz")
|
||||||
il.Ignore("bar", "foo")
|
il.ignore("bar", "foo")
|
||||||
eq_(2, len(il))
|
eq_(2, len(il))
|
||||||
|
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ def test_iterate():
|
|||||||
il = IgnoreList()
|
il = IgnoreList()
|
||||||
expected = [("foo", "bar"), ("bar", "baz"), ("foo", "baz")]
|
expected = [("foo", "bar"), ("bar", "baz"), ("foo", "baz")]
|
||||||
for i in expected:
|
for i in expected:
|
||||||
il.Ignore(i[0], i[1])
|
il.ignore(i[0], i[1])
|
||||||
for i in il:
|
for i in il:
|
||||||
expected.remove(i) # No exception should be raised
|
expected.remove(i) # No exception should be raised
|
||||||
assert not expected # expected should be empty
|
assert not expected # expected should be empty
|
||||||
@@ -129,18 +129,18 @@ def test_iterate():
|
|||||||
|
|
||||||
def test_filter():
|
def test_filter():
|
||||||
il = IgnoreList()
|
il = IgnoreList()
|
||||||
il.Ignore("foo", "bar")
|
il.ignore("foo", "bar")
|
||||||
il.Ignore("bar", "baz")
|
il.ignore("bar", "baz")
|
||||||
il.Ignore("foo", "baz")
|
il.ignore("foo", "baz")
|
||||||
il.Filter(lambda f, s: f == "bar")
|
il.filter(lambda f, s: f == "bar")
|
||||||
eq_(1, len(il))
|
eq_(1, len(il))
|
||||||
assert not il.AreIgnored("foo", "bar")
|
assert not il.are_ignored("foo", "bar")
|
||||||
assert il.AreIgnored("bar", "baz")
|
assert il.are_ignored("bar", "baz")
|
||||||
|
|
||||||
|
|
||||||
def test_save_with_non_ascii_items():
|
def test_save_with_non_ascii_items():
|
||||||
il = IgnoreList()
|
il = IgnoreList()
|
||||||
il.Ignore("\xac", "\xbf")
|
il.ignore("\xac", "\xbf")
|
||||||
f = io.BytesIO()
|
f = io.BytesIO()
|
||||||
try:
|
try:
|
||||||
il.save_to_xml(f)
|
il.save_to_xml(f)
|
||||||
@@ -151,29 +151,29 @@ def test_save_with_non_ascii_items():
|
|||||||
def test_len():
|
def test_len():
|
||||||
il = IgnoreList()
|
il = IgnoreList()
|
||||||
eq_(0, len(il))
|
eq_(0, len(il))
|
||||||
il.Ignore("foo", "bar")
|
il.ignore("foo", "bar")
|
||||||
eq_(1, len(il))
|
eq_(1, len(il))
|
||||||
|
|
||||||
|
|
||||||
def test_nonzero():
|
def test_nonzero():
|
||||||
il = IgnoreList()
|
il = IgnoreList()
|
||||||
assert not il
|
assert not il
|
||||||
il.Ignore("foo", "bar")
|
il.ignore("foo", "bar")
|
||||||
assert il
|
assert il
|
||||||
|
|
||||||
|
|
||||||
def test_remove():
|
def test_remove():
|
||||||
il = IgnoreList()
|
il = IgnoreList()
|
||||||
il.Ignore("foo", "bar")
|
il.ignore("foo", "bar")
|
||||||
il.Ignore("foo", "baz")
|
il.ignore("foo", "baz")
|
||||||
il.remove("bar", "foo")
|
il.remove("bar", "foo")
|
||||||
eq_(len(il), 1)
|
eq_(len(il), 1)
|
||||||
assert not il.AreIgnored("foo", "bar")
|
assert not il.are_ignored("foo", "bar")
|
||||||
|
|
||||||
|
|
||||||
def test_remove_non_existant():
|
def test_remove_non_existant():
|
||||||
il = IgnoreList()
|
il = IgnoreList()
|
||||||
il.Ignore("foo", "bar")
|
il.ignore("foo", "bar")
|
||||||
il.Ignore("foo", "baz")
|
il.ignore("foo", "baz")
|
||||||
with raises(ValueError):
|
with raises(ValueError):
|
||||||
il.remove("foo", "bleh")
|
il.remove("foo", "bleh")
|
||||||
|
|||||||
@@ -402,7 +402,7 @@ class TestCaseResultsMarkings:
|
|||||||
self.results.make_ref(d)
|
self.results.make_ref(d)
|
||||||
eq_("0 / 3 (0.00 B / 3.00 B) duplicates marked.", self.results.stat_line)
|
eq_("0 / 3 (0.00 B / 3.00 B) duplicates marked.", self.results.stat_line)
|
||||||
|
|
||||||
def test_SaveXML(self):
|
def test_save_xml(self):
|
||||||
self.results.mark(self.objects[1])
|
self.results.mark(self.objects[1])
|
||||||
self.results.mark_invert()
|
self.results.mark_invert()
|
||||||
f = io.BytesIO()
|
f = io.BytesIO()
|
||||||
@@ -419,7 +419,7 @@ class TestCaseResultsMarkings:
|
|||||||
eq_("n", d1.get("marked"))
|
eq_("n", d1.get("marked"))
|
||||||
eq_("y", d2.get("marked"))
|
eq_("y", d2.get("marked"))
|
||||||
|
|
||||||
def test_LoadXML(self):
|
def test_load_xml(self):
|
||||||
def get_file(path):
|
def get_file(path):
|
||||||
return [f for f in self.objects if str(f.path) == path][0]
|
return [f for f in self.objects if str(f.path) == path][0]
|
||||||
|
|
||||||
@@ -485,7 +485,7 @@ class TestCaseResultsXML:
|
|||||||
eq_("ibabtu", d1.get("words"))
|
eq_("ibabtu", d1.get("words"))
|
||||||
eq_("ibabtu", d2.get("words"))
|
eq_("ibabtu", d2.get("words"))
|
||||||
|
|
||||||
def test_LoadXML(self):
|
def test_load_xml(self):
|
||||||
def get_file(path):
|
def get_file(path):
|
||||||
return [f for f in self.objects if str(f.path) == path][0]
|
return [f for f in self.objects if str(f.path) == path][0]
|
||||||
|
|
||||||
@@ -517,7 +517,7 @@ class TestCaseResultsXML:
|
|||||||
eq_(["ibabtu"], g2[0].words)
|
eq_(["ibabtu"], g2[0].words)
|
||||||
eq_(["ibabtu"], g2[1].words)
|
eq_(["ibabtu"], g2[1].words)
|
||||||
|
|
||||||
def test_LoadXML_with_filename(self, tmpdir):
|
def test_load_xml_with_filename(self, tmpdir):
|
||||||
def get_file(path):
|
def get_file(path):
|
||||||
return [f for f in self.objects if str(f.path) == path][0]
|
return [f for f in self.objects if str(f.path) == path][0]
|
||||||
|
|
||||||
@@ -529,7 +529,7 @@ class TestCaseResultsXML:
|
|||||||
r.load_from_xml(filename, get_file)
|
r.load_from_xml(filename, get_file)
|
||||||
eq_(2, len(r.groups))
|
eq_(2, len(r.groups))
|
||||||
|
|
||||||
def test_LoadXML_with_some_files_that_dont_exist_anymore(self):
|
def test_load_xml_with_some_files_that_dont_exist_anymore(self):
|
||||||
def get_file(path):
|
def get_file(path):
|
||||||
if path.endswith("ibabtu 2"):
|
if path.endswith("ibabtu 2"):
|
||||||
return None
|
return None
|
||||||
@@ -545,7 +545,7 @@ class TestCaseResultsXML:
|
|||||||
eq_(1, len(r.groups))
|
eq_(1, len(r.groups))
|
||||||
eq_(3, len(r.groups[0]))
|
eq_(3, len(r.groups[0]))
|
||||||
|
|
||||||
def test_LoadXML_missing_attributes_and_bogus_elements(self):
|
def test_load_xml_missing_attributes_and_bogus_elements(self):
|
||||||
def get_file(path):
|
def get_file(path):
|
||||||
return [f for f in self.objects if str(f.path) == path][0]
|
return [f for f in self.objects if str(f.path) == path][0]
|
||||||
|
|
||||||
|
|||||||
@@ -52,10 +52,12 @@ def test_empty(fake_fileexists):
|
|||||||
def test_default_settings(fake_fileexists):
|
def test_default_settings(fake_fileexists):
|
||||||
s = Scanner()
|
s = Scanner()
|
||||||
eq_(s.min_match_percentage, 80)
|
eq_(s.min_match_percentage, 80)
|
||||||
eq_(s.scan_type, ScanType.Filename)
|
eq_(s.scan_type, ScanType.FILENAME)
|
||||||
eq_(s.mix_file_kind, True)
|
eq_(s.mix_file_kind, True)
|
||||||
eq_(s.word_weighting, False)
|
eq_(s.word_weighting, False)
|
||||||
eq_(s.match_similar_words, False)
|
eq_(s.match_similar_words, False)
|
||||||
|
eq_(s.size_threshold, 0)
|
||||||
|
eq_(s.large_size_threshold, 0)
|
||||||
eq_(s.big_file_size_threshold, 0)
|
eq_(s.big_file_size_threshold, 0)
|
||||||
|
|
||||||
|
|
||||||
@@ -98,7 +100,7 @@ def test_trim_all_ref_groups(fake_fileexists):
|
|||||||
eq_(s.discarded_file_count, 0)
|
eq_(s.discarded_file_count, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_priorize(fake_fileexists):
|
def test_prioritize(fake_fileexists):
|
||||||
s = Scanner()
|
s = Scanner()
|
||||||
f = [
|
f = [
|
||||||
no("foo", path="p1"),
|
no("foo", path="p1"),
|
||||||
@@ -119,7 +121,7 @@ def test_priorize(fake_fileexists):
|
|||||||
|
|
||||||
def test_content_scan(fake_fileexists):
|
def test_content_scan(fake_fileexists):
|
||||||
s = Scanner()
|
s = Scanner()
|
||||||
s.scan_type = ScanType.Contents
|
s.scan_type = ScanType.CONTENTS
|
||||||
f = [no("foo"), no("bar"), no("bleh")]
|
f = [no("foo"), no("bar"), no("bleh")]
|
||||||
f[0].md5 = f[0].md5partial = f[0].md5samples = "foobar"
|
f[0].md5 = f[0].md5partial = f[0].md5samples = "foobar"
|
||||||
f[1].md5 = f[1].md5partial = f[1].md5samples = "foobar"
|
f[1].md5 = f[1].md5partial = f[1].md5samples = "foobar"
|
||||||
@@ -133,18 +135,62 @@ def test_content_scan(fake_fileexists):
|
|||||||
def test_content_scan_compare_sizes_first(fake_fileexists):
|
def test_content_scan_compare_sizes_first(fake_fileexists):
|
||||||
class MyFile(no):
|
class MyFile(no):
|
||||||
@property
|
@property
|
||||||
def md5(file):
|
def md5(self):
|
||||||
raise AssertionError()
|
raise AssertionError()
|
||||||
|
|
||||||
s = Scanner()
|
s = Scanner()
|
||||||
s.scan_type = ScanType.Contents
|
s.scan_type = ScanType.CONTENTS
|
||||||
f = [MyFile("foo", 1), MyFile("bar", 2)]
|
f = [MyFile("foo", 1), MyFile("bar", 2)]
|
||||||
eq_(len(s.get_dupe_groups(f)), 0)
|
eq_(len(s.get_dupe_groups(f)), 0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ignore_file_size(fake_fileexists):
|
||||||
|
s = Scanner()
|
||||||
|
s.scan_type = ScanType.CONTENTS
|
||||||
|
small_size = 10 # 10KB
|
||||||
|
s.size_threshold = 0
|
||||||
|
large_size = 100 * 1024 * 1024 # 100MB
|
||||||
|
s.large_size_threshold = 0
|
||||||
|
f = [
|
||||||
|
no("smallignore1", small_size - 1),
|
||||||
|
no("smallignore2", small_size - 1),
|
||||||
|
no("small1", small_size),
|
||||||
|
no("small2", small_size),
|
||||||
|
no("large1", large_size),
|
||||||
|
no("large2", large_size),
|
||||||
|
no("largeignore1", large_size + 1),
|
||||||
|
no("largeignore2", large_size + 1),
|
||||||
|
]
|
||||||
|
f[0].md5 = f[0].md5partial = f[0].md5samples = "smallignore"
|
||||||
|
f[1].md5 = f[1].md5partial = f[1].md5samples = "smallignore"
|
||||||
|
f[2].md5 = f[2].md5partial = f[2].md5samples = "small"
|
||||||
|
f[3].md5 = f[3].md5partial = f[3].md5samples = "small"
|
||||||
|
f[4].md5 = f[4].md5partial = f[4].md5samples = "large"
|
||||||
|
f[5].md5 = f[5].md5partial = f[5].md5samples = "large"
|
||||||
|
f[6].md5 = f[6].md5partial = f[6].md5samples = "largeignore"
|
||||||
|
f[7].md5 = f[7].md5partial = f[7].md5samples = "largeignore"
|
||||||
|
|
||||||
|
r = s.get_dupe_groups(f)
|
||||||
|
# No ignores
|
||||||
|
eq_(len(r), 4)
|
||||||
|
# Ignore smaller
|
||||||
|
s.size_threshold = small_size
|
||||||
|
r = s.get_dupe_groups(f)
|
||||||
|
eq_(len(r), 3)
|
||||||
|
# Ignore larger
|
||||||
|
s.size_threshold = 0
|
||||||
|
s.large_size_threshold = large_size
|
||||||
|
r = s.get_dupe_groups(f)
|
||||||
|
eq_(len(r), 3)
|
||||||
|
# Ignore both
|
||||||
|
s.size_threshold = small_size
|
||||||
|
r = s.get_dupe_groups(f)
|
||||||
|
eq_(len(r), 2)
|
||||||
|
|
||||||
|
|
||||||
def test_big_file_partial_hashes(fake_fileexists):
|
def test_big_file_partial_hashes(fake_fileexists):
|
||||||
s = Scanner()
|
s = Scanner()
|
||||||
s.scan_type = ScanType.Contents
|
s.scan_type = ScanType.CONTENTS
|
||||||
|
|
||||||
smallsize = 1
|
smallsize = 1
|
||||||
bigsize = 100 * 1024 * 1024 # 100MB
|
bigsize = 100 * 1024 * 1024 # 100MB
|
||||||
@@ -173,7 +219,7 @@ def test_big_file_partial_hashes(fake_fileexists):
|
|||||||
|
|
||||||
def test_min_match_perc_doesnt_matter_for_content_scan(fake_fileexists):
|
def test_min_match_perc_doesnt_matter_for_content_scan(fake_fileexists):
|
||||||
s = Scanner()
|
s = Scanner()
|
||||||
s.scan_type = ScanType.Contents
|
s.scan_type = ScanType.CONTENTS
|
||||||
f = [no("foo"), no("bar"), no("bleh")]
|
f = [no("foo"), no("bar"), no("bleh")]
|
||||||
f[0].md5 = f[0].md5partial = f[0].md5samples = "foobar"
|
f[0].md5 = f[0].md5partial = f[0].md5samples = "foobar"
|
||||||
f[1].md5 = f[1].md5partial = f[1].md5samples = "foobar"
|
f[1].md5 = f[1].md5partial = f[1].md5samples = "foobar"
|
||||||
@@ -190,7 +236,7 @@ def test_min_match_perc_doesnt_matter_for_content_scan(fake_fileexists):
|
|||||||
|
|
||||||
def test_content_scan_doesnt_put_md5_in_words_at_the_end(fake_fileexists):
|
def test_content_scan_doesnt_put_md5_in_words_at_the_end(fake_fileexists):
|
||||||
s = Scanner()
|
s = Scanner()
|
||||||
s.scan_type = ScanType.Contents
|
s.scan_type = ScanType.CONTENTS
|
||||||
f = [no("foo"), no("bar")]
|
f = [no("foo"), no("bar")]
|
||||||
f[0].md5 = f[0].md5partial = f[0].md5samples = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
|
f[0].md5 = f[0].md5partial = f[0].md5samples = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
|
||||||
f[1].md5 = f[1].md5partial = f[1].md5samples = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
|
f[1].md5 = f[1].md5partial = f[1].md5samples = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
|
||||||
@@ -256,7 +302,7 @@ def test_similar_words(fake_fileexists):
|
|||||||
|
|
||||||
def test_fields(fake_fileexists):
|
def test_fields(fake_fileexists):
|
||||||
s = Scanner()
|
s = Scanner()
|
||||||
s.scan_type = ScanType.Fields
|
s.scan_type = ScanType.FIELDS
|
||||||
f = [no("The White Stripes - Little Ghost"), no("The White Stripes - Little Acorn")]
|
f = [no("The White Stripes - Little Ghost"), no("The White Stripes - Little Acorn")]
|
||||||
r = s.get_dupe_groups(f)
|
r = s.get_dupe_groups(f)
|
||||||
eq_(len(r), 0)
|
eq_(len(r), 0)
|
||||||
@@ -264,7 +310,7 @@ def test_fields(fake_fileexists):
|
|||||||
|
|
||||||
def test_fields_no_order(fake_fileexists):
|
def test_fields_no_order(fake_fileexists):
|
||||||
s = Scanner()
|
s = Scanner()
|
||||||
s.scan_type = ScanType.FieldsNoOrder
|
s.scan_type = ScanType.FIELDSNOORDER
|
||||||
f = [no("The White Stripes - Little Ghost"), no("Little Ghost - The White Stripes")]
|
f = [no("The White Stripes - Little Ghost"), no("Little Ghost - The White Stripes")]
|
||||||
r = s.get_dupe_groups(f)
|
r = s.get_dupe_groups(f)
|
||||||
eq_(len(r), 1)
|
eq_(len(r), 1)
|
||||||
@@ -272,7 +318,7 @@ def test_fields_no_order(fake_fileexists):
|
|||||||
|
|
||||||
def test_tag_scan(fake_fileexists):
|
def test_tag_scan(fake_fileexists):
|
||||||
s = Scanner()
|
s = Scanner()
|
||||||
s.scan_type = ScanType.Tag
|
s.scan_type = ScanType.TAG
|
||||||
o1 = no("foo")
|
o1 = no("foo")
|
||||||
o2 = no("bar")
|
o2 = no("bar")
|
||||||
o1.artist = "The White Stripes"
|
o1.artist = "The White Stripes"
|
||||||
@@ -285,7 +331,7 @@ def test_tag_scan(fake_fileexists):
|
|||||||
|
|
||||||
def test_tag_with_album_scan(fake_fileexists):
|
def test_tag_with_album_scan(fake_fileexists):
|
||||||
s = Scanner()
|
s = Scanner()
|
||||||
s.scan_type = ScanType.Tag
|
s.scan_type = ScanType.TAG
|
||||||
s.scanned_tags = set(["artist", "album", "title"])
|
s.scanned_tags = set(["artist", "album", "title"])
|
||||||
o1 = no("foo")
|
o1 = no("foo")
|
||||||
o2 = no("bar")
|
o2 = no("bar")
|
||||||
@@ -305,7 +351,7 @@ def test_tag_with_album_scan(fake_fileexists):
|
|||||||
|
|
||||||
def test_that_dash_in_tags_dont_create_new_fields(fake_fileexists):
|
def test_that_dash_in_tags_dont_create_new_fields(fake_fileexists):
|
||||||
s = Scanner()
|
s = Scanner()
|
||||||
s.scan_type = ScanType.Tag
|
s.scan_type = ScanType.TAG
|
||||||
s.scanned_tags = set(["artist", "album", "title"])
|
s.scanned_tags = set(["artist", "album", "title"])
|
||||||
s.min_match_percentage = 50
|
s.min_match_percentage = 50
|
||||||
o1 = no("foo")
|
o1 = no("foo")
|
||||||
@@ -322,7 +368,7 @@ def test_that_dash_in_tags_dont_create_new_fields(fake_fileexists):
|
|||||||
|
|
||||||
def test_tag_scan_with_different_scanned(fake_fileexists):
|
def test_tag_scan_with_different_scanned(fake_fileexists):
|
||||||
s = Scanner()
|
s = Scanner()
|
||||||
s.scan_type = ScanType.Tag
|
s.scan_type = ScanType.TAG
|
||||||
s.scanned_tags = set(["track", "year"])
|
s.scanned_tags = set(["track", "year"])
|
||||||
o1 = no("foo")
|
o1 = no("foo")
|
||||||
o2 = no("bar")
|
o2 = no("bar")
|
||||||
@@ -340,7 +386,7 @@ def test_tag_scan_with_different_scanned(fake_fileexists):
|
|||||||
|
|
||||||
def test_tag_scan_only_scans_existing_tags(fake_fileexists):
|
def test_tag_scan_only_scans_existing_tags(fake_fileexists):
|
||||||
s = Scanner()
|
s = Scanner()
|
||||||
s.scan_type = ScanType.Tag
|
s.scan_type = ScanType.TAG
|
||||||
s.scanned_tags = set(["artist", "foo"])
|
s.scanned_tags = set(["artist", "foo"])
|
||||||
o1 = no("foo")
|
o1 = no("foo")
|
||||||
o2 = no("bar")
|
o2 = no("bar")
|
||||||
@@ -354,7 +400,7 @@ def test_tag_scan_only_scans_existing_tags(fake_fileexists):
|
|||||||
|
|
||||||
def test_tag_scan_converts_to_str(fake_fileexists):
|
def test_tag_scan_converts_to_str(fake_fileexists):
|
||||||
s = Scanner()
|
s = Scanner()
|
||||||
s.scan_type = ScanType.Tag
|
s.scan_type = ScanType.TAG
|
||||||
s.scanned_tags = set(["track"])
|
s.scanned_tags = set(["track"])
|
||||||
o1 = no("foo")
|
o1 = no("foo")
|
||||||
o2 = no("bar")
|
o2 = no("bar")
|
||||||
@@ -369,7 +415,7 @@ def test_tag_scan_converts_to_str(fake_fileexists):
|
|||||||
|
|
||||||
def test_tag_scan_non_ascii(fake_fileexists):
|
def test_tag_scan_non_ascii(fake_fileexists):
|
||||||
s = Scanner()
|
s = Scanner()
|
||||||
s.scan_type = ScanType.Tag
|
s.scan_type = ScanType.TAG
|
||||||
s.scanned_tags = set(["title"])
|
s.scanned_tags = set(["title"])
|
||||||
o1 = no("foo")
|
o1 = no("foo")
|
||||||
o2 = no("bar")
|
o2 = no("bar")
|
||||||
@@ -391,8 +437,8 @@ def test_ignore_list(fake_fileexists):
|
|||||||
f2.path = Path("dir2/foobar")
|
f2.path = Path("dir2/foobar")
|
||||||
f3.path = Path("dir3/foobar")
|
f3.path = Path("dir3/foobar")
|
||||||
ignore_list = IgnoreList()
|
ignore_list = IgnoreList()
|
||||||
ignore_list.Ignore(str(f1.path), str(f2.path))
|
ignore_list.ignore(str(f1.path), str(f2.path))
|
||||||
ignore_list.Ignore(str(f1.path), str(f3.path))
|
ignore_list.ignore(str(f1.path), str(f3.path))
|
||||||
r = s.get_dupe_groups([f1, f2, f3], ignore_list=ignore_list)
|
r = s.get_dupe_groups([f1, f2, f3], ignore_list=ignore_list)
|
||||||
eq_(len(r), 1)
|
eq_(len(r), 1)
|
||||||
g = r[0]
|
g = r[0]
|
||||||
@@ -415,8 +461,8 @@ def test_ignore_list_checks_for_unicode(fake_fileexists):
|
|||||||
f2.path = Path("foo2\u00e9")
|
f2.path = Path("foo2\u00e9")
|
||||||
f3.path = Path("foo3\u00e9")
|
f3.path = Path("foo3\u00e9")
|
||||||
ignore_list = IgnoreList()
|
ignore_list = IgnoreList()
|
||||||
ignore_list.Ignore(str(f1.path), str(f2.path))
|
ignore_list.ignore(str(f1.path), str(f2.path))
|
||||||
ignore_list.Ignore(str(f1.path), str(f3.path))
|
ignore_list.ignore(str(f1.path), str(f3.path))
|
||||||
r = s.get_dupe_groups([f1, f2, f3], ignore_list=ignore_list)
|
r = s.get_dupe_groups([f1, f2, f3], ignore_list=ignore_list)
|
||||||
eq_(len(r), 1)
|
eq_(len(r), 1)
|
||||||
g = r[0]
|
g = r[0]
|
||||||
@@ -520,7 +566,7 @@ def test_dont_group_files_that_dont_exist(tmpdir):
|
|||||||
# In this test, we have to delete one of the files between the get_matches() part and the
|
# In this test, we have to delete one of the files between the get_matches() part and the
|
||||||
# get_groups() part.
|
# get_groups() part.
|
||||||
s = Scanner()
|
s = Scanner()
|
||||||
s.scan_type = ScanType.Contents
|
s.scan_type = ScanType.CONTENTS
|
||||||
p = Path(str(tmpdir))
|
p = Path(str(tmpdir))
|
||||||
p["file1"].open("w").write("foo")
|
p["file1"].open("w").write("foo")
|
||||||
p["file2"].open("w").write("foo")
|
p["file2"].open("w").write("foo")
|
||||||
@@ -539,7 +585,7 @@ def test_folder_scan_exclude_subfolder_matches(fake_fileexists):
|
|||||||
# when doing a Folders scan type, don't include matches for folders whose parent folder already
|
# when doing a Folders scan type, don't include matches for folders whose parent folder already
|
||||||
# match.
|
# match.
|
||||||
s = Scanner()
|
s = Scanner()
|
||||||
s.scan_type = ScanType.Folders
|
s.scan_type = ScanType.FOLDERS
|
||||||
topf1 = no("top folder 1", size=42)
|
topf1 = no("top folder 1", size=42)
|
||||||
topf1.md5 = topf1.md5partial = topf1.md5samples = b"some_md5_1"
|
topf1.md5 = topf1.md5partial = topf1.md5samples = b"some_md5_1"
|
||||||
topf1.path = Path("/topf1")
|
topf1.path = Path("/topf1")
|
||||||
@@ -574,7 +620,7 @@ def test_dont_count_ref_files_as_discarded(fake_fileexists):
|
|||||||
# However, this causes problems in "discarded" counting and we make sure here that we don't
|
# However, this causes problems in "discarded" counting and we make sure here that we don't
|
||||||
# report discarded matches in exact duplicate scans.
|
# report discarded matches in exact duplicate scans.
|
||||||
s = Scanner()
|
s = Scanner()
|
||||||
s.scan_type = ScanType.Contents
|
s.scan_type = ScanType.CONTENTS
|
||||||
o1 = no("foo", path="p1")
|
o1 = no("foo", path="p1")
|
||||||
o2 = no("foo", path="p2")
|
o2 = no("foo", path="p2")
|
||||||
o3 = no("foo", path="p3")
|
o3 = no("foo", path="p3")
|
||||||
@@ -587,8 +633,8 @@ def test_dont_count_ref_files_as_discarded(fake_fileexists):
|
|||||||
eq_(s.discarded_file_count, 0)
|
eq_(s.discarded_file_count, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_priorize_me(fake_fileexists):
|
def test_prioritize_me(fake_fileexists):
|
||||||
# in ScannerME, bitrate goes first (right after is_ref) in priorization
|
# in ScannerME, bitrate goes first (right after is_ref) in prioritization
|
||||||
s = ScannerME()
|
s = ScannerME()
|
||||||
o1, o2 = no("foo", path="p1"), no("foo", path="p2")
|
o1, o2 = no("foo", path="p1"), no("foo", path="p2")
|
||||||
o1.bitrate = 1
|
o1.bitrate = 1
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
from hscommon.util import format_time_decimal
|
from hscommon.util import format_time_decimal
|
||||||
|
|
||||||
@@ -58,3 +60,7 @@ def fix_surrogate_encoding(s, encoding="utf-8"):
|
|||||||
return s.encode(encoding, "replace").decode(encoding)
|
return s.encode(encoding, "replace").decode(encoding)
|
||||||
else:
|
else:
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def executable_folder():
|
||||||
|
return os.path.dirname(os.path.abspath(sys.argv[0]))
|
||||||
|
|||||||
@@ -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)
|
=== 4.1.1 (2021-03-21)
|
||||||
|
|
||||||
* Add Japanese
|
* Add Japanese
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
Häufig gestellte Fragen
|
Häufig gestellte Fragen
|
||||||
==========================
|
==========================
|
||||||
|
|
||||||
.. topic:: What is |appname|?
|
.. topic:: What is dupeGuru?
|
||||||
|
|
||||||
.. only:: edition_se
|
.. only:: edition_se
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ Häufig gestellte Fragen
|
|||||||
|
|
||||||
.. topic:: Was sind die Demo-Einschränkungen von dupeGuru?
|
.. topic:: Was sind die Demo-Einschränkungen von dupeGuru?
|
||||||
|
|
||||||
Keine, |appname| ist `Fairware <http://open.hardcoded.net/about/>`_.
|
Keine, dupeGuru ist `Fairware <http://open.hardcoded.net/about/>`_.
|
||||||
|
|
||||||
.. topic:: Die Markierungsbox einer Datei, die ich löschen möchte, ist deaktiviert. Was muss ich tun?
|
.. topic:: Die Markierungsbox einer Datei, die ich löschen möchte, ist deaktiviert. Was muss ich tun?
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,13 @@
|
|||||||
|appname| Hilfe
|
dupeGuru Hilfe
|
||||||
===============
|
===============
|
||||||
|
|
||||||
.. only:: edition_se
|
.. only:: edition_se
|
||||||
|
|
||||||
Dieses Dokument ist auch auf `Englisch <http://www.hardcoded.net/dupeguru/help/en/>`__ und `Französisch <http://www.hardcoded.net/dupeguru/help/fr/>`__ verfügbar.
|
Dieses Dokument ist auch auf `Englisch <http://dupeguru.voltaicideas.net/help/en/>`__ und `Französisch <http://dupeguru.voltaicideas.net/help/fr/>`__ verfügbar.
|
||||||
|
|
||||||
.. only:: edition_me
|
|
||||||
|
|
||||||
Dieses Dokument ist auch auf `Englisch <http://www.hardcoded.net/dupeguru/help/en/>`__ und `Französisch <http://www.hardcoded.net/dupeguru_me/help/fr/>`__ verfügbar.
|
|
||||||
|
|
||||||
.. only:: edition_pe
|
|
||||||
|
|
||||||
Dieses Dokument ist auch auf `Englisch <http://www.hardcoded.net/dupeguru/help/en/>`__ und `Französisch <http://www.hardcoded.net/dupeguru_pe/help/fr/>`__ verfügbar.
|
|
||||||
|
|
||||||
.. only:: edition_se or edition_me
|
.. only:: edition_se or edition_me
|
||||||
|
|
||||||
|appname| ist ein Tool zum Auffinden von Duplikaten auf Ihrem Computer. Es kann entweder Dateinamen oder Inhalte scannen. Der Dateiname-Scan stellt einen lockeren Suchalgorithmus zur Verfügung, der sogar Duplikate findet, die nicht den exakten selben Namen haben.
|
dupeGuru ist ein Tool zum Auffinden von Duplikaten auf Ihrem Computer. Es kann entweder Dateinamen oder Inhalte scannen. Der Dateiname-Scan stellt einen lockeren Suchalgorithmus zur Verfügung, der sogar Duplikate findet, die nicht den exakten selben Namen haben.
|
||||||
|
|
||||||
.. only:: edition_pe
|
.. only:: edition_pe
|
||||||
|
|
||||||
@@ -23,7 +15,7 @@
|
|||||||
|
|
||||||
Obwohl dupeGuru auch leicht ohne Dokumentation genutzt werden kann, ist es sinnvoll die Hilfe zu lesen. Wenn Sie nach einer Führung für den ersten Duplikatscan suchen, werfen Sie einen Blick auf die :doc:`Schnellstart <quick_start>` Sektion
|
Obwohl dupeGuru auch leicht ohne Dokumentation genutzt werden kann, ist es sinnvoll die Hilfe zu lesen. Wenn Sie nach einer Führung für den ersten Duplikatscan suchen, werfen Sie einen Blick auf die :doc:`Schnellstart <quick_start>` Sektion
|
||||||
|
|
||||||
Es ist eine gute Idee |appname| aktuell zu halten. Sie können die neueste Version auf der `homepage`_ finden.
|
Es ist eine gute Idee dupeGuru aktuell zu halten. Sie können die neueste Version auf der http://dupeguru.voltaicideas.net finden.
|
||||||
|
|
||||||
Inhalte:
|
Inhalte:
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
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**,
|
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.
|
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
|
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.
|
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`_.
|
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
|
.. _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/arsenetar/dupeguru
|
||||||
.. _Source code repository: https://github.com/hsoft/dupeguru
|
.. _Issue Tracker: https://github.com/arsenetar/issues
|
||||||
.. _Issue Tracker: https://github.com/hsoft/dupeguru/issues
|
.. _Issue labels meaning: https://github.com/arsenetar/wiki/issue-labels
|
||||||
.. _Issue labels meaning: https://github.com/hsoft/dupeguru/wiki/issue-labels
|
|
||||||
.. _Sphinx: http://sphinx-doc.org/
|
.. _Sphinx: http://sphinx-doc.org/
|
||||||
.. _reST: http://en.wikipedia.org/wiki/ReStructuredText
|
.. _reST: http://en.wikipedia.org/wiki/ReStructuredText
|
||||||
.. _translator guide: https://github.com/hsoft/dupeguru/wiki/Translator-Guide
|
.. _translator guide: https://github.com/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
|
files to the Trash, but delete them immediately. In some cases, for example on network storage
|
||||||
(NAS), this has been known to work when normal deletion didn't.
|
(NAS), this has been known to work when normal deletion didn't.
|
||||||
|
|
||||||
If this fail, `HS forums`_ might be of some help.
|
|
||||||
|
|
||||||
Why is Picture mode's contents scan so slow?
|
Why is Picture mode's contents scan so slow?
|
||||||
--------------------------------------------
|
--------------------------------------------
|
||||||
|
|
||||||
@@ -178,7 +176,6 @@ Preferences are stored elsewhere:
|
|||||||
* Linux: ``~/.config/Hardcoded Software/dupeGuru.conf``
|
* Linux: ``~/.config/Hardcoded Software/dupeGuru.conf``
|
||||||
* Mac OS X: In the built-in ``defaults`` system, as ``com.hardcoded-software.dupeguru``
|
* Mac OS X: In the built-in ``defaults`` system, as ``com.hardcoded-software.dupeguru``
|
||||||
|
|
||||||
.. _HS forums: https://forum.hardcoded.net/
|
.. _Github: https://github.com/arsenetar/dupeguru
|
||||||
.. _Github: https://github.com/hsoft/dupeguru
|
.. _open an issue: https://github.com/arsenetar/dupeguru/wiki/issue-labels
|
||||||
.. _open an issue: https://github.com/hsoft/dupeguru/wiki/issue-labels
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ dupeGuru help
|
|||||||
|
|
||||||
This help document is also available in these languages:
|
This help document is also available in these languages:
|
||||||
|
|
||||||
* `French <http://www.hardcoded.net/dupeguru/help/fr>`__
|
* `French <http://dupeguru.voltaicideas.net/help/fr>`__
|
||||||
* `German <http://www.hardcoded.net/dupeguru/help/de>`__
|
* `German <http://dupeguru.voltaicideas.net/help/de>`__
|
||||||
* `Armenian <http://www.hardcoded.net/dupeguru/help/hy>`__
|
* `Armenian <http://dupeguru.voltaicideas.net/help/hy>`__
|
||||||
* `Russian <http://www.hardcoded.net/dupeguru/help/ru>`__
|
* `Russian <http://dupeguru.voltaicideas.net/help/ru>`__
|
||||||
* `Ukrainian <http://www.hardcoded.net/dupeguru/help/uk>`__
|
* `Ukrainian <http://dupeguru.voltaicideas.net/help/uk>`__
|
||||||
|
|
||||||
dupeGuru is a tool to find duplicate files on your computer. It has three
|
dupeGuru is a tool to find duplicate files on your computer. It has three
|
||||||
modes, Standard, Music and Picture, with each mode having its own scan types
|
modes, Standard, Music and Picture, with each mode having its own scan types
|
||||||
@@ -42,4 +42,4 @@ Indices and tables
|
|||||||
* :ref:`genindex`
|
* :ref:`genindex`
|
||||||
* :ref:`search`
|
* :ref:`search`
|
||||||
|
|
||||||
.. _homepage: https://www.hardcoded.net/dupeguru
|
.. _homepage: https://dupeguru.voltaicideas.net/
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ Foire aux questions
|
|||||||
|
|
||||||
.. contents::
|
.. contents::
|
||||||
|
|
||||||
Qu'est-ce que |appname|?
|
Qu'est-ce que dupeGuru?
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
.. only:: edition_se
|
.. only:: edition_se
|
||||||
|
|||||||
@@ -1,21 +1,13 @@
|
|||||||
Aide |appname|
|
Aide dupeGuru
|
||||||
===============
|
===============
|
||||||
|
|
||||||
.. only:: edition_se
|
.. only:: edition_se
|
||||||
|
|
||||||
Ce document est aussi disponible en `anglais <http://www.hardcoded.net/dupeguru/help/en/>`__, en `allemand <http://www.hardcoded.net/dupeguru/help/de/>`__ et en `arménien <http://www.hardcoded.net/dupeguru/help/hy/>`__.
|
Ce document est aussi disponible en `anglais <http://dupeguru.voltaicideas.net/help/en/>`__, en `allemand <http://dupeguru.voltaicideas.net/help/de/>`__ et en `arménien <http://dupeguru.voltaicideas.net/help/hy/>`__.
|
||||||
|
|
||||||
.. only:: edition_me
|
|
||||||
|
|
||||||
Ce document est aussi disponible en `anglais <http://www.hardcoded.net/dupeguru_me/help/en/>`__, en `allemand <http://www.hardcoded.net/dupeguru_me/help/de/>`__ et en `arménien <http://www.hardcoded.net/dupeguru_me/help/hy/>`__.
|
|
||||||
|
|
||||||
.. only:: edition_pe
|
|
||||||
|
|
||||||
Ce document est aussi disponible en `anglais <http://www.hardcoded.net/dupeguru_pe/help/en/>`__, en `allemand <http://www.hardcoded.net/dupeguru_pe/help/de/>`__ et en `arménien <http://www.hardcoded.net/dupeguru_pe/help/hy/>`__.
|
|
||||||
|
|
||||||
.. only:: edition_se or edition_me
|
.. only:: edition_se or edition_me
|
||||||
|
|
||||||
|appname| est un outil pour trouver des doublons parmi vos fichiers. Il peut comparer soit les noms de fichiers, soit le contenu. Le comparateur de nom de fichier peut trouver des doublons même si les noms ne sont pas exactement pareils.
|
dupeGuru est un outil pour trouver des doublons parmi vos fichiers. Il peut comparer soit les noms de fichiers, soit le contenu. Le comparateur de nom de fichier peut trouver des doublons même si les noms ne sont pas exactement pareils.
|
||||||
|
|
||||||
.. only:: edition_pe
|
.. only:: edition_pe
|
||||||
|
|
||||||
@@ -23,7 +15,7 @@ Aide |appname|
|
|||||||
|
|
||||||
Bien que dupeGuru puisse être utilisé sans lire l'aide, une telle lecture vous permettra de bien comprendre comment l'application fonctionne. Pour un guide rapide pour une première utilisation, référez vous à la section :doc:`Démarrage Rapide <quick_start>`.
|
Bien que dupeGuru puisse être utilisé sans lire l'aide, une telle lecture vous permettra de bien comprendre comment l'application fonctionne. Pour un guide rapide pour une première utilisation, référez vous à la section :doc:`Démarrage Rapide <quick_start>`.
|
||||||
|
|
||||||
C'est toujours une bonne idée de garder |appname| à jour. Vous pouvez télécharger la dernière version sur sa `page web`_.
|
C'est toujours une bonne idée de garder dupeGuru à jour. Vous pouvez télécharger la dernière version sur sa http://dupeguru.voltaicideas.net.
|
||||||
|
|
||||||
Contents:
|
Contents:
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
Հաճախ Տրվող Հարցեր
|
Հաճախ Տրվող Հարցեր
|
||||||
==========================
|
==========================
|
||||||
|
|
||||||
.. topic:: Ի՞նչ է |appname|-ը:
|
.. topic:: Ի՞նչ է dupeGuru-ը:
|
||||||
|
|
||||||
.. only:: edition_se
|
.. only:: edition_se
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,13 @@
|
|||||||
|appname| help
|
dupeGuru help
|
||||||
===============
|
===============
|
||||||
|
|
||||||
.. only:: edition_se
|
.. only:: edition_se
|
||||||
|
|
||||||
Այս փաստաթուղթը հասանելի է նաև՝ `Ֆրանսերեն <http://www.hardcoded.net/dupeguru/help/fr/>`__ և `Գերմաներեն <http://www.hardcoded.net/dupeguru/help/de/>`__.
|
Այս փաստաթուղթը հասանելի է նաև՝ `Ֆրանսերեն <http://dupeguru.voltaicideas.net/help/fr/>`__ և `Գերմաներեն <http://dupeguru.voltaicideas.net/help/de/>`__.
|
||||||
|
|
||||||
.. only:: edition_me
|
|
||||||
|
|
||||||
Այս փաստաթուղթը հասանելի է նաև՝ `Ֆրանսերեն <http://www.hardcoded.net/dupeguru_me/help/fr/>`__ և `Գերմաներեն <http://www.hardcoded.net/dupeguru_me/help/de/>`__.
|
|
||||||
|
|
||||||
.. only:: edition_pe
|
|
||||||
|
|
||||||
Այս փաստաթուղթը հասանելի է նաև՝ `Ֆրանսերեն <http://www.hardcoded.net/dupeguru_pe/help/fr/>`__ և `Գերմաներեն <http://www.hardcoded.net/dupeguru_pe/help/de/>`__.
|
|
||||||
|
|
||||||
.. only:: edition_se or edition_me
|
.. only:: edition_se or edition_me
|
||||||
|
|
||||||
|appname| ծրագիր է՝ գտնելու կրկնօրինակ ունեցող ֆայլեր Ձեր համակարգչում: Այն կարող է անգամ ստուգել ֆայլի անունները կան բովանդակությունը: Ֆայլի անվան ստուգման հնարավորությունները ոչ ճշգրիտ համընկման ալգորիթմով, որը կարող է գտնել ֆայլի անվան կրկնօրինակներ, անգամ եթե դրանք նույնը չեն:
|
dupeGuru ծրագիր է՝ գտնելու կրկնօրինակ ունեցող ֆայլեր Ձեր համակարգչում: Այն կարող է անգամ ստուգել ֆայլի անունները կան բովանդակությունը: Ֆայլի անվան ստուգման հնարավորությունները ոչ ճշգրիտ համընկման ալգորիթմով, որը կարող է գտնել ֆայլի անվան կրկնօրինակներ, անգամ եթե դրանք նույնը չեն:
|
||||||
|
|
||||||
.. only:: edition_pe
|
.. only:: edition_pe
|
||||||
|
|
||||||
@@ -23,7 +15,7 @@
|
|||||||
|
|
||||||
Չնայած dupeGuru-ն կարող է հեշտությամբ օգտագործվել առանց օգնության, այնուհանդերձ եթե կարդաք այս ֆայլը, այն մեծապես կօգնի Ձեզ ընկալելու ծրագրի աշխատանքը: Եթե Դուք նայում եք ձեռնարկը կրկնօրինակների առաջին ստուգման համար, ապա կարող եք ընտրել :doc:`Արագ Սկիզբ <quick_start>` հատվածը:
|
Չնայած dupeGuru-ն կարող է հեշտությամբ օգտագործվել առանց օգնության, այնուհանդերձ եթե կարդաք այս ֆայլը, այն մեծապես կօգնի Ձեզ ընկալելու ծրագրի աշխատանքը: Եթե Դուք նայում եք ձեռնարկը կրկնօրինակների առաջին ստուգման համար, ապա կարող եք ընտրել :doc:`Արագ Սկիզբ <quick_start>` հատվածը:
|
||||||
|
|
||||||
Շատ լավ միտք է պահելու |appname| թարմացված: Կարող եք բեռնել վեբ կայքի համապատասխան էջից `homepage`_:
|
Շատ լավ միտք է պահելու dupeGuru թարմացված: Կարող եք բեռնել վեբ կայքի համապատասխան էջից http://dupeguru.voltaicideas.net:
|
||||||
|
|
||||||
Պարունակությունը.
|
Պարունակությունը.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
Часто задаваемые вопросы
|
Часто задаваемые вопросы
|
||||||
==========================
|
==========================
|
||||||
|
|
||||||
.. topic:: Что такое |appname|?
|
.. topic:: Что такое dupeGuru?
|
||||||
|
|
||||||
.. only:: edition_se
|
.. only:: edition_se
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,11 @@
|
|||||||
|appname| help
|
dupeGuru help
|
||||||
===============
|
===============
|
||||||
|
|
||||||
.. only:: edition_se
|
Этот документ также доступна на `французском <http://dupeguru.voltaicideas.net/help/fr/>`__, `немецком <http://dupeguru.voltaicideas.net/help/de/>`__ и `армянский <http://dupeguru.voltaicideas.net/help/hy/>`__.
|
||||||
|
|
||||||
Этот документ также доступна на `французском <http://www.hardcoded.net/dupeguru/help/fr/>`__, `немецком <http://www.hardcoded.net/dupeguru/help/de/>`__ и `армянский <http://www.hardcoded.net/dupeguru/help/hy/>`__.
|
|
||||||
|
|
||||||
.. only:: edition_me
|
|
||||||
|
|
||||||
Этот документ также доступна на `французском <http://www.hardcoded.net/dupeguru_me/help/fr/>`__, `немецкий <http://www.hardcoded.net/dupeguru_me/help/de/>`__ и `армянский <http://www.hardcoded.net/dupeguru_me/help/hy/>`__.
|
|
||||||
|
|
||||||
.. only:: edition_pe
|
|
||||||
|
|
||||||
Этот документ также доступна на `французском <http://www.hardcoded.net/dupeguru_pe/help/fr/>`__, `немецкий <http://www.hardcoded.net/dupeguru_pe/help/de/>`__ и `армянский <http://www.hardcoded.net/dupeguru_pe/help/hy/>`__.
|
|
||||||
|
|
||||||
.. only:: edition_se or edition_me
|
.. only:: edition_se or edition_me
|
||||||
|
|
||||||
|appname| есть инструмент для поиска дубликатов файлов на вашем компьютере. Он может сканировать либо имен файлов или содержимого.Имя файла функций сканирования нечеткого соответствия алгоритма, который позволяет найти одинаковые имена файлов, даже если они не совсем то же самое.
|
dupeGuru есть инструмент для поиска дубликатов файлов на вашем компьютере. Он может сканировать либо имен файлов или содержимого.Имя файла функций сканирования нечеткого соответствия алгоритма, который позволяет найти одинаковые имена файлов, даже если они не совсем то же самое.
|
||||||
|
|
||||||
.. only:: edition_pe
|
.. only:: edition_pe
|
||||||
|
|
||||||
@@ -23,7 +13,7 @@
|
|||||||
|
|
||||||
Хотя dupeGuru может быть легко использована без документации, чтение этого файла поможет вам освоить его. Если вы ищете руководство для вашей первой дублировать сканирования, вы можете взглянуть на раздел :doc:`Быстрый <quick_start>` Начало.
|
Хотя dupeGuru может быть легко использована без документации, чтение этого файла поможет вам освоить его. Если вы ищете руководство для вашей первой дублировать сканирования, вы можете взглянуть на раздел :doc:`Быстрый <quick_start>` Начало.
|
||||||
|
|
||||||
Это хорошая идея, чтобы сохранить |appname| обновлен. Вы можете скачать последнюю версию на своей `homepage`_.
|
Это хорошая идея, чтобы сохранить dupeGuru обновлен. Вы можете скачать последнюю версию на своей http://dupeguru.voltaicideas.net.
|
||||||
Содержание:
|
Содержание:
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
Часті питання
|
Часті питання
|
||||||
==========================
|
==========================
|
||||||
|
|
||||||
.. topic:: Що таке |appname|?
|
.. topic:: Що таке dupeGuru?
|
||||||
|
|
||||||
.. only:: edition_se
|
.. only:: edition_se
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,13 @@
|
|||||||
|appname| help
|
dupeGuru help
|
||||||
===============
|
===============
|
||||||
|
|
||||||
.. only:: edition_se
|
.. only:: edition_se
|
||||||
|
|
||||||
Цей документ також доступна на `французькому <http://www.hardcoded.net/dupeguru/help/fr/>`__, `німецький <http://www.hardcoded.net/dupeguru/help/de/>`__ і `Вірменський <http://www.hardcoded.net/dupeguru/help/hy/>`__.
|
Цей документ також доступна на `французькому <http://dupeguru.voltaicideas.net/help/fr/>`__, `німецький <http://dupeguru.voltaicideas.net/help/de/>`__ і `Вірменський <http://dupeguru.voltaicideas.net/help/hy/>`__.
|
||||||
|
|
||||||
.. only:: edition_me
|
|
||||||
|
|
||||||
Цей документ також доступна на `французькому <http://www.hardcoded.net/dupeguru_me/help/fr/>`__, `німецький <http://www.hardcoded.net/dupeguru_me/help/de/>`__ і `Вірменський <http://www.hardcoded.net/dupeguru_me/help/hy/>`__.
|
|
||||||
|
|
||||||
.. only:: edition_pe
|
|
||||||
|
|
||||||
Цей документ також доступна на `французькому <http://www.hardcoded.net/dupeguru_pe/help/fr/>`__, `німецький <http://www.hardcoded.net/dupeguru_pe/help/de/>`__ і `Вірменський <http://www.hardcoded.net/dupeguru_pe/help/hy/>`__.
|
|
||||||
|
|
||||||
.. only:: edition_se or edition_me
|
.. only:: edition_se or edition_me
|
||||||
|
|
||||||
|appname| це інструмент для пошуку дублікатів файлів на вашому комп'ютері. Він може сканувати або імен файлів або вмісту. Файл функцій сканування нечіткого відповідності алгоритму, який дозволяє знайти однакові імена файлів, навіть якщо вони не зовсім те ж саме.
|
dupeGuru це інструмент для пошуку дублікатів файлів на вашому комп'ютері. Він може сканувати або імен файлів або вмісту. Файл функцій сканування нечіткого відповідності алгоритму, який дозволяє знайти однакові імена файлів, навіть якщо вони не зовсім те ж саме.
|
||||||
|
|
||||||
.. only:: edition_pe
|
.. only:: edition_pe
|
||||||
|
|
||||||
@@ -23,7 +15,7 @@
|
|||||||
|
|
||||||
Хоча dupeGuru може бути легко використана без документації, читання цього файлу допоможе вам освоїти його. Якщо ви шукаєте керівництво для вашої першої дублювати сканування, ви можете поглянути на: :doc:`Quick Start <quick_start>`
|
Хоча dupeGuru може бути легко використана без документації, читання цього файлу допоможе вам освоїти його. Якщо ви шукаєте керівництво для вашої першої дублювати сканування, ви можете поглянути на: :doc:`Quick Start <quick_start>`
|
||||||
|
|
||||||
Це гарна ідея, щоб зберегти |appname| оновлено. Ви можете завантажити останню версію на своєму `homepage`_.
|
Це гарна ідея, щоб зберегти dupeGuru оновлено. Ви можете завантажити останню версію на своєму http://dupeguru.voltaicideas.net.
|
||||||
|
|
||||||
Contents:
|
Contents:
|
||||||
|
|
||||||
|
|||||||
@@ -336,7 +336,6 @@ def read_changelog_file(filename):
|
|||||||
with open(filename, "rt", encoding="utf-8") as fp:
|
with open(filename, "rt", encoding="utf-8") as fp:
|
||||||
contents = fp.read()
|
contents = fp.read()
|
||||||
splitted = re_changelog_header.split(contents)[1:] # the first item is empty
|
splitted = re_changelog_header.split(contents)[1:] # the first item is empty
|
||||||
# splitted = [version1, date1, desc1, version2, date2, ...]
|
|
||||||
result = []
|
result = []
|
||||||
for version, date_str, description in iter_by_three(iter(splitted)):
|
for version, date_str, description in iter_by_three(iter(splitted)):
|
||||||
date = datetime.strptime(date_str, "%Y-%m-%d").date()
|
date = datetime.strptime(date_str, "%Y-%m-%d").date()
|
||||||
@@ -399,8 +398,8 @@ def create_osx_app_structure(
|
|||||||
# `resources`: A list of paths of files or folders going in the "Resources" folder.
|
# `resources`: A list of paths of files or folders going in the "Resources" folder.
|
||||||
# `frameworks`: Same as above for "Frameworks".
|
# `frameworks`: Same as above for "Frameworks".
|
||||||
# `symlink_resources`: If True, will symlink resources into the structure instead of copying them.
|
# `symlink_resources`: If True, will symlink resources into the structure instead of copying them.
|
||||||
app = OSXAppStructure(dest, infoplist)
|
app = OSXAppStructure(dest)
|
||||||
app.create()
|
app.create(infoplist)
|
||||||
app.copy_executable(executable)
|
app.copy_executable(executable)
|
||||||
app.copy_resources(*resources, use_symlinks=symlink_resources)
|
app.copy_resources(*resources, use_symlinks=symlink_resources)
|
||||||
app.copy_frameworks(*frameworks)
|
app.copy_frameworks(*frameworks)
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import traceback
|
|||||||
# Taken from http://bzimmer.ziclix.com/2008/12/17/python-thread-dumps/
|
# Taken from http://bzimmer.ziclix.com/2008/12/17/python-thread-dumps/
|
||||||
def stacktraces():
|
def stacktraces():
|
||||||
code = []
|
code = []
|
||||||
for threadId, stack in sys._current_frames().items():
|
for thread_id, stack in sys._current_frames().items():
|
||||||
code.append("\n# ThreadID: %s" % threadId)
|
code.append("\n# ThreadID: %s" % thread_id)
|
||||||
for filename, lineno, name, line in traceback.extract_stack(stack):
|
for filename, lineno, name, line in traceback.extract_stack(stack):
|
||||||
code.append('File: "%s", line %d, in %s' % (filename, lineno, name))
|
code.append('File: "%s", line %d, in %s' % (filename, lineno, name))
|
||||||
if line:
|
if line:
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import logging
|
|||||||
|
|
||||||
|
|
||||||
class SpecialFolder:
|
class SpecialFolder:
|
||||||
AppData = 1
|
APPDATA = 1
|
||||||
Cache = 2
|
CACHE = 2
|
||||||
|
|
||||||
|
|
||||||
def open_url(url):
|
def open_url(url):
|
||||||
@@ -30,7 +30,7 @@ def reveal_path(path):
|
|||||||
_reveal_path(str(path))
|
_reveal_path(str(path))
|
||||||
|
|
||||||
|
|
||||||
def special_folder_path(special_folder, appname=None):
|
def special_folder_path(special_folder, appname=None, portable=False):
|
||||||
"""Returns the path of ``special_folder``.
|
"""Returns the path of ``special_folder``.
|
||||||
|
|
||||||
``special_folder`` is a SpecialFolder.* const. The result is the special folder for the current
|
``special_folder`` is a SpecialFolder.* const. The result is the special folder for the current
|
||||||
@@ -38,7 +38,7 @@ def special_folder_path(special_folder, appname=None):
|
|||||||
|
|
||||||
You can override the application name with ``appname``. This argument is ingored under Qt.
|
You can override the application name with ``appname``. This argument is ingored under Qt.
|
||||||
"""
|
"""
|
||||||
return _special_folder_path(special_folder, appname)
|
return _special_folder_path(special_folder, appname, portable=portable)
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -54,8 +54,8 @@ try:
|
|||||||
_open_path = proxy.openPath_
|
_open_path = proxy.openPath_
|
||||||
_reveal_path = proxy.revealPath_
|
_reveal_path = proxy.revealPath_
|
||||||
|
|
||||||
def _special_folder_path(special_folder, appname=None):
|
def _special_folder_path(special_folder, appname=None, portable=False):
|
||||||
if special_folder == SpecialFolder.Cache:
|
if special_folder == SpecialFolder.CACHE:
|
||||||
base = proxy.getCachePath()
|
base = proxy.getCachePath()
|
||||||
else:
|
else:
|
||||||
base = proxy.getAppdataPath()
|
base = proxy.getAppdataPath()
|
||||||
@@ -63,11 +63,14 @@ try:
|
|||||||
appname = proxy.bundleInfo_("CFBundleName")
|
appname = proxy.bundleInfo_("CFBundleName")
|
||||||
return op.join(base, appname)
|
return op.join(base, appname)
|
||||||
|
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
try:
|
try:
|
||||||
from PyQt5.QtCore import QUrl, QStandardPaths
|
from PyQt5.QtCore import QUrl, QStandardPaths
|
||||||
from PyQt5.QtGui import QDesktopServices
|
from PyQt5.QtGui import QDesktopServices
|
||||||
|
from qtlib.util import get_appdata
|
||||||
|
from core.util import executable_folder
|
||||||
|
from hscommon.plat import ISWINDOWS, ISOSX
|
||||||
|
import subprocess
|
||||||
|
|
||||||
def _open_url(url):
|
def _open_url(url):
|
||||||
QDesktopServices.openUrl(QUrl(url))
|
QDesktopServices.openUrl(QUrl(url))
|
||||||
@@ -77,14 +80,22 @@ except ImportError:
|
|||||||
QDesktopServices.openUrl(url)
|
QDesktopServices.openUrl(url)
|
||||||
|
|
||||||
def _reveal_path(path):
|
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)))
|
_open_path(op.dirname(str(path)))
|
||||||
|
|
||||||
def _special_folder_path(special_folder, appname=None):
|
def _special_folder_path(special_folder, appname=None, portable=False):
|
||||||
if special_folder == SpecialFolder.Cache:
|
if special_folder == SpecialFolder.CACHE:
|
||||||
qtfolder = QStandardPaths.CacheLocation
|
if ISWINDOWS and portable:
|
||||||
|
folder = op.join(executable_folder(), "cache")
|
||||||
else:
|
else:
|
||||||
qtfolder = QStandardPaths.DataLocation
|
folder = QStandardPaths.standardLocations(QStandardPaths.CacheLocation)[0]
|
||||||
return QStandardPaths.standardLocations(qtfolder)[0]
|
else:
|
||||||
|
folder = get_appdata(portable)
|
||||||
|
return folder
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# We're either running tests, and these functions don't matter much or we're in a really
|
# We're either running tests, and these functions don't matter much or we're in a really
|
||||||
@@ -92,10 +103,12 @@ except ImportError:
|
|||||||
logging.warning("Can't setup desktop functions!")
|
logging.warning("Can't setup desktop functions!")
|
||||||
|
|
||||||
def _open_path(path):
|
def _open_path(path):
|
||||||
|
# Dummy for tests
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _reveal_path(path):
|
def _reveal_path(path):
|
||||||
|
# Dummy for tests
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _special_folder_path(special_folder, appname=None):
|
def _special_folder_path(special_folder, appname=None, portable=False):
|
||||||
return "/tmp"
|
return "/tmp"
|
||||||
|
|||||||
@@ -139,31 +139,34 @@ class Job:
|
|||||||
self._progress = progress
|
self._progress = progress
|
||||||
if self._progress > self._currmax:
|
if self._progress > self._currmax:
|
||||||
self._progress = self._currmax
|
self._progress = self._currmax
|
||||||
if self._progress < 0:
|
|
||||||
self._progress = 0
|
|
||||||
self._do_update(desc)
|
self._do_update(desc)
|
||||||
|
|
||||||
|
|
||||||
class NullJob:
|
class NullJob:
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
# Null job does nothing
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def add_progress(self, *args, **kwargs):
|
def add_progress(self, *args, **kwargs):
|
||||||
|
# Null job does nothing
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def check_if_cancelled(self):
|
def check_if_cancelled(self):
|
||||||
|
# Null job does nothing
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def iter_with_progress(self, sequence, *args, **kwargs):
|
def iter_with_progress(self, sequence, *args, **kwargs):
|
||||||
return iter(sequence)
|
return iter(sequence)
|
||||||
|
|
||||||
def start_job(self, *args, **kwargs):
|
def start_job(self, *args, **kwargs):
|
||||||
|
# Null job does nothing
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def start_subjob(self, *args, **kwargs):
|
def start_subjob(self, *args, **kwargs):
|
||||||
return NullJob()
|
return NullJob()
|
||||||
|
|
||||||
def set_progress(self, *args, **kwargs):
|
def set_progress(self, *args, **kwargs):
|
||||||
|
# Null job does nothing
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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()}
|
COCOA2PO = {v: k for k, v in PO2COCOA.items()}
|
||||||
|
|
||||||
|
STRING_EXT = ".strings"
|
||||||
|
|
||||||
|
|
||||||
def get_langs(folder):
|
def get_langs(folder):
|
||||||
return [name for name in os.listdir(folder) if op.isdir(op.join(folder, name))]
|
return [name for name in os.listdir(folder) if op.isdir(op.join(folder, name))]
|
||||||
@@ -152,7 +154,7 @@ def strings2pot(target, dest):
|
|||||||
|
|
||||||
|
|
||||||
def allstrings2pot(lprojpath, dest, excludes=None):
|
def allstrings2pot(lprojpath, dest, excludes=None):
|
||||||
allstrings = files_with_ext(lprojpath, ".strings")
|
allstrings = files_with_ext(lprojpath, STRING_EXT)
|
||||||
if excludes:
|
if excludes:
|
||||||
allstrings = [p for p in allstrings if op.splitext(op.basename(p))[0] not in excludes]
|
allstrings = [p for p in allstrings if op.splitext(op.basename(p))[0] not in excludes]
|
||||||
for strings_path in allstrings:
|
for strings_path in allstrings:
|
||||||
@@ -210,7 +212,7 @@ def generate_cocoa_strings_from_code(code_folder, dest_folder):
|
|||||||
def generate_cocoa_strings_from_xib(xib_folder):
|
def generate_cocoa_strings_from_xib(xib_folder):
|
||||||
xibs = [op.join(xib_folder, fn) for fn in os.listdir(xib_folder) if fn.endswith(".xib")]
|
xibs = [op.join(xib_folder, fn) for fn in os.listdir(xib_folder) if fn.endswith(".xib")]
|
||||||
for xib in xibs:
|
for xib in xibs:
|
||||||
dest = xib.replace(".xib", ".strings")
|
dest = xib.replace(".xib", STRING_EXT)
|
||||||
print_and_do("ibtool {} --generate-strings-file {}".format(xib, dest))
|
print_and_do("ibtool {} --generate-strings-file {}".format(xib, dest))
|
||||||
print_and_do("iconv -f utf-16 -t utf-8 {0} | tee {0}".format(dest))
|
print_and_do("iconv -f utf-16 -t utf-8 {0} | tee {0}".format(dest))
|
||||||
|
|
||||||
@@ -226,6 +228,6 @@ def localize_stringsfile(stringsfile, dest_root_folder):
|
|||||||
|
|
||||||
|
|
||||||
def localize_all_stringsfiles(src_folder, dest_root_folder):
|
def localize_all_stringsfiles(src_folder, dest_root_folder):
|
||||||
stringsfiles = [op.join(src_folder, fn) for fn in os.listdir(src_folder) if fn.endswith(".strings")]
|
stringsfiles = [op.join(src_folder, fn) for fn in os.listdir(src_folder) if fn.endswith(STRING_EXT)]
|
||||||
for path in stringsfiles:
|
for path in stringsfiles:
|
||||||
localize_stringsfile(path, dest_root_folder)
|
localize_stringsfile(path, dest_root_folder)
|
||||||
|
|||||||
@@ -167,10 +167,10 @@ def getFilesForName(name):
|
|||||||
# check for glob chars
|
# check for glob chars
|
||||||
if containsAny(name, "*?[]"):
|
if containsAny(name, "*?[]"):
|
||||||
files = glob.glob(name)
|
files = glob.glob(name)
|
||||||
list = []
|
file_list = []
|
||||||
for file in files:
|
for file in files:
|
||||||
list.extend(getFilesForName(file))
|
file_list.extend(getFilesForName(file))
|
||||||
return list
|
return file_list
|
||||||
|
|
||||||
# try to find module or package
|
# try to find module or package
|
||||||
name = _get_modpkg_path(name)
|
name = _get_modpkg_path(name)
|
||||||
@@ -179,9 +179,9 @@ def getFilesForName(name):
|
|||||||
|
|
||||||
if os.path.isdir(name):
|
if os.path.isdir(name):
|
||||||
# find all python files in directory
|
# find all python files in directory
|
||||||
list = []
|
file_list = []
|
||||||
os.walk(name, _visit_pyfiles, list)
|
os.walk(name, _visit_pyfiles, file_list)
|
||||||
return list
|
return file_list
|
||||||
elif os.path.exists(name):
|
elif os.path.exists(name):
|
||||||
# a single file
|
# a single file
|
||||||
return [name]
|
return [name]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
import os.path as op
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .build import read_changelog_file, filereplace
|
from .build import read_changelog_file, filereplace
|
||||||
@@ -48,9 +48,9 @@ def gen(
|
|||||||
if confrepl is None:
|
if confrepl is None:
|
||||||
confrepl = {}
|
confrepl = {}
|
||||||
if confpath is None:
|
if confpath is None:
|
||||||
confpath = op.join(basepath, "conf.tmpl")
|
confpath = Path(basepath, "conf.tmpl")
|
||||||
if changelogtmpl is None:
|
if changelogtmpl is None:
|
||||||
changelogtmpl = op.join(basepath, "changelog.tmpl")
|
changelogtmpl = Path(basepath, "changelog.tmpl")
|
||||||
changelog = read_changelog_file(changelogpath)
|
changelog = read_changelog_file(changelogpath)
|
||||||
tix = tixgen(tixurl)
|
tix = tixgen(tixurl)
|
||||||
rendered_logs = []
|
rendered_logs = []
|
||||||
@@ -62,13 +62,13 @@ def gen(
|
|||||||
rendered = CHANGELOG_FORMAT.format(version=log["version"], date=log["date_str"], description=description)
|
rendered = CHANGELOG_FORMAT.format(version=log["version"], date=log["date_str"], description=description)
|
||||||
rendered_logs.append(rendered)
|
rendered_logs.append(rendered)
|
||||||
confrepl["version"] = changelog[0]["version"]
|
confrepl["version"] = changelog[0]["version"]
|
||||||
changelog_out = op.join(basepath, "changelog.rst")
|
changelog_out = Path(basepath, "changelog.rst")
|
||||||
filereplace(changelogtmpl, changelog_out, changelog="\n".join(rendered_logs))
|
filereplace(changelogtmpl, changelog_out, changelog="\n".join(rendered_logs))
|
||||||
if op.exists(confpath):
|
if Path(confpath).exists():
|
||||||
conf_out = op.join(basepath, "conf.py")
|
conf_out = Path(basepath, "conf.py")
|
||||||
filereplace(confpath, conf_out, **confrepl)
|
filereplace(confpath, conf_out, **confrepl)
|
||||||
# Call the sphinx_build function, which is the same as doing sphinx-build from cli
|
# Call the sphinx_build function, which is the same as doing sphinx-build from cli
|
||||||
try:
|
try:
|
||||||
sphinx_build([basepath, destpath])
|
sphinx_build([str(basepath), str(destpath)])
|
||||||
except SystemExit:
|
except SystemExit:
|
||||||
print("Sphinx called sys.exit(), but we're cancelling it because we don't actually want to exit")
|
print("Sphinx called sys.exit(), but we're cancelling it because we don't actually want to exit")
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class _ActualThread(threading.Thread):
|
|||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
self._run = True
|
self._run = True
|
||||||
self.lastrowid = -1
|
self.lastrowid = -1
|
||||||
self.setDaemon(True)
|
self.daemon = True
|
||||||
self.start()
|
self.start()
|
||||||
|
|
||||||
def _query(self, query):
|
def _query(self, query):
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from ..path import Path
|
|||||||
from ..testutil import eq_
|
from ..testutil import eq_
|
||||||
|
|
||||||
|
|
||||||
class TestCase_GetConflictedName:
|
class TestCaseGetConflictedName:
|
||||||
def test_simple(self):
|
def test_simple(self):
|
||||||
name = get_conflicted_name(["bar"], "bar")
|
name = get_conflicted_name(["bar"], "bar")
|
||||||
eq_("[000] bar", name)
|
eq_("[000] bar", name)
|
||||||
@@ -46,7 +46,7 @@ class TestCase_GetConflictedName:
|
|||||||
eq_("[000] bar", name)
|
eq_("[000] bar", name)
|
||||||
|
|
||||||
|
|
||||||
class TestCase_GetUnconflictedName:
|
class TestCaseGetUnconflictedName:
|
||||||
def test_main(self):
|
def test_main(self):
|
||||||
eq_("foobar", get_unconflicted_name("[000] foobar"))
|
eq_("foobar", get_unconflicted_name("[000] foobar"))
|
||||||
eq_("foobar", get_unconflicted_name("[9999] foobar"))
|
eq_("foobar", get_unconflicted_name("[9999] foobar"))
|
||||||
@@ -56,7 +56,7 @@ class TestCase_GetUnconflictedName:
|
|||||||
eq_("foo [000] bar", get_unconflicted_name("foo [000] bar"))
|
eq_("foo [000] bar", get_unconflicted_name("foo [000] bar"))
|
||||||
|
|
||||||
|
|
||||||
class TestCase_IsConflicted:
|
class TestCaseIsConflicted:
|
||||||
def test_main(self):
|
def test_main(self):
|
||||||
assert is_conflicted("[000] foobar")
|
assert is_conflicted("[000] foobar")
|
||||||
assert is_conflicted("[9999] foobar")
|
assert is_conflicted("[9999] foobar")
|
||||||
@@ -66,7 +66,7 @@ class TestCase_IsConflicted:
|
|||||||
assert not is_conflicted("foo [000] bar")
|
assert not is_conflicted("foo [000] bar")
|
||||||
|
|
||||||
|
|
||||||
class TestCase_move_copy:
|
class TestCaseMoveCopy:
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def do_setup(self, request):
|
def do_setup(self, request):
|
||||||
tmpdir = request.getfixturevalue("tmpdir")
|
tmpdir = request.getfixturevalue("tmpdir")
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ def test_init_with_tuple_and_list(force_ossep):
|
|||||||
|
|
||||||
def test_init_with_invalid_value(force_ossep):
|
def test_init_with_invalid_value(force_ossep):
|
||||||
try:
|
try:
|
||||||
path = Path(42) # noqa: F841
|
Path(42)
|
||||||
assert False
|
assert False
|
||||||
except TypeError:
|
except TypeError:
|
||||||
pass
|
pass
|
||||||
@@ -142,8 +142,6 @@ def test_path_slice(force_ossep):
|
|||||||
eq_((), foobar[:foobar])
|
eq_((), foobar[:foobar])
|
||||||
abcd = Path("a/b/c/d")
|
abcd = Path("a/b/c/d")
|
||||||
a = Path("a")
|
a = Path("a")
|
||||||
b = Path("b") # noqa: #F841
|
|
||||||
c = Path("c") # noqa: #F841
|
|
||||||
d = Path("d")
|
d = Path("d")
|
||||||
z = Path("z")
|
z = Path("z")
|
||||||
eq_("b/c", abcd[a:d])
|
eq_("b/c", abcd[a:d])
|
||||||
@@ -216,7 +214,7 @@ def test_str_repr_of_mix_between_non_ascii_str_and_unicode(force_ossep):
|
|||||||
eq_("foo\u00e9/bar".encode(sys.getfilesystemencoding()), p.tobytes())
|
eq_("foo\u00e9/bar".encode(sys.getfilesystemencoding()), p.tobytes())
|
||||||
|
|
||||||
|
|
||||||
def test_Path_of_a_Path_returns_self(force_ossep):
|
def test_path_of_a_path_returns_self(force_ossep):
|
||||||
# if Path() is called with a path as value, just return value.
|
# if Path() is called with a path as value, just return value.
|
||||||
p = Path("foo/bar")
|
p = Path("foo/bar")
|
||||||
assert Path(p) is p
|
assert Path(p) is p
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ def test_make_sure_theres_no_messup_between_queries():
|
|||||||
threads = []
|
threads = []
|
||||||
for i in range(1, 101):
|
for i in range(1, 101):
|
||||||
t = threading.Thread(target=run, args=(i,))
|
t = threading.Thread(target=run, args=(i,))
|
||||||
t.start
|
t.start()
|
||||||
threads.append(t)
|
threads.append(t)
|
||||||
while threads:
|
while threads:
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class TestRow(Row):
|
|||||||
self._index = index
|
self._index = index
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
|
# Does nothing for test
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
@@ -75,14 +76,17 @@ def test_allow_edit_when_attr_is_property_with_fset():
|
|||||||
class TestRow(Row):
|
class TestRow(Row):
|
||||||
@property
|
@property
|
||||||
def foo(self):
|
def foo(self):
|
||||||
|
# property only for existence checks
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bar(self):
|
def bar(self):
|
||||||
|
# property only for existence checks
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@bar.setter
|
@bar.setter
|
||||||
def bar(self, value):
|
def bar(self, value):
|
||||||
|
# setter only for existence checks
|
||||||
pass
|
pass
|
||||||
|
|
||||||
row = TestRow(Table())
|
row = TestRow(Table())
|
||||||
@@ -97,10 +101,12 @@ def test_can_edit_prop_has_priority_over_fset_checks():
|
|||||||
class TestRow(Row):
|
class TestRow(Row):
|
||||||
@property
|
@property
|
||||||
def bar(self):
|
def bar(self):
|
||||||
|
# property only for existence checks
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@bar.setter
|
@bar.setter
|
||||||
def bar(self, value):
|
def bar(self, value):
|
||||||
|
# setter only for existence checks
|
||||||
pass
|
pass
|
||||||
|
|
||||||
can_edit_bar = False
|
can_edit_bar = False
|
||||||
|
|||||||
@@ -236,49 +236,8 @@ def test_multi_replace():
|
|||||||
|
|
||||||
# --- Files
|
# --- Files
|
||||||
|
|
||||||
# These test cases needed https://github.com/hsoft/pytest-monkeyplus/ which appears to not be compatible with latest
|
|
||||||
# pytest, looking at where this is used only appears to be in hscommon.localize_all_stringfiles at top level.
|
|
||||||
# Right now this repo does not seem to utilize any of that functionality so going to leave these tests out for now.
|
|
||||||
# TODO decide if fixing these tests is worth it or not.
|
|
||||||
|
|
||||||
# class TestCase_modified_after:
|
class TestCaseDeleteIfEmpty:
|
||||||
# def test_first_is_modified_after(self, monkeyplus):
|
|
||||||
# monkeyplus.patch_osstat("first", st_mtime=42)
|
|
||||||
# monkeyplus.patch_osstat("second", st_mtime=41)
|
|
||||||
# assert modified_after("first", "second")
|
|
||||||
|
|
||||||
# def test_second_is_modified_after(self, monkeyplus):
|
|
||||||
# monkeyplus.patch_osstat("first", st_mtime=42)
|
|
||||||
# monkeyplus.patch_osstat("second", st_mtime=43)
|
|
||||||
# assert not modified_after("first", "second")
|
|
||||||
|
|
||||||
# def test_same_mtime(self, monkeyplus):
|
|
||||||
# monkeyplus.patch_osstat("first", st_mtime=42)
|
|
||||||
# monkeyplus.patch_osstat("second", st_mtime=42)
|
|
||||||
# assert not modified_after("first", "second")
|
|
||||||
|
|
||||||
# def test_first_file_does_not_exist(self, monkeyplus):
|
|
||||||
# # when the first file doesn't exist, we return False
|
|
||||||
# monkeyplus.patch_osstat("second", st_mtime=42)
|
|
||||||
# assert not modified_after("does_not_exist", "second") # no crash
|
|
||||||
|
|
||||||
# def test_second_file_does_not_exist(self, monkeyplus):
|
|
||||||
# # when the second file doesn't exist, we return True
|
|
||||||
# monkeyplus.patch_osstat("first", st_mtime=42)
|
|
||||||
# assert modified_after("first", "does_not_exist") # no crash
|
|
||||||
|
|
||||||
# def test_first_file_is_none(self, monkeyplus):
|
|
||||||
# # when the first file is None, we return False
|
|
||||||
# monkeyplus.patch_osstat("second", st_mtime=42)
|
|
||||||
# assert not modified_after(None, "second") # no crash
|
|
||||||
|
|
||||||
# def test_second_file_is_none(self, monkeyplus):
|
|
||||||
# # when the second file is None, we return True
|
|
||||||
# monkeyplus.patch_osstat("first", st_mtime=42)
|
|
||||||
# assert modified_after("first", None) # no crash
|
|
||||||
|
|
||||||
|
|
||||||
class TestCase_delete_if_empty:
|
|
||||||
def test_is_empty(self, tmpdir):
|
def test_is_empty(self, tmpdir):
|
||||||
testpath = Path(str(tmpdir))
|
testpath = Path(str(tmpdir))
|
||||||
assert delete_if_empty(testpath)
|
assert delete_if_empty(testpath)
|
||||||
@@ -330,9 +289,11 @@ class TestCase_delete_if_empty:
|
|||||||
delete_if_empty(Path(str(tmpdir))) # no crash
|
delete_if_empty(Path(str(tmpdir))) # no crash
|
||||||
|
|
||||||
|
|
||||||
class TestCase_open_if_filename:
|
class TestCaseOpenIfFilename:
|
||||||
|
FILE_NAME = "test.txt"
|
||||||
|
|
||||||
def test_file_name(self, tmpdir):
|
def test_file_name(self, tmpdir):
|
||||||
filepath = str(tmpdir.join("test.txt"))
|
filepath = str(tmpdir.join(self.FILE_NAME))
|
||||||
open(filepath, "wb").write(b"test_data")
|
open(filepath, "wb").write(b"test_data")
|
||||||
file, close = open_if_filename(filepath)
|
file, close = open_if_filename(filepath)
|
||||||
assert close
|
assert close
|
||||||
@@ -348,16 +309,18 @@ class TestCase_open_if_filename:
|
|||||||
eq_("test_data", file.read())
|
eq_("test_data", file.read())
|
||||||
|
|
||||||
def test_mode_is_passed_to_open(self, tmpdir):
|
def test_mode_is_passed_to_open(self, tmpdir):
|
||||||
filepath = str(tmpdir.join("test.txt"))
|
filepath = str(tmpdir.join(self.FILE_NAME))
|
||||||
open(filepath, "w").close()
|
open(filepath, "w").close()
|
||||||
file, close = open_if_filename(filepath, "a")
|
file, close = open_if_filename(filepath, "a")
|
||||||
eq_("a", file.mode)
|
eq_("a", file.mode)
|
||||||
file.close()
|
file.close()
|
||||||
|
|
||||||
|
|
||||||
class TestCase_FileOrPath:
|
class TestCaseFileOrPath:
|
||||||
|
FILE_NAME = "test.txt"
|
||||||
|
|
||||||
def test_path(self, tmpdir):
|
def test_path(self, tmpdir):
|
||||||
filepath = str(tmpdir.join("test.txt"))
|
filepath = str(tmpdir.join(self.FILE_NAME))
|
||||||
open(filepath, "wb").write(b"test_data")
|
open(filepath, "wb").write(b"test_data")
|
||||||
with FileOrPath(filepath) as fp:
|
with FileOrPath(filepath) as fp:
|
||||||
eq_(b"test_data", fp.read())
|
eq_(b"test_data", fp.read())
|
||||||
@@ -370,7 +333,7 @@ class TestCase_FileOrPath:
|
|||||||
eq_("test_data", fp.read())
|
eq_("test_data", fp.read())
|
||||||
|
|
||||||
def test_mode_is_passed_to_open(self, tmpdir):
|
def test_mode_is_passed_to_open(self, tmpdir):
|
||||||
filepath = str(tmpdir.join("test.txt"))
|
filepath = str(tmpdir.join(self.FILE_NAME))
|
||||||
open(filepath, "w").close()
|
open(filepath, "w").close()
|
||||||
with FileOrPath(filepath, "a") as fp:
|
with FileOrPath(filepath, "a") as fp:
|
||||||
eq_("a", fp.mode)
|
eq_("a", fp.mode)
|
||||||
|
|||||||
@@ -230,8 +230,8 @@ def log_calls(func):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
unifiedArgs = _unify_args(func, args, kwargs)
|
unified_args = _unify_args(func, args, kwargs)
|
||||||
wrapper.calls.append(unifiedArgs)
|
wrapper.calls.append(unified_args)
|
||||||
return func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
wrapper.calls = []
|
wrapper.calls = []
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ def get_locale_name(lang):
|
|||||||
"it": "it_IT",
|
"it": "it_IT",
|
||||||
"ja": "ja_JP",
|
"ja": "ja_JP",
|
||||||
"ko": "ko_KR",
|
"ko": "ko_KR",
|
||||||
|
"ms": "ms_MY",
|
||||||
"nl": "nl_NL",
|
"nl": "nl_NL",
|
||||||
"pl_PL": "pl_PL",
|
"pl_PL": "pl_PL",
|
||||||
"pt_BR": "pt_BR",
|
"pt_BR": "pt_BR",
|
||||||
@@ -131,11 +132,11 @@ def install_gettext_trans(base_folder, lang):
|
|||||||
def install_gettext_trans_under_cocoa():
|
def install_gettext_trans_under_cocoa():
|
||||||
from cocoa import proxy
|
from cocoa import proxy
|
||||||
|
|
||||||
resFolder = proxy.getResourcePath()
|
res_folder = proxy.getResourcePath()
|
||||||
baseFolder = op.join(resFolder, "locale")
|
base_folder = op.join(res_folder, "locale")
|
||||||
currentLang = proxy.systemLang()
|
current_lang = proxy.systemLang()
|
||||||
install_gettext_trans(baseFolder, currentLang)
|
install_gettext_trans(base_folder, current_lang)
|
||||||
localename = get_locale_name(currentLang)
|
localename = get_locale_name(current_lang)
|
||||||
if localename is not None:
|
if localename is not None:
|
||||||
locale.setlocale(locale.LC_ALL, localename)
|
locale.setlocale(locale.LC_ALL, localename)
|
||||||
|
|
||||||
@@ -149,7 +150,9 @@ def install_gettext_trans_under_qt(base_folder, lang=None):
|
|||||||
if not lang:
|
if not lang:
|
||||||
lang = str(QLocale.system().name())[:2]
|
lang = str(QLocale.system().name())[:2]
|
||||||
localename = get_locale_name(lang)
|
localename = get_locale_name(lang)
|
||||||
if localename is not None:
|
if localename is None:
|
||||||
|
lang = "en"
|
||||||
|
localename = get_locale_name(lang)
|
||||||
try:
|
try:
|
||||||
locale.setlocale(locale.LC_ALL, localename)
|
locale.setlocale(locale.LC_ALL, localename)
|
||||||
except locale.Error:
|
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
|
``plural_word``: If the plural rule for word is more complex than adding a 's', specify a plural
|
||||||
"""
|
"""
|
||||||
number = round(number, decimals)
|
number = round(number, decimals)
|
||||||
format = "%%1.%df %%s" % decimals
|
plural_format = "%%1.%df %%s" % decimals
|
||||||
if number > 1:
|
if number > 1:
|
||||||
if plural_word is None:
|
if plural_word is None:
|
||||||
word += "s"
|
word += "s"
|
||||||
else:
|
else:
|
||||||
word = plural_word
|
word = plural_word
|
||||||
return format % (number, word)
|
return plural_format % (number, word)
|
||||||
|
|
||||||
|
|
||||||
def format_time(seconds, with_hours=True):
|
def format_time(seconds, with_hours=True):
|
||||||
@@ -226,7 +226,7 @@ def format_time_decimal(seconds):
|
|||||||
|
|
||||||
|
|
||||||
SIZE_DESC = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
|
SIZE_DESC = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
|
||||||
SIZE_VALS = tuple(1024 ** i for i in range(1, 9))
|
SIZE_VALS = tuple(1024**i for i in range(1, 9))
|
||||||
|
|
||||||
|
|
||||||
def format_size(size, decimal=0, forcepower=-1, showdesc=True):
|
def format_size(size, decimal=0, forcepower=-1, showdesc=True):
|
||||||
@@ -252,16 +252,16 @@ def format_size(size, decimal=0, forcepower=-1, showdesc=True):
|
|||||||
div = SIZE_VALS[i - 1]
|
div = SIZE_VALS[i - 1]
|
||||||
else:
|
else:
|
||||||
div = 1
|
div = 1
|
||||||
format = "%%%d.%df" % (decimal, decimal)
|
size_format = "%%%d.%df" % (decimal, decimal)
|
||||||
negative = size < 0
|
negative = size < 0
|
||||||
divided_size = (0.0 + abs(size)) / div
|
divided_size = (0.0 + abs(size)) / div
|
||||||
if decimal == 0:
|
if decimal == 0:
|
||||||
divided_size = ceil(divided_size)
|
divided_size = ceil(divided_size)
|
||||||
else:
|
else:
|
||||||
divided_size = ceil(divided_size * (10 ** decimal)) / (10 ** decimal)
|
divided_size = ceil(divided_size * (10**decimal)) / (10**decimal)
|
||||||
if negative:
|
if negative:
|
||||||
divided_size *= -1
|
divided_size *= -1
|
||||||
result = format % divided_size
|
result = size_format % divided_size
|
||||||
if showdesc:
|
if showdesc:
|
||||||
result += " " + SIZE_DESC[i]
|
result += " " + SIZE_DESC[i]
|
||||||
return result
|
return result
|
||||||
@@ -292,7 +292,7 @@ def multi_replace(s, replace_from, replace_to=""):
|
|||||||
the same length as ``replace_from``, it will be transformed into a list.
|
the same length as ``replace_from``, it will be transformed into a list.
|
||||||
"""
|
"""
|
||||||
if isinstance(replace_to, str) and (len(replace_from) != len(replace_to)):
|
if isinstance(replace_to, str) and (len(replace_from) != len(replace_to)):
|
||||||
replace_to = [replace_to for r in replace_from]
|
replace_to = [replace_to for _ in replace_from]
|
||||||
if len(replace_from) != len(replace_to):
|
if len(replace_from) != len(replace_to):
|
||||||
raise ValueError("len(replace_from) must be equal to len(replace_to)")
|
raise ValueError("len(replace_from) must be equal to len(replace_to)")
|
||||||
replace = list(zip(replace_from, replace_to))
|
replace = list(zip(replace_from, replace_to))
|
||||||
|
|||||||
@@ -36,95 +36,103 @@ msgstr ""
|
|||||||
msgid "Sending to Trash"
|
msgid "Sending to Trash"
|
||||||
msgstr ""
|
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."
|
msgid "A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:318
|
#: core\app.py:300
|
||||||
msgid "No duplicates found."
|
msgid "No duplicates found."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:333
|
#: core\app.py:315
|
||||||
msgid "All marked files were copied successfully."
|
msgid "All marked files were copied successfully."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:334
|
#: core\app.py:317
|
||||||
msgid "All marked files were moved successfully."
|
msgid "All marked files were moved successfully."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:335
|
#: core\app.py:319
|
||||||
|
msgid "All marked files were deleted successfully."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core\app.py:321
|
||||||
msgid "All marked files were successfully sent to Trash."
|
msgid "All marked files were successfully sent to Trash."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:343
|
#: core\app.py:326
|
||||||
msgid "Could not load file: {}"
|
msgid "Could not load file: {}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:399
|
#: core\app.py:382
|
||||||
msgid "'{}' already is in the list."
|
msgid "'{}' already is in the list."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:401
|
#: core\app.py:384
|
||||||
msgid "'{}' does not exist."
|
msgid "'{}' does not exist."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:410
|
#: core\app.py:392
|
||||||
msgid "All selected %d matches are going to be ignored in all subsequent scans. Continue?"
|
msgid "All selected %d matches are going to be ignored in all subsequent scans. Continue?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:486
|
#: core\app.py:469
|
||||||
msgid "Select a directory to copy marked files to"
|
msgid "Select a directory to copy marked files to"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:487
|
#: core\app.py:471
|
||||||
msgid "Select a directory to move marked files to"
|
msgid "Select a directory to move marked files to"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:527
|
#: core\app.py:510
|
||||||
msgid "Select a destination for your exported CSV"
|
msgid "Select a destination for your exported CSV"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:534 core\app.py:803 core\app.py:813
|
#: core\app.py:516 core\app.py:771 core\app.py:781
|
||||||
msgid "Couldn't write to file: {}"
|
msgid "Couldn't write to file: {}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:559
|
#: core\app.py:539
|
||||||
msgid "You have no custom command set up. Set it up in your preferences."
|
msgid "You have no custom command set up. Set it up in your preferences."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:727 core\app.py:740
|
#: core\app.py:695 core\app.py:707
|
||||||
msgid "You are about to remove %d files from results. Continue?"
|
msgid "You are about to remove %d files from results. Continue?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:776
|
#: core\app.py:743
|
||||||
msgid "{} duplicate groups were changed by the re-prioritization."
|
msgid "{} duplicate groups were changed by the re-prioritization."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:823
|
#: core\app.py:790
|
||||||
msgid "The selected directories contain no scannable file."
|
msgid "The selected directories contain no scannable file."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:837
|
#: core\app.py:803
|
||||||
msgid "Collecting files to scan"
|
msgid "Collecting files to scan"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:893
|
#: core\app.py:850
|
||||||
msgid "%s (%d discarded)"
|
msgid "%s (%d discarded)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\engine.py:255 core\engine.py:299
|
#: core\directories.py:191
|
||||||
msgid "0 matches found"
|
msgid "Collected {} files to scan"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\engine.py:273 core\engine.py:307
|
#: core\directories.py:207
|
||||||
msgid "%d matches found"
|
msgid "Collected {} folders to scan"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\gui\deletion_options.py:73
|
#: core\engine.py:27
|
||||||
|
msgid "%d matches found from %d groups"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core\gui\deletion_options.py:71
|
||||||
msgid "You are sending {} file(s) to the Trash."
|
msgid "You are sending {} file(s) to the Trash."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\gui\exclude_list_table.py:15
|
#: core\gui\exclude_list_table.py:14
|
||||||
msgid "Regular Expressions"
|
msgid "Regular Expressions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -156,15 +164,15 @@ msgstr ""
|
|||||||
msgid "Analyzed %d/%d pictures"
|
msgid "Analyzed %d/%d pictures"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\pe\matchblock.py:181
|
#: core\pe\matchblock.py:177
|
||||||
msgid "Performed %d/%d chunk matches"
|
msgid "Performed %d/%d chunk matches"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\pe\matchblock.py:191
|
#: core\pe\matchblock.py:185
|
||||||
msgid "Preparing for matching"
|
msgid "Preparing for matching"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\pe\matchblock.py:244
|
#: core\pe\matchblock.py:234
|
||||||
msgid "Verified %d/%d matches"
|
msgid "Verified %d/%d matches"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -212,23 +220,23 @@ msgstr ""
|
|||||||
msgid "Oldest"
|
msgid "Oldest"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\results.py:144
|
#: core\results.py:134
|
||||||
msgid "%d / %d (%s / %s) duplicates marked."
|
msgid "%d / %d (%s / %s) duplicates marked."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\results.py:151
|
#: core\results.py:141
|
||||||
msgid " filter: %s"
|
msgid " filter: %s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\scanner.py:85
|
#: core\scanner.py:90
|
||||||
msgid "Read size of %d/%d files"
|
msgid "Read size of %d/%d files"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\scanner.py:109
|
#: core\scanner.py:116
|
||||||
msgid "Read metadata of %d/%d files"
|
msgid "Read metadata of %d/%d files"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\scanner.py:147
|
#: core\scanner.py:154
|
||||||
msgid "Almost done! Fiddling with results..."
|
msgid "Almost done! Fiddling with results..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ msgstr "Kopíruji"
|
|||||||
msgid "Sending to Trash"
|
msgid "Sending to Trash"
|
||||||
msgstr "Vyhazuji do koše"
|
msgstr "Vyhazuji do koše"
|
||||||
|
|
||||||
#: core\app.py:308
|
#: core\app.py:289
|
||||||
msgid ""
|
msgid ""
|
||||||
"A previous action is still hanging in there. You can't start a new one yet. "
|
"A previous action is still hanging in there. You can't start a new one yet. "
|
||||||
"Wait a few seconds, then try again."
|
"Wait a few seconds, then try again."
|
||||||
@@ -55,35 +55,39 @@ msgstr ""
|
|||||||
"Předchozí akce stále nebyla ukončena. Novou zatím nemůžete spustit. Počkejte"
|
"Předchozí akce stále nebyla ukončena. Novou zatím nemůžete spustit. Počkejte"
|
||||||
" pár sekund a zkuste to znovu."
|
" pár sekund a zkuste to znovu."
|
||||||
|
|
||||||
#: core\app.py:318
|
#: core\app.py:300
|
||||||
msgid "No duplicates found."
|
msgid "No duplicates found."
|
||||||
msgstr "Nebyli nalezeny žádné duplicity."
|
msgstr "Nebyli nalezeny žádné duplicity."
|
||||||
|
|
||||||
#: core\app.py:333
|
#: core\app.py:315
|
||||||
msgid "All marked files were copied successfully."
|
msgid "All marked files were copied successfully."
|
||||||
msgstr "Všechny označené soubory byly úspěšně zkopírovány."
|
msgstr "Všechny označené soubory byly úspěšně zkopírovány."
|
||||||
|
|
||||||
#: core\app.py:334
|
#: core\app.py:317
|
||||||
msgid "All marked files were moved successfully."
|
msgid "All marked files were moved successfully."
|
||||||
msgstr "Všechny označené soubory byly úspěšně přesunuty."
|
msgstr "Všechny označené soubory byly úspěšně přesunuty."
|
||||||
|
|
||||||
#: core\app.py:335
|
#: core\app.py:319
|
||||||
|
msgid "All marked files were deleted successfully."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core\app.py:321
|
||||||
msgid "All marked files were successfully sent to Trash."
|
msgid "All marked files were successfully sent to Trash."
|
||||||
msgstr "Všechny označené soubory byly úspěšně odeslány do koše."
|
msgstr "Všechny označené soubory byly úspěšně odeslány do koše."
|
||||||
|
|
||||||
#: core\app.py:343
|
#: core\app.py:326
|
||||||
msgid "Could not load file: {}"
|
msgid "Could not load file: {}"
|
||||||
msgstr "Soubor se nepodařilo načíst: {}"
|
msgstr "Soubor se nepodařilo načíst: {}"
|
||||||
|
|
||||||
#: core\app.py:399
|
#: core\app.py:382
|
||||||
msgid "'{}' already is in the list."
|
msgid "'{}' already is in the list."
|
||||||
msgstr "'{}' již je v seznamu."
|
msgstr "'{}' již je v seznamu."
|
||||||
|
|
||||||
#: core\app.py:401
|
#: core\app.py:384
|
||||||
msgid "'{}' does not exist."
|
msgid "'{}' does not exist."
|
||||||
msgstr "'{}' neexistuje."
|
msgstr "'{}' neexistuje."
|
||||||
|
|
||||||
#: core\app.py:410
|
#: core\app.py:392
|
||||||
msgid ""
|
msgid ""
|
||||||
"All selected %d matches are going to be ignored in all subsequent scans. "
|
"All selected %d matches are going to be ignored in all subsequent scans. "
|
||||||
"Continue?"
|
"Continue?"
|
||||||
@@ -91,60 +95,64 @@ msgstr ""
|
|||||||
"Všech %d vybraných shod bude v následujících hledáních ignorováno. "
|
"Všech %d vybraných shod bude v následujících hledáních ignorováno. "
|
||||||
"Pokračovat?"
|
"Pokračovat?"
|
||||||
|
|
||||||
#: core\app.py:486
|
#: core\app.py:469
|
||||||
msgid "Select a directory to copy marked files to"
|
msgid "Select a directory to copy marked files to"
|
||||||
msgstr "Vyberte adresář, do kterého chcete zkopírovat označené soubory"
|
msgstr "Vyberte adresář, do kterého chcete zkopírovat označené soubory"
|
||||||
|
|
||||||
#: core\app.py:487
|
#: core\app.py:471
|
||||||
msgid "Select a directory to move marked files to"
|
msgid "Select a directory to move marked files to"
|
||||||
msgstr "Vyberte adresář, kam chcete přesunout označené soubory"
|
msgstr "Vyberte adresář, kam chcete přesunout označené soubory"
|
||||||
|
|
||||||
#: core\app.py:527
|
#: core\app.py:510
|
||||||
msgid "Select a destination for your exported CSV"
|
msgid "Select a destination for your exported CSV"
|
||||||
msgstr "Vyberte cíl pro exportovaný soubor CSV"
|
msgstr "Vyberte cíl pro exportovaný soubor CSV"
|
||||||
|
|
||||||
#: core\app.py:534 core\app.py:801 core\app.py:811
|
#: core\app.py:516 core\app.py:771 core\app.py:781
|
||||||
msgid "Couldn't write to file: {}"
|
msgid "Couldn't write to file: {}"
|
||||||
msgstr "Nelze zapisovat do souboru: {}"
|
msgstr "Nelze zapisovat do souboru: {}"
|
||||||
|
|
||||||
#: core\app.py:559
|
#: core\app.py:539
|
||||||
msgid "You have no custom command set up. Set it up in your preferences."
|
msgid "You have no custom command set up. Set it up in your preferences."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Nedefinoval jste žádný uživatelský příkaz. Nadefinujete ho v předvolbách."
|
"Nedefinoval jste žádný uživatelský příkaz. Nadefinujete ho v předvolbách."
|
||||||
|
|
||||||
#: core\app.py:727 core\app.py:740
|
#: core\app.py:695 core\app.py:707
|
||||||
msgid "You are about to remove %d files from results. Continue?"
|
msgid "You are about to remove %d files from results. Continue?"
|
||||||
msgstr "Chystáte se z výsledků odstranit %d souborů. Pokračovat?"
|
msgstr "Chystáte se z výsledků odstranit %d souborů. Pokračovat?"
|
||||||
|
|
||||||
#: core\app.py:774
|
#: core\app.py:743
|
||||||
msgid "{} duplicate groups were changed by the re-prioritization."
|
msgid "{} duplicate groups were changed by the re-prioritization."
|
||||||
msgstr "{} duplicitní skupiny byly změněny změně priorit."
|
msgstr "{} duplicitní skupiny byly změněny změně priorit."
|
||||||
|
|
||||||
#: core\app.py:821
|
#: core\app.py:790
|
||||||
msgid "The selected directories contain no scannable file."
|
msgid "The selected directories contain no scannable file."
|
||||||
msgstr "Vybrané adresáře neobsahují žádné soubory vhodné k prohledávání."
|
msgstr "Vybrané adresáře neobsahují žádné soubory vhodné k prohledávání."
|
||||||
|
|
||||||
#: core\app.py:835
|
#: core\app.py:803
|
||||||
msgid "Collecting files to scan"
|
msgid "Collecting files to scan"
|
||||||
msgstr "Shromažďuji prohlížené soubory"
|
msgstr "Shromažďuji prohlížené soubory"
|
||||||
|
|
||||||
#: core\app.py:891
|
#: core\app.py:850
|
||||||
msgid "%s (%d discarded)"
|
msgid "%s (%d discarded)"
|
||||||
msgstr "%s (%d vyřazeno)"
|
msgstr "%s (%d vyřazeno)"
|
||||||
|
|
||||||
#: core\engine.py:244 core\engine.py:288
|
#: core\directories.py:191
|
||||||
msgid "0 matches found"
|
msgid "Collected {} files to scan"
|
||||||
msgstr "Nalezeno 0 shod"
|
msgstr ""
|
||||||
|
|
||||||
#: core\engine.py:262 core\engine.py:296
|
#: core\directories.py:207
|
||||||
msgid "%d matches found"
|
msgid "Collected {} folders to scan"
|
||||||
msgstr "Nalezeno %d shod"
|
msgstr ""
|
||||||
|
|
||||||
#: core\gui\deletion_options.py:73
|
#: core\engine.py:27
|
||||||
|
msgid "%d matches found from %d groups"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core\gui\deletion_options.py:71
|
||||||
msgid "You are sending {} file(s) to the Trash."
|
msgid "You are sending {} file(s) to the Trash."
|
||||||
msgstr "Posíláte-{} soubory do koše."
|
msgstr "Posíláte-{} soubory do koše."
|
||||||
|
|
||||||
#: core\gui\exclude_list_table.py:15
|
#: core\gui\exclude_list_table.py:14
|
||||||
msgid "Regular Expressions"
|
msgid "Regular Expressions"
|
||||||
msgstr "Regulární výrazy"
|
msgstr "Regulární výrazy"
|
||||||
|
|
||||||
@@ -176,15 +184,15 @@ msgstr "Obsah"
|
|||||||
msgid "Analyzed %d/%d pictures"
|
msgid "Analyzed %d/%d pictures"
|
||||||
msgstr "Analyzováno %d/%d snímků"
|
msgstr "Analyzováno %d/%d snímků"
|
||||||
|
|
||||||
#: core\pe\matchblock.py:181
|
#: core\pe\matchblock.py:177
|
||||||
msgid "Performed %d/%d chunk matches"
|
msgid "Performed %d/%d chunk matches"
|
||||||
msgstr "Provedeno %d/%d porovnání bloků"
|
msgstr "Provedeno %d/%d porovnání bloků"
|
||||||
|
|
||||||
#: core\pe\matchblock.py:191
|
#: core\pe\matchblock.py:185
|
||||||
msgid "Preparing for matching"
|
msgid "Preparing for matching"
|
||||||
msgstr "Připravuji porovnávání"
|
msgstr "Připravuji porovnávání"
|
||||||
|
|
||||||
#: core\pe\matchblock.py:244
|
#: core\pe\matchblock.py:234
|
||||||
msgid "Verified %d/%d matches"
|
msgid "Verified %d/%d matches"
|
||||||
msgstr "Ověřeno %d/%d shod"
|
msgstr "Ověřeno %d/%d shod"
|
||||||
|
|
||||||
@@ -232,23 +240,23 @@ msgstr "Nejnovější"
|
|||||||
msgid "Oldest"
|
msgid "Oldest"
|
||||||
msgstr "Nejstarší"
|
msgstr "Nejstarší"
|
||||||
|
|
||||||
#: core\results.py:142
|
#: core\results.py:134
|
||||||
msgid "%d / %d (%s / %s) duplicates marked."
|
msgid "%d / %d (%s / %s) duplicates marked."
|
||||||
msgstr "%d / %d (%s / %s) duplicit označeno."
|
msgstr "%d / %d (%s / %s) duplicit označeno."
|
||||||
|
|
||||||
#: core\results.py:149
|
#: core\results.py:141
|
||||||
msgid " filter: %s"
|
msgid " filter: %s"
|
||||||
msgstr " filtr: %s"
|
msgstr " filtr: %s"
|
||||||
|
|
||||||
#: core\scanner.py:85
|
#: core\scanner.py:90
|
||||||
msgid "Read size of %d/%d files"
|
msgid "Read size of %d/%d files"
|
||||||
msgstr "Read size of %d/%d files"
|
msgstr "Read size of %d/%d files"
|
||||||
|
|
||||||
#: core\scanner.py:109
|
#: core\scanner.py:116
|
||||||
msgid "Read metadata of %d/%d files"
|
msgid "Read metadata of %d/%d files"
|
||||||
msgstr "Načtena metadata %d/%d souborů"
|
msgstr "Načtena metadata %d/%d souborů"
|
||||||
|
|
||||||
#: core\scanner.py:147
|
#: core\scanner.py:154
|
||||||
msgid "Almost done! Fiddling with results..."
|
msgid "Almost done! Fiddling with results..."
|
||||||
msgstr "Skoro hotovo! Fidlování s výsledky..."
|
msgstr "Skoro hotovo! Fidlování s výsledky..."
|
||||||
|
|
||||||
|
|||||||
@@ -937,3 +937,43 @@ msgstr "Všeobecné"
|
|||||||
#: qt\preferences_dialog.py:286
|
#: qt\preferences_dialog.py:286
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
msgstr "Zobrazit"
|
msgstr "Zobrazit"
|
||||||
|
|
||||||
|
#: qt\se\preferences_dialog.py:70
|
||||||
|
msgid "Partially hash files bigger than"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\se\preferences_dialog.py:80
|
||||||
|
msgid "MB"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:163
|
||||||
|
msgid "Use native OS dialogs"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:166
|
||||||
|
msgid ""
|
||||||
|
"For actions such as file/folder selection use the OS native dialogs.\n"
|
||||||
|
"Some native dialogs have limited functionality."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\se\preferences_dialog.py:68
|
||||||
|
msgid "Ignore files larger than"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\app.py:135 qt\app.py:293
|
||||||
|
msgid "Clear Cache"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\app.py:294
|
||||||
|
msgid ""
|
||||||
|
"Do you really want to clear the cache? This will remove all cached file "
|
||||||
|
"hashes and picture analysis."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\app.py:299
|
||||||
|
msgid "Cache cleared."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:173
|
||||||
|
msgid "Use dark style"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
# Translators:
|
# Translators:
|
||||||
# Andrew Senetar <arsenetar@gmail.com>, 2021
|
# Andrew Senetar <arsenetar@gmail.com>, 2021
|
||||||
# Fuan <jcfrt@posteo.net>, 2021
|
# Fuan <jcfrt@posteo.net>, 2021
|
||||||
|
# Robert M, 2021
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\n"
|
"Last-Translator: Robert M, 2021\n"
|
||||||
"Language-Team: German (https://www.transifex.com/voltaicideas/teams/116153/de/)\n"
|
"Language-Team: German (https://www.transifex.com/voltaicideas/teams/116153/de/)\n"
|
||||||
"Language: de\n"
|
"Language: de\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
@@ -47,7 +48,7 @@ msgstr "Kopiere"
|
|||||||
msgid "Sending to Trash"
|
msgid "Sending to Trash"
|
||||||
msgstr "Verschiebe in den Papierkorb"
|
msgstr "Verschiebe in den Papierkorb"
|
||||||
|
|
||||||
#: core\app.py:308
|
#: core\app.py:289
|
||||||
msgid ""
|
msgid ""
|
||||||
"A previous action is still hanging in there. You can't start a new one yet. "
|
"A previous action is still hanging in there. You can't start a new one yet. "
|
||||||
"Wait a few seconds, then try again."
|
"Wait a few seconds, then try again."
|
||||||
@@ -55,36 +56,40 @@ msgstr ""
|
|||||||
"Eine vorherige Aktion ist noch in der Bearbeitung. Sie können noch keine "
|
"Eine vorherige Aktion ist noch in der Bearbeitung. Sie können noch keine "
|
||||||
"Neue starten. Warten Sie einige Sekunden und versuchen es erneut."
|
"Neue starten. Warten Sie einige Sekunden und versuchen es erneut."
|
||||||
|
|
||||||
#: core\app.py:318
|
#: core\app.py:300
|
||||||
msgid "No duplicates found."
|
msgid "No duplicates found."
|
||||||
msgstr "Keine Duplikate gefunden."
|
msgstr "Keine Duplikate gefunden."
|
||||||
|
|
||||||
#: core\app.py:333
|
#: core\app.py:315
|
||||||
msgid "All marked files were copied successfully."
|
msgid "All marked files were copied successfully."
|
||||||
msgstr "Alle markierten Dateien wurden erfolgreich kopiert."
|
msgstr "Alle markierten Dateien wurden erfolgreich kopiert."
|
||||||
|
|
||||||
#: core\app.py:334
|
#: core\app.py:317
|
||||||
msgid "All marked files were moved successfully."
|
msgid "All marked files were moved successfully."
|
||||||
msgstr "Alle markierten Dateien wurden erfolgreich verschoben."
|
msgstr "Alle markierten Dateien wurden erfolgreich verschoben."
|
||||||
|
|
||||||
#: core\app.py:335
|
#: core\app.py:319
|
||||||
|
msgid "All marked files were deleted successfully."
|
||||||
|
msgstr "Alle markierten Dateien wurden erfolgreich gelöscht."
|
||||||
|
|
||||||
|
#: core\app.py:321
|
||||||
msgid "All marked files were successfully sent to Trash."
|
msgid "All marked files were successfully sent to Trash."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Alle markierten Dateien wurden erfolgreich in den Papierkorb verschoben."
|
"Alle markierten Dateien wurden erfolgreich in den Papierkorb verschoben."
|
||||||
|
|
||||||
#: core\app.py:343
|
#: core\app.py:326
|
||||||
msgid "Could not load file: {}"
|
msgid "Could not load file: {}"
|
||||||
msgstr "Konnte Datei {} nicht laden."
|
msgstr "Konnte Datei {} nicht laden."
|
||||||
|
|
||||||
#: core\app.py:399
|
#: core\app.py:382
|
||||||
msgid "'{}' already is in the list."
|
msgid "'{}' already is in the list."
|
||||||
msgstr "'{}' ist bereits in der Liste."
|
msgstr "'{}' ist bereits in der Liste."
|
||||||
|
|
||||||
#: core\app.py:401
|
#: core\app.py:384
|
||||||
msgid "'{}' does not exist."
|
msgid "'{}' does not exist."
|
||||||
msgstr "'{}' existiert nicht."
|
msgstr "'{}' existiert nicht."
|
||||||
|
|
||||||
#: core\app.py:410
|
#: core\app.py:392
|
||||||
msgid ""
|
msgid ""
|
||||||
"All selected %d matches are going to be ignored in all subsequent scans. "
|
"All selected %d matches are going to be ignored in all subsequent scans. "
|
||||||
"Continue?"
|
"Continue?"
|
||||||
@@ -92,65 +97,69 @@ msgstr ""
|
|||||||
"Alle %d ausgewählten Dateien werden in zukünftigen Scans ignoriert. "
|
"Alle %d ausgewählten Dateien werden in zukünftigen Scans ignoriert. "
|
||||||
"Fortfahren?"
|
"Fortfahren?"
|
||||||
|
|
||||||
#: core\app.py:486
|
#: core\app.py:469
|
||||||
msgid "Select a directory to copy marked files to"
|
msgid "Select a directory to copy marked files to"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Wählen Sie ein Verzeichnis aus, in das markierte Dateien kopiert werden "
|
"Wählen Sie ein Verzeichnis aus, in das markierte Dateien kopiert werden "
|
||||||
"sollen"
|
"sollen"
|
||||||
|
|
||||||
#: core\app.py:487
|
#: core\app.py:471
|
||||||
msgid "Select a directory to move marked files to"
|
msgid "Select a directory to move marked files to"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Wählen Sie ein Verzeichnis aus, in das markierte Dateien verschoben werden "
|
"Wählen Sie ein Verzeichnis aus, in das markierte Dateien verschoben werden "
|
||||||
"sollen"
|
"sollen"
|
||||||
|
|
||||||
#: core\app.py:527
|
#: core\app.py:510
|
||||||
msgid "Select a destination for your exported CSV"
|
msgid "Select a destination for your exported CSV"
|
||||||
msgstr "Zielverzeichnis für den CSV Export angeben"
|
msgstr "Zielverzeichnis für den CSV Export angeben"
|
||||||
|
|
||||||
#: core\app.py:534 core\app.py:801 core\app.py:811
|
#: core\app.py:516 core\app.py:771 core\app.py:781
|
||||||
msgid "Couldn't write to file: {}"
|
msgid "Couldn't write to file: {}"
|
||||||
msgstr "Konnte Datei {} nicht schreiben."
|
msgstr "Konnte Datei {} nicht schreiben."
|
||||||
|
|
||||||
#: core\app.py:559
|
#: core\app.py:539
|
||||||
msgid "You have no custom command set up. Set it up in your preferences."
|
msgid "You have no custom command set up. Set it up in your preferences."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Sie haben noch keinen Befehl erstellt. Bitte dies in den Einstellungen vornehmen.\n"
|
"Sie haben noch keinen Befehl erstellt. Bitte dies in den Einstellungen vornehmen.\n"
|
||||||
"Bsp.: \"C:\\Program Files\\Diff\\Diff.exe\" \"%d\" \"%r\""
|
"Bsp.: \"C:\\Program Files\\Diff\\Diff.exe\" \"%d\" \"%r\""
|
||||||
|
|
||||||
#: core\app.py:727 core\app.py:740
|
#: core\app.py:695 core\app.py:707
|
||||||
msgid "You are about to remove %d files from results. Continue?"
|
msgid "You are about to remove %d files from results. Continue?"
|
||||||
msgstr "%d Dateien werden aus der Ergebnisliste entfernt. Fortfahren?"
|
msgstr "%d Dateien werden aus der Ergebnisliste entfernt. Fortfahren?"
|
||||||
|
|
||||||
#: core\app.py:774
|
#: core\app.py:743
|
||||||
msgid "{} duplicate groups were changed by the re-prioritization."
|
msgid "{} duplicate groups were changed by the re-prioritization."
|
||||||
msgstr "{} Duplikat-Gruppen wurden durch die Neu-Priorisierung geändert."
|
msgstr "{} Duplikat-Gruppen wurden durch die Neu-Priorisierung geändert."
|
||||||
|
|
||||||
#: core\app.py:821
|
#: core\app.py:790
|
||||||
msgid "The selected directories contain no scannable file."
|
msgid "The selected directories contain no scannable file."
|
||||||
msgstr "Ausgewählte Ordner enthalten keine scannbaren Dateien."
|
msgstr "Ausgewählte Ordner enthalten keine scannbaren Dateien."
|
||||||
|
|
||||||
#: core\app.py:835
|
#: core\app.py:803
|
||||||
msgid "Collecting files to scan"
|
msgid "Collecting files to scan"
|
||||||
msgstr "Sammle zu scannende Dateien..."
|
msgstr "Sammle zu scannende Dateien..."
|
||||||
|
|
||||||
#: core\app.py:891
|
#: core\app.py:850
|
||||||
msgid "%s (%d discarded)"
|
msgid "%s (%d discarded)"
|
||||||
msgstr "%s (%d verworfen)"
|
msgstr "%s (%d verworfen)"
|
||||||
|
|
||||||
#: core\engine.py:244 core\engine.py:288
|
#: core\directories.py:191
|
||||||
msgid "0 matches found"
|
msgid "Collected {} files to scan"
|
||||||
msgstr "0 Übereinstimmungen gefunden"
|
msgstr "{} Dateien für Scan gesammelt"
|
||||||
|
|
||||||
#: core\engine.py:262 core\engine.py:296
|
#: core\directories.py:207
|
||||||
msgid "%d matches found"
|
msgid "Collected {} folders to scan"
|
||||||
msgstr "%d Übereinstimmungen gefunden"
|
msgstr "{} Ordner für Scan gesammelt"
|
||||||
|
|
||||||
#: core\gui\deletion_options.py:73
|
#: core\engine.py:27
|
||||||
|
msgid "%d matches found from %d groups"
|
||||||
|
msgstr "%d Treffer in %d Gruppen gefunden"
|
||||||
|
|
||||||
|
#: core\gui\deletion_options.py:71
|
||||||
msgid "You are sending {} file(s) to the Trash."
|
msgid "You are sending {} file(s) to the Trash."
|
||||||
msgstr "Verschiebe {} Datei(en) in den Papierkorb."
|
msgstr "Verschiebe {} Datei(en) in den Papierkorb."
|
||||||
|
|
||||||
#: core\gui\exclude_list_table.py:15
|
#: core\gui\exclude_list_table.py:14
|
||||||
msgid "Regular Expressions"
|
msgid "Regular Expressions"
|
||||||
msgstr "Reguläre Ausdrücke"
|
msgstr "Reguläre Ausdrücke"
|
||||||
|
|
||||||
@@ -182,15 +191,15 @@ msgstr "Inhalt"
|
|||||||
msgid "Analyzed %d/%d pictures"
|
msgid "Analyzed %d/%d pictures"
|
||||||
msgstr "Analysiere Bild %d/%d"
|
msgstr "Analysiere Bild %d/%d"
|
||||||
|
|
||||||
#: core\pe\matchblock.py:181
|
#: core\pe\matchblock.py:177
|
||||||
msgid "Performed %d/%d chunk matches"
|
msgid "Performed %d/%d chunk matches"
|
||||||
msgstr "%d/%d Chunk-Matches ausgeführt"
|
msgstr "%d/%d Chunk-Matches ausgeführt"
|
||||||
|
|
||||||
#: core\pe\matchblock.py:191
|
#: core\pe\matchblock.py:185
|
||||||
msgid "Preparing for matching"
|
msgid "Preparing for matching"
|
||||||
msgstr "Bereite Matching vor"
|
msgstr "Bereite Matching vor"
|
||||||
|
|
||||||
#: core\pe\matchblock.py:244
|
#: core\pe\matchblock.py:234
|
||||||
msgid "Verified %d/%d matches"
|
msgid "Verified %d/%d matches"
|
||||||
msgstr "%d/%d verifizierte Übereinstimmungen"
|
msgstr "%d/%d verifizierte Übereinstimmungen"
|
||||||
|
|
||||||
@@ -238,23 +247,23 @@ msgstr "Neuste"
|
|||||||
msgid "Oldest"
|
msgid "Oldest"
|
||||||
msgstr "Älterste"
|
msgstr "Älterste"
|
||||||
|
|
||||||
#: core\results.py:142
|
#: core\results.py:134
|
||||||
msgid "%d / %d (%s / %s) duplicates marked."
|
msgid "%d / %d (%s / %s) duplicates marked."
|
||||||
msgstr "%d / %d (%s / %s) Duplikate markiert."
|
msgstr "%d / %d (%s / %s) Duplikate markiert."
|
||||||
|
|
||||||
#: core\results.py:149
|
#: core\results.py:141
|
||||||
msgid " filter: %s"
|
msgid " filter: %s"
|
||||||
msgstr " Filter: %s"
|
msgstr " Filter: %s"
|
||||||
|
|
||||||
#: core\scanner.py:85
|
#: core\scanner.py:90
|
||||||
msgid "Read size of %d/%d files"
|
msgid "Read size of %d/%d files"
|
||||||
msgstr "Lese Größe von %d/%d Dateien"
|
msgstr "Lese Größe von %d/%d Dateien"
|
||||||
|
|
||||||
#: core\scanner.py:109
|
#: core\scanner.py:116
|
||||||
msgid "Read metadata of %d/%d files"
|
msgid "Read metadata of %d/%d files"
|
||||||
msgstr "Lese Metadaten von %d/%d Dateien"
|
msgstr "Lese Metadaten von %d/%d Dateien"
|
||||||
|
|
||||||
#: core\scanner.py:147
|
#: core\scanner.py:154
|
||||||
msgid "Almost done! Fiddling with results..."
|
msgid "Almost done! Fiddling with results..."
|
||||||
msgstr "Fast fertig! Arrangiere Ergebnisse..."
|
msgstr "Fast fertig! Arrangiere Ergebnisse..."
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
# Translators:
|
# Translators:
|
||||||
# Andrew Senetar <arsenetar@gmail.com>, 2021
|
# Andrew Senetar <arsenetar@gmail.com>, 2021
|
||||||
# Fuan <jcfrt@posteo.net>, 2021
|
# Fuan <jcfrt@posteo.net>, 2021
|
||||||
|
# Robert M, 2021
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\n"
|
"Last-Translator: Robert M, 2021\n"
|
||||||
"Language-Team: German (https://www.transifex.com/voltaicideas/teams/116153/de/)\n"
|
"Language-Team: German (https://www.transifex.com/voltaicideas/teams/116153/de/)\n"
|
||||||
"Language: de\n"
|
"Language: de\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
@@ -946,3 +947,45 @@ msgstr "Allgemeines"
|
|||||||
#: qt\preferences_dialog.py:286
|
#: qt\preferences_dialog.py:286
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
msgstr "Anzeige"
|
msgstr "Anzeige"
|
||||||
|
|
||||||
|
#: qt\se\preferences_dialog.py:70
|
||||||
|
msgid "Partially hash files bigger than"
|
||||||
|
msgstr "Dateien partiell hashen die größer sind als"
|
||||||
|
|
||||||
|
#: qt\se\preferences_dialog.py:80
|
||||||
|
msgid "MB"
|
||||||
|
msgstr "MB"
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:163
|
||||||
|
msgid "Use native OS dialogs"
|
||||||
|
msgstr "Benutzer System-eigene Dialoge"
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:166
|
||||||
|
msgid ""
|
||||||
|
"For actions such as file/folder selection use the OS native dialogs.\n"
|
||||||
|
"Some native dialogs have limited functionality."
|
||||||
|
msgstr ""
|
||||||
|
"Benutzer System-eigene Dialoge für Aktionen wie Datei/Ordern-Auswahl\n"
|
||||||
|
"Manche System-eigene Dialoge sind in ihren Funktionen limitiert."
|
||||||
|
|
||||||
|
#: qt\se\preferences_dialog.py:68
|
||||||
|
msgid "Ignore files larger than"
|
||||||
|
msgstr "Ignoriere Dateien größer als"
|
||||||
|
|
||||||
|
#: qt\app.py:135 qt\app.py:293
|
||||||
|
msgid "Clear Cache"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\app.py:294
|
||||||
|
msgid ""
|
||||||
|
"Do you really want to clear the cache? This will remove all cached file "
|
||||||
|
"hashes and picture analysis."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\app.py:299
|
||||||
|
msgid "Cache cleared."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:173
|
||||||
|
msgid "Use dark style"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ msgstr "Αντιγραφή"
|
|||||||
msgid "Sending to Trash"
|
msgid "Sending to Trash"
|
||||||
msgstr "Αποστολή στα σκουπίδια"
|
msgstr "Αποστολή στα σκουπίδια"
|
||||||
|
|
||||||
#: core\app.py:308
|
#: core\app.py:289
|
||||||
msgid ""
|
msgid ""
|
||||||
"A previous action is still hanging in there. You can't start a new one yet. "
|
"A previous action is still hanging in there. You can't start a new one yet. "
|
||||||
"Wait a few seconds, then try again."
|
"Wait a few seconds, then try again."
|
||||||
@@ -56,94 +56,102 @@ msgstr ""
|
|||||||
"Μια προηγούμενη ενέργεια είναι σε εξέλιξη. Δεν μπορείτε να ξεκινήσετε "
|
"Μια προηγούμενη ενέργεια είναι σε εξέλιξη. Δεν μπορείτε να ξεκινήσετε "
|
||||||
"καινούργια ακόμα. Περιμένετε λίγα δευτερόλεπτα, έπειτα προσπαθήστε ξανά."
|
"καινούργια ακόμα. Περιμένετε λίγα δευτερόλεπτα, έπειτα προσπαθήστε ξανά."
|
||||||
|
|
||||||
#: core\app.py:318
|
#: core\app.py:300
|
||||||
msgid "No duplicates found."
|
msgid "No duplicates found."
|
||||||
msgstr "Δεν βρέθηκαν διπλότυπα."
|
msgstr "Δεν βρέθηκαν διπλότυπα."
|
||||||
|
|
||||||
#: core\app.py:333
|
#: core\app.py:315
|
||||||
msgid "All marked files were copied successfully."
|
msgid "All marked files were copied successfully."
|
||||||
msgstr "Όλα τα επιλεγμένα αρχεία αντιγράφηκαν επιτυχώς."
|
msgstr "Όλα τα επιλεγμένα αρχεία αντιγράφηκαν επιτυχώς."
|
||||||
|
|
||||||
#: core\app.py:334
|
#: core\app.py:317
|
||||||
msgid "All marked files were moved successfully."
|
msgid "All marked files were moved successfully."
|
||||||
msgstr "Όλα τα επιλεγμένα αρχεία μετακινήθηκαν επιτυχώς."
|
msgstr "Όλα τα επιλεγμένα αρχεία μετακινήθηκαν επιτυχώς."
|
||||||
|
|
||||||
#: core\app.py:335
|
#: core\app.py:319
|
||||||
|
msgid "All marked files were deleted successfully."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core\app.py:321
|
||||||
msgid "All marked files were successfully sent to Trash."
|
msgid "All marked files were successfully sent to Trash."
|
||||||
msgstr "Όλα τα επιλεγμένα αρχεία στάλθηκαν με επιτυχία στον κάδο."
|
msgstr "Όλα τα επιλεγμένα αρχεία στάλθηκαν με επιτυχία στον κάδο."
|
||||||
|
|
||||||
#: core\app.py:343
|
#: core\app.py:326
|
||||||
msgid "Could not load file: {}"
|
msgid "Could not load file: {}"
|
||||||
msgstr "Δεν ήταν δυνατή η φόρτωση του αρχείου: {}"
|
msgstr "Δεν ήταν δυνατή η φόρτωση του αρχείου: {}"
|
||||||
|
|
||||||
#: core\app.py:399
|
#: core\app.py:382
|
||||||
msgid "'{}' already is in the list."
|
msgid "'{}' already is in the list."
|
||||||
msgstr "'{}' υπάρχει ήδη στη λίστα."
|
msgstr "'{}' υπάρχει ήδη στη λίστα."
|
||||||
|
|
||||||
#: core\app.py:401
|
#: core\app.py:384
|
||||||
msgid "'{}' does not exist."
|
msgid "'{}' does not exist."
|
||||||
msgstr "'{}' δεν υπάρχει."
|
msgstr "'{}' δεν υπάρχει."
|
||||||
|
|
||||||
#: core\app.py:410
|
#: core\app.py:392
|
||||||
msgid ""
|
msgid ""
|
||||||
"All selected %d matches are going to be ignored in all subsequent scans. "
|
"All selected %d matches are going to be ignored in all subsequent scans. "
|
||||||
"Continue?"
|
"Continue?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Όλα τα επιλεγμένα %d στοιχεία θα αγνοηθούν σε μελλοντικές σαρώσεις.Συνέχεια;"
|
"Όλα τα επιλεγμένα %d στοιχεία θα αγνοηθούν σε μελλοντικές σαρώσεις.Συνέχεια;"
|
||||||
|
|
||||||
#: core\app.py:486
|
#: core\app.py:469
|
||||||
msgid "Select a directory to copy marked files to"
|
msgid "Select a directory to copy marked files to"
|
||||||
msgstr "Επιλέξτε έναν κατάλογο για να αντιγράψετε επισημασμένα αρχεία."
|
msgstr "Επιλέξτε έναν κατάλογο για να αντιγράψετε επισημασμένα αρχεία."
|
||||||
|
|
||||||
#: core\app.py:487
|
#: core\app.py:471
|
||||||
msgid "Select a directory to move marked files to"
|
msgid "Select a directory to move marked files to"
|
||||||
msgstr "Επιλέξτε έναν κατάλογο για να μετακινήσετε τα επισημασμένα αρχεία."
|
msgstr "Επιλέξτε έναν κατάλογο για να μετακινήσετε τα επισημασμένα αρχεία."
|
||||||
|
|
||||||
#: core\app.py:527
|
#: core\app.py:510
|
||||||
msgid "Select a destination for your exported CSV"
|
msgid "Select a destination for your exported CSV"
|
||||||
msgstr "Επιλέξτε έναν προορισμό για το εξαγόμενο CSV σας"
|
msgstr "Επιλέξτε έναν προορισμό για το εξαγόμενο CSV σας"
|
||||||
|
|
||||||
#: core\app.py:534 core\app.py:801 core\app.py:811
|
#: core\app.py:516 core\app.py:771 core\app.py:781
|
||||||
msgid "Couldn't write to file: {}"
|
msgid "Couldn't write to file: {}"
|
||||||
msgstr "Δεν ήταν δυνατή η εγγραφή στο αρχείο: {}"
|
msgstr "Δεν ήταν δυνατή η εγγραφή στο αρχείο: {}"
|
||||||
|
|
||||||
#: core\app.py:559
|
#: core\app.py:539
|
||||||
msgid "You have no custom command set up. Set it up in your preferences."
|
msgid "You have no custom command set up. Set it up in your preferences."
|
||||||
msgstr "Δεν έχετε ορίσει ειδική εντολή. Ρυθμίστε τη στις προτιμήσεις σας. "
|
msgstr "Δεν έχετε ορίσει ειδική εντολή. Ρυθμίστε τη στις προτιμήσεις σας. "
|
||||||
|
|
||||||
#: core\app.py:727 core\app.py:740
|
#: core\app.py:695 core\app.py:707
|
||||||
msgid "You are about to remove %d files from results. Continue?"
|
msgid "You are about to remove %d files from results. Continue?"
|
||||||
msgstr "Πρόκειται να αφαιρέσετε %d αρχεία από τα αποτελέσματα. Συνέχεια;"
|
msgstr "Πρόκειται να αφαιρέσετε %d αρχεία από τα αποτελέσματα. Συνέχεια;"
|
||||||
|
|
||||||
#: core\app.py:774
|
#: core\app.py:743
|
||||||
msgid "{} duplicate groups were changed by the re-prioritization."
|
msgid "{} duplicate groups were changed by the re-prioritization."
|
||||||
msgstr "{} ομάδες διπλοτύπων άλλαξαν από το επαναπροσδιορισμό."
|
msgstr "{} ομάδες διπλοτύπων άλλαξαν από το επαναπροσδιορισμό."
|
||||||
|
|
||||||
#: core\app.py:821
|
#: core\app.py:790
|
||||||
msgid "The selected directories contain no scannable file."
|
msgid "The selected directories contain no scannable file."
|
||||||
msgstr "Οι επιλεγμένοι φάκελοι δεν περιέχουν σαρώσιμα αρχεία."
|
msgstr "Οι επιλεγμένοι φάκελοι δεν περιέχουν σαρώσιμα αρχεία."
|
||||||
|
|
||||||
#: core\app.py:835
|
#: core\app.py:803
|
||||||
msgid "Collecting files to scan"
|
msgid "Collecting files to scan"
|
||||||
msgstr "Συλλογή αρχείων για σάρωση"
|
msgstr "Συλλογή αρχείων για σάρωση"
|
||||||
|
|
||||||
#: core\app.py:891
|
#: core\app.py:850
|
||||||
msgid "%s (%d discarded)"
|
msgid "%s (%d discarded)"
|
||||||
msgstr "%s (%d απορρίφθηκαν)"
|
msgstr "%s (%d απορρίφθηκαν)"
|
||||||
|
|
||||||
#: core\engine.py:244 core\engine.py:288
|
#: core\directories.py:191
|
||||||
msgid "0 matches found"
|
msgid "Collected {} files to scan"
|
||||||
msgstr "0 διπλότυπα βρέθηκαν"
|
msgstr ""
|
||||||
|
|
||||||
#: core\engine.py:262 core\engine.py:296
|
#: core\directories.py:207
|
||||||
msgid "%d matches found"
|
msgid "Collected {} folders to scan"
|
||||||
msgstr "Βρέθηκαν %d διπλότυπα"
|
msgstr ""
|
||||||
|
|
||||||
#: core\gui\deletion_options.py:73
|
#: core\engine.py:27
|
||||||
|
msgid "%d matches found from %d groups"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core\gui\deletion_options.py:71
|
||||||
msgid "You are sending {} file(s) to the Trash."
|
msgid "You are sending {} file(s) to the Trash."
|
||||||
msgstr "Στέλνετε {} αρχεία στα σκουπίδια."
|
msgstr "Στέλνετε {} αρχεία στα σκουπίδια."
|
||||||
|
|
||||||
#: core\gui\exclude_list_table.py:15
|
#: core\gui\exclude_list_table.py:14
|
||||||
msgid "Regular Expressions"
|
msgid "Regular Expressions"
|
||||||
msgstr "Κανονικές εκφράσεις"
|
msgstr "Κανονικές εκφράσεις"
|
||||||
|
|
||||||
@@ -175,15 +183,15 @@ msgstr "Περιεχόμενα"
|
|||||||
msgid "Analyzed %d/%d pictures"
|
msgid "Analyzed %d/%d pictures"
|
||||||
msgstr "Ανάλυση %d/%d εικόνων"
|
msgstr "Ανάλυση %d/%d εικόνων"
|
||||||
|
|
||||||
#: core\pe\matchblock.py:181
|
#: core\pe\matchblock.py:177
|
||||||
msgid "Performed %d/%d chunk matches"
|
msgid "Performed %d/%d chunk matches"
|
||||||
msgstr "Εκτέλεση %d/%d μερικής ταυτοποίησης"
|
msgstr "Εκτέλεση %d/%d μερικής ταυτοποίησης"
|
||||||
|
|
||||||
#: core\pe\matchblock.py:191
|
#: core\pe\matchblock.py:185
|
||||||
msgid "Preparing for matching"
|
msgid "Preparing for matching"
|
||||||
msgstr "Προετοιμασία για σύγκριση"
|
msgstr "Προετοιμασία για σύγκριση"
|
||||||
|
|
||||||
#: core\pe\matchblock.py:244
|
#: core\pe\matchblock.py:234
|
||||||
msgid "Verified %d/%d matches"
|
msgid "Verified %d/%d matches"
|
||||||
msgstr "Πιστοποίηση %d/%d ταυτόσημων"
|
msgstr "Πιστοποίηση %d/%d ταυτόσημων"
|
||||||
|
|
||||||
@@ -231,23 +239,23 @@ msgstr "Νεώτερο"
|
|||||||
msgid "Oldest"
|
msgid "Oldest"
|
||||||
msgstr "Παλαιότερο"
|
msgstr "Παλαιότερο"
|
||||||
|
|
||||||
#: core\results.py:142
|
#: core\results.py:134
|
||||||
msgid "%d / %d (%s / %s) duplicates marked."
|
msgid "%d / %d (%s / %s) duplicates marked."
|
||||||
msgstr "%d / %d (%s / %s) επιλεγμένα διπλότυπα."
|
msgstr "%d / %d (%s / %s) επιλεγμένα διπλότυπα."
|
||||||
|
|
||||||
#: core\results.py:149
|
#: core\results.py:141
|
||||||
msgid " filter: %s"
|
msgid " filter: %s"
|
||||||
msgstr " φίλτρο: %s"
|
msgstr " φίλτρο: %s"
|
||||||
|
|
||||||
#: core\scanner.py:85
|
#: core\scanner.py:90
|
||||||
msgid "Read size of %d/%d files"
|
msgid "Read size of %d/%d files"
|
||||||
msgstr "Ανάγνωση μεγέθους %d/%d αρχείων"
|
msgstr "Ανάγνωση μεγέθους %d/%d αρχείων"
|
||||||
|
|
||||||
#: core\scanner.py:109
|
#: core\scanner.py:116
|
||||||
msgid "Read metadata of %d/%d files"
|
msgid "Read metadata of %d/%d files"
|
||||||
msgstr "Ανάγνωση μεταδεδομένων των %d/%d αρχείων"
|
msgstr "Ανάγνωση μεταδεδομένων των %d/%d αρχείων"
|
||||||
|
|
||||||
#: core\scanner.py:147
|
#: core\scanner.py:154
|
||||||
msgid "Almost done! Fiddling with results..."
|
msgid "Almost done! Fiddling with results..."
|
||||||
msgstr "Σχεδόν τελείωσα! Παιχνίδι με αποτελέσματα ..."
|
msgstr "Σχεδόν τελείωσα! Παιχνίδι με αποτελέσματα ..."
|
||||||
|
|
||||||
|
|||||||
@@ -954,3 +954,43 @@ msgstr "Γενικός"
|
|||||||
#: qt\preferences_dialog.py:286
|
#: qt\preferences_dialog.py:286
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
msgstr "Απεικόνιση"
|
msgstr "Απεικόνιση"
|
||||||
|
|
||||||
|
#: qt\se\preferences_dialog.py:70
|
||||||
|
msgid "Partially hash files bigger than"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\se\preferences_dialog.py:80
|
||||||
|
msgid "MB"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:163
|
||||||
|
msgid "Use native OS dialogs"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:166
|
||||||
|
msgid ""
|
||||||
|
"For actions such as file/folder selection use the OS native dialogs.\n"
|
||||||
|
"Some native dialogs have limited functionality."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\se\preferences_dialog.py:68
|
||||||
|
msgid "Ignore files larger than"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\app.py:135 qt\app.py:293
|
||||||
|
msgid "Clear Cache"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\app.py:294
|
||||||
|
msgid ""
|
||||||
|
"Do you really want to clear the cache? This will remove all cached file "
|
||||||
|
"hashes and picture analysis."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\app.py:299
|
||||||
|
msgid "Cache cleared."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:173
|
||||||
|
msgid "Use dark style"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ msgstr "Copiando"
|
|||||||
msgid "Sending to Trash"
|
msgid "Sending to Trash"
|
||||||
msgstr "Enviando a la Papelera"
|
msgstr "Enviando a la Papelera"
|
||||||
|
|
||||||
#: core\app.py:308
|
#: core\app.py:289
|
||||||
msgid ""
|
msgid ""
|
||||||
"A previous action is still hanging in there. You can't start a new one yet. "
|
"A previous action is still hanging in there. You can't start a new one yet. "
|
||||||
"Wait a few seconds, then try again."
|
"Wait a few seconds, then try again."
|
||||||
@@ -55,36 +55,40 @@ msgstr ""
|
|||||||
"Una acción previa sigue ejecutándose. No puede abrir una nueva todavía. "
|
"Una acción previa sigue ejecutándose. No puede abrir una nueva todavía. "
|
||||||
"Espere unos segundos y vuelva a intentarlo."
|
"Espere unos segundos y vuelva a intentarlo."
|
||||||
|
|
||||||
#: core\app.py:318
|
#: core\app.py:300
|
||||||
msgid "No duplicates found."
|
msgid "No duplicates found."
|
||||||
msgstr "No se han encontrado duplicados."
|
msgstr "No se han encontrado duplicados."
|
||||||
|
|
||||||
#: core\app.py:333
|
#: core\app.py:315
|
||||||
msgid "All marked files were copied successfully."
|
msgid "All marked files were copied successfully."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Todos los ficheros seleccionados han sido copiados satisfactoriamente."
|
"Todos los ficheros seleccionados han sido copiados satisfactoriamente."
|
||||||
|
|
||||||
#: core\app.py:334
|
#: core\app.py:317
|
||||||
msgid "All marked files were moved successfully."
|
msgid "All marked files were moved successfully."
|
||||||
msgstr "Todos los ficheros seleccionados se han movidos satisfactoriamente."
|
msgstr "Todos los ficheros seleccionados se han movidos satisfactoriamente."
|
||||||
|
|
||||||
#: core\app.py:335
|
#: core\app.py:319
|
||||||
|
msgid "All marked files were deleted successfully."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core\app.py:321
|
||||||
msgid "All marked files were successfully sent to Trash."
|
msgid "All marked files were successfully sent to Trash."
|
||||||
msgstr "Todo los ficheros marcados se han enviado a la papelera exitosamente."
|
msgstr "Todo los ficheros marcados se han enviado a la papelera exitosamente."
|
||||||
|
|
||||||
#: core\app.py:343
|
#: core\app.py:326
|
||||||
msgid "Could not load file: {}"
|
msgid "Could not load file: {}"
|
||||||
msgstr "No se pudo cargar el archivo: {}"
|
msgstr "No se pudo cargar el archivo: {}"
|
||||||
|
|
||||||
#: core\app.py:399
|
#: core\app.py:382
|
||||||
msgid "'{}' already is in the list."
|
msgid "'{}' already is in the list."
|
||||||
msgstr "'{}' ya está en la lista."
|
msgstr "'{}' ya está en la lista."
|
||||||
|
|
||||||
#: core\app.py:401
|
#: core\app.py:384
|
||||||
msgid "'{}' does not exist."
|
msgid "'{}' does not exist."
|
||||||
msgstr "'{}' no existe."
|
msgstr "'{}' no existe."
|
||||||
|
|
||||||
#: core\app.py:410
|
#: core\app.py:392
|
||||||
msgid ""
|
msgid ""
|
||||||
"All selected %d matches are going to be ignored in all subsequent scans. "
|
"All selected %d matches are going to be ignored in all subsequent scans. "
|
||||||
"Continue?"
|
"Continue?"
|
||||||
@@ -92,59 +96,63 @@ msgstr ""
|
|||||||
"Todas las %d coincidencias seleccionadas van a ser ignoradas en las "
|
"Todas las %d coincidencias seleccionadas van a ser ignoradas en las "
|
||||||
"subsiguientes exploraciones. ¿Continuar?"
|
"subsiguientes exploraciones. ¿Continuar?"
|
||||||
|
|
||||||
#: core\app.py:486
|
#: core\app.py:469
|
||||||
msgid "Select a directory to copy marked files to"
|
msgid "Select a directory to copy marked files to"
|
||||||
msgstr "Seleccione un directorio donde desee copiar los archivos marcados"
|
msgstr "Seleccione un directorio donde desee copiar los archivos marcados"
|
||||||
|
|
||||||
#: core\app.py:487
|
#: core\app.py:471
|
||||||
msgid "Select a directory to move marked files to"
|
msgid "Select a directory to move marked files to"
|
||||||
msgstr "Seleccione un directorio al que desee mover los archivos marcados"
|
msgstr "Seleccione un directorio al que desee mover los archivos marcados"
|
||||||
|
|
||||||
#: core\app.py:527
|
#: core\app.py:510
|
||||||
msgid "Select a destination for your exported CSV"
|
msgid "Select a destination for your exported CSV"
|
||||||
msgstr "Seleccionar un destino para el CSV seleccionado"
|
msgstr "Seleccionar un destino para el CSV seleccionado"
|
||||||
|
|
||||||
#: core\app.py:534 core\app.py:801 core\app.py:811
|
#: core\app.py:516 core\app.py:771 core\app.py:781
|
||||||
msgid "Couldn't write to file: {}"
|
msgid "Couldn't write to file: {}"
|
||||||
msgstr "No se pudo escribir en el archivo: {}"
|
msgstr "No se pudo escribir en el archivo: {}"
|
||||||
|
|
||||||
#: core\app.py:559
|
#: core\app.py:539
|
||||||
msgid "You have no custom command set up. Set it up in your preferences."
|
msgid "You have no custom command set up. Set it up in your preferences."
|
||||||
msgstr "No hay comandos configurados. Establézcalos en sus preferencias."
|
msgstr "No hay comandos configurados. Establézcalos en sus preferencias."
|
||||||
|
|
||||||
#: core\app.py:727 core\app.py:740
|
#: core\app.py:695 core\app.py:707
|
||||||
msgid "You are about to remove %d files from results. Continue?"
|
msgid "You are about to remove %d files from results. Continue?"
|
||||||
msgstr "Está a punto de eliminar %d ficheros de resultados. ¿Continuar?"
|
msgstr "Está a punto de eliminar %d ficheros de resultados. ¿Continuar?"
|
||||||
|
|
||||||
#: core\app.py:774
|
#: core\app.py:743
|
||||||
msgid "{} duplicate groups were changed by the re-prioritization."
|
msgid "{} duplicate groups were changed by the re-prioritization."
|
||||||
msgstr "{} grupos de duplicados han sido cambiados por la re-priorización"
|
msgstr "{} grupos de duplicados han sido cambiados por la re-priorización"
|
||||||
|
|
||||||
#: core\app.py:821
|
#: core\app.py:790
|
||||||
msgid "The selected directories contain no scannable file."
|
msgid "The selected directories contain no scannable file."
|
||||||
msgstr "Las carpetas seleccionadas no contienen ficheros para explorar."
|
msgstr "Las carpetas seleccionadas no contienen ficheros para explorar."
|
||||||
|
|
||||||
#: core\app.py:835
|
#: core\app.py:803
|
||||||
msgid "Collecting files to scan"
|
msgid "Collecting files to scan"
|
||||||
msgstr "Recopilando ficheros a explorar"
|
msgstr "Recopilando ficheros a explorar"
|
||||||
|
|
||||||
#: core\app.py:891
|
#: core\app.py:850
|
||||||
msgid "%s (%d discarded)"
|
msgid "%s (%d discarded)"
|
||||||
msgstr "%s (%d descartados)"
|
msgstr "%s (%d descartados)"
|
||||||
|
|
||||||
#: core\engine.py:244 core\engine.py:288
|
#: core\directories.py:191
|
||||||
msgid "0 matches found"
|
msgid "Collected {} files to scan"
|
||||||
msgstr "0 coincidencias"
|
msgstr ""
|
||||||
|
|
||||||
#: core\engine.py:262 core\engine.py:296
|
#: core\directories.py:207
|
||||||
msgid "%d matches found"
|
msgid "Collected {} folders to scan"
|
||||||
msgstr "%d coincidencias encontradas"
|
msgstr ""
|
||||||
|
|
||||||
#: core\gui\deletion_options.py:73
|
#: core\engine.py:27
|
||||||
|
msgid "%d matches found from %d groups"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core\gui\deletion_options.py:71
|
||||||
msgid "You are sending {} file(s) to the Trash."
|
msgid "You are sending {} file(s) to the Trash."
|
||||||
msgstr "Enviando {} fichero(s) a la Papelera"
|
msgstr "Enviando {} fichero(s) a la Papelera"
|
||||||
|
|
||||||
#: core\gui\exclude_list_table.py:15
|
#: core\gui\exclude_list_table.py:14
|
||||||
msgid "Regular Expressions"
|
msgid "Regular Expressions"
|
||||||
msgstr "Expresiones regulares"
|
msgstr "Expresiones regulares"
|
||||||
|
|
||||||
@@ -177,15 +185,15 @@ msgstr "Contenido"
|
|||||||
msgid "Analyzed %d/%d pictures"
|
msgid "Analyzed %d/%d pictures"
|
||||||
msgstr "Analizadas %d/%d imágenes"
|
msgstr "Analizadas %d/%d imágenes"
|
||||||
|
|
||||||
#: core\pe\matchblock.py:181
|
#: core\pe\matchblock.py:177
|
||||||
msgid "Performed %d/%d chunk matches"
|
msgid "Performed %d/%d chunk matches"
|
||||||
msgstr "Realizado %d/%d trozos coincidentes"
|
msgstr "Realizado %d/%d trozos coincidentes"
|
||||||
|
|
||||||
#: core\pe\matchblock.py:191
|
#: core\pe\matchblock.py:185
|
||||||
msgid "Preparing for matching"
|
msgid "Preparing for matching"
|
||||||
msgstr "Preparando para coincidencias"
|
msgstr "Preparando para coincidencias"
|
||||||
|
|
||||||
#: core\pe\matchblock.py:244
|
#: core\pe\matchblock.py:234
|
||||||
msgid "Verified %d/%d matches"
|
msgid "Verified %d/%d matches"
|
||||||
msgstr "Verificadas %d/%d coincidencias"
|
msgstr "Verificadas %d/%d coincidencias"
|
||||||
|
|
||||||
@@ -233,23 +241,23 @@ msgstr "El más nuevo"
|
|||||||
msgid "Oldest"
|
msgid "Oldest"
|
||||||
msgstr "El más antiguo"
|
msgstr "El más antiguo"
|
||||||
|
|
||||||
#: core\results.py:142
|
#: core\results.py:134
|
||||||
msgid "%d / %d (%s / %s) duplicates marked."
|
msgid "%d / %d (%s / %s) duplicates marked."
|
||||||
msgstr "%d / %d (%s / %s) duplicados marcados."
|
msgstr "%d / %d (%s / %s) duplicados marcados."
|
||||||
|
|
||||||
#: core\results.py:149
|
#: core\results.py:141
|
||||||
msgid " filter: %s"
|
msgid " filter: %s"
|
||||||
msgstr "filtro: %s"
|
msgstr "filtro: %s"
|
||||||
|
|
||||||
#: core\scanner.py:85
|
#: core\scanner.py:90
|
||||||
msgid "Read size of %d/%d files"
|
msgid "Read size of %d/%d files"
|
||||||
msgstr "Tamaño de lectura de %d/%d ficheros"
|
msgstr "Tamaño de lectura de %d/%d ficheros"
|
||||||
|
|
||||||
#: core\scanner.py:109
|
#: core\scanner.py:116
|
||||||
msgid "Read metadata of %d/%d files"
|
msgid "Read metadata of %d/%d files"
|
||||||
msgstr "Leyendo metadatos de %d/%d ficheros"
|
msgstr "Leyendo metadatos de %d/%d ficheros"
|
||||||
|
|
||||||
#: core\scanner.py:147
|
#: core\scanner.py:154
|
||||||
msgid "Almost done! Fiddling with results..."
|
msgid "Almost done! Fiddling with results..."
|
||||||
msgstr "¡Casi termino! Jugando con los resultados..."
|
msgstr "¡Casi termino! Jugando con los resultados..."
|
||||||
|
|
||||||
|
|||||||
@@ -947,3 +947,43 @@ msgstr "General"
|
|||||||
#: qt\preferences_dialog.py:286
|
#: qt\preferences_dialog.py:286
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
msgstr "Visualización"
|
msgstr "Visualización"
|
||||||
|
|
||||||
|
#: qt\se\preferences_dialog.py:70
|
||||||
|
msgid "Partially hash files bigger than"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\se\preferences_dialog.py:80
|
||||||
|
msgid "MB"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:163
|
||||||
|
msgid "Use native OS dialogs"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:166
|
||||||
|
msgid ""
|
||||||
|
"For actions such as file/folder selection use the OS native dialogs.\n"
|
||||||
|
"Some native dialogs have limited functionality."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\se\preferences_dialog.py:68
|
||||||
|
msgid "Ignore files larger than"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\app.py:135 qt\app.py:293
|
||||||
|
msgid "Clear Cache"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\app.py:294
|
||||||
|
msgid ""
|
||||||
|
"Do you really want to clear the cache? This will remove all cached file "
|
||||||
|
"hashes and picture analysis."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\app.py:299
|
||||||
|
msgid "Cache cleared."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:173
|
||||||
|
msgid "Use dark style"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ msgstr "Copie en cours"
|
|||||||
msgid "Sending to Trash"
|
msgid "Sending to Trash"
|
||||||
msgstr "Envoi de fichiers à la corbeille"
|
msgstr "Envoi de fichiers à la corbeille"
|
||||||
|
|
||||||
#: core\app.py:308
|
#: core\app.py:289
|
||||||
msgid ""
|
msgid ""
|
||||||
"A previous action is still hanging in there. You can't start a new one yet. "
|
"A previous action is still hanging in there. You can't start a new one yet. "
|
||||||
"Wait a few seconds, then try again."
|
"Wait a few seconds, then try again."
|
||||||
@@ -55,95 +55,103 @@ msgstr ""
|
|||||||
"Une action précédente est encore en cours. Attendez quelques secondes avant "
|
"Une action précédente est encore en cours. Attendez quelques secondes avant "
|
||||||
"d'en repartir une nouvelle."
|
"d'en repartir une nouvelle."
|
||||||
|
|
||||||
#: core\app.py:318
|
#: core\app.py:300
|
||||||
msgid "No duplicates found."
|
msgid "No duplicates found."
|
||||||
msgstr "Aucun doublon trouvé."
|
msgstr "Aucun doublon trouvé."
|
||||||
|
|
||||||
#: core\app.py:333
|
#: core\app.py:315
|
||||||
msgid "All marked files were copied successfully."
|
msgid "All marked files were copied successfully."
|
||||||
msgstr "Tous les fichiers marqués ont été copiés correctement."
|
msgstr "Tous les fichiers marqués ont été copiés correctement."
|
||||||
|
|
||||||
#: core\app.py:334
|
#: core\app.py:317
|
||||||
msgid "All marked files were moved successfully."
|
msgid "All marked files were moved successfully."
|
||||||
msgstr "Tous les fichiers marqués ont été déplacés correctement."
|
msgstr "Tous les fichiers marqués ont été déplacés correctement."
|
||||||
|
|
||||||
#: core\app.py:335
|
#: core\app.py:319
|
||||||
|
msgid "All marked files were deleted successfully."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core\app.py:321
|
||||||
msgid "All marked files were successfully sent to Trash."
|
msgid "All marked files were successfully sent to Trash."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Tous les fichiers marqués ont été correctement envoyés à la corbeille."
|
"Tous les fichiers marqués ont été correctement envoyés à la corbeille."
|
||||||
|
|
||||||
#: core\app.py:343
|
#: core\app.py:326
|
||||||
msgid "Could not load file: {}"
|
msgid "Could not load file: {}"
|
||||||
msgstr "Impossible d'ouvrir le fichier: {}"
|
msgstr "Impossible d'ouvrir le fichier: {}"
|
||||||
|
|
||||||
#: core\app.py:399
|
#: core\app.py:382
|
||||||
msgid "'{}' already is in the list."
|
msgid "'{}' already is in the list."
|
||||||
msgstr "'{}' est déjà dans la liste."
|
msgstr "'{}' est déjà dans la liste."
|
||||||
|
|
||||||
#: core\app.py:401
|
#: core\app.py:384
|
||||||
msgid "'{}' does not exist."
|
msgid "'{}' does not exist."
|
||||||
msgstr "'{}' n'existe pas."
|
msgstr "'{}' n'existe pas."
|
||||||
|
|
||||||
#: core\app.py:410
|
#: core\app.py:392
|
||||||
msgid ""
|
msgid ""
|
||||||
"All selected %d matches are going to be ignored in all subsequent scans. "
|
"All selected %d matches are going to be ignored in all subsequent scans. "
|
||||||
"Continue?"
|
"Continue?"
|
||||||
msgstr "%d fichiers seront ignorés des prochains scans. Continuer?"
|
msgstr "%d fichiers seront ignorés des prochains scans. Continuer?"
|
||||||
|
|
||||||
#: core\app.py:486
|
#: core\app.py:469
|
||||||
msgid "Select a directory to copy marked files to"
|
msgid "Select a directory to copy marked files to"
|
||||||
msgstr "Sélectionnez un dossier vers lequel copier les fichiers marqués."
|
msgstr "Sélectionnez un dossier vers lequel copier les fichiers marqués."
|
||||||
|
|
||||||
#: core\app.py:487
|
#: core\app.py:471
|
||||||
msgid "Select a directory to move marked files to"
|
msgid "Select a directory to move marked files to"
|
||||||
msgstr "Sélectionnez un dossier vers lequel déplacer les fichiers marqués."
|
msgstr "Sélectionnez un dossier vers lequel déplacer les fichiers marqués."
|
||||||
|
|
||||||
#: core\app.py:527
|
#: core\app.py:510
|
||||||
msgid "Select a destination for your exported CSV"
|
msgid "Select a destination for your exported CSV"
|
||||||
msgstr "Choisissez une destination pour votre exportation CSV"
|
msgstr "Choisissez une destination pour votre exportation CSV"
|
||||||
|
|
||||||
#: core\app.py:534 core\app.py:801 core\app.py:811
|
#: core\app.py:516 core\app.py:771 core\app.py:781
|
||||||
msgid "Couldn't write to file: {}"
|
msgid "Couldn't write to file: {}"
|
||||||
msgstr "Impossible d'écrire le fichier: {}"
|
msgstr "Impossible d'écrire le fichier: {}"
|
||||||
|
|
||||||
#: core\app.py:559
|
#: core\app.py:539
|
||||||
msgid "You have no custom command set up. Set it up in your preferences."
|
msgid "You have no custom command set up. Set it up in your preferences."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Vous n'avez pas de commande personnalisée. Ajoutez-la dans vos préférences."
|
"Vous n'avez pas de commande personnalisée. Ajoutez-la dans vos préférences."
|
||||||
|
|
||||||
#: core\app.py:727 core\app.py:740
|
#: core\app.py:695 core\app.py:707
|
||||||
msgid "You are about to remove %d files from results. Continue?"
|
msgid "You are about to remove %d files from results. Continue?"
|
||||||
msgstr "%d fichiers seront retirés des résultats. Continuer?"
|
msgstr "%d fichiers seront retirés des résultats. Continuer?"
|
||||||
|
|
||||||
#: core\app.py:774
|
#: core\app.py:743
|
||||||
msgid "{} duplicate groups were changed by the re-prioritization."
|
msgid "{} duplicate groups were changed by the re-prioritization."
|
||||||
msgstr "{} groupes de doublons ont été modifiés par la re-prioritisation."
|
msgstr "{} groupes de doublons ont été modifiés par la re-prioritisation."
|
||||||
|
|
||||||
#: core\app.py:821
|
#: core\app.py:790
|
||||||
msgid "The selected directories contain no scannable file."
|
msgid "The selected directories contain no scannable file."
|
||||||
msgstr "Les dossiers sélectionnés ne contiennent pas de fichiers valides."
|
msgstr "Les dossiers sélectionnés ne contiennent pas de fichiers valides."
|
||||||
|
|
||||||
#: core\app.py:835
|
#: core\app.py:803
|
||||||
msgid "Collecting files to scan"
|
msgid "Collecting files to scan"
|
||||||
msgstr "Collecte des fichiers à scanner"
|
msgstr "Collecte des fichiers à scanner"
|
||||||
|
|
||||||
#: core\app.py:891
|
#: core\app.py:850
|
||||||
msgid "%s (%d discarded)"
|
msgid "%s (%d discarded)"
|
||||||
msgstr "%s (%d hors-groupe)"
|
msgstr "%s (%d hors-groupe)"
|
||||||
|
|
||||||
#: core\engine.py:244 core\engine.py:288
|
#: core\directories.py:191
|
||||||
msgid "0 matches found"
|
msgid "Collected {} files to scan"
|
||||||
msgstr "0 paires trouvées"
|
msgstr ""
|
||||||
|
|
||||||
#: core\engine.py:262 core\engine.py:296
|
#: core\directories.py:207
|
||||||
msgid "%d matches found"
|
msgid "Collected {} folders to scan"
|
||||||
msgstr "%d paires trouvées"
|
msgstr ""
|
||||||
|
|
||||||
#: core\gui\deletion_options.py:73
|
#: core\engine.py:27
|
||||||
|
msgid "%d matches found from %d groups"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core\gui\deletion_options.py:71
|
||||||
msgid "You are sending {} file(s) to the Trash."
|
msgid "You are sending {} file(s) to the Trash."
|
||||||
msgstr "Vous envoyez {} fichier(s) à la corbeille."
|
msgstr "Vous envoyez {} fichier(s) à la corbeille."
|
||||||
|
|
||||||
#: core\gui\exclude_list_table.py:15
|
#: core\gui\exclude_list_table.py:14
|
||||||
msgid "Regular Expressions"
|
msgid "Regular Expressions"
|
||||||
msgstr "Expressions régulières"
|
msgstr "Expressions régulières"
|
||||||
|
|
||||||
@@ -177,15 +185,15 @@ msgstr "Contenu"
|
|||||||
msgid "Analyzed %d/%d pictures"
|
msgid "Analyzed %d/%d pictures"
|
||||||
msgstr "Analyzé %d/%d images"
|
msgstr "Analyzé %d/%d images"
|
||||||
|
|
||||||
#: core\pe\matchblock.py:181
|
#: core\pe\matchblock.py:177
|
||||||
msgid "Performed %d/%d chunk matches"
|
msgid "Performed %d/%d chunk matches"
|
||||||
msgstr "%d/%d blocs d'images comparés"
|
msgstr "%d/%d blocs d'images comparés"
|
||||||
|
|
||||||
#: core\pe\matchblock.py:191
|
#: core\pe\matchblock.py:185
|
||||||
msgid "Preparing for matching"
|
msgid "Preparing for matching"
|
||||||
msgstr "Préparation pour la comparaison"
|
msgstr "Préparation pour la comparaison"
|
||||||
|
|
||||||
#: core\pe\matchblock.py:244
|
#: core\pe\matchblock.py:234
|
||||||
msgid "Verified %d/%d matches"
|
msgid "Verified %d/%d matches"
|
||||||
msgstr "Vérifié %d/%d paires"
|
msgstr "Vérifié %d/%d paires"
|
||||||
|
|
||||||
@@ -233,23 +241,23 @@ msgstr "Plus récent"
|
|||||||
msgid "Oldest"
|
msgid "Oldest"
|
||||||
msgstr "Moins récent"
|
msgstr "Moins récent"
|
||||||
|
|
||||||
#: core\results.py:142
|
#: core\results.py:134
|
||||||
msgid "%d / %d (%s / %s) duplicates marked."
|
msgid "%d / %d (%s / %s) duplicates marked."
|
||||||
msgstr "%d / %d (%s / %s) doublons marqués."
|
msgstr "%d / %d (%s / %s) doublons marqués."
|
||||||
|
|
||||||
#: core\results.py:149
|
#: core\results.py:141
|
||||||
msgid " filter: %s"
|
msgid " filter: %s"
|
||||||
msgstr " filtre: %s"
|
msgstr " filtre: %s"
|
||||||
|
|
||||||
#: core\scanner.py:85
|
#: core\scanner.py:90
|
||||||
msgid "Read size of %d/%d files"
|
msgid "Read size of %d/%d files"
|
||||||
msgstr "Lu la taille de %d/%d fichiers"
|
msgstr "Lu la taille de %d/%d fichiers"
|
||||||
|
|
||||||
#: core\scanner.py:109
|
#: core\scanner.py:116
|
||||||
msgid "Read metadata of %d/%d files"
|
msgid "Read metadata of %d/%d files"
|
||||||
msgstr "Lu les métadonnées de %d/%d fichiers"
|
msgstr "Lu les métadonnées de %d/%d fichiers"
|
||||||
|
|
||||||
#: core\scanner.py:147
|
#: core\scanner.py:154
|
||||||
msgid "Almost done! Fiddling with results..."
|
msgid "Almost done! Fiddling with results..."
|
||||||
msgstr "Bientôt terminé! Bidouille des résultats..."
|
msgstr "Bientôt terminé! Bidouille des résultats..."
|
||||||
|
|
||||||
|
|||||||
@@ -942,3 +942,43 @@ msgstr "Général"
|
|||||||
#: qt\preferences_dialog.py:286
|
#: qt\preferences_dialog.py:286
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
msgstr "Affichage"
|
msgstr "Affichage"
|
||||||
|
|
||||||
|
#: qt\se\preferences_dialog.py:70
|
||||||
|
msgid "Partially hash files bigger than"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\se\preferences_dialog.py:80
|
||||||
|
msgid "MB"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:163
|
||||||
|
msgid "Use native OS dialogs"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:166
|
||||||
|
msgid ""
|
||||||
|
"For actions such as file/folder selection use the OS native dialogs.\n"
|
||||||
|
"Some native dialogs have limited functionality."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\se\preferences_dialog.py:68
|
||||||
|
msgid "Ignore files larger than"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\app.py:135 qt\app.py:293
|
||||||
|
msgid "Clear Cache"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\app.py:294
|
||||||
|
msgid ""
|
||||||
|
"Do you really want to clear the cache? This will remove all cached file "
|
||||||
|
"hashes and picture analysis."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\app.py:299
|
||||||
|
msgid "Cache cleared."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:173
|
||||||
|
msgid "Use dark style"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ msgstr "Պատճենվում է"
|
|||||||
msgid "Sending to Trash"
|
msgid "Sending to Trash"
|
||||||
msgstr "Ուղարկվում է Աղբարկղ"
|
msgstr "Ուղարկվում է Աղբարկղ"
|
||||||
|
|
||||||
#: core\app.py:308
|
#: core\app.py:289
|
||||||
msgid ""
|
msgid ""
|
||||||
"A previous action is still hanging in there. You can't start a new one yet. "
|
"A previous action is still hanging in there. You can't start a new one yet. "
|
||||||
"Wait a few seconds, then try again."
|
"Wait a few seconds, then try again."
|
||||||
@@ -56,95 +56,103 @@ msgstr ""
|
|||||||
"Նախորդ գործողությունը դեռևս ձեռադրում է այստեղ: Չեք կարող սկսել մեկ ուրիշը: "
|
"Նախորդ գործողությունը դեռևս ձեռադրում է այստեղ: Չեք կարող սկսել մեկ ուրիշը: "
|
||||||
"Սպասեք մի քանի վայրկյան և կրկին փորձեք:"
|
"Սպասեք մի քանի վայրկյան և կրկին փորձեք:"
|
||||||
|
|
||||||
#: core\app.py:318
|
#: core\app.py:300
|
||||||
msgid "No duplicates found."
|
msgid "No duplicates found."
|
||||||
msgstr "Կրկնօրինակներ չկան:"
|
msgstr "Կրկնօրինակներ չկան:"
|
||||||
|
|
||||||
#: core\app.py:333
|
#: core\app.py:315
|
||||||
msgid "All marked files were copied successfully."
|
msgid "All marked files were copied successfully."
|
||||||
msgstr "Բոլոր նշված ֆայլերը հաջողությամբ պատճենվել են:"
|
msgstr "Բոլոր նշված ֆայլերը հաջողությամբ պատճենվել են:"
|
||||||
|
|
||||||
#: core\app.py:334
|
#: core\app.py:317
|
||||||
msgid "All marked files were moved successfully."
|
msgid "All marked files were moved successfully."
|
||||||
msgstr "Բոլոր նշված ֆայլերը հաջողությամբ տեղափոխվել են:"
|
msgstr "Բոլոր նշված ֆայլերը հաջողությամբ տեղափոխվել են:"
|
||||||
|
|
||||||
#: core\app.py:335
|
#: core\app.py:319
|
||||||
|
msgid "All marked files were deleted successfully."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core\app.py:321
|
||||||
msgid "All marked files were successfully sent to Trash."
|
msgid "All marked files were successfully sent to Trash."
|
||||||
msgstr "Բոլոր նշված ֆայլերը հաջողությամբ Ջնջվել են:"
|
msgstr "Բոլոր նշված ֆայլերը հաջողությամբ Ջնջվել են:"
|
||||||
|
|
||||||
#: core\app.py:343
|
#: core\app.py:326
|
||||||
msgid "Could not load file: {}"
|
msgid "Could not load file: {}"
|
||||||
msgstr "Հնարավոր չէ բեռնել ֆայլը: {}"
|
msgstr "Հնարավոր չէ բեռնել ֆայլը: {}"
|
||||||
|
|
||||||
#: core\app.py:399
|
#: core\app.py:382
|
||||||
msgid "'{}' already is in the list."
|
msgid "'{}' already is in the list."
|
||||||
msgstr "'{}'-ը արդեն առկա է ցանկում:"
|
msgstr "'{}'-ը արդեն առկա է ցանկում:"
|
||||||
|
|
||||||
#: core\app.py:401
|
#: core\app.py:384
|
||||||
msgid "'{}' does not exist."
|
msgid "'{}' does not exist."
|
||||||
msgstr "'{}'-ը գոյություն չունի:"
|
msgstr "'{}'-ը գոյություն չունի:"
|
||||||
|
|
||||||
#: core\app.py:410
|
#: core\app.py:392
|
||||||
msgid ""
|
msgid ""
|
||||||
"All selected %d matches are going to be ignored in all subsequent scans. "
|
"All selected %d matches are going to be ignored in all subsequent scans. "
|
||||||
"Continue?"
|
"Continue?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Ընտրված %d համընկնումները կանտեսվեն հետագա բոլոր ստուգումներից: Շարունակե՞լ:"
|
"Ընտրված %d համընկնումները կանտեսվեն հետագա բոլոր ստուգումներից: Շարունակե՞լ:"
|
||||||
|
|
||||||
#: core\app.py:486
|
#: core\app.py:469
|
||||||
msgid "Select a directory to copy marked files to"
|
msgid "Select a directory to copy marked files to"
|
||||||
msgstr "Ընտրեք գրացուցակ, որտեղ ցանկանում եք պատճենել նշված ֆայլերը"
|
msgstr "Ընտրեք գրացուցակ, որտեղ ցանկանում եք պատճենել նշված ֆայլերը"
|
||||||
|
|
||||||
#: core\app.py:487
|
#: core\app.py:471
|
||||||
msgid "Select a directory to move marked files to"
|
msgid "Select a directory to move marked files to"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Խնդրում ենք ընտրել գրացուցակ, որտեղ ցանկանում եք տեղափոխել նշված ֆայլերը"
|
"Խնդրում ենք ընտրել գրացուցակ, որտեղ ցանկանում եք տեղափոխել նշված ֆայլերը"
|
||||||
|
|
||||||
#: core\app.py:527
|
#: core\app.py:510
|
||||||
msgid "Select a destination for your exported CSV"
|
msgid "Select a destination for your exported CSV"
|
||||||
msgstr "Ընտրեք նպատակակետ ձեր արտահանված CSV- ի համար"
|
msgstr "Ընտրեք նպատակակետ ձեր արտահանված CSV- ի համար"
|
||||||
|
|
||||||
#: core\app.py:534 core\app.py:801 core\app.py:811
|
#: core\app.py:516 core\app.py:771 core\app.py:781
|
||||||
msgid "Couldn't write to file: {}"
|
msgid "Couldn't write to file: {}"
|
||||||
msgstr "Չէր կարող գրել է ֆայլը: {}"
|
msgstr "Չէր կարող գրել է ֆայլը: {}"
|
||||||
|
|
||||||
#: core\app.py:559
|
#: core\app.py:539
|
||||||
msgid "You have no custom command set up. Set it up in your preferences."
|
msgid "You have no custom command set up. Set it up in your preferences."
|
||||||
msgstr "Դուք չեք կատարել Հրամանի ընտրություն: Կատարեք այն կարգավորումներում:"
|
msgstr "Դուք չեք կատարել Հրամանի ընտրություն: Կատարեք այն կարգավորումներում:"
|
||||||
|
|
||||||
#: core\app.py:727 core\app.py:740
|
#: core\app.py:695 core\app.py:707
|
||||||
msgid "You are about to remove %d files from results. Continue?"
|
msgid "You are about to remove %d files from results. Continue?"
|
||||||
msgstr "Դուք պատրաստվում եք ջնջելու %d ֆայլեր: Շարունակե՞լ:"
|
msgstr "Դուք պատրաստվում եք ջնջելու %d ֆայլեր: Շարունակե՞լ:"
|
||||||
|
|
||||||
#: core\app.py:774
|
#: core\app.py:743
|
||||||
msgid "{} duplicate groups were changed by the re-prioritization."
|
msgid "{} duplicate groups were changed by the re-prioritization."
|
||||||
msgstr "{} կրկնօրինակ խմբերը փոխվել են առաջնահերթության կարգով:"
|
msgstr "{} կրկնօրինակ խմբերը փոխվել են առաջնահերթության կարգով:"
|
||||||
|
|
||||||
#: core\app.py:821
|
#: core\app.py:790
|
||||||
msgid "The selected directories contain no scannable file."
|
msgid "The selected directories contain no scannable file."
|
||||||
msgstr "Ընտրված թղթապանակները պարունակում են չստուգվող ֆայլ:"
|
msgstr "Ընտրված թղթապանակները պարունակում են չստուգվող ֆայլ:"
|
||||||
|
|
||||||
#: core\app.py:835
|
#: core\app.py:803
|
||||||
msgid "Collecting files to scan"
|
msgid "Collecting files to scan"
|
||||||
msgstr "Հավաքվում են ֆայլեր՝ ստուգելու համար"
|
msgstr "Հավաքվում են ֆայլեր՝ ստուգելու համար"
|
||||||
|
|
||||||
#: core\app.py:891
|
#: core\app.py:850
|
||||||
msgid "%s (%d discarded)"
|
msgid "%s (%d discarded)"
|
||||||
msgstr "%s (%d անպիտան)"
|
msgstr "%s (%d անպիտան)"
|
||||||
|
|
||||||
#: core\engine.py:244 core\engine.py:288
|
#: core\directories.py:191
|
||||||
msgid "0 matches found"
|
msgid "Collected {} files to scan"
|
||||||
msgstr "0 համընկնում է գտնվել"
|
msgstr ""
|
||||||
|
|
||||||
#: core\engine.py:262 core\engine.py:296
|
#: core\directories.py:207
|
||||||
msgid "%d matches found"
|
msgid "Collected {} folders to scan"
|
||||||
msgstr "%d համընկնում է գտնվել"
|
msgstr ""
|
||||||
|
|
||||||
#: core\gui\deletion_options.py:73
|
#: core\engine.py:27
|
||||||
|
msgid "%d matches found from %d groups"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core\gui\deletion_options.py:71
|
||||||
msgid "You are sending {} file(s) to the Trash."
|
msgid "You are sending {} file(s) to the Trash."
|
||||||
msgstr "Դուք {} ֆայլ եք ուղարկում աղբարկղ:"
|
msgstr "Դուք {} ֆայլ եք ուղարկում աղբարկղ:"
|
||||||
|
|
||||||
#: core\gui\exclude_list_table.py:15
|
#: core\gui\exclude_list_table.py:14
|
||||||
msgid "Regular Expressions"
|
msgid "Regular Expressions"
|
||||||
msgstr "Կանոնավոր արտահայտություններ"
|
msgstr "Կանոնավոր արտահայտություններ"
|
||||||
|
|
||||||
@@ -176,15 +184,15 @@ msgstr "Բովանդակություն"
|
|||||||
msgid "Analyzed %d/%d pictures"
|
msgid "Analyzed %d/%d pictures"
|
||||||
msgstr "Ստուգվում է %d/%d նկարները"
|
msgstr "Ստուգվում է %d/%d նկարները"
|
||||||
|
|
||||||
#: core\pe\matchblock.py:181
|
#: core\pe\matchblock.py:177
|
||||||
msgid "Performed %d/%d chunk matches"
|
msgid "Performed %d/%d chunk matches"
|
||||||
msgstr "Կատարվում է %d/%d տվյալի համընկնում"
|
msgstr "Կատարվում է %d/%d տվյալի համընկնում"
|
||||||
|
|
||||||
#: core\pe\matchblock.py:191
|
#: core\pe\matchblock.py:185
|
||||||
msgid "Preparing for matching"
|
msgid "Preparing for matching"
|
||||||
msgstr "Նախապատրաստեցվում է համընկնումը"
|
msgstr "Նախապատրաստեցվում է համընկնումը"
|
||||||
|
|
||||||
#: core\pe\matchblock.py:244
|
#: core\pe\matchblock.py:234
|
||||||
msgid "Verified %d/%d matches"
|
msgid "Verified %d/%d matches"
|
||||||
msgstr "Ստուգում է %d/%d համընկնումները"
|
msgstr "Ստուգում է %d/%d համընկնումները"
|
||||||
|
|
||||||
@@ -232,23 +240,23 @@ msgstr "Նորագույնը"
|
|||||||
msgid "Oldest"
|
msgid "Oldest"
|
||||||
msgstr "Ամենահինը"
|
msgstr "Ամենահինը"
|
||||||
|
|
||||||
#: core\results.py:142
|
#: core\results.py:134
|
||||||
msgid "%d / %d (%s / %s) duplicates marked."
|
msgid "%d / %d (%s / %s) duplicates marked."
|
||||||
msgstr "%d / %d (%s / %s) նշված կրկնօրինակներ:"
|
msgstr "%d / %d (%s / %s) նշված կրկնօրինակներ:"
|
||||||
|
|
||||||
#: core\results.py:149
|
#: core\results.py:141
|
||||||
msgid " filter: %s"
|
msgid " filter: %s"
|
||||||
msgstr "ֆիլտր. %s"
|
msgstr "ֆիլտր. %s"
|
||||||
|
|
||||||
#: core\scanner.py:85
|
#: core\scanner.py:90
|
||||||
msgid "Read size of %d/%d files"
|
msgid "Read size of %d/%d files"
|
||||||
msgstr "Կարդալ %d/%d ֆայլերի չափը"
|
msgstr "Կարդալ %d/%d ֆայլերի չափը"
|
||||||
|
|
||||||
#: core\scanner.py:109
|
#: core\scanner.py:116
|
||||||
msgid "Read metadata of %d/%d files"
|
msgid "Read metadata of %d/%d files"
|
||||||
msgstr "Կարդալ %d/%d ֆայլերի մետատվյալները"
|
msgstr "Կարդալ %d/%d ֆայլերի մետատվյալները"
|
||||||
|
|
||||||
#: core\scanner.py:147
|
#: core\scanner.py:154
|
||||||
msgid "Almost done! Fiddling with results..."
|
msgid "Almost done! Fiddling with results..."
|
||||||
msgstr "Գրեթե արված է! Արդյունքների կազմակերպում..."
|
msgstr "Գրեթե արված է! Արդյունքների կազմակերպում..."
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
# Translators:
|
# Translators:
|
||||||
# Andrew Senetar <arsenetar@gmail.com>, 2021
|
# Andrew Senetar <arsenetar@gmail.com>, 2021
|
||||||
# Fuan <jcfrt@posteo.net>, 2021
|
# Fuan <jcfrt@posteo.net>, 2021
|
||||||
|
# Emanuele, 2021
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\n"
|
"Last-Translator: Emanuele, 2021\n"
|
||||||
"Language-Team: Italian (https://www.transifex.com/voltaicideas/teams/116153/it/)\n"
|
"Language-Team: Italian (https://www.transifex.com/voltaicideas/teams/116153/it/)\n"
|
||||||
"Language: it\n"
|
"Language: it\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
@@ -48,7 +49,7 @@ msgstr "Copia in corso"
|
|||||||
msgid "Sending to Trash"
|
msgid "Sending to Trash"
|
||||||
msgstr "Spostamento nel cestino"
|
msgstr "Spostamento nel cestino"
|
||||||
|
|
||||||
#: core\app.py:308
|
#: core\app.py:289
|
||||||
msgid ""
|
msgid ""
|
||||||
"A previous action is still hanging in there. You can't start a new one yet. "
|
"A previous action is still hanging in there. You can't start a new one yet. "
|
||||||
"Wait a few seconds, then try again."
|
"Wait a few seconds, then try again."
|
||||||
@@ -56,35 +57,39 @@ msgstr ""
|
|||||||
"Un'azione precedente è ancora in corso. Non puoi cominciarne una nuova. "
|
"Un'azione precedente è ancora in corso. Non puoi cominciarne una nuova. "
|
||||||
"Aspetta qualche secondo e quindi riprova."
|
"Aspetta qualche secondo e quindi riprova."
|
||||||
|
|
||||||
#: core\app.py:318
|
#: core\app.py:300
|
||||||
msgid "No duplicates found."
|
msgid "No duplicates found."
|
||||||
msgstr "Non sono stati trovati dei duplicati."
|
msgstr "Non sono stati trovati dei duplicati."
|
||||||
|
|
||||||
#: core\app.py:333
|
#: core\app.py:315
|
||||||
msgid "All marked files were copied successfully."
|
msgid "All marked files were copied successfully."
|
||||||
msgstr "Tutti i file marcati sono stati copiati correttamente."
|
msgstr "Tutti i file marcati sono stati copiati correttamente."
|
||||||
|
|
||||||
#: core\app.py:334
|
#: core\app.py:317
|
||||||
msgid "All marked files were moved successfully."
|
msgid "All marked files were moved successfully."
|
||||||
msgstr "Tutti i file marcati sono stati spostati correttamente."
|
msgstr "Tutti i file marcati sono stati spostati correttamente."
|
||||||
|
|
||||||
#: core\app.py:335
|
#: core\app.py:319
|
||||||
|
msgid "All marked files were deleted successfully."
|
||||||
|
msgstr "Tutti i file marcati sono stati cancellati correttamente."
|
||||||
|
|
||||||
|
#: core\app.py:321
|
||||||
msgid "All marked files were successfully sent to Trash."
|
msgid "All marked files were successfully sent to Trash."
|
||||||
msgstr "Tutti i file marcati sono stati spostati nel cestino."
|
msgstr "Tutti i file marcati sono stati spostati nel cestino."
|
||||||
|
|
||||||
#: core\app.py:343
|
#: core\app.py:326
|
||||||
msgid "Could not load file: {}"
|
msgid "Could not load file: {}"
|
||||||
msgstr "Impossibile caricare il file: {}"
|
msgstr "Impossibile caricare il file: {}"
|
||||||
|
|
||||||
#: core\app.py:399
|
#: core\app.py:382
|
||||||
msgid "'{}' already is in the list."
|
msgid "'{}' already is in the list."
|
||||||
msgstr "'{}' è già nella lista."
|
msgstr "'{}' è già nella lista."
|
||||||
|
|
||||||
#: core\app.py:401
|
#: core\app.py:384
|
||||||
msgid "'{}' does not exist."
|
msgid "'{}' does not exist."
|
||||||
msgstr "'{}' non esiste."
|
msgstr "'{}' non esiste."
|
||||||
|
|
||||||
#: core\app.py:410
|
#: core\app.py:392
|
||||||
msgid ""
|
msgid ""
|
||||||
"All selected %d matches are going to be ignored in all subsequent scans. "
|
"All selected %d matches are going to be ignored in all subsequent scans. "
|
||||||
"Continue?"
|
"Continue?"
|
||||||
@@ -92,62 +97,66 @@ msgstr ""
|
|||||||
"Tutti i %d elementi che coincidono verranno ignorati in tutte le scansioni "
|
"Tutti i %d elementi che coincidono verranno ignorati in tutte le scansioni "
|
||||||
"successive. Continuare?"
|
"successive. Continuare?"
|
||||||
|
|
||||||
#: core\app.py:486
|
#: core\app.py:469
|
||||||
msgid "Select a directory to copy marked files to"
|
msgid "Select a directory to copy marked files to"
|
||||||
msgstr "Seleziona una directory in cui desideri copiare i file contrassegnati"
|
msgstr "Seleziona una directory in cui desideri copiare i file contrassegnati"
|
||||||
|
|
||||||
#: core\app.py:487
|
#: core\app.py:471
|
||||||
msgid "Select a directory to move marked files to"
|
msgid "Select a directory to move marked files to"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Seleziona una directory in cui desideri spostare i file contrassegnati"
|
"Seleziona una directory in cui desideri spostare i file contrassegnati"
|
||||||
|
|
||||||
#: core\app.py:527
|
#: core\app.py:510
|
||||||
msgid "Select a destination for your exported CSV"
|
msgid "Select a destination for your exported CSV"
|
||||||
msgstr "Seleziona una destinazione per il file CSV"
|
msgstr "Seleziona una destinazione per il file CSV"
|
||||||
|
|
||||||
#: core\app.py:534 core\app.py:801 core\app.py:811
|
#: core\app.py:516 core\app.py:771 core\app.py:781
|
||||||
msgid "Couldn't write to file: {}"
|
msgid "Couldn't write to file: {}"
|
||||||
msgstr "Impossibile modificare il file: {}"
|
msgstr "Impossibile modificare il file: {}"
|
||||||
|
|
||||||
#: core\app.py:559
|
#: core\app.py:539
|
||||||
msgid "You have no custom command set up. Set it up in your preferences."
|
msgid "You have no custom command set up. Set it up in your preferences."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Non hai impostato nessun comando personalizzato. Impostalo nelle tue "
|
"Non hai impostato nessun comando personalizzato. Impostalo nelle tue "
|
||||||
"preferenze."
|
"preferenze."
|
||||||
|
|
||||||
#: core\app.py:727 core\app.py:740
|
#: core\app.py:695 core\app.py:707
|
||||||
msgid "You are about to remove %d files from results. Continue?"
|
msgid "You are about to remove %d files from results. Continue?"
|
||||||
msgstr "Stai per rimuovere %d file dai risultati. Continuare?"
|
msgstr "Stai per rimuovere %d file dai risultati. Continuare?"
|
||||||
|
|
||||||
#: core\app.py:774
|
#: core\app.py:743
|
||||||
msgid "{} duplicate groups were changed by the re-prioritization."
|
msgid "{} duplicate groups were changed by the re-prioritization."
|
||||||
msgstr "{} gruppi duplicati sono stati cambiati dalla nuova priorirità"
|
msgstr "{} gruppi duplicati sono stati cambiati dalla nuova priorirità"
|
||||||
|
|
||||||
#: core\app.py:821
|
#: core\app.py:790
|
||||||
msgid "The selected directories contain no scannable file."
|
msgid "The selected directories contain no scannable file."
|
||||||
msgstr "Le cartelle selezionate non contengono file da scansionare."
|
msgstr "Le cartelle selezionate non contengono file da scansionare."
|
||||||
|
|
||||||
#: core\app.py:835
|
#: core\app.py:803
|
||||||
msgid "Collecting files to scan"
|
msgid "Collecting files to scan"
|
||||||
msgstr "Raccolta file da scansionare"
|
msgstr "Raccolta file da scansionare"
|
||||||
|
|
||||||
#: core\app.py:891
|
#: core\app.py:850
|
||||||
msgid "%s (%d discarded)"
|
msgid "%s (%d discarded)"
|
||||||
msgstr "%s (%d scartati)"
|
msgstr "%s (%d scartati)"
|
||||||
|
|
||||||
#: core\engine.py:244 core\engine.py:288
|
#: core\directories.py:191
|
||||||
msgid "0 matches found"
|
msgid "Collected {} files to scan"
|
||||||
msgstr "Nessun duplicato trovato"
|
msgstr "Raccolti {} file da scansionare"
|
||||||
|
|
||||||
#: core\engine.py:262 core\engine.py:296
|
#: core\directories.py:207
|
||||||
msgid "%d matches found"
|
msgid "Collected {} folders to scan"
|
||||||
msgstr "Trovato/i %d duplicato/i"
|
msgstr "Raccolte {} cartelle da scansionare"
|
||||||
|
|
||||||
#: core\gui\deletion_options.py:73
|
#: core\engine.py:27
|
||||||
|
msgid "%d matches found from %d groups"
|
||||||
|
msgstr "%d corrispondeze trovate da %d gruppi"
|
||||||
|
|
||||||
|
#: core\gui\deletion_options.py:71
|
||||||
msgid "You are sending {} file(s) to the Trash."
|
msgid "You are sending {} file(s) to the Trash."
|
||||||
msgstr "Stai spostando {} file al Cestino."
|
msgstr "Stai spostando {} file al Cestino."
|
||||||
|
|
||||||
#: core\gui\exclude_list_table.py:15
|
#: core\gui\exclude_list_table.py:14
|
||||||
msgid "Regular Expressions"
|
msgid "Regular Expressions"
|
||||||
msgstr "Espressioni regolari"
|
msgstr "Espressioni regolari"
|
||||||
|
|
||||||
@@ -181,15 +190,15 @@ msgstr "Contenuti"
|
|||||||
msgid "Analyzed %d/%d pictures"
|
msgid "Analyzed %d/%d pictures"
|
||||||
msgstr "Analizzate %d/%d immagini"
|
msgstr "Analizzate %d/%d immagini"
|
||||||
|
|
||||||
#: core\pe\matchblock.py:181
|
#: core\pe\matchblock.py:177
|
||||||
msgid "Performed %d/%d chunk matches"
|
msgid "Performed %d/%d chunk matches"
|
||||||
msgstr "Effettuate %d/%d comparazioni sui sottogruppi di immagini"
|
msgstr "Effettuate %d/%d comparazioni sui sottogruppi di immagini"
|
||||||
|
|
||||||
#: core\pe\matchblock.py:191
|
#: core\pe\matchblock.py:185
|
||||||
msgid "Preparing for matching"
|
msgid "Preparing for matching"
|
||||||
msgstr "Preparazione per la comparazione"
|
msgstr "Preparazione per la comparazione"
|
||||||
|
|
||||||
#: core\pe\matchblock.py:244
|
#: core\pe\matchblock.py:234
|
||||||
msgid "Verified %d/%d matches"
|
msgid "Verified %d/%d matches"
|
||||||
msgstr "Verificate %d/%d somiglianze"
|
msgstr "Verificate %d/%d somiglianze"
|
||||||
|
|
||||||
@@ -237,23 +246,23 @@ msgstr "Il più nuovo"
|
|||||||
msgid "Oldest"
|
msgid "Oldest"
|
||||||
msgstr "Il più vecchio"
|
msgstr "Il più vecchio"
|
||||||
|
|
||||||
#: core\results.py:142
|
#: core\results.py:134
|
||||||
msgid "%d / %d (%s / %s) duplicates marked."
|
msgid "%d / %d (%s / %s) duplicates marked."
|
||||||
msgstr "%d / %d (%s / %s) duplicati marcati."
|
msgstr "%d / %d (%s / %s) duplicati marcati."
|
||||||
|
|
||||||
#: core\results.py:149
|
#: core\results.py:141
|
||||||
msgid " filter: %s"
|
msgid " filter: %s"
|
||||||
msgstr " filtro: %s"
|
msgstr " filtro: %s"
|
||||||
|
|
||||||
#: core\scanner.py:85
|
#: core\scanner.py:90
|
||||||
msgid "Read size of %d/%d files"
|
msgid "Read size of %d/%d files"
|
||||||
msgstr "Lettura dimensione di %d/%d file"
|
msgstr "Lettura dimensione di %d/%d file"
|
||||||
|
|
||||||
#: core\scanner.py:109
|
#: core\scanner.py:116
|
||||||
msgid "Read metadata of %d/%d files"
|
msgid "Read metadata of %d/%d files"
|
||||||
msgstr "Lettura metadata di %d/%d files"
|
msgstr "Lettura metadata di %d/%d files"
|
||||||
|
|
||||||
#: core\scanner.py:147
|
#: core\scanner.py:154
|
||||||
msgid "Almost done! Fiddling with results..."
|
msgid "Almost done! Fiddling with results..."
|
||||||
msgstr "Quasi finito! Sto organizzando i risultati..."
|
msgstr "Quasi finito! Sto organizzando i risultati..."
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
# Translators:
|
# Translators:
|
||||||
# Andrew Senetar <arsenetar@gmail.com>, 2021
|
# Andrew Senetar <arsenetar@gmail.com>, 2021
|
||||||
# Fuan <jcfrt@posteo.net>, 2021
|
# Fuan <jcfrt@posteo.net>, 2021
|
||||||
|
# Emanuele, 2021
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\n"
|
"Last-Translator: Emanuele, 2021\n"
|
||||||
"Language-Team: Italian (https://www.transifex.com/voltaicideas/teams/116153/it/)\n"
|
"Language-Team: Italian (https://www.transifex.com/voltaicideas/teams/116153/it/)\n"
|
||||||
"Language: it\n"
|
"Language: it\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
@@ -951,3 +952,45 @@ msgstr "Generale"
|
|||||||
#: qt\preferences_dialog.py:286
|
#: qt\preferences_dialog.py:286
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
msgstr "Schermo"
|
msgstr "Schermo"
|
||||||
|
|
||||||
|
#: qt\se\preferences_dialog.py:70
|
||||||
|
msgid "Partially hash files bigger than"
|
||||||
|
msgstr "Calcola hash parziale di file più grandi di"
|
||||||
|
|
||||||
|
#: qt\se\preferences_dialog.py:80
|
||||||
|
msgid "MB"
|
||||||
|
msgstr "MB"
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:163
|
||||||
|
msgid "Use native OS dialogs"
|
||||||
|
msgstr "Usa le finestre di dialogo native del Sistema Operativo"
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:166
|
||||||
|
msgid ""
|
||||||
|
"For actions such as file/folder selection use the OS native dialogs.\n"
|
||||||
|
"Some native dialogs have limited functionality."
|
||||||
|
msgstr ""
|
||||||
|
"Per azioni come selezione di file/cartelle usa le finestre di dialogo native del Sistema Operativo.\n"
|
||||||
|
"Alcune finestre di dialogo native hanno funzionalità limitate."
|
||||||
|
|
||||||
|
#: qt\se\preferences_dialog.py:68
|
||||||
|
msgid "Ignore files larger than"
|
||||||
|
msgstr "Ignora file più grandi di"
|
||||||
|
|
||||||
|
#: qt\app.py:135 qt\app.py:293
|
||||||
|
msgid "Clear Cache"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\app.py:294
|
||||||
|
msgid ""
|
||||||
|
"Do you really want to clear the cache? This will remove all cached file "
|
||||||
|
"hashes and picture analysis."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\app.py:299
|
||||||
|
msgid "Cache cleared."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:173
|
||||||
|
msgid "Use dark style"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -44,99 +44,107 @@ msgstr "コピー中"
|
|||||||
msgid "Sending to Trash"
|
msgid "Sending to Trash"
|
||||||
msgstr "ごみ箱に送信します"
|
msgstr "ごみ箱に送信します"
|
||||||
|
|
||||||
#: core\app.py:308
|
#: core\app.py:289
|
||||||
msgid ""
|
msgid ""
|
||||||
"A previous action is still hanging in there. You can't start a new one yet. "
|
"A previous action is still hanging in there. You can't start a new one yet. "
|
||||||
"Wait a few seconds, then try again."
|
"Wait a few seconds, then try again."
|
||||||
msgstr "前のアクションはまだそこにぶら下がっています。 まだ新しいものを始めることはできません。 数秒待ってから、再試行してください。"
|
msgstr "前のアクションはまだそこにぶら下がっています。 まだ新しいものを始めることはできません。 数秒待ってから、再試行してください。"
|
||||||
|
|
||||||
#: core\app.py:318
|
#: core\app.py:300
|
||||||
msgid "No duplicates found."
|
msgid "No duplicates found."
|
||||||
msgstr "重複は見つかりませんでした。"
|
msgstr "重複は見つかりませんでした。"
|
||||||
|
|
||||||
#: core\app.py:333
|
#: core\app.py:315
|
||||||
msgid "All marked files were copied successfully."
|
msgid "All marked files were copied successfully."
|
||||||
msgstr "マークされたファイルはすべて正常にコピーされました。"
|
msgstr "マークされたファイルはすべて正常にコピーされました。"
|
||||||
|
|
||||||
#: core\app.py:334
|
#: core\app.py:317
|
||||||
msgid "All marked files were moved successfully."
|
msgid "All marked files were moved successfully."
|
||||||
msgstr "マークされたファイルはすべて正常に移動されました。"
|
msgstr "マークされたファイルはすべて正常に移動されました。"
|
||||||
|
|
||||||
#: core\app.py:335
|
#: core\app.py:319
|
||||||
|
msgid "All marked files were deleted successfully."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core\app.py:321
|
||||||
msgid "All marked files were successfully sent to Trash."
|
msgid "All marked files were successfully sent to Trash."
|
||||||
msgstr "マークされたファイルはすべてごみ箱に正常に送信されました。"
|
msgstr "マークされたファイルはすべてごみ箱に正常に送信されました。"
|
||||||
|
|
||||||
#: core\app.py:343
|
#: core\app.py:326
|
||||||
msgid "Could not load file: {}"
|
msgid "Could not load file: {}"
|
||||||
msgstr "ファイルを読み込めませんでした:{}"
|
msgstr "ファイルを読み込めませんでした:{}"
|
||||||
|
|
||||||
#: core\app.py:399
|
#: core\app.py:382
|
||||||
msgid "'{}' already is in the list."
|
msgid "'{}' already is in the list."
|
||||||
msgstr "「{}」既にリストに含まれています。"
|
msgstr "「{}」既にリストに含まれています。"
|
||||||
|
|
||||||
#: core\app.py:401
|
#: core\app.py:384
|
||||||
msgid "'{}' does not exist."
|
msgid "'{}' does not exist."
|
||||||
msgstr "'{}' 存在しません。"
|
msgstr "'{}' 存在しません。"
|
||||||
|
|
||||||
#: core\app.py:410
|
#: core\app.py:392
|
||||||
msgid ""
|
msgid ""
|
||||||
"All selected %d matches are going to be ignored in all subsequent scans. "
|
"All selected %d matches are going to be ignored in all subsequent scans. "
|
||||||
"Continue?"
|
"Continue?"
|
||||||
msgstr "選択した%d個の一致は、以降のすべてのスキャンで無視されます。 継続する?"
|
msgstr "選択した%d個の一致は、以降のすべてのスキャンで無視されます。 継続する?"
|
||||||
|
|
||||||
#: core\app.py:486
|
#: core\app.py:469
|
||||||
msgid "Select a directory to copy marked files to"
|
msgid "Select a directory to copy marked files to"
|
||||||
msgstr "マークされたファイルをコピーするディレクトリを選択してください"
|
msgstr "マークされたファイルをコピーするディレクトリを選択してください"
|
||||||
|
|
||||||
#: core\app.py:487
|
#: core\app.py:471
|
||||||
msgid "Select a directory to move marked files to"
|
msgid "Select a directory to move marked files to"
|
||||||
msgstr "マークされたファイルを移動するディレクトリを選択してください"
|
msgstr "マークされたファイルを移動するディレクトリを選択してください"
|
||||||
|
|
||||||
#: core\app.py:527
|
#: core\app.py:510
|
||||||
msgid "Select a destination for your exported CSV"
|
msgid "Select a destination for your exported CSV"
|
||||||
msgstr "エクスポートしたCSVの宛先を選択します。"
|
msgstr "エクスポートしたCSVの宛先を選択します。"
|
||||||
|
|
||||||
#: core\app.py:534 core\app.py:801 core\app.py:811
|
#: core\app.py:516 core\app.py:771 core\app.py:781
|
||||||
msgid "Couldn't write to file: {}"
|
msgid "Couldn't write to file: {}"
|
||||||
msgstr "ファイルに書き込めませんでした:{}"
|
msgstr "ファイルに書き込めませんでした:{}"
|
||||||
|
|
||||||
#: core\app.py:559
|
#: core\app.py:539
|
||||||
msgid "You have no custom command set up. Set it up in your preferences."
|
msgid "You have no custom command set up. Set it up in your preferences."
|
||||||
msgstr "カスタムコマンドは設定されていません。 お好みで設定してください。"
|
msgstr "カスタムコマンドは設定されていません。 お好みで設定してください。"
|
||||||
|
|
||||||
#: core\app.py:727 core\app.py:740
|
#: core\app.py:695 core\app.py:707
|
||||||
msgid "You are about to remove %d files from results. Continue?"
|
msgid "You are about to remove %d files from results. Continue?"
|
||||||
msgstr "結果から%d個のファイルを削除しようとしています。 継続する?"
|
msgstr "結果から%d個のファイルを削除しようとしています。 継続する?"
|
||||||
|
|
||||||
#: core\app.py:774
|
#: core\app.py:743
|
||||||
msgid "{} duplicate groups were changed by the re-prioritization."
|
msgid "{} duplicate groups were changed by the re-prioritization."
|
||||||
msgstr "{}重複するグループは、再優先順位付けによって変更されました。"
|
msgstr "{}重複するグループは、再優先順位付けによって変更されました。"
|
||||||
|
|
||||||
#: core\app.py:821
|
#: core\app.py:790
|
||||||
msgid "The selected directories contain no scannable file."
|
msgid "The selected directories contain no scannable file."
|
||||||
msgstr "選択したディレクトリにはスキャン可能なファイルが含まれていません。"
|
msgstr "選択したディレクトリにはスキャン可能なファイルが含まれていません。"
|
||||||
|
|
||||||
#: core\app.py:835
|
#: core\app.py:803
|
||||||
msgid "Collecting files to scan"
|
msgid "Collecting files to scan"
|
||||||
msgstr "スキャンするファイルを収集しています"
|
msgstr "スキャンするファイルを収集しています"
|
||||||
|
|
||||||
#: core\app.py:891
|
#: core\app.py:850
|
||||||
msgid "%s (%d discarded)"
|
msgid "%s (%d discarded)"
|
||||||
msgstr "%s (%d 廃棄)"
|
msgstr "%s (%d 廃棄)"
|
||||||
|
|
||||||
#: core\engine.py:244 core\engine.py:288
|
#: core\directories.py:191
|
||||||
msgid "0 matches found"
|
msgid "Collected {} files to scan"
|
||||||
msgstr "一致するものが見つかりません"
|
msgstr ""
|
||||||
|
|
||||||
#: core\engine.py:262 core\engine.py:296
|
#: core\directories.py:207
|
||||||
msgid "%d matches found"
|
msgid "Collected {} folders to scan"
|
||||||
msgstr "%d の一致が見つかりました"
|
msgstr ""
|
||||||
|
|
||||||
#: core\gui\deletion_options.py:73
|
#: core\engine.py:27
|
||||||
|
msgid "%d matches found from %d groups"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core\gui\deletion_options.py:71
|
||||||
msgid "You are sending {} file(s) to the Trash."
|
msgid "You are sending {} file(s) to the Trash."
|
||||||
msgstr "{}個のファイルをゴミ箱に送信しています"
|
msgstr "{}個のファイルをゴミ箱に送信しています"
|
||||||
|
|
||||||
#: core\gui\exclude_list_table.py:15
|
#: core\gui\exclude_list_table.py:14
|
||||||
msgid "Regular Expressions"
|
msgid "Regular Expressions"
|
||||||
msgstr "正規表現"
|
msgstr "正規表現"
|
||||||
|
|
||||||
@@ -168,15 +176,15 @@ msgstr "内容"
|
|||||||
msgid "Analyzed %d/%d pictures"
|
msgid "Analyzed %d/%d pictures"
|
||||||
msgstr "%d/%d 枚の写真を分析しました"
|
msgstr "%d/%d 枚の写真を分析しました"
|
||||||
|
|
||||||
#: core\pe\matchblock.py:181
|
#: core\pe\matchblock.py:177
|
||||||
msgid "Performed %d/%d chunk matches"
|
msgid "Performed %d/%d chunk matches"
|
||||||
msgstr "チャンクマッチを%d/%d回実行しました"
|
msgstr "チャンクマッチを%d/%d回実行しました"
|
||||||
|
|
||||||
#: core\pe\matchblock.py:191
|
#: core\pe\matchblock.py:185
|
||||||
msgid "Preparing for matching"
|
msgid "Preparing for matching"
|
||||||
msgstr "マッチングの準備"
|
msgstr "マッチングの準備"
|
||||||
|
|
||||||
#: core\pe\matchblock.py:244
|
#: core\pe\matchblock.py:234
|
||||||
msgid "Verified %d/%d matches"
|
msgid "Verified %d/%d matches"
|
||||||
msgstr "%d/%d件の一致を確認"
|
msgstr "%d/%d件の一致を確認"
|
||||||
|
|
||||||
@@ -224,23 +232,23 @@ msgstr "最新"
|
|||||||
msgid "Oldest"
|
msgid "Oldest"
|
||||||
msgstr "最古"
|
msgstr "最古"
|
||||||
|
|
||||||
#: core\results.py:142
|
#: core\results.py:134
|
||||||
msgid "%d / %d (%s / %s) duplicates marked."
|
msgid "%d / %d (%s / %s) duplicates marked."
|
||||||
msgstr "%d / %d (%s / %s)マークされた重複。"
|
msgstr "%d / %d (%s / %s)マークされた重複。"
|
||||||
|
|
||||||
#: core\results.py:149
|
#: core\results.py:141
|
||||||
msgid " filter: %s"
|
msgid " filter: %s"
|
||||||
msgstr "フィルタ: %s"
|
msgstr "フィルタ: %s"
|
||||||
|
|
||||||
#: core\scanner.py:85
|
#: core\scanner.py:90
|
||||||
msgid "Read size of %d/%d files"
|
msgid "Read size of %d/%d files"
|
||||||
msgstr "%d/%dファイルのサイズを読み取った"
|
msgstr "%d/%dファイルのサイズを読み取った"
|
||||||
|
|
||||||
#: core\scanner.py:109
|
#: core\scanner.py:116
|
||||||
msgid "Read metadata of %d/%d files"
|
msgid "Read metadata of %d/%d files"
|
||||||
msgstr "%d/%dファイルのメタデータを読み取った"
|
msgstr "%d/%dファイルのメタデータを読み取った"
|
||||||
|
|
||||||
#: core\scanner.py:147
|
#: core\scanner.py:154
|
||||||
msgid "Almost done! Fiddling with results..."
|
msgid "Almost done! Fiddling with results..."
|
||||||
msgstr "ほぼ完了しました! 結果をいじっています..."
|
msgstr "ほぼ完了しました! 結果をいじっています..."
|
||||||
|
|
||||||
|
|||||||
@@ -925,3 +925,43 @@ msgstr "一般"
|
|||||||
#: qt\preferences_dialog.py:286
|
#: qt\preferences_dialog.py:286
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
msgstr "表示"
|
msgstr "表示"
|
||||||
|
|
||||||
|
#: qt\se\preferences_dialog.py:70
|
||||||
|
msgid "Partially hash files bigger than"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\se\preferences_dialog.py:80
|
||||||
|
msgid "MB"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:163
|
||||||
|
msgid "Use native OS dialogs"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:166
|
||||||
|
msgid ""
|
||||||
|
"For actions such as file/folder selection use the OS native dialogs.\n"
|
||||||
|
"Some native dialogs have limited functionality."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\se\preferences_dialog.py:68
|
||||||
|
msgid "Ignore files larger than"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\app.py:135 qt\app.py:293
|
||||||
|
msgid "Clear Cache"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\app.py:294
|
||||||
|
msgid ""
|
||||||
|
"Do you really want to clear the cache? This will remove all cached file "
|
||||||
|
"hashes and picture analysis."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\app.py:299
|
||||||
|
msgid "Cache cleared."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:173
|
||||||
|
msgid "Use dark style"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -45,99 +45,107 @@ msgstr "복사중"
|
|||||||
msgid "Sending to Trash"
|
msgid "Sending to Trash"
|
||||||
msgstr "휴지통으로 보내기"
|
msgstr "휴지통으로 보내기"
|
||||||
|
|
||||||
#: core\app.py:308
|
#: core\app.py:289
|
||||||
msgid ""
|
msgid ""
|
||||||
"A previous action is still hanging in there. You can't start a new one yet. "
|
"A previous action is still hanging in there. You can't start a new one yet. "
|
||||||
"Wait a few seconds, then try again."
|
"Wait a few seconds, then try again."
|
||||||
msgstr "이전 작업이 여전히 거기에 걸려 있습니다. 아직 새로운 것을 시작할 수 없습니다. 몇 초 후에 다시 시도하십시오."
|
msgstr "이전 작업이 여전히 거기에 걸려 있습니다. 아직 새로운 것을 시작할 수 없습니다. 몇 초 후에 다시 시도하십시오."
|
||||||
|
|
||||||
#: core\app.py:318
|
#: core\app.py:300
|
||||||
msgid "No duplicates found."
|
msgid "No duplicates found."
|
||||||
msgstr "중복 파일이 없습니다."
|
msgstr "중복 파일이 없습니다."
|
||||||
|
|
||||||
#: core\app.py:333
|
#: core\app.py:315
|
||||||
msgid "All marked files were copied successfully."
|
msgid "All marked files were copied successfully."
|
||||||
msgstr "표시된 모든 파일이 성공적으로 복사되었습니다."
|
msgstr "표시된 모든 파일이 성공적으로 복사되었습니다."
|
||||||
|
|
||||||
#: core\app.py:334
|
#: core\app.py:317
|
||||||
msgid "All marked files were moved successfully."
|
msgid "All marked files were moved successfully."
|
||||||
msgstr "표시된 모든 파일이 성공적으로 이동되었습니다."
|
msgstr "표시된 모든 파일이 성공적으로 이동되었습니다."
|
||||||
|
|
||||||
#: core\app.py:335
|
#: core\app.py:319
|
||||||
|
msgid "All marked files were deleted successfully."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core\app.py:321
|
||||||
msgid "All marked files were successfully sent to Trash."
|
msgid "All marked files were successfully sent to Trash."
|
||||||
msgstr "표시된 모든 파일이 성공적으로 휴지통으로 전송되었습니다."
|
msgstr "표시된 모든 파일이 성공적으로 휴지통으로 전송되었습니다."
|
||||||
|
|
||||||
#: core\app.py:343
|
#: core\app.py:326
|
||||||
msgid "Could not load file: {}"
|
msgid "Could not load file: {}"
|
||||||
msgstr "파일을로드 할 수 없습니다 : {}"
|
msgstr "파일을로드 할 수 없습니다 : {}"
|
||||||
|
|
||||||
#: core\app.py:399
|
#: core\app.py:382
|
||||||
msgid "'{}' already is in the list."
|
msgid "'{}' already is in the list."
|
||||||
msgstr "'{}' 는 이미 목록에 있습니다."
|
msgstr "'{}' 는 이미 목록에 있습니다."
|
||||||
|
|
||||||
#: core\app.py:401
|
#: core\app.py:384
|
||||||
msgid "'{}' does not exist."
|
msgid "'{}' does not exist."
|
||||||
msgstr "'{}' 가 존재하지 않습니다."
|
msgstr "'{}' 가 존재하지 않습니다."
|
||||||
|
|
||||||
#: core\app.py:410
|
#: core\app.py:392
|
||||||
msgid ""
|
msgid ""
|
||||||
"All selected %d matches are going to be ignored in all subsequent scans. "
|
"All selected %d matches are going to be ignored in all subsequent scans. "
|
||||||
"Continue?"
|
"Continue?"
|
||||||
msgstr "선택된 %d개의 일치 항목은 모든 후속 검색에서 무시됩니다. 계속하다?"
|
msgstr "선택된 %d개의 일치 항목은 모든 후속 검색에서 무시됩니다. 계속하다?"
|
||||||
|
|
||||||
#: core\app.py:486
|
#: core\app.py:469
|
||||||
msgid "Select a directory to copy marked files to"
|
msgid "Select a directory to copy marked files to"
|
||||||
msgstr "표시된 파일을 복사 할 디렉토리를 선택하십시오"
|
msgstr "표시된 파일을 복사 할 디렉토리를 선택하십시오"
|
||||||
|
|
||||||
#: core\app.py:487
|
#: core\app.py:471
|
||||||
msgid "Select a directory to move marked files to"
|
msgid "Select a directory to move marked files to"
|
||||||
msgstr "표시된 파일을 이동할 디렉토리를 선택하십시오"
|
msgstr "표시된 파일을 이동할 디렉토리를 선택하십시오"
|
||||||
|
|
||||||
#: core\app.py:527
|
#: core\app.py:510
|
||||||
msgid "Select a destination for your exported CSV"
|
msgid "Select a destination for your exported CSV"
|
||||||
msgstr "내 보낸 CSV의 대상을 선택하십시오"
|
msgstr "내 보낸 CSV의 대상을 선택하십시오"
|
||||||
|
|
||||||
#: core\app.py:534 core\app.py:801 core\app.py:811
|
#: core\app.py:516 core\app.py:771 core\app.py:781
|
||||||
msgid "Couldn't write to file: {}"
|
msgid "Couldn't write to file: {}"
|
||||||
msgstr "파일에 쓸 수 없습니다 : {}"
|
msgstr "파일에 쓸 수 없습니다 : {}"
|
||||||
|
|
||||||
#: core\app.py:559
|
#: core\app.py:539
|
||||||
msgid "You have no custom command set up. Set it up in your preferences."
|
msgid "You have no custom command set up. Set it up in your preferences."
|
||||||
msgstr "사용자 지정 명령을 설정하지 않았습니다. 기본 설정에서 설정하십시오."
|
msgstr "사용자 지정 명령을 설정하지 않았습니다. 기본 설정에서 설정하십시오."
|
||||||
|
|
||||||
#: core\app.py:727 core\app.py:740
|
#: core\app.py:695 core\app.py:707
|
||||||
msgid "You are about to remove %d files from results. Continue?"
|
msgid "You are about to remove %d files from results. Continue?"
|
||||||
msgstr "결과에서 %d 개의 파일을 제거하려고합니다. 계속하다?"
|
msgstr "결과에서 %d 개의 파일을 제거하려고합니다. 계속하다?"
|
||||||
|
|
||||||
#: core\app.py:774
|
#: core\app.py:743
|
||||||
msgid "{} duplicate groups were changed by the re-prioritization."
|
msgid "{} duplicate groups were changed by the re-prioritization."
|
||||||
msgstr "{} 개의 중복 그룹이 우선 순위 재 지정으로 변경되었습니다."
|
msgstr "{} 개의 중복 그룹이 우선 순위 재 지정으로 변경되었습니다."
|
||||||
|
|
||||||
#: core\app.py:821
|
#: core\app.py:790
|
||||||
msgid "The selected directories contain no scannable file."
|
msgid "The selected directories contain no scannable file."
|
||||||
msgstr "선택한 디렉토리에 스캔 가능한 파일이 없습니다."
|
msgstr "선택한 디렉토리에 스캔 가능한 파일이 없습니다."
|
||||||
|
|
||||||
#: core\app.py:835
|
#: core\app.py:803
|
||||||
msgid "Collecting files to scan"
|
msgid "Collecting files to scan"
|
||||||
msgstr "스캔 할 파일 수집"
|
msgstr "스캔 할 파일 수집"
|
||||||
|
|
||||||
#: core\app.py:891
|
#: core\app.py:850
|
||||||
msgid "%s (%d discarded)"
|
msgid "%s (%d discarded)"
|
||||||
msgstr "%s (%d 폐기)"
|
msgstr "%s (%d 폐기)"
|
||||||
|
|
||||||
#: core\engine.py:244 core\engine.py:288
|
#: core\directories.py:191
|
||||||
msgid "0 matches found"
|
msgid "Collected {} files to scan"
|
||||||
msgstr "일치하는 항목이 없습니다"
|
msgstr ""
|
||||||
|
|
||||||
#: core\engine.py:262 core\engine.py:296
|
#: core\directories.py:207
|
||||||
msgid "%d matches found"
|
msgid "Collected {} folders to scan"
|
||||||
msgstr "%d개의 일치 항목을 찾았습니다."
|
msgstr ""
|
||||||
|
|
||||||
#: core\gui\deletion_options.py:73
|
#: core\engine.py:27
|
||||||
|
msgid "%d matches found from %d groups"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core\gui\deletion_options.py:71
|
||||||
msgid "You are sending {} file(s) to the Trash."
|
msgid "You are sending {} file(s) to the Trash."
|
||||||
msgstr "{}개의 파일을 휴지통으로 보내고 있습니다."
|
msgstr "{}개의 파일을 휴지통으로 보내고 있습니다."
|
||||||
|
|
||||||
#: core\gui\exclude_list_table.py:15
|
#: core\gui\exclude_list_table.py:14
|
||||||
msgid "Regular Expressions"
|
msgid "Regular Expressions"
|
||||||
msgstr "정규식"
|
msgstr "정규식"
|
||||||
|
|
||||||
@@ -169,15 +177,15 @@ msgstr "내용"
|
|||||||
msgid "Analyzed %d/%d pictures"
|
msgid "Analyzed %d/%d pictures"
|
||||||
msgstr "%d/%d 사진 분석"
|
msgstr "%d/%d 사진 분석"
|
||||||
|
|
||||||
#: core\pe\matchblock.py:181
|
#: core\pe\matchblock.py:177
|
||||||
msgid "Performed %d/%d chunk matches"
|
msgid "Performed %d/%d chunk matches"
|
||||||
msgstr "%d/%d 청크 매치 수행"
|
msgstr "%d/%d 청크 매치 수행"
|
||||||
|
|
||||||
#: core\pe\matchblock.py:191
|
#: core\pe\matchblock.py:185
|
||||||
msgid "Preparing for matching"
|
msgid "Preparing for matching"
|
||||||
msgstr "매칭 준비"
|
msgstr "매칭 준비"
|
||||||
|
|
||||||
#: core\pe\matchblock.py:244
|
#: core\pe\matchblock.py:234
|
||||||
msgid "Verified %d/%d matches"
|
msgid "Verified %d/%d matches"
|
||||||
msgstr "%d/%d 일치 확인"
|
msgstr "%d/%d 일치 확인"
|
||||||
|
|
||||||
@@ -225,23 +233,23 @@ msgstr "최신"
|
|||||||
msgid "Oldest"
|
msgid "Oldest"
|
||||||
msgstr "가장 오래된"
|
msgstr "가장 오래된"
|
||||||
|
|
||||||
#: core\results.py:142
|
#: core\results.py:134
|
||||||
msgid "%d / %d (%s / %s) duplicates marked."
|
msgid "%d / %d (%s / %s) duplicates marked."
|
||||||
msgstr "%d / %d (%s / %s) 개의 중복이 표시되었습니다."
|
msgstr "%d / %d (%s / %s) 개의 중복이 표시되었습니다."
|
||||||
|
|
||||||
#: core\results.py:149
|
#: core\results.py:141
|
||||||
msgid " filter: %s"
|
msgid " filter: %s"
|
||||||
msgstr "필터: %s"
|
msgstr "필터: %s"
|
||||||
|
|
||||||
#: core\scanner.py:85
|
#: core\scanner.py:90
|
||||||
msgid "Read size of %d/%d files"
|
msgid "Read size of %d/%d files"
|
||||||
msgstr "%d/%d 개의 파일을 읽을 수 있습니다."
|
msgstr "%d/%d 개의 파일을 읽을 수 있습니다."
|
||||||
|
|
||||||
#: core\scanner.py:109
|
#: core\scanner.py:116
|
||||||
msgid "Read metadata of %d/%d files"
|
msgid "Read metadata of %d/%d files"
|
||||||
msgstr "%d/%d 개 파일의 메타 데이터를 읽었습니다."
|
msgstr "%d/%d 개 파일의 메타 데이터를 읽었습니다."
|
||||||
|
|
||||||
#: core\scanner.py:147
|
#: core\scanner.py:154
|
||||||
msgid "Almost done! Fiddling with results..."
|
msgid "Almost done! Fiddling with results..."
|
||||||
msgstr "거의 완료되었습니다! 결과를 만지작 거리는 중..."
|
msgstr "거의 완료되었습니다! 결과를 만지작 거리는 중..."
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user