1
0
mirror of https://github.com/arsenetar/dupeguru.git synced 2026-01-25 16:11:39 +00:00

Compare commits

...

25 Commits

Author SHA1 Message Date
Virgil Dupras
24643a9b5d Updated copyright year to 2014 in Cocoa about boxes
Better late than never.
2014-10-12 13:19:55 -04:00
Virgil Dupras
045051ce06 Fixed formatting in changelog_pe 2014-10-12 10:52:41 -04:00
Virgil Dupras
7c3728ca47 Converted hscommon.jobprogress.qt to Qt5 2014-10-12 10:52:21 -04:00
Virgil Dupras
91be1c7336 pe v2.10.1 2014-10-12 10:47:18 -04:00
Virgil Dupras
162378bb0a Updated hscommon 2014-10-12 10:39:21 -04:00
Virgil Dupras
4e3cad5702 Fixed minor typo 2014-10-12 10:15:07 -04:00
Virgil Dupras
321f8ab406 Catch MemoryError better in PE's block matching algo
fixes #264 (for good this time, hopefully)
2014-10-05 22:22:59 -04:00
Virgil Dupras
5b3d5f5d1c Tweaked the main dev help page to have actual reflinks 2014-10-05 20:12:38 -04:00
Virgil Dupras
372a682610 Catch MemoryError in PE's block matching algo
fixes #264 (hopefully)
2014-10-05 17:13:36 -04:00
Virgil Dupras
44266273bf Included hscommon.jobprogress in the devdocs 2014-10-05 17:12:10 -04:00
Virgil Dupras
ac32305532 Integrated the jobprogress library into hscommon
I have a fix to make in it and it's really silly to pretend that this
lib is of any use to anybody outside HS apps. Bringing it back here will
make things more simple.
2014-10-05 16:31:16 -04:00
Virgil Dupras
87c2fa2573 Updated README which was a bit outdated 2014-10-04 17:01:22 -04:00
Virgil Dupras
db63b63cfd Fix crash in PE when reading some EXIF tags
The crash was caused by ObjP, which crashed when converting `NSDictionary` containing unsupported types.

Updating ObjP to v1.3.1 does the trick.

fixes #263
fixes #265
2014-10-04 16:35:26 -04:00
Virgil Dupras
6725b2bf0f Updated German localisation, by Frank Weber 2014-09-28 13:40:09 -04:00
Virgil Dupras
990e73c383 Catch Spinx SystemExit when building help
In a recent Sphinx release, it started calling `sys.exit()` and that
caused our whole build process to exit prematurely.
2014-09-13 16:05:40 -04:00
Virgil Dupras
9e9e73aa6b qtlib: Fix broken SelectableList
It was still using `.reset()`, which disappeared in Qt5.

Fixes #254.
2014-07-01 08:30:56 -04:00
Virgil Dupras
8434befe1f me v6.8.0 2014-05-11 09:26:55 -04:00
Virgil Dupras
1114ac5613 Fixed debian packaging 2014-05-11 09:11:38 -04:00
Virgil Dupras
f5f29d775c Adapt IPhotoPlistParser to Python 3.4
This also means that Python 3.3 isn't supported anymore for that part.
Updated README accordingly.
2014-05-03 15:12:13 -04:00
Virgil Dupras
ebd7f1b4ce pe v2.10.0 2014-05-03 13:57:00 -04:00
Virgil Dupras
279b7ad10c Fix typo in README 2014-05-03 13:53:16 -04:00
Virgil Dupras
878205fc49 Fix empty ignore List dialog bug in PE
Re-instantiating a new scanner for PE  made the ignore list dialog
target the wrong ignore list. We now only instantiate a scanner once.

Fixes #253
2014-05-03 13:44:38 -04:00
Virgil Dupras
b16df32150 I'm giving PyCharm a try 2014-05-03 13:39:39 -04:00
Virgil Dupras
04b06f7704 Removed the setNativeMenuBar() call under Qt
I put it there to make the menu usable under Ubuntu 13.10, but since
14.04, this line actually brakes it.
2014-05-03 09:34:41 -04:00
Virgil Dupras
c6ea1c62d4 Fixed Windows packaging 2014-04-21 10:00:53 -04:00
55 changed files with 1240 additions and 980 deletions

1
.gitignore vendored
View File

@@ -5,6 +5,7 @@
*.pyd *.pyd
*.waf* *.waf*
.lock-waf* .lock-waf*
.idea
build build
dist dist

View File

@@ -3,7 +3,7 @@
[dupeGuru][dupeguru] is a cross-platform (Linux, OS X, Windows) GUI tool to find duplicate files in [dupeGuru][dupeguru] is a cross-platform (Linux, OS X, Windows) GUI tool to find duplicate files in
a system. It's written mostly in Python 3 and has the peculiarity of using a system. It's written mostly in Python 3 and has the peculiarity of using
[multiple GUI toolkits][cross-toolkit], all using the same core Python code. On OS X, the UI layer [multiple GUI toolkits][cross-toolkit], all using the same core Python code. On OS X, the UI layer
is written in Objective-C and uses Cocoa. On Linux and Windows, it's written in Python and uses Qt4. is written in Objective-C and uses Cocoa. On Linux and Windows, it's written in Python and uses Qt5.
dupeGuru comes in 3 editions (standard, music and picture) which are all buildable from this same dupeGuru comes in 3 editions (standard, music and picture) which are all buildable from this same
source tree. You choose the edition you want to build in a ``configure.py`` flag. source tree. You choose the edition you want to build in a ``configure.py`` flag.
@@ -47,7 +47,7 @@ Prerequisites are installed through `pip`. However, some of them are not "pip in
to be installed manually. to be installed manually.
* All systems: [Python 3.3+][python] and [setuptools][setuptools] * All systems: [Python 3.3+][python] and [setuptools][setuptools]
* Mac OS X: The last XCode to have the 10.6 SDK included. * Mac OS X: The last XCode to have the 10.7 SDK included. Python 3.4+.
* Windows: Visual Studio 2010, [PyQt 5.0+][pyqt], [cx_Freeze][cxfreeze] and * Windows: Visual Studio 2010, [PyQt 5.0+][pyqt], [cx_Freeze][cxfreeze] and
[Advanced Installer][advinst] (you only need the last two if you want to create an installer) [Advanced Installer][advinst] (you only need the last two if you want to create an installer)
@@ -63,12 +63,12 @@ On Arch, it's:
Use Python's built-in `pyvenv` to create a virtual environment in which we're going to install our. Use Python's built-in `pyvenv` to create a virtual environment in which we're going to install our.
Python-related dependencies. `pyvenv` is built-in Python but, unlike its `virtualenv` predecessor, Python-related dependencies. `pyvenv` is built-in Python but, unlike its `virtualenv` predecessor,
it doesn't install setuptools and pip, so it has to be installed manually: it doesn't install setuptools and pip (unless you use Python 3.4+), so it has to be installed
manually:
$ pyvenv --system-site-packages env $ pyvenv --system-site-packages env
$ source env/bin/activate $ source env/bin/activate
$ wget https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py -O - | python $ python get-pip.py
$ easy_install pip
Then, you can install pip requirements in your virtualenv: Then, you can install pip requirements in your virtualenv:
@@ -96,3 +96,4 @@ You can also package dupeGuru into an installable package with:
[pyqt]: http://www.riverbankcomputing.com [pyqt]: http://www.riverbankcomputing.com
[cxfreeze]: http://cx-freeze.sourceforge.net/ [cxfreeze]: http://cx-freeze.sourceforge.net/
[advinst]: http://www.advancedinstaller.com [advinst]: http://www.advancedinstaller.com

View File

@@ -110,8 +110,9 @@ def build_cocoa(edition, dev):
'me': ['core_me'] + appscript_pkgs + ['hsaudiotag'], 'me': ['core_me'] + appscript_pkgs + ['hsaudiotag'],
'pe': ['core_pe'] + appscript_pkgs, 'pe': ['core_pe'] + appscript_pkgs,
}[edition] }[edition]
tocopy = ['core', 'hscommon', 'cocoa/inter', 'cocoalib/cocoa', 'jobprogress', 'objp', tocopy = [
'send2trash'] + specific_packages 'core', 'hscommon', 'cocoa/inter', 'cocoalib/cocoa', 'objp', 'send2trash'
] + specific_packages
copy_packages(tocopy, pydep_folder, create_links=dev) copy_packages(tocopy, pydep_folder, create_links=dev)
sys.path.insert(0, 'build') sys.path.insert(0, 'build')
extra_deps = None extra_deps = None

View File

@@ -31,7 +31,7 @@
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>
<string>NSApplication</string> <string>NSApplication</string>
<key>NSHumanReadableCopyright</key> <key>NSHumanReadableCopyright</key>
<string>© Hardcoded Software, 2013</string> <string>© Hardcoded Software, 2014</string>
<key>SUFeedURL</key> <key>SUFeedURL</key>
<string>http://www.hardcoded.net/updates/dupeguru_me.appcast</string> <string>http://www.hardcoded.net/updates/dupeguru_me.appcast</string>
<key>SUPublicDSAKeyFile</key> <key>SUPublicDSAKeyFile</key>

View File

@@ -31,7 +31,7 @@
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>
<string>NSApplication</string> <string>NSApplication</string>
<key>NSHumanReadableCopyright</key> <key>NSHumanReadableCopyright</key>
<string>© Hardcoded Software, 2013</string> <string>© Hardcoded Software, 2014</string>
<key>SUFeedURL</key> <key>SUFeedURL</key>
<string>http://www.hardcoded.net/updates/dupeguru_pe.appcast</string> <string>http://www.hardcoded.net/updates/dupeguru_pe.appcast</string>
<key>SUPublicDSAKeyFile</key> <key>SUPublicDSAKeyFile</key>

View File

@@ -29,7 +29,7 @@
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>
<string>NSApplication</string> <string>NSApplication</string>
<key>NSHumanReadableCopyright</key> <key>NSHumanReadableCopyright</key>
<string>© Hardcoded Software, 2013</string> <string>© Hardcoded Software, 2014</string>
<key>SUFeedURL</key> <key>SUFeedURL</key>
<string>http://www.hardcoded.net/updates/dupeguru.appcast</string> <string>http://www.hardcoded.net/updates/dupeguru.appcast</string>
<key>SUPublicDSAKeyFile</key> <key>SUPublicDSAKeyFile</key>

View File

@@ -113,5 +113,6 @@ def patch_threaded_job_performer():
# _async_run, under cocoa, has to be run within an autorelease pool to prevent leaks. # _async_run, under cocoa, has to be run within an autorelease pool to prevent leaks.
# You only need this patch is you use one of CocoaProxy's function (which allocate objc # You only need this patch is you use one of CocoaProxy's function (which allocate objc
# structures) inside a threaded job. # structures) inside a threaded job.
from jobprogress.performer import ThreadedJobPerformer from hscommon.jobprogress.performer import ThreadedJobPerformer
ThreadedJobPerformer._async_run = autoreleasepool(ThreadedJobPerformer._async_run) ThreadedJobPerformer._async_run = autoreleasepool(ThreadedJobPerformer._async_run)

View File

@@ -15,7 +15,7 @@ import time
import shutil import shutil
from send2trash import send2trash from send2trash import send2trash
from jobprogress import job from hscommon.jobprogress import job
from hscommon.notify import Broadcaster from hscommon.notify import Broadcaster
from hscommon.path import Path from hscommon.path import Path
from hscommon.conflict import smart_move, smart_copy from hscommon.conflict import smart_move, smart_copy
@@ -152,8 +152,8 @@ class DupeGuru(Broadcaster):
# select_dest_folder(prompt: str) --> str # select_dest_folder(prompt: str) --> str
# select_dest_file(prompt: str, ext: str) --> str # select_dest_file(prompt: str, ext: str) --> str
# in fairware prompts, we don't mention the edition, it's too long.
PROMPT_NAME = "dupeGuru" PROMPT_NAME = "dupeGuru"
SCANNER_CLASS = scanner.Scanner
def __init__(self, view): def __init__(self, view):
if view.get_default(DEBUG_MODE_PREFERENCE): if view.get_default(DEBUG_MODE_PREFERENCE):
@@ -166,7 +166,7 @@ class DupeGuru(Broadcaster):
os.makedirs(self.appdata) os.makedirs(self.appdata)
self.directories = directories.Directories() self.directories = directories.Directories()
self.results = results.Results(self) self.results = results.Results(self)
self.scanner = scanner.Scanner() self.scanner = self.SCANNER_CLASS()
self.options = { self.options = {
'escape_filter_regexp': True, 'escape_filter_regexp': True,
'clean_empty_dirs': False, 'clean_empty_dirs': False,

View File

@@ -9,7 +9,7 @@
from xml.etree import ElementTree as ET from xml.etree import ElementTree as ET
import logging import logging
from jobprogress import job from hscommon.jobprogress import job
from hscommon.path import Path from hscommon.path import Path
from hscommon.util import FileOrPath from hscommon.util import FileOrPath

View File

@@ -15,7 +15,7 @@ from unicodedata import normalize
from hscommon.util import flatten, multi_replace from hscommon.util import flatten, multi_replace
from hscommon.trans import tr from hscommon.trans import tr
from jobprogress import job from hscommon.jobprogress import job
(WEIGHT_WORDS, (WEIGHT_WORDS,
MATCH_SIMILAR_WORDS, MATCH_SIMILAR_WORDS,

View File

@@ -12,7 +12,7 @@ import os
import os.path as op import os.path as op
from xml.etree import ElementTree as ET from xml.etree import ElementTree as ET
from jobprogress.job import nulljob from hscommon.jobprogress.job import nulljob
from hscommon.conflict import get_conflicted_name from hscommon.conflict import get_conflicted_name
from hscommon.util import flatten, nonone, FileOrPath, format_size from hscommon.util import flatten, nonone, FileOrPath, format_size
from hscommon.trans import tr from hscommon.trans import tr

View File

@@ -10,7 +10,7 @@ import logging
import re import re
import os.path as op import os.path as op
from jobprogress import job from hscommon.jobprogress import job
from hscommon.util import dedupe, rem_file_ext, get_file_ext from hscommon.util import dedupe, rem_file_ext, get_file_ext
from hscommon.trans import tr from hscommon.trans import tr

View File

@@ -15,7 +15,7 @@ from hscommon.path import Path
import hscommon.conflict import hscommon.conflict
import hscommon.util import hscommon.util
from hscommon.testutil import CallLogger, eq_, log_calls from hscommon.testutil import CallLogger, eq_, log_calls
from jobprogress.job import Job from hscommon.jobprogress.job import Job
from .base import DupeGuru, TestApp from .base import DupeGuru, TestApp
from .results_test import GetTestGroups from .results_test import GetTestGroups

View File

@@ -10,7 +10,7 @@ from hscommon.testutil import TestApp as TestAppBase, eq_, with_app
from hscommon.path import Path from hscommon.path import Path
from hscommon.util import get_file_ext, format_size from hscommon.util import get_file_ext, format_size
from hscommon.gui.column import Column from hscommon.gui.column import Column
from jobprogress.job import nulljob, JobCancelled from hscommon.jobprogress.job import nulljob, JobCancelled
from .. import engine from .. import engine
from .. import prioritize from .. import prioritize

View File

@@ -8,7 +8,7 @@
import sys import sys
from jobprogress import job from hscommon.jobprogress import job
from hscommon.util import first from hscommon.util import first
from hscommon.testutil import eq_, log_calls from hscommon.testutil import eq_, log_calls

View File

@@ -6,7 +6,7 @@
# which should be included with this package. The terms are also available at # which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license # http://www.hardcoded.net/licenses/bsd_license
from jobprogress import job from hscommon.jobprogress import job
from hscommon.path import Path from hscommon.path import Path
from hscommon.testutil import eq_ from hscommon.testutil import eq_

View File

@@ -1,2 +1,2 @@
__version__ = '6.7.0' __version__ = '6.8.0'
__appname__ = 'dupeGuru Music Edition' __appname__ = 'dupeGuru Music Edition'

View File

@@ -1,2 +1,2 @@
__version__ = '2.9.0' __version__ = '2.10.1'
__appname__ = 'dupeGuru Picture Edition' __appname__ = 'dupeGuru Picture Edition'

View File

@@ -17,10 +17,10 @@ from .result_table import ResultTable
class DupeGuru(DupeGuruBase): class DupeGuru(DupeGuruBase):
NAME = __appname__ NAME = __appname__
METADATA_TO_READ = ['size', 'mtime', 'dimensions', 'exif_timestamp'] METADATA_TO_READ = ['size', 'mtime', 'dimensions', 'exif_timestamp']
SCANNER_CLASS = ScannerPE
def __init__(self, view): def __init__(self, view):
DupeGuruBase.__init__(self, view) DupeGuruBase.__init__(self, view)
self.scanner = ScannerPE()
self.scanner.cache_path = op.join(self.appdata, 'cached_pictures.db') self.scanner.cache_path = op.join(self.appdata, 'cached_pictures.db')
def _get_dupe_sort_key(self, dupe, get_group, key, delta): def _get_dupe_sort_key(self, dupe, get_group, key, delta):

View File

@@ -8,24 +8,24 @@
import plistlib import plistlib
class IPhotoPlistParser(plistlib.PlistParser): class IPhotoPlistParser(plistlib._PlistParser):
"""A parser for iPhoto plists. """A parser for iPhoto plists.
iPhoto plists tend to be malformed, so we have to subclass the built-in parser to be a bit more iPhoto plists tend to be malformed, so we have to subclass the built-in parser to be a bit more
lenient. lenient.
""" """
def __init__(self): def __init__(self):
plistlib.PlistParser.__init__(self) plistlib._PlistParser.__init__(self, use_builtin_types=True, dict_type=dict)
# For debugging purposes, we remember the last bit of data to be analyzed so that we can # For debugging purposes, we remember the last bit of data to be analyzed so that we can
# log it in case of an exception # log it in case of an exception
self.lastdata = '' self.lastdata = ''
def getData(self): def get_data(self):
self.lastdata = plistlib.PlistParser.getData(self) self.lastdata = plistlib._PlistParser.get_data(self)
return self.lastdata return self.lastdata
def end_integer(self): def end_integer(self):
try: try:
self.addObject(int(self.getData())) self.add_object(int(self.get_data()))
except ValueError: except ValueError:
self.addObject(0) self.add_object(0)

View File

@@ -10,9 +10,9 @@ import logging
import multiprocessing import multiprocessing
from itertools import combinations from itertools import combinations
from hscommon.util import extract from hscommon.util import extract, iterconsume
from hscommon.trans import tr from hscommon.trans import tr
from jobprogress import job from hscommon.jobprogress import job
from core.engine import Match from core.engine import Match
from .block import avgdiff, DifferentBlockCountError, NoBlocksError from .block import avgdiff, DifferentBlockCountError, NoBlocksError
@@ -175,6 +175,7 @@ def getmatches(pictures, cache_path, threshold=75, match_scaled=False, j=job.nul
comparisons_to_do = list(combinations(chunks + [None], 2)) comparisons_to_do = list(combinations(chunks + [None], 2))
comparison_count = 0 comparison_count = 0
j.start_job(len(comparisons_to_do)) j.start_job(len(comparisons_to_do))
try:
for ref_chunk, other_chunk in comparisons_to_do: for ref_chunk, other_chunk in comparisons_to_do:
picinfo = {p.cache_id: get_picinfo(p) for p in ref_chunk} picinfo = {p.cache_id: get_picinfo(p) for p in ref_chunk}
ref_ids = [p.cache_id for p in ref_chunk] ref_ids = [p.cache_id for p in ref_chunk]
@@ -187,10 +188,23 @@ def getmatches(pictures, cache_path, threshold=75, match_scaled=False, j=job.nul
async_results.append(pool.apply_async(async_compare, args)) async_results.append(pool.apply_async(async_compare, args))
collect_results() collect_results()
collect_results(collect_all=True) collect_results(collect_all=True)
except MemoryError:
# Rare, but possible, even in 64bit situations (ref #264). What do we do now? We free us
# some wiggle room, log about the incident, and stop matching right here. We then process
# the matches we have. The rest of the process doesn't allocate much and we should be
# alright.
del comparisons_to_do, chunks, pictures # some wiggle room for the next statements
logging.warning("Ran out of memory when scanning! We had %d matches.", len(matches))
del matches[-len(matches)//3:] # some wiggle room to ensure we don't run out of memory again.
pool.close() pool.close()
result = [] result = []
for ref_id, other_id, percentage in j.iter_with_progress(matches, tr("Verified %d/%d matches"), every=10): myiter = j.iter_with_progress(
iterconsume(matches, reverse=False),
tr("Verified %d/%d matches"),
every=10,
count=len(matches),
)
for ref_id, other_id, percentage in myiter:
ref = id2picture[ref_id] ref = id2picture[ref_id]
other = id2picture[other_id] other = id2picture[other_id]
if percentage == 100 and ref.md5 != other.md5: if percentage == 100 and ref.md5 != other.md5:
@@ -202,3 +216,4 @@ def getmatches(pictures, cache_path, threshold=75, match_scaled=False, j=job.nul
return result return result
multiprocessing.freeze_support() multiprocessing.freeze_support()

View File

@@ -1,3 +1,16 @@
=== 6.8.0 (2014-05-11)
* This is mostly a dependencies upgrade.
* Upgraded to Python 3.3.
* Upgraded to Qt 5.
* Minimum Windows version is now Windows 7 64bit.
* Minimum Ubuntu version is now 14.04.
* Minimum OS X version is now 10.7 (Lion).
* ... But with a couple of little improvements.
* Improved documentation.
* Overwrite subfolders' state when setting states in folder dialog (#248)
* The error report dialog now brings the user to Github issues.
=== 6.7.0 (2013-12-08) === 6.7.0 (2013-12-08)
* Disable symlink/hardlink deletion option when not relevant. (#247) * Disable symlink/hardlink deletion option when not relevant. (#247)

View File

@@ -1,3 +1,25 @@
=== 2.10.1 (2014-10-12)
* Catch MemoryError better in block matching algo. (#264)
* Fix crash when reading some EXIF tags. [Mac] (#263, #265)
* Fixed ``AttributeError: 'ComboboxModel' object has no attribute 'reset'``. [Linux, Windows] (#254)
* Fixed a build problem introduced by Sphinx 1.2.3.
* Updated German localisation, by Frank Weber.
=== 2.10.0 (2014-05-03)
* This is mostly a dependencies upgrade.
* Upgraded to Python 3.3.
* Upgraded to Qt 5.
* Minimum Windows version is now Windows 7 64bit.
* Minimum Ubuntu version is now 14.04.
* Minimum OS X version is now 10.7 (Lion).
* ... But with a couple of little improvements.
* Improved documentation.
* Overwrite subfolders' state when setting states in folder dialog (#248)
* Fix empty Ignore List dialog bug. (#253)
* The error report dialog now brings the user to Github issues.
=== 2.9.0 (2013-12-22) === 2.9.0 (2013-12-22)
* Read RAW pictures EXIF tags. [Mac] (#234) * Read RAW pictures EXIF tags. [Mac] (#234)

View File

@@ -25,7 +25,7 @@ sys.path.insert(0, os.path.abspath(os.path.join('..', '..')))
def fix_nulljob_in_sig(app, what, name, obj, options, signature, return_annotation): def fix_nulljob_in_sig(app, what, name, obj, options, signature, return_annotation):
if signature: if signature:
signature = re.sub(r"<jobprogress.job.NullJob object at 0x[\da-f]+>", "nulljob", signature) signature = re.sub(r"<hscommon.jobprogress.job.NullJob object at 0x[\da-f]+>", "nulljob", signature)
return signature, return_annotation return signature, return_annotation
def setup(app): def setup(app):

View File

@@ -10,6 +10,8 @@ Unten befindet sich die Liste aller Menschen, die direkt oder indirekt zu dupeGu
| **Gregor Tätzner, deutsche Übersetzung** | **Gregor Tätzner, deutsche Übersetzung**
| **Frank Weber, deutsche Übersetzung**
| **Eric Dee, chinesische Übersetzung** | **Eric Dee, chinesische Übersetzung**
| **Aleš Nehyba, Czech localization** | **Aleš Nehyba, Czech localization**

View File

@@ -10,6 +10,8 @@ Below is the list of people who contributed, directly or indirectly to dupeGuru.
| **Gregor Tätzner, German localization** | **Gregor Tätzner, German localization**
| **Frank Weber, German localization**
| **Eric Dee, Chinese localization** | **Eric Dee, Chinese localization**
| **Aleš Nehyba, Czech localization** | **Aleš Nehyba, Czech localization**

View File

@@ -3,6 +3,7 @@ hscommon
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
:glob:
build build
conflict conflict
@@ -10,10 +11,6 @@ hscommon
notify notify
path path
util util
gui/base jobprogress/*
gui/text_field gui/*
gui/selectable_list
gui/table
gui/tree
gui/column
gui/progress_window

View File

@@ -0,0 +1,17 @@
hscommon.jobprogress.job
========================
.. automodule:: hscommon.jobprogress.job
.. autosummary::
Job
NullJob
.. autoclass:: Job
:members:
:private-members:
.. autoclass:: NullJob
:members:

View File

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

View File

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

View File

@@ -12,16 +12,16 @@ dupeGuru's codebase has quite a few design flaws. The Model, View and Controller
different classes, scattered around. If you're aware of that, it might help you to understand what different classes, scattered around. If you're aware of that, it might help you to understand what
the heck is going on. the heck is going on.
The central piece of dupeGuru is ``dupeguru.app.DupeGuru`` (in the ``core`` code). It's the only The central piece of dupeGuru is :class:`core.app.DupeGuru`. It's the only
interface to the python's code for the GUI code. A duplicate scan is started with interface to the python's code for the GUI code. A duplicate scan is started with
``start_scanning()``, directories are added through ``add_directory()``, etc.. :meth:`core.app.DupeGuru.start_scanning()`, directories are added through
:meth:`core.app.DupeGuru.add_directory()`, etc..
A lot of functionalities of the App are implemented in the platform-specific subclasses of A lot of functionalities of the App are implemented in the platform-specific subclasses of
``app.DupeGuru``, like ``app_cocoa.DupeGuru``, or the ``base.app.DupeGuru`` class in the PyQt :class:`core.app.DupeGuru`, like ``DupeGuru`` in ``cocoa/inter/app.py``, or the ``DupeGuru`` class
codebase. For example, when performing "Remove Selected From Results", in ``qt/base/app.py``. For example, when performing "Remove Selected From Results",
``app_cocoa.Dupeguru.RemoveSelected()`` on the Obj-C side, and ``RemoveSelected()`` on the cocoa side, and ``remove_duplicates()`` on the PyQt side, are
``base.app.DupeGuru.remove_duplicates()`` on the PyQt side, are respectively called to perform the respectively called to perform the thing.
thing. All of this is quite ugly, I know (see the "Refactoring" section below).
.. _jobs: .. _jobs:
@@ -29,23 +29,26 @@ Jobs
---- ----
A lot of operations in dupeGuru take a significant amount of time. This is why there's a generalized A lot of operations in dupeGuru take a significant amount of time. This is why there's a generalized
threaded job mechanism built-in ``app.DupeGuru``. First, ``app.DupeGuru`` has a ``progress`` member threaded job mechanism built-in :class:`~core.app.DupeGuru`. First, :class:`~core.app.DupeGuru` has
which is an instance of ``jobprogress.job.ThreadedJobPerformer``. It lets the GUI code know of the a ``progress`` member which is an instance of
progress of the current threaded job. When ``app.DupeGuru`` needs to start a job, it calls :class:`~hscommon.jobprogress.performer.ThreadedJobPerformer`. It lets the GUI code know of the progress
of the current threaded job. When :class:`~core.app.DupeGuru` needs to start a job, it calls
``_start_job()`` and the platform specific subclass deals with the details of starting the job. ``_start_job()`` and the platform specific subclass deals with the details of starting the job.
Core principles Core principles
--------------- ---------------
The core of the duplicate matching takes place (for SE and ME, not PE) in ``dupeguru.engine``. The core of the duplicate matching takes place (for SE and ME, not PE) in :mod:`core.engine`.
There's ``MatchFactory.getmatches()`` which take a list of ``fs.File`` instances and return a list There's :func:`core.engine.getmatches` which take a list of :class:`core.fs.File` instances and
of ``(firstfile, secondfile, match_percentage)`` matches. Then, there's ``get_groups()`` which takes return a list of ``(firstfile, secondfile, match_percentage)`` matches. Then, there's
a list of matches and returns a list of ``Group`` instances (a ``Group`` is basically a list of :func:`core.engine.get_groups` which takes a list of matches and returns a list of
``fs.File`` matching together). :class:`.Group` instances (a :class:`.Group` is basically a list of :class:`.File` matching
together).
When a scan is over, the final result (the list of groups from ``get_groups()``) is placed into When a scan is over, the final result (the list of groups from :func:`.get_groups`) is placed into
``app.DupeGuru.results``, which is a ``results.Results`` instance. The ``Results`` instance is where :attr:`core.app.DupeGuru.results`, which is a :class:`core.results.Results` instance. The
all the dupe marking, sorting, removing, power marking, etc. takes place. :class:`~.Results` instance is where all the dupe marking, sorting, removing, power marking, etc.
takes place.
API API
--- ---

View File

@@ -9,6 +9,8 @@ Voici la liste des contributeurs de dupeGuru. Merci!
| **Gregor Tätzner, localisation allemande** | **Gregor Tätzner, localisation allemande**
| **Frank Weber, localisation allemande**
| **Eric Dee, localisation choinoise** | **Eric Dee, localisation choinoise**
| **Aleš Nehyba, localisation tchèque** | **Aleš Nehyba, localisation tchèque**

View File

@@ -10,6 +10,8 @@
| **Gregor Tätzner, Գերմաներեն թարգմանիչը** | **Gregor Tätzner, Գերմաներեն թարգմանիչը**
| **Frank Weber, Գերմաներեն թարգմանիչը**
| **Eric Dee, Չինարեն թարգմանիչը** | **Eric Dee, Չինարեն թարգմանիչը**
| **Aleš Nehyba, Չեխերեն թարգմանիչը** | **Aleš Nehyba, Չեխերեն թարգմանիչը**

View File

@@ -10,6 +10,8 @@
| **Gregor Tätzner, Немецкая локализация** | **Gregor Tätzner, Немецкая локализация**
| **Frank Weber, Немецкая локализация**
| **Eric Dee, Китайская локализация** | **Eric Dee, Китайская локализация**
| **Aleš Nehyba, Чешский локализации** | **Aleš Nehyba, Чешский локализации**

View File

@@ -10,6 +10,8 @@
| **Gregor Tätzner, Німецька локалізація** | **Gregor Tätzner, Німецька локалізація**
| **Frank Weber, Німецька локалізація**
| **Eric Dee, Китайська локалізація** | **Eric Dee, Китайська локалізація**
| **Aleš Nehyba, Чеський локалізації** | **Aleš Nehyba, Чеський локалізації**

View File

@@ -79,7 +79,6 @@ except ImportError:
else: else:
qtfolder = QStandardPaths.DataLocation qtfolder = QStandardPaths.DataLocation
return QStandardPaths.standardLocations(qtfolder)[0] return QStandardPaths.standardLocations(qtfolder)[0]
except ImportError: except ImportError:
# We're either running tests, and these functions don't matter much or we're in a really # We're either running tests, and these functions don't matter much or we're in a really
# weird situation. Let's just have dummy fallbacks. # weird situation. Let's just have dummy fallbacks.

View File

@@ -5,8 +5,7 @@
# which should be included with this package. The terms are also available at # which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license # http://www.hardcoded.net/licenses/bsd_license
from jobprogress.performer import ThreadedJobPerformer from ..jobprogress.performer import ThreadedJobPerformer
from .base import GUIObject from .base import GUIObject
from .text_field import TextField from .text_field import TextField
@@ -41,7 +40,7 @@ class ProgressWindowView:
class ProgressWindow(GUIObject, ThreadedJobPerformer): class ProgressWindow(GUIObject, ThreadedJobPerformer):
"""Cross-toolkit GUI-enabled progress window. """Cross-toolkit GUI-enabled progress window.
This class allows you to run a long running, `job enabled`_ function in a separate thread and This class allows you to run a long running, job enabled function in a separate thread and
allow the user to follow its progress with a progress dialog. allow the user to follow its progress with a progress dialog.
To use it, you start your long-running job with :meth:`run` and then have your UI layer To use it, you start your long-running job with :meth:`run` and then have your UI layer
@@ -49,13 +48,11 @@ class ProgressWindow(GUIObject, ThreadedJobPerformer):
:meth:`pulse` in the main thread because GUI toolkit usually only support calling UI-related :meth:`pulse` in the main thread because GUI toolkit usually only support calling UI-related
functions from the main thread. functions from the main thread.
We subclass :class:`.GUIObject` and ``ThreadedJobPerformer`` (from the ``jobprogress`` library). We subclass :class:`.GUIObject` and :class:`.ThreadedJobPerformer`.
Expected view: :class:`ProgressWindowView`. Expected view: :class:`ProgressWindowView`.
:param finishfunc: A function ``f(jobid)`` that is called when a job is completed. ``jobid`` is :param finishfunc: A function ``f(jobid)`` that is called when a job is completed. ``jobid`` is
an arbitrary id passed to :meth:`run`. an arbitrary id passed to :meth:`run`.
.. _job enabled: https://pypi.python.org/pypi/jobprogress
""" """
def __init__(self, finish_func): def __init__(self, finish_func):
# finish_func(jobid) is the function that is called when a job is completed. # finish_func(jobid) is the function that is called when a job is completed.
@@ -105,8 +102,8 @@ class ProgressWindow(GUIObject, ThreadedJobPerformer):
def run(self, jobid, title, target, args=()): def run(self, jobid, title, target, args=()):
"""Starts a threaded job. """Starts a threaded job.
The ``target`` function will be sent, as its first argument, a ``Job`` instance (from the The ``target`` function will be sent, as its first argument, a :class:`.Job` instance which
``jobprogress`` library) which it can use to report on its progress. it can use to report on its progress.
:param jobid: Arbitrary identifier which will be passed to ``finish_func()`` at the end. :param jobid: Arbitrary identifier which will be passed to ``finish_func()`` at the end.
:param title: A title for the task you're starting. :param title: A title for the task you're starting.

View File

166
hscommon/jobprogress/job.py Normal file
View File

@@ -0,0 +1,166 @@
# Created By: Virgil Dupras
# Created On: 2004/12/20
# Copyright 2011 Hardcoded Software (http://www.hardcoded.net)
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
class JobCancelled(Exception):
"The user has cancelled the job"
class JobInProgressError(Exception):
"A job is already being performed, you can't perform more than one at the same time."
class JobCountError(Exception):
"The number of jobs started have exceeded the number of jobs allowed"
class Job:
"""Manages a job's progression and return it's progression through a callback.
Note that this class is not foolproof. For example, you could call
start_subjob, and then call add_progress from the parent job, and nothing
would stop you from doing it. However, it would mess your progression
because it is the sub job that is supposed to drive the progression.
Another example would be to start a subjob, then start another, and call
add_progress from the old subjob. Once again, it would mess your progression.
There are no stops because it would remove the lightweight aspect of the
class (A Job would need to have a Parent instead of just a callback,
and the parent could be None. A lot of checks for nothing.).
Another one is that nothing stops you from calling add_progress right after
SkipJob.
"""
#---Magic functions
def __init__(self, job_proportions, callback):
"""Initialize the Job with 'jobcount' jobs. Start every job with
start_job(). Every time the job progress is updated, 'callback' is called
'callback' takes a 'progress' int param, and a optional 'desc'
parameter. Callback must return false if the job must be cancelled.
"""
if not hasattr(callback, '__call__'):
raise TypeError("'callback' MUST be set when creating a Job")
if isinstance(job_proportions, int):
job_proportions = [1] * job_proportions
self._job_proportions = list(job_proportions)
self._jobcount = sum(job_proportions)
self._callback = callback
self._current_job = 0
self._passed_jobs = 0
self._progress = 0
self._currmax = 1
#---Private
def _subjob_callback(self, progress, desc=''):
"""This is the callback passed to children jobs.
"""
self.set_progress(progress, desc)
return True #if JobCancelled has to be raised, it will be at the highest level
def _do_update(self, desc):
"""Calls the callback function with a % progress as a parameter.
The parameter is a int in the 0-100 range.
"""
if self._current_job:
passed_progress = self._passed_jobs * self._currmax
current_progress = self._current_job * self._progress
total_progress = self._jobcount * self._currmax
progress = ((passed_progress + current_progress) * 100) // total_progress
else:
progress = -1 # indeterminate
# It's possible that callback doesn't support a desc arg
result = self._callback(progress, desc) if desc else self._callback(progress)
if not result:
raise JobCancelled()
#---Public
def add_progress(self, progress=1, desc=''):
self.set_progress(self._progress + progress, desc)
def check_if_cancelled(self):
self._do_update('')
def iter_with_progress(self, iterable, desc_format=None, every=1, count=None):
"""Iterate through ``iterable`` while automatically adding progress.
WARNING: We need our iterable's length. If ``iterable`` is not a sequence (that is,
something we can call ``len()`` on), you *have* to specify a count through the ``count``
argument. If ``count`` is ``None``, ``len(iterable)`` is used.
"""
if count is None:
count = len(iterable)
desc = ''
if desc_format:
desc = desc_format % (0, count)
self.start_job(count, desc)
for i, element in enumerate(iterable, start=1):
yield element
if i % every == 0:
if desc_format:
desc = desc_format % (i, count)
self.add_progress(progress=every, desc=desc)
if desc_format:
desc = desc_format % (count, count)
self.set_progress(100, desc)
def start_job(self, max_progress=100, desc=''):
"""Begin work on the next job. You must not call start_job more than
'jobcount' (in __init__) times.
'max' is the job units you are to perform.
'desc' is the description of the job.
"""
self._passed_jobs += self._current_job
try:
self._current_job = self._job_proportions.pop(0)
except IndexError:
raise JobCountError()
self._progress = 0
self._currmax = max(1, max_progress)
self._do_update(desc)
def start_subjob(self, job_proportions, desc=''):
"""Starts a sub job. Use this when you want to split a job into
multiple smaller jobs. Pretty handy when starting a process where you
know how many subjobs you will have, but don't know the work unit count
for every of them.
returns the Job object
"""
self.start_job(100, desc)
return Job(job_proportions, self._subjob_callback)
def set_progress(self, progress, desc=''):
"""Sets the progress of the current job to 'progress', and call the
callback
"""
self._progress = progress
if self._progress > self._currmax:
self._progress = self._currmax
if self._progress < 0:
self._progress = 0
self._do_update(desc)
class NullJob:
def __init__(self, *args, **kwargs):
pass
def add_progress(self, *args, **kwargs):
pass
def check_if_cancelled(self):
pass
def iter_with_progress(self, sequence, *args, **kwargs):
return iter(sequence)
def start_job(self, *args, **kwargs):
pass
def start_subjob(self, *args, **kwargs):
return NullJob()
def set_progress(self, *args, **kwargs):
pass
nulljob = NullJob()

View File

@@ -0,0 +1,72 @@
# Created By: Virgil Dupras
# Created On: 2010-11-19
# Copyright 2011 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
from threading import Thread
import sys
from .job import Job, JobInProgressError, JobCancelled
class ThreadedJobPerformer:
"""Run threaded jobs and track progress.
To run a threaded job, first create a job with _create_job(), then call _run_threaded(), with
your work function as a parameter.
Example:
j = self._create_job()
self._run_threaded(self.some_work_func, (arg1, arg2, j))
"""
_job_running = False
last_error = None
#--- Protected
def create_job(self):
if self._job_running:
raise JobInProgressError()
self.last_progress = -1
self.last_desc = ''
self.job_cancelled = False
return Job(1, self._update_progress)
def _async_run(self, *args):
target = args[0]
args = tuple(args[1:])
self._job_running = True
self.last_error = None
try:
target(*args)
except JobCancelled:
pass
except Exception as e:
self.last_error = e
self.last_traceback = sys.exc_info()[2]
finally:
self._job_running = False
self.last_progress = None
def reraise_if_error(self):
"""Reraises the error that happened in the thread if any.
Call this after the caller of run_threaded detected that self._job_running returned to False
"""
if self.last_error is not None:
raise self.last_error.with_traceback(self.last_traceback)
def _update_progress(self, newprogress, newdesc=''):
self.last_progress = newprogress
if newdesc:
self.last_desc = newdesc
return not self.job_cancelled
def run_threaded(self, target, args=()):
if self._job_running:
raise JobInProgressError()
args = (target, ) + args
Thread(target=self._async_run, args=args).start()

View File

@@ -0,0 +1,52 @@
# Created By: Virgil Dupras
# Created On: 2009-09-14
# Copyright 2011 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
from PyQt5.QtCore import pyqtSignal, Qt, QTimer
from PyQt5.QtWidgets import QProgressDialog
from . import performer
class Progress(QProgressDialog, performer.ThreadedJobPerformer):
finished = pyqtSignal(['QString'])
def __init__(self, parent):
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint
QProgressDialog.__init__(self, '', "Cancel", 0, 100, parent, flags)
self.setModal(True)
self.setAutoReset(False)
self.setAutoClose(False)
self._timer = QTimer()
self._jobid = ''
self._timer.timeout.connect(self.updateProgress)
def updateProgress(self):
# the values might change before setValue happens
last_progress = self.last_progress
last_desc = self.last_desc
if not self._job_running or last_progress is None:
self._timer.stop()
self.close()
if not self.job_cancelled:
self.finished.emit(self._jobid)
return
if self.wasCanceled():
self.job_cancelled = True
return
if last_desc:
self.setLabelText(last_desc)
self.setValue(last_progress)
def run(self, jobid, title, target, args=()):
self._jobid = jobid
self.reset()
self.setLabelText('')
self.run_threaded(target, args)
self.setWindowTitle(title)
self.show()
self._timer.start(500)

0
hscommon/path.py Executable file → Normal file
View File

View File

@@ -1,179 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2009-05-16
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
import re
from hashlib import md5
from . import desktop
from .trans import trget
tr = trget('hscommon')
ALL_APPS = [
(1, 'dupeGuru'),
(2, 'moneyGuru'),
(3, 'musicGuru'),
(6, 'PdfMasher'),
]
OLDAPPIDS = {
1: {1, 4, 5},
2: {6, },
3: {2, },
}
class InvalidCodeError(Exception):
"""The supplied code is invalid."""
DEMO_PROMPT = tr("{name} is fairware, which means \"open source software developed with expectation "
"of fair contributions from users\". It's a very interesting concept, but one year of fairware has "
"shown that most people just want to know how much it costs and not be bothered with theories "
"about intellectual property."
"\n\n"
"So I won't bother you and will be very straightforward: You can try {name} for free but you have "
"to buy it in order to use it without limitations. In demo mode, {name} {limitation}."
"\n\n"
"So it's as simple as this. If you're curious about fairware, however, I encourage you to read "
"more about it by clicking on the \"Fairware?\" button.")
class RegistrableApplication:
#--- View interface
# get_default(key_name)
# set_default(key_name, value)
# setup_as_registered()
# show_message(msg)
# show_demo_nag(prompt)
PROMPT_NAME = "<undefined>"
DEMO_LIMITATION = "<undefined>"
def __init__(self, view, appid):
self.view = view
self.appid = appid
self.registered = False
self.fairware_mode = False
self.registration_code = ''
self.registration_email = ''
self._unpaid_hours = None
@staticmethod
def _is_code_valid(appid, code, email):
if len(code) != 32:
return False
appid = str(appid)
for i in range(100):
blob = appid + email + str(i) + 'aybabtu'
digest = md5(blob.encode('utf-8')).hexdigest()
if digest == code:
return True
return False
def _set_registration(self, code, email):
self.validate_code(code, email)
self.registration_code = code
self.registration_email = email
self.registered = True
self.view.setup_as_registered()
def initial_registration_setup(self):
# Should be called only after the app is finished launching
if self.registered:
# We've already set registration in a hardcoded way (for example, for the Ubuntu Store)
# Just ignore registration, but not before having set as registered.
self.view.setup_as_registered()
return
code = self.view.get_default('RegistrationCode')
email = self.view.get_default('RegistrationEmail')
if code and email:
try:
self._set_registration(code, email)
except InvalidCodeError:
pass
if not self.registered:
if self.view.get_default('FairwareMode'):
self.fairware_mode = True
if not self.fairware_mode:
prompt = DEMO_PROMPT.format(name=self.PROMPT_NAME, limitation=self.DEMO_LIMITATION)
self.view.show_demo_nag(prompt)
def validate_code(self, code, email):
code = code.strip().lower()
email = email.strip().lower()
if self._is_code_valid(self.appid, code, email):
return
# Check if it's not an old reg code
for oldappid in OLDAPPIDS.get(self.appid, []):
if self._is_code_valid(oldappid, code, email):
return
# let's see if the user didn't mix the fields up
if self._is_code_valid(self.appid, email, code):
msg = "Invalid Code. It seems like you inverted the 'Registration Code' and"\
"'Registration E-mail' field."
raise InvalidCodeError(msg)
# Is the code a paypal transaction id?
if re.match(r'^[a-z\d]{17}$', code) is not None:
msg = "The code you submitted looks like a Paypal transaction ID. Registration codes are "\
"32 digits codes which you should have received in a separate e-mail. If you haven't "\
"received it yet, please visit http://www.hardcoded.net/support/"
raise InvalidCodeError(msg)
# Invalid, let's see if it's a code for another app.
for appid, appname in ALL_APPS:
if self._is_code_valid(appid, code, email):
msg = "This code is a {0} code. You're running the wrong application. You can "\
"download the correct application at http://www.hardcoded.net".format(appname)
raise InvalidCodeError(msg)
DEFAULT_MSG = "Your code is invalid. Make sure that you wrote the good code. Also make sure "\
"that the e-mail you gave is the same as the e-mail you used for your purchase."
raise InvalidCodeError(DEFAULT_MSG)
def set_registration(self, code, email, register_os):
if not self.fairware_mode and 'fairware' in {code.strip().lower(), email.strip().lower()}:
self.fairware_mode = True
self.view.set_default('FairwareMode', True)
self.view.show_message("Fairware mode enabled.")
return True
try:
self._set_registration(code, email)
self.view.show_message("Your code is valid. Thanks!")
if register_os:
self.register_os()
self.view.set_default('RegistrationCode', self.registration_code)
self.view.set_default('RegistrationEmail', self.registration_email)
return True
except InvalidCodeError as e:
self.view.show_message(str(e))
return False
def register_os(self):
# We don't do that anymore.
pass
def contribute(self):
desktop.open_url("http://open.hardcoded.net/contribute/")
def buy(self):
desktop.open_url("http://www.hardcoded.net/purchase.htm")
def about_fairware(self):
desktop.open_url("http://open.hardcoded.net/about/")
@property
def should_show_fairware_reminder(self):
return (not self.registered) and (self.fairware_mode) and (self.unpaid_hours >= 1)
@property
def should_apply_demo_limitation(self):
return (not self.registered) and (not self.fairware_mode)
@property
def unpaid_hours(self):
# We don't bother verifying unpaid hours anymore, the only app that still has fairware
# dialogs is dupeGuru and it has a huge surplus. Now, "fairware mode" really means
# "free mode".
return 0

View File

@@ -66,4 +66,8 @@ def gen(basepath, destpath, changelogpath, tixurl, confrepl=None, confpath=None,
# missing dependencies which are in the virtualenv). Here, we do exactly what is done when # missing dependencies which are in the virtualenv). Here, we do exactly what is done when
# calling the command from bash. # calling the command from bash.
cmd = load_entry_point('Sphinx', 'console_scripts', 'sphinx-build') cmd = load_entry_point('Sphinx', 'console_scripts', 'sphinx-build')
try:
cmd(['sphinx-build', basepath, destpath]) cmd(['sphinx-build', basepath, destpath])
except SystemExit:
print("Sphinx called sys.exit(), but we're cancelling it because we don't actually want to exit")

View File

@@ -65,6 +65,12 @@ def test_trailiter():
eq_(list(trailiter(['foo', 'bar'], skipfirst=True)), [('foo', 'bar')]) eq_(list(trailiter(['foo', 'bar'], skipfirst=True)), [('foo', 'bar')])
eq_(list(trailiter([], skipfirst=True)), []) # no crash eq_(list(trailiter([], skipfirst=True)), []) # no crash
def test_iterconsume():
# We just want to make sure that we return *all* items and that we're not mistakenly skipping
# one.
eq_(list(range(2500)), list(iterconsume(list(range(2500)))))
eq_(list(reversed(range(2500))), list(iterconsume(list(range(2500)), reverse=False)))
#--- String #--- String
def test_escape(): def test_escape():

View File

@@ -117,6 +117,21 @@ def trailiter(iterable, skipfirst=False):
yield prev, item yield prev, item
prev = item prev = item
def iterconsume(seq, reverse=True):
"""Iterate over ``seq`` and pops yielded objects.
Because we use the ``pop()`` method, we reverse ``seq`` before proceeding. If you don't need
to do that, set ``reverse`` to ``False``.
This is useful in tight memory situation where you are looping over a sequence of objects that
are going to be discarded afterwards. If you're creating other objects during that iteration
you might want to use this to avoid ``MemoryError``.
"""
if reverse:
seq.reverse()
while seq:
yield seq.pop()
#--- String related #--- String related
def escape(s, to_escape, escape_with='\\'): def escape(s, to_escape, escape_with='\\'):

View File

@@ -1,9 +1,10 @@
# Translators: # Translators:
# Harakiri1337, 2014
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: dupeGuru\n" "Project-Id-Version: dupeGuru\n"
"PO-Revision-Date: 2013-11-20 11:53+0000\n" "PO-Revision-Date: 2014-06-03 21:56+0000\n"
"Last-Translator: hsoft <hsoft@hardcoded.net>\n" "Last-Translator: Harakiri1337\n"
"Language-Team: German (http://www.transifex.com/projects/p/dupeguru/language/de/)\n" "Language-Team: German (http://www.transifex.com/projects/p/dupeguru/language/de/)\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: utf-8\n" "Content-Transfer-Encoding: utf-8\n"
@@ -32,7 +33,7 @@ msgstr "Ordner"
#: core/prioritize.py:88 core_me/result_table.py:18 core_pe/result_table.py:18 #: core/prioritize.py:88 core_me/result_table.py:18 core_pe/result_table.py:18
#: core_se/result_table.py:18 #: core_se/result_table.py:18
msgid "Filename" msgid "Filename"
msgstr "Filename" msgstr "Dateiname"
#: core/prioritize.py:147 #: core/prioritize.py:147
msgid "Size" msgid "Size"
@@ -41,11 +42,11 @@ msgstr "Größe"
#: core/prioritize.py:153 core_me/result_table.py:25 #: core/prioritize.py:153 core_me/result_table.py:25
#: core_pe/result_table.py:24 core_se/result_table.py:22 #: core_pe/result_table.py:24 core_se/result_table.py:22
msgid "Modification" msgid "Modification"
msgstr "Modifikation" msgstr "Geändert"
#: core_me/prioritize.py:16 #: core_me/prioritize.py:16
msgid "Duration" msgid "Duration"
msgstr "" msgstr "Dauer"
#: core_me/prioritize.py:22 core_me/result_table.py:22 #: core_me/prioritize.py:22 core_me/result_table.py:22
msgid "Bitrate" msgid "Bitrate"
@@ -53,7 +54,7 @@ msgstr "Bitrate"
#: core_me/prioritize.py:28 #: core_me/prioritize.py:28
msgid "Samplerate" msgid "Samplerate"
msgstr "" msgstr "Abtastrate"
#: core_me/result_table.py:20 #: core_me/result_table.py:20
msgid "Size (MB)" msgid "Size (MB)"
@@ -89,7 +90,7 @@ msgstr "Jahr"
#: core_me/result_table.py:31 #: core_me/result_table.py:31
msgid "Track Number" msgid "Track Number"
msgstr "Stück Nummer" msgstr "Titel Nummer"
#: core_me/result_table.py:32 #: core_me/result_table.py:32
msgid "Comment" msgid "Comment"
@@ -102,16 +103,16 @@ msgstr "Übereinstimmung %"
#: core_me/result_table.py:34 core_se/result_table.py:24 #: core_me/result_table.py:34 core_se/result_table.py:24
msgid "Words Used" msgid "Words Used"
msgstr "Wörter genutzt" msgstr "genutzte Wörter"
#: core_me/result_table.py:35 core_pe/result_table.py:26 #: core_me/result_table.py:35 core_pe/result_table.py:26
#: core_se/result_table.py:25 #: core_se/result_table.py:25
msgid "Dupe Count" msgid "Dupe Count"
msgstr "Anzahl Duplikate" msgstr "Anzahl der Duplikate"
#: core_pe/prioritize.py:16 core_pe/result_table.py:22 #: core_pe/prioritize.py:16 core_pe/result_table.py:22
msgid "Dimensions" msgid "Dimensions"
msgstr "Dimensionen" msgstr "Auflösung"
#: core_pe/result_table.py:20 core_se/result_table.py:20 #: core_pe/result_table.py:20 core_se/result_table.py:20
msgid "Size (KB)" msgid "Size (KB)"
@@ -119,4 +120,4 @@ msgstr "Größe (KB)"
#: core_pe/result_table.py:23 #: core_pe/result_table.py:23
msgid "EXIF Timestamp" msgid "EXIF Timestamp"
msgstr "" msgstr "EXIF Zeitstempel"

View File

@@ -1,9 +1,11 @@
# Translators: # Translators:
# Harakiri1337, 2014
# Frank Weber <frank.weber@gmail.com>, 2014
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: dupeGuru\n" "Project-Id-Version: dupeGuru\n"
"PO-Revision-Date: 2013-12-07 15:22+0000\n" "PO-Revision-Date: 2014-09-26 21:24+0000\n"
"Last-Translator: hsoft <hsoft@hardcoded.net>\n" "Last-Translator: Frank Weber <frank.weber@gmail.com>\n"
"Language-Team: German (http://www.transifex.com/projects/p/dupeguru/language/de/)\n" "Language-Team: German (http://www.transifex.com/projects/p/dupeguru/language/de/)\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: utf-8\n" "Content-Transfer-Encoding: utf-8\n"
@@ -12,17 +14,19 @@ msgstr ""
#: core/app.py:39 #: core/app.py:39
msgid "There are no marked duplicates. Nothing has been done." msgid "There are no marked duplicates. Nothing has been done."
msgstr "" msgstr "Keine markierten Duplikate, daher wurde nichts getan."
#: core/app.py:40 #: core/app.py:40
msgid "There are no selected duplicates. Nothing has been done." msgid "There are no selected duplicates. Nothing has been done."
msgstr "" msgstr "Keine ausgewählten Duplikate, daher wurde nichts getan."
#: core/app.py:41 #: core/app.py:41
msgid "" msgid ""
"You're about to open many files at once. Depending on what those files are " "You're about to open many files at once. Depending on what those files are "
"opened with, doing so can create quite a mess. Continue?" "opened with, doing so can create quite a mess. Continue?"
msgstr "" msgstr ""
"Sie sind dabei, sehr viele Dateien gleichzeitig zu öffnen. Das kann zu "
"ziemlichem Durcheinander führen! Trotzdem fortfahren?"
#: core/app.py:57 #: core/app.py:57
msgid "Scanning for duplicates" msgid "Scanning for duplicates"
@@ -30,23 +34,23 @@ msgstr "Suche nach Duplikaten"
#: core/app.py:58 #: core/app.py:58
msgid "Loading" msgid "Loading"
msgstr "Laden" msgstr "Lade"
#: core/app.py:59 #: core/app.py:59
msgid "Moving" msgid "Moving"
msgstr "Verschieben" msgstr "Verschiebe"
#: core/app.py:60 #: core/app.py:60
msgid "Copying" msgid "Copying"
msgstr "Kopieren" msgstr "Kopiere"
#: core/app.py:61 #: core/app.py:61
msgid "Sending to Trash" msgid "Sending to Trash"
msgstr "Verschiebe in den Mülleimer" msgstr "Verschiebe in den Papierkorb"
#: core/app.py:64 #: core/app.py:64
msgid "Sending files to the recycle bin" msgid "Sending files to the recycle bin"
msgstr "Sende Dateien in den Mülleimer" msgstr "Verschiebe Dateien in den Papierkorb"
#: core/app.py:290 #: core/app.py:290
msgid "" msgid ""
@@ -62,29 +66,32 @@ msgstr "Keine Duplikate gefunden."
#: core/app.py:310 #: core/app.py:310
msgid "All marked files were copied successfully." msgid "All marked files were copied successfully."
msgstr "" msgstr "Alle markierten Dateien wurden erfolgreich kopiert."
#: core/app.py:311 #: core/app.py:311
msgid "All marked files were moved successfully." msgid "All marked files were moved successfully."
msgstr "" msgstr "Alle markierten Dateien wurden erfolgreich verschoben."
#: core/app.py:312 #: core/app.py:312
msgid "All marked files were successfully sent to Trash." msgid "All marked files were successfully sent to Trash."
msgstr "" msgstr ""
"Alle markierten Dateien wurden erfolgreich in den Papierkorb verschoben."
#: core/app.py:349 #: core/app.py:349
msgid "'{}' already is in the list." msgid "'{}' already is in the list."
msgstr "" msgstr "'{}' ist bereits in der Liste."
#: core/app.py:351 #: core/app.py:351
msgid "'{}' does not exist." msgid "'{}' does not exist."
msgstr "" msgstr "'{}' existiert nicht."
#: core/app.py:360 #: core/app.py:360
msgid "" msgid ""
"All selected %d matches are going to be ignored in all subsequent scans. " "All selected %d matches are going to be ignored in all subsequent scans. "
"Continue?" "Continue?"
msgstr "%d Dateien werden in zukünftigen Scans ignoriert werden. Fortfahren?" msgstr ""
"Alle %d ausgewählten Dateien werden in zukünftigen Scans ignoriert. "
"Fortfahren?"
#: core/app.py:426 #: core/app.py:426
msgid "copy" msgid "copy"
@@ -96,17 +103,17 @@ msgstr "verschieben"
#: core/app.py:427 #: core/app.py:427
msgid "Select a directory to {} marked files to" msgid "Select a directory to {} marked files to"
msgstr "Wählen sie einen Ordner zum {} der ausgewählten Dateien" msgstr "Wählen Sie einen Ordner zum {} der ausgewählten Dateien."
#: core/app.py:464 #: core/app.py:464
msgid "Select a destination for your exported CSV" msgid "Select a destination for your exported CSV"
msgstr "" msgstr "Zielverzeichnis für den CSV Export angeben"
#: core/app.py:489 #: core/app.py:489
msgid "You have no custom command set up. Set it up in your preferences." msgid "You have no custom command set up. Set it up in your preferences."
msgstr "" msgstr ""
"Sie haben keinen eigenen Befehl erstellt. Bitte in den Einstellungen " "Sie haben noch keinen Befehl erstellt. Bitte dies in den Einstellungen vornehmen.\n"
"konfigurieren." "Bsp.: \"C:\\Program Files\\Diff\\Diff.exe\" \"%d\" \"%r\""
#: core/app.py:641 core/app.py:654 #: core/app.py:641 core/app.py:654
msgid "You are about to remove %d files from results. Continue?" msgid "You are about to remove %d files from results. Continue?"
@@ -114,15 +121,15 @@ msgstr "%d Dateien werden aus der Ergebnisliste entfernt. Fortfahren?"
#: core/app.py:688 #: core/app.py:688
msgid "{} duplicate groups were changed by the re-prioritization." msgid "{} duplicate groups were changed by the re-prioritization."
msgstr "" msgstr "{} Duplikat-Gruppen wurden durch die Neu-Priorisierung geändert."
#: core/app.py:716 #: core/app.py:716
msgid "Collecting files to scan" msgid "Collecting files to scan"
msgstr "Sammle Dateien zum Scannen" msgstr "Sammle zu scannende Dateien..."
#: core/app.py:727 #: core/app.py:727
msgid "The selected directories contain no scannable file." msgid "The selected directories contain no scannable file."
msgstr "Der ausgewählte Ordner enthält keine scannbare Dateien." msgstr "Ausgewählte Ordner enthalten keine scannbaren Dateien."
#: core/app.py:768 #: core/app.py:768
msgid "%s (%d discarded)" msgid "%s (%d discarded)"
@@ -130,11 +137,11 @@ msgstr "%s (%d verworfen)"
#: core/engine.py:220 core/engine.py:265 #: core/engine.py:220 core/engine.py:265
msgid "0 matches found" msgid "0 matches found"
msgstr "0 Paare gefunden" msgstr "0 Übereinstimmungen gefunden"
#: core/engine.py:238 core/engine.py:273 #: core/engine.py:238 core/engine.py:273
msgid "%d matches found" msgid "%d matches found"
msgstr "%d Paare gefunden" msgstr "%d Übereinstimmungen gefunden"
#: core/engine.py:258 core/scanner.py:79 #: core/engine.py:258 core/scanner.py:79
msgid "Read size of %d/%d files" msgid "Read size of %d/%d files"
@@ -142,51 +149,51 @@ msgstr "Lese Größe von %d/%d Dateien"
#: core/engine.py:464 #: core/engine.py:464
msgid "Grouped %d/%d matches" msgid "Grouped %d/%d matches"
msgstr "%d/%d Paare gruppiert" msgstr "%d/%d Übereinstimmungen gruppiert"
#: core/gui/deletion_options.py:69 #: core/gui/deletion_options.py:69
msgid "You are sending {} file(s) to the Trash." msgid "You are sending {} file(s) to the Trash."
msgstr "" msgstr "Verschiebe {} Datei(en) in den Papierkorb."
#: core/gui/ignore_list_dialog.py:24 #: core/gui/ignore_list_dialog.py:24
msgid "Do you really want to remove all %d items from the ignore list?" msgid "Do you really want to remove all %d items from the ignore list?"
msgstr "Möchten Sie wirklich alle %d Einträge aus der Ignorier-Liste löschen?" msgstr "Möchten Sie wirklich alle %d Einträge aus der Ausnahmeliste löschen?"
#: core/prioritize.py:68 #: core/prioritize.py:68
msgid "None" msgid "None"
msgstr "" msgstr "Nichts"
#: core/prioritize.py:96 #: core/prioritize.py:96
msgid "Ends with number" msgid "Ends with number"
msgstr "Ends with number" msgstr "Endet mit Zahl"
#: core/prioritize.py:97 #: core/prioritize.py:97
msgid "Doesn't end with number" msgid "Doesn't end with number"
msgstr "Doesn't end with number" msgstr "Endet nicht mit Zahl"
#: core/prioritize.py:98 #: core/prioritize.py:98
msgid "Longest" msgid "Longest"
msgstr "" msgstr "Längste"
#: core/prioritize.py:99 #: core/prioritize.py:99
msgid "Shortest" msgid "Shortest"
msgstr "" msgstr "Kürzeste"
#: core/prioritize.py:132 #: core/prioritize.py:132
msgid "Highest" msgid "Highest"
msgstr "Highest" msgstr "Höchste"
#: core/prioritize.py:132 #: core/prioritize.py:132
msgid "Lowest" msgid "Lowest"
msgstr "Lowest" msgstr "Niedrigste"
#: core/prioritize.py:159 #: core/prioritize.py:159
msgid "Newest" msgid "Newest"
msgstr "Newest" msgstr "Neuste"
#: core/prioritize.py:159 #: core/prioritize.py:159
msgid "Oldest" msgid "Oldest"
msgstr "Oldest" msgstr "Älterste"
#: core/results.py:126 #: core/results.py:126
msgid "%d / %d (%s / %s) duplicates marked." msgid "%d / %d (%s / %s) duplicates marked."
@@ -202,11 +209,11 @@ msgstr "Lese Metadaten von %d/%d Dateien"
#: core/scanner.py:130 #: core/scanner.py:130
msgid "Removing false matches" msgid "Removing false matches"
msgstr "Entferne Falschpositive." msgstr "Entferne falsche Übereinstimmungen"
#: core/scanner.py:154 #: core/scanner.py:154
msgid "Processed %d/%d matches against the ignore list" msgid "Processed %d/%d matches against the ignore list"
msgstr "Verarbeitung von %d/%d Paaren gegen die Ignorier-Liste" msgstr "%d/%d Treffer mit der Ausnahmeliste abgeglichen"
#: core/scanner.py:176 #: core/scanner.py:176
msgid "Doing group prioritization" msgid "Doing group prioritization"
@@ -214,20 +221,20 @@ msgstr "Gruppenpriorisierung"
#: core_pe/matchblock.py:61 #: core_pe/matchblock.py:61
msgid "Analyzed %d/%d pictures" msgid "Analyzed %d/%d pictures"
msgstr "Analysiere %d/%d Bilder" msgstr "Analysiere Bild %d/%d"
#: core_pe/matchblock.py:153 #: core_pe/matchblock.py:153
msgid "Performed %d/%d chunk matches" msgid "Performed %d/%d chunk matches"
msgstr "Performed %d/%d chunk matches" msgstr "%d/%d Chunk-Matches ausgeführt"
#: core_pe/matchblock.py:158 #: core_pe/matchblock.py:158
msgid "Preparing for matching" msgid "Preparing for matching"
msgstr "Vorbereitung auf den Vergleich" msgstr "Bereite Matching vor"
#: core_pe/matchblock.py:193 #: core_pe/matchblock.py:193
msgid "Verified %d/%d matches" msgid "Verified %d/%d matches"
msgstr "%d/%d verifizierte Paare" msgstr "%d/%d verifizierte Übereinstimmungen"
#: core_pe/matchexif.py:18 #: core_pe/matchexif.py:18
msgid "Read EXIF of %d/%d pictures" msgid "Read EXIF of %d/%d pictures"
msgstr "" msgstr "Lese EXIF von Bild %d/%d"

View File

@@ -1,9 +1,11 @@
# Translators: # Translators:
# Harakiri1337, 2014
# Frank Weber <frank.weber@gmail.com>, 2014
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: dupeGuru\n" "Project-Id-Version: dupeGuru\n"
"PO-Revision-Date: 2013-12-07 15:22+0000\n" "PO-Revision-Date: 2014-09-26 21:15+0000\n"
"Last-Translator: hsoft <hsoft@hardcoded.net>\n" "Last-Translator: Frank Weber <frank.weber@gmail.com>\n"
"Language-Team: German (http://www.transifex.com/projects/p/dupeguru/language/de/)\n" "Language-Team: German (http://www.transifex.com/projects/p/dupeguru/language/de/)\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: utf-8\n" "Content-Transfer-Encoding: utf-8\n"
@@ -12,49 +14,50 @@ msgstr ""
#: cocoa/inter/app_me.py:34 #: cocoa/inter/app_me.py:34
msgid "Removing dead tracks from your iTunes Library" msgid "Removing dead tracks from your iTunes Library"
msgstr "Entferne tote Stücke aus Ihrer iTunes Bibliothek." msgstr "Entferne tote Tracks aus Ihrer iTunes Bibliothek"
#: cocoa/inter/app_me.py:35 #: cocoa/inter/app_me.py:35
msgid "Scanning the iTunes Library" msgid "Scanning the iTunes Library"
msgstr "Scanne die iTunes Bibiliothek" msgstr "Durchsuche die iTunes-Bibliothek"
#: cocoa/inter/app_me.py:158 cocoa/inter/app_pe.py:200 #: cocoa/inter/app_me.py:158 cocoa/inter/app_pe.py:200
msgid "Sending dupes to the Trash" msgid "Sending dupes to the Trash"
msgstr "Verschiebe Duplikate in den Mülleimer" msgstr "Schicke Duplikate in den Papierkorb"
#: cocoa/inter/app_me.py:160 #: cocoa/inter/app_me.py:160
msgid "Talking to iTunes. Don't touch it!" msgid "Talking to iTunes. Don't touch it!"
msgstr "" msgstr "Kommuniziere mit iTunes. Bitte warten!"
#: cocoa/inter/app_me.py:195 #: cocoa/inter/app_me.py:195
msgid "" msgid ""
"Your iTunes Library contains %d dead tracks ready to be removed. Continue?" "Your iTunes Library contains %d dead tracks ready to be removed. Continue?"
msgstr "" msgstr ""
"Your iTunes Library contains %d dead tracks ready to be removed. Continue?" "Ihre iTunes-Bibliothek enthält %d tote Tracks zum Entfernen. Fortsetzen?"
#: cocoa/inter/app_me.py:199 #: cocoa/inter/app_me.py:199
msgid "You have no dead tracks in your iTunes Library" msgid "You have no dead tracks in your iTunes Library"
msgstr "You have no dead tracks in your iTunes Library" msgstr "Sie haben keine toten Tracks in Ihrer iTunes-Bibliothek"
#: cocoa/inter/app_me.py:217 #: cocoa/inter/app_me.py:217
msgid "The iTunes application couldn't be found." msgid "The iTunes application couldn't be found."
msgstr "" msgstr "Das iTunes-Programm konnte nicht gefunden werden."
#: cocoa/inter/app_pe.py:202 #: cocoa/inter/app_pe.py:202
msgid "Talking to iPhoto. Don't touch it!" msgid "Talking to iPhoto. Don't touch it!"
msgstr "" msgstr "Kommuniziere mit iPhoto. Bitte warten!"
#: cocoa/inter/app_pe.py:211 #: cocoa/inter/app_pe.py:211
msgid "Talking to Aperture. Don't touch it!" msgid "Talking to Aperture. Don't touch it!"
msgstr "" msgstr "Kommuniziere mit Aperture. Bitte warten!"
#: cocoa/inter/app_pe.py:284 #: cocoa/inter/app_pe.py:284
msgid "Deleted Aperture photos were sent to a project called \"dupeGuru Trash\"." msgid "Deleted Aperture photos were sent to a project called \"dupeGuru Trash\"."
msgstr "" msgstr ""
"Gelöschte Aperture-Fotos wurden dem Projekt \"dupeGuru Trash\" hinzugefügt."
#: cocoa/inter/app_pe.py:310 #: cocoa/inter/app_pe.py:310
msgid "The iPhoto application couldn't be found." msgid "The iPhoto application couldn't be found."
msgstr "The iPhoto application couldn't be found." msgstr "Das iPhoto-Programm konnte nicht gefunden werden."
#: qt/base/app.py:83 #: qt/base/app.py:83
msgid "Quit" msgid "Quit"
@@ -67,7 +70,7 @@ msgstr "Einstellungen"
#: qt/base/app.py:85 qt/base/ignore_list_dialog.py:32 #: qt/base/app.py:85 qt/base/ignore_list_dialog.py:32
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Ignore List" msgid "Ignore List"
msgstr "" msgstr "Ausnahme-Liste"
#: qt/base/app.py:86 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/app.py:86 cocoa/base/en.lproj/Localizable.strings:0
msgid "dupeGuru Help" msgid "dupeGuru Help"
@@ -87,51 +90,56 @@ msgstr "Debug Log öffnen"
#: qt/base/app.py:198 #: qt/base/app.py:198
msgid "{} file (*.{})" msgid "{} file (*.{})"
msgstr "" msgstr "{} Datei (*.{})"
#: qt/base/deletion_options.py:30 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/deletion_options.py:30 cocoa/base/en.lproj/Localizable.strings:0
msgid "Deletion Options" msgid "Deletion Options"
msgstr "" msgstr "Lösch-Optionen"
#: qt/base/deletion_options.py:35 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/deletion_options.py:35 cocoa/base/en.lproj/Localizable.strings:0
msgid "Link deleted files" msgid "Link deleted files"
msgstr "" msgstr "Verlinke gelöschte Dateien"
#: qt/base/deletion_options.py:37 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/deletion_options.py:37 cocoa/base/en.lproj/Localizable.strings:0
msgid "" msgid ""
"After having deleted a duplicate, place a link targeting the reference file " "After having deleted a duplicate, place a link targeting the reference file "
"to replace the deleted file." "to replace the deleted file."
msgstr "" msgstr ""
"Doppelte Dateien werden gelöscht, an deren Stelle wird eine Verknüpfung auf "
"die Referenz-Datei erstellt."
#: qt/base/deletion_options.py:42 #: qt/base/deletion_options.py:42
msgid "Hardlink" msgid "Hardlink"
msgstr "" msgstr "Hardlink"
#: qt/base/deletion_options.py:42 #: qt/base/deletion_options.py:42
msgid "Symlink" msgid "Symlink"
msgstr "" msgstr "Symlink"
#: qt/base/deletion_options.py:46 #: qt/base/deletion_options.py:46
msgid " (unsupported)" msgid " (unsupported)"
msgstr "" msgstr "(nicht unterstützt)"
#: qt/base/deletion_options.py:47 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/deletion_options.py:47 cocoa/base/en.lproj/Localizable.strings:0
msgid "Directly delete files" msgid "Directly delete files"
msgstr "" msgstr "Ohne Papierkorb löschen"
#: qt/base/deletion_options.py:49 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/deletion_options.py:49 cocoa/base/en.lproj/Localizable.strings:0
msgid "" msgid ""
"Instead of sending files to trash, delete them directly. This option is " "Instead of sending files to trash, delete them directly. This option is "
"usually used as a workaround when the normal deletion method doesn't work." "usually used as a workaround when the normal deletion method doesn't work."
msgstr "" msgstr ""
"Anstatt Dateien in den Papierkorb zu verschieben, können Sie diese direkt "
"löschen. Diese Option wird in der Regel genutzt, falls die normale "
"Löschmethode nicht funktioniert."
#: qt/base/deletion_options.py:55 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/deletion_options.py:55 cocoa/base/en.lproj/Localizable.strings:0
msgid "Proceed" msgid "Proceed"
msgstr "" msgstr "Fortfahren"
#: qt/base/deletion_options.py:56 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/deletion_options.py:56 cocoa/base/en.lproj/Localizable.strings:0
msgid "Cancel" msgid "Cancel"
msgstr "Cancel" msgstr "Abbrechen"
#: qt/base/details_table.py:16 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/details_table.py:16 cocoa/base/en.lproj/Localizable.strings:0
msgid "Attribute" msgid "Attribute"
@@ -148,7 +156,7 @@ msgstr "Referenz"
#: qt/base/directories_dialog.py:58 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/directories_dialog.py:58 cocoa/base/en.lproj/Localizable.strings:0
msgid "Load Results..." msgid "Load Results..."
msgstr "Lade Ergebnisse..." msgstr "Ergebnis laden..."
#: qt/base/directories_dialog.py:59 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/directories_dialog.py:59 cocoa/base/en.lproj/Localizable.strings:0
msgid "Results Window" msgid "Results Window"
@@ -161,7 +169,7 @@ msgstr "Ordner hinzufügen..."
#: qt/base/directories_dialog.py:68 qt/base/result_window.py:77 #: qt/base/directories_dialog.py:68 qt/base/result_window.py:77
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "File" msgid "File"
msgstr "Ablage" msgstr "Datei"
#: qt/base/directories_dialog.py:70 qt/base/result_window.py:85 #: qt/base/directories_dialog.py:70 qt/base/result_window.py:85
msgid "View" msgid "View"
@@ -174,11 +182,11 @@ msgstr "Hilfe"
#: qt/base/directories_dialog.py:74 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/directories_dialog.py:74 cocoa/base/en.lproj/Localizable.strings:0
msgid "Load Recent Results" msgid "Load Recent Results"
msgstr "Lade letzte Ergebnisse" msgstr "Lade letztes Suchergebnis"
#: qt/base/directories_dialog.py:108 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/directories_dialog.py:108 cocoa/base/en.lproj/Localizable.strings:0
msgid "Select folders to scan and press \"Scan\"." msgid "Select folders to scan and press \"Scan\"."
msgstr "Zu scannende Ordner auswählen und \"Scan\" drücken." msgstr "Zu durchsuchende Ordner auswählen und \"Suche starten\" drücken."
#: qt/base/directories_dialog.py:132 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/directories_dialog.py:132 cocoa/base/en.lproj/Localizable.strings:0
msgid "Load Results" msgid "Load Results"
@@ -186,7 +194,7 @@ msgstr "Lade Ergebnisse"
#: qt/base/directories_dialog.py:135 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/directories_dialog.py:135 cocoa/base/en.lproj/Localizable.strings:0
msgid "Scan" msgid "Scan"
msgstr "Scan" msgstr "Suche starten"
#: qt/base/directories_dialog.py:179 #: qt/base/directories_dialog.py:179
msgid "Unsaved results" msgid "Unsaved results"
@@ -194,15 +202,16 @@ msgstr "Ungespeicherte Ergebnisse"
#: qt/base/directories_dialog.py:180 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/directories_dialog.py:180 cocoa/base/en.lproj/Localizable.strings:0
msgid "You have unsaved results, do you really want to quit?" msgid "You have unsaved results, do you really want to quit?"
msgstr "Sie haben ungespeicherte Ergebnisse. Wollen Sie wirklich beenden?" msgstr ""
"Sie haben ungespeicherte Ergebnisse. Wollen Sie wirklich dupeGuru beenden?"
#: qt/base/directories_dialog.py:188 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/directories_dialog.py:188 cocoa/base/en.lproj/Localizable.strings:0
msgid "Select a folder to add to the scanning list" msgid "Select a folder to add to the scanning list"
msgstr "Wählen Sie einen Ordner aus, um ihn der Scanliste hinzuzufügen." msgstr "Wählen Sie einen Ordner aus, um ihn der Scanliste hinzuzufügen"
#: qt/base/directories_dialog.py:205 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/directories_dialog.py:205 cocoa/base/en.lproj/Localizable.strings:0
msgid "Select a results file to load" msgid "Select a results file to load"
msgstr "Wählen Sie eine Ergebnisliste zum Laden aus." msgstr "Wählen Sie eine Ergebnisdatei zum Laden aus"
#: qt/base/directories_dialog.py:206 #: qt/base/directories_dialog.py:206
msgid "All Files (*.*)" msgid "All Files (*.*)"
@@ -210,11 +219,11 @@ msgstr "Alle Dateien (*.*)"
#: qt/base/directories_dialog.py:206 qt/base/result_window.py:287 #: qt/base/directories_dialog.py:206 qt/base/result_window.py:287
msgid "dupeGuru Results (*.dupeguru)" msgid "dupeGuru Results (*.dupeguru)"
msgstr "dupeGuru Ergebnisse (*.dupeguru)" msgstr "dupeGuru Suchergebnisse (*.dupeguru)"
#: qt/base/directories_dialog.py:217 #: qt/base/directories_dialog.py:217
msgid "Start a new scan" msgid "Start a new scan"
msgstr "Starte einen neuen Scan" msgstr "Starte einen neuen Suchlauf"
#: qt/base/directories_dialog.py:218 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/directories_dialog.py:218 cocoa/base/en.lproj/Localizable.strings:0
msgid "You have unsaved results, do you really want to continue?" msgid "You have unsaved results, do you really want to continue?"
@@ -238,11 +247,11 @@ msgstr "Normal"
#: qt/base/ignore_list_dialog.py:45 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/ignore_list_dialog.py:45 cocoa/base/en.lproj/Localizable.strings:0
msgid "Remove Selected" msgid "Remove Selected"
msgstr "" msgstr "Auswahl löschen"
#: qt/base/ignore_list_dialog.py:46 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/ignore_list_dialog.py:46 cocoa/base/en.lproj/Localizable.strings:0
msgid "Clear" msgid "Clear"
msgstr "" msgstr "Liste leeren"
#: qt/base/ignore_list_dialog.py:47 qt/base/problem_dialog.py:57 #: qt/base/ignore_list_dialog.py:47 qt/base/problem_dialog.py:57
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
@@ -259,15 +268,15 @@ msgstr "Filter Empfindlichkeit:"
#: qt/base/preferences_dialog.py:76 #: qt/base/preferences_dialog.py:76
msgid "More Results" msgid "More Results"
msgstr "mehr Ergebnisse" msgstr "Mehr Ergebnisse"
#: qt/base/preferences_dialog.py:81 #: qt/base/preferences_dialog.py:81
msgid "Fewer Results" msgid "Fewer Results"
msgstr "weniger Ergebnisse" msgstr "Weniger Ergebnisse"
#: qt/base/preferences_dialog.py:88 #: qt/base/preferences_dialog.py:88
msgid "Font size:" msgid "Font size:"
msgstr "Font size:" msgstr "Schriftgröße:"
#: qt/base/preferences_dialog.py:92 #: qt/base/preferences_dialog.py:92
msgid "Language:" msgid "Language:"
@@ -279,7 +288,7 @@ msgstr "Kopieren und Verschieben:"
#: qt/base/preferences_dialog.py:101 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/preferences_dialog.py:101 cocoa/base/en.lproj/Localizable.strings:0
msgid "Right in destination" msgid "Right in destination"
msgstr "Direkt im Ziel" msgstr "Direkt ins Ziel"
#: qt/base/preferences_dialog.py:102 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/preferences_dialog.py:102 cocoa/base/en.lproj/Localizable.strings:0
msgid "Recreate relative path" msgid "Recreate relative path"
@@ -291,7 +300,7 @@ msgstr "Absoluten Pfad neu erstellen"
#: qt/base/preferences_dialog.py:106 #: qt/base/preferences_dialog.py:106
msgid "Custom Command (arguments: %d for dupe, %r for ref):" msgid "Custom Command (arguments: %d for dupe, %r for ref):"
msgstr "Eigener Befehl (Argumente: %d für Duplikat, %r für Referenz):" msgstr "Eigener Befehl (Variablen: %d für Duplikat, %r für Referenz):"
#: qt/base/preferences_dialog.py:184 #: qt/base/preferences_dialog.py:184
msgid "dupeGuru has to restart for language changes to take effect." msgid "dupeGuru has to restart for language changes to take effect."
@@ -299,7 +308,7 @@ msgstr "dupeGuru muss neustarten, um die Sprachänderung durchzuführen."
#: qt/base/prioritize_dialog.py:71 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/prioritize_dialog.py:71 cocoa/base/en.lproj/Localizable.strings:0
msgid "Re-Prioritize duplicates" msgid "Re-Prioritize duplicates"
msgstr "Re-Prioritize duplicates" msgstr "Re-priorisiere Duplikate"
#: qt/base/prioritize_dialog.py:75 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/prioritize_dialog.py:75 cocoa/base/en.lproj/Localizable.strings:0
msgid "" msgid ""
@@ -307,9 +316,9 @@ msgid ""
" the best to these criteria to their respective group's reference position. " " the best to these criteria to their respective group's reference position. "
"Read the help file for more information." "Read the help file for more information."
msgstr "" msgstr ""
"Add criteria to the right box and click OK to send the dupes that correspond" "Fügen Sie Kriterien zur rechten Box hinzu. Klicken Sie OK, um die Duplikate,"
" the best to these criteria to their respective group's reference position. " " die diesen Kriterien am besten entsprechen, zur Referenzposition der "
"Read the help file for more information." "entsprechenden Gruppe zu senden. Lesen Sie die Hilfe für mehr Informationen."
#: qt/base/problem_dialog.py:31 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/problem_dialog.py:31 cocoa/base/en.lproj/Localizable.strings:0
msgid "Problems!" msgid "Problems!"
@@ -321,8 +330,9 @@ msgid ""
"these problems are described in the table below. Those files were not " "these problems are described in the table below. Those files were not "
"removed from your results." "removed from your results."
msgstr "" msgstr ""
"Es gab Probleme bei der Verarbeitung einiger (aller) Dateien. Der Grund der " "Es gab Probleme bei der Verarbeitung einiger (aller) Dateien. Der Ursache "
"Probleme ist unten in der Tabelle beschrieben." "dieser Probleme ist unten genauer beschrieben. Diese Dateien wurden "
"\"nicht\" aus Ihren Suchergebnissen entfernt."
#: qt/base/problem_dialog.py:52 #: qt/base/problem_dialog.py:52
msgid "Reveal Selected" msgid "Reveal Selected"
@@ -346,11 +356,11 @@ msgstr "Nur Duplikate anzeigen"
#: qt/base/result_window.py:47 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/result_window.py:47 cocoa/base/en.lproj/Localizable.strings:0
msgid "Show Delta Values" msgid "Show Delta Values"
msgstr "Zeige Deltawerte" msgstr "Zeige Delta-Werte"
#: qt/base/result_window.py:48 #: qt/base/result_window.py:48
msgid "Send Marked to Recycle Bin..." msgid "Send Marked to Recycle Bin..."
msgstr "Verschiebe Markierte in den Mülleimer..." msgstr "Verschiebe Markierte in den Papierkorb..."
#: qt/base/result_window.py:49 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/result_window.py:49 cocoa/base/en.lproj/Localizable.strings:0
msgid "Move Marked to..." msgid "Move Marked to..."
@@ -366,31 +376,31 @@ msgstr "Entferne Markierte aus den Ergebnissen"
#: qt/base/result_window.py:52 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/result_window.py:52 cocoa/base/en.lproj/Localizable.strings:0
msgid "Re-Prioritize Results..." msgid "Re-Prioritize Results..."
msgstr "Entferne Ausgewählte aus den Ergebnissen..." msgstr "Re-priorisiere Ergebnisse..."
#: qt/base/result_window.py:53 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/result_window.py:53 cocoa/base/en.lproj/Localizable.strings:0
msgid "Remove Selected from Results" msgid "Remove Selected from Results"
msgstr "Entferne Ausgewählte aus den Ergebnissen" msgstr "Entferne Auswahl aus den Ergebnissen"
#: qt/base/result_window.py:54 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/result_window.py:54 cocoa/base/en.lproj/Localizable.strings:0
msgid "Add Selected to Ignore List" msgid "Add Selected to Ignore List"
msgstr "Füge Ausgewählte der Ignorier-Liste hinzu" msgstr "Füge Auswahl der Ausnahmeliste hinzu"
#: qt/base/result_window.py:55 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/result_window.py:55 cocoa/base/en.lproj/Localizable.strings:0
msgid "Make Selected into Reference" msgid "Make Selected into Reference"
msgstr "" msgstr "Mache Auswahl zur Referenz"
#: qt/base/result_window.py:56 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/result_window.py:56 cocoa/base/en.lproj/Localizable.strings:0
msgid "Open Selected with Default Application" msgid "Open Selected with Default Application"
msgstr "Öffne Ausgewählte mit Standardanwendung" msgstr "Öffne Auswahl mit Standard-Anwendung"
#: qt/base/result_window.py:57 #: qt/base/result_window.py:57
msgid "Open Containing Folder of Selected" msgid "Open Containing Folder of Selected"
msgstr "Öffne beeinhaltenden Ordner der Ausgewählten" msgstr "Öffne den Über-Ordner der Auswahl"
#: qt/base/result_window.py:58 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/result_window.py:58 cocoa/base/en.lproj/Localizable.strings:0
msgid "Rename Selected" msgid "Rename Selected"
msgstr "Ausgewählte umbenennen" msgstr "Auswahl umbenennen"
#: qt/base/result_window.py:59 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/result_window.py:59 cocoa/base/en.lproj/Localizable.strings:0
msgid "Mark All" msgid "Mark All"
@@ -402,19 +412,19 @@ msgstr "Nichts markieren"
#: qt/base/result_window.py:61 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/result_window.py:61 cocoa/base/en.lproj/Localizable.strings:0
msgid "Invert Marking" msgid "Invert Marking"
msgstr "Markierung invertieren" msgstr "Auswahl umkehren"
#: qt/base/result_window.py:62 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/result_window.py:62 cocoa/base/en.lproj/Localizable.strings:0
msgid "Mark Selected" msgid "Mark Selected"
msgstr "Ausgewählte markieren" msgstr "Auswahl markieren"
#: qt/base/result_window.py:63 #: qt/base/result_window.py:63
msgid "Export To HTML" msgid "Export To HTML"
msgstr "Exportiere als HTML" msgstr "Exportiere als HTML..."
#: qt/base/result_window.py:64 #: qt/base/result_window.py:64
msgid "Export To CSV" msgid "Export To CSV"
msgstr "" msgstr "Exportiere als CSV..."
#: qt/base/result_window.py:65 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/result_window.py:65 cocoa/base/en.lproj/Localizable.strings:0
msgid "Save Results..." msgid "Save Results..."
@@ -434,7 +444,7 @@ msgstr "Spalten"
#: qt/base/result_window.py:141 #: qt/base/result_window.py:141
msgid "Reset to Defaults" msgid "Reset to Defaults"
msgstr "Voreinstellungen" msgstr "Auf Voreinstellung zurücksetzen"
#: qt/base/result_window.py:163 #: qt/base/result_window.py:163
msgid "{} Results" msgid "{} Results"
@@ -442,15 +452,15 @@ msgstr "{} (Ergebnisse)"
#: qt/base/result_window.py:171 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/result_window.py:171 cocoa/base/en.lproj/Localizable.strings:0
msgid "Dupes Only" msgid "Dupes Only"
msgstr "Dupes Only" msgstr "Nur Duplikate anzeigen"
#: qt/base/result_window.py:172 #: qt/base/result_window.py:172
msgid "Delta Values" msgid "Delta Values"
msgstr "" msgstr "Zeige Delta-Werte"
#: qt/base/result_window.py:286 cocoa/base/en.lproj/Localizable.strings:0 #: qt/base/result_window.py:286 cocoa/base/en.lproj/Localizable.strings:0
msgid "Select a file to save your results to" msgid "Select a file to save your results to"
msgstr "Datei zum Speichern der Ergebnisliste auswählen." msgstr "Datei zum Speichern der Suchergebnisse auswählen"
#: qt/me/preferences_dialog.py:39 qt/se/preferences_dialog.py:39 #: qt/me/preferences_dialog.py:39 qt/se/preferences_dialog.py:39
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
@@ -472,7 +482,7 @@ msgstr "Tags"
#: qt/me/preferences_dialog.py:43 qt/pe/preferences_dialog.py:33 #: qt/me/preferences_dialog.py:43 qt/pe/preferences_dialog.py:33
#: qt/se/preferences_dialog.py:40 cocoa/base/en.lproj/Localizable.strings:0 #: qt/se/preferences_dialog.py:40 cocoa/base/en.lproj/Localizable.strings:0
msgid "Contents" msgid "Contents"
msgstr "Inhalt" msgstr "Inhalte"
#: qt/me/preferences_dialog.py:44 #: qt/me/preferences_dialog.py:44
msgid "Audio Contents" msgid "Audio Contents"
@@ -480,11 +490,11 @@ msgstr "Audio Inhalte"
#: qt/me/preferences_dialog.py:55 cocoa/base/en.lproj/Localizable.strings:0 #: qt/me/preferences_dialog.py:55 cocoa/base/en.lproj/Localizable.strings:0
msgid "Tags to scan:" msgid "Tags to scan:"
msgstr "folgende Tags scannen:" msgstr "Folgende Tags scannen:"
#: qt/me/preferences_dialog.py:61 cocoa/base/en.lproj/Localizable.strings:0 #: qt/me/preferences_dialog.py:61 cocoa/base/en.lproj/Localizable.strings:0
msgid "Track" msgid "Track"
msgstr "Stück" msgstr "Track"
#: qt/me/preferences_dialog.py:63 cocoa/base/en.lproj/Localizable.strings:0 #: qt/me/preferences_dialog.py:63 cocoa/base/en.lproj/Localizable.strings:0
msgid "Artist" msgid "Artist"
@@ -514,7 +524,7 @@ msgstr "Wortgewichtung"
#: qt/me/preferences_dialog.py:77 qt/se/preferences_dialog.py:51 #: qt/me/preferences_dialog.py:77 qt/se/preferences_dialog.py:51
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Match similar words" msgid "Match similar words"
msgstr "Vergleiche ähnliche Wörter" msgstr "Gleiche ähnliche Wörter ab"
#: qt/me/preferences_dialog.py:79 qt/pe/preferences_dialog.py:41 #: qt/me/preferences_dialog.py:79 qt/pe/preferences_dialog.py:41
#: qt/se/preferences_dialog.py:53 cocoa/base/en.lproj/Localizable.strings:0 #: qt/se/preferences_dialog.py:53 cocoa/base/en.lproj/Localizable.strings:0
@@ -534,7 +544,7 @@ msgstr "Entferne leere Ordner beim Löschen oder Verschieben"
#: qt/me/preferences_dialog.py:85 qt/pe/preferences_dialog.py:47 #: qt/me/preferences_dialog.py:85 qt/pe/preferences_dialog.py:47
#: qt/se/preferences_dialog.py:76 cocoa/base/en.lproj/Localizable.strings:0 #: qt/se/preferences_dialog.py:76 cocoa/base/en.lproj/Localizable.strings:0
msgid "Ignore duplicates hardlinking to the same file" msgid "Ignore duplicates hardlinking to the same file"
msgstr "Ignoriere Duplikate die mit derselben Datei verknüpft sind" msgstr "Ignoriere Duplikate mit Hardlinks auf dieselbe Datei"
#: qt/me/preferences_dialog.py:87 qt/pe/preferences_dialog.py:49 #: qt/me/preferences_dialog.py:87 qt/pe/preferences_dialog.py:49
#: qt/se/preferences_dialog.py:78 cocoa/base/en.lproj/Localizable.strings:0 #: qt/se/preferences_dialog.py:78 cocoa/base/en.lproj/Localizable.strings:0
@@ -543,16 +553,16 @@ msgstr "Debug Modus (Neustart nötig)"
#: qt/pe/preferences_dialog.py:34 cocoa/base/en.lproj/Localizable.strings:0 #: qt/pe/preferences_dialog.py:34 cocoa/base/en.lproj/Localizable.strings:0
msgid "EXIF Timestamp" msgid "EXIF Timestamp"
msgstr "EXIF Timestamp" msgstr "EXIF Zeitstempel"
#: qt/pe/preferences_dialog.py:39 cocoa/base/en.lproj/Localizable.strings:0 #: qt/pe/preferences_dialog.py:39 cocoa/base/en.lproj/Localizable.strings:0
msgid "Match pictures of different dimensions" msgid "Match pictures of different dimensions"
msgstr "Vergleiche Bilder mit unterschiedlicher Auflösung" msgstr "Gleiche Bilder mit unterschiedlicher Auflösung ab"
#: qt/pe/result_window.py:19 qt/pe/result_window.py:25 #: qt/pe/result_window.py:19 qt/pe/result_window.py:25
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Clear Picture Cache" msgid "Clear Picture Cache"
msgstr "Bildzwischenspeicher leeren" msgstr "Bilder-Cache leeren"
#: qt/pe/result_window.py:26 cocoa/base/en.lproj/Localizable.strings:0 #: qt/pe/result_window.py:26 cocoa/base/en.lproj/Localizable.strings:0
msgid "Do you really want to remove all your cached picture analysis?" msgid "Do you really want to remove all your cached picture analysis?"
@@ -561,7 +571,7 @@ msgstr ""
#: qt/pe/result_window.py:29 #: qt/pe/result_window.py:29
msgid "Picture cache cleared." msgid "Picture cache cleared."
msgstr "Bildzwischenspeicher geleert." msgstr "Bilder-Cache geleert."
#: qt/se/preferences_dialog.py:41 cocoa/base/en.lproj/Localizable.strings:0 #: qt/se/preferences_dialog.py:41 cocoa/base/en.lproj/Localizable.strings:0
msgid "Folders" msgid "Folders"
@@ -577,35 +587,35 @@ msgstr "KB"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "%@ Results" msgid "%@ Results"
msgstr "" msgstr "%@ Ergebnisse"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Action" msgid "Action"
msgstr "Action" msgstr "Aktion"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Add Aperture Library" msgid "Add Aperture Library"
msgstr "" msgstr "Füge Aperture-Bibliothek hinzu"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Add iPhoto Library" msgid "Add iPhoto Library"
msgstr "Add iPhoto Library" msgstr "Füge iPhoto-Bibliothek hinzu"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Add iTunes Library" msgid "Add iTunes Library"
msgstr "" msgstr "Füge iTunes-Bibliothek hinzu"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Add New Folder..." msgid "Add New Folder..."
msgstr "Add New Folder..." msgstr "Neuer Ordner..."
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Advanced" msgid "Advanced"
msgstr "Advanced" msgstr "Fortgeschritten"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Audio Content" msgid "Audio Content"
msgstr "Audio Content" msgstr "Audio Inhalt"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Automatically check for updates" msgid "Automatically check for updates"
@@ -613,7 +623,7 @@ msgstr "Automatisch nach Updates suchen"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Basic" msgid "Basic"
msgstr "Basic" msgstr "Einfach"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Bring All to Front" msgid "Bring All to Front"
@@ -621,15 +631,15 @@ msgstr "Alle nach vorne bringen"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Check for update..." msgid "Check for update..."
msgstr "Check for update..." msgstr "Auf Updates prüfen..."
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Close Window" msgid "Close Window"
msgstr "Fenster Schließen" msgstr "Fenster schließen"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Content" msgid "Content"
msgstr "Content" msgstr "Inhalt"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Copy" msgid "Copy"
@@ -637,7 +647,7 @@ msgstr "Kopieren"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Custom command (arguments: %d for dupe, %r for ref):" msgid "Custom command (arguments: %d for dupe, %r for ref):"
msgstr "Custom command (arguments: %d for dupe, %r for ref):" msgstr "Eigener Befehl (Variablen: %d für Duplikat, %r für Referenz):"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Cut" msgid "Cut"
@@ -649,7 +659,7 @@ msgstr "Delta"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Details of Selected File" msgid "Details of Selected File"
msgstr "Details of Selected File" msgstr "Details der ausgewählten Datei"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Details Panel" msgid "Details Panel"
@@ -657,27 +667,27 @@ msgstr "Details Panel"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Directories" msgid "Directories"
msgstr "Directories" msgstr "Verzeichnisse"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "dupeGuru" msgid "dupeGuru"
msgstr "" msgstr "dupeGuru"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "dupeGuru ME Preferences" msgid "dupeGuru ME Preferences"
msgstr "dupeGuru ME Preferences" msgstr "dupeGuru ME Einstellungen"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "dupeGuru PE Preferences" msgid "dupeGuru PE Preferences"
msgstr "dupeGuru PE Preferences" msgstr "dupeGuru PE Einstellungen"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "dupeGuru Preferences" msgid "dupeGuru Preferences"
msgstr "dupeGuru Preferences" msgstr "dupeGuru Einstellungen"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "dupeGuru Results" msgid "dupeGuru Results"
msgstr "dupeGuru Results" msgstr "dupeGuru Ergebnisse"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "dupeGuru Website" msgid "dupeGuru Website"
@@ -689,15 +699,15 @@ msgstr "Bearbeiten"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Export Results to CSV" msgid "Export Results to CSV"
msgstr "" msgstr "Exportiere als CSV..."
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Export Results to XHTML" msgid "Export Results to XHTML"
msgstr "Export Results to XHTML" msgstr "Exportiere als XHTML..."
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Fewer results" msgid "Fewer results"
msgstr "Fewer results" msgstr "Weniger Suchergebnisse"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Filter" msgid "Filter"
@@ -705,19 +715,19 @@ msgstr "Filter"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Filter hardness:" msgid "Filter hardness:"
msgstr "Filter hardness:" msgstr "Filter Empfindlichkeit:"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Filter Results..." msgid "Filter Results..."
msgstr "" msgstr "Filter Suchergebnisse..."
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Folder Selection Window" msgid "Folder Selection Window"
msgstr "Folder Selection Window" msgstr "Ordner-Auswahlfenster"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Font Size:" msgid "Font Size:"
msgstr "" msgstr "Schriftgröße:"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Hide dupeGuru" msgid "Hide dupeGuru"
@@ -733,19 +743,19 @@ msgstr "Ignoriere Dateien kleiner als:"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Load from file..." msgid "Load from file..."
msgstr "Load from file..." msgstr "Lade von Datei..."
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Minimize" msgid "Minimize"
msgstr "Im Dock ablegen" msgstr "Minimieren"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Mode" msgid "Mode"
msgstr "Mode" msgstr "Modus"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "More results" msgid "More results"
msgstr "More results" msgstr "Mehr Suchergebnisse"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Ok" msgid "Ok"
@@ -753,19 +763,19 @@ msgstr "Ok"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Options" msgid "Options"
msgstr "Options" msgstr "Optionen"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Paste" msgid "Paste"
msgstr "Einsetzen" msgstr "Einfügen"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Preferences..." msgid "Preferences..."
msgstr "Preferences..." msgstr "Einstellungen..."
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Quick Look" msgid "Quick Look"
msgstr "" msgstr "Quick Look"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Quit dupeGuru" msgid "Quit dupeGuru"
@@ -773,35 +783,35 @@ msgstr "dupeGuru beenden"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Remove Dead Tracks in iTunes" msgid "Remove Dead Tracks in iTunes"
msgstr "Remove Dead Tracks in iTunes" msgstr "Entferne tote Tracks in iTunes"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Reset to Default" msgid "Reset to Default"
msgstr "Reset to Default" msgstr "Auf Voreinstellung zurücksetzen"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Reset To Defaults" msgid "Reset To Defaults"
msgstr "" msgstr "Auf Voreinstellungen zurücksetzen"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Reveal" msgid "Reveal"
msgstr "" msgstr "Zeige"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Reveal Selected in Finder" msgid "Reveal Selected in Finder"
msgstr "Reveal Selected in Finder" msgstr "Zeige Auswahl im Finder"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Select All" msgid "Select All"
msgstr "" msgstr "Alles markieren"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Send Marked to Trash..." msgid "Send Marked to Trash..."
msgstr "" msgstr "Verschiebe Markierte in den Papierkorb..."
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Services" msgid "Services"
msgstr "" msgstr "Services"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Show All" msgid "Show All"
@@ -809,11 +819,11 @@ msgstr "Alle einblenden"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Start Duplicate Scan" msgid "Start Duplicate Scan"
msgstr "Start Duplicate Scan" msgstr "Starte Duplikat-Scan"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "The name '%@' already exists." msgid "The name '%@' already exists."
msgstr "The name '%@' already exists." msgstr "Der Name '%@' existiert bereits."
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Window" msgid "Window"
@@ -821,4 +831,4 @@ msgstr "Fenster"
#: cocoa/base/en.lproj/Localizable.strings:0 #: cocoa/base/en.lproj/Localizable.strings:0
msgid "Zoom" msgid "Zoom"
msgstr "Zoomen" msgstr "Zoom"

View File

@@ -18,7 +18,7 @@ import glob
from hscommon.plat import ISWINDOWS, ISLINUX from hscommon.plat import ISWINDOWS, ISLINUX
from hscommon.build import (add_to_pythonpath, print_and_do, copy_packages, build_debian_changelog, from hscommon.build import (add_to_pythonpath, print_and_do, copy_packages, build_debian_changelog,
copy_qt_plugins, get_module_version, filereplace, copy, setup_package_argparser, copy_qt_plugins, get_module_version, filereplace, copy, setup_package_argparser,
package_cocoa_app_in_dmg, copy_all) package_cocoa_app_in_dmg, copy_all, find_in_path)
def parse_args(): def parse_args():
parser = ArgumentParser() parser = ArgumentParser()
@@ -38,6 +38,7 @@ def package_windows(edition, dev):
print("Qt packaging only works under Windows.") print("Qt packaging only works under Windows.")
return return
from cx_Freeze import setup, Executable from cx_Freeze import setup, Executable
from PyQt5.QtCore import QLibraryInfo
add_to_pythonpath('.') add_to_pythonpath('.')
app_version = get_module_version('core_{}'.format(edition)) app_version = get_module_version('core_{}'.format(edition))
distdir = 'dist' distdir = 'dist'
@@ -80,13 +81,15 @@ def package_windows(edition, dev):
executables=executables executables=executables
) )
print("Removing useless DLLs") print("Removing useless files")
# Huge useless dll that appeared with Qt5
for fn in glob.glob(op.join(distdir, 'icu*.dll')):
os.remove(fn)
# Debug info that cx_freeze brings in. # Debug info that cx_freeze brings in.
for fn in glob.glob(op.join(distdir, '*', '*.pdb')): for fn in glob.glob(op.join(distdir, '*', '*.pdb')):
os.remove(fn) os.remove(fn)
print("Copying forgotten DLLs")
qtlibpath = QLibraryInfo.location(QLibraryInfo.LibrariesPath)
shutil.copy(op.join(qtlibpath, 'libEGL.dll'), distdir)
shutil.copy(find_in_path('msvcp110.dll'), distdir)
print("Copying the rest")
help_path = op.join('build', 'help') help_path = op.join('build', 'help')
print("Copying {} to dist\\help".format(help_path)) print("Copying {} to dist\\help".format(help_path))
shutil.copytree(help_path, op.join(distdir, 'help')) shutil.copytree(help_path, op.join(distdir, 'help'))
@@ -126,7 +129,7 @@ def package_debian_distribution(edition, distribution):
ed = lambda s: s.format(edition) ed = lambda s: s.format(edition)
destpath = op.join('build', 'dupeguru-{0}-{1}'.format(edition, version)) destpath = op.join('build', 'dupeguru-{0}-{1}'.format(edition, version))
srcpath = op.join(destpath, 'src') srcpath = op.join(destpath, 'src')
packages = ['hscommon', 'core', ed('core_{0}'), 'qtlib', 'qt', 'send2trash', 'jobprogress'] packages = ['hscommon', 'core', ed('core_{0}'), 'qtlib', 'qt', 'send2trash']
if edition == 'me': if edition == 'me':
packages.append('hsaudiotag') packages.append('hsaudiotag')
copy_files_to_package(srcpath, packages, with_so=False) copy_files_to_package(srcpath, packages, with_so=False)
@@ -168,7 +171,7 @@ def package_arch(edition):
print("Packaging for Arch") print("Packaging for Arch")
ed = lambda s: s.format(edition) ed = lambda s: s.format(edition)
srcpath = op.join('build', ed('dupeguru-{}-arch')) srcpath = op.join('build', ed('dupeguru-{}-arch'))
packages = ['hscommon', 'core', ed('core_{0}'), 'qtlib', 'qt', 'send2trash', 'jobprogress'] packages = ['hscommon', 'core', ed('core_{0}'), 'qtlib', 'qt', 'send2trash']
if edition == 'me': if edition == 'me':
packages.append('hsaudiotag') packages.append('hsaudiotag')
copy_files_to_package(srcpath, packages, with_so=True) copy_files_to_package(srcpath, packages, with_so=True)

View File

@@ -2,7 +2,7 @@ Source: {pkgname}
Section: devel Section: devel
Priority: extra Priority: extra
Maintainer: Virgil Dupras <hsoft@hardcoded.net> Maintainer: Virgil Dupras <hsoft@hardcoded.net>
Build-Depends: debhelper (>= 7), python3-dev Build-Depends: debhelper (>= 7), python3-dev, python3-setuptools
Standards-Version: 3.8.1 Standards-Version: 3.8.1
Homepage: http://www.hardcoded.net Homepage: http://www.hardcoded.net

View File

@@ -71,7 +71,6 @@ class ResultWindow(QMainWindow):
def _setupMenu(self): def _setupMenu(self):
self.menubar = QMenuBar() self.menubar = QMenuBar()
self.menubar.setNativeMenuBar(False)
self.menubar.setGeometry(QRect(0, 0, 630, 22)) self.menubar.setGeometry(QRect(0, 0, 630, 22))
self.menuFile = QMenu(self.menubar) self.menuFile = QMenu(self.menubar)
self.menuFile.setTitle(tr("File")) self.menuFile.setTitle(tr("File"))

View File

@@ -41,7 +41,8 @@ class SelectableList(QAbstractListModel):
#--- model --> view #--- model --> view
def refresh(self): def refresh(self):
self._updating = True self._updating = True
self.reset() self.beginResetModel()
self.endResetModel()
self._updating = False self._updating = False
self._restoreSelection() self._restoreSelection()

View File

@@ -1,4 +1,5 @@
-r requirements.txt -r requirements.txt
objp>=1.2.0 objp>=1.3.1
appscript>=1.0.0 appscript>=1.0.0
xibless>=0.4.1 xibless>=0.4.1

View File

@@ -1,4 +1,3 @@
jobprogress>=1.0.4
Send2Trash>=1.3.0 Send2Trash>=1.3.0
sphinx>=1.2.2 sphinx>=1.2.2
polib>=1.0.4 polib>=1.0.4