├── .ctags ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── codeql-analysis.yml │ ├── default.yml │ └── tx-push.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .sonarcloud.properties ├── .tx └── config ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── CONTRIBUTING.md ├── CREDITS ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── Windows.md ├── build.py ├── commitlint.config.js ├── core ├── __init__.py ├── app.py ├── directories.py ├── engine.py ├── exclude.py ├── export.py ├── fs.py ├── gui │ ├── __init__.py │ ├── base.py │ ├── deletion_options.py │ ├── details_panel.py │ ├── directory_tree.py │ ├── exclude_list_dialog.py │ ├── exclude_list_table.py │ ├── ignore_list_dialog.py │ ├── ignore_list_table.py │ ├── prioritize_dialog.py │ ├── problem_dialog.py │ ├── problem_table.py │ ├── result_table.py │ └── stats_label.py ├── ignore.py ├── markable.py ├── me │ ├── __init__.py │ ├── fs.py │ ├── prioritize.py │ ├── result_table.py │ └── scanner.py ├── pe │ ├── __init__.py │ ├── block.py │ ├── block.pyi │ ├── cache.py │ ├── cache.pyi │ ├── cache_sqlite.py │ ├── exif.py │ ├── matchblock.py │ ├── matchexif.py │ ├── modules │ │ ├── block.c │ │ ├── block_osx.m │ │ ├── cache.c │ │ ├── common.c │ │ └── common.h │ ├── photo.py │ ├── prioritize.py │ ├── result_table.py │ └── scanner.py ├── prioritize.py ├── results.py ├── scanner.py ├── se │ ├── __init__.py │ ├── fs.py │ ├── result_table.py │ └── scanner.py ├── tests │ ├── __init__.py │ ├── app_test.py │ ├── base.py │ ├── block_test.py │ ├── cache_test.py │ ├── conftest.py │ ├── directories_test.py │ ├── engine_test.py │ ├── exclude_test.py │ ├── fs_test.py │ ├── ignore_test.py │ ├── markable_test.py │ ├── prioritize_test.py │ ├── result_table_test.py │ ├── results_test.py │ └── scanner_test.py └── util.py ├── help ├── changelog ├── changelog.tmpl ├── conf.tmpl ├── de │ ├── faq.rst │ ├── folders.rst │ ├── index.rst │ ├── preferences.rst │ ├── quick_start.rst │ ├── reprioritize.rst │ └── results.rst ├── en │ ├── contribute.rst │ ├── developer │ │ ├── core │ │ │ ├── app.rst │ │ │ ├── directories.rst │ │ │ ├── engine.rst │ │ │ ├── fs.rst │ │ │ ├── gui │ │ │ │ ├── deletion_options.rst │ │ │ │ └── index.rst │ │ │ ├── index.rst │ │ │ └── results.rst │ │ ├── hscommon │ │ │ ├── build.rst │ │ │ ├── conflict.rst │ │ │ ├── desktop.rst │ │ │ ├── gui │ │ │ │ ├── base.rst │ │ │ │ ├── column.rst │ │ │ │ ├── progress_window.rst │ │ │ │ ├── selectable_list.rst │ │ │ │ ├── table.rst │ │ │ │ ├── text_field.rst │ │ │ │ └── tree.rst │ │ │ ├── index.rst │ │ │ ├── jobprogress │ │ │ │ ├── job.rst │ │ │ │ └── performer.rst │ │ │ ├── notify.rst │ │ │ ├── path.rst │ │ │ └── util.rst │ │ └── index.rst │ ├── faq.rst │ ├── folders.rst │ ├── index.rst │ ├── preferences.rst │ ├── quick_start.rst │ ├── reprioritize.rst │ ├── results.rst │ └── scan.rst ├── fr │ ├── faq.rst │ ├── folders.rst │ ├── index.rst │ ├── preferences.rst │ ├── quick_start.rst │ ├── reprioritize.rst │ └── results.rst ├── hy │ ├── faq.rst │ ├── folders.rst │ ├── index.rst │ ├── preferences.rst │ ├── quick_start.rst │ ├── reprioritize.rst │ └── results.rst ├── ru │ ├── faq.rst │ ├── folders.rst │ ├── index.rst │ ├── preferences.rst │ ├── quick_start.rst │ ├── reprioritize.rst │ └── results.rst └── uk │ ├── faq.rst │ ├── folders.rst │ ├── index.rst │ ├── preferences.rst │ ├── quick_start.rst │ ├── reprioritize.rst │ └── results.rst ├── hscommon ├── LICENSE ├── README ├── __init__.py ├── build.py ├── conflict.py ├── desktop.py ├── gui │ ├── __init__.py │ ├── base.py │ ├── column.py │ ├── progress_window.py │ ├── selectable_list.py │ ├── table.py │ ├── text_field.py │ └── tree.py ├── jobprogress │ ├── __init__.py │ ├── job.py │ └── performer.py ├── loc.py ├── notify.py ├── path.py ├── plat.py ├── pygettext.py ├── sphinxgen.py ├── tests │ ├── __init__.py │ ├── conflict_test.py │ ├── notify_test.py │ ├── path_test.py │ ├── selectable_list_test.py │ ├── table_test.py │ ├── tree_test.py │ └── util_test.py ├── testutil.py ├── trans.py └── util.py ├── images ├── dgme_logo.ico ├── dgme_logo_128.png ├── dgme_logo_32.png ├── dgpe_logo.ico ├── dgpe_logo_128.png ├── dgpe_logo_32.png ├── dgse_logo.ico ├── dgse_logo_128.png ├── dgse_logo_32.png ├── dialog-error.png ├── dupeguru.icns ├── exchange.icns ├── exchange.ico ├── exchange.png ├── exchange_purple.png ├── exchange_purple_upscaled.png ├── exchange_purple_waifu_s4_tta8.png ├── exchange_purple_waifu_s4_tta8.xcf ├── exchange_waifu_s4_tta8.png ├── folder32.png ├── minus_8.png ├── old_zoom_best_fit.png ├── old_zoom_in.png ├── old_zoom_original.png ├── old_zoom_out.png ├── plus_8.png └── search_clear_13.png ├── locale ├── ar │ └── LC_MESSAGES │ │ ├── columns.po │ │ ├── core.po │ │ └── ui.po ├── columns.pot ├── core.pot ├── cs │ └── LC_MESSAGES │ │ ├── columns.po │ │ ├── core.po │ │ └── ui.po ├── de │ └── LC_MESSAGES │ │ ├── columns.po │ │ ├── core.po │ │ └── ui.po ├── el │ └── LC_MESSAGES │ │ ├── columns.po │ │ ├── core.po │ │ └── ui.po ├── en │ └── LC_MESSAGES │ │ ├── columns.po │ │ ├── core.po │ │ └── ui.po ├── es │ └── LC_MESSAGES │ │ ├── columns.po │ │ ├── core.po │ │ └── ui.po ├── fr │ └── LC_MESSAGES │ │ ├── columns.po │ │ ├── core.po │ │ └── ui.po ├── hy │ └── LC_MESSAGES │ │ ├── columns.po │ │ ├── core.po │ │ └── ui.po ├── it │ └── LC_MESSAGES │ │ ├── columns.po │ │ ├── core.po │ │ └── ui.po ├── ja │ └── LC_MESSAGES │ │ ├── columns.po │ │ ├── core.po │ │ └── ui.po ├── ko │ └── LC_MESSAGES │ │ ├── columns.po │ │ ├── core.po │ │ └── ui.po ├── ms │ └── LC_MESSAGES │ │ ├── columns.po │ │ ├── core.po │ │ └── ui.po ├── nl │ └── LC_MESSAGES │ │ ├── columns.po │ │ ├── core.po │ │ └── ui.po ├── pl_PL │ └── LC_MESSAGES │ │ ├── columns.po │ │ ├── core.po │ │ └── ui.po ├── pt_BR │ └── LC_MESSAGES │ │ ├── columns.po │ │ ├── core.po │ │ └── ui.po ├── ru │ └── LC_MESSAGES │ │ ├── columns.po │ │ ├── core.po │ │ └── ui.po ├── tr │ └── LC_MESSAGES │ │ ├── columns.po │ │ ├── core.po │ │ └── ui.po ├── ui.pot ├── uk │ └── LC_MESSAGES │ │ ├── columns.po │ │ ├── core.po │ │ └── ui.po ├── vi │ └── LC_MESSAGES │ │ ├── columns.po │ │ ├── core.po │ │ └── ui.po ├── zh_CN │ └── LC_MESSAGES │ │ ├── columns.po │ │ ├── core.po │ │ └── ui.po └── zh_TW │ └── LC_MESSAGES │ ├── columns.po │ ├── core.po │ └── ui.po ├── macos.md ├── package.py ├── pkg ├── arch │ ├── dupeguru.desktop │ └── dupeguru.json ├── debian │ ├── Makefile │ ├── build_pe_modules.py │ ├── changelog │ ├── compat │ ├── control │ ├── copyright │ ├── dirs │ ├── dupeguru.desktop │ ├── dupeguru.json │ ├── rules │ └── source │ │ ├── format │ │ └── options └── dupeguru.desktop ├── pyproject.toml ├── qt ├── __init__.py ├── about_box.py ├── app.py ├── column.py ├── deletion_options.py ├── details_dialog.py ├── details_table.py ├── dg.qrc ├── directories_dialog.py ├── directories_model.py ├── error_report_dialog.py ├── exclude_list_dialog.py ├── exclude_list_table.py ├── ignore_list_dialog.py ├── ignore_list_table.py ├── me │ ├── __init__.py │ ├── details_dialog.py │ ├── preferences_dialog.py │ └── results_model.py ├── pe │ ├── __init__.py │ ├── block.py │ ├── block.pyi │ ├── details_dialog.py │ ├── image_viewer.py │ ├── modules │ │ └── block.c │ ├── photo.py │ ├── preferences_dialog.py │ └── results_model.py ├── platform.py ├── preferences.py ├── preferences_dialog.py ├── prioritize_dialog.py ├── problem_dialog.py ├── problem_table.py ├── progress_window.py ├── radio_box.py ├── recent.py ├── result_window.py ├── results_model.py ├── se │ ├── __init__.py │ ├── details_dialog.py │ ├── preferences_dialog.py │ └── results_model.py ├── search_edit.py ├── selectable_list.py ├── stats_label.py ├── tabbed_window.py ├── table.py ├── tree_model.py └── util.py ├── requirements-extra.txt ├── requirements.txt ├── run.py ├── setup.cfg ├── setup.nsi ├── setup.py ├── tox.ini └── win_version_info.temp /.ctags: -------------------------------------------------------------------------------- 1 | -R 2 | --exclude=build 3 | --exclude=env 4 | --exclude=.tox 5 | --python-kinds=-i 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: arsenetar 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. Windows 10 / OSX 10.15 / Ubuntu 20.04 / Arch Linux] 28 | - Version [e.g. 4.1.0] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. You may include the debug log although it is normally best to attach it as a file. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [master] 9 | schedule: 10 | - cron: "24 20 * * 2" 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | language: ["cpp", "python"] 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v2 29 | 30 | # Initializes the CodeQL tools for scanning. 31 | - name: Initialize CodeQL 32 | uses: github/codeql-action/init@v1 33 | with: 34 | languages: ${{ matrix.language }} 35 | # If you wish to specify custom queries, you can do so here or in a config file. 36 | # By default, queries listed here will override any specified in a config file. 37 | # Prefix the list here with "+" to use these queries and those in the config file. 38 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 39 | - if: matrix.language == 'cpp' 40 | name: Build Cpp 41 | run: | 42 | sudo apt-get update 43 | sudo apt-get install python3-pyqt5 44 | make modules 45 | - if: matrix.language == 'python' 46 | name: Autobuild 47 | uses: github/codeql-action/autobuild@v1 48 | # Analysis 49 | - name: Perform CodeQL Analysis 50 | uses: github/codeql-action/analyze@v1 51 | -------------------------------------------------------------------------------- /.github/workflows/default.yml: -------------------------------------------------------------------------------- 1 | # Workflow lints, and checks format in parallel then runs tests on all platforms 2 | 3 | name: Default CI/CD 4 | 5 | on: 6 | push: 7 | pull_request: 8 | branches: [master] 9 | 10 | jobs: 11 | pre-commit: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Python 3.12 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: "3.12" 19 | - uses: pre-commit/action@v3.0.1 20 | test: 21 | needs: [pre-commit] 22 | runs-on: ${{ matrix.os }} 23 | strategy: 24 | matrix: 25 | os: [ubuntu-latest] 26 | python-version: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12"] 27 | include: 28 | - os: windows-latest 29 | python-version: "3.12" 30 | - os: macos-latest 31 | python-version: "3.12" 32 | 33 | steps: 34 | - uses: actions/checkout@v4 35 | - name: Set up Python ${{ matrix.python-version }} 36 | uses: actions/setup-python@v5 37 | with: 38 | python-version: ${{ matrix.python-version }} 39 | - name: Install dependencies 40 | run: | 41 | python -m pip install --upgrade pip 42 | pip install setuptools 43 | pip install -r requirements.txt -r requirements-extra.txt 44 | - name: Build python modules 45 | run: | 46 | python build.py --modules 47 | - name: Run tests 48 | run: | 49 | pytest core hscommon 50 | - name: Upload Artifacts 51 | if: matrix.os == 'ubuntu-latest' 52 | uses: actions/upload-artifact@v4 53 | with: 54 | name: modules ${{ matrix.python-version }} 55 | path: build/**/*.so 56 | merge-artifacts: 57 | needs: [test] 58 | runs-on: ubuntu-latest 59 | steps: 60 | - name: Merge Artifacts 61 | uses: actions/upload-artifact/merge@v4 62 | with: 63 | name: modules 64 | pattern: modules* 65 | delete-merged: true 66 | -------------------------------------------------------------------------------- /.github/workflows/tx-push.yml: -------------------------------------------------------------------------------- 1 | # Push translation source to Transifex 2 | name: Transifex Sync 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | paths: 9 | - locale/*.pot 10 | 11 | env: 12 | TX_VERSION: "v1.6.10" 13 | 14 | jobs: 15 | push-source: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Get Transifex Client 20 | run: | 21 | curl -o- https://raw.githubusercontent.com/transifex/cli/master/install.sh | bash -s -- $TX_VERSION 22 | - name: Update & Push Translation Sources 23 | env: 24 | TX_TOKEN: ${{ secrets.TX_TOKEN }} 25 | run: | 26 | ./tx push -s --use-git-timestamps 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | #*.pot 57 | 58 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 59 | __pypackages__/ 60 | 61 | # Environments 62 | .env 63 | .venv 64 | env*/ 65 | venv/ 66 | ENV/ 67 | env.bak/ 68 | venv.bak/ 69 | 70 | # mypy 71 | .mypy_cache/ 72 | .dmypy.json 73 | dmypy.json 74 | 75 | # Pyre type checker 76 | .pyre/ 77 | 78 | # pytype static type analyzer 79 | .pytype/ 80 | 81 | # Cython debug symbols 82 | cython_debug/ 83 | 84 | # macOS 85 | .DS_Store 86 | 87 | # Visual Studio Code 88 | .vscode/* 89 | !.vscode/settings.json 90 | !.vscode/tasks.json 91 | !.vscode/launch.json 92 | !.vscode/extensions.json 93 | !.vscode/*.code-snippets 94 | 95 | # Local History for Visual Studio Code 96 | .history/ 97 | 98 | # Built Visual Studio Code Extensions 99 | *.vsix 100 | 101 | # dupeGuru Specific 102 | /qt/*_rc.py 103 | /help/*/conf.py 104 | /help/*/changelog.rst 105 | cocoa/autogen 106 | /cocoa/*/Info.plist 107 | /cocoa/*/build 108 | 109 | *.waf* 110 | .lock-waf* 111 | /tags 112 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.5.0 4 | hooks: 5 | - id: check-yaml 6 | - id: check-toml 7 | - id: end-of-file-fixer 8 | exclude: ".*.json" 9 | - id: trailing-whitespace 10 | - repo: https://github.com/psf/black 11 | rev: 24.2.0 12 | hooks: 13 | - id: black 14 | - repo: https://github.com/PyCQA/flake8 15 | rev: 7.0.0 16 | hooks: 17 | - id: flake8 18 | exclude: ^(.tox|env|build|dist|help|qt/dg_rc.py|pkg).* 19 | - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook 20 | rev: v9.11.0 21 | hooks: 22 | - id: commitlint 23 | stages: [commit-msg] 24 | additional_dependencies: ["@commitlint/config-conventional"] 25 | -------------------------------------------------------------------------------- /.sonarcloud.properties: -------------------------------------------------------------------------------- 1 | sonar.python.version=3.7, 3.8, 3.9, 3.10, 3.11 2 | -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | 4 | [o:voltaicideas:p:dupeguru-1:r:columns] 5 | file_filter = locale//LC_MESSAGES/columns.po 6 | source_file = locale/columns.pot 7 | source_lang = en 8 | type = PO 9 | 10 | [o:voltaicideas:p:dupeguru-1:r:core] 11 | file_filter = locale//LC_MESSAGES/core.po 12 | source_file = locale/core.pot 13 | source_lang = en 14 | type = PO 15 | 16 | [o:voltaicideas:p:dupeguru-1:r:ui] 17 | file_filter = locale//LC_MESSAGES/ui.po 18 | source_file = locale/ui.pot 19 | source_lang = en 20 | type = PO 21 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // List of extensions which should be recommended for users of this workspace. 3 | "recommendations": [ 4 | "redhat.vscode-yaml", 5 | "ms-python.vscode-pylance", 6 | "ms-python.python", 7 | "ms-python.black-formatter", 8 | ], 9 | // List of extensions recommended by VS Code that should not be recommended for 10 | // users of this workspace. 11 | "unwantedRecommendations": [] 12 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "DupuGuru", 9 | "type": "debugpy", 10 | "request": "launch", 11 | "program": "run.py", 12 | "console": "integratedTerminal", 13 | "subProcess": true, 14 | "justMyCode": false 15 | }, 16 | ] 17 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Dupras", 4 | "hscommon" 5 | ], 6 | "editor.rulers": [ 7 | 88, 8 | 120 9 | ], 10 | "python.languageServer": "Pylance", 11 | "yaml.schemaStore.enable": true, 12 | "[python]": { 13 | "editor.formatOnSave": true, 14 | "editor.defaultFormatter": "ms-python.black-formatter" 15 | }, 16 | "python.testing.pytestEnabled": true 17 | } -------------------------------------------------------------------------------- /CREDITS: -------------------------------------------------------------------------------- 1 | To know who contributed to dupeGuru, you can look at the commit log, but not all contributions 2 | result in a commit. This file lists contributors who don't necessarily appear in the commit log. 3 | 4 | * Jason Cho, Exchange icon 5 | * schollidesign (https://findicons.com/pack/1035/human_o2), Zoom-in, Zoom-out, Zoom-best-fit, Zoom-original icons 6 | * Jérôme Cantin, Main icon 7 | * Gregor Tätzner, German localization 8 | * Frank Weber, German localization 9 | * Eric Dee, Chinese localization 10 | * Aleš Nehyba, Czech localization 11 | * Paolo Rossi, Italian localization 12 | * Hrant Ohanyan, Armenian localization 13 | * Igor Pavlov, Russian localization 14 | * Kyrill Detinov, Russian localization 15 | * Yuri Petrashko, Ukrainian localization 16 | * Nickolas Pohilets, Ukrainian localization 17 | * Victor Figueiredo, Brazilian localization 18 | * Phan Anh, Vietnamese localization 19 | * Gabriel Koutilellis, Greek localization 20 | 21 | Thanks! 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include core *.h 2 | recursive-include core *.m 3 | include run.py 4 | graft locale 5 | graft help 6 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | const Configuration = { 2 | /* 3 | * Resolve and load @commitlint/config-conventional from node_modules. 4 | * Referenced packages must be installed 5 | */ 6 | extends: ['@commitlint/config-conventional'], 7 | /* 8 | * Any rules defined here will override rules from @commitlint/config-conventional 9 | */ 10 | rules: { 11 | 'header-max-length': [2, 'always', 72], 12 | 'subject-case': [2, 'always', 'sentence-case'], 13 | 'scope-enum': [2, 'always'], 14 | }, 15 | }; 16 | 17 | module.exports = Configuration; 18 | -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "4.3.1" 2 | __appname__ = "dupeGuru" 3 | -------------------------------------------------------------------------------- /core/gui/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Meta GUI elements in dupeGuru 3 | ----------------------------- 4 | 5 | dupeGuru is designed with a `cross-toolkit`_ approach in mind. It means that its core code 6 | (which doesn't depend on any GUI toolkit) has elements which preformat core information in a way 7 | that makes it easy for a UI layer to consume. 8 | 9 | For example, we have :class:`~core.gui.ResultTable` which takes information from 10 | :class:`~core.results.Results` and mashes it in rows and columns which are ready to be fetched by 11 | either Cocoa's ``NSTableView`` or Qt's ``QTableView``. It tells them which cell is supposed to be 12 | blue, which is supposed to be orange, does the sorting logic, holds selection, etc.. 13 | 14 | .. _cross-toolkit: http://www.hardcoded.net/articles/cross-toolkit-software 15 | """ 16 | -------------------------------------------------------------------------------- /core/gui/base.py: -------------------------------------------------------------------------------- 1 | # Created By: Virgil Dupras 2 | # Created On: 2010-02-06 3 | # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) 4 | # 5 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 6 | # which should be included with this package. The terms are also available at 7 | # http://www.gnu.org/licenses/gpl-3.0.html 8 | 9 | from hscommon.notify import Listener 10 | 11 | 12 | class DupeGuruGUIObject(Listener): 13 | def __init__(self, app): 14 | Listener.__init__(self, app) 15 | self.app = app 16 | 17 | def directories_changed(self): 18 | # Implemented in child classes 19 | pass 20 | 21 | def dupes_selected(self): 22 | # Implemented in child classes 23 | pass 24 | 25 | def marking_changed(self): 26 | # Implemented in child classes 27 | pass 28 | 29 | def results_changed(self): 30 | # Implemented in child classes 31 | pass 32 | 33 | def results_changed_but_keep_selection(self): 34 | # Implemented in child classes 35 | pass 36 | -------------------------------------------------------------------------------- /core/gui/details_panel.py: -------------------------------------------------------------------------------- 1 | # Created By: Virgil Dupras 2 | # Created On: 2010-02-05 3 | # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) 4 | # 5 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 6 | # which should be included with this package. The terms are also available at 7 | # http://www.gnu.org/licenses/gpl-3.0.html 8 | 9 | from hscommon.gui.base import GUIObject 10 | from core.gui.base import DupeGuruGUIObject 11 | 12 | 13 | class DetailsPanel(GUIObject, DupeGuruGUIObject): 14 | def __init__(self, app): 15 | GUIObject.__init__(self, multibind=True) 16 | DupeGuruGUIObject.__init__(self, app) 17 | self._table = [] 18 | 19 | def _view_updated(self): 20 | self._refresh() 21 | self.view.refresh() 22 | 23 | # --- Private 24 | def _refresh(self): 25 | if self.app.selected_dupes: 26 | dupe = self.app.selected_dupes[0] 27 | group = self.app.results.get_group_of_duplicate(dupe) 28 | else: 29 | dupe = None 30 | group = None 31 | data1 = self.app.get_display_info(dupe, group, False) 32 | # we don't want the two sides of the table to display the stats for the same file 33 | ref = group.ref if group is not None and group.ref is not dupe else None 34 | data2 = self.app.get_display_info(ref, group, False) 35 | columns = self.app.result_table.COLUMNS[1:] # first column is the 'marked' column 36 | self._table = [(c.display, data1[c.name], data2[c.name]) for c in columns] 37 | 38 | # --- Public 39 | def row_count(self): 40 | return len(self._table) 41 | 42 | def row(self, row_index): 43 | return self._table[row_index] 44 | 45 | # --- Event Handlers 46 | def dupes_selected(self): 47 | self._view_updated() 48 | -------------------------------------------------------------------------------- /core/gui/ignore_list_dialog.py: -------------------------------------------------------------------------------- 1 | # Created On: 2012/03/13 2 | # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) 3 | # 4 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 5 | # which should be included with this package. The terms are also available at 6 | # http://www.gnu.org/licenses/gpl-3.0.html 7 | 8 | from hscommon.trans import tr 9 | from core.gui.ignore_list_table import IgnoreListTable 10 | 11 | 12 | class IgnoreListDialog: 13 | # --- View interface 14 | # show() 15 | # 16 | 17 | def __init__(self, app): 18 | self.app = app 19 | self.ignore_list = self.app.ignore_list 20 | self.ignore_list_table = IgnoreListTable(self) # GUITable 21 | 22 | def clear(self): 23 | if not self.ignore_list: 24 | return 25 | msg = tr("Do you really want to remove all %d items from the ignore list?") % len(self.ignore_list) 26 | if self.app.view.ask_yes_no(msg): 27 | self.ignore_list.clear() 28 | self.refresh() 29 | 30 | def refresh(self): 31 | self.ignore_list_table.refresh() 32 | 33 | def remove_selected(self): 34 | for row in self.ignore_list_table.selected_rows: 35 | self.ignore_list.remove(row.path1_original, row.path2_original) 36 | self.refresh() 37 | 38 | def show(self): 39 | self.view.show() 40 | -------------------------------------------------------------------------------- /core/gui/ignore_list_table.py: -------------------------------------------------------------------------------- 1 | # Created By: Virgil Dupras 2 | # Created On: 2012-03-13 3 | # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) 4 | # 5 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 6 | # which should be included with this package. The terms are also available at 7 | # http://www.gnu.org/licenses/gpl-3.0.html 8 | 9 | from hscommon.gui.table import GUITable, Row 10 | from hscommon.gui.column import Column, Columns 11 | from hscommon.trans import trget 12 | 13 | coltr = trget("columns") 14 | 15 | 16 | class IgnoreListTable(GUITable): 17 | COLUMNS = [ 18 | # the str concat below saves us needless localization. 19 | Column("path1", coltr("File Path") + " 1"), 20 | Column("path2", coltr("File Path") + " 2"), 21 | ] 22 | 23 | def __init__(self, ignore_list_dialog): 24 | GUITable.__init__(self) 25 | self._columns = Columns(self) 26 | self.view = None 27 | self.dialog = ignore_list_dialog 28 | 29 | # --- Override 30 | def _fill(self): 31 | for path1, path2 in self.dialog.ignore_list: 32 | self.append(IgnoreListRow(self, path1, path2)) 33 | 34 | 35 | class IgnoreListRow(Row): 36 | def __init__(self, table, path1, path2): 37 | Row.__init__(self, table) 38 | self.path1_original = path1 39 | self.path2_original = path2 40 | self.path1 = str(path1) 41 | self.path2 = str(path2) 42 | -------------------------------------------------------------------------------- /core/gui/prioritize_dialog.py: -------------------------------------------------------------------------------- 1 | # Created By: Virgil Dupras 2 | # Created On: 2011-09-06 3 | # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) 4 | # 5 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 6 | # which should be included with this package. The terms are also available at 7 | # http://www.gnu.org/licenses/gpl-3.0.html 8 | 9 | from hscommon.gui.base import GUIObject 10 | from hscommon.gui.selectable_list import GUISelectableList 11 | 12 | 13 | class CriterionCategoryList(GUISelectableList): 14 | def __init__(self, dialog): 15 | self.dialog = dialog 16 | GUISelectableList.__init__(self, [c.NAME for c in dialog.categories]) 17 | 18 | def _update_selection(self): 19 | self.dialog.select_category(self.dialog.categories[self.selected_index]) 20 | GUISelectableList._update_selection(self) 21 | 22 | 23 | class PrioritizationList(GUISelectableList): 24 | def __init__(self, dialog): 25 | self.dialog = dialog 26 | GUISelectableList.__init__(self) 27 | 28 | def _refresh_contents(self): 29 | self[:] = [crit.display for crit in self.dialog.prioritizations] 30 | 31 | def move_indexes(self, indexes, dest_index): 32 | indexes.sort() 33 | prilist = self.dialog.prioritizations 34 | selected = [prilist[i] for i in indexes] 35 | for i in reversed(indexes): 36 | del prilist[i] 37 | prilist[dest_index:dest_index] = selected 38 | self._refresh_contents() 39 | 40 | def remove_selected(self): 41 | prilist = self.dialog.prioritizations 42 | for i in sorted(self.selected_indexes, reverse=True): 43 | del prilist[i] 44 | self._refresh_contents() 45 | 46 | 47 | class PrioritizeDialog(GUIObject): 48 | def __init__(self, app): 49 | GUIObject.__init__(self) 50 | self.app = app 51 | self.categories = [cat(app.results) for cat in app._prioritization_categories()] 52 | self.category_list = CriterionCategoryList(self) 53 | self.criteria = [] 54 | self.criteria_list = GUISelectableList() 55 | self.prioritizations = [] 56 | self.prioritization_list = PrioritizationList(self) 57 | 58 | # --- Override 59 | def _view_updated(self): 60 | self.category_list.select(0) 61 | 62 | # --- Private 63 | def _sort_key(self, dupe): 64 | return tuple(crit.sort_key(dupe) for crit in self.prioritizations) 65 | 66 | # --- Public 67 | def select_category(self, category): 68 | self.criteria = category.criteria_list() 69 | self.criteria_list[:] = [c.display_value for c in self.criteria] 70 | 71 | def add_selected(self): 72 | # Add selected criteria in criteria_list to prioritization_list. 73 | if self.criteria_list.selected_index is None: 74 | return 75 | for i in self.criteria_list.selected_indexes: 76 | crit = self.criteria[i] 77 | self.prioritizations.append(crit) 78 | del crit 79 | self.prioritization_list[:] = [crit.display for crit in self.prioritizations] 80 | 81 | def remove_selected(self): 82 | self.prioritization_list.remove_selected() 83 | self.prioritization_list.select([]) 84 | 85 | def perform_reprioritization(self): 86 | self.app.reprioritize_groups(self._sort_key) 87 | -------------------------------------------------------------------------------- /core/gui/problem_dialog.py: -------------------------------------------------------------------------------- 1 | # Created By: Virgil Dupras 2 | # Created On: 2010-04-12 3 | # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) 4 | # 5 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 6 | # which should be included with this package. The terms are also available at 7 | # http://www.gnu.org/licenses/gpl-3.0.html 8 | 9 | from hscommon import desktop 10 | 11 | from core.gui.problem_table import ProblemTable 12 | 13 | 14 | class ProblemDialog: 15 | def __init__(self, app): 16 | self.app = app 17 | self._selected_dupe = None 18 | self.problem_table = ProblemTable(self) 19 | 20 | def refresh(self): 21 | self._selected_dupe = None 22 | self.problem_table.refresh() 23 | 24 | def reveal_selected_dupe(self): 25 | if self._selected_dupe is not None: 26 | desktop.reveal_path(self._selected_dupe.path) 27 | 28 | def select_dupe(self, dupe): 29 | self._selected_dupe = dupe 30 | -------------------------------------------------------------------------------- /core/gui/problem_table.py: -------------------------------------------------------------------------------- 1 | # Created By: Virgil Dupras 2 | # Created On: 2010-04-12 3 | # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) 4 | # 5 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 6 | # which should be included with this package. The terms are also available at 7 | # http://www.gnu.org/licenses/gpl-3.0.html 8 | 9 | from hscommon.gui.table import GUITable, Row 10 | from hscommon.gui.column import Column, Columns 11 | from hscommon.trans import trget 12 | 13 | coltr = trget("columns") 14 | 15 | 16 | class ProblemTable(GUITable): 17 | COLUMNS = [ 18 | Column("path", coltr("File Path")), 19 | Column("msg", coltr("Error Message")), 20 | ] 21 | 22 | def __init__(self, problem_dialog): 23 | GUITable.__init__(self) 24 | self._columns = Columns(self) 25 | self.dialog = problem_dialog 26 | 27 | # --- Override 28 | def _update_selection(self): 29 | row = self.selected_row 30 | dupe = row.dupe if row is not None else None 31 | self.dialog.select_dupe(dupe) 32 | 33 | def _fill(self): 34 | problems = self.dialog.app.results.problems 35 | for dupe, msg in problems: 36 | self.append(ProblemRow(self, dupe, msg)) 37 | 38 | 39 | class ProblemRow(Row): 40 | def __init__(self, table, dupe, msg): 41 | Row.__init__(self, table) 42 | self.dupe = dupe 43 | self.msg = msg 44 | self.path = str(dupe.path) 45 | -------------------------------------------------------------------------------- /core/gui/stats_label.py: -------------------------------------------------------------------------------- 1 | # Created By: Virgil Dupras 2 | # Created On: 2010-02-11 3 | # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) 4 | # 5 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 6 | # which should be included with this package. The terms are also available at 7 | # http://www.gnu.org/licenses/gpl-3.0.html 8 | 9 | from core.gui.base import DupeGuruGUIObject 10 | 11 | 12 | class StatsLabel(DupeGuruGUIObject): 13 | def _view_updated(self): 14 | self.view.refresh() 15 | 16 | @property 17 | def display(self): 18 | return self.app.stat_line 19 | 20 | def results_changed(self): 21 | self.view.refresh() 22 | 23 | marking_changed = results_changed 24 | -------------------------------------------------------------------------------- /core/me/__init__.py: -------------------------------------------------------------------------------- 1 | from core.me import fs, prioritize, result_table, scanner # noqa 2 | -------------------------------------------------------------------------------- /core/me/prioritize.py: -------------------------------------------------------------------------------- 1 | # Created On: 2011/09/16 2 | # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) 3 | # 4 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 5 | # which should be included with this package. The terms are also available at 6 | # http://www.gnu.org/licenses/gpl-3.0.html 7 | 8 | from hscommon.trans import trget 9 | 10 | from core.prioritize import ( 11 | KindCategory, 12 | FolderCategory, 13 | FilenameCategory, 14 | NumericalCategory, 15 | SizeCategory, 16 | MtimeCategory, 17 | ) 18 | 19 | coltr = trget("columns") 20 | 21 | 22 | class DurationCategory(NumericalCategory): 23 | NAME = coltr("Duration") 24 | 25 | def extract_value(self, dupe): 26 | return dupe.duration 27 | 28 | 29 | class BitrateCategory(NumericalCategory): 30 | NAME = coltr("Bitrate") 31 | 32 | def extract_value(self, dupe): 33 | return dupe.bitrate 34 | 35 | 36 | class SamplerateCategory(NumericalCategory): 37 | NAME = coltr("Samplerate") 38 | 39 | def extract_value(self, dupe): 40 | return dupe.samplerate 41 | 42 | 43 | def all_categories(): 44 | return [ 45 | KindCategory, 46 | FolderCategory, 47 | FilenameCategory, 48 | SizeCategory, 49 | DurationCategory, 50 | BitrateCategory, 51 | SamplerateCategory, 52 | MtimeCategory, 53 | ] 54 | -------------------------------------------------------------------------------- /core/me/result_table.py: -------------------------------------------------------------------------------- 1 | # Created On: 2011-11-27 2 | # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) 3 | # 4 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 5 | # which should be included with this package. The terms are also available at 6 | # http://www.gnu.org/licenses/gpl-3.0.html 7 | 8 | from hscommon.gui.column import Column 9 | from hscommon.trans import trget 10 | 11 | from core.gui.result_table import ResultTable as ResultTableBase 12 | 13 | coltr = trget("columns") 14 | 15 | 16 | class ResultTable(ResultTableBase): 17 | COLUMNS = [ 18 | Column("marked", ""), 19 | Column("name", coltr("Filename")), 20 | Column("folder_path", coltr("Folder"), visible=False, optional=True), 21 | Column("size", coltr("Size (MB)"), optional=True), 22 | Column("duration", coltr("Time"), optional=True), 23 | Column("bitrate", coltr("Bitrate"), optional=True), 24 | Column("samplerate", coltr("Sample Rate"), visible=False, optional=True), 25 | Column("extension", coltr("Kind"), optional=True), 26 | Column("mtime", coltr("Modification"), visible=False, optional=True), 27 | Column("title", coltr("Title"), visible=False, optional=True), 28 | Column("artist", coltr("Artist"), visible=False, optional=True), 29 | Column("album", coltr("Album"), visible=False, optional=True), 30 | Column("genre", coltr("Genre"), visible=False, optional=True), 31 | Column("year", coltr("Year"), visible=False, optional=True), 32 | Column("track", coltr("Track Number"), visible=False, optional=True), 33 | Column("comment", coltr("Comment"), visible=False, optional=True), 34 | Column("percentage", coltr("Match %"), optional=True), 35 | Column("words", coltr("Words Used"), visible=False, optional=True), 36 | Column("dupe_count", coltr("Dupe Count"), visible=False, optional=True), 37 | ] 38 | DELTA_COLUMNS = {"size", "duration", "bitrate", "samplerate", "mtime"} 39 | -------------------------------------------------------------------------------- /core/me/scanner.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Hardcoded Software (http://www.hardcoded.net) 2 | # 3 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 4 | # which should be included with this package. The terms are also available at 5 | # http://www.gnu.org/licenses/gpl-3.0.html 6 | 7 | from hscommon.trans import tr 8 | 9 | from core.scanner import Scanner as ScannerBase, ScanOption, ScanType 10 | 11 | 12 | class ScannerME(ScannerBase): 13 | @staticmethod 14 | def _key_func(dupe): 15 | return (-dupe.bitrate, -dupe.size) 16 | 17 | @staticmethod 18 | def get_scan_options(): 19 | return [ 20 | ScanOption(ScanType.FILENAME, tr("Filename")), 21 | ScanOption(ScanType.FIELDS, tr("Filename - Fields")), 22 | ScanOption(ScanType.FIELDSNOORDER, tr("Filename - Fields (No Order)")), 23 | ScanOption(ScanType.TAG, tr("Tags")), 24 | ScanOption(ScanType.CONTENTS, tr("Contents")), 25 | ] 26 | -------------------------------------------------------------------------------- /core/pe/__init__.py: -------------------------------------------------------------------------------- 1 | from core.pe import ( # noqa 2 | block, 3 | cache, 4 | exif, 5 | matchblock, 6 | matchexif, 7 | photo, 8 | prioritize, 9 | result_table, 10 | scanner, 11 | ) 12 | -------------------------------------------------------------------------------- /core/pe/block.pyi: -------------------------------------------------------------------------------- 1 | from typing import Tuple, List, Union, Sequence 2 | 3 | _block = Tuple[int, int, int] 4 | 5 | class NoBlocksError(Exception): ... # noqa: E302, E701 6 | class DifferentBlockCountError(Exception): ... # noqa E701 7 | 8 | def getblock(image: object) -> Union[_block, None]: ... # noqa: E302 9 | def getblocks2(image: object, block_count_per_side: int) -> Union[List[_block], None]: ... 10 | def diff(first: _block, second: _block) -> int: ... 11 | def avgdiff( # noqa: E302 12 | first: Sequence[_block], second: Sequence[_block], limit: int = 768, min_iterations: int = 1 13 | ) -> Union[int, None]: ... 14 | -------------------------------------------------------------------------------- /core/pe/cache.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Virgil Dupras 2 | # 3 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 4 | # which should be included with this package. The terms are also available at 5 | # http://www.gnu.org/licenses/gpl-3.0.html 6 | 7 | from core.pe._cache import bytes_to_colors # noqa 8 | 9 | 10 | def colors_to_bytes(colors): 11 | """Transform the 3 sized tuples 'colors' into a bytes string. 12 | 13 | [(0,100,255)] --> b'\x00d\xff' 14 | [(1,2,3),(4,5,6)] --> b'\x01\x02\x03\x04\x05\x06' 15 | """ 16 | return b"".join(map(bytes, colors)) 17 | -------------------------------------------------------------------------------- /core/pe/cache.pyi: -------------------------------------------------------------------------------- 1 | from typing import Union, Tuple, List 2 | 3 | _block = Tuple[int, int, int] 4 | 5 | def colors_to_bytes(colors: List[_block]) -> bytes: ... # noqa: E302 6 | def bytes_to_colors(s: bytes) -> Union[List[_block], None]: ... 7 | -------------------------------------------------------------------------------- /core/pe/matchexif.py: -------------------------------------------------------------------------------- 1 | # Created By: Virgil Dupras 2 | # Created On: 2011-04-20 3 | # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) 4 | # 5 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 6 | # which should be included with this package. The terms are also available at 7 | # http://www.gnu.org/licenses/gpl-3.0.html 8 | 9 | from collections import defaultdict 10 | from itertools import combinations 11 | 12 | from hscommon.trans import tr 13 | 14 | from core.engine import Match 15 | 16 | 17 | def getmatches(files, match_scaled, j): 18 | timestamp2pic = defaultdict(set) 19 | for picture in j.iter_with_progress(files, tr("Read EXIF of %d/%d pictures")): 20 | timestamp = picture.exif_timestamp 21 | if timestamp: 22 | timestamp2pic[timestamp].add(picture) 23 | if "0000:00:00 00:00:00" in timestamp2pic: # very likely false matches 24 | del timestamp2pic["0000:00:00 00:00:00"] 25 | matches = [] 26 | for pictures in timestamp2pic.values(): 27 | for p1, p2 in combinations(pictures, 2): 28 | if (not match_scaled) and (p1.dimensions != p2.dimensions): 29 | continue 30 | matches.append(Match(p1, p2, 100)) 31 | return matches 32 | -------------------------------------------------------------------------------- /core/pe/modules/cache.c: -------------------------------------------------------------------------------- 1 | /* Created By: Virgil Dupras 2 | * Created On: 2010-01-30 3 | * Copyright 2014 Hardcoded Software (http://www.hardcoded.net) 4 | * 5 | * This software is licensed under the "BSD" License as described in the 6 | * "LICENSE" file, which should be included with this package. The terms are 7 | * also available at http://www.hardcoded.net/licenses/bsd_license 8 | */ 9 | 10 | #include "common.h" 11 | 12 | static PyObject *cache_bytes_to_colors(PyObject *self, PyObject *args) { 13 | char *y; 14 | Py_ssize_t char_count, i, color_count; 15 | PyObject *result; 16 | unsigned long r, g, b; 17 | Py_ssize_t ci; 18 | PyObject *color_tuple; 19 | 20 | if (!PyArg_ParseTuple(args, "y#", &y, &char_count)) { 21 | return NULL; 22 | } 23 | 24 | color_count = char_count / 3; 25 | result = PyList_New(color_count); 26 | if (result == NULL) { 27 | return NULL; 28 | } 29 | 30 | for (i = 0; i < color_count; i++) { 31 | ci = i * 3; 32 | r = (unsigned char)y[ci]; 33 | g = (unsigned char)y[ci + 1]; 34 | b = (unsigned char)y[ci + 2]; 35 | 36 | color_tuple = inttuple(3, r, g, b); 37 | if (color_tuple == NULL) { 38 | Py_DECREF(result); 39 | return NULL; 40 | } 41 | PyList_SET_ITEM(result, i, color_tuple); 42 | } 43 | 44 | return result; 45 | } 46 | 47 | static PyMethodDef CacheMethods[] = { 48 | {"bytes_to_colors", cache_bytes_to_colors, METH_VARARGS, 49 | "Transform the bytes 's' into a list of 3 sized tuples."}, 50 | {NULL, NULL, 0, NULL} /* Sentinel */ 51 | }; 52 | 53 | static struct PyModuleDef CacheDef = {PyModuleDef_HEAD_INIT, 54 | "_cache", 55 | NULL, 56 | -1, 57 | CacheMethods, 58 | NULL, 59 | NULL, 60 | NULL, 61 | NULL}; 62 | 63 | PyObject *PyInit__cache(void) { 64 | PyObject *m = PyModule_Create(&CacheDef); 65 | if (m == NULL) { 66 | return NULL; 67 | } 68 | return m; 69 | } 70 | -------------------------------------------------------------------------------- /core/pe/modules/common.c: -------------------------------------------------------------------------------- 1 | /* Created By: Virgil Dupras 2 | * Created On: 2010-02-04 3 | * Copyright 2014 Hardcoded Software (http://www.hardcoded.net) 4 | * 5 | * This software is licensed under the "BSD" License as described in the "LICENSE" file, 6 | * which should be included with this package. The terms are also available at 7 | * http://www.hardcoded.net/licenses/bsd_license 8 | */ 9 | 10 | #include "common.h" 11 | 12 | #ifndef _MSC_VER 13 | int max(int a, int b) 14 | { 15 | return b > a ? b : a; 16 | } 17 | 18 | int min(int a, int b) 19 | { 20 | return b < a ? b : a; 21 | } 22 | #endif 23 | 24 | PyObject* inttuple(int n, ...) 25 | { 26 | int i; 27 | PyObject *pnumber; 28 | PyObject *result; 29 | va_list numbers; 30 | 31 | va_start(numbers, n); 32 | result = PyTuple_New(n); 33 | 34 | for (i=0; i`__ und `Französisch `__ verfügbar. 7 | 8 | .. only:: edition_se or edition_me 9 | 10 | 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. 11 | 12 | .. only:: edition_pe 13 | 14 | dupeGuru Picture Edition (kurz PE) ist ein Tool zum Auffinden von doppelten Bildern auf Ihrem Computer. Es findet nicht nur exakte Übereinstimmungen, sondern auch Duplikate unterschiedlichen Dateityps (PNG, JPG, GIF etc..) und Qualität. 15 | 16 | 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 ` Sektion 17 | 18 | Es ist eine gute Idee dupeGuru aktuell zu halten. Sie können die neueste Version auf der http://dupeguru.voltaicideas.net finden. 19 | 20 | Inhalte: 21 | 22 | .. toctree:: 23 | :maxdepth: 2 24 | 25 | quick_start 26 | folders 27 | preferences 28 | results 29 | reprioritize 30 | faq 31 | changelog 32 | -------------------------------------------------------------------------------- /help/de/quick_start.rst: -------------------------------------------------------------------------------- 1 | Schnellstart 2 | ============ 3 | 4 | Damit Sie sich schnell mit dupeGuru zurechtfinden, machen wir für den Anfang einen Standardscan mit den Voreinstellungen. 5 | 6 | * dupeGuru starten. 7 | * Zu scannende Ordner entweder mit drag & drop oder dem "+" Knopf auswählen. 8 | * Drücken Sie auf **Scan**. 9 | * Warten Sie bis der Scanvorgang fertig ist. 10 | * Betrachten Sie jedes Duplikat (die eingerückten Dateien) und überprüfen ob es wirklich ein Duplikat der Referenzdatei ist (die obere nicht eingerückte Datei ohne Markierungsfeld). 11 | * Wenn eine Datei kein Duplikat ist, wählen Sie es aus und drücken auf **Aktionen-->Entferne Ausgewählte aus den Ergebnissen**. 12 | * Erst wenn Sie sicher sind, das keine Falsch-Duplikate mehr in den Ergebnissen sind, drücken Sie auf **Bearbeiten-->Alle markieren**, und dann **Aktionen-->Verschiebe Markierte in den Mülleimer**. 13 | 14 | Das war nur ein einfacher Scan. Es gibt viele Optionen mit denen der Suchdurchlauf beeinflusst werden und einige Methoden zur Begutachtung und Veränderung der Ergebnisliste. Um mehr über sie zu erfahren, lesen Sie die restlichen Hilfedateien. 15 | -------------------------------------------------------------------------------- /help/de/reprioritize.rst: -------------------------------------------------------------------------------- 1 | Re-Prioritizing duplicates 2 | ========================== 3 | 4 | dupeGuru tries to automatically determine which duplicate should go in each group's reference 5 | position, but sometimes it gets it wrong. In many cases, clever dupe sorting with "Delta Values" 6 | and "Dupes Only" options in addition to the "Make Selected into Reference" action does the trick, but 7 | sometimes, a more powerful option is needed. This is where the Re-Prioritization dialog comes into 8 | play. You can summon it through the "Re-Prioritize Results" item in the "Actions" menu. 9 | 10 | This dialog allows you to select criteria according to which a reference dupe will be selected in 11 | each dupe group. The list of available criteria is on the left and the list of criteria you've 12 | selected is on the right. 13 | 14 | A criteria is a category followed by an argument. For example, "Size (Highest)" means that the dupe 15 | with the biggest size will win. "Folder (/foo/bar)" means that dupes in this folder will win. To add 16 | a criterion to the rightmost list, first select a category in the combobox, then select a 17 | subargument in the list below, and then click on the right pointing arrow button. 18 | 19 | The order of the list on the right is important (you can re-order items through drag & drop). When 20 | picking a dupe for reference position, the first criterion is used. If there's a tie, the second 21 | criterion is used and so on and so on. For example, if your arguments are "Size (Highest)" and then 22 | "Filename (Doesn't end with a number)", the reference file that will be picked in a group will be 23 | the biggest file, and if two or more files have the same size, the one that has a filename that 24 | doesn't end with a number will be used. When all criteria result in ties, the order in which dupes 25 | previously were in the group will be used. 26 | -------------------------------------------------------------------------------- /help/en/developer/core/app.rst: -------------------------------------------------------------------------------- 1 | core.app 2 | ======== 3 | 4 | .. automodule:: core.app 5 | :members: 6 | -------------------------------------------------------------------------------- /help/en/developer/core/directories.rst: -------------------------------------------------------------------------------- 1 | core.directories 2 | ================ 3 | 4 | .. automodule:: core.directories 5 | :members: 6 | -------------------------------------------------------------------------------- /help/en/developer/core/engine.rst: -------------------------------------------------------------------------------- 1 | core.engine 2 | =========== 3 | 4 | .. automodule:: core.engine 5 | 6 | .. autoclass:: Match 7 | 8 | .. autoclass:: Group 9 | :members: 10 | 11 | .. autofunction:: build_word_dict 12 | .. autofunction:: compare 13 | .. autofunction:: compare_fields 14 | .. autofunction:: getmatches 15 | .. autofunction:: getmatches_by_contents 16 | .. autofunction:: get_groups 17 | .. autofunction:: merge_similar_words 18 | .. autofunction:: reduce_common_words 19 | 20 | .. _fields: 21 | 22 | Fields 23 | ------ 24 | 25 | Fields are groups of words which each represent a significant part of the whole name. This concept 26 | is sifnificant in music file names, where we often have names like "My Artist - a very long title 27 | with many many words". 28 | 29 | This title has 10 words. If you run as scan with a bit of tolerance, let's say 90%, you'll be able 30 | to find a dupe that has only one "many" in the song title. However, you would also get false 31 | duplicates from a title like "My Giraffe - a very long title with many many words", which is of 32 | course a very different song and it doesn't make sense to match them. 33 | 34 | When matching by fields, each field (separated by "-") is considered as a separate string to match 35 | independently. After all fields are matched, the lowest result is kept. In the "Giraffe" example we 36 | gave, the result would be 50% instead of 90% in normal mode. 37 | -------------------------------------------------------------------------------- /help/en/developer/core/fs.rst: -------------------------------------------------------------------------------- 1 | core.fs 2 | ======= 3 | 4 | .. automodule:: core.fs 5 | :members: 6 | -------------------------------------------------------------------------------- /help/en/developer/core/gui/deletion_options.rst: -------------------------------------------------------------------------------- 1 | core.gui.deletion_options 2 | ========================= 3 | 4 | .. automodule:: core.gui.deletion_options 5 | :members: 6 | -------------------------------------------------------------------------------- /help/en/developer/core/gui/index.rst: -------------------------------------------------------------------------------- 1 | core.gui 2 | ======== 3 | 4 | .. automodule:: core.gui 5 | :members: 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | 10 | deletion_options 11 | -------------------------------------------------------------------------------- /help/en/developer/core/index.rst: -------------------------------------------------------------------------------- 1 | core 2 | ==== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | app 8 | fs 9 | engine 10 | directories 11 | results 12 | gui/index 13 | -------------------------------------------------------------------------------- /help/en/developer/core/results.rst: -------------------------------------------------------------------------------- 1 | core.results 2 | ============ 3 | 4 | .. automodule:: core.results 5 | :members: 6 | -------------------------------------------------------------------------------- /help/en/developer/hscommon/build.rst: -------------------------------------------------------------------------------- 1 | hscommon.build 2 | ============== 3 | 4 | .. automodule:: hscommon.build 5 | :members: 6 | -------------------------------------------------------------------------------- /help/en/developer/hscommon/conflict.rst: -------------------------------------------------------------------------------- 1 | hscommon.conflict 2 | ================= 3 | 4 | .. automodule:: hscommon.conflict 5 | :members: 6 | -------------------------------------------------------------------------------- /help/en/developer/hscommon/desktop.rst: -------------------------------------------------------------------------------- 1 | hscommon.desktop 2 | ================ 3 | 4 | .. automodule:: hscommon.desktop 5 | :members: 6 | -------------------------------------------------------------------------------- /help/en/developer/hscommon/gui/base.rst: -------------------------------------------------------------------------------- 1 | hscommon.gui.base 2 | ================= 3 | 4 | .. automodule:: hscommon.gui.base 5 | 6 | .. autosummary:: 7 | 8 | GUIObject 9 | 10 | .. autoclass:: GUIObject 11 | :members: 12 | :private-members: 13 | -------------------------------------------------------------------------------- /help/en/developer/hscommon/gui/column.rst: -------------------------------------------------------------------------------- 1 | hscommon.gui.column 2 | ============================ 3 | 4 | .. automodule:: hscommon.gui.column 5 | 6 | .. autosummary:: 7 | 8 | Columns 9 | Column 10 | ColumnsView 11 | PrefAccessInterface 12 | 13 | .. autoclass:: Columns 14 | :members: 15 | :private-members: 16 | 17 | .. autoclass:: Column 18 | :members: 19 | :private-members: 20 | 21 | .. autoclass:: ColumnsView 22 | :members: 23 | 24 | .. autoclass:: PrefAccessInterface 25 | :members: 26 | -------------------------------------------------------------------------------- /help/en/developer/hscommon/gui/progress_window.rst: -------------------------------------------------------------------------------- 1 | hscommon.gui.progress_window 2 | ============================ 3 | 4 | .. automodule:: hscommon.gui.progress_window 5 | 6 | .. autosummary:: 7 | 8 | ProgressWindow 9 | ProgressWindowView 10 | 11 | .. autoclass:: ProgressWindow 12 | :members: 13 | :private-members: 14 | 15 | .. autoclass:: ProgressWindowView 16 | :members: 17 | :private-members: 18 | -------------------------------------------------------------------------------- /help/en/developer/hscommon/gui/selectable_list.rst: -------------------------------------------------------------------------------- 1 | hscommon.gui.selectable_list 2 | ============================ 3 | 4 | .. automodule:: hscommon.gui.selectable_list 5 | 6 | .. autosummary:: 7 | 8 | Selectable 9 | SelectableList 10 | GUISelectableList 11 | GUISelectableListView 12 | 13 | .. autoclass:: Selectable 14 | :members: 15 | :private-members: 16 | 17 | .. autoclass:: SelectableList 18 | :members: 19 | :private-members: 20 | 21 | .. autoclass:: GUISelectableList 22 | :members: 23 | :private-members: 24 | 25 | .. autoclass:: GUISelectableListView 26 | :members: 27 | -------------------------------------------------------------------------------- /help/en/developer/hscommon/gui/table.rst: -------------------------------------------------------------------------------- 1 | hscommon.gui.table 2 | ================== 3 | 4 | .. automodule:: hscommon.gui.table 5 | 6 | .. autosummary:: 7 | 8 | Table 9 | Row 10 | GUITable 11 | GUITableView 12 | 13 | .. autoclass:: Table 14 | :members: 15 | :private-members: 16 | 17 | .. autoclass:: Row 18 | :members: 19 | :private-members: 20 | 21 | .. autoclass:: GUITable 22 | :members: 23 | :private-members: 24 | 25 | .. autoclass:: GUITableView 26 | :members: 27 | -------------------------------------------------------------------------------- /help/en/developer/hscommon/gui/text_field.rst: -------------------------------------------------------------------------------- 1 | hscommon.gui.text_field 2 | ======================= 3 | 4 | .. automodule:: hscommon.gui.text_field 5 | 6 | .. autosummary:: 7 | 8 | TextField 9 | TextFieldView 10 | 11 | .. autoclass:: TextField 12 | :members: 13 | :private-members: 14 | 15 | .. autoclass:: TextFieldView 16 | :members: 17 | -------------------------------------------------------------------------------- /help/en/developer/hscommon/gui/tree.rst: -------------------------------------------------------------------------------- 1 | hscommon.gui.tree 2 | ================= 3 | 4 | .. automodule:: hscommon.gui.tree 5 | 6 | .. autosummary:: 7 | 8 | Tree 9 | Node 10 | 11 | .. autoclass:: Tree 12 | :members: 13 | :private-members: 14 | 15 | .. autoclass:: Node 16 | :members: 17 | :private-members: 18 | -------------------------------------------------------------------------------- /help/en/developer/hscommon/index.rst: -------------------------------------------------------------------------------- 1 | hscommon 2 | ======== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :glob: 7 | 8 | build 9 | conflict 10 | desktop 11 | notify 12 | path 13 | util 14 | jobprogress/* 15 | gui/* 16 | -------------------------------------------------------------------------------- /help/en/developer/hscommon/jobprogress/job.rst: -------------------------------------------------------------------------------- 1 | hscommon.jobprogress.job 2 | ======================== 3 | 4 | .. automodule:: hscommon.jobprogress.job 5 | 6 | .. autosummary:: 7 | 8 | Job 9 | NullJob 10 | 11 | .. autoclass:: Job 12 | :members: 13 | :private-members: 14 | 15 | .. autoclass:: NullJob 16 | :members: 17 | -------------------------------------------------------------------------------- /help/en/developer/hscommon/jobprogress/performer.rst: -------------------------------------------------------------------------------- 1 | hscommon.jobprogress.performer 2 | ============================== 3 | 4 | .. automodule:: hscommon.jobprogress.performer 5 | 6 | .. autosummary:: 7 | 8 | ThreadedJobPerformer 9 | 10 | .. autoclass:: ThreadedJobPerformer 11 | :members: 12 | -------------------------------------------------------------------------------- /help/en/developer/hscommon/notify.rst: -------------------------------------------------------------------------------- 1 | hscommon.notify 2 | =============== 3 | 4 | .. automodule:: hscommon.notify 5 | :members: 6 | -------------------------------------------------------------------------------- /help/en/developer/hscommon/path.rst: -------------------------------------------------------------------------------- 1 | hscommon.path 2 | ============= 3 | 4 | .. automodule:: hscommon.path 5 | :members: 6 | -------------------------------------------------------------------------------- /help/en/developer/hscommon/util.rst: -------------------------------------------------------------------------------- 1 | hscommon.util 2 | ============= 3 | 4 | .. automodule:: hscommon.util 5 | :members: 6 | -------------------------------------------------------------------------------- /help/en/folders.rst: -------------------------------------------------------------------------------- 1 | Folder Selection 2 | ================ 3 | 4 | The first window you see when you launch dupeGuru is the folder selection window. This windows 5 | contains the basic input dupeGuru needs to start a scan: 6 | 7 | * An Application Mode selection 8 | * A Scan Type selection 9 | * Folders to scan 10 | 11 | Application Mode 12 | ---------------- 13 | 14 | dupeGuru had three main modes: Standard, Music and Picture. 15 | 16 | Standard is for any type of files. This makes this mode the most polyvalent, but it lacks 17 | specialized features other modes have. 18 | 19 | Music mode scans only music files, but it supports tags comparison and its results window has many 20 | audio-related informational columns. 21 | 22 | Picture mode scans only pictures, but its contents scan type is a powerful fuzzy matcher that can 23 | find pictures that are similar without being exactly the same. 24 | 25 | Choosing an application mode not only changes available scan types in the selector below, but also 26 | changes available options in the preferences panel. Thus, if you want to fine tune your scan, be 27 | sure to open the preferences panel **after** you've selected the application mode. 28 | 29 | Scan Type 30 | --------- 31 | 32 | This selector determines the type of the scan we'll do. See :doc:`scan` for details about scan 33 | types. 34 | 35 | Folder List 36 | ----------- 37 | 38 | To add a folder, click on the **+** button. If you added folder before, a popup 39 | menu with a list of recent folders you added will pop. You can click on one of 40 | them to add it directly to your list. If you click on the first item of the 41 | popup menu, **Add New Folder...**, you will be prompted for a folder to add. If 42 | you never added a folder, no menu will pop and you will directly be prompted 43 | for a new folder to add. 44 | 45 | An alternate way to add folders to the list is to drag them in the list. 46 | 47 | To remove a folder, select the folder to remove and click on **-**. If a subfolder is selected when 48 | you click the button, the selected folder will be set to **excluded** state (see below) instead of 49 | being removed. 50 | 51 | Folder states 52 | ------------- 53 | 54 | Every folder can be in one of these 3 states: 55 | 56 | **Normal:** 57 | Duplicates found in this folder can be deleted. 58 | **Reference:** 59 | Duplicates found in this folder **cannot** be deleted. Files from this folder can 60 | only end up in **reference** position in the dupe group. If more than one file from reference 61 | folders end up in the same dupe group, only one will be kept. The others will be removed from 62 | the group. 63 | **Excluded:** 64 | Files in this directory will not be included in the scan. 65 | 66 | The default state of a folder is, of course, **Normal**. You can use **Reference** state for a 67 | folder if you want to be sure that you won't delete any file from it. 68 | 69 | When you set the state of a directory, all subfolders of this folder automatically inherit this 70 | state unless you explicitly set a subfolder's state. 71 | 72 | Scan 73 | ---- 74 | 75 | When you're ready, click on the **Scan** button to initiate the scanning process. When it's done, 76 | you'll be shown the :doc:`results`. 77 | -------------------------------------------------------------------------------- /help/en/index.rst: -------------------------------------------------------------------------------- 1 | dupeGuru help 2 | ============= 3 | 4 | This help document is also available in these languages: 5 | 6 | * `French `__ 7 | * `German `__ 8 | * `Armenian `__ 9 | * `Russian `__ 10 | * `Ukrainian `__ 11 | 12 | dupeGuru is a tool to find duplicate files on your computer. It has three 13 | modes, Standard, Music and Picture, with each mode having its own scan types 14 | and little features. 15 | 16 | Although dupeGuru can easily be used without documentation, reading this file 17 | will help you to master it. If you are looking for guidance for your first 18 | duplicate scan, you can take a look at the :doc:`Quick Start ` 19 | section. 20 | 21 | It is a good idea to keep dupeGuru updated. You can download the latest version on its `homepage`_. 22 | 23 | Contents: 24 | 25 | .. toctree:: 26 | :maxdepth: 2 27 | 28 | contribute 29 | quick_start 30 | folders 31 | preferences 32 | scan 33 | results 34 | reprioritize 35 | faq 36 | developer/index 37 | changelog 38 | 39 | Indices and tables 40 | ================== 41 | 42 | * :ref:`genindex` 43 | * :ref:`search` 44 | 45 | .. _homepage: https://dupeguru.voltaicideas.net/ 46 | -------------------------------------------------------------------------------- /help/en/quick_start.rst: -------------------------------------------------------------------------------- 1 | Quick Start 2 | =========== 3 | 4 | To get you quickly started with dupeGuru, let's just make a standard scan using default preferences. 5 | 6 | * Launch dupeGuru. 7 | * Add folders to scan with either drag & drop or the "+" button. 8 | * Click on **Scan**. 9 | * Wait until the scan process is over. 10 | * Look at every duplicate (The files that are indented) and verify that it is indeed a duplicate to the group's reference (The file above the duplicate that is not indented and have a disabled mark box). 11 | * If a file is a false duplicate, select it and click on **Actions-->Remove Selected from Results**. 12 | * Once you are sure that there is no false duplicate in your results, click on **Edit-->Mark All**, and then **Actions-->Send Marked to Recycle bin**. 13 | 14 | That is only a basic scan. There are a lot of tweaking you can do to get different results and several methods of examining and modifying your results. To know about them, just read the rest of this help file. 15 | -------------------------------------------------------------------------------- /help/en/reprioritize.rst: -------------------------------------------------------------------------------- 1 | Re-Prioritizing duplicates 2 | ========================== 3 | 4 | dupeGuru tries to automatically determine which duplicate should go in each group's reference 5 | position, but sometimes it gets it wrong. In many cases, clever dupe sorting with "Delta Values" 6 | and "Dupes Only" options in addition to the "Make Selected into Reference" action does the trick, 7 | but sometimes, a more powerful option is needed. This is where the Re-Prioritization dialog comes 8 | into play. You can summon it through the "Re-Prioritize Results" item in the "Actions" menu. 9 | 10 | This dialog allows you to select criteria according to which a reference dupe will be selected in 11 | each dupe group. The list of available criteria is on the left and the list of criteria you've 12 | selected is on the right. 13 | 14 | A criteria is a category followed by an argument. For example, "Size (Highest)" means that the dupe 15 | with the biggest size will win. "Folder (/foo/bar)" means that dupes in this folder will win. To add 16 | a criterion to the rightmost list, first select a category in the combobox, then select a 17 | subargument in the list below, and then click on the right pointing arrow button. 18 | 19 | The order of the list on the right is important (you can re-order items through drag & drop). When 20 | picking a dupe for reference position, the first criterion is used. If there's a tie, the second 21 | criterion is used and so on and so on. For example, if your arguments are "Size (Highest)" and then 22 | "Filename (Doesn't end with a number)", the reference file that will be picked in a group will be 23 | the biggest file, and if two or more files have the same size, the one that has a filename that 24 | doesn't end with a number will be used. When all criteria result in ties, the order in which dupes 25 | previously were in the group will be used. 26 | -------------------------------------------------------------------------------- /help/fr/index.rst: -------------------------------------------------------------------------------- 1 | Aide dupeGuru 2 | =============== 3 | 4 | .. only:: edition_se 5 | 6 | Ce document est aussi disponible en `anglais `__, en `allemand `__ et en `arménien `__. 7 | 8 | .. only:: edition_se or edition_me 9 | 10 | 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. 11 | 12 | .. only:: edition_pe 13 | 14 | dupeGuru Picture Edition est un outil pour trouver des doublons parmi vos images. Non seulement il permet de trouver les doublons exactes, mais il est aussi capable de trouver les images ayant de légères différences, étant de format différent ou bien ayant une qualité différente. 15 | 16 | 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 `. 17 | 18 | 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. 19 | 20 | Contents: 21 | 22 | .. toctree:: 23 | :maxdepth: 2 24 | 25 | quick_start 26 | folders 27 | preferences 28 | results 29 | reprioritize 30 | faq 31 | changelog 32 | -------------------------------------------------------------------------------- /help/fr/quick_start.rst: -------------------------------------------------------------------------------- 1 | Démarrage rapide 2 | ================= 3 | 4 | Voici les étapes à suivre pour faire un simple scan par défaut: 5 | 6 | * Démarrer dupeGuru. 7 | * Ajouter les dossiers à scanner soit avec le drag & drop, soit avec le boutton "+". 8 | * Cliquez sur **Scan**. 9 | * Attendez que le scan soit completé. 10 | * Vérifiez que les doublons (les fichiers légèrement indentés) soient vraiment le doublon de la référence du groupe (le fichier au haut du groupe qui ne peut pas être marqué). 11 | * Si vous voyer un faux doublon, sélectionnez le puis cliquez sur l'action **Retirer sélectionnés des résultats**. 12 | * Quand vous êtes certains de ne pas avoir de faux doublons dans vos résultats, cliquez sur **Tout marquer** dans le menu Marquer et cliquez sur l'action **Envoyer marqués à la corbeille**. 13 | 14 | Ceci est seulement un scan de base. Il est possible de configurer dupeGuru afin d'obtenir exactement le type de résultat recherché. Pour en savoir plus, il lisez le reste du fichier d'aide. 15 | -------------------------------------------------------------------------------- /help/fr/reprioritize.rst: -------------------------------------------------------------------------------- 1 | Re-Prioritizing duplicates 2 | ========================== 3 | 4 | dupeGuru tries to automatically determine which duplicate should go in each group's reference 5 | position, but sometimes it gets it wrong. In many cases, clever dupe sorting with "Delta Values" 6 | and "Dupes Only" options in addition to the "Make Selected into Reference" action does the trick, 7 | but sometimes, a more powerful option is needed. This is where the Re-Prioritization dialog comes 8 | into play. You can summon it through the "Re-Prioritize Results" item in the "Actions" menu. 9 | 10 | This dialog allows you to select criteria according to which a reference dupe will be selected in 11 | each dupe group. The list of available criteria is on the left and the list of criteria you've 12 | selected is on the right. 13 | 14 | A criteria is a category followed by an argument. For example, "Size (Highest)" means that the dupe 15 | with the biggest size will win. "Folder (/foo/bar)" means that dupes in this folder will win. To add 16 | a criterion to the rightmost list, first select a category in the combobox, then select a 17 | subargument in the list below, and then click on the right pointing arrow button. 18 | 19 | The order of the list on the right is important (you can re-order items through drag & drop). When 20 | picking a dupe for reference position, the first criterion is used. If there's a tie, the second 21 | criterion is used and so on and so on. For example, if your arguments are "Size (Highest)" and then 22 | "Filename (Doesn't end with a number)", the reference file that will be picked in a group will be 23 | the biggest file, and if two or more files have the same size, the one that has a filename that 24 | doesn't end with a number will be used. When all criteria result in ties, the order in which dupes 25 | previously were in the group will be used. 26 | -------------------------------------------------------------------------------- /help/hy/folders.rst: -------------------------------------------------------------------------------- 1 | Թղթապանակի ընտրություն 2 | ======================= 3 | 4 | Առաջին թղթապանակը, որ Դուք տեսնում եք dupeGuru-ն բացելիս դա թղթապանակի ընտրությունն է: Այս պատուհանը պարունակում է թղթապանակների ցանկը, որոնք կստուգվեն **Ստուգել** սեղմելիս: 5 | 6 | Այս պատուհանը շատ հեշտ է օգտագործել: Եթե ցանկանում եք ավելացնել թղթապանակ, ապա սեղմեք **+** կոճակը: Եթե մինչ այդ ավելացնեք թղթապանակը, ապա կերևա ավելացված վերջին թղթապանակների ցանկը: Կարող եք սեղմել նրանցից մեկի վրա՝ ավելացնելու համար ուղղակի Ձեր ցանկում: Եթե սեղմեք հայտնվող պատուհանի առաջին ֆայլին՝ **Ավելացնել նոր թղթապանակ...**, ապա Ձեզ հարցում կկատարվի թղթապանակ ավելացնելու մասին: Եթե երբեք չեք ավելացրել թղթապանակ, ապա ոչ մի ընտրացանկ չի երևա և Ձեզ ուղղակի հարցում կարվի նոր թղթապանակ ավելացնելու մասին: 7 | 8 | Այլընտրանքյին ճանապարհով թղթապանակներ կարող եք ավելացնել պարզապես դրանք գցելով ցանկում: 9 | 10 | Թղթապանակը հեռացնելու համար ընտրեք թղթապանակը, սեղմեք **-**: Եթե ընտրված է ենթաթղթպանակը, երբ Դուք սեղմում եք կոճակին, ընտրված թղթապանակը կնշվի որպես **բացառված** (նայեք այստեղ)՝ ջնջվելու փոխարեն: 11 | 12 | Թղթապանակի վիճակը 13 | ------------------ 14 | 15 | Յուրաքանչյուր թղթապանակ կարող է լինել հետևյալ 3 եղանակներից մեկում. 16 | 17 | * **Նորմալ.** Այս թղթապանակում գտնված կրկնօրինակները կարող են ջնջվել: 18 | * **Հղված.** Կրկնօրինակներ են գտնվել այս թղթապանակում, որոնք **չեն կարող** ջնջվել: Ֆայլերը այս թղթապանակից կարող են միայն ավարտվել **հղում** դիրքով խմբում: Եթե մեկ ֆայլից ավելի են հղման թղթապանակների հղումները, ապա միայն մեկը կպահվի: Մնացածը կջնջվեմ խմբից: 19 | * **Բացառված.** Ֆայլերը այս թղթապանակում կներառվեն ստուգման մեջ: 20 | 21 | Թղթապանակի հիմնական վիճակը, իհարկե՛ **Նորմալ է**: Կարող եք օգտագործել **Հղված** վիճակը թղթապանակի համար, եթե ցանկանում եք համոզված լինել, որ ոչ մի ֆայլ չի ջնջվի: 22 | 23 | Եթե նշել եք թղթապանակի վիճակը, բոլոր ենթաթղթապանակները միանգամից կժառանգեն այս վիճակը, եթե վիճակը պարզորոշ տրված է թղթապանակի կարգում: 24 | 25 | .. todo:: Add iPhoto/Aperture/iTunes libraries notes 26 | -------------------------------------------------------------------------------- /help/hy/index.rst: -------------------------------------------------------------------------------- 1 | dupeGuru help 2 | =============== 3 | 4 | .. only:: edition_se 5 | 6 | Այս փաստաթուղթը հասանելի է նաև՝ `Ֆրանսերեն `__ և `Գերմաներեն `__. 7 | 8 | .. only:: edition_se or edition_me 9 | 10 | dupeGuru ծրագիր է՝ գտնելու կրկնօրինակ ունեցող ֆայլեր Ձեր համակարգչում: Այն կարող է անգամ ստուգել ֆայլի անունները կան բովանդակությունը: Ֆայլի անվան ստուգման հնարավորությունները ոչ ճշգրիտ համընկման ալգորիթմով, որը կարող է գտնել ֆայլի անվան կրկնօրինակներ, անգամ եթե դրանք նույնը չեն: 11 | 12 | .. only:: edition_pe 13 | 14 | dupeGuru Picture Edition-ը (PE՝ կարճ) գործիք է, որը գտնում է նկարների կրկնօրինակները Ձեր համակարգչում: Գտնում է ոչ միայն նույնանման կրկնօրինակները, այլ նաև կարող է գտնել տարբեր տեսակի և որակի նկարներ (PNG, JPG, GIF և այլն...): 15 | 16 | Չնայած dupeGuru-ն կարող է հեշտությամբ օգտագործվել առանց օգնության, այնուհանդերձ եթե կարդաք այս ֆայլը, այն մեծապես կօգնի Ձեզ ընկալելու ծրագրի աշխատանքը: Եթե Դուք նայում եք ձեռնարկը կրկնօրինակների առաջին ստուգման համար, ապա կարող եք ընտրել :doc:`Արագ Սկիզբ ` հատվածը: 17 | 18 | Շատ լավ միտք է պահելու dupeGuru թարմացված: Կարող եք բեռնել վեբ կայքի համապատասխան էջից http://dupeguru.voltaicideas.net: 19 | 20 | Պարունակությունը. 21 | 22 | .. toctree:: 23 | :maxdepth: 2 24 | 25 | quick_start 26 | folders 27 | preferences 28 | results 29 | reprioritize 30 | faq 31 | changelog 32 | -------------------------------------------------------------------------------- /help/hy/quick_start.rst: -------------------------------------------------------------------------------- 1 | Արագ Սկիզբ 2 | =========== 3 | 4 | Արագ սկսելու համար dupeGuru-ն, պարզապես կատարեք ստանդարտ ստուգում՝ օգտագործելով ծրագրային կարգավորումները: 5 | 6 | * Բացել dupeGuru-ն: 7 | * Ավելացնել թղթապանակներ՝ ստուգելու համար նաև վերցնել & գցելը կամ "+" կոճակը: 8 | * Սեղմեք **Ստուգել**: 9 | * Սպասեք, մինչ ստուգումը կավարտվի: 10 | * Նայեք ցանկացած կրկնօրինակին (Ֆայլեր, որոնք նշվել են) և ստուգվել, իրականում կրկնօրինակել խմբի հղմանը (Ֆայլը կրկնօրինակելուց առաջ չի նշվում և ընտրված չէ): 11 | * Եթե ֆայլը սխալ կրկնօրինակ է, ապա ընտրեք այն և սեղմեք **Գործողություններ-->Հեռացնել ընտրվածը Արդյունքներից**: 12 | * Եթե համոզված եք, որ կրկնօրինակը արդյունքներում կա, ապա սեղմեք **Խմբագրել-->Նշել բոլորը**, և ապա **Գործողություններ-->Ուղարկել Նշվածը Աղբարկղ**: 13 | 14 | Սա միայն բազային ստուգում է: Կան բազմաթիվ կարգավորումներ, որոնք հնարավորություն են տալիս նշելու տարբեր արդյունքներ և մի քանի եղանակներ արդյունքների փոփոխման: Մանրամասների համար կարդացեք Օգնության ֆայլը: 15 | -------------------------------------------------------------------------------- /help/hy/reprioritize.rst: -------------------------------------------------------------------------------- 1 | Վերաառաջնայնության կրկնօրինակներ 2 | ================================ 3 | 4 | dupeGuru-ը փորձում է որոշել, թե որ կրկնօրինակները պետք է գնան յուրաքանչյուր խմբի դիրքում, 5 | բայց երբեմն սխալ է ստանում: Շատ դեպքերում, խելամիտ դասավորումը "Դելտա նշանակության" 6 | և "Միայն սխալները" ընտրանքների ավելացնելով "Դարձնել ընտրվածը հղում" գործողության խորամանկություն է, բայց 7 | երբեմն, պահանջվում են ավելի լավ ընտրանքներ: Ահա այստեղ է, որ վերաառաջնայնավորման պատուհանը բացվում է: 8 | Կարող եք կանչել այն "Վերաառաջնայնավորման արդյունքները" կետից՝ "Գործողություններ" ընտրացանկից: 9 | 10 | Այս պատուհանը հնարավորություն է տալիս Ձեզ ընտրելու չափանիշներ՝ հղման սխալին համապատասխան և կընտրվի 11 | յուրաքանչյուր սխալի խումբը: Հասանելի չափանիշների ցանկը ձախում է և Ձեր ընտրած չափանիշների ցանկը գտնվում է 12 | աջում: 13 | 14 | Չափանիշն դա բաժինն է, որը հետևում է փաստարկին: Օրինակ՝ "Չափը (Բարձրագույն)" նշանակում է, որ սխալը 15 | հետևում է մեծագույն չափի հաղթողին: "Թղթապանակը (/foo/bar)" նշանակում է, որ սխալները թղթապանակում կհաղթեն: Ավելացնելու համար 16 | փաստարկ ամենաաջ մասում, նախ ընտրեք բաժինը, ապա ընտրեք 17 | ենթափաստարկ հետևյալ ցանկում և ապա սեղմեք կոճակի սլաքի աջ մասում: 18 | 19 | Ցանկի կարգը աջից շատ կարևոր է (կարող եք վերակարգավորել ֆայլերը վերցնել և գցելու միջոցով): Երբ 20 | սխալի տեղորոշումը հղման դիրքում է, ապա օգտագործվում է առաջին փաստարկը: Եթե դա կապված է, ապա երկրորդ 21 | փաստարկն է օգտագործվում և այլն և այլն: Օրինակ, եթե Ձեր փաստարկները "Չափը (բարձրագույն)" են և ապա 22 | "Ֆայլի անունը (Չի ավարտվում թվով)", ապա հղման ֆայլը, որը կընտրվի խմբում, ապա կլինի 23 | մեծագույն ֆայլը և եթե երկու կամ ավելի ֆայլեր ունեն նույն չափը, ապա մեկը ունի ֆայլի անուն, որը 24 | չի ավարտվում թվով, կօգտագործվի: Երբ փաստարկի արդյունքը կապված է, կարգը, որի սխալները 25 | նախկինում էին, խումբը պետք է օգտագործվի: 26 | -------------------------------------------------------------------------------- /help/ru/folders.rst: -------------------------------------------------------------------------------- 1 | Выбор папки 2 | ================ 3 | 4 | Первое окно, вы видите, когда вы запускаете dupeGuru это окно выбора папки. Это окно содержит список папок, которые будут сканироваться при нажатии на **Scan**. 5 | 6 | Это окно довольно проста в использовании. Если вы хотите добавить папку, нажмите на кнопку **+**. Если вы добавили папки прежде, всплывающее меню со списком последних папки добавил появится. Вы можете нажать на одну из них, чтобы добавить его прямо в свой список. Если нажать на первый пункт всплывающего меню, **Добавить новую папку ...**, вам будет предложено ввести папку добавить. Если вы никогда не добавляется папка, не появится меню, и вы будете непосредственно будет предложено ввести новую папку добавить. 7 | 8 | Альтернативный способ для добавления папок в список, чтобы перетащить их в списке. 9 | 10 | Чтобы удалить папку, выберите папку, удалить, и нажмите на **-**. Если папке выбирается при нажатии кнопки, выбранной папки будет установлен в ** ** исключены состояния (см. ниже), а не удален. 11 | 12 | Папка государств 13 | ---------------- 14 | 15 | Каждая папка может находиться в одном из этих 3-х государств: 16 | 17 | * **Нормальный:** дубликаты найдены в эту папку можно удалить. 18 | * **Справка:** Дубликаты найти в этой папке не может **быть удалены** . Файлы из этой папки можно только в конечном итоге в **ссылка** позиция в группе обмануть. Если более чем один файл из папки ссылку в конечном итоге в той же группе обмануть, только один, будут сохранены.Другие будут удалены из группы. 19 | * **Не включено:** Файлы в этом каталоге не будет включен в проверку. 20 | 21 | Состояние по умолчанию к папке, конечно, **Нормальный**. Вы можете использовать **Ссылка** состояние для папки, если вы хотите быть уверены, что вы не будете удалять любые файлы из него. 22 | 23 | Когда вы устанавливаете состояние каталог, все подпапки этой папки автоматически наследует это состояние, если явно не включенное состояние подпапку в. 24 | 25 | .. todo:: Add iPhoto/Aperture/iTunes libraries notes 26 | -------------------------------------------------------------------------------- /help/ru/index.rst: -------------------------------------------------------------------------------- 1 | dupeGuru help 2 | =============== 3 | 4 | Этот документ также доступна на `французском `__, `немецком `__ и `армянский `__. 5 | 6 | .. only:: edition_se or edition_me 7 | 8 | dupeGuru есть инструмент для поиска дубликатов файлов на вашем компьютере. Он может сканировать либо имен файлов или содержимого.Имя файла функций сканирования нечеткого соответствия алгоритма, который позволяет найти одинаковые имена файлов, даже если они не совсем то же самое. 9 | 10 | .. only:: edition_pe 11 | 12 | dupeGuru Picture Edition (PE для краткости) представляет собой инструмент для поиска дубликатов фотографий на вашем компьютере. Не только он может найти точные соответствия, но он также может найти дубликаты среди фотографий разного рода (PNG, JPG, GIF и т.д..) И качество. 13 | 14 | Хотя dupeGuru может быть легко использована без документации, чтение этого файла поможет вам освоить его. Если вы ищете руководство для вашей первой дублировать сканирования, вы можете взглянуть на раздел :doc:`Быстрый ` Начало. 15 | 16 | Это хорошая идея, чтобы сохранить dupeGuru обновлен. Вы можете скачать последнюю версию на своей http://dupeguru.voltaicideas.net. 17 | Содержание: 18 | 19 | .. toctree:: 20 | :maxdepth: 2 21 | 22 | quick_start 23 | folders 24 | preferences 25 | results 26 | reprioritize 27 | faq 28 | changelog 29 | -------------------------------------------------------------------------------- /help/ru/quick_start.rst: -------------------------------------------------------------------------------- 1 | Быстрый старт 2 | ============= 3 | 4 | Чтобы вы быстро начали с dupeGuru, давайте просто делать сканирование с помощью стандартных настроек по умолчанию. 5 | 6 | * Запуск dupeGuru. 7 | * Добавление папок для сканирования либо перетащить & капли или кнопку "+". 8 | * Нажмите на **сканирование**. 9 | * Подождите, пока процесс сканирования завершен. 10 | * Посмотрите на каждый дубликат (файлы, которые отступом) и убедитесь, что это действительно дубликат ссылкой группы (файл выше дублировать без отступа и инвалидов окна знак). 11 | * Если файл ложных дубликатов, выделите ее и нажмите **Действия -> Удалить выбранные из результатов**. 12 | * Если вы уверены, что нет ложных дубликатов в результатах, нажмите на **Изменить -> Отметить Все**, а затем **Действия -> Отправить Помечено в Корзину**. 13 | 14 | Это только основные сканирования. Есть много настройки вы можете сделать, чтобы получить разные результаты и несколько методов изучения и изменения ваших результатов. Чтобы узнать о них, только что прочитал остальную часть этого файла справки. 15 | -------------------------------------------------------------------------------- /help/ru/reprioritize.rst: -------------------------------------------------------------------------------- 1 | Повторное приоритетов дубликатов 2 | ================================ 3 | 4 | dupeGuru пытается автоматически определить, какие дубликат должен отправиться в ссылку каждой группы 5 | позиции, но иногда это делается неправильно. Во многих случаях, умный обмануть сортировки с "Ценности Дельта" 6 | и "обманутые Только" варианты в дополнение к "Сделать выбранной ссылки" действие делает трюк, но 7 | иногда, более мощный вариант не требуется. Здесь изменения приоритетов в диалог вступает в 8 | играть. Вы можете вызвать его через "изменить приоритеты Результаты" пункт в меню "Действия". 9 | 10 | Этот диалог позволяет вам выбрать критерии, по которым ссылка обмануть будут отобраны в 11 | каждой группе обмануть.Список доступных критериев слева и перечень критериев вы 12 | Выбранная справа. 13 | 14 | Критериев категории следуют аргумент. Например, "Размер (Высший)" означает, что обмануть 15 | с крупным размером победит. "Свойства папки (/ Foo / Bar)" означает, что обманутые в этой папке будет победить. Для добавления 16 | критерий правом списке, сначала выберите категорию в выпадающем списке, затем выберите 17 | subargument в приведенном ниже списке, а затем нажмите на правую стрелку кнопки. 18 | 19 | Порядок списка справа важно (вы можете изменить порядок элементов через перетащить и отпустить). когда 20 | сбор обмануть для справки позицию, первый критерий используется. Если есть галстук, второй 21 | критерий используется и так далее и так далее. Например, если ваши аргументы "Размер (высший)", а затем 22 | "Имя файла (Не оканчивается на номер)", ссылке на файл, который будет выбран в группе будет 23 | крупнейших файл, а если два или несколько файлов имеют одинаковый размер, который имеет имя файла с 24 | не заканчивается номер будет использоваться. Когда все критерии привести к связи, порядок, в котором обманутые 25 | ранее были в группе будет использоваться. 26 | -------------------------------------------------------------------------------- /help/uk/folders.rst: -------------------------------------------------------------------------------- 1 | Вибір папки 2 | ================ 3 | 4 | Перше вікно, ви бачите, коли ви запускаєте dupeGuru це вікно вибору папки. Це вікно містить список папок, які будуть скануватися при натисканні на **Сканування**.Це вікно досить проста у використанні. Якщо ви хочете додати папку, натисніть на кнопку **+**. Якщо ви додали папки перш, спливаюче меню зі списком останніх папки додав з'явиться. Ви можете натиснути на одну з них, щоб додати його прямо в свій список. Якщо натиснути на перший пункт меню, **Додати новий папку ...**, вам буде запропоновано ввести папку додати. Якщо ви ніколи не додається папка, не з'явиться меню, і ви будете безпосередньо буде запропоновано ввести нову папку додати. 5 | 6 | Альтернативний спосіб для додавання папок в список, щоб перетягнути їх в списку. 7 | 8 | Щоб видалити папку, виберіть папку, видалити, і натисніть на **-**. Якщо папці вибирається при натисканні кнопки, обраної папки буде встановлений в **виключені** стану (див. нижче), а не видалений. 9 | 10 | Папка держав 11 | ------------- 12 | 13 | Кожна папка може знаходитися в одному з цих 3-х держав: 14 | 15 | * ** Нормальний: ** дублікати знайдені в цю папку можна видалити. 16 | * ** Довідка: ** Дублікати знайти в цій папці **не може** бути видалені. Файли з цієї папки можна тільки в кінцевому підсумку в **посилання** позиція в групі обдурити. Якщо більш ніж один файл з папки посилання в кінцевому підсумку в тій же групі обдурити, тільки один, будуть збережені. Інші будуть видалені з групи. 17 | * ** Не включено: ** Файли в цьому каталозі не буде включений у перевірку. 18 | 19 | Стан за замовчуванням до папки, звичайно, **Нормальний**. Ви можете використовувати **Посилання** стан для папки, якщо ви хочете бути впевнені, що ви не будете видаляти будь-які файли з нього. 20 | 21 | Коли ви встановлюєте стан каталог, все підпапки цієї папки автоматично успадковує цей стан, якщо явно не включений стан підпапку в. 22 | 23 | .. todo:: Add iPhoto/Aperture/iTunes libraries notes 24 | -------------------------------------------------------------------------------- /help/uk/index.rst: -------------------------------------------------------------------------------- 1 | dupeGuru help 2 | =============== 3 | 4 | .. only:: edition_se 5 | 6 | Цей документ також доступна на `французькому `__, `німецький `__ і `Вірменський `__. 7 | 8 | .. only:: edition_se or edition_me 9 | 10 | dupeGuru це інструмент для пошуку дублікатів файлів на вашому комп'ютері. Він може сканувати або імен файлів або вмісту. Файл функцій сканування нечіткого відповідності алгоритму, який дозволяє знайти однакові імена файлів, навіть якщо вони не зовсім те ж саме. 11 | 12 | .. only:: edition_pe 13 | 14 | dupeGuru Picture Edition (PE для стислості) являє собою інструмент для пошуку дублікатів фотографій на вашому комп'ютері. Не тільки він може знайти точні відповідності, але він також може знайти дублікати серед фотографій різного роду (PNG, JPG, GIF і т.д..) І якість. 15 | 16 | Хоча dupeGuru може бути легко використана без документації, читання цього файлу допоможе вам освоїти його. Якщо ви шукаєте керівництво для вашої першої дублювати сканування, ви можете поглянути на: :doc:`Quick Start ` 17 | 18 | Це гарна ідея, щоб зберегти dupeGuru оновлено. Ви можете завантажити останню версію на своєму http://dupeguru.voltaicideas.net. 19 | 20 | Contents: 21 | 22 | .. toctree:: 23 | :maxdepth: 2 24 | 25 | quick_start 26 | folders 27 | preferences 28 | results 29 | reprioritize 30 | faq 31 | changelog 32 | -------------------------------------------------------------------------------- /help/uk/quick_start.rst: -------------------------------------------------------------------------------- 1 | Швидкий старт 2 | ============== 3 | 4 | Щоб ви швидко почали з dupeGuru, давайте просто робити сканування за допомогою стандартних параметрів за замовчуванням. 5 | 6 | * Запуск dupeGuru. 7 | * Додавання папок для сканування або перетягнути & краплі або кнопку "+". 8 | * Натисніть на сканування. 9 | * Почекайте, поки процес сканування завершено. 10 | * Подивіться на кожен дублікат (файли, які відступом) і переконайтеся, що це дійсно дублікат посиланням групи (файл вище дублювати без відступу та інвалідів вікна знак). 11 | * Якщо файл помилкових дублікатів, виділіть її та натисніть **Дії -> Видалити вибрані з результатів**. 12 | * Якщо ви впевнені, що немає помилкових дублікатів в результатах, натисніть на **Редагувати -> Позначити Всі**, а потім **Дії -> Отправить Позначено до кошику**. 13 | 14 | Це тільки основні сканування. Є багато налаштування ви можете зробити, щоб отримати різні результати і кілька методів вивчення та зміни ваших результатів. Щоб дізнатися про них, щойно прочитав решту цього файлу довідки. 15 | -------------------------------------------------------------------------------- /help/uk/reprioritize.rst: -------------------------------------------------------------------------------- 1 | Повторне пріоритетів дублікатів 2 | ================================ 3 | 4 | dupeGuru намагається автоматично визначити, які дублікат повинен відправитися в заслання кожної групи 5 | позиції, але іноді це робиться неправильно. У багатьох випадках, розумний обдурити сортування з "Цінності Дельта" 6 | і "ошукані Тільки" варіанти на додаток до "Зробити вибраної посилання" дія робить трюк, але 7 | іноді, більш потужний варіант не потрібно. Тут зміни пріоритетів в діалог вступає в 8 | грати. Ви можете викликати його через "змінити пріоритети Результати" пункт в меню "Дії". 9 | 10 | Цей діалог дозволяє вам вибрати критерії, за якими посилання обдурити будуть відібрані в 11 | кожній групі обдурити. Список доступних критеріїв зліва і перелік критеріїв ви 12 | Обрана справа. 13 | 14 | Критеріїв категорії слідують аргумент. Наприклад, "Розмір (Вищий)" означає, що обдурити 15 | з великим розміром переможе. "Властивості папки (/Foo/Bar)" означає, що ошукані в цій папці буде перемогти. для додавання 16 | критерій правом списку, спочатку виберіть категорію в спадному списку і виберіть 17 | subargument в наведеному нижче списку, а потім натисніть на праву стрілку кнопки. 18 | 19 | Порядок списку праворуч важливо (ви можете змінити порядок елементів через перетягнути і відпустити). коли 20 | збір обдурити для довідки позицію, перший критерій використовується. Якщо є краватка, другий 21 | критерій використовується і так далі і так далі. Наприклад, якщо ваші аргументи "Розмір (вищий)", а потім 22 | "Файл (Не закінчується на номер)", заслання на файл, який буде обраний у групі буде 23 | найбільших файл, а якщо два або декілька файлів мають однаковий розмір, який має ім'я файлу з 24 | не закінчується номер буде використовуватися. Коли всі критерії привести до зв'язку, порядок, в якому ошукані 25 | раніше були в групі буде використовуватися. 26 | -------------------------------------------------------------------------------- /hscommon/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014, Hardcoded Software Inc., http://www.hardcoded.net 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | * Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 9 | 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | -------------------------------------------------------------------------------- /hscommon/README: -------------------------------------------------------------------------------- 1 | This module is common code used in all Hardcoded Software applications. It has no stable API so 2 | it is not recommended to actually depend on it. But if you want to copy bits and pieces for your own 3 | apps, be my guest. 4 | -------------------------------------------------------------------------------- /hscommon/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/hscommon/__init__.py -------------------------------------------------------------------------------- /hscommon/conflict.py: -------------------------------------------------------------------------------- 1 | # Created By: Virgil Dupras 2 | # Created On: 2008-01-08 3 | # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) 4 | 5 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 6 | # which should be included with this package. The terms are also available at 7 | # http://www.gnu.org/licenses/gpl-3.0.html 8 | 9 | """When you have to deal with names that have to be unique and can conflict together, you can use 10 | this module that deals with conflicts by prepending unique numbers in ``[]`` brackets to the name. 11 | """ 12 | 13 | import re 14 | import os 15 | import shutil 16 | 17 | from errno import EISDIR, EACCES 18 | from pathlib import Path 19 | from typing import Callable, List 20 | 21 | # This matches [123], but not [12] (3 digits being the minimum). 22 | # It also matches [1234] [12345] etc.. 23 | # And only at the start of the string 24 | re_conflict = re.compile(r"^\[\d{3}\d*\] ") 25 | 26 | 27 | def get_conflicted_name(other_names: List[str], name: str) -> str: 28 | """Returns name with a ``[000]`` number in front of it. 29 | 30 | The number between brackets depends on how many conlicted filenames 31 | there already are in other_names. 32 | """ 33 | name = get_unconflicted_name(name) 34 | if name not in other_names: 35 | return name 36 | i = 0 37 | while True: 38 | newname = "[%03d] %s" % (i, name) 39 | if newname not in other_names: 40 | return newname 41 | i += 1 42 | 43 | 44 | def get_unconflicted_name(name: str) -> str: 45 | """Returns ``name`` without ``[]`` brackets. 46 | 47 | Brackets which, of course, might have been added by func:`get_conflicted_name`. 48 | """ 49 | return re_conflict.sub("", name, 1) 50 | 51 | 52 | def is_conflicted(name: str) -> bool: 53 | """Returns whether ``name`` is prepended with a bracketed number.""" 54 | return re_conflict.match(name) is not None 55 | 56 | 57 | def _smart_move_or_copy(operation: Callable, source_path: Path, dest_path: Path) -> None: 58 | """Use move() or copy() to move and copy file with the conflict management.""" 59 | if dest_path.is_dir() and not source_path.is_dir(): 60 | dest_path = dest_path.joinpath(source_path.name) 61 | if dest_path.exists(): 62 | filename = dest_path.name 63 | dest_dir_path = dest_path.parent 64 | newname = get_conflicted_name(os.listdir(str(dest_dir_path)), filename) 65 | dest_path = dest_dir_path.joinpath(newname) 66 | operation(str(source_path), str(dest_path)) 67 | 68 | 69 | def smart_move(source_path: Path, dest_path: Path) -> None: 70 | """Same as :func:`smart_copy`, but it moves files instead.""" 71 | _smart_move_or_copy(shutil.move, source_path, dest_path) 72 | 73 | 74 | def smart_copy(source_path: Path, dest_path: Path) -> None: 75 | """Copies ``source_path`` to ``dest_path``, recursively and with conflict resolution.""" 76 | try: 77 | _smart_move_or_copy(shutil.copy, source_path, dest_path) 78 | except OSError as e: 79 | # It's a directory, code is 21 on OS X / Linux (EISDIR) and 13 on Windows (EACCES) 80 | if e.errno in (EISDIR, EACCES): 81 | _smart_move_or_copy(shutil.copytree, source_path, dest_path) 82 | else: 83 | raise 84 | -------------------------------------------------------------------------------- /hscommon/gui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/hscommon/gui/__init__.py -------------------------------------------------------------------------------- /hscommon/jobprogress/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/hscommon/jobprogress/__init__.py -------------------------------------------------------------------------------- /hscommon/jobprogress/performer.py: -------------------------------------------------------------------------------- 1 | # Created By: Virgil Dupras 2 | # Created On: 2010-11-19 3 | # Copyright 2011 Hardcoded Software (http://www.hardcoded.net) 4 | # 5 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 6 | # which should be included with this package. The terms are also available at 7 | # http://www.gnu.org/licenses/gpl-3.0.html 8 | 9 | from threading import Thread 10 | import sys 11 | from typing import Callable, Tuple, Union 12 | 13 | from hscommon.jobprogress.job import Job, JobInProgressError, JobCancelled 14 | 15 | 16 | class ThreadedJobPerformer: 17 | """Run threaded jobs and track progress. 18 | 19 | To run a threaded job, first create a job with _create_job(), then call _run_threaded(), with 20 | your work function as a parameter. 21 | 22 | Example: 23 | 24 | j = self._create_job() 25 | self._run_threaded(self.some_work_func, (arg1, arg2, j)) 26 | """ 27 | 28 | _job_running = False 29 | last_error = None 30 | 31 | # --- Protected 32 | def create_job(self) -> Job: 33 | if self._job_running: 34 | raise JobInProgressError() 35 | self.last_progress: Union[int, None] = -1 36 | self.last_desc = "" 37 | self.job_cancelled = False 38 | return Job(1, self._update_progress) 39 | 40 | def _async_run(self, *args) -> None: 41 | target = args[0] 42 | args = tuple(args[1:]) 43 | self._job_running = True 44 | self.last_error = None 45 | try: 46 | target(*args) 47 | except JobCancelled: 48 | pass 49 | except Exception as e: 50 | self.last_error = e 51 | self.last_traceback = sys.exc_info()[2] 52 | finally: 53 | self._job_running = False 54 | self.last_progress = None 55 | 56 | def reraise_if_error(self) -> None: 57 | """Reraises the error that happened in the thread if any. 58 | 59 | Call this after the caller of run_threaded detected that self._job_running returned to False 60 | """ 61 | if self.last_error is not None: 62 | raise self.last_error.with_traceback(self.last_traceback) 63 | 64 | def _update_progress(self, newprogress: int, newdesc: str = "") -> bool: 65 | self.last_progress = newprogress 66 | if newdesc: 67 | self.last_desc = newdesc 68 | return not self.job_cancelled 69 | 70 | def run_threaded(self, target: Callable, args: Tuple = ()) -> None: 71 | if self._job_running: 72 | raise JobInProgressError() 73 | args = (target,) + args 74 | Thread(target=self._async_run, args=args).start() 75 | -------------------------------------------------------------------------------- /hscommon/path.py: -------------------------------------------------------------------------------- 1 | # Created By: Virgil Dupras 2 | # Created On: 2006/02/21 3 | # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) 4 | 5 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 6 | # which should be included with this package. The terms are also available at 7 | # http://www.gnu.org/licenses/gpl-3.0.html 8 | 9 | import logging 10 | from functools import wraps 11 | from inspect import signature 12 | from pathlib import Path 13 | 14 | 15 | def pathify(f): 16 | """Ensure that every annotated :class:`Path` arguments are actually paths. 17 | 18 | When a function is decorated with ``@pathify``, every argument with annotated as Path will be 19 | converted to a Path if it wasn't already. Example:: 20 | 21 | @pathify 22 | def foo(path: Path, otherarg): 23 | return path.listdir() 24 | 25 | Calling ``foo('/bar', 0)`` will convert ``'/bar'`` to ``Path('/bar')``. 26 | """ 27 | sig = signature(f) 28 | pindexes = {i for i, p in enumerate(sig.parameters.values()) if p.annotation is Path} 29 | pkeys = {k: v for k, v in sig.parameters.items() if v.annotation is Path} 30 | 31 | def path_or_none(p): 32 | return None if p is None else Path(p) 33 | 34 | @wraps(f) 35 | def wrapped(*args, **kwargs): 36 | args = tuple((path_or_none(a) if i in pindexes else a) for i, a in enumerate(args)) 37 | kwargs = {k: (path_or_none(v) if k in pkeys else v) for k, v in kwargs.items()} 38 | return f(*args, **kwargs) 39 | 40 | return wrapped 41 | 42 | 43 | def log_io_error(func): 44 | """Catches OSError, IOError and WindowsError and log them""" 45 | 46 | @wraps(func) 47 | def wrapper(path, *args, **kwargs): 48 | try: 49 | return func(path, *args, **kwargs) 50 | except OSError as e: 51 | msg = 'Error "{0}" during operation "{1}" on "{2}": "{3}"' 52 | classname = e.__class__.__name__ 53 | funcname = func.__name__ 54 | logging.warning(msg.format(classname, funcname, str(path), str(e))) 55 | 56 | return wrapper 57 | -------------------------------------------------------------------------------- /hscommon/plat.py: -------------------------------------------------------------------------------- 1 | # Created On: 2011/09/22 2 | # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) 3 | # 4 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 5 | # which should be included with this package. The terms are also available at 6 | # http://www.gnu.org/licenses/gpl-3.0.html 7 | 8 | # Yes, I know, there's the 'platform' unit for this kind of stuff, but the thing is that I got a 9 | # crash on startup once simply for importing this module and since then I don't trust it. One day, 10 | # I'll investigate the cause of that crash further. 11 | 12 | import sys 13 | 14 | ISWINDOWS = sys.platform == "win32" 15 | ISOSX = sys.platform == "darwin" 16 | ISLINUX = sys.platform.startswith("linux") 17 | -------------------------------------------------------------------------------- /hscommon/sphinxgen.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Virgil Dupras 2 | # 3 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 4 | # which should be included with this package. The terms are also available at 5 | # http://www.gnu.org/licenses/gpl-3.0.html 6 | 7 | from pathlib import Path 8 | import re 9 | from typing import Callable, Dict, Union 10 | 11 | from hscommon.build import read_changelog_file, filereplace 12 | from sphinx.cmd.build import build_main as sphinx_build 13 | 14 | CHANGELOG_FORMAT = """ 15 | {version} ({date}) 16 | ---------------------- 17 | 18 | {description} 19 | """ 20 | 21 | 22 | def tixgen(tixurl: str) -> Callable[[str], str]: 23 | """This is a filter *generator*. tixurl is a url pattern for the tix with a {0} placeholder 24 | for the tix # 25 | """ 26 | urlpattern = tixurl.format("\\1") # will be replaced buy the content of the first group in re 27 | R = re.compile(r"#(\d+)") 28 | repl = f"`#\\1 <{urlpattern}>`__" 29 | return lambda text: R.sub(repl, text) 30 | 31 | 32 | def gen( 33 | basepath: Path, 34 | destpath: Path, 35 | changelogpath: Path, 36 | tixurl: str, 37 | confrepl: Union[Dict[str, str], None] = None, 38 | confpath: Union[Path, None] = None, 39 | changelogtmpl: Union[Path, None] = None, 40 | ) -> None: 41 | """Generate sphinx docs with all bells and whistles. 42 | 43 | basepath: The base sphinx source path. 44 | destpath: The final path of html files 45 | changelogpath: The path to the changelog file to insert in changelog.rst. 46 | tixurl: The URL (with one formattable argument for the tix number) to the ticket system. 47 | confrepl: Dictionary containing replacements that have to be made in conf.py. {name: replacement} 48 | """ 49 | if confrepl is None: 50 | confrepl = {} 51 | if confpath is None: 52 | confpath = Path(basepath, "conf.tmpl") 53 | if changelogtmpl is None: 54 | changelogtmpl = Path(basepath, "changelog.tmpl") 55 | changelog = read_changelog_file(changelogpath) 56 | tix = tixgen(tixurl) 57 | rendered_logs = [] 58 | for log in changelog: 59 | description = tix(log["description"]) 60 | # The format of the changelog descriptions is in markdown, but since we only use bulled list 61 | # and links, it's not worth depending on the markdown package. A simple regexp suffice. 62 | description = re.sub(r"\[(.*?)\]\((.*?)\)", "`\\1 <\\2>`__", description) 63 | rendered = CHANGELOG_FORMAT.format(version=log["version"], date=log["date_str"], description=description) 64 | rendered_logs.append(rendered) 65 | confrepl["version"] = changelog[0]["version"] 66 | changelog_out = Path(basepath, "changelog.rst") 67 | filereplace(changelogtmpl, changelog_out, changelog="\n".join(rendered_logs)) 68 | if Path(confpath).exists(): 69 | conf_out = Path(basepath, "conf.py") 70 | filereplace(confpath, conf_out, **confrepl) 71 | # Call the sphinx_build function, which is the same as doing sphinx-build from cli 72 | try: 73 | sphinx_build([str(basepath), str(destpath)]) 74 | except SystemExit: 75 | print("Sphinx called sys.exit(), but we're cancelling it because we don't actually want to exit") 76 | -------------------------------------------------------------------------------- /hscommon/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/hscommon/tests/__init__.py -------------------------------------------------------------------------------- /hscommon/tests/path_test.py: -------------------------------------------------------------------------------- 1 | # Created By: Virgil Dupras 2 | # Created On: 2006/02/21 3 | # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) 4 | 5 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 6 | # which should be included with this package. The terms are also available at 7 | # http://www.gnu.org/licenses/gpl-3.0.html 8 | 9 | from hscommon.path import pathify 10 | from pathlib import Path 11 | 12 | 13 | def test_pathify(): 14 | @pathify 15 | def foo(a: Path, b, c: Path): 16 | return a, b, c 17 | 18 | a, b, c = foo("foo", 0, c=Path("bar")) 19 | assert isinstance(a, Path) 20 | assert a == Path("foo") 21 | assert b == 0 22 | assert isinstance(c, Path) 23 | assert c == Path("bar") 24 | 25 | 26 | def test_pathify_preserve_none(): 27 | # @pathify preserves None value and doesn't try to return a Path 28 | @pathify 29 | def foo(a: Path): 30 | return a 31 | 32 | a = foo(None) 33 | assert a is None 34 | -------------------------------------------------------------------------------- /hscommon/tests/selectable_list_test.py: -------------------------------------------------------------------------------- 1 | # Created By: Virgil Dupras 2 | # Created On: 2011-09-06 3 | # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) 4 | # 5 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 6 | # which should be included with this package. The terms are also available at 7 | # http://www.gnu.org/licenses/gpl-3.0.html 8 | 9 | from hscommon.testutil import eq_, callcounter, CallLogger 10 | from hscommon.gui.selectable_list import SelectableList, GUISelectableList 11 | 12 | 13 | def test_in(): 14 | # When a SelectableList is in a list, doing "in list" with another instance returns false, even 15 | # if they're the same as lists. 16 | sl = SelectableList() 17 | some_list = [sl] 18 | assert SelectableList() not in some_list 19 | 20 | 21 | def test_selection_range(): 22 | # selection is correctly adjusted on deletion 23 | sl = SelectableList(["foo", "bar", "baz"]) 24 | sl.selected_index = 3 25 | eq_(sl.selected_index, 2) 26 | del sl[2] 27 | eq_(sl.selected_index, 1) 28 | 29 | 30 | def test_update_selection_called(): 31 | # _update_selection_is called after a change in selection. However, we only do so on select() 32 | # calls. I follow the old behavior of the Table class. At the moment, I don't quite remember 33 | # why there was a specific select() method for triggering _update_selection(), but I think I 34 | # remember there was a reason, so I keep it that way. 35 | sl = SelectableList(["foo", "bar"]) 36 | sl._update_selection = callcounter() 37 | sl.select(1) 38 | eq_(sl._update_selection.callcount, 1) 39 | sl.selected_index = 0 40 | eq_(sl._update_selection.callcount, 1) # no call 41 | 42 | 43 | def test_guicalls(): 44 | # A GUISelectableList appropriately calls its view. 45 | sl = GUISelectableList(["foo", "bar"]) 46 | sl.view = CallLogger() 47 | sl.view.check_gui_calls(["refresh"]) # Upon setting the view, we get a call to refresh() 48 | sl[1] = "baz" 49 | sl.view.check_gui_calls(["refresh"]) 50 | sl.append("foo") 51 | sl.view.check_gui_calls(["refresh"]) 52 | del sl[2] 53 | sl.view.check_gui_calls(["refresh"]) 54 | sl.remove("baz") 55 | sl.view.check_gui_calls(["refresh"]) 56 | sl.insert(0, "foo") 57 | sl.view.check_gui_calls(["refresh"]) 58 | sl.select(1) 59 | sl.view.check_gui_calls(["update_selection"]) 60 | # XXX We have to give up on this for now because of a breakage it causes in the tables. 61 | # sl.select(1) # don't update when selection stays the same 62 | # gui.check_gui_calls([]) 63 | 64 | 65 | def test_search_by_prefix(): 66 | sl = SelectableList(["foo", "bAr", "baZ"]) 67 | eq_(sl.search_by_prefix("b"), 1) 68 | eq_(sl.search_by_prefix("BA"), 1) 69 | eq_(sl.search_by_prefix("BAZ"), 2) 70 | eq_(sl.search_by_prefix("BAZZ"), -1) 71 | -------------------------------------------------------------------------------- /images/dgme_logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/images/dgme_logo.ico -------------------------------------------------------------------------------- /images/dgme_logo_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/images/dgme_logo_128.png -------------------------------------------------------------------------------- /images/dgme_logo_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/images/dgme_logo_32.png -------------------------------------------------------------------------------- /images/dgpe_logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/images/dgpe_logo.ico -------------------------------------------------------------------------------- /images/dgpe_logo_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/images/dgpe_logo_128.png -------------------------------------------------------------------------------- /images/dgpe_logo_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/images/dgpe_logo_32.png -------------------------------------------------------------------------------- /images/dgse_logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/images/dgse_logo.ico -------------------------------------------------------------------------------- /images/dgse_logo_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/images/dgse_logo_128.png -------------------------------------------------------------------------------- /images/dgse_logo_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/images/dgse_logo_32.png -------------------------------------------------------------------------------- /images/dialog-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/images/dialog-error.png -------------------------------------------------------------------------------- /images/dupeguru.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/images/dupeguru.icns -------------------------------------------------------------------------------- /images/exchange.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/images/exchange.icns -------------------------------------------------------------------------------- /images/exchange.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/images/exchange.ico -------------------------------------------------------------------------------- /images/exchange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/images/exchange.png -------------------------------------------------------------------------------- /images/exchange_purple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/images/exchange_purple.png -------------------------------------------------------------------------------- /images/exchange_purple_upscaled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/images/exchange_purple_upscaled.png -------------------------------------------------------------------------------- /images/exchange_purple_waifu_s4_tta8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/images/exchange_purple_waifu_s4_tta8.png -------------------------------------------------------------------------------- /images/exchange_purple_waifu_s4_tta8.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/images/exchange_purple_waifu_s4_tta8.xcf -------------------------------------------------------------------------------- /images/exchange_waifu_s4_tta8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/images/exchange_waifu_s4_tta8.png -------------------------------------------------------------------------------- /images/folder32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/images/folder32.png -------------------------------------------------------------------------------- /images/minus_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/images/minus_8.png -------------------------------------------------------------------------------- /images/old_zoom_best_fit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/images/old_zoom_best_fit.png -------------------------------------------------------------------------------- /images/old_zoom_in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/images/old_zoom_in.png -------------------------------------------------------------------------------- /images/old_zoom_original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/images/old_zoom_original.png -------------------------------------------------------------------------------- /images/old_zoom_out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/images/old_zoom_out.png -------------------------------------------------------------------------------- /images/plus_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/images/plus_8.png -------------------------------------------------------------------------------- /images/search_clear_13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/images/search_clear_13.png -------------------------------------------------------------------------------- /locale/columns.pot: -------------------------------------------------------------------------------- 1 | 2 | msgid "" 3 | msgstr "" 4 | "Content-Type: text/plain; charset=utf-8\n" 5 | "Content-Transfer-Encoding: utf-8\n" 6 | 7 | #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 8 | #: core\gui\problem_table.py:18 9 | msgid "File Path" 10 | msgstr "" 11 | 12 | #: core\gui\problem_table.py:19 13 | msgid "Error Message" 14 | msgstr "" 15 | 16 | #: core\me\prioritize.py:23 17 | msgid "Duration" 18 | msgstr "" 19 | 20 | #: core\me\prioritize.py:30 core\me\result_table.py:23 21 | msgid "Bitrate" 22 | msgstr "" 23 | 24 | #: core\me\prioritize.py:37 25 | msgid "Samplerate" 26 | msgstr "" 27 | 28 | #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:94 29 | #: core\se\result_table.py:19 30 | msgid "Filename" 31 | msgstr "" 32 | 33 | #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 34 | #: core\se\result_table.py:20 35 | msgid "Folder" 36 | msgstr "" 37 | 38 | #: core\me\result_table.py:21 39 | msgid "Size (MB)" 40 | msgstr "" 41 | 42 | #: core\me\result_table.py:22 43 | msgid "Time" 44 | msgstr "" 45 | 46 | #: core\me\result_table.py:24 47 | msgid "Sample Rate" 48 | msgstr "" 49 | 50 | #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 51 | #: core\se\result_table.py:22 52 | msgid "Kind" 53 | msgstr "" 54 | 55 | #: core\me\result_table.py:26 core\pe\result_table.py:25 56 | #: core\prioritize.py:165 core\se\result_table.py:23 57 | msgid "Modification" 58 | msgstr "" 59 | 60 | #: core\me\result_table.py:27 61 | msgid "Title" 62 | msgstr "" 63 | 64 | #: core\me\result_table.py:28 65 | msgid "Artist" 66 | msgstr "" 67 | 68 | #: core\me\result_table.py:29 69 | msgid "Album" 70 | msgstr "" 71 | 72 | #: core\me\result_table.py:30 73 | msgid "Genre" 74 | msgstr "" 75 | 76 | #: core\me\result_table.py:31 77 | msgid "Year" 78 | msgstr "" 79 | 80 | #: core\me\result_table.py:32 81 | msgid "Track Number" 82 | msgstr "" 83 | 84 | #: core\me\result_table.py:33 85 | msgid "Comment" 86 | msgstr "" 87 | 88 | #: core\me\result_table.py:34 core\pe\result_table.py:26 89 | #: core\se\result_table.py:24 90 | msgid "Match %" 91 | msgstr "" 92 | 93 | #: core\me\result_table.py:35 core\se\result_table.py:25 94 | msgid "Words Used" 95 | msgstr "" 96 | 97 | #: core\me\result_table.py:36 core\pe\result_table.py:27 98 | #: core\se\result_table.py:26 99 | msgid "Dupe Count" 100 | msgstr "" 101 | 102 | #: core\pe\prioritize.py:23 core\pe\result_table.py:23 103 | msgid "Dimensions" 104 | msgstr "" 105 | 106 | #: core\pe\result_table.py:21 core\se\result_table.py:21 107 | msgid "Size (KB)" 108 | msgstr "" 109 | 110 | #: core\pe\result_table.py:24 111 | msgid "EXIF Timestamp" 112 | msgstr "" 113 | 114 | #: core\prioritize.py:158 115 | msgid "Size" 116 | msgstr "" 117 | -------------------------------------------------------------------------------- /locale/de/LC_MESSAGES/columns.po: -------------------------------------------------------------------------------- 1 | # Translators: 2 | # Andrew Senetar , 2021 3 | # 4 | msgid "" 5 | msgstr "" 6 | "Last-Translator: Andrew Senetar , 2021\n" 7 | "Language-Team: German (https://www.transifex.com/voltaicideas/teams/116153/de/)\n" 8 | "Language: de\n" 9 | "Content-Type: text/plain; charset=UTF-8\n" 10 | "Content-Transfer-Encoding: utf-8\n" 11 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 12 | 13 | #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 14 | #: core\gui\problem_table.py:18 15 | msgid "File Path" 16 | msgstr "Dateipfad" 17 | 18 | #: core\gui\problem_table.py:19 19 | msgid "Error Message" 20 | msgstr "Fehlermeldung" 21 | 22 | #: core\me\prioritize.py:23 23 | msgid "Duration" 24 | msgstr "Dauer" 25 | 26 | #: core\me\prioritize.py:30 core\me\result_table.py:23 27 | msgid "Bitrate" 28 | msgstr "Bitrate" 29 | 30 | #: core\me\prioritize.py:37 31 | msgid "Samplerate" 32 | msgstr "Abtastrate" 33 | 34 | #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92 35 | #: core\se\result_table.py:19 36 | msgid "Filename" 37 | msgstr "Dateiname" 38 | 39 | #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 40 | #: core\se\result_table.py:20 41 | msgid "Folder" 42 | msgstr "Ordner" 43 | 44 | #: core\me\result_table.py:21 45 | msgid "Size (MB)" 46 | msgstr "Größe (MB)" 47 | 48 | #: core\me\result_table.py:22 49 | msgid "Time" 50 | msgstr "Zeit" 51 | 52 | #: core\me\result_table.py:24 53 | msgid "Sample Rate" 54 | msgstr "Abtastrate" 55 | 56 | #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 57 | #: core\se\result_table.py:22 58 | msgid "Kind" 59 | msgstr "Typ" 60 | 61 | #: core\me\result_table.py:26 core\pe\result_table.py:25 62 | #: core\prioritize.py:163 core\se\result_table.py:23 63 | msgid "Modification" 64 | msgstr "Geändert" 65 | 66 | #: core\me\result_table.py:27 67 | msgid "Title" 68 | msgstr "Titel" 69 | 70 | #: core\me\result_table.py:28 71 | msgid "Artist" 72 | msgstr "Künstler" 73 | 74 | #: core\me\result_table.py:29 75 | msgid "Album" 76 | msgstr "Album" 77 | 78 | #: core\me\result_table.py:30 79 | msgid "Genre" 80 | msgstr "Genre" 81 | 82 | #: core\me\result_table.py:31 83 | msgid "Year" 84 | msgstr "Jahr" 85 | 86 | #: core\me\result_table.py:32 87 | msgid "Track Number" 88 | msgstr "Titel Nummer" 89 | 90 | #: core\me\result_table.py:33 91 | msgid "Comment" 92 | msgstr "Kommentar" 93 | 94 | #: core\me\result_table.py:34 core\pe\result_table.py:26 95 | #: core\se\result_table.py:24 96 | msgid "Match %" 97 | msgstr "Übereinstimmung %" 98 | 99 | #: core\me\result_table.py:35 core\se\result_table.py:25 100 | msgid "Words Used" 101 | msgstr "genutzte Wörter" 102 | 103 | #: core\me\result_table.py:36 core\pe\result_table.py:27 104 | #: core\se\result_table.py:26 105 | msgid "Dupe Count" 106 | msgstr "Anzahl der Duplikate" 107 | 108 | #: core\pe\prioritize.py:23 core\pe\result_table.py:23 109 | msgid "Dimensions" 110 | msgstr "Auflösung" 111 | 112 | #: core\pe\result_table.py:21 core\se\result_table.py:21 113 | msgid "Size (KB)" 114 | msgstr "Größe (KB)" 115 | 116 | #: core\pe\result_table.py:24 117 | msgid "EXIF Timestamp" 118 | msgstr "EXIF Zeitstempel" 119 | 120 | #: core\prioritize.py:156 121 | msgid "Size" 122 | msgstr "Größe" 123 | -------------------------------------------------------------------------------- /locale/en/LC_MESSAGES/columns.po: -------------------------------------------------------------------------------- 1 | # 2 | msgid "" 3 | msgstr "" 4 | "Content-Type: text/plain; charset=utf-8\n" 5 | "Content-Transfer-Encoding: utf-8\n" 6 | 7 | #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 8 | #: core\gui\problem_table.py:18 9 | msgid "File Path" 10 | msgstr "File Path" 11 | 12 | #: core\gui\problem_table.py:19 13 | msgid "Error Message" 14 | msgstr "Error Message" 15 | 16 | #: core\me\prioritize.py:23 17 | msgid "Duration" 18 | msgstr "Duration" 19 | 20 | #: core\me\prioritize.py:30 core\me\result_table.py:23 21 | msgid "Bitrate" 22 | msgstr "Bitrate" 23 | 24 | #: core\me\prioritize.py:37 25 | msgid "Samplerate" 26 | msgstr "Samplerate" 27 | 28 | #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92 29 | #: core\se\result_table.py:19 30 | msgid "Filename" 31 | msgstr "Filename" 32 | 33 | #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 34 | #: core\se\result_table.py:20 35 | msgid "Folder" 36 | msgstr "Folder" 37 | 38 | #: core\me\result_table.py:21 39 | msgid "Size (MB)" 40 | msgstr "Size (MB)" 41 | 42 | #: core\me\result_table.py:22 43 | msgid "Time" 44 | msgstr "Time" 45 | 46 | #: core\me\result_table.py:24 47 | msgid "Sample Rate" 48 | msgstr "Sample Rate" 49 | 50 | #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 51 | #: core\se\result_table.py:22 52 | msgid "Kind" 53 | msgstr "Kind" 54 | 55 | #: core\me\result_table.py:26 core\pe\result_table.py:25 56 | #: core\prioritize.py:163 core\se\result_table.py:23 57 | msgid "Modification" 58 | msgstr "Modification" 59 | 60 | #: core\me\result_table.py:27 61 | msgid "Title" 62 | msgstr "Title" 63 | 64 | #: core\me\result_table.py:28 65 | msgid "Artist" 66 | msgstr "Artist" 67 | 68 | #: core\me\result_table.py:29 69 | msgid "Album" 70 | msgstr "Album" 71 | 72 | #: core\me\result_table.py:30 73 | msgid "Genre" 74 | msgstr "Genre" 75 | 76 | #: core\me\result_table.py:31 77 | msgid "Year" 78 | msgstr "Year" 79 | 80 | #: core\me\result_table.py:32 81 | msgid "Track Number" 82 | msgstr "Track Number" 83 | 84 | #: core\me\result_table.py:33 85 | msgid "Comment" 86 | msgstr "Comment" 87 | 88 | #: core\me\result_table.py:34 core\pe\result_table.py:26 89 | #: core\se\result_table.py:24 90 | msgid "Match %" 91 | msgstr "Match %" 92 | 93 | #: core\me\result_table.py:35 core\se\result_table.py:25 94 | msgid "Words Used" 95 | msgstr "Words Used" 96 | 97 | #: core\me\result_table.py:36 core\pe\result_table.py:27 98 | #: core\se\result_table.py:26 99 | msgid "Dupe Count" 100 | msgstr "Dupe Count" 101 | 102 | #: core\pe\prioritize.py:23 core\pe\result_table.py:23 103 | msgid "Dimensions" 104 | msgstr "Dimensions" 105 | 106 | #: core\pe\result_table.py:21 core\se\result_table.py:21 107 | msgid "Size (KB)" 108 | msgstr "Size (KB)" 109 | 110 | #: core\pe\result_table.py:24 111 | msgid "EXIF Timestamp" 112 | msgstr "EXIF Timestamp" 113 | 114 | #: core\prioritize.py:156 115 | msgid "Size" 116 | msgstr "Size" 117 | -------------------------------------------------------------------------------- /locale/fr/LC_MESSAGES/columns.po: -------------------------------------------------------------------------------- 1 | # Translators: 2 | # Andrew Senetar , 2021 3 | # Fuan , 2021 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Last-Translator: Fuan , 2021\n" 8 | "Language-Team: French (https://www.transifex.com/voltaicideas/teams/116153/fr/)\n" 9 | "Language: fr\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: utf-8\n" 12 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 13 | 14 | #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 15 | #: core\gui\problem_table.py:18 16 | msgid "File Path" 17 | msgstr "Chemin du fichier" 18 | 19 | #: core\gui\problem_table.py:19 20 | msgid "Error Message" 21 | msgstr "Message d'erreur" 22 | 23 | #: core\me\prioritize.py:23 24 | msgid "Duration" 25 | msgstr "Durée" 26 | 27 | #: core\me\prioritize.py:30 core\me\result_table.py:23 28 | msgid "Bitrate" 29 | msgstr "Bitrate" 30 | 31 | #: core\me\prioritize.py:37 32 | msgid "Samplerate" 33 | msgstr "Échantillonnage" 34 | 35 | #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92 36 | #: core\se\result_table.py:19 37 | msgid "Filename" 38 | msgstr "Nom de fichier" 39 | 40 | #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 41 | #: core\se\result_table.py:20 42 | msgid "Folder" 43 | msgstr "Dossier" 44 | 45 | #: core\me\result_table.py:21 46 | msgid "Size (MB)" 47 | msgstr "Taille (MB)" 48 | 49 | #: core\me\result_table.py:22 50 | msgid "Time" 51 | msgstr "Temps" 52 | 53 | #: core\me\result_table.py:24 54 | msgid "Sample Rate" 55 | msgstr "Sample Rate" 56 | 57 | #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 58 | #: core\se\result_table.py:22 59 | msgid "Kind" 60 | msgstr "Type" 61 | 62 | #: core\me\result_table.py:26 core\pe\result_table.py:25 63 | #: core\prioritize.py:163 core\se\result_table.py:23 64 | msgid "Modification" 65 | msgstr "Modification" 66 | 67 | #: core\me\result_table.py:27 68 | msgid "Title" 69 | msgstr "Titre" 70 | 71 | #: core\me\result_table.py:28 72 | msgid "Artist" 73 | msgstr "Artiste" 74 | 75 | #: core\me\result_table.py:29 76 | msgid "Album" 77 | msgstr "Album" 78 | 79 | #: core\me\result_table.py:30 80 | msgid "Genre" 81 | msgstr "Genre" 82 | 83 | #: core\me\result_table.py:31 84 | msgid "Year" 85 | msgstr "Année" 86 | 87 | #: core\me\result_table.py:32 88 | msgid "Track Number" 89 | msgstr "Track" 90 | 91 | #: core\me\result_table.py:33 92 | msgid "Comment" 93 | msgstr "Commentaire" 94 | 95 | #: core\me\result_table.py:34 core\pe\result_table.py:26 96 | #: core\se\result_table.py:24 97 | msgid "Match %" 98 | msgstr "Match %" 99 | 100 | #: core\me\result_table.py:35 core\se\result_table.py:25 101 | msgid "Words Used" 102 | msgstr "Mots" 103 | 104 | #: core\me\result_table.py:36 core\pe\result_table.py:27 105 | #: core\se\result_table.py:26 106 | msgid "Dupe Count" 107 | msgstr "Nombre de Doublons" 108 | 109 | #: core\pe\prioritize.py:23 core\pe\result_table.py:23 110 | msgid "Dimensions" 111 | msgstr "Dimensions" 112 | 113 | #: core\pe\result_table.py:21 core\se\result_table.py:21 114 | msgid "Size (KB)" 115 | msgstr "Taille (KB)" 116 | 117 | #: core\pe\result_table.py:24 118 | msgid "EXIF Timestamp" 119 | msgstr "Date EXIF" 120 | 121 | #: core\prioritize.py:156 122 | msgid "Size" 123 | msgstr "Taille" 124 | -------------------------------------------------------------------------------- /locale/hy/LC_MESSAGES/columns.po: -------------------------------------------------------------------------------- 1 | # Translators: 2 | # Andrew Senetar , 2021 3 | # Fuan , 2021 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Last-Translator: Fuan , 2021\n" 8 | "Language-Team: Armenian (https://www.transifex.com/voltaicideas/teams/116153/hy/)\n" 9 | "Language: hy\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: utf-8\n" 12 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 13 | 14 | #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 15 | #: core\gui\problem_table.py:18 16 | msgid "File Path" 17 | msgstr "Ֆայլի ճ-ը" 18 | 19 | #: core\gui\problem_table.py:19 20 | msgid "Error Message" 21 | msgstr "Սխալի գրությունը" 22 | 23 | #: core\me\prioritize.py:23 24 | msgid "Duration" 25 | msgstr "Տևողությունը" 26 | 27 | #: core\me\prioritize.py:30 core\me\result_table.py:23 28 | msgid "Bitrate" 29 | msgstr "Բիթրեյթը" 30 | 31 | #: core\me\prioritize.py:37 32 | msgid "Samplerate" 33 | msgstr "Սիմպլրեյթը" 34 | 35 | #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92 36 | #: core\se\result_table.py:19 37 | msgid "Filename" 38 | msgstr "Ֆայլի անունը" 39 | 40 | #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 41 | #: core\se\result_table.py:20 42 | msgid "Folder" 43 | msgstr "Թղթապանակ" 44 | 45 | #: core\me\result_table.py:21 46 | msgid "Size (MB)" 47 | msgstr "Չափը (ՄԲ)" 48 | 49 | #: core\me\result_table.py:22 50 | msgid "Time" 51 | msgstr "Ժամանակը" 52 | 53 | #: core\me\result_table.py:24 54 | msgid "Sample Rate" 55 | msgstr "Սեմփլրեյթը" 56 | 57 | #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 58 | #: core\se\result_table.py:22 59 | msgid "Kind" 60 | msgstr "Տեսակ" 61 | 62 | #: core\me\result_table.py:26 core\pe\result_table.py:25 63 | #: core\prioritize.py:163 core\se\result_table.py:23 64 | msgid "Modification" 65 | msgstr "Փոփոխությունը" 66 | 67 | #: core\me\result_table.py:27 68 | msgid "Title" 69 | msgstr "Անունը" 70 | 71 | #: core\me\result_table.py:28 72 | msgid "Artist" 73 | msgstr "Կատարողը" 74 | 75 | #: core\me\result_table.py:29 76 | msgid "Album" 77 | msgstr "Ալբոմը" 78 | 79 | #: core\me\result_table.py:30 80 | msgid "Genre" 81 | msgstr "Ժանրը" 82 | 83 | #: core\me\result_table.py:31 84 | msgid "Year" 85 | msgstr "Տարին" 86 | 87 | #: core\me\result_table.py:32 88 | msgid "Track Number" 89 | msgstr "Շավիղի համարը" 90 | 91 | #: core\me\result_table.py:33 92 | msgid "Comment" 93 | msgstr "Մեկնաբանություն" 94 | 95 | #: core\me\result_table.py:34 core\pe\result_table.py:26 96 | #: core\se\result_table.py:24 97 | msgid "Match %" 98 | msgstr "Համընկնում %-ին" 99 | 100 | #: core\me\result_table.py:35 core\se\result_table.py:25 101 | msgid "Words Used" 102 | msgstr "Բառ է օգտ." 103 | 104 | #: core\me\result_table.py:36 core\pe\result_table.py:27 105 | #: core\se\result_table.py:26 106 | msgid "Dupe Count" 107 | msgstr "Խաբկանքի ք-ը" 108 | 109 | #: core\pe\prioritize.py:23 core\pe\result_table.py:23 110 | msgid "Dimensions" 111 | msgstr "Չափերը" 112 | 113 | #: core\pe\result_table.py:21 core\se\result_table.py:21 114 | msgid "Size (KB)" 115 | msgstr "Չափը (ԿԲ)" 116 | 117 | #: core\pe\result_table.py:24 118 | msgid "EXIF Timestamp" 119 | msgstr "EXIF Timestamp" 120 | 121 | #: core\prioritize.py:156 122 | msgid "Size" 123 | msgstr "Չափը" 124 | -------------------------------------------------------------------------------- /locale/it/LC_MESSAGES/columns.po: -------------------------------------------------------------------------------- 1 | # Translators: 2 | # Andrew Senetar , 2021 3 | # 4 | msgid "" 5 | msgstr "" 6 | "Last-Translator: Andrew Senetar , 2021\n" 7 | "Language-Team: Italian (https://www.transifex.com/voltaicideas/teams/116153/it/)\n" 8 | "Language: it\n" 9 | "Content-Type: text/plain; charset=UTF-8\n" 10 | "Content-Transfer-Encoding: utf-8\n" 11 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 12 | 13 | #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 14 | #: core\gui\problem_table.py:18 15 | msgid "File Path" 16 | msgstr "Percorso del file" 17 | 18 | #: core\gui\problem_table.py:19 19 | msgid "Error Message" 20 | msgstr "Messaggio di errore" 21 | 22 | #: core\me\prioritize.py:23 23 | msgid "Duration" 24 | msgstr "Durata" 25 | 26 | #: core\me\prioritize.py:30 core\me\result_table.py:23 27 | msgid "Bitrate" 28 | msgstr "Bitrate" 29 | 30 | #: core\me\prioritize.py:37 31 | msgid "Samplerate" 32 | msgstr "Campionamento" 33 | 34 | #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92 35 | #: core\se\result_table.py:19 36 | msgid "Filename" 37 | msgstr "Nome del file" 38 | 39 | #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 40 | #: core\se\result_table.py:20 41 | msgid "Folder" 42 | msgstr "Cartella" 43 | 44 | #: core\me\result_table.py:21 45 | msgid "Size (MB)" 46 | msgstr "Dimensione (MB)" 47 | 48 | #: core\me\result_table.py:22 49 | msgid "Time" 50 | msgstr "Tempo" 51 | 52 | #: core\me\result_table.py:24 53 | msgid "Sample Rate" 54 | msgstr "Campionamento" 55 | 56 | #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 57 | #: core\se\result_table.py:22 58 | msgid "Kind" 59 | msgstr "Tipo" 60 | 61 | #: core\me\result_table.py:26 core\pe\result_table.py:25 62 | #: core\prioritize.py:163 core\se\result_table.py:23 63 | msgid "Modification" 64 | msgstr "Modificato" 65 | 66 | #: core\me\result_table.py:27 67 | msgid "Title" 68 | msgstr "Titolo" 69 | 70 | #: core\me\result_table.py:28 71 | msgid "Artist" 72 | msgstr "Artista" 73 | 74 | #: core\me\result_table.py:29 75 | msgid "Album" 76 | msgstr "Album" 77 | 78 | #: core\me\result_table.py:30 79 | msgid "Genre" 80 | msgstr "Genere" 81 | 82 | #: core\me\result_table.py:31 83 | msgid "Year" 84 | msgstr "Anno" 85 | 86 | #: core\me\result_table.py:32 87 | msgid "Track Number" 88 | msgstr "Numero traccia" 89 | 90 | #: core\me\result_table.py:33 91 | msgid "Comment" 92 | msgstr "Commento" 93 | 94 | #: core\me\result_table.py:34 core\pe\result_table.py:26 95 | #: core\se\result_table.py:24 96 | msgid "Match %" 97 | msgstr "Somiglianza %" 98 | 99 | #: core\me\result_table.py:35 core\se\result_table.py:25 100 | msgid "Words Used" 101 | msgstr "Parole usate" 102 | 103 | #: core\me\result_table.py:36 core\pe\result_table.py:27 104 | #: core\se\result_table.py:26 105 | msgid "Dupe Count" 106 | msgstr "Conteggio duplicati" 107 | 108 | #: core\pe\prioritize.py:23 core\pe\result_table.py:23 109 | msgid "Dimensions" 110 | msgstr "Dimensioni" 111 | 112 | #: core\pe\result_table.py:21 core\se\result_table.py:21 113 | msgid "Size (KB)" 114 | msgstr "Dimensione (KB)" 115 | 116 | #: core\pe\result_table.py:24 117 | msgid "EXIF Timestamp" 118 | msgstr "Data EXIF" 119 | 120 | #: core\prioritize.py:156 121 | msgid "Size" 122 | msgstr "Dimensione" 123 | -------------------------------------------------------------------------------- /locale/ja/LC_MESSAGES/columns.po: -------------------------------------------------------------------------------- 1 | # Translators: 2 | # Fuan , 2021 3 | # 4 | msgid "" 5 | msgstr "" 6 | "Last-Translator: Fuan , 2021\n" 7 | "Language-Team: Japanese (https://www.transifex.com/voltaicideas/teams/116153/ja/)\n" 8 | "Language: ja\n" 9 | "Content-Type: text/plain; charset=UTF-8\n" 10 | "Content-Transfer-Encoding: utf-8\n" 11 | "Plural-Forms: nplurals=1; plural=0;\n" 12 | 13 | #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 14 | #: core\gui\problem_table.py:18 15 | msgid "File Path" 16 | msgstr "ファイルパス" 17 | 18 | #: core\gui\problem_table.py:19 19 | msgid "Error Message" 20 | msgstr "エラーメッセージ" 21 | 22 | #: core\me\prioritize.py:23 23 | msgid "Duration" 24 | msgstr "デュレーション" 25 | 26 | #: core\me\prioritize.py:30 core\me\result_table.py:23 27 | msgid "Bitrate" 28 | msgstr "ビットレート" 29 | 30 | #: core\me\prioritize.py:37 31 | msgid "Samplerate" 32 | msgstr "サンプルレート" 33 | 34 | #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92 35 | #: core\se\result_table.py:19 36 | msgid "Filename" 37 | msgstr "ファイル名" 38 | 39 | #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 40 | #: core\se\result_table.py:20 41 | msgid "Folder" 42 | msgstr "フォルダ" 43 | 44 | #: core\me\result_table.py:21 45 | msgid "Size (MB)" 46 | msgstr "サイズ(MB)" 47 | 48 | #: core\me\result_table.py:22 49 | msgid "Time" 50 | msgstr "デュレーション" 51 | 52 | #: core\me\result_table.py:24 53 | msgid "Sample Rate" 54 | msgstr "サンプルレート" 55 | 56 | #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 57 | #: core\se\result_table.py:22 58 | msgid "Kind" 59 | msgstr "種類" 60 | 61 | #: core\me\result_table.py:26 core\pe\result_table.py:25 62 | #: core\prioritize.py:163 core\se\result_table.py:23 63 | msgid "Modification" 64 | msgstr "変更" 65 | 66 | #: core\me\result_table.py:27 67 | msgid "Title" 68 | msgstr "タイトル" 69 | 70 | #: core\me\result_table.py:28 71 | msgid "Artist" 72 | msgstr "アーティスト" 73 | 74 | #: core\me\result_table.py:29 75 | msgid "Album" 76 | msgstr "アルバム" 77 | 78 | #: core\me\result_table.py:30 79 | msgid "Genre" 80 | msgstr "ジャンル" 81 | 82 | #: core\me\result_table.py:31 83 | msgid "Year" 84 | msgstr "年" 85 | 86 | #: core\me\result_table.py:32 87 | msgid "Track Number" 88 | msgstr "トラック番号" 89 | 90 | #: core\me\result_table.py:33 91 | msgid "Comment" 92 | msgstr "コメント" 93 | 94 | #: core\me\result_table.py:34 core\pe\result_table.py:26 95 | #: core\se\result_table.py:24 96 | msgid "Match %" 97 | msgstr "一致率" 98 | 99 | #: core\me\result_table.py:35 core\se\result_table.py:25 100 | msgid "Words Used" 101 | msgstr "使用した単語" 102 | 103 | #: core\me\result_table.py:36 core\pe\result_table.py:27 104 | #: core\se\result_table.py:26 105 | msgid "Dupe Count" 106 | msgstr "重複カウント" 107 | 108 | #: core\pe\prioritize.py:23 core\pe\result_table.py:23 109 | msgid "Dimensions" 110 | msgstr "寸法" 111 | 112 | #: core\pe\result_table.py:21 core\se\result_table.py:21 113 | msgid "Size (KB)" 114 | msgstr "サイズ(KB)" 115 | 116 | #: core\pe\result_table.py:24 117 | msgid "EXIF Timestamp" 118 | msgstr "EXIFタイムスタンプ" 119 | 120 | #: core\prioritize.py:156 121 | msgid "Size" 122 | msgstr "サイズ" 123 | -------------------------------------------------------------------------------- /locale/ko/LC_MESSAGES/columns.po: -------------------------------------------------------------------------------- 1 | # Translators: 2 | # Sangdon Lim, 2022 3 | # Andrew Senetar , 2023 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Last-Translator: Andrew Senetar , 2023\n" 8 | "Language-Team: Korean (https://app.transifex.com/voltaicideas/teams/116153/ko/)\n" 9 | "Language: ko\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: utf-8\n" 12 | "Plural-Forms: nplurals=1; plural=0;\n" 13 | 14 | #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 15 | #: core\gui\problem_table.py:18 16 | msgid "File Path" 17 | msgstr "파일 경로" 18 | 19 | #: core\gui\problem_table.py:19 20 | msgid "Error Message" 21 | msgstr "오류 메시지" 22 | 23 | #: core\me\prioritize.py:23 24 | msgid "Duration" 25 | msgstr "길이" 26 | 27 | #: core\me\prioritize.py:30 core\me\result_table.py:23 28 | msgid "Bitrate" 29 | msgstr "비트전송률" 30 | 31 | #: core\me\prioritize.py:37 32 | msgid "Samplerate" 33 | msgstr "샘플전송률" 34 | 35 | #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:94 36 | #: core\se\result_table.py:19 37 | msgid "Filename" 38 | msgstr "파일 이름" 39 | 40 | #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 41 | #: core\se\result_table.py:20 42 | msgid "Folder" 43 | msgstr "폴더" 44 | 45 | #: core\me\result_table.py:21 46 | msgid "Size (MB)" 47 | msgstr "크기 (MB)" 48 | 49 | #: core\me\result_table.py:22 50 | msgid "Time" 51 | msgstr "시간" 52 | 53 | #: core\me\result_table.py:24 54 | msgid "Sample Rate" 55 | msgstr "샘플전송률" 56 | 57 | #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 58 | #: core\se\result_table.py:22 59 | msgid "Kind" 60 | msgstr "종류" 61 | 62 | #: core\me\result_table.py:26 core\pe\result_table.py:25 63 | #: core\prioritize.py:165 core\se\result_table.py:23 64 | msgid "Modification" 65 | msgstr "수정날짜" 66 | 67 | #: core\me\result_table.py:27 68 | msgid "Title" 69 | msgstr "곡명" 70 | 71 | #: core\me\result_table.py:28 72 | msgid "Artist" 73 | msgstr "아티스트" 74 | 75 | #: core\me\result_table.py:29 76 | msgid "Album" 77 | msgstr "앨범" 78 | 79 | #: core\me\result_table.py:30 80 | msgid "Genre" 81 | msgstr "장르" 82 | 83 | #: core\me\result_table.py:31 84 | msgid "Year" 85 | msgstr "년도" 86 | 87 | #: core\me\result_table.py:32 88 | msgid "Track Number" 89 | msgstr "트랙 번호" 90 | 91 | #: core\me\result_table.py:33 92 | msgid "Comment" 93 | msgstr "주석" 94 | 95 | #: core\me\result_table.py:34 core\pe\result_table.py:26 96 | #: core\se\result_table.py:24 97 | msgid "Match %" 98 | msgstr "일치율%" 99 | 100 | #: core\me\result_table.py:35 core\se\result_table.py:25 101 | msgid "Words Used" 102 | msgstr "단어 목록" 103 | 104 | #: core\me\result_table.py:36 core\pe\result_table.py:27 105 | #: core\se\result_table.py:26 106 | msgid "Dupe Count" 107 | msgstr "중복파일 갯수" 108 | 109 | #: core\pe\prioritize.py:23 core\pe\result_table.py:23 110 | msgid "Dimensions" 111 | msgstr "가로세로 크기" 112 | 113 | #: core\pe\result_table.py:21 core\se\result_table.py:21 114 | msgid "Size (KB)" 115 | msgstr "크기 (KB)" 116 | 117 | #: core\pe\result_table.py:24 118 | msgid "EXIF Timestamp" 119 | msgstr "EXIF 타임스탬프" 120 | 121 | #: core\prioritize.py:158 122 | msgid "Size" 123 | msgstr "크기" 124 | -------------------------------------------------------------------------------- /locale/nl/LC_MESSAGES/columns.po: -------------------------------------------------------------------------------- 1 | # Translators: 2 | # Andrew Senetar , 2021 3 | # Bas , 2021 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Last-Translator: Bas , 2021\n" 8 | "Language-Team: Dutch (https://www.transifex.com/voltaicideas/teams/116153/nl/)\n" 9 | "Language: nl\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: utf-8\n" 12 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 13 | 14 | #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 15 | #: core\gui\problem_table.py:18 16 | msgid "File Path" 17 | msgstr "Bestandspad" 18 | 19 | #: core\gui\problem_table.py:19 20 | msgid "Error Message" 21 | msgstr "Foutmelding" 22 | 23 | #: core\me\prioritize.py:23 24 | msgid "Duration" 25 | msgstr "Tijdsduur" 26 | 27 | #: core\me\prioritize.py:30 core\me\result_table.py:23 28 | msgid "Bitrate" 29 | msgstr "Bitrate" 30 | 31 | #: core\me\prioritize.py:37 32 | msgid "Samplerate" 33 | msgstr "Sample frequentie" 34 | 35 | #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92 36 | #: core\se\result_table.py:19 37 | msgid "Filename" 38 | msgstr "Bestandsnaam" 39 | 40 | #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 41 | #: core\se\result_table.py:20 42 | msgid "Folder" 43 | msgstr "Map" 44 | 45 | #: core\me\result_table.py:21 46 | msgid "Size (MB)" 47 | msgstr "Grootte (MB)" 48 | 49 | #: core\me\result_table.py:22 50 | msgid "Time" 51 | msgstr "Tijd" 52 | 53 | #: core\me\result_table.py:24 54 | msgid "Sample Rate" 55 | msgstr "Sample Frequentie" 56 | 57 | #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 58 | #: core\se\result_table.py:22 59 | msgid "Kind" 60 | msgstr "Soort" 61 | 62 | #: core\me\result_table.py:26 core\pe\result_table.py:25 63 | #: core\prioritize.py:163 core\se\result_table.py:23 64 | msgid "Modification" 65 | msgstr "Aanpassing" 66 | 67 | #: core\me\result_table.py:27 68 | msgid "Title" 69 | msgstr "Titel" 70 | 71 | #: core\me\result_table.py:28 72 | msgid "Artist" 73 | msgstr "Artiest" 74 | 75 | #: core\me\result_table.py:29 76 | msgid "Album" 77 | msgstr "Album" 78 | 79 | #: core\me\result_table.py:30 80 | msgid "Genre" 81 | msgstr "Genre" 82 | 83 | #: core\me\result_table.py:31 84 | msgid "Year" 85 | msgstr "Jaar" 86 | 87 | #: core\me\result_table.py:32 88 | msgid "Track Number" 89 | msgstr "Track nummer" 90 | 91 | #: core\me\result_table.py:33 92 | msgid "Comment" 93 | msgstr "Commentaar" 94 | 95 | #: core\me\result_table.py:34 core\pe\result_table.py:26 96 | #: core\se\result_table.py:24 97 | msgid "Match %" 98 | msgstr "Zekerheid %" 99 | 100 | #: core\me\result_table.py:35 core\se\result_table.py:25 101 | msgid "Words Used" 102 | msgstr "Woorden gebruikt" 103 | 104 | #: core\me\result_table.py:36 core\pe\result_table.py:27 105 | #: core\se\result_table.py:26 106 | msgid "Dupe Count" 107 | msgstr "Dubbel telling" 108 | 109 | #: core\pe\prioritize.py:23 core\pe\result_table.py:23 110 | msgid "Dimensions" 111 | msgstr "Afmetingen" 112 | 113 | #: core\pe\result_table.py:21 core\se\result_table.py:21 114 | msgid "Size (KB)" 115 | msgstr "Grootte (KB)" 116 | 117 | #: core\pe\result_table.py:24 118 | msgid "EXIF Timestamp" 119 | msgstr "EXIF Tijdstip" 120 | 121 | #: core\prioritize.py:156 122 | msgid "Size" 123 | msgstr "Grootte" 124 | -------------------------------------------------------------------------------- /locale/pt_BR/LC_MESSAGES/columns.po: -------------------------------------------------------------------------------- 1 | # Translators: 2 | # Andrew Senetar , 2021 3 | # 4 | msgid "" 5 | msgstr "" 6 | "Last-Translator: Andrew Senetar , 2021\n" 7 | "Language-Team: Portuguese (Brazil) (https://www.transifex.com/voltaicideas/teams/116153/pt_BR/)\n" 8 | "Language: pt_BR\n" 9 | "Content-Type: text/plain; charset=UTF-8\n" 10 | "Content-Transfer-Encoding: utf-8\n" 11 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 12 | 13 | #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 14 | #: core\gui\problem_table.py:18 15 | msgid "File Path" 16 | msgstr "Caminho" 17 | 18 | #: core\gui\problem_table.py:19 19 | msgid "Error Message" 20 | msgstr "Mensagem de Erro" 21 | 22 | #: core\me\prioritize.py:23 23 | msgid "Duration" 24 | msgstr "Duração" 25 | 26 | #: core\me\prioritize.py:30 core\me\result_table.py:23 27 | msgid "Bitrate" 28 | msgstr "Taxa de Bits" 29 | 30 | #: core\me\prioritize.py:37 31 | msgid "Samplerate" 32 | msgstr "Amostragem" 33 | 34 | #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92 35 | #: core\se\result_table.py:19 36 | msgid "Filename" 37 | msgstr "Nome do Arquivo" 38 | 39 | #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 40 | #: core\se\result_table.py:20 41 | msgid "Folder" 42 | msgstr "Pasta" 43 | 44 | #: core\me\result_table.py:21 45 | msgid "Size (MB)" 46 | msgstr "Tamanho" 47 | 48 | #: core\me\result_table.py:22 49 | msgid "Time" 50 | msgstr "Duração" 51 | 52 | #: core\me\result_table.py:24 53 | msgid "Sample Rate" 54 | msgstr "Tamanho da Amostra" 55 | 56 | #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 57 | #: core\se\result_table.py:22 58 | msgid "Kind" 59 | msgstr "Tipo" 60 | 61 | #: core\me\result_table.py:26 core\pe\result_table.py:25 62 | #: core\prioritize.py:163 core\se\result_table.py:23 63 | msgid "Modification" 64 | msgstr "Modificado" 65 | 66 | #: core\me\result_table.py:27 67 | msgid "Title" 68 | msgstr "Nome" 69 | 70 | #: core\me\result_table.py:28 71 | msgid "Artist" 72 | msgstr "Artista" 73 | 74 | #: core\me\result_table.py:29 75 | msgid "Album" 76 | msgstr "Álbum" 77 | 78 | #: core\me\result_table.py:30 79 | msgid "Genre" 80 | msgstr "Gênero" 81 | 82 | #: core\me\result_table.py:31 83 | msgid "Year" 84 | msgstr "Ano" 85 | 86 | #: core\me\result_table.py:32 87 | msgid "Track Number" 88 | msgstr "Número da Faixa" 89 | 90 | #: core\me\result_table.py:33 91 | msgid "Comment" 92 | msgstr "Comentário" 93 | 94 | #: core\me\result_table.py:34 core\pe\result_table.py:26 95 | #: core\se\result_table.py:24 96 | msgid "Match %" 97 | msgstr "% Precisão" 98 | 99 | #: core\me\result_table.py:35 core\se\result_table.py:25 100 | msgid "Words Used" 101 | msgstr "Palavras Usadas" 102 | 103 | #: core\me\result_table.py:36 core\pe\result_table.py:27 104 | #: core\se\result_table.py:26 105 | msgid "Dupe Count" 106 | msgstr "Duplicatas" 107 | 108 | #: core\pe\prioritize.py:23 core\pe\result_table.py:23 109 | msgid "Dimensions" 110 | msgstr "Dimensões" 111 | 112 | #: core\pe\result_table.py:21 core\se\result_table.py:21 113 | msgid "Size (KB)" 114 | msgstr "Tamanho" 115 | 116 | #: core\pe\result_table.py:24 117 | msgid "EXIF Timestamp" 118 | msgstr "Timestamp EXIF" 119 | 120 | #: core\prioritize.py:156 121 | msgid "Size" 122 | msgstr "Tamanho" 123 | -------------------------------------------------------------------------------- /locale/vi/LC_MESSAGES/columns.po: -------------------------------------------------------------------------------- 1 | # Translators: 2 | # Andrew Senetar , 2021 3 | # 4 | msgid "" 5 | msgstr "" 6 | "Last-Translator: Andrew Senetar , 2021\n" 7 | "Language-Team: Vietnamese (https://www.transifex.com/voltaicideas/teams/116153/vi/)\n" 8 | "Language: vi\n" 9 | "Content-Type: text/plain; charset=UTF-8\n" 10 | "Content-Transfer-Encoding: utf-8\n" 11 | "Plural-Forms: nplurals=1; plural=0;\n" 12 | 13 | #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 14 | #: core\gui\problem_table.py:18 15 | msgid "File Path" 16 | msgstr "Đường dẫn tập tin" 17 | 18 | #: core\gui\problem_table.py:19 19 | msgid "Error Message" 20 | msgstr "Thông báo lỗi" 21 | 22 | #: core\me\prioritize.py:23 23 | msgid "Duration" 24 | msgstr "Độ dài" 25 | 26 | #: core\me\prioritize.py:30 core\me\result_table.py:23 27 | msgid "Bitrate" 28 | msgstr "Bitrate" 29 | 30 | #: core\me\prioritize.py:37 31 | msgid "Samplerate" 32 | msgstr "Samplerate" 33 | 34 | #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92 35 | #: core\se\result_table.py:19 36 | msgid "Filename" 37 | msgstr "Tên tập tin" 38 | 39 | #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 40 | #: core\se\result_table.py:20 41 | msgid "Folder" 42 | msgstr "Thư mục" 43 | 44 | #: core\me\result_table.py:21 45 | msgid "Size (MB)" 46 | msgstr "Kích thước (MB)" 47 | 48 | #: core\me\result_table.py:22 49 | msgid "Time" 50 | msgstr "Thời gian" 51 | 52 | #: core\me\result_table.py:24 53 | msgid "Sample Rate" 54 | msgstr "Sample Rate" 55 | 56 | #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 57 | #: core\se\result_table.py:22 58 | msgid "Kind" 59 | msgstr "Loại" 60 | 61 | #: core\me\result_table.py:26 core\pe\result_table.py:25 62 | #: core\prioritize.py:163 core\se\result_table.py:23 63 | msgid "Modification" 64 | msgstr "Chỉnh sửa" 65 | 66 | #: core\me\result_table.py:27 67 | msgid "Title" 68 | msgstr "Tiêu đề" 69 | 70 | #: core\me\result_table.py:28 71 | msgid "Artist" 72 | msgstr "Nghệ sĩ" 73 | 74 | #: core\me\result_table.py:29 75 | msgid "Album" 76 | msgstr "Album" 77 | 78 | #: core\me\result_table.py:30 79 | msgid "Genre" 80 | msgstr "Loại nhạc" 81 | 82 | #: core\me\result_table.py:31 83 | msgid "Year" 84 | msgstr "Năm" 85 | 86 | #: core\me\result_table.py:32 87 | msgid "Track Number" 88 | msgstr "Số track" 89 | 90 | #: core\me\result_table.py:33 91 | msgid "Comment" 92 | msgstr "Bình luận" 93 | 94 | #: core\me\result_table.py:34 core\pe\result_table.py:26 95 | #: core\se\result_table.py:24 96 | msgid "Match %" 97 | msgstr "Tỉ lệ khớp %" 98 | 99 | #: core\me\result_table.py:35 core\se\result_table.py:25 100 | msgid "Words Used" 101 | msgstr "Từ được dùng" 102 | 103 | #: core\me\result_table.py:36 core\pe\result_table.py:27 104 | #: core\se\result_table.py:26 105 | msgid "Dupe Count" 106 | msgstr "Số lần bị lừa" 107 | 108 | #: core\pe\prioritize.py:23 core\pe\result_table.py:23 109 | msgid "Dimensions" 110 | msgstr "Chiều" 111 | 112 | #: core\pe\result_table.py:21 core\se\result_table.py:21 113 | msgid "Size (KB)" 114 | msgstr "Kích thước (KB)" 115 | 116 | #: core\pe\result_table.py:24 117 | msgid "EXIF Timestamp" 118 | msgstr "EXIF Timestamp" 119 | 120 | #: core\prioritize.py:156 121 | msgid "Size" 122 | msgstr "Kích thước" 123 | -------------------------------------------------------------------------------- /locale/zh_CN/LC_MESSAGES/columns.po: -------------------------------------------------------------------------------- 1 | # Translators: 2 | # Andrew Senetar , 2021 3 | # Chris Ocelot, 2021 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Last-Translator: Chris Ocelot, 2021\n" 8 | "Language-Team: Chinese (China) (https://www.transifex.com/voltaicideas/teams/116153/zh_CN/)\n" 9 | "Language: zh_CN\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: utf-8\n" 12 | "Plural-Forms: nplurals=1; plural=0;\n" 13 | 14 | #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 15 | #: core\gui\problem_table.py:18 16 | msgid "File Path" 17 | msgstr "文件路径" 18 | 19 | #: core\gui\problem_table.py:19 20 | msgid "Error Message" 21 | msgstr "错误信息" 22 | 23 | #: core\me\prioritize.py:23 24 | msgid "Duration" 25 | msgstr "持续时间" 26 | 27 | #: core\me\prioritize.py:30 core\me\result_table.py:23 28 | msgid "Bitrate" 29 | msgstr "比特率" 30 | 31 | #: core\me\prioritize.py:37 32 | msgid "Samplerate" 33 | msgstr "采样率" 34 | 35 | #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92 36 | #: core\se\result_table.py:19 37 | msgid "Filename" 38 | msgstr "文件名称" 39 | 40 | #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 41 | #: core\se\result_table.py:20 42 | msgid "Folder" 43 | msgstr "文件夹" 44 | 45 | #: core\me\result_table.py:21 46 | msgid "Size (MB)" 47 | msgstr "大小 (MB)" 48 | 49 | #: core\me\result_table.py:22 50 | msgid "Time" 51 | msgstr "时间" 52 | 53 | #: core\me\result_table.py:24 54 | msgid "Sample Rate" 55 | msgstr "采样率" 56 | 57 | #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 58 | #: core\se\result_table.py:22 59 | msgid "Kind" 60 | msgstr "类型" 61 | 62 | #: core\me\result_table.py:26 core\pe\result_table.py:25 63 | #: core\prioritize.py:163 core\se\result_table.py:23 64 | msgid "Modification" 65 | msgstr "编辑日期" 66 | 67 | #: core\me\result_table.py:27 68 | msgid "Title" 69 | msgstr "歌曲名" 70 | 71 | #: core\me\result_table.py:28 72 | msgid "Artist" 73 | msgstr "作者" 74 | 75 | #: core\me\result_table.py:29 76 | msgid "Album" 77 | msgstr "专辑" 78 | 79 | #: core\me\result_table.py:30 80 | msgid "Genre" 81 | msgstr "音乐类型" 82 | 83 | #: core\me\result_table.py:31 84 | msgid "Year" 85 | msgstr "年" 86 | 87 | #: core\me\result_table.py:32 88 | msgid "Track Number" 89 | msgstr "音轨号" 90 | 91 | #: core\me\result_table.py:33 92 | msgid "Comment" 93 | msgstr "注释" 94 | 95 | #: core\me\result_table.py:34 core\pe\result_table.py:26 96 | #: core\se\result_table.py:24 97 | msgid "Match %" 98 | msgstr "匹配度 %" 99 | 100 | #: core\me\result_table.py:35 core\se\result_table.py:25 101 | msgid "Words Used" 102 | msgstr "使用过的词语" 103 | 104 | #: core\me\result_table.py:36 core\pe\result_table.py:27 105 | #: core\se\result_table.py:26 106 | msgid "Dupe Count" 107 | msgstr "重复文件数" 108 | 109 | #: core\pe\prioritize.py:23 core\pe\result_table.py:23 110 | msgid "Dimensions" 111 | msgstr "规格" 112 | 113 | #: core\pe\result_table.py:21 core\se\result_table.py:21 114 | msgid "Size (KB)" 115 | msgstr "大小 (KB)" 116 | 117 | #: core\pe\result_table.py:24 118 | msgid "EXIF Timestamp" 119 | msgstr "EXIF 时间戳" 120 | 121 | #: core\prioritize.py:156 122 | msgid "Size" 123 | msgstr "大小" 124 | -------------------------------------------------------------------------------- /locale/zh_TW/LC_MESSAGES/columns.po: -------------------------------------------------------------------------------- 1 | # Translators: 2 | # Chris Ocelot, 2022 3 | # Andrew Senetar , 2022 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Last-Translator: Andrew Senetar , 2022\n" 8 | "Language-Team: Chinese (Taiwan) (https://www.transifex.com/voltaicideas/teams/116153/zh_TW/)\n" 9 | "Language: zh_TW\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: utf-8\n" 12 | "Plural-Forms: nplurals=1; plural=0;\n" 13 | 14 | #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 15 | #: core\gui\problem_table.py:18 16 | msgid "File Path" 17 | msgstr "文件路径" 18 | 19 | #: core\gui\problem_table.py:19 20 | msgid "Error Message" 21 | msgstr "错误信息" 22 | 23 | #: core\me\prioritize.py:23 24 | msgid "Duration" 25 | msgstr "持续时间" 26 | 27 | #: core\me\prioritize.py:30 core\me\result_table.py:23 28 | msgid "Bitrate" 29 | msgstr "比特率" 30 | 31 | #: core\me\prioritize.py:37 32 | msgid "Samplerate" 33 | msgstr "采样率" 34 | 35 | #: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:94 36 | #: core\se\result_table.py:19 37 | msgid "Filename" 38 | msgstr "文件名" 39 | 40 | #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 41 | #: core\se\result_table.py:20 42 | msgid "Folder" 43 | msgstr "文件夹" 44 | 45 | #: core\me\result_table.py:21 46 | msgid "Size (MB)" 47 | msgstr "大小 (MB)" 48 | 49 | #: core\me\result_table.py:22 50 | msgid "Time" 51 | msgstr "时间" 52 | 53 | #: core\me\result_table.py:24 54 | msgid "Sample Rate" 55 | msgstr "采样率" 56 | 57 | #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 58 | #: core\se\result_table.py:22 59 | msgid "Kind" 60 | msgstr "类型" 61 | 62 | #: core\me\result_table.py:26 core\pe\result_table.py:25 63 | #: core\prioritize.py:165 core\se\result_table.py:23 64 | msgid "Modification" 65 | msgstr "编辑日期" 66 | 67 | #: core\me\result_table.py:27 68 | msgid "Title" 69 | msgstr "歌曲名" 70 | 71 | #: core\me\result_table.py:28 72 | msgid "Artist" 73 | msgstr "作者" 74 | 75 | #: core\me\result_table.py:29 76 | msgid "Album" 77 | msgstr "专辑" 78 | 79 | #: core\me\result_table.py:30 80 | msgid "Genre" 81 | msgstr "音乐类型" 82 | 83 | #: core\me\result_table.py:31 84 | msgid "Year" 85 | msgstr "年" 86 | 87 | #: core\me\result_table.py:32 88 | msgid "Track Number" 89 | msgstr "音轨号" 90 | 91 | #: core\me\result_table.py:33 92 | msgid "Comment" 93 | msgstr "注释" 94 | 95 | #: core\me\result_table.py:34 core\pe\result_table.py:26 96 | #: core\se\result_table.py:24 97 | msgid "Match %" 98 | msgstr "匹配度 %" 99 | 100 | #: core\me\result_table.py:35 core\se\result_table.py:25 101 | msgid "Words Used" 102 | msgstr "使用过的词语" 103 | 104 | #: core\me\result_table.py:36 core\pe\result_table.py:27 105 | #: core\se\result_table.py:26 106 | msgid "Dupe Count" 107 | msgstr "重复文件数" 108 | 109 | #: core\pe\prioritize.py:23 core\pe\result_table.py:23 110 | msgid "Dimensions" 111 | msgstr "规格" 112 | 113 | #: core\pe\result_table.py:21 core\se\result_table.py:21 114 | msgid "Size (KB)" 115 | msgstr "大小 (KB)" 116 | 117 | #: core\pe\result_table.py:24 118 | msgid "EXIF Timestamp" 119 | msgstr "EXIF 时间戳" 120 | 121 | #: core\prioritize.py:158 122 | msgid "Size" 123 | msgstr "大小" 124 | -------------------------------------------------------------------------------- /macos.md: -------------------------------------------------------------------------------- 1 | ## How to build dupeGuru for macos 2 | These instructions are for the Qt version of the UI on macOS. 3 | 4 | *Note: The Cocoa UI of dupeGuru is hosted in a separate repo: https://github.com/arsenetar/dupeguru-cocoa and is no longer "supported".* 5 | ### Prerequisites 6 | 7 | - [Python 3.7+][python] 8 | - [Xcode 12.3][xcode] or just Xcode command line tools (older versions can be used if not interested in arm macs) 9 | - [Homebrew][homebrew] 10 | - [qt5](https://www.qt.io/) 11 | 12 | #### Prerequisite setup 13 | 1. Install Xcode if desired 14 | 2. Install [Homebrew][homebrew], if not on the path after install (arm based Macs) create `~/.zshrc` 15 | with `export PATH="/opt/homebrew/bin:$PATH"`. Will need to reload terminal or source the file to take 16 | effect. 17 | 3. Install qt5 with `brew`. If you are using a version of macos without system python 3.7+ then you will 18 | also need to install that via brew or with pyenv. 19 | 20 | $ brew install qt5 21 | 22 | NOTE: Using `brew` to install qt5 is to allow pyqt5 to build without a native wheel 23 | available. If you are using an intel based mac you can probably skip this step. 24 | 25 | 4. May need to launch a new terminal to have everything working. 26 | 27 | ### With build.py 28 | OSX comes with a version of python 3 by default in newer versions of OSX. To produce universal 29 | builds either the 3.8 version shipped in macos or 3.9.1 or newer needs to be used. If needing to 30 | build pyqt5 from source then the first line below is needed, else it may be omitted. (Path shown is 31 | for an arm mac.) 32 | 33 | $ export PATH="/opt/homebrew/opt/qt/bin:$PATH" 34 | $ cd 35 | $ python3 -m venv ./env 36 | $ source ./env/bin/activate 37 | $ pip install -r requirements.txt 38 | $ python build.py 39 | $ python run.py 40 | 41 | ### Generate OSX Packages 42 | The extra requirements need to be installed to run packaging: `pip install -r requirements-extra.txt`. 43 | Run the following in the respective virtual environment. 44 | 45 | $ python package.py 46 | 47 | This will produce a dupeGuru.app in the dist folder. 48 | 49 | ### Running tests 50 | The complete test suite can be run with tox just like on linux. NOTE: The extra requirements need to 51 | be installed to run unit tests: `pip install -r requirements-extra.txt`. 52 | 53 | [python]: http://www.python.org/ 54 | [homebrew]: https://brew.sh/ 55 | [xcode]: https://developer.apple.com/xcode/ 56 | -------------------------------------------------------------------------------- /pkg/arch/dupeguru.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name={longname} 3 | Comment=Find duplicate files. 4 | Exec={execname} 5 | Icon={iconpath} 6 | Terminal=false 7 | Type=Application 8 | Categories=Utility; 9 | -------------------------------------------------------------------------------- /pkg/arch/dupeguru.json: -------------------------------------------------------------------------------- 1 | { 2 | "pkgname": "dupeguru", 3 | "longname": "dupeGuru", 4 | "execname": "dupeguru", 5 | "arch": "any", 6 | "iconpath": "dupeguru" 7 | } 8 | -------------------------------------------------------------------------------- /pkg/debian/Makefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | all: 4 | dh_prep 5 | dh_installdirs 6 | touch build_pe_modules.py 7 | python3 build_pe_modules.py 8 | chmod +x src/run.py 9 | cp -R src/ "$(CURDIR)/debian/{pkgname}/usr/share/{execname}" 10 | cp "$(CURDIR)/debian/{execname}.desktop" "$(CURDIR)/debian/{pkgname}/usr/share/applications" 11 | mkdir -p "$(CURDIR)/debian/{pkgname}/usr/share/pixmaps" 12 | ln -s "/usr/share/{execname}/dgse_logo_128.png" "$(CURDIR)/debian/{pkgname}/usr/share/pixmaps/{execname}.png" 13 | ln -s "/usr/share/{execname}/run.py" "$(CURDIR)/debian/{pkgname}/usr/bin/{execname}" 14 | -------------------------------------------------------------------------------- /pkg/debian/build_pe_modules.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import os.path as op 4 | import shutil 5 | import importlib 6 | 7 | from setuptools import setup, Extension 8 | 9 | sys.path.insert(1, op.abspath("src")) 10 | 11 | from hscommon.build import move_all 12 | 13 | exts = [ 14 | Extension("_block", [op.join("modules", "block.c"), op.join("modules", "common.c")]), 15 | Extension("_cache", [op.join("modules", "cache.c"), op.join("modules", "common.c")]), 16 | Extension("_block_qt", [op.join("modules", "block_qt.c")]), 17 | ] 18 | setup( 19 | script_args=["build_ext", "--inplace"], 20 | ext_modules=exts, 21 | ) 22 | move_all("_block_qt*", op.join("src", "qt", "pe")) 23 | move_all("_cache*", op.join("src", "core/pe")) 24 | move_all("_block*", op.join("src", "core/pe")) 25 | -------------------------------------------------------------------------------- /pkg/debian/changelog: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/pkg/debian/changelog -------------------------------------------------------------------------------- /pkg/debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /pkg/debian/control: -------------------------------------------------------------------------------- 1 | Source: {pkgname} 2 | Section: devel 3 | Priority: extra 4 | Maintainer: Virgil Dupras , Eugene San (eugenesan) 5 | Build-Depends: debhelper (>= 7), python3-dev, python3-setuptools 6 | Standards-Version: 3.9.2 7 | Homepage: https://dupeguru.voltaicideas.net 8 | Vcs-Browser: https://github.com/arsenetar/dupeguru 9 | Vcs-Git: https://github.com/arsenetar/dupeguru.git 10 | 11 | Package: {pkgname} 12 | Architecture: {arch} 13 | Depends: ${shlibs:Depends}, python3 (>=3.7), python3 (<<3.12), python3-pyqt5, python3-mutagen, python3-semantic-version 14 | Provides: dupeguru-se, dupeguru-me, dupeguru-pe 15 | Replaces: dupeguru-se, dupeguru-me, dupeguru-pe 16 | Conflicts: dupeguru-se, dupeguru-me, dupeguru-pe 17 | Description: {longname} 18 | dupeGuru is a cross-platform (Linux and OS X) GUI tool to find duplicate files in a system. 19 | It's written mostly in Python 3 and has the peculiarity of using multiple GUI toolkits, 20 | all using the same core Python code. 21 | On OS X, the UI layer is written in Objective-C and uses Cocoa. 22 | On Linux, it's written in Python and uses Qt5. 23 | -------------------------------------------------------------------------------- /pkg/debian/copyright: -------------------------------------------------------------------------------- 1 | Copyright 2014 Hardcoded Software Inc. (http://www.hardcoded.net) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | * Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 9 | * If the source code has been published less than two years ago, any redistribution, in whole or in part, must retain full licensing functionality, without any attempt to change, obscure or in other ways circumvent its intent. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /pkg/debian/dirs: -------------------------------------------------------------------------------- 1 | usr/bin 2 | usr/share 3 | usr/share/applications 4 | -------------------------------------------------------------------------------- /pkg/debian/dupeguru.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name={longname} 3 | Comment=Find duplicate files. 4 | Exec={execname} 5 | Icon={iconpath} 6 | Terminal=false 7 | Type=Application 8 | Categories=Utility; 9 | -------------------------------------------------------------------------------- /pkg/debian/dupeguru.json: -------------------------------------------------------------------------------- 1 | { 2 | "pkgname": "dupeguru", 3 | "longname": "dupeGuru", 4 | "execname": "dupeguru", 5 | "arch": "any", 6 | "iconpath": "dupeguru" 7 | } 8 | -------------------------------------------------------------------------------- /pkg/debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | %: 3 | dh $@ 4 | -------------------------------------------------------------------------------- /pkg/debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /pkg/debian/source/options: -------------------------------------------------------------------------------- 1 | compression = "xz" 2 | -------------------------------------------------------------------------------- /pkg/dupeguru.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=dupeGuru 3 | Comment=Find duplicate files. 4 | Exec=dupeguru 5 | Icon=dupeguru 6 | Terminal=false 7 | Type=Application 8 | Categories=Utility; 9 | Keywords=file manager;gui; 10 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | [tool.black] 5 | line-length = 120 6 | [tool.isort] 7 | # make it compatible with black 8 | profile = "black" 9 | skip_gitignore = true 10 | -------------------------------------------------------------------------------- /qt/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/qt/__init__.py -------------------------------------------------------------------------------- /qt/dg.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | ../images/dgse_logo_32.png 4 | ../images/dgse_logo_128.png 5 | ../images/plus_8.png 6 | ../images/minus_8.png 7 | ../images/search_clear_13.png 8 | ../images/exchange_purple_upscaled.png 9 | ../images/old_zoom_in.png 10 | ../images/old_zoom_out.png 11 | ../images/old_zoom_original.png 12 | ../images/old_zoom_best_fit.png 13 | ../images/dialog-error.png 14 | 15 | 16 | -------------------------------------------------------------------------------- /qt/exclude_list_table.py: -------------------------------------------------------------------------------- 1 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 2 | # which should be included with this package. The terms are also available at 3 | # http://www.gnu.org/licenses/gpl-3.0.html 4 | 5 | from PyQt5.QtCore import Qt 6 | from PyQt5.QtGui import QFont, QFontMetrics, QIcon, QColor 7 | 8 | from qt.column import Column 9 | from qt.table import Table 10 | from hscommon.trans import trget 11 | 12 | tr = trget("ui") 13 | 14 | 15 | class ExcludeListTable(Table): 16 | """Model for exclude list""" 17 | 18 | COLUMNS = [Column("marked", default_width=15), Column("regex", default_width=230)] 19 | 20 | def __init__(self, app, view, **kwargs): 21 | model = app.model.exclude_list_dialog.exclude_list_table # pointer to GUITable 22 | super().__init__(model, view, **kwargs) 23 | font = view.font() 24 | font.setPointSize(app.prefs.tableFontSize) 25 | view.setFont(font) 26 | fm = QFontMetrics(font) 27 | view.verticalHeader().setDefaultSectionSize(fm.height() + 2) 28 | 29 | def _getData(self, row, column, role): 30 | if column.name == "marked": 31 | if role == Qt.CheckStateRole and row.markable: 32 | return Qt.Checked if row.marked else Qt.Unchecked 33 | if role == Qt.ToolTipRole and not row.markable: 34 | return tr("Compilation error: ") + row.get_cell_value("error") 35 | if role == Qt.DecorationRole and not row.markable: 36 | return QIcon.fromTheme("dialog-error", QIcon(":/error")) 37 | return None 38 | if role == Qt.DisplayRole: 39 | return row.data[column.name] 40 | elif role == Qt.FontRole: 41 | return QFont(self.view.font()) 42 | elif role == Qt.BackgroundRole and column.name == "regex": 43 | if row.highlight: 44 | return QColor(10, 200, 10) # green 45 | elif role == Qt.EditRole and column.name == "regex": 46 | return row.data[column.name] 47 | return None 48 | 49 | def _getFlags(self, row, column): 50 | flags = Qt.ItemIsEnabled 51 | if column.name == "marked": 52 | if row.markable: 53 | flags |= Qt.ItemIsUserCheckable 54 | elif column.name == "regex": 55 | flags |= Qt.ItemIsEditable | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled 56 | return flags 57 | 58 | def _setData(self, row, column, value, role): 59 | if role == Qt.CheckStateRole: 60 | if column.name == "marked": 61 | row.marked = bool(value) 62 | return True 63 | elif role == Qt.EditRole and column.name == "regex": 64 | return self.model.rename_selected(value) 65 | return False 66 | -------------------------------------------------------------------------------- /qt/ignore_list_dialog.py: -------------------------------------------------------------------------------- 1 | # Created By: Virgil Dupras 2 | # Created On: 2012-03-13 3 | # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) 4 | # 5 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 6 | # which should be included with this package. The terms are also available at 7 | # http://www.gnu.org/licenses/gpl-3.0.html 8 | 9 | from PyQt5.QtCore import Qt 10 | from PyQt5.QtWidgets import ( 11 | QDialog, 12 | QVBoxLayout, 13 | QPushButton, 14 | QTableView, 15 | QAbstractItemView, 16 | ) 17 | 18 | from hscommon.trans import trget 19 | from qt.util import horizontal_wrap 20 | from qt.ignore_list_table import IgnoreListTable 21 | 22 | tr = trget("ui") 23 | 24 | 25 | class IgnoreListDialog(QDialog): 26 | def __init__(self, parent, model, **kwargs): 27 | flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint 28 | super().__init__(parent, flags, **kwargs) 29 | self.specific_actions = frozenset() 30 | self._setupUi() 31 | self.model = model 32 | self.model.view = self 33 | self.table = IgnoreListTable(self.model.ignore_list_table, view=self.tableView) 34 | 35 | self.removeSelectedButton.clicked.connect(self.model.remove_selected) 36 | self.clearButton.clicked.connect(self.model.clear) 37 | self.closeButton.clicked.connect(self.accept) 38 | 39 | def _setupUi(self): 40 | self.setWindowTitle(tr("Ignore List")) 41 | self.resize(540, 330) 42 | self.verticalLayout = QVBoxLayout(self) 43 | self.verticalLayout.setContentsMargins(0, 0, 0, 0) 44 | self.tableView = QTableView() 45 | self.tableView.setEditTriggers(QAbstractItemView.NoEditTriggers) 46 | self.tableView.setSelectionMode(QAbstractItemView.ExtendedSelection) 47 | self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows) 48 | self.tableView.setShowGrid(False) 49 | self.tableView.horizontalHeader().setStretchLastSection(True) 50 | self.tableView.verticalHeader().setDefaultSectionSize(18) 51 | self.tableView.verticalHeader().setHighlightSections(False) 52 | self.tableView.verticalHeader().setVisible(False) 53 | self.tableView.setWordWrap(False) 54 | self.verticalLayout.addWidget(self.tableView) 55 | self.removeSelectedButton = QPushButton(tr("Remove Selected")) 56 | self.clearButton = QPushButton(tr("Clear")) 57 | self.closeButton = QPushButton(tr("Close")) 58 | self.verticalLayout.addLayout( 59 | horizontal_wrap([self.removeSelectedButton, self.clearButton, None, self.closeButton]) 60 | ) 61 | 62 | # --- model --> view 63 | def show(self): 64 | super().show() 65 | -------------------------------------------------------------------------------- /qt/ignore_list_table.py: -------------------------------------------------------------------------------- 1 | # Created On: 2012-03-13 2 | # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) 3 | # 4 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 5 | # which should be included with this package. The terms are also available at 6 | # http://www.gnu.org/licenses/gpl-3.0.html 7 | 8 | from qt.column import Column 9 | from qt.table import Table 10 | 11 | 12 | class IgnoreListTable(Table): 13 | """Ignore list model""" 14 | 15 | COLUMNS = [ 16 | Column("path1", default_width=230), 17 | Column("path2", default_width=230), 18 | ] 19 | -------------------------------------------------------------------------------- /qt/me/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/qt/me/__init__.py -------------------------------------------------------------------------------- /qt/me/details_dialog.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Hardcoded Software (http://www.hardcoded.net) 2 | # 3 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 4 | # which should be included with this package. The terms are also available at 5 | # http://www.gnu.org/licenses/gpl-3.0.html 6 | 7 | from PyQt5.QtCore import QSize 8 | from PyQt5.QtWidgets import QAbstractItemView 9 | 10 | from hscommon.trans import trget 11 | from qt.details_dialog import DetailsDialog as DetailsDialogBase 12 | from qt.details_table import DetailsTable 13 | 14 | tr = trget("ui") 15 | 16 | 17 | class DetailsDialog(DetailsDialogBase): 18 | def _setupUi(self): 19 | self.setWindowTitle(tr("Details")) 20 | self.resize(502, 295) 21 | self.setMinimumSize(QSize(250, 250)) 22 | self.tableView = DetailsTable(self) 23 | self.tableView.setAlternatingRowColors(True) 24 | self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows) 25 | self.tableView.setShowGrid(False) 26 | self.setWidget(self.tableView) 27 | -------------------------------------------------------------------------------- /qt/me/results_model.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Hardcoded Software (http://www.hardcoded.net) 2 | # 3 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 4 | # which should be included with this package. The terms are also available at 5 | # http://www.gnu.org/licenses/gpl-3.0.html 6 | 7 | from qt.column import Column 8 | from qt.results_model import ResultsModel as ResultsModelBase 9 | 10 | 11 | class ResultsModel(ResultsModelBase): 12 | COLUMNS = [ 13 | Column("marked", default_width=30), 14 | Column("name", default_width=200), 15 | Column("folder_path", default_width=180), 16 | Column("size", default_width=60), 17 | Column("duration", default_width=60), 18 | Column("bitrate", default_width=50), 19 | Column("samplerate", default_width=60), 20 | Column("extension", default_width=40), 21 | Column("mtime", default_width=120), 22 | Column("title", default_width=120), 23 | Column("artist", default_width=120), 24 | Column("album", default_width=120), 25 | Column("genre", default_width=80), 26 | Column("year", default_width=40), 27 | Column("track", default_width=40), 28 | Column("comment", default_width=120), 29 | Column("percentage", default_width=60), 30 | Column("words", default_width=120), 31 | Column("dupe_count", default_width=80), 32 | ] 33 | -------------------------------------------------------------------------------- /qt/pe/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/qt/pe/__init__.py -------------------------------------------------------------------------------- /qt/pe/block.py: -------------------------------------------------------------------------------- 1 | # Created By: Virgil Dupras 2 | # Created On: 2009-05-10 3 | # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) 4 | # 5 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 6 | # which should be included with this package. The terms are also available at 7 | # http://www.gnu.org/licenses/gpl-3.0.html 8 | 9 | from qt.pe._block_qt import getblocks # NOQA 10 | 11 | # Converted to C 12 | # def getblock(image): 13 | # width = image.width() 14 | # height = image.height() 15 | # if width: 16 | # pixel_count = width * height 17 | # red = green = blue = 0 18 | # s = image.bits().asstring(image.numBytes()) 19 | # for i in xrange(pixel_count): 20 | # offset = i * 3 21 | # red += ord(s[offset]) 22 | # green += ord(s[offset + 1]) 23 | # blue += ord(s[offset + 2]) 24 | # return (red // pixel_count, green // pixel_count, blue // pixel_count) 25 | # else: 26 | # return (0, 0, 0) 27 | # 28 | # def getblocks(image, block_count_per_side): 29 | # width = image.width() 30 | # height = image.height() 31 | # if not width: 32 | # return [] 33 | # block_width = max(width // block_count_per_side, 1) 34 | # block_height = max(height // block_count_per_side, 1) 35 | # result = [] 36 | # for ih in xrange(block_count_per_side): 37 | # top = min(ih * block_height, height - block_height) 38 | # for iw in range(block_count_per_side): 39 | # left = min(iw * block_width, width - block_width) 40 | # crop = image.copy(left, top, block_width, block_height) 41 | # result.append(getblock(crop)) 42 | # return result 43 | -------------------------------------------------------------------------------- /qt/pe/block.pyi: -------------------------------------------------------------------------------- 1 | from typing import Tuple, List, Union 2 | from PyQt5.QtGui import QImage 3 | 4 | _block = Tuple[int, int, int] 5 | 6 | def getblock(image: QImage) -> _block: ... # noqa: E302 7 | def getblocks(image: QImage, block_count_per_side: int) -> Union[List[_block], None]: ... 8 | -------------------------------------------------------------------------------- /qt/pe/photo.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Hardcoded Software (http://www.hardcoded.net) 2 | # 3 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 4 | # which should be included with this package. The terms are also available at 5 | # http://www.gnu.org/licenses/gpl-3.0.html 6 | 7 | import logging 8 | 9 | from PyQt5.QtGui import QImage, QImageReader, QTransform 10 | 11 | from core.pe.photo import Photo as PhotoBase 12 | 13 | from qt.pe.block import getblocks 14 | 15 | 16 | class File(PhotoBase): 17 | def _plat_get_dimensions(self): 18 | try: 19 | ir = QImageReader(str(self.path)) 20 | size = ir.size() 21 | if size.isValid(): 22 | return (size.width(), size.height()) 23 | else: 24 | return (0, 0) 25 | except OSError: 26 | logging.warning("Could not read image '%s'", str(self.path)) 27 | return (0, 0) 28 | 29 | def _plat_get_blocks(self, block_count_per_side, orientation): 30 | image = QImage(str(self.path)) 31 | image = image.convertToFormat(QImage.Format_RGB888) 32 | if not isinstance(orientation, int): 33 | logging.warning( 34 | "Orientation for file '%s' was a %s '%s', not an int.", 35 | str(self.path), 36 | type(orientation), 37 | orientation, 38 | ) 39 | try: 40 | orientation = int(orientation) 41 | except Exception as e: 42 | logging.exception( 43 | "Skipping transformation because could not convert %s to int. %s", 44 | type(orientation), 45 | e, 46 | ) 47 | return getblocks(image, block_count_per_side) 48 | # MYSTERY TO SOLVE: For reasons I cannot explain, orientations 5 and 7 don't work for 49 | # duplicate scanning. The transforms seems to work fine (if I try to save the image after 50 | # the transform, we see that the image has been correctly flipped and rotated), but the 51 | # analysis part yields wrong blocks. I spent enought time with this feature, so I'll leave 52 | # like that for now. (by the way, orientations 5 and 7 work fine under Cocoa) 53 | if 2 <= orientation <= 8: 54 | t = QTransform() 55 | if orientation == 2: 56 | t.scale(-1, 1) 57 | elif orientation == 3: 58 | t.rotate(180) 59 | elif orientation == 4: 60 | t.scale(1, -1) 61 | elif orientation == 5: 62 | t.scale(-1, 1) 63 | t.rotate(90) 64 | elif orientation == 6: 65 | t.rotate(90) 66 | elif orientation == 7: 67 | t.scale(-1, 1) 68 | t.rotate(270) 69 | elif orientation == 8: 70 | t.rotate(270) 71 | image = image.transformed(t) 72 | return getblocks(image, block_count_per_side) 73 | -------------------------------------------------------------------------------- /qt/pe/results_model.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Hardcoded Software (http://www.hardcoded.net) 2 | # 3 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 4 | # which should be included with this package. The terms are also available at 5 | # http://www.gnu.org/licenses/gpl-3.0.html 6 | 7 | from qt.column import Column 8 | from qt.results_model import ResultsModel as ResultsModelBase 9 | 10 | 11 | class ResultsModel(ResultsModelBase): 12 | COLUMNS = [ 13 | Column("marked", default_width=30), 14 | Column("name", default_width=200), 15 | Column("folder_path", default_width=180), 16 | Column("size", default_width=60), 17 | Column("extension", default_width=40), 18 | Column("dimensions", default_width=100), 19 | Column("exif_timestamp", default_width=120), 20 | Column("mtime", default_width=120), 21 | Column("percentage", default_width=60), 22 | Column("dupe_count", default_width=80), 23 | ] 24 | -------------------------------------------------------------------------------- /qt/platform.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Hardcoded Software (http://www.hardcoded.net) 2 | # 3 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 4 | # which should be included with this package. The terms are also available at 5 | # http://www.gnu.org/licenses/gpl-3.0.html 6 | 7 | import os.path as op 8 | from hscommon.plat import ISWINDOWS, ISOSX, ISLINUX 9 | 10 | if op.exists(__file__): 11 | # We want to get the absolute path or our root folder. We know that in that folder we're 12 | # inside qt/, so we just go back one level. 13 | BASE_PATH = op.abspath(op.join(op.dirname(__file__), "..")) 14 | else: 15 | # Should be a frozen environment 16 | if ISOSX: 17 | BASE_PATH = op.abspath(op.join(op.dirname(__file__), "..", "..", "Resources")) 18 | else: 19 | # For others our base path is ''. 20 | BASE_PATH = "" 21 | HELP_PATH = op.join(BASE_PATH, "help", "en") 22 | 23 | if ISWINDOWS: 24 | INITIAL_FOLDER_IN_DIALOGS = "C:\\" 25 | elif ISOSX: 26 | INITIAL_FOLDER_IN_DIALOGS = "/" 27 | elif ISLINUX: 28 | INITIAL_FOLDER_IN_DIALOGS = "/" 29 | else: 30 | # unsupported platform, however '/' is a good guess for a path which is available 31 | INITIAL_FOLDER_IN_DIALOGS = "/" 32 | -------------------------------------------------------------------------------- /qt/problem_dialog.py: -------------------------------------------------------------------------------- 1 | # Created By: Virgil Dupras 2 | # Created On: 2010-04-12 3 | # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) 4 | # 5 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 6 | # which should be included with this package. The terms are also available at 7 | # http://www.gnu.org/licenses/gpl-3.0.html 8 | 9 | from PyQt5.QtCore import Qt 10 | from PyQt5.QtWidgets import ( 11 | QDialog, 12 | QVBoxLayout, 13 | QHBoxLayout, 14 | QPushButton, 15 | QSpacerItem, 16 | QSizePolicy, 17 | QLabel, 18 | QTableView, 19 | QAbstractItemView, 20 | ) 21 | 22 | from qt.util import move_to_screen_center 23 | from hscommon.trans import trget 24 | from qt.problem_table import ProblemTable 25 | 26 | tr = trget("ui") 27 | 28 | 29 | class ProblemDialog(QDialog): 30 | def __init__(self, parent, model, **kwargs): 31 | flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint 32 | super().__init__(parent, flags, **kwargs) 33 | self._setupUi() 34 | self.model = model 35 | self.model.view = self 36 | self.table = ProblemTable(self.model.problem_table, view=self.tableView) 37 | 38 | self.revealButton.clicked.connect(self.model.reveal_selected_dupe) 39 | self.closeButton.clicked.connect(self.accept) 40 | 41 | def _setupUi(self): 42 | self.setWindowTitle(tr("Problems!")) 43 | self.resize(413, 323) 44 | self.verticalLayout = QVBoxLayout(self) 45 | self.label = QLabel(self) 46 | msg = tr( 47 | "There were problems processing some (or all) of the files. The cause of " 48 | "these problems are described in the table below. Those files were not " 49 | "removed from your results." 50 | ) 51 | self.label.setText(msg) 52 | self.label.setWordWrap(True) 53 | self.verticalLayout.addWidget(self.label) 54 | self.tableView = QTableView(self) 55 | self.tableView.setEditTriggers(QAbstractItemView.NoEditTriggers) 56 | self.tableView.setSelectionMode(QAbstractItemView.SingleSelection) 57 | self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows) 58 | self.tableView.setShowGrid(False) 59 | self.tableView.horizontalHeader().setStretchLastSection(True) 60 | self.tableView.verticalHeader().setDefaultSectionSize(18) 61 | self.tableView.verticalHeader().setHighlightSections(False) 62 | self.verticalLayout.addWidget(self.tableView) 63 | self.horizontalLayout = QHBoxLayout() 64 | self.revealButton = QPushButton(self) 65 | self.revealButton.setText(tr("Reveal Selected")) 66 | self.horizontalLayout.addWidget(self.revealButton) 67 | spacer_item = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) 68 | self.horizontalLayout.addItem(spacer_item) 69 | self.closeButton = QPushButton(self) 70 | self.closeButton.setText(tr("Close")) 71 | self.closeButton.setDefault(True) 72 | self.horizontalLayout.addWidget(self.closeButton) 73 | self.verticalLayout.addLayout(self.horizontalLayout) 74 | 75 | def showEvent(self, event): 76 | # have to do this here as the frameGeometry is not correct until shown 77 | move_to_screen_center(self) 78 | super().showEvent(event) 79 | -------------------------------------------------------------------------------- /qt/problem_table.py: -------------------------------------------------------------------------------- 1 | # Created By: Virgil Dupras 2 | # Created On: 2010-04-12 3 | # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) 4 | # 5 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 6 | # which should be included with this package. The terms are also available at 7 | # http://www.gnu.org/licenses/gpl-3.0.html 8 | 9 | from qt.column import Column 10 | from qt.table import Table 11 | 12 | 13 | class ProblemTable(Table): 14 | COLUMNS = [ 15 | Column("path", default_width=150), 16 | Column("msg", default_width=150), 17 | ] 18 | 19 | def __init__(self, model, view, **kwargs): 20 | super().__init__(model, view, **kwargs) 21 | # we have to prevent Return from initiating editing. 22 | # self.view.editSelected = lambda: None 23 | -------------------------------------------------------------------------------- /qt/radio_box.py: -------------------------------------------------------------------------------- 1 | # Created On: 2010-06-02 2 | # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) 3 | # 4 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 5 | # which should be included with this package. The terms are also available at 6 | # http://www.gnu.org/licenses/gpl-3.0.html 7 | 8 | from PyQt5.QtCore import pyqtSignal 9 | from PyQt5.QtWidgets import QWidget, QHBoxLayout, QRadioButton 10 | 11 | from qt.util import horizontal_spacer 12 | 13 | 14 | class RadioBox(QWidget): 15 | def __init__(self, parent=None, items=None, spread=True, **kwargs): 16 | # If spread is False, insert a spacer in the layout so that the items don't use all the 17 | # space they're given but rather align left. 18 | if items is None: 19 | items = [] 20 | super().__init__(parent, **kwargs) 21 | self._buttons = [] 22 | self._labels = items 23 | self._selected_index = 0 24 | self._spacer = horizontal_spacer() if not spread else None 25 | self._layout = QHBoxLayout(self) 26 | self._update_buttons() 27 | 28 | # --- Private 29 | def _update_buttons(self): 30 | if self._spacer is not None: 31 | self._layout.removeItem(self._spacer) 32 | to_remove = self._buttons[len(self._labels) :] 33 | for button in to_remove: 34 | self._layout.removeWidget(button) 35 | button.setParent(None) 36 | del self._buttons[len(self._labels) :] 37 | to_add = self._labels[len(self._buttons) :] 38 | for _ in to_add: 39 | button = QRadioButton(self) 40 | self._buttons.append(button) 41 | self._layout.addWidget(button) 42 | button.toggled.connect(self.buttonToggled) 43 | if self._spacer is not None: 44 | self._layout.addItem(self._spacer) 45 | if not self._buttons: 46 | return 47 | for button, label in zip(self._buttons, self._labels): 48 | button.setText(label) 49 | self._update_selection() 50 | 51 | def _update_selection(self): 52 | self._selected_index = max(0, min(self._selected_index, len(self._buttons) - 1)) 53 | selected = self._buttons[self._selected_index] 54 | selected.setChecked(True) 55 | 56 | # --- Event Handlers 57 | def buttonToggled(self): 58 | for i, button in enumerate(self._buttons): 59 | if button.isChecked(): 60 | self._selected_index = i 61 | self.itemSelected.emit(i) 62 | break 63 | 64 | # --- Signals 65 | itemSelected = pyqtSignal(int) 66 | 67 | # --- Properties 68 | @property 69 | def buttons(self): 70 | return self._buttons[:] 71 | 72 | @property 73 | def items(self): 74 | return self._labels[:] 75 | 76 | @items.setter 77 | def items(self, value): 78 | self._labels = value 79 | self._update_buttons() 80 | 81 | @property 82 | def selected_index(self): 83 | return self._selected_index 84 | 85 | @selected_index.setter 86 | def selected_index(self, value): 87 | self._selected_index = value 88 | self._update_selection() 89 | -------------------------------------------------------------------------------- /qt/recent.py: -------------------------------------------------------------------------------- 1 | # Created By: Virgil Dupras 2 | # Created On: 2009-11-12 3 | # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) 4 | # 5 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 6 | # which should be included with this package. The terms are also available at 7 | # http://www.gnu.org/licenses/gpl-3.0.html 8 | 9 | from collections import namedtuple 10 | 11 | from PyQt5.QtCore import pyqtSignal, QObject 12 | from PyQt5.QtWidgets import QAction 13 | 14 | from hscommon.trans import trget 15 | from hscommon.util import dedupe 16 | 17 | tr = trget("ui") 18 | 19 | MenuEntry = namedtuple("MenuEntry", "menu fixedItemCount") 20 | 21 | 22 | class Recent(QObject): 23 | def __init__(self, app, pref_name, max_item_count=10, **kwargs): 24 | super().__init__(**kwargs) 25 | self._app = app 26 | self._menuEntries = [] 27 | self._prefName = pref_name 28 | self._maxItemCount = max_item_count 29 | self._items = [] 30 | self._loadFromPrefs() 31 | 32 | self._app.willSavePrefs.connect(self._saveToPrefs) 33 | 34 | # --- Private 35 | def _loadFromPrefs(self): 36 | items = getattr(self._app.prefs, self._prefName) 37 | if not isinstance(items, list): 38 | items = [] 39 | self._items = items 40 | 41 | def _insertItem(self, item): 42 | self._items = dedupe([item] + self._items)[: self._maxItemCount] 43 | 44 | def _refreshMenu(self, menu_entry): 45 | menu, fixed_item_count = menu_entry 46 | for action in menu.actions()[fixed_item_count:]: 47 | menu.removeAction(action) 48 | for item in self._items: 49 | action = QAction(item, menu) 50 | action.setData(item) 51 | action.triggered.connect(self.menuItemWasClicked) 52 | menu.addAction(action) 53 | menu.addSeparator() 54 | action = QAction(tr("Clear List"), menu) 55 | action.triggered.connect(self.clear) 56 | menu.addAction(action) 57 | 58 | def _refreshAllMenus(self): 59 | for menu_entry in self._menuEntries: 60 | self._refreshMenu(menu_entry) 61 | 62 | def _saveToPrefs(self): 63 | setattr(self._app.prefs, self._prefName, self._items) 64 | 65 | # --- Public 66 | def addMenu(self, menu): 67 | menu_entry = MenuEntry(menu, len(menu.actions())) 68 | self._menuEntries.append(menu_entry) 69 | self._refreshMenu(menu_entry) 70 | 71 | def clear(self): 72 | self._items = [] 73 | self._refreshAllMenus() 74 | self.itemsChanged.emit() 75 | 76 | def insertItem(self, item): 77 | self._insertItem(str(item)) 78 | self._refreshAllMenus() 79 | self.itemsChanged.emit() 80 | 81 | def isEmpty(self): 82 | return not bool(self._items) 83 | 84 | # --- Event Handlers 85 | def menuItemWasClicked(self): 86 | action = self.sender() 87 | if action is not None: 88 | item = action.data() 89 | self.mustOpenItem.emit(item) 90 | self._refreshAllMenus() 91 | 92 | # --- Signals 93 | mustOpenItem = pyqtSignal(str) 94 | itemsChanged = pyqtSignal() 95 | -------------------------------------------------------------------------------- /qt/se/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arsenetar/dupeguru/8f197ea7e1ba8864d616f1493f5d9b94546b74d2/qt/se/__init__.py -------------------------------------------------------------------------------- /qt/se/details_dialog.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Hardcoded Software (http://www.hardcoded.net) 2 | # 3 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 4 | # which should be included with this package. The terms are also available at 5 | # http://www.gnu.org/licenses/gpl-3.0.html 6 | 7 | from PyQt5.QtCore import QSize 8 | from PyQt5.QtWidgets import QAbstractItemView 9 | 10 | from hscommon.trans import trget 11 | from qt.details_dialog import DetailsDialog as DetailsDialogBase 12 | from qt.details_table import DetailsTable 13 | 14 | tr = trget("ui") 15 | 16 | 17 | class DetailsDialog(DetailsDialogBase): 18 | def _setupUi(self): 19 | self.setWindowTitle(tr("Details")) 20 | self.resize(502, 186) 21 | self.setMinimumSize(QSize(200, 0)) 22 | self.tableView = DetailsTable(self) 23 | self.tableView.setAlternatingRowColors(True) 24 | self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows) 25 | self.tableView.setShowGrid(False) 26 | self.setWidget(self.tableView) 27 | -------------------------------------------------------------------------------- /qt/se/results_model.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Hardcoded Software (http://www.hardcoded.net) 2 | # 3 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 4 | # which should be included with this package. The terms are also available at 5 | # http://www.gnu.org/licenses/gpl-3.0.html 6 | 7 | from qt.column import Column 8 | from qt.results_model import ResultsModel as ResultsModelBase 9 | 10 | 11 | class ResultsModel(ResultsModelBase): 12 | COLUMNS = [ 13 | Column("marked", default_width=30), 14 | Column("name", default_width=200), 15 | Column("folder_path", default_width=180), 16 | Column("size", default_width=60), 17 | Column("extension", default_width=40), 18 | Column("mtime", default_width=120), 19 | Column("percentage", default_width=60), 20 | Column("words", default_width=120), 21 | Column("dupe_count", default_width=80), 22 | ] 23 | -------------------------------------------------------------------------------- /qt/stats_label.py: -------------------------------------------------------------------------------- 1 | # Created By: Virgil Dupras 2 | # Created On: 2010-02-12 3 | # Copyright 2015 Hardcoded Software (http://www.hardcoded.net) 4 | # 5 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 6 | # which should be included with this package. The terms are also available at 7 | # http://www.gnu.org/licenses/gpl-3.0.html 8 | 9 | 10 | class StatsLabel: 11 | def __init__(self, model, view): 12 | self.view = view 13 | self.model = model 14 | self.model.view = self 15 | 16 | def refresh(self): 17 | self.view.setText(self.model.display) 18 | -------------------------------------------------------------------------------- /requirements-extra.txt: -------------------------------------------------------------------------------- 1 | pytest>=7,<8 2 | flake8 3 | black 4 | pyinstaller>=5.6,<6.0; sys_platform != 'linux' 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | distro>=1.8.0,<2.0.0 2 | mutagen>=1.46.0,<2.0.0 3 | polib>=1.1.0,<2.0.0 4 | PyQt5 >=5.15.0,<6.0; sys_platform != 'linux' 5 | pywin32>=304; sys_platform == 'win32' 6 | semantic-version>=2.0.0,<3.0.0 7 | Send2Trash>=1.8.2,<2.0.0 8 | sphinx>=5.3.0,<8.0.0 9 | xxhash>=3.0.0,<4.0.0 10 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # Copyright 2017 Virgil Dupras 3 | # 4 | # This software is licensed under the "GPLv3" License as described in the "LICENSE" file, 5 | # which should be included with this package. The terms are also available at 6 | # http://www.gnu.org/licenses/gpl-3.0.html 7 | 8 | import sys 9 | import os.path as op 10 | import gc 11 | 12 | from PyQt5.QtCore import QCoreApplication 13 | from PyQt5.QtGui import QIcon, QPixmap 14 | from PyQt5.QtWidgets import QApplication 15 | 16 | from hscommon.trans import install_gettext_trans_under_qt 17 | from qt.error_report_dialog import install_excepthook 18 | from qt.util import setup_qt_logging, create_qsettings 19 | from qt import dg_rc # noqa: F401 20 | from qt.platform import BASE_PATH 21 | from core import __version__, __appname__ 22 | 23 | # SIGQUIT is not defined on Windows 24 | if sys.platform == "win32": 25 | from signal import signal, SIGINT, SIGTERM 26 | 27 | SIGQUIT = SIGTERM 28 | else: 29 | from signal import signal, SIGINT, SIGTERM, SIGQUIT 30 | 31 | global dgapp 32 | dgapp = None 33 | 34 | 35 | def signal_handler(sig, frame): 36 | global dgapp 37 | if dgapp is None: 38 | return 39 | if sig in (SIGINT, SIGTERM, SIGQUIT): 40 | dgapp.SIGTERM.emit() 41 | 42 | 43 | def setup_signals(): 44 | signal(SIGINT, signal_handler) 45 | signal(SIGTERM, signal_handler) 46 | signal(SIGQUIT, signal_handler) 47 | 48 | 49 | def main(): 50 | app = QApplication(sys.argv) 51 | QCoreApplication.setOrganizationName("Hardcoded Software") 52 | QCoreApplication.setApplicationName(__appname__) 53 | QCoreApplication.setApplicationVersion(__version__) 54 | setup_qt_logging() 55 | settings = create_qsettings() 56 | lang = settings.value("Language") 57 | locale_folder = op.join(BASE_PATH, "locale") 58 | install_gettext_trans_under_qt(locale_folder, lang) 59 | # Handle OS signals 60 | setup_signals() 61 | # Let the Python interpreter runs every 500ms to handle signals. This is 62 | # required because Python cannot handle signals while the Qt event loop is 63 | # running. 64 | from PyQt5.QtCore import QTimer 65 | 66 | timer = QTimer() 67 | timer.start(500) 68 | timer.timeout.connect(lambda: None) 69 | # Many strings are translated at import time, so this is why we only import after the translator 70 | # has been installed 71 | from qt.app import DupeGuru 72 | 73 | app.setWindowIcon(QIcon(QPixmap(f":/{DupeGuru.LOGO_NAME}"))) 74 | global dgapp 75 | dgapp = DupeGuru() 76 | install_excepthook("https://github.com/arsenetar/dupeguru/issues") 77 | result = app.exec() 78 | # I was getting weird crashes when quitting under Windows, and manually deleting main app 79 | # references with gc.collect() in between seems to fix the problem. 80 | del dgapp 81 | gc.collect() 82 | del app 83 | gc.collect() 84 | return result 85 | 86 | 87 | if __name__ == "__main__": 88 | sys.exit(main()) 89 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = dupeGuru 3 | version = attr: core.__version__ 4 | url = https://github.com/arsenetar/dupeguru 5 | project_urls = 6 | Bug Reports = https://github.com/arsenetar/dupeguru/issues 7 | author = Andrew Senetar 8 | author_email = arsenetar@voltaicideas.net 9 | license = GPLv3 10 | license_files = license 11 | description = dupeGuru is a tool to find duplicate files on your computer. 12 | long_description = file:README.md 13 | long_description_content_type = text/markdown 14 | classifiers = 15 | Development Status :: 5 - Production/Stable 16 | Intended Audience :: End Users/Desktop 17 | License :: OSI Approved :: GNU General Public License v3 (GPLv3) 18 | Operating System :: MacOS :: MacOS X 19 | Operating System :: Microsoft :: Windows 20 | Operating System :: POSIX 21 | Programming Language :: Python :: 3.7 22 | Programming Language :: Python :: 3.8 23 | Programming Language :: Python :: 3.9 24 | Programming Language :: Python :: 3.10 25 | Programming Language :: Python :: 3 :: Only 26 | Topic :: Desktop Environment :: File Managers 27 | 28 | [options] 29 | packages = find: 30 | python_requires = >=3.7 31 | install_requires = 32 | Send2Trash>=1.8.2,<2.0.0 33 | mutagen>=1.46.0,<2.0.0 34 | distro>=1.8.0,<2.0.0 35 | PyQt5 >=5.15.0,<6.0; sys_platform != 'linux' 36 | pywin32>=228; sys_platform == 'win32' 37 | semantic-version>=2.0.0,<3.0.0 38 | xxhash>=3.0.0,<4.0.0 39 | setup_requires = 40 | sphinx>=3.0.0 41 | polib>=1.1.0 42 | tests_require = 43 | pytest >=6,<7 44 | include_package_data = true 45 | 46 | [options.entry_points] 47 | console_scripts = 48 | dupeguru = run.py 49 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, Extension 2 | from pathlib import Path 3 | 4 | exts = [ 5 | Extension( 6 | "core.pe._block", 7 | [ 8 | str(Path("core", "pe", "modules", "block.c")), 9 | str(Path("core", "pe", "modules", "common.c")), 10 | ], 11 | include_dirs=[str(Path("core", "pe", "modules"))], 12 | ), 13 | Extension( 14 | "core.pe._cache", 15 | [ 16 | str(Path("core", "pe", "modules", "cache.c")), 17 | str(Path("core", "pe", "modules", "common.c")), 18 | ], 19 | include_dirs=[str(Path("core", "pe", "modules"))], 20 | ), 21 | Extension("qt.pe._block_qt", [str(Path("qt", "pe", "modules", "block.c"))]), 22 | ] 23 | 24 | headers = [str(Path("core", "pe", "modules", "common.h"))] 25 | 26 | setup(ext_modules=exts, headers=headers) 27 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py37,py38,py39,py310,py311 3 | skipsdist = True 4 | skip_missing_interpreters = True 5 | 6 | [testenv] 7 | setenv = 8 | PYTHON="{envpython}" 9 | commands = 10 | python build.py --modules 11 | flake8 12 | black --check . 13 | {posargs:py.test core hscommon} 14 | deps = 15 | -r{toxinidir}/requirements.txt 16 | -r{toxinidir}/requirements-extra.txt 17 | 18 | [flake8] 19 | exclude = .tox,env*,build,help,qt/dg_rc.py,pkg 20 | max-line-length = 120 21 | select = C,E,F,W,B,B950 22 | extend-ignore = E203,W503 23 | -------------------------------------------------------------------------------- /win_version_info.temp: -------------------------------------------------------------------------------- 1 | # UTF-8 2 | # 3 | # For more details about fixed file info 'ffi' see: 4 | # http://msdn.microsoft.com/en-us/library/ms646997.aspx 5 | VSVersionInfo( 6 | ffi=FixedFileInfo( 7 | # filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4) 8 | # Set not needed items to zero 0. 9 | filevers=({0}, {1}, {2}, 0), 10 | prodvers=({0}, {1}, {2}, 0), 11 | # Contains a bitmask that specifies the valid bits 'flags'r 12 | mask=0x3f, 13 | # Contains a bitmask that specifies the Boolean attributes of the file. 14 | flags=0x0, 15 | # The operating system for which this file was designed. 16 | # 0x4 - NT and there is no need to change it. 17 | OS=0x40004, 18 | # The general type of file. 19 | # 0x1 - the file is an application. 20 | fileType=0x1, 21 | # The function of the file. 22 | # 0x0 - the function is not defined for this fileType 23 | subtype=0x0, 24 | # Creation date and time stamp. 25 | date=(0, 0) 26 | ), 27 | kids=[ 28 | StringFileInfo( 29 | [ 30 | StringTable( 31 | u'040904B0', 32 | [StringStruct(u'CompanyName', u'Hardcoded Software'), 33 | StringStruct(u'FileDescription', u'dupeGuru'), 34 | StringStruct(u'FileVersion', u'{0}.{1}.{2}.0'), 35 | StringStruct(u'InternalName', u'dupeGuru'), 36 | StringStruct(u'LegalCopyright', u'© Hardcoded Software'), 37 | StringStruct(u'OriginalFilename', u'dupeguru-win{3}.exe'), 38 | StringStruct(u'ProductName', u'dupeGuru'), 39 | StringStruct(u'ProductVersion', u'{0}.{1}.{2}.0')]) 40 | ]), 41 | VarFileInfo([VarStruct(u'Translation', [1033, 1200])]) 42 | ] 43 | ) 44 | --------------------------------------------------------------------------------