Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79800bc6ed | ||
|
|
6e7b95b2cf | ||
|
|
bf09c4ce8a | ||
|
|
b4a73771c2 | ||
|
|
2166a0996c | ||
|
|
24643a9b5d | ||
|
|
045051ce06 | ||
|
|
7c3728ca47 | ||
|
|
91be1c7336 | ||
|
|
162378bb0a | ||
|
|
4e3cad5702 | ||
|
|
321f8ab406 | ||
|
|
5b3d5f5d1c | ||
|
|
372a682610 | ||
|
|
44266273bf | ||
|
|
ac32305532 | ||
|
|
87c2fa2573 | ||
|
|
db63b63cfd | ||
|
|
6725b2bf0f | ||
|
|
990e73c383 | ||
|
|
9e9e73aa6b | ||
|
|
8434befe1f | ||
|
|
1114ac5613 | ||
|
|
f5f29d775c | ||
|
|
ebd7f1b4ce | ||
|
|
279b7ad10c | ||
|
|
878205fc49 | ||
|
|
b16df32150 | ||
|
|
04b06f7704 | ||
|
|
c6ea1c62d4 |
2
.gitignore
vendored
@@ -5,6 +5,8 @@
|
||||
*.pyd
|
||||
*.waf*
|
||||
.lock-waf*
|
||||
.idea
|
||||
.tox
|
||||
|
||||
build
|
||||
dist
|
||||
|
||||
11
README.md
@@ -3,7 +3,7 @@
|
||||
[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
|
||||
[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
|
||||
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.
|
||||
|
||||
* 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
|
||||
[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.
|
||||
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
|
||||
$ source env/bin/activate
|
||||
$ wget https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py -O - | python
|
||||
$ easy_install pip
|
||||
$ python get-pip.py
|
||||
|
||||
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
|
||||
[cxfreeze]: http://cx-freeze.sourceforge.net/
|
||||
[advinst]: http://www.advancedinstaller.com
|
||||
|
||||
|
||||
120
build.py
@@ -18,10 +18,12 @@ import compileall
|
||||
from setuptools import setup, Extension
|
||||
|
||||
from hscommon import sphinxgen
|
||||
from hscommon.build import (add_to_pythonpath, print_and_do, copy_packages, filereplace,
|
||||
from hscommon.build import (
|
||||
add_to_pythonpath, print_and_do, copy_packages, filereplace,
|
||||
get_module_version, move_all, copy_all, OSXAppStructure,
|
||||
build_cocoalib_xibless, fix_qt_resource_file, build_cocoa_ext, copy_embeddable_python_dylib,
|
||||
collect_stdlib_dependencies, copy)
|
||||
collect_stdlib_dependencies, copy
|
||||
)
|
||||
from hscommon import loc
|
||||
from hscommon.plat import ISOSX, ISLINUX
|
||||
from hscommon.util import ensure_folder, delete_files_with_pattern
|
||||
@@ -29,24 +31,42 @@ from hscommon.util import ensure_folder, delete_files_with_pattern
|
||||
def parse_args():
|
||||
usage = "usage: %prog [options]"
|
||||
parser = OptionParser(usage=usage)
|
||||
parser.add_option('--clean', action='store_true', dest='clean',
|
||||
help="Clean build folder before building")
|
||||
parser.add_option('--doc', action='store_true', dest='doc',
|
||||
help="Build only the help file")
|
||||
parser.add_option('--loc', action='store_true', dest='loc',
|
||||
help="Build only localization")
|
||||
parser.add_option('--cocoa-ext', action='store_true', dest='cocoa_ext',
|
||||
help="Build only Cocoa extensions")
|
||||
parser.add_option('--cocoa-compile', action='store_true', dest='cocoa_compile',
|
||||
help="Build only Cocoa executable")
|
||||
parser.add_option('--xibless', action='store_true', dest='xibless',
|
||||
help="Build only xibless UIs")
|
||||
parser.add_option('--updatepot', action='store_true', dest='updatepot',
|
||||
help="Generate .pot files from source code.")
|
||||
parser.add_option('--mergepot', action='store_true', dest='mergepot',
|
||||
help="Update all .po files based on .pot files.")
|
||||
parser.add_option('--normpo', action='store_true', dest='normpo',
|
||||
help="Normalize all PO files (do this before commit).")
|
||||
parser.add_option(
|
||||
'--clean', action='store_true', dest='clean',
|
||||
help="Clean build folder before building"
|
||||
)
|
||||
parser.add_option(
|
||||
'--doc', action='store_true', dest='doc',
|
||||
help="Build only the help file"
|
||||
)
|
||||
parser.add_option(
|
||||
'--loc', action='store_true', dest='loc',
|
||||
help="Build only localization"
|
||||
)
|
||||
parser.add_option(
|
||||
'--cocoa-ext', action='store_true', dest='cocoa_ext',
|
||||
help="Build only Cocoa extensions"
|
||||
)
|
||||
parser.add_option(
|
||||
'--cocoa-compile', action='store_true', dest='cocoa_compile',
|
||||
help="Build only Cocoa executable"
|
||||
)
|
||||
parser.add_option(
|
||||
'--xibless', action='store_true', dest='xibless',
|
||||
help="Build only xibless UIs"
|
||||
)
|
||||
parser.add_option(
|
||||
'--updatepot', action='store_true', dest='updatepot',
|
||||
help="Generate .pot files from source code."
|
||||
)
|
||||
parser.add_option(
|
||||
'--mergepot', action='store_true', dest='mergepot',
|
||||
help="Update all .po files based on .pot files."
|
||||
)
|
||||
parser.add_option(
|
||||
'--normpo', action='store_true', dest='normpo',
|
||||
help="Normalize all PO files (do this before commit)."
|
||||
)
|
||||
(options, args) = parser.parse_args()
|
||||
return options
|
||||
|
||||
@@ -75,12 +95,20 @@ def build_xibless(edition, dest='cocoa/autogen'):
|
||||
('preferences_panel.py', 'PreferencesPanel_UI'),
|
||||
]
|
||||
for srcname, dstname in FNPAIRS:
|
||||
xibless.generate(op.join('cocoa', 'base', 'ui', srcname), op.join(dest, dstname),
|
||||
localizationTable='Localizable', args={'edition': edition})
|
||||
xibless.generate(
|
||||
op.join('cocoa', 'base', 'ui', srcname), op.join(dest, dstname),
|
||||
localizationTable='Localizable', args={'edition': edition}
|
||||
)
|
||||
if edition == 'pe':
|
||||
xibless.generate('cocoa/pe/ui/details_panel.py', op.join(dest, 'DetailsPanel_UI'), localizationTable='Localizable')
|
||||
xibless.generate(
|
||||
'cocoa/pe/ui/details_panel.py', op.join(dest, 'DetailsPanel_UI'),
|
||||
localizationTable='Localizable'
|
||||
)
|
||||
else:
|
||||
xibless.generate('cocoa/base/ui/details_panel.py', op.join(dest, 'DetailsPanel_UI'), localizationTable='Localizable')
|
||||
xibless.generate(
|
||||
'cocoa/base/ui/details_panel.py', op.join(dest, 'DetailsPanel_UI'),
|
||||
localizationTable='Localizable'
|
||||
)
|
||||
|
||||
def build_cocoa(edition, dev):
|
||||
print("Creating OS X app structure")
|
||||
@@ -110,8 +138,9 @@ def build_cocoa(edition, dev):
|
||||
'me': ['core_me'] + appscript_pkgs + ['hsaudiotag'],
|
||||
'pe': ['core_pe'] + appscript_pkgs,
|
||||
}[edition]
|
||||
tocopy = ['core', 'hscommon', 'cocoa/inter', 'cocoalib/cocoa', 'jobprogress', 'objp',
|
||||
'send2trash'] + specific_packages
|
||||
tocopy = [
|
||||
'core', 'hscommon', 'cocoa/inter', 'cocoalib/cocoa', 'objp', 'send2trash'
|
||||
] + specific_packages
|
||||
copy_packages(tocopy, pydep_folder, create_links=dev)
|
||||
sys.path.insert(0, 'build')
|
||||
extra_deps = None
|
||||
@@ -224,8 +253,10 @@ def build_updatepot():
|
||||
os.remove(cocoalib_pot)
|
||||
loc.strings2pot(op.join('cocoalib', 'en.lproj', 'cocoalib.strings'), cocoalib_pot)
|
||||
print("Enhancing ui.pot with Cocoa's strings files")
|
||||
loc.strings2pot(op.join('cocoa', 'base', 'en.lproj', 'Localizable.strings'),
|
||||
op.join('locale', 'ui.pot'))
|
||||
loc.strings2pot(
|
||||
op.join('cocoa', 'base', 'en.lproj', 'Localizable.strings'),
|
||||
op.join('locale', 'ui.pot')
|
||||
)
|
||||
|
||||
def build_mergepot():
|
||||
print("Updating .po files using .pot files")
|
||||
@@ -242,11 +273,15 @@ def build_cocoa_proxy_module():
|
||||
print("Building Cocoa Proxy")
|
||||
import objp.p2o
|
||||
objp.p2o.generate_python_proxy_code('cocoalib/cocoa/CocoaProxy.h', 'build/CocoaProxy.m')
|
||||
build_cocoa_ext("CocoaProxy", 'cocoalib/cocoa',
|
||||
['cocoalib/cocoa/CocoaProxy.m', 'build/CocoaProxy.m', 'build/ObjP.m',
|
||||
'cocoalib/HSErrorReportWindow.m', 'cocoa/autogen/HSErrorReportWindow_UI.m'],
|
||||
build_cocoa_ext(
|
||||
"CocoaProxy", 'cocoalib/cocoa',
|
||||
[
|
||||
'cocoalib/cocoa/CocoaProxy.m', 'build/CocoaProxy.m', 'build/ObjP.m',
|
||||
'cocoalib/HSErrorReportWindow.m', 'cocoa/autogen/HSErrorReportWindow_UI.m'
|
||||
],
|
||||
['AppKit', 'CoreServices'],
|
||||
['cocoalib', 'cocoa/autogen'])
|
||||
['cocoalib', 'cocoa/autogen']
|
||||
)
|
||||
|
||||
def build_cocoa_bridging_interfaces(edition):
|
||||
print("Building Cocoa Bridging Interfaces")
|
||||
@@ -254,9 +289,11 @@ def build_cocoa_bridging_interfaces(edition):
|
||||
import objp.p2o
|
||||
add_to_pythonpath('cocoa')
|
||||
add_to_pythonpath('cocoalib')
|
||||
from cocoa.inter import (PyGUIObject, GUIObjectView, PyColumns, ColumnsView, PyOutline,
|
||||
from cocoa.inter import (
|
||||
PyGUIObject, GUIObjectView, PyColumns, ColumnsView, PyOutline,
|
||||
OutlineView, PySelectableList, SelectableListView, PyTable, TableView, PyBaseApp,
|
||||
PyTextField, ProgressWindowView, PyProgressWindow)
|
||||
PyTextField, ProgressWindowView, PyProgressWindow
|
||||
)
|
||||
from inter.deletion_options import PyDeletionOptions, DeletionOptionsView
|
||||
from inter.details_panel import PyDetailsPanel, DetailsPanelView
|
||||
from inter.directory_outline import PyDirectoryOutline, DirectoryOutlineView
|
||||
@@ -268,16 +305,20 @@ def build_cocoa_bridging_interfaces(edition):
|
||||
from inter.stats_label import PyStatsLabel, StatsLabelView
|
||||
from inter.app import PyDupeGuruBase, DupeGuruView
|
||||
appmod = importlib.import_module('inter.app_{}'.format(edition))
|
||||
allclasses = [PyGUIObject, PyColumns, PyOutline, PySelectableList, PyTable, PyBaseApp,
|
||||
allclasses = [
|
||||
PyGUIObject, PyColumns, PyOutline, PySelectableList, PyTable, PyBaseApp,
|
||||
PyDetailsPanel, PyDirectoryOutline, PyPrioritizeDialog, PyPrioritizeList, PyProblemDialog,
|
||||
PyIgnoreListDialog, PyDeletionOptions, PyResultTable, PyStatsLabel, PyDupeGuruBase,
|
||||
PyTextField, PyProgressWindow, appmod.PyDupeGuru]
|
||||
PyTextField, PyProgressWindow, appmod.PyDupeGuru
|
||||
]
|
||||
for class_ in allclasses:
|
||||
objp.o2p.generate_objc_code(class_, 'cocoa/autogen', inherit=True)
|
||||
allclasses = [GUIObjectView, ColumnsView, OutlineView, SelectableListView, TableView,
|
||||
allclasses = [
|
||||
GUIObjectView, ColumnsView, OutlineView, SelectableListView, TableView,
|
||||
DetailsPanelView, DirectoryOutlineView, PrioritizeDialogView, PrioritizeListView,
|
||||
IgnoreListDialogView, DeletionOptionsView, ResultTableView, StatsLabelView,
|
||||
ProgressWindowView, DupeGuruView]
|
||||
ProgressWindowView, DupeGuruView
|
||||
]
|
||||
clsspecs = [objp.o2p.spec_from_python_class(class_) for class_ in allclasses]
|
||||
objp.p2o.generate_python_proxy_code_from_clsspec(clsspecs, 'build/CocoaViews.m')
|
||||
build_cocoa_ext('CocoaViews', 'cocoa/inter', ['build/CocoaViews.m', 'build/ObjP.m'])
|
||||
@@ -296,7 +337,8 @@ def build_pe_modules(ui):
|
||||
extra_link_args=[
|
||||
"-framework", "CoreFoundation",
|
||||
"-framework", "Foundation",
|
||||
"-framework", "ApplicationServices",]
|
||||
"-framework", "ApplicationServices",
|
||||
]
|
||||
))
|
||||
setup(
|
||||
script_args=['build_ext', '--inplace'],
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>© Hardcoded Software, 2013</string>
|
||||
<string>© Hardcoded Software, 2014</string>
|
||||
<key>SUFeedURL</key>
|
||||
<string>http://www.hardcoded.net/updates/dupeguru_me.appcast</string>
|
||||
<key>SUPublicDSAKeyFile</key>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>© Hardcoded Software, 2013</string>
|
||||
<string>© Hardcoded Software, 2014</string>
|
||||
<key>SUFeedURL</key>
|
||||
<string>http://www.hardcoded.net/updates/dupeguru_pe.appcast</string>
|
||||
<key>SUPublicDSAKeyFile</key>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>© Hardcoded Software, 2013</string>
|
||||
<string>© Hardcoded Software, 2014</string>
|
||||
<key>SUFeedURL</key>
|
||||
<string>http://www.hardcoded.net/updates/dupeguru.appcast</string>
|
||||
<key>SUPublicDSAKeyFile</key>
|
||||
|
||||
@@ -113,5 +113,6 @@ def patch_threaded_job_performer():
|
||||
# _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
|
||||
# structures) inside a threaded job.
|
||||
from jobprogress.performer import ThreadedJobPerformer
|
||||
from hscommon.jobprogress.performer import ThreadedJobPerformer
|
||||
ThreadedJobPerformer._async_run = autoreleasepool(ThreadedJobPerformer._async_run)
|
||||
|
||||
|
||||
20
configure.py
@@ -6,7 +6,6 @@
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
import sys
|
||||
from optparse import OptionParser
|
||||
import json
|
||||
|
||||
@@ -29,11 +28,18 @@ def main(options):
|
||||
if __name__ == '__main__':
|
||||
usage = "usage: %prog [options]"
|
||||
parser = OptionParser(usage=usage)
|
||||
parser.add_option('--edition', dest='edition',
|
||||
help="dupeGuru edition to build (se, me or pe). Default is se.")
|
||||
parser.add_option('--ui', dest='ui',
|
||||
help="Type of UI to build. 'qt' or 'cocoa'. Default is determined by your system.")
|
||||
parser.add_option('--dev', action='store_true', dest='dev', default=False,
|
||||
help="If this flag is set, will configure for dev builds.")
|
||||
parser.add_option(
|
||||
'--edition', dest='edition',
|
||||
help="dupeGuru edition to build (se, me or pe). Default is se."
|
||||
)
|
||||
parser.add_option(
|
||||
'--ui', dest='ui',
|
||||
help="Type of UI to build. 'qt' or 'cocoa'. Default is determined by your system."
|
||||
)
|
||||
parser.add_option(
|
||||
'--dev', action='store_true', dest='dev', default=False,
|
||||
help="If this flag is set, will configure for dev builds."
|
||||
)
|
||||
(options, args) = parser.parse_args()
|
||||
main(options)
|
||||
|
||||
|
||||
47
core/app.py
@@ -15,7 +15,7 @@ import time
|
||||
import shutil
|
||||
|
||||
from send2trash import send2trash
|
||||
from jobprogress import job
|
||||
from hscommon.jobprogress import job
|
||||
from hscommon.notify import Broadcaster
|
||||
from hscommon.path import Path
|
||||
from hscommon.conflict import smart_move, smart_copy
|
||||
@@ -38,8 +38,10 @@ DEBUG_MODE_PREFERENCE = 'DebugMode'
|
||||
|
||||
MSG_NO_MARKED_DUPES = tr("There are no marked duplicates. Nothing has been done.")
|
||||
MSG_NO_SELECTED_DUPES = tr("There are no selected duplicates. Nothing has been done.")
|
||||
MSG_MANY_FILES_TO_OPEN = tr("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?")
|
||||
MSG_MANY_FILES_TO_OPEN = tr(
|
||||
"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?"
|
||||
)
|
||||
|
||||
class DestType:
|
||||
Direct = 0
|
||||
@@ -152,8 +154,8 @@ class DupeGuru(Broadcaster):
|
||||
# select_dest_folder(prompt: 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"
|
||||
SCANNER_CLASS = scanner.Scanner
|
||||
|
||||
def __init__(self, view):
|
||||
if view.get_default(DEBUG_MODE_PREFERENCE):
|
||||
@@ -166,7 +168,7 @@ class DupeGuru(Broadcaster):
|
||||
os.makedirs(self.appdata)
|
||||
self.directories = directories.Directories()
|
||||
self.results = results.Results(self)
|
||||
self.scanner = scanner.Scanner()
|
||||
self.scanner = self.SCANNER_CLASS()
|
||||
self.options = {
|
||||
'escape_filter_regexp': True,
|
||||
'clean_empty_dirs': False,
|
||||
@@ -265,8 +267,10 @@ class DupeGuru(Broadcaster):
|
||||
return None
|
||||
|
||||
def _get_export_data(self):
|
||||
columns = [col for col in self.result_table.columns.ordered_columns
|
||||
if col.visible and col.name != 'marked']
|
||||
columns = [
|
||||
col for col in self.result_table.columns.ordered_columns
|
||||
if col.visible and col.name != 'marked'
|
||||
]
|
||||
colnames = [col.display for col in columns]
|
||||
rows = []
|
||||
for group_id, group in enumerate(self.results.groups):
|
||||
@@ -278,8 +282,10 @@ class DupeGuru(Broadcaster):
|
||||
return colnames, rows
|
||||
|
||||
def _results_changed(self):
|
||||
self.selected_dupes = [d for d in self.selected_dupes
|
||||
if self.results.get_group_of_duplicate(d) is not None]
|
||||
self.selected_dupes = [
|
||||
d for d in self.selected_dupes
|
||||
if self.results.get_group_of_duplicate(d) is not None
|
||||
]
|
||||
self.notify('results_changed')
|
||||
|
||||
def _start_job(self, jobid, func, args=()):
|
||||
@@ -287,7 +293,10 @@ class DupeGuru(Broadcaster):
|
||||
try:
|
||||
self.progress_window.run(jobid, title, func, args=args)
|
||||
except job.JobInProgressError:
|
||||
msg = tr("A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again.")
|
||||
msg = tr(
|
||||
"A previous action is still hanging in there. You can't start a new one yet. Wait "
|
||||
"a few seconds, then try again."
|
||||
)
|
||||
self.view.show_message(msg)
|
||||
|
||||
def _job_completed(self, jobid):
|
||||
@@ -439,8 +448,10 @@ class DupeGuru(Broadcaster):
|
||||
return
|
||||
if not self.deletion_options.show(self.results.mark_count):
|
||||
return
|
||||
args = [self.deletion_options.link_deleted, self.deletion_options.use_hardlinks,
|
||||
self.deletion_options.direct]
|
||||
args = [
|
||||
self.deletion_options.link_deleted, self.deletion_options.use_hardlinks,
|
||||
self.deletion_options.direct
|
||||
]
|
||||
logging.debug("Starting deletion job with args %r", args)
|
||||
self._start_job(JobType.Delete, self._do_delete, args=args)
|
||||
|
||||
@@ -464,7 +475,10 @@ class DupeGuru(Broadcaster):
|
||||
dest_file = self.view.select_dest_file(tr("Select a destination for your exported CSV"), 'csv')
|
||||
if dest_file:
|
||||
colnames, rows = self._get_export_data()
|
||||
try:
|
||||
export.export_to_csv(dest_file, colnames, rows)
|
||||
except OSError as e:
|
||||
self.view.show_message(tr("Couldn't write to file: {}").format(str(e)))
|
||||
|
||||
def get_display_info(self, dupe, group, delta=False):
|
||||
def empty_data():
|
||||
@@ -550,8 +564,10 @@ class DupeGuru(Broadcaster):
|
||||
# If no group was changed, however, we don't touch the selection.
|
||||
if not self.result_table.power_marker:
|
||||
if changed_groups:
|
||||
self.selected_dupes = [d for d in self.selected_dupes
|
||||
if self.results.get_group_of_duplicate(d).ref is d]
|
||||
self.selected_dupes = [
|
||||
d for d in self.selected_dupes
|
||||
if self.results.get_group_of_duplicate(d).ref is d
|
||||
]
|
||||
self.notify('results_changed')
|
||||
else:
|
||||
# If we're in "Dupes Only" mode (previously called Power Marker), things are a bit
|
||||
@@ -705,7 +721,10 @@ class DupeGuru(Broadcaster):
|
||||
|
||||
:param str filename: path of the file to save results (as XML) to.
|
||||
"""
|
||||
try:
|
||||
self.results.save_to_xml(filename)
|
||||
except OSError as e:
|
||||
self.view.show_message(tr("Couldn't write to file: {}").format(str(e)))
|
||||
|
||||
def start_scanning(self):
|
||||
"""Starts an async job to scan for duplicates.
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
from xml.etree import ElementTree as ET
|
||||
import logging
|
||||
|
||||
from jobprogress import job
|
||||
from hscommon.jobprogress import job
|
||||
from hscommon.path import Path
|
||||
from hscommon.util import FileOrPath
|
||||
|
||||
@@ -95,7 +95,8 @@ class Directories:
|
||||
file.is_ref = state == DirectoryState.Reference
|
||||
filepaths.add(file.path)
|
||||
yield file
|
||||
# it's possible that a folder (bundle) gets into the file list. in that case, we don't want to recurse into it
|
||||
# it's possible that a folder (bundle) gets into the file list. in that case, we don't
|
||||
# want to recurse into it
|
||||
subfolders = [p for p in from_path.listdir() if not p.islink() and p.isdir() and p not in filepaths]
|
||||
for subfolder in subfolders:
|
||||
for file in self._get_files(subfolder, j):
|
||||
|
||||
@@ -15,11 +15,13 @@ from unicodedata import normalize
|
||||
|
||||
from hscommon.util import flatten, multi_replace
|
||||
from hscommon.trans import tr
|
||||
from jobprogress import job
|
||||
from hscommon.jobprogress import job
|
||||
|
||||
(WEIGHT_WORDS,
|
||||
(
|
||||
WEIGHT_WORDS,
|
||||
MATCH_SIMILAR_WORDS,
|
||||
NO_FIELD_ORDER) = range(3)
|
||||
NO_FIELD_ORDER,
|
||||
) = range(3)
|
||||
|
||||
JOB_REFRESH_RATE = 100
|
||||
|
||||
@@ -259,6 +261,7 @@ def getmatches_by_contents(files, sizeattr='size', partial=False, j=job.nulljob)
|
||||
filesize = getattr(file, sizeattr)
|
||||
if filesize:
|
||||
size2files[filesize].add(file)
|
||||
del files
|
||||
possible_matches = [files for files in size2files.values() if len(files) > 1]
|
||||
del size2files
|
||||
result = []
|
||||
@@ -495,7 +498,10 @@ def get_groups(matches, j=job.nulljob):
|
||||
matched_files = set(flatten(groups))
|
||||
orphan_matches = []
|
||||
for group in groups:
|
||||
orphan_matches += set(m for m in group.discard_matches() if not any(obj in matched_files for obj in [m.first, m.second]))
|
||||
orphan_matches += {
|
||||
m for m in group.discard_matches()
|
||||
if not any(obj in matched_files for obj in [m.first, m.second])
|
||||
}
|
||||
if groups and orphan_matches:
|
||||
groups += get_groups(orphan_matches) # no job, as it isn't supposed to take a long time
|
||||
return groups
|
||||
|
||||
@@ -32,6 +32,7 @@ NOT_SET = object()
|
||||
|
||||
class FSError(Exception):
|
||||
cls_message = "An error has occured on '{name}' in '{parent}'"
|
||||
|
||||
def __init__(self, fsobject, parent=None):
|
||||
message = self.cls_message
|
||||
if isinstance(fsobject, str):
|
||||
|
||||
@@ -13,3 +13,4 @@ blue, which is supposed to be orange, does the sorting logic, holds selection, e
|
||||
|
||||
.. _cross-toolkit: http://www.hardcoded.net/articles/cross-toolkit-software
|
||||
"""
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
from hscommon.notify import Listener
|
||||
from hscommon.gui.base import NoopGUI
|
||||
|
||||
class DupeGuruGUIObject(Listener):
|
||||
def __init__(self, app):
|
||||
|
||||
@@ -71,6 +71,7 @@ class PrioritizeDialog(GUIObject):
|
||||
return
|
||||
crit = self.criteria[self.criteria_list.selected_index]
|
||||
self.prioritizations.append(crit)
|
||||
del crit
|
||||
self.prioritization_list[:] = [crit.display for crit in self.prioritizations]
|
||||
|
||||
def remove_selected(self):
|
||||
|
||||
@@ -12,7 +12,7 @@ import os
|
||||
import os.path as op
|
||||
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.util import flatten, nonone, FileOrPath, format_size
|
||||
from hscommon.trans import tr
|
||||
@@ -96,7 +96,10 @@ class Results(Markable):
|
||||
self.__dupes = flatten(group.dupes for group in self.groups)
|
||||
if None in self.__dupes:
|
||||
# This is debug logging to try to figure out #44
|
||||
logging.warning("There is a None value in the Results' dupe list. dupes: %r groups: %r", self.__dupes, self.groups)
|
||||
logging.warning(
|
||||
"There is a None value in the Results' dupe list. dupes: %r groups: %r",
|
||||
self.__dupes, self.groups
|
||||
)
|
||||
if self.__filtered_dupes:
|
||||
self.__dupes = [dupe for dupe in self.__dupes if dupe in self.__filtered_dupes]
|
||||
sd = self.__dupes_sort_descriptor
|
||||
@@ -249,7 +252,8 @@ class Results(Markable):
|
||||
second_file = dupes[int(attrs['second'])]
|
||||
percentage = int(attrs['percentage'])
|
||||
group.add_match(engine.Match(first_file, second_file, percentage))
|
||||
except (IndexError, KeyError, ValueError): # Covers missing attr, non-int values and indexes out of bounds
|
||||
except (IndexError, KeyError, ValueError):
|
||||
# Covers missing attr, non-int values and indexes out of bounds
|
||||
pass
|
||||
if (not group.matches) and (len(dupes) >= 2):
|
||||
do_match(dupes[0], dupes[1:], group)
|
||||
@@ -411,3 +415,4 @@ class Results(Markable):
|
||||
dupes = property(__get_dupe_list)
|
||||
groups = property(__get_groups, __set_groups)
|
||||
stat_line = property(__get_stat_line)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import logging
|
||||
import re
|
||||
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.trans import tr
|
||||
|
||||
@@ -81,7 +81,9 @@ class Scanner:
|
||||
files = [f for f in files if f.size >= self.size_threshold]
|
||||
if self.scan_type in {ScanType.Contents, ScanType.ContentsAudio, ScanType.Folders}:
|
||||
sizeattr = 'audiosize' if self.scan_type == ScanType.ContentsAudio else 'size'
|
||||
return engine.getmatches_by_contents(files, sizeattr, partial=self.scan_type==ScanType.ContentsAudio, j=j)
|
||||
return engine.getmatches_by_contents(
|
||||
files, sizeattr, partial=self.scan_type == ScanType.ContentsAudio, j=j
|
||||
)
|
||||
else:
|
||||
j = j.start_subjob([2, 8])
|
||||
kw = {}
|
||||
@@ -94,7 +96,11 @@ class Scanner:
|
||||
func = {
|
||||
ScanType.Filename: lambda f: engine.getwords(rem_file_ext(f.name)),
|
||||
ScanType.Fields: lambda f: engine.getfields(rem_file_ext(f.name)),
|
||||
ScanType.Tag: lambda f: [engine.getwords(str(getattr(f, attrname))) for attrname in SCANNABLE_TAGS if attrname in self.scanned_tags],
|
||||
ScanType.Tag: lambda f: [
|
||||
engine.getwords(str(getattr(f, attrname)))
|
||||
for attrname in SCANNABLE_TAGS
|
||||
if attrname in self.scanned_tags
|
||||
],
|
||||
}[self.scan_type]
|
||||
for f in j.iter_with_progress(files, tr("Read metadata of %d/%d files")):
|
||||
logging.debug("Reading metadata of {}".format(str(f.path)))
|
||||
@@ -152,8 +158,10 @@ class Scanner:
|
||||
if self.ignore_list:
|
||||
j = j.start_subjob(2)
|
||||
iter_matches = j.iter_with_progress(matches, tr("Processed %d/%d matches against the ignore list"))
|
||||
matches = [m for m in iter_matches
|
||||
if not self.ignore_list.AreIgnored(str(m.first.path), str(m.second.path))]
|
||||
matches = [
|
||||
m for m in iter_matches
|
||||
if not self.ignore_list.AreIgnored(str(m.first.path), str(m.second.path))
|
||||
]
|
||||
logging.info('Grouping matches')
|
||||
groups = engine.get_groups(matches, j)
|
||||
matched_files = dedupe([m.first for m in matches] + [m.second for m in matches])
|
||||
@@ -185,3 +193,4 @@ class Scanner:
|
||||
scanned_tags = {'artist', 'title'}
|
||||
size_threshold = 0
|
||||
word_weighting = False
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from hscommon.path import Path
|
||||
import hscommon.conflict
|
||||
import hscommon.util
|
||||
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 .results_test import GetTestGroups
|
||||
|
||||
@@ -10,7 +10,7 @@ from hscommon.testutil import TestApp as TestAppBase, eq_, with_app
|
||||
from hscommon.path import Path
|
||||
from hscommon.util import get_file_ext, format_size
|
||||
from hscommon.gui.column import Column
|
||||
from jobprogress.job import nulljob, JobCancelled
|
||||
from hscommon.jobprogress.job import nulljob, JobCancelled
|
||||
|
||||
from .. import engine
|
||||
from .. import prioritize
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import sys
|
||||
|
||||
from jobprogress import job
|
||||
from hscommon.jobprogress import job
|
||||
from hscommon.util import first
|
||||
from hscommon.testutil import eq_, log_calls
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
from jobprogress import job
|
||||
from hscommon.jobprogress import job
|
||||
from hscommon.path import Path
|
||||
from hscommon.testutil import eq_
|
||||
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
__version__ = '6.7.0'
|
||||
__version__ = '6.8.0'
|
||||
__appname__ = 'dupeGuru Music Edition'
|
||||
|
||||
|
||||
@@ -13,8 +13,10 @@ from .result_table import ResultTable
|
||||
|
||||
class DupeGuru(DupeGuruBase):
|
||||
NAME = __appname__
|
||||
METADATA_TO_READ = ['size', 'mtime', 'duration', 'bitrate', 'samplerate', 'title', 'artist',
|
||||
'album', 'genre', 'year', 'track', 'comment']
|
||||
METADATA_TO_READ = [
|
||||
'size', 'mtime', 'duration', 'bitrate', 'samplerate', 'title', 'artist',
|
||||
'album', 'genre', 'year', 'track', 'comment'
|
||||
]
|
||||
|
||||
def __init__(self, view):
|
||||
DupeGuruBase.__init__(self, view)
|
||||
|
||||
@@ -12,8 +12,10 @@ from hscommon.util import get_file_ext, format_size, format_time
|
||||
from core.app import format_timestamp, format_perc, format_words, format_dupe_count
|
||||
from core import fs
|
||||
|
||||
TAG_FIELDS = {'audiosize', 'duration', 'bitrate', 'samplerate', 'title', 'artist',
|
||||
'album', 'genre', 'year', 'track', 'comment'}
|
||||
TAG_FIELDS = {
|
||||
'audiosize', 'duration', 'bitrate', 'samplerate', 'title', 'artist',
|
||||
'album', 'genre', 'year', 'track', 'comment'
|
||||
}
|
||||
|
||||
class MusicFile(fs.File):
|
||||
INITIAL_INFO = fs.File.INITIAL_INFO.copy()
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
|
||||
from hscommon.trans import trget
|
||||
|
||||
from core.prioritize import (KindCategory, FolderCategory, FilenameCategory, NumericalCategory,
|
||||
SizeCategory, MtimeCategory)
|
||||
from core.prioritize import (
|
||||
KindCategory, FolderCategory, FilenameCategory, NumericalCategory,
|
||||
SizeCategory, MtimeCategory
|
||||
)
|
||||
|
||||
coltr = trget('columns')
|
||||
|
||||
@@ -31,5 +33,8 @@ class SamplerateCategory(NumericalCategory):
|
||||
return dupe.samplerate
|
||||
|
||||
def all_categories():
|
||||
return [KindCategory, FolderCategory, FilenameCategory, SizeCategory, DurationCategory,
|
||||
BitrateCategory, SamplerateCategory, MtimeCategory]
|
||||
return [
|
||||
KindCategory, FolderCategory, FilenameCategory, SizeCategory, DurationCategory,
|
||||
BitrateCategory, SamplerateCategory, MtimeCategory
|
||||
]
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
__version__ = '2.9.0'
|
||||
__version__ = '2.10.1'
|
||||
__appname__ = 'dupeGuru Picture Edition'
|
||||
@@ -17,10 +17,10 @@ from .result_table import ResultTable
|
||||
class DupeGuru(DupeGuruBase):
|
||||
NAME = __appname__
|
||||
METADATA_TO_READ = ['size', 'mtime', 'dimensions', 'exif_timestamp']
|
||||
SCANNER_CLASS = ScannerPE
|
||||
|
||||
def __init__(self, view):
|
||||
DupeGuruBase.__init__(self, view)
|
||||
self.scanner = ScannerPE()
|
||||
self.scanner.cache_path = op.join(self.appdata, 'cached_pictures.db')
|
||||
|
||||
def _get_dupe_sort_key(self, dupe, get_group, key, delta):
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
from ._block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2
|
||||
from ._block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2 # NOQA
|
||||
|
||||
# Converted to C
|
||||
# def getblock(image):
|
||||
|
||||
@@ -93,9 +93,9 @@ class Cache:
|
||||
def _create_con(self, second_try=False):
|
||||
def create_tables():
|
||||
logging.debug("Creating picture cache tables.")
|
||||
self.con.execute("drop table if exists pictures");
|
||||
self.con.execute("drop index if exists idx_path");
|
||||
self.con.execute("create table pictures(path TEXT, mtime INTEGER, blocks TEXT)");
|
||||
self.con.execute("drop table if exists pictures")
|
||||
self.con.execute("drop index if exists idx_path")
|
||||
self.con.execute("create table pictures(path TEXT, mtime INTEGER, blocks TEXT)")
|
||||
self.con.execute("create index idx_path on pictures (path)")
|
||||
|
||||
self.con = sqlite.connect(self.dbname, isolation_level=None)
|
||||
|
||||
@@ -8,24 +8,24 @@
|
||||
|
||||
import plistlib
|
||||
|
||||
class IPhotoPlistParser(plistlib.PlistParser):
|
||||
class IPhotoPlistParser(plistlib._PlistParser):
|
||||
"""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
|
||||
lenient.
|
||||
"""
|
||||
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
|
||||
# log it in case of an exception
|
||||
self.lastdata = ''
|
||||
|
||||
def getData(self):
|
||||
self.lastdata = plistlib.PlistParser.getData(self)
|
||||
def get_data(self):
|
||||
self.lastdata = plistlib._PlistParser.get_data(self)
|
||||
return self.lastdata
|
||||
|
||||
def end_integer(self):
|
||||
try:
|
||||
self.addObject(int(self.getData()))
|
||||
self.add_object(int(self.get_data()))
|
||||
except ValueError:
|
||||
self.addObject(0)
|
||||
self.add_object(0)
|
||||
|
||||
@@ -10,9 +10,9 @@ import logging
|
||||
import multiprocessing
|
||||
from itertools import combinations
|
||||
|
||||
from hscommon.util import extract
|
||||
from hscommon.util import extract, iterconsume
|
||||
from hscommon.trans import tr
|
||||
from jobprogress import job
|
||||
from hscommon.jobprogress import job
|
||||
|
||||
from core.engine import Match
|
||||
from .block import avgdiff, DifferentBlockCountError, NoBlocksError
|
||||
@@ -93,8 +93,10 @@ def get_chunks(pictures):
|
||||
chunk_count = max(min_chunk_count, chunk_count)
|
||||
chunk_size = (len(pictures) // chunk_count) + 1
|
||||
chunk_size = max(MIN_CHUNK_SIZE, chunk_size)
|
||||
logging.info("Creating %d chunks with a chunk size of %d for %d pictures", chunk_count,
|
||||
chunk_size, len(pictures))
|
||||
logging.info(
|
||||
"Creating %d chunks with a chunk size of %d for %d pictures", chunk_count,
|
||||
chunk_size, len(pictures)
|
||||
)
|
||||
chunks = [pictures[i:i+chunk_size] for i in range(0, len(pictures), chunk_size)]
|
||||
return chunks
|
||||
|
||||
@@ -142,7 +144,7 @@ def getmatches(pictures, cache_path, threshold=75, match_scaled=False, j=job.nul
|
||||
|
||||
def collect_results(collect_all=False):
|
||||
# collect results and wait until the queue is small enough to accomodate a new results.
|
||||
nonlocal async_results, matches, comparison_count
|
||||
nonlocal async_results, matches, comparison_count, comparisons_to_do
|
||||
limit = 0 if collect_all else RESULTS_QUEUE_LIMIT
|
||||
while len(async_results) > limit:
|
||||
ready, working = extract(lambda r: r.ready(), async_results)
|
||||
@@ -150,7 +152,8 @@ def getmatches(pictures, cache_path, threshold=75, match_scaled=False, j=job.nul
|
||||
matches += result.get()
|
||||
async_results.remove(result)
|
||||
comparison_count += 1
|
||||
progress_msg = tr("Performed %d/%d chunk matches") % (comparison_count, len(comparisons_to_do))
|
||||
# About the NOQA below: I think there's a bug in pyflakes. To investigate...
|
||||
progress_msg = tr("Performed %d/%d chunk matches") % (comparison_count, len(comparisons_to_do)) # NOQA
|
||||
j.set_progress(comparison_count, progress_msg)
|
||||
|
||||
j = j.start_subjob([3, 7])
|
||||
@@ -175,6 +178,7 @@ def getmatches(pictures, cache_path, threshold=75, match_scaled=False, j=job.nul
|
||||
comparisons_to_do = list(combinations(chunks + [None], 2))
|
||||
comparison_count = 0
|
||||
j.start_job(len(comparisons_to_do))
|
||||
try:
|
||||
for ref_chunk, other_chunk in comparisons_to_do:
|
||||
picinfo = {p.cache_id: get_picinfo(p) for p in ref_chunk}
|
||||
ref_ids = [p.cache_id for p in ref_chunk]
|
||||
@@ -187,10 +191,23 @@ def getmatches(pictures, cache_path, threshold=75, match_scaled=False, j=job.nul
|
||||
async_results.append(pool.apply_async(async_compare, args))
|
||||
collect_results()
|
||||
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()
|
||||
|
||||
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]
|
||||
other = id2picture[other_id]
|
||||
if percentage == 100 and ref.md5 != other.md5:
|
||||
@@ -202,3 +219,4 @@ def getmatches(pictures, cache_path, threshold=75, match_scaled=False, j=job.nul
|
||||
return result
|
||||
|
||||
multiprocessing.freeze_support()
|
||||
|
||||
|
||||
@@ -28,3 +28,4 @@ def getmatches(files, match_scaled, j):
|
||||
continue
|
||||
matches.append(Match(p1, p2, 100))
|
||||
return matches
|
||||
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
|
||||
from hscommon.trans import trget
|
||||
|
||||
from core.prioritize import (KindCategory, FolderCategory, FilenameCategory, NumericalCategory,
|
||||
SizeCategory, MtimeCategory)
|
||||
from core.prioritize import (
|
||||
KindCategory, FolderCategory, FilenameCategory, NumericalCategory,
|
||||
SizeCategory, MtimeCategory
|
||||
)
|
||||
|
||||
coltr = trget('columns')
|
||||
|
||||
@@ -23,5 +25,7 @@ class DimensionsCategory(NumericalCategory):
|
||||
return (-width, -height)
|
||||
|
||||
def all_categories():
|
||||
return [KindCategory, FolderCategory, FilenameCategory, SizeCategory, DimensionsCategory,
|
||||
MtimeCategory]
|
||||
return [
|
||||
KindCategory, FolderCategory, FilenameCategory, SizeCategory, DimensionsCategory,
|
||||
MtimeCategory
|
||||
]
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
__version__ = '3.9.0'
|
||||
__version__ = '3.9.1'
|
||||
__appname__ = 'dupeGuru'
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
* Disable symlink/hardlink deletion option when not relevant. (#247)
|
||||
|
||||
@@ -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)
|
||||
|
||||
* Read RAW pictures EXIF tags. [Mac] (#234)
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
=== 3.9.1 (2014-10-17)
|
||||
|
||||
* Fixed ``AttributeError: 'ComboboxModel' object has no attribute 'reset'``. [Linux, Windows] (#254)
|
||||
* Fixed ``PermissionError`` on saving results. (#266)
|
||||
* Fixed a build problem introduced by Sphinx 1.2.3.
|
||||
* Updated German localisation, by Frank Weber.
|
||||
|
||||
=== 3.9.0 (2014-04-19)
|
||||
|
||||
* This is mostly a dependencies upgrade.
|
||||
|
||||
@@ -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):
|
||||
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
|
||||
|
||||
def setup(app):
|
||||
|
||||
@@ -10,6 +10,8 @@ Unten befindet sich die Liste aller Menschen, die direkt oder indirekt zu dupeGu
|
||||
|
||||
| **Gregor Tätzner, deutsche Übersetzung**
|
||||
|
||||
| **Frank Weber, deutsche Übersetzung**
|
||||
|
||||
| **Eric Dee, chinesische Übersetzung**
|
||||
|
||||
| **Aleš Nehyba, Czech localization**
|
||||
|
||||
@@ -10,6 +10,8 @@ Below is the list of people who contributed, directly or indirectly to dupeGuru.
|
||||
|
||||
| **Gregor Tätzner, German localization**
|
||||
|
||||
| **Frank Weber, German localization**
|
||||
|
||||
| **Eric Dee, Chinese localization**
|
||||
|
||||
| **Aleš Nehyba, Czech localization**
|
||||
|
||||
@@ -3,6 +3,7 @@ hscommon
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:glob:
|
||||
|
||||
build
|
||||
conflict
|
||||
@@ -10,10 +11,6 @@ hscommon
|
||||
notify
|
||||
path
|
||||
util
|
||||
gui/base
|
||||
gui/text_field
|
||||
gui/selectable_list
|
||||
gui/table
|
||||
gui/tree
|
||||
gui/column
|
||||
gui/progress_window
|
||||
jobprogress/*
|
||||
gui/*
|
||||
|
||||
|
||||
17
help/en/developer/hscommon/jobprogress/job.rst
Normal file
@@ -0,0 +1,17 @@
|
||||
hscommon.jobprogress.job
|
||||
========================
|
||||
|
||||
.. automodule:: hscommon.jobprogress.job
|
||||
|
||||
.. autosummary::
|
||||
|
||||
Job
|
||||
NullJob
|
||||
|
||||
.. autoclass:: Job
|
||||
:members:
|
||||
:private-members:
|
||||
|
||||
.. autoclass:: NullJob
|
||||
:members:
|
||||
|
||||
12
help/en/developer/hscommon/jobprogress/performer.rst
Normal file
@@ -0,0 +1,12 @@
|
||||
hscommon.jobprogress.performer
|
||||
==============================
|
||||
|
||||
.. automodule:: hscommon.jobprogress.performer
|
||||
|
||||
.. autosummary::
|
||||
|
||||
ThreadedJobPerformer
|
||||
|
||||
.. autoclass:: ThreadedJobPerformer
|
||||
:members:
|
||||
|
||||
12
help/en/developer/hscommon/jobprogress/qt.rst
Normal file
@@ -0,0 +1,12 @@
|
||||
hscommon.jobprogress.qt
|
||||
=======================
|
||||
|
||||
.. automodule:: hscommon.jobprogress.qt
|
||||
|
||||
.. autosummary::
|
||||
|
||||
Progress
|
||||
|
||||
.. autoclass:: Progress
|
||||
:members:
|
||||
|
||||
@@ -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
|
||||
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
|
||||
``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
|
||||
``app.DupeGuru``, like ``app_cocoa.DupeGuru``, or the ``base.app.DupeGuru`` class in the PyQt
|
||||
codebase. For example, when performing "Remove Selected From Results",
|
||||
``app_cocoa.Dupeguru.RemoveSelected()`` on the Obj-C side, and
|
||||
``base.app.DupeGuru.remove_duplicates()`` on the PyQt side, are respectively called to perform the
|
||||
thing. All of this is quite ugly, I know (see the "Refactoring" section below).
|
||||
:class:`core.app.DupeGuru`, like ``DupeGuru`` in ``cocoa/inter/app.py``, or the ``DupeGuru`` class
|
||||
in ``qt/base/app.py``. For example, when performing "Remove Selected From Results",
|
||||
``RemoveSelected()`` on the cocoa side, and ``remove_duplicates()`` on the PyQt side, are
|
||||
respectively called to perform the thing.
|
||||
|
||||
.. _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
|
||||
threaded job mechanism built-in ``app.DupeGuru``. First, ``app.DupeGuru`` has a ``progress`` member
|
||||
which is an instance of ``jobprogress.job.ThreadedJobPerformer``. It lets the GUI code know of the
|
||||
progress of the current threaded job. When ``app.DupeGuru`` needs to start a job, it calls
|
||||
threaded job mechanism built-in :class:`~core.app.DupeGuru`. First, :class:`~core.app.DupeGuru` has
|
||||
a ``progress`` member which is an instance of
|
||||
: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.
|
||||
|
||||
Core principles
|
||||
---------------
|
||||
|
||||
The core of the duplicate matching takes place (for SE and ME, not PE) in ``dupeguru.engine``.
|
||||
There's ``MatchFactory.getmatches()`` which take a list of ``fs.File`` instances and return a list
|
||||
of ``(firstfile, secondfile, match_percentage)`` matches. Then, there's ``get_groups()`` which takes
|
||||
a list of matches and returns a list of ``Group`` instances (a ``Group`` is basically a list of
|
||||
``fs.File`` matching together).
|
||||
The core of the duplicate matching takes place (for SE and ME, not PE) in :mod:`core.engine`.
|
||||
There's :func:`core.engine.getmatches` which take a list of :class:`core.fs.File` instances and
|
||||
return a list of ``(firstfile, secondfile, match_percentage)`` matches. Then, there's
|
||||
:func:`core.engine.get_groups` which takes a list of matches and returns a list of
|
||||
: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
|
||||
``app.DupeGuru.results``, which is a ``results.Results`` instance. The ``Results`` instance is where
|
||||
all the dupe marking, sorting, removing, power marking, etc. takes place.
|
||||
When a scan is over, the final result (the list of groups from :func:`.get_groups`) is placed into
|
||||
:attr:`core.app.DupeGuru.results`, which is a :class:`core.results.Results` instance. The
|
||||
:class:`~.Results` instance is where all the dupe marking, sorting, removing, power marking, etc.
|
||||
takes place.
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
@@ -9,6 +9,8 @@ Voici la liste des contributeurs de dupeGuru. Merci!
|
||||
|
||||
| **Gregor Tätzner, localisation allemande**
|
||||
|
||||
| **Frank Weber, localisation allemande**
|
||||
|
||||
| **Eric Dee, localisation choinoise**
|
||||
|
||||
| **Aleš Nehyba, localisation tchèque**
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
|
||||
| **Gregor Tätzner, Գերմաներեն թարգմանիչը**
|
||||
|
||||
| **Frank Weber, Գերմաներեն թարգմանիչը**
|
||||
|
||||
| **Eric Dee, Չինարեն թարգմանիչը**
|
||||
|
||||
| **Aleš Nehyba, Չեխերեն թարգմանիչը**
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
|
||||
| **Gregor Tätzner, Немецкая локализация**
|
||||
|
||||
| **Frank Weber, Немецкая локализация**
|
||||
|
||||
| **Eric Dee, Китайская локализация**
|
||||
|
||||
| **Aleš Nehyba, Чешский локализации**
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
|
||||
| **Gregor Tätzner, Німецька локалізація**
|
||||
|
||||
| **Frank Weber, Німецька локалізація**
|
||||
|
||||
| **Eric Dee, Китайська локалізація**
|
||||
|
||||
| **Aleš Nehyba, Чеський локалізації**
|
||||
|
||||
@@ -106,12 +106,22 @@ def get_module_version(modulename):
|
||||
return mod.__version__
|
||||
|
||||
def setup_package_argparser(parser):
|
||||
parser.add_argument('--sign', dest='sign_identity',
|
||||
help="Sign app under specified identity before packaging (OS X only)")
|
||||
parser.add_argument('--nosign', action='store_true', dest='nosign',
|
||||
help="Don't sign the packaged app (OS X only)")
|
||||
parser.add_argument('--src-pkg', action='store_true', dest='src_pkg',
|
||||
help="Build a tar.gz of the current source.")
|
||||
parser.add_argument(
|
||||
'--sign', dest='sign_identity',
|
||||
help="Sign app under specified identity before packaging (OS X only)"
|
||||
)
|
||||
parser.add_argument(
|
||||
'--nosign', action='store_true', dest='nosign',
|
||||
help="Don't sign the packaged app (OS X only)"
|
||||
)
|
||||
parser.add_argument(
|
||||
'--src-pkg', action='store_true', dest='src_pkg',
|
||||
help="Build a tar.gz of the current source."
|
||||
)
|
||||
parser.add_argument(
|
||||
'--arch-pkg', action='store_true', dest='arch_pkg',
|
||||
help="Force Arch Linux packaging type, regardless of distro name."
|
||||
)
|
||||
|
||||
# `args` come from an ArgumentParser updated with setup_package_argparser()
|
||||
def package_cocoa_app_in_dmg(app_path, destfolder, args):
|
||||
|
||||
@@ -79,7 +79,6 @@ except ImportError:
|
||||
else:
|
||||
qtfolder = QStandardPaths.DataLocation
|
||||
return QStandardPaths.standardLocations(qtfolder)[0]
|
||||
|
||||
except ImportError:
|
||||
# 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.
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
from jobprogress.performer import ThreadedJobPerformer
|
||||
|
||||
from ..jobprogress.performer import ThreadedJobPerformer
|
||||
from .base import GUIObject
|
||||
from .text_field import TextField
|
||||
|
||||
@@ -41,7 +40,7 @@ class ProgressWindowView:
|
||||
class ProgressWindow(GUIObject, ThreadedJobPerformer):
|
||||
"""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.
|
||||
|
||||
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
|
||||
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`.
|
||||
|
||||
:param finishfunc: A function ``f(jobid)`` that is called when a job is completed. ``jobid`` is
|
||||
an arbitrary id passed to :meth:`run`.
|
||||
|
||||
.. _job enabled: https://pypi.python.org/pypi/jobprogress
|
||||
"""
|
||||
def __init__(self, finish_func):
|
||||
# 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=()):
|
||||
"""Starts a threaded job.
|
||||
|
||||
The ``target`` function will be sent, as its first argument, a ``Job`` instance (from the
|
||||
``jobprogress`` library) which it can use to report on its progress.
|
||||
The ``target`` function will be sent, as its first argument, a :class:`.Job` instance which
|
||||
it can use to report on its progress.
|
||||
|
||||
:param jobid: Arbitrary identifier which will be passed to ``finish_func()`` at the end.
|
||||
:param title: A title for the task you're starting.
|
||||
|
||||
0
hscommon/jobprogress/__init__.py
Normal file
166
hscommon/jobprogress/job.py
Normal 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()
|
||||
72
hscommon/jobprogress/performer.py
Normal 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()
|
||||
|
||||
52
hscommon/jobprogress/qt.py
Normal 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
179
hscommon/reg.py
@@ -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
|
||||
|
||||
@@ -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
|
||||
# calling the command from bash.
|
||||
cmd = load_entry_point('Sphinx', 'console_scripts', 'sphinx-build')
|
||||
try:
|
||||
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")
|
||||
|
||||
|
||||
@@ -65,6 +65,12 @@ def test_trailiter():
|
||||
eq_(list(trailiter(['foo', 'bar'], skipfirst=True)), [('foo', 'bar')])
|
||||
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
|
||||
|
||||
def test_escape():
|
||||
|
||||
@@ -117,6 +117,21 @@ def trailiter(iterable, skipfirst=False):
|
||||
yield 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
|
||||
|
||||
def escape(s, to_escape, escape_with='\\'):
|
||||
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 167 B After Width: | Height: | Size: 212 B |
|
Before Width: | Height: | Size: 201 B After Width: | Height: | Size: 231 B |
@@ -1,9 +1,10 @@
|
||||
# Translators:
|
||||
# Harakiri1337, 2014
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: dupeGuru\n"
|
||||
"PO-Revision-Date: 2013-11-20 11:53+0000\n"
|
||||
"Last-Translator: hsoft <hsoft@hardcoded.net>\n"
|
||||
"PO-Revision-Date: 2014-06-03 21:56+0000\n"
|
||||
"Last-Translator: Harakiri1337\n"
|
||||
"Language-Team: German (http://www.transifex.com/projects/p/dupeguru/language/de/)\n"
|
||||
"Content-Type: text/plain; charset=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_se/result_table.py:18
|
||||
msgid "Filename"
|
||||
msgstr "Filename"
|
||||
msgstr "Dateiname"
|
||||
|
||||
#: core/prioritize.py:147
|
||||
msgid "Size"
|
||||
@@ -41,11 +42,11 @@ msgstr "Größe"
|
||||
#: core/prioritize.py:153 core_me/result_table.py:25
|
||||
#: core_pe/result_table.py:24 core_se/result_table.py:22
|
||||
msgid "Modification"
|
||||
msgstr "Modifikation"
|
||||
msgstr "Geändert"
|
||||
|
||||
#: core_me/prioritize.py:16
|
||||
msgid "Duration"
|
||||
msgstr ""
|
||||
msgstr "Dauer"
|
||||
|
||||
#: core_me/prioritize.py:22 core_me/result_table.py:22
|
||||
msgid "Bitrate"
|
||||
@@ -53,7 +54,7 @@ msgstr "Bitrate"
|
||||
|
||||
#: core_me/prioritize.py:28
|
||||
msgid "Samplerate"
|
||||
msgstr ""
|
||||
msgstr "Abtastrate"
|
||||
|
||||
#: core_me/result_table.py:20
|
||||
msgid "Size (MB)"
|
||||
@@ -89,7 +90,7 @@ msgstr "Jahr"
|
||||
|
||||
#: core_me/result_table.py:31
|
||||
msgid "Track Number"
|
||||
msgstr "Stück Nummer"
|
||||
msgstr "Titel Nummer"
|
||||
|
||||
#: core_me/result_table.py:32
|
||||
msgid "Comment"
|
||||
@@ -102,16 +103,16 @@ msgstr "Übereinstimmung %"
|
||||
|
||||
#: core_me/result_table.py:34 core_se/result_table.py:24
|
||||
msgid "Words Used"
|
||||
msgstr "Wörter genutzt"
|
||||
msgstr "genutzte Wörter"
|
||||
|
||||
#: core_me/result_table.py:35 core_pe/result_table.py:26
|
||||
#: core_se/result_table.py:25
|
||||
msgid "Dupe Count"
|
||||
msgstr "Anzahl Duplikate"
|
||||
msgstr "Anzahl der Duplikate"
|
||||
|
||||
#: core_pe/prioritize.py:16 core_pe/result_table.py:22
|
||||
msgid "Dimensions"
|
||||
msgstr "Dimensionen"
|
||||
msgstr "Auflösung"
|
||||
|
||||
#: core_pe/result_table.py:20 core_se/result_table.py:20
|
||||
msgid "Size (KB)"
|
||||
@@ -119,4 +120,4 @@ msgstr "Größe (KB)"
|
||||
|
||||
#: core_pe/result_table.py:23
|
||||
msgid "EXIF Timestamp"
|
||||
msgstr ""
|
||||
msgstr "EXIF Zeitstempel"
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# Translators:
|
||||
# Harakiri1337, 2014
|
||||
# Frank Weber <frank.weber@gmail.com>, 2014
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: dupeGuru\n"
|
||||
"PO-Revision-Date: 2013-12-07 15:22+0000\n"
|
||||
"Last-Translator: hsoft <hsoft@hardcoded.net>\n"
|
||||
"PO-Revision-Date: 2014-09-26 21:24+0000\n"
|
||||
"Last-Translator: Frank Weber <frank.weber@gmail.com>\n"
|
||||
"Language-Team: German (http://www.transifex.com/projects/p/dupeguru/language/de/)\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: utf-8\n"
|
||||
@@ -12,17 +14,19 @@ msgstr ""
|
||||
|
||||
#: core/app.py:39
|
||||
msgid "There are no marked duplicates. Nothing has been done."
|
||||
msgstr ""
|
||||
msgstr "Keine markierten Duplikate, daher wurde nichts getan."
|
||||
|
||||
#: core/app.py:40
|
||||
msgid "There are no selected duplicates. Nothing has been done."
|
||||
msgstr ""
|
||||
msgstr "Keine ausgewählten Duplikate, daher wurde nichts getan."
|
||||
|
||||
#: core/app.py:41
|
||||
msgid ""
|
||||
"You're about to open many files at once. Depending on what those files are "
|
||||
"opened with, doing so can create quite a mess. Continue?"
|
||||
msgstr ""
|
||||
"Sie sind dabei, sehr viele Dateien gleichzeitig zu öffnen. Das kann zu "
|
||||
"ziemlichem Durcheinander führen! Trotzdem fortfahren?"
|
||||
|
||||
#: core/app.py:57
|
||||
msgid "Scanning for duplicates"
|
||||
@@ -30,23 +34,23 @@ msgstr "Suche nach Duplikaten"
|
||||
|
||||
#: core/app.py:58
|
||||
msgid "Loading"
|
||||
msgstr "Laden"
|
||||
msgstr "Lade"
|
||||
|
||||
#: core/app.py:59
|
||||
msgid "Moving"
|
||||
msgstr "Verschieben"
|
||||
msgstr "Verschiebe"
|
||||
|
||||
#: core/app.py:60
|
||||
msgid "Copying"
|
||||
msgstr "Kopieren"
|
||||
msgstr "Kopiere"
|
||||
|
||||
#: core/app.py:61
|
||||
msgid "Sending to Trash"
|
||||
msgstr "Verschiebe in den Mülleimer"
|
||||
msgstr "Verschiebe in den Papierkorb"
|
||||
|
||||
#: core/app.py:64
|
||||
msgid "Sending files to the recycle bin"
|
||||
msgstr "Sende Dateien in den Mülleimer"
|
||||
msgstr "Verschiebe Dateien in den Papierkorb"
|
||||
|
||||
#: core/app.py:290
|
||||
msgid ""
|
||||
@@ -62,29 +66,32 @@ msgstr "Keine Duplikate gefunden."
|
||||
|
||||
#: core/app.py:310
|
||||
msgid "All marked files were copied successfully."
|
||||
msgstr ""
|
||||
msgstr "Alle markierten Dateien wurden erfolgreich kopiert."
|
||||
|
||||
#: core/app.py:311
|
||||
msgid "All marked files were moved successfully."
|
||||
msgstr ""
|
||||
msgstr "Alle markierten Dateien wurden erfolgreich verschoben."
|
||||
|
||||
#: core/app.py:312
|
||||
msgid "All marked files were successfully sent to Trash."
|
||||
msgstr ""
|
||||
"Alle markierten Dateien wurden erfolgreich in den Papierkorb verschoben."
|
||||
|
||||
#: core/app.py:349
|
||||
msgid "'{}' already is in the list."
|
||||
msgstr ""
|
||||
msgstr "'{}' ist bereits in der Liste."
|
||||
|
||||
#: core/app.py:351
|
||||
msgid "'{}' does not exist."
|
||||
msgstr ""
|
||||
msgstr "'{}' existiert nicht."
|
||||
|
||||
#: core/app.py:360
|
||||
msgid ""
|
||||
"All selected %d matches are going to be ignored in all subsequent scans. "
|
||||
"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
|
||||
msgid "copy"
|
||||
@@ -96,17 +103,17 @@ msgstr "verschieben"
|
||||
|
||||
#: core/app.py:427
|
||||
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
|
||||
msgid "Select a destination for your exported CSV"
|
||||
msgstr ""
|
||||
msgstr "Zielverzeichnis für den CSV Export angeben"
|
||||
|
||||
#: core/app.py:489
|
||||
msgid "You have no custom command set up. Set it up in your preferences."
|
||||
msgstr ""
|
||||
"Sie haben keinen eigenen Befehl erstellt. Bitte in den Einstellungen "
|
||||
"konfigurieren."
|
||||
"Sie haben noch keinen Befehl erstellt. Bitte dies in den Einstellungen vornehmen.\n"
|
||||
"Bsp.: \"C:\\Program Files\\Diff\\Diff.exe\" \"%d\" \"%r\""
|
||||
|
||||
#: core/app.py:641 core/app.py:654
|
||||
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
|
||||
msgid "{} duplicate groups were changed by the re-prioritization."
|
||||
msgstr ""
|
||||
msgstr "{} Duplikat-Gruppen wurden durch die Neu-Priorisierung geändert."
|
||||
|
||||
#: core/app.py:716
|
||||
msgid "Collecting files to scan"
|
||||
msgstr "Sammle Dateien zum Scannen"
|
||||
msgstr "Sammle zu scannende Dateien..."
|
||||
|
||||
#: core/app.py:727
|
||||
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
|
||||
msgid "%s (%d discarded)"
|
||||
@@ -130,11 +137,11 @@ msgstr "%s (%d verworfen)"
|
||||
|
||||
#: core/engine.py:220 core/engine.py:265
|
||||
msgid "0 matches found"
|
||||
msgstr "0 Paare gefunden"
|
||||
msgstr "0 Übereinstimmungen gefunden"
|
||||
|
||||
#: core/engine.py:238 core/engine.py:273
|
||||
msgid "%d matches found"
|
||||
msgstr "%d Paare gefunden"
|
||||
msgstr "%d Übereinstimmungen gefunden"
|
||||
|
||||
#: core/engine.py:258 core/scanner.py:79
|
||||
msgid "Read size of %d/%d files"
|
||||
@@ -142,51 +149,51 @@ msgstr "Lese Größe von %d/%d Dateien"
|
||||
|
||||
#: core/engine.py:464
|
||||
msgid "Grouped %d/%d matches"
|
||||
msgstr "%d/%d Paare gruppiert"
|
||||
msgstr "%d/%d Übereinstimmungen gruppiert"
|
||||
|
||||
#: core/gui/deletion_options.py:69
|
||||
msgid "You are sending {} file(s) to the Trash."
|
||||
msgstr ""
|
||||
msgstr "Verschiebe {} Datei(en) in den Papierkorb."
|
||||
|
||||
#: core/gui/ignore_list_dialog.py:24
|
||||
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
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
msgstr "Nichts"
|
||||
|
||||
#: core/prioritize.py:96
|
||||
msgid "Ends with number"
|
||||
msgstr "Ends with number"
|
||||
msgstr "Endet mit Zahl"
|
||||
|
||||
#: core/prioritize.py:97
|
||||
msgid "Doesn't end with number"
|
||||
msgstr "Doesn't end with number"
|
||||
msgstr "Endet nicht mit Zahl"
|
||||
|
||||
#: core/prioritize.py:98
|
||||
msgid "Longest"
|
||||
msgstr ""
|
||||
msgstr "Längste"
|
||||
|
||||
#: core/prioritize.py:99
|
||||
msgid "Shortest"
|
||||
msgstr ""
|
||||
msgstr "Kürzeste"
|
||||
|
||||
#: core/prioritize.py:132
|
||||
msgid "Highest"
|
||||
msgstr "Highest"
|
||||
msgstr "Höchste"
|
||||
|
||||
#: core/prioritize.py:132
|
||||
msgid "Lowest"
|
||||
msgstr "Lowest"
|
||||
msgstr "Niedrigste"
|
||||
|
||||
#: core/prioritize.py:159
|
||||
msgid "Newest"
|
||||
msgstr "Newest"
|
||||
msgstr "Neuste"
|
||||
|
||||
#: core/prioritize.py:159
|
||||
msgid "Oldest"
|
||||
msgstr "Oldest"
|
||||
msgstr "Älterste"
|
||||
|
||||
#: core/results.py:126
|
||||
msgid "%d / %d (%s / %s) duplicates marked."
|
||||
@@ -202,11 +209,11 @@ msgstr "Lese Metadaten von %d/%d Dateien"
|
||||
|
||||
#: core/scanner.py:130
|
||||
msgid "Removing false matches"
|
||||
msgstr "Entferne Falschpositive."
|
||||
msgstr "Entferne falsche Übereinstimmungen"
|
||||
|
||||
#: core/scanner.py:154
|
||||
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
|
||||
msgid "Doing group prioritization"
|
||||
@@ -214,20 +221,20 @@ msgstr "Gruppenpriorisierung"
|
||||
|
||||
#: core_pe/matchblock.py:61
|
||||
msgid "Analyzed %d/%d pictures"
|
||||
msgstr "Analysiere %d/%d Bilder"
|
||||
msgstr "Analysiere Bild %d/%d"
|
||||
|
||||
#: core_pe/matchblock.py:153
|
||||
msgid "Performed %d/%d chunk matches"
|
||||
msgstr "Performed %d/%d chunk matches"
|
||||
msgstr "%d/%d Chunk-Matches ausgeführt"
|
||||
|
||||
#: core_pe/matchblock.py:158
|
||||
msgid "Preparing for matching"
|
||||
msgstr "Vorbereitung auf den Vergleich"
|
||||
msgstr "Bereite Matching vor"
|
||||
|
||||
#: core_pe/matchblock.py:193
|
||||
msgid "Verified %d/%d matches"
|
||||
msgstr "%d/%d verifizierte Paare"
|
||||
msgstr "%d/%d verifizierte Übereinstimmungen"
|
||||
|
||||
#: core_pe/matchexif.py:18
|
||||
msgid "Read EXIF of %d/%d pictures"
|
||||
msgstr ""
|
||||
msgstr "Lese EXIF von Bild %d/%d"
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# Translators:
|
||||
# Harakiri1337, 2014
|
||||
# Frank Weber <frank.weber@gmail.com>, 2014
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: dupeGuru\n"
|
||||
"PO-Revision-Date: 2013-12-07 15:22+0000\n"
|
||||
"Last-Translator: hsoft <hsoft@hardcoded.net>\n"
|
||||
"PO-Revision-Date: 2014-09-26 21:15+0000\n"
|
||||
"Last-Translator: Frank Weber <frank.weber@gmail.com>\n"
|
||||
"Language-Team: German (http://www.transifex.com/projects/p/dupeguru/language/de/)\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: utf-8\n"
|
||||
@@ -12,49 +14,50 @@ msgstr ""
|
||||
|
||||
#: cocoa/inter/app_me.py:34
|
||||
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
|
||||
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
|
||||
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
|
||||
msgid "Talking to iTunes. Don't touch it!"
|
||||
msgstr ""
|
||||
msgstr "Kommuniziere mit iTunes. Bitte warten!"
|
||||
|
||||
#: cocoa/inter/app_me.py:195
|
||||
msgid ""
|
||||
"Your iTunes Library contains %d dead tracks ready to be removed. Continue?"
|
||||
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
|
||||
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
|
||||
msgid "The iTunes application couldn't be found."
|
||||
msgstr ""
|
||||
msgstr "Das iTunes-Programm konnte nicht gefunden werden."
|
||||
|
||||
#: cocoa/inter/app_pe.py:202
|
||||
msgid "Talking to iPhoto. Don't touch it!"
|
||||
msgstr ""
|
||||
msgstr "Kommuniziere mit iPhoto. Bitte warten!"
|
||||
|
||||
#: cocoa/inter/app_pe.py:211
|
||||
msgid "Talking to Aperture. Don't touch it!"
|
||||
msgstr ""
|
||||
msgstr "Kommuniziere mit Aperture. Bitte warten!"
|
||||
|
||||
#: cocoa/inter/app_pe.py:284
|
||||
msgid "Deleted Aperture photos were sent to a project called \"dupeGuru Trash\"."
|
||||
msgstr ""
|
||||
"Gelöschte Aperture-Fotos wurden dem Projekt \"dupeGuru Trash\" hinzugefügt."
|
||||
|
||||
#: cocoa/inter/app_pe.py:310
|
||||
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
|
||||
msgid "Quit"
|
||||
@@ -67,7 +70,7 @@ msgstr "Einstellungen"
|
||||
#: qt/base/app.py:85 qt/base/ignore_list_dialog.py:32
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Ignore List"
|
||||
msgstr ""
|
||||
msgstr "Ausnahme-Liste"
|
||||
|
||||
#: qt/base/app.py:86 cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "dupeGuru Help"
|
||||
@@ -87,51 +90,56 @@ msgstr "Debug Log öffnen"
|
||||
|
||||
#: qt/base/app.py:198
|
||||
msgid "{} file (*.{})"
|
||||
msgstr ""
|
||||
msgstr "{} Datei (*.{})"
|
||||
|
||||
#: qt/base/deletion_options.py:30 cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Deletion Options"
|
||||
msgstr ""
|
||||
msgstr "Lösch-Optionen"
|
||||
|
||||
#: qt/base/deletion_options.py:35 cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Link deleted files"
|
||||
msgstr ""
|
||||
msgstr "Verlinke gelöschte Dateien"
|
||||
|
||||
#: qt/base/deletion_options.py:37 cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid ""
|
||||
"After having deleted a duplicate, place a link targeting the reference file "
|
||||
"to replace the deleted file."
|
||||
msgstr ""
|
||||
"Doppelte Dateien werden gelöscht, an deren Stelle wird eine Verknüpfung auf "
|
||||
"die Referenz-Datei erstellt."
|
||||
|
||||
#: qt/base/deletion_options.py:42
|
||||
msgid "Hardlink"
|
||||
msgstr ""
|
||||
msgstr "Hardlink"
|
||||
|
||||
#: qt/base/deletion_options.py:42
|
||||
msgid "Symlink"
|
||||
msgstr ""
|
||||
msgstr "Symlink"
|
||||
|
||||
#: qt/base/deletion_options.py:46
|
||||
msgid " (unsupported)"
|
||||
msgstr ""
|
||||
msgstr "(nicht unterstützt)"
|
||||
|
||||
#: qt/base/deletion_options.py:47 cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Directly delete files"
|
||||
msgstr ""
|
||||
msgstr "Ohne Papierkorb löschen"
|
||||
|
||||
#: qt/base/deletion_options.py:49 cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid ""
|
||||
"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."
|
||||
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
|
||||
msgid "Proceed"
|
||||
msgstr ""
|
||||
msgstr "Fortfahren"
|
||||
|
||||
#: qt/base/deletion_options.py:56 cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Cancel"
|
||||
msgstr "Cancel"
|
||||
msgstr "Abbrechen"
|
||||
|
||||
#: qt/base/details_table.py:16 cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Attribute"
|
||||
@@ -148,7 +156,7 @@ msgstr "Referenz"
|
||||
|
||||
#: qt/base/directories_dialog.py:58 cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Load Results..."
|
||||
msgstr "Lade Ergebnisse..."
|
||||
msgstr "Ergebnis laden..."
|
||||
|
||||
#: qt/base/directories_dialog.py:59 cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Results Window"
|
||||
@@ -161,7 +169,7 @@ msgstr "Ordner hinzufügen..."
|
||||
#: qt/base/directories_dialog.py:68 qt/base/result_window.py:77
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "File"
|
||||
msgstr "Ablage"
|
||||
msgstr "Datei"
|
||||
|
||||
#: qt/base/directories_dialog.py:70 qt/base/result_window.py:85
|
||||
msgid "View"
|
||||
@@ -174,11 +182,11 @@ msgstr "Hilfe"
|
||||
|
||||
#: qt/base/directories_dialog.py:74 cocoa/base/en.lproj/Localizable.strings:0
|
||||
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
|
||||
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
|
||||
msgid "Load Results"
|
||||
@@ -186,7 +194,7 @@ msgstr "Lade Ergebnisse"
|
||||
|
||||
#: qt/base/directories_dialog.py:135 cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Scan"
|
||||
msgstr "Scan"
|
||||
msgstr "Suche starten"
|
||||
|
||||
#: qt/base/directories_dialog.py:179
|
||||
msgid "Unsaved results"
|
||||
@@ -194,15 +202,16 @@ msgstr "Ungespeicherte Ergebnisse"
|
||||
|
||||
#: qt/base/directories_dialog.py:180 cocoa/base/en.lproj/Localizable.strings:0
|
||||
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
|
||||
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
|
||||
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
|
||||
msgid "All Files (*.*)"
|
||||
@@ -210,11 +219,11 @@ msgstr "Alle Dateien (*.*)"
|
||||
|
||||
#: qt/base/directories_dialog.py:206 qt/base/result_window.py:287
|
||||
msgid "dupeGuru Results (*.dupeguru)"
|
||||
msgstr "dupeGuru Ergebnisse (*.dupeguru)"
|
||||
msgstr "dupeGuru Suchergebnisse (*.dupeguru)"
|
||||
|
||||
#: qt/base/directories_dialog.py:217
|
||||
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
|
||||
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
|
||||
msgid "Remove Selected"
|
||||
msgstr ""
|
||||
msgstr "Auswahl löschen"
|
||||
|
||||
#: qt/base/ignore_list_dialog.py:46 cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Clear"
|
||||
msgstr ""
|
||||
msgstr "Liste leeren"
|
||||
|
||||
#: qt/base/ignore_list_dialog.py:47 qt/base/problem_dialog.py:57
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
@@ -259,15 +268,15 @@ msgstr "Filter Empfindlichkeit:"
|
||||
|
||||
#: qt/base/preferences_dialog.py:76
|
||||
msgid "More Results"
|
||||
msgstr "mehr Ergebnisse"
|
||||
msgstr "Mehr Ergebnisse"
|
||||
|
||||
#: qt/base/preferences_dialog.py:81
|
||||
msgid "Fewer Results"
|
||||
msgstr "weniger Ergebnisse"
|
||||
msgstr "Weniger Ergebnisse"
|
||||
|
||||
#: qt/base/preferences_dialog.py:88
|
||||
msgid "Font size:"
|
||||
msgstr "Font size:"
|
||||
msgstr "Schriftgröße:"
|
||||
|
||||
#: qt/base/preferences_dialog.py:92
|
||||
msgid "Language:"
|
||||
@@ -279,7 +288,7 @@ msgstr "Kopieren und Verschieben:"
|
||||
|
||||
#: qt/base/preferences_dialog.py:101 cocoa/base/en.lproj/Localizable.strings:0
|
||||
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
|
||||
msgid "Recreate relative path"
|
||||
@@ -291,7 +300,7 @@ msgstr "Absoluten Pfad neu erstellen"
|
||||
|
||||
#: qt/base/preferences_dialog.py:106
|
||||
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
|
||||
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
|
||||
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
|
||||
msgid ""
|
||||
@@ -307,9 +316,9 @@ msgid ""
|
||||
" the best to these criteria to their respective group's reference position. "
|
||||
"Read the help file for more information."
|
||||
msgstr ""
|
||||
"Add criteria to the right box and click OK to send the dupes that correspond"
|
||||
" the best to these criteria to their respective group's reference position. "
|
||||
"Read the help file for more information."
|
||||
"Fügen Sie Kriterien zur rechten Box hinzu. Klicken Sie OK, um die Duplikate,"
|
||||
" die diesen Kriterien am besten entsprechen, zur Referenzposition der "
|
||||
"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
|
||||
msgid "Problems!"
|
||||
@@ -321,8 +330,9 @@ msgid ""
|
||||
"these problems are described in the table below. Those files were not "
|
||||
"removed from your results."
|
||||
msgstr ""
|
||||
"Es gab Probleme bei der Verarbeitung einiger (aller) Dateien. Der Grund der "
|
||||
"Probleme ist unten in der Tabelle beschrieben."
|
||||
"Es gab Probleme bei der Verarbeitung einiger (aller) Dateien. Der Ursache "
|
||||
"dieser Probleme ist unten genauer beschrieben. Diese Dateien wurden "
|
||||
"\"nicht\" aus Ihren Suchergebnissen entfernt."
|
||||
|
||||
#: qt/base/problem_dialog.py:52
|
||||
msgid "Reveal Selected"
|
||||
@@ -346,11 +356,11 @@ msgstr "Nur Duplikate anzeigen"
|
||||
|
||||
#: qt/base/result_window.py:47 cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Show Delta Values"
|
||||
msgstr "Zeige Deltawerte"
|
||||
msgstr "Zeige Delta-Werte"
|
||||
|
||||
#: qt/base/result_window.py:48
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
msgid "Make Selected into Reference"
|
||||
msgstr ""
|
||||
msgstr "Mache Auswahl zur Referenz"
|
||||
|
||||
#: qt/base/result_window.py:56 cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Open Selected with Default Application"
|
||||
msgstr "Öffne Ausgewählte mit Standardanwendung"
|
||||
msgstr "Öffne Auswahl mit Standard-Anwendung"
|
||||
|
||||
#: qt/base/result_window.py:57
|
||||
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
|
||||
msgid "Rename Selected"
|
||||
msgstr "Ausgewählte umbenennen"
|
||||
msgstr "Auswahl umbenennen"
|
||||
|
||||
#: qt/base/result_window.py:59 cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Mark All"
|
||||
@@ -402,19 +412,19 @@ msgstr "Nichts markieren"
|
||||
|
||||
#: qt/base/result_window.py:61 cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Invert Marking"
|
||||
msgstr "Markierung invertieren"
|
||||
msgstr "Auswahl umkehren"
|
||||
|
||||
#: qt/base/result_window.py:62 cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Mark Selected"
|
||||
msgstr "Ausgewählte markieren"
|
||||
msgstr "Auswahl markieren"
|
||||
|
||||
#: qt/base/result_window.py:63
|
||||
msgid "Export To HTML"
|
||||
msgstr "Exportiere als HTML"
|
||||
msgstr "Exportiere als HTML..."
|
||||
|
||||
#: qt/base/result_window.py:64
|
||||
msgid "Export To CSV"
|
||||
msgstr ""
|
||||
msgstr "Exportiere als CSV..."
|
||||
|
||||
#: qt/base/result_window.py:65 cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Save Results..."
|
||||
@@ -434,7 +444,7 @@ msgstr "Spalten"
|
||||
|
||||
#: qt/base/result_window.py:141
|
||||
msgid "Reset to Defaults"
|
||||
msgstr "Voreinstellungen"
|
||||
msgstr "Auf Voreinstellung zurücksetzen"
|
||||
|
||||
#: qt/base/result_window.py:163
|
||||
msgid "{} Results"
|
||||
@@ -442,15 +452,15 @@ msgstr "{} (Ergebnisse)"
|
||||
|
||||
#: qt/base/result_window.py:171 cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Dupes Only"
|
||||
msgstr "Dupes Only"
|
||||
msgstr "Nur Duplikate anzeigen"
|
||||
|
||||
#: qt/base/result_window.py:172
|
||||
msgid "Delta Values"
|
||||
msgstr ""
|
||||
msgstr "Zeige Delta-Werte"
|
||||
|
||||
#: qt/base/result_window.py:286 cocoa/base/en.lproj/Localizable.strings:0
|
||||
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
|
||||
#: 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/se/preferences_dialog.py:40 cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Contents"
|
||||
msgstr "Inhalt"
|
||||
msgstr "Inhalte"
|
||||
|
||||
#: qt/me/preferences_dialog.py:44
|
||||
msgid "Audio Contents"
|
||||
@@ -480,11 +490,11 @@ msgstr "Audio Inhalte"
|
||||
|
||||
#: qt/me/preferences_dialog.py:55 cocoa/base/en.lproj/Localizable.strings:0
|
||||
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
|
||||
msgid "Track"
|
||||
msgstr "Stück"
|
||||
msgstr "Track"
|
||||
|
||||
#: qt/me/preferences_dialog.py:63 cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Artist"
|
||||
@@ -514,7 +524,7 @@ msgstr "Wortgewichtung"
|
||||
#: qt/me/preferences_dialog.py:77 qt/se/preferences_dialog.py:51
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
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/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/se/preferences_dialog.py:76 cocoa/base/en.lproj/Localizable.strings:0
|
||||
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/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
|
||||
msgid "EXIF Timestamp"
|
||||
msgstr "EXIF Timestamp"
|
||||
msgstr "EXIF Zeitstempel"
|
||||
|
||||
#: qt/pe/preferences_dialog.py:39 cocoa/base/en.lproj/Localizable.strings:0
|
||||
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
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Clear Picture Cache"
|
||||
msgstr "Bildzwischenspeicher leeren"
|
||||
msgstr "Bilder-Cache leeren"
|
||||
|
||||
#: 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?"
|
||||
@@ -561,7 +571,7 @@ msgstr ""
|
||||
|
||||
#: qt/pe/result_window.py:29
|
||||
msgid "Picture cache cleared."
|
||||
msgstr "Bildzwischenspeicher geleert."
|
||||
msgstr "Bilder-Cache geleert."
|
||||
|
||||
#: qt/se/preferences_dialog.py:41 cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Folders"
|
||||
@@ -577,35 +587,35 @@ msgstr "KB"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "%@ Results"
|
||||
msgstr ""
|
||||
msgstr "%@ Ergebnisse"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Action"
|
||||
msgstr "Action"
|
||||
msgstr "Aktion"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Add Aperture Library"
|
||||
msgstr ""
|
||||
msgstr "Füge Aperture-Bibliothek hinzu"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Add iPhoto Library"
|
||||
msgstr "Add iPhoto Library"
|
||||
msgstr "Füge iPhoto-Bibliothek hinzu"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Add iTunes Library"
|
||||
msgstr ""
|
||||
msgstr "Füge iTunes-Bibliothek hinzu"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Add New Folder..."
|
||||
msgstr "Add New Folder..."
|
||||
msgstr "Neuer Ordner..."
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Advanced"
|
||||
msgstr "Advanced"
|
||||
msgstr "Fortgeschritten"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Audio Content"
|
||||
msgstr "Audio Content"
|
||||
msgstr "Audio Inhalt"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Automatically check for updates"
|
||||
@@ -613,7 +623,7 @@ msgstr "Automatisch nach Updates suchen"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Basic"
|
||||
msgstr "Basic"
|
||||
msgstr "Einfach"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Bring All to Front"
|
||||
@@ -621,15 +631,15 @@ msgstr "Alle nach vorne bringen"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Check for update..."
|
||||
msgstr "Check for update..."
|
||||
msgstr "Auf Updates prüfen..."
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Close Window"
|
||||
msgstr "Fenster Schließen"
|
||||
msgstr "Fenster schließen"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Content"
|
||||
msgstr "Content"
|
||||
msgstr "Inhalt"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Copy"
|
||||
@@ -637,7 +647,7 @@ msgstr "Kopieren"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
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
|
||||
msgid "Cut"
|
||||
@@ -649,7 +659,7 @@ msgstr "Delta"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Details of Selected File"
|
||||
msgstr "Details of Selected File"
|
||||
msgstr "Details der ausgewählten Datei"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Details Panel"
|
||||
@@ -657,27 +667,27 @@ msgstr "Details Panel"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Directories"
|
||||
msgstr "Directories"
|
||||
msgstr "Verzeichnisse"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "dupeGuru"
|
||||
msgstr ""
|
||||
msgstr "dupeGuru"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "dupeGuru ME Preferences"
|
||||
msgstr "dupeGuru ME Preferences"
|
||||
msgstr "dupeGuru ME Einstellungen"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "dupeGuru PE Preferences"
|
||||
msgstr "dupeGuru PE Preferences"
|
||||
msgstr "dupeGuru PE Einstellungen"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "dupeGuru Preferences"
|
||||
msgstr "dupeGuru Preferences"
|
||||
msgstr "dupeGuru Einstellungen"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "dupeGuru Results"
|
||||
msgstr "dupeGuru Results"
|
||||
msgstr "dupeGuru Ergebnisse"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "dupeGuru Website"
|
||||
@@ -689,15 +699,15 @@ msgstr "Bearbeiten"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Export Results to CSV"
|
||||
msgstr ""
|
||||
msgstr "Exportiere als CSV..."
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Export Results to XHTML"
|
||||
msgstr "Export Results to XHTML"
|
||||
msgstr "Exportiere als XHTML..."
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Fewer results"
|
||||
msgstr "Fewer results"
|
||||
msgstr "Weniger Suchergebnisse"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Filter"
|
||||
@@ -705,19 +715,19 @@ msgstr "Filter"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Filter hardness:"
|
||||
msgstr "Filter hardness:"
|
||||
msgstr "Filter Empfindlichkeit:"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Filter Results..."
|
||||
msgstr ""
|
||||
msgstr "Filter Suchergebnisse..."
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Folder Selection Window"
|
||||
msgstr "Folder Selection Window"
|
||||
msgstr "Ordner-Auswahlfenster"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Font Size:"
|
||||
msgstr ""
|
||||
msgstr "Schriftgröße:"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Hide dupeGuru"
|
||||
@@ -733,19 +743,19 @@ msgstr "Ignoriere Dateien kleiner als:"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Load from file..."
|
||||
msgstr "Load from file..."
|
||||
msgstr "Lade von Datei..."
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Minimize"
|
||||
msgstr "Im Dock ablegen"
|
||||
msgstr "Minimieren"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Mode"
|
||||
msgstr "Mode"
|
||||
msgstr "Modus"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "More results"
|
||||
msgstr "More results"
|
||||
msgstr "Mehr Suchergebnisse"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Ok"
|
||||
@@ -753,19 +763,19 @@ msgstr "Ok"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Options"
|
||||
msgstr "Options"
|
||||
msgstr "Optionen"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Paste"
|
||||
msgstr "Einsetzen"
|
||||
msgstr "Einfügen"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Preferences..."
|
||||
msgstr "Preferences..."
|
||||
msgstr "Einstellungen..."
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Quick Look"
|
||||
msgstr ""
|
||||
msgstr "Quick Look"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Quit dupeGuru"
|
||||
@@ -773,35 +783,35 @@ msgstr "dupeGuru beenden"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
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
|
||||
msgid "Reset to Default"
|
||||
msgstr "Reset to Default"
|
||||
msgstr "Auf Voreinstellung zurücksetzen"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Reset To Defaults"
|
||||
msgstr ""
|
||||
msgstr "Auf Voreinstellungen zurücksetzen"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Reveal"
|
||||
msgstr ""
|
||||
msgstr "Zeige"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Reveal Selected in Finder"
|
||||
msgstr "Reveal Selected in Finder"
|
||||
msgstr "Zeige Auswahl im Finder"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Select All"
|
||||
msgstr ""
|
||||
msgstr "Alles markieren"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Send Marked to Trash..."
|
||||
msgstr ""
|
||||
msgstr "Verschiebe Markierte in den Papierkorb..."
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Services"
|
||||
msgstr ""
|
||||
msgstr "Services"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Show All"
|
||||
@@ -809,11 +819,11 @@ msgstr "Alle einblenden"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Start Duplicate Scan"
|
||||
msgstr "Start Duplicate Scan"
|
||||
msgstr "Starte Duplikat-Scan"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "The name '%@' already exists."
|
||||
msgstr "The name '%@' already exists."
|
||||
msgstr "Der Name '%@' existiert bereits."
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Window"
|
||||
@@ -821,4 +831,4 @@ msgstr "Fenster"
|
||||
|
||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||
msgid "Zoom"
|
||||
msgstr "Zoomen"
|
||||
msgstr "Zoom"
|
||||
|
||||
34
package.py
@@ -16,9 +16,11 @@ import platform
|
||||
import glob
|
||||
|
||||
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,
|
||||
package_cocoa_app_in_dmg, copy_all)
|
||||
package_cocoa_app_in_dmg, copy_all, find_in_path
|
||||
)
|
||||
|
||||
def parse_args():
|
||||
parser = ArgumentParser()
|
||||
@@ -38,6 +40,7 @@ def package_windows(edition, dev):
|
||||
print("Qt packaging only works under Windows.")
|
||||
return
|
||||
from cx_Freeze import setup, Executable
|
||||
from PyQt5.QtCore import QLibraryInfo
|
||||
add_to_pythonpath('.')
|
||||
app_version = get_module_version('core_{}'.format(edition))
|
||||
distdir = 'dist'
|
||||
@@ -51,7 +54,8 @@ def package_windows(edition, dev):
|
||||
plugin_names = ['accessible', 'codecs', 'iconengines', 'imageformats']
|
||||
copy_qt_plugins(plugin_names, plugin_dest)
|
||||
|
||||
# Since v4.2.3, cx_freeze started to falsely include tkinter in the package. We exclude it explicitly because of that.
|
||||
# Since v4.2.3, cx_freeze started to falsely include tkinter in the package. We exclude it
|
||||
# explicitly because of that.
|
||||
options = {
|
||||
'build_exe': {
|
||||
'includes': 'atexit',
|
||||
@@ -80,13 +84,15 @@ def package_windows(edition, dev):
|
||||
executables=executables
|
||||
)
|
||||
|
||||
print("Removing useless DLLs")
|
||||
# Huge useless dll that appeared with Qt5
|
||||
for fn in glob.glob(op.join(distdir, 'icu*.dll')):
|
||||
os.remove(fn)
|
||||
print("Removing useless files")
|
||||
# Debug info that cx_freeze brings in.
|
||||
for fn in glob.glob(op.join(distdir, '*', '*.pdb')):
|
||||
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')
|
||||
print("Copying {} to dist\\help".format(help_path))
|
||||
shutil.copytree(help_path, op.join(distdir, 'help'))
|
||||
@@ -126,7 +132,7 @@ def package_debian_distribution(edition, distribution):
|
||||
ed = lambda s: s.format(edition)
|
||||
destpath = op.join('build', 'dupeguru-{0}-{1}'.format(edition, version))
|
||||
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':
|
||||
packages.append('hsaudiotag')
|
||||
copy_files_to_package(srcpath, packages, with_so=False)
|
||||
@@ -148,8 +154,10 @@ def package_debian_distribution(edition, distribution):
|
||||
changelog_dest = op.join(debdest, 'changelog')
|
||||
project_name = debopts['pkgname']
|
||||
from_version = {'se': '2.9.2', 'me': '5.7.2', 'pe': '1.8.5'}[edition]
|
||||
build_debian_changelog(changelogpath, changelog_dest, project_name, from_version=from_version,
|
||||
distribution=distribution)
|
||||
build_debian_changelog(
|
||||
changelogpath, changelog_dest, project_name, from_version=from_version,
|
||||
distribution=distribution
|
||||
)
|
||||
shutil.copy(op.join('images', ed('dg{0}_logo_128.png')), srcpath)
|
||||
os.chdir(destpath)
|
||||
cmd = "dpkg-buildpackage -S"
|
||||
@@ -168,7 +176,7 @@ def package_arch(edition):
|
||||
print("Packaging for Arch")
|
||||
ed = lambda s: s.format(edition)
|
||||
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':
|
||||
packages.append('hsaudiotag')
|
||||
copy_files_to_package(srcpath, packages, with_so=True)
|
||||
@@ -206,7 +214,10 @@ def main():
|
||||
if ISWINDOWS:
|
||||
package_windows(edition, dev)
|
||||
elif ISLINUX:
|
||||
if not args.arch_pkg:
|
||||
distname, _, _ = platform.dist()
|
||||
else:
|
||||
distname = 'arch'
|
||||
if distname == 'arch':
|
||||
package_arch(edition)
|
||||
else:
|
||||
@@ -216,3 +227,4 @@ def main():
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ Source: {pkgname}
|
||||
Section: devel
|
||||
Priority: extra
|
||||
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
|
||||
Homepage: http://www.hardcoded.net
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ from PyQt5.QtGui import QDesktopServices
|
||||
from PyQt5.QtWidgets import QApplication, QFileDialog, QDialog, QMessageBox
|
||||
|
||||
from hscommon.trans import trget
|
||||
from hscommon.plat import ISLINUX
|
||||
from hscommon import desktop
|
||||
|
||||
from qtlib.about_box import AboutBox
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# cxfreeze has some problems detecting all dependencies.
|
||||
# This modules explicitly import those problematic modules.
|
||||
# flake8: noqa
|
||||
|
||||
import xml.etree.ElementPath
|
||||
import gzip
|
||||
|
||||
@@ -34,8 +34,10 @@ class DeletionOptions(QDialog):
|
||||
self.verticalLayout.addWidget(self.msgLabel)
|
||||
self.linkCheckbox = QCheckBox(tr("Link deleted files"))
|
||||
self.verticalLayout.addWidget(self.linkCheckbox)
|
||||
text = tr("After having deleted a duplicate, place a link targeting the reference file "
|
||||
"to replace the deleted file.")
|
||||
text = tr(
|
||||
"After having deleted a duplicate, place a link targeting the reference file "
|
||||
"to replace the deleted file."
|
||||
)
|
||||
self.linkMessageLabel = QLabel(text)
|
||||
self.linkMessageLabel.setWordWrap(True)
|
||||
self.verticalLayout.addWidget(self.linkMessageLabel)
|
||||
@@ -46,8 +48,10 @@ class DeletionOptions(QDialog):
|
||||
self.linkCheckbox.setText(self.linkCheckbox.text() + tr(" (unsupported)"))
|
||||
self.directCheckbox = QCheckBox(tr("Directly delete files"))
|
||||
self.verticalLayout.addWidget(self.directCheckbox)
|
||||
text = tr("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.")
|
||||
text = tr(
|
||||
"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."
|
||||
)
|
||||
self.directMessageLabel = QLabel(text)
|
||||
self.directMessageLabel.setWordWrap(True)
|
||||
self.verticalLayout.addWidget(self.directMessageLabel)
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
from PyQt5.QtCore import QRect
|
||||
from PyQt5.QtWidgets import (QWidget, QFileDialog, QHeaderView, QVBoxLayout, QHBoxLayout, QTreeView,
|
||||
from PyQt5.QtWidgets import (
|
||||
QWidget, QFileDialog, QHeaderView, QVBoxLayout, QHBoxLayout, QTreeView,
|
||||
QAbstractItemView, QSpacerItem, QSizePolicy, QPushButton, QMainWindow, QMenuBar, QMenu, QLabel,
|
||||
QApplication)
|
||||
QApplication
|
||||
)
|
||||
from PyQt5.QtGui import QPixmap, QIcon
|
||||
|
||||
from hscommon.trans import trget
|
||||
@@ -225,7 +227,7 @@ class DirectoriesDialog(QMainWindow):
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
from . import dg_rc
|
||||
from . import dg_rc # NOQA
|
||||
from ..testapp import TestApp
|
||||
app = QApplication([])
|
||||
dgapp = TestApp()
|
||||
|
||||
@@ -9,8 +9,10 @@
|
||||
import urllib.parse
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, Qt, QRect, QUrl, QModelIndex, QItemSelection
|
||||
from PyQt5.QtWidgets import (QComboBox, QStyledItemDelegate, QStyle, QStyleOptionComboBox,
|
||||
QStyleOptionViewItem, QApplication)
|
||||
from PyQt5.QtWidgets import (
|
||||
QComboBox, QStyledItemDelegate, QStyle, QStyleOptionComboBox,
|
||||
QStyleOptionViewItem, QApplication
|
||||
)
|
||||
from PyQt5.QtGui import QBrush
|
||||
|
||||
from hscommon.trans import trget
|
||||
@@ -23,7 +25,7 @@ STATES = [tr("Normal"), tr("Reference"), tr("Excluded")]
|
||||
|
||||
class DirectoriesDelegate(QStyledItemDelegate):
|
||||
def createEditor(self, parent, option, index):
|
||||
editor = QComboBox(parent);
|
||||
editor = QComboBox(parent)
|
||||
editor.addItems(STATES)
|
||||
return editor
|
||||
|
||||
@@ -47,7 +49,7 @@ class DirectoriesDelegate(QStyledItemDelegate):
|
||||
|
||||
def setEditorData(self, editor, index):
|
||||
value = index.model().data(index, Qt.EditRole)
|
||||
editor.setCurrentIndex(value);
|
||||
editor.setCurrentIndex(value)
|
||||
editor.showPopup()
|
||||
|
||||
def setModelData(self, editor, model, index):
|
||||
|
||||
@@ -45,8 +45,12 @@ class IgnoreListDialog(QDialog):
|
||||
self.removeSelectedButton = QPushButton(tr("Remove Selected"))
|
||||
self.clearButton = QPushButton(tr("Clear"))
|
||||
self.closeButton = QPushButton(tr("Close"))
|
||||
self.verticalLayout.addLayout(horizontalWrap([self.removeSelectedButton, self.clearButton,
|
||||
None, self.closeButton]))
|
||||
self.verticalLayout.addLayout(
|
||||
horizontalWrap([
|
||||
self.removeSelectedButton, self.clearButton,
|
||||
None, self.closeButton
|
||||
])
|
||||
)
|
||||
|
||||
#--- model --> view
|
||||
def show(self):
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
from PyQt5.QtCore import Qt, QSize
|
||||
from PyQt5.QtWidgets import (QDialog, QDialogButtonBox, QVBoxLayout, QHBoxLayout, QLabel, QComboBox,
|
||||
QSlider, QSizePolicy, QSpacerItem, QCheckBox, QLineEdit, QMessageBox, QSpinBox)
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog, QDialogButtonBox, QVBoxLayout, QHBoxLayout, QLabel, QComboBox,
|
||||
QSlider, QSizePolicy, QSpacerItem, QCheckBox, QLineEdit, QMessageBox, QSpinBox
|
||||
)
|
||||
|
||||
from hscommon.plat import ISOSX, ISLINUX
|
||||
from hscommon.trans import trget
|
||||
@@ -190,3 +192,4 @@ class PreferencesDialogBase(QDialog):
|
||||
role = self.buttonBox.buttonRole(button)
|
||||
if role == QDialogButtonBox.ResetRole:
|
||||
self.resetToDefaults()
|
||||
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
from PyQt5.QtCore import Qt, QMimeData, QByteArray
|
||||
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QComboBox, QListView,
|
||||
QDialogButtonBox, QAbstractItemView, QLabel, QStyle, QSplitter, QWidget, QSizePolicy)
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QComboBox, QListView,
|
||||
QDialogButtonBox, QAbstractItemView, QLabel, QStyle, QSplitter, QWidget, QSizePolicy
|
||||
)
|
||||
|
||||
from hscommon.trans import trget
|
||||
from qtlib.selectable_list import ComboboxModel, ListviewModel
|
||||
@@ -59,7 +61,9 @@ class PrioritizeDialog(QDialog):
|
||||
self.model = PrioritizeDialogModel(app=app.model)
|
||||
self.categoryList = ComboboxModel(model=self.model.category_list, view=self.categoryCombobox)
|
||||
self.criteriaList = ListviewModel(model=self.model.criteria_list, view=self.criteriaListView)
|
||||
self.prioritizationList = PrioritizationList(model=self.model.prioritization_list, view=self.prioritizationListView)
|
||||
self.prioritizationList = PrioritizationList(
|
||||
model=self.model.prioritization_list, view=self.prioritizationListView
|
||||
)
|
||||
self.model.view = self
|
||||
|
||||
self.addCriteriaButton.clicked.connect(self.model.add_selected)
|
||||
@@ -72,9 +76,11 @@ class PrioritizeDialog(QDialog):
|
||||
self.resize(700, 400)
|
||||
|
||||
#widgets
|
||||
msg = tr("Add criteria to the right box and click OK to send the dupes that correspond the "
|
||||
msg = tr(
|
||||
"Add criteria to the right box and click OK to send the dupes that correspond the "
|
||||
"best to these criteria to their respective group's "
|
||||
"reference position. Read the help file for more information.")
|
||||
"reference position. Read the help file for more information."
|
||||
)
|
||||
self.promptLabel = QLabel(msg)
|
||||
self.promptLabel.setWordWrap(True)
|
||||
self.categoryCombobox = QComboBox()
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QSpacerItem, QSizePolicy,
|
||||
QLabel, QTableView, QAbstractItemView, QApplication)
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QSpacerItem, QSizePolicy,
|
||||
QLabel, QTableView, QAbstractItemView, QApplication
|
||||
)
|
||||
|
||||
from hscommon.trans import trget
|
||||
from .problem_table import ProblemTable
|
||||
@@ -32,9 +34,11 @@ class ProblemDialog(QDialog):
|
||||
self.resize(413, 323)
|
||||
self.verticalLayout = QVBoxLayout(self)
|
||||
self.label = QLabel(self)
|
||||
msg = tr("There were problems processing some (or all) of the files. The cause of "
|
||||
msg = tr(
|
||||
"There were problems processing some (or all) of the files. The cause of "
|
||||
"these problems are described in the table below. Those files were not "
|
||||
"removed from your results.")
|
||||
"removed from your results."
|
||||
)
|
||||
self.label.setText(msg)
|
||||
self.label.setWordWrap(True)
|
||||
self.verticalLayout.addWidget(self.label)
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
from PyQt5.QtCore import Qt, QRect
|
||||
from PyQt5.QtWidgets import (QMainWindow, QMenu, QLabel, QFileDialog, QMenuBar, QWidget,
|
||||
QVBoxLayout, QAbstractItemView, QStatusBar, QDialog, QPushButton, QCheckBox)
|
||||
from PyQt5.QtWidgets import (
|
||||
QMainWindow, QMenu, QLabel, QFileDialog, QMenuBar, QWidget,
|
||||
QVBoxLayout, QAbstractItemView, QStatusBar, QDialog, QPushButton, QCheckBox
|
||||
)
|
||||
|
||||
from hscommon.trans import trget
|
||||
from qtlib.util import moveToScreenCenter, horizontalWrap, createActions
|
||||
@@ -50,11 +52,23 @@ class ResultWindow(QMainWindow):
|
||||
('actionCopyMarked', 'Ctrl+Shift+M', '', tr("Copy Marked to..."), self.copyTriggered),
|
||||
('actionRemoveMarked', 'Ctrl+R', '', tr("Remove Marked from Results"), self.removeMarkedTriggered),
|
||||
('actionReprioritize', '', '', tr("Re-Prioritize Results..."), self.reprioritizeTriggered),
|
||||
('actionRemoveSelected', 'Ctrl+Del', '', tr("Remove Selected from Results"), self.removeSelectedTriggered),
|
||||
('actionIgnoreSelected', 'Ctrl+Shift+Del', '', tr("Add Selected to Ignore List"), self.addToIgnoreListTriggered),
|
||||
('actionMakeSelectedReference', 'Ctrl+Space', '', tr("Make Selected into Reference"), self.app.model.make_selected_reference),
|
||||
(
|
||||
'actionRemoveSelected', 'Ctrl+Del', '',
|
||||
tr("Remove Selected from Results"), self.removeSelectedTriggered
|
||||
),
|
||||
(
|
||||
'actionIgnoreSelected', 'Ctrl+Shift+Del', '',
|
||||
tr("Add Selected to Ignore List"), self.addToIgnoreListTriggered
|
||||
),
|
||||
(
|
||||
'actionMakeSelectedReference', 'Ctrl+Space', '',
|
||||
tr("Make Selected into Reference"), self.app.model.make_selected_reference
|
||||
),
|
||||
('actionOpenSelected', 'Ctrl+O', '', tr("Open Selected with Default Application"), self.openTriggered),
|
||||
('actionRevealSelected', 'Ctrl+Shift+O', '', tr("Open Containing Folder of Selected"), self.revealTriggered),
|
||||
(
|
||||
'actionRevealSelected', 'Ctrl+Shift+O', '',
|
||||
tr("Open Containing Folder of Selected"), self.revealTriggered
|
||||
),
|
||||
('actionRenameSelected', 'F2', '', tr("Rename Selected"), self.renameTriggered),
|
||||
('actionMarkAll', 'Ctrl+A', '', tr("Mark All"), self.markAllTriggered),
|
||||
('actionMarkNone', 'Ctrl+Shift+A', '', tr("Mark None"), self.markNoneTriggered),
|
||||
@@ -71,7 +85,6 @@ class ResultWindow(QMainWindow):
|
||||
|
||||
def _setupMenu(self):
|
||||
self.menubar = QMenuBar()
|
||||
self.menubar.setNativeMenuBar(False)
|
||||
self.menubar.setGeometry(QRect(0, 0, 630, 22))
|
||||
self.menuFile = QMenu(self.menubar)
|
||||
self.menuFile.setTitle(tr("File"))
|
||||
@@ -171,8 +184,10 @@ class ResultWindow(QMainWindow):
|
||||
self.deltaValuesCheckBox = QCheckBox(tr("Delta Values"))
|
||||
self.searchEdit = SearchEdit()
|
||||
self.searchEdit.setMaximumWidth(300)
|
||||
self.horizontalLayout = horizontalWrap([self.actionsButton, self.detailsButton,
|
||||
self.dupesOnlyCheckBox, self.deltaValuesCheckBox, None, self.searchEdit, 8])
|
||||
self.horizontalLayout = horizontalWrap([
|
||||
self.actionsButton, self.detailsButton,
|
||||
self.dupesOnlyCheckBox, self.deltaValuesCheckBox, None, self.searchEdit, 8
|
||||
])
|
||||
self.horizontalLayout.setSpacing(8)
|
||||
self.verticalLayout.addLayout(self.horizontalLayout)
|
||||
self.resultsView = ResultsView(self.centralwidget)
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
|
||||
import sys
|
||||
from PyQt5.QtCore import QSize
|
||||
from PyQt5.QtWidgets import (QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QSpacerItem, QWidget,
|
||||
QApplication)
|
||||
from PyQt5.QtWidgets import (
|
||||
QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QSpacerItem, QWidget,
|
||||
QApplication
|
||||
)
|
||||
|
||||
from hscommon.trans import trget
|
||||
from core.scanner import ScanType
|
||||
@@ -117,8 +119,10 @@ class PreferencesDialog(PreferencesDialogBase):
|
||||
#--- Events
|
||||
def scanTypeChanged(self, index):
|
||||
scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()]
|
||||
word_based = scan_type in (ScanType.Filename, ScanType.Fields, ScanType.FieldsNoOrder,
|
||||
ScanType.Tag)
|
||||
word_based = scan_type in (
|
||||
ScanType.Filename, ScanType.Fields, ScanType.FieldsNoOrder,
|
||||
ScanType.Tag
|
||||
)
|
||||
tag_based = scan_type == ScanType.Tag
|
||||
self.filterHardnessSlider.setEnabled(word_based)
|
||||
self.matchSimilarBox.setEnabled(word_based)
|
||||
@@ -138,3 +142,4 @@ if __name__ == '__main__':
|
||||
dialog = PreferencesDialog(None, dgapp)
|
||||
dialog.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
@@ -30,3 +30,4 @@ class ResultsModel(ResultsModelBase):
|
||||
Column('words', defaultWidth=120),
|
||||
Column('dupe_count', defaultWidth=80),
|
||||
]
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
from ._block_qt import getblocks
|
||||
from ._block_qt import getblocks # NOQA
|
||||
|
||||
# Converted to C
|
||||
# def getblock(image):
|
||||
|
||||
@@ -76,3 +76,4 @@ if __name__ == '__main__':
|
||||
dialog = PreferencesDialog(None, dgapp)
|
||||
dialog.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
@@ -21,3 +21,4 @@ class ResultsModel(ResultsModelBase):
|
||||
Column('percentage', defaultWidth=60),
|
||||
Column('dupe_count', defaultWidth=80),
|
||||
]
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ from .preferences_dialog import PreferencesDialog
|
||||
|
||||
class Directories(DirectoriesBase):
|
||||
ROOT_PATH_TO_EXCLUDE = frozenset(['windows', 'program files'])
|
||||
|
||||
def _default_state_for_path(self, path):
|
||||
result = DirectoriesBase._default_state_for_path(self, path)
|
||||
if result is not None:
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
|
||||
import sys
|
||||
from PyQt5.QtCore import QSize
|
||||
from PyQt5.QtWidgets import (QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QSpacerItem, QWidget,
|
||||
QLineEdit, QApplication)
|
||||
from PyQt5.QtWidgets import (
|
||||
QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QSpacerItem, QWidget,
|
||||
QLineEdit, QApplication
|
||||
)
|
||||
|
||||
from hscommon.plat import ISWINDOWS, ISLINUX
|
||||
from hscommon.trans import trget
|
||||
@@ -73,7 +75,10 @@ class PreferencesDialog(PreferencesDialogBase):
|
||||
spacerItem1 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
|
||||
self.horizontalLayout_2.addItem(spacerItem1)
|
||||
self.verticalLayout_4.addLayout(self.horizontalLayout_2)
|
||||
self._setupAddCheckbox('ignoreHardlinkMatches', tr("Ignore duplicates hardlinking to the same file"), self.widget)
|
||||
self._setupAddCheckbox(
|
||||
'ignoreHardlinkMatches',
|
||||
tr("Ignore duplicates hardlinking to the same file"), self.widget
|
||||
)
|
||||
self.verticalLayout_4.addWidget(self.ignoreHardlinkMatches)
|
||||
self._setupAddCheckbox('debugModeBox', tr("Debug mode (restart required)"), self.widget)
|
||||
self.verticalLayout_4.addWidget(self.debugModeBox)
|
||||
|
||||
@@ -20,3 +20,4 @@ class ResultsModel(ResultsModelBase):
|
||||
Column('words', defaultWidth=120),
|
||||
Column('dupe_count', defaultWidth=80),
|
||||
]
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 398 B |
@@ -41,7 +41,8 @@ class SelectableList(QAbstractListModel):
|
||||
#--- model --> view
|
||||
def refresh(self):
|
||||
self._updating = True
|
||||
self.reset()
|
||||
self.beginResetModel()
|
||||
self.endResetModel()
|
||||
self._updating = False
|
||||
self._restoreSelection()
|
||||
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
pytest>=2.0.0
|
||||
pytest-monkeyplus>=1.0.0
|
||||
flake8
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
-r requirements.txt
|
||||
objp>=1.2.0
|
||||
objp>=1.3.1
|
||||
appscript>=1.0.0
|
||||
xibless>=0.4.1
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
jobprogress>=1.0.4
|
||||
Send2Trash>=1.3.0
|
||||
sphinx>=1.2.2
|
||||
polib>=1.0.4
|
||||
|
||||
17
tox.ini
Normal file
@@ -0,0 +1,17 @@
|
||||
[tox]
|
||||
envlist = py33,py34
|
||||
skipsdist = True
|
||||
|
||||
[testenv]
|
||||
commands =
|
||||
flake8
|
||||
py.test core core_se core_me core_pe hscommon
|
||||
deps =
|
||||
-r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/requirements-extra.txt
|
||||
|
||||
[flake8]
|
||||
exclude = .tox,env,build,hscommon,qtlib,cocoalib,cocoa,help,./get-pip.py,./qt/dg_rc.py,./core*/tests,qt/run_template.py,cocoa/run_template.py,./run.py,./pkg
|
||||
max-line-length = 120
|
||||
ignore = W391,W293,E302,E261,E226,E227,W291,E262,E303,E265
|
||||
|
||||