Compare commits
23 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 |
1
.gitignore
vendored
@@ -6,6 +6,7 @@
|
|||||||
*.waf*
|
*.waf*
|
||||||
.lock-waf*
|
.lock-waf*
|
||||||
.idea
|
.idea
|
||||||
|
.tox
|
||||||
|
|
||||||
build
|
build
|
||||||
dist
|
dist
|
||||||
|
|||||||
@@ -63,12 +63,12 @@ On Arch, it's:
|
|||||||
|
|
||||||
Use Python's built-in `pyvenv` to create a virtual environment in which we're going to install our.
|
Use Python's built-in `pyvenv` to create a virtual environment in which we're going to install our.
|
||||||
Python-related dependencies. `pyvenv` is built-in Python but, unlike its `virtualenv` predecessor,
|
Python-related dependencies. `pyvenv` is built-in Python but, unlike its `virtualenv` predecessor,
|
||||||
it doesn't install setuptools and pip, so it has to be installed manually:
|
it doesn't install setuptools and pip (unless you use Python 3.4+), so it has to be installed
|
||||||
|
manually:
|
||||||
|
|
||||||
$ pyvenv --system-site-packages env
|
$ pyvenv --system-site-packages env
|
||||||
$ source env/bin/activate
|
$ source env/bin/activate
|
||||||
$ wget https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py -O - | python
|
$ python get-pip.py
|
||||||
$ easy_install pip
|
|
||||||
|
|
||||||
Then, you can install pip requirements in your virtualenv:
|
Then, you can install pip requirements in your virtualenv:
|
||||||
|
|
||||||
@@ -96,3 +96,4 @@ You can also package dupeGuru into an installable package with:
|
|||||||
[pyqt]: http://www.riverbankcomputing.com
|
[pyqt]: http://www.riverbankcomputing.com
|
||||||
[cxfreeze]: http://cx-freeze.sourceforge.net/
|
[cxfreeze]: http://cx-freeze.sourceforge.net/
|
||||||
[advinst]: http://www.advancedinstaller.com
|
[advinst]: http://www.advancedinstaller.com
|
||||||
|
|
||||||
|
|||||||
132
build.py
@@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2009-12-30
|
# Created On: 2009-12-30
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
@@ -18,10 +18,12 @@ import compileall
|
|||||||
from setuptools import setup, Extension
|
from setuptools import setup, Extension
|
||||||
|
|
||||||
from hscommon import sphinxgen
|
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,
|
get_module_version, move_all, copy_all, OSXAppStructure,
|
||||||
build_cocoalib_xibless, fix_qt_resource_file, build_cocoa_ext, copy_embeddable_python_dylib,
|
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 import loc
|
||||||
from hscommon.plat import ISOSX, ISLINUX
|
from hscommon.plat import ISOSX, ISLINUX
|
||||||
from hscommon.util import ensure_folder, delete_files_with_pattern
|
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():
|
def parse_args():
|
||||||
usage = "usage: %prog [options]"
|
usage = "usage: %prog [options]"
|
||||||
parser = OptionParser(usage=usage)
|
parser = OptionParser(usage=usage)
|
||||||
parser.add_option('--clean', action='store_true', dest='clean',
|
parser.add_option(
|
||||||
help="Clean build folder before building")
|
'--clean', action='store_true', dest='clean',
|
||||||
parser.add_option('--doc', action='store_true', dest='doc',
|
help="Clean build folder before building"
|
||||||
help="Build only the help file")
|
)
|
||||||
parser.add_option('--loc', action='store_true', dest='loc',
|
parser.add_option(
|
||||||
help="Build only localization")
|
'--doc', action='store_true', dest='doc',
|
||||||
parser.add_option('--cocoa-ext', action='store_true', dest='cocoa_ext',
|
help="Build only the help file"
|
||||||
help="Build only Cocoa extensions")
|
)
|
||||||
parser.add_option('--cocoa-compile', action='store_true', dest='cocoa_compile',
|
parser.add_option(
|
||||||
help="Build only Cocoa executable")
|
'--loc', action='store_true', dest='loc',
|
||||||
parser.add_option('--xibless', action='store_true', dest='xibless',
|
help="Build only localization"
|
||||||
help="Build only xibless UIs")
|
)
|
||||||
parser.add_option('--updatepot', action='store_true', dest='updatepot',
|
parser.add_option(
|
||||||
help="Generate .pot files from source code.")
|
'--cocoa-ext', action='store_true', dest='cocoa_ext',
|
||||||
parser.add_option('--mergepot', action='store_true', dest='mergepot',
|
help="Build only Cocoa extensions"
|
||||||
help="Update all .po files based on .pot files.")
|
)
|
||||||
parser.add_option('--normpo', action='store_true', dest='normpo',
|
parser.add_option(
|
||||||
help="Normalize all PO files (do this before commit).")
|
'--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()
|
(options, args) = parser.parse_args()
|
||||||
return options
|
return options
|
||||||
|
|
||||||
@@ -75,12 +95,20 @@ def build_xibless(edition, dest='cocoa/autogen'):
|
|||||||
('preferences_panel.py', 'PreferencesPanel_UI'),
|
('preferences_panel.py', 'PreferencesPanel_UI'),
|
||||||
]
|
]
|
||||||
for srcname, dstname in FNPAIRS:
|
for srcname, dstname in FNPAIRS:
|
||||||
xibless.generate(op.join('cocoa', 'base', 'ui', srcname), op.join(dest, dstname),
|
xibless.generate(
|
||||||
localizationTable='Localizable', args={'edition': edition})
|
op.join('cocoa', 'base', 'ui', srcname), op.join(dest, dstname),
|
||||||
|
localizationTable='Localizable', args={'edition': edition}
|
||||||
|
)
|
||||||
if edition == 'pe':
|
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:
|
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):
|
def build_cocoa(edition, dev):
|
||||||
print("Creating OS X app structure")
|
print("Creating OS X app structure")
|
||||||
@@ -110,15 +138,16 @@ def build_cocoa(edition, dev):
|
|||||||
'me': ['core_me'] + appscript_pkgs + ['hsaudiotag'],
|
'me': ['core_me'] + appscript_pkgs + ['hsaudiotag'],
|
||||||
'pe': ['core_pe'] + appscript_pkgs,
|
'pe': ['core_pe'] + appscript_pkgs,
|
||||||
}[edition]
|
}[edition]
|
||||||
tocopy = ['core', 'hscommon', 'cocoa/inter', 'cocoalib/cocoa', 'jobprogress', 'objp',
|
tocopy = [
|
||||||
'send2trash'] + specific_packages
|
'core', 'hscommon', 'cocoa/inter', 'cocoalib/cocoa', 'objp', 'send2trash'
|
||||||
|
] + specific_packages
|
||||||
copy_packages(tocopy, pydep_folder, create_links=dev)
|
copy_packages(tocopy, pydep_folder, create_links=dev)
|
||||||
sys.path.insert(0, 'build')
|
sys.path.insert(0, 'build')
|
||||||
extra_deps = None
|
extra_deps = None
|
||||||
if edition == 'pe':
|
if edition == 'pe':
|
||||||
# ModuleFinder can't seem to correctly detect the multiprocessing dependency, so we have
|
# ModuleFinder can't seem to correctly detect the multiprocessing dependency, so we have
|
||||||
# to manually specify it.
|
# to manually specify it.
|
||||||
extra_deps=['multiprocessing']
|
extra_deps = ['multiprocessing']
|
||||||
collect_stdlib_dependencies('build/dg_cocoa.py', pydep_folder, extra_deps=extra_deps)
|
collect_stdlib_dependencies('build/dg_cocoa.py', pydep_folder, extra_deps=extra_deps)
|
||||||
del sys.path[0]
|
del sys.path[0]
|
||||||
# Views are not referenced by python code, so they're not found by the collector.
|
# Views are not referenced by python code, so they're not found by the collector.
|
||||||
@@ -224,8 +253,10 @@ def build_updatepot():
|
|||||||
os.remove(cocoalib_pot)
|
os.remove(cocoalib_pot)
|
||||||
loc.strings2pot(op.join('cocoalib', 'en.lproj', 'cocoalib.strings'), cocoalib_pot)
|
loc.strings2pot(op.join('cocoalib', 'en.lproj', 'cocoalib.strings'), cocoalib_pot)
|
||||||
print("Enhancing ui.pot with Cocoa's strings files")
|
print("Enhancing ui.pot with Cocoa's strings files")
|
||||||
loc.strings2pot(op.join('cocoa', 'base', 'en.lproj', 'Localizable.strings'),
|
loc.strings2pot(
|
||||||
op.join('locale', 'ui.pot'))
|
op.join('cocoa', 'base', 'en.lproj', 'Localizable.strings'),
|
||||||
|
op.join('locale', 'ui.pot')
|
||||||
|
)
|
||||||
|
|
||||||
def build_mergepot():
|
def build_mergepot():
|
||||||
print("Updating .po files using .pot files")
|
print("Updating .po files using .pot files")
|
||||||
@@ -242,11 +273,15 @@ def build_cocoa_proxy_module():
|
|||||||
print("Building Cocoa Proxy")
|
print("Building Cocoa Proxy")
|
||||||
import objp.p2o
|
import objp.p2o
|
||||||
objp.p2o.generate_python_proxy_code('cocoalib/cocoa/CocoaProxy.h', 'build/CocoaProxy.m')
|
objp.p2o.generate_python_proxy_code('cocoalib/cocoa/CocoaProxy.h', 'build/CocoaProxy.m')
|
||||||
build_cocoa_ext("CocoaProxy", 'cocoalib/cocoa',
|
build_cocoa_ext(
|
||||||
['cocoalib/cocoa/CocoaProxy.m', 'build/CocoaProxy.m', 'build/ObjP.m',
|
"CocoaProxy", 'cocoalib/cocoa',
|
||||||
'cocoalib/HSErrorReportWindow.m', 'cocoa/autogen/HSErrorReportWindow_UI.m'],
|
[
|
||||||
|
'cocoalib/cocoa/CocoaProxy.m', 'build/CocoaProxy.m', 'build/ObjP.m',
|
||||||
|
'cocoalib/HSErrorReportWindow.m', 'cocoa/autogen/HSErrorReportWindow_UI.m'
|
||||||
|
],
|
||||||
['AppKit', 'CoreServices'],
|
['AppKit', 'CoreServices'],
|
||||||
['cocoalib', 'cocoa/autogen'])
|
['cocoalib', 'cocoa/autogen']
|
||||||
|
)
|
||||||
|
|
||||||
def build_cocoa_bridging_interfaces(edition):
|
def build_cocoa_bridging_interfaces(edition):
|
||||||
print("Building Cocoa Bridging Interfaces")
|
print("Building Cocoa Bridging Interfaces")
|
||||||
@@ -254,9 +289,11 @@ def build_cocoa_bridging_interfaces(edition):
|
|||||||
import objp.p2o
|
import objp.p2o
|
||||||
add_to_pythonpath('cocoa')
|
add_to_pythonpath('cocoa')
|
||||||
add_to_pythonpath('cocoalib')
|
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,
|
OutlineView, PySelectableList, SelectableListView, PyTable, TableView, PyBaseApp,
|
||||||
PyTextField, ProgressWindowView, PyProgressWindow)
|
PyTextField, ProgressWindowView, PyProgressWindow
|
||||||
|
)
|
||||||
from inter.deletion_options import PyDeletionOptions, DeletionOptionsView
|
from inter.deletion_options import PyDeletionOptions, DeletionOptionsView
|
||||||
from inter.details_panel import PyDetailsPanel, DetailsPanelView
|
from inter.details_panel import PyDetailsPanel, DetailsPanelView
|
||||||
from inter.directory_outline import PyDirectoryOutline, DirectoryOutlineView
|
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.stats_label import PyStatsLabel, StatsLabelView
|
||||||
from inter.app import PyDupeGuruBase, DupeGuruView
|
from inter.app import PyDupeGuruBase, DupeGuruView
|
||||||
appmod = importlib.import_module('inter.app_{}'.format(edition))
|
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,
|
PyDetailsPanel, PyDirectoryOutline, PyPrioritizeDialog, PyPrioritizeList, PyProblemDialog,
|
||||||
PyIgnoreListDialog, PyDeletionOptions, PyResultTable, PyStatsLabel, PyDupeGuruBase,
|
PyIgnoreListDialog, PyDeletionOptions, PyResultTable, PyStatsLabel, PyDupeGuruBase,
|
||||||
PyTextField, PyProgressWindow, appmod.PyDupeGuru]
|
PyTextField, PyProgressWindow, appmod.PyDupeGuru
|
||||||
|
]
|
||||||
for class_ in allclasses:
|
for class_ in allclasses:
|
||||||
objp.o2p.generate_objc_code(class_, 'cocoa/autogen', inherit=True)
|
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,
|
DetailsPanelView, DirectoryOutlineView, PrioritizeDialogView, PrioritizeListView,
|
||||||
IgnoreListDialogView, DeletionOptionsView, ResultTableView, StatsLabelView,
|
IgnoreListDialogView, DeletionOptionsView, ResultTableView, StatsLabelView,
|
||||||
ProgressWindowView, DupeGuruView]
|
ProgressWindowView, DupeGuruView
|
||||||
|
]
|
||||||
clsspecs = [objp.o2p.spec_from_python_class(class_) for class_ in allclasses]
|
clsspecs = [objp.o2p.spec_from_python_class(class_) for class_ in allclasses]
|
||||||
objp.p2o.generate_python_proxy_code_from_clsspec(clsspecs, 'build/CocoaViews.m')
|
objp.p2o.generate_python_proxy_code_from_clsspec(clsspecs, 'build/CocoaViews.m')
|
||||||
build_cocoa_ext('CocoaViews', 'cocoa/inter', ['build/CocoaViews.m', 'build/ObjP.m'])
|
build_cocoa_ext('CocoaViews', 'cocoa/inter', ['build/CocoaViews.m', 'build/ObjP.m'])
|
||||||
@@ -296,11 +337,12 @@ def build_pe_modules(ui):
|
|||||||
extra_link_args=[
|
extra_link_args=[
|
||||||
"-framework", "CoreFoundation",
|
"-framework", "CoreFoundation",
|
||||||
"-framework", "Foundation",
|
"-framework", "Foundation",
|
||||||
"-framework", "ApplicationServices",]
|
"-framework", "ApplicationServices",
|
||||||
|
]
|
||||||
))
|
))
|
||||||
setup(
|
setup(
|
||||||
script_args = ['build_ext', '--inplace'],
|
script_args=['build_ext', '--inplace'],
|
||||||
ext_modules = exts,
|
ext_modules=exts,
|
||||||
)
|
)
|
||||||
move_all('_block_qt*', op.join('qt', 'pe'))
|
move_all('_block_qt*', op.join('qt', 'pe'))
|
||||||
move_all('_block*', 'core_pe')
|
move_all('_block*', 'core_pe')
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
<key>NSPrincipalClass</key>
|
<key>NSPrincipalClass</key>
|
||||||
<string>NSApplication</string>
|
<string>NSApplication</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
<string>© Hardcoded Software, 2013</string>
|
<string>© Hardcoded Software, 2014</string>
|
||||||
<key>SUFeedURL</key>
|
<key>SUFeedURL</key>
|
||||||
<string>http://www.hardcoded.net/updates/dupeguru_me.appcast</string>
|
<string>http://www.hardcoded.net/updates/dupeguru_me.appcast</string>
|
||||||
<key>SUPublicDSAKeyFile</key>
|
<key>SUPublicDSAKeyFile</key>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
<key>NSPrincipalClass</key>
|
<key>NSPrincipalClass</key>
|
||||||
<string>NSApplication</string>
|
<string>NSApplication</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
<string>© Hardcoded Software, 2013</string>
|
<string>© Hardcoded Software, 2014</string>
|
||||||
<key>SUFeedURL</key>
|
<key>SUFeedURL</key>
|
||||||
<string>http://www.hardcoded.net/updates/dupeguru_pe.appcast</string>
|
<string>http://www.hardcoded.net/updates/dupeguru_pe.appcast</string>
|
||||||
<key>SUPublicDSAKeyFile</key>
|
<key>SUPublicDSAKeyFile</key>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
<key>NSPrincipalClass</key>
|
<key>NSPrincipalClass</key>
|
||||||
<string>NSApplication</string>
|
<string>NSApplication</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
<string>© Hardcoded Software, 2013</string>
|
<string>© Hardcoded Software, 2014</string>
|
||||||
<key>SUFeedURL</key>
|
<key>SUFeedURL</key>
|
||||||
<string>http://www.hardcoded.net/updates/dupeguru.appcast</string>
|
<string>http://www.hardcoded.net/updates/dupeguru.appcast</string>
|
||||||
<key>SUPublicDSAKeyFile</key>
|
<key>SUPublicDSAKeyFile</key>
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# Created On: 2007-10-06
|
# Created On: 2007-10-06
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
|
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -26,7 +26,7 @@ def autoreleasepool(func):
|
|||||||
|
|
||||||
def as_fetch(as_list, as_type, step_size=1000):
|
def as_fetch(as_list, as_type, step_size=1000):
|
||||||
"""When fetching items from a very big list through applescript, the connection with the app
|
"""When fetching items from a very big list through applescript, the connection with the app
|
||||||
will timeout. This function is to circumvent that. 'as_type' is the type of the items in the
|
will timeout. This function is to circumvent that. 'as_type' is the type of the items in the
|
||||||
list (found in appscript.k). If we don't pass it to the 'each' arg of 'count()', it doesn't work.
|
list (found in appscript.k). If we don't pass it to the 'each' arg of 'count()', it doesn't work.
|
||||||
applescript is rather stupid..."""
|
applescript is rather stupid..."""
|
||||||
result = []
|
result = []
|
||||||
@@ -66,7 +66,7 @@ def extract_tb_noline(tb):
|
|||||||
|
|
||||||
def safe_format_exception(type, value, tb):
|
def safe_format_exception(type, value, tb):
|
||||||
"""Format exception from type, value and tb and fallback if there's a problem.
|
"""Format exception from type, value and tb and fallback if there's a problem.
|
||||||
|
|
||||||
In some cases in threaded exceptions under Cocoa, I get tracebacks targeting pyc files instead
|
In some cases in threaded exceptions under Cocoa, I get tracebacks targeting pyc files instead
|
||||||
of py files, which results in traceback.format_exception() trying to print lines from pyc files
|
of py files, which results in traceback.format_exception() trying to print lines from pyc files
|
||||||
and then crashing when trying to interpret that binary data as utf-8. We want a fallback in
|
and then crashing when trying to interpret that binary data as utf-8. We want a fallback in
|
||||||
@@ -113,5 +113,6 @@ def patch_threaded_job_performer():
|
|||||||
# _async_run, under cocoa, has to be run within an autorelease pool to prevent leaks.
|
# _async_run, under cocoa, has to be run within an autorelease pool to prevent leaks.
|
||||||
# You only need this patch is you use one of CocoaProxy's function (which allocate objc
|
# You only need this patch is you use one of CocoaProxy's function (which allocate objc
|
||||||
# structures) inside a threaded job.
|
# structures) inside a threaded job.
|
||||||
from jobprogress.performer import ThreadedJobPerformer
|
from hscommon.jobprogress.performer import ThreadedJobPerformer
|
||||||
ThreadedJobPerformer._async_run = autoreleasepool(ThreadedJobPerformer._async_run)
|
ThreadedJobPerformer._async_run = autoreleasepool(ThreadedJobPerformer._async_run)
|
||||||
|
|
||||||
|
|||||||
26
configure.py
@@ -1,12 +1,11 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2009-12-30
|
# Created On: 2009-12-30
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
import sys
|
|
||||||
from optparse import OptionParser
|
from optparse import OptionParser
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@@ -29,11 +28,18 @@ def main(options):
|
|||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
usage = "usage: %prog [options]"
|
usage = "usage: %prog [options]"
|
||||||
parser = OptionParser(usage=usage)
|
parser = OptionParser(usage=usage)
|
||||||
parser.add_option('--edition', dest='edition',
|
parser.add_option(
|
||||||
help="dupeGuru edition to build (se, me or pe). Default is se.")
|
'--edition', dest='edition',
|
||||||
parser.add_option('--ui', dest='ui',
|
help="dupeGuru edition to build (se, me or pe). Default is se."
|
||||||
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,
|
parser.add_option(
|
||||||
help="If this flag is set, will configure for dev builds.")
|
'--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()
|
(options, args) = parser.parse_args()
|
||||||
main(options)
|
main(options)
|
||||||
|
|
||||||
|
|||||||
235
core/app.py
@@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2006/11/11
|
# Created On: 2006/11/11
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -15,7 +15,7 @@ import time
|
|||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from send2trash import send2trash
|
from send2trash import send2trash
|
||||||
from jobprogress import job
|
from hscommon.jobprogress import job
|
||||||
from hscommon.notify import Broadcaster
|
from hscommon.notify import Broadcaster
|
||||||
from hscommon.path import Path
|
from hscommon.path import Path
|
||||||
from hscommon.conflict import smart_move, smart_copy
|
from hscommon.conflict import smart_move, smart_copy
|
||||||
@@ -38,8 +38,10 @@ DEBUG_MODE_PREFERENCE = 'DebugMode'
|
|||||||
|
|
||||||
MSG_NO_MARKED_DUPES = tr("There are no marked duplicates. Nothing has been done.")
|
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_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 "
|
MSG_MANY_FILES_TO_OPEN = tr(
|
||||||
"files are opened with, doing so can create quite a mess. Continue?")
|
"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:
|
class DestType:
|
||||||
Direct = 0
|
Direct = 0
|
||||||
@@ -78,7 +80,7 @@ def format_words(w):
|
|||||||
return '(%s)' % ', '.join(do_format(item) for item in w)
|
return '(%s)' % ', '.join(do_format(item) for item in w)
|
||||||
else:
|
else:
|
||||||
return w.replace('\n', ' ')
|
return w.replace('\n', ' ')
|
||||||
|
|
||||||
return ', '.join(do_format(item) for item in w)
|
return ', '.join(do_format(item) for item in w)
|
||||||
|
|
||||||
def format_perc(p):
|
def format_perc(p):
|
||||||
@@ -110,33 +112,33 @@ def fix_surrogate_encoding(s, encoding='utf-8'):
|
|||||||
|
|
||||||
class DupeGuru(Broadcaster):
|
class DupeGuru(Broadcaster):
|
||||||
"""Holds everything together.
|
"""Holds everything together.
|
||||||
|
|
||||||
Instantiated once per running application, it holds a reference to every high-level object
|
Instantiated once per running application, it holds a reference to every high-level object
|
||||||
whose reference needs to be held: :class:`~core.results.Results`, :class:`Scanner`,
|
whose reference needs to be held: :class:`~core.results.Results`, :class:`Scanner`,
|
||||||
:class:`~core.directories.Directories`, :mod:`core.gui` instances, etc..
|
:class:`~core.directories.Directories`, :mod:`core.gui` instances, etc..
|
||||||
|
|
||||||
It also hosts high level methods and acts as a coordinator for all those elements. This is why
|
It also hosts high level methods and acts as a coordinator for all those elements. This is why
|
||||||
some of its methods seem a bit shallow, like for example :meth:`mark_all` and
|
some of its methods seem a bit shallow, like for example :meth:`mark_all` and
|
||||||
:meth:`remove_duplicates`. These methos are just proxies for a method in :attr:`results`, but
|
:meth:`remove_duplicates`. These methos are just proxies for a method in :attr:`results`, but
|
||||||
they are also followed by a notification call which is very important if we want GUI elements
|
they are also followed by a notification call which is very important if we want GUI elements
|
||||||
to be correctly notified of a change in the data they're presenting.
|
to be correctly notified of a change in the data they're presenting.
|
||||||
|
|
||||||
.. attribute:: directories
|
.. attribute:: directories
|
||||||
|
|
||||||
Instance of :class:`~core.directories.Directories`. It holds the current folder selection.
|
Instance of :class:`~core.directories.Directories`. It holds the current folder selection.
|
||||||
|
|
||||||
.. attribute:: results
|
.. attribute:: results
|
||||||
|
|
||||||
Instance of :class:`core.results.Results`. Holds the results of the latest scan.
|
Instance of :class:`core.results.Results`. Holds the results of the latest scan.
|
||||||
|
|
||||||
.. attribute:: selected_dupes
|
.. attribute:: selected_dupes
|
||||||
|
|
||||||
List of currently selected dupes from our :attr:`results`. Whenever the user changes its
|
List of currently selected dupes from our :attr:`results`. Whenever the user changes its
|
||||||
selection at the UI level, :attr:`result_table` takes care of updating this attribute, so
|
selection at the UI level, :attr:`result_table` takes care of updating this attribute, so
|
||||||
you can trust that it's always up-to-date.
|
you can trust that it's always up-to-date.
|
||||||
|
|
||||||
.. attribute:: result_table
|
.. attribute:: result_table
|
||||||
|
|
||||||
Instance of :mod:`meta-gui <core.gui>` table listing the results from :attr:`results`
|
Instance of :mod:`meta-gui <core.gui>` table listing the results from :attr:`results`
|
||||||
"""
|
"""
|
||||||
#--- View interface
|
#--- View interface
|
||||||
@@ -154,7 +156,7 @@ class DupeGuru(Broadcaster):
|
|||||||
|
|
||||||
PROMPT_NAME = "dupeGuru"
|
PROMPT_NAME = "dupeGuru"
|
||||||
SCANNER_CLASS = scanner.Scanner
|
SCANNER_CLASS = scanner.Scanner
|
||||||
|
|
||||||
def __init__(self, view):
|
def __init__(self, view):
|
||||||
if view.get_default(DEBUG_MODE_PREFERENCE):
|
if view.get_default(DEBUG_MODE_PREFERENCE):
|
||||||
logging.getLogger().setLevel(logging.DEBUG)
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
@@ -185,14 +187,14 @@ class DupeGuru(Broadcaster):
|
|||||||
children = [self.result_table, self.directory_tree, self.stats_label, self.details_panel]
|
children = [self.result_table, self.directory_tree, self.stats_label, self.details_panel]
|
||||||
for child in children:
|
for child in children:
|
||||||
child.connect()
|
child.connect()
|
||||||
|
|
||||||
#--- Virtual
|
#--- Virtual
|
||||||
def _prioritization_categories(self):
|
def _prioritization_categories(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def _create_result_table(self):
|
def _create_result_table(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
#--- Private
|
#--- Private
|
||||||
def _get_dupe_sort_key(self, dupe, get_group, key, delta):
|
def _get_dupe_sort_key(self, dupe, get_group, key, delta):
|
||||||
if key == 'marked':
|
if key == 'marked':
|
||||||
@@ -212,7 +214,7 @@ class DupeGuru(Broadcaster):
|
|||||||
same = cmp_value(dupe, key) == refval
|
same = cmp_value(dupe, key) == refval
|
||||||
result = (same, result)
|
result = (same, result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _get_group_sort_key(self, group, key):
|
def _get_group_sort_key(self, group, key):
|
||||||
if key == 'percentage':
|
if key == 'percentage':
|
||||||
return group.percentage
|
return group.percentage
|
||||||
@@ -221,15 +223,15 @@ class DupeGuru(Broadcaster):
|
|||||||
if key == 'marked':
|
if key == 'marked':
|
||||||
return len([dupe for dupe in group.dupes if self.results.is_marked(dupe)])
|
return len([dupe for dupe in group.dupes if self.results.is_marked(dupe)])
|
||||||
return cmp_value(group.ref, key)
|
return cmp_value(group.ref, key)
|
||||||
|
|
||||||
def _do_delete(self, j, link_deleted, use_hardlinks, direct_deletion):
|
def _do_delete(self, j, link_deleted, use_hardlinks, direct_deletion):
|
||||||
def op(dupe):
|
def op(dupe):
|
||||||
j.add_progress()
|
j.add_progress()
|
||||||
return self._do_delete_dupe(dupe, link_deleted, use_hardlinks, direct_deletion)
|
return self._do_delete_dupe(dupe, link_deleted, use_hardlinks, direct_deletion)
|
||||||
|
|
||||||
j.start_job(self.results.mark_count)
|
j.start_job(self.results.mark_count)
|
||||||
self.results.perform_on_marked(op, True)
|
self.results.perform_on_marked(op, True)
|
||||||
|
|
||||||
def _do_delete_dupe(self, dupe, link_deleted, use_hardlinks, direct_deletion):
|
def _do_delete_dupe(self, dupe, link_deleted, use_hardlinks, direct_deletion):
|
||||||
if not dupe.path.exists():
|
if not dupe.path.exists():
|
||||||
return
|
return
|
||||||
@@ -248,11 +250,11 @@ class DupeGuru(Broadcaster):
|
|||||||
linkfunc = os.link if use_hardlinks else os.symlink
|
linkfunc = os.link if use_hardlinks else os.symlink
|
||||||
linkfunc(str(ref.path), str_path)
|
linkfunc(str(ref.path), str_path)
|
||||||
self.clean_empty_dirs(dupe.path.parent())
|
self.clean_empty_dirs(dupe.path.parent())
|
||||||
|
|
||||||
def _create_file(self, path):
|
def _create_file(self, path):
|
||||||
# We add fs.Folder to fileclasses in case the file we're loading contains folder paths.
|
# We add fs.Folder to fileclasses in case the file we're loading contains folder paths.
|
||||||
return fs.get_file(path, self.directories.fileclasses + [fs.Folder])
|
return fs.get_file(path, self.directories.fileclasses + [fs.Folder])
|
||||||
|
|
||||||
def _get_file(self, str_path):
|
def _get_file(self, str_path):
|
||||||
path = Path(str_path)
|
path = Path(str_path)
|
||||||
f = self._create_file(path)
|
f = self._create_file(path)
|
||||||
@@ -263,10 +265,12 @@ class DupeGuru(Broadcaster):
|
|||||||
return f
|
return f
|
||||||
except EnvironmentError:
|
except EnvironmentError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _get_export_data(self):
|
def _get_export_data(self):
|
||||||
columns = [col for col in self.result_table.columns.ordered_columns
|
columns = [
|
||||||
if col.visible and col.name != 'marked']
|
col for col in self.result_table.columns.ordered_columns
|
||||||
|
if col.visible and col.name != 'marked'
|
||||||
|
]
|
||||||
colnames = [col.display for col in columns]
|
colnames = [col.display for col in columns]
|
||||||
rows = []
|
rows = []
|
||||||
for group_id, group in enumerate(self.results.groups):
|
for group_id, group in enumerate(self.results.groups):
|
||||||
@@ -276,20 +280,25 @@ class DupeGuru(Broadcaster):
|
|||||||
row.insert(0, group_id)
|
row.insert(0, group_id)
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
return colnames, rows
|
return colnames, rows
|
||||||
|
|
||||||
def _results_changed(self):
|
def _results_changed(self):
|
||||||
self.selected_dupes = [d for d in self.selected_dupes
|
self.selected_dupes = [
|
||||||
if self.results.get_group_of_duplicate(d) is not None]
|
d for d in self.selected_dupes
|
||||||
|
if self.results.get_group_of_duplicate(d) is not None
|
||||||
|
]
|
||||||
self.notify('results_changed')
|
self.notify('results_changed')
|
||||||
|
|
||||||
def _start_job(self, jobid, func, args=()):
|
def _start_job(self, jobid, func, args=()):
|
||||||
title = JOBID2TITLE[jobid]
|
title = JOBID2TITLE[jobid]
|
||||||
try:
|
try:
|
||||||
self.progress_window.run(jobid, title, func, args=args)
|
self.progress_window.run(jobid, title, func, args=args)
|
||||||
except job.JobInProgressError:
|
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)
|
self.view.show_message(msg)
|
||||||
|
|
||||||
def _job_completed(self, jobid):
|
def _job_completed(self, jobid):
|
||||||
if jobid == JobType.Scan:
|
if jobid == JobType.Scan:
|
||||||
self._results_changed()
|
self._results_changed()
|
||||||
@@ -312,7 +321,7 @@ class DupeGuru(Broadcaster):
|
|||||||
JobType.Delete: tr("All marked files were successfully sent to Trash."),
|
JobType.Delete: tr("All marked files were successfully sent to Trash."),
|
||||||
}[jobid]
|
}[jobid]
|
||||||
self.view.show_message(msg)
|
self.view.show_message(msg)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _remove_hardlink_dupes(files):
|
def _remove_hardlink_dupes(files):
|
||||||
seen_inodes = set()
|
seen_inodes = set()
|
||||||
@@ -327,19 +336,19 @@ class DupeGuru(Broadcaster):
|
|||||||
seen_inodes.add(inode)
|
seen_inodes.add(inode)
|
||||||
result.append(file)
|
result.append(file)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _select_dupes(self, dupes):
|
def _select_dupes(self, dupes):
|
||||||
if dupes == self.selected_dupes:
|
if dupes == self.selected_dupes:
|
||||||
return
|
return
|
||||||
self.selected_dupes = dupes
|
self.selected_dupes = dupes
|
||||||
self.notify('dupes_selected')
|
self.notify('dupes_selected')
|
||||||
|
|
||||||
#--- Public
|
#--- Public
|
||||||
def add_directory(self, d):
|
def add_directory(self, d):
|
||||||
"""Adds folder ``d`` to :attr:`directories`.
|
"""Adds folder ``d`` to :attr:`directories`.
|
||||||
|
|
||||||
Shows an error message dialog if something bad happens.
|
Shows an error message dialog if something bad happens.
|
||||||
|
|
||||||
:param str d: path of folder to add
|
:param str d: path of folder to add
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
@@ -349,7 +358,7 @@ class DupeGuru(Broadcaster):
|
|||||||
self.view.show_message(tr("'{}' already is in the list.").format(d))
|
self.view.show_message(tr("'{}' already is in the list.").format(d))
|
||||||
except directories.InvalidPathError:
|
except directories.InvalidPathError:
|
||||||
self.view.show_message(tr("'{}' does not exist.").format(d))
|
self.view.show_message(tr("'{}' does not exist.").format(d))
|
||||||
|
|
||||||
def add_selected_to_ignore_list(self):
|
def add_selected_to_ignore_list(self):
|
||||||
"""Adds :attr:`selected_dupes` to :attr:`scanner`'s ignore list.
|
"""Adds :attr:`selected_dupes` to :attr:`scanner`'s ignore list.
|
||||||
"""
|
"""
|
||||||
@@ -367,10 +376,10 @@ class DupeGuru(Broadcaster):
|
|||||||
self.scanner.ignore_list.Ignore(str(other.path), str(dupe.path))
|
self.scanner.ignore_list.Ignore(str(other.path), str(dupe.path))
|
||||||
self.remove_duplicates(dupes)
|
self.remove_duplicates(dupes)
|
||||||
self.ignore_list_dialog.refresh()
|
self.ignore_list_dialog.refresh()
|
||||||
|
|
||||||
def apply_filter(self, filter):
|
def apply_filter(self, filter):
|
||||||
"""Apply a filter ``filter`` to the results so that it shows only dupe groups that match it.
|
"""Apply a filter ``filter`` to the results so that it shows only dupe groups that match it.
|
||||||
|
|
||||||
:param str filter: filter to apply
|
:param str filter: filter to apply
|
||||||
"""
|
"""
|
||||||
self.results.apply_filter(None)
|
self.results.apply_filter(None)
|
||||||
@@ -379,12 +388,12 @@ class DupeGuru(Broadcaster):
|
|||||||
filter = escape(filter, '*', '.')
|
filter = escape(filter, '*', '.')
|
||||||
self.results.apply_filter(filter)
|
self.results.apply_filter(filter)
|
||||||
self._results_changed()
|
self._results_changed()
|
||||||
|
|
||||||
def clean_empty_dirs(self, path):
|
def clean_empty_dirs(self, path):
|
||||||
if self.options['clean_empty_dirs']:
|
if self.options['clean_empty_dirs']:
|
||||||
while delete_if_empty(path, ['.DS_Store']):
|
while delete_if_empty(path, ['.DS_Store']):
|
||||||
path = path.parent()
|
path = path.parent()
|
||||||
|
|
||||||
def copy_or_move(self, dupe, copy: bool, destination: str, dest_type: DestType):
|
def copy_or_move(self, dupe, copy: bool, destination: str, dest_type: DestType):
|
||||||
source_path = dupe.path
|
source_path = dupe.path
|
||||||
location_path = first(p for p in self.directories if dupe.path in p)
|
location_path = first(p for p in self.directories if dupe.path in p)
|
||||||
@@ -406,20 +415,20 @@ class DupeGuru(Broadcaster):
|
|||||||
else:
|
else:
|
||||||
smart_move(source_path, dest_path)
|
smart_move(source_path, dest_path)
|
||||||
self.clean_empty_dirs(source_path.parent())
|
self.clean_empty_dirs(source_path.parent())
|
||||||
|
|
||||||
def copy_or_move_marked(self, copy):
|
def copy_or_move_marked(self, copy):
|
||||||
"""Start an async move (or copy) job on marked duplicates.
|
"""Start an async move (or copy) job on marked duplicates.
|
||||||
|
|
||||||
:param bool copy: If True, duplicates will be copied instead of moved
|
:param bool copy: If True, duplicates will be copied instead of moved
|
||||||
"""
|
"""
|
||||||
def do(j):
|
def do(j):
|
||||||
def op(dupe):
|
def op(dupe):
|
||||||
j.add_progress()
|
j.add_progress()
|
||||||
self.copy_or_move(dupe, copy, destination, desttype)
|
self.copy_or_move(dupe, copy, destination, desttype)
|
||||||
|
|
||||||
j.start_job(self.results.mark_count)
|
j.start_job(self.results.mark_count)
|
||||||
self.results.perform_on_marked(op, not copy)
|
self.results.perform_on_marked(op, not copy)
|
||||||
|
|
||||||
if not self.results.mark_count:
|
if not self.results.mark_count:
|
||||||
self.view.show_message(MSG_NO_MARKED_DUPES)
|
self.view.show_message(MSG_NO_MARKED_DUPES)
|
||||||
return
|
return
|
||||||
@@ -430,7 +439,7 @@ class DupeGuru(Broadcaster):
|
|||||||
desttype = self.options['copymove_dest_type']
|
desttype = self.options['copymove_dest_type']
|
||||||
jobid = JobType.Copy if copy else JobType.Move
|
jobid = JobType.Copy if copy else JobType.Move
|
||||||
self._start_job(jobid, do)
|
self._start_job(jobid, do)
|
||||||
|
|
||||||
def delete_marked(self):
|
def delete_marked(self):
|
||||||
"""Start an async job to send marked duplicates to the trash.
|
"""Start an async job to send marked duplicates to the trash.
|
||||||
"""
|
"""
|
||||||
@@ -439,14 +448,16 @@ class DupeGuru(Broadcaster):
|
|||||||
return
|
return
|
||||||
if not self.deletion_options.show(self.results.mark_count):
|
if not self.deletion_options.show(self.results.mark_count):
|
||||||
return
|
return
|
||||||
args = [self.deletion_options.link_deleted, self.deletion_options.use_hardlinks,
|
args = [
|
||||||
self.deletion_options.direct]
|
self.deletion_options.link_deleted, self.deletion_options.use_hardlinks,
|
||||||
|
self.deletion_options.direct
|
||||||
|
]
|
||||||
logging.debug("Starting deletion job with args %r", args)
|
logging.debug("Starting deletion job with args %r", args)
|
||||||
self._start_job(JobType.Delete, self._do_delete, args=args)
|
self._start_job(JobType.Delete, self._do_delete, args=args)
|
||||||
|
|
||||||
def export_to_xhtml(self):
|
def export_to_xhtml(self):
|
||||||
"""Export current results to XHTML.
|
"""Export current results to XHTML.
|
||||||
|
|
||||||
The configuration of the :attr:`result_table` (columns order and visibility) is used to
|
The configuration of the :attr:`result_table` (columns order and visibility) is used to
|
||||||
determine how the data is presented in the export. In other words, the exported table in
|
determine how the data is presented in the export. In other words, the exported table in
|
||||||
the resulting XHTML will look just like the results table.
|
the resulting XHTML will look just like the results table.
|
||||||
@@ -454,18 +465,21 @@ class DupeGuru(Broadcaster):
|
|||||||
colnames, rows = self._get_export_data()
|
colnames, rows = self._get_export_data()
|
||||||
export_path = export.export_to_xhtml(colnames, rows)
|
export_path = export.export_to_xhtml(colnames, rows)
|
||||||
desktop.open_path(export_path)
|
desktop.open_path(export_path)
|
||||||
|
|
||||||
def export_to_csv(self):
|
def export_to_csv(self):
|
||||||
"""Export current results to CSV.
|
"""Export current results to CSV.
|
||||||
|
|
||||||
The columns and their order in the resulting CSV file is determined in the same way as in
|
The columns and their order in the resulting CSV file is determined in the same way as in
|
||||||
:meth:`export_to_xhtml`.
|
:meth:`export_to_xhtml`.
|
||||||
"""
|
"""
|
||||||
dest_file = self.view.select_dest_file(tr("Select a destination for your exported CSV"), 'csv')
|
dest_file = self.view.select_dest_file(tr("Select a destination for your exported CSV"), 'csv')
|
||||||
if dest_file:
|
if dest_file:
|
||||||
colnames, rows = self._get_export_data()
|
colnames, rows = self._get_export_data()
|
||||||
export.export_to_csv(dest_file, colnames, rows)
|
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 get_display_info(self, dupe, group, delta=False):
|
||||||
def empty_data():
|
def empty_data():
|
||||||
return {c.name: '---' for c in self.result_table.COLUMNS[1:]}
|
return {c.name: '---' for c in self.result_table.COLUMNS[1:]}
|
||||||
@@ -476,10 +490,10 @@ class DupeGuru(Broadcaster):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.warning("Exception on GetDisplayInfo for %s: %s", str(dupe.path), str(e))
|
logging.warning("Exception on GetDisplayInfo for %s: %s", str(dupe.path), str(e))
|
||||||
return empty_data()
|
return empty_data()
|
||||||
|
|
||||||
def invoke_custom_command(self):
|
def invoke_custom_command(self):
|
||||||
"""Calls command in ``CustomCommand`` pref with ``%d`` and ``%r`` placeholders replaced.
|
"""Calls command in ``CustomCommand`` pref with ``%d`` and ``%r`` placeholders replaced.
|
||||||
|
|
||||||
Using the current selection, ``%d`` is replaced with the currently selected dupe and ``%r``
|
Using the current selection, ``%d`` is replaced with the currently selected dupe and ``%r``
|
||||||
is replaced with that dupe's ref file. If there's no selection, the command is not invoked.
|
is replaced with that dupe's ref file. If there's no selection, the command is not invoked.
|
||||||
If the dupe is a ref, ``%d`` and ``%r`` will be the same.
|
If the dupe is a ref, ``%d`` and ``%r`` will be the same.
|
||||||
@@ -506,10 +520,10 @@ class DupeGuru(Broadcaster):
|
|||||||
subprocess.Popen(exename + args, shell=True, cwd=path)
|
subprocess.Popen(exename + args, shell=True, cwd=path)
|
||||||
else:
|
else:
|
||||||
subprocess.Popen(cmd, shell=True)
|
subprocess.Popen(cmd, shell=True)
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
"""Load directory selection and ignore list from files in appdata.
|
"""Load directory selection and ignore list from files in appdata.
|
||||||
|
|
||||||
This method is called during startup so that directory selection and ignore list, which
|
This method is called during startup so that directory selection and ignore list, which
|
||||||
is persistent data, is the same as when the last session was closed (when :meth:`save` was
|
is persistent data, is the same as when the last session was closed (when :meth:`save` was
|
||||||
called).
|
called).
|
||||||
@@ -519,19 +533,19 @@ class DupeGuru(Broadcaster):
|
|||||||
p = op.join(self.appdata, 'ignore_list.xml')
|
p = op.join(self.appdata, 'ignore_list.xml')
|
||||||
self.scanner.ignore_list.load_from_xml(p)
|
self.scanner.ignore_list.load_from_xml(p)
|
||||||
self.ignore_list_dialog.refresh()
|
self.ignore_list_dialog.refresh()
|
||||||
|
|
||||||
def load_from(self, filename):
|
def load_from(self, filename):
|
||||||
"""Start an async job to load results from ``filename``.
|
"""Start an async job to load results from ``filename``.
|
||||||
|
|
||||||
:param str filename: path of the XML file (created with :meth:`save_as`) to load
|
:param str filename: path of the XML file (created with :meth:`save_as`) to load
|
||||||
"""
|
"""
|
||||||
def do(j):
|
def do(j):
|
||||||
self.results.load_from_xml(filename, self._get_file, j)
|
self.results.load_from_xml(filename, self._get_file, j)
|
||||||
self._start_job(JobType.Load, do)
|
self._start_job(JobType.Load, do)
|
||||||
|
|
||||||
def make_selected_reference(self):
|
def make_selected_reference(self):
|
||||||
"""Promote :attr:`selected_dupes` to reference position within their respective groups.
|
"""Promote :attr:`selected_dupes` to reference position within their respective groups.
|
||||||
|
|
||||||
Each selected dupe will become the :attr:`~core.engine.Group.ref` of its group. If there's
|
Each selected dupe will become the :attr:`~core.engine.Group.ref` of its group. If there's
|
||||||
more than one dupe selected for the same group, only the first (in the order currently shown
|
more than one dupe selected for the same group, only the first (in the order currently shown
|
||||||
in :attr:`result_table`) dupe will be promoted.
|
in :attr:`result_table`) dupe will be promoted.
|
||||||
@@ -550,8 +564,10 @@ class DupeGuru(Broadcaster):
|
|||||||
# If no group was changed, however, we don't touch the selection.
|
# If no group was changed, however, we don't touch the selection.
|
||||||
if not self.result_table.power_marker:
|
if not self.result_table.power_marker:
|
||||||
if changed_groups:
|
if changed_groups:
|
||||||
self.selected_dupes = [d for d in self.selected_dupes
|
self.selected_dupes = [
|
||||||
if self.results.get_group_of_duplicate(d).ref is d]
|
d for d in self.selected_dupes
|
||||||
|
if self.results.get_group_of_duplicate(d).ref is d
|
||||||
|
]
|
||||||
self.notify('results_changed')
|
self.notify('results_changed')
|
||||||
else:
|
else:
|
||||||
# If we're in "Dupes Only" mode (previously called Power Marker), things are a bit
|
# If we're in "Dupes Only" mode (previously called Power Marker), things are a bit
|
||||||
@@ -560,28 +576,28 @@ class DupeGuru(Broadcaster):
|
|||||||
# do is to keep our selection index-wise (different dupe selection, but same index
|
# do is to keep our selection index-wise (different dupe selection, but same index
|
||||||
# selection).
|
# selection).
|
||||||
self.notify('results_changed_but_keep_selection')
|
self.notify('results_changed_but_keep_selection')
|
||||||
|
|
||||||
def mark_all(self):
|
def mark_all(self):
|
||||||
"""Set all dupes in the results as marked.
|
"""Set all dupes in the results as marked.
|
||||||
"""
|
"""
|
||||||
self.results.mark_all()
|
self.results.mark_all()
|
||||||
self.notify('marking_changed')
|
self.notify('marking_changed')
|
||||||
|
|
||||||
def mark_none(self):
|
def mark_none(self):
|
||||||
"""Set all dupes in the results as unmarked.
|
"""Set all dupes in the results as unmarked.
|
||||||
"""
|
"""
|
||||||
self.results.mark_none()
|
self.results.mark_none()
|
||||||
self.notify('marking_changed')
|
self.notify('marking_changed')
|
||||||
|
|
||||||
def mark_invert(self):
|
def mark_invert(self):
|
||||||
"""Invert the marked state of all dupes in the results.
|
"""Invert the marked state of all dupes in the results.
|
||||||
"""
|
"""
|
||||||
self.results.mark_invert()
|
self.results.mark_invert()
|
||||||
self.notify('marking_changed')
|
self.notify('marking_changed')
|
||||||
|
|
||||||
def mark_dupe(self, dupe, marked):
|
def mark_dupe(self, dupe, marked):
|
||||||
"""Change marked status of ``dupe``.
|
"""Change marked status of ``dupe``.
|
||||||
|
|
||||||
:param dupe: dupe to mark/unmark
|
:param dupe: dupe to mark/unmark
|
||||||
:type dupe: :class:`~core.fs.File`
|
:type dupe: :class:`~core.fs.File`
|
||||||
:param bool marked: True = mark, False = unmark
|
:param bool marked: True = mark, False = unmark
|
||||||
@@ -591,7 +607,7 @@ class DupeGuru(Broadcaster):
|
|||||||
else:
|
else:
|
||||||
self.results.unmark(dupe)
|
self.results.unmark(dupe)
|
||||||
self.notify('marking_changed')
|
self.notify('marking_changed')
|
||||||
|
|
||||||
def open_selected(self):
|
def open_selected(self):
|
||||||
"""Open :attr:`selected_dupes` with their associated application.
|
"""Open :attr:`selected_dupes` with their associated application.
|
||||||
"""
|
"""
|
||||||
@@ -600,16 +616,16 @@ class DupeGuru(Broadcaster):
|
|||||||
return
|
return
|
||||||
for dupe in self.selected_dupes:
|
for dupe in self.selected_dupes:
|
||||||
desktop.open_path(dupe.path)
|
desktop.open_path(dupe.path)
|
||||||
|
|
||||||
def purge_ignore_list(self):
|
def purge_ignore_list(self):
|
||||||
"""Remove files that don't exist from :attr:`ignore_list`.
|
"""Remove files that don't exist from :attr:`ignore_list`.
|
||||||
"""
|
"""
|
||||||
self.scanner.ignore_list.Filter(lambda f,s:op.exists(f) and op.exists(s))
|
self.scanner.ignore_list.Filter(lambda f, s: op.exists(f) and op.exists(s))
|
||||||
self.ignore_list_dialog.refresh()
|
self.ignore_list_dialog.refresh()
|
||||||
|
|
||||||
def remove_directories(self, indexes):
|
def remove_directories(self, indexes):
|
||||||
"""Remove root directories at ``indexes`` from :attr:`directories`.
|
"""Remove root directories at ``indexes`` from :attr:`directories`.
|
||||||
|
|
||||||
:param indexes: Indexes of the directories to remove.
|
:param indexes: Indexes of the directories to remove.
|
||||||
:type indexes: list of int
|
:type indexes: list of int
|
||||||
"""
|
"""
|
||||||
@@ -620,30 +636,30 @@ class DupeGuru(Broadcaster):
|
|||||||
self.notify('directories_changed')
|
self.notify('directories_changed')
|
||||||
except IndexError:
|
except IndexError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def remove_duplicates(self, duplicates):
|
def remove_duplicates(self, duplicates):
|
||||||
"""Remove ``duplicates`` from :attr:`results`.
|
"""Remove ``duplicates`` from :attr:`results`.
|
||||||
|
|
||||||
Calls :meth:`~core.results.Results.remove_duplicates` and send appropriate notifications.
|
Calls :meth:`~core.results.Results.remove_duplicates` and send appropriate notifications.
|
||||||
|
|
||||||
:param duplicates: duplicates to remove.
|
:param duplicates: duplicates to remove.
|
||||||
:type duplicates: list of :class:`~core.fs.File`
|
:type duplicates: list of :class:`~core.fs.File`
|
||||||
"""
|
"""
|
||||||
self.results.remove_duplicates(self.without_ref(duplicates))
|
self.results.remove_duplicates(self.without_ref(duplicates))
|
||||||
self.notify('results_changed_but_keep_selection')
|
self.notify('results_changed_but_keep_selection')
|
||||||
|
|
||||||
def remove_marked(self):
|
def remove_marked(self):
|
||||||
"""Removed marked duplicates from the results (without touching the files themselves).
|
"""Removed marked duplicates from the results (without touching the files themselves).
|
||||||
"""
|
"""
|
||||||
if not self.results.mark_count:
|
if not self.results.mark_count:
|
||||||
self.view.show_message(MSG_NO_MARKED_DUPES)
|
self.view.show_message(MSG_NO_MARKED_DUPES)
|
||||||
return
|
return
|
||||||
msg = tr("You are about to remove %d files from results. Continue?")
|
msg = tr("You are about to remove %d files from results. Continue?")
|
||||||
if not self.view.ask_yes_no(msg % self.results.mark_count):
|
if not self.view.ask_yes_no(msg % self.results.mark_count):
|
||||||
return
|
return
|
||||||
self.results.perform_on_marked(lambda x:None, True)
|
self.results.perform_on_marked(lambda x: None, True)
|
||||||
self._results_changed()
|
self._results_changed()
|
||||||
|
|
||||||
def remove_selected(self):
|
def remove_selected(self):
|
||||||
"""Removed :attr:`selected_dupes` from the results (without touching the files themselves).
|
"""Removed :attr:`selected_dupes` from the results (without touching the files themselves).
|
||||||
"""
|
"""
|
||||||
@@ -651,16 +667,16 @@ class DupeGuru(Broadcaster):
|
|||||||
if not dupes:
|
if not dupes:
|
||||||
self.view.show_message(MSG_NO_SELECTED_DUPES)
|
self.view.show_message(MSG_NO_SELECTED_DUPES)
|
||||||
return
|
return
|
||||||
msg = tr("You are about to remove %d files from results. Continue?")
|
msg = tr("You are about to remove %d files from results. Continue?")
|
||||||
if not self.view.ask_yes_no(msg % len(dupes)):
|
if not self.view.ask_yes_no(msg % len(dupes)):
|
||||||
return
|
return
|
||||||
self.remove_duplicates(dupes)
|
self.remove_duplicates(dupes)
|
||||||
|
|
||||||
def rename_selected(self, newname):
|
def rename_selected(self, newname):
|
||||||
"""Renames the selected dupes's file to ``newname``.
|
"""Renames the selected dupes's file to ``newname``.
|
||||||
|
|
||||||
If there's more than one selected dupes, the first one is used.
|
If there's more than one selected dupes, the first one is used.
|
||||||
|
|
||||||
:param str newname: The filename to rename the dupe's file to.
|
:param str newname: The filename to rename the dupe's file to.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
@@ -670,13 +686,13 @@ class DupeGuru(Broadcaster):
|
|||||||
except (IndexError, fs.FSError) as e:
|
except (IndexError, fs.FSError) as e:
|
||||||
logging.warning("dupeGuru Warning: %s" % str(e))
|
logging.warning("dupeGuru Warning: %s" % str(e))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def reprioritize_groups(self, sort_key):
|
def reprioritize_groups(self, sort_key):
|
||||||
"""Sort dupes in each group (in :attr:`results`) according to ``sort_key``.
|
"""Sort dupes in each group (in :attr:`results`) according to ``sort_key``.
|
||||||
|
|
||||||
Called by the re-prioritize dialog. Calls :meth:`~core.engine.Group.prioritize` and, once
|
Called by the re-prioritize dialog. Calls :meth:`~core.engine.Group.prioritize` and, once
|
||||||
the sorting is done, show a message that confirms the action.
|
the sorting is done, show a message that confirms the action.
|
||||||
|
|
||||||
:param sort_key: The key being sent to :meth:`~core.engine.Group.prioritize`
|
:param sort_key: The key being sent to :meth:`~core.engine.Group.prioritize`
|
||||||
:type sort_key: f(dupe)
|
:type sort_key: f(dupe)
|
||||||
"""
|
"""
|
||||||
@@ -687,11 +703,11 @@ class DupeGuru(Broadcaster):
|
|||||||
self._results_changed()
|
self._results_changed()
|
||||||
msg = tr("{} duplicate groups were changed by the re-prioritization.").format(count)
|
msg = tr("{} duplicate groups were changed by the re-prioritization.").format(count)
|
||||||
self.view.show_message(msg)
|
self.view.show_message(msg)
|
||||||
|
|
||||||
def reveal_selected(self):
|
def reveal_selected(self):
|
||||||
if self.selected_dupes:
|
if self.selected_dupes:
|
||||||
desktop.reveal_path(self.selected_dupes[0].path)
|
desktop.reveal_path(self.selected_dupes[0].path)
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
if not op.exists(self.appdata):
|
if not op.exists(self.appdata):
|
||||||
os.makedirs(self.appdata)
|
os.makedirs(self.appdata)
|
||||||
@@ -699,17 +715,20 @@ class DupeGuru(Broadcaster):
|
|||||||
p = op.join(self.appdata, 'ignore_list.xml')
|
p = op.join(self.appdata, 'ignore_list.xml')
|
||||||
self.scanner.ignore_list.save_to_xml(p)
|
self.scanner.ignore_list.save_to_xml(p)
|
||||||
self.notify('save_session')
|
self.notify('save_session')
|
||||||
|
|
||||||
def save_as(self, filename):
|
def save_as(self, filename):
|
||||||
"""Save results in ``filename``.
|
"""Save results in ``filename``.
|
||||||
|
|
||||||
:param str filename: path of the file to save results (as XML) to.
|
:param str filename: path of the file to save results (as XML) to.
|
||||||
"""
|
"""
|
||||||
self.results.save_to_xml(filename)
|
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):
|
def start_scanning(self):
|
||||||
"""Starts an async job to scan for duplicates.
|
"""Starts an async job to scan for duplicates.
|
||||||
|
|
||||||
Scans folders selected in :attr:`directories` and put the results in :attr:`results`
|
Scans folders selected in :attr:`directories` and put the results in :attr:`results`
|
||||||
"""
|
"""
|
||||||
def do(j):
|
def do(j):
|
||||||
@@ -722,14 +741,14 @@ class DupeGuru(Broadcaster):
|
|||||||
files = self._remove_hardlink_dupes(files)
|
files = self._remove_hardlink_dupes(files)
|
||||||
logging.info('Scanning %d files' % len(files))
|
logging.info('Scanning %d files' % len(files))
|
||||||
self.results.groups = self.scanner.get_dupe_groups(files, j)
|
self.results.groups = self.scanner.get_dupe_groups(files, j)
|
||||||
|
|
||||||
if not self.directories.has_any_file():
|
if not self.directories.has_any_file():
|
||||||
self.view.show_message(tr("The selected directories contain no scannable file."))
|
self.view.show_message(tr("The selected directories contain no scannable file."))
|
||||||
return
|
return
|
||||||
self.results.groups = []
|
self.results.groups = []
|
||||||
self._results_changed()
|
self._results_changed()
|
||||||
self._start_job(JobType.Scan, do)
|
self._start_job(JobType.Scan, do)
|
||||||
|
|
||||||
def toggle_selected_mark_state(self):
|
def toggle_selected_mark_state(self):
|
||||||
selected = self.without_ref(self.selected_dupes)
|
selected = self.without_ref(self.selected_dupes)
|
||||||
if not selected:
|
if not selected:
|
||||||
@@ -741,12 +760,12 @@ class DupeGuru(Broadcaster):
|
|||||||
for dupe in selected:
|
for dupe in selected:
|
||||||
markfunc(dupe)
|
markfunc(dupe)
|
||||||
self.notify('marking_changed')
|
self.notify('marking_changed')
|
||||||
|
|
||||||
def without_ref(self, dupes):
|
def without_ref(self, dupes):
|
||||||
"""Returns ``dupes`` with all reference elements removed.
|
"""Returns ``dupes`` with all reference elements removed.
|
||||||
"""
|
"""
|
||||||
return [dupe for dupe in dupes if self.results.get_group_of_duplicate(dupe).ref is not dupe]
|
return [dupe for dupe in dupes if self.results.get_group_of_duplicate(dupe).ref is not dupe]
|
||||||
|
|
||||||
def get_default(self, key, fallback_value=None):
|
def get_default(self, key, fallback_value=None):
|
||||||
result = nonone(self.view.get_default(key), fallback_value)
|
result = nonone(self.view.get_default(key), fallback_value)
|
||||||
if fallback_value is not None and not isinstance(result, type(fallback_value)):
|
if fallback_value is not None and not isinstance(result, type(fallback_value)):
|
||||||
@@ -756,10 +775,10 @@ class DupeGuru(Broadcaster):
|
|||||||
except Exception:
|
except Exception:
|
||||||
result = fallback_value
|
result = fallback_value
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def set_default(self, key, value):
|
def set_default(self, key, value):
|
||||||
self.view.set_default(key, value)
|
self.view.set_default(key, value)
|
||||||
|
|
||||||
#--- Properties
|
#--- Properties
|
||||||
@property
|
@property
|
||||||
def stat_line(self):
|
def stat_line(self):
|
||||||
@@ -767,4 +786,4 @@ class DupeGuru(Broadcaster):
|
|||||||
if self.scanner.discarded_file_count:
|
if self.scanner.discarded_file_count:
|
||||||
result = tr("%s (%d discarded)") % (result, self.scanner.discarded_file_count)
|
result = tr("%s (%d discarded)") % (result, self.scanner.discarded_file_count)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2006/02/27
|
# Created On: 2006/02/27
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from xml.etree import ElementTree as ET
|
from xml.etree import ElementTree as ET
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from jobprogress import job
|
from hscommon.jobprogress import job
|
||||||
from hscommon.path import Path
|
from hscommon.path import Path
|
||||||
from hscommon.util import FileOrPath
|
from hscommon.util import FileOrPath
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ __all__ = [
|
|||||||
|
|
||||||
class DirectoryState:
|
class DirectoryState:
|
||||||
"""Enum describing how a folder should be considered.
|
"""Enum describing how a folder should be considered.
|
||||||
|
|
||||||
* DirectoryState.Normal: Scan all files normally
|
* DirectoryState.Normal: Scan all files normally
|
||||||
* DirectoryState.Reference: Scan files, but make sure never to delete any of them
|
* DirectoryState.Reference: Scan files, but make sure never to delete any of them
|
||||||
* DirectoryState.Excluded: Don't scan this folder
|
* DirectoryState.Excluded: Don't scan this folder
|
||||||
@@ -41,10 +41,10 @@ class InvalidPathError(Exception):
|
|||||||
|
|
||||||
class Directories:
|
class Directories:
|
||||||
"""Holds user folder selection.
|
"""Holds user folder selection.
|
||||||
|
|
||||||
Manages the selection that the user make through the folder selection dialog. It also manages
|
Manages the selection that the user make through the folder selection dialog. It also manages
|
||||||
folder states, and how recursion applies to them.
|
folder states, and how recursion applies to them.
|
||||||
|
|
||||||
Then, when the user starts the scan, :meth:`get_files` is called to retrieve all files (wrapped
|
Then, when the user starts the scan, :meth:`get_files` is called to retrieve all files (wrapped
|
||||||
in :mod:`core.fs`) that have to be scanned according to the chosen folders/states.
|
in :mod:`core.fs`) that have to be scanned according to the chosen folders/states.
|
||||||
"""
|
"""
|
||||||
@@ -55,28 +55,28 @@ class Directories:
|
|||||||
self.states = {}
|
self.states = {}
|
||||||
self.fileclasses = fileclasses
|
self.fileclasses = fileclasses
|
||||||
self.folderclass = fs.Folder
|
self.folderclass = fs.Folder
|
||||||
|
|
||||||
def __contains__(self, path):
|
def __contains__(self, path):
|
||||||
for p in self._dirs:
|
for p in self._dirs:
|
||||||
if path in p:
|
if path in p:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def __delitem__(self,key):
|
def __delitem__(self, key):
|
||||||
self._dirs.__delitem__(key)
|
self._dirs.__delitem__(key)
|
||||||
|
|
||||||
def __getitem__(self,key):
|
def __getitem__(self, key):
|
||||||
return self._dirs.__getitem__(key)
|
return self._dirs.__getitem__(key)
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
return len(self._dirs)
|
return len(self._dirs)
|
||||||
|
|
||||||
#---Private
|
#---Private
|
||||||
def _default_state_for_path(self, path):
|
def _default_state_for_path(self, path):
|
||||||
# Override this in subclasses to specify the state of some special folders.
|
# Override this in subclasses to specify the state of some special folders.
|
||||||
if path.name.startswith('.'): # hidden
|
if path.name.startswith('.'): # hidden
|
||||||
return DirectoryState.Excluded
|
return DirectoryState.Excluded
|
||||||
|
|
||||||
def _get_files(self, from_path, j):
|
def _get_files(self, from_path, j):
|
||||||
j.check_if_cancelled()
|
j.check_if_cancelled()
|
||||||
state = self.get_state(from_path)
|
state = self.get_state(from_path)
|
||||||
@@ -95,14 +95,15 @@ class Directories:
|
|||||||
file.is_ref = state == DirectoryState.Reference
|
file.is_ref = state == DirectoryState.Reference
|
||||||
filepaths.add(file.path)
|
filepaths.add(file.path)
|
||||||
yield file
|
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]
|
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 subfolder in subfolders:
|
||||||
for file in self._get_files(subfolder, j):
|
for file in self._get_files(subfolder, j):
|
||||||
yield file
|
yield file
|
||||||
except (EnvironmentError, fs.InvalidPath):
|
except (EnvironmentError, fs.InvalidPath):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _get_folders(self, from_folder, j):
|
def _get_folders(self, from_folder, j):
|
||||||
j.check_if_cancelled()
|
j.check_if_cancelled()
|
||||||
try:
|
try:
|
||||||
@@ -116,16 +117,16 @@ class Directories:
|
|||||||
yield from_folder
|
yield from_folder
|
||||||
except (EnvironmentError, fs.InvalidPath):
|
except (EnvironmentError, fs.InvalidPath):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
#---Public
|
#---Public
|
||||||
def add_path(self, path):
|
def add_path(self, path):
|
||||||
"""Adds ``path`` to self, if not already there.
|
"""Adds ``path`` to self, if not already there.
|
||||||
|
|
||||||
Raises :exc:`AlreadyThereError` if ``path`` is already in self. If path is a directory
|
Raises :exc:`AlreadyThereError` if ``path`` is already in self. If path is a directory
|
||||||
containing some of the directories already present in self, ``path`` will be added, but all
|
containing some of the directories already present in self, ``path`` will be added, but all
|
||||||
directories under it will be removed. Can also raise :exc:`InvalidPathError` if ``path``
|
directories under it will be removed. Can also raise :exc:`InvalidPathError` if ``path``
|
||||||
does not exist.
|
does not exist.
|
||||||
|
|
||||||
:param Path path: path to add
|
:param Path path: path to add
|
||||||
"""
|
"""
|
||||||
if path in self:
|
if path in self:
|
||||||
@@ -134,43 +135,43 @@ class Directories:
|
|||||||
raise InvalidPathError()
|
raise InvalidPathError()
|
||||||
self._dirs = [p for p in self._dirs if p not in path]
|
self._dirs = [p for p in self._dirs if p not in path]
|
||||||
self._dirs.append(path)
|
self._dirs.append(path)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_subfolders(path):
|
def get_subfolders(path):
|
||||||
"""Returns a sorted list of paths corresponding to subfolders in ``path``.
|
"""Returns a sorted list of paths corresponding to subfolders in ``path``.
|
||||||
|
|
||||||
:param Path path: get subfolders from there
|
:param Path path: get subfolders from there
|
||||||
:rtype: list of Path
|
:rtype: list of Path
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
subpaths = [p for p in path.listdir() if p.isdir()]
|
subpaths = [p for p in path.listdir() if p.isdir()]
|
||||||
subpaths.sort(key=lambda x:x.name.lower())
|
subpaths.sort(key=lambda x: x.name.lower())
|
||||||
return subpaths
|
return subpaths
|
||||||
except EnvironmentError:
|
except EnvironmentError:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def get_files(self, j=job.nulljob):
|
def get_files(self, j=job.nulljob):
|
||||||
"""Returns a list of all files that are not excluded.
|
"""Returns a list of all files that are not excluded.
|
||||||
|
|
||||||
Returned files also have their ``is_ref`` attr set if applicable.
|
Returned files also have their ``is_ref`` attr set if applicable.
|
||||||
"""
|
"""
|
||||||
for path in self._dirs:
|
for path in self._dirs:
|
||||||
for file in self._get_files(path, j):
|
for file in self._get_files(path, j):
|
||||||
yield file
|
yield file
|
||||||
|
|
||||||
def get_folders(self, j=job.nulljob):
|
def get_folders(self, j=job.nulljob):
|
||||||
"""Returns a list of all folders that are not excluded.
|
"""Returns a list of all folders that are not excluded.
|
||||||
|
|
||||||
Returned folders also have their ``is_ref`` attr set if applicable.
|
Returned folders also have their ``is_ref`` attr set if applicable.
|
||||||
"""
|
"""
|
||||||
for path in self._dirs:
|
for path in self._dirs:
|
||||||
from_folder = self.folderclass(path)
|
from_folder = self.folderclass(path)
|
||||||
for folder in self._get_folders(from_folder, j):
|
for folder in self._get_folders(from_folder, j):
|
||||||
yield folder
|
yield folder
|
||||||
|
|
||||||
def get_state(self, path):
|
def get_state(self, path):
|
||||||
"""Returns the state of ``path``.
|
"""Returns the state of ``path``.
|
||||||
|
|
||||||
:rtype: :class:`DirectoryState`
|
:rtype: :class:`DirectoryState`
|
||||||
"""
|
"""
|
||||||
if path in self.states:
|
if path in self.states:
|
||||||
@@ -183,12 +184,12 @@ class Directories:
|
|||||||
return self.get_state(parent)
|
return self.get_state(parent)
|
||||||
else:
|
else:
|
||||||
return DirectoryState.Normal
|
return DirectoryState.Normal
|
||||||
|
|
||||||
def has_any_file(self):
|
def has_any_file(self):
|
||||||
"""Returns whether selected folders contain any file.
|
"""Returns whether selected folders contain any file.
|
||||||
|
|
||||||
Because it stops at the first file it finds, it's much faster than get_files().
|
Because it stops at the first file it finds, it's much faster than get_files().
|
||||||
|
|
||||||
:rtype: bool
|
:rtype: bool
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
@@ -196,10 +197,10 @@ class Directories:
|
|||||||
return True
|
return True
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def load_from_file(self, infile):
|
def load_from_file(self, infile):
|
||||||
"""Load folder selection from ``infile``.
|
"""Load folder selection from ``infile``.
|
||||||
|
|
||||||
:param file infile: path or file pointer to XML generated through :meth:`save_to_file`
|
:param file infile: path or file pointer to XML generated through :meth:`save_to_file`
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
@@ -222,10 +223,10 @@ class Directories:
|
|||||||
path = attrib['path']
|
path = attrib['path']
|
||||||
state = attrib['value']
|
state = attrib['value']
|
||||||
self.states[Path(path)] = int(state)
|
self.states[Path(path)] = int(state)
|
||||||
|
|
||||||
def save_to_file(self, outfile):
|
def save_to_file(self, outfile):
|
||||||
"""Save folder selection as XML to ``outfile``.
|
"""Save folder selection as XML to ``outfile``.
|
||||||
|
|
||||||
:param file outfile: path or file pointer to XML file to save to.
|
:param file outfile: path or file pointer to XML file to save to.
|
||||||
"""
|
"""
|
||||||
with FileOrPath(outfile, 'wb') as fp:
|
with FileOrPath(outfile, 'wb') as fp:
|
||||||
@@ -239,10 +240,10 @@ class Directories:
|
|||||||
state_node.set('value', str(state))
|
state_node.set('value', str(state))
|
||||||
tree = ET.ElementTree(root)
|
tree = ET.ElementTree(root)
|
||||||
tree.write(fp, encoding='utf-8')
|
tree.write(fp, encoding='utf-8')
|
||||||
|
|
||||||
def set_state(self, path, state):
|
def set_state(self, path, state):
|
||||||
"""Set the state of folder at ``path``.
|
"""Set the state of folder at ``path``.
|
||||||
|
|
||||||
:param Path path: path of the target folder
|
:param Path path: path of the target folder
|
||||||
:param state: state to set folder to
|
:param state: state to set folder to
|
||||||
:type state: :class:`DirectoryState`
|
:type state: :class:`DirectoryState`
|
||||||
@@ -253,4 +254,4 @@ class Directories:
|
|||||||
if path.is_parent_of(iter_path):
|
if path.is_parent_of(iter_path):
|
||||||
del self.states[iter_path]
|
del self.states[iter_path]
|
||||||
self.states[path] = state
|
self.states[path] = state
|
||||||
|
|
||||||
|
|||||||
112
core/engine.py
@@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2006/01/29
|
# Created On: 2006/01/29
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
import difflib
|
import difflib
|
||||||
@@ -15,11 +15,13 @@ from unicodedata import normalize
|
|||||||
|
|
||||||
from hscommon.util import flatten, multi_replace
|
from hscommon.util import flatten, multi_replace
|
||||||
from hscommon.trans import tr
|
from hscommon.trans import tr
|
||||||
from jobprogress import job
|
from hscommon.jobprogress import job
|
||||||
|
|
||||||
(WEIGHT_WORDS,
|
(
|
||||||
MATCH_SIMILAR_WORDS,
|
WEIGHT_WORDS,
|
||||||
NO_FIELD_ORDER) = range(3)
|
MATCH_SIMILAR_WORDS,
|
||||||
|
NO_FIELD_ORDER,
|
||||||
|
) = range(3)
|
||||||
|
|
||||||
JOB_REFRESH_RATE = 100
|
JOB_REFRESH_RATE = 100
|
||||||
|
|
||||||
@@ -45,7 +47,7 @@ def unpack_fields(fields):
|
|||||||
|
|
||||||
def compare(first, second, flags=()):
|
def compare(first, second, flags=()):
|
||||||
"""Returns the % of words that match between ``first`` and ``second``
|
"""Returns the % of words that match between ``first`` and ``second``
|
||||||
|
|
||||||
The result is a ``int`` in the range 0..100.
|
The result is a ``int`` in the range 0..100.
|
||||||
``first`` and ``second`` can be either a string or a list (of words).
|
``first`` and ``second`` can be either a string or a list (of words).
|
||||||
"""
|
"""
|
||||||
@@ -53,7 +55,7 @@ def compare(first, second, flags=()):
|
|||||||
return 0
|
return 0
|
||||||
if any(isinstance(element, list) for element in first):
|
if any(isinstance(element, list) for element in first):
|
||||||
return compare_fields(first, second, flags)
|
return compare_fields(first, second, flags)
|
||||||
second = second[:] #We must use a copy of second because we remove items from it
|
second = second[:] #We must use a copy of second because we remove items from it
|
||||||
match_similar = MATCH_SIMILAR_WORDS in flags
|
match_similar = MATCH_SIMILAR_WORDS in flags
|
||||||
weight_words = WEIGHT_WORDS in flags
|
weight_words = WEIGHT_WORDS in flags
|
||||||
joined = first + second
|
joined = first + second
|
||||||
@@ -77,9 +79,9 @@ def compare(first, second, flags=()):
|
|||||||
|
|
||||||
def compare_fields(first, second, flags=()):
|
def compare_fields(first, second, flags=()):
|
||||||
"""Returns the score for the lowest matching :ref:`fields`.
|
"""Returns the score for the lowest matching :ref:`fields`.
|
||||||
|
|
||||||
``first`` and ``second`` must be lists of lists of string. Each sub-list is then compared with
|
``first`` and ``second`` must be lists of lists of string. Each sub-list is then compared with
|
||||||
:func:`compare`.
|
:func:`compare`.
|
||||||
"""
|
"""
|
||||||
if len(first) != len(second):
|
if len(first) != len(second):
|
||||||
return 0
|
return 0
|
||||||
@@ -104,10 +106,10 @@ def compare_fields(first, second, flags=()):
|
|||||||
|
|
||||||
def build_word_dict(objects, j=job.nulljob):
|
def build_word_dict(objects, j=job.nulljob):
|
||||||
"""Returns a dict of objects mapped by their words.
|
"""Returns a dict of objects mapped by their words.
|
||||||
|
|
||||||
objects must have a ``words`` attribute being a list of strings or a list of lists of strings
|
objects must have a ``words`` attribute being a list of strings or a list of lists of strings
|
||||||
(:ref:`fields`).
|
(:ref:`fields`).
|
||||||
|
|
||||||
The result will be a dict with words as keys, lists of objects as values.
|
The result will be a dict with words as keys, lists of objects as values.
|
||||||
"""
|
"""
|
||||||
result = defaultdict(set)
|
result = defaultdict(set)
|
||||||
@@ -118,7 +120,7 @@ def build_word_dict(objects, j=job.nulljob):
|
|||||||
|
|
||||||
def merge_similar_words(word_dict):
|
def merge_similar_words(word_dict):
|
||||||
"""Take all keys in ``word_dict`` that are similar, and merge them together.
|
"""Take all keys in ``word_dict`` that are similar, and merge them together.
|
||||||
|
|
||||||
``word_dict`` has been built with :func:`build_word_dict`. Similarity is computed with Python's
|
``word_dict`` has been built with :func:`build_word_dict`. Similarity is computed with Python's
|
||||||
``difflib.get_close_matches()``, which computes the number of edits that are necessary to make
|
``difflib.get_close_matches()``, which computes the number of edits that are necessary to make
|
||||||
a word equal to the other.
|
a word equal to the other.
|
||||||
@@ -138,9 +140,9 @@ def merge_similar_words(word_dict):
|
|||||||
|
|
||||||
def reduce_common_words(word_dict, threshold):
|
def reduce_common_words(word_dict, threshold):
|
||||||
"""Remove all objects from ``word_dict`` values where the object count >= ``threshold``
|
"""Remove all objects from ``word_dict`` values where the object count >= ``threshold``
|
||||||
|
|
||||||
``word_dict`` has been built with :func:`build_word_dict`.
|
``word_dict`` has been built with :func:`build_word_dict`.
|
||||||
|
|
||||||
The exception to this removal are the objects where all the words of the object are common.
|
The exception to this removal are the objects where all the words of the object are common.
|
||||||
Because if we remove them, we will miss some duplicates!
|
Because if we remove them, we will miss some duplicates!
|
||||||
"""
|
"""
|
||||||
@@ -181,17 +183,17 @@ class Match(namedtuple('Match', 'first second percentage')):
|
|||||||
exact scan methods, such as Contents scans, this will always be 100.
|
exact scan methods, such as Contents scans, this will always be 100.
|
||||||
"""
|
"""
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
def get_match(first, second, flags=()):
|
def get_match(first, second, flags=()):
|
||||||
#it is assumed here that first and second both have a "words" attribute
|
#it is assumed here that first and second both have a "words" attribute
|
||||||
percentage = compare(first.words, second.words, flags)
|
percentage = compare(first.words, second.words, flags)
|
||||||
return Match(first, second, percentage)
|
return Match(first, second, percentage)
|
||||||
|
|
||||||
def getmatches(
|
def getmatches(
|
||||||
objects, min_match_percentage=0, match_similar_words=False, weight_words=False,
|
objects, min_match_percentage=0, match_similar_words=False, weight_words=False,
|
||||||
no_field_order=False, j=job.nulljob):
|
no_field_order=False, j=job.nulljob):
|
||||||
"""Returns a list of :class:`Match` within ``objects`` after fuzzily matching their words.
|
"""Returns a list of :class:`Match` within ``objects`` after fuzzily matching their words.
|
||||||
|
|
||||||
:param objects: List of :class:`~core.fs.File` to match.
|
:param objects: List of :class:`~core.fs.File` to match.
|
||||||
:param int min_match_percentage: minimum % of words that have to match.
|
:param int min_match_percentage: minimum % of words that have to match.
|
||||||
:param bool match_similar_words: make similar words (see :func:`merge_similar_words`) match.
|
:param bool match_similar_words: make similar words (see :func:`merge_similar_words`) match.
|
||||||
@@ -246,7 +248,7 @@ def getmatches(
|
|||||||
|
|
||||||
def getmatches_by_contents(files, sizeattr='size', partial=False, j=job.nulljob):
|
def getmatches_by_contents(files, sizeattr='size', partial=False, j=job.nulljob):
|
||||||
"""Returns a list of :class:`Match` within ``files`` if their contents is the same.
|
"""Returns a list of :class:`Match` within ``files`` if their contents is the same.
|
||||||
|
|
||||||
:param str sizeattr: attibute name of the :class:`~core.fs.file` that returns the size of the
|
:param str sizeattr: attibute name of the :class:`~core.fs.file` that returns the size of the
|
||||||
file to use for comparison.
|
file to use for comparison.
|
||||||
:param bool partial: if true, will use the "md5partial" attribute instead of "md5" to compute
|
:param bool partial: if true, will use the "md5partial" attribute instead of "md5" to compute
|
||||||
@@ -259,6 +261,7 @@ def getmatches_by_contents(files, sizeattr='size', partial=False, j=job.nulljob)
|
|||||||
filesize = getattr(file, sizeattr)
|
filesize = getattr(file, sizeattr)
|
||||||
if filesize:
|
if filesize:
|
||||||
size2files[filesize].add(file)
|
size2files[filesize].add(file)
|
||||||
|
del files
|
||||||
possible_matches = [files for files in size2files.values() if len(files) > 1]
|
possible_matches = [files for files in size2files.values() if len(files) > 1]
|
||||||
del size2files
|
del size2files
|
||||||
result = []
|
result = []
|
||||||
@@ -278,44 +281,44 @@ class Group:
|
|||||||
|
|
||||||
This manages match pairs into groups and ensures that all files in the group match to each
|
This manages match pairs into groups and ensures that all files in the group match to each
|
||||||
other.
|
other.
|
||||||
|
|
||||||
.. attribute:: ref
|
.. attribute:: ref
|
||||||
|
|
||||||
The "reference" file, which is the file among the group that isn't going to be deleted.
|
The "reference" file, which is the file among the group that isn't going to be deleted.
|
||||||
|
|
||||||
.. attribute:: ordered
|
.. attribute:: ordered
|
||||||
|
|
||||||
Ordered list of duplicates in the group (including the :attr:`ref`).
|
Ordered list of duplicates in the group (including the :attr:`ref`).
|
||||||
|
|
||||||
.. attribute:: unordered
|
.. attribute:: unordered
|
||||||
|
|
||||||
Set duplicates in the group (including the :attr:`ref`).
|
Set duplicates in the group (including the :attr:`ref`).
|
||||||
|
|
||||||
.. attribute:: dupes
|
.. attribute:: dupes
|
||||||
|
|
||||||
An ordered list of the group's duplicate, without :attr:`ref`. Equivalent to
|
An ordered list of the group's duplicate, without :attr:`ref`. Equivalent to
|
||||||
``ordered[1:]``
|
``ordered[1:]``
|
||||||
|
|
||||||
.. attribute:: percentage
|
.. attribute:: percentage
|
||||||
|
|
||||||
Average match percentage of match pairs containing :attr:`ref`.
|
Average match percentage of match pairs containing :attr:`ref`.
|
||||||
"""
|
"""
|
||||||
#---Override
|
#---Override
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._clear()
|
self._clear()
|
||||||
|
|
||||||
def __contains__(self, item):
|
def __contains__(self, item):
|
||||||
return item in self.unordered
|
return item in self.unordered
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
return self.ordered.__getitem__(key)
|
return self.ordered.__getitem__(key)
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return iter(self.ordered)
|
return iter(self.ordered)
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
return len(self.ordered)
|
return len(self.ordered)
|
||||||
|
|
||||||
#---Private
|
#---Private
|
||||||
def _clear(self):
|
def _clear(self):
|
||||||
self._percentage = None
|
self._percentage = None
|
||||||
@@ -324,22 +327,22 @@ class Group:
|
|||||||
self.candidates = defaultdict(set)
|
self.candidates = defaultdict(set)
|
||||||
self.ordered = []
|
self.ordered = []
|
||||||
self.unordered = set()
|
self.unordered = set()
|
||||||
|
|
||||||
def _get_matches_for_ref(self):
|
def _get_matches_for_ref(self):
|
||||||
if self._matches_for_ref is None:
|
if self._matches_for_ref is None:
|
||||||
ref = self.ref
|
ref = self.ref
|
||||||
self._matches_for_ref = [match for match in self.matches if ref in match]
|
self._matches_for_ref = [match for match in self.matches if ref in match]
|
||||||
return self._matches_for_ref
|
return self._matches_for_ref
|
||||||
|
|
||||||
#---Public
|
#---Public
|
||||||
def add_match(self, match):
|
def add_match(self, match):
|
||||||
"""Adds ``match`` to internal match list and possibly add duplicates to the group.
|
"""Adds ``match`` to internal match list and possibly add duplicates to the group.
|
||||||
|
|
||||||
A duplicate can only be considered as such if it matches all other duplicates in the group.
|
A duplicate can only be considered as such if it matches all other duplicates in the group.
|
||||||
This method registers that pair (A, B) represented in ``match`` as possible candidates and,
|
This method registers that pair (A, B) represented in ``match`` as possible candidates and,
|
||||||
if A and/or B end up matching every other duplicates in the group, add these duplicates to
|
if A and/or B end up matching every other duplicates in the group, add these duplicates to
|
||||||
the group.
|
the group.
|
||||||
|
|
||||||
:param tuple match: pair of :class:`~core.fs.File` to add
|
:param tuple match: pair of :class:`~core.fs.File` to add
|
||||||
"""
|
"""
|
||||||
def add_candidate(item, match):
|
def add_candidate(item, match):
|
||||||
@@ -348,7 +351,7 @@ class Group:
|
|||||||
if self.unordered <= matches:
|
if self.unordered <= matches:
|
||||||
self.ordered.append(item)
|
self.ordered.append(item)
|
||||||
self.unordered.add(item)
|
self.unordered.add(item)
|
||||||
|
|
||||||
if match in self.matches:
|
if match in self.matches:
|
||||||
return
|
return
|
||||||
self.matches.add(match)
|
self.matches.add(match)
|
||||||
@@ -359,17 +362,17 @@ class Group:
|
|||||||
add_candidate(second, first)
|
add_candidate(second, first)
|
||||||
self._percentage = None
|
self._percentage = None
|
||||||
self._matches_for_ref = None
|
self._matches_for_ref = None
|
||||||
|
|
||||||
def discard_matches(self):
|
def discard_matches(self):
|
||||||
"""Remove all recorded matches that didn't result in a duplicate being added to the group.
|
"""Remove all recorded matches that didn't result in a duplicate being added to the group.
|
||||||
|
|
||||||
You can call this after the duplicate scanning process to free a bit of memory.
|
You can call this after the duplicate scanning process to free a bit of memory.
|
||||||
"""
|
"""
|
||||||
discarded = set(m for m in self.matches if not all(obj in self.unordered for obj in [m.first, m.second]))
|
discarded = set(m for m in self.matches if not all(obj in self.unordered for obj in [m.first, m.second]))
|
||||||
self.matches -= discarded
|
self.matches -= discarded
|
||||||
self.candidates = defaultdict(set)
|
self.candidates = defaultdict(set)
|
||||||
return discarded
|
return discarded
|
||||||
|
|
||||||
def get_match_of(self, item):
|
def get_match_of(self, item):
|
||||||
"""Returns the match pair between ``item`` and :attr:`ref`.
|
"""Returns the match pair between ``item`` and :attr:`ref`.
|
||||||
"""
|
"""
|
||||||
@@ -378,10 +381,10 @@ class Group:
|
|||||||
for m in self._get_matches_for_ref():
|
for m in self._get_matches_for_ref():
|
||||||
if item in m:
|
if item in m:
|
||||||
return m
|
return m
|
||||||
|
|
||||||
def prioritize(self, key_func, tie_breaker=None):
|
def prioritize(self, key_func, tie_breaker=None):
|
||||||
"""Reorders :attr:`ordered` according to ``key_func``.
|
"""Reorders :attr:`ordered` according to ``key_func``.
|
||||||
|
|
||||||
:param key_func: Key (f(x)) to be used for sorting
|
:param key_func: Key (f(x)) to be used for sorting
|
||||||
:param tie_breaker: function to be used to select the reference position in case the top
|
:param tie_breaker: function to be used to select the reference position in case the top
|
||||||
duplicates have the same key_func() result.
|
duplicates have the same key_func() result.
|
||||||
@@ -405,7 +408,7 @@ class Group:
|
|||||||
self.switch_ref(ref)
|
self.switch_ref(ref)
|
||||||
return True
|
return True
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
def remove_dupe(self, item, discard_matches=True):
|
def remove_dupe(self, item, discard_matches=True):
|
||||||
try:
|
try:
|
||||||
self.ordered.remove(item)
|
self.ordered.remove(item)
|
||||||
@@ -419,7 +422,7 @@ class Group:
|
|||||||
self._clear()
|
self._clear()
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def switch_ref(self, with_dupe):
|
def switch_ref(self, with_dupe):
|
||||||
"""Make the :attr:`ref` dupe of the group switch position with ``with_dupe``.
|
"""Make the :attr:`ref` dupe of the group switch position with ``with_dupe``.
|
||||||
"""
|
"""
|
||||||
@@ -433,9 +436,9 @@ class Group:
|
|||||||
return True
|
return True
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
dupes = property(lambda self: self[1:])
|
dupes = property(lambda self: self[1:])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def percentage(self):
|
def percentage(self):
|
||||||
if self._percentage is None:
|
if self._percentage is None:
|
||||||
@@ -445,16 +448,16 @@ class Group:
|
|||||||
else:
|
else:
|
||||||
self._percentage = 0
|
self._percentage = 0
|
||||||
return self._percentage
|
return self._percentage
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ref(self):
|
def ref(self):
|
||||||
if self:
|
if self:
|
||||||
return self[0]
|
return self[0]
|
||||||
|
|
||||||
|
|
||||||
def get_groups(matches, j=job.nulljob):
|
def get_groups(matches, j=job.nulljob):
|
||||||
"""Returns a list of :class:`Group` from ``matches``.
|
"""Returns a list of :class:`Group` from ``matches``.
|
||||||
|
|
||||||
Create groups out of match pairs in the smartest way possible.
|
Create groups out of match pairs in the smartest way possible.
|
||||||
"""
|
"""
|
||||||
matches.sort(key=lambda match: -match.percentage)
|
matches.sort(key=lambda match: -match.percentage)
|
||||||
@@ -495,7 +498,10 @@ def get_groups(matches, j=job.nulljob):
|
|||||||
matched_files = set(flatten(groups))
|
matched_files = set(flatten(groups))
|
||||||
orphan_matches = []
|
orphan_matches = []
|
||||||
for group in groups:
|
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:
|
if groups and orphan_matches:
|
||||||
groups += get_groups(orphan_matches) # no job, as it isn't supposed to take a long time
|
groups += get_groups(orphan_matches) # no job, as it isn't supposed to take a long time
|
||||||
return groups
|
return groups
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2006/09/16
|
# Created On: 2006/09/16
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
import os.path as op
|
import os.path as op
|
||||||
@@ -19,56 +19,56 @@ MAIN_TEMPLATE = """
|
|||||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
<head>
|
<head>
|
||||||
<meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
|
<meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
|
||||||
<title>dupeGuru Results</title>
|
<title>dupeGuru Results</title>
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
BODY
|
BODY
|
||||||
{
|
{
|
||||||
background-color:white;
|
background-color:white;
|
||||||
}
|
}
|
||||||
|
|
||||||
BODY,A,P,UL,TABLE,TR,TD
|
BODY,A,P,UL,TABLE,TR,TD
|
||||||
{
|
{
|
||||||
font-family:Tahoma,Arial,sans-serif;
|
font-family:Tahoma,Arial,sans-serif;
|
||||||
font-size:10pt;
|
font-size:10pt;
|
||||||
color: #4477AA;
|
color: #4477AA;
|
||||||
}
|
}
|
||||||
|
|
||||||
TABLE
|
TABLE
|
||||||
{
|
{
|
||||||
background-color: #225588;
|
background-color: #225588;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
}
|
}
|
||||||
|
|
||||||
TR
|
TR
|
||||||
{
|
{
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
TH
|
TH
|
||||||
{
|
{
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: black;
|
color: black;
|
||||||
background-color: #C8D6E5;
|
background-color: #C8D6E5;
|
||||||
}
|
}
|
||||||
|
|
||||||
TH TD
|
TH TD
|
||||||
{
|
{
|
||||||
color:black;
|
color:black;
|
||||||
}
|
}
|
||||||
|
|
||||||
TD
|
TD
|
||||||
{
|
{
|
||||||
padding-left: 2pt;
|
padding-left: 2pt;
|
||||||
}
|
}
|
||||||
|
|
||||||
TD.rightelem
|
TD.rightelem
|
||||||
{
|
{
|
||||||
text-align:right;
|
text-align:right;
|
||||||
/*padding-left:0pt;*/
|
/*padding-left:0pt;*/
|
||||||
padding-right: 2pt;
|
padding-right: 2pt;
|
||||||
width: 17%;
|
width: 17%;
|
||||||
}
|
}
|
||||||
|
|
||||||
TD.indented
|
TD.indented
|
||||||
@@ -78,19 +78,19 @@ TD.indented
|
|||||||
|
|
||||||
H1
|
H1
|
||||||
{
|
{
|
||||||
font-family:"Courier New",monospace;
|
font-family:"Courier New",monospace;
|
||||||
color:#6699CC;
|
color:#6699CC;
|
||||||
font-size:18pt;
|
font-size:18pt;
|
||||||
color:#6da500;
|
color:#6da500;
|
||||||
border-color: #70A0CF;
|
border-color: #70A0CF;
|
||||||
border-width: 1pt;
|
border-width: 1pt;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
margin-top: 16pt;
|
margin-top: 16pt;
|
||||||
margin-left: 5%;
|
margin-left: 5%;
|
||||||
margin-right: 5%;
|
margin-right: 5%;
|
||||||
padding-top: 2pt;
|
padding-top: 2pt;
|
||||||
padding-bottom:2pt;
|
padding-bottom:2pt;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
63
core/fs.py
@@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2009-10-22
|
# Created On: 2009-10-22
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
# This is a fork from hsfs. The reason for this fork is that hsfs has been designed for musicGuru
|
# This is a fork from hsfs. The reason for this fork is that hsfs has been designed for musicGuru
|
||||||
@@ -32,6 +32,7 @@ NOT_SET = object()
|
|||||||
|
|
||||||
class FSError(Exception):
|
class FSError(Exception):
|
||||||
cls_message = "An error has occured on '{name}' in '{parent}'"
|
cls_message = "An error has occured on '{name}' in '{parent}'"
|
||||||
|
|
||||||
def __init__(self, fsobject, parent=None):
|
def __init__(self, fsobject, parent=None):
|
||||||
message = self.cls_message
|
message = self.cls_message
|
||||||
if isinstance(fsobject, str):
|
if isinstance(fsobject, str):
|
||||||
@@ -42,7 +43,7 @@ class FSError(Exception):
|
|||||||
name = ''
|
name = ''
|
||||||
parentname = str(parent) if parent is not None else ''
|
parentname = str(parent) if parent is not None else ''
|
||||||
Exception.__init__(self, message.format(name=name, parent=parentname))
|
Exception.__init__(self, message.format(name=name, parent=parentname))
|
||||||
|
|
||||||
|
|
||||||
class AlreadyExistsError(FSError):
|
class AlreadyExistsError(FSError):
|
||||||
"The directory or file name we're trying to add already exists"
|
"The directory or file name we're trying to add already exists"
|
||||||
@@ -57,7 +58,7 @@ class InvalidDestinationError(FSError):
|
|||||||
cls_message = "'{name}' is an invalid destination for this operation."
|
cls_message = "'{name}' is an invalid destination for this operation."
|
||||||
|
|
||||||
class OperationError(FSError):
|
class OperationError(FSError):
|
||||||
"""A copy/move/delete operation has been called, but the checkup after the
|
"""A copy/move/delete operation has been called, but the checkup after the
|
||||||
operation shows that it didn't work."""
|
operation shows that it didn't work."""
|
||||||
cls_message = "Operation on '{name}' failed."
|
cls_message = "Operation on '{name}' failed."
|
||||||
|
|
||||||
@@ -74,15 +75,15 @@ class File:
|
|||||||
# files, I saved 35% memory usage with "unread" files (no _read_info() call) and gains become
|
# files, I saved 35% memory usage with "unread" files (no _read_info() call) and gains become
|
||||||
# even greater when we take into account read attributes (70%!). Yeah, it's worth it.
|
# even greater when we take into account read attributes (70%!). Yeah, it's worth it.
|
||||||
__slots__ = ('path', 'is_ref', 'words') + tuple(INITIAL_INFO.keys())
|
__slots__ = ('path', 'is_ref', 'words') + tuple(INITIAL_INFO.keys())
|
||||||
|
|
||||||
def __init__(self, path):
|
def __init__(self, path):
|
||||||
self.path = path
|
self.path = path
|
||||||
for attrname in self.INITIAL_INFO:
|
for attrname in self.INITIAL_INFO:
|
||||||
setattr(self, attrname, NOT_SET)
|
setattr(self, attrname, NOT_SET)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<{} {}>".format(self.__class__.__name__, str(self.path))
|
return "<{} {}>".format(self.__class__.__name__, str(self.path))
|
||||||
|
|
||||||
def __getattribute__(self, attrname):
|
def __getattribute__(self, attrname):
|
||||||
result = object.__getattribute__(self, attrname)
|
result = object.__getattribute__(self, attrname)
|
||||||
if result is NOT_SET:
|
if result is NOT_SET:
|
||||||
@@ -94,12 +95,12 @@ class File:
|
|||||||
if result is NOT_SET:
|
if result is NOT_SET:
|
||||||
result = self.INITIAL_INFO[attrname]
|
result = self.INITIAL_INFO[attrname]
|
||||||
return result
|
return result
|
||||||
|
|
||||||
#This offset is where we should start reading the file to get a partial md5
|
#This offset is where we should start reading the file to get a partial md5
|
||||||
#For audio file, it should be where audio data starts
|
#For audio file, it should be where audio data starts
|
||||||
def _get_md5partial_offset_and_size(self):
|
def _get_md5partial_offset_and_size(self):
|
||||||
return (0x4000, 0x4000) #16Kb
|
return (0x4000, 0x4000) #16Kb
|
||||||
|
|
||||||
def _read_info(self, field):
|
def _read_info(self, field):
|
||||||
if field in ('size', 'mtime'):
|
if field in ('size', 'mtime'):
|
||||||
stats = self.path.stat()
|
stats = self.path.stat()
|
||||||
@@ -129,24 +130,24 @@ class File:
|
|||||||
fp.close()
|
fp.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _read_all_info(self, attrnames=None):
|
def _read_all_info(self, attrnames=None):
|
||||||
"""Cache all possible info.
|
"""Cache all possible info.
|
||||||
|
|
||||||
If `attrnames` is not None, caches only attrnames.
|
If `attrnames` is not None, caches only attrnames.
|
||||||
"""
|
"""
|
||||||
if attrnames is None:
|
if attrnames is None:
|
||||||
attrnames = self.INITIAL_INFO.keys()
|
attrnames = self.INITIAL_INFO.keys()
|
||||||
for attrname in attrnames:
|
for attrname in attrnames:
|
||||||
getattr(self, attrname)
|
getattr(self, attrname)
|
||||||
|
|
||||||
#--- Public
|
#--- Public
|
||||||
@classmethod
|
@classmethod
|
||||||
def can_handle(cls, path):
|
def can_handle(cls, path):
|
||||||
"""Returns whether this file wrapper class can handle ``path``.
|
"""Returns whether this file wrapper class can handle ``path``.
|
||||||
"""
|
"""
|
||||||
return not path.islink() and path.isfile()
|
return not path.islink() and path.isfile()
|
||||||
|
|
||||||
def rename(self, newname):
|
def rename(self, newname):
|
||||||
if newname == self.name:
|
if newname == self.name:
|
||||||
return
|
return
|
||||||
@@ -160,42 +161,42 @@ class File:
|
|||||||
if not destpath.exists():
|
if not destpath.exists():
|
||||||
raise OperationError(self)
|
raise OperationError(self)
|
||||||
self.path = destpath
|
self.path = destpath
|
||||||
|
|
||||||
def get_display_info(self, group, delta):
|
def get_display_info(self, group, delta):
|
||||||
"""Returns a display-ready dict of dupe's data.
|
"""Returns a display-ready dict of dupe's data.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
#--- Properties
|
#--- Properties
|
||||||
@property
|
@property
|
||||||
def extension(self):
|
def extension(self):
|
||||||
return get_file_ext(self.name)
|
return get_file_ext(self.name)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
return self.path.name
|
return self.path.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def folder_path(self):
|
def folder_path(self):
|
||||||
return self.path.parent()
|
return self.path.parent()
|
||||||
|
|
||||||
|
|
||||||
class Folder(File):
|
class Folder(File):
|
||||||
"""A wrapper around a folder path.
|
"""A wrapper around a folder path.
|
||||||
|
|
||||||
It has the size/md5 info of a File, but it's value are the sum of its subitems.
|
It has the size/md5 info of a File, but it's value are the sum of its subitems.
|
||||||
"""
|
"""
|
||||||
__slots__ = File.__slots__ + ('_subfolders', )
|
__slots__ = File.__slots__ + ('_subfolders', )
|
||||||
|
|
||||||
def __init__(self, path):
|
def __init__(self, path):
|
||||||
File.__init__(self, path)
|
File.__init__(self, path)
|
||||||
self._subfolders = None
|
self._subfolders = None
|
||||||
|
|
||||||
def _all_items(self):
|
def _all_items(self):
|
||||||
folders = self.subfolders
|
folders = self.subfolders
|
||||||
files = get_files(self.path)
|
files = get_files(self.path)
|
||||||
return folders + files
|
return folders + files
|
||||||
|
|
||||||
def _read_info(self, field):
|
def _read_info(self, field):
|
||||||
if field in {'size', 'mtime'}:
|
if field in {'size', 'mtime'}:
|
||||||
size = sum((f.size for f in self._all_items()), 0)
|
size = sum((f.size for f in self._all_items()), 0)
|
||||||
@@ -208,31 +209,31 @@ class Folder(File):
|
|||||||
# different md5 if a file gets moved in a different subdirectory.
|
# different md5 if a file gets moved in a different subdirectory.
|
||||||
def get_dir_md5_concat():
|
def get_dir_md5_concat():
|
||||||
items = self._all_items()
|
items = self._all_items()
|
||||||
items.sort(key=lambda f:f.path)
|
items.sort(key=lambda f: f.path)
|
||||||
md5s = [getattr(f, field) for f in items]
|
md5s = [getattr(f, field) for f in items]
|
||||||
return b''.join(md5s)
|
return b''.join(md5s)
|
||||||
|
|
||||||
md5 = hashlib.md5(get_dir_md5_concat())
|
md5 = hashlib.md5(get_dir_md5_concat())
|
||||||
digest = md5.digest()
|
digest = md5.digest()
|
||||||
setattr(self, field, digest)
|
setattr(self, field, digest)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def subfolders(self):
|
def subfolders(self):
|
||||||
if self._subfolders is None:
|
if self._subfolders is None:
|
||||||
subfolders = [p for p in self.path.listdir() if not p.islink() and p.isdir()]
|
subfolders = [p for p in self.path.listdir() if not p.islink() and p.isdir()]
|
||||||
self._subfolders = [self.__class__(p) for p in subfolders]
|
self._subfolders = [self.__class__(p) for p in subfolders]
|
||||||
return self._subfolders
|
return self._subfolders
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def can_handle(cls, path):
|
def can_handle(cls, path):
|
||||||
return not path.islink() and path.isdir()
|
return not path.islink() and path.isdir()
|
||||||
|
|
||||||
|
|
||||||
def get_file(path, fileclasses=[File]):
|
def get_file(path, fileclasses=[File]):
|
||||||
"""Wraps ``path`` around its appropriate :class:`File` class.
|
"""Wraps ``path`` around its appropriate :class:`File` class.
|
||||||
|
|
||||||
Whether a class is "appropriate" is decided by :meth:`File.can_handle`
|
Whether a class is "appropriate" is decided by :meth:`File.can_handle`
|
||||||
|
|
||||||
:param Path path: path to wrap
|
:param Path path: path to wrap
|
||||||
:param fileclasses: List of candidate :class:`File` classes
|
:param fileclasses: List of candidate :class:`File` classes
|
||||||
"""
|
"""
|
||||||
@@ -242,7 +243,7 @@ def get_file(path, fileclasses=[File]):
|
|||||||
|
|
||||||
def get_files(path, fileclasses=[File]):
|
def get_files(path, fileclasses=[File]):
|
||||||
"""Returns a list of :class:`File` for each file contained in ``path``.
|
"""Returns a list of :class:`File` for each file contained in ``path``.
|
||||||
|
|
||||||
:param Path path: path to scan
|
:param Path path: path to scan
|
||||||
:param fileclasses: List of candidate :class:`File` classes
|
:param fileclasses: List of candidate :class:`File` classes
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -12,4 +12,5 @@ either Cocoa's ``NSTableView`` or Qt's ``QTableView``. It tells them which cell
|
|||||||
blue, which is supposed to be orange, does the sorting logic, holds selection, etc..
|
blue, which is supposed to be orange, does the sorting logic, holds selection, etc..
|
||||||
|
|
||||||
.. _cross-toolkit: http://www.hardcoded.net/articles/cross-toolkit-software
|
.. _cross-toolkit: http://www.hardcoded.net/articles/cross-toolkit-software
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +1,30 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2010-02-06
|
# Created On: 2010-02-06
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from hscommon.notify import Listener
|
from hscommon.notify import Listener
|
||||||
from hscommon.gui.base import NoopGUI
|
|
||||||
|
|
||||||
class DupeGuruGUIObject(Listener):
|
class DupeGuruGUIObject(Listener):
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
Listener.__init__(self, app)
|
Listener.__init__(self, app)
|
||||||
self.app = app
|
self.app = app
|
||||||
|
|
||||||
def directories_changed(self):
|
def directories_changed(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def dupes_selected(self):
|
def dupes_selected(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def marking_changed(self):
|
def marking_changed(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def results_changed(self):
|
def results_changed(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def results_changed_but_keep_selection(self):
|
def results_changed_but_keep_selection(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2011-09-06
|
# Created On: 2011-09-06
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from hscommon.gui.base import GUIObject
|
from hscommon.gui.base import GUIObject
|
||||||
@@ -13,7 +13,7 @@ class CriterionCategoryList(GUISelectableList):
|
|||||||
def __init__(self, dialog):
|
def __init__(self, dialog):
|
||||||
self.dialog = dialog
|
self.dialog = dialog
|
||||||
GUISelectableList.__init__(self, [c.NAME for c in dialog.categories])
|
GUISelectableList.__init__(self, [c.NAME for c in dialog.categories])
|
||||||
|
|
||||||
def _update_selection(self):
|
def _update_selection(self):
|
||||||
self.dialog.select_category(self.dialog.categories[self.selected_index])
|
self.dialog.select_category(self.dialog.categories[self.selected_index])
|
||||||
GUISelectableList._update_selection(self)
|
GUISelectableList._update_selection(self)
|
||||||
@@ -22,10 +22,10 @@ class PrioritizationList(GUISelectableList):
|
|||||||
def __init__(self, dialog):
|
def __init__(self, dialog):
|
||||||
self.dialog = dialog
|
self.dialog = dialog
|
||||||
GUISelectableList.__init__(self)
|
GUISelectableList.__init__(self)
|
||||||
|
|
||||||
def _refresh_contents(self):
|
def _refresh_contents(self):
|
||||||
self[:] = [crit.display for crit in self.dialog.prioritizations]
|
self[:] = [crit.display for crit in self.dialog.prioritizations]
|
||||||
|
|
||||||
def move_indexes(self, indexes, dest_index):
|
def move_indexes(self, indexes, dest_index):
|
||||||
indexes.sort()
|
indexes.sort()
|
||||||
prilist = self.dialog.prioritizations
|
prilist = self.dialog.prioritizations
|
||||||
@@ -34,7 +34,7 @@ class PrioritizationList(GUISelectableList):
|
|||||||
del prilist[i]
|
del prilist[i]
|
||||||
prilist[dest_index:dest_index] = selected
|
prilist[dest_index:dest_index] = selected
|
||||||
self._refresh_contents()
|
self._refresh_contents()
|
||||||
|
|
||||||
def remove_selected(self):
|
def remove_selected(self):
|
||||||
prilist = self.dialog.prioritizations
|
prilist = self.dialog.prioritizations
|
||||||
for i in sorted(self.selected_indexes, reverse=True):
|
for i in sorted(self.selected_indexes, reverse=True):
|
||||||
@@ -51,15 +51,15 @@ class PrioritizeDialog(GUIObject):
|
|||||||
self.criteria_list = GUISelectableList()
|
self.criteria_list = GUISelectableList()
|
||||||
self.prioritizations = []
|
self.prioritizations = []
|
||||||
self.prioritization_list = PrioritizationList(self)
|
self.prioritization_list = PrioritizationList(self)
|
||||||
|
|
||||||
#--- Override
|
#--- Override
|
||||||
def _view_updated(self):
|
def _view_updated(self):
|
||||||
self.category_list.select(0)
|
self.category_list.select(0)
|
||||||
|
|
||||||
#--- Private
|
#--- Private
|
||||||
def _sort_key(self, dupe):
|
def _sort_key(self, dupe):
|
||||||
return tuple(crit.sort_key(dupe) for crit in self.prioritizations)
|
return tuple(crit.sort_key(dupe) for crit in self.prioritizations)
|
||||||
|
|
||||||
#--- Public
|
#--- Public
|
||||||
def select_category(self, category):
|
def select_category(self, category):
|
||||||
self.criteria = category.criteria_list()
|
self.criteria = category.criteria_list()
|
||||||
@@ -71,10 +71,11 @@ class PrioritizeDialog(GUIObject):
|
|||||||
return
|
return
|
||||||
crit = self.criteria[self.criteria_list.selected_index]
|
crit = self.criteria[self.criteria_list.selected_index]
|
||||||
self.prioritizations.append(crit)
|
self.prioritizations.append(crit)
|
||||||
|
del crit
|
||||||
self.prioritization_list[:] = [crit.display for crit in self.prioritizations]
|
self.prioritization_list[:] = [crit.display for crit in self.prioritizations]
|
||||||
|
|
||||||
def remove_selected(self):
|
def remove_selected(self):
|
||||||
self.prioritization_list.remove_selected()
|
self.prioritization_list.remove_selected()
|
||||||
|
|
||||||
def perform_reprioritization(self):
|
def perform_reprioritization(self):
|
||||||
self.app.reprioritize_groups(self._sort_key)
|
self.app.reprioritize_groups(self._sort_key)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2006/05/02
|
# Created On: 2006/05/02
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from xml.etree import ElementTree as ET
|
from xml.etree import ElementTree as ET
|
||||||
@@ -12,7 +12,7 @@ from hscommon.util import FileOrPath
|
|||||||
|
|
||||||
class IgnoreList:
|
class IgnoreList:
|
||||||
"""An ignore list implementation that is iterable, filterable and exportable to XML.
|
"""An ignore list implementation that is iterable, filterable and exportable to XML.
|
||||||
|
|
||||||
Call Ignore to add an ignore list entry, and AreIgnore to check if 2 items are in the list.
|
Call Ignore to add an ignore list entry, and AreIgnore to check if 2 items are in the list.
|
||||||
When iterated, 2 sized tuples will be returned, the tuples containing 2 items ignored together.
|
When iterated, 2 sized tuples will be returned, the tuples containing 2 items ignored together.
|
||||||
"""
|
"""
|
||||||
@@ -20,43 +20,43 @@ class IgnoreList:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._ignored = {}
|
self._ignored = {}
|
||||||
self._count = 0
|
self._count = 0
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
for first,seconds in self._ignored.items():
|
for first, seconds in self._ignored.items():
|
||||||
for second in seconds:
|
for second in seconds:
|
||||||
yield (first,second)
|
yield (first, second)
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
return self._count
|
return self._count
|
||||||
|
|
||||||
#---Public
|
#---Public
|
||||||
def AreIgnored(self,first,second):
|
def AreIgnored(self, first, second):
|
||||||
def do_check(first,second):
|
def do_check(first, second):
|
||||||
try:
|
try:
|
||||||
matches = self._ignored[first]
|
matches = self._ignored[first]
|
||||||
return second in matches
|
return second in matches
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return do_check(first,second) or do_check(second,first)
|
return do_check(first, second) or do_check(second, first)
|
||||||
|
|
||||||
def Clear(self):
|
def Clear(self):
|
||||||
self._ignored = {}
|
self._ignored = {}
|
||||||
self._count = 0
|
self._count = 0
|
||||||
|
|
||||||
def Filter(self,func):
|
def Filter(self, func):
|
||||||
"""Applies a filter on all ignored items, and remove all matches where func(first,second)
|
"""Applies a filter on all ignored items, and remove all matches where func(first,second)
|
||||||
doesn't return True.
|
doesn't return True.
|
||||||
"""
|
"""
|
||||||
filtered = IgnoreList()
|
filtered = IgnoreList()
|
||||||
for first,second in self:
|
for first, second in self:
|
||||||
if func(first,second):
|
if func(first, second):
|
||||||
filtered.Ignore(first,second)
|
filtered.Ignore(first, second)
|
||||||
self._ignored = filtered._ignored
|
self._ignored = filtered._ignored
|
||||||
self._count = filtered._count
|
self._count = filtered._count
|
||||||
|
|
||||||
def Ignore(self,first,second):
|
def Ignore(self, first, second):
|
||||||
if self.AreIgnored(first,second):
|
if self.AreIgnored(first, second):
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
matches = self._ignored[first]
|
matches = self._ignored[first]
|
||||||
@@ -70,7 +70,7 @@ class IgnoreList:
|
|||||||
matches.add(second)
|
matches.add(second)
|
||||||
self._ignored[first] = matches
|
self._ignored[first] = matches
|
||||||
self._count += 1
|
self._count += 1
|
||||||
|
|
||||||
def remove(self, first, second):
|
def remove(self, first, second):
|
||||||
def inner(first, second):
|
def inner(first, second):
|
||||||
try:
|
try:
|
||||||
@@ -85,14 +85,14 @@ class IgnoreList:
|
|||||||
return False
|
return False
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not inner(first, second):
|
if not inner(first, second):
|
||||||
if not inner(second, first):
|
if not inner(second, first):
|
||||||
raise ValueError()
|
raise ValueError()
|
||||||
|
|
||||||
def load_from_xml(self, infile):
|
def load_from_xml(self, infile):
|
||||||
"""Loads the ignore list from a XML created with save_to_xml.
|
"""Loads the ignore list from a XML created with save_to_xml.
|
||||||
|
|
||||||
infile can be a file object or a filename.
|
infile can be a file object or a filename.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
@@ -109,10 +109,10 @@ class IgnoreList:
|
|||||||
subfile_path = sfn.get('path')
|
subfile_path = sfn.get('path')
|
||||||
if subfile_path:
|
if subfile_path:
|
||||||
self.Ignore(file_path, subfile_path)
|
self.Ignore(file_path, subfile_path)
|
||||||
|
|
||||||
def save_to_xml(self, outfile):
|
def save_to_xml(self, outfile):
|
||||||
"""Create a XML file that can be used by load_from_xml.
|
"""Create a XML file that can be used by load_from_xml.
|
||||||
|
|
||||||
outfile can be a file object or a filename.
|
outfile can be a file object or a filename.
|
||||||
"""
|
"""
|
||||||
root = ET.Element('ignore_list')
|
root = ET.Element('ignore_list')
|
||||||
@@ -125,5 +125,5 @@ class IgnoreList:
|
|||||||
tree = ET.ElementTree(root)
|
tree = ET.ElementTree(root)
|
||||||
with FileOrPath(outfile, 'wb') as fp:
|
with FileOrPath(outfile, 'wb') as fp:
|
||||||
tree.write(fp, encoding='utf-8')
|
tree.write(fp, encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
115
core/results.py
@@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2006/02/23
|
# Created On: 2006/02/23
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -12,7 +12,7 @@ import os
|
|||||||
import os.path as op
|
import os.path as op
|
||||||
from xml.etree import ElementTree as ET
|
from xml.etree import ElementTree as ET
|
||||||
|
|
||||||
from jobprogress.job import nulljob
|
from hscommon.jobprogress.job import nulljob
|
||||||
from hscommon.conflict import get_conflicted_name
|
from hscommon.conflict import get_conflicted_name
|
||||||
from hscommon.util import flatten, nonone, FileOrPath, format_size
|
from hscommon.util import flatten, nonone, FileOrPath, format_size
|
||||||
from hscommon.trans import tr
|
from hscommon.trans import tr
|
||||||
@@ -22,15 +22,15 @@ from .markable import Markable
|
|||||||
|
|
||||||
class Results(Markable):
|
class Results(Markable):
|
||||||
"""Manages a collection of duplicate :class:`~core.engine.Group`.
|
"""Manages a collection of duplicate :class:`~core.engine.Group`.
|
||||||
|
|
||||||
This class takes care or marking, sorting and filtering duplicate groups.
|
This class takes care or marking, sorting and filtering duplicate groups.
|
||||||
|
|
||||||
.. attribute:: groups
|
.. attribute:: groups
|
||||||
|
|
||||||
The list of :class:`~core.engine.Group` contained managed by this instance.
|
The list of :class:`~core.engine.Group` contained managed by this instance.
|
||||||
|
|
||||||
.. attribute:: dupes
|
.. attribute:: dupes
|
||||||
|
|
||||||
A list of all duplicates (:class:`~core.fs.File` instances), without ref, contained in the
|
A list of all duplicates (:class:`~core.fs.File` instances), without ref, contained in the
|
||||||
currently managed :attr:`groups`.
|
currently managed :attr:`groups`.
|
||||||
"""
|
"""
|
||||||
@@ -50,16 +50,16 @@ class Results(Markable):
|
|||||||
self.app = app
|
self.app = app
|
||||||
self.problems = [] # (dupe, error_msg)
|
self.problems = [] # (dupe, error_msg)
|
||||||
self.is_modified = False
|
self.is_modified = False
|
||||||
|
|
||||||
def _did_mark(self, dupe):
|
def _did_mark(self, dupe):
|
||||||
self.__marked_size += dupe.size
|
self.__marked_size += dupe.size
|
||||||
|
|
||||||
def _did_unmark(self, dupe):
|
def _did_unmark(self, dupe):
|
||||||
self.__marked_size -= dupe.size
|
self.__marked_size -= dupe.size
|
||||||
|
|
||||||
def _get_markable_count(self):
|
def _get_markable_count(self):
|
||||||
return self.__total_count
|
return self.__total_count
|
||||||
|
|
||||||
def _is_markable(self, dupe):
|
def _is_markable(self, dupe):
|
||||||
if dupe.is_ref:
|
if dupe.is_ref:
|
||||||
return False
|
return False
|
||||||
@@ -71,45 +71,48 @@ class Results(Markable):
|
|||||||
if self.__filtered_dupes and dupe not in self.__filtered_dupes:
|
if self.__filtered_dupes and dupe not in self.__filtered_dupes:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def mark_all(self):
|
def mark_all(self):
|
||||||
if self.__filters:
|
if self.__filters:
|
||||||
self.mark_multiple(self.__filtered_dupes)
|
self.mark_multiple(self.__filtered_dupes)
|
||||||
else:
|
else:
|
||||||
Markable.mark_all(self)
|
Markable.mark_all(self)
|
||||||
|
|
||||||
def mark_invert(self):
|
def mark_invert(self):
|
||||||
if self.__filters:
|
if self.__filters:
|
||||||
self.mark_toggle_multiple(self.__filtered_dupes)
|
self.mark_toggle_multiple(self.__filtered_dupes)
|
||||||
else:
|
else:
|
||||||
Markable.mark_invert(self)
|
Markable.mark_invert(self)
|
||||||
|
|
||||||
def mark_none(self):
|
def mark_none(self):
|
||||||
if self.__filters:
|
if self.__filters:
|
||||||
self.unmark_multiple(self.__filtered_dupes)
|
self.unmark_multiple(self.__filtered_dupes)
|
||||||
else:
|
else:
|
||||||
Markable.mark_none(self)
|
Markable.mark_none(self)
|
||||||
|
|
||||||
#---Private
|
#---Private
|
||||||
def __get_dupe_list(self):
|
def __get_dupe_list(self):
|
||||||
if self.__dupes is None:
|
if self.__dupes is None:
|
||||||
self.__dupes = flatten(group.dupes for group in self.groups)
|
self.__dupes = flatten(group.dupes for group in self.groups)
|
||||||
if None in self.__dupes:
|
if None in self.__dupes:
|
||||||
# This is debug logging to try to figure out #44
|
# 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:
|
if self.__filtered_dupes:
|
||||||
self.__dupes = [dupe for dupe in self.__dupes if dupe in self.__filtered_dupes]
|
self.__dupes = [dupe for dupe in self.__dupes if dupe in self.__filtered_dupes]
|
||||||
sd = self.__dupes_sort_descriptor
|
sd = self.__dupes_sort_descriptor
|
||||||
if sd:
|
if sd:
|
||||||
self.sort_dupes(sd[0], sd[1], sd[2])
|
self.sort_dupes(sd[0], sd[1], sd[2])
|
||||||
return self.__dupes
|
return self.__dupes
|
||||||
|
|
||||||
def __get_groups(self):
|
def __get_groups(self):
|
||||||
if self.__filtered_groups is None:
|
if self.__filtered_groups is None:
|
||||||
return self.__groups
|
return self.__groups
|
||||||
else:
|
else:
|
||||||
return self.__filtered_groups
|
return self.__filtered_groups
|
||||||
|
|
||||||
def __get_stat_line(self):
|
def __get_stat_line(self):
|
||||||
if self.__filtered_dupes is None:
|
if self.__filtered_dupes is None:
|
||||||
mark_count = self.mark_count
|
mark_count = self.mark_count
|
||||||
@@ -132,7 +135,7 @@ class Results(Markable):
|
|||||||
if self.__filters:
|
if self.__filters:
|
||||||
result += tr(" filter: %s") % ' --> '.join(self.__filters)
|
result += tr(" filter: %s") % ' --> '.join(self.__filters)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def __recalculate_stats(self):
|
def __recalculate_stats(self):
|
||||||
self.__total_size = 0
|
self.__total_size = 0
|
||||||
self.__total_count = 0
|
self.__total_count = 0
|
||||||
@@ -140,7 +143,7 @@ class Results(Markable):
|
|||||||
markable = [dupe for dupe in group.dupes if self._is_markable(dupe)]
|
markable = [dupe for dupe in group.dupes if self._is_markable(dupe)]
|
||||||
self.__total_count += len(markable)
|
self.__total_count += len(markable)
|
||||||
self.__total_size += sum(dupe.size for dupe in markable)
|
self.__total_size += sum(dupe.size for dupe in markable)
|
||||||
|
|
||||||
def __set_groups(self, new_groups):
|
def __set_groups(self, new_groups):
|
||||||
self.mark_none()
|
self.mark_none()
|
||||||
self.__groups = new_groups
|
self.__groups = new_groups
|
||||||
@@ -155,18 +158,18 @@ class Results(Markable):
|
|||||||
self.apply_filter(None)
|
self.apply_filter(None)
|
||||||
for filter_str in old_filters:
|
for filter_str in old_filters:
|
||||||
self.apply_filter(filter_str)
|
self.apply_filter(filter_str)
|
||||||
|
|
||||||
#---Public
|
#---Public
|
||||||
def apply_filter(self, filter_str):
|
def apply_filter(self, filter_str):
|
||||||
"""Applies a filter ``filter_str`` to :attr:`groups`
|
"""Applies a filter ``filter_str`` to :attr:`groups`
|
||||||
|
|
||||||
When you apply the filter, only dupes with the filename matching ``filter_str`` will be in
|
When you apply the filter, only dupes with the filename matching ``filter_str`` will be in
|
||||||
in the results. To cancel the filter, just call apply_filter with ``filter_str`` to None,
|
in the results. To cancel the filter, just call apply_filter with ``filter_str`` to None,
|
||||||
and the results will go back to normal.
|
and the results will go back to normal.
|
||||||
|
|
||||||
If call apply_filter on a filtered results, the filter will be applied
|
If call apply_filter on a filtered results, the filter will be applied
|
||||||
*on the filtered results*.
|
*on the filtered results*.
|
||||||
|
|
||||||
:param str filter_str: a string containing a regexp to filter dupes with.
|
:param str filter_str: a string containing a regexp to filter dupes with.
|
||||||
"""
|
"""
|
||||||
if not filter_str:
|
if not filter_str:
|
||||||
@@ -193,7 +196,7 @@ class Results(Markable):
|
|||||||
if sd:
|
if sd:
|
||||||
self.sort_groups(sd[0], sd[1])
|
self.sort_groups(sd[0], sd[1])
|
||||||
self.__dupes = None
|
self.__dupes = None
|
||||||
|
|
||||||
def get_group_of_duplicate(self, dupe):
|
def get_group_of_duplicate(self, dupe):
|
||||||
"""Returns :class:`~core.engine.Group` in which ``dupe`` belongs.
|
"""Returns :class:`~core.engine.Group` in which ``dupe`` belongs.
|
||||||
"""
|
"""
|
||||||
@@ -201,12 +204,12 @@ class Results(Markable):
|
|||||||
return self.__group_of_duplicate[dupe]
|
return self.__group_of_duplicate[dupe]
|
||||||
except (TypeError, KeyError):
|
except (TypeError, KeyError):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
is_markable = _is_markable
|
is_markable = _is_markable
|
||||||
|
|
||||||
def load_from_xml(self, infile, get_file, j=nulljob):
|
def load_from_xml(self, infile, get_file, j=nulljob):
|
||||||
"""Load results from ``infile``.
|
"""Load results from ``infile``.
|
||||||
|
|
||||||
:param infile: a file or path pointing to an XML file created with :meth:`save_to_xml`.
|
:param infile: a file or path pointing to an XML file created with :meth:`save_to_xml`.
|
||||||
:param get_file: a function f(path) returning a :class:`~core.fs.File` wrapping the path.
|
:param get_file: a function f(path) returning a :class:`~core.fs.File` wrapping the path.
|
||||||
:param j: A :ref:`job progress instance <jobs>`.
|
:param j: A :ref:`job progress instance <jobs>`.
|
||||||
@@ -217,7 +220,7 @@ class Results(Markable):
|
|||||||
for other_file in other_files:
|
for other_file in other_files:
|
||||||
group.add_match(engine.get_match(ref_file, other_file))
|
group.add_match(engine.get_match(ref_file, other_file))
|
||||||
do_match(other_files[0], other_files[1:], group)
|
do_match(other_files[0], other_files[1:], group)
|
||||||
|
|
||||||
self.apply_filter(None)
|
self.apply_filter(None)
|
||||||
try:
|
try:
|
||||||
root = ET.parse(infile).getroot()
|
root = ET.parse(infile).getroot()
|
||||||
@@ -249,19 +252,20 @@ class Results(Markable):
|
|||||||
second_file = dupes[int(attrs['second'])]
|
second_file = dupes[int(attrs['second'])]
|
||||||
percentage = int(attrs['percentage'])
|
percentage = int(attrs['percentage'])
|
||||||
group.add_match(engine.Match(first_file, second_file, 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
|
pass
|
||||||
if (not group.matches) and (len(dupes) >= 2):
|
if (not group.matches) and (len(dupes) >= 2):
|
||||||
do_match(dupes[0], dupes[1:], group)
|
do_match(dupes[0], dupes[1:], group)
|
||||||
group.prioritize(lambda x: dupes.index(x))
|
group.prioritize(lambda x: dupes.index(x))
|
||||||
if len(group):
|
if len(group):
|
||||||
groups.append(group)
|
groups.append(group)
|
||||||
j.add_progress()
|
j.add_progress()
|
||||||
self.groups = groups
|
self.groups = groups
|
||||||
for dupe_file in marked:
|
for dupe_file in marked:
|
||||||
self.mark(dupe_file)
|
self.mark(dupe_file)
|
||||||
self.is_modified = False
|
self.is_modified = False
|
||||||
|
|
||||||
def make_ref(self, dupe):
|
def make_ref(self, dupe):
|
||||||
"""Make ``dupe`` take the :attr:`~core.engine.Group.ref` position of its group.
|
"""Make ``dupe`` take the :attr:`~core.engine.Group.ref` position of its group.
|
||||||
"""
|
"""
|
||||||
@@ -279,13 +283,13 @@ class Results(Markable):
|
|||||||
self.__dupes = None
|
self.__dupes = None
|
||||||
self.is_modified = True
|
self.is_modified = True
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def perform_on_marked(self, func, remove_from_results):
|
def perform_on_marked(self, func, remove_from_results):
|
||||||
"""Performs ``func`` on all marked dupes.
|
"""Performs ``func`` on all marked dupes.
|
||||||
|
|
||||||
If an ``EnvironmentError`` is raised during the call, the problematic dupe is added to
|
If an ``EnvironmentError`` is raised during the call, the problematic dupe is added to
|
||||||
self.problems.
|
self.problems.
|
||||||
|
|
||||||
:param bool remove_from_results: If true, dupes which had ``func`` applied and didn't cause
|
:param bool remove_from_results: If true, dupes which had ``func`` applied and didn't cause
|
||||||
any problem.
|
any problem.
|
||||||
"""
|
"""
|
||||||
@@ -303,10 +307,10 @@ class Results(Markable):
|
|||||||
self.mark_none()
|
self.mark_none()
|
||||||
for dupe, _ in self.problems:
|
for dupe, _ in self.problems:
|
||||||
self.mark(dupe)
|
self.mark(dupe)
|
||||||
|
|
||||||
def remove_duplicates(self, dupes):
|
def remove_duplicates(self, dupes):
|
||||||
"""Remove ``dupes`` from their respective :class:`~core.engine.Group`.
|
"""Remove ``dupes`` from their respective :class:`~core.engine.Group`.
|
||||||
|
|
||||||
Also, remove the group from :attr:`groups` if it ends up empty.
|
Also, remove the group from :attr:`groups` if it ends up empty.
|
||||||
"""
|
"""
|
||||||
affected_groups = set()
|
affected_groups = set()
|
||||||
@@ -331,10 +335,10 @@ class Results(Markable):
|
|||||||
group.discard_matches()
|
group.discard_matches()
|
||||||
self.__dupes = None
|
self.__dupes = None
|
||||||
self.is_modified = bool(self.__groups)
|
self.is_modified = bool(self.__groups)
|
||||||
|
|
||||||
def save_to_xml(self, outfile):
|
def save_to_xml(self, outfile):
|
||||||
"""Save results to ``outfile`` in XML.
|
"""Save results to ``outfile`` in XML.
|
||||||
|
|
||||||
:param outfile: file object or path.
|
:param outfile: file object or path.
|
||||||
"""
|
"""
|
||||||
self.apply_filter(None)
|
self.apply_filter(None)
|
||||||
@@ -362,11 +366,11 @@ class Results(Markable):
|
|||||||
match_elem.set('second', str(dupe2index[match.second]))
|
match_elem.set('second', str(dupe2index[match.second]))
|
||||||
match_elem.set('percentage', str(int(match.percentage)))
|
match_elem.set('percentage', str(int(match.percentage)))
|
||||||
tree = ET.ElementTree(root)
|
tree = ET.ElementTree(root)
|
||||||
|
|
||||||
def do_write(outfile):
|
def do_write(outfile):
|
||||||
with FileOrPath(outfile, 'wb') as fp:
|
with FileOrPath(outfile, 'wb') as fp:
|
||||||
tree.write(fp, encoding='utf-8')
|
tree.write(fp, encoding='utf-8')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
do_write(outfile)
|
do_write(outfile)
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
@@ -381,10 +385,10 @@ class Results(Markable):
|
|||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
self.is_modified = False
|
self.is_modified = False
|
||||||
|
|
||||||
def sort_dupes(self, key, asc=True, delta=False):
|
def sort_dupes(self, key, asc=True, delta=False):
|
||||||
"""Sort :attr:`dupes` according to ``key``.
|
"""Sort :attr:`dupes` according to ``key``.
|
||||||
|
|
||||||
:param str key: key attribute name to sort with.
|
:param str key: key attribute name to sort with.
|
||||||
:param bool asc: If false, sorting is reversed.
|
:param bool asc: If false, sorting is reversed.
|
||||||
:param bool delta: If true, sorting occurs using :ref:`delta values <deltavalues>`.
|
:param bool delta: If true, sorting occurs using :ref:`delta values <deltavalues>`.
|
||||||
@@ -393,21 +397,22 @@ class Results(Markable):
|
|||||||
self.__get_dupe_list()
|
self.__get_dupe_list()
|
||||||
keyfunc = lambda d: self.app._get_dupe_sort_key(d, lambda: self.get_group_of_duplicate(d), key, delta)
|
keyfunc = lambda d: self.app._get_dupe_sort_key(d, lambda: self.get_group_of_duplicate(d), key, delta)
|
||||||
self.__dupes.sort(key=keyfunc, reverse=not asc)
|
self.__dupes.sort(key=keyfunc, reverse=not asc)
|
||||||
self.__dupes_sort_descriptor = (key,asc,delta)
|
self.__dupes_sort_descriptor = (key, asc, delta)
|
||||||
|
|
||||||
def sort_groups(self, key, asc=True):
|
def sort_groups(self, key, asc=True):
|
||||||
"""Sort :attr:`groups` according to ``key``.
|
"""Sort :attr:`groups` according to ``key``.
|
||||||
|
|
||||||
The :attr:`~core.engine.Group.ref` of each group is used to extract values for sorting.
|
The :attr:`~core.engine.Group.ref` of each group is used to extract values for sorting.
|
||||||
|
|
||||||
:param str key: key attribute name to sort with.
|
:param str key: key attribute name to sort with.
|
||||||
:param bool asc: If false, sorting is reversed.
|
:param bool asc: If false, sorting is reversed.
|
||||||
"""
|
"""
|
||||||
keyfunc = lambda g: self.app._get_group_sort_key(g, key)
|
keyfunc = lambda g: self.app._get_group_sort_key(g, key)
|
||||||
self.groups.sort(key=keyfunc, reverse=not asc)
|
self.groups.sort(key=keyfunc, reverse=not asc)
|
||||||
self.__groups_sort_descriptor = (key,asc)
|
self.__groups_sort_descriptor = (key, asc)
|
||||||
|
|
||||||
#---Properties
|
#---Properties
|
||||||
dupes = property(__get_dupe_list)
|
dupes = property(__get_dupe_list)
|
||||||
groups = property(__get_groups, __set_groups)
|
groups = property(__get_groups, __set_groups)
|
||||||
stat_line = property(__get_stat_line)
|
stat_line = property(__get_stat_line)
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2006/03/03
|
# Created On: 2006/03/03
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import os.path as op
|
import os.path as op
|
||||||
|
|
||||||
from jobprogress import job
|
from hscommon.jobprogress import job
|
||||||
from hscommon.util import dedupe, rem_file_ext, get_file_ext
|
from hscommon.util import dedupe, rem_file_ext, get_file_ext
|
||||||
from hscommon.trans import tr
|
from hscommon.trans import tr
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ class ScanType:
|
|||||||
Folders = 4
|
Folders = 4
|
||||||
Contents = 5
|
Contents = 5
|
||||||
ContentsAudio = 6
|
ContentsAudio = 6
|
||||||
|
|
||||||
#PE
|
#PE
|
||||||
FuzzyBlock = 10
|
FuzzyBlock = 10
|
||||||
ExifTimestamp = 11
|
ExifTimestamp = 11
|
||||||
@@ -72,7 +72,7 @@ class Scanner:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.ignore_list = IgnoreList()
|
self.ignore_list = IgnoreList()
|
||||||
self.discarded_file_count = 0
|
self.discarded_file_count = 0
|
||||||
|
|
||||||
def _getmatches(self, files, j):
|
def _getmatches(self, files, j):
|
||||||
if self.size_threshold:
|
if self.size_threshold:
|
||||||
j = j.start_subjob([2, 8])
|
j = j.start_subjob([2, 8])
|
||||||
@@ -81,7 +81,9 @@ class Scanner:
|
|||||||
files = [f for f in files if f.size >= self.size_threshold]
|
files = [f for f in files if f.size >= self.size_threshold]
|
||||||
if self.scan_type in {ScanType.Contents, ScanType.ContentsAudio, ScanType.Folders}:
|
if self.scan_type in {ScanType.Contents, ScanType.ContentsAudio, ScanType.Folders}:
|
||||||
sizeattr = 'audiosize' if self.scan_type == ScanType.ContentsAudio else 'size'
|
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:
|
else:
|
||||||
j = j.start_subjob([2, 8])
|
j = j.start_subjob([2, 8])
|
||||||
kw = {}
|
kw = {}
|
||||||
@@ -94,17 +96,21 @@ class Scanner:
|
|||||||
func = {
|
func = {
|
||||||
ScanType.Filename: lambda f: engine.getwords(rem_file_ext(f.name)),
|
ScanType.Filename: lambda f: engine.getwords(rem_file_ext(f.name)),
|
||||||
ScanType.Fields: lambda f: engine.getfields(rem_file_ext(f.name)),
|
ScanType.Fields: lambda f: engine.getfields(rem_file_ext(f.name)),
|
||||||
ScanType.Tag: lambda f: [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]
|
}[self.scan_type]
|
||||||
for f in j.iter_with_progress(files, tr("Read metadata of %d/%d files")):
|
for f in j.iter_with_progress(files, tr("Read metadata of %d/%d files")):
|
||||||
logging.debug("Reading metadata of {}".format(str(f.path)))
|
logging.debug("Reading metadata of {}".format(str(f.path)))
|
||||||
f.words = func(f)
|
f.words = func(f)
|
||||||
return engine.getmatches(files, j=j, **kw)
|
return engine.getmatches(files, j=j, **kw)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _key_func(dupe):
|
def _key_func(dupe):
|
||||||
return -dupe.size
|
return -dupe.size
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _tie_breaker(ref, dupe):
|
def _tie_breaker(ref, dupe):
|
||||||
refname = rem_file_ext(ref.name).lower()
|
refname = rem_file_ext(ref.name).lower()
|
||||||
@@ -118,7 +124,7 @@ class Scanner:
|
|||||||
if is_same_with_digit(refname, dupename):
|
if is_same_with_digit(refname, dupename):
|
||||||
return True
|
return True
|
||||||
return len(dupe.path) > len(ref.path)
|
return len(dupe.path) > len(ref.path)
|
||||||
|
|
||||||
def get_dupe_groups(self, files, j=job.nulljob):
|
def get_dupe_groups(self, files, j=job.nulljob):
|
||||||
j = j.start_subjob([8, 2])
|
j = j.start_subjob([8, 2])
|
||||||
for f in (f for f in files if not hasattr(f, 'is_ref')):
|
for f in (f for f in files if not hasattr(f, 'is_ref')):
|
||||||
@@ -152,8 +158,10 @@ class Scanner:
|
|||||||
if self.ignore_list:
|
if self.ignore_list:
|
||||||
j = j.start_subjob(2)
|
j = j.start_subjob(2)
|
||||||
iter_matches = j.iter_with_progress(matches, tr("Processed %d/%d matches against the ignore list"))
|
iter_matches = j.iter_with_progress(matches, tr("Processed %d/%d matches against the ignore list"))
|
||||||
matches = [m for m in iter_matches
|
matches = [
|
||||||
if not self.ignore_list.AreIgnored(str(m.first.path), str(m.second.path))]
|
m for m in iter_matches
|
||||||
|
if not self.ignore_list.AreIgnored(str(m.first.path), str(m.second.path))
|
||||||
|
]
|
||||||
logging.info('Grouping matches')
|
logging.info('Grouping matches')
|
||||||
groups = engine.get_groups(matches, j)
|
groups = engine.get_groups(matches, j)
|
||||||
matched_files = dedupe([m.first for m in matches] + [m.second for m in matches])
|
matched_files = dedupe([m.first for m in matches] + [m.second for m in matches])
|
||||||
@@ -177,11 +185,12 @@ class Scanner:
|
|||||||
for g in groups:
|
for g in groups:
|
||||||
g.prioritize(self._key_func, self._tie_breaker)
|
g.prioritize(self._key_func, self._tie_breaker)
|
||||||
return groups
|
return groups
|
||||||
|
|
||||||
match_similar_words = False
|
match_similar_words = False
|
||||||
min_match_percentage = 80
|
min_match_percentage = 80
|
||||||
mix_file_kind = True
|
mix_file_kind = True
|
||||||
scan_type = ScanType.Filename
|
scan_type = ScanType.Filename
|
||||||
scanned_tags = {'artist', 'title'}
|
scanned_tags = {'artist', 'title'}
|
||||||
size_threshold = 0
|
size_threshold = 0
|
||||||
word_weighting = False
|
word_weighting = False
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2007-06-23
|
# Created On: 2007-06-23
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -15,7 +15,7 @@ from hscommon.path import Path
|
|||||||
import hscommon.conflict
|
import hscommon.conflict
|
||||||
import hscommon.util
|
import hscommon.util
|
||||||
from hscommon.testutil import CallLogger, eq_, log_calls
|
from hscommon.testutil import CallLogger, eq_, log_calls
|
||||||
from jobprogress.job import Job
|
from hscommon.jobprogress.job import Job
|
||||||
|
|
||||||
from .base import DupeGuru, TestApp
|
from .base import DupeGuru, TestApp
|
||||||
from .results_test import GetTestGroups
|
from .results_test import GetTestGroups
|
||||||
@@ -36,7 +36,7 @@ class TestCaseDupeGuru:
|
|||||||
assert call['filter_str'] is None
|
assert call['filter_str'] is None
|
||||||
call = dgapp.results.apply_filter.calls[1]
|
call = dgapp.results.apply_filter.calls[1]
|
||||||
eq_('foo', call['filter_str'])
|
eq_('foo', call['filter_str'])
|
||||||
|
|
||||||
def test_apply_filter_escapes_regexp(self, monkeypatch):
|
def test_apply_filter_escapes_regexp(self, monkeypatch):
|
||||||
dgapp = TestApp().app
|
dgapp = TestApp().app
|
||||||
monkeypatch.setattr(dgapp.results, 'apply_filter', log_calls(dgapp.results.apply_filter))
|
monkeypatch.setattr(dgapp.results, 'apply_filter', log_calls(dgapp.results.apply_filter))
|
||||||
@@ -50,7 +50,7 @@ class TestCaseDupeGuru:
|
|||||||
dgapp.apply_filter('(abc)')
|
dgapp.apply_filter('(abc)')
|
||||||
call = dgapp.results.apply_filter.calls[5]
|
call = dgapp.results.apply_filter.calls[5]
|
||||||
eq_('(abc)', call['filter_str'])
|
eq_('(abc)', call['filter_str'])
|
||||||
|
|
||||||
def test_copy_or_move(self, tmpdir, monkeypatch):
|
def test_copy_or_move(self, tmpdir, monkeypatch):
|
||||||
# The goal here is just to have a test for a previous blowup I had. I know my test coverage
|
# The goal here is just to have a test for a previous blowup I had. I know my test coverage
|
||||||
# for this unit is pathetic. What's done is done. My approach now is to add tests for
|
# for this unit is pathetic. What's done is done. My approach now is to add tests for
|
||||||
@@ -69,7 +69,7 @@ class TestCaseDupeGuru:
|
|||||||
call = hscommon.conflict.smart_copy.calls[0]
|
call = hscommon.conflict.smart_copy.calls[0]
|
||||||
eq_(call['dest_path'], op.join('some_destination', 'foo'))
|
eq_(call['dest_path'], op.join('some_destination', 'foo'))
|
||||||
eq_(call['source_path'], f.path)
|
eq_(call['source_path'], f.path)
|
||||||
|
|
||||||
def test_copy_or_move_clean_empty_dirs(self, tmpdir, monkeypatch):
|
def test_copy_or_move_clean_empty_dirs(self, tmpdir, monkeypatch):
|
||||||
tmppath = Path(str(tmpdir))
|
tmppath = Path(str(tmpdir))
|
||||||
sourcepath = tmppath['source']
|
sourcepath = tmppath['source']
|
||||||
@@ -83,13 +83,13 @@ class TestCaseDupeGuru:
|
|||||||
calls = app.clean_empty_dirs.calls
|
calls = app.clean_empty_dirs.calls
|
||||||
eq_(1, len(calls))
|
eq_(1, len(calls))
|
||||||
eq_(sourcepath, calls[0]['path'])
|
eq_(sourcepath, calls[0]['path'])
|
||||||
|
|
||||||
def test_Scan_with_objects_evaluating_to_false(self):
|
def test_Scan_with_objects_evaluating_to_false(self):
|
||||||
class FakeFile(fs.File):
|
class FakeFile(fs.File):
|
||||||
def __bool__(self):
|
def __bool__(self):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
# At some point, any() was used in a wrong way that made Scan() wrongly return 1
|
# At some point, any() was used in a wrong way that made Scan() wrongly return 1
|
||||||
app = TestApp().app
|
app = TestApp().app
|
||||||
f1, f2 = [FakeFile('foo') for i in range(2)]
|
f1, f2 = [FakeFile('foo') for i in range(2)]
|
||||||
@@ -97,7 +97,7 @@ class TestCaseDupeGuru:
|
|||||||
assert not (bool(f1) and bool(f2))
|
assert not (bool(f1) and bool(f2))
|
||||||
add_fake_files_to_directories(app.directories, [f1, f2])
|
add_fake_files_to_directories(app.directories, [f1, f2])
|
||||||
app.start_scanning() # no exception
|
app.start_scanning() # no exception
|
||||||
|
|
||||||
@mark.skipif("not hasattr(os, 'link')")
|
@mark.skipif("not hasattr(os, 'link')")
|
||||||
def test_ignore_hardlink_matches(self, tmpdir):
|
def test_ignore_hardlink_matches(self, tmpdir):
|
||||||
# If the ignore_hardlink_matches option is set, don't match files hardlinking to the same
|
# If the ignore_hardlink_matches option is set, don't match files hardlinking to the same
|
||||||
@@ -111,7 +111,7 @@ class TestCaseDupeGuru:
|
|||||||
app.options['ignore_hardlink_matches'] = True
|
app.options['ignore_hardlink_matches'] = True
|
||||||
app.start_scanning()
|
app.start_scanning()
|
||||||
eq_(len(app.results.groups), 0)
|
eq_(len(app.results.groups), 0)
|
||||||
|
|
||||||
def test_rename_when_nothing_is_selected(self):
|
def test_rename_when_nothing_is_selected(self):
|
||||||
# Issue #140
|
# Issue #140
|
||||||
# It's possible that rename operation has its selected row swept off from under it, thus
|
# It's possible that rename operation has its selected row swept off from under it, thus
|
||||||
@@ -127,11 +127,11 @@ class TestCaseDupeGuru_clean_empty_dirs:
|
|||||||
# XXX This monkeypatch is temporary. will be fixed in a better monkeypatcher.
|
# XXX This monkeypatch is temporary. will be fixed in a better monkeypatcher.
|
||||||
monkeypatch.setattr(app, 'delete_if_empty', hscommon.util.delete_if_empty)
|
monkeypatch.setattr(app, 'delete_if_empty', hscommon.util.delete_if_empty)
|
||||||
self.app = TestApp().app
|
self.app = TestApp().app
|
||||||
|
|
||||||
def test_option_off(self, do_setup):
|
def test_option_off(self, do_setup):
|
||||||
self.app.clean_empty_dirs(Path('/foo/bar'))
|
self.app.clean_empty_dirs(Path('/foo/bar'))
|
||||||
eq_(0, len(hscommon.util.delete_if_empty.calls))
|
eq_(0, len(hscommon.util.delete_if_empty.calls))
|
||||||
|
|
||||||
def test_option_on(self, do_setup):
|
def test_option_on(self, do_setup):
|
||||||
self.app.options['clean_empty_dirs'] = True
|
self.app.options['clean_empty_dirs'] = True
|
||||||
self.app.clean_empty_dirs(Path('/foo/bar'))
|
self.app.clean_empty_dirs(Path('/foo/bar'))
|
||||||
@@ -139,13 +139,13 @@ class TestCaseDupeGuru_clean_empty_dirs:
|
|||||||
eq_(1, len(calls))
|
eq_(1, len(calls))
|
||||||
eq_(Path('/foo/bar'), calls[0]['path'])
|
eq_(Path('/foo/bar'), calls[0]['path'])
|
||||||
eq_(['.DS_Store'], calls[0]['files_to_delete'])
|
eq_(['.DS_Store'], calls[0]['files_to_delete'])
|
||||||
|
|
||||||
def test_recurse_up(self, do_setup, monkeypatch):
|
def test_recurse_up(self, do_setup, monkeypatch):
|
||||||
# delete_if_empty must be recursively called up in the path until it returns False
|
# delete_if_empty must be recursively called up in the path until it returns False
|
||||||
@log_calls
|
@log_calls
|
||||||
def mock_delete_if_empty(path, files_to_delete=[]):
|
def mock_delete_if_empty(path, files_to_delete=[]):
|
||||||
return len(path) > 1
|
return len(path) > 1
|
||||||
|
|
||||||
monkeypatch.setattr(hscommon.util, 'delete_if_empty', mock_delete_if_empty)
|
monkeypatch.setattr(hscommon.util, 'delete_if_empty', mock_delete_if_empty)
|
||||||
# XXX This monkeypatch is temporary. will be fixed in a better monkeypatcher.
|
# XXX This monkeypatch is temporary. will be fixed in a better monkeypatcher.
|
||||||
monkeypatch.setattr(app, 'delete_if_empty', mock_delete_if_empty)
|
monkeypatch.setattr(app, 'delete_if_empty', mock_delete_if_empty)
|
||||||
@@ -156,7 +156,7 @@ class TestCaseDupeGuru_clean_empty_dirs:
|
|||||||
eq_(Path('not-empty/empty/empty'), calls[0]['path'])
|
eq_(Path('not-empty/empty/empty'), calls[0]['path'])
|
||||||
eq_(Path('not-empty/empty'), calls[1]['path'])
|
eq_(Path('not-empty/empty'), calls[1]['path'])
|
||||||
eq_(Path('not-empty'), calls[2]['path'])
|
eq_(Path('not-empty'), calls[2]['path'])
|
||||||
|
|
||||||
|
|
||||||
class TestCaseDupeGuruWithResults:
|
class TestCaseDupeGuruWithResults:
|
||||||
def pytest_funcarg__do_setup(self, request):
|
def pytest_funcarg__do_setup(self, request):
|
||||||
@@ -173,7 +173,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
tmppath['foo'].mkdir()
|
tmppath['foo'].mkdir()
|
||||||
tmppath['bar'].mkdir()
|
tmppath['bar'].mkdir()
|
||||||
self.app.directories.add_path(tmppath)
|
self.app.directories.add_path(tmppath)
|
||||||
|
|
||||||
def test_GetObjects(self, do_setup):
|
def test_GetObjects(self, do_setup):
|
||||||
objects = self.objects
|
objects = self.objects
|
||||||
groups = self.groups
|
groups = self.groups
|
||||||
@@ -186,7 +186,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
r = self.rtable[4]
|
r = self.rtable[4]
|
||||||
assert r._group is groups[1]
|
assert r._group is groups[1]
|
||||||
assert r._dupe is objects[4]
|
assert r._dupe is objects[4]
|
||||||
|
|
||||||
def test_GetObjects_after_sort(self, do_setup):
|
def test_GetObjects_after_sort(self, do_setup):
|
||||||
objects = self.objects
|
objects = self.objects
|
||||||
groups = self.groups[:] # we need an un-sorted reference
|
groups = self.groups[:] # we need an un-sorted reference
|
||||||
@@ -194,14 +194,14 @@ class TestCaseDupeGuruWithResults:
|
|||||||
r = self.rtable[1]
|
r = self.rtable[1]
|
||||||
assert r._group is groups[1]
|
assert r._group is groups[1]
|
||||||
assert r._dupe is objects[4]
|
assert r._dupe is objects[4]
|
||||||
|
|
||||||
def test_selected_result_node_paths_after_deletion(self, do_setup):
|
def test_selected_result_node_paths_after_deletion(self, do_setup):
|
||||||
# cases where the selected dupes aren't there are correctly handled
|
# cases where the selected dupes aren't there are correctly handled
|
||||||
self.rtable.select([1, 2, 3])
|
self.rtable.select([1, 2, 3])
|
||||||
self.app.remove_selected()
|
self.app.remove_selected()
|
||||||
# The first 2 dupes have been removed. The 3rd one is a ref. it stays there, in first pos.
|
# The first 2 dupes have been removed. The 3rd one is a ref. it stays there, in first pos.
|
||||||
eq_(self.rtable.selected_indexes, [1]) # no exception
|
eq_(self.rtable.selected_indexes, [1]) # no exception
|
||||||
|
|
||||||
def test_selectResultNodePaths(self, do_setup):
|
def test_selectResultNodePaths(self, do_setup):
|
||||||
app = self.app
|
app = self.app
|
||||||
objects = self.objects
|
objects = self.objects
|
||||||
@@ -209,7 +209,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
eq_(len(app.selected_dupes), 2)
|
eq_(len(app.selected_dupes), 2)
|
||||||
assert app.selected_dupes[0] is objects[1]
|
assert app.selected_dupes[0] is objects[1]
|
||||||
assert app.selected_dupes[1] is objects[2]
|
assert app.selected_dupes[1] is objects[2]
|
||||||
|
|
||||||
def test_selectResultNodePaths_with_ref(self, do_setup):
|
def test_selectResultNodePaths_with_ref(self, do_setup):
|
||||||
app = self.app
|
app = self.app
|
||||||
objects = self.objects
|
objects = self.objects
|
||||||
@@ -218,26 +218,26 @@ class TestCaseDupeGuruWithResults:
|
|||||||
assert app.selected_dupes[0] is objects[1]
|
assert app.selected_dupes[0] is objects[1]
|
||||||
assert app.selected_dupes[1] is objects[2]
|
assert app.selected_dupes[1] is objects[2]
|
||||||
assert app.selected_dupes[2] is self.groups[1].ref
|
assert app.selected_dupes[2] is self.groups[1].ref
|
||||||
|
|
||||||
def test_selectResultNodePaths_after_sort(self, do_setup):
|
def test_selectResultNodePaths_after_sort(self, do_setup):
|
||||||
app = self.app
|
app = self.app
|
||||||
objects = self.objects
|
objects = self.objects
|
||||||
groups = self.groups[:] #To keep the old order in memory
|
groups = self.groups[:] #To keep the old order in memory
|
||||||
self.rtable.sort('name', False) #0
|
self.rtable.sort('name', False) #0
|
||||||
#Now, the group order is supposed to be reversed
|
#Now, the group order is supposed to be reversed
|
||||||
self.rtable.select([1, 2, 3])
|
self.rtable.select([1, 2, 3])
|
||||||
eq_(len(app.selected_dupes), 3)
|
eq_(len(app.selected_dupes), 3)
|
||||||
assert app.selected_dupes[0] is objects[4]
|
assert app.selected_dupes[0] is objects[4]
|
||||||
assert app.selected_dupes[1] is groups[0].ref
|
assert app.selected_dupes[1] is groups[0].ref
|
||||||
assert app.selected_dupes[2] is objects[1]
|
assert app.selected_dupes[2] is objects[1]
|
||||||
|
|
||||||
def test_selected_powermarker_node_paths(self, do_setup):
|
def test_selected_powermarker_node_paths(self, do_setup):
|
||||||
# app.selected_dupes is correctly converted into paths
|
# app.selected_dupes is correctly converted into paths
|
||||||
self.rtable.power_marker = True
|
self.rtable.power_marker = True
|
||||||
self.rtable.select([0, 1, 2])
|
self.rtable.select([0, 1, 2])
|
||||||
self.rtable.power_marker = False
|
self.rtable.power_marker = False
|
||||||
eq_(self.rtable.selected_indexes, [1, 2, 4])
|
eq_(self.rtable.selected_indexes, [1, 2, 4])
|
||||||
|
|
||||||
def test_selected_powermarker_node_paths_after_deletion(self, do_setup):
|
def test_selected_powermarker_node_paths_after_deletion(self, do_setup):
|
||||||
# cases where the selected dupes aren't there are correctly handled
|
# cases where the selected dupes aren't there are correctly handled
|
||||||
app = self.app
|
app = self.app
|
||||||
@@ -245,7 +245,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
self.rtable.select([0, 1, 2])
|
self.rtable.select([0, 1, 2])
|
||||||
app.remove_selected()
|
app.remove_selected()
|
||||||
eq_(self.rtable.selected_indexes, []) # no exception
|
eq_(self.rtable.selected_indexes, []) # no exception
|
||||||
|
|
||||||
def test_selectPowerMarkerRows_after_sort(self, do_setup):
|
def test_selectPowerMarkerRows_after_sort(self, do_setup):
|
||||||
app = self.app
|
app = self.app
|
||||||
objects = self.objects
|
objects = self.objects
|
||||||
@@ -256,7 +256,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
assert app.selected_dupes[0] is objects[4]
|
assert app.selected_dupes[0] is objects[4]
|
||||||
assert app.selected_dupes[1] is objects[2]
|
assert app.selected_dupes[1] is objects[2]
|
||||||
assert app.selected_dupes[2] is objects[1]
|
assert app.selected_dupes[2] is objects[1]
|
||||||
|
|
||||||
def test_toggle_selected_mark_state(self, do_setup):
|
def test_toggle_selected_mark_state(self, do_setup):
|
||||||
app = self.app
|
app = self.app
|
||||||
objects = self.objects
|
objects = self.objects
|
||||||
@@ -270,7 +270,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
assert not app.results.is_marked(objects[2])
|
assert not app.results.is_marked(objects[2])
|
||||||
assert not app.results.is_marked(objects[3])
|
assert not app.results.is_marked(objects[3])
|
||||||
assert app.results.is_marked(objects[4])
|
assert app.results.is_marked(objects[4])
|
||||||
|
|
||||||
def test_toggle_selected_mark_state_with_different_selected_state(self, do_setup):
|
def test_toggle_selected_mark_state_with_different_selected_state(self, do_setup):
|
||||||
# When marking selected dupes with a heterogenous selection, mark all selected dupes. When
|
# When marking selected dupes with a heterogenous selection, mark all selected dupes. When
|
||||||
# it's homogenous, simply toggle.
|
# it's homogenous, simply toggle.
|
||||||
@@ -285,7 +285,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
eq_(app.results.mark_count, 2)
|
eq_(app.results.mark_count, 2)
|
||||||
app.toggle_selected_mark_state()
|
app.toggle_selected_mark_state()
|
||||||
eq_(app.results.mark_count, 0)
|
eq_(app.results.mark_count, 0)
|
||||||
|
|
||||||
def test_refreshDetailsWithSelected(self, do_setup):
|
def test_refreshDetailsWithSelected(self, do_setup):
|
||||||
self.rtable.select([1, 4])
|
self.rtable.select([1, 4])
|
||||||
eq_(self.dpanel.row(0), ('Filename', 'bar bleh', 'foo bar'))
|
eq_(self.dpanel.row(0), ('Filename', 'bar bleh', 'foo bar'))
|
||||||
@@ -293,7 +293,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
self.rtable.select([])
|
self.rtable.select([])
|
||||||
eq_(self.dpanel.row(0), ('Filename', '---', '---'))
|
eq_(self.dpanel.row(0), ('Filename', '---', '---'))
|
||||||
self.dpanel.view.check_gui_calls(['refresh'])
|
self.dpanel.view.check_gui_calls(['refresh'])
|
||||||
|
|
||||||
def test_makeSelectedReference(self, do_setup):
|
def test_makeSelectedReference(self, do_setup):
|
||||||
app = self.app
|
app = self.app
|
||||||
objects = self.objects
|
objects = self.objects
|
||||||
@@ -302,7 +302,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
app.make_selected_reference()
|
app.make_selected_reference()
|
||||||
assert groups[0].ref is objects[1]
|
assert groups[0].ref is objects[1]
|
||||||
assert groups[1].ref is objects[4]
|
assert groups[1].ref is objects[4]
|
||||||
|
|
||||||
def test_makeSelectedReference_by_selecting_two_dupes_in_the_same_group(self, do_setup):
|
def test_makeSelectedReference_by_selecting_two_dupes_in_the_same_group(self, do_setup):
|
||||||
app = self.app
|
app = self.app
|
||||||
objects = self.objects
|
objects = self.objects
|
||||||
@@ -312,7 +312,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
app.make_selected_reference()
|
app.make_selected_reference()
|
||||||
assert groups[0].ref is objects[1]
|
assert groups[0].ref is objects[1]
|
||||||
assert groups[1].ref is objects[4]
|
assert groups[1].ref is objects[4]
|
||||||
|
|
||||||
def test_removeSelected(self, do_setup):
|
def test_removeSelected(self, do_setup):
|
||||||
app = self.app
|
app = self.app
|
||||||
self.rtable.select([1, 4])
|
self.rtable.select([1, 4])
|
||||||
@@ -320,7 +320,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
eq_(len(app.results.dupes), 1) # the first path is now selected
|
eq_(len(app.results.dupes), 1) # the first path is now selected
|
||||||
app.remove_selected()
|
app.remove_selected()
|
||||||
eq_(len(app.results.dupes), 0)
|
eq_(len(app.results.dupes), 0)
|
||||||
|
|
||||||
def test_addDirectory_simple(self, do_setup):
|
def test_addDirectory_simple(self, do_setup):
|
||||||
# There's already a directory in self.app, so adding another once makes 2 of em
|
# There's already a directory in self.app, so adding another once makes 2 of em
|
||||||
app = self.app
|
app = self.app
|
||||||
@@ -328,7 +328,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
otherpath = Path(op.dirname(__file__))
|
otherpath = Path(op.dirname(__file__))
|
||||||
app.add_directory(otherpath)
|
app.add_directory(otherpath)
|
||||||
eq_(len(app.directories), 2)
|
eq_(len(app.directories), 2)
|
||||||
|
|
||||||
def test_addDirectory_already_there(self, do_setup):
|
def test_addDirectory_already_there(self, do_setup):
|
||||||
app = self.app
|
app = self.app
|
||||||
otherpath = Path(op.dirname(__file__))
|
otherpath = Path(op.dirname(__file__))
|
||||||
@@ -336,13 +336,13 @@ class TestCaseDupeGuruWithResults:
|
|||||||
app.add_directory(otherpath)
|
app.add_directory(otherpath)
|
||||||
eq_(len(app.view.messages), 1)
|
eq_(len(app.view.messages), 1)
|
||||||
assert "already" in app.view.messages[0]
|
assert "already" in app.view.messages[0]
|
||||||
|
|
||||||
def test_addDirectory_does_not_exist(self, do_setup):
|
def test_addDirectory_does_not_exist(self, do_setup):
|
||||||
app = self.app
|
app = self.app
|
||||||
app.add_directory('/does_not_exist')
|
app.add_directory('/does_not_exist')
|
||||||
eq_(len(app.view.messages), 1)
|
eq_(len(app.view.messages), 1)
|
||||||
assert "exist" in app.view.messages[0]
|
assert "exist" in app.view.messages[0]
|
||||||
|
|
||||||
def test_ignore(self, do_setup):
|
def test_ignore(self, do_setup):
|
||||||
app = self.app
|
app = self.app
|
||||||
self.rtable.select([4]) #The dupe of the second, 2 sized group
|
self.rtable.select([4]) #The dupe of the second, 2 sized group
|
||||||
@@ -352,7 +352,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
app.add_selected_to_ignore_list()
|
app.add_selected_to_ignore_list()
|
||||||
#BOTH the ref and the other dupe should have been added
|
#BOTH the ref and the other dupe should have been added
|
||||||
eq_(len(app.scanner.ignore_list), 3)
|
eq_(len(app.scanner.ignore_list), 3)
|
||||||
|
|
||||||
def test_purgeIgnoreList(self, do_setup, tmpdir):
|
def test_purgeIgnoreList(self, do_setup, tmpdir):
|
||||||
app = self.app
|
app = self.app
|
||||||
p1 = str(tmpdir.join('file1'))
|
p1 = str(tmpdir.join('file1'))
|
||||||
@@ -367,19 +367,19 @@ class TestCaseDupeGuruWithResults:
|
|||||||
eq_(1,len(app.scanner.ignore_list))
|
eq_(1,len(app.scanner.ignore_list))
|
||||||
assert app.scanner.ignore_list.AreIgnored(p1,p2)
|
assert app.scanner.ignore_list.AreIgnored(p1,p2)
|
||||||
assert not app.scanner.ignore_list.AreIgnored(dne,p1)
|
assert not app.scanner.ignore_list.AreIgnored(dne,p1)
|
||||||
|
|
||||||
def test_only_unicode_is_added_to_ignore_list(self, do_setup):
|
def test_only_unicode_is_added_to_ignore_list(self, do_setup):
|
||||||
def FakeIgnore(first,second):
|
def FakeIgnore(first,second):
|
||||||
if not isinstance(first,str):
|
if not isinstance(first,str):
|
||||||
self.fail()
|
self.fail()
|
||||||
if not isinstance(second,str):
|
if not isinstance(second,str):
|
||||||
self.fail()
|
self.fail()
|
||||||
|
|
||||||
app = self.app
|
app = self.app
|
||||||
app.scanner.ignore_list.Ignore = FakeIgnore
|
app.scanner.ignore_list.Ignore = FakeIgnore
|
||||||
self.rtable.select([4])
|
self.rtable.select([4])
|
||||||
app.add_selected_to_ignore_list()
|
app.add_selected_to_ignore_list()
|
||||||
|
|
||||||
def test_cancel_scan_with_previous_results(self, do_setup):
|
def test_cancel_scan_with_previous_results(self, do_setup):
|
||||||
# When doing a scan with results being present prior to the scan, correctly invalidate the
|
# When doing a scan with results being present prior to the scan, correctly invalidate the
|
||||||
# results table.
|
# results table.
|
||||||
@@ -388,7 +388,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
add_fake_files_to_directories(app.directories, self.objects) # We want the scan to at least start
|
add_fake_files_to_directories(app.directories, self.objects) # We want the scan to at least start
|
||||||
app.start_scanning() # will be cancelled immediately
|
app.start_scanning() # will be cancelled immediately
|
||||||
eq_(len(self.rtable), 0)
|
eq_(len(self.rtable), 0)
|
||||||
|
|
||||||
def test_selected_dupes_after_removal(self, do_setup):
|
def test_selected_dupes_after_removal(self, do_setup):
|
||||||
# Purge the app's `selected_dupes` attribute when removing dupes, or else it might cause a
|
# Purge the app's `selected_dupes` attribute when removing dupes, or else it might cause a
|
||||||
# crash later with None refs.
|
# crash later with None refs.
|
||||||
@@ -398,7 +398,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
app.remove_marked()
|
app.remove_marked()
|
||||||
eq_(len(self.rtable), 0)
|
eq_(len(self.rtable), 0)
|
||||||
eq_(app.selected_dupes, [])
|
eq_(app.selected_dupes, [])
|
||||||
|
|
||||||
def test_dont_crash_on_delta_powermarker_dupecount_sort(self, do_setup):
|
def test_dont_crash_on_delta_powermarker_dupecount_sort(self, do_setup):
|
||||||
# Don't crash when sorting by dupe count or percentage while delta+powermarker are enabled.
|
# Don't crash when sorting by dupe count or percentage while delta+powermarker are enabled.
|
||||||
# Ref #238
|
# Ref #238
|
||||||
@@ -410,7 +410,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
# don't crash
|
# don't crash
|
||||||
self.rtable.sort('percentage', False)
|
self.rtable.sort('percentage', False)
|
||||||
# don't crash
|
# don't crash
|
||||||
|
|
||||||
|
|
||||||
class TestCaseDupeGuru_renameSelected:
|
class TestCaseDupeGuru_renameSelected:
|
||||||
def pytest_funcarg__do_setup(self, request):
|
def pytest_funcarg__do_setup(self, request):
|
||||||
@@ -437,7 +437,7 @@ class TestCaseDupeGuru_renameSelected:
|
|||||||
self.groups = groups
|
self.groups = groups
|
||||||
self.p = p
|
self.p = p
|
||||||
self.files = files
|
self.files = files
|
||||||
|
|
||||||
def test_simple(self, do_setup):
|
def test_simple(self, do_setup):
|
||||||
app = self.app
|
app = self.app
|
||||||
g = self.groups[0]
|
g = self.groups[0]
|
||||||
@@ -447,7 +447,7 @@ class TestCaseDupeGuru_renameSelected:
|
|||||||
assert 'renamed' in names
|
assert 'renamed' in names
|
||||||
assert 'foo bar 2' not in names
|
assert 'foo bar 2' not in names
|
||||||
eq_(g.dupes[0].name, 'renamed')
|
eq_(g.dupes[0].name, 'renamed')
|
||||||
|
|
||||||
def test_none_selected(self, do_setup, monkeypatch):
|
def test_none_selected(self, do_setup, monkeypatch):
|
||||||
app = self.app
|
app = self.app
|
||||||
g = self.groups[0]
|
g = self.groups[0]
|
||||||
@@ -460,7 +460,7 @@ class TestCaseDupeGuru_renameSelected:
|
|||||||
assert 'renamed' not in names
|
assert 'renamed' not in names
|
||||||
assert 'foo bar 2' in names
|
assert 'foo bar 2' in names
|
||||||
eq_(g.dupes[0].name, 'foo bar 2')
|
eq_(g.dupes[0].name, 'foo bar 2')
|
||||||
|
|
||||||
def test_name_already_exists(self, do_setup, monkeypatch):
|
def test_name_already_exists(self, do_setup, monkeypatch):
|
||||||
app = self.app
|
app = self.app
|
||||||
g = self.groups[0]
|
g = self.groups[0]
|
||||||
@@ -473,7 +473,7 @@ class TestCaseDupeGuru_renameSelected:
|
|||||||
assert 'foo bar 1' in names
|
assert 'foo bar 1' in names
|
||||||
assert 'foo bar 2' in names
|
assert 'foo bar 2' in names
|
||||||
eq_(g.dupes[0].name, 'foo bar 2')
|
eq_(g.dupes[0].name, 'foo bar 2')
|
||||||
|
|
||||||
|
|
||||||
class TestAppWithDirectoriesInTree:
|
class TestAppWithDirectoriesInTree:
|
||||||
def pytest_funcarg__do_setup(self, request):
|
def pytest_funcarg__do_setup(self, request):
|
||||||
@@ -487,7 +487,7 @@ class TestAppWithDirectoriesInTree:
|
|||||||
self.dtree = app.dtree
|
self.dtree = app.dtree
|
||||||
self.dtree.add_directory(p)
|
self.dtree.add_directory(p)
|
||||||
self.dtree.view.clear_calls()
|
self.dtree.view.clear_calls()
|
||||||
|
|
||||||
def test_set_root_as_ref_makes_subfolders_ref_as_well(self, do_setup):
|
def test_set_root_as_ref_makes_subfolders_ref_as_well(self, do_setup):
|
||||||
# Setting a node state to something also affect subnodes. These subnodes must be correctly
|
# Setting a node state to something also affect subnodes. These subnodes must be correctly
|
||||||
# refreshed.
|
# refreshed.
|
||||||
@@ -500,4 +500,4 @@ class TestAppWithDirectoriesInTree:
|
|||||||
subnode = node[0]
|
subnode = node[0]
|
||||||
eq_(subnode.state, 1)
|
eq_(subnode.state, 1)
|
||||||
self.dtree.view.check_gui_calls(['refresh_states'])
|
self.dtree.view.check_gui_calls(['refresh_states'])
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2011/09/07
|
# Created On: 2011/09/07
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from hscommon.testutil import TestApp as TestAppBase, eq_, with_app
|
from hscommon.testutil import TestApp as TestAppBase, eq_, with_app
|
||||||
from hscommon.path import Path
|
from hscommon.path import Path
|
||||||
from hscommon.util import get_file_ext, format_size
|
from hscommon.util import get_file_ext, format_size
|
||||||
from hscommon.gui.column import Column
|
from hscommon.gui.column import Column
|
||||||
from jobprogress.job import nulljob, JobCancelled
|
from hscommon.jobprogress.job import nulljob, JobCancelled
|
||||||
|
|
||||||
from .. import engine
|
from .. import engine
|
||||||
from .. import prioritize
|
from .. import prioritize
|
||||||
@@ -23,28 +23,28 @@ from ..gui.prioritize_dialog import PrioritizeDialog
|
|||||||
|
|
||||||
class DupeGuruView:
|
class DupeGuruView:
|
||||||
JOB = nulljob
|
JOB = nulljob
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.messages = []
|
self.messages = []
|
||||||
|
|
||||||
def start_job(self, jobid, func, args=()):
|
def start_job(self, jobid, func, args=()):
|
||||||
try:
|
try:
|
||||||
func(self.JOB, *args)
|
func(self.JOB, *args)
|
||||||
except JobCancelled:
|
except JobCancelled:
|
||||||
return
|
return
|
||||||
|
|
||||||
def get_default(self, key_name):
|
def get_default(self, key_name):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def set_default(self, key_name, value):
|
def set_default(self, key_name, value):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def show_message(self, msg):
|
def show_message(self, msg):
|
||||||
self.messages.append(msg)
|
self.messages.append(msg)
|
||||||
|
|
||||||
def ask_yes_no(self, prompt):
|
def ask_yes_no(self, prompt):
|
||||||
return True # always answer yes
|
return True # always answer yes
|
||||||
|
|
||||||
|
|
||||||
class ResultTable(ResultTableBase):
|
class ResultTable(ResultTableBase):
|
||||||
COLUMNS = [
|
COLUMNS = [
|
||||||
@@ -55,21 +55,21 @@ class ResultTable(ResultTableBase):
|
|||||||
Column('extension', 'Kind'),
|
Column('extension', 'Kind'),
|
||||||
]
|
]
|
||||||
DELTA_COLUMNS = {'size', }
|
DELTA_COLUMNS = {'size', }
|
||||||
|
|
||||||
class DupeGuru(DupeGuruBase):
|
class DupeGuru(DupeGuruBase):
|
||||||
NAME = 'dupeGuru'
|
NAME = 'dupeGuru'
|
||||||
METADATA_TO_READ = ['size']
|
METADATA_TO_READ = ['size']
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
DupeGuruBase.__init__(self, DupeGuruView())
|
DupeGuruBase.__init__(self, DupeGuruView())
|
||||||
self.appdata = '/tmp'
|
self.appdata = '/tmp'
|
||||||
|
|
||||||
def _prioritization_categories(self):
|
def _prioritization_categories(self):
|
||||||
return prioritize.all_categories()
|
return prioritize.all_categories()
|
||||||
|
|
||||||
def _create_result_table(self):
|
def _create_result_table(self):
|
||||||
return ResultTable(self)
|
return ResultTable(self)
|
||||||
|
|
||||||
|
|
||||||
class NamedObject:
|
class NamedObject:
|
||||||
def __init__(self, name="foobar", with_words=False, size=1, folder=None):
|
def __init__(self, name="foobar", with_words=False, size=1, folder=None):
|
||||||
@@ -83,10 +83,10 @@ class NamedObject:
|
|||||||
if with_words:
|
if with_words:
|
||||||
self.words = getwords(name)
|
self.words = getwords(name)
|
||||||
self.is_ref = False
|
self.is_ref = False
|
||||||
|
|
||||||
def __bool__(self):
|
def __bool__(self):
|
||||||
return False #Make sure that operations are made correctly when the bool value of files is false.
|
return False #Make sure that operations are made correctly when the bool value of files is false.
|
||||||
|
|
||||||
def get_display_info(self, group, delta):
|
def get_display_info(self, group, delta):
|
||||||
size = self.size
|
size = self.size
|
||||||
m = group.get_match_of(self)
|
m = group.get_match_of(self)
|
||||||
@@ -99,19 +99,19 @@ class NamedObject:
|
|||||||
'size': format_size(size, 0, 1, False),
|
'size': format_size(size, 0, 1, False),
|
||||||
'extension': self.extension if hasattr(self, 'extension') else '---',
|
'extension': self.extension if hasattr(self, 'extension') else '---',
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path(self):
|
def path(self):
|
||||||
return self._folder[self.name]
|
return self._folder[self.name]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def folder_path(self):
|
def folder_path(self):
|
||||||
return self.path.parent()
|
return self.path.parent()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def extension(self):
|
def extension(self):
|
||||||
return get_file_ext(self.name)
|
return get_file_ext(self.name)
|
||||||
|
|
||||||
# Returns a group set that looks like that:
|
# Returns a group set that looks like that:
|
||||||
# "foo bar" (1)
|
# "foo bar" (1)
|
||||||
# "bar bleh" (1024)
|
# "bar bleh" (1024)
|
||||||
@@ -135,7 +135,7 @@ class TestApp(TestAppBase):
|
|||||||
if hasattr(gui, 'columns'): # tables
|
if hasattr(gui, 'columns'): # tables
|
||||||
gui.columns.view = self.make_logger()
|
gui.columns.view = self.make_logger()
|
||||||
return gui
|
return gui
|
||||||
|
|
||||||
TestAppBase.__init__(self)
|
TestAppBase.__init__(self)
|
||||||
make_gui = self.make_gui
|
make_gui = self.make_gui
|
||||||
self.app = DupeGuru()
|
self.app = DupeGuru()
|
||||||
@@ -153,14 +153,14 @@ class TestApp(TestAppBase):
|
|||||||
link_gui(self.app.progress_window)
|
link_gui(self.app.progress_window)
|
||||||
link_gui(self.app.progress_window.jobdesc_textfield)
|
link_gui(self.app.progress_window.jobdesc_textfield)
|
||||||
link_gui(self.app.progress_window.progressdesc_textfield)
|
link_gui(self.app.progress_window.progressdesc_textfield)
|
||||||
|
|
||||||
#--- Helpers
|
#--- Helpers
|
||||||
def select_pri_criterion(self, name):
|
def select_pri_criterion(self, name):
|
||||||
# Select a main prioritize criterion by name instead of by index. Makes tests more
|
# Select a main prioritize criterion by name instead of by index. Makes tests more
|
||||||
# maintainable.
|
# maintainable.
|
||||||
index = self.pdialog.category_list.index(name)
|
index = self.pdialog.category_list.index(name)
|
||||||
self.pdialog.category_list.select(index)
|
self.pdialog.category_list.select(index)
|
||||||
|
|
||||||
def add_pri_criterion(self, name, index):
|
def add_pri_criterion(self, name, index):
|
||||||
self.select_pri_criterion(name)
|
self.select_pri_criterion(name)
|
||||||
self.pdialog.criteria_list.select([index])
|
self.pdialog.criteria_list.select([index])
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2006/01/29
|
# Created On: 2006/01/29
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from jobprogress import job
|
from hscommon.jobprogress import job
|
||||||
from hscommon.util import first
|
from hscommon.util import first
|
||||||
from hscommon.testutil import eq_, log_calls
|
from hscommon.testutil import eq_, log_calls
|
||||||
|
|
||||||
@@ -48,119 +48,119 @@ class TestCasegetwords:
|
|||||||
def test_spaces(self):
|
def test_spaces(self):
|
||||||
eq_(['a', 'b', 'c', 'd'], getwords("a b c d"))
|
eq_(['a', 'b', 'c', 'd'], getwords("a b c d"))
|
||||||
eq_(['a', 'b', 'c', 'd'], getwords(" a b c d "))
|
eq_(['a', 'b', 'c', 'd'], getwords(" a b c d "))
|
||||||
|
|
||||||
def test_splitter_chars(self):
|
def test_splitter_chars(self):
|
||||||
eq_(
|
eq_(
|
||||||
[chr(i) for i in range(ord('a'),ord('z')+1)],
|
[chr(i) for i in range(ord('a'),ord('z')+1)],
|
||||||
getwords("a-b_c&d+e(f)g;h\\i[j]k{l}m:n.o,p<q>r/s?t~u!v@w#x$y*z")
|
getwords("a-b_c&d+e(f)g;h\\i[j]k{l}m:n.o,p<q>r/s?t~u!v@w#x$y*z")
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_joiner_chars(self):
|
def test_joiner_chars(self):
|
||||||
eq_(["aec"], getwords("a'e\u0301c"))
|
eq_(["aec"], getwords("a'e\u0301c"))
|
||||||
|
|
||||||
def test_empty(self):
|
def test_empty(self):
|
||||||
eq_([], getwords(''))
|
eq_([], getwords(''))
|
||||||
|
|
||||||
def test_returns_lowercase(self):
|
def test_returns_lowercase(self):
|
||||||
eq_(['foo', 'bar'], getwords('FOO BAR'))
|
eq_(['foo', 'bar'], getwords('FOO BAR'))
|
||||||
|
|
||||||
def test_decompose_unicode(self):
|
def test_decompose_unicode(self):
|
||||||
eq_(getwords('foo\xe9bar'), ['fooebar'])
|
eq_(getwords('foo\xe9bar'), ['fooebar'])
|
||||||
|
|
||||||
|
|
||||||
class TestCasegetfields:
|
class TestCasegetfields:
|
||||||
def test_simple(self):
|
def test_simple(self):
|
||||||
eq_([['a', 'b'], ['c', 'd', 'e']], getfields('a b - c d e'))
|
eq_([['a', 'b'], ['c', 'd', 'e']], getfields('a b - c d e'))
|
||||||
|
|
||||||
def test_empty(self):
|
def test_empty(self):
|
||||||
eq_([], getfields(''))
|
eq_([], getfields(''))
|
||||||
|
|
||||||
def test_cleans_empty_fields(self):
|
def test_cleans_empty_fields(self):
|
||||||
expected = [['a', 'bc', 'def']]
|
expected = [['a', 'bc', 'def']]
|
||||||
actual = getfields(' - a bc def')
|
actual = getfields(' - a bc def')
|
||||||
eq_(expected, actual)
|
eq_(expected, actual)
|
||||||
expected = [['bc', 'def']]
|
expected = [['bc', 'def']]
|
||||||
|
|
||||||
|
|
||||||
class TestCaseunpack_fields:
|
class TestCaseunpack_fields:
|
||||||
def test_with_fields(self):
|
def test_with_fields(self):
|
||||||
expected = ['a', 'b', 'c', 'd', 'e', 'f']
|
expected = ['a', 'b', 'c', 'd', 'e', 'f']
|
||||||
actual = unpack_fields([['a'], ['b', 'c'], ['d', 'e', 'f']])
|
actual = unpack_fields([['a'], ['b', 'c'], ['d', 'e', 'f']])
|
||||||
eq_(expected, actual)
|
eq_(expected, actual)
|
||||||
|
|
||||||
def test_without_fields(self):
|
def test_without_fields(self):
|
||||||
expected = ['a', 'b', 'c', 'd', 'e', 'f']
|
expected = ['a', 'b', 'c', 'd', 'e', 'f']
|
||||||
actual = unpack_fields(['a', 'b', 'c', 'd', 'e', 'f'])
|
actual = unpack_fields(['a', 'b', 'c', 'd', 'e', 'f'])
|
||||||
eq_(expected, actual)
|
eq_(expected, actual)
|
||||||
|
|
||||||
def test_empty(self):
|
def test_empty(self):
|
||||||
eq_([], unpack_fields([]))
|
eq_([], unpack_fields([]))
|
||||||
|
|
||||||
|
|
||||||
class TestCaseWordCompare:
|
class TestCaseWordCompare:
|
||||||
def test_list(self):
|
def test_list(self):
|
||||||
eq_(100, compare(['a', 'b', 'c', 'd'],['a', 'b', 'c', 'd']))
|
eq_(100, compare(['a', 'b', 'c', 'd'],['a', 'b', 'c', 'd']))
|
||||||
eq_(86, compare(['a', 'b', 'c', 'd'],['a', 'b', 'c']))
|
eq_(86, compare(['a', 'b', 'c', 'd'],['a', 'b', 'c']))
|
||||||
|
|
||||||
def test_unordered(self):
|
def test_unordered(self):
|
||||||
#Sometimes, users don't want fuzzy matching too much When they set the slider
|
#Sometimes, users don't want fuzzy matching too much When they set the slider
|
||||||
#to 100, they don't expect a filename with the same words, but not the same order, to match.
|
#to 100, they don't expect a filename with the same words, but not the same order, to match.
|
||||||
#Thus, we want to return 99 in that case.
|
#Thus, we want to return 99 in that case.
|
||||||
eq_(99, compare(['a', 'b', 'c', 'd'], ['d', 'b', 'c', 'a']))
|
eq_(99, compare(['a', 'b', 'c', 'd'], ['d', 'b', 'c', 'a']))
|
||||||
|
|
||||||
def test_word_occurs_twice(self):
|
def test_word_occurs_twice(self):
|
||||||
#if a word occurs twice in first, but once in second, we want the word to be only counted once
|
#if a word occurs twice in first, but once in second, we want the word to be only counted once
|
||||||
eq_(89, compare(['a', 'b', 'c', 'd', 'a'], ['d', 'b', 'c', 'a']))
|
eq_(89, compare(['a', 'b', 'c', 'd', 'a'], ['d', 'b', 'c', 'a']))
|
||||||
|
|
||||||
def test_uses_copy_of_lists(self):
|
def test_uses_copy_of_lists(self):
|
||||||
first = ['foo', 'bar']
|
first = ['foo', 'bar']
|
||||||
second = ['bar', 'bleh']
|
second = ['bar', 'bleh']
|
||||||
compare(first, second)
|
compare(first, second)
|
||||||
eq_(['foo', 'bar'], first)
|
eq_(['foo', 'bar'], first)
|
||||||
eq_(['bar', 'bleh'], second)
|
eq_(['bar', 'bleh'], second)
|
||||||
|
|
||||||
def test_word_weight(self):
|
def test_word_weight(self):
|
||||||
eq_(int((6.0 / 13.0) * 100), compare(['foo', 'bar'], ['bar', 'bleh'], (WEIGHT_WORDS, )))
|
eq_(int((6.0 / 13.0) * 100), compare(['foo', 'bar'], ['bar', 'bleh'], (WEIGHT_WORDS, )))
|
||||||
|
|
||||||
def test_similar_words(self):
|
def test_similar_words(self):
|
||||||
eq_(100, compare(['the', 'white', 'stripes'],['the', 'whites', 'stripe'], (MATCH_SIMILAR_WORDS, )))
|
eq_(100, compare(['the', 'white', 'stripes'],['the', 'whites', 'stripe'], (MATCH_SIMILAR_WORDS, )))
|
||||||
|
|
||||||
def test_empty(self):
|
def test_empty(self):
|
||||||
eq_(0, compare([], []))
|
eq_(0, compare([], []))
|
||||||
|
|
||||||
def test_with_fields(self):
|
def test_with_fields(self):
|
||||||
eq_(67, compare([['a', 'b'], ['c', 'd', 'e']], [['a', 'b'], ['c', 'd', 'f']]))
|
eq_(67, compare([['a', 'b'], ['c', 'd', 'e']], [['a', 'b'], ['c', 'd', 'f']]))
|
||||||
|
|
||||||
def test_propagate_flags_with_fields(self, monkeypatch):
|
def test_propagate_flags_with_fields(self, monkeypatch):
|
||||||
def mock_compare(first, second, flags):
|
def mock_compare(first, second, flags):
|
||||||
eq_((0, 1, 2, 3, 5), flags)
|
eq_((0, 1, 2, 3, 5), flags)
|
||||||
|
|
||||||
monkeypatch.setattr(engine, 'compare_fields', mock_compare)
|
monkeypatch.setattr(engine, 'compare_fields', mock_compare)
|
||||||
compare([['a']], [['a']], (0, 1, 2, 3, 5))
|
compare([['a']], [['a']], (0, 1, 2, 3, 5))
|
||||||
|
|
||||||
|
|
||||||
class TestCaseWordCompareWithFields:
|
class TestCaseWordCompareWithFields:
|
||||||
def test_simple(self):
|
def test_simple(self):
|
||||||
eq_(67, compare_fields([['a', 'b'], ['c', 'd', 'e']], [['a', 'b'], ['c', 'd', 'f']]))
|
eq_(67, compare_fields([['a', 'b'], ['c', 'd', 'e']], [['a', 'b'], ['c', 'd', 'f']]))
|
||||||
|
|
||||||
def test_empty(self):
|
def test_empty(self):
|
||||||
eq_(0, compare_fields([], []))
|
eq_(0, compare_fields([], []))
|
||||||
|
|
||||||
def test_different_length(self):
|
def test_different_length(self):
|
||||||
eq_(0, compare_fields([['a'], ['b']], [['a'], ['b'], ['c']]))
|
eq_(0, compare_fields([['a'], ['b']], [['a'], ['b'], ['c']]))
|
||||||
|
|
||||||
def test_propagates_flags(self, monkeypatch):
|
def test_propagates_flags(self, monkeypatch):
|
||||||
def mock_compare(first, second, flags):
|
def mock_compare(first, second, flags):
|
||||||
eq_((0, 1, 2, 3, 5), flags)
|
eq_((0, 1, 2, 3, 5), flags)
|
||||||
|
|
||||||
monkeypatch.setattr(engine, 'compare_fields', mock_compare)
|
monkeypatch.setattr(engine, 'compare_fields', mock_compare)
|
||||||
compare_fields([['a']], [['a']],(0, 1, 2, 3, 5))
|
compare_fields([['a']], [['a']],(0, 1, 2, 3, 5))
|
||||||
|
|
||||||
def test_order(self):
|
def test_order(self):
|
||||||
first = [['a', 'b'], ['c', 'd', 'e']]
|
first = [['a', 'b'], ['c', 'd', 'e']]
|
||||||
second = [['c', 'd', 'f'], ['a', 'b']]
|
second = [['c', 'd', 'f'], ['a', 'b']]
|
||||||
eq_(0, compare_fields(first, second))
|
eq_(0, compare_fields(first, second))
|
||||||
|
|
||||||
def test_no_order(self):
|
def test_no_order(self):
|
||||||
first = [['a','b'],['c','d','e']]
|
first = [['a','b'],['c','d','e']]
|
||||||
second = [['c','d','f'],['a','b']]
|
second = [['c','d','f'],['a','b']]
|
||||||
@@ -168,10 +168,10 @@ class TestCaseWordCompareWithFields:
|
|||||||
first = [['a','b'],['a','b']] #a field can only be matched once.
|
first = [['a','b'],['a','b']] #a field can only be matched once.
|
||||||
second = [['c','d','f'],['a','b']]
|
second = [['c','d','f'],['a','b']]
|
||||||
eq_(0, compare_fields(first, second, (NO_FIELD_ORDER, )))
|
eq_(0, compare_fields(first, second, (NO_FIELD_ORDER, )))
|
||||||
first = [['a','b'],['a','b','c']]
|
first = [['a','b'],['a','b','c']]
|
||||||
second = [['c','d','f'],['a','b']]
|
second = [['c','d','f'],['a','b']]
|
||||||
eq_(33, compare_fields(first, second, (NO_FIELD_ORDER, )))
|
eq_(33, compare_fields(first, second, (NO_FIELD_ORDER, )))
|
||||||
|
|
||||||
def test_compare_fields_without_order_doesnt_alter_fields(self):
|
def test_compare_fields_without_order_doesnt_alter_fields(self):
|
||||||
#The NO_ORDER comp type altered the fields!
|
#The NO_ORDER comp type altered the fields!
|
||||||
first = [['a','b'],['c','d','e']]
|
first = [['a','b'],['c','d','e']]
|
||||||
@@ -179,7 +179,7 @@ class TestCaseWordCompareWithFields:
|
|||||||
eq_(67, compare_fields(first, second, (NO_FIELD_ORDER, )))
|
eq_(67, compare_fields(first, second, (NO_FIELD_ORDER, )))
|
||||||
eq_([['a','b'],['c','d','e']],first)
|
eq_([['a','b'],['c','d','e']],first)
|
||||||
eq_([['c','d','f'],['a','b']],second)
|
eq_([['c','d','f'],['a','b']],second)
|
||||||
|
|
||||||
|
|
||||||
class TestCasebuild_word_dict:
|
class TestCasebuild_word_dict:
|
||||||
def test_with_standard_words(self):
|
def test_with_standard_words(self):
|
||||||
@@ -199,30 +199,30 @@ class TestCasebuild_word_dict:
|
|||||||
assert l[2] in d['baz']
|
assert l[2] in d['baz']
|
||||||
eq_(1,len(d['bleh']))
|
eq_(1,len(d['bleh']))
|
||||||
assert l[2] in d['bleh']
|
assert l[2] in d['bleh']
|
||||||
|
|
||||||
def test_unpack_fields(self):
|
def test_unpack_fields(self):
|
||||||
o = NamedObject('')
|
o = NamedObject('')
|
||||||
o.words = [['foo','bar'],['baz']]
|
o.words = [['foo','bar'],['baz']]
|
||||||
d = build_word_dict([o])
|
d = build_word_dict([o])
|
||||||
eq_(3,len(d))
|
eq_(3,len(d))
|
||||||
eq_(1,len(d['foo']))
|
eq_(1,len(d['foo']))
|
||||||
|
|
||||||
def test_words_are_unaltered(self):
|
def test_words_are_unaltered(self):
|
||||||
o = NamedObject('')
|
o = NamedObject('')
|
||||||
o.words = [['foo','bar'],['baz']]
|
o.words = [['foo','bar'],['baz']]
|
||||||
build_word_dict([o])
|
build_word_dict([o])
|
||||||
eq_([['foo','bar'],['baz']],o.words)
|
eq_([['foo','bar'],['baz']],o.words)
|
||||||
|
|
||||||
def test_object_instances_can_only_be_once_in_words_object_list(self):
|
def test_object_instances_can_only_be_once_in_words_object_list(self):
|
||||||
o = NamedObject('foo foo',True)
|
o = NamedObject('foo foo',True)
|
||||||
d = build_word_dict([o])
|
d = build_word_dict([o])
|
||||||
eq_(1,len(d['foo']))
|
eq_(1,len(d['foo']))
|
||||||
|
|
||||||
def test_job(self):
|
def test_job(self):
|
||||||
def do_progress(p,d=''):
|
def do_progress(p,d=''):
|
||||||
self.log.append(p)
|
self.log.append(p)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
j = job.Job(1,do_progress)
|
j = job.Job(1,do_progress)
|
||||||
self.log = []
|
self.log = []
|
||||||
s = "foo bar"
|
s = "foo bar"
|
||||||
@@ -230,7 +230,7 @@ class TestCasebuild_word_dict:
|
|||||||
# We don't have intermediate log because iter_with_progress is called with every > 1
|
# We don't have intermediate log because iter_with_progress is called with every > 1
|
||||||
eq_(0,self.log[0])
|
eq_(0,self.log[0])
|
||||||
eq_(100,self.log[1])
|
eq_(100,self.log[1])
|
||||||
|
|
||||||
|
|
||||||
class TestCasemerge_similar_words:
|
class TestCasemerge_similar_words:
|
||||||
def test_some_similar_words(self):
|
def test_some_similar_words(self):
|
||||||
@@ -242,8 +242,8 @@ class TestCasemerge_similar_words:
|
|||||||
merge_similar_words(d)
|
merge_similar_words(d)
|
||||||
eq_(1,len(d))
|
eq_(1,len(d))
|
||||||
eq_(3,len(d['foobar']))
|
eq_(3,len(d['foobar']))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class TestCasereduce_common_words:
|
class TestCasereduce_common_words:
|
||||||
def test_typical(self):
|
def test_typical(self):
|
||||||
@@ -254,7 +254,7 @@ class TestCasereduce_common_words:
|
|||||||
reduce_common_words(d, 50)
|
reduce_common_words(d, 50)
|
||||||
assert 'foo' not in d
|
assert 'foo' not in d
|
||||||
eq_(49,len(d['bar']))
|
eq_(49,len(d['bar']))
|
||||||
|
|
||||||
def test_dont_remove_objects_with_only_common_words(self):
|
def test_dont_remove_objects_with_only_common_words(self):
|
||||||
d = {
|
d = {
|
||||||
'common': set([NamedObject("common uncommon",True) for i in range(50)] + [NamedObject("common",True)]),
|
'common': set([NamedObject("common uncommon",True) for i in range(50)] + [NamedObject("common",True)]),
|
||||||
@@ -263,7 +263,7 @@ class TestCasereduce_common_words:
|
|||||||
reduce_common_words(d, 50)
|
reduce_common_words(d, 50)
|
||||||
eq_(1,len(d['common']))
|
eq_(1,len(d['common']))
|
||||||
eq_(1,len(d['uncommon']))
|
eq_(1,len(d['uncommon']))
|
||||||
|
|
||||||
def test_values_still_are_set_instances(self):
|
def test_values_still_are_set_instances(self):
|
||||||
d = {
|
d = {
|
||||||
'common': set([NamedObject("common uncommon",True) for i in range(50)] + [NamedObject("common",True)]),
|
'common': set([NamedObject("common uncommon",True) for i in range(50)] + [NamedObject("common",True)]),
|
||||||
@@ -272,7 +272,7 @@ class TestCasereduce_common_words:
|
|||||||
reduce_common_words(d, 50)
|
reduce_common_words(d, 50)
|
||||||
assert isinstance(d['common'],set)
|
assert isinstance(d['common'],set)
|
||||||
assert isinstance(d['uncommon'],set)
|
assert isinstance(d['uncommon'],set)
|
||||||
|
|
||||||
def test_dont_raise_KeyError_when_a_word_has_been_removed(self):
|
def test_dont_raise_KeyError_when_a_word_has_been_removed(self):
|
||||||
#If a word has been removed by the reduce, an object in a subsequent common word that
|
#If a word has been removed by the reduce, an object in a subsequent common word that
|
||||||
#contains the word that has been removed would cause a KeyError.
|
#contains the word that has been removed would cause a KeyError.
|
||||||
@@ -285,14 +285,14 @@ class TestCasereduce_common_words:
|
|||||||
reduce_common_words(d, 50)
|
reduce_common_words(d, 50)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
self.fail()
|
self.fail()
|
||||||
|
|
||||||
def test_unpack_fields(self):
|
def test_unpack_fields(self):
|
||||||
#object.words may be fields.
|
#object.words may be fields.
|
||||||
def create_it():
|
def create_it():
|
||||||
o = NamedObject('')
|
o = NamedObject('')
|
||||||
o.words = [['foo','bar'],['baz']]
|
o.words = [['foo','bar'],['baz']]
|
||||||
return o
|
return o
|
||||||
|
|
||||||
d = {
|
d = {
|
||||||
'foo': set([create_it() for i in range(50)])
|
'foo': set([create_it() for i in range(50)])
|
||||||
}
|
}
|
||||||
@@ -300,7 +300,7 @@ class TestCasereduce_common_words:
|
|||||||
reduce_common_words(d, 50)
|
reduce_common_words(d, 50)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
self.fail("must support fields.")
|
self.fail("must support fields.")
|
||||||
|
|
||||||
def test_consider_a_reduced_common_word_common_even_after_reduction(self):
|
def test_consider_a_reduced_common_word_common_even_after_reduction(self):
|
||||||
#There was a bug in the code that causeda word that has already been reduced not to
|
#There was a bug in the code that causeda word that has already been reduced not to
|
||||||
#be counted as a common word for subsequent words. For example, if 'foo' is processed
|
#be counted as a common word for subsequent words. For example, if 'foo' is processed
|
||||||
@@ -316,7 +316,7 @@ class TestCasereduce_common_words:
|
|||||||
eq_(1,len(d['foo']))
|
eq_(1,len(d['foo']))
|
||||||
eq_(1,len(d['bar']))
|
eq_(1,len(d['bar']))
|
||||||
eq_(49,len(d['baz']))
|
eq_(49,len(d['baz']))
|
||||||
|
|
||||||
|
|
||||||
class TestCaseget_match:
|
class TestCaseget_match:
|
||||||
def test_simple(self):
|
def test_simple(self):
|
||||||
@@ -328,7 +328,7 @@ class TestCaseget_match:
|
|||||||
eq_(['bar','bleh'],m.second.words)
|
eq_(['bar','bleh'],m.second.words)
|
||||||
assert m.first is o1
|
assert m.first is o1
|
||||||
assert m.second is o2
|
assert m.second is o2
|
||||||
|
|
||||||
def test_in(self):
|
def test_in(self):
|
||||||
o1 = NamedObject("foo",True)
|
o1 = NamedObject("foo",True)
|
||||||
o2 = NamedObject("bar",True)
|
o2 = NamedObject("bar",True)
|
||||||
@@ -336,15 +336,15 @@ class TestCaseget_match:
|
|||||||
assert o1 in m
|
assert o1 in m
|
||||||
assert o2 in m
|
assert o2 in m
|
||||||
assert object() not in m
|
assert object() not in m
|
||||||
|
|
||||||
def test_word_weight(self):
|
def test_word_weight(self):
|
||||||
eq_(int((6.0 / 13.0) * 100),get_match(NamedObject("foo bar",True),NamedObject("bar bleh",True),(WEIGHT_WORDS,)).percentage)
|
eq_(int((6.0 / 13.0) * 100),get_match(NamedObject("foo bar",True),NamedObject("bar bleh",True),(WEIGHT_WORDS,)).percentage)
|
||||||
|
|
||||||
|
|
||||||
class TestCaseGetMatches:
|
class TestCaseGetMatches:
|
||||||
def test_empty(self):
|
def test_empty(self):
|
||||||
eq_(getmatches([]), [])
|
eq_(getmatches([]), [])
|
||||||
|
|
||||||
def test_simple(self):
|
def test_simple(self):
|
||||||
l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject("a b c foo")]
|
l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject("a b c foo")]
|
||||||
r = getmatches(l)
|
r = getmatches(l)
|
||||||
@@ -353,7 +353,7 @@ class TestCaseGetMatches:
|
|||||||
assert_match(m, 'foo bar', 'bar bleh')
|
assert_match(m, 'foo bar', 'bar bleh')
|
||||||
m = first(m for m in r if m.percentage == 33) #"foo bar" and "a b c foo"
|
m = first(m for m in r if m.percentage == 33) #"foo bar" and "a b c foo"
|
||||||
assert_match(m, 'foo bar', 'a b c foo')
|
assert_match(m, 'foo bar', 'a b c foo')
|
||||||
|
|
||||||
def test_null_and_unrelated_objects(self):
|
def test_null_and_unrelated_objects(self):
|
||||||
l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject(""),NamedObject("unrelated object")]
|
l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject(""),NamedObject("unrelated object")]
|
||||||
r = getmatches(l)
|
r = getmatches(l)
|
||||||
@@ -361,22 +361,22 @@ class TestCaseGetMatches:
|
|||||||
m = r[0]
|
m = r[0]
|
||||||
eq_(m.percentage, 50)
|
eq_(m.percentage, 50)
|
||||||
assert_match(m, 'foo bar', 'bar bleh')
|
assert_match(m, 'foo bar', 'bar bleh')
|
||||||
|
|
||||||
def test_twice_the_same_word(self):
|
def test_twice_the_same_word(self):
|
||||||
l = [NamedObject("foo foo bar"),NamedObject("bar bleh")]
|
l = [NamedObject("foo foo bar"),NamedObject("bar bleh")]
|
||||||
r = getmatches(l)
|
r = getmatches(l)
|
||||||
eq_(1,len(r))
|
eq_(1,len(r))
|
||||||
|
|
||||||
def test_twice_the_same_word_when_preworded(self):
|
def test_twice_the_same_word_when_preworded(self):
|
||||||
l = [NamedObject("foo foo bar",True),NamedObject("bar bleh",True)]
|
l = [NamedObject("foo foo bar",True),NamedObject("bar bleh",True)]
|
||||||
r = getmatches(l)
|
r = getmatches(l)
|
||||||
eq_(1,len(r))
|
eq_(1,len(r))
|
||||||
|
|
||||||
def test_two_words_match(self):
|
def test_two_words_match(self):
|
||||||
l = [NamedObject("foo bar"),NamedObject("foo bar bleh")]
|
l = [NamedObject("foo bar"),NamedObject("foo bar bleh")]
|
||||||
r = getmatches(l)
|
r = getmatches(l)
|
||||||
eq_(1,len(r))
|
eq_(1,len(r))
|
||||||
|
|
||||||
def test_match_files_with_only_common_words(self):
|
def test_match_files_with_only_common_words(self):
|
||||||
#If a word occurs more than 50 times, it is excluded from the matching process
|
#If a word occurs more than 50 times, it is excluded from the matching process
|
||||||
#The problem with the common_word_threshold is that the files containing only common
|
#The problem with the common_word_threshold is that the files containing only common
|
||||||
@@ -385,18 +385,18 @@ class TestCaseGetMatches:
|
|||||||
l = [NamedObject("foo") for i in range(50)]
|
l = [NamedObject("foo") for i in range(50)]
|
||||||
r = getmatches(l)
|
r = getmatches(l)
|
||||||
eq_(1225,len(r))
|
eq_(1225,len(r))
|
||||||
|
|
||||||
def test_use_words_already_there_if_there(self):
|
def test_use_words_already_there_if_there(self):
|
||||||
o1 = NamedObject('foo')
|
o1 = NamedObject('foo')
|
||||||
o2 = NamedObject('bar')
|
o2 = NamedObject('bar')
|
||||||
o2.words = ['foo']
|
o2.words = ['foo']
|
||||||
eq_(1, len(getmatches([o1,o2])))
|
eq_(1, len(getmatches([o1,o2])))
|
||||||
|
|
||||||
def test_job(self):
|
def test_job(self):
|
||||||
def do_progress(p,d=''):
|
def do_progress(p,d=''):
|
||||||
self.log.append(p)
|
self.log.append(p)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
j = job.Job(1,do_progress)
|
j = job.Job(1,do_progress)
|
||||||
self.log = []
|
self.log = []
|
||||||
s = "foo bar"
|
s = "foo bar"
|
||||||
@@ -404,12 +404,12 @@ class TestCaseGetMatches:
|
|||||||
assert len(self.log) > 2
|
assert len(self.log) > 2
|
||||||
eq_(0,self.log[0])
|
eq_(0,self.log[0])
|
||||||
eq_(100,self.log[-1])
|
eq_(100,self.log[-1])
|
||||||
|
|
||||||
def test_weight_words(self):
|
def test_weight_words(self):
|
||||||
l = [NamedObject("foo bar"),NamedObject("bar bleh")]
|
l = [NamedObject("foo bar"),NamedObject("bar bleh")]
|
||||||
m = getmatches(l, weight_words=True)[0]
|
m = getmatches(l, weight_words=True)[0]
|
||||||
eq_(int((6.0 / 13.0) * 100),m.percentage)
|
eq_(int((6.0 / 13.0) * 100),m.percentage)
|
||||||
|
|
||||||
def test_similar_word(self):
|
def test_similar_word(self):
|
||||||
l = [NamedObject("foobar"),NamedObject("foobars")]
|
l = [NamedObject("foobar"),NamedObject("foobars")]
|
||||||
eq_(len(getmatches(l, match_similar_words=True)), 1)
|
eq_(len(getmatches(l, match_similar_words=True)), 1)
|
||||||
@@ -420,16 +420,16 @@ class TestCaseGetMatches:
|
|||||||
eq_(len(getmatches(l, match_similar_words=True)), 1)
|
eq_(len(getmatches(l, match_similar_words=True)), 1)
|
||||||
l = [NamedObject("foobar"),NamedObject("foosbar")]
|
l = [NamedObject("foobar"),NamedObject("foosbar")]
|
||||||
eq_(len(getmatches(l, match_similar_words=True)), 1)
|
eq_(len(getmatches(l, match_similar_words=True)), 1)
|
||||||
|
|
||||||
def test_single_object_with_similar_words(self):
|
def test_single_object_with_similar_words(self):
|
||||||
l = [NamedObject("foo foos")]
|
l = [NamedObject("foo foos")]
|
||||||
eq_(len(getmatches(l, match_similar_words=True)), 0)
|
eq_(len(getmatches(l, match_similar_words=True)), 0)
|
||||||
|
|
||||||
def test_double_words_get_counted_only_once(self):
|
def test_double_words_get_counted_only_once(self):
|
||||||
l = [NamedObject("foo bar foo bleh"),NamedObject("foo bar bleh bar")]
|
l = [NamedObject("foo bar foo bleh"),NamedObject("foo bar bleh bar")]
|
||||||
m = getmatches(l)[0]
|
m = getmatches(l)[0]
|
||||||
eq_(75,m.percentage)
|
eq_(75,m.percentage)
|
||||||
|
|
||||||
def test_with_fields(self):
|
def test_with_fields(self):
|
||||||
o1 = NamedObject("foo bar - foo bleh")
|
o1 = NamedObject("foo bar - foo bleh")
|
||||||
o2 = NamedObject("foo bar - bleh bar")
|
o2 = NamedObject("foo bar - bleh bar")
|
||||||
@@ -437,7 +437,7 @@ class TestCaseGetMatches:
|
|||||||
o2.words = getfields(o2.name)
|
o2.words = getfields(o2.name)
|
||||||
m = getmatches([o1, o2])[0]
|
m = getmatches([o1, o2])[0]
|
||||||
eq_(50, m.percentage)
|
eq_(50, m.percentage)
|
||||||
|
|
||||||
def test_with_fields_no_order(self):
|
def test_with_fields_no_order(self):
|
||||||
o1 = NamedObject("foo bar - foo bleh")
|
o1 = NamedObject("foo bar - foo bleh")
|
||||||
o2 = NamedObject("bleh bang - foo bar")
|
o2 = NamedObject("bleh bang - foo bar")
|
||||||
@@ -445,11 +445,11 @@ class TestCaseGetMatches:
|
|||||||
o2.words = getfields(o2.name)
|
o2.words = getfields(o2.name)
|
||||||
m = getmatches([o1, o2], no_field_order=True)[0]
|
m = getmatches([o1, o2], no_field_order=True)[0]
|
||||||
eq_(m.percentage, 50)
|
eq_(m.percentage, 50)
|
||||||
|
|
||||||
def test_only_match_similar_when_the_option_is_set(self):
|
def test_only_match_similar_when_the_option_is_set(self):
|
||||||
l = [NamedObject("foobar"),NamedObject("foobars")]
|
l = [NamedObject("foobar"),NamedObject("foobars")]
|
||||||
eq_(len(getmatches(l, match_similar_words=False)), 0)
|
eq_(len(getmatches(l, match_similar_words=False)), 0)
|
||||||
|
|
||||||
def test_dont_recurse_do_match(self):
|
def test_dont_recurse_do_match(self):
|
||||||
# with nosetests, the stack is increased. The number has to be high enough not to be failing falsely
|
# with nosetests, the stack is increased. The number has to be high enough not to be failing falsely
|
||||||
sys.setrecursionlimit(100)
|
sys.setrecursionlimit(100)
|
||||||
@@ -460,19 +460,19 @@ class TestCaseGetMatches:
|
|||||||
self.fail()
|
self.fail()
|
||||||
finally:
|
finally:
|
||||||
sys.setrecursionlimit(1000)
|
sys.setrecursionlimit(1000)
|
||||||
|
|
||||||
def test_min_match_percentage(self):
|
def test_min_match_percentage(self):
|
||||||
l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject("a b c foo")]
|
l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject("a b c foo")]
|
||||||
r = getmatches(l, min_match_percentage=50)
|
r = getmatches(l, min_match_percentage=50)
|
||||||
eq_(1,len(r)) #Only "foo bar" / "bar bleh" should match
|
eq_(1,len(r)) #Only "foo bar" / "bar bleh" should match
|
||||||
|
|
||||||
def test_MemoryError(self, monkeypatch):
|
def test_MemoryError(self, monkeypatch):
|
||||||
@log_calls
|
@log_calls
|
||||||
def mocked_match(first, second, flags):
|
def mocked_match(first, second, flags):
|
||||||
if len(mocked_match.calls) > 42:
|
if len(mocked_match.calls) > 42:
|
||||||
raise MemoryError()
|
raise MemoryError()
|
||||||
return Match(first, second, 0)
|
return Match(first, second, 0)
|
||||||
|
|
||||||
objects = [NamedObject() for i in range(10)] # results in 45 matches
|
objects = [NamedObject() for i in range(10)] # results in 45 matches
|
||||||
monkeypatch.setattr(engine, 'get_match', mocked_match)
|
monkeypatch.setattr(engine, 'get_match', mocked_match)
|
||||||
try:
|
try:
|
||||||
@@ -480,13 +480,13 @@ class TestCaseGetMatches:
|
|||||||
except MemoryError:
|
except MemoryError:
|
||||||
self.fail('MemorryError must be handled')
|
self.fail('MemorryError must be handled')
|
||||||
eq_(42, len(r))
|
eq_(42, len(r))
|
||||||
|
|
||||||
|
|
||||||
class TestCaseGetMatchesByContents:
|
class TestCaseGetMatchesByContents:
|
||||||
def test_dont_compare_empty_files(self):
|
def test_dont_compare_empty_files(self):
|
||||||
o1, o2 = no(size=0), no(size=0)
|
o1, o2 = no(size=0), no(size=0)
|
||||||
assert not getmatches_by_contents([o1, o2])
|
assert not getmatches_by_contents([o1, o2])
|
||||||
|
|
||||||
|
|
||||||
class TestCaseGroup:
|
class TestCaseGroup:
|
||||||
def test_empy(self):
|
def test_empy(self):
|
||||||
@@ -494,7 +494,7 @@ class TestCaseGroup:
|
|||||||
eq_(None,g.ref)
|
eq_(None,g.ref)
|
||||||
eq_([],g.dupes)
|
eq_([],g.dupes)
|
||||||
eq_(0,len(g.matches))
|
eq_(0,len(g.matches))
|
||||||
|
|
||||||
def test_add_match(self):
|
def test_add_match(self):
|
||||||
g = Group()
|
g = Group()
|
||||||
m = get_match(NamedObject("foo",True),NamedObject("bar",True))
|
m = get_match(NamedObject("foo",True),NamedObject("bar",True))
|
||||||
@@ -503,7 +503,7 @@ class TestCaseGroup:
|
|||||||
eq_([m.second],g.dupes)
|
eq_([m.second],g.dupes)
|
||||||
eq_(1,len(g.matches))
|
eq_(1,len(g.matches))
|
||||||
assert m in g.matches
|
assert m in g.matches
|
||||||
|
|
||||||
def test_multiple_add_match(self):
|
def test_multiple_add_match(self):
|
||||||
g = Group()
|
g = Group()
|
||||||
o1 = NamedObject("a",True)
|
o1 = NamedObject("a",True)
|
||||||
@@ -529,13 +529,13 @@ class TestCaseGroup:
|
|||||||
g.add_match(get_match(o3,o4))
|
g.add_match(get_match(o3,o4))
|
||||||
eq_([o2,o3,o4],g.dupes)
|
eq_([o2,o3,o4],g.dupes)
|
||||||
eq_(6,len(g.matches))
|
eq_(6,len(g.matches))
|
||||||
|
|
||||||
def test_len(self):
|
def test_len(self):
|
||||||
g = Group()
|
g = Group()
|
||||||
eq_(0,len(g))
|
eq_(0,len(g))
|
||||||
g.add_match(get_match(NamedObject("foo",True),NamedObject("bar",True)))
|
g.add_match(get_match(NamedObject("foo",True),NamedObject("bar",True)))
|
||||||
eq_(2,len(g))
|
eq_(2,len(g))
|
||||||
|
|
||||||
def test_add_same_match_twice(self):
|
def test_add_same_match_twice(self):
|
||||||
g = Group()
|
g = Group()
|
||||||
m = get_match(NamedObject("foo",True),NamedObject("foo",True))
|
m = get_match(NamedObject("foo",True),NamedObject("foo",True))
|
||||||
@@ -545,7 +545,7 @@ class TestCaseGroup:
|
|||||||
g.add_match(m)
|
g.add_match(m)
|
||||||
eq_(2,len(g))
|
eq_(2,len(g))
|
||||||
eq_(1,len(g.matches))
|
eq_(1,len(g.matches))
|
||||||
|
|
||||||
def test_in(self):
|
def test_in(self):
|
||||||
g = Group()
|
g = Group()
|
||||||
o1 = NamedObject("foo",True)
|
o1 = NamedObject("foo",True)
|
||||||
@@ -554,7 +554,7 @@ class TestCaseGroup:
|
|||||||
g.add_match(get_match(o1,o2))
|
g.add_match(get_match(o1,o2))
|
||||||
assert o1 in g
|
assert o1 in g
|
||||||
assert o2 in g
|
assert o2 in g
|
||||||
|
|
||||||
def test_remove(self):
|
def test_remove(self):
|
||||||
g = Group()
|
g = Group()
|
||||||
o1 = NamedObject("foo",True)
|
o1 = NamedObject("foo",True)
|
||||||
@@ -571,7 +571,7 @@ class TestCaseGroup:
|
|||||||
g.remove_dupe(o1)
|
g.remove_dupe(o1)
|
||||||
eq_(0,len(g.matches))
|
eq_(0,len(g.matches))
|
||||||
eq_(0,len(g))
|
eq_(0,len(g))
|
||||||
|
|
||||||
def test_remove_with_ref_dupes(self):
|
def test_remove_with_ref_dupes(self):
|
||||||
g = Group()
|
g = Group()
|
||||||
o1 = NamedObject("foo",True)
|
o1 = NamedObject("foo",True)
|
||||||
@@ -584,7 +584,7 @@ class TestCaseGroup:
|
|||||||
o2.is_ref = True
|
o2.is_ref = True
|
||||||
g.remove_dupe(o3)
|
g.remove_dupe(o3)
|
||||||
eq_(0,len(g))
|
eq_(0,len(g))
|
||||||
|
|
||||||
def test_switch_ref(self):
|
def test_switch_ref(self):
|
||||||
o1 = NamedObject(with_words=True)
|
o1 = NamedObject(with_words=True)
|
||||||
o2 = NamedObject(with_words=True)
|
o2 = NamedObject(with_words=True)
|
||||||
@@ -598,7 +598,7 @@ class TestCaseGroup:
|
|||||||
assert o2 is g.ref
|
assert o2 is g.ref
|
||||||
g.switch_ref(NamedObject('',True))
|
g.switch_ref(NamedObject('',True))
|
||||||
assert o2 is g.ref
|
assert o2 is g.ref
|
||||||
|
|
||||||
def test_switch_ref_from_ref_dir(self):
|
def test_switch_ref_from_ref_dir(self):
|
||||||
# When the ref dupe is from a ref dir, switch_ref() does nothing
|
# When the ref dupe is from a ref dir, switch_ref() does nothing
|
||||||
o1 = no(with_words=True)
|
o1 = no(with_words=True)
|
||||||
@@ -608,7 +608,7 @@ class TestCaseGroup:
|
|||||||
g.add_match(get_match(o1, o2))
|
g.add_match(get_match(o1, o2))
|
||||||
g.switch_ref(o2)
|
g.switch_ref(o2)
|
||||||
assert o1 is g.ref
|
assert o1 is g.ref
|
||||||
|
|
||||||
def test_get_match_of(self):
|
def test_get_match_of(self):
|
||||||
g = Group()
|
g = Group()
|
||||||
for m in get_match_triangle():
|
for m in get_match_triangle():
|
||||||
@@ -619,7 +619,7 @@ class TestCaseGroup:
|
|||||||
assert o in m
|
assert o in m
|
||||||
assert g.get_match_of(NamedObject('',True)) is None
|
assert g.get_match_of(NamedObject('',True)) is None
|
||||||
assert g.get_match_of(g.ref) is None
|
assert g.get_match_of(g.ref) is None
|
||||||
|
|
||||||
def test_percentage(self):
|
def test_percentage(self):
|
||||||
#percentage should return the avg percentage in relation to the ref
|
#percentage should return the avg percentage in relation to the ref
|
||||||
m1,m2,m3 = get_match_triangle()
|
m1,m2,m3 = get_match_triangle()
|
||||||
@@ -638,11 +638,11 @@ class TestCaseGroup:
|
|||||||
g.add_match(m1)
|
g.add_match(m1)
|
||||||
g.add_match(m2)
|
g.add_match(m2)
|
||||||
eq_(66,g.percentage)
|
eq_(66,g.percentage)
|
||||||
|
|
||||||
def test_percentage_on_empty_group(self):
|
def test_percentage_on_empty_group(self):
|
||||||
g = Group()
|
g = Group()
|
||||||
eq_(0,g.percentage)
|
eq_(0,g.percentage)
|
||||||
|
|
||||||
def test_prioritize(self):
|
def test_prioritize(self):
|
||||||
m1,m2,m3 = get_match_triangle()
|
m1,m2,m3 = get_match_triangle()
|
||||||
o1 = m1.first
|
o1 = m1.first
|
||||||
@@ -658,7 +658,7 @@ class TestCaseGroup:
|
|||||||
assert o1 is g.ref
|
assert o1 is g.ref
|
||||||
assert g.prioritize(lambda x:x.name)
|
assert g.prioritize(lambda x:x.name)
|
||||||
assert o3 is g.ref
|
assert o3 is g.ref
|
||||||
|
|
||||||
def test_prioritize_with_tie_breaker(self):
|
def test_prioritize_with_tie_breaker(self):
|
||||||
# if the ref has the same key as one or more of the dupe, run the tie_breaker func among them
|
# if the ref has the same key as one or more of the dupe, run the tie_breaker func among them
|
||||||
g = get_test_group()
|
g = get_test_group()
|
||||||
@@ -666,9 +666,9 @@ class TestCaseGroup:
|
|||||||
tie_breaker = lambda ref, dupe: dupe is o3
|
tie_breaker = lambda ref, dupe: dupe is o3
|
||||||
g.prioritize(lambda x:0, tie_breaker)
|
g.prioritize(lambda x:0, tie_breaker)
|
||||||
assert g.ref is o3
|
assert g.ref is o3
|
||||||
|
|
||||||
def test_prioritize_with_tie_breaker_runs_on_all_dupes(self):
|
def test_prioritize_with_tie_breaker_runs_on_all_dupes(self):
|
||||||
# Even if a dupe is chosen to switch with ref with a tie breaker, we still run the tie breaker
|
# Even if a dupe is chosen to switch with ref with a tie breaker, we still run the tie breaker
|
||||||
# with other dupes and the newly chosen ref
|
# with other dupes and the newly chosen ref
|
||||||
g = get_test_group()
|
g = get_test_group()
|
||||||
o1, o2, o3 = g.ordered
|
o1, o2, o3 = g.ordered
|
||||||
@@ -678,7 +678,7 @@ class TestCaseGroup:
|
|||||||
tie_breaker = lambda ref, dupe: dupe.foo > ref.foo
|
tie_breaker = lambda ref, dupe: dupe.foo > ref.foo
|
||||||
g.prioritize(lambda x:0, tie_breaker)
|
g.prioritize(lambda x:0, tie_breaker)
|
||||||
assert g.ref is o3
|
assert g.ref is o3
|
||||||
|
|
||||||
def test_prioritize_with_tie_breaker_runs_only_on_tie_dupes(self):
|
def test_prioritize_with_tie_breaker_runs_only_on_tie_dupes(self):
|
||||||
# The tie breaker only runs on dupes that had the same value for the key_func
|
# The tie breaker only runs on dupes that had the same value for the key_func
|
||||||
g = get_test_group()
|
g = get_test_group()
|
||||||
@@ -693,7 +693,7 @@ class TestCaseGroup:
|
|||||||
tie_breaker = lambda ref, dupe: dupe.bar > ref.bar
|
tie_breaker = lambda ref, dupe: dupe.bar > ref.bar
|
||||||
g.prioritize(key_func, tie_breaker)
|
g.prioritize(key_func, tie_breaker)
|
||||||
assert g.ref is o2
|
assert g.ref is o2
|
||||||
|
|
||||||
def test_prioritize_with_ref_dupe(self):
|
def test_prioritize_with_ref_dupe(self):
|
||||||
# when the ref dupe of a group is from a ref dir, make it stay on top.
|
# when the ref dupe of a group is from a ref dir, make it stay on top.
|
||||||
g = get_test_group()
|
g = get_test_group()
|
||||||
@@ -702,7 +702,7 @@ class TestCaseGroup:
|
|||||||
o2.size = 2
|
o2.size = 2
|
||||||
g.prioritize(lambda x: -x.size)
|
g.prioritize(lambda x: -x.size)
|
||||||
assert g.ref is o1
|
assert g.ref is o1
|
||||||
|
|
||||||
def test_prioritize_nothing_changes(self):
|
def test_prioritize_nothing_changes(self):
|
||||||
# prioritize() returns False when nothing changes in the group.
|
# prioritize() returns False when nothing changes in the group.
|
||||||
g = get_test_group()
|
g = get_test_group()
|
||||||
@@ -710,14 +710,14 @@ class TestCaseGroup:
|
|||||||
g[1].name = 'b'
|
g[1].name = 'b'
|
||||||
g[2].name = 'c'
|
g[2].name = 'c'
|
||||||
assert not g.prioritize(lambda x:x.name)
|
assert not g.prioritize(lambda x:x.name)
|
||||||
|
|
||||||
def test_list_like(self):
|
def test_list_like(self):
|
||||||
g = Group()
|
g = Group()
|
||||||
o1,o2 = (NamedObject("foo",True),NamedObject("bar",True))
|
o1,o2 = (NamedObject("foo",True),NamedObject("bar",True))
|
||||||
g.add_match(get_match(o1,o2))
|
g.add_match(get_match(o1,o2))
|
||||||
assert g[0] is o1
|
assert g[0] is o1
|
||||||
assert g[1] is o2
|
assert g[1] is o2
|
||||||
|
|
||||||
def test_discard_matches(self):
|
def test_discard_matches(self):
|
||||||
g = Group()
|
g = Group()
|
||||||
o1,o2,o3 = (NamedObject("foo",True),NamedObject("bar",True),NamedObject("baz",True))
|
o1,o2,o3 = (NamedObject("foo",True),NamedObject("bar",True),NamedObject("baz",True))
|
||||||
@@ -726,13 +726,13 @@ class TestCaseGroup:
|
|||||||
g.discard_matches()
|
g.discard_matches()
|
||||||
eq_(1,len(g.matches))
|
eq_(1,len(g.matches))
|
||||||
eq_(0,len(g.candidates))
|
eq_(0,len(g.candidates))
|
||||||
|
|
||||||
|
|
||||||
class TestCaseget_groups:
|
class TestCaseget_groups:
|
||||||
def test_empty(self):
|
def test_empty(self):
|
||||||
r = get_groups([])
|
r = get_groups([])
|
||||||
eq_([],r)
|
eq_([],r)
|
||||||
|
|
||||||
def test_simple(self):
|
def test_simple(self):
|
||||||
l = [NamedObject("foo bar"),NamedObject("bar bleh")]
|
l = [NamedObject("foo bar"),NamedObject("bar bleh")]
|
||||||
matches = getmatches(l)
|
matches = getmatches(l)
|
||||||
@@ -742,7 +742,7 @@ class TestCaseget_groups:
|
|||||||
g = r[0]
|
g = r[0]
|
||||||
assert g.ref is m.first
|
assert g.ref is m.first
|
||||||
eq_([m.second],g.dupes)
|
eq_([m.second],g.dupes)
|
||||||
|
|
||||||
def test_group_with_multiple_matches(self):
|
def test_group_with_multiple_matches(self):
|
||||||
#This results in 3 matches
|
#This results in 3 matches
|
||||||
l = [NamedObject("foo"),NamedObject("foo"),NamedObject("foo")]
|
l = [NamedObject("foo"),NamedObject("foo"),NamedObject("foo")]
|
||||||
@@ -751,7 +751,7 @@ class TestCaseget_groups:
|
|||||||
eq_(1,len(r))
|
eq_(1,len(r))
|
||||||
g = r[0]
|
g = r[0]
|
||||||
eq_(3,len(g))
|
eq_(3,len(g))
|
||||||
|
|
||||||
def test_must_choose_a_group(self):
|
def test_must_choose_a_group(self):
|
||||||
l = [NamedObject("a b"),NamedObject("a b"),NamedObject("b c"),NamedObject("c d"),NamedObject("c d")]
|
l = [NamedObject("a b"),NamedObject("a b"),NamedObject("b c"),NamedObject("c d"),NamedObject("c d")]
|
||||||
#There will be 2 groups here: group "a b" and group "c d"
|
#There will be 2 groups here: group "a b" and group "c d"
|
||||||
@@ -760,7 +760,7 @@ class TestCaseget_groups:
|
|||||||
r = get_groups(matches)
|
r = get_groups(matches)
|
||||||
eq_(2,len(r))
|
eq_(2,len(r))
|
||||||
eq_(5,len(r[0])+len(r[1]))
|
eq_(5,len(r[0])+len(r[1]))
|
||||||
|
|
||||||
def test_should_all_go_in_the_same_group(self):
|
def test_should_all_go_in_the_same_group(self):
|
||||||
l = [NamedObject("a b"),NamedObject("a b"),NamedObject("a b"),NamedObject("a b")]
|
l = [NamedObject("a b"),NamedObject("a b"),NamedObject("a b"),NamedObject("a b")]
|
||||||
#There will be 2 groups here: group "a b" and group "c d"
|
#There will be 2 groups here: group "a b" and group "c d"
|
||||||
@@ -768,7 +768,7 @@ class TestCaseget_groups:
|
|||||||
matches = getmatches(l)
|
matches = getmatches(l)
|
||||||
r = get_groups(matches)
|
r = get_groups(matches)
|
||||||
eq_(1,len(r))
|
eq_(1,len(r))
|
||||||
|
|
||||||
def test_give_priority_to_matches_with_higher_percentage(self):
|
def test_give_priority_to_matches_with_higher_percentage(self):
|
||||||
o1 = NamedObject(with_words=True)
|
o1 = NamedObject(with_words=True)
|
||||||
o2 = NamedObject(with_words=True)
|
o2 = NamedObject(with_words=True)
|
||||||
@@ -782,14 +782,14 @@ class TestCaseget_groups:
|
|||||||
assert o1 not in g
|
assert o1 not in g
|
||||||
assert o2 in g
|
assert o2 in g
|
||||||
assert o3 in g
|
assert o3 in g
|
||||||
|
|
||||||
def test_four_sized_group(self):
|
def test_four_sized_group(self):
|
||||||
l = [NamedObject("foobar") for i in range(4)]
|
l = [NamedObject("foobar") for i in range(4)]
|
||||||
m = getmatches(l)
|
m = getmatches(l)
|
||||||
r = get_groups(m)
|
r = get_groups(m)
|
||||||
eq_(1,len(r))
|
eq_(1,len(r))
|
||||||
eq_(4,len(r[0]))
|
eq_(4,len(r[0]))
|
||||||
|
|
||||||
def test_referenced_by_ref2(self):
|
def test_referenced_by_ref2(self):
|
||||||
o1 = NamedObject(with_words=True)
|
o1 = NamedObject(with_words=True)
|
||||||
o2 = NamedObject(with_words=True)
|
o2 = NamedObject(with_words=True)
|
||||||
@@ -799,12 +799,12 @@ class TestCaseget_groups:
|
|||||||
m3 = get_match(o3,o2)
|
m3 = get_match(o3,o2)
|
||||||
r = get_groups([m1,m2,m3])
|
r = get_groups([m1,m2,m3])
|
||||||
eq_(3,len(r[0]))
|
eq_(3,len(r[0]))
|
||||||
|
|
||||||
def test_job(self):
|
def test_job(self):
|
||||||
def do_progress(p,d=''):
|
def do_progress(p,d=''):
|
||||||
self.log.append(p)
|
self.log.append(p)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
self.log = []
|
self.log = []
|
||||||
j = job.Job(1,do_progress)
|
j = job.Job(1,do_progress)
|
||||||
m1,m2,m3 = get_match_triangle()
|
m1,m2,m3 = get_match_triangle()
|
||||||
@@ -813,7 +813,7 @@ class TestCaseget_groups:
|
|||||||
get_groups([m1,m2,m3,m4],j)
|
get_groups([m1,m2,m3,m4],j)
|
||||||
eq_(0,self.log[0])
|
eq_(0,self.log[0])
|
||||||
eq_(100,self.log[-1])
|
eq_(100,self.log[-1])
|
||||||
|
|
||||||
def test_group_admissible_discarded_dupes(self):
|
def test_group_admissible_discarded_dupes(self):
|
||||||
# If, with a (A, B, C, D) set, all match with A, but C and D don't match with B and that the
|
# If, with a (A, B, C, D) set, all match with A, but C and D don't match with B and that the
|
||||||
# (A, B) match is the highest (thus resulting in an (A, B) group), still match C and D
|
# (A, B) match is the highest (thus resulting in an (A, B) group), still match C and D
|
||||||
@@ -830,4 +830,4 @@ class TestCaseget_groups:
|
|||||||
assert B in g1
|
assert B in g1
|
||||||
assert C in g2
|
assert C in g2
|
||||||
assert D in g2
|
assert D in g2
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2006/03/03
|
# Created On: 2006/03/03
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from jobprogress import job
|
from hscommon.jobprogress import job
|
||||||
from hscommon.path import Path
|
from hscommon.path import Path
|
||||||
from hscommon.testutil import eq_
|
from hscommon.testutil import eq_
|
||||||
|
|
||||||
@@ -25,10 +25,10 @@ class NamedObject:
|
|||||||
self.size = size
|
self.size = size
|
||||||
self.path = path
|
self.path = path
|
||||||
self.words = getwords(name)
|
self.words = getwords(name)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<NamedObject %r %r>' % (self.name, self.path)
|
return '<NamedObject %r %r>' % (self.name, self.path)
|
||||||
|
|
||||||
|
|
||||||
no = NamedObject
|
no = NamedObject
|
||||||
|
|
||||||
@@ -384,7 +384,7 @@ def test_file_evaluates_to_false(fake_fileexists):
|
|||||||
class FalseNamedObject(NamedObject):
|
class FalseNamedObject(NamedObject):
|
||||||
def __bool__(self):
|
def __bool__(self):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
s = Scanner()
|
s = Scanner()
|
||||||
f1 = FalseNamedObject('foobar', path='p1')
|
f1 = FalseNamedObject('foobar', path='p1')
|
||||||
@@ -445,7 +445,7 @@ def test_tie_breaker_same_name_plus_digit(fake_fileexists):
|
|||||||
assert group.ref is o5
|
assert group.ref is o5
|
||||||
|
|
||||||
def test_partial_group_match(fake_fileexists):
|
def test_partial_group_match(fake_fileexists):
|
||||||
# Count the number of discarded matches (when a file doesn't match all other dupes of the
|
# Count the number of discarded matches (when a file doesn't match all other dupes of the
|
||||||
# group) in Scanner.discarded_file_count
|
# group) in Scanner.discarded_file_count
|
||||||
s = Scanner()
|
s = Scanner()
|
||||||
o1, o2, o3 = no('a b'), no('a'), no('b')
|
o1, o2, o3 = no('a b'), no('a'), no('b')
|
||||||
@@ -476,7 +476,7 @@ def test_dont_group_files_that_dont_exist(tmpdir):
|
|||||||
file2.path.remove()
|
file2.path.remove()
|
||||||
return [Match(file1, file2, 100)]
|
return [Match(file1, file2, 100)]
|
||||||
s._getmatches = getmatches
|
s._getmatches = getmatches
|
||||||
|
|
||||||
assert not s.get_dupe_groups([file1, file2])
|
assert not s.get_dupe_groups([file1, file2])
|
||||||
|
|
||||||
def test_folder_scan_exclude_subfolder_matches(fake_fileexists):
|
def test_folder_scan_exclude_subfolder_matches(fake_fileexists):
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
__version__ = '6.7.0'
|
__version__ = '6.8.0'
|
||||||
__appname__ = 'dupeGuru Music Edition'
|
__appname__ = 'dupeGuru Music Edition'
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Created On: 2011/09/20
|
# Created On: 2011/09/20
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from core.app import DupeGuru as DupeGuruBase
|
from core.app import DupeGuru as DupeGuruBase
|
||||||
@@ -13,28 +13,30 @@ from .result_table import ResultTable
|
|||||||
|
|
||||||
class DupeGuru(DupeGuruBase):
|
class DupeGuru(DupeGuruBase):
|
||||||
NAME = __appname__
|
NAME = __appname__
|
||||||
METADATA_TO_READ = ['size', 'mtime', 'duration', 'bitrate', 'samplerate', 'title', 'artist',
|
METADATA_TO_READ = [
|
||||||
'album', 'genre', 'year', 'track', 'comment']
|
'size', 'mtime', 'duration', 'bitrate', 'samplerate', 'title', 'artist',
|
||||||
|
'album', 'genre', 'year', 'track', 'comment'
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self, view):
|
def __init__(self, view):
|
||||||
DupeGuruBase.__init__(self, view)
|
DupeGuruBase.__init__(self, view)
|
||||||
self.scanner = scanner.ScannerME()
|
self.scanner = scanner.ScannerME()
|
||||||
self.directories.fileclasses = [fs.MusicFile]
|
self.directories.fileclasses = [fs.MusicFile]
|
||||||
|
|
||||||
def _get_dupe_sort_key(self, dupe, get_group, key, delta):
|
def _get_dupe_sort_key(self, dupe, get_group, key, delta):
|
||||||
if key == 'folder_path':
|
if key == 'folder_path':
|
||||||
dupe_folder_path = getattr(dupe, 'display_folder_path', dupe.folder_path)
|
dupe_folder_path = getattr(dupe, 'display_folder_path', dupe.folder_path)
|
||||||
return str(dupe_folder_path).lower()
|
return str(dupe_folder_path).lower()
|
||||||
return DupeGuruBase._get_dupe_sort_key(self, dupe, get_group, key, delta)
|
return DupeGuruBase._get_dupe_sort_key(self, dupe, get_group, key, delta)
|
||||||
|
|
||||||
def _get_group_sort_key(self, group, key):
|
def _get_group_sort_key(self, group, key):
|
||||||
if key == 'folder_path':
|
if key == 'folder_path':
|
||||||
dupe_folder_path = getattr(group.ref, 'display_folder_path', group.ref.folder_path)
|
dupe_folder_path = getattr(group.ref, 'display_folder_path', group.ref.folder_path)
|
||||||
return str(dupe_folder_path).lower()
|
return str(dupe_folder_path).lower()
|
||||||
return DupeGuruBase._get_group_sort_key(self, group, key)
|
return DupeGuruBase._get_group_sort_key(self, group, key)
|
||||||
|
|
||||||
def _prioritization_categories(self):
|
def _prioritization_categories(self):
|
||||||
return prioritize.all_categories()
|
return prioritize.all_categories()
|
||||||
|
|
||||||
def _create_result_table(self):
|
def _create_result_table(self):
|
||||||
return ResultTable(self)
|
return ResultTable(self)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2009-10-23
|
# Created On: 2009-10-23
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from hsaudiotag import auto
|
from hsaudiotag import auto
|
||||||
@@ -12,32 +12,34 @@ 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.app import format_timestamp, format_perc, format_words, format_dupe_count
|
||||||
from core import fs
|
from core import fs
|
||||||
|
|
||||||
TAG_FIELDS = {'audiosize', 'duration', 'bitrate', 'samplerate', 'title', 'artist',
|
TAG_FIELDS = {
|
||||||
'album', 'genre', 'year', 'track', 'comment'}
|
'audiosize', 'duration', 'bitrate', 'samplerate', 'title', 'artist',
|
||||||
|
'album', 'genre', 'year', 'track', 'comment'
|
||||||
|
}
|
||||||
|
|
||||||
class MusicFile(fs.File):
|
class MusicFile(fs.File):
|
||||||
INITIAL_INFO = fs.File.INITIAL_INFO.copy()
|
INITIAL_INFO = fs.File.INITIAL_INFO.copy()
|
||||||
INITIAL_INFO.update({
|
INITIAL_INFO.update({
|
||||||
'audiosize': 0,
|
'audiosize': 0,
|
||||||
'bitrate' : 0,
|
'bitrate': 0,
|
||||||
'duration' : 0,
|
'duration': 0,
|
||||||
'samplerate':0,
|
'samplerate': 0,
|
||||||
'artist' : '',
|
'artist': '',
|
||||||
'album' : '',
|
'album': '',
|
||||||
'title' : '',
|
'title': '',
|
||||||
'genre' : '',
|
'genre': '',
|
||||||
'comment' : '',
|
'comment': '',
|
||||||
'year' : '',
|
'year': '',
|
||||||
'track' : 0,
|
'track': 0,
|
||||||
})
|
})
|
||||||
__slots__ = fs.File.__slots__ + tuple(INITIAL_INFO.keys())
|
__slots__ = fs.File.__slots__ + tuple(INITIAL_INFO.keys())
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def can_handle(cls, path):
|
def can_handle(cls, path):
|
||||||
if not fs.File.can_handle(path):
|
if not fs.File.can_handle(path):
|
||||||
return False
|
return False
|
||||||
return get_file_ext(path.name) in auto.EXT2CLASS
|
return get_file_ext(path.name) in auto.EXT2CLASS
|
||||||
|
|
||||||
def get_display_info(self, group, delta):
|
def get_display_info(self, group, delta):
|
||||||
size = self.size
|
size = self.size
|
||||||
duration = self.duration
|
duration = self.duration
|
||||||
@@ -67,7 +69,7 @@ class MusicFile(fs.File):
|
|||||||
'bitrate': str(bitrate),
|
'bitrate': str(bitrate),
|
||||||
'samplerate': str(samplerate),
|
'samplerate': str(samplerate),
|
||||||
'extension': self.extension,
|
'extension': self.extension,
|
||||||
'mtime': format_timestamp(mtime,delta and m),
|
'mtime': format_timestamp(mtime, delta and m),
|
||||||
'title': self.title,
|
'title': self.title,
|
||||||
'artist': self.artist,
|
'artist': self.artist,
|
||||||
'album': self.album,
|
'album': self.album,
|
||||||
@@ -79,11 +81,11 @@ class MusicFile(fs.File):
|
|||||||
'words': format_words(self.words) if hasattr(self, 'words') else '',
|
'words': format_words(self.words) if hasattr(self, 'words') else '',
|
||||||
'dupe_count': format_dupe_count(dupe_count),
|
'dupe_count': format_dupe_count(dupe_count),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_md5partial_offset_and_size(self):
|
def _get_md5partial_offset_and_size(self):
|
||||||
f = auto.File(str(self.path))
|
f = auto.File(str(self.path))
|
||||||
return (f.audio_offset, f.audio_size)
|
return (f.audio_offset, f.audio_size)
|
||||||
|
|
||||||
def _read_info(self, field):
|
def _read_info(self, field):
|
||||||
fs.File._read_info(self, field)
|
fs.File._read_info(self, field)
|
||||||
if field in TAG_FIELDS:
|
if field in TAG_FIELDS:
|
||||||
@@ -99,4 +101,4 @@ class MusicFile(fs.File):
|
|||||||
self.comment = f.comment
|
self.comment = f.comment
|
||||||
self.year = f.year
|
self.year = f.year
|
||||||
self.track = f.track
|
self.track = f.track
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +1,40 @@
|
|||||||
# Created On: 2011/09/16
|
# Created On: 2011/09/16
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
|
|
||||||
from core.prioritize import (KindCategory, FolderCategory, FilenameCategory, NumericalCategory,
|
from core.prioritize import (
|
||||||
SizeCategory, MtimeCategory)
|
KindCategory, FolderCategory, FilenameCategory, NumericalCategory,
|
||||||
|
SizeCategory, MtimeCategory
|
||||||
|
)
|
||||||
|
|
||||||
coltr = trget('columns')
|
coltr = trget('columns')
|
||||||
|
|
||||||
class DurationCategory(NumericalCategory):
|
class DurationCategory(NumericalCategory):
|
||||||
NAME = coltr("Duration")
|
NAME = coltr("Duration")
|
||||||
|
|
||||||
def extract_value(self, dupe):
|
def extract_value(self, dupe):
|
||||||
return dupe.duration
|
return dupe.duration
|
||||||
|
|
||||||
class BitrateCategory(NumericalCategory):
|
class BitrateCategory(NumericalCategory):
|
||||||
NAME = coltr("Bitrate")
|
NAME = coltr("Bitrate")
|
||||||
|
|
||||||
def extract_value(self, dupe):
|
def extract_value(self, dupe):
|
||||||
return dupe.bitrate
|
return dupe.bitrate
|
||||||
|
|
||||||
class SamplerateCategory(NumericalCategory):
|
class SamplerateCategory(NumericalCategory):
|
||||||
NAME = coltr("Samplerate")
|
NAME = coltr("Samplerate")
|
||||||
|
|
||||||
def extract_value(self, dupe):
|
def extract_value(self, dupe):
|
||||||
return dupe.samplerate
|
return dupe.samplerate
|
||||||
|
|
||||||
def all_categories():
|
def all_categories():
|
||||||
return [KindCategory, FolderCategory, FilenameCategory, SizeCategory, DurationCategory,
|
return [
|
||||||
BitrateCategory, SamplerateCategory, MtimeCategory]
|
KindCategory, FolderCategory, FilenameCategory, SizeCategory, DurationCategory,
|
||||||
|
BitrateCategory, SamplerateCategory, MtimeCategory
|
||||||
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
__version__ = '2.10.0'
|
__version__ = '2.10.1'
|
||||||
__appname__ = 'dupeGuru Picture Edition'
|
__appname__ = 'dupeGuru Picture Edition'
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2006/09/01
|
# Created On: 2006/09/01
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from ._block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2
|
from ._block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2 # NOQA
|
||||||
|
|
||||||
# Converted to C
|
# Converted to C
|
||||||
# def getblock(image):
|
# def getblock(image):
|
||||||
# """Returns a 3 sized tuple containing the mean color of 'image'.
|
# """Returns a 3 sized tuple containing the mean color of 'image'.
|
||||||
#
|
#
|
||||||
# image: a PIL image or crop.
|
# image: a PIL image or crop.
|
||||||
# """
|
# """
|
||||||
# if image.size[0]:
|
# if image.size[0]:
|
||||||
@@ -28,7 +28,7 @@ from ._block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2
|
|||||||
# This is not used anymore
|
# This is not used anymore
|
||||||
# def getblocks(image,blocksize):
|
# def getblocks(image,blocksize):
|
||||||
# """Returns a list of blocks (3 sized tuples).
|
# """Returns a list of blocks (3 sized tuples).
|
||||||
#
|
#
|
||||||
# image: A PIL image to base the blocks on.
|
# image: A PIL image to base the blocks on.
|
||||||
# blocksize: The size of the blocks to be create. This is a single integer, defining
|
# blocksize: The size of the blocks to be create. This is a single integer, defining
|
||||||
# both width and height (blocks are square).
|
# both width and height (blocks are square).
|
||||||
@@ -46,7 +46,7 @@ from ._block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2
|
|||||||
# Converted to C
|
# Converted to C
|
||||||
# def getblocks2(image,block_count_per_side):
|
# def getblocks2(image,block_count_per_side):
|
||||||
# """Returns a list of blocks (3 sized tuples).
|
# """Returns a list of blocks (3 sized tuples).
|
||||||
#
|
#
|
||||||
# image: A PIL image to base the blocks on.
|
# image: A PIL image to base the blocks on.
|
||||||
# block_count_per_side: This integer determine the number of blocks the function will return.
|
# block_count_per_side: This integer determine the number of blocks the function will return.
|
||||||
# If it is 10, for example, 100 blocks will be returns (10 width, 10 height). The blocks will not
|
# If it is 10, for example, 100 blocks will be returns (10 width, 10 height). The blocks will not
|
||||||
@@ -73,7 +73,7 @@ from ._block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2
|
|||||||
# Converted to C
|
# Converted to C
|
||||||
# def diff(first, second):
|
# def diff(first, second):
|
||||||
# """Returns the difference between the first block and the second.
|
# """Returns the difference between the first block and the second.
|
||||||
#
|
#
|
||||||
# It returns an absolute sum of the 3 differences (RGB).
|
# It returns an absolute sum of the 3 differences (RGB).
|
||||||
# """
|
# """
|
||||||
# r1, g1, b1 = first
|
# r1, g1, b1 = first
|
||||||
@@ -83,7 +83,7 @@ from ._block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2
|
|||||||
# Converted to C
|
# Converted to C
|
||||||
# def avgdiff(first, second, limit=768, min_iterations=1):
|
# def avgdiff(first, second, limit=768, min_iterations=1):
|
||||||
# """Returns the average diff between first blocks and seconds.
|
# """Returns the average diff between first blocks and seconds.
|
||||||
#
|
#
|
||||||
# If the result surpasses limit, limit + 1 is returned, except if less than min_iterations
|
# If the result surpasses limit, limit + 1 is returned, except if less than min_iterations
|
||||||
# iterations have been made in the blocks.
|
# iterations have been made in the blocks.
|
||||||
# """
|
# """
|
||||||
@@ -106,7 +106,7 @@ from ._block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2
|
|||||||
# This is not used anymore
|
# This is not used anymore
|
||||||
# def maxdiff(first,second,limit=768):
|
# def maxdiff(first,second,limit=768):
|
||||||
# """Returns the max diff between first blocks and seconds.
|
# """Returns the max diff between first blocks and seconds.
|
||||||
#
|
#
|
||||||
# If the result surpasses limit, the first max being over limit is returned.
|
# If the result surpasses limit, the first max being over limit is returned.
|
||||||
# """
|
# """
|
||||||
# if len(first) != len(second):
|
# if len(first) != len(second):
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2006/09/14
|
# Created On: 2006/09/14
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -15,11 +15,11 @@ from ._cache import string_to_colors
|
|||||||
|
|
||||||
def colors_to_string(colors):
|
def colors_to_string(colors):
|
||||||
"""Transform the 3 sized tuples 'colors' into a hex string.
|
"""Transform the 3 sized tuples 'colors' into a hex string.
|
||||||
|
|
||||||
[(0,100,255)] --> 0064ff
|
[(0,100,255)] --> 0064ff
|
||||||
[(1,2,3),(4,5,6)] --> 010203040506
|
[(1,2,3),(4,5,6)] --> 010203040506
|
||||||
"""
|
"""
|
||||||
return ''.join(['%02x%02x%02x' % (r,g,b) for r,g,b in colors])
|
return ''.join(['%02x%02x%02x' % (r, g, b) for r, g, b in colors])
|
||||||
|
|
||||||
# This function is an important bottleneck of dupeGuru PE. It has been converted to C.
|
# This function is an important bottleneck of dupeGuru PE. It has been converted to C.
|
||||||
# def string_to_colors(s):
|
# def string_to_colors(s):
|
||||||
@@ -38,18 +38,18 @@ class Cache:
|
|||||||
self.dbname = db
|
self.dbname = db
|
||||||
self.con = None
|
self.con = None
|
||||||
self._create_con()
|
self._create_con()
|
||||||
|
|
||||||
def __contains__(self, key):
|
def __contains__(self, key):
|
||||||
sql = "select count(*) from pictures where path = ?"
|
sql = "select count(*) from pictures where path = ?"
|
||||||
result = self.con.execute(sql, [key]).fetchall()
|
result = self.con.execute(sql, [key]).fetchall()
|
||||||
return result[0][0] > 0
|
return result[0][0] > 0
|
||||||
|
|
||||||
def __delitem__(self, key):
|
def __delitem__(self, key):
|
||||||
if key not in self:
|
if key not in self:
|
||||||
raise KeyError(key)
|
raise KeyError(key)
|
||||||
sql = "delete from pictures where path = ?"
|
sql = "delete from pictures where path = ?"
|
||||||
self.con.execute(sql, [key])
|
self.con.execute(sql, [key])
|
||||||
|
|
||||||
# Optimized
|
# Optimized
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
if isinstance(key, int):
|
if isinstance(key, int):
|
||||||
@@ -62,17 +62,17 @@ class Cache:
|
|||||||
return result
|
return result
|
||||||
else:
|
else:
|
||||||
raise KeyError(key)
|
raise KeyError(key)
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
sql = "select path from pictures"
|
sql = "select path from pictures"
|
||||||
result = self.con.execute(sql)
|
result = self.con.execute(sql)
|
||||||
return (row[0] for row in result)
|
return (row[0] for row in result)
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
sql = "select count(*) from pictures"
|
sql = "select count(*) from pictures"
|
||||||
result = self.con.execute(sql).fetchall()
|
result = self.con.execute(sql).fetchall()
|
||||||
return result[0][0]
|
return result[0][0]
|
||||||
|
|
||||||
def __setitem__(self, path_str, blocks):
|
def __setitem__(self, path_str, blocks):
|
||||||
blocks = colors_to_string(blocks)
|
blocks = colors_to_string(blocks)
|
||||||
if op.exists(path_str):
|
if op.exists(path_str):
|
||||||
@@ -89,15 +89,15 @@ class Cache:
|
|||||||
logging.warning('Picture cache could not set value for key %r', path_str)
|
logging.warning('Picture cache could not set value for key %r', path_str)
|
||||||
except sqlite.DatabaseError as e:
|
except sqlite.DatabaseError as e:
|
||||||
logging.warning('DatabaseError while setting value for key %r: %s', path_str, str(e))
|
logging.warning('DatabaseError while setting value for key %r: %s', path_str, str(e))
|
||||||
|
|
||||||
def _create_con(self, second_try=False):
|
def _create_con(self, second_try=False):
|
||||||
def create_tables():
|
def create_tables():
|
||||||
logging.debug("Creating picture cache tables.")
|
logging.debug("Creating picture cache tables.")
|
||||||
self.con.execute("drop table if exists pictures");
|
self.con.execute("drop table if exists pictures")
|
||||||
self.con.execute("drop index if exists idx_path");
|
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 table pictures(path TEXT, mtime INTEGER, blocks TEXT)")
|
||||||
self.con.execute("create index idx_path on pictures (path)")
|
self.con.execute("create index idx_path on pictures (path)")
|
||||||
|
|
||||||
self.con = sqlite.connect(self.dbname, isolation_level=None)
|
self.con = sqlite.connect(self.dbname, isolation_level=None)
|
||||||
try:
|
try:
|
||||||
self.con.execute("select path, mtime, blocks from pictures where 1=2")
|
self.con.execute("select path, mtime, blocks from pictures where 1=2")
|
||||||
@@ -110,23 +110,23 @@ class Cache:
|
|||||||
self.con.close()
|
self.con.close()
|
||||||
os.remove(self.dbname)
|
os.remove(self.dbname)
|
||||||
self._create_con(second_try=True)
|
self._create_con(second_try=True)
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
self.close()
|
self.close()
|
||||||
if self.dbname != ':memory:':
|
if self.dbname != ':memory:':
|
||||||
os.remove(self.dbname)
|
os.remove(self.dbname)
|
||||||
self._create_con()
|
self._create_con()
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
if self.con is not None:
|
if self.con is not None:
|
||||||
self.con.close()
|
self.con.close()
|
||||||
self.con = None
|
self.con = None
|
||||||
|
|
||||||
def filter(self, func):
|
def filter(self, func):
|
||||||
to_delete = [key for key in self if not func(key)]
|
to_delete = [key for key in self if not func(key)]
|
||||||
for key in to_delete:
|
for key in to_delete:
|
||||||
del self[key]
|
del self[key]
|
||||||
|
|
||||||
def get_id(self, path):
|
def get_id(self, path):
|
||||||
sql = "select rowid from pictures where path = ?"
|
sql = "select rowid from pictures where path = ?"
|
||||||
result = self.con.execute(sql, [path]).fetchone()
|
result = self.con.execute(sql, [path]).fetchone()
|
||||||
@@ -134,15 +134,15 @@ class Cache:
|
|||||||
return result[0]
|
return result[0]
|
||||||
else:
|
else:
|
||||||
raise ValueError(path)
|
raise ValueError(path)
|
||||||
|
|
||||||
def get_multiple(self, rowids):
|
def get_multiple(self, rowids):
|
||||||
sql = "select rowid, blocks from pictures where rowid in (%s)" % ','.join(map(str, rowids))
|
sql = "select rowid, blocks from pictures where rowid in (%s)" % ','.join(map(str, rowids))
|
||||||
cur = self.con.execute(sql)
|
cur = self.con.execute(sql)
|
||||||
return ((rowid, string_to_colors(blocks)) for rowid, blocks in cur)
|
return ((rowid, string_to_colors(blocks)) for rowid, blocks in cur)
|
||||||
|
|
||||||
def purge_outdated(self):
|
def purge_outdated(self):
|
||||||
"""Go through the cache and purge outdated records.
|
"""Go through the cache and purge outdated records.
|
||||||
|
|
||||||
A record is outdated if the picture doesn't exist or if its mtime is greater than the one in
|
A record is outdated if the picture doesn't exist or if its mtime is greater than the one in
|
||||||
the db.
|
the db.
|
||||||
"""
|
"""
|
||||||
@@ -159,4 +159,4 @@ class Cache:
|
|||||||
if todelete:
|
if todelete:
|
||||||
sql = "delete from pictures where rowid in (%s)" % ','.join(map(str, todelete))
|
sql = "delete from pictures where rowid in (%s)" % ','.join(map(str, todelete))
|
||||||
self.con.execute(sql)
|
self.con.execute(sql)
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2011-04-20
|
# Created On: 2011-04-20
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
# Heavily based on http://topo.math.u-psud.fr/~bousch/exifdump.py by Thierry Bousch (Public Domain)
|
# Heavily based on http://topo.math.u-psud.fr/~bousch/exifdump.py by Thierry Bousch (Public Domain)
|
||||||
@@ -181,7 +181,7 @@ class Fraction:
|
|||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '%d/%d' % (self.num, self.den)
|
return '%d/%d' % (self.num, self.den)
|
||||||
|
|
||||||
|
|
||||||
class TIFF_file:
|
class TIFF_file:
|
||||||
def __init__(self, data):
|
def __init__(self, data):
|
||||||
@@ -201,14 +201,14 @@ class TIFF_file:
|
|||||||
logging.debug(self.endian)
|
logging.debug(self.endian)
|
||||||
logging.debug("Slice for offset %d length %d: %r and value: %d", offset, length, slice, val)
|
logging.debug("Slice for offset %d length %d: %r and value: %d", offset, length, slice, val)
|
||||||
return val
|
return val
|
||||||
|
|
||||||
def first_IFD(self):
|
def first_IFD(self):
|
||||||
return self.s2n(4, 4)
|
return self.s2n(4, 4)
|
||||||
|
|
||||||
def next_IFD(self, ifd):
|
def next_IFD(self, ifd):
|
||||||
entries = self.s2n(ifd, 2)
|
entries = self.s2n(ifd, 2)
|
||||||
return self.s2n(ifd + 2 + 12 * entries, 4)
|
return self.s2n(ifd + 2 + 12 * entries, 4)
|
||||||
|
|
||||||
def list_IFDs(self):
|
def list_IFDs(self):
|
||||||
i = self.first_IFD()
|
i = self.first_IFD()
|
||||||
a = []
|
a = []
|
||||||
@@ -216,7 +216,7 @@ class TIFF_file:
|
|||||||
a.append(i)
|
a.append(i)
|
||||||
i = self.next_IFD(i)
|
i = self.next_IFD(i)
|
||||||
return a
|
return a
|
||||||
|
|
||||||
def dump_IFD(self, ifd):
|
def dump_IFD(self, ifd):
|
||||||
entries = self.s2n(ifd, 2)
|
entries = self.s2n(ifd, 2)
|
||||||
logging.debug("Entries for IFD %d: %d", ifd, entries)
|
logging.debug("Entries for IFD %d: %d", ifd, entries)
|
||||||
@@ -230,7 +230,7 @@ class TIFF_file:
|
|||||||
type = self.s2n(entry+2, 2)
|
type = self.s2n(entry+2, 2)
|
||||||
if not 1 <= type <= 10:
|
if not 1 <= type <= 10:
|
||||||
continue # not handled
|
continue # not handled
|
||||||
typelen = [ 1, 1, 2, 4, 8, 1, 1, 2, 4, 8 ] [type-1]
|
typelen = [1, 1, 2, 4, 8, 1, 1, 2, 4, 8][type-1]
|
||||||
count = self.s2n(entry+4, 4)
|
count = self.s2n(entry+4, 4)
|
||||||
if count > MAX_COUNT:
|
if count > MAX_COUNT:
|
||||||
logging.debug("Probably corrupt. Aborting.")
|
logging.debug("Probably corrupt. Aborting.")
|
||||||
@@ -247,7 +247,7 @@ class TIFF_file:
|
|||||||
for j in range(count):
|
for j in range(count):
|
||||||
if type in {5, 10}:
|
if type in {5, 10}:
|
||||||
# The type is either 5 or 10
|
# The type is either 5 or 10
|
||||||
value_j = Fraction(self.s2n(offset, 4, signed),
|
value_j = Fraction(self.s2n(offset, 4, signed),
|
||||||
self.s2n(offset+4, 4, signed))
|
self.s2n(offset+4, 4, signed))
|
||||||
else:
|
else:
|
||||||
# Not a fraction
|
# Not a fraction
|
||||||
@@ -255,7 +255,7 @@ class TIFF_file:
|
|||||||
values.append(value_j)
|
values.append(value_j)
|
||||||
offset = offset + typelen
|
offset = offset + typelen
|
||||||
# Now "values" is either a string or an array
|
# Now "values" is either a string or an array
|
||||||
a.append((tag,type,values))
|
a.append((tag, type, values))
|
||||||
return a
|
return a
|
||||||
|
|
||||||
def read_exif_header(fp):
|
def read_exif_header(fp):
|
||||||
@@ -283,13 +283,13 @@ def get_fields(fp):
|
|||||||
logging.debug("Exif header length: %d bytes", length)
|
logging.debug("Exif header length: %d bytes", length)
|
||||||
data = fp.read(length-8)
|
data = fp.read(length-8)
|
||||||
data_format = data[0]
|
data_format = data[0]
|
||||||
logging.debug("%s format", {INTEL_ENDIAN:'Intel', MOTOROLA_ENDIAN:'Motorola'}[data_format])
|
logging.debug("%s format", {INTEL_ENDIAN: 'Intel', MOTOROLA_ENDIAN: 'Motorola'}[data_format])
|
||||||
T = TIFF_file(data)
|
T = TIFF_file(data)
|
||||||
# There may be more than one IFD per file, but we only read the first one because others are
|
# There may be more than one IFD per file, but we only read the first one because others are
|
||||||
# most likely thumbnails.
|
# most likely thumbnails.
|
||||||
main_IFD_offset = T.first_IFD()
|
main_IFD_offset = T.first_IFD()
|
||||||
result = {}
|
result = {}
|
||||||
|
|
||||||
def add_tag_to_result(tag, values):
|
def add_tag_to_result(tag, values):
|
||||||
try:
|
try:
|
||||||
stag = EXIF_TAGS[tag]
|
stag = EXIF_TAGS[tag]
|
||||||
@@ -298,7 +298,7 @@ def get_fields(fp):
|
|||||||
if stag in result:
|
if stag in result:
|
||||||
return # don't overwrite data
|
return # don't overwrite data
|
||||||
result[stag] = values
|
result[stag] = values
|
||||||
|
|
||||||
logging.debug("IFD at offset %d", main_IFD_offset)
|
logging.debug("IFD at offset %d", main_IFD_offset)
|
||||||
IFD = T.dump_IFD(main_IFD_offset)
|
IFD = T.dump_IFD(main_IFD_offset)
|
||||||
exif_off = gps_off = 0
|
exif_off = gps_off = 0
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2007/02/25
|
# Created On: 2007/02/25
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
from itertools import combinations
|
from itertools import combinations
|
||||||
|
|
||||||
from hscommon.util import extract
|
from hscommon.util import extract, iterconsume
|
||||||
from hscommon.trans import tr
|
from hscommon.trans import tr
|
||||||
from jobprogress import job
|
from hscommon.jobprogress import job
|
||||||
|
|
||||||
from core.engine import Match
|
from core.engine import Match
|
||||||
from .block import avgdiff, DifferentBlockCountError, NoBlocksError
|
from .block import avgdiff, DifferentBlockCountError, NoBlocksError
|
||||||
@@ -93,8 +93,10 @@ def get_chunks(pictures):
|
|||||||
chunk_count = max(min_chunk_count, chunk_count)
|
chunk_count = max(min_chunk_count, chunk_count)
|
||||||
chunk_size = (len(pictures) // chunk_count) + 1
|
chunk_size = (len(pictures) // chunk_count) + 1
|
||||||
chunk_size = max(MIN_CHUNK_SIZE, chunk_size)
|
chunk_size = max(MIN_CHUNK_SIZE, chunk_size)
|
||||||
logging.info("Creating %d chunks with a chunk size of %d for %d pictures", chunk_count,
|
logging.info(
|
||||||
chunk_size, len(pictures))
|
"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)]
|
chunks = [pictures[i:i+chunk_size] for i in range(0, len(pictures), chunk_size)]
|
||||||
return chunks
|
return chunks
|
||||||
|
|
||||||
@@ -132,17 +134,17 @@ def async_compare(ref_ids, other_ids, dbname, threshold, picinfo):
|
|||||||
results.append((ref_id, other_id, percentage))
|
results.append((ref_id, other_id, percentage))
|
||||||
cache.close()
|
cache.close()
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def getmatches(pictures, cache_path, threshold=75, match_scaled=False, j=job.nulljob):
|
def getmatches(pictures, cache_path, threshold=75, match_scaled=False, j=job.nulljob):
|
||||||
def get_picinfo(p):
|
def get_picinfo(p):
|
||||||
if match_scaled:
|
if match_scaled:
|
||||||
return (None, p.is_ref)
|
return (None, p.is_ref)
|
||||||
else:
|
else:
|
||||||
return (p.dimensions, p.is_ref)
|
return (p.dimensions, p.is_ref)
|
||||||
|
|
||||||
def collect_results(collect_all=False):
|
def collect_results(collect_all=False):
|
||||||
# collect results and wait until the queue is small enough to accomodate a new results.
|
# 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
|
limit = 0 if collect_all else RESULTS_QUEUE_LIMIT
|
||||||
while len(async_results) > limit:
|
while len(async_results) > limit:
|
||||||
ready, working = extract(lambda r: r.ready(), async_results)
|
ready, working = extract(lambda r: r.ready(), async_results)
|
||||||
@@ -150,9 +152,10 @@ def getmatches(pictures, cache_path, threshold=75, match_scaled=False, j=job.nul
|
|||||||
matches += result.get()
|
matches += result.get()
|
||||||
async_results.remove(result)
|
async_results.remove(result)
|
||||||
comparison_count += 1
|
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.set_progress(comparison_count, progress_msg)
|
||||||
|
|
||||||
j = j.start_subjob([3, 7])
|
j = j.start_subjob([3, 7])
|
||||||
pictures = prepare_pictures(pictures, cache_path, with_dimensions=not match_scaled, j=j)
|
pictures = prepare_pictures(pictures, cache_path, with_dimensions=not match_scaled, j=j)
|
||||||
j = j.start_subjob([9, 1], tr("Preparing for matching"))
|
j = j.start_subjob([9, 1], tr("Preparing for matching"))
|
||||||
@@ -175,22 +178,36 @@ def getmatches(pictures, cache_path, threshold=75, match_scaled=False, j=job.nul
|
|||||||
comparisons_to_do = list(combinations(chunks + [None], 2))
|
comparisons_to_do = list(combinations(chunks + [None], 2))
|
||||||
comparison_count = 0
|
comparison_count = 0
|
||||||
j.start_job(len(comparisons_to_do))
|
j.start_job(len(comparisons_to_do))
|
||||||
for ref_chunk, other_chunk in comparisons_to_do:
|
try:
|
||||||
picinfo = {p.cache_id: get_picinfo(p) for p in ref_chunk}
|
for ref_chunk, other_chunk in comparisons_to_do:
|
||||||
ref_ids = [p.cache_id for p in ref_chunk]
|
picinfo = {p.cache_id: get_picinfo(p) for p in ref_chunk}
|
||||||
if other_chunk is not None:
|
ref_ids = [p.cache_id for p in ref_chunk]
|
||||||
other_ids = [p.cache_id for p in other_chunk]
|
if other_chunk is not None:
|
||||||
picinfo.update({p.cache_id: get_picinfo(p) for p in other_chunk})
|
other_ids = [p.cache_id for p in other_chunk]
|
||||||
else:
|
picinfo.update({p.cache_id: get_picinfo(p) for p in other_chunk})
|
||||||
other_ids = None
|
else:
|
||||||
args = (ref_ids, other_ids, cache_path, threshold, picinfo)
|
other_ids = None
|
||||||
async_results.append(pool.apply_async(async_compare, args))
|
args = (ref_ids, other_ids, cache_path, threshold, picinfo)
|
||||||
collect_results()
|
async_results.append(pool.apply_async(async_compare, args))
|
||||||
collect_results(collect_all=True)
|
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()
|
pool.close()
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for ref_id, other_id, percentage in j.iter_with_progress(matches, tr("Verified %d/%d matches"), every=10):
|
myiter = j.iter_with_progress(
|
||||||
|
iterconsume(matches, reverse=False),
|
||||||
|
tr("Verified %d/%d matches"),
|
||||||
|
every=10,
|
||||||
|
count=len(matches),
|
||||||
|
)
|
||||||
|
for ref_id, other_id, percentage in myiter:
|
||||||
ref = id2picture[ref_id]
|
ref = id2picture[ref_id]
|
||||||
other = id2picture[other_id]
|
other = id2picture[other_id]
|
||||||
if percentage == 100 and ref.md5 != other.md5:
|
if percentage == 100 and ref.md5 != other.md5:
|
||||||
@@ -201,4 +218,5 @@ def getmatches(pictures, cache_path, threshold=75, match_scaled=False, j=job.nul
|
|||||||
result.append(get_match(ref, other, percentage))
|
result.append(get_match(ref, other, percentage))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
multiprocessing.freeze_support()
|
multiprocessing.freeze_support()
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2011-04-20
|
# Created On: 2011-04-20
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
@@ -27,4 +27,5 @@ def getmatches(files, match_scaled, j):
|
|||||||
if (not match_scaled) and (p1.dimensions != p2.dimensions):
|
if (not match_scaled) and (p1.dimensions != p2.dimensions):
|
||||||
continue
|
continue
|
||||||
matches.append(Match(p1, p2, 100))
|
matches.append(Match(p1, p2, 100))
|
||||||
return matches
|
return matches
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2011-05-29
|
# Created On: 2011-05-29
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -23,20 +23,20 @@ def get_delta_dimensions(value, ref_value):
|
|||||||
class Photo(fs.File):
|
class Photo(fs.File):
|
||||||
INITIAL_INFO = fs.File.INITIAL_INFO.copy()
|
INITIAL_INFO = fs.File.INITIAL_INFO.copy()
|
||||||
INITIAL_INFO.update({
|
INITIAL_INFO.update({
|
||||||
'dimensions': (0,0),
|
'dimensions': (0, 0),
|
||||||
'exif_timestamp': '',
|
'exif_timestamp': '',
|
||||||
})
|
})
|
||||||
__slots__ = fs.File.__slots__ + tuple(INITIAL_INFO.keys())
|
__slots__ = fs.File.__slots__ + tuple(INITIAL_INFO.keys())
|
||||||
|
|
||||||
# These extensions are supported on all platforms
|
# These extensions are supported on all platforms
|
||||||
HANDLED_EXTS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff', 'tif'}
|
HANDLED_EXTS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff', 'tif'}
|
||||||
|
|
||||||
def _plat_get_dimensions(self):
|
def _plat_get_dimensions(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def _plat_get_blocks(self, block_count_per_side, orientation):
|
def _plat_get_blocks(self, block_count_per_side, orientation):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def _get_orientation(self):
|
def _get_orientation(self):
|
||||||
if not hasattr(self, '_cached_orientation'):
|
if not hasattr(self, '_cached_orientation'):
|
||||||
try:
|
try:
|
||||||
@@ -48,7 +48,7 @@ class Photo(fs.File):
|
|||||||
except Exception: # Couldn't read EXIF data, no transforms
|
except Exception: # Couldn't read EXIF data, no transforms
|
||||||
self._cached_orientation = 0
|
self._cached_orientation = 0
|
||||||
return self._cached_orientation
|
return self._cached_orientation
|
||||||
|
|
||||||
def _get_exif_timestamp(self):
|
def _get_exif_timestamp(self):
|
||||||
try:
|
try:
|
||||||
with self.path.open('rb') as fp:
|
with self.path.open('rb') as fp:
|
||||||
@@ -57,11 +57,11 @@ class Photo(fs.File):
|
|||||||
except Exception:
|
except Exception:
|
||||||
logging.info("Couldn't read EXIF of picture: %s", self.path)
|
logging.info("Couldn't read EXIF of picture: %s", self.path)
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def can_handle(cls, path):
|
def can_handle(cls, path):
|
||||||
return fs.File.can_handle(path) and get_file_ext(path.name) in cls.HANDLED_EXTS
|
return fs.File.can_handle(path) and get_file_ext(path.name) in cls.HANDLED_EXTS
|
||||||
|
|
||||||
def get_display_info(self, group, delta):
|
def get_display_info(self, group, delta):
|
||||||
size = self.size
|
size = self.size
|
||||||
mtime = self.mtime
|
mtime = self.mtime
|
||||||
@@ -90,7 +90,7 @@ class Photo(fs.File):
|
|||||||
'percentage': format_perc(percentage),
|
'percentage': format_perc(percentage),
|
||||||
'dupe_count': format_dupe_count(dupe_count),
|
'dupe_count': format_dupe_count(dupe_count),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _read_info(self, field):
|
def _read_info(self, field):
|
||||||
fs.File._read_info(self, field)
|
fs.File._read_info(self, field)
|
||||||
if field == 'dimensions':
|
if field == 'dimensions':
|
||||||
@@ -99,7 +99,7 @@ class Photo(fs.File):
|
|||||||
self.dimensions = (self.dimensions[1], self.dimensions[0])
|
self.dimensions = (self.dimensions[1], self.dimensions[0])
|
||||||
elif field == 'exif_timestamp':
|
elif field == 'exif_timestamp':
|
||||||
self.exif_timestamp = self._get_exif_timestamp()
|
self.exif_timestamp = self._get_exif_timestamp()
|
||||||
|
|
||||||
def get_blocks(self, block_count_per_side):
|
def get_blocks(self, block_count_per_side):
|
||||||
return self._plat_get_blocks(block_count_per_side, self._get_orientation())
|
return self._plat_get_blocks(block_count_per_side, self._get_orientation())
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,31 @@
|
|||||||
# Created On: 2011/09/16
|
# Created On: 2011/09/16
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
|
|
||||||
from core.prioritize import (KindCategory, FolderCategory, FilenameCategory, NumericalCategory,
|
from core.prioritize import (
|
||||||
SizeCategory, MtimeCategory)
|
KindCategory, FolderCategory, FilenameCategory, NumericalCategory,
|
||||||
|
SizeCategory, MtimeCategory
|
||||||
|
)
|
||||||
|
|
||||||
coltr = trget('columns')
|
coltr = trget('columns')
|
||||||
|
|
||||||
class DimensionsCategory(NumericalCategory):
|
class DimensionsCategory(NumericalCategory):
|
||||||
NAME = coltr("Dimensions")
|
NAME = coltr("Dimensions")
|
||||||
|
|
||||||
def extract_value(self, dupe):
|
def extract_value(self, dupe):
|
||||||
return dupe.dimensions
|
return dupe.dimensions
|
||||||
|
|
||||||
def invert_numerical_value(self, value):
|
def invert_numerical_value(self, value):
|
||||||
width, height = value
|
width, height = value
|
||||||
return (-width, -height)
|
return (-width, -height)
|
||||||
|
|
||||||
def all_categories():
|
def all_categories():
|
||||||
return [KindCategory, FolderCategory, FilenameCategory, SizeCategory, DimensionsCategory,
|
return [
|
||||||
MtimeCategory]
|
KindCategory, FolderCategory, FilenameCategory, SizeCategory, DimensionsCategory,
|
||||||
|
MtimeCategory
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
__version__ = '3.9.0'
|
__version__ = '3.9.1'
|
||||||
__appname__ = 'dupeGuru'
|
__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)
|
=== 6.7.0 (2013-12-08)
|
||||||
|
|
||||||
* Disable symlink/hardlink deletion option when not relevant. (#247)
|
* Disable symlink/hardlink deletion option when not relevant. (#247)
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
=== 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)
|
=== 2.10.0 (2014-05-03)
|
||||||
|
|
||||||
* This is mostly a dependencies upgrade.
|
* This is mostly a dependencies upgrade.
|
||||||
|
|||||||
@@ -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)
|
=== 3.9.0 (2014-04-19)
|
||||||
|
|
||||||
* This is mostly a dependencies upgrade.
|
* 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):
|
def fix_nulljob_in_sig(app, what, name, obj, options, signature, return_annotation):
|
||||||
if signature:
|
if signature:
|
||||||
signature = re.sub(r"<jobprogress.job.NullJob object at 0x[\da-f]+>", "nulljob", signature)
|
signature = re.sub(r"<hscommon.jobprogress.job.NullJob object at 0x[\da-f]+>", "nulljob", signature)
|
||||||
return signature, return_annotation
|
return signature, return_annotation
|
||||||
|
|
||||||
def setup(app):
|
def setup(app):
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ Unten befindet sich die Liste aller Menschen, die direkt oder indirekt zu dupeGu
|
|||||||
|
|
||||||
| **Gregor Tätzner, deutsche Übersetzung**
|
| **Gregor Tätzner, deutsche Übersetzung**
|
||||||
|
|
||||||
|
| **Frank Weber, deutsche Übersetzung**
|
||||||
|
|
||||||
| **Eric Dee, chinesische Übersetzung**
|
| **Eric Dee, chinesische Übersetzung**
|
||||||
|
|
||||||
| **Aleš Nehyba, Czech localization**
|
| **Aleš Nehyba, Czech localization**
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ Below is the list of people who contributed, directly or indirectly to dupeGuru.
|
|||||||
|
|
||||||
| **Gregor Tätzner, German localization**
|
| **Gregor Tätzner, German localization**
|
||||||
|
|
||||||
|
| **Frank Weber, German localization**
|
||||||
|
|
||||||
| **Eric Dee, Chinese localization**
|
| **Eric Dee, Chinese localization**
|
||||||
|
|
||||||
| **Aleš Nehyba, Czech localization**
|
| **Aleš Nehyba, Czech localization**
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ hscommon
|
|||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
:glob:
|
||||||
|
|
||||||
build
|
build
|
||||||
conflict
|
conflict
|
||||||
@@ -10,10 +11,6 @@ hscommon
|
|||||||
notify
|
notify
|
||||||
path
|
path
|
||||||
util
|
util
|
||||||
gui/base
|
jobprogress/*
|
||||||
gui/text_field
|
gui/*
|
||||||
gui/selectable_list
|
|
||||||
gui/table
|
|
||||||
gui/tree
|
|
||||||
gui/column
|
|
||||||
gui/progress_window
|
|
||||||
|
|||||||
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
|
different classes, scattered around. If you're aware of that, it might help you to understand what
|
||||||
the heck is going on.
|
the heck is going on.
|
||||||
|
|
||||||
The central piece of dupeGuru is ``dupeguru.app.DupeGuru`` (in the ``core`` code). It's the only
|
The central piece of dupeGuru is :class:`core.app.DupeGuru`. It's the only
|
||||||
interface to the python's code for the GUI code. A duplicate scan is started with
|
interface to the python's code for the GUI code. A duplicate scan is started with
|
||||||
``start_scanning()``, directories are added through ``add_directory()``, etc..
|
:meth:`core.app.DupeGuru.start_scanning()`, directories are added through
|
||||||
|
:meth:`core.app.DupeGuru.add_directory()`, etc..
|
||||||
|
|
||||||
A lot of functionalities of the App are implemented in the platform-specific subclasses of
|
A lot of functionalities of the App are implemented in the platform-specific subclasses of
|
||||||
``app.DupeGuru``, like ``app_cocoa.DupeGuru``, or the ``base.app.DupeGuru`` class in the PyQt
|
:class:`core.app.DupeGuru`, like ``DupeGuru`` in ``cocoa/inter/app.py``, or the ``DupeGuru`` class
|
||||||
codebase. For example, when performing "Remove Selected From Results",
|
in ``qt/base/app.py``. For example, when performing "Remove Selected From Results",
|
||||||
``app_cocoa.Dupeguru.RemoveSelected()`` on the Obj-C side, and
|
``RemoveSelected()`` on the cocoa side, and ``remove_duplicates()`` on the PyQt side, are
|
||||||
``base.app.DupeGuru.remove_duplicates()`` on the PyQt side, are respectively called to perform the
|
respectively called to perform the thing.
|
||||||
thing. All of this is quite ugly, I know (see the "Refactoring" section below).
|
|
||||||
|
|
||||||
.. _jobs:
|
.. _jobs:
|
||||||
|
|
||||||
@@ -29,23 +29,26 @@ Jobs
|
|||||||
----
|
----
|
||||||
|
|
||||||
A lot of operations in dupeGuru take a significant amount of time. This is why there's a generalized
|
A lot of operations in dupeGuru take a significant amount of time. This is why there's a generalized
|
||||||
threaded job mechanism built-in ``app.DupeGuru``. First, ``app.DupeGuru`` has a ``progress`` member
|
threaded job mechanism built-in :class:`~core.app.DupeGuru`. First, :class:`~core.app.DupeGuru` has
|
||||||
which is an instance of ``jobprogress.job.ThreadedJobPerformer``. It lets the GUI code know of the
|
a ``progress`` member which is an instance of
|
||||||
progress of the current threaded job. When ``app.DupeGuru`` needs to start a job, it calls
|
:class:`~hscommon.jobprogress.performer.ThreadedJobPerformer`. It lets the GUI code know of the progress
|
||||||
|
of the current threaded job. When :class:`~core.app.DupeGuru` needs to start a job, it calls
|
||||||
``_start_job()`` and the platform specific subclass deals with the details of starting the job.
|
``_start_job()`` and the platform specific subclass deals with the details of starting the job.
|
||||||
|
|
||||||
Core principles
|
Core principles
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
The core of the duplicate matching takes place (for SE and ME, not PE) in ``dupeguru.engine``.
|
The core of the duplicate matching takes place (for SE and ME, not PE) in :mod:`core.engine`.
|
||||||
There's ``MatchFactory.getmatches()`` which take a list of ``fs.File`` instances and return a list
|
There's :func:`core.engine.getmatches` which take a list of :class:`core.fs.File` instances and
|
||||||
of ``(firstfile, secondfile, match_percentage)`` matches. Then, there's ``get_groups()`` which takes
|
return a list of ``(firstfile, secondfile, match_percentage)`` matches. Then, there's
|
||||||
a list of matches and returns a list of ``Group`` instances (a ``Group`` is basically a list of
|
:func:`core.engine.get_groups` which takes a list of matches and returns a list of
|
||||||
``fs.File`` matching together).
|
:class:`.Group` instances (a :class:`.Group` is basically a list of :class:`.File` matching
|
||||||
|
together).
|
||||||
|
|
||||||
When a scan is over, the final result (the list of groups from ``get_groups()``) is placed into
|
When a scan is over, the final result (the list of groups from :func:`.get_groups`) is placed into
|
||||||
``app.DupeGuru.results``, which is a ``results.Results`` instance. The ``Results`` instance is where
|
:attr:`core.app.DupeGuru.results`, which is a :class:`core.results.Results` instance. The
|
||||||
all the dupe marking, sorting, removing, power marking, etc. takes place.
|
:class:`~.Results` instance is where all the dupe marking, sorting, removing, power marking, etc.
|
||||||
|
takes place.
|
||||||
|
|
||||||
API
|
API
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ Voici la liste des contributeurs de dupeGuru. Merci!
|
|||||||
|
|
||||||
| **Gregor Tätzner, localisation allemande**
|
| **Gregor Tätzner, localisation allemande**
|
||||||
|
|
||||||
|
| **Frank Weber, localisation allemande**
|
||||||
|
|
||||||
| **Eric Dee, localisation choinoise**
|
| **Eric Dee, localisation choinoise**
|
||||||
|
|
||||||
| **Aleš Nehyba, localisation tchèque**
|
| **Aleš Nehyba, localisation tchèque**
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
|
|
||||||
| **Gregor Tätzner, Գերմաներեն թարգմանիչը**
|
| **Gregor Tätzner, Գերմաներեն թարգմանիչը**
|
||||||
|
|
||||||
|
| **Frank Weber, Գերմաներեն թարգմանիչը**
|
||||||
|
|
||||||
| **Eric Dee, Չինարեն թարգմանիչը**
|
| **Eric Dee, Չինարեն թարգմանիչը**
|
||||||
|
|
||||||
| **Aleš Nehyba, Չեխերեն թարգմանիչը**
|
| **Aleš Nehyba, Չեխերեն թարգմանիչը**
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
|
|
||||||
| **Gregor Tätzner, Немецкая локализация**
|
| **Gregor Tätzner, Немецкая локализация**
|
||||||
|
|
||||||
|
| **Frank Weber, Немецкая локализация**
|
||||||
|
|
||||||
| **Eric Dee, Китайская локализация**
|
| **Eric Dee, Китайская локализация**
|
||||||
|
|
||||||
| **Aleš Nehyba, Чешский локализации**
|
| **Aleš Nehyba, Чешский локализации**
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
|
|
||||||
| **Gregor Tätzner, Німецька локалізація**
|
| **Gregor Tätzner, Німецька локалізація**
|
||||||
|
|
||||||
|
| **Frank Weber, Німецька локалізація**
|
||||||
|
|
||||||
| **Eric Dee, Китайська локалізація**
|
| **Eric Dee, Китайська локалізація**
|
||||||
|
|
||||||
| **Aleš Nehyba, Чеський локалізації**
|
| **Aleš Nehyba, Чеський локалізації**
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# Created On: 2009-03-03
|
# Created On: 2009-03-03
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
|
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
"""This module is a collection of function to help in HS apps build process.
|
"""This module is a collection of function to help in HS apps build process.
|
||||||
@@ -96,7 +96,7 @@ def filereplace(filename, outfilename=None, **kwargs):
|
|||||||
fp.close()
|
fp.close()
|
||||||
# We can't use str.format() because in some files, there might be {} characters that mess with it.
|
# We can't use str.format() because in some files, there might be {} characters that mess with it.
|
||||||
for key, item in kwargs.items():
|
for key, item in kwargs.items():
|
||||||
contents = contents.replace('{{{}}}'.format(key), item)
|
contents = contents.replace('{{{}}}'.format(key), item)
|
||||||
fp = open(outfilename, 'wt', encoding='utf-8')
|
fp = open(outfilename, 'wt', encoding='utf-8')
|
||||||
fp.write(contents)
|
fp.write(contents)
|
||||||
fp.close()
|
fp.close()
|
||||||
@@ -106,12 +106,22 @@ def get_module_version(modulename):
|
|||||||
return mod.__version__
|
return mod.__version__
|
||||||
|
|
||||||
def setup_package_argparser(parser):
|
def setup_package_argparser(parser):
|
||||||
parser.add_argument('--sign', dest='sign_identity',
|
parser.add_argument(
|
||||||
help="Sign app under specified identity before packaging (OS X only)")
|
'--sign', dest='sign_identity',
|
||||||
parser.add_argument('--nosign', action='store_true', dest='nosign',
|
help="Sign app under specified identity before packaging (OS X only)"
|
||||||
help="Don't sign the packaged app (OS X only)")
|
)
|
||||||
parser.add_argument('--src-pkg', action='store_true', dest='src_pkg',
|
parser.add_argument(
|
||||||
help="Build a tar.gz of the current source.")
|
'--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()
|
# `args` come from an ArgumentParser updated with setup_package_argparser()
|
||||||
def package_cocoa_app_in_dmg(app_path, destfolder, args):
|
def package_cocoa_app_in_dmg(app_path, destfolder, args):
|
||||||
@@ -131,7 +141,7 @@ def package_cocoa_app_in_dmg(app_path, destfolder, args):
|
|||||||
|
|
||||||
def build_dmg(app_path, destfolder):
|
def build_dmg(app_path, destfolder):
|
||||||
"""Builds a DMG volume with application at ``app_path`` and puts it in ``dest_path``.
|
"""Builds a DMG volume with application at ``app_path`` and puts it in ``dest_path``.
|
||||||
|
|
||||||
The name of the resulting DMG volume is determined by the app's name and version.
|
The name of the resulting DMG volume is determined by the app's name and version.
|
||||||
"""
|
"""
|
||||||
print(repr(op.join(app_path, 'Contents', 'Info.plist')))
|
print(repr(op.join(app_path, 'Contents', 'Info.plist')))
|
||||||
@@ -176,7 +186,7 @@ def add_to_pythonpath(path):
|
|||||||
# from there.
|
# from there.
|
||||||
def copy_packages(packages_names, dest, create_links=False, extra_ignores=None):
|
def copy_packages(packages_names, dest, create_links=False, extra_ignores=None):
|
||||||
"""Copy python packages ``packages_names`` to ``dest``, spurious data.
|
"""Copy python packages ``packages_names`` to ``dest``, spurious data.
|
||||||
|
|
||||||
Copy will happen without tests, testdata, mercurial data or C extension module source with it.
|
Copy will happen without tests, testdata, mercurial data or C extension module source with it.
|
||||||
``py2app`` include and exclude rules are **quite** funky, and doing this is the only reliable
|
``py2app`` include and exclude rules are **quite** funky, and doing this is the only reliable
|
||||||
way to make sure we don't end up with useless stuff in our app.
|
way to make sure we don't end up with useless stuff in our app.
|
||||||
@@ -223,7 +233,7 @@ def copy_qt_plugins(folder_names, dest): # This is only for Windows
|
|||||||
def build_debian_changelog(changelogpath, destfile, pkgname, from_version=None,
|
def build_debian_changelog(changelogpath, destfile, pkgname, from_version=None,
|
||||||
distribution='precise', fix_version=None):
|
distribution='precise', fix_version=None):
|
||||||
"""Builds a debian changelog out of a YAML changelog.
|
"""Builds a debian changelog out of a YAML changelog.
|
||||||
|
|
||||||
Use fix_version to patch the top changelog to that version (if, for example, there was a
|
Use fix_version to patch the top changelog to that version (if, for example, there was a
|
||||||
packaging error and you need to quickly fix it)
|
packaging error and you need to quickly fix it)
|
||||||
"""
|
"""
|
||||||
@@ -233,7 +243,7 @@ def build_debian_changelog(changelogpath, destfile, pkgname, from_version=None,
|
|||||||
desc = desc.replace(' ', ' ')
|
desc = desc.replace(' ', ' ')
|
||||||
result = desc.split('*')
|
result = desc.split('*')
|
||||||
return [s.strip() for s in result if s.strip()]
|
return [s.strip() for s in result if s.strip()]
|
||||||
|
|
||||||
ENTRY_MODEL = "{pkg} ({version}~{distribution}) {distribution}; urgency=low\n\n{changes}\n -- Virgil Dupras <hsoft@hardcoded.net> {date}\n\n"
|
ENTRY_MODEL = "{pkg} ({version}~{distribution}) {distribution}; urgency=low\n\n{changes}\n -- Virgil Dupras <hsoft@hardcoded.net> {date}\n\n"
|
||||||
CHANGE_MODEL = " * {description}\n"
|
CHANGE_MODEL = " * {description}\n"
|
||||||
changelogs = read_changelog_file(changelogpath)
|
changelogs = read_changelog_file(changelogpath)
|
||||||
@@ -269,7 +279,7 @@ def read_changelog_file(filename):
|
|||||||
date = next(it)
|
date = next(it)
|
||||||
description = next(it)
|
description = next(it)
|
||||||
yield version, date, description
|
yield version, date, description
|
||||||
|
|
||||||
with open(filename, 'rt', encoding='utf-8') as fp:
|
with open(filename, 'rt', encoding='utf-8') as fp:
|
||||||
contents = fp.read()
|
contents = fp.read()
|
||||||
splitted = re_changelog_header.split(contents)[1:] # the first item is empty
|
splitted = re_changelog_header.split(contents)[1:] # the first item is empty
|
||||||
@@ -289,7 +299,7 @@ class OSXAppStructure:
|
|||||||
self.resources = op.join(self.contents, 'Resources')
|
self.resources = op.join(self.contents, 'Resources')
|
||||||
self.frameworks = op.join(self.contents, 'Frameworks')
|
self.frameworks = op.join(self.contents, 'Frameworks')
|
||||||
self.infoplist = op.join(self.contents, 'Info.plist')
|
self.infoplist = op.join(self.contents, 'Info.plist')
|
||||||
|
|
||||||
def create(self, infoplist):
|
def create(self, infoplist):
|
||||||
ensure_empty_folder(self.dest)
|
ensure_empty_folder(self.dest)
|
||||||
os.makedirs(self.macos)
|
os.makedirs(self.macos)
|
||||||
@@ -297,24 +307,24 @@ class OSXAppStructure:
|
|||||||
os.mkdir(self.frameworks)
|
os.mkdir(self.frameworks)
|
||||||
copy(infoplist, self.infoplist)
|
copy(infoplist, self.infoplist)
|
||||||
open(op.join(self.contents, 'PkgInfo'), 'wt').write("APPLxxxx")
|
open(op.join(self.contents, 'PkgInfo'), 'wt').write("APPLxxxx")
|
||||||
|
|
||||||
def copy_executable(self, executable):
|
def copy_executable(self, executable):
|
||||||
info = plistlib.readPlist(self.infoplist)
|
info = plistlib.readPlist(self.infoplist)
|
||||||
self.executablename = info['CFBundleExecutable']
|
self.executablename = info['CFBundleExecutable']
|
||||||
self.executablepath = op.join(self.macos, self.executablename)
|
self.executablepath = op.join(self.macos, self.executablename)
|
||||||
copy(executable, self.executablepath)
|
copy(executable, self.executablepath)
|
||||||
|
|
||||||
def copy_resources(self, *resources, use_symlinks=False):
|
def copy_resources(self, *resources, use_symlinks=False):
|
||||||
for path in resources:
|
for path in resources:
|
||||||
resource_dest = op.join(self.resources, op.basename(path))
|
resource_dest = op.join(self.resources, op.basename(path))
|
||||||
action = symlink if use_symlinks else copy
|
action = symlink if use_symlinks else copy
|
||||||
action(op.abspath(path), resource_dest)
|
action(op.abspath(path), resource_dest)
|
||||||
|
|
||||||
def copy_frameworks(self, *frameworks):
|
def copy_frameworks(self, *frameworks):
|
||||||
for path in frameworks:
|
for path in frameworks:
|
||||||
framework_dest = op.join(self.frameworks, op.basename(path))
|
framework_dest = op.join(self.frameworks, op.basename(path))
|
||||||
copy(path, framework_dest)
|
copy(path, framework_dest)
|
||||||
|
|
||||||
|
|
||||||
def create_osx_app_structure(dest, executable, infoplist, resources=None, frameworks=None,
|
def create_osx_app_structure(dest, executable, infoplist, resources=None, frameworks=None,
|
||||||
symlink_resources=False):
|
symlink_resources=False):
|
||||||
@@ -338,7 +348,7 @@ class OSXFrameworkStructure:
|
|||||||
self.headers = op.join(self.contents, 'Headers')
|
self.headers = op.join(self.contents, 'Headers')
|
||||||
self.infoplist = op.join(self.resources, 'Info.plist')
|
self.infoplist = op.join(self.resources, 'Info.plist')
|
||||||
self._update_executable_path()
|
self._update_executable_path()
|
||||||
|
|
||||||
def _update_executable_path(self):
|
def _update_executable_path(self):
|
||||||
if not op.exists(self.infoplist):
|
if not op.exists(self.infoplist):
|
||||||
self.executablename = self.executablepath = None
|
self.executablename = self.executablepath = None
|
||||||
@@ -346,7 +356,7 @@ class OSXFrameworkStructure:
|
|||||||
info = plistlib.readPlist(self.infoplist)
|
info = plistlib.readPlist(self.infoplist)
|
||||||
self.executablename = info['CFBundleExecutable']
|
self.executablename = info['CFBundleExecutable']
|
||||||
self.executablepath = op.join(self.contents, self.executablename)
|
self.executablepath = op.join(self.contents, self.executablename)
|
||||||
|
|
||||||
def create(self, infoplist):
|
def create(self, infoplist):
|
||||||
ensure_empty_folder(self.dest)
|
ensure_empty_folder(self.dest)
|
||||||
os.makedirs(self.contents)
|
os.makedirs(self.contents)
|
||||||
@@ -354,7 +364,7 @@ class OSXFrameworkStructure:
|
|||||||
os.mkdir(self.headers)
|
os.mkdir(self.headers)
|
||||||
copy(infoplist, self.infoplist)
|
copy(infoplist, self.infoplist)
|
||||||
self._update_executable_path()
|
self._update_executable_path()
|
||||||
|
|
||||||
def create_symlinks(self):
|
def create_symlinks(self):
|
||||||
# Only call this after create() and copy_executable()
|
# Only call this after create() and copy_executable()
|
||||||
rel = lambda path: op.relpath(path, self.dest)
|
rel = lambda path: op.relpath(path, self.dest)
|
||||||
@@ -362,22 +372,22 @@ class OSXFrameworkStructure:
|
|||||||
os.symlink(rel(self.executablepath), op.join(self.dest, self.executablename))
|
os.symlink(rel(self.executablepath), op.join(self.dest, self.executablename))
|
||||||
os.symlink(rel(self.headers), op.join(self.dest, 'Headers'))
|
os.symlink(rel(self.headers), op.join(self.dest, 'Headers'))
|
||||||
os.symlink(rel(self.resources), op.join(self.dest, 'Resources'))
|
os.symlink(rel(self.resources), op.join(self.dest, 'Resources'))
|
||||||
|
|
||||||
def copy_executable(self, executable):
|
def copy_executable(self, executable):
|
||||||
copy(executable, self.executablepath)
|
copy(executable, self.executablepath)
|
||||||
|
|
||||||
def copy_resources(self, *resources, use_symlinks=False):
|
def copy_resources(self, *resources, use_symlinks=False):
|
||||||
for path in resources:
|
for path in resources:
|
||||||
resource_dest = op.join(self.resources, op.basename(path))
|
resource_dest = op.join(self.resources, op.basename(path))
|
||||||
action = symlink if use_symlinks else copy
|
action = symlink if use_symlinks else copy
|
||||||
action(op.abspath(path), resource_dest)
|
action(op.abspath(path), resource_dest)
|
||||||
|
|
||||||
def copy_headers(self, *headers, use_symlinks=False):
|
def copy_headers(self, *headers, use_symlinks=False):
|
||||||
for path in headers:
|
for path in headers:
|
||||||
header_dest = op.join(self.headers, op.basename(path))
|
header_dest = op.join(self.headers, op.basename(path))
|
||||||
action = symlink if use_symlinks else copy
|
action = symlink if use_symlinks else copy
|
||||||
action(op.abspath(path), header_dest)
|
action(op.abspath(path), header_dest)
|
||||||
|
|
||||||
|
|
||||||
def build_cocoalib_xibless(dest='cocoa/autogen'):
|
def build_cocoalib_xibless(dest='cocoa/autogen'):
|
||||||
import xibless
|
import xibless
|
||||||
@@ -415,7 +425,7 @@ def collect_stdlib_dependencies(script, dest_folder, extra_deps=None):
|
|||||||
if not (path.startswith(sysprefix) or path.startswith(real_lib_prefix)):
|
if not (path.startswith(sysprefix) or path.startswith(real_lib_prefix)):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
ensure_folder(dest_folder)
|
ensure_folder(dest_folder)
|
||||||
mf = modulefinder.ModuleFinder()
|
mf = modulefinder.ModuleFinder()
|
||||||
mf.run_script(script)
|
mf.run_script(script)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2013-10-12
|
# Created On: 2013-10-12
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
import os.path as op
|
import os.path as op
|
||||||
@@ -30,10 +30,10 @@ def reveal_path(path):
|
|||||||
|
|
||||||
def special_folder_path(special_folder, appname=None):
|
def special_folder_path(special_folder, appname=None):
|
||||||
"""Returns the path of ``special_folder``.
|
"""Returns the path of ``special_folder``.
|
||||||
|
|
||||||
``special_folder`` is a SpecialFolder.* const. The result is the special folder for the current
|
``special_folder`` is a SpecialFolder.* const. The result is the special folder for the current
|
||||||
application. The running process' application info is used to determine relevant information.
|
application. The running process' application info is used to determine relevant information.
|
||||||
|
|
||||||
You can override the application name with ``appname``. This argument is ingored under Qt.
|
You can override the application name with ``appname``. This argument is ingored under Qt.
|
||||||
"""
|
"""
|
||||||
return _special_folder_path(special_folder, appname)
|
return _special_folder_path(special_folder, appname)
|
||||||
@@ -49,7 +49,7 @@ try:
|
|||||||
_open_url = proxy.openURL_
|
_open_url = proxy.openURL_
|
||||||
_open_path = proxy.openPath_
|
_open_path = proxy.openPath_
|
||||||
_reveal_path = proxy.revealPath_
|
_reveal_path = proxy.revealPath_
|
||||||
|
|
||||||
def _special_folder_path(special_folder, appname=None):
|
def _special_folder_path(special_folder, appname=None):
|
||||||
if special_folder == SpecialFolder.Cache:
|
if special_folder == SpecialFolder.Cache:
|
||||||
base = proxy.getCachePath()
|
base = proxy.getCachePath()
|
||||||
@@ -58,7 +58,7 @@ try:
|
|||||||
if not appname:
|
if not appname:
|
||||||
appname = proxy.bundleInfo_('CFBundleName')
|
appname = proxy.bundleInfo_('CFBundleName')
|
||||||
return op.join(base, appname)
|
return op.join(base, appname)
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
try:
|
try:
|
||||||
from PyQt5.QtCore import QUrl, QStandardPaths
|
from PyQt5.QtCore import QUrl, QStandardPaths
|
||||||
@@ -69,26 +69,25 @@ except ImportError:
|
|||||||
def _open_path(path):
|
def _open_path(path):
|
||||||
url = QUrl.fromLocalFile(str(path))
|
url = QUrl.fromLocalFile(str(path))
|
||||||
QDesktopServices.openUrl(url)
|
QDesktopServices.openUrl(url)
|
||||||
|
|
||||||
def _reveal_path(path):
|
def _reveal_path(path):
|
||||||
_open_path(op.dirname(str(path)))
|
_open_path(op.dirname(str(path)))
|
||||||
|
|
||||||
def _special_folder_path(special_folder, appname=None):
|
def _special_folder_path(special_folder, appname=None):
|
||||||
if special_folder == SpecialFolder.Cache:
|
if special_folder == SpecialFolder.Cache:
|
||||||
qtfolder = QStandardPaths.CacheLocation
|
qtfolder = QStandardPaths.CacheLocation
|
||||||
else:
|
else:
|
||||||
qtfolder = QStandardPaths.DataLocation
|
qtfolder = QStandardPaths.DataLocation
|
||||||
return QStandardPaths.standardLocations(qtfolder)[0]
|
return QStandardPaths.standardLocations(qtfolder)[0]
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# We're either running tests, and these functions don't matter much or we're in a really
|
# We're either running tests, and these functions don't matter much or we're in a really
|
||||||
# weird situation. Let's just have dummy fallbacks.
|
# weird situation. Let's just have dummy fallbacks.
|
||||||
logging.warning("Can't setup desktop functions!")
|
logging.warning("Can't setup desktop functions!")
|
||||||
def _open_path(path):
|
def _open_path(path):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _reveal_path(path):
|
def _reveal_path(path):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _special_folder_path(special_folder, appname=None):
|
def _special_folder_path(special_folder, appname=None):
|
||||||
return '/tmp'
|
return '/tmp'
|
||||||
|
|||||||
@@ -1,61 +1,58 @@
|
|||||||
# Created On: 2013/07/01
|
# Created On: 2013/07/01
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from jobprogress.performer import ThreadedJobPerformer
|
from ..jobprogress.performer import ThreadedJobPerformer
|
||||||
|
|
||||||
from .base import GUIObject
|
from .base import GUIObject
|
||||||
from .text_field import TextField
|
from .text_field import TextField
|
||||||
|
|
||||||
class ProgressWindowView:
|
class ProgressWindowView:
|
||||||
"""Expected interface for :class:`ProgressWindow`'s view.
|
"""Expected interface for :class:`ProgressWindow`'s view.
|
||||||
|
|
||||||
*Not actually used in the code. For documentation purposes only.*
|
*Not actually used in the code. For documentation purposes only.*
|
||||||
|
|
||||||
Our view, some kind window with a progress bar, two labels and a cancel button, is expected
|
Our view, some kind window with a progress bar, two labels and a cancel button, is expected
|
||||||
to properly respond to its callbacks.
|
to properly respond to its callbacks.
|
||||||
|
|
||||||
It's also expected to call :meth:`ProgressWindow.cancel` when the cancel button is clicked.
|
It's also expected to call :meth:`ProgressWindow.cancel` when the cancel button is clicked.
|
||||||
"""
|
"""
|
||||||
def show(self):
|
def show(self):
|
||||||
"""Show the dialog.
|
"""Show the dialog.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Close the dialog.
|
"""Close the dialog.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def set_progress(self, progress):
|
def set_progress(self, progress):
|
||||||
"""Set the progress of the progress bar to ``progress``.
|
"""Set the progress of the progress bar to ``progress``.
|
||||||
|
|
||||||
Not all jobs are equally responsive on their job progress report and it is recommended that
|
Not all jobs are equally responsive on their job progress report and it is recommended that
|
||||||
you put your progressbar in "indeterminate" mode as long as you haven't received the first
|
you put your progressbar in "indeterminate" mode as long as you haven't received the first
|
||||||
``set_progress()`` call to avoid letting the user think that the app is frozen.
|
``set_progress()`` call to avoid letting the user think that the app is frozen.
|
||||||
|
|
||||||
:param int progress: a value between ``0`` and ``100``.
|
:param int progress: a value between ``0`` and ``100``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class ProgressWindow(GUIObject, ThreadedJobPerformer):
|
class ProgressWindow(GUIObject, ThreadedJobPerformer):
|
||||||
"""Cross-toolkit GUI-enabled progress window.
|
"""Cross-toolkit GUI-enabled progress window.
|
||||||
|
|
||||||
This class allows you to run a long running, `job enabled`_ function in a separate thread and
|
This class allows you to run a long running, job enabled function in a separate thread and
|
||||||
allow the user to follow its progress with a progress dialog.
|
allow the user to follow its progress with a progress dialog.
|
||||||
|
|
||||||
To use it, you start your long-running job with :meth:`run` and then have your UI layer
|
To use it, you start your long-running job with :meth:`run` and then have your UI layer
|
||||||
regularly call :meth:`pulse` to refresh the job status in the UI. It is advised that you call
|
regularly call :meth:`pulse` to refresh the job status in the UI. It is advised that you call
|
||||||
:meth:`pulse` in the main thread because GUI toolkit usually only support calling UI-related
|
:meth:`pulse` in the main thread because GUI toolkit usually only support calling UI-related
|
||||||
functions from the main thread.
|
functions from the main thread.
|
||||||
|
|
||||||
We subclass :class:`.GUIObject` and ``ThreadedJobPerformer`` (from the ``jobprogress`` library).
|
We subclass :class:`.GUIObject` and :class:`.ThreadedJobPerformer`.
|
||||||
Expected view: :class:`ProgressWindowView`.
|
Expected view: :class:`ProgressWindowView`.
|
||||||
|
|
||||||
:param finishfunc: A function ``f(jobid)`` that is called when a job is completed. ``jobid`` is
|
:param finishfunc: A function ``f(jobid)`` that is called when a job is completed. ``jobid`` is
|
||||||
an arbitrary id passed to :meth:`run`.
|
an arbitrary id passed to :meth:`run`.
|
||||||
|
|
||||||
.. _job enabled: https://pypi.python.org/pypi/jobprogress
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, finish_func):
|
def __init__(self, finish_func):
|
||||||
# finish_func(jobid) is the function that is called when a job is completed.
|
# finish_func(jobid) is the function that is called when a job is completed.
|
||||||
@@ -68,7 +65,7 @@ class ProgressWindow(GUIObject, ThreadedJobPerformer):
|
|||||||
#: during its course.
|
#: during its course.
|
||||||
self.progressdesc_textfield = TextField()
|
self.progressdesc_textfield = TextField()
|
||||||
self.jobid = None
|
self.jobid = None
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self):
|
||||||
"""Call for a user-initiated job cancellation.
|
"""Call for a user-initiated job cancellation.
|
||||||
"""
|
"""
|
||||||
@@ -77,13 +74,13 @@ class ProgressWindow(GUIObject, ThreadedJobPerformer):
|
|||||||
# we verify that the job is still running.
|
# we verify that the job is still running.
|
||||||
if self._job_running:
|
if self._job_running:
|
||||||
self.job_cancelled = True
|
self.job_cancelled = True
|
||||||
|
|
||||||
def pulse(self):
|
def pulse(self):
|
||||||
"""Update progress reports in the GUI.
|
"""Update progress reports in the GUI.
|
||||||
|
|
||||||
Call this regularly from the GUI main run loop. The values might change before
|
Call this regularly from the GUI main run loop. The values might change before
|
||||||
:meth:`ProgressWindowView.set_progress` happens.
|
:meth:`ProgressWindowView.set_progress` happens.
|
||||||
|
|
||||||
If the job is finished, ``pulse()`` will take care of closing the window and re-raising any
|
If the job is finished, ``pulse()`` will take care of closing the window and re-raising any
|
||||||
exception that might have been raised during the job (in the main thread this time). If
|
exception that might have been raised during the job (in the main thread this time). If
|
||||||
there was no exception, ``finish_func(jobid)`` is called to let you take appropriate action.
|
there was no exception, ``finish_func(jobid)`` is called to let you take appropriate action.
|
||||||
@@ -101,13 +98,13 @@ class ProgressWindow(GUIObject, ThreadedJobPerformer):
|
|||||||
if last_desc:
|
if last_desc:
|
||||||
self.progressdesc_textfield.text = last_desc
|
self.progressdesc_textfield.text = last_desc
|
||||||
self.view.set_progress(last_progress)
|
self.view.set_progress(last_progress)
|
||||||
|
|
||||||
def run(self, jobid, title, target, args=()):
|
def run(self, jobid, title, target, args=()):
|
||||||
"""Starts a threaded job.
|
"""Starts a threaded job.
|
||||||
|
|
||||||
The ``target`` function will be sent, as its first argument, a ``Job`` instance (from the
|
The ``target`` function will be sent, as its first argument, a :class:`.Job` instance which
|
||||||
``jobprogress`` library) which it can use to report on its progress.
|
it can use to report on its progress.
|
||||||
|
|
||||||
:param jobid: Arbitrary identifier which will be passed to ``finish_func()`` at the end.
|
:param jobid: Arbitrary identifier which will be passed to ``finish_func()`` at the end.
|
||||||
:param title: A title for the task you're starting.
|
:param title: A title for the task you're starting.
|
||||||
:param target: The function that does your famous long running job.
|
:param target: The function that does your famous long running job.
|
||||||
@@ -122,4 +119,4 @@ class ProgressWindow(GUIObject, ThreadedJobPerformer):
|
|||||||
self.run_threaded(target, args)
|
self.run_threaded(target, args)
|
||||||
self.jobdesc_textfield.text = title
|
self.jobdesc_textfield.text = title
|
||||||
self.view.show()
|
self.view.show()
|
||||||
|
|
||||||
|
|||||||
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
|
|
||||||
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2011-01-12
|
# Created On: 2011-01-12
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
import os.path as op
|
import os.path as op
|
||||||
@@ -31,7 +31,7 @@ def tixgen(tixurl):
|
|||||||
|
|
||||||
def gen(basepath, destpath, changelogpath, tixurl, confrepl=None, confpath=None, changelogtmpl=None):
|
def gen(basepath, destpath, changelogpath, tixurl, confrepl=None, confpath=None, changelogtmpl=None):
|
||||||
"""Generate sphinx docs with all bells and whistles.
|
"""Generate sphinx docs with all bells and whistles.
|
||||||
|
|
||||||
basepath: The base sphinx source path.
|
basepath: The base sphinx source path.
|
||||||
destpath: The final path of html files
|
destpath: The final path of html files
|
||||||
changelogpath: The path to the changelog file to insert in changelog.rst.
|
changelogpath: The path to the changelog file to insert in changelog.rst.
|
||||||
@@ -66,4 +66,8 @@ def gen(basepath, destpath, changelogpath, tixurl, confrepl=None, confpath=None,
|
|||||||
# missing dependencies which are in the virtualenv). Here, we do exactly what is done when
|
# missing dependencies which are in the virtualenv). Here, we do exactly what is done when
|
||||||
# calling the command from bash.
|
# calling the command from bash.
|
||||||
cmd = load_entry_point('Sphinx', 'console_scripts', 'sphinx-build')
|
cmd = load_entry_point('Sphinx', 'console_scripts', 'sphinx-build')
|
||||||
cmd(['sphinx-build', basepath, destpath])
|
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")
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2011-01-11
|
# Created On: 2011-01-11
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
@@ -65,6 +65,12 @@ def test_trailiter():
|
|||||||
eq_(list(trailiter(['foo', 'bar'], skipfirst=True)), [('foo', 'bar')])
|
eq_(list(trailiter(['foo', 'bar'], skipfirst=True)), [('foo', 'bar')])
|
||||||
eq_(list(trailiter([], skipfirst=True)), []) # no crash
|
eq_(list(trailiter([], skipfirst=True)), []) # no crash
|
||||||
|
|
||||||
|
def test_iterconsume():
|
||||||
|
# We just want to make sure that we return *all* items and that we're not mistakenly skipping
|
||||||
|
# one.
|
||||||
|
eq_(list(range(2500)), list(iterconsume(list(range(2500)))))
|
||||||
|
eq_(list(reversed(range(2500))), list(iterconsume(list(range(2500)), reverse=False)))
|
||||||
|
|
||||||
#--- String
|
#--- String
|
||||||
|
|
||||||
def test_escape():
|
def test_escape():
|
||||||
@@ -188,63 +194,63 @@ class TestCase_modified_after:
|
|||||||
monkeyplus.patch_osstat('first', st_mtime=42)
|
monkeyplus.patch_osstat('first', st_mtime=42)
|
||||||
monkeyplus.patch_osstat('second', st_mtime=41)
|
monkeyplus.patch_osstat('second', st_mtime=41)
|
||||||
assert modified_after('first', 'second')
|
assert modified_after('first', 'second')
|
||||||
|
|
||||||
def test_second_is_modified_after(self, monkeyplus):
|
def test_second_is_modified_after(self, monkeyplus):
|
||||||
monkeyplus.patch_osstat('first', st_mtime=42)
|
monkeyplus.patch_osstat('first', st_mtime=42)
|
||||||
monkeyplus.patch_osstat('second', st_mtime=43)
|
monkeyplus.patch_osstat('second', st_mtime=43)
|
||||||
assert not modified_after('first', 'second')
|
assert not modified_after('first', 'second')
|
||||||
|
|
||||||
def test_same_mtime(self, monkeyplus):
|
def test_same_mtime(self, monkeyplus):
|
||||||
monkeyplus.patch_osstat('first', st_mtime=42)
|
monkeyplus.patch_osstat('first', st_mtime=42)
|
||||||
monkeyplus.patch_osstat('second', st_mtime=42)
|
monkeyplus.patch_osstat('second', st_mtime=42)
|
||||||
assert not modified_after('first', 'second')
|
assert not modified_after('first', 'second')
|
||||||
|
|
||||||
def test_first_file_does_not_exist(self, monkeyplus):
|
def test_first_file_does_not_exist(self, monkeyplus):
|
||||||
# when the first file doesn't exist, we return False
|
# when the first file doesn't exist, we return False
|
||||||
monkeyplus.patch_osstat('second', st_mtime=42)
|
monkeyplus.patch_osstat('second', st_mtime=42)
|
||||||
assert not modified_after('does_not_exist', 'second') # no crash
|
assert not modified_after('does_not_exist', 'second') # no crash
|
||||||
|
|
||||||
def test_second_file_does_not_exist(self, monkeyplus):
|
def test_second_file_does_not_exist(self, monkeyplus):
|
||||||
# when the second file doesn't exist, we return True
|
# when the second file doesn't exist, we return True
|
||||||
monkeyplus.patch_osstat('first', st_mtime=42)
|
monkeyplus.patch_osstat('first', st_mtime=42)
|
||||||
assert modified_after('first', 'does_not_exist') # no crash
|
assert modified_after('first', 'does_not_exist') # no crash
|
||||||
|
|
||||||
def test_first_file_is_none(self, monkeyplus):
|
def test_first_file_is_none(self, monkeyplus):
|
||||||
# when the first file is None, we return False
|
# when the first file is None, we return False
|
||||||
monkeyplus.patch_osstat('second', st_mtime=42)
|
monkeyplus.patch_osstat('second', st_mtime=42)
|
||||||
assert not modified_after(None, 'second') # no crash
|
assert not modified_after(None, 'second') # no crash
|
||||||
|
|
||||||
def test_second_file_is_none(self, monkeyplus):
|
def test_second_file_is_none(self, monkeyplus):
|
||||||
# when the second file is None, we return True
|
# when the second file is None, we return True
|
||||||
monkeyplus.patch_osstat('first', st_mtime=42)
|
monkeyplus.patch_osstat('first', st_mtime=42)
|
||||||
assert modified_after('first', None) # no crash
|
assert modified_after('first', None) # no crash
|
||||||
|
|
||||||
|
|
||||||
class TestCase_delete_if_empty:
|
class TestCase_delete_if_empty:
|
||||||
def test_is_empty(self, tmpdir):
|
def test_is_empty(self, tmpdir):
|
||||||
testpath = Path(str(tmpdir))
|
testpath = Path(str(tmpdir))
|
||||||
assert delete_if_empty(testpath)
|
assert delete_if_empty(testpath)
|
||||||
assert not testpath.exists()
|
assert not testpath.exists()
|
||||||
|
|
||||||
def test_not_empty(self, tmpdir):
|
def test_not_empty(self, tmpdir):
|
||||||
testpath = Path(str(tmpdir))
|
testpath = Path(str(tmpdir))
|
||||||
testpath['foo'].mkdir()
|
testpath['foo'].mkdir()
|
||||||
assert not delete_if_empty(testpath)
|
assert not delete_if_empty(testpath)
|
||||||
assert testpath.exists()
|
assert testpath.exists()
|
||||||
|
|
||||||
def test_with_files_to_delete(self, tmpdir):
|
def test_with_files_to_delete(self, tmpdir):
|
||||||
testpath = Path(str(tmpdir))
|
testpath = Path(str(tmpdir))
|
||||||
testpath['foo'].open('w')
|
testpath['foo'].open('w')
|
||||||
testpath['bar'].open('w')
|
testpath['bar'].open('w')
|
||||||
assert delete_if_empty(testpath, ['foo', 'bar'])
|
assert delete_if_empty(testpath, ['foo', 'bar'])
|
||||||
assert not testpath.exists()
|
assert not testpath.exists()
|
||||||
|
|
||||||
def test_directory_in_files_to_delete(self, tmpdir):
|
def test_directory_in_files_to_delete(self, tmpdir):
|
||||||
testpath = Path(str(tmpdir))
|
testpath = Path(str(tmpdir))
|
||||||
testpath['foo'].mkdir()
|
testpath['foo'].mkdir()
|
||||||
assert not delete_if_empty(testpath, ['foo'])
|
assert not delete_if_empty(testpath, ['foo'])
|
||||||
assert testpath.exists()
|
assert testpath.exists()
|
||||||
|
|
||||||
def test_delete_files_to_delete_only_if_dir_is_empty(self, tmpdir):
|
def test_delete_files_to_delete_only_if_dir_is_empty(self, tmpdir):
|
||||||
testpath = Path(str(tmpdir))
|
testpath = Path(str(tmpdir))
|
||||||
testpath['foo'].open('w')
|
testpath['foo'].open('w')
|
||||||
@@ -252,25 +258,25 @@ class TestCase_delete_if_empty:
|
|||||||
assert not delete_if_empty(testpath, ['foo'])
|
assert not delete_if_empty(testpath, ['foo'])
|
||||||
assert testpath.exists()
|
assert testpath.exists()
|
||||||
assert testpath['foo'].exists()
|
assert testpath['foo'].exists()
|
||||||
|
|
||||||
def test_doesnt_exist(self):
|
def test_doesnt_exist(self):
|
||||||
# When the 'path' doesn't exist, just do nothing.
|
# When the 'path' doesn't exist, just do nothing.
|
||||||
delete_if_empty(Path('does_not_exist')) # no crash
|
delete_if_empty(Path('does_not_exist')) # no crash
|
||||||
|
|
||||||
def test_is_file(self, tmpdir):
|
def test_is_file(self, tmpdir):
|
||||||
# When 'path' is a file, do nothing.
|
# When 'path' is a file, do nothing.
|
||||||
p = Path(str(tmpdir)) + 'filename'
|
p = Path(str(tmpdir)) + 'filename'
|
||||||
p.open('w').close()
|
p.open('w').close()
|
||||||
delete_if_empty(p) # no crash
|
delete_if_empty(p) # no crash
|
||||||
|
|
||||||
def test_ioerror(self, tmpdir, monkeypatch):
|
def test_ioerror(self, tmpdir, monkeypatch):
|
||||||
# if an IO error happens during the operation, ignore it.
|
# if an IO error happens during the operation, ignore it.
|
||||||
def do_raise(*args, **kw):
|
def do_raise(*args, **kw):
|
||||||
raise OSError()
|
raise OSError()
|
||||||
|
|
||||||
monkeypatch.setattr(Path, 'rmdir', do_raise)
|
monkeypatch.setattr(Path, 'rmdir', do_raise)
|
||||||
delete_if_empty(Path(str(tmpdir))) # no crash
|
delete_if_empty(Path(str(tmpdir))) # no crash
|
||||||
|
|
||||||
|
|
||||||
class TestCase_open_if_filename:
|
class TestCase_open_if_filename:
|
||||||
def test_file_name(self, tmpdir):
|
def test_file_name(self, tmpdir):
|
||||||
@@ -280,7 +286,7 @@ class TestCase_open_if_filename:
|
|||||||
assert close
|
assert close
|
||||||
eq_(b'test_data', file.read())
|
eq_(b'test_data', file.read())
|
||||||
file.close()
|
file.close()
|
||||||
|
|
||||||
def test_opened_file(self):
|
def test_opened_file(self):
|
||||||
sio = StringIO()
|
sio = StringIO()
|
||||||
sio.write('test_data')
|
sio.write('test_data')
|
||||||
@@ -288,14 +294,14 @@ class TestCase_open_if_filename:
|
|||||||
file, close = open_if_filename(sio)
|
file, close = open_if_filename(sio)
|
||||||
assert not close
|
assert not close
|
||||||
eq_('test_data', file.read())
|
eq_('test_data', file.read())
|
||||||
|
|
||||||
def test_mode_is_passed_to_open(self, tmpdir):
|
def test_mode_is_passed_to_open(self, tmpdir):
|
||||||
filepath = str(tmpdir.join('test.txt'))
|
filepath = str(tmpdir.join('test.txt'))
|
||||||
open(filepath, 'w').close()
|
open(filepath, 'w').close()
|
||||||
file, close = open_if_filename(filepath, 'a')
|
file, close = open_if_filename(filepath, 'a')
|
||||||
eq_('a', file.mode)
|
eq_('a', file.mode)
|
||||||
file.close()
|
file.close()
|
||||||
|
|
||||||
|
|
||||||
class TestCase_FileOrPath:
|
class TestCase_FileOrPath:
|
||||||
def test_path(self, tmpdir):
|
def test_path(self, tmpdir):
|
||||||
@@ -303,17 +309,17 @@ class TestCase_FileOrPath:
|
|||||||
open(filepath, 'wb').write(b'test_data')
|
open(filepath, 'wb').write(b'test_data')
|
||||||
with FileOrPath(filepath) as fp:
|
with FileOrPath(filepath) as fp:
|
||||||
eq_(b'test_data', fp.read())
|
eq_(b'test_data', fp.read())
|
||||||
|
|
||||||
def test_opened_file(self):
|
def test_opened_file(self):
|
||||||
sio = StringIO()
|
sio = StringIO()
|
||||||
sio.write('test_data')
|
sio.write('test_data')
|
||||||
sio.seek(0)
|
sio.seek(0)
|
||||||
with FileOrPath(sio) as fp:
|
with FileOrPath(sio) as fp:
|
||||||
eq_('test_data', fp.read())
|
eq_('test_data', fp.read())
|
||||||
|
|
||||||
def test_mode_is_passed_to_open(self, tmpdir):
|
def test_mode_is_passed_to_open(self, tmpdir):
|
||||||
filepath = str(tmpdir.join('test.txt'))
|
filepath = str(tmpdir.join('test.txt'))
|
||||||
open(filepath, 'w').close()
|
open(filepath, 'w').close()
|
||||||
with FileOrPath(filepath, 'a') as fp:
|
with FileOrPath(filepath, 'a') as fp:
|
||||||
eq_('a', fp.mode)
|
eq_('a', fp.mode)
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2011-01-11
|
# Created On: 2011-01-11
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
@@ -42,7 +42,7 @@ def minmax(value, min_value, max_value):
|
|||||||
|
|
||||||
def dedupe(iterable):
|
def dedupe(iterable):
|
||||||
"""Returns a list of elements in ``iterable`` with all dupes removed.
|
"""Returns a list of elements in ``iterable`` with all dupes removed.
|
||||||
|
|
||||||
The order of the elements is preserved.
|
The order of the elements is preserved.
|
||||||
"""
|
"""
|
||||||
result = []
|
result = []
|
||||||
@@ -56,7 +56,7 @@ def dedupe(iterable):
|
|||||||
|
|
||||||
def flatten(iterables, start_with=None):
|
def flatten(iterables, start_with=None):
|
||||||
"""Takes a list of lists ``iterables`` and returns a list containing elements of every list.
|
"""Takes a list of lists ``iterables`` and returns a list containing elements of every list.
|
||||||
|
|
||||||
If ``start_with`` is not ``None``, the result will start with ``start_with`` items, exactly as
|
If ``start_with`` is not ``None``, the result will start with ``start_with`` items, exactly as
|
||||||
if ``start_with`` would be the first item of lists.
|
if ``start_with`` would be the first item of lists.
|
||||||
"""
|
"""
|
||||||
@@ -104,7 +104,7 @@ def allsame(iterable):
|
|||||||
|
|
||||||
def trailiter(iterable, skipfirst=False):
|
def trailiter(iterable, skipfirst=False):
|
||||||
"""Yields (prev_element, element), starting with (None, first_element).
|
"""Yields (prev_element, element), starting with (None, first_element).
|
||||||
|
|
||||||
If skipfirst is True, there will be no (None, item1) element and we'll start
|
If skipfirst is True, there will be no (None, item1) element and we'll start
|
||||||
directly with (item1, item2).
|
directly with (item1, item2).
|
||||||
"""
|
"""
|
||||||
@@ -117,6 +117,21 @@ def trailiter(iterable, skipfirst=False):
|
|||||||
yield prev, item
|
yield prev, item
|
||||||
prev = item
|
prev = item
|
||||||
|
|
||||||
|
def iterconsume(seq, reverse=True):
|
||||||
|
"""Iterate over ``seq`` and pops yielded objects.
|
||||||
|
|
||||||
|
Because we use the ``pop()`` method, we reverse ``seq`` before proceeding. If you don't need
|
||||||
|
to do that, set ``reverse`` to ``False``.
|
||||||
|
|
||||||
|
This is useful in tight memory situation where you are looping over a sequence of objects that
|
||||||
|
are going to be discarded afterwards. If you're creating other objects during that iteration
|
||||||
|
you might want to use this to avoid ``MemoryError``.
|
||||||
|
"""
|
||||||
|
if reverse:
|
||||||
|
seq.reverse()
|
||||||
|
while seq:
|
||||||
|
yield seq.pop()
|
||||||
|
|
||||||
#--- String related
|
#--- String related
|
||||||
|
|
||||||
def escape(s, to_escape, escape_with='\\'):
|
def escape(s, to_escape, escape_with='\\'):
|
||||||
@@ -144,7 +159,7 @@ def rem_file_ext(filename):
|
|||||||
|
|
||||||
def pluralize(number, word, decimals=0, plural_word=None):
|
def pluralize(number, word, decimals=0, plural_word=None):
|
||||||
"""Returns a pluralized string with ``number`` in front of ``word``.
|
"""Returns a pluralized string with ``number`` in front of ``word``.
|
||||||
|
|
||||||
Adds a 's' to s if ``number`` > 1.
|
Adds a 's' to s if ``number`` > 1.
|
||||||
``number``: The number to go in front of s
|
``number``: The number to go in front of s
|
||||||
``word``: The word to go after number
|
``word``: The word to go after number
|
||||||
@@ -162,7 +177,7 @@ def pluralize(number, word, decimals=0, plural_word=None):
|
|||||||
|
|
||||||
def format_time(seconds, with_hours=True):
|
def format_time(seconds, with_hours=True):
|
||||||
"""Transforms seconds in a hh:mm:ss string.
|
"""Transforms seconds in a hh:mm:ss string.
|
||||||
|
|
||||||
If ``with_hours`` if false, the format is mm:ss.
|
If ``with_hours`` if false, the format is mm:ss.
|
||||||
"""
|
"""
|
||||||
minus = seconds < 0
|
minus = seconds < 0
|
||||||
@@ -202,14 +217,14 @@ SIZE_DESC = ('B','KB','MB','GB','TB','PB','EB','ZB','YB')
|
|||||||
SIZE_VALS = tuple(1024 ** i for i in range(1,9))
|
SIZE_VALS = tuple(1024 ** i for i in range(1,9))
|
||||||
def format_size(size, decimal=0, forcepower=-1, showdesc=True):
|
def format_size(size, decimal=0, forcepower=-1, showdesc=True):
|
||||||
"""Transform a byte count in a formatted string (KB, MB etc..).
|
"""Transform a byte count in a formatted string (KB, MB etc..).
|
||||||
|
|
||||||
``size`` is the number of bytes to format.
|
``size`` is the number of bytes to format.
|
||||||
``decimal`` is the number digits after the dot.
|
``decimal`` is the number digits after the dot.
|
||||||
``forcepower`` is the desired suffix. 0 is B, 1 is KB, 2 is MB etc.. if kept at -1, the suffix
|
``forcepower`` is the desired suffix. 0 is B, 1 is KB, 2 is MB etc.. if kept at -1, the suffix
|
||||||
will be automatically chosen (so the resulting number is always below 1024).
|
will be automatically chosen (so the resulting number is always below 1024).
|
||||||
if ``showdesc`` is ``True``, the suffix will be shown after the number.
|
if ``showdesc`` is ``True``, the suffix will be shown after the number.
|
||||||
Usage example::
|
Usage example::
|
||||||
|
|
||||||
>>> format_size(1234, decimal=2, showdesc=True)
|
>>> format_size(1234, decimal=2, showdesc=True)
|
||||||
'1.21 KB'
|
'1.21 KB'
|
||||||
"""
|
"""
|
||||||
@@ -283,7 +298,7 @@ def iterdaterange(start, end):
|
|||||||
@pathify
|
@pathify
|
||||||
def modified_after(first_path: Path, second_path: Path):
|
def modified_after(first_path: Path, second_path: Path):
|
||||||
"""Returns ``True`` if first_path's mtime is higher than second_path's mtime.
|
"""Returns ``True`` if first_path's mtime is higher than second_path's mtime.
|
||||||
|
|
||||||
If one of the files doesn't exist or is ``None``, it is considered "never modified".
|
If one of the files doesn't exist or is ``None``, it is considered "never modified".
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
@@ -326,11 +341,11 @@ def delete_if_empty(path: Path, files_to_delete=[]):
|
|||||||
|
|
||||||
def open_if_filename(infile, mode='rb'):
|
def open_if_filename(infile, mode='rb'):
|
||||||
"""If ``infile`` is a string, it opens and returns it. If it's already a file object, it simply returns it.
|
"""If ``infile`` is a string, it opens and returns it. If it's already a file object, it simply returns it.
|
||||||
|
|
||||||
This function returns ``(file, should_close_flag)``. The should_close_flag is True is a file has
|
This function returns ``(file, should_close_flag)``. The should_close_flag is True is a file has
|
||||||
effectively been opened (if we already pass a file object, we assume that the responsibility for
|
effectively been opened (if we already pass a file object, we assume that the responsibility for
|
||||||
closing the file has already been taken). Example usage::
|
closing the file has already been taken). Example usage::
|
||||||
|
|
||||||
fp, shouldclose = open_if_filename(infile)
|
fp, shouldclose = open_if_filename(infile)
|
||||||
dostuff()
|
dostuff()
|
||||||
if shouldclose:
|
if shouldclose:
|
||||||
@@ -370,9 +385,9 @@ def delete_files_with_pattern(folder_path, pattern, recursive=True):
|
|||||||
|
|
||||||
class FileOrPath:
|
class FileOrPath:
|
||||||
"""Does the same as :func:`open_if_filename`, but it can be used with a ``with`` statement.
|
"""Does the same as :func:`open_if_filename`, but it can be used with a ``with`` statement.
|
||||||
|
|
||||||
Example::
|
Example::
|
||||||
|
|
||||||
with FileOrPath(infile):
|
with FileOrPath(infile):
|
||||||
dostuff()
|
dostuff()
|
||||||
"""
|
"""
|
||||||
@@ -381,12 +396,12 @@ class FileOrPath:
|
|||||||
self.mode = mode
|
self.mode = mode
|
||||||
self.mustclose = False
|
self.mustclose = False
|
||||||
self.fp = None
|
self.fp = None
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
self.fp, self.mustclose = open_if_filename(self.file_or_path, self.mode)
|
self.fp, self.mustclose = open_if_filename(self.file_or_path, self.mode)
|
||||||
return self.fp
|
return self.fp
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, traceback):
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
if self.fp and self.mustclose:
|
if self.fp and self.mustclose:
|
||||||
self.fp.close()
|
self.fp.close()
|
||||||
|
|
||||||
|
|||||||
|
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:
|
# Translators:
|
||||||
|
# Harakiri1337, 2014
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: dupeGuru\n"
|
"Project-Id-Version: dupeGuru\n"
|
||||||
"PO-Revision-Date: 2013-11-20 11:53+0000\n"
|
"PO-Revision-Date: 2014-06-03 21:56+0000\n"
|
||||||
"Last-Translator: hsoft <hsoft@hardcoded.net>\n"
|
"Last-Translator: Harakiri1337\n"
|
||||||
"Language-Team: German (http://www.transifex.com/projects/p/dupeguru/language/de/)\n"
|
"Language-Team: German (http://www.transifex.com/projects/p/dupeguru/language/de/)\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: utf-8\n"
|
"Content-Transfer-Encoding: utf-8\n"
|
||||||
@@ -32,7 +33,7 @@ msgstr "Ordner"
|
|||||||
#: core/prioritize.py:88 core_me/result_table.py:18 core_pe/result_table.py:18
|
#: core/prioritize.py:88 core_me/result_table.py:18 core_pe/result_table.py:18
|
||||||
#: core_se/result_table.py:18
|
#: core_se/result_table.py:18
|
||||||
msgid "Filename"
|
msgid "Filename"
|
||||||
msgstr "Filename"
|
msgstr "Dateiname"
|
||||||
|
|
||||||
#: core/prioritize.py:147
|
#: core/prioritize.py:147
|
||||||
msgid "Size"
|
msgid "Size"
|
||||||
@@ -41,11 +42,11 @@ msgstr "Größe"
|
|||||||
#: core/prioritize.py:153 core_me/result_table.py:25
|
#: core/prioritize.py:153 core_me/result_table.py:25
|
||||||
#: core_pe/result_table.py:24 core_se/result_table.py:22
|
#: core_pe/result_table.py:24 core_se/result_table.py:22
|
||||||
msgid "Modification"
|
msgid "Modification"
|
||||||
msgstr "Modifikation"
|
msgstr "Geändert"
|
||||||
|
|
||||||
#: core_me/prioritize.py:16
|
#: core_me/prioritize.py:16
|
||||||
msgid "Duration"
|
msgid "Duration"
|
||||||
msgstr ""
|
msgstr "Dauer"
|
||||||
|
|
||||||
#: core_me/prioritize.py:22 core_me/result_table.py:22
|
#: core_me/prioritize.py:22 core_me/result_table.py:22
|
||||||
msgid "Bitrate"
|
msgid "Bitrate"
|
||||||
@@ -53,7 +54,7 @@ msgstr "Bitrate"
|
|||||||
|
|
||||||
#: core_me/prioritize.py:28
|
#: core_me/prioritize.py:28
|
||||||
msgid "Samplerate"
|
msgid "Samplerate"
|
||||||
msgstr ""
|
msgstr "Abtastrate"
|
||||||
|
|
||||||
#: core_me/result_table.py:20
|
#: core_me/result_table.py:20
|
||||||
msgid "Size (MB)"
|
msgid "Size (MB)"
|
||||||
@@ -89,7 +90,7 @@ msgstr "Jahr"
|
|||||||
|
|
||||||
#: core_me/result_table.py:31
|
#: core_me/result_table.py:31
|
||||||
msgid "Track Number"
|
msgid "Track Number"
|
||||||
msgstr "Stück Nummer"
|
msgstr "Titel Nummer"
|
||||||
|
|
||||||
#: core_me/result_table.py:32
|
#: core_me/result_table.py:32
|
||||||
msgid "Comment"
|
msgid "Comment"
|
||||||
@@ -102,16 +103,16 @@ msgstr "Übereinstimmung %"
|
|||||||
|
|
||||||
#: core_me/result_table.py:34 core_se/result_table.py:24
|
#: core_me/result_table.py:34 core_se/result_table.py:24
|
||||||
msgid "Words Used"
|
msgid "Words Used"
|
||||||
msgstr "Wörter genutzt"
|
msgstr "genutzte Wörter"
|
||||||
|
|
||||||
#: core_me/result_table.py:35 core_pe/result_table.py:26
|
#: core_me/result_table.py:35 core_pe/result_table.py:26
|
||||||
#: core_se/result_table.py:25
|
#: core_se/result_table.py:25
|
||||||
msgid "Dupe Count"
|
msgid "Dupe Count"
|
||||||
msgstr "Anzahl Duplikate"
|
msgstr "Anzahl der Duplikate"
|
||||||
|
|
||||||
#: core_pe/prioritize.py:16 core_pe/result_table.py:22
|
#: core_pe/prioritize.py:16 core_pe/result_table.py:22
|
||||||
msgid "Dimensions"
|
msgid "Dimensions"
|
||||||
msgstr "Dimensionen"
|
msgstr "Auflösung"
|
||||||
|
|
||||||
#: core_pe/result_table.py:20 core_se/result_table.py:20
|
#: core_pe/result_table.py:20 core_se/result_table.py:20
|
||||||
msgid "Size (KB)"
|
msgid "Size (KB)"
|
||||||
@@ -119,4 +120,4 @@ msgstr "Größe (KB)"
|
|||||||
|
|
||||||
#: core_pe/result_table.py:23
|
#: core_pe/result_table.py:23
|
||||||
msgid "EXIF Timestamp"
|
msgid "EXIF Timestamp"
|
||||||
msgstr ""
|
msgstr "EXIF Zeitstempel"
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
# Translators:
|
# Translators:
|
||||||
|
# Harakiri1337, 2014
|
||||||
|
# Frank Weber <frank.weber@gmail.com>, 2014
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: dupeGuru\n"
|
"Project-Id-Version: dupeGuru\n"
|
||||||
"PO-Revision-Date: 2013-12-07 15:22+0000\n"
|
"PO-Revision-Date: 2014-09-26 21:24+0000\n"
|
||||||
"Last-Translator: hsoft <hsoft@hardcoded.net>\n"
|
"Last-Translator: Frank Weber <frank.weber@gmail.com>\n"
|
||||||
"Language-Team: German (http://www.transifex.com/projects/p/dupeguru/language/de/)\n"
|
"Language-Team: German (http://www.transifex.com/projects/p/dupeguru/language/de/)\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: utf-8\n"
|
"Content-Transfer-Encoding: utf-8\n"
|
||||||
@@ -12,17 +14,19 @@ msgstr ""
|
|||||||
|
|
||||||
#: core/app.py:39
|
#: core/app.py:39
|
||||||
msgid "There are no marked duplicates. Nothing has been done."
|
msgid "There are no marked duplicates. Nothing has been done."
|
||||||
msgstr ""
|
msgstr "Keine markierten Duplikate, daher wurde nichts getan."
|
||||||
|
|
||||||
#: core/app.py:40
|
#: core/app.py:40
|
||||||
msgid "There are no selected duplicates. Nothing has been done."
|
msgid "There are no selected duplicates. Nothing has been done."
|
||||||
msgstr ""
|
msgstr "Keine ausgewählten Duplikate, daher wurde nichts getan."
|
||||||
|
|
||||||
#: core/app.py:41
|
#: core/app.py:41
|
||||||
msgid ""
|
msgid ""
|
||||||
"You're about to open many files at once. Depending on what those files are "
|
"You're about to open many files at once. Depending on what those files are "
|
||||||
"opened with, doing so can create quite a mess. Continue?"
|
"opened with, doing so can create quite a mess. Continue?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Sie sind dabei, sehr viele Dateien gleichzeitig zu öffnen. Das kann zu "
|
||||||
|
"ziemlichem Durcheinander führen! Trotzdem fortfahren?"
|
||||||
|
|
||||||
#: core/app.py:57
|
#: core/app.py:57
|
||||||
msgid "Scanning for duplicates"
|
msgid "Scanning for duplicates"
|
||||||
@@ -30,23 +34,23 @@ msgstr "Suche nach Duplikaten"
|
|||||||
|
|
||||||
#: core/app.py:58
|
#: core/app.py:58
|
||||||
msgid "Loading"
|
msgid "Loading"
|
||||||
msgstr "Laden"
|
msgstr "Lade"
|
||||||
|
|
||||||
#: core/app.py:59
|
#: core/app.py:59
|
||||||
msgid "Moving"
|
msgid "Moving"
|
||||||
msgstr "Verschieben"
|
msgstr "Verschiebe"
|
||||||
|
|
||||||
#: core/app.py:60
|
#: core/app.py:60
|
||||||
msgid "Copying"
|
msgid "Copying"
|
||||||
msgstr "Kopieren"
|
msgstr "Kopiere"
|
||||||
|
|
||||||
#: core/app.py:61
|
#: core/app.py:61
|
||||||
msgid "Sending to Trash"
|
msgid "Sending to Trash"
|
||||||
msgstr "Verschiebe in den Mülleimer"
|
msgstr "Verschiebe in den Papierkorb"
|
||||||
|
|
||||||
#: core/app.py:64
|
#: core/app.py:64
|
||||||
msgid "Sending files to the recycle bin"
|
msgid "Sending files to the recycle bin"
|
||||||
msgstr "Sende Dateien in den Mülleimer"
|
msgstr "Verschiebe Dateien in den Papierkorb"
|
||||||
|
|
||||||
#: core/app.py:290
|
#: core/app.py:290
|
||||||
msgid ""
|
msgid ""
|
||||||
@@ -62,29 +66,32 @@ msgstr "Keine Duplikate gefunden."
|
|||||||
|
|
||||||
#: core/app.py:310
|
#: core/app.py:310
|
||||||
msgid "All marked files were copied successfully."
|
msgid "All marked files were copied successfully."
|
||||||
msgstr ""
|
msgstr "Alle markierten Dateien wurden erfolgreich kopiert."
|
||||||
|
|
||||||
#: core/app.py:311
|
#: core/app.py:311
|
||||||
msgid "All marked files were moved successfully."
|
msgid "All marked files were moved successfully."
|
||||||
msgstr ""
|
msgstr "Alle markierten Dateien wurden erfolgreich verschoben."
|
||||||
|
|
||||||
#: core/app.py:312
|
#: core/app.py:312
|
||||||
msgid "All marked files were successfully sent to Trash."
|
msgid "All marked files were successfully sent to Trash."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Alle markierten Dateien wurden erfolgreich in den Papierkorb verschoben."
|
||||||
|
|
||||||
#: core/app.py:349
|
#: core/app.py:349
|
||||||
msgid "'{}' already is in the list."
|
msgid "'{}' already is in the list."
|
||||||
msgstr ""
|
msgstr "'{}' ist bereits in der Liste."
|
||||||
|
|
||||||
#: core/app.py:351
|
#: core/app.py:351
|
||||||
msgid "'{}' does not exist."
|
msgid "'{}' does not exist."
|
||||||
msgstr ""
|
msgstr "'{}' existiert nicht."
|
||||||
|
|
||||||
#: core/app.py:360
|
#: core/app.py:360
|
||||||
msgid ""
|
msgid ""
|
||||||
"All selected %d matches are going to be ignored in all subsequent scans. "
|
"All selected %d matches are going to be ignored in all subsequent scans. "
|
||||||
"Continue?"
|
"Continue?"
|
||||||
msgstr "%d Dateien werden in zukünftigen Scans ignoriert werden. Fortfahren?"
|
msgstr ""
|
||||||
|
"Alle %d ausgewählten Dateien werden in zukünftigen Scans ignoriert. "
|
||||||
|
"Fortfahren?"
|
||||||
|
|
||||||
#: core/app.py:426
|
#: core/app.py:426
|
||||||
msgid "copy"
|
msgid "copy"
|
||||||
@@ -96,17 +103,17 @@ msgstr "verschieben"
|
|||||||
|
|
||||||
#: core/app.py:427
|
#: core/app.py:427
|
||||||
msgid "Select a directory to {} marked files to"
|
msgid "Select a directory to {} marked files to"
|
||||||
msgstr "Wählen sie einen Ordner zum {} der ausgewählten Dateien"
|
msgstr "Wählen Sie einen Ordner zum {} der ausgewählten Dateien."
|
||||||
|
|
||||||
#: core/app.py:464
|
#: core/app.py:464
|
||||||
msgid "Select a destination for your exported CSV"
|
msgid "Select a destination for your exported CSV"
|
||||||
msgstr ""
|
msgstr "Zielverzeichnis für den CSV Export angeben"
|
||||||
|
|
||||||
#: core/app.py:489
|
#: core/app.py:489
|
||||||
msgid "You have no custom command set up. Set it up in your preferences."
|
msgid "You have no custom command set up. Set it up in your preferences."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Sie haben keinen eigenen Befehl erstellt. Bitte in den Einstellungen "
|
"Sie haben noch keinen Befehl erstellt. Bitte dies in den Einstellungen vornehmen.\n"
|
||||||
"konfigurieren."
|
"Bsp.: \"C:\\Program Files\\Diff\\Diff.exe\" \"%d\" \"%r\""
|
||||||
|
|
||||||
#: core/app.py:641 core/app.py:654
|
#: core/app.py:641 core/app.py:654
|
||||||
msgid "You are about to remove %d files from results. Continue?"
|
msgid "You are about to remove %d files from results. Continue?"
|
||||||
@@ -114,15 +121,15 @@ msgstr "%d Dateien werden aus der Ergebnisliste entfernt. Fortfahren?"
|
|||||||
|
|
||||||
#: core/app.py:688
|
#: core/app.py:688
|
||||||
msgid "{} duplicate groups were changed by the re-prioritization."
|
msgid "{} duplicate groups were changed by the re-prioritization."
|
||||||
msgstr ""
|
msgstr "{} Duplikat-Gruppen wurden durch die Neu-Priorisierung geändert."
|
||||||
|
|
||||||
#: core/app.py:716
|
#: core/app.py:716
|
||||||
msgid "Collecting files to scan"
|
msgid "Collecting files to scan"
|
||||||
msgstr "Sammle Dateien zum Scannen"
|
msgstr "Sammle zu scannende Dateien..."
|
||||||
|
|
||||||
#: core/app.py:727
|
#: core/app.py:727
|
||||||
msgid "The selected directories contain no scannable file."
|
msgid "The selected directories contain no scannable file."
|
||||||
msgstr "Der ausgewählte Ordner enthält keine scannbare Dateien."
|
msgstr "Ausgewählte Ordner enthalten keine scannbaren Dateien."
|
||||||
|
|
||||||
#: core/app.py:768
|
#: core/app.py:768
|
||||||
msgid "%s (%d discarded)"
|
msgid "%s (%d discarded)"
|
||||||
@@ -130,11 +137,11 @@ msgstr "%s (%d verworfen)"
|
|||||||
|
|
||||||
#: core/engine.py:220 core/engine.py:265
|
#: core/engine.py:220 core/engine.py:265
|
||||||
msgid "0 matches found"
|
msgid "0 matches found"
|
||||||
msgstr "0 Paare gefunden"
|
msgstr "0 Übereinstimmungen gefunden"
|
||||||
|
|
||||||
#: core/engine.py:238 core/engine.py:273
|
#: core/engine.py:238 core/engine.py:273
|
||||||
msgid "%d matches found"
|
msgid "%d matches found"
|
||||||
msgstr "%d Paare gefunden"
|
msgstr "%d Übereinstimmungen gefunden"
|
||||||
|
|
||||||
#: core/engine.py:258 core/scanner.py:79
|
#: core/engine.py:258 core/scanner.py:79
|
||||||
msgid "Read size of %d/%d files"
|
msgid "Read size of %d/%d files"
|
||||||
@@ -142,51 +149,51 @@ msgstr "Lese Größe von %d/%d Dateien"
|
|||||||
|
|
||||||
#: core/engine.py:464
|
#: core/engine.py:464
|
||||||
msgid "Grouped %d/%d matches"
|
msgid "Grouped %d/%d matches"
|
||||||
msgstr "%d/%d Paare gruppiert"
|
msgstr "%d/%d Übereinstimmungen gruppiert"
|
||||||
|
|
||||||
#: core/gui/deletion_options.py:69
|
#: core/gui/deletion_options.py:69
|
||||||
msgid "You are sending {} file(s) to the Trash."
|
msgid "You are sending {} file(s) to the Trash."
|
||||||
msgstr ""
|
msgstr "Verschiebe {} Datei(en) in den Papierkorb."
|
||||||
|
|
||||||
#: core/gui/ignore_list_dialog.py:24
|
#: core/gui/ignore_list_dialog.py:24
|
||||||
msgid "Do you really want to remove all %d items from the ignore list?"
|
msgid "Do you really want to remove all %d items from the ignore list?"
|
||||||
msgstr "Möchten Sie wirklich alle %d Einträge aus der Ignorier-Liste löschen?"
|
msgstr "Möchten Sie wirklich alle %d Einträge aus der Ausnahmeliste löschen?"
|
||||||
|
|
||||||
#: core/prioritize.py:68
|
#: core/prioritize.py:68
|
||||||
msgid "None"
|
msgid "None"
|
||||||
msgstr ""
|
msgstr "Nichts"
|
||||||
|
|
||||||
#: core/prioritize.py:96
|
#: core/prioritize.py:96
|
||||||
msgid "Ends with number"
|
msgid "Ends with number"
|
||||||
msgstr "Ends with number"
|
msgstr "Endet mit Zahl"
|
||||||
|
|
||||||
#: core/prioritize.py:97
|
#: core/prioritize.py:97
|
||||||
msgid "Doesn't end with number"
|
msgid "Doesn't end with number"
|
||||||
msgstr "Doesn't end with number"
|
msgstr "Endet nicht mit Zahl"
|
||||||
|
|
||||||
#: core/prioritize.py:98
|
#: core/prioritize.py:98
|
||||||
msgid "Longest"
|
msgid "Longest"
|
||||||
msgstr ""
|
msgstr "Längste"
|
||||||
|
|
||||||
#: core/prioritize.py:99
|
#: core/prioritize.py:99
|
||||||
msgid "Shortest"
|
msgid "Shortest"
|
||||||
msgstr ""
|
msgstr "Kürzeste"
|
||||||
|
|
||||||
#: core/prioritize.py:132
|
#: core/prioritize.py:132
|
||||||
msgid "Highest"
|
msgid "Highest"
|
||||||
msgstr "Highest"
|
msgstr "Höchste"
|
||||||
|
|
||||||
#: core/prioritize.py:132
|
#: core/prioritize.py:132
|
||||||
msgid "Lowest"
|
msgid "Lowest"
|
||||||
msgstr "Lowest"
|
msgstr "Niedrigste"
|
||||||
|
|
||||||
#: core/prioritize.py:159
|
#: core/prioritize.py:159
|
||||||
msgid "Newest"
|
msgid "Newest"
|
||||||
msgstr "Newest"
|
msgstr "Neuste"
|
||||||
|
|
||||||
#: core/prioritize.py:159
|
#: core/prioritize.py:159
|
||||||
msgid "Oldest"
|
msgid "Oldest"
|
||||||
msgstr "Oldest"
|
msgstr "Älterste"
|
||||||
|
|
||||||
#: core/results.py:126
|
#: core/results.py:126
|
||||||
msgid "%d / %d (%s / %s) duplicates marked."
|
msgid "%d / %d (%s / %s) duplicates marked."
|
||||||
@@ -202,11 +209,11 @@ msgstr "Lese Metadaten von %d/%d Dateien"
|
|||||||
|
|
||||||
#: core/scanner.py:130
|
#: core/scanner.py:130
|
||||||
msgid "Removing false matches"
|
msgid "Removing false matches"
|
||||||
msgstr "Entferne Falschpositive."
|
msgstr "Entferne falsche Übereinstimmungen"
|
||||||
|
|
||||||
#: core/scanner.py:154
|
#: core/scanner.py:154
|
||||||
msgid "Processed %d/%d matches against the ignore list"
|
msgid "Processed %d/%d matches against the ignore list"
|
||||||
msgstr "Verarbeitung von %d/%d Paaren gegen die Ignorier-Liste"
|
msgstr "%d/%d Treffer mit der Ausnahmeliste abgeglichen"
|
||||||
|
|
||||||
#: core/scanner.py:176
|
#: core/scanner.py:176
|
||||||
msgid "Doing group prioritization"
|
msgid "Doing group prioritization"
|
||||||
@@ -214,20 +221,20 @@ msgstr "Gruppenpriorisierung"
|
|||||||
|
|
||||||
#: core_pe/matchblock.py:61
|
#: core_pe/matchblock.py:61
|
||||||
msgid "Analyzed %d/%d pictures"
|
msgid "Analyzed %d/%d pictures"
|
||||||
msgstr "Analysiere %d/%d Bilder"
|
msgstr "Analysiere Bild %d/%d"
|
||||||
|
|
||||||
#: core_pe/matchblock.py:153
|
#: core_pe/matchblock.py:153
|
||||||
msgid "Performed %d/%d chunk matches"
|
msgid "Performed %d/%d chunk matches"
|
||||||
msgstr "Performed %d/%d chunk matches"
|
msgstr "%d/%d Chunk-Matches ausgeführt"
|
||||||
|
|
||||||
#: core_pe/matchblock.py:158
|
#: core_pe/matchblock.py:158
|
||||||
msgid "Preparing for matching"
|
msgid "Preparing for matching"
|
||||||
msgstr "Vorbereitung auf den Vergleich"
|
msgstr "Bereite Matching vor"
|
||||||
|
|
||||||
#: core_pe/matchblock.py:193
|
#: core_pe/matchblock.py:193
|
||||||
msgid "Verified %d/%d matches"
|
msgid "Verified %d/%d matches"
|
||||||
msgstr "%d/%d verifizierte Paare"
|
msgstr "%d/%d verifizierte Übereinstimmungen"
|
||||||
|
|
||||||
#: core_pe/matchexif.py:18
|
#: core_pe/matchexif.py:18
|
||||||
msgid "Read EXIF of %d/%d pictures"
|
msgid "Read EXIF of %d/%d pictures"
|
||||||
msgstr ""
|
msgstr "Lese EXIF von Bild %d/%d"
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
# Translators:
|
# Translators:
|
||||||
|
# Harakiri1337, 2014
|
||||||
|
# Frank Weber <frank.weber@gmail.com>, 2014
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: dupeGuru\n"
|
"Project-Id-Version: dupeGuru\n"
|
||||||
"PO-Revision-Date: 2013-12-07 15:22+0000\n"
|
"PO-Revision-Date: 2014-09-26 21:15+0000\n"
|
||||||
"Last-Translator: hsoft <hsoft@hardcoded.net>\n"
|
"Last-Translator: Frank Weber <frank.weber@gmail.com>\n"
|
||||||
"Language-Team: German (http://www.transifex.com/projects/p/dupeguru/language/de/)\n"
|
"Language-Team: German (http://www.transifex.com/projects/p/dupeguru/language/de/)\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: utf-8\n"
|
"Content-Transfer-Encoding: utf-8\n"
|
||||||
@@ -12,49 +14,50 @@ msgstr ""
|
|||||||
|
|
||||||
#: cocoa/inter/app_me.py:34
|
#: cocoa/inter/app_me.py:34
|
||||||
msgid "Removing dead tracks from your iTunes Library"
|
msgid "Removing dead tracks from your iTunes Library"
|
||||||
msgstr "Entferne tote Stücke aus Ihrer iTunes Bibliothek."
|
msgstr "Entferne tote Tracks aus Ihrer iTunes Bibliothek"
|
||||||
|
|
||||||
#: cocoa/inter/app_me.py:35
|
#: cocoa/inter/app_me.py:35
|
||||||
msgid "Scanning the iTunes Library"
|
msgid "Scanning the iTunes Library"
|
||||||
msgstr "Scanne die iTunes Bibiliothek"
|
msgstr "Durchsuche die iTunes-Bibliothek"
|
||||||
|
|
||||||
#: cocoa/inter/app_me.py:158 cocoa/inter/app_pe.py:200
|
#: cocoa/inter/app_me.py:158 cocoa/inter/app_pe.py:200
|
||||||
msgid "Sending dupes to the Trash"
|
msgid "Sending dupes to the Trash"
|
||||||
msgstr "Verschiebe Duplikate in den Mülleimer"
|
msgstr "Schicke Duplikate in den Papierkorb"
|
||||||
|
|
||||||
#: cocoa/inter/app_me.py:160
|
#: cocoa/inter/app_me.py:160
|
||||||
msgid "Talking to iTunes. Don't touch it!"
|
msgid "Talking to iTunes. Don't touch it!"
|
||||||
msgstr ""
|
msgstr "Kommuniziere mit iTunes. Bitte warten!"
|
||||||
|
|
||||||
#: cocoa/inter/app_me.py:195
|
#: cocoa/inter/app_me.py:195
|
||||||
msgid ""
|
msgid ""
|
||||||
"Your iTunes Library contains %d dead tracks ready to be removed. Continue?"
|
"Your iTunes Library contains %d dead tracks ready to be removed. Continue?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Your iTunes Library contains %d dead tracks ready to be removed. Continue?"
|
"Ihre iTunes-Bibliothek enthält %d tote Tracks zum Entfernen. Fortsetzen?"
|
||||||
|
|
||||||
#: cocoa/inter/app_me.py:199
|
#: cocoa/inter/app_me.py:199
|
||||||
msgid "You have no dead tracks in your iTunes Library"
|
msgid "You have no dead tracks in your iTunes Library"
|
||||||
msgstr "You have no dead tracks in your iTunes Library"
|
msgstr "Sie haben keine toten Tracks in Ihrer iTunes-Bibliothek"
|
||||||
|
|
||||||
#: cocoa/inter/app_me.py:217
|
#: cocoa/inter/app_me.py:217
|
||||||
msgid "The iTunes application couldn't be found."
|
msgid "The iTunes application couldn't be found."
|
||||||
msgstr ""
|
msgstr "Das iTunes-Programm konnte nicht gefunden werden."
|
||||||
|
|
||||||
#: cocoa/inter/app_pe.py:202
|
#: cocoa/inter/app_pe.py:202
|
||||||
msgid "Talking to iPhoto. Don't touch it!"
|
msgid "Talking to iPhoto. Don't touch it!"
|
||||||
msgstr ""
|
msgstr "Kommuniziere mit iPhoto. Bitte warten!"
|
||||||
|
|
||||||
#: cocoa/inter/app_pe.py:211
|
#: cocoa/inter/app_pe.py:211
|
||||||
msgid "Talking to Aperture. Don't touch it!"
|
msgid "Talking to Aperture. Don't touch it!"
|
||||||
msgstr ""
|
msgstr "Kommuniziere mit Aperture. Bitte warten!"
|
||||||
|
|
||||||
#: cocoa/inter/app_pe.py:284
|
#: cocoa/inter/app_pe.py:284
|
||||||
msgid "Deleted Aperture photos were sent to a project called \"dupeGuru Trash\"."
|
msgid "Deleted Aperture photos were sent to a project called \"dupeGuru Trash\"."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Gelöschte Aperture-Fotos wurden dem Projekt \"dupeGuru Trash\" hinzugefügt."
|
||||||
|
|
||||||
#: cocoa/inter/app_pe.py:310
|
#: cocoa/inter/app_pe.py:310
|
||||||
msgid "The iPhoto application couldn't be found."
|
msgid "The iPhoto application couldn't be found."
|
||||||
msgstr "The iPhoto application couldn't be found."
|
msgstr "Das iPhoto-Programm konnte nicht gefunden werden."
|
||||||
|
|
||||||
#: qt/base/app.py:83
|
#: qt/base/app.py:83
|
||||||
msgid "Quit"
|
msgid "Quit"
|
||||||
@@ -67,7 +70,7 @@ msgstr "Einstellungen"
|
|||||||
#: qt/base/app.py:85 qt/base/ignore_list_dialog.py:32
|
#: qt/base/app.py:85 qt/base/ignore_list_dialog.py:32
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Ignore List"
|
msgid "Ignore List"
|
||||||
msgstr ""
|
msgstr "Ausnahme-Liste"
|
||||||
|
|
||||||
#: qt/base/app.py:86 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/app.py:86 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "dupeGuru Help"
|
msgid "dupeGuru Help"
|
||||||
@@ -87,51 +90,56 @@ msgstr "Debug Log öffnen"
|
|||||||
|
|
||||||
#: qt/base/app.py:198
|
#: qt/base/app.py:198
|
||||||
msgid "{} file (*.{})"
|
msgid "{} file (*.{})"
|
||||||
msgstr ""
|
msgstr "{} Datei (*.{})"
|
||||||
|
|
||||||
#: qt/base/deletion_options.py:30 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/deletion_options.py:30 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Deletion Options"
|
msgid "Deletion Options"
|
||||||
msgstr ""
|
msgstr "Lösch-Optionen"
|
||||||
|
|
||||||
#: qt/base/deletion_options.py:35 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/deletion_options.py:35 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Link deleted files"
|
msgid "Link deleted files"
|
||||||
msgstr ""
|
msgstr "Verlinke gelöschte Dateien"
|
||||||
|
|
||||||
#: qt/base/deletion_options.py:37 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/deletion_options.py:37 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid ""
|
msgid ""
|
||||||
"After having deleted a duplicate, place a link targeting the reference file "
|
"After having deleted a duplicate, place a link targeting the reference file "
|
||||||
"to replace the deleted file."
|
"to replace the deleted file."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Doppelte Dateien werden gelöscht, an deren Stelle wird eine Verknüpfung auf "
|
||||||
|
"die Referenz-Datei erstellt."
|
||||||
|
|
||||||
#: qt/base/deletion_options.py:42
|
#: qt/base/deletion_options.py:42
|
||||||
msgid "Hardlink"
|
msgid "Hardlink"
|
||||||
msgstr ""
|
msgstr "Hardlink"
|
||||||
|
|
||||||
#: qt/base/deletion_options.py:42
|
#: qt/base/deletion_options.py:42
|
||||||
msgid "Symlink"
|
msgid "Symlink"
|
||||||
msgstr ""
|
msgstr "Symlink"
|
||||||
|
|
||||||
#: qt/base/deletion_options.py:46
|
#: qt/base/deletion_options.py:46
|
||||||
msgid " (unsupported)"
|
msgid " (unsupported)"
|
||||||
msgstr ""
|
msgstr "(nicht unterstützt)"
|
||||||
|
|
||||||
#: qt/base/deletion_options.py:47 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/deletion_options.py:47 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Directly delete files"
|
msgid "Directly delete files"
|
||||||
msgstr ""
|
msgstr "Ohne Papierkorb löschen"
|
||||||
|
|
||||||
#: qt/base/deletion_options.py:49 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/deletion_options.py:49 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid ""
|
msgid ""
|
||||||
"Instead of sending files to trash, delete them directly. This option is "
|
"Instead of sending files to trash, delete them directly. This option is "
|
||||||
"usually used as a workaround when the normal deletion method doesn't work."
|
"usually used as a workaround when the normal deletion method doesn't work."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Anstatt Dateien in den Papierkorb zu verschieben, können Sie diese direkt "
|
||||||
|
"löschen. Diese Option wird in der Regel genutzt, falls die normale "
|
||||||
|
"Löschmethode nicht funktioniert."
|
||||||
|
|
||||||
#: qt/base/deletion_options.py:55 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/deletion_options.py:55 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Proceed"
|
msgid "Proceed"
|
||||||
msgstr ""
|
msgstr "Fortfahren"
|
||||||
|
|
||||||
#: qt/base/deletion_options.py:56 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/deletion_options.py:56 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr "Cancel"
|
msgstr "Abbrechen"
|
||||||
|
|
||||||
#: qt/base/details_table.py:16 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/details_table.py:16 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Attribute"
|
msgid "Attribute"
|
||||||
@@ -148,7 +156,7 @@ msgstr "Referenz"
|
|||||||
|
|
||||||
#: qt/base/directories_dialog.py:58 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/directories_dialog.py:58 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Load Results..."
|
msgid "Load Results..."
|
||||||
msgstr "Lade Ergebnisse..."
|
msgstr "Ergebnis laden..."
|
||||||
|
|
||||||
#: qt/base/directories_dialog.py:59 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/directories_dialog.py:59 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Results Window"
|
msgid "Results Window"
|
||||||
@@ -161,7 +169,7 @@ msgstr "Ordner hinzufügen..."
|
|||||||
#: qt/base/directories_dialog.py:68 qt/base/result_window.py:77
|
#: qt/base/directories_dialog.py:68 qt/base/result_window.py:77
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "File"
|
msgid "File"
|
||||||
msgstr "Ablage"
|
msgstr "Datei"
|
||||||
|
|
||||||
#: qt/base/directories_dialog.py:70 qt/base/result_window.py:85
|
#: qt/base/directories_dialog.py:70 qt/base/result_window.py:85
|
||||||
msgid "View"
|
msgid "View"
|
||||||
@@ -174,11 +182,11 @@ msgstr "Hilfe"
|
|||||||
|
|
||||||
#: qt/base/directories_dialog.py:74 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/directories_dialog.py:74 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Load Recent Results"
|
msgid "Load Recent Results"
|
||||||
msgstr "Lade letzte Ergebnisse"
|
msgstr "Lade letztes Suchergebnis"
|
||||||
|
|
||||||
#: qt/base/directories_dialog.py:108 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/directories_dialog.py:108 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Select folders to scan and press \"Scan\"."
|
msgid "Select folders to scan and press \"Scan\"."
|
||||||
msgstr "Zu scannende Ordner auswählen und \"Scan\" drücken."
|
msgstr "Zu durchsuchende Ordner auswählen und \"Suche starten\" drücken."
|
||||||
|
|
||||||
#: qt/base/directories_dialog.py:132 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/directories_dialog.py:132 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Load Results"
|
msgid "Load Results"
|
||||||
@@ -186,7 +194,7 @@ msgstr "Lade Ergebnisse"
|
|||||||
|
|
||||||
#: qt/base/directories_dialog.py:135 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/directories_dialog.py:135 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Scan"
|
msgid "Scan"
|
||||||
msgstr "Scan"
|
msgstr "Suche starten"
|
||||||
|
|
||||||
#: qt/base/directories_dialog.py:179
|
#: qt/base/directories_dialog.py:179
|
||||||
msgid "Unsaved results"
|
msgid "Unsaved results"
|
||||||
@@ -194,15 +202,16 @@ msgstr "Ungespeicherte Ergebnisse"
|
|||||||
|
|
||||||
#: qt/base/directories_dialog.py:180 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/directories_dialog.py:180 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "You have unsaved results, do you really want to quit?"
|
msgid "You have unsaved results, do you really want to quit?"
|
||||||
msgstr "Sie haben ungespeicherte Ergebnisse. Wollen Sie wirklich beenden?"
|
msgstr ""
|
||||||
|
"Sie haben ungespeicherte Ergebnisse. Wollen Sie wirklich dupeGuru beenden?"
|
||||||
|
|
||||||
#: qt/base/directories_dialog.py:188 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/directories_dialog.py:188 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Select a folder to add to the scanning list"
|
msgid "Select a folder to add to the scanning list"
|
||||||
msgstr "Wählen Sie einen Ordner aus, um ihn der Scanliste hinzuzufügen."
|
msgstr "Wählen Sie einen Ordner aus, um ihn der Scanliste hinzuzufügen"
|
||||||
|
|
||||||
#: qt/base/directories_dialog.py:205 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/directories_dialog.py:205 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Select a results file to load"
|
msgid "Select a results file to load"
|
||||||
msgstr "Wählen Sie eine Ergebnisliste zum Laden aus."
|
msgstr "Wählen Sie eine Ergebnisdatei zum Laden aus"
|
||||||
|
|
||||||
#: qt/base/directories_dialog.py:206
|
#: qt/base/directories_dialog.py:206
|
||||||
msgid "All Files (*.*)"
|
msgid "All Files (*.*)"
|
||||||
@@ -210,11 +219,11 @@ msgstr "Alle Dateien (*.*)"
|
|||||||
|
|
||||||
#: qt/base/directories_dialog.py:206 qt/base/result_window.py:287
|
#: qt/base/directories_dialog.py:206 qt/base/result_window.py:287
|
||||||
msgid "dupeGuru Results (*.dupeguru)"
|
msgid "dupeGuru Results (*.dupeguru)"
|
||||||
msgstr "dupeGuru Ergebnisse (*.dupeguru)"
|
msgstr "dupeGuru Suchergebnisse (*.dupeguru)"
|
||||||
|
|
||||||
#: qt/base/directories_dialog.py:217
|
#: qt/base/directories_dialog.py:217
|
||||||
msgid "Start a new scan"
|
msgid "Start a new scan"
|
||||||
msgstr "Starte einen neuen Scan"
|
msgstr "Starte einen neuen Suchlauf"
|
||||||
|
|
||||||
#: qt/base/directories_dialog.py:218 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/directories_dialog.py:218 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "You have unsaved results, do you really want to continue?"
|
msgid "You have unsaved results, do you really want to continue?"
|
||||||
@@ -238,11 +247,11 @@ msgstr "Normal"
|
|||||||
|
|
||||||
#: qt/base/ignore_list_dialog.py:45 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/ignore_list_dialog.py:45 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Remove Selected"
|
msgid "Remove Selected"
|
||||||
msgstr ""
|
msgstr "Auswahl löschen"
|
||||||
|
|
||||||
#: qt/base/ignore_list_dialog.py:46 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/ignore_list_dialog.py:46 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Clear"
|
msgid "Clear"
|
||||||
msgstr ""
|
msgstr "Liste leeren"
|
||||||
|
|
||||||
#: qt/base/ignore_list_dialog.py:47 qt/base/problem_dialog.py:57
|
#: qt/base/ignore_list_dialog.py:47 qt/base/problem_dialog.py:57
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
@@ -259,15 +268,15 @@ msgstr "Filter Empfindlichkeit:"
|
|||||||
|
|
||||||
#: qt/base/preferences_dialog.py:76
|
#: qt/base/preferences_dialog.py:76
|
||||||
msgid "More Results"
|
msgid "More Results"
|
||||||
msgstr "mehr Ergebnisse"
|
msgstr "Mehr Ergebnisse"
|
||||||
|
|
||||||
#: qt/base/preferences_dialog.py:81
|
#: qt/base/preferences_dialog.py:81
|
||||||
msgid "Fewer Results"
|
msgid "Fewer Results"
|
||||||
msgstr "weniger Ergebnisse"
|
msgstr "Weniger Ergebnisse"
|
||||||
|
|
||||||
#: qt/base/preferences_dialog.py:88
|
#: qt/base/preferences_dialog.py:88
|
||||||
msgid "Font size:"
|
msgid "Font size:"
|
||||||
msgstr "Font size:"
|
msgstr "Schriftgröße:"
|
||||||
|
|
||||||
#: qt/base/preferences_dialog.py:92
|
#: qt/base/preferences_dialog.py:92
|
||||||
msgid "Language:"
|
msgid "Language:"
|
||||||
@@ -279,7 +288,7 @@ msgstr "Kopieren und Verschieben:"
|
|||||||
|
|
||||||
#: qt/base/preferences_dialog.py:101 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/preferences_dialog.py:101 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Right in destination"
|
msgid "Right in destination"
|
||||||
msgstr "Direkt im Ziel"
|
msgstr "Direkt ins Ziel"
|
||||||
|
|
||||||
#: qt/base/preferences_dialog.py:102 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/preferences_dialog.py:102 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Recreate relative path"
|
msgid "Recreate relative path"
|
||||||
@@ -291,7 +300,7 @@ msgstr "Absoluten Pfad neu erstellen"
|
|||||||
|
|
||||||
#: qt/base/preferences_dialog.py:106
|
#: qt/base/preferences_dialog.py:106
|
||||||
msgid "Custom Command (arguments: %d for dupe, %r for ref):"
|
msgid "Custom Command (arguments: %d for dupe, %r for ref):"
|
||||||
msgstr "Eigener Befehl (Argumente: %d für Duplikat, %r für Referenz):"
|
msgstr "Eigener Befehl (Variablen: %d für Duplikat, %r für Referenz):"
|
||||||
|
|
||||||
#: qt/base/preferences_dialog.py:184
|
#: qt/base/preferences_dialog.py:184
|
||||||
msgid "dupeGuru has to restart for language changes to take effect."
|
msgid "dupeGuru has to restart for language changes to take effect."
|
||||||
@@ -299,7 +308,7 @@ msgstr "dupeGuru muss neustarten, um die Sprachänderung durchzuführen."
|
|||||||
|
|
||||||
#: qt/base/prioritize_dialog.py:71 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/prioritize_dialog.py:71 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Re-Prioritize duplicates"
|
msgid "Re-Prioritize duplicates"
|
||||||
msgstr "Re-Prioritize duplicates"
|
msgstr "Re-priorisiere Duplikate"
|
||||||
|
|
||||||
#: qt/base/prioritize_dialog.py:75 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/prioritize_dialog.py:75 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid ""
|
msgid ""
|
||||||
@@ -307,9 +316,9 @@ msgid ""
|
|||||||
" the best to these criteria to their respective group's reference position. "
|
" the best to these criteria to their respective group's reference position. "
|
||||||
"Read the help file for more information."
|
"Read the help file for more information."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Add criteria to the right box and click OK to send the dupes that correspond"
|
"Fügen Sie Kriterien zur rechten Box hinzu. Klicken Sie OK, um die Duplikate,"
|
||||||
" the best to these criteria to their respective group's reference position. "
|
" die diesen Kriterien am besten entsprechen, zur Referenzposition der "
|
||||||
"Read the help file for more information."
|
"entsprechenden Gruppe zu senden. Lesen Sie die Hilfe für mehr Informationen."
|
||||||
|
|
||||||
#: qt/base/problem_dialog.py:31 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/problem_dialog.py:31 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Problems!"
|
msgid "Problems!"
|
||||||
@@ -321,8 +330,9 @@ msgid ""
|
|||||||
"these problems are described in the table below. Those files were not "
|
"these problems are described in the table below. Those files were not "
|
||||||
"removed from your results."
|
"removed from your results."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Es gab Probleme bei der Verarbeitung einiger (aller) Dateien. Der Grund der "
|
"Es gab Probleme bei der Verarbeitung einiger (aller) Dateien. Der Ursache "
|
||||||
"Probleme ist unten in der Tabelle beschrieben."
|
"dieser Probleme ist unten genauer beschrieben. Diese Dateien wurden "
|
||||||
|
"\"nicht\" aus Ihren Suchergebnissen entfernt."
|
||||||
|
|
||||||
#: qt/base/problem_dialog.py:52
|
#: qt/base/problem_dialog.py:52
|
||||||
msgid "Reveal Selected"
|
msgid "Reveal Selected"
|
||||||
@@ -346,11 +356,11 @@ msgstr "Nur Duplikate anzeigen"
|
|||||||
|
|
||||||
#: qt/base/result_window.py:47 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/result_window.py:47 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Show Delta Values"
|
msgid "Show Delta Values"
|
||||||
msgstr "Zeige Deltawerte"
|
msgstr "Zeige Delta-Werte"
|
||||||
|
|
||||||
#: qt/base/result_window.py:48
|
#: qt/base/result_window.py:48
|
||||||
msgid "Send Marked to Recycle Bin..."
|
msgid "Send Marked to Recycle Bin..."
|
||||||
msgstr "Verschiebe Markierte in den Mülleimer..."
|
msgstr "Verschiebe Markierte in den Papierkorb..."
|
||||||
|
|
||||||
#: qt/base/result_window.py:49 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/result_window.py:49 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Move Marked to..."
|
msgid "Move Marked to..."
|
||||||
@@ -366,31 +376,31 @@ msgstr "Entferne Markierte aus den Ergebnissen"
|
|||||||
|
|
||||||
#: qt/base/result_window.py:52 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/result_window.py:52 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Re-Prioritize Results..."
|
msgid "Re-Prioritize Results..."
|
||||||
msgstr "Entferne Ausgewählte aus den Ergebnissen..."
|
msgstr "Re-priorisiere Ergebnisse..."
|
||||||
|
|
||||||
#: qt/base/result_window.py:53 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/result_window.py:53 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Remove Selected from Results"
|
msgid "Remove Selected from Results"
|
||||||
msgstr "Entferne Ausgewählte aus den Ergebnissen"
|
msgstr "Entferne Auswahl aus den Ergebnissen"
|
||||||
|
|
||||||
#: qt/base/result_window.py:54 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/result_window.py:54 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Add Selected to Ignore List"
|
msgid "Add Selected to Ignore List"
|
||||||
msgstr "Füge Ausgewählte der Ignorier-Liste hinzu"
|
msgstr "Füge Auswahl der Ausnahmeliste hinzu"
|
||||||
|
|
||||||
#: qt/base/result_window.py:55 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/result_window.py:55 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Make Selected into Reference"
|
msgid "Make Selected into Reference"
|
||||||
msgstr ""
|
msgstr "Mache Auswahl zur Referenz"
|
||||||
|
|
||||||
#: qt/base/result_window.py:56 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/result_window.py:56 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Open Selected with Default Application"
|
msgid "Open Selected with Default Application"
|
||||||
msgstr "Öffne Ausgewählte mit Standardanwendung"
|
msgstr "Öffne Auswahl mit Standard-Anwendung"
|
||||||
|
|
||||||
#: qt/base/result_window.py:57
|
#: qt/base/result_window.py:57
|
||||||
msgid "Open Containing Folder of Selected"
|
msgid "Open Containing Folder of Selected"
|
||||||
msgstr "Öffne beeinhaltenden Ordner der Ausgewählten"
|
msgstr "Öffne den Über-Ordner der Auswahl"
|
||||||
|
|
||||||
#: qt/base/result_window.py:58 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/result_window.py:58 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Rename Selected"
|
msgid "Rename Selected"
|
||||||
msgstr "Ausgewählte umbenennen"
|
msgstr "Auswahl umbenennen"
|
||||||
|
|
||||||
#: qt/base/result_window.py:59 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/result_window.py:59 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Mark All"
|
msgid "Mark All"
|
||||||
@@ -402,19 +412,19 @@ msgstr "Nichts markieren"
|
|||||||
|
|
||||||
#: qt/base/result_window.py:61 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/result_window.py:61 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Invert Marking"
|
msgid "Invert Marking"
|
||||||
msgstr "Markierung invertieren"
|
msgstr "Auswahl umkehren"
|
||||||
|
|
||||||
#: qt/base/result_window.py:62 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/result_window.py:62 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Mark Selected"
|
msgid "Mark Selected"
|
||||||
msgstr "Ausgewählte markieren"
|
msgstr "Auswahl markieren"
|
||||||
|
|
||||||
#: qt/base/result_window.py:63
|
#: qt/base/result_window.py:63
|
||||||
msgid "Export To HTML"
|
msgid "Export To HTML"
|
||||||
msgstr "Exportiere als HTML"
|
msgstr "Exportiere als HTML..."
|
||||||
|
|
||||||
#: qt/base/result_window.py:64
|
#: qt/base/result_window.py:64
|
||||||
msgid "Export To CSV"
|
msgid "Export To CSV"
|
||||||
msgstr ""
|
msgstr "Exportiere als CSV..."
|
||||||
|
|
||||||
#: qt/base/result_window.py:65 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/result_window.py:65 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Save Results..."
|
msgid "Save Results..."
|
||||||
@@ -434,7 +444,7 @@ msgstr "Spalten"
|
|||||||
|
|
||||||
#: qt/base/result_window.py:141
|
#: qt/base/result_window.py:141
|
||||||
msgid "Reset to Defaults"
|
msgid "Reset to Defaults"
|
||||||
msgstr "Voreinstellungen"
|
msgstr "Auf Voreinstellung zurücksetzen"
|
||||||
|
|
||||||
#: qt/base/result_window.py:163
|
#: qt/base/result_window.py:163
|
||||||
msgid "{} Results"
|
msgid "{} Results"
|
||||||
@@ -442,15 +452,15 @@ msgstr "{} (Ergebnisse)"
|
|||||||
|
|
||||||
#: qt/base/result_window.py:171 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/result_window.py:171 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Dupes Only"
|
msgid "Dupes Only"
|
||||||
msgstr "Dupes Only"
|
msgstr "Nur Duplikate anzeigen"
|
||||||
|
|
||||||
#: qt/base/result_window.py:172
|
#: qt/base/result_window.py:172
|
||||||
msgid "Delta Values"
|
msgid "Delta Values"
|
||||||
msgstr ""
|
msgstr "Zeige Delta-Werte"
|
||||||
|
|
||||||
#: qt/base/result_window.py:286 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/base/result_window.py:286 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Select a file to save your results to"
|
msgid "Select a file to save your results to"
|
||||||
msgstr "Datei zum Speichern der Ergebnisliste auswählen."
|
msgstr "Datei zum Speichern der Suchergebnisse auswählen"
|
||||||
|
|
||||||
#: qt/me/preferences_dialog.py:39 qt/se/preferences_dialog.py:39
|
#: qt/me/preferences_dialog.py:39 qt/se/preferences_dialog.py:39
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
@@ -472,7 +482,7 @@ msgstr "Tags"
|
|||||||
#: qt/me/preferences_dialog.py:43 qt/pe/preferences_dialog.py:33
|
#: qt/me/preferences_dialog.py:43 qt/pe/preferences_dialog.py:33
|
||||||
#: qt/se/preferences_dialog.py:40 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/se/preferences_dialog.py:40 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Contents"
|
msgid "Contents"
|
||||||
msgstr "Inhalt"
|
msgstr "Inhalte"
|
||||||
|
|
||||||
#: qt/me/preferences_dialog.py:44
|
#: qt/me/preferences_dialog.py:44
|
||||||
msgid "Audio Contents"
|
msgid "Audio Contents"
|
||||||
@@ -480,11 +490,11 @@ msgstr "Audio Inhalte"
|
|||||||
|
|
||||||
#: qt/me/preferences_dialog.py:55 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/me/preferences_dialog.py:55 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Tags to scan:"
|
msgid "Tags to scan:"
|
||||||
msgstr "folgende Tags scannen:"
|
msgstr "Folgende Tags scannen:"
|
||||||
|
|
||||||
#: qt/me/preferences_dialog.py:61 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/me/preferences_dialog.py:61 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Track"
|
msgid "Track"
|
||||||
msgstr "Stück"
|
msgstr "Track"
|
||||||
|
|
||||||
#: qt/me/preferences_dialog.py:63 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/me/preferences_dialog.py:63 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Artist"
|
msgid "Artist"
|
||||||
@@ -514,7 +524,7 @@ msgstr "Wortgewichtung"
|
|||||||
#: qt/me/preferences_dialog.py:77 qt/se/preferences_dialog.py:51
|
#: qt/me/preferences_dialog.py:77 qt/se/preferences_dialog.py:51
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Match similar words"
|
msgid "Match similar words"
|
||||||
msgstr "Vergleiche ähnliche Wörter"
|
msgstr "Gleiche ähnliche Wörter ab"
|
||||||
|
|
||||||
#: qt/me/preferences_dialog.py:79 qt/pe/preferences_dialog.py:41
|
#: qt/me/preferences_dialog.py:79 qt/pe/preferences_dialog.py:41
|
||||||
#: qt/se/preferences_dialog.py:53 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/se/preferences_dialog.py:53 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
@@ -534,7 +544,7 @@ msgstr "Entferne leere Ordner beim Löschen oder Verschieben"
|
|||||||
#: qt/me/preferences_dialog.py:85 qt/pe/preferences_dialog.py:47
|
#: qt/me/preferences_dialog.py:85 qt/pe/preferences_dialog.py:47
|
||||||
#: qt/se/preferences_dialog.py:76 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/se/preferences_dialog.py:76 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Ignore duplicates hardlinking to the same file"
|
msgid "Ignore duplicates hardlinking to the same file"
|
||||||
msgstr "Ignoriere Duplikate die mit derselben Datei verknüpft sind"
|
msgstr "Ignoriere Duplikate mit Hardlinks auf dieselbe Datei"
|
||||||
|
|
||||||
#: qt/me/preferences_dialog.py:87 qt/pe/preferences_dialog.py:49
|
#: qt/me/preferences_dialog.py:87 qt/pe/preferences_dialog.py:49
|
||||||
#: qt/se/preferences_dialog.py:78 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/se/preferences_dialog.py:78 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
@@ -543,16 +553,16 @@ msgstr "Debug Modus (Neustart nötig)"
|
|||||||
|
|
||||||
#: qt/pe/preferences_dialog.py:34 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/pe/preferences_dialog.py:34 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "EXIF Timestamp"
|
msgid "EXIF Timestamp"
|
||||||
msgstr "EXIF Timestamp"
|
msgstr "EXIF Zeitstempel"
|
||||||
|
|
||||||
#: qt/pe/preferences_dialog.py:39 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/pe/preferences_dialog.py:39 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Match pictures of different dimensions"
|
msgid "Match pictures of different dimensions"
|
||||||
msgstr "Vergleiche Bilder mit unterschiedlicher Auflösung"
|
msgstr "Gleiche Bilder mit unterschiedlicher Auflösung ab"
|
||||||
|
|
||||||
#: qt/pe/result_window.py:19 qt/pe/result_window.py:25
|
#: qt/pe/result_window.py:19 qt/pe/result_window.py:25
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Clear Picture Cache"
|
msgid "Clear Picture Cache"
|
||||||
msgstr "Bildzwischenspeicher leeren"
|
msgstr "Bilder-Cache leeren"
|
||||||
|
|
||||||
#: qt/pe/result_window.py:26 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/pe/result_window.py:26 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Do you really want to remove all your cached picture analysis?"
|
msgid "Do you really want to remove all your cached picture analysis?"
|
||||||
@@ -561,7 +571,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: qt/pe/result_window.py:29
|
#: qt/pe/result_window.py:29
|
||||||
msgid "Picture cache cleared."
|
msgid "Picture cache cleared."
|
||||||
msgstr "Bildzwischenspeicher geleert."
|
msgstr "Bilder-Cache geleert."
|
||||||
|
|
||||||
#: qt/se/preferences_dialog.py:41 cocoa/base/en.lproj/Localizable.strings:0
|
#: qt/se/preferences_dialog.py:41 cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Folders"
|
msgid "Folders"
|
||||||
@@ -577,35 +587,35 @@ msgstr "KB"
|
|||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "%@ Results"
|
msgid "%@ Results"
|
||||||
msgstr ""
|
msgstr "%@ Ergebnisse"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Action"
|
msgid "Action"
|
||||||
msgstr "Action"
|
msgstr "Aktion"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Add Aperture Library"
|
msgid "Add Aperture Library"
|
||||||
msgstr ""
|
msgstr "Füge Aperture-Bibliothek hinzu"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Add iPhoto Library"
|
msgid "Add iPhoto Library"
|
||||||
msgstr "Add iPhoto Library"
|
msgstr "Füge iPhoto-Bibliothek hinzu"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Add iTunes Library"
|
msgid "Add iTunes Library"
|
||||||
msgstr ""
|
msgstr "Füge iTunes-Bibliothek hinzu"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Add New Folder..."
|
msgid "Add New Folder..."
|
||||||
msgstr "Add New Folder..."
|
msgstr "Neuer Ordner..."
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Advanced"
|
msgid "Advanced"
|
||||||
msgstr "Advanced"
|
msgstr "Fortgeschritten"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Audio Content"
|
msgid "Audio Content"
|
||||||
msgstr "Audio Content"
|
msgstr "Audio Inhalt"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Automatically check for updates"
|
msgid "Automatically check for updates"
|
||||||
@@ -613,7 +623,7 @@ msgstr "Automatisch nach Updates suchen"
|
|||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Basic"
|
msgid "Basic"
|
||||||
msgstr "Basic"
|
msgstr "Einfach"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Bring All to Front"
|
msgid "Bring All to Front"
|
||||||
@@ -621,15 +631,15 @@ msgstr "Alle nach vorne bringen"
|
|||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Check for update..."
|
msgid "Check for update..."
|
||||||
msgstr "Check for update..."
|
msgstr "Auf Updates prüfen..."
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Close Window"
|
msgid "Close Window"
|
||||||
msgstr "Fenster Schließen"
|
msgstr "Fenster schließen"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Content"
|
msgid "Content"
|
||||||
msgstr "Content"
|
msgstr "Inhalt"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Copy"
|
msgid "Copy"
|
||||||
@@ -637,7 +647,7 @@ msgstr "Kopieren"
|
|||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Custom command (arguments: %d for dupe, %r for ref):"
|
msgid "Custom command (arguments: %d for dupe, %r for ref):"
|
||||||
msgstr "Custom command (arguments: %d for dupe, %r for ref):"
|
msgstr "Eigener Befehl (Variablen: %d für Duplikat, %r für Referenz):"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Cut"
|
msgid "Cut"
|
||||||
@@ -649,7 +659,7 @@ msgstr "Delta"
|
|||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Details of Selected File"
|
msgid "Details of Selected File"
|
||||||
msgstr "Details of Selected File"
|
msgstr "Details der ausgewählten Datei"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Details Panel"
|
msgid "Details Panel"
|
||||||
@@ -657,27 +667,27 @@ msgstr "Details Panel"
|
|||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Directories"
|
msgid "Directories"
|
||||||
msgstr "Directories"
|
msgstr "Verzeichnisse"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "dupeGuru"
|
msgid "dupeGuru"
|
||||||
msgstr ""
|
msgstr "dupeGuru"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "dupeGuru ME Preferences"
|
msgid "dupeGuru ME Preferences"
|
||||||
msgstr "dupeGuru ME Preferences"
|
msgstr "dupeGuru ME Einstellungen"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "dupeGuru PE Preferences"
|
msgid "dupeGuru PE Preferences"
|
||||||
msgstr "dupeGuru PE Preferences"
|
msgstr "dupeGuru PE Einstellungen"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "dupeGuru Preferences"
|
msgid "dupeGuru Preferences"
|
||||||
msgstr "dupeGuru Preferences"
|
msgstr "dupeGuru Einstellungen"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "dupeGuru Results"
|
msgid "dupeGuru Results"
|
||||||
msgstr "dupeGuru Results"
|
msgstr "dupeGuru Ergebnisse"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "dupeGuru Website"
|
msgid "dupeGuru Website"
|
||||||
@@ -689,15 +699,15 @@ msgstr "Bearbeiten"
|
|||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Export Results to CSV"
|
msgid "Export Results to CSV"
|
||||||
msgstr ""
|
msgstr "Exportiere als CSV..."
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Export Results to XHTML"
|
msgid "Export Results to XHTML"
|
||||||
msgstr "Export Results to XHTML"
|
msgstr "Exportiere als XHTML..."
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Fewer results"
|
msgid "Fewer results"
|
||||||
msgstr "Fewer results"
|
msgstr "Weniger Suchergebnisse"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Filter"
|
msgid "Filter"
|
||||||
@@ -705,19 +715,19 @@ msgstr "Filter"
|
|||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Filter hardness:"
|
msgid "Filter hardness:"
|
||||||
msgstr "Filter hardness:"
|
msgstr "Filter Empfindlichkeit:"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Filter Results..."
|
msgid "Filter Results..."
|
||||||
msgstr ""
|
msgstr "Filter Suchergebnisse..."
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Folder Selection Window"
|
msgid "Folder Selection Window"
|
||||||
msgstr "Folder Selection Window"
|
msgstr "Ordner-Auswahlfenster"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Font Size:"
|
msgid "Font Size:"
|
||||||
msgstr ""
|
msgstr "Schriftgröße:"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Hide dupeGuru"
|
msgid "Hide dupeGuru"
|
||||||
@@ -733,19 +743,19 @@ msgstr "Ignoriere Dateien kleiner als:"
|
|||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Load from file..."
|
msgid "Load from file..."
|
||||||
msgstr "Load from file..."
|
msgstr "Lade von Datei..."
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Minimize"
|
msgid "Minimize"
|
||||||
msgstr "Im Dock ablegen"
|
msgstr "Minimieren"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Mode"
|
msgid "Mode"
|
||||||
msgstr "Mode"
|
msgstr "Modus"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "More results"
|
msgid "More results"
|
||||||
msgstr "More results"
|
msgstr "Mehr Suchergebnisse"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Ok"
|
msgid "Ok"
|
||||||
@@ -753,19 +763,19 @@ msgstr "Ok"
|
|||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Options"
|
msgid "Options"
|
||||||
msgstr "Options"
|
msgstr "Optionen"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Paste"
|
msgid "Paste"
|
||||||
msgstr "Einsetzen"
|
msgstr "Einfügen"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Preferences..."
|
msgid "Preferences..."
|
||||||
msgstr "Preferences..."
|
msgstr "Einstellungen..."
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Quick Look"
|
msgid "Quick Look"
|
||||||
msgstr ""
|
msgstr "Quick Look"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Quit dupeGuru"
|
msgid "Quit dupeGuru"
|
||||||
@@ -773,35 +783,35 @@ msgstr "dupeGuru beenden"
|
|||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Remove Dead Tracks in iTunes"
|
msgid "Remove Dead Tracks in iTunes"
|
||||||
msgstr "Remove Dead Tracks in iTunes"
|
msgstr "Entferne tote Tracks in iTunes"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Reset to Default"
|
msgid "Reset to Default"
|
||||||
msgstr "Reset to Default"
|
msgstr "Auf Voreinstellung zurücksetzen"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Reset To Defaults"
|
msgid "Reset To Defaults"
|
||||||
msgstr ""
|
msgstr "Auf Voreinstellungen zurücksetzen"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Reveal"
|
msgid "Reveal"
|
||||||
msgstr ""
|
msgstr "Zeige"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Reveal Selected in Finder"
|
msgid "Reveal Selected in Finder"
|
||||||
msgstr "Reveal Selected in Finder"
|
msgstr "Zeige Auswahl im Finder"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Select All"
|
msgid "Select All"
|
||||||
msgstr ""
|
msgstr "Alles markieren"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Send Marked to Trash..."
|
msgid "Send Marked to Trash..."
|
||||||
msgstr ""
|
msgstr "Verschiebe Markierte in den Papierkorb..."
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Services"
|
msgid "Services"
|
||||||
msgstr ""
|
msgstr "Services"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Show All"
|
msgid "Show All"
|
||||||
@@ -809,11 +819,11 @@ msgstr "Alle einblenden"
|
|||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Start Duplicate Scan"
|
msgid "Start Duplicate Scan"
|
||||||
msgstr "Start Duplicate Scan"
|
msgstr "Starte Duplikat-Scan"
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "The name '%@' already exists."
|
msgid "The name '%@' already exists."
|
||||||
msgstr "The name '%@' already exists."
|
msgstr "Der Name '%@' existiert bereits."
|
||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Window"
|
msgid "Window"
|
||||||
@@ -821,4 +831,4 @@ msgstr "Fenster"
|
|||||||
|
|
||||||
#: cocoa/base/en.lproj/Localizable.strings:0
|
#: cocoa/base/en.lproj/Localizable.strings:0
|
||||||
msgid "Zoom"
|
msgid "Zoom"
|
||||||
msgstr "Zoomen"
|
msgstr "Zoom"
|
||||||
|
|||||||
35
package.py
@@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2009-12-30
|
# Created On: 2009-12-30
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -16,9 +16,11 @@ import platform
|
|||||||
import glob
|
import glob
|
||||||
|
|
||||||
from hscommon.plat import ISWINDOWS, ISLINUX
|
from hscommon.plat import ISWINDOWS, ISLINUX
|
||||||
from hscommon.build import (add_to_pythonpath, print_and_do, copy_packages, build_debian_changelog,
|
from hscommon.build import (
|
||||||
|
add_to_pythonpath, print_and_do, copy_packages, build_debian_changelog,
|
||||||
copy_qt_plugins, get_module_version, filereplace, copy, setup_package_argparser,
|
copy_qt_plugins, get_module_version, filereplace, copy, setup_package_argparser,
|
||||||
package_cocoa_app_in_dmg, copy_all, find_in_path)
|
package_cocoa_app_in_dmg, copy_all, find_in_path
|
||||||
|
)
|
||||||
|
|
||||||
def parse_args():
|
def parse_args():
|
||||||
parser = ArgumentParser()
|
parser = ArgumentParser()
|
||||||
@@ -42,17 +44,18 @@ def package_windows(edition, dev):
|
|||||||
add_to_pythonpath('.')
|
add_to_pythonpath('.')
|
||||||
app_version = get_module_version('core_{}'.format(edition))
|
app_version = get_module_version('core_{}'.format(edition))
|
||||||
distdir = 'dist'
|
distdir = 'dist'
|
||||||
|
|
||||||
if op.exists(distdir):
|
if op.exists(distdir):
|
||||||
shutil.rmtree(distdir)
|
shutil.rmtree(distdir)
|
||||||
|
|
||||||
if not dev:
|
if not dev:
|
||||||
# Copy qt plugins
|
# Copy qt plugins
|
||||||
plugin_dest = distdir
|
plugin_dest = distdir
|
||||||
plugin_names = ['accessible', 'codecs', 'iconengines', 'imageformats']
|
plugin_names = ['accessible', 'codecs', 'iconengines', 'imageformats']
|
||||||
copy_qt_plugins(plugin_names, plugin_dest)
|
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 = {
|
options = {
|
||||||
'build_exe': {
|
'build_exe': {
|
||||||
'includes': 'atexit',
|
'includes': 'atexit',
|
||||||
@@ -129,7 +132,7 @@ def package_debian_distribution(edition, distribution):
|
|||||||
ed = lambda s: s.format(edition)
|
ed = lambda s: s.format(edition)
|
||||||
destpath = op.join('build', 'dupeguru-{0}-{1}'.format(edition, version))
|
destpath = op.join('build', 'dupeguru-{0}-{1}'.format(edition, version))
|
||||||
srcpath = op.join(destpath, 'src')
|
srcpath = op.join(destpath, 'src')
|
||||||
packages = ['hscommon', 'core', ed('core_{0}'), 'qtlib', 'qt', 'send2trash', 'jobprogress']
|
packages = ['hscommon', 'core', ed('core_{0}'), 'qtlib', 'qt', 'send2trash']
|
||||||
if edition == 'me':
|
if edition == 'me':
|
||||||
packages.append('hsaudiotag')
|
packages.append('hsaudiotag')
|
||||||
copy_files_to_package(srcpath, packages, with_so=False)
|
copy_files_to_package(srcpath, packages, with_so=False)
|
||||||
@@ -151,8 +154,10 @@ def package_debian_distribution(edition, distribution):
|
|||||||
changelog_dest = op.join(debdest, 'changelog')
|
changelog_dest = op.join(debdest, 'changelog')
|
||||||
project_name = debopts['pkgname']
|
project_name = debopts['pkgname']
|
||||||
from_version = {'se': '2.9.2', 'me': '5.7.2', 'pe': '1.8.5'}[edition]
|
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,
|
build_debian_changelog(
|
||||||
distribution=distribution)
|
changelogpath, changelog_dest, project_name, from_version=from_version,
|
||||||
|
distribution=distribution
|
||||||
|
)
|
||||||
shutil.copy(op.join('images', ed('dg{0}_logo_128.png')), srcpath)
|
shutil.copy(op.join('images', ed('dg{0}_logo_128.png')), srcpath)
|
||||||
os.chdir(destpath)
|
os.chdir(destpath)
|
||||||
cmd = "dpkg-buildpackage -S"
|
cmd = "dpkg-buildpackage -S"
|
||||||
@@ -171,7 +176,7 @@ def package_arch(edition):
|
|||||||
print("Packaging for Arch")
|
print("Packaging for Arch")
|
||||||
ed = lambda s: s.format(edition)
|
ed = lambda s: s.format(edition)
|
||||||
srcpath = op.join('build', ed('dupeguru-{}-arch'))
|
srcpath = op.join('build', ed('dupeguru-{}-arch'))
|
||||||
packages = ['hscommon', 'core', ed('core_{0}'), 'qtlib', 'qt', 'send2trash', 'jobprogress']
|
packages = ['hscommon', 'core', ed('core_{0}'), 'qtlib', 'qt', 'send2trash']
|
||||||
if edition == 'me':
|
if edition == 'me':
|
||||||
packages.append('hsaudiotag')
|
packages.append('hsaudiotag')
|
||||||
copy_files_to_package(srcpath, packages, with_so=True)
|
copy_files_to_package(srcpath, packages, with_so=True)
|
||||||
@@ -209,7 +214,10 @@ def main():
|
|||||||
if ISWINDOWS:
|
if ISWINDOWS:
|
||||||
package_windows(edition, dev)
|
package_windows(edition, dev)
|
||||||
elif ISLINUX:
|
elif ISLINUX:
|
||||||
distname, _, _ = platform.dist()
|
if not args.arch_pkg:
|
||||||
|
distname, _, _ = platform.dist()
|
||||||
|
else:
|
||||||
|
distname = 'arch'
|
||||||
if distname == 'arch':
|
if distname == 'arch':
|
||||||
package_arch(edition)
|
package_arch(edition)
|
||||||
else:
|
else:
|
||||||
@@ -219,3 +227,4 @@ def main():
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ Source: {pkgname}
|
|||||||
Section: devel
|
Section: devel
|
||||||
Priority: extra
|
Priority: extra
|
||||||
Maintainer: Virgil Dupras <hsoft@hardcoded.net>
|
Maintainer: Virgil Dupras <hsoft@hardcoded.net>
|
||||||
Build-Depends: debhelper (>= 7), python3-dev
|
Build-Depends: debhelper (>= 7), python3-dev, python3-setuptools
|
||||||
Standards-Version: 3.8.1
|
Standards-Version: 3.8.1
|
||||||
Homepage: http://www.hardcoded.net
|
Homepage: http://www.hardcoded.net
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2009-04-25
|
# Created On: 2009-04-25
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
@@ -14,13 +14,12 @@ from PyQt5.QtGui import QDesktopServices
|
|||||||
from PyQt5.QtWidgets import QApplication, QFileDialog, QDialog, QMessageBox
|
from PyQt5.QtWidgets import QApplication, QFileDialog, QDialog, QMessageBox
|
||||||
|
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
from hscommon.plat import ISLINUX
|
|
||||||
from hscommon import desktop
|
from hscommon import desktop
|
||||||
|
|
||||||
from qtlib.about_box import AboutBox
|
from qtlib.about_box import AboutBox
|
||||||
from qtlib.recent import Recent
|
from qtlib.recent import Recent
|
||||||
from qtlib.util import createActions
|
from qtlib.util import createActions
|
||||||
from qtlib.progress_window import ProgressWindow
|
from qtlib.progress_window import ProgressWindow
|
||||||
|
|
||||||
from . import platform
|
from . import platform
|
||||||
from .result_window import ResultWindow
|
from .result_window import ResultWindow
|
||||||
@@ -35,13 +34,13 @@ class DupeGuru(QObject):
|
|||||||
MODELCLASS = None
|
MODELCLASS = None
|
||||||
LOGO_NAME = '<replace this>'
|
LOGO_NAME = '<replace this>'
|
||||||
NAME = '<replace this>'
|
NAME = '<replace this>'
|
||||||
|
|
||||||
DETAILS_DIALOG_CLASS = None
|
DETAILS_DIALOG_CLASS = None
|
||||||
RESULT_WINDOW_CLASS = ResultWindow
|
RESULT_WINDOW_CLASS = ResultWindow
|
||||||
RESULT_MODEL_CLASS = None
|
RESULT_MODEL_CLASS = None
|
||||||
PREFERENCES_CLASS = None
|
PREFERENCES_CLASS = None
|
||||||
PREFERENCES_DIALOG_CLASS = None
|
PREFERENCES_DIALOG_CLASS = None
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.prefs = self.PREFERENCES_CLASS()
|
self.prefs = self.PREFERENCES_CLASS()
|
||||||
@@ -49,7 +48,7 @@ class DupeGuru(QObject):
|
|||||||
self.model = self.MODELCLASS(view=self)
|
self.model = self.MODELCLASS(view=self)
|
||||||
self._setup()
|
self._setup()
|
||||||
self.prefsChanged.emit(self.prefs)
|
self.prefsChanged.emit(self.prefs)
|
||||||
|
|
||||||
#--- Private
|
#--- Private
|
||||||
def _setup(self):
|
def _setup(self):
|
||||||
self._setupActions()
|
self._setupActions()
|
||||||
@@ -58,24 +57,24 @@ class DupeGuru(QObject):
|
|||||||
self.recentResults.mustOpenItem.connect(self.model.load_from)
|
self.recentResults.mustOpenItem.connect(self.model.load_from)
|
||||||
self.directories_dialog = DirectoriesDialog(self)
|
self.directories_dialog = DirectoriesDialog(self)
|
||||||
self.resultWindow = self.RESULT_WINDOW_CLASS(self.directories_dialog, self)
|
self.resultWindow = self.RESULT_WINDOW_CLASS(self.directories_dialog, self)
|
||||||
self.progress_window = ProgressWindow(self.resultWindow, self.model.progress_window)
|
self.progress_window = ProgressWindow(self.resultWindow, self.model.progress_window)
|
||||||
self.details_dialog = self.DETAILS_DIALOG_CLASS(self.resultWindow, self)
|
self.details_dialog = self.DETAILS_DIALOG_CLASS(self.resultWindow, self)
|
||||||
self.problemDialog = ProblemDialog(parent=self.resultWindow, model=self.model.problem_dialog)
|
self.problemDialog = ProblemDialog(parent=self.resultWindow, model=self.model.problem_dialog)
|
||||||
self.ignoreListDialog = IgnoreListDialog(parent=self.resultWindow, model=self.model.ignore_list_dialog)
|
self.ignoreListDialog = IgnoreListDialog(parent=self.resultWindow, model=self.model.ignore_list_dialog)
|
||||||
self.deletionOptions = DeletionOptions(parent=self.resultWindow, model=self.model.deletion_options)
|
self.deletionOptions = DeletionOptions(parent=self.resultWindow, model=self.model.deletion_options)
|
||||||
self.preferences_dialog = self.PREFERENCES_DIALOG_CLASS(self.resultWindow, self)
|
self.preferences_dialog = self.PREFERENCES_DIALOG_CLASS(self.resultWindow, self)
|
||||||
self.about_box = AboutBox(self.resultWindow, self)
|
self.about_box = AboutBox(self.resultWindow, self)
|
||||||
|
|
||||||
self.directories_dialog.show()
|
self.directories_dialog.show()
|
||||||
self.model.load()
|
self.model.load()
|
||||||
|
|
||||||
# The timer scheme is because if the nag is not shown before the application is
|
# The timer scheme is because if the nag is not shown before the application is
|
||||||
# completely initialized, the nag will be shown before the app shows up in the task bar
|
# completely initialized, the nag will be shown before the app shows up in the task bar
|
||||||
# In some circumstances, the nag is hidden by other window, which may make the user think
|
# In some circumstances, the nag is hidden by other window, which may make the user think
|
||||||
# that the application haven't launched.
|
# that the application haven't launched.
|
||||||
QTimer.singleShot(0, self.finishedLaunching)
|
QTimer.singleShot(0, self.finishedLaunching)
|
||||||
QCoreApplication.instance().aboutToQuit.connect(self.application_will_terminate)
|
QCoreApplication.instance().aboutToQuit.connect(self.application_will_terminate)
|
||||||
|
|
||||||
def _setupActions(self):
|
def _setupActions(self):
|
||||||
# Setup actions that are common to both the directory dialog and the results window.
|
# Setup actions that are common to both the directory dialog and the results window.
|
||||||
# (name, shortcut, icon, desc, func)
|
# (name, shortcut, icon, desc, func)
|
||||||
@@ -88,61 +87,61 @@ class DupeGuru(QObject):
|
|||||||
('actionOpenDebugLog', '', '', tr("Open Debug Log"), self.openDebugLogTriggered),
|
('actionOpenDebugLog', '', '', tr("Open Debug Log"), self.openDebugLogTriggered),
|
||||||
]
|
]
|
||||||
createActions(ACTIONS, self)
|
createActions(ACTIONS, self)
|
||||||
|
|
||||||
def _update_options(self):
|
def _update_options(self):
|
||||||
self.model.scanner.mix_file_kind = self.prefs.mix_file_kind
|
self.model.scanner.mix_file_kind = self.prefs.mix_file_kind
|
||||||
self.model.options['escape_filter_regexp'] = self.prefs.use_regexp
|
self.model.options['escape_filter_regexp'] = self.prefs.use_regexp
|
||||||
self.model.options['clean_empty_dirs'] = self.prefs.remove_empty_folders
|
self.model.options['clean_empty_dirs'] = self.prefs.remove_empty_folders
|
||||||
self.model.options['ignore_hardlink_matches'] = self.prefs.ignore_hardlink_matches
|
self.model.options['ignore_hardlink_matches'] = self.prefs.ignore_hardlink_matches
|
||||||
self.model.options['copymove_dest_type'] = self.prefs.destination_type
|
self.model.options['copymove_dest_type'] = self.prefs.destination_type
|
||||||
|
|
||||||
#--- Public
|
#--- Public
|
||||||
def add_selected_to_ignore_list(self):
|
def add_selected_to_ignore_list(self):
|
||||||
self.model.add_selected_to_ignore_list()
|
self.model.add_selected_to_ignore_list()
|
||||||
|
|
||||||
def remove_selected(self):
|
def remove_selected(self):
|
||||||
self.model.remove_selected(self)
|
self.model.remove_selected(self)
|
||||||
|
|
||||||
def confirm(self, title, msg, default_button=QMessageBox.Yes):
|
def confirm(self, title, msg, default_button=QMessageBox.Yes):
|
||||||
active = QApplication.activeWindow()
|
active = QApplication.activeWindow()
|
||||||
buttons = QMessageBox.Yes | QMessageBox.No
|
buttons = QMessageBox.Yes | QMessageBox.No
|
||||||
answer = QMessageBox.question(active, title, msg, buttons, default_button)
|
answer = QMessageBox.question(active, title, msg, buttons, default_button)
|
||||||
return answer == QMessageBox.Yes
|
return answer == QMessageBox.Yes
|
||||||
|
|
||||||
def invokeCustomCommand(self):
|
def invokeCustomCommand(self):
|
||||||
self.model.invoke_custom_command()
|
self.model.invoke_custom_command()
|
||||||
|
|
||||||
def show_details(self):
|
def show_details(self):
|
||||||
self.details_dialog.show()
|
self.details_dialog.show()
|
||||||
|
|
||||||
def showResultsWindow(self):
|
def showResultsWindow(self):
|
||||||
self.resultWindow.show()
|
self.resultWindow.show()
|
||||||
|
|
||||||
#--- Signals
|
#--- Signals
|
||||||
willSavePrefs = pyqtSignal()
|
willSavePrefs = pyqtSignal()
|
||||||
prefsChanged = pyqtSignal(object)
|
prefsChanged = pyqtSignal(object)
|
||||||
|
|
||||||
#--- Events
|
#--- Events
|
||||||
def finishedLaunching(self):
|
def finishedLaunching(self):
|
||||||
if sys.getfilesystemencoding() == 'ascii':
|
if sys.getfilesystemencoding() == 'ascii':
|
||||||
# No need to localize this, it's a debugging message.
|
# No need to localize this, it's a debugging message.
|
||||||
msg = "Something is wrong with the way your system locale is set. If the files you're "\
|
msg = "Something is wrong with the way your system locale is set. If the files you're "\
|
||||||
"scanning have accented letters, you'll probably get a crash. It is advised that "\
|
"scanning have accented letters, you'll probably get a crash. It is advised that "\
|
||||||
"you set your system locale properly."
|
"you set your system locale properly."
|
||||||
QMessageBox.warning(self.directories_dialog, "Wrong Locale", msg)
|
QMessageBox.warning(self.directories_dialog, "Wrong Locale", msg)
|
||||||
|
|
||||||
def application_will_terminate(self):
|
def application_will_terminate(self):
|
||||||
self.willSavePrefs.emit()
|
self.willSavePrefs.emit()
|
||||||
self.prefs.save()
|
self.prefs.save()
|
||||||
self.model.save()
|
self.model.save()
|
||||||
|
|
||||||
def ignoreListTriggered(self):
|
def ignoreListTriggered(self):
|
||||||
self.model.ignore_list_dialog.show()
|
self.model.ignore_list_dialog.show()
|
||||||
|
|
||||||
def openDebugLogTriggered(self):
|
def openDebugLogTriggered(self):
|
||||||
debugLogPath = op.join(self.model.appdata, 'debug.log')
|
debugLogPath = op.join(self.model.appdata, 'debug.log')
|
||||||
desktop.open_path(debugLogPath)
|
desktop.open_path(debugLogPath)
|
||||||
|
|
||||||
def preferencesTriggered(self):
|
def preferencesTriggered(self):
|
||||||
self.preferences_dialog.load()
|
self.preferences_dialog.load()
|
||||||
result = self.preferences_dialog.exec()
|
result = self.preferences_dialog.exec()
|
||||||
@@ -151,42 +150,42 @@ class DupeGuru(QObject):
|
|||||||
self.prefs.save()
|
self.prefs.save()
|
||||||
self._update_options()
|
self._update_options()
|
||||||
self.prefsChanged.emit(self.prefs)
|
self.prefsChanged.emit(self.prefs)
|
||||||
|
|
||||||
def quitTriggered(self):
|
def quitTriggered(self):
|
||||||
self.directories_dialog.close()
|
self.directories_dialog.close()
|
||||||
|
|
||||||
def showAboutBoxTriggered(self):
|
def showAboutBoxTriggered(self):
|
||||||
self.about_box.show()
|
self.about_box.show()
|
||||||
|
|
||||||
def showHelpTriggered(self):
|
def showHelpTriggered(self):
|
||||||
base_path = platform.HELP_PATH
|
base_path = platform.HELP_PATH
|
||||||
url = QUrl.fromLocalFile(op.abspath(op.join(base_path, 'index.html')))
|
url = QUrl.fromLocalFile(op.abspath(op.join(base_path, 'index.html')))
|
||||||
QDesktopServices.openUrl(url)
|
QDesktopServices.openUrl(url)
|
||||||
|
|
||||||
#--- model --> view
|
#--- model --> view
|
||||||
def get_default(self, key):
|
def get_default(self, key):
|
||||||
return self.prefs.get_value(key)
|
return self.prefs.get_value(key)
|
||||||
|
|
||||||
def set_default(self, key, value):
|
def set_default(self, key, value):
|
||||||
self.prefs.set_value(key, value)
|
self.prefs.set_value(key, value)
|
||||||
|
|
||||||
def show_message(self, msg):
|
def show_message(self, msg):
|
||||||
window = QApplication.activeWindow()
|
window = QApplication.activeWindow()
|
||||||
QMessageBox.information(window, '', msg)
|
QMessageBox.information(window, '', msg)
|
||||||
|
|
||||||
def ask_yes_no(self, prompt):
|
def ask_yes_no(self, prompt):
|
||||||
return self.confirm('', prompt)
|
return self.confirm('', prompt)
|
||||||
|
|
||||||
def show_results_window(self):
|
def show_results_window(self):
|
||||||
self.showResultsWindow()
|
self.showResultsWindow()
|
||||||
|
|
||||||
def show_problem_dialog(self):
|
def show_problem_dialog(self):
|
||||||
self.problemDialog.show()
|
self.problemDialog.show()
|
||||||
|
|
||||||
def select_dest_folder(self, prompt):
|
def select_dest_folder(self, prompt):
|
||||||
flags = QFileDialog.ShowDirsOnly
|
flags = QFileDialog.ShowDirsOnly
|
||||||
return QFileDialog.getExistingDirectory(self.resultWindow, prompt, '', flags)
|
return QFileDialog.getExistingDirectory(self.resultWindow, prompt, '', flags)
|
||||||
|
|
||||||
def select_dest_file(self, prompt, extension):
|
def select_dest_file(self, prompt, extension):
|
||||||
files = tr("{} file (*.{})").format(extension.upper(), extension)
|
files = tr("{} file (*.{})").format(extension.upper(), extension)
|
||||||
destination, chosen_filter = QFileDialog.getSaveFileName(self.resultWindow, prompt, '', files)
|
destination, chosen_filter = QFileDialog.getSaveFileName(self.resultWindow, prompt, '', files)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# cxfreeze has some problems detecting all dependencies.
|
# cxfreeze has some problems detecting all dependencies.
|
||||||
# This modules explicitly import those problematic modules.
|
# This modules explicitly import those problematic modules.
|
||||||
|
# flake8: noqa
|
||||||
|
|
||||||
import xml.etree.ElementPath
|
import xml.etree.ElementPath
|
||||||
import gzip
|
import gzip
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2012-05-30
|
# Created On: 2012-05-30
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
@@ -21,11 +21,11 @@ class DeletionOptions(QDialog):
|
|||||||
self.model = model
|
self.model = model
|
||||||
self._setupUi()
|
self._setupUi()
|
||||||
self.model.view = self
|
self.model.view = self
|
||||||
|
|
||||||
self.linkCheckbox.stateChanged.connect(self.linkCheckboxChanged)
|
self.linkCheckbox.stateChanged.connect(self.linkCheckboxChanged)
|
||||||
self.buttonBox.accepted.connect(self.accept)
|
self.buttonBox.accepted.connect(self.accept)
|
||||||
self.buttonBox.rejected.connect(self.reject)
|
self.buttonBox.rejected.connect(self.reject)
|
||||||
|
|
||||||
def _setupUi(self):
|
def _setupUi(self):
|
||||||
self.setWindowTitle(tr("Deletion Options"))
|
self.setWindowTitle(tr("Deletion Options"))
|
||||||
self.resize(400, 270)
|
self.resize(400, 270)
|
||||||
@@ -34,8 +34,10 @@ class DeletionOptions(QDialog):
|
|||||||
self.verticalLayout.addWidget(self.msgLabel)
|
self.verticalLayout.addWidget(self.msgLabel)
|
||||||
self.linkCheckbox = QCheckBox(tr("Link deleted files"))
|
self.linkCheckbox = QCheckBox(tr("Link deleted files"))
|
||||||
self.verticalLayout.addWidget(self.linkCheckbox)
|
self.verticalLayout.addWidget(self.linkCheckbox)
|
||||||
text = tr("After having deleted a duplicate, place a link targeting the reference file "
|
text = tr(
|
||||||
"to replace the deleted file.")
|
"After having deleted a duplicate, place a link targeting the reference file "
|
||||||
|
"to replace the deleted file."
|
||||||
|
)
|
||||||
self.linkMessageLabel = QLabel(text)
|
self.linkMessageLabel = QLabel(text)
|
||||||
self.linkMessageLabel.setWordWrap(True)
|
self.linkMessageLabel.setWordWrap(True)
|
||||||
self.verticalLayout.addWidget(self.linkMessageLabel)
|
self.verticalLayout.addWidget(self.linkMessageLabel)
|
||||||
@@ -46,8 +48,10 @@ class DeletionOptions(QDialog):
|
|||||||
self.linkCheckbox.setText(self.linkCheckbox.text() + tr(" (unsupported)"))
|
self.linkCheckbox.setText(self.linkCheckbox.text() + tr(" (unsupported)"))
|
||||||
self.directCheckbox = QCheckBox(tr("Directly delete files"))
|
self.directCheckbox = QCheckBox(tr("Directly delete files"))
|
||||||
self.verticalLayout.addWidget(self.directCheckbox)
|
self.verticalLayout.addWidget(self.directCheckbox)
|
||||||
text = tr("Instead of sending files to trash, delete them directly. This option is usually "
|
text = tr(
|
||||||
"used as a workaround when the normal deletion method doesn't work.")
|
"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 = QLabel(text)
|
||||||
self.directMessageLabel.setWordWrap(True)
|
self.directMessageLabel.setWordWrap(True)
|
||||||
self.verticalLayout.addWidget(self.directMessageLabel)
|
self.verticalLayout.addWidget(self.directMessageLabel)
|
||||||
@@ -55,15 +59,15 @@ class DeletionOptions(QDialog):
|
|||||||
self.buttonBox.addButton(tr("Proceed"), QDialogButtonBox.AcceptRole)
|
self.buttonBox.addButton(tr("Proceed"), QDialogButtonBox.AcceptRole)
|
||||||
self.buttonBox.addButton(tr("Cancel"), QDialogButtonBox.RejectRole)
|
self.buttonBox.addButton(tr("Cancel"), QDialogButtonBox.RejectRole)
|
||||||
self.verticalLayout.addWidget(self.buttonBox)
|
self.verticalLayout.addWidget(self.buttonBox)
|
||||||
|
|
||||||
#--- Signals
|
#--- Signals
|
||||||
def linkCheckboxChanged(self, changed: int):
|
def linkCheckboxChanged(self, changed: int):
|
||||||
self.model.link_deleted = bool(changed)
|
self.model.link_deleted = bool(changed)
|
||||||
|
|
||||||
#--- model --> view
|
#--- model --> view
|
||||||
def update_msg(self, msg: str):
|
def update_msg(self, msg: str):
|
||||||
self.msgLabel.setText(msg)
|
self.msgLabel.setText(msg)
|
||||||
|
|
||||||
def show(self):
|
def show(self):
|
||||||
self.linkCheckbox.setChecked(self.model.link_deleted)
|
self.linkCheckbox.setChecked(self.model.link_deleted)
|
||||||
self.linkTypeRadio.selected_index = 1 if self.model.use_hardlinks else 0
|
self.linkTypeRadio.selected_index = 1 if self.model.use_hardlinks else 0
|
||||||
@@ -73,7 +77,7 @@ class DeletionOptions(QDialog):
|
|||||||
self.model.use_hardlinks = self.linkTypeRadio.selected_index == 1
|
self.model.use_hardlinks = self.linkTypeRadio.selected_index == 1
|
||||||
self.model.direct = self.directCheckbox.isChecked()
|
self.model.direct = self.directCheckbox.isChecked()
|
||||||
return result == QDialog.Accepted
|
return result == QDialog.Accepted
|
||||||
|
|
||||||
def set_hardlink_option_enabled(self, is_enabled: bool):
|
def set_hardlink_option_enabled(self, is_enabled: bool):
|
||||||
self.linkTypeRadio.setEnabled(is_enabled)
|
self.linkTypeRadio.setEnabled(is_enabled)
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2009-04-25
|
# Created On: 2009-04-25
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from PyQt5.QtCore import QRect
|
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,
|
QAbstractItemView, QSpacerItem, QSizePolicy, QPushButton, QMainWindow, QMenuBar, QMenu, QLabel,
|
||||||
QApplication)
|
QApplication
|
||||||
|
)
|
||||||
from PyQt5.QtGui import QPixmap, QIcon
|
from PyQt5.QtGui import QPixmap, QIcon
|
||||||
|
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
@@ -39,7 +41,7 @@ class DirectoriesDialog(QMainWindow):
|
|||||||
self._updateRemoveButton()
|
self._updateRemoveButton()
|
||||||
self._updateLoadResultsButton()
|
self._updateLoadResultsButton()
|
||||||
self._setupBindings()
|
self._setupBindings()
|
||||||
|
|
||||||
def _setupBindings(self):
|
def _setupBindings(self):
|
||||||
self.scanButton.clicked.connect(self.scanButtonClicked)
|
self.scanButton.clicked.connect(self.scanButtonClicked)
|
||||||
self.loadResultsButton.clicked.connect(self.actionLoadResults.trigger)
|
self.loadResultsButton.clicked.connect(self.actionLoadResults.trigger)
|
||||||
@@ -51,7 +53,7 @@ class DirectoriesDialog(QMainWindow):
|
|||||||
self.recentFolders.mustOpenItem.connect(self.app.model.add_directory)
|
self.recentFolders.mustOpenItem.connect(self.app.model.add_directory)
|
||||||
self.directoriesModel.foldersAdded.connect(self.directoriesModelAddedFolders)
|
self.directoriesModel.foldersAdded.connect(self.directoriesModelAddedFolders)
|
||||||
self.app.willSavePrefs.connect(self.appWillSavePrefs)
|
self.app.willSavePrefs.connect(self.appWillSavePrefs)
|
||||||
|
|
||||||
def _setupActions(self):
|
def _setupActions(self):
|
||||||
# (name, shortcut, icon, desc, func)
|
# (name, shortcut, icon, desc, func)
|
||||||
ACTIONS = [
|
ACTIONS = [
|
||||||
@@ -60,7 +62,7 @@ class DirectoriesDialog(QMainWindow):
|
|||||||
('actionAddFolder', '', '', tr("Add Folder..."), self.addFolderTriggered),
|
('actionAddFolder', '', '', tr("Add Folder..."), self.addFolderTriggered),
|
||||||
]
|
]
|
||||||
createActions(ACTIONS, self)
|
createActions(ACTIONS, self)
|
||||||
|
|
||||||
def _setupMenu(self):
|
def _setupMenu(self):
|
||||||
self.menubar = QMenuBar(self)
|
self.menubar = QMenuBar(self)
|
||||||
self.menubar.setGeometry(QRect(0, 0, 42, 22))
|
self.menubar.setGeometry(QRect(0, 0, 42, 22))
|
||||||
@@ -73,7 +75,7 @@ class DirectoriesDialog(QMainWindow):
|
|||||||
self.menuLoadRecent = QMenu(self.menuFile)
|
self.menuLoadRecent = QMenu(self.menuFile)
|
||||||
self.menuLoadRecent.setTitle(tr("Load Recent Results"))
|
self.menuLoadRecent.setTitle(tr("Load Recent Results"))
|
||||||
self.setMenuBar(self.menubar)
|
self.setMenuBar(self.menubar)
|
||||||
|
|
||||||
self.menuFile.addAction(self.actionLoadResults)
|
self.menuFile.addAction(self.actionLoadResults)
|
||||||
self.menuFile.addAction(self.menuLoadRecent.menuAction())
|
self.menuFile.addAction(self.menuLoadRecent.menuAction())
|
||||||
self.menuFile.addSeparator()
|
self.menuFile.addSeparator()
|
||||||
@@ -84,21 +86,21 @@ class DirectoriesDialog(QMainWindow):
|
|||||||
self.menuHelp.addAction(self.app.actionShowHelp)
|
self.menuHelp.addAction(self.app.actionShowHelp)
|
||||||
self.menuHelp.addAction(self.app.actionOpenDebugLog)
|
self.menuHelp.addAction(self.app.actionOpenDebugLog)
|
||||||
self.menuHelp.addAction(self.app.actionAbout)
|
self.menuHelp.addAction(self.app.actionAbout)
|
||||||
|
|
||||||
self.menubar.addAction(self.menuFile.menuAction())
|
self.menubar.addAction(self.menuFile.menuAction())
|
||||||
self.menubar.addAction(self.menuView.menuAction())
|
self.menubar.addAction(self.menuView.menuAction())
|
||||||
self.menubar.addAction(self.menuHelp.menuAction())
|
self.menubar.addAction(self.menuHelp.menuAction())
|
||||||
|
|
||||||
# Recent folders menu
|
# Recent folders menu
|
||||||
self.menuRecentFolders = QMenu()
|
self.menuRecentFolders = QMenu()
|
||||||
self.menuRecentFolders.addAction(self.actionAddFolder)
|
self.menuRecentFolders.addAction(self.actionAddFolder)
|
||||||
self.menuRecentFolders.addSeparator()
|
self.menuRecentFolders.addSeparator()
|
||||||
|
|
||||||
# Recent results menu
|
# Recent results menu
|
||||||
self.menuRecentResults = QMenu()
|
self.menuRecentResults = QMenu()
|
||||||
self.menuRecentResults.addAction(self.actionLoadResults)
|
self.menuRecentResults.addAction(self.actionLoadResults)
|
||||||
self.menuRecentResults.addSeparator()
|
self.menuRecentResults.addSeparator()
|
||||||
|
|
||||||
def _setupUi(self):
|
def _setupUi(self):
|
||||||
self.setWindowTitle(self.app.NAME)
|
self.setWindowTitle(self.app.NAME)
|
||||||
self.resize(420, 338)
|
self.resize(420, 338)
|
||||||
@@ -110,8 +112,8 @@ class DirectoriesDialog(QMainWindow):
|
|||||||
self.treeView.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
self.treeView.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
||||||
self.treeView.setSelectionBehavior(QAbstractItemView.SelectRows)
|
self.treeView.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||||
self.treeView.setAcceptDrops(True)
|
self.treeView.setAcceptDrops(True)
|
||||||
triggers = QAbstractItemView.DoubleClicked|QAbstractItemView.EditKeyPressed\
|
triggers = QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed\
|
||||||
|QAbstractItemView.SelectedClicked
|
| QAbstractItemView.SelectedClicked
|
||||||
self.treeView.setEditTriggers(triggers)
|
self.treeView.setEditTriggers(triggers)
|
||||||
self.treeView.setDragDropOverwriteMode(True)
|
self.treeView.setDragDropOverwriteMode(True)
|
||||||
self.treeView.setDragDropMode(QAbstractItemView.DropOnly)
|
self.treeView.setDragDropMode(QAbstractItemView.DropOnly)
|
||||||
@@ -136,41 +138,41 @@ class DirectoriesDialog(QMainWindow):
|
|||||||
self.horizontalLayout.addWidget(self.scanButton)
|
self.horizontalLayout.addWidget(self.scanButton)
|
||||||
self.verticalLayout.addLayout(self.horizontalLayout)
|
self.verticalLayout.addLayout(self.horizontalLayout)
|
||||||
self.setCentralWidget(self.centralwidget)
|
self.setCentralWidget(self.centralwidget)
|
||||||
|
|
||||||
self._setupActions()
|
self._setupActions()
|
||||||
self._setupMenu()
|
self._setupMenu()
|
||||||
|
|
||||||
if self.app.prefs.directoriesWindowRect is not None:
|
if self.app.prefs.directoriesWindowRect is not None:
|
||||||
self.setGeometry(self.app.prefs.directoriesWindowRect)
|
self.setGeometry(self.app.prefs.directoriesWindowRect)
|
||||||
else:
|
else:
|
||||||
moveToScreenCenter(self)
|
moveToScreenCenter(self)
|
||||||
|
|
||||||
def _setupColumns(self):
|
def _setupColumns(self):
|
||||||
header = self.treeView.header()
|
header = self.treeView.header()
|
||||||
header.setStretchLastSection(False)
|
header.setStretchLastSection(False)
|
||||||
header.setSectionResizeMode(0, QHeaderView.Stretch)
|
header.setSectionResizeMode(0, QHeaderView.Stretch)
|
||||||
header.setSectionResizeMode(1, QHeaderView.Fixed)
|
header.setSectionResizeMode(1, QHeaderView.Fixed)
|
||||||
header.resizeSection(1, 100)
|
header.resizeSection(1, 100)
|
||||||
|
|
||||||
def _updateAddButton(self):
|
def _updateAddButton(self):
|
||||||
if self.recentFolders.isEmpty():
|
if self.recentFolders.isEmpty():
|
||||||
self.addFolderButton.setMenu(None)
|
self.addFolderButton.setMenu(None)
|
||||||
else:
|
else:
|
||||||
self.addFolderButton.setMenu(self.menuRecentFolders)
|
self.addFolderButton.setMenu(self.menuRecentFolders)
|
||||||
|
|
||||||
def _updateRemoveButton(self):
|
def _updateRemoveButton(self):
|
||||||
indexes = self.treeView.selectedIndexes()
|
indexes = self.treeView.selectedIndexes()
|
||||||
if not indexes:
|
if not indexes:
|
||||||
self.removeFolderButton.setEnabled(False)
|
self.removeFolderButton.setEnabled(False)
|
||||||
return
|
return
|
||||||
self.removeFolderButton.setEnabled(True)
|
self.removeFolderButton.setEnabled(True)
|
||||||
|
|
||||||
def _updateLoadResultsButton(self):
|
def _updateLoadResultsButton(self):
|
||||||
if self.app.recentResults.isEmpty():
|
if self.app.recentResults.isEmpty():
|
||||||
self.loadResultsButton.setMenu(None)
|
self.loadResultsButton.setMenu(None)
|
||||||
else:
|
else:
|
||||||
self.loadResultsButton.setMenu(self.menuRecentResults)
|
self.loadResultsButton.setMenu(self.menuRecentResults)
|
||||||
|
|
||||||
#--- QWidget overrides
|
#--- QWidget overrides
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
event.accept()
|
event.accept()
|
||||||
@@ -181,7 +183,7 @@ class DirectoriesDialog(QMainWindow):
|
|||||||
event.ignore()
|
event.ignore()
|
||||||
if event.isAccepted():
|
if event.isAccepted():
|
||||||
QApplication.quit()
|
QApplication.quit()
|
||||||
|
|
||||||
#--- Events
|
#--- Events
|
||||||
def addFolderTriggered(self):
|
def addFolderTriggered(self):
|
||||||
title = tr("Select a folder to add to the scanning list")
|
title = tr("Select a folder to add to the scanning list")
|
||||||
@@ -192,14 +194,14 @@ class DirectoriesDialog(QMainWindow):
|
|||||||
self.lastAddedFolder = dirpath
|
self.lastAddedFolder = dirpath
|
||||||
self.app.model.add_directory(dirpath)
|
self.app.model.add_directory(dirpath)
|
||||||
self.recentFolders.insertItem(dirpath)
|
self.recentFolders.insertItem(dirpath)
|
||||||
|
|
||||||
def appWillSavePrefs(self):
|
def appWillSavePrefs(self):
|
||||||
self.app.prefs.directoriesWindowRect = self.geometry()
|
self.app.prefs.directoriesWindowRect = self.geometry()
|
||||||
|
|
||||||
def directoriesModelAddedFolders(self, folders):
|
def directoriesModelAddedFolders(self, folders):
|
||||||
for folder in folders:
|
for folder in folders:
|
||||||
self.recentFolders.insertItem(folder)
|
self.recentFolders.insertItem(folder)
|
||||||
|
|
||||||
def loadResultsTriggered(self):
|
def loadResultsTriggered(self):
|
||||||
title = tr("Select a results file to load")
|
title = tr("Select a results file to load")
|
||||||
files = ';;'.join([tr("dupeGuru Results (*.dupeguru)"), tr("All Files (*.*)")])
|
files = ';;'.join([tr("dupeGuru Results (*.dupeguru)"), tr("All Files (*.*)")])
|
||||||
@@ -207,10 +209,10 @@ class DirectoriesDialog(QMainWindow):
|
|||||||
if destination:
|
if destination:
|
||||||
self.app.model.load_from(destination)
|
self.app.model.load_from(destination)
|
||||||
self.app.recentResults.insertItem(destination)
|
self.app.recentResults.insertItem(destination)
|
||||||
|
|
||||||
def removeFolderButtonClicked(self):
|
def removeFolderButtonClicked(self):
|
||||||
self.directoriesModel.model.remove_selected()
|
self.directoriesModel.model.remove_selected()
|
||||||
|
|
||||||
def scanButtonClicked(self):
|
def scanButtonClicked(self):
|
||||||
if self.app.model.results.is_modified:
|
if self.app.model.results.is_modified:
|
||||||
title = tr("Start a new scan")
|
title = tr("Start a new scan")
|
||||||
@@ -218,17 +220,17 @@ class DirectoriesDialog(QMainWindow):
|
|||||||
if not self.app.confirm(title, msg):
|
if not self.app.confirm(title, msg):
|
||||||
return
|
return
|
||||||
self.app.model.start_scanning()
|
self.app.model.start_scanning()
|
||||||
|
|
||||||
def selectionChanged(self, selected, deselected):
|
def selectionChanged(self, selected, deselected):
|
||||||
self._updateRemoveButton()
|
self._updateRemoveButton()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import sys
|
import sys
|
||||||
from . import dg_rc
|
from . import dg_rc # NOQA
|
||||||
from ..testapp import TestApp
|
from ..testapp import TestApp
|
||||||
app = QApplication([])
|
app = QApplication([])
|
||||||
dgapp = TestApp()
|
dgapp = TestApp()
|
||||||
dialog = DirectoriesDialog(None, dgapp)
|
dialog = DirectoriesDialog(None, dgapp)
|
||||||
dialog.show()
|
dialog.show()
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec_())
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2009-04-25
|
# Created On: 2009-04-25
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtSignal, Qt, QRect, QUrl, QModelIndex, QItemSelection
|
from PyQt5.QtCore import pyqtSignal, Qt, QRect, QUrl, QModelIndex, QItemSelection
|
||||||
from PyQt5.QtWidgets import (QComboBox, QStyledItemDelegate, QStyle, QStyleOptionComboBox,
|
from PyQt5.QtWidgets import (
|
||||||
QStyleOptionViewItem, QApplication)
|
QComboBox, QStyledItemDelegate, QStyle, QStyleOptionComboBox,
|
||||||
|
QStyleOptionViewItem, QApplication
|
||||||
|
)
|
||||||
from PyQt5.QtGui import QBrush
|
from PyQt5.QtGui import QBrush
|
||||||
|
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
@@ -23,10 +25,10 @@ STATES = [tr("Normal"), tr("Reference"), tr("Excluded")]
|
|||||||
|
|
||||||
class DirectoriesDelegate(QStyledItemDelegate):
|
class DirectoriesDelegate(QStyledItemDelegate):
|
||||||
def createEditor(self, parent, option, index):
|
def createEditor(self, parent, option, index):
|
||||||
editor = QComboBox(parent);
|
editor = QComboBox(parent)
|
||||||
editor.addItems(STATES)
|
editor.addItems(STATES)
|
||||||
return editor
|
return editor
|
||||||
|
|
||||||
def paint(self, painter, option, index):
|
def paint(self, painter, option, index):
|
||||||
self.initStyleOption(option, index)
|
self.initStyleOption(option, index)
|
||||||
# No idea why, but this cast is required if we want to have access to the V4 valuess
|
# No idea why, but this cast is required if we want to have access to the V4 valuess
|
||||||
@@ -44,19 +46,19 @@ class DirectoriesDelegate(QStyledItemDelegate):
|
|||||||
painter.drawText(rect, Qt.AlignLeft, option.text)
|
painter.drawText(rect, Qt.AlignLeft, option.text)
|
||||||
else:
|
else:
|
||||||
super().paint(painter, option, index)
|
super().paint(painter, option, index)
|
||||||
|
|
||||||
def setEditorData(self, editor, index):
|
def setEditorData(self, editor, index):
|
||||||
value = index.model().data(index, Qt.EditRole)
|
value = index.model().data(index, Qt.EditRole)
|
||||||
editor.setCurrentIndex(value);
|
editor.setCurrentIndex(value)
|
||||||
editor.showPopup()
|
editor.showPopup()
|
||||||
|
|
||||||
def setModelData(self, editor, model, index):
|
def setModelData(self, editor, model, index):
|
||||||
value = editor.currentIndex()
|
value = editor.currentIndex()
|
||||||
model.setData(index, value, Qt.EditRole)
|
model.setData(index, value, Qt.EditRole)
|
||||||
|
|
||||||
def updateEditorGeometry(self, editor, option, index):
|
def updateEditorGeometry(self, editor, option, index):
|
||||||
editor.setGeometry(option.rect)
|
editor.setGeometry(option.rect)
|
||||||
|
|
||||||
|
|
||||||
class DirectoriesModel(TreeModel):
|
class DirectoriesModel(TreeModel):
|
||||||
def __init__(self, model, view, **kwargs):
|
def __init__(self, model, view, **kwargs):
|
||||||
@@ -65,18 +67,18 @@ class DirectoriesModel(TreeModel):
|
|||||||
self.model.view = self
|
self.model.view = self
|
||||||
self.view = view
|
self.view = view
|
||||||
self.view.setModel(self)
|
self.view.setModel(self)
|
||||||
|
|
||||||
self.view.selectionModel().selectionChanged[(QItemSelection, QItemSelection)].connect(self.selectionChanged)
|
self.view.selectionModel().selectionChanged[(QItemSelection, QItemSelection)].connect(self.selectionChanged)
|
||||||
|
|
||||||
def _createNode(self, ref, row):
|
def _createNode(self, ref, row):
|
||||||
return RefNode(self, None, ref, row)
|
return RefNode(self, None, ref, row)
|
||||||
|
|
||||||
def _getChildren(self):
|
def _getChildren(self):
|
||||||
return list(self.model)
|
return list(self.model)
|
||||||
|
|
||||||
def columnCount(self, parent=QModelIndex()):
|
def columnCount(self, parent=QModelIndex()):
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
def data(self, index, role):
|
def data(self, index, role):
|
||||||
if not index.isValid():
|
if not index.isValid():
|
||||||
return None
|
return None
|
||||||
@@ -96,7 +98,7 @@ class DirectoriesModel(TreeModel):
|
|||||||
elif state == 2:
|
elif state == 2:
|
||||||
return QBrush(Qt.red)
|
return QBrush(Qt.red)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def dropMimeData(self, mimeData, action, row, column, parentIndex):
|
def dropMimeData(self, mimeData, action, row, column, parentIndex):
|
||||||
# the data in mimeData is urlencoded **in utf-8**!!! What we do is to decode, the mime data
|
# the data in mimeData is urlencoded **in utf-8**!!! What we do is to decode, the mime data
|
||||||
# with 'ascii', which works since it's urlencoded. Then, we pass that to urllib.
|
# with 'ascii', which works since it's urlencoded. Then, we pass that to urllib.
|
||||||
@@ -111,7 +113,7 @@ class DirectoriesModel(TreeModel):
|
|||||||
self.foldersAdded.emit(paths)
|
self.foldersAdded.emit(paths)
|
||||||
self.reset()
|
self.reset()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def flags(self, index):
|
def flags(self, index):
|
||||||
if not index.isValid():
|
if not index.isValid():
|
||||||
return Qt.ItemIsEnabled | Qt.ItemIsDropEnabled
|
return Qt.ItemIsEnabled | Qt.ItemIsDropEnabled
|
||||||
@@ -119,16 +121,16 @@ class DirectoriesModel(TreeModel):
|
|||||||
if index.column() == 1:
|
if index.column() == 1:
|
||||||
result |= Qt.ItemIsEditable
|
result |= Qt.ItemIsEditable
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def headerData(self, section, orientation, role):
|
def headerData(self, section, orientation, role):
|
||||||
if orientation == Qt.Horizontal:
|
if orientation == Qt.Horizontal:
|
||||||
if role == Qt.DisplayRole and section < len(HEADERS):
|
if role == Qt.DisplayRole and section < len(HEADERS):
|
||||||
return HEADERS[section]
|
return HEADERS[section]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def mimeTypes(self):
|
def mimeTypes(self):
|
||||||
return ['text/uri-list']
|
return ['text/uri-list']
|
||||||
|
|
||||||
def setData(self, index, value, role):
|
def setData(self, index, value, role):
|
||||||
if not index.isValid() or role != Qt.EditRole or index.column() != 1:
|
if not index.isValid() or role != Qt.EditRole or index.column() != 1:
|
||||||
return False
|
return False
|
||||||
@@ -136,24 +138,24 @@ class DirectoriesModel(TreeModel):
|
|||||||
ref = node.ref
|
ref = node.ref
|
||||||
ref.state = value
|
ref.state = value
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def supportedDropActions(self):
|
def supportedDropActions(self):
|
||||||
# Normally, the correct action should be ActionLink, but the drop doesn't work. It doesn't
|
# Normally, the correct action should be ActionLink, but the drop doesn't work. It doesn't
|
||||||
# work with ActionMove either. So screw that, and accept anything.
|
# work with ActionMove either. So screw that, and accept anything.
|
||||||
return Qt.ActionMask
|
return Qt.ActionMask
|
||||||
|
|
||||||
#--- Events
|
#--- Events
|
||||||
def selectionChanged(self, selected, deselected):
|
def selectionChanged(self, selected, deselected):
|
||||||
newNodes = [modelIndex.internalPointer().ref for modelIndex in self.view.selectionModel().selectedRows()]
|
newNodes = [modelIndex.internalPointer().ref for modelIndex in self.view.selectionModel().selectedRows()]
|
||||||
self.model.selected_nodes = newNodes
|
self.model.selected_nodes = newNodes
|
||||||
|
|
||||||
#--- Signals
|
#--- Signals
|
||||||
foldersAdded = pyqtSignal(list)
|
foldersAdded = pyqtSignal(list)
|
||||||
|
|
||||||
#--- model --> view
|
#--- model --> view
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
def refresh_states(self):
|
def refresh_states(self):
|
||||||
self.refreshData()
|
self.refreshData()
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2012-03-13
|
# Created On: 2012-03-13
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
@@ -23,11 +23,11 @@ class IgnoreListDialog(QDialog):
|
|||||||
self.model = model
|
self.model = model
|
||||||
self.model.view = self
|
self.model.view = self
|
||||||
self.table = IgnoreListTable(self.model.ignore_list_table, view=self.tableView)
|
self.table = IgnoreListTable(self.model.ignore_list_table, view=self.tableView)
|
||||||
|
|
||||||
self.removeSelectedButton.clicked.connect(self.model.remove_selected)
|
self.removeSelectedButton.clicked.connect(self.model.remove_selected)
|
||||||
self.clearButton.clicked.connect(self.model.clear)
|
self.clearButton.clicked.connect(self.model.clear)
|
||||||
self.closeButton.clicked.connect(self.accept)
|
self.closeButton.clicked.connect(self.accept)
|
||||||
|
|
||||||
def _setupUi(self):
|
def _setupUi(self):
|
||||||
self.setWindowTitle(tr("Ignore List"))
|
self.setWindowTitle(tr("Ignore List"))
|
||||||
self.resize(540, 330)
|
self.resize(540, 330)
|
||||||
@@ -45,10 +45,14 @@ class IgnoreListDialog(QDialog):
|
|||||||
self.removeSelectedButton = QPushButton(tr("Remove Selected"))
|
self.removeSelectedButton = QPushButton(tr("Remove Selected"))
|
||||||
self.clearButton = QPushButton(tr("Clear"))
|
self.clearButton = QPushButton(tr("Clear"))
|
||||||
self.closeButton = QPushButton(tr("Close"))
|
self.closeButton = QPushButton(tr("Close"))
|
||||||
self.verticalLayout.addLayout(horizontalWrap([self.removeSelectedButton, self.clearButton,
|
self.verticalLayout.addLayout(
|
||||||
None, self.closeButton]))
|
horizontalWrap([
|
||||||
|
self.removeSelectedButton, self.clearButton,
|
||||||
|
None, self.closeButton
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
#--- model --> view
|
#--- model --> view
|
||||||
def show(self):
|
def show(self):
|
||||||
super().show()
|
super().show()
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2011-01-21
|
# Created On: 2011-01-21
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, QSize
|
from PyQt5.QtCore import Qt, QSize
|
||||||
from PyQt5.QtWidgets import (QDialog, QDialogButtonBox, QVBoxLayout, QHBoxLayout, QLabel, QComboBox,
|
from PyQt5.QtWidgets import (
|
||||||
QSlider, QSizePolicy, QSpacerItem, QCheckBox, QLineEdit, QMessageBox, QSpinBox)
|
QDialog, QDialogButtonBox, QVBoxLayout, QHBoxLayout, QLabel, QComboBox,
|
||||||
|
QSlider, QSizePolicy, QSpacerItem, QCheckBox, QLineEdit, QMessageBox, QSpinBox
|
||||||
|
)
|
||||||
|
|
||||||
from hscommon.plat import ISOSX, ISLINUX
|
from hscommon.plat import ISOSX, ISLINUX
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
@@ -25,12 +27,12 @@ class PreferencesDialogBase(QDialog):
|
|||||||
super().__init__(parent, flags, **kwargs)
|
super().__init__(parent, flags, **kwargs)
|
||||||
self.app = app
|
self.app = app
|
||||||
self._setupUi()
|
self._setupUi()
|
||||||
|
|
||||||
self.filterHardnessSlider.valueChanged['int'].connect(self.filterHardnessLabel.setNum)
|
self.filterHardnessSlider.valueChanged['int'].connect(self.filterHardnessLabel.setNum)
|
||||||
self.buttonBox.clicked.connect(self.buttonClicked)
|
self.buttonBox.clicked.connect(self.buttonClicked)
|
||||||
self.buttonBox.accepted.connect(self.accept)
|
self.buttonBox.accepted.connect(self.accept)
|
||||||
self.buttonBox.rejected.connect(self.reject)
|
self.buttonBox.rejected.connect(self.reject)
|
||||||
|
|
||||||
def _setupScanTypeBox(self, labels):
|
def _setupScanTypeBox(self, labels):
|
||||||
self.scanTypeHLayout = QHBoxLayout()
|
self.scanTypeHLayout = QHBoxLayout()
|
||||||
self.scanTypeLabel = QLabel(self)
|
self.scanTypeLabel = QLabel(self)
|
||||||
@@ -43,7 +45,7 @@ class PreferencesDialogBase(QDialog):
|
|||||||
self.scanTypeComboBox.addItem(label)
|
self.scanTypeComboBox.addItem(label)
|
||||||
self.scanTypeHLayout.addWidget(self.scanTypeComboBox)
|
self.scanTypeHLayout.addWidget(self.scanTypeComboBox)
|
||||||
self.widgetsVLayout.addLayout(self.scanTypeHLayout)
|
self.widgetsVLayout.addLayout(self.scanTypeHLayout)
|
||||||
|
|
||||||
def _setupFilterHardnessBox(self):
|
def _setupFilterHardnessBox(self):
|
||||||
self.filterHardnessHLayout = QHBoxLayout()
|
self.filterHardnessHLayout = QHBoxLayout()
|
||||||
self.filterHardnessLabel = QLabel(self)
|
self.filterHardnessLabel = QLabel(self)
|
||||||
@@ -82,7 +84,7 @@ class PreferencesDialogBase(QDialog):
|
|||||||
self.filterHardnessHLayoutSub2.addWidget(self.fewerResultsLabel)
|
self.filterHardnessHLayoutSub2.addWidget(self.fewerResultsLabel)
|
||||||
self.filterHardnessVLayout.addLayout(self.filterHardnessHLayoutSub2)
|
self.filterHardnessVLayout.addLayout(self.filterHardnessHLayoutSub2)
|
||||||
self.filterHardnessHLayout.addLayout(self.filterHardnessVLayout)
|
self.filterHardnessHLayout.addLayout(self.filterHardnessVLayout)
|
||||||
|
|
||||||
def _setupBottomPart(self):
|
def _setupBottomPart(self):
|
||||||
# The bottom part of the pref panel is always the same in all editions.
|
# The bottom part of the pref panel is always the same in all editions.
|
||||||
self.fontSizeLabel = QLabel(tr("Font size:"))
|
self.fontSizeLabel = QLabel(tr("Font size:"))
|
||||||
@@ -107,18 +109,18 @@ class PreferencesDialogBase(QDialog):
|
|||||||
self.widgetsVLayout.addWidget(self.customCommandLabel)
|
self.widgetsVLayout.addWidget(self.customCommandLabel)
|
||||||
self.customCommandEdit = QLineEdit(self)
|
self.customCommandEdit = QLineEdit(self)
|
||||||
self.widgetsVLayout.addWidget(self.customCommandEdit)
|
self.widgetsVLayout.addWidget(self.customCommandEdit)
|
||||||
|
|
||||||
def _setupAddCheckbox(self, name, label, parent=None):
|
def _setupAddCheckbox(self, name, label, parent=None):
|
||||||
if parent is None:
|
if parent is None:
|
||||||
parent = self
|
parent = self
|
||||||
cb = QCheckBox(parent)
|
cb = QCheckBox(parent)
|
||||||
cb.setText(label)
|
cb.setText(label)
|
||||||
setattr(self, name, cb)
|
setattr(self, name, cb)
|
||||||
|
|
||||||
def _setupPreferenceWidgets(self):
|
def _setupPreferenceWidgets(self):
|
||||||
# Edition-specific
|
# Edition-specific
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _setupUi(self):
|
def _setupUi(self):
|
||||||
self.setWindowTitle(tr("Preferences"))
|
self.setWindowTitle(tr("Preferences"))
|
||||||
self.resize(304, 263)
|
self.resize(304, 263)
|
||||||
@@ -134,15 +136,15 @@ class PreferencesDialogBase(QDialog):
|
|||||||
if (not ISOSX) and (not ISLINUX):
|
if (not ISOSX) and (not ISLINUX):
|
||||||
self.mainVLayout.removeWidget(self.ignoreHardlinkMatches)
|
self.mainVLayout.removeWidget(self.ignoreHardlinkMatches)
|
||||||
self.ignoreHardlinkMatches.setHidden(True)
|
self.ignoreHardlinkMatches.setHidden(True)
|
||||||
|
|
||||||
def _load(self, prefs, setchecked):
|
def _load(self, prefs, setchecked):
|
||||||
# Edition-specific
|
# Edition-specific
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _save(self, prefs, ischecked):
|
def _save(self, prefs, ischecked):
|
||||||
# Edition-specific
|
# Edition-specific
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def load(self, prefs=None):
|
def load(self, prefs=None):
|
||||||
if prefs is None:
|
if prefs is None:
|
||||||
prefs = self.app.prefs
|
prefs = self.app.prefs
|
||||||
@@ -163,7 +165,7 @@ class PreferencesDialogBase(QDialog):
|
|||||||
langindex = 0
|
langindex = 0
|
||||||
self.languageComboBox.setCurrentIndex(langindex)
|
self.languageComboBox.setCurrentIndex(langindex)
|
||||||
self._load(prefs, setchecked)
|
self._load(prefs, setchecked)
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
prefs = self.app.prefs
|
prefs = self.app.prefs
|
||||||
prefs.filter_hardness = self.filterHardnessSlider.value()
|
prefs.filter_hardness = self.filterHardnessSlider.value()
|
||||||
@@ -184,9 +186,10 @@ class PreferencesDialogBase(QDialog):
|
|||||||
QMessageBox.information(self, "", tr("dupeGuru has to restart for language changes to take effect."))
|
QMessageBox.information(self, "", tr("dupeGuru has to restart for language changes to take effect."))
|
||||||
self.app.prefs.language = lang
|
self.app.prefs.language = lang
|
||||||
self._save(prefs, ischecked)
|
self._save(prefs, ischecked)
|
||||||
|
|
||||||
#--- Events
|
#--- Events
|
||||||
def buttonClicked(self, button):
|
def buttonClicked(self, button):
|
||||||
role = self.buttonBox.buttonRole(button)
|
role = self.buttonBox.buttonRole(button)
|
||||||
if role == QDialogButtonBox.ResetRole:
|
if role == QDialogButtonBox.ResetRole:
|
||||||
self.resetToDefaults()
|
self.resetToDefaults()
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2011-09-06
|
# Created On: 2011-09-06
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, QMimeData, QByteArray
|
from PyQt5.QtCore import Qt, QMimeData, QByteArray
|
||||||
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QComboBox, QListView,
|
from PyQt5.QtWidgets import (
|
||||||
QDialogButtonBox, QAbstractItemView, QLabel, QStyle, QSplitter, QWidget, QSizePolicy)
|
QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QComboBox, QListView,
|
||||||
|
QDialogButtonBox, QAbstractItemView, QLabel, QStyle, QSplitter, QWidget, QSizePolicy
|
||||||
|
)
|
||||||
|
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
from qtlib.selectable_list import ComboboxModel, ListviewModel
|
from qtlib.selectable_list import ComboboxModel, ListviewModel
|
||||||
@@ -24,7 +26,7 @@ class PrioritizationList(ListviewModel):
|
|||||||
if not index.isValid():
|
if not index.isValid():
|
||||||
return Qt.ItemIsEnabled | Qt.ItemIsDropEnabled
|
return Qt.ItemIsEnabled | Qt.ItemIsDropEnabled
|
||||||
return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled
|
return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled
|
||||||
|
|
||||||
#--- Drag & Drop
|
#--- Drag & Drop
|
||||||
def dropMimeData(self, mimeData, action, row, column, parentIndex):
|
def dropMimeData(self, mimeData, action, row, column, parentIndex):
|
||||||
if not mimeData.hasFormat(MIME_INDEXES):
|
if not mimeData.hasFormat(MIME_INDEXES):
|
||||||
@@ -37,17 +39,17 @@ class PrioritizationList(ListviewModel):
|
|||||||
indexes = list(map(int, strMimeData.split(',')))
|
indexes = list(map(int, strMimeData.split(',')))
|
||||||
self.model.move_indexes(indexes, row)
|
self.model.move_indexes(indexes, row)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def mimeData(self, indexes):
|
def mimeData(self, indexes):
|
||||||
rows = {str(index.row()) for index in indexes}
|
rows = {str(index.row()) for index in indexes}
|
||||||
data = ','.join(rows)
|
data = ','.join(rows)
|
||||||
mimeData = QMimeData()
|
mimeData = QMimeData()
|
||||||
mimeData.setData(MIME_INDEXES, QByteArray(data.encode()))
|
mimeData.setData(MIME_INDEXES, QByteArray(data.encode()))
|
||||||
return mimeData
|
return mimeData
|
||||||
|
|
||||||
def mimeTypes(self):
|
def mimeTypes(self):
|
||||||
return [MIME_INDEXES]
|
return [MIME_INDEXES]
|
||||||
|
|
||||||
def supportedDropActions(self):
|
def supportedDropActions(self):
|
||||||
return Qt.MoveAction
|
return Qt.MoveAction
|
||||||
|
|
||||||
@@ -59,9 +61,11 @@ class PrioritizeDialog(QDialog):
|
|||||||
self.model = PrioritizeDialogModel(app=app.model)
|
self.model = PrioritizeDialogModel(app=app.model)
|
||||||
self.categoryList = ComboboxModel(model=self.model.category_list, view=self.categoryCombobox)
|
self.categoryList = ComboboxModel(model=self.model.category_list, view=self.categoryCombobox)
|
||||||
self.criteriaList = ListviewModel(model=self.model.criteria_list, view=self.criteriaListView)
|
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.model.view = self
|
||||||
|
|
||||||
self.addCriteriaButton.clicked.connect(self.model.add_selected)
|
self.addCriteriaButton.clicked.connect(self.model.add_selected)
|
||||||
self.removeCriteriaButton.clicked.connect(self.model.remove_selected)
|
self.removeCriteriaButton.clicked.connect(self.model.remove_selected)
|
||||||
self.buttonBox.accepted.connect(self.accept)
|
self.buttonBox.accepted.connect(self.accept)
|
||||||
@@ -70,11 +74,13 @@ class PrioritizeDialog(QDialog):
|
|||||||
def _setupUi(self):
|
def _setupUi(self):
|
||||||
self.setWindowTitle(tr("Re-Prioritize duplicates"))
|
self.setWindowTitle(tr("Re-Prioritize duplicates"))
|
||||||
self.resize(700, 400)
|
self.resize(700, 400)
|
||||||
|
|
||||||
#widgets
|
#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 "
|
"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 = QLabel(msg)
|
||||||
self.promptLabel.setWordWrap(True)
|
self.promptLabel.setWordWrap(True)
|
||||||
self.categoryCombobox = QComboBox()
|
self.categoryCombobox = QComboBox()
|
||||||
@@ -88,7 +94,7 @@ class PrioritizeDialog(QDialog):
|
|||||||
self.prioritizationListView.setSelectionBehavior(QAbstractItemView.SelectRows)
|
self.prioritizationListView.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||||
self.buttonBox = QDialogButtonBox()
|
self.buttonBox = QDialogButtonBox()
|
||||||
self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel|QDialogButtonBox.Ok)
|
self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel|QDialogButtonBox.Ok)
|
||||||
|
|
||||||
# layout
|
# layout
|
||||||
self.mainLayout = QVBoxLayout(self)
|
self.mainLayout = QVBoxLayout(self)
|
||||||
self.mainLayout.addWidget(self.promptLabel)
|
self.mainLayout.addWidget(self.promptLabel)
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2010-04-12
|
# Created On: 2010-04-12
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QSpacerItem, QSizePolicy,
|
from PyQt5.QtWidgets import (
|
||||||
QLabel, QTableView, QAbstractItemView, QApplication)
|
QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QSpacerItem, QSizePolicy,
|
||||||
|
QLabel, QTableView, QAbstractItemView, QApplication
|
||||||
|
)
|
||||||
|
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
from .problem_table import ProblemTable
|
from .problem_table import ProblemTable
|
||||||
@@ -23,18 +25,20 @@ class ProblemDialog(QDialog):
|
|||||||
self.model = model
|
self.model = model
|
||||||
self.model.view = self
|
self.model.view = self
|
||||||
self.table = ProblemTable(self.model.problem_table, view=self.tableView)
|
self.table = ProblemTable(self.model.problem_table, view=self.tableView)
|
||||||
|
|
||||||
self.revealButton.clicked.connect(self.model.reveal_selected_dupe)
|
self.revealButton.clicked.connect(self.model.reveal_selected_dupe)
|
||||||
self.closeButton.clicked.connect(self.accept)
|
self.closeButton.clicked.connect(self.accept)
|
||||||
|
|
||||||
def _setupUi(self):
|
def _setupUi(self):
|
||||||
self.setWindowTitle(tr("Problems!"))
|
self.setWindowTitle(tr("Problems!"))
|
||||||
self.resize(413, 323)
|
self.resize(413, 323)
|
||||||
self.verticalLayout = QVBoxLayout(self)
|
self.verticalLayout = QVBoxLayout(self)
|
||||||
self.label = QLabel(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 "
|
"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.setText(msg)
|
||||||
self.label.setWordWrap(True)
|
self.label.setWordWrap(True)
|
||||||
self.verticalLayout.addWidget(self.label)
|
self.verticalLayout.addWidget(self.label)
|
||||||
@@ -58,7 +62,7 @@ class ProblemDialog(QDialog):
|
|||||||
self.closeButton.setDefault(True)
|
self.closeButton.setDefault(True)
|
||||||
self.horizontalLayout.addWidget(self.closeButton)
|
self.horizontalLayout.addWidget(self.closeButton)
|
||||||
self.verticalLayout.addLayout(self.horizontalLayout)
|
self.verticalLayout.addLayout(self.horizontalLayout)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import sys
|
import sys
|
||||||
@@ -67,4 +71,4 @@ if __name__ == '__main__':
|
|||||||
dgapp = TestApp()
|
dgapp = TestApp()
|
||||||
dialog = ProblemDialog(None, dgapp)
|
dialog = ProblemDialog(None, dgapp)
|
||||||
dialog.show()
|
dialog.show()
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec_())
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2009-04-25
|
# Created On: 2009-04-25
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, QRect
|
from PyQt5.QtCore import Qt, QRect
|
||||||
from PyQt5.QtWidgets import (QMainWindow, QMenu, QLabel, QFileDialog, QMenuBar, QWidget,
|
from PyQt5.QtWidgets import (
|
||||||
QVBoxLayout, QAbstractItemView, QStatusBar, QDialog, QPushButton, QCheckBox)
|
QMainWindow, QMenu, QLabel, QFileDialog, QMenuBar, QWidget,
|
||||||
|
QVBoxLayout, QAbstractItemView, QStatusBar, QDialog, QPushButton, QCheckBox
|
||||||
|
)
|
||||||
|
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
from qtlib.util import moveToScreenCenter, horizontalWrap, createActions
|
from qtlib.util import moveToScreenCenter, horizontalWrap, createActions
|
||||||
@@ -28,7 +30,7 @@ class ResultWindow(QMainWindow):
|
|||||||
self.resultsModel = app.RESULT_MODEL_CLASS(self.app, self.resultsView)
|
self.resultsModel = app.RESULT_MODEL_CLASS(self.app, self.resultsView)
|
||||||
self.stats = StatsLabel(app.model.stats_label, self.statusLabel)
|
self.stats = StatsLabel(app.model.stats_label, self.statusLabel)
|
||||||
self._update_column_actions_status()
|
self._update_column_actions_status()
|
||||||
|
|
||||||
self.menuColumns.triggered.connect(self.columnToggled)
|
self.menuColumns.triggered.connect(self.columnToggled)
|
||||||
self.resultsView.doubleClicked.connect(self.resultsDoubleClicked)
|
self.resultsView.doubleClicked.connect(self.resultsDoubleClicked)
|
||||||
self.resultsView.spacePressed.connect(self.resultsSpacePressed)
|
self.resultsView.spacePressed.connect(self.resultsSpacePressed)
|
||||||
@@ -37,7 +39,7 @@ class ResultWindow(QMainWindow):
|
|||||||
self.deltaValuesCheckBox.stateChanged.connect(self.deltaTriggered)
|
self.deltaValuesCheckBox.stateChanged.connect(self.deltaTriggered)
|
||||||
self.searchEdit.searchChanged.connect(self.searchChanged)
|
self.searchEdit.searchChanged.connect(self.searchChanged)
|
||||||
self.app.willSavePrefs.connect(self.appWillSavePrefs)
|
self.app.willSavePrefs.connect(self.appWillSavePrefs)
|
||||||
|
|
||||||
def _setupActions(self):
|
def _setupActions(self):
|
||||||
# (name, shortcut, icon, desc, func)
|
# (name, shortcut, icon, desc, func)
|
||||||
ACTIONS = [
|
ACTIONS = [
|
||||||
@@ -50,11 +52,23 @@ class ResultWindow(QMainWindow):
|
|||||||
('actionCopyMarked', 'Ctrl+Shift+M', '', tr("Copy Marked to..."), self.copyTriggered),
|
('actionCopyMarked', 'Ctrl+Shift+M', '', tr("Copy Marked to..."), self.copyTriggered),
|
||||||
('actionRemoveMarked', 'Ctrl+R', '', tr("Remove Marked from Results"), self.removeMarkedTriggered),
|
('actionRemoveMarked', 'Ctrl+R', '', tr("Remove Marked from Results"), self.removeMarkedTriggered),
|
||||||
('actionReprioritize', '', '', tr("Re-Prioritize Results..."), self.reprioritizeTriggered),
|
('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),
|
'actionRemoveSelected', 'Ctrl+Del', '',
|
||||||
('actionMakeSelectedReference', 'Ctrl+Space', '', tr("Make Selected into Reference"), self.app.model.make_selected_reference),
|
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),
|
('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),
|
('actionRenameSelected', 'F2', '', tr("Rename Selected"), self.renameTriggered),
|
||||||
('actionMarkAll', 'Ctrl+A', '', tr("Mark All"), self.markAllTriggered),
|
('actionMarkAll', 'Ctrl+A', '', tr("Mark All"), self.markAllTriggered),
|
||||||
('actionMarkNone', 'Ctrl+Shift+A', '', tr("Mark None"), self.markNoneTriggered),
|
('actionMarkNone', 'Ctrl+Shift+A', '', tr("Mark None"), self.markNoneTriggered),
|
||||||
@@ -68,7 +82,7 @@ class ResultWindow(QMainWindow):
|
|||||||
createActions(ACTIONS, self)
|
createActions(ACTIONS, self)
|
||||||
self.actionDelta.setCheckable(True)
|
self.actionDelta.setCheckable(True)
|
||||||
self.actionPowerMarker.setCheckable(True)
|
self.actionPowerMarker.setCheckable(True)
|
||||||
|
|
||||||
def _setupMenu(self):
|
def _setupMenu(self):
|
||||||
self.menubar = QMenuBar()
|
self.menubar = QMenuBar()
|
||||||
self.menubar.setGeometry(QRect(0, 0, 630, 22))
|
self.menubar.setGeometry(QRect(0, 0, 630, 22))
|
||||||
@@ -85,7 +99,7 @@ class ResultWindow(QMainWindow):
|
|||||||
self.menuHelp = QMenu(self.menubar)
|
self.menuHelp = QMenu(self.menubar)
|
||||||
self.menuHelp.setTitle(tr("Help"))
|
self.menuHelp.setTitle(tr("Help"))
|
||||||
self.setMenuBar(self.menubar)
|
self.setMenuBar(self.menubar)
|
||||||
|
|
||||||
self.menuActions.addAction(self.actionDeleteMarked)
|
self.menuActions.addAction(self.actionDeleteMarked)
|
||||||
self.menuActions.addAction(self.actionMoveMarked)
|
self.menuActions.addAction(self.actionMoveMarked)
|
||||||
self.menuActions.addAction(self.actionCopyMarked)
|
self.menuActions.addAction(self.actionCopyMarked)
|
||||||
@@ -118,14 +132,14 @@ class ResultWindow(QMainWindow):
|
|||||||
self.menuFile.addAction(self.actionExportToCSV)
|
self.menuFile.addAction(self.actionExportToCSV)
|
||||||
self.menuFile.addSeparator()
|
self.menuFile.addSeparator()
|
||||||
self.menuFile.addAction(self.app.actionQuit)
|
self.menuFile.addAction(self.app.actionQuit)
|
||||||
|
|
||||||
self.menubar.addAction(self.menuFile.menuAction())
|
self.menubar.addAction(self.menuFile.menuAction())
|
||||||
self.menubar.addAction(self.menuMark.menuAction())
|
self.menubar.addAction(self.menuMark.menuAction())
|
||||||
self.menubar.addAction(self.menuActions.menuAction())
|
self.menubar.addAction(self.menuActions.menuAction())
|
||||||
self.menubar.addAction(self.menuColumns.menuAction())
|
self.menubar.addAction(self.menuColumns.menuAction())
|
||||||
self.menubar.addAction(self.menuView.menuAction())
|
self.menubar.addAction(self.menuView.menuAction())
|
||||||
self.menubar.addAction(self.menuHelp.menuAction())
|
self.menubar.addAction(self.menuHelp.menuAction())
|
||||||
|
|
||||||
# Columns menu
|
# Columns menu
|
||||||
menu = self.menuColumns
|
menu = self.menuColumns
|
||||||
self._column_actions = []
|
self._column_actions = []
|
||||||
@@ -138,7 +152,7 @@ class ResultWindow(QMainWindow):
|
|||||||
menu.addSeparator()
|
menu.addSeparator()
|
||||||
action = menu.addAction(tr("Reset to Defaults"))
|
action = menu.addAction(tr("Reset to Defaults"))
|
||||||
action.item_index = -1
|
action.item_index = -1
|
||||||
|
|
||||||
# Action menu
|
# Action menu
|
||||||
actionMenu = QMenu(tr("Actions"), self.menubar)
|
actionMenu = QMenu(tr("Actions"), self.menubar)
|
||||||
actionMenu.addAction(self.actionDeleteMarked)
|
actionMenu.addAction(self.actionDeleteMarked)
|
||||||
@@ -156,7 +170,7 @@ class ResultWindow(QMainWindow):
|
|||||||
actionMenu.addAction(self.actionRenameSelected)
|
actionMenu.addAction(self.actionRenameSelected)
|
||||||
self.actionActions.setMenu(actionMenu)
|
self.actionActions.setMenu(actionMenu)
|
||||||
self.actionsButton.setMenu(self.actionActions.menu())
|
self.actionsButton.setMenu(self.actionActions.menu())
|
||||||
|
|
||||||
def _setupUi(self):
|
def _setupUi(self):
|
||||||
self.setWindowTitle(tr("{} Results").format(self.app.NAME))
|
self.setWindowTitle(tr("{} Results").format(self.app.NAME))
|
||||||
self.resize(630, 514)
|
self.resize(630, 514)
|
||||||
@@ -170,8 +184,10 @@ class ResultWindow(QMainWindow):
|
|||||||
self.deltaValuesCheckBox = QCheckBox(tr("Delta Values"))
|
self.deltaValuesCheckBox = QCheckBox(tr("Delta Values"))
|
||||||
self.searchEdit = SearchEdit()
|
self.searchEdit = SearchEdit()
|
||||||
self.searchEdit.setMaximumWidth(300)
|
self.searchEdit.setMaximumWidth(300)
|
||||||
self.horizontalLayout = horizontalWrap([self.actionsButton, self.detailsButton,
|
self.horizontalLayout = horizontalWrap([
|
||||||
self.dupesOnlyCheckBox, self.deltaValuesCheckBox, None, self.searchEdit, 8])
|
self.actionsButton, self.detailsButton,
|
||||||
|
self.dupesOnlyCheckBox, self.deltaValuesCheckBox, None, self.searchEdit, 8
|
||||||
|
])
|
||||||
self.horizontalLayout.setSpacing(8)
|
self.horizontalLayout.setSpacing(8)
|
||||||
self.verticalLayout.addLayout(self.horizontalLayout)
|
self.verticalLayout.addLayout(self.horizontalLayout)
|
||||||
self.resultsView = ResultsView(self.centralwidget)
|
self.resultsView = ResultsView(self.centralwidget)
|
||||||
@@ -193,7 +209,7 @@ class ResultWindow(QMainWindow):
|
|||||||
self.setStatusBar(self.statusbar)
|
self.setStatusBar(self.statusbar)
|
||||||
self.statusLabel = QLabel(self)
|
self.statusLabel = QLabel(self)
|
||||||
self.statusbar.addPermanentWidget(self.statusLabel, 1)
|
self.statusbar.addPermanentWidget(self.statusLabel, 1)
|
||||||
|
|
||||||
if self.app.prefs.resultWindowIsMaximized:
|
if self.app.prefs.resultWindowIsMaximized:
|
||||||
self.setWindowState(self.windowState() | Qt.WindowMaximized)
|
self.setWindowState(self.windowState() | Qt.WindowMaximized)
|
||||||
else:
|
else:
|
||||||
@@ -201,85 +217,85 @@ class ResultWindow(QMainWindow):
|
|||||||
self.setGeometry(self.app.prefs.resultWindowRect)
|
self.setGeometry(self.app.prefs.resultWindowRect)
|
||||||
else:
|
else:
|
||||||
moveToScreenCenter(self)
|
moveToScreenCenter(self)
|
||||||
|
|
||||||
#--- Private
|
#--- Private
|
||||||
def _update_column_actions_status(self):
|
def _update_column_actions_status(self):
|
||||||
# Update menu checked state
|
# Update menu checked state
|
||||||
menu_items = self.app.model.result_table.columns.menu_items()
|
menu_items = self.app.model.result_table.columns.menu_items()
|
||||||
for action, (display, visible) in zip(self._column_actions, menu_items):
|
for action, (display, visible) in zip(self._column_actions, menu_items):
|
||||||
action.setChecked(visible)
|
action.setChecked(visible)
|
||||||
|
|
||||||
#--- Actions
|
#--- Actions
|
||||||
def actionsTriggered(self):
|
def actionsTriggered(self):
|
||||||
self.actionsButton.showMenu()
|
self.actionsButton.showMenu()
|
||||||
|
|
||||||
def addToIgnoreListTriggered(self):
|
def addToIgnoreListTriggered(self):
|
||||||
self.app.model.add_selected_to_ignore_list()
|
self.app.model.add_selected_to_ignore_list()
|
||||||
|
|
||||||
def copyTriggered(self):
|
def copyTriggered(self):
|
||||||
self.app.model.copy_or_move_marked(True)
|
self.app.model.copy_or_move_marked(True)
|
||||||
|
|
||||||
def deleteTriggered(self):
|
def deleteTriggered(self):
|
||||||
self.app.model.delete_marked()
|
self.app.model.delete_marked()
|
||||||
|
|
||||||
def deltaTriggered(self, state=None):
|
def deltaTriggered(self, state=None):
|
||||||
# The sender can be either the action or the checkbox, but both have a isChecked() method.
|
# The sender can be either the action or the checkbox, but both have a isChecked() method.
|
||||||
self.resultsModel.delta_values = self.sender().isChecked()
|
self.resultsModel.delta_values = self.sender().isChecked()
|
||||||
self.actionDelta.setChecked(self.resultsModel.delta_values)
|
self.actionDelta.setChecked(self.resultsModel.delta_values)
|
||||||
self.deltaValuesCheckBox.setChecked(self.resultsModel.delta_values)
|
self.deltaValuesCheckBox.setChecked(self.resultsModel.delta_values)
|
||||||
|
|
||||||
def detailsTriggered(self):
|
def detailsTriggered(self):
|
||||||
self.app.show_details()
|
self.app.show_details()
|
||||||
|
|
||||||
def markAllTriggered(self):
|
def markAllTriggered(self):
|
||||||
self.app.model.mark_all()
|
self.app.model.mark_all()
|
||||||
|
|
||||||
def markInvertTriggered(self):
|
def markInvertTriggered(self):
|
||||||
self.app.model.mark_invert()
|
self.app.model.mark_invert()
|
||||||
|
|
||||||
def markNoneTriggered(self):
|
def markNoneTriggered(self):
|
||||||
self.app.model.mark_none()
|
self.app.model.mark_none()
|
||||||
|
|
||||||
def markSelectedTriggered(self):
|
def markSelectedTriggered(self):
|
||||||
self.app.model.toggle_selected_mark_state()
|
self.app.model.toggle_selected_mark_state()
|
||||||
|
|
||||||
def moveTriggered(self):
|
def moveTriggered(self):
|
||||||
self.app.model.copy_or_move_marked(False)
|
self.app.model.copy_or_move_marked(False)
|
||||||
|
|
||||||
def openTriggered(self):
|
def openTriggered(self):
|
||||||
self.app.model.open_selected()
|
self.app.model.open_selected()
|
||||||
|
|
||||||
def powerMarkerTriggered(self, state=None):
|
def powerMarkerTriggered(self, state=None):
|
||||||
# see deltaTriggered
|
# see deltaTriggered
|
||||||
self.resultsModel.power_marker = self.sender().isChecked()
|
self.resultsModel.power_marker = self.sender().isChecked()
|
||||||
self.actionPowerMarker.setChecked(self.resultsModel.power_marker)
|
self.actionPowerMarker.setChecked(self.resultsModel.power_marker)
|
||||||
self.dupesOnlyCheckBox.setChecked(self.resultsModel.power_marker)
|
self.dupesOnlyCheckBox.setChecked(self.resultsModel.power_marker)
|
||||||
|
|
||||||
def preferencesTriggered(self):
|
def preferencesTriggered(self):
|
||||||
self.app.show_preferences()
|
self.app.show_preferences()
|
||||||
|
|
||||||
def removeMarkedTriggered(self):
|
def removeMarkedTriggered(self):
|
||||||
self.app.model.remove_marked()
|
self.app.model.remove_marked()
|
||||||
|
|
||||||
def removeSelectedTriggered(self):
|
def removeSelectedTriggered(self):
|
||||||
self.app.model.remove_selected()
|
self.app.model.remove_selected()
|
||||||
|
|
||||||
def renameTriggered(self):
|
def renameTriggered(self):
|
||||||
index = self.resultsView.selectionModel().currentIndex()
|
index = self.resultsView.selectionModel().currentIndex()
|
||||||
# Our index is the current row, with column set to 0. Our filename column is 1 and that's
|
# Our index is the current row, with column set to 0. Our filename column is 1 and that's
|
||||||
# what we want.
|
# what we want.
|
||||||
index = index.sibling(index.row(), 1)
|
index = index.sibling(index.row(), 1)
|
||||||
self.resultsView.edit(index)
|
self.resultsView.edit(index)
|
||||||
|
|
||||||
def reprioritizeTriggered(self):
|
def reprioritizeTriggered(self):
|
||||||
dlg = PrioritizeDialog(self, self.app)
|
dlg = PrioritizeDialog(self, self.app)
|
||||||
result = dlg.exec()
|
result = dlg.exec()
|
||||||
if result == QDialog.Accepted:
|
if result == QDialog.Accepted:
|
||||||
dlg.model.perform_reprioritization()
|
dlg.model.perform_reprioritization()
|
||||||
|
|
||||||
def revealTriggered(self):
|
def revealTriggered(self):
|
||||||
self.app.model.reveal_selected()
|
self.app.model.reveal_selected()
|
||||||
|
|
||||||
def saveResultsTriggered(self):
|
def saveResultsTriggered(self):
|
||||||
title = tr("Select a file to save your results to")
|
title = tr("Select a file to save your results to")
|
||||||
files = tr("dupeGuru Results (*.dupeguru)")
|
files = tr("dupeGuru Results (*.dupeguru)")
|
||||||
@@ -289,13 +305,13 @@ class ResultWindow(QMainWindow):
|
|||||||
destination = '{}.dupeguru'.format(destination)
|
destination = '{}.dupeguru'.format(destination)
|
||||||
self.app.model.save_as(destination)
|
self.app.model.save_as(destination)
|
||||||
self.app.recentResults.insertItem(destination)
|
self.app.recentResults.insertItem(destination)
|
||||||
|
|
||||||
#--- Events
|
#--- Events
|
||||||
def appWillSavePrefs(self):
|
def appWillSavePrefs(self):
|
||||||
prefs = self.app.prefs
|
prefs = self.app.prefs
|
||||||
prefs.resultWindowIsMaximized = self.isMaximized()
|
prefs.resultWindowIsMaximized = self.isMaximized()
|
||||||
prefs.resultWindowRect = self.geometry()
|
prefs.resultWindowRect = self.geometry()
|
||||||
|
|
||||||
def columnToggled(self, action):
|
def columnToggled(self, action):
|
||||||
index = action.item_index
|
index = action.item_index
|
||||||
if index == -1:
|
if index == -1:
|
||||||
@@ -304,16 +320,16 @@ class ResultWindow(QMainWindow):
|
|||||||
else:
|
else:
|
||||||
visible = self.app.model.result_table.columns.toggle_menu_item(index)
|
visible = self.app.model.result_table.columns.toggle_menu_item(index)
|
||||||
action.setChecked(visible)
|
action.setChecked(visible)
|
||||||
|
|
||||||
def contextMenuEvent(self, event):
|
def contextMenuEvent(self, event):
|
||||||
self.actionActions.menu().exec_(event.globalPos())
|
self.actionActions.menu().exec_(event.globalPos())
|
||||||
|
|
||||||
def resultsDoubleClicked(self, modelIndex):
|
def resultsDoubleClicked(self, modelIndex):
|
||||||
self.app.model.open_selected()
|
self.app.model.open_selected()
|
||||||
|
|
||||||
def resultsSpacePressed(self):
|
def resultsSpacePressed(self):
|
||||||
self.app.model.toggle_selected_mark_state()
|
self.app.model.toggle_selected_mark_state()
|
||||||
|
|
||||||
def searchChanged(self):
|
def searchChanged(self):
|
||||||
self.app.model.apply_filter(self.searchEdit.text())
|
self.app.model.apply_filter(self.searchEdit.text())
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2009-04-29
|
# Created On: 2009-04-29
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from PyQt5.QtCore import QSize
|
from PyQt5.QtCore import QSize
|
||||||
from PyQt5.QtWidgets import (QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QSpacerItem, QWidget,
|
from PyQt5.QtWidgets import (
|
||||||
QApplication)
|
QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QSpacerItem, QWidget,
|
||||||
|
QApplication
|
||||||
|
)
|
||||||
|
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
from core.scanner import ScanType
|
from core.scanner import ScanType
|
||||||
@@ -31,9 +33,9 @@ SCAN_TYPE_ORDER = [
|
|||||||
class PreferencesDialog(PreferencesDialogBase):
|
class PreferencesDialog(PreferencesDialogBase):
|
||||||
def __init__(self, parent, app):
|
def __init__(self, parent, app):
|
||||||
PreferencesDialogBase.__init__(self, parent, app)
|
PreferencesDialogBase.__init__(self, parent, app)
|
||||||
|
|
||||||
self.scanTypeComboBox.currentIndexChanged[int].connect(self.scanTypeChanged)
|
self.scanTypeComboBox.currentIndexChanged[int].connect(self.scanTypeChanged)
|
||||||
|
|
||||||
def _setupPreferenceWidgets(self):
|
def _setupPreferenceWidgets(self):
|
||||||
scanTypeLabels = [
|
scanTypeLabels = [
|
||||||
tr("Filename"),
|
tr("Filename"),
|
||||||
@@ -87,7 +89,7 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
self._setupAddCheckbox('debugModeBox', tr("Debug mode (restart required)"))
|
self._setupAddCheckbox('debugModeBox', tr("Debug mode (restart required)"))
|
||||||
self.widgetsVLayout.addWidget(self.debugModeBox)
|
self.widgetsVLayout.addWidget(self.debugModeBox)
|
||||||
self._setupBottomPart()
|
self._setupBottomPart()
|
||||||
|
|
||||||
def _load(self, prefs, setchecked):
|
def _load(self, prefs, setchecked):
|
||||||
scan_type_index = SCAN_TYPE_ORDER.index(prefs.scan_type)
|
scan_type_index = SCAN_TYPE_ORDER.index(prefs.scan_type)
|
||||||
self.scanTypeComboBox.setCurrentIndex(scan_type_index)
|
self.scanTypeComboBox.setCurrentIndex(scan_type_index)
|
||||||
@@ -99,7 +101,7 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
setchecked(self.tagYearBox, prefs.scan_tag_year)
|
setchecked(self.tagYearBox, prefs.scan_tag_year)
|
||||||
setchecked(self.matchSimilarBox, prefs.match_similar)
|
setchecked(self.matchSimilarBox, prefs.match_similar)
|
||||||
setchecked(self.wordWeightingBox, prefs.word_weighting)
|
setchecked(self.wordWeightingBox, prefs.word_weighting)
|
||||||
|
|
||||||
def _save(self, prefs, ischecked):
|
def _save(self, prefs, ischecked):
|
||||||
prefs.scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()]
|
prefs.scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()]
|
||||||
prefs.scan_tag_track = ischecked(self.tagTrackBox)
|
prefs.scan_tag_track = ischecked(self.tagTrackBox)
|
||||||
@@ -110,15 +112,17 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
prefs.scan_tag_year = ischecked(self.tagYearBox)
|
prefs.scan_tag_year = ischecked(self.tagYearBox)
|
||||||
prefs.match_similar = ischecked(self.matchSimilarBox)
|
prefs.match_similar = ischecked(self.matchSimilarBox)
|
||||||
prefs.word_weighting = ischecked(self.wordWeightingBox)
|
prefs.word_weighting = ischecked(self.wordWeightingBox)
|
||||||
|
|
||||||
def resetToDefaults(self):
|
def resetToDefaults(self):
|
||||||
self.load(preferences.Preferences())
|
self.load(preferences.Preferences())
|
||||||
|
|
||||||
#--- Events
|
#--- Events
|
||||||
def scanTypeChanged(self, index):
|
def scanTypeChanged(self, index):
|
||||||
scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()]
|
scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()]
|
||||||
word_based = scan_type in (ScanType.Filename, ScanType.Fields, ScanType.FieldsNoOrder,
|
word_based = scan_type in (
|
||||||
ScanType.Tag)
|
ScanType.Filename, ScanType.Fields, ScanType.FieldsNoOrder,
|
||||||
|
ScanType.Tag
|
||||||
|
)
|
||||||
tag_based = scan_type == ScanType.Tag
|
tag_based = scan_type == ScanType.Tag
|
||||||
self.filterHardnessSlider.setEnabled(word_based)
|
self.filterHardnessSlider.setEnabled(word_based)
|
||||||
self.matchSimilarBox.setEnabled(word_based)
|
self.matchSimilarBox.setEnabled(word_based)
|
||||||
@@ -129,7 +133,7 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
self.tagTitleBox.setEnabled(tag_based)
|
self.tagTitleBox.setEnabled(tag_based)
|
||||||
self.tagGenreBox.setEnabled(tag_based)
|
self.tagGenreBox.setEnabled(tag_based)
|
||||||
self.tagYearBox.setEnabled(tag_based)
|
self.tagYearBox.setEnabled(tag_based)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
from ..testapp import TestApp
|
from ..testapp import TestApp
|
||||||
@@ -137,4 +141,5 @@ if __name__ == '__main__':
|
|||||||
dgapp = TestApp()
|
dgapp = TestApp()
|
||||||
dialog = PreferencesDialog(None, dgapp)
|
dialog = PreferencesDialog(None, dgapp)
|
||||||
dialog.show()
|
dialog.show()
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec_())
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Created On: 2011-11-27
|
# Created On: 2011-11-27
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from qtlib.column import Column
|
from qtlib.column import Column
|
||||||
@@ -29,4 +29,5 @@ class ResultsModel(ResultsModelBase):
|
|||||||
Column('percentage', defaultWidth=60),
|
Column('percentage', defaultWidth=60),
|
||||||
Column('words', defaultWidth=120),
|
Column('words', defaultWidth=120),
|
||||||
Column('dupe_count', defaultWidth=80),
|
Column('dupe_count', defaultWidth=80),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2009-05-10
|
# Created On: 2009-05-10
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from ._block_qt import getblocks
|
from ._block_qt import getblocks # NOQA
|
||||||
|
|
||||||
# Converted to C
|
# Converted to C
|
||||||
# def getblock(image):
|
# def getblock(image):
|
||||||
@@ -24,7 +24,7 @@ from ._block_qt import getblocks
|
|||||||
# return (red // pixel_count, green // pixel_count, blue // pixel_count)
|
# return (red // pixel_count, green // pixel_count, blue // pixel_count)
|
||||||
# else:
|
# else:
|
||||||
# return (0, 0, 0)
|
# return (0, 0, 0)
|
||||||
#
|
#
|
||||||
# def getblocks(image, block_count_per_side):
|
# def getblocks(image, block_count_per_side):
|
||||||
# width = image.width()
|
# width = image.width()
|
||||||
# height = image.height()
|
# height = image.height()
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2009-04-29
|
# Created On: 2009-04-29
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
@@ -25,9 +25,9 @@ SCAN_TYPE_ORDER = [
|
|||||||
class PreferencesDialog(PreferencesDialogBase):
|
class PreferencesDialog(PreferencesDialogBase):
|
||||||
def __init__(self, parent, app):
|
def __init__(self, parent, app):
|
||||||
PreferencesDialogBase.__init__(self, parent, app)
|
PreferencesDialogBase.__init__(self, parent, app)
|
||||||
|
|
||||||
self.scanTypeComboBox.currentIndexChanged[int].connect(self.scanTypeChanged)
|
self.scanTypeComboBox.currentIndexChanged[int].connect(self.scanTypeChanged)
|
||||||
|
|
||||||
def _setupPreferenceWidgets(self):
|
def _setupPreferenceWidgets(self):
|
||||||
scanTypeLabels = [
|
scanTypeLabels = [
|
||||||
tr("Contents"),
|
tr("Contents"),
|
||||||
@@ -49,25 +49,25 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
self._setupAddCheckbox('debugModeBox', tr("Debug mode (restart required)"))
|
self._setupAddCheckbox('debugModeBox', tr("Debug mode (restart required)"))
|
||||||
self.widgetsVLayout.addWidget(self.debugModeBox)
|
self.widgetsVLayout.addWidget(self.debugModeBox)
|
||||||
self._setupBottomPart()
|
self._setupBottomPart()
|
||||||
|
|
||||||
def _load(self, prefs, setchecked):
|
def _load(self, prefs, setchecked):
|
||||||
scan_type_index = SCAN_TYPE_ORDER.index(prefs.scan_type)
|
scan_type_index = SCAN_TYPE_ORDER.index(prefs.scan_type)
|
||||||
self.scanTypeComboBox.setCurrentIndex(scan_type_index)
|
self.scanTypeComboBox.setCurrentIndex(scan_type_index)
|
||||||
setchecked(self.matchScaledBox, prefs.match_scaled)
|
setchecked(self.matchScaledBox, prefs.match_scaled)
|
||||||
|
|
||||||
def _save(self, prefs, ischecked):
|
def _save(self, prefs, ischecked):
|
||||||
prefs.scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()]
|
prefs.scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()]
|
||||||
prefs.match_scaled = ischecked(self.matchScaledBox)
|
prefs.match_scaled = ischecked(self.matchScaledBox)
|
||||||
|
|
||||||
def resetToDefaults(self):
|
def resetToDefaults(self):
|
||||||
self.load(preferences.Preferences())
|
self.load(preferences.Preferences())
|
||||||
|
|
||||||
#--- Events
|
#--- Events
|
||||||
def scanTypeChanged(self, index):
|
def scanTypeChanged(self, index):
|
||||||
scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()]
|
scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()]
|
||||||
fuzzy_scan = scan_type == ScanType.FuzzyBlock
|
fuzzy_scan = scan_type == ScanType.FuzzyBlock
|
||||||
self.filterHardnessSlider.setEnabled(fuzzy_scan)
|
self.filterHardnessSlider.setEnabled(fuzzy_scan)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
from ..testapp import TestApp
|
from ..testapp import TestApp
|
||||||
@@ -75,4 +75,5 @@ if __name__ == '__main__':
|
|||||||
dgapp = TestApp()
|
dgapp = TestApp()
|
||||||
dialog = PreferencesDialog(None, dgapp)
|
dialog = PreferencesDialog(None, dgapp)
|
||||||
dialog.show()
|
dialog.show()
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec_())
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Created On: 2011-11-27
|
# Created On: 2011-11-27
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from qtlib.column import Column
|
from qtlib.column import Column
|
||||||
@@ -20,4 +20,5 @@ class ResultsModel(ResultsModelBase):
|
|||||||
Column('mtime', defaultWidth=120),
|
Column('mtime', defaultWidth=120),
|
||||||
Column('percentage', defaultWidth=60),
|
Column('percentage', defaultWidth=60),
|
||||||
Column('dupe_count', defaultWidth=80),
|
Column('dupe_count', defaultWidth=80),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
15
qt/se/app.py
@@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2009-05-24
|
# Created On: 2009-05-24
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from core_se import __appname__
|
from core_se import __appname__
|
||||||
@@ -18,6 +18,7 @@ from .preferences_dialog import PreferencesDialog
|
|||||||
|
|
||||||
class Directories(DirectoriesBase):
|
class Directories(DirectoriesBase):
|
||||||
ROOT_PATH_TO_EXCLUDE = frozenset(['windows', 'program files'])
|
ROOT_PATH_TO_EXCLUDE = frozenset(['windows', 'program files'])
|
||||||
|
|
||||||
def _default_state_for_path(self, path):
|
def _default_state_for_path(self, path):
|
||||||
result = DirectoriesBase._default_state_for_path(self, path)
|
result = DirectoriesBase._default_state_for_path(self, path)
|
||||||
if result is not None:
|
if result is not None:
|
||||||
@@ -30,16 +31,16 @@ class DupeGuru(DupeGuruBase):
|
|||||||
EDITION = 'se'
|
EDITION = 'se'
|
||||||
LOGO_NAME = 'logo_se'
|
LOGO_NAME = 'logo_se'
|
||||||
NAME = __appname__
|
NAME = __appname__
|
||||||
|
|
||||||
DETAILS_DIALOG_CLASS = DetailsDialog
|
DETAILS_DIALOG_CLASS = DetailsDialog
|
||||||
RESULT_MODEL_CLASS = ResultsModel
|
RESULT_MODEL_CLASS = ResultsModel
|
||||||
PREFERENCES_CLASS = Preferences
|
PREFERENCES_CLASS = Preferences
|
||||||
PREFERENCES_DIALOG_CLASS = PreferencesDialog
|
PREFERENCES_DIALOG_CLASS = PreferencesDialog
|
||||||
|
|
||||||
def _setup(self):
|
def _setup(self):
|
||||||
self.directories = Directories()
|
self.directories = Directories()
|
||||||
DupeGuruBase._setup(self)
|
DupeGuruBase._setup(self)
|
||||||
|
|
||||||
def _update_options(self):
|
def _update_options(self):
|
||||||
DupeGuruBase._update_options(self)
|
DupeGuruBase._update_options(self)
|
||||||
self.model.scanner.min_match_percentage = self.prefs.filter_hardness
|
self.model.scanner.min_match_percentage = self.prefs.filter_hardness
|
||||||
@@ -48,4 +49,4 @@ class DupeGuru(DupeGuruBase):
|
|||||||
self.model.scanner.match_similar_words = self.prefs.match_similar
|
self.model.scanner.match_similar_words = self.prefs.match_similar
|
||||||
threshold = self.prefs.small_file_threshold if self.prefs.ignore_small_files else 0
|
threshold = self.prefs.small_file_threshold if self.prefs.ignore_small_files else 0
|
||||||
self.model.scanner.size_threshold = threshold * 1024 # threshold is in KB. the scanner wants bytes
|
self.model.scanner.size_threshold = threshold * 1024 # threshold is in KB. the scanner wants bytes
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2009-05-24
|
# Created On: 2009-05-24
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from PyQt5.QtCore import QSize
|
from PyQt5.QtCore import QSize
|
||||||
from PyQt5.QtWidgets import (QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QSpacerItem, QWidget,
|
from PyQt5.QtWidgets import (
|
||||||
QLineEdit, QApplication)
|
QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QSpacerItem, QWidget,
|
||||||
|
QLineEdit, QApplication
|
||||||
|
)
|
||||||
|
|
||||||
from hscommon.plat import ISWINDOWS, ISLINUX
|
from hscommon.plat import ISWINDOWS, ISLINUX
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
@@ -31,9 +33,9 @@ SCAN_TYPE_ORDER = [
|
|||||||
class PreferencesDialog(PreferencesDialogBase):
|
class PreferencesDialog(PreferencesDialogBase):
|
||||||
def __init__(self, parent, app, **kwargs):
|
def __init__(self, parent, app, **kwargs):
|
||||||
super().__init__(parent, app, **kwargs)
|
super().__init__(parent, app, **kwargs)
|
||||||
|
|
||||||
self.scanTypeComboBox.currentIndexChanged[int].connect(self.scanTypeChanged)
|
self.scanTypeComboBox.currentIndexChanged[int].connect(self.scanTypeChanged)
|
||||||
|
|
||||||
def _setupPreferenceWidgets(self):
|
def _setupPreferenceWidgets(self):
|
||||||
scanTypeLabels = [
|
scanTypeLabels = [
|
||||||
tr("Filename"),
|
tr("Filename"),
|
||||||
@@ -73,23 +75,26 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
spacerItem1 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
|
spacerItem1 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
|
||||||
self.horizontalLayout_2.addItem(spacerItem1)
|
self.horizontalLayout_2.addItem(spacerItem1)
|
||||||
self.verticalLayout_4.addLayout(self.horizontalLayout_2)
|
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.verticalLayout_4.addWidget(self.ignoreHardlinkMatches)
|
||||||
self._setupAddCheckbox('debugModeBox', tr("Debug mode (restart required)"), self.widget)
|
self._setupAddCheckbox('debugModeBox', tr("Debug mode (restart required)"), self.widget)
|
||||||
self.verticalLayout_4.addWidget(self.debugModeBox)
|
self.verticalLayout_4.addWidget(self.debugModeBox)
|
||||||
self.widgetsVLayout.addWidget(self.widget)
|
self.widgetsVLayout.addWidget(self.widget)
|
||||||
self._setupBottomPart()
|
self._setupBottomPart()
|
||||||
|
|
||||||
def _setupUi(self):
|
def _setupUi(self):
|
||||||
PreferencesDialogBase._setupUi(self)
|
PreferencesDialogBase._setupUi(self)
|
||||||
|
|
||||||
if ISLINUX:
|
if ISLINUX:
|
||||||
# Under linux, whether it's a Qt layout bug or something else, the size threshold text edit
|
# Under linux, whether it's a Qt layout bug or something else, the size threshold text edit
|
||||||
# doesn't have enough space, so we make the pref pane higher to compensate.
|
# doesn't have enough space, so we make the pref pane higher to compensate.
|
||||||
self.resize(self.width(), 530)
|
self.resize(self.width(), 530)
|
||||||
elif ISWINDOWS:
|
elif ISWINDOWS:
|
||||||
self.resize(self.width(), 440)
|
self.resize(self.width(), 440)
|
||||||
|
|
||||||
def _load(self, prefs, setchecked):
|
def _load(self, prefs, setchecked):
|
||||||
scan_type_index = SCAN_TYPE_ORDER.index(prefs.scan_type)
|
scan_type_index = SCAN_TYPE_ORDER.index(prefs.scan_type)
|
||||||
self.scanTypeComboBox.setCurrentIndex(scan_type_index)
|
self.scanTypeComboBox.setCurrentIndex(scan_type_index)
|
||||||
@@ -97,17 +102,17 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
setchecked(self.wordWeightingBox, prefs.word_weighting)
|
setchecked(self.wordWeightingBox, prefs.word_weighting)
|
||||||
setchecked(self.ignoreSmallFilesBox, prefs.ignore_small_files)
|
setchecked(self.ignoreSmallFilesBox, prefs.ignore_small_files)
|
||||||
self.sizeThresholdEdit.setText(str(prefs.small_file_threshold))
|
self.sizeThresholdEdit.setText(str(prefs.small_file_threshold))
|
||||||
|
|
||||||
def _save(self, prefs, ischecked):
|
def _save(self, prefs, ischecked):
|
||||||
prefs.scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()]
|
prefs.scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()]
|
||||||
prefs.match_similar = ischecked(self.matchSimilarBox)
|
prefs.match_similar = ischecked(self.matchSimilarBox)
|
||||||
prefs.word_weighting = ischecked(self.wordWeightingBox)
|
prefs.word_weighting = ischecked(self.wordWeightingBox)
|
||||||
prefs.ignore_small_files = ischecked(self.ignoreSmallFilesBox)
|
prefs.ignore_small_files = ischecked(self.ignoreSmallFilesBox)
|
||||||
prefs.small_file_threshold = tryint(self.sizeThresholdEdit.text())
|
prefs.small_file_threshold = tryint(self.sizeThresholdEdit.text())
|
||||||
|
|
||||||
def resetToDefaults(self):
|
def resetToDefaults(self):
|
||||||
self.load(preferences.Preferences())
|
self.load(preferences.Preferences())
|
||||||
|
|
||||||
#--- Events
|
#--- Events
|
||||||
def scanTypeChanged(self, index):
|
def scanTypeChanged(self, index):
|
||||||
scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()]
|
scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()]
|
||||||
@@ -115,7 +120,7 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
self.filterHardnessSlider.setEnabled(word_based)
|
self.filterHardnessSlider.setEnabled(word_based)
|
||||||
self.matchSimilarBox.setEnabled(word_based)
|
self.matchSimilarBox.setEnabled(word_based)
|
||||||
self.wordWeightingBox.setEnabled(word_based)
|
self.wordWeightingBox.setEnabled(word_based)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
from ..testapp import TestApp
|
from ..testapp import TestApp
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Created On: 2011-11-27
|
# Created On: 2011-11-27
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from qtlib.column import Column
|
from qtlib.column import Column
|
||||||
@@ -19,4 +19,5 @@ class ResultsModel(ResultsModelBase):
|
|||||||
Column('percentage', defaultWidth=60),
|
Column('percentage', defaultWidth=60),
|
||||||
Column('words', defaultWidth=120),
|
Column('words', defaultWidth=120),
|
||||||
Column('dupe_count', defaultWidth=80),
|
Column('dupe_count', defaultWidth=80),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 398 B |
@@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2011-09-06
|
# Created On: 2011-09-06
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, QAbstractListModel, QItemSelection, QItemSelectionModel
|
from PyQt5.QtCore import Qt, QAbstractListModel, QItemSelection, QItemSelectionModel
|
||||||
@@ -16,7 +16,7 @@ class SelectableList(QAbstractListModel):
|
|||||||
self.model = model
|
self.model = model
|
||||||
self.view.setModel(self)
|
self.view.setModel(self)
|
||||||
self.model.view = self
|
self.model.view = self
|
||||||
|
|
||||||
#--- Override
|
#--- Override
|
||||||
def data(self, index, role):
|
def data(self, index, role):
|
||||||
if not index.isValid():
|
if not index.isValid():
|
||||||
@@ -25,26 +25,27 @@ class SelectableList(QAbstractListModel):
|
|||||||
if role in {Qt.DisplayRole, Qt.EditRole}:
|
if role in {Qt.DisplayRole, Qt.EditRole}:
|
||||||
return self.model[index.row()]
|
return self.model[index.row()]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def rowCount(self, index):
|
def rowCount(self, index):
|
||||||
if index.isValid():
|
if index.isValid():
|
||||||
return 0
|
return 0
|
||||||
return len(self.model)
|
return len(self.model)
|
||||||
|
|
||||||
#--- Virtual
|
#--- Virtual
|
||||||
def _updateSelection(self):
|
def _updateSelection(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def _restoreSelection(self):
|
def _restoreSelection(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
#--- model --> view
|
#--- model --> view
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
self._updating = True
|
self._updating = True
|
||||||
self.reset()
|
self.beginResetModel()
|
||||||
|
self.endResetModel()
|
||||||
self._updating = False
|
self._updating = False
|
||||||
self._restoreSelection()
|
self._restoreSelection()
|
||||||
|
|
||||||
def update_selection(self):
|
def update_selection(self):
|
||||||
self._restoreSelection()
|
self._restoreSelection()
|
||||||
|
|
||||||
@@ -52,18 +53,18 @@ class ComboboxModel(SelectableList):
|
|||||||
def __init__(self, model, view, **kwargs):
|
def __init__(self, model, view, **kwargs):
|
||||||
super().__init__(model, view, **kwargs)
|
super().__init__(model, view, **kwargs)
|
||||||
self.view.currentIndexChanged[int].connect(self.selectionChanged)
|
self.view.currentIndexChanged[int].connect(self.selectionChanged)
|
||||||
|
|
||||||
#--- Override
|
#--- Override
|
||||||
def _updateSelection(self):
|
def _updateSelection(self):
|
||||||
index = self.view.currentIndex()
|
index = self.view.currentIndex()
|
||||||
if index != self.model.selected_index:
|
if index != self.model.selected_index:
|
||||||
self.model.select(index)
|
self.model.select(index)
|
||||||
|
|
||||||
def _restoreSelection(self):
|
def _restoreSelection(self):
|
||||||
index = self.model.selected_index
|
index = self.model.selected_index
|
||||||
if index is not None:
|
if index is not None:
|
||||||
self.view.setCurrentIndex(index)
|
self.view.setCurrentIndex(index)
|
||||||
|
|
||||||
#--- Events
|
#--- Events
|
||||||
def selectionChanged(self, index):
|
def selectionChanged(self, index):
|
||||||
if not self._updating:
|
if not self._updating:
|
||||||
@@ -74,13 +75,13 @@ class ListviewModel(SelectableList):
|
|||||||
super().__init__(model, view, **kwargs)
|
super().__init__(model, view, **kwargs)
|
||||||
self.view.selectionModel().selectionChanged[(QItemSelection, QItemSelection)].connect(
|
self.view.selectionModel().selectionChanged[(QItemSelection, QItemSelection)].connect(
|
||||||
self.selectionChanged)
|
self.selectionChanged)
|
||||||
|
|
||||||
#--- Override
|
#--- Override
|
||||||
def _updateSelection(self):
|
def _updateSelection(self):
|
||||||
newIndexes = [modelIndex.row() for modelIndex in self.view.selectionModel().selectedRows()]
|
newIndexes = [modelIndex.row() for modelIndex in self.view.selectionModel().selectedRows()]
|
||||||
if newIndexes != self.model.selected_indexes:
|
if newIndexes != self.model.selected_indexes:
|
||||||
self.model.select(newIndexes)
|
self.model.select(newIndexes)
|
||||||
|
|
||||||
def _restoreSelection(self):
|
def _restoreSelection(self):
|
||||||
newSelection = QItemSelection()
|
newSelection = QItemSelection()
|
||||||
for index in self.model.selected_indexes:
|
for index in self.model.selected_indexes:
|
||||||
@@ -94,4 +95,4 @@ class ListviewModel(SelectableList):
|
|||||||
def selectionChanged(self, index):
|
def selectionChanged(self, index):
|
||||||
if not self._updating:
|
if not self._updating:
|
||||||
self._updateSelection()
|
self._updateSelection()
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
pytest>=2.0.0
|
pytest>=2.0.0
|
||||||
pytest-monkeyplus>=1.0.0
|
pytest-monkeyplus>=1.0.0
|
||||||
|
flake8
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
objp>=1.2.0
|
objp>=1.3.1
|
||||||
appscript>=1.0.0
|
appscript>=1.0.0
|
||||||
xibless>=0.4.1
|
xibless>=0.4.1
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
jobprogress>=1.0.4
|
|
||||||
Send2Trash>=1.3.0
|
Send2Trash>=1.3.0
|
||||||
sphinx>=1.2.2
|
sphinx>=1.2.2
|
||||||
polib>=1.0.4
|
polib>=1.0.4
|
||||||
|
|||||||