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

Compare commits

..

33 Commits

Author SHA1 Message Date
Virgil Dupras
f21804c769 Fixed typo in changelog. 2010-08-18 08:56:22 +02:00
Virgil Dupras
4bc05a8d46 dgse v2.11.0 was delayed. 2010-08-18 07:59:26 +02:00
Virgil Dupras
eebe2b0e80 Fixed debian packaging some more. 2010-08-18 07:55:01 +02:00
Virgil Dupras
250a496a78 Fixed debian packaging for SE under Python 3. 2010-08-17 07:26:46 -07:00
Virgil Dupras
29163ed053 se v2.11.0 2010-08-17 11:32:20 +02:00
Virgil Dupras
cc05661f9e Qt: Fixed packaging which didn't work under py3k. 2010-08-17 09:27:38 +01:00
Virgil Dupras
89409c22d1 Removed dependencies on PIL. Man, I wish I had known about QImageReader sooner... That was a little stupid on my part not to look further than QImage. 2010-08-17 09:38:58 +02:00
Virgil Dupras
e2f240ebc9 Prettified the build system by getting rid of those "gen.py" files and hardcoded "python3" calls. Also, ported Qt's block.c to Python3, which hadn't been done yet. 2010-08-17 09:30:25 +02:00
Virgil Dupras
8d56f4c33b Fixed broken test. 2010-08-15 15:09:40 +02:00
Virgil Dupras
36eccb7122 Removed the "all files are refs" error message and made the "no files, can't scan" message quicker. That's because when scanning iPhoto libraries with big libraries, the GUI would hang because these checks would involve loading the whole library. 2010-08-15 15:07:44 +02:00
Virgil Dupras
c8827769b4 Removed dependency on lxml (it made the final package much bigger, and building it on windows is not fun). 2010-08-15 14:42:55 +02:00
Virgil Dupras
12e6c400b9 Fixes here and there to make dupeGuru PE run. 2010-08-15 14:23:16 +02:00
Virgil Dupras
4c273a7910 [#102 state:fixed] Remember the size/position of all window between launches. 2010-08-15 12:27:15 +02:00
Virgil Dupras
58da335b17 Enum-ified Scan Type constants, looks nicer. 2010-08-14 19:52:23 +02:00
Virgil Dupras
5b2d506462 [#15 state:fixed] Improved tie breaker in cases where filenames end with digits inside brackets. 2010-08-14 19:32:09 +02:00
Virgil Dupras
531430d44a Updated dependencies in the README file. 2010-08-14 13:20:57 +02:00
Virgil Dupras
7450eec7eb Added Load/Save Results menu items, allowing to save results at arbitrary places. 2010-08-13 13:06:18 +02:00
Virgil Dupras
3a5802435f Only save results on quit if the results are actually modified. 2010-08-13 11:48:05 +02:00
Virgil Dupras
1b6b058097 Added a is_modified flag to Results. 2010-08-13 11:37:45 +02:00
Virgil Dupras
a5797a2350 Semi-pytest-ified results_test. 2010-08-13 09:48:37 +02:00
Virgil Dupras
e81a5147c5 Adjusted details panel's eight in SE. 2010-08-13 09:32:05 +02:00
Virgil Dupras
565c990687 [#101 state:fixed] Remove the Creation Time column. 2010-08-13 09:26:38 +02:00
Virgil Dupras
0ccdfe0e26 Adjusted xcode project to registration.xib move due to localization. 2010-08-12 17:29:22 +02:00
Virgil Dupras
f8a558e3a7 Updated cocoalib subrepo. 2010-08-12 17:24:54 +02:00
Virgil Dupras
c5fa195cc6 [#103 state:fixed] Correctly hide progress dialog when a job was completed while dupeGuru was inactive. 2010-08-12 17:21:01 +02:00
Virgil Dupras
3a821edd45 Results loading now takes place in one shot (file locate and metadata read). It makes weeding out the bad files more convenient and fixes the Cancel loading glitch where we end up with "ghost" results. 2010-08-12 15:57:47 +02:00
Virgil Dupras
854d194f88 Converted to py3k. There's probably some bugs still. So far, I managed to run dupeGuru SE under pyobjc and qt. 2010-08-11 16:39:06 +02:00
Virgil Dupras
fb79daad6a Added tag before-py3k for changeset 634b66415c65 2010-08-11 16:15:32 +02:00
Virgil Dupras
b2ae0e8759 Added tag pe1.9.1 for changeset 724ff565dd78 2010-07-17 10:33:19 +02:00
Virgil Dupras
09f73988b3 pe v1.9.1 2010-07-17 07:14:39 +02:00
Virgil Dupras
9e6f289319 Added tag me5.8.1 for changeset 3742e83edd9e 2010-07-16 10:04:28 +02:00
Virgil Dupras
d2a55ffd31 me v5.8.1 2010-07-16 08:53:43 +02:00
Virgil Dupras
793c2aa423 Added tag se2.10.1 for changeset f71d405e62ba 2010-07-15 09:21:19 +02:00
103 changed files with 1933 additions and 967 deletions

View File

@@ -19,3 +19,8 @@ cbcf9c80fee4c908ef2efbf1c143c9e47676c9b2 pe1.8.0
388a7e5aef6385e515189f4a15b4c4fed3ae2fcf me5.8.0
27501167e3b9262ecb60c967941294f36d77eb25 pe1.9.0
cb0a860430bacd712820bce426bcf47a4135efe1 se2.10.1
cb0a860430bacd712820bce426bcf47a4135efe1 se2.10.1
f71d405e62badcfdc1b037facaac043cece40ee5 se2.10.1
3742e83edd9eadf44e1a501859f5e2462b1ef6fd me5.8.1
724ff565dd785fb739774588c6ee652cfc0612d5 pe1.9.1
634b66415c6529f46ae4f837318027cc9d70c3b5 before-py3k

18
README
View File

@@ -25,12 +25,11 @@ Before being able to build dupeGuru, a few dependencies have to be installed:
General dependencies
-----
- Python 2.6 (http://www.python.org)
- Send2Trash (http://hg.hardcoded.net/send2trash)
- hsutil (http://hg.hardcoded.net/hsutil)
- hsaudiotag (for ME) (http://hg.hardcoded.net/hsaudiotag)
- lxml, to read and write XML files. (http://codespeak.net/lxml/)
- Mako, to generate help files. (http://www.makotemplates.org/)
- Python 3.1 (http://www.python.org)
- Send2Trash3k (http://hg.hardcoded.net/send2trash3k)
- hsutil3k (http://hg.hardcoded.net/hsutil3k)
- hsaudiotag3k (for ME) (http://hg.hardcoded.net/hsaudiotag3k)
- Markdown, to generate help files. (http://pypi.python.org/pypi/Markdown)
- PyYaml, for help files and the build system. (http://pyyaml.org/)
- py.test, to run unit tests. (http://codespeak.net/py/dist/test/)
@@ -39,15 +38,14 @@ OS X prerequisites
- XCode 3.1 (http://developer.apple.com/TOOLS/xcode/)
- Sparkle (http://sparkle.andymatuschak.org/)
- PyObjC 2.2. (http://pyobjc.sourceforge.net/)
- py2app (http://svn.pythonmac.org/py2app/py2app/trunk/doc/index.html)
- PyObjC 2.3. (http://pyobjc.sourceforge.net/)
- py2app 0.5.4 (http://svn.pythonmac.org/py2app/py2app/trunk/doc/index.html)
Windows prerequisites
---
- Visual Studio 2008 (Express is enough) is needed to build C extensions. (http://www.microsoft.com/Express/)
- PyQt 4.6 (http://www.riverbankcomputing.co.uk/news)
- Python Imaging Library for dupeGuru PE. (http://www.pythonware.com/products/pil/)
- PyQt 4.7 (http://www.riverbankcomputing.co.uk/news)
- cx_Freeze, if you want to build a exe. You don't need it if you just want to run dupeGuru. (http://cx-freeze.sourceforge.net/)
- Advanced Installer, if you want to build the installer file. (http://www.advancedinstaller.com/)

View File

@@ -13,6 +13,7 @@ import os.path as op
import shutil
from setuptools import setup
from distutils.extension import Extension
import yaml
from hscommon import helpgen
@@ -20,10 +21,10 @@ from hscommon.build import add_to_pythonpath, print_and_do, build_all_qt_ui, cop
def build_cocoa(edition, dev, help_destpath):
if not dev:
print "Building help index"
print("Building help index")
os.system('open -a /Developer/Applications/Utilities/Help\\ Indexer.app {0}'.format(help_destpath))
print "Building dg_cocoa.plugin"
print("Building dg_cocoa.plugin")
if op.exists('build'):
shutil.rmtree('build')
os.mkdir('build')
@@ -54,7 +55,7 @@ def build_cocoa(edition, dev, help_destpath):
pthpath = op.join(pluginpath, 'Contents/Resources/dev.pth')
open(pthpath, 'w').write(op.abspath('.'))
os.chdir(cocoa_project_path)
print "Building the XCode project"
print("Building the XCode project")
args = []
if dev:
args.append('-configuration dev')
@@ -65,25 +66,58 @@ def build_cocoa(edition, dev, help_destpath):
os.chdir('..')
def build_qt(edition, dev):
print("Building Qt stuff")
build_all_qt_ui(op.join('qtlib', 'ui'))
build_all_qt_ui(op.join('qt', 'base'))
build_all_qt_ui(op.join('qt', edition))
print_and_do("pyrcc4 {0} > {1}".format(op.join('qt', 'base', 'dg.qrc'), op.join('qt', 'base', 'dg_rc.py')))
if edition == 'pe':
os.chdir(op.join('qt', edition))
os.system('python gen.py')
os.chdir(op.join('..', '..'))
print_and_do("pyrcc4 -py3 {0} > {1}".format(op.join('qt', 'base', 'dg.qrc'), op.join('qt', 'base', 'dg_rc.py')))
def build_pe_modules(ui):
def move(src, dst):
if not op.exists(src):
return
if op.exists(dst):
os.remove(dst)
print('Moving %s --> %s' % (src, dst))
os.rename(src, dst)
print("Building PE Modules")
exts = [
Extension("_block", [op.join('core_pe', 'modules', 'block.c'), op.join('core_pe', 'modules', 'common.c')]),
Extension("_cache", [op.join('core_pe', 'modules', 'cache.c'), op.join('core_pe', 'modules', 'common.c')]),
]
if ui == 'qt':
exts.append(Extension("_block_qt", [op.join('qt', 'pe', 'modules', 'block.c')]))
elif ui == 'cocoa':
exts.append(Extension(
"_block_osx", [op.join('core_pe', 'modules', 'block_osx.m'), op.join('core_pe', 'modules', 'common.c')],
extra_link_args=[
"-framework", "CoreFoundation",
"-framework", "Foundation",
"-framework", "ApplicationServices",]
))
setup(
script_args = ['build_ext', '--inplace'],
ext_modules = exts,
)
move('_block.so', op.join('core_pe', '_block.so'))
move('_block.pyd', op.join('core_pe', '_block.pyd'))
move('_block_osx.so', op.join('core_pe', '_block_osx.so'))
move('_cache.so', op.join('core_pe', '_cache.so'))
move('_cache.pyd', op.join('core_pe', '_cache.pyd'))
move('_block_qt.so', op.join('qt', 'pe', '_block.so'))
move('_block_qt.pyd', op.join('qt', 'pe', '_block.pyd'))
def main():
conf = yaml.load(open('conf.yaml'))
edition = conf['edition']
ui = conf['ui']
dev = conf['dev']
print "Building dupeGuru {0} with UI {1}".format(edition.upper(), ui)
print("Building dupeGuru {0} with UI {1}".format(edition.upper(), ui))
if dev:
print "Building in Dev mode"
print("Building in Dev mode")
add_to_pythonpath('.')
print "Generating Help"
print("Generating Help")
windows = sys.platform == 'win32'
profile = 'win_en' if windows else 'osx_en'
help_dir = 'help_{0}'.format(edition)
@@ -91,11 +125,9 @@ def main():
help_basepath = op.abspath(help_dir)
help_destpath = op.abspath(op.join(help_dir, dest_dir))
helpgen.gen(help_basepath, help_destpath, profile=profile)
print "Building dupeGuru"
print("Building dupeGuru")
if edition == 'pe':
os.chdir('core_pe')
os.system('python gen.py')
os.chdir('..')
build_pe_modules(ui)
if ui == 'cocoa':
build_cocoa(edition, dev, help_destpath)
elif ui == 'qt':

View File

@@ -14,7 +14,9 @@ http://www.hardcoded.net/licenses/hs_license
- (NSNumber *)addDirectory:(NSString *)name;
- (void)removeDirectory:(NSNumber *)index;
- (void)loadResults;
- (void)loadResultsFrom:(NSString *)filename;
- (void)saveResults;
- (void)saveResultsAs:(NSString *)filename;
- (void)loadIgnoreList;
- (void)saveIgnoreList;
- (void)clearIgnoreList;

View File

@@ -49,6 +49,7 @@ http://www.hardcoded.net/licenses/hs_license
- (IBAction)filter:(id)sender;
- (IBAction)ignoreSelected:(id)sender;
- (IBAction)invokeCustomCommand:(id)sender;
- (IBAction)loadResults:(id)sender;
- (IBAction)markAll:(id)sender;
- (IBAction)markInvert:(id)sender;
- (IBAction)markNone:(id)sender;
@@ -61,6 +62,7 @@ http://www.hardcoded.net/licenses/hs_license
- (IBAction)renameSelected:(id)sender;
- (IBAction)resetColumnsToDefault:(id)sender;
- (IBAction)revealSelected:(id)sender;
- (IBAction)saveResults:(id)sender;
- (IBAction)showPreferencesPanel:(id)sender;
- (IBAction)startDuplicateScan:(id)sender;
- (IBAction)switchSelected:(id)sender;

View File

@@ -212,6 +212,21 @@ http://www.hardcoded.net/licenses/hs_license
}
}
- (IBAction)loadResults:(id)sender
{
NSOpenPanel *op = [NSOpenPanel openPanel];
[op setCanChooseFiles:YES];
[op setCanChooseDirectories:NO];
[op setCanCreateDirectories:NO];
[op setAllowsMultipleSelection:NO];
[op setAllowedFileTypes:[NSArray arrayWithObject:@"dupeguru"]];
[op setTitle:@"Select a results file to load"];
if ([op runModal] == NSOKButton) {
NSString *filename = [[op filenames] objectAtIndex:0];
[py loadResultsFrom:filename];
}
}
- (IBAction)markAll:(id)sender
{
[py markAll];
@@ -303,6 +318,17 @@ http://www.hardcoded.net/licenses/hs_license
[preferencesPanel showWindow:sender];
}
- (IBAction)saveResults:(id)sender
{
NSSavePanel *sp = [NSSavePanel savePanel];
[sp setCanCreateDirectories:YES];
[sp setAllowedFileTypes:[NSArray arrayWithObject:@"dupeguru"]];
[sp setTitle:@"Select a file to save your results to"];
if ([sp runModal] == NSOKButton) {
[py saveResultsAs:[sp filename]];
}
}
- (IBAction)startDuplicateScan:(id)sender
{
// Virtual

View File

@@ -2,13 +2,13 @@
<archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="7.10">
<data>
<int key="IBDocument.SystemTarget">1050</int>
<string key="IBDocument.SystemVersion">10C540</string>
<string key="IBDocument.InterfaceBuilderVersion">740</string>
<string key="IBDocument.AppKitVersion">1038.25</string>
<string key="IBDocument.HIToolboxVersion">458.00</string>
<string key="IBDocument.SystemVersion">10F569</string>
<string key="IBDocument.InterfaceBuilderVersion">788</string>
<string key="IBDocument.AppKitVersion">1038.29</string>
<string key="IBDocument.HIToolboxVersion">461.00</string>
<object class="NSMutableDictionary" key="IBDocument.PluginVersions">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="NS.object.0">740</string>
<string key="NS.object.0">788</string>
</object>
<object class="NSMutableArray" key="IBDocument.EditedObjectIDs">
<bool key="EncodedWithXMLCoder">YES</bool>
@@ -41,7 +41,7 @@
<object class="NSWindowTemplate" id="476890502">
<int key="NSWindowStyleMask">155</int>
<int key="NSWindowBacking">2</int>
<string key="NSWindowRect">{{33, 261}, {451, 161}}</string>
<string key="NSWindowRect">{{33, 261}, {451, 146}}</string>
<int key="NSWTFlags">-260571136</int>
<string key="NSWindowTitle">Details of Selected File</string>
<object class="NSMutableString" key="NSWindowClass">
@@ -51,7 +51,7 @@
<characters key="NS.bytes">View</characters>
</object>
<string key="NSWindowContentMaxSize">{1.79769e+308, 1.79769e+308}</string>
<string key="NSWindowContentMinSize">{451, 161}</string>
<string key="NSWindowContentMinSize">{451, 146}</string>
<object class="NSView" key="NSWindowView" id="1027711962">
<reference key="NSNextResponder"/>
<int key="NSvFlags">256</int>
@@ -70,7 +70,7 @@
<object class="NSTableView" id="251969872">
<reference key="NSNextResponder" ref="488480549"/>
<int key="NSvFlags">256</int>
<string key="NSFrameSize">{449, 143}</string>
<string key="NSFrameSize">{449, 128}</string>
<reference key="NSSuperview" ref="488480549"/>
<int key="NSTag">2</int>
<bool key="NSEnabled">YES</bool>
@@ -224,7 +224,7 @@
<int key="NSTableViewDraggingDestinationStyle">0</int>
</object>
</object>
<string key="NSFrame">{{1, 17}, {449, 143}}</string>
<string key="NSFrame">{{1, 17}, {449, 128}}</string>
<reference key="NSSuperview" ref="362108788"/>
<reference key="NSNextKeyView" ref="251969872"/>
<reference key="NSDocView" ref="251969872"/>
@@ -266,7 +266,7 @@
</object>
<reference ref="717380566"/>
</object>
<string key="NSFrameSize">{451, 161}</string>
<string key="NSFrameSize">{451, 146}</string>
<reference key="NSSuperview" ref="1027711962"/>
<reference key="NSNextKeyView" ref="488480549"/>
<int key="NSsFlags">530</int>
@@ -278,12 +278,13 @@
<bytes key="NSScrollAmts">QSAAAEEgAABBgAAAQYAAAA</bytes>
</object>
</object>
<string key="NSFrameSize">{451, 161}</string>
<string key="NSFrameSize">{451, 146}</string>
<reference key="NSSuperview"/>
</object>
<string key="NSScreenRect">{{0, 0}, {1024, 746}}</string>
<string key="NSMinSize">{451, 177}</string>
<string key="NSMinSize">{451, 162}</string>
<string key="NSMaxSize">{1.79769e+308, 1.79769e+308}</string>
<string key="NSFrameAutosaveName">DetailsPanel</string>
</object>
</object>
<object class="IBObjectContainer" key="IBDocument.Objects">
@@ -497,12 +498,12 @@
<boolean value="YES"/>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<boolean value="YES"/>
<string>{{109, 656}, {451, 161}}</string>
<string>{{109, 671}, {451, 146}}</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>{{109, 656}, {451, 161}}</string>
<string>{{109, 671}, {451, 146}}</string>
<boolean value="YES"/>
<boolean value="YES"/>
<string>{451, 161}</string>
<string>{451, 146}</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<boolean value="YES"/>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
@@ -536,11 +537,18 @@
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBPartialClassDescription">
<string key="className">DetailsPanel</string>
<string key="superclassName">NSWindowController</string>
<string key="superclassName">HSWindowController</string>
<object class="NSMutableDictionary" key="outlets">
<string key="NS.key.0">detailsTable</string>
<string key="NS.object.0">NSTableView</string>
</object>
<object class="NSMutableDictionary" key="toOneOutletInfosByName">
<string key="NS.key.0">detailsTable</string>
<object class="IBToOneOutletInfo" key="NS.object.0">
<string key="name">detailsTable</string>
<string key="candidateClassName">NSTableView</string>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">../base/DetailsPanel.h</string>
@@ -548,7 +556,7 @@
</object>
<object class="IBPartialClassDescription">
<string key="className">DetailsPanel</string>
<string key="superclassName">NSWindowController</string>
<string key="superclassName">HSWindowController</string>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBUserSource</string>
<string key="minorKey"/>
@@ -562,6 +570,32 @@
<string key="minorKey"/>
</object>
</object>
<object class="IBPartialClassDescription">
<string key="className">HSWindowController</string>
<string key="superclassName">NSWindowController</string>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">../controllers/HSWindowController.h</string>
</object>
</object>
<object class="IBPartialClassDescription">
<string key="className">NSObject</string>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">../views/HSOutlineView.h</string>
</object>
</object>
<object class="IBPartialClassDescription">
<string key="className">NSObject</string>
<object class="IBClassDescriptionSource" key="sourceIdentifier" id="111130406">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">../views/NSTableViewAdditions.h</string>
</object>
</object>
<object class="IBPartialClassDescription">
<string key="className">NSTableView</string>
<reference key="sourceIdentifier" ref="111130406"/>
</object>
</object>
<object class="NSMutableArray" key="referencedPartialClassDescriptionsV3.2+">
<bool key="EncodedWithXMLCoder">YES</bool>
@@ -1029,6 +1063,13 @@
<string key="NS.key.0">showWindow:</string>
<string key="NS.object.0">id</string>
</object>
<object class="NSMutableDictionary" key="actionInfosByName">
<string key="NS.key.0">showWindow:</string>
<object class="IBActionInfo" key="NS.object.0">
<string key="name">showWindow:</string>
<string key="candidateClassName">id</string>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBFrameworkSource</string>
<string key="minorKey">AppKit.framework/Headers/NSWindowController.h</string>
@@ -1037,6 +1078,7 @@
</object>
</object>
<int key="IBDocument.localizationMode">0</int>
<string key="IBDocument.TargetRuntimeIdentifier">IBCocoaFramework</string>
<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencies">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.macosx</string>
<integer value="1050" key="NS.object.0"/>

View File

@@ -2,13 +2,13 @@
<archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="7.10">
<data>
<int key="IBDocument.SystemTarget">1050</int>
<string key="IBDocument.SystemVersion">10C540</string>
<string key="IBDocument.InterfaceBuilderVersion">740</string>
<string key="IBDocument.AppKitVersion">1038.25</string>
<string key="IBDocument.HIToolboxVersion">458.00</string>
<string key="IBDocument.SystemVersion">10F569</string>
<string key="IBDocument.InterfaceBuilderVersion">788</string>
<string key="IBDocument.AppKitVersion">1038.29</string>
<string key="IBDocument.HIToolboxVersion">461.00</string>
<object class="NSMutableDictionary" key="IBDocument.PluginVersions">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="NS.object.0">740</string>
<string key="NS.object.0">788</string>
</object>
<object class="NSMutableArray" key="IBDocument.EditedObjectIDs">
<bool key="EncodedWithXMLCoder">YES</bool>
@@ -428,6 +428,7 @@
<string key="NSScreenRect">{{0, 0}, {1440, 878}}</string>
<string key="NSMinSize">{369, 291}</string>
<string key="NSMaxSize">{1.79769e+308, 1.79769e+308}</string>
<string key="NSFrameAutosaveName">DirectoryPanel</string>
</object>
</object>
<object class="IBObjectContainer" key="IBDocument.Objects">
@@ -869,6 +870,35 @@
<string>id</string>
</object>
</object>
<object class="NSMutableDictionary" key="actionInfosByName">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>askForDirectory:</string>
<string>popupAddDirectoryMenu:</string>
<string>removeSelectedDirectory:</string>
<string>toggleVisible:</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBActionInfo">
<string key="name">askForDirectory:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">popupAddDirectoryMenu:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">removeSelectedDirectory:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">toggleVisible:</string>
<string key="candidateClassName">id</string>
</object>
</object>
</object>
<object class="NSMutableDictionary" key="outlets">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
@@ -884,6 +914,30 @@
<string>NSButton</string>
</object>
</object>
<object class="NSMutableDictionary" key="toOneOutletInfosByName">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>addButtonPopUp</string>
<string>outlineView</string>
<string>removeButton</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBToOneOutletInfo">
<string key="name">addButtonPopUp</string>
<string key="candidateClassName">NSPopUpButton</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">outlineView</string>
<string key="candidateClassName">HSOutlineView</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">removeButton</string>
<string key="candidateClassName">NSButton</string>
</object>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">../base/DirectoryPanel.h</string>
@@ -1437,6 +1491,13 @@
<string key="NS.key.0">showWindow:</string>
<string key="NS.object.0">id</string>
</object>
<object class="NSMutableDictionary" key="actionInfosByName">
<string key="NS.key.0">showWindow:</string>
<object class="IBActionInfo" key="NS.object.0">
<string key="name">showWindow:</string>
<string key="candidateClassName">id</string>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBFrameworkSource</string>
<string key="minorKey">AppKit.framework/Headers/NSWindowController.h</string>
@@ -1445,6 +1506,7 @@
</object>
</object>
<int key="IBDocument.localizationMode">0</int>
<string key="IBDocument.TargetRuntimeIdentifier">IBCocoaFramework</string>
<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencies">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.macosx</string>
<integer value="1050" key="NS.object.0"/>
@@ -1460,5 +1522,18 @@
<bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool>
<string key="IBDocument.LastKnownRelativeProjectPath">../../se/dupeguru.xcodeproj</string>
<int key="IBDocument.defaultPropertyAccessControl">3</int>
<object class="NSMutableDictionary" key="IBDocument.LastKnownImageSizes">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>NSMenuCheckmark</string>
<string>NSMenuMixedState</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>{9, 8}</string>
<string>{7, 2}</string>
</object>
</object>
</data>
</archive>

View File

@@ -2,18 +2,17 @@
<archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="7.10">
<data>
<int key="IBDocument.SystemTarget">1050</int>
<string key="IBDocument.SystemVersion">10D573</string>
<string key="IBDocument.InterfaceBuilderVersion">740</string>
<string key="IBDocument.SystemVersion">10F569</string>
<string key="IBDocument.InterfaceBuilderVersion">788</string>
<string key="IBDocument.AppKitVersion">1038.29</string>
<string key="IBDocument.HIToolboxVersion">460.00</string>
<string key="IBDocument.HIToolboxVersion">461.00</string>
<object class="NSMutableDictionary" key="IBDocument.PluginVersions">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="NS.object.0">740</string>
<string key="NS.object.0">788</string>
</object>
<object class="NSMutableArray" key="IBDocument.EditedObjectIDs">
<bool key="EncodedWithXMLCoder">YES</bool>
<integer value="598"/>
<integer value="219"/>
<integer value="1204"/>
</object>
<object class="NSArray" key="IBDocument.PluginDependencies">
<bool key="EncodedWithXMLCoder">YES</bool>
@@ -83,9 +82,11 @@
<string key="NSToolbarItemPaletteLabel">Power Marker</string>
<nil key="NSToolbarItemToolTip"/>
<object class="NSSegmentedControl" key="NSToolbarItemView" id="35398541">
<nil key="NSNextResponder"/>
<reference key="NSNextResponder"/>
<int key="NSvFlags">256</int>
<string key="NSFrame">{{7, 14}, {67, 24}}</string>
<reference key="NSSuperview"/>
<reference key="NSWindow"/>
<bool key="NSEnabled">YES</bool>
<object class="NSSegmentedCell" key="NSCell" id="431579725">
<int key="NSCellFlags">67239424</int>
@@ -176,9 +177,11 @@
<string key="NSToolbarItemPaletteLabel">Filter</string>
<nil key="NSToolbarItemToolTip"/>
<object class="NSSearchField" key="NSToolbarItemView" id="1013657232">
<nil key="NSNextResponder"/>
<reference key="NSNextResponder"/>
<int key="NSvFlags">258</int>
<string key="NSFrame">{{0, 14}, {81, 22}}</string>
<reference key="NSSuperview"/>
<reference key="NSWindow"/>
<bool key="NSEnabled">YES</bool>
<object class="NSSearchFieldCell" key="NSCell" id="484816507">
<int key="NSCellFlags">343014976</int>
@@ -320,9 +323,11 @@
<string key="NSToolbarItemPaletteLabel">Action</string>
<nil key="NSToolbarItemToolTip"/>
<object class="NSPopUpButton" key="NSToolbarItemView" id="165812138">
<nil key="NSNextResponder"/>
<reference key="NSNextResponder"/>
<int key="NSvFlags">256</int>
<string key="NSFrame">{{0, 14}, {58, 26}}</string>
<reference key="NSSuperview"/>
<reference key="NSWindow"/>
<bool key="NSEnabled">YES</bool>
<object class="NSPopUpButtonCell" key="NSCell" id="436420677">
<int key="NSCellFlags">-2076049856</int>
@@ -525,9 +530,11 @@
<string key="NSToolbarItemPaletteLabel">Delta Values</string>
<nil key="NSToolbarItemToolTip"/>
<object class="NSSegmentedControl" key="NSToolbarItemView" id="311230297">
<nil key="NSNextResponder"/>
<reference key="NSNextResponder"/>
<int key="NSvFlags">256</int>
<string key="NSFrame">{{4, 14}, {67, 24}}</string>
<reference key="NSSuperview"/>
<reference key="NSWindow"/>
<bool key="NSEnabled">YES</bool>
<object class="NSSegmentedCell" key="NSCell" id="211272396">
<int key="NSCellFlags">67239424</int>
@@ -674,7 +681,7 @@
<string key="NSWindowContentMaxSize">{1.79769e+308, 1.79769e+308}</string>
<string key="NSWindowContentMinSize">{340, 340}</string>
<object class="NSView" key="NSWindowView" id="455829030">
<reference key="NSNextResponder"/>
<nil key="NSNextResponder"/>
<int key="NSvFlags">256</int>
<object class="NSMutableArray" key="NSSubviews">
<bool key="EncodedWithXMLCoder">YES</bool>
@@ -823,7 +830,6 @@
</object>
<string key="NSFrame">{{1, 17}, {515, 317}}</string>
<reference key="NSSuperview" ref="417210994"/>
<reference key="NSNextKeyView" ref="40047569"/>
<reference key="NSDocView" ref="40047569"/>
<reference key="NSBGColor" ref="91259834"/>
<int key="NScvFlags">4</int>
@@ -856,7 +862,6 @@
</object>
<string key="NSFrame">{{1, 0}, {515, 17}}</string>
<reference key="NSSuperview" ref="417210994"/>
<reference key="NSNextKeyView" ref="837301452"/>
<reference key="NSDocView" ref="837301452"/>
<reference key="NSBGColor" ref="91259834"/>
<int key="NScvFlags">4</int>
@@ -865,7 +870,6 @@
</object>
<string key="NSFrame">{{20, 45}, {517, 335}}</string>
<reference key="NSSuperview" ref="455829030"/>
<reference key="NSNextKeyView" ref="948758365"/>
<int key="NSsFlags">562</int>
<reference key="NSVScroller" ref="167459243"/>
<reference key="NSHScroller" ref="916628114"/>
@@ -897,7 +901,6 @@
</object>
</object>
<string key="NSFrameSize">{557, 400}</string>
<reference key="NSSuperview"/>
</object>
<string key="NSScreenRect">{{0, 0}, {1440, 878}}</string>
<string key="NSMinSize">{340, 418}</string>
@@ -1029,6 +1032,48 @@
<string key="NSName">_NSAppleMenu</string>
</object>
</object>
<object class="NSMenuItem" id="252491888">
<reference key="NSMenu" ref="133452984"/>
<string key="NSTitle">File</string>
<string key="NSKeyEquiv"/>
<int key="NSMnemonicLoc">2147483647</int>
<reference key="NSOnImage" ref="852972005"/>
<reference key="NSMixedImage" ref="218295580"/>
<string key="NSAction">submenuAction:</string>
<object class="NSMenu" key="NSSubmenu" id="948321368">
<string key="NSTitle">File</string>
<object class="NSMutableArray" key="NSMenuItems">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSMenuItem" id="777321316">
<reference key="NSMenu" ref="948321368"/>
<string key="NSTitle">Load Results...</string>
<string key="NSKeyEquiv">o</string>
<int key="NSKeyEquivModMask">1048576</int>
<int key="NSMnemonicLoc">2147483647</int>
<reference key="NSOnImage" ref="852972005"/>
<reference key="NSMixedImage" ref="218295580"/>
</object>
<object class="NSMenuItem" id="975401896">
<reference key="NSMenu" ref="948321368"/>
<string key="NSTitle">Save Results...</string>
<string key="NSKeyEquiv">s</string>
<int key="NSKeyEquivModMask">1048576</int>
<int key="NSMnemonicLoc">2147483647</int>
<reference key="NSOnImage" ref="852972005"/>
<reference key="NSMixedImage" ref="218295580"/>
</object>
<object class="NSMenuItem" id="630362403">
<reference key="NSMenu" ref="948321368"/>
<string key="NSTitle">Export Results to XHTML</string>
<string key="NSKeyEquiv">E</string>
<int key="NSKeyEquivModMask">1048576</int>
<int key="NSMnemonicLoc">2147483647</int>
<reference key="NSOnImage" ref="852972005"/>
<reference key="NSMixedImage" ref="218295580"/>
</object>
</object>
</object>
</object>
<object class="NSMenuItem" id="551331186">
<reference key="NSMenu" ref="133452984"/>
<string key="NSTitle">Edit</string>
@@ -1137,7 +1182,7 @@
<object class="NSMenuItem" id="1035429435">
<reference key="NSMenu" ref="600111647"/>
<string key="NSTitle">Start Duplicate Scan</string>
<string key="NSKeyEquiv">s</string>
<string key="NSKeyEquiv">d</string>
<int key="NSKeyEquivModMask">1048576</int>
<int key="NSMnemonicLoc">2147483647</int>
<reference key="NSOnImage" ref="852972005"/>
@@ -1152,15 +1197,6 @@
<reference key="NSOnImage" ref="852972005"/>
<reference key="NSMixedImage" ref="218295580"/>
</object>
<object class="NSMenuItem" id="630362403">
<reference key="NSMenu" ref="600111647"/>
<string key="NSTitle">Export Results to XHTML</string>
<string key="NSKeyEquiv">E</string>
<int key="NSKeyEquivModMask">1048576</int>
<int key="NSMnemonicLoc">2147483647</int>
<reference key="NSOnImage" ref="852972005"/>
<reference key="NSMixedImage" ref="218295580"/>
</object>
<object class="NSMenuItem" id="189815600">
<reference key="NSMenu" ref="600111647"/>
<bool key="NSIsDisabled">YES</bool>
@@ -2221,6 +2257,22 @@
</object>
<int key="connectionID">1178</int>
</object>
<object class="IBConnectionRecord">
<object class="IBActionConnection" key="connection">
<string key="label">saveResults:</string>
<reference key="source" ref="339936126"/>
<reference key="destination" ref="975401896"/>
</object>
<int key="connectionID">1207</int>
</object>
<object class="IBConnectionRecord">
<object class="IBActionConnection" key="connection">
<string key="label">loadResults:</string>
<reference key="source" ref="339936126"/>
<reference key="destination" ref="777321316"/>
</object>
<int key="connectionID">1208</int>
</object>
</object>
<object class="IBMutableOrderedSet" key="objectRecords">
<object class="NSArray" key="orderedObjects">
@@ -2336,6 +2388,7 @@
<reference ref="385797557"/>
<reference ref="958594216"/>
<reference ref="551331186"/>
<reference ref="252491888"/>
</object>
<reference key="parent" ref="0"/>
<string key="objectName">MainMenu</string>
@@ -2545,7 +2598,6 @@
<reference ref="578499792"/>
<reference ref="189815600"/>
<reference ref="564101661"/>
<reference ref="630362403"/>
<reference ref="747820446"/>
<reference ref="517397504"/>
</object>
@@ -2621,11 +2673,6 @@
<reference key="object" ref="564101661"/>
<reference key="parent" ref="600111647"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">947</int>
<reference key="object" ref="630362403"/>
<reference key="parent" ref="600111647"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">618</int>
<reference key="object" ref="385797557"/>
@@ -3089,6 +3136,41 @@
<reference key="object" ref="517397504"/>
<reference key="parent" ref="600111647"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">1203</int>
<reference key="object" ref="252491888"/>
<object class="NSMutableArray" key="children">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="948321368"/>
</object>
<reference key="parent" ref="133452984"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">1204</int>
<reference key="object" ref="948321368"/>
<object class="NSMutableArray" key="children">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="777321316"/>
<reference ref="975401896"/>
<reference ref="630362403"/>
</object>
<reference key="parent" ref="252491888"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">1205</int>
<reference key="object" ref="777321316"/>
<reference key="parent" ref="948321368"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">1206</int>
<reference key="object" ref="975401896"/>
<reference key="parent" ref="948321368"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">947</int>
<reference key="object" ref="630362403"/>
<reference key="parent" ref="948321368"/>
</object>
</object>
</object>
<object class="NSMutableDictionary" key="flattenedProperties">
@@ -3113,6 +3195,7 @@
<string>1028.ImportedFromIB2</string>
<string>103.IBPluginDependency</string>
<string>103.ImportedFromIB2</string>
<string>106.IBEditorWindowLastContentRect</string>
<string>106.IBPluginDependency</string>
<string>106.ImportedFromIB2</string>
<string>111.IBPluginDependency</string>
@@ -3141,6 +3224,11 @@
<string>1172.IBPluginDependency</string>
<string>1173.IBPluginDependency</string>
<string>1177.IBPluginDependency</string>
<string>1203.IBPluginDependency</string>
<string>1204.IBEditorWindowLastContentRect</string>
<string>1204.IBPluginDependency</string>
<string>1205.IBPluginDependency</string>
<string>1206.IBPluginDependency</string>
<string>134.IBPluginDependency</string>
<string>134.ImportedFromIB2</string>
<string>136.IBPluginDependency</string>
@@ -3177,6 +3265,7 @@
<string>222.ImportedFromIB2</string>
<string>23.IBPluginDependency</string>
<string>23.ImportedFromIB2</string>
<string>24.IBEditorWindowLastContentRect</string>
<string>24.IBPluginDependency</string>
<string>24.ImportedFromIB2</string>
<string>29.IBEditorWindowLastContentRect</string>
@@ -3314,6 +3403,7 @@
<string>955.ImportedFromIB2</string>
<string>959.IBPluginDependency</string>
<string>959.ImportedFromIB2</string>
<string>960.IBEditorWindowLastContentRect</string>
<string>960.IBPluginDependency</string>
<string>960.ImportedFromIB2</string>
<string>961.IBPluginDependency</string>
@@ -3322,6 +3412,7 @@
<string>962.ImportedFromIB2</string>
<string>965.IBPluginDependency</string>
<string>965.ImportedFromIB2</string>
<string>966.IBEditorWindowLastContentRect</string>
<string>966.IBPluginDependency</string>
<string>966.ImportedFromIB2</string>
<string>985.IBPluginDependency</string>
@@ -3351,6 +3442,7 @@
<boolean value="YES"/>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<boolean value="YES"/>
<string>{{602, 725}, {196, 43}}</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<boolean value="YES"/>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
@@ -3368,7 +3460,7 @@
<boolean value="YES"/>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<boolean value="YES"/>
<string>{{79, 766}, {617, 0}}</string>
<string>{{409, 745}, {617, 0}}</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
@@ -3380,6 +3472,11 @@
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>{{242, 705}, {258, 63}}</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<boolean value="YES"/>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<boolean value="YES"/>
@@ -3415,9 +3512,10 @@
<boolean value="YES"/>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<boolean value="YES"/>
<string>{{531, 625}, {193, 143}}</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<boolean value="YES"/>
<string>{{140, 768}, {481, 20}}</string>
<string>{{140, 768}, {523, 20}}</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<boolean value="YES"/>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
@@ -3448,7 +3546,7 @@
<boolean value="YES"/>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<boolean value="YES"/>
<string>{{286, 455}, {361, 313}}</string>
<string>{{328, 475}, {361, 293}}</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<boolean value="YES"/>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
@@ -3468,7 +3566,7 @@
<boolean value="YES"/>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<boolean value="YES"/>
<string>{{355, 762}, {64, 6}}</string>
<string>{{397, 762}, {64, 6}}</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<boolean value="YES"/>
<string>{{182, 609}, {331, 133}}</string>
@@ -3552,6 +3650,7 @@
<boolean value="YES"/>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<boolean value="YES"/>
<string>{{475, 725}, {167, 43}}</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<boolean value="YES"/>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
@@ -3560,6 +3659,7 @@
<boolean value="YES"/>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<boolean value="YES"/>
<string>{{284, 615}, {188, 153}}</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<boolean value="YES"/>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
@@ -3586,7 +3686,7 @@
</object>
</object>
<nil key="sourceID"/>
<int key="maxID">1178</int>
<int key="maxID">1208</int>
</object>
<object class="IBClassDescriber" key="IBDocument.Classes">
<object class="NSMutableArray" key="referencedPartialClassDescriptions">
@@ -3607,6 +3707,25 @@
<string>id</string>
</object>
</object>
<object class="NSMutableDictionary" key="actionInfosByName">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>openWebsite:</string>
<string>toggleDirectories:</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBActionInfo">
<string key="name">openWebsite:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">toggleDirectories:</string>
<string key="candidateClassName">id</string>
</object>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">AppDelegate.h</string>
@@ -3619,6 +3738,13 @@
<string key="NS.key.0">unlockApp:</string>
<string key="NS.object.0">id</string>
</object>
<object class="NSMutableDictionary" key="actionInfosByName">
<string key="NS.key.0">unlockApp:</string>
<object class="IBActionInfo" key="NS.object.0">
<string key="name">unlockApp:</string>
<string key="candidateClassName">id</string>
</object>
</object>
<object class="NSMutableDictionary" key="outlets">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
@@ -3634,6 +3760,30 @@
<string>NSMenuItem</string>
</object>
</object>
<object class="NSMutableDictionary" key="toOneOutletInfosByName">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>py</string>
<string>recentDirectories</string>
<string>unlockMenuItem</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBToOneOutletInfo">
<string key="name">py</string>
<string key="candidateClassName">PyDupeGuru</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">recentDirectories</string>
<string key="candidateClassName">RecentDirectories</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">unlockMenuItem</string>
<string key="candidateClassName">NSMenuItem</string>
</object>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBUserSource</string>
<string key="minorKey"/>
@@ -3646,6 +3796,13 @@
<string key="NS.key.0">unlockApp:</string>
<string key="NS.object.0">id</string>
</object>
<object class="NSMutableDictionary" key="actionInfosByName">
<string key="NS.key.0">unlockApp:</string>
<object class="IBActionInfo" key="NS.object.0">
<string key="name">unlockApp:</string>
<string key="candidateClassName">id</string>
</object>
</object>
<object class="NSMutableDictionary" key="outlets">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
@@ -3663,6 +3820,35 @@
<string>NSMenuItem</string>
</object>
</object>
<object class="NSMutableDictionary" key="toOneOutletInfosByName">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>py</string>
<string>recentDirectories</string>
<string>result</string>
<string>unlockMenuItem</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBToOneOutletInfo">
<string key="name">py</string>
<string key="candidateClassName">PyDupeGuruBase</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">recentDirectories</string>
<string key="candidateClassName">RecentDirectories</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">result</string>
<string key="candidateClassName">ResultWindowBase</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">unlockMenuItem</string>
<string key="candidateClassName">NSMenuItem</string>
</object>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">../base/AppDelegate.h</string>
@@ -3771,6 +3957,25 @@
<string>id</string>
</object>
</object>
<object class="NSMutableDictionary" key="actionInfosByName">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>clearMenu:</string>
<string>menuClick:</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBActionInfo">
<string key="name">clearMenu:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">menuClick:</string>
<string key="candidateClassName">id</string>
</object>
</object>
</object>
<object class="NSMutableDictionary" key="outlets">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
@@ -3784,6 +3989,25 @@
<string>NSMenu</string>
</object>
</object>
<object class="NSMutableDictionary" key="toOneOutletInfosByName">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>delegate</string>
<string>menu</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBToOneOutletInfo">
<string key="name">delegate</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">menu</string>
<string key="candidateClassName">NSMenu</string>
</object>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">../RecentDirectories.h</string>
@@ -3813,6 +4037,25 @@
<string>id</string>
</object>
</object>
<object class="NSMutableDictionary" key="actionInfosByName">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>resetColumnsToDefault:</string>
<string>startDuplicateScan:</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBActionInfo">
<string key="name">resetColumnsToDefault:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">startDuplicateScan:</string>
<string key="candidateClassName">id</string>
</object>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">ResultWindow.h</string>
@@ -3834,6 +4077,7 @@
<string>filter:</string>
<string>ignoreSelected:</string>
<string>invokeCustomCommand:</string>
<string>loadResults:</string>
<string>markAll:</string>
<string>markInvert:</string>
<string>markNone:</string>
@@ -3846,6 +4090,7 @@
<string>renameSelected:</string>
<string>resetColumnsToDefault:</string>
<string>revealSelected:</string>
<string>saveResults:</string>
<string>showPreferencesPanel:</string>
<string>startDuplicateScan:</string>
<string>switchSelected:</string>
@@ -3884,6 +4129,167 @@
<string>id</string>
<string>id</string>
<string>id</string>
<string>id</string>
<string>id</string>
</object>
</object>
<object class="NSMutableDictionary" key="actionInfosByName">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>changeDelta:</string>
<string>changePowerMarker:</string>
<string>clearIgnoreList:</string>
<string>copyMarked:</string>
<string>deleteMarked:</string>
<string>exportToXHTML:</string>
<string>filter:</string>
<string>ignoreSelected:</string>
<string>invokeCustomCommand:</string>
<string>loadResults:</string>
<string>markAll:</string>
<string>markInvert:</string>
<string>markNone:</string>
<string>markSelected:</string>
<string>moveMarked:</string>
<string>openClicked:</string>
<string>openSelected:</string>
<string>removeMarked:</string>
<string>removeSelected:</string>
<string>renameSelected:</string>
<string>resetColumnsToDefault:</string>
<string>revealSelected:</string>
<string>saveResults:</string>
<string>showPreferencesPanel:</string>
<string>startDuplicateScan:</string>
<string>switchSelected:</string>
<string>toggleColumn:</string>
<string>toggleDelta:</string>
<string>toggleDetailsPanel:</string>
<string>togglePowerMarker:</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBActionInfo">
<string key="name">changeDelta:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">changePowerMarker:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">clearIgnoreList:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">copyMarked:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">deleteMarked:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">exportToXHTML:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">filter:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">ignoreSelected:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">invokeCustomCommand:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">loadResults:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">markAll:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">markInvert:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">markNone:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">markSelected:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">moveMarked:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">openClicked:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">openSelected:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">removeMarked:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">removeSelected:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">renameSelected:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">resetColumnsToDefault:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">revealSelected:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">saveResults:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">showPreferencesPanel:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">startDuplicateScan:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">switchSelected:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">toggleColumn:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">toggleDelta:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">toggleDetailsPanel:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">togglePowerMarker:</string>
<string key="candidateClassName">id</string>
</object>
</object>
</object>
<object class="NSMutableDictionary" key="outlets">
@@ -3911,6 +4317,55 @@
<string>NSTextField</string>
</object>
</object>
<object class="NSMutableDictionary" key="toOneOutletInfosByName">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>app</string>
<string>columnsMenu</string>
<string>deltaSwitch</string>
<string>filterField</string>
<string>matches</string>
<string>pmSwitch</string>
<string>py</string>
<string>stats</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBToOneOutletInfo">
<string key="name">app</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">columnsMenu</string>
<string key="candidateClassName">NSMenu</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">deltaSwitch</string>
<string key="candidateClassName">NSSegmentedControl</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">filterField</string>
<string key="candidateClassName">NSSearchField</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">matches</string>
<string key="candidateClassName">HSOutlineView</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">pmSwitch</string>
<string key="candidateClassName">NSSegmentedControl</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">py</string>
<string key="candidateClassName">PyDupeGuruBase</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">stats</string>
<string key="candidateClassName">NSTextField</string>
</object>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">../base/ResultWindow.h</string>
@@ -4502,6 +4957,13 @@
<string key="NS.key.0">showWindow:</string>
<string key="NS.object.0">id</string>
</object>
<object class="NSMutableDictionary" key="actionInfosByName">
<string key="NS.key.0">showWindow:</string>
<object class="IBActionInfo" key="NS.object.0">
<string key="name">showWindow:</string>
<string key="candidateClassName">id</string>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBFrameworkSource</string>
<string key="minorKey">AppKit.framework/Headers/NSWindowController.h</string>
@@ -4514,15 +4976,30 @@
<string key="NS.key.0">checkForUpdates:</string>
<string key="NS.object.0">id</string>
</object>
<object class="NSMutableDictionary" key="actionInfosByName">
<string key="NS.key.0">checkForUpdates:</string>
<object class="IBActionInfo" key="NS.object.0">
<string key="name">checkForUpdates:</string>
<string key="candidateClassName">id</string>
</object>
</object>
<object class="NSMutableDictionary" key="outlets">
<string key="NS.key.0">delegate</string>
<string key="NS.object.0">id</string>
</object>
<object class="NSMutableDictionary" key="toOneOutletInfosByName">
<string key="NS.key.0">delegate</string>
<object class="IBToOneOutletInfo" key="NS.object.0">
<string key="name">delegate</string>
<string key="candidateClassName">id</string>
</object>
</object>
<reference key="sourceIdentifier" ref="311396825"/>
</object>
</object>
</object>
<int key="IBDocument.localizationMode">0</int>
<string key="IBDocument.TargetRuntimeIdentifier">IBCocoaFramework</string>
<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencies">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.macosx</string>
<integer value="1050" key="NS.object.0"/>
@@ -4538,5 +5015,28 @@
<bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool>
<string key="IBDocument.LastKnownRelativeProjectPath">../../se/dupeguru.xcodeproj</string>
<int key="IBDocument.defaultPropertyAccessControl">3</int>
<object class="NSMutableDictionary" key="IBDocument.LastKnownImageSizes">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>NSActionTemplate</string>
<string>NSApplicationIcon</string>
<string>NSMenuCheckmark</string>
<string>NSMenuMixedState</string>
<string>details32</string>
<string>folder32</string>
<string>preferences32</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>{15, 15}</string>
<string>{128, 128}</string>
<string>{9, 8}</string>
<string>{7, 2}</string>
<string>{48, 48}</string>
<string>{32, 32}</string>
<string>{32, 32}</string>
</object>
</object>
</data>
</archive>

View File

@@ -23,7 +23,7 @@
<key>CFBundleSignature</key>
<string>hsft</string>
<key>CFBundleVersion</key>
<string>5.8.0</string>
<string>5.8.1</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>

View File

@@ -20,7 +20,7 @@ http://www.hardcoded.net/licenses/hs_license
{
[super awakeFromNib];
[[self window] setTitle:@"dupeGuru Music Edition"];
NSMutableIndexSet *deltaColumns = [NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(2,7)];
NSMutableIndexSet *deltaColumns = [NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(2,6)];
[deltaColumns removeIndex:6];
[outline setDeltaColumns:deltaColumns];
}
@@ -38,13 +38,13 @@ http://www.hardcoded.net/licenses/hs_license
[columnsOrder addObject:@"2"];
[columnsOrder addObject:@"3"];
[columnsOrder addObject:@"4"];
[columnsOrder addObject:@"16"];
[columnsOrder addObject:@"15"];
NSMutableDictionary *columnsWidth = [NSMutableDictionary dictionary];
[columnsWidth setObject:i2n(214) forKey:@"0"];
[columnsWidth setObject:i2n(63) forKey:@"2"];
[columnsWidth setObject:i2n(50) forKey:@"3"];
[columnsWidth setObject:i2n(50) forKey:@"4"];
[columnsWidth setObject:i2n(57) forKey:@"16"];
[columnsWidth setObject:i2n(57) forKey:@"15"];
[self restoreColumnsPosition:columnsOrder widths:columnsWidth];
}
@@ -69,8 +69,6 @@ http://www.hardcoded.net/licenses/hs_license
[_py setMixFileKind:[ud objectForKey:@"mixFileKind"]];
[_py setMatchSimilarWords:[ud objectForKey:@"matchSimilarWords"]];
NSInteger r = n2i([py doScan]);
if (r == 1)
[Dialogs showMessage:@"You cannot make a duplicate scan with only reference directories."];
if (r == 3)
{
[Dialogs showMessage:@"The selected directories contain no scannable file."];
@@ -96,18 +94,17 @@ http://www.hardcoded.net/licenses/hs_license
[_resultColumns addObject:brCol];
[_resultColumns addObject:[self getColumnForIdentifier:5 title:@"Sample Rate" width:60 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:6 title:@"Kind" width:40 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:7 title:@"Creation" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:8 title:@"Modification" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:9 title:@"Title" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:10 title:@"Artist" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:11 title:@"Album" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:12 title:@"Genre" width:80 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:13 title:@"Year" width:40 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:14 title:@"Track Number" width:40 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:15 title:@"Comment" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:16 title:@"Match %" width:57 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:17 title:@"Words Used" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:18 title:@"Dupe Count" width:80 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:7 title:@"Modification" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:8 title:@"Title" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:9 title:@"Artist" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:10 title:@"Album" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:11 title:@"Genre" width:80 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:12 title:@"Year" width:40 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:13 title:@"Track Number" width:40 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:14 title:@"Comment" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:15 title:@"Match %" width:57 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:16 title:@"Words Used" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:17 title:@"Dupe Count" width:80 refCol:refCol]];
}
/* Notifications */

View File

@@ -8,13 +8,12 @@ from hscommon.cocoa import signature
from core.app_cocoa_inter import PyDupeGuruBase, PyDetailsPanel
from core_me.app_cocoa import DupeGuruME
from core.scanner import (SCAN_TYPE_FILENAME, SCAN_TYPE_FIELDS, SCAN_TYPE_FIELDS_NO_ORDER,
SCAN_TYPE_TAG, SCAN_TYPE_CONTENT, SCAN_TYPE_CONTENT_AUDIO)
from core.scanner import ScanType
# Fix py2app imports which chokes on relative imports and other stuff
from core_me import app_cocoa, data, fs, scanner
from hsaudiotag import aiff, flac, genres, id3v1, id3v2, mp4, mpeg, ogg, wma
from lxml import etree, _elementpath
import xml.etree.ElementPath
import gzip
class PyDupeGuru(PyDupeGuruBase):
@@ -41,12 +40,12 @@ class PyDupeGuru(PyDupeGuruBase):
def setScanType_(self, scan_type):
try:
self.py.scanner.scan_type = [
SCAN_TYPE_FILENAME,
SCAN_TYPE_FIELDS,
SCAN_TYPE_FIELDS_NO_ORDER,
SCAN_TYPE_TAG,
SCAN_TYPE_CONTENT,
SCAN_TYPE_CONTENT_AUDIO
ScanType.Filename,
ScanType.Fields,
ScanType.FieldsNoOrder,
ScanType.Tag,
ScanType.Contents,
ScanType.ContentsAudio,
][scan_type]
except IndexError:
pass

View File

@@ -44,7 +44,6 @@
CE49DEF60FDFEB810098617B /* BRSingleLineFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = CE49DEF30FDFEB810098617B /* BRSingleLineFormatter.m */; };
CE4B59C81119919700C06C9E /* ErrorReportWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE4B59C51119919700C06C9E /* ErrorReportWindow.xib */; };
CE4B59C91119919700C06C9E /* progress.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE4B59C61119919700C06C9E /* progress.xib */; };
CE4B59CA1119919700C06C9E /* registration.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE4B59C71119919700C06C9E /* registration.xib */; };
CE515DF30FC6C12E00EC695D /* Dialogs.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515DE10FC6C12E00EC695D /* Dialogs.m */; };
CE515DF40FC6C12E00EC695D /* HSErrorReportWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515DE30FC6C12E00EC695D /* HSErrorReportWindow.m */; };
CE515DF60FC6C12E00EC695D /* ProgressController.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515DE70FC6C12E00EC695D /* ProgressController.m */; };
@@ -61,6 +60,7 @@
CE848A1909DD85810004CB44 /* Consts.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE848A1809DD85810004CB44 /* Consts.h */; };
CE900AD2109B238600754048 /* Preferences.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE900AD1109B238600754048 /* Preferences.xib */; };
CE900AD7109B2A9B00754048 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE900AD6109B2A9B00754048 /* MainMenu.xib */; };
CECC563B12144A9000ABF262 /* registration.xib in Resources */ = {isa = PBXBuildFile; fileRef = CECC563912144A9000ABF262 /* registration.xib */; };
CEDF07A3112493B200EE5BC0 /* StatsLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = CEDF07A2112493B200EE5BC0 /* StatsLabel.m */; };
CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */ = {isa = PBXBuildFile; fileRef = CEEB135109C837A2004D2330 /* dupeguru.icns */; };
CEFC294609C89E3D00D9F998 /* folder32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC294509C89E3D00D9F998 /* folder32.png */; };
@@ -133,7 +133,6 @@
CE49DEF30FDFEB810098617B /* BRSingleLineFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BRSingleLineFormatter.m; path = ../../cocoalib/brsinglelineformatter/BRSingleLineFormatter.m; sourceTree = SOURCE_ROOT; };
CE4B59C51119919700C06C9E /* ErrorReportWindow.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ErrorReportWindow.xib; sourceTree = "<group>"; };
CE4B59C61119919700C06C9E /* progress.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = progress.xib; sourceTree = "<group>"; };
CE4B59C71119919700C06C9E /* registration.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = registration.xib; sourceTree = "<group>"; };
CE515DE00FC6C12E00EC695D /* Dialogs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Dialogs.h; path = ../../cocoalib/Dialogs.h; sourceTree = SOURCE_ROOT; };
CE515DE10FC6C12E00EC695D /* Dialogs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Dialogs.m; path = ../../cocoalib/Dialogs.m; sourceTree = SOURCE_ROOT; };
CE515DE20FC6C12E00EC695D /* HSErrorReportWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HSErrorReportWindow.h; path = ../../cocoalib/HSErrorReportWindow.h; sourceTree = SOURCE_ROOT; };
@@ -165,6 +164,7 @@
CE848A1809DD85810004CB44 /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Consts.h; sourceTree = "<group>"; };
CE900AD1109B238600754048 /* Preferences.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = Preferences.xib; sourceTree = "<group>"; };
CE900AD6109B2A9B00754048 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = MainMenu.xib; path = ../../base/xib/MainMenu.xib; sourceTree = "<group>"; };
CECC563A12144A9000ABF262 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = ../../cocoalib/en.lproj/registration.xib; sourceTree = SOURCE_ROOT; };
CED0A591111C9FD10020AD7D /* PyDetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyDetailsPanel.h; path = ../base/PyDetailsPanel.h; sourceTree = SOURCE_ROOT; };
CEDF07A0112493B200EE5BC0 /* PyStatsLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyStatsLabel.h; path = ../base/PyStatsLabel.h; sourceTree = SOURCE_ROOT; };
CEDF07A1112493B200EE5BC0 /* StatsLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = StatsLabel.h; path = ../base/StatsLabel.h; sourceTree = SOURCE_ROOT; };
@@ -342,9 +342,9 @@
CE4B59C41119919700C06C9E /* xib */ = {
isa = PBXGroup;
children = (
CECC563912144A9000ABF262 /* registration.xib */,
CE4B59C51119919700C06C9E /* ErrorReportWindow.xib */,
CE4B59C61119919700C06C9E /* progress.xib */,
CE4B59C71119919700C06C9E /* registration.xib */,
);
name = xib;
path = ../../cocoalib/xib;
@@ -451,6 +451,13 @@
buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "dupeguru" */;
compatibilityVersion = "Xcode 3.0";
hasScannedForEncodings = 1;
knownRegions = (
English,
Japanese,
French,
German,
en,
);
mainGroup = 29B97314FDCFA39411CA2CEA /* dupeguru */;
projectDirPath = "";
projectRoot = "";
@@ -478,8 +485,8 @@
CE900AD7109B2A9B00754048 /* MainMenu.xib in Resources */,
CE4B59C81119919700C06C9E /* ErrorReportWindow.xib in Resources */,
CE4B59C91119919700C06C9E /* progress.xib in Resources */,
CE4B59CA1119919700C06C9E /* registration.xib in Resources */,
CE0A0C061175A24800DCA3C6 /* ProblemDialog.xib in Resources */,
CECC563B12144A9000ABF262 /* registration.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -523,6 +530,18 @@
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
CECC563912144A9000ABF262 /* registration.xib */ = {
isa = PBXVariantGroup;
children = (
CECC563A12144A9000ABF262 /* en */,
);
name = registration.xib;
path = ../../cocoalib/xib;
sourceTree = SOURCE_ROOT;
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
C01FCF4C08A954540054247B /* release */ = {
isa = XCBuildConfiguration;

View File

@@ -23,7 +23,7 @@
<key>CFBundleSignature</key>
<string>hsft</string>
<key>CFBundleVersion</key>
<string>1.9.0</string>
<string>1.9.1</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>

View File

@@ -20,9 +20,8 @@ http://www.hardcoded.net/licenses/hs_license
{
[super awakeFromNib];
[[self window] setTitle:@"dupeGuru Picture Edition"];
NSMutableIndexSet *deltaColumns = [NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(2,5)];
[deltaColumns removeIndex:3];
[deltaColumns removeIndex:4];
NSMutableIndexSet *deltaColumns = [NSMutableIndexSet indexSetWithIndex:2];
[deltaColumns addIndex:5];
[outline setDeltaColumns:deltaColumns];
}
@@ -41,13 +40,13 @@ http://www.hardcoded.net/licenses/hs_license
[columnsOrder addObject:@"1"];
[columnsOrder addObject:@"2"];
[columnsOrder addObject:@"4"];
[columnsOrder addObject:@"7"];
[columnsOrder addObject:@"6"];
NSMutableDictionary *columnsWidth = [NSMutableDictionary dictionary];
[columnsWidth setObject:i2n(121) forKey:@"0"];
[columnsWidth setObject:i2n(120) forKey:@"1"];
[columnsWidth setObject:i2n(63) forKey:@"2"];
[columnsWidth setObject:i2n(73) forKey:@"4"];
[columnsWidth setObject:i2n(58) forKey:@"7"];
[columnsWidth setObject:i2n(58) forKey:@"6"];
[self restoreColumnsPosition:columnsOrder widths:columnsWidth];
}
@@ -66,8 +65,6 @@ http://www.hardcoded.net/licenses/hs_license
int r = n2i([py doScan]);
if (r != 0)
[[ProgressController mainProgressController] hide];
if (r == 1)
[Dialogs showMessage:@"You cannot make a duplicate scan with only reference directories."];
if (r == 3)
{
[Dialogs showMessage:@"The selected directories contain no scannable file."];
@@ -92,9 +89,8 @@ http://www.hardcoded.net/licenses/hs_license
[_resultColumns addObject:sizeCol];
[_resultColumns addObject:[self getColumnForIdentifier:3 title:@"Kind" width:40 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:4 title:@"Dimensions" width:80 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:5 title:@"Creation" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:6 title:@"Modification" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:7 title:@"Match %" width:58 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:8 title:@"Dupe Count" width:80 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:5 title:@"Modification" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:6 title:@"Match %" width:58 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:7 title:@"Dupe Count" width:80 refCol:refCol]];
}
@end

View File

@@ -8,9 +8,13 @@ from core.app_cocoa_inter import PyDupeGuruBase, PyDetailsPanel
from core_pe import app_cocoa as app_pe_cocoa
# Fix py2app imports which chokes on relative imports and other stuff
from core_pe import block, cache, matchbase, data, _block_osx
from lxml import etree, _elementpath
import hsutil.conflict
import core.engine, core.fs, core.app
import core_pe.block, core_pe.cache, core_pe.matchbase, core_pe.data, core_pe._block_osx
import xml.etree.ElementPath
import gzip
import aem.kae
import appscript.defaultterminology
class PyDupeGuru(PyDupeGuruBase):
def init(self):
@@ -23,10 +27,10 @@ class PyDupeGuru(PyDupeGuruBase):
#---Information
def getSelectedDupePath(self):
return unicode(self.py.selected_dupe_path())
return str(self.py.selected_dupe_path())
def getSelectedDupeRefPath(self):
return unicode(self.py.selected_dupe_ref_path())
return str(self.py.selected_dupe_ref_path())
#---Properties
def setMatchScaled_(self,match_scaled):

View File

@@ -27,7 +27,6 @@
CE77C8A810946CE20078B0DB /* DetailsPanel.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE77C8A710946CE20078B0DB /* DetailsPanel.xib */; };
CE7AC9181119911200D02F6C /* ErrorReportWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE7AC9151119911200D02F6C /* ErrorReportWindow.xib */; };
CE7AC9191119911200D02F6C /* progress.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE7AC9161119911200D02F6C /* progress.xib */; };
CE7AC91A1119911200D02F6C /* registration.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE7AC9171119911200D02F6C /* registration.xib */; };
CE80DB2E0FC192D60086DCA6 /* Dialogs.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB1C0FC192D60086DCA6 /* Dialogs.m */; };
CE80DB2F0FC192D60086DCA6 /* HSErrorReportWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB1E0FC192D60086DCA6 /* HSErrorReportWindow.m */; };
CE80DB310FC192D60086DCA6 /* ProgressController.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB220FC192D60086DCA6 /* ProgressController.m */; };
@@ -41,6 +40,7 @@
CE80DB8B0FC1951C0086DCA6 /* DirectoryPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB860FC1951C0086DCA6 /* DirectoryPanel.m */; };
CE80DB8C0FC1951C0086DCA6 /* ResultWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB890FC1951C0086DCA6 /* ResultWindow.m */; };
CE848A1909DD85810004CB44 /* Consts.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE848A1809DD85810004CB44 /* Consts.h */; };
CE895D7B12144A7800E74705 /* registration.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE895D7912144A7800E74705 /* registration.xib */; };
CE95865E112C516400F95FD2 /* ResultOutline.m in Sources */ = {isa = PBXBuildFile; fileRef = CE95865B112C516400F95FD2 /* ResultOutline.m */; };
CE95865F112C516400F95FD2 /* StatsLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE95865D112C516400F95FD2 /* StatsLabel.m */; };
CE9EA7561122C96C008CD2BC /* HSGUIController.m in Sources */ = {isa = PBXBuildFile; fileRef = CE9EA7441122C96C008CD2BC /* HSGUIController.m */; };
@@ -110,7 +110,6 @@
CE77C8A710946CE20078B0DB /* DetailsPanel.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DetailsPanel.xib; sourceTree = "<group>"; };
CE7AC9151119911200D02F6C /* ErrorReportWindow.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ErrorReportWindow.xib; sourceTree = "<group>"; };
CE7AC9161119911200D02F6C /* progress.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = progress.xib; sourceTree = "<group>"; };
CE7AC9171119911200D02F6C /* registration.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = registration.xib; sourceTree = "<group>"; };
CE80DB1B0FC192D60086DCA6 /* Dialogs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Dialogs.h; path = ../../cocoalib/Dialogs.h; sourceTree = SOURCE_ROOT; };
CE80DB1C0FC192D60086DCA6 /* Dialogs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Dialogs.m; path = ../../cocoalib/Dialogs.m; sourceTree = SOURCE_ROOT; };
CE80DB1D0FC192D60086DCA6 /* HSErrorReportWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HSErrorReportWindow.h; path = ../../cocoalib/HSErrorReportWindow.h; sourceTree = SOURCE_ROOT; };
@@ -139,6 +138,7 @@
CE80DB880FC1951C0086DCA6 /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ResultWindow.h; path = ../base/ResultWindow.h; sourceTree = SOURCE_ROOT; };
CE80DB890FC1951C0086DCA6 /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ResultWindow.m; path = ../base/ResultWindow.m; sourceTree = SOURCE_ROOT; };
CE848A1809DD85810004CB44 /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Consts.h; sourceTree = "<group>"; };
CE895D7A12144A7800E74705 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = ../../cocoalib/en.lproj/registration.xib; sourceTree = SOURCE_ROOT; };
CE958658112C516400F95FD2 /* PyResultTree.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyResultTree.h; path = ../base/PyResultTree.h; sourceTree = SOURCE_ROOT; };
CE958659112C516400F95FD2 /* PyStatsLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyStatsLabel.h; path = ../base/PyStatsLabel.h; sourceTree = SOURCE_ROOT; };
CE95865A112C516400F95FD2 /* ResultOutline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ResultOutline.h; path = ../base/ResultOutline.h; sourceTree = SOURCE_ROOT; };
@@ -294,9 +294,9 @@
CE7AC9141119911200D02F6C /* xib */ = {
isa = PBXGroup;
children = (
CE895D7912144A7800E74705 /* registration.xib */,
CE7AC9151119911200D02F6C /* ErrorReportWindow.xib */,
CE7AC9161119911200D02F6C /* progress.xib */,
CE7AC9171119911200D02F6C /* registration.xib */,
);
name = xib;
path = ../../cocoalib/xib;
@@ -458,6 +458,13 @@
buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "dupeguru" */;
compatibilityVersion = "Xcode 3.0";
hasScannedForEncodings = 1;
knownRegions = (
English,
Japanese,
French,
German,
en,
);
mainGroup = 29B97314FDCFA39411CA2CEA /* dupeguru */;
projectDirPath = "";
projectRoot = "";
@@ -486,8 +493,8 @@
CE031754109B345200517EE6 /* MainMenu.xib in Resources */,
CE7AC9181119911200D02F6C /* ErrorReportWindow.xib in Resources */,
CE7AC9191119911200D02F6C /* progress.xib in Resources */,
CE7AC91A1119911200D02F6C /* registration.xib in Resources */,
CE0C2AC81177021600BC749F /* ProblemDialog.xib in Resources */,
CE895D7B12144A7800E74705 /* registration.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -534,6 +541,18 @@
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
CE895D7912144A7800E74705 /* registration.xib */ = {
isa = PBXVariantGroup;
children = (
CE895D7A12144A7800E74705 /* en */,
);
name = registration.xib;
path = ../../cocoalib/xib;
sourceTree = SOURCE_ROOT;
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
C01FCF4C08A954540054247B /* release */ = {
isa = XCBuildConfiguration;

View File

@@ -2,13 +2,13 @@
<archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="7.10">
<data>
<int key="IBDocument.SystemTarget">1050</int>
<string key="IBDocument.SystemVersion">10C540</string>
<string key="IBDocument.InterfaceBuilderVersion">740</string>
<string key="IBDocument.AppKitVersion">1038.25</string>
<string key="IBDocument.HIToolboxVersion">458.00</string>
<string key="IBDocument.SystemVersion">10F569</string>
<string key="IBDocument.InterfaceBuilderVersion">788</string>
<string key="IBDocument.AppKitVersion">1038.29</string>
<string key="IBDocument.HIToolboxVersion">461.00</string>
<object class="NSMutableDictionary" key="IBDocument.PluginVersions">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="NS.object.0">740</string>
<string key="NS.object.0">788</string>
</object>
<object class="NSMutableArray" key="IBDocument.EditedObjectIDs">
<bool key="EncodedWithXMLCoder">YES</bool>
@@ -434,6 +434,7 @@
<string key="NSScreenRect">{{0, 0}, {1440, 878}}</string>
<string key="NSMinSize">{451, 177}</string>
<string key="NSMaxSize">{1.79769e+308, 1.79769e+308}</string>
<string key="NSFrameAutosaveName">DetailsPanel</string>
</object>
</object>
<object class="IBObjectContainer" key="IBDocument.Objects">
@@ -866,6 +867,13 @@
<string key="NS.key.0">detailsTable</string>
<string key="NS.object.0">NSTableView</string>
</object>
<object class="NSMutableDictionary" key="toOneOutletInfosByName">
<string key="NS.key.0">detailsTable</string>
<object class="IBToOneOutletInfo" key="NS.object.0">
<string key="name">detailsTable</string>
<string key="candidateClassName">NSTableView</string>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">../base/DetailsPanel.h</string>
@@ -891,6 +899,35 @@
<string>NSProgressIndicator</string>
</object>
</object>
<object class="NSMutableDictionary" key="toOneOutletInfosByName">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>dupeImage</string>
<string>dupeProgressIndicator</string>
<string>refImage</string>
<string>refProgressIndicator</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBToOneOutletInfo">
<string key="name">dupeImage</string>
<string key="candidateClassName">NSImageView</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">dupeProgressIndicator</string>
<string key="candidateClassName">NSProgressIndicator</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">refImage</string>
<string key="candidateClassName">NSImageView</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">refProgressIndicator</string>
<string key="candidateClassName">NSProgressIndicator</string>
</object>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">DetailsPanel.h</string>
@@ -903,6 +940,13 @@
<string key="NS.key.0">detailsTable</string>
<string key="NS.object.0">NSTableView</string>
</object>
<object class="NSMutableDictionary" key="toOneOutletInfosByName">
<string key="NS.key.0">detailsTable</string>
<object class="IBToOneOutletInfo" key="NS.object.0">
<string key="name">detailsTable</string>
<string key="candidateClassName">NSTableView</string>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBUserSource</string>
<string key="minorKey"/>
@@ -1423,6 +1467,13 @@
<string key="NS.key.0">showWindow:</string>
<string key="NS.object.0">id</string>
</object>
<object class="NSMutableDictionary" key="actionInfosByName">
<string key="NS.key.0">showWindow:</string>
<object class="IBActionInfo" key="NS.object.0">
<string key="name">showWindow:</string>
<string key="candidateClassName">id</string>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBFrameworkSource</string>
<string key="minorKey">AppKit.framework/Headers/NSWindowController.h</string>
@@ -1431,6 +1482,7 @@
</object>
</object>
<int key="IBDocument.localizationMode">0</int>
<string key="IBDocument.TargetRuntimeIdentifier">IBCocoaFramework</string>
<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencies">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.macosx</string>
<integer value="1050" key="NS.object.0"/>
@@ -1446,5 +1498,9 @@
<bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool>
<nil key="IBDocument.LastKnownRelativeProjectPath"/>
<int key="IBDocument.defaultPropertyAccessControl">3</int>
<object class="NSMutableDictionary" key="IBDocument.LastKnownImageSizes">
<string key="NS.key.0">NSApplicationIcon</string>
<string key="NS.object.0">{128, 128}</string>
</object>
</data>
</archive>

View File

@@ -23,7 +23,7 @@
<key>CFBundleSignature</key>
<string>hsft</string>
<key>CFBundleVersion</key>
<string>2.10.1</string>
<string>2.11.0</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>

View File

@@ -18,8 +18,8 @@ http://www.hardcoded.net/licenses/hs_license
- (void)awakeFromNib
{
[super awakeFromNib];
NSMutableIndexSet *deltaColumns = [NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(2,4)];
[deltaColumns removeIndex:3];
NSMutableIndexSet *deltaColumns = [NSMutableIndexSet indexSetWithIndex:2];
[deltaColumns addIndex:4];
[outline setDeltaColumns:deltaColumns];
}
@@ -30,12 +30,12 @@ http://www.hardcoded.net/licenses/hs_license
[columnsOrder addObject:@"0"];
[columnsOrder addObject:@"1"];
[columnsOrder addObject:@"2"];
[columnsOrder addObject:@"6"];
[columnsOrder addObject:@"5"];
NSMutableDictionary *columnsWidth = [NSMutableDictionary dictionary];
[columnsWidth setObject:i2n(195) forKey:@"0"];
[columnsWidth setObject:i2n(120) forKey:@"1"];
[columnsWidth setObject:i2n(63) forKey:@"2"];
[columnsWidth setObject:i2n(60) forKey:@"6"];
[columnsWidth setObject:i2n(60) forKey:@"5"];
[self restoreColumnsPosition:columnsOrder widths:columnsWidth];
}
@@ -59,8 +59,6 @@ http://www.hardcoded.net/licenses/hs_license
int r = n2i([py doScan]);
if (r != 0)
[[ProgressController mainProgressController] hide];
if (r == 1)
[Dialogs showMessage:@"You cannot make a duplicate scan with only reference directories."];
if (r == 3)
{
[Dialogs showMessage:@"The selected directories contain no scannable file."];
@@ -80,10 +78,9 @@ http://www.hardcoded.net/licenses/hs_license
[[sizeCol dataCell] setAlignment:NSRightTextAlignment];
[_resultColumns addObject:sizeCol];
[_resultColumns addObject:[self getColumnForIdentifier:3 title:@"Kind" width:40 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:4 title:@"Creation" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:5 title:@"Modification" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:6 title:@"Match %" width:60 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:7 title:@"Words Used" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:8 title:@"Dupe Count" width:80 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:4 title:@"Modification" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:5 title:@"Match %" width:60 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:6 title:@"Words Used" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:7 title:@"Dupe Count" width:80 refCol:refCol]];
}
@end

View File

@@ -6,13 +6,15 @@
from hscommon.cocoa import signature
from core import scanner
from core.scanner import ScanType
from core.app_cocoa_inter import PyDupeGuruBase, PyDetailsPanel
from core_se.app_cocoa import DupeGuru
# Fix py2app imports with chokes on relative imports and other stuff
from core_se import fs, data
from lxml import etree, _elementpath
import hsutil.conflict
import core.engine, core.fs, core.app
import core_se.fs, core_se.data
import xml.etree.ElementPath
import gzip
class PyDupeGuru(PyDupeGuruBase):
@@ -28,8 +30,8 @@ class PyDupeGuru(PyDupeGuruBase):
def setScanType_(self,scan_type):
try:
self.py.scanner.scan_type = [
scanner.SCAN_TYPE_FILENAME,
scanner.SCAN_TYPE_CONTENT
ScanType.Filename,
ScanType.Contents,
][scan_type]
except IndexError:
pass

View File

@@ -12,7 +12,6 @@
CE073F6309CAE1A3005C1D2F /* dupeguru_help in Resources */ = {isa = PBXBuildFile; fileRef = CE073F5409CAE1A3005C1D2F /* dupeguru_help */; };
CE19BC6311199231007CCEB0 /* ErrorReportWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE19BC6011199231007CCEB0 /* ErrorReportWindow.xib */; };
CE19BC6411199231007CCEB0 /* progress.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE19BC6111199231007CCEB0 /* progress.xib */; };
CE19BC6511199231007CCEB0 /* registration.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE19BC6211199231007CCEB0 /* registration.xib */; };
CE381C9609914ACE003581CE /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CE381C9409914ACE003581CE /* AppDelegate.m */; };
CE381C9C09914ADF003581CE /* ResultWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE381C9A09914ADF003581CE /* ResultWindow.m */; };
CE381D0509915304003581CE /* dg_cocoa.plugin in Resources */ = {isa = PBXBuildFile; fileRef = CE381CF509915304003581CE /* dg_cocoa.plugin */; };
@@ -33,6 +32,7 @@
CE91F215113BC22D0010360B /* ResultOutline.m in Sources */ = {isa = PBXBuildFile; fileRef = CE91F212113BC22D0010360B /* ResultOutline.m */; };
CE91F216113BC22D0010360B /* StatsLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE91F214113BC22D0010360B /* StatsLabel.m */; };
CEAC6811109B0B7E00B43C85 /* Preferences.xib in Resources */ = {isa = PBXBuildFile; fileRef = CEAC6810109B0B7E00B43C85 /* Preferences.xib */; };
CEBC6C3912144A4B007B43AE /* registration.xib in Resources */ = {isa = PBXBuildFile; fileRef = CEBC6C3712144A4B007B43AE /* registration.xib */; };
CEBE4D74111F0EE1009AAC6D /* HSWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = CEBE4D73111F0EE1009AAC6D /* HSWindowController.m */; };
CEDD92DA0FDD01640031C7B7 /* BRSingleLineFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = CEDD92D70FDD01640031C7B7 /* BRSingleLineFormatter.m */; };
CEE7EA130FE675C80004E467 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CEE7EA120FE675C80004E467 /* DetailsPanel.m */; };
@@ -78,7 +78,6 @@
CE073F5409CAE1A3005C1D2F /* dupeguru_help */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dupeguru_help; path = ../../help_se/dupeguru_help; sourceTree = "<group>"; };
CE19BC6011199231007CCEB0 /* ErrorReportWindow.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ErrorReportWindow.xib; sourceTree = "<group>"; };
CE19BC6111199231007CCEB0 /* progress.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = progress.xib; sourceTree = "<group>"; };
CE19BC6211199231007CCEB0 /* registration.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = registration.xib; sourceTree = "<group>"; };
CE381C9409914ACE003581CE /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = SOURCE_ROOT; };
CE381C9509914ACE003581CE /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = SOURCE_ROOT; };
CE381C9A09914ADF003581CE /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = ResultWindow.m; sourceTree = SOURCE_ROOT; };
@@ -118,6 +117,7 @@
CE91F213113BC22D0010360B /* StatsLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = StatsLabel.h; path = ../base/StatsLabel.h; sourceTree = SOURCE_ROOT; };
CE91F214113BC22D0010360B /* StatsLabel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = StatsLabel.m; path = ../base/StatsLabel.m; sourceTree = SOURCE_ROOT; };
CEAC6810109B0B7E00B43C85 /* Preferences.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Preferences.xib; path = xib/Preferences.xib; sourceTree = "<group>"; };
CEBC6C3812144A4B007B43AE /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = ../../cocoalib/en.lproj/registration.xib; sourceTree = SOURCE_ROOT; };
CEBE4D72111F0EE1009AAC6D /* HSWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HSWindowController.h; sourceTree = "<group>"; };
CEBE4D73111F0EE1009AAC6D /* HSWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HSWindowController.m; sourceTree = "<group>"; };
CEDD92D60FDD01640031C7B7 /* BRSingleLineFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BRSingleLineFormatter.h; path = ../../cocoalib/brsinglelineformatter/BRSingleLineFormatter.h; sourceTree = SOURCE_ROOT; };
@@ -249,9 +249,9 @@
CE19BC5F11199231007CCEB0 /* xib */ = {
isa = PBXGroup;
children = (
CEBC6C3712144A4B007B43AE /* registration.xib */,
CE19BC6011199231007CCEB0 /* ErrorReportWindow.xib */,
CE19BC6111199231007CCEB0 /* progress.xib */,
CE19BC6211199231007CCEB0 /* registration.xib */,
);
name = xib;
path = ../../cocoalib/xib;
@@ -422,6 +422,13 @@
buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "dupeguru" */;
compatibilityVersion = "Xcode 3.0";
hasScannedForEncodings = 1;
knownRegions = (
English,
Japanese,
French,
German,
en,
);
mainGroup = 29B97314FDCFA39411CA2CEA /* dupeguru */;
projectDirPath = "";
projectRoot = "";
@@ -449,8 +456,8 @@
CE3A46FA109B212E002ABFD5 /* MainMenu.xib in Resources */,
CE19BC6311199231007CCEB0 /* ErrorReportWindow.xib in Resources */,
CE19BC6411199231007CCEB0 /* progress.xib in Resources */,
CE19BC6511199231007CCEB0 /* registration.xib in Resources */,
CE647E591173026F006D28BA /* ProblemDialog.xib in Resources */,
CEBC6C3912144A4B007B43AE /* registration.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -493,6 +500,18 @@
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
CEBC6C3712144A4B007B43AE /* registration.xib */ = {
isa = PBXVariantGroup;
children = (
CEBC6C3812144A4B007B43AE /* en */,
);
name = registration.xib;
path = ../../cocoalib/xib;
sourceTree = SOURCE_ROOT;
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
C01FCF4C08A954540054247B /* release */ = {
isa = XCBuildConfiguration;

View File

@@ -18,7 +18,7 @@ def main(edition, ui, dev):
if ui not in ('cocoa', 'qt'):
ui = 'cocoa' if sys.platform == 'darwin' else 'qt'
build_type = 'Dev' if dev else 'Release'
print "Configuring dupeGuru {0} for UI {1} ({2})".format(edition.upper(), ui, build_type)
print("Configuring dupeGuru {0} for UI {1} ({2})".format(edition.upper(), ui, build_type))
conf = {
'edition': edition,
'ui': ui,

View File

@@ -6,7 +6,7 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from __future__ import unicode_literals
import os
import os.path as op
@@ -33,9 +33,6 @@ JOB_DELETE = 'job_delete'
class NoScannableFileError(Exception):
pass
class AllFilesAreRefError(Exception):
pass
class DupeGuru(RegistrableApplication, Broadcaster):
DEMO_LIMIT_DESC = "In the demo version, only 10 duplicates per session can be sent to the recycle bin, moved or copied."
@@ -76,23 +73,13 @@ class DupeGuru(RegistrableApplication, Broadcaster):
def _do_delete_dupe(self, dupe):
if not io.exists(dupe.path):
return
send2trash(unicode(dupe.path)) # Raises OSError when there's a problem
send2trash(str(dupe.path)) # Raises OSError when there's a problem
self.clean_empty_dirs(dupe.path[:-1])
def _do_load(self, j):
self.directories.load_from_file(op.join(self.appdata, 'last_directories.xml'))
self.notify('directories_changed')
j = j.start_subjob([1, 9])
self.results.load_from_xml(op.join(self.appdata, 'last_results.xml'), self._get_file, j)
files = flatten(g[:] for g in self.results.groups)
try:
for file in j.iter_with_progress(files, 'Reading metadata %d/%d'):
file._read_all_info(attrnames=self.data.METADATA_TO_READ)
except (OSError, IOError):
# If this error is raised, it means that a file was deleted while we were reading
# metadata. Proper handling of this rare occurrence is complex because there's no easy
# way to remove an arbitrary file from the Results. Thus, we simply clear them.
self.results.groups = []
def _get_display_info(self, dupe, group, delta=False):
if (dupe is None) or (group is None):
@@ -100,12 +87,19 @@ class DupeGuru(RegistrableApplication, Broadcaster):
try:
return self.data.GetDisplayInfo(dupe, group, delta)
except Exception as e:
logging.warning("Exception on GetDisplayInfo for %s: %s", unicode(dupe.path), unicode(e))
logging.warning("Exception on GetDisplayInfo for %s: %s", str(dupe.path), str(e))
return ['---'] * len(self.data.COLUMNS)
def _get_file(self, str_path):
path = Path(str_path)
return fs.get_file(path, self.directories.fileclasses)
f = fs.get_file(path, self.directories.fileclasses)
if f is None:
return None
try:
f._read_all_info(attrnames=self.data.METADATA_TO_READ)
return f
except EnvironmentError:
return None
def _job_completed(self, jobid):
# Must be called by subclasses when they detect that an async job is completed.
@@ -149,7 +143,7 @@ class DupeGuru(RegistrableApplication, Broadcaster):
g = self.results.get_group_of_duplicate(dupe)
for other in g:
if other is not dupe:
self.scanner.ignore_list.Ignore(unicode(other.path), unicode(dupe.path))
self.scanner.ignore_list.Ignore(str(other.path), str(dupe.path))
self.remove_duplicates(dupes)
def apply_filter(self, filter):
@@ -208,7 +202,7 @@ class DupeGuru(RegistrableApplication, Broadcaster):
def export_to_xhtml(self, column_ids):
column_ids = [colid for colid in column_ids if colid.isdigit()]
column_ids = map(int, column_ids)
column_ids = list(map(int, column_ids))
column_ids.sort()
colnames = [col['display'] for i, col in enumerate(self.data.COLUMNS) if i in column_ids]
rows = []
@@ -232,8 +226,8 @@ class DupeGuru(RegistrableApplication, Broadcaster):
dupe = self.selected_dupes[0]
group = self.results.get_group_of_duplicate(dupe)
ref = group.ref
cmd = cmd.replace('%d', unicode(dupe.path))
cmd = cmd.replace('%r', unicode(ref.path))
cmd = cmd.replace('%d', str(dupe.path))
cmd = cmd.replace('%r', str(ref.path))
match = re.match(r'"([^"]+)"(.*)', cmd)
if match is not None:
# This code here is because subprocess. Popen doesn't seem to accept, under Windows,
@@ -249,6 +243,11 @@ class DupeGuru(RegistrableApplication, Broadcaster):
self._start_job(JOB_LOAD, self._do_load)
self.load_ignore_list()
def load_from(self, filename):
def do(j):
self.results.load_from_xml(filename, self._get_file, j)
self._start_job(JOB_LOAD, do)
def load_ignore_list(self):
p = op.join(self.appdata, 'ignore_list.xml')
self.scanner.ignore_list.load_from_xml(p)
@@ -313,7 +312,7 @@ class DupeGuru(RegistrableApplication, Broadcaster):
d.rename(newname)
return True
except (IndexError, fs.FSError) as e:
logging.warning("dupeGuru Warning: %s" % unicode(e))
logging.warning("dupeGuru Warning: %s" % str(e))
return False
def reveal_selected(self):
@@ -324,7 +323,13 @@ class DupeGuru(RegistrableApplication, Broadcaster):
if not op.exists(self.appdata):
os.makedirs(self.appdata)
self.directories.save_to_file(op.join(self.appdata, 'last_directories.xml'))
self.results.save_to_xml(op.join(self.appdata, 'last_results.xml'))
if self.results.is_modified:
self.results.save_to_xml(op.join(self.appdata, 'last_results.xml'))
def save_as(self, filename):
self.results.save_to_xml(filename)
# It's not because we saved it here that we don't want to save it in appdata when we quit
self.results.is_modified = True
def save_ignore_list(self):
if not op.exists(self.appdata):
@@ -339,12 +344,8 @@ class DupeGuru(RegistrableApplication, Broadcaster):
logging.info('Scanning %d files' % len(files))
self.results.groups = self.scanner.GetDupeGroups(files, j)
files = self.directories.get_files()
first_file = first(files)
if first_file is None:
if not self.directories.has_any_file():
raise NoScannableFileError()
if first_file.is_ref and all(f.is_ref for f in files):
raise AllFilesAreRefError()
self.results.groups = []
self._start_job(JOB_SCAN, do)

View File

@@ -49,11 +49,11 @@ class DupeGuru(app.DupeGuru):
#--- Override
@staticmethod
def _open_path(path):
NSWorkspace.sharedWorkspace().openFile_(unicode(path))
NSWorkspace.sharedWorkspace().openFile_(str(path))
@staticmethod
def _reveal_path(path):
NSWorkspace.sharedWorkspace().selectFile_inFileViewerRootedAtPath_(unicode(path), '')
NSWorkspace.sharedWorkspace().selectFile_inFileViewerRootedAtPath_(str(path), '')
def _start_job(self, jobid, func):
try:
@@ -76,6 +76,4 @@ class DupeGuru(app.DupeGuru):
return 0
except app.NoScannableFileError:
return 3
except app.AllFilesAreRefError:
return 1

View File

@@ -46,6 +46,9 @@ class PyDupeGuruBase(PyRegistrable):
def loadResults(self):
self.py.load()
def loadResultsFrom_(self, filename):
self.py.load_from(filename)
def markAll(self):
self.py.mark_all()
@@ -67,6 +70,9 @@ class PyDupeGuruBase(PyRegistrable):
def saveResults(self):
self.py.save()
def saveResultsAs_(self, filename):
self.py.save_as(filename)
#---Actions
def addSelectedToIgnoreList(self):
self.py.add_selected_to_ignore_list()

View File

@@ -11,7 +11,7 @@ from hsutil.str import format_time, FT_DECIMAL, format_size
import time
def format_path(p):
return unicode(p[:-1])
return str(p[:-1])
def format_timestamp(t, delta):
if delta:
@@ -38,4 +38,4 @@ def format_dupe_count(c):
return str(c) if c else '---'
def cmp_value(value):
return value.lower() if isinstance(value, basestring) else value
return value.lower() if isinstance(value, str) else value

View File

@@ -6,7 +6,7 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from lxml import etree
from xml.etree import ElementTree as ET
from hsutil import io
from hsutil.files import FileOrPath
@@ -124,12 +124,19 @@ class Directories(object):
else:
return STATE_NORMAL
def has_any_file(self):
try:
next(self.get_files())
return True
except StopIteration:
return False
def load_from_file(self, infile):
try:
root = etree.parse(infile).getroot()
except:
root = ET.parse(infile).getroot()
except Exception:
return
for rdn in root.iterchildren('root_directory'):
for rdn in root.getiterator('root_directory'):
attrib = rdn.attrib
if 'path' not in attrib:
continue
@@ -138,7 +145,7 @@ class Directories(object):
self.add_path(Path(path))
except (AlreadyThereError, InvalidPathError):
pass
for sn in root.iterchildren('state'):
for sn in root.getiterator('state'):
attrib = sn.attrib
if not ('path' in attrib and 'value' in attrib):
continue
@@ -148,15 +155,15 @@ class Directories(object):
def save_to_file(self, outfile):
with FileOrPath(outfile, 'wb') as fp:
root = etree.Element('directories')
root = ET.Element('directories')
for root_path in self:
root_path_node = etree.SubElement(root, 'root_directory')
root_path_node.set('path', unicode(root_path))
for path, state in self.states.iteritems():
state_node = etree.SubElement(root, 'state')
state_node.set('path', unicode(path))
state_node.set('value', unicode(state))
tree = etree.ElementTree(root)
root_path_node = ET.SubElement(root, 'root_directory')
root_path_node.set('path', str(root_path))
for path, state in self.states.items():
state_node = ET.SubElement(root, 'state')
state_node.set('path', str(path))
state_node.set('value', str(state))
tree = ET.ElementTree(root)
tree.write(fp, encoding='utf-8')
def set_state(self, path, state):

View File

@@ -6,7 +6,7 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from __future__ import division
import difflib
import itertools
import logging
@@ -25,15 +25,15 @@ NO_FIELD_ORDER) = range(3)
JOB_REFRESH_RATE = 100
def getwords(s):
if isinstance(s, unicode):
if isinstance(s, str):
s = normalize('NFD', s)
s = multi_replace(s, "-_&+():;\\[]{}.,<>/?~!@#$*", ' ').lower()
s = ''.join(c for c in s if c in string.ascii_letters + string.digits + string.whitespace)
return filter(None, s.split(' ')) # filter() is to remove empty elements
return [_f for _f in s.split(' ') if _f] # remove empty elements
def getfields(s):
fields = [getwords(field) for field in s.split(' - ')]
return filter(None, fields)
return [_f for _f in fields if _f]
def unpack_fields(fields):
result = []
@@ -118,7 +118,7 @@ def build_word_dict(objects, j=job.nulljob):
def merge_similar_words(word_dict):
"""Take all keys in word_dict that are similar, and merge them together.
"""
keys = word_dict.keys()
keys = list(word_dict.keys())
keys.sort(key=len)# we want the shortest word to stay
while keys:
key = keys.pop(0)
@@ -138,7 +138,7 @@ def reduce_common_words(word_dict, threshold):
Because if we remove them, we will miss some duplicates!
"""
uncommon_words = set(word for word, objects in word_dict.items() if len(objects) < threshold)
for word, objects in word_dict.items():
for word, objects in list(word_dict.items()):
if len(objects) < threshold:
continue
reduced = set()

View File

@@ -13,7 +13,7 @@ from tempfile import mkdtemp
# Yes, this is a very low-tech solution, but at least it doesn't have all these annoying dependency
# and resource problems.
MAIN_TEMPLATE = u"""
MAIN_TEMPLATE = """
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Strict//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'>
<html xmlns="http://www.w3.org/1999/xhtml">
@@ -104,33 +104,33 @@ $rows
</html>
"""
COLHEADERS_TEMPLATE = u"<th>{name}</th>"
COLHEADERS_TEMPLATE = "<th>{name}</th>"
ROW_TEMPLATE = u"""
ROW_TEMPLATE = """
<tr>
<td class="{indented}">{filename}</td>{cells}
</tr>
"""
CELL_TEMPLATE = u"""<td>{value}</td>"""
CELL_TEMPLATE = """<td>{value}</td>"""
def export_to_xhtml(colnames, rows):
# a row is a list of values with the first value being a flag indicating if the row should be indented
if rows:
assert len(rows[0]) == len(colnames) + 1 # + 1 is for the "indented" flag
colheaders = u''.join(COLHEADERS_TEMPLATE.format(name=name) for name in colnames)
colheaders = ''.join(COLHEADERS_TEMPLATE.format(name=name) for name in colnames)
rendered_rows = []
for row in rows:
# [2:] is to remove the indented flag + filename
indented = u'indented' if row[0] else u''
indented = 'indented' if row[0] else ''
filename = row[1]
cells = u''.join(CELL_TEMPLATE.format(value=value) for value in row[2:])
cells = ''.join(CELL_TEMPLATE.format(value=value) for value in row[2:])
rendered_rows.append(ROW_TEMPLATE.format(indented=indented, filename=filename, cells=cells))
rendered_rows = u''.join(rendered_rows)
rendered_rows = ''.join(rendered_rows)
# The main template can't use format because the css code uses {}
content = MAIN_TEMPLATE.replace('$colheaders', colheaders).replace('$rows', rendered_rows)
folder = mkdtemp()
destpath = op.join(folder, u'export.htm')
destpath = op.join(folder, 'export.htm')
fp = open(destpath, 'w')
fp.write(content.encode('utf-8'))
fp.close()

View File

@@ -12,7 +12,7 @@
# resulting needless complexity and memory usage. It's been a while since I wanted to do that fork,
# and I'm doing it now.
from __future__ import unicode_literals
import hashlib
import logging
@@ -25,13 +25,13 @@ class FSError(Exception):
cls_message = "An error has occured on '{name}' in '{parent}'"
def __init__(self, fsobject, parent=None):
message = self.cls_message
if isinstance(fsobject, basestring):
if isinstance(fsobject, str):
name = fsobject
elif isinstance(fsobject, File):
name = fsobject.name
else:
name = ''
parentname = unicode(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))
@@ -55,7 +55,6 @@ class OperationError(FSError):
class File(object):
INITIAL_INFO = {
'size': 0,
'ctime': 0,
'mtime': 0,
'md5': '',
'md5partial': '',
@@ -82,10 +81,9 @@ class File(object):
raise AttributeError()
def _read_info(self, field):
if field in ('size', 'ctime', 'mtime'):
if field in ('size', 'mtime'):
stats = io.stat(self.path)
self.size = nonone(stats.st_size, 0)
self.ctime = nonone(stats.st_ctime, 0)
self.mtime = nonone(stats.st_mtime, 0)
elif field == 'md5partial':
try:
@@ -119,7 +117,7 @@ class File(object):
If `attrnames` is not None, caches only attrnames.
"""
if attrnames is None:
attrnames = self.INITIAL_INFO.keys()
attrnames = list(self.INITIAL_INFO.keys())
for attrname in attrnames:
if attrname not in self.__dict__:
self._read_info(attrname)

View File

@@ -32,7 +32,7 @@ class DetailsPanel(GUIObject):
ref = group.ref if group is not None and group.ref is not dupe else None
l2 = self.app._get_display_info(ref, group, False)
names = [c['display'] for c in self.app.data.COLUMNS]
self._table = zip(names, l1, l2)
self._table = list(zip(names, l1, l2))
#--- Public
def row_count(self):

View File

@@ -62,7 +62,7 @@ class DirectoryTree(GUIObject, Tree):
def _refresh(self):
self.clear()
for path in self.app.directories:
self.append(DirectoryNode(self.app, path, unicode(path)))
self.append(DirectoryNode(self.app, path, str(path)))
def add_directory(self, path):
self.app.add_directory(path)

View File

@@ -39,5 +39,5 @@ class ProblemRow(Row):
Row.__init__(self, table)
self.dupe = dupe
self.msg = msg
self.path = unicode(dupe.path)
self.path = str(dupe.path)

View File

@@ -63,7 +63,7 @@ class ResultTree(GUIObject, Tree):
def _select_nodes(self, nodes):
Tree._select_nodes(self, nodes)
self.app._select_dupes(map(attrgetter('_dupe'), nodes))
self.app._select_dupes(list(map(attrgetter('_dupe'), nodes)))
#--- Private
def _refresh(self):

View File

@@ -6,7 +6,7 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from lxml import etree
from xml.etree import ElementTree as ET
from hsutil.files import FileOrPath
@@ -22,7 +22,7 @@ class IgnoreList(object):
self._count = 0
def __iter__(self):
for first,seconds in self._ignored.iteritems():
for first,seconds in self._ignored.items():
for second in seconds:
yield (first,second)
@@ -77,14 +77,16 @@ class IgnoreList(object):
infile can be a file object or a filename.
"""
try:
root = etree.parse(infile).getroot()
root = ET.parse(infile).getroot()
except Exception:
return
for fn in root.iterchildren('file'):
file_elems = (e for e in root if e.tag == 'file')
for fn in file_elems:
file_path = fn.get('path')
if not file_path:
continue
for sfn in fn.iterchildren('file'):
subfile_elems = (e for e in fn if e.tag == 'file')
for sfn in subfile_elems:
subfile_path = sfn.get('path')
if subfile_path:
self.Ignore(file_path, subfile_path)
@@ -94,14 +96,14 @@ class IgnoreList(object):
outfile can be a file object or a filename.
"""
root = etree.Element('ignore_list')
root = ET.Element('ignore_list')
for filename, subfiles in self._ignored.items():
file_node = etree.SubElement(root, 'file')
file_node = ET.SubElement(root, 'file')
file_node.set('path', filename)
for subfilename in subfiles:
subfile_node = etree.SubElement(file_node, 'file')
subfile_node = ET.SubElement(file_node, 'file')
subfile_node.set('path', subfilename)
tree = etree.ElementTree(root)
tree = ET.ElementTree(root)
with FileOrPath(outfile, 'wb') as fp:
tree.write(fp, encoding='utf-8')

View File

@@ -8,7 +8,7 @@
import logging
import re
from lxml import etree
from xml.etree import ElementTree as ET
from . import engine
from hscommon.job import nulljob
@@ -33,6 +33,7 @@ class Results(Markable):
self.__marked_size = 0
self.data = data_module
self.problems = [] # (dupe, error_msg)
self.is_modified = False
def _did_mark(self, dupe):
self.__marked_size += dupe.size
@@ -115,6 +116,7 @@ class Results(Markable):
self.__group_of_duplicate[dupe] = g
if not hasattr(dupe, 'is_ref'):
dupe.is_ref = False
self.is_modified = True
old_filters = nonone(self.__filters, [])
self.apply_filter(None)
for filter_str in old_filters:
@@ -147,7 +149,7 @@ class Results(Markable):
self.__filters.append(filter_str)
if self.__filtered_dupes is None:
self.__filtered_dupes = flatten(g[:] for g in self.groups)
self.__filtered_dupes = set(dupe for dupe in self.__filtered_dupes if filter_re.search(unicode(dupe.path)))
self.__filtered_dupes = set(dupe for dupe in self.__filtered_dupes if filter_re.search(str(dupe.path)))
filtered_groups = set()
for dupe in self.__filtered_dupes:
filtered_groups.add(self.get_group_of_duplicate(dupe))
@@ -176,16 +178,16 @@ class Results(Markable):
self.apply_filter(None)
try:
root = etree.parse(infile).getroot()
root = ET.parse(infile).getroot()
except Exception:
return
group_elems = list(root.iterchildren('group'))
group_elems = list(root.getiterator('group'))
groups = []
marked = set()
for group_elem in j.iter_with_progress(group_elems, every=100):
group = engine.Group()
dupes = []
for file_elem in group_elem.iterchildren('file'):
for file_elem in group_elem.getiterator('file'):
path = file_elem.get('path')
words = file_elem.get('words', '')
if not path:
@@ -198,7 +200,7 @@ class Results(Markable):
dupes.append(file)
if file_elem.get('marked') == 'y':
marked.add(file)
for match_elem in group_elem.iterchildren('match'):
for match_elem in group_elem.getiterator('match'):
try:
attrs = match_elem.attrib
first_file = dupes[int(attrs['first'])]
@@ -216,6 +218,7 @@ class Results(Markable):
self.groups = groups
for dupe_file in marked:
self.mark(dupe_file)
self.is_modified = False
def make_ref(self, dupe):
g = self.get_group_of_duplicate(dupe)
@@ -229,6 +232,7 @@ class Results(Markable):
self.__total_count -= 1
self.__total_size -= dupe.size
self.__dupes = None
self.is_modified = True
def perform_on_marked(self, func, remove_from_results):
# Performs `func` on all marked dupes. If an EnvironmentError is raised during the call,
@@ -241,7 +245,7 @@ class Results(Markable):
func(dupe)
to_remove.append(dupe)
except EnvironmentError as e:
self.problems.append((dupe, unicode(e)))
self.problems.append((dupe, str(e)))
if remove_from_results:
self.remove_duplicates(to_remove)
self.mark_none()
@@ -269,13 +273,14 @@ class Results(Markable):
for group in affected_groups:
group.discard_matches()
self.__dupes = None
self.is_modified = True
def save_to_xml(self, outfile):
self.apply_filter(None)
root = etree.Element('results')
root = ET.Element('results')
# writer = XMLGenerator(outfile, 'utf-8')
for g in self.groups:
group_elem = etree.SubElement(root, 'group')
group_elem = ET.SubElement(root, 'group')
dupe2index = {}
for index, d in enumerate(g):
dupe2index[d] = index
@@ -283,22 +288,23 @@ class Results(Markable):
words = engine.unpack_fields(d.words)
except AttributeError:
words = ()
file_elem = etree.SubElement(group_elem, 'file')
file_elem = ET.SubElement(group_elem, 'file')
try:
file_elem.set('path', unicode(d.path))
file_elem.set('path', str(d.path))
file_elem.set('words', ','.join(words))
except ValueError: # If there's an invalid character, just skip the file
file_elem.set('path', '')
file_elem.set('is_ref', ('y' if d.is_ref else 'n'))
file_elem.set('marked', ('y' if self.is_marked(d) else 'n'))
for match in g.matches:
match_elem = etree.SubElement(group_elem, 'match')
match_elem.set('first', unicode(dupe2index[match.first]))
match_elem.set('second', unicode(dupe2index[match.second]))
match_elem.set('percentage', unicode(int(match.percentage)))
tree = etree.ElementTree(root)
match_elem = ET.SubElement(group_elem, 'match')
match_elem.set('first', str(dupe2index[match.first]))
match_elem.set('second', str(dupe2index[match.second]))
match_elem.set('percentage', str(int(match.percentage)))
tree = ET.ElementTree(root)
with FileOrPath(outfile, 'wb') as fp:
tree.write(fp, encoding='utf-8')
self.is_modified = False
def sort_dupes(self, key, asc=True, delta=False):
if not self.__dupes:

View File

@@ -1,109 +1,123 @@
# Created By: Virgil Dupras
# Created On: 2006/03/03
# Copyright 2010 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" 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/hs_license
import logging
from hscommon import job
from hsutil import io
from hsutil.misc import dedupe
from hsutil.str import get_file_ext, rem_file_ext
from . import engine
from .ignore import IgnoreList
(SCAN_TYPE_FILENAME,
SCAN_TYPE_FIELDS,
SCAN_TYPE_FIELDS_NO_ORDER,
SCAN_TYPE_TAG,
UNUSED, # Must not be removed. Constants here are what scan_type in the prefs are.
SCAN_TYPE_CONTENT,
SCAN_TYPE_CONTENT_AUDIO) = range(7)
SCANNABLE_TAGS = ['track', 'artist', 'album', 'title', 'genre', 'year']
class Scanner(object):
def __init__(self):
self.ignore_list = IgnoreList()
self.discarded_file_count = 0
def _getmatches(self, files, j):
if self.size_threshold:
j = j.start_subjob([2, 8])
for f in j.iter_with_progress(files, 'Read size of %d/%d files'):
f.size # pre-read, makes a smoother progress if read here (especially for bundles)
files = [f for f in files if f.size >= self.size_threshold]
if self.scan_type in (SCAN_TYPE_CONTENT, SCAN_TYPE_CONTENT_AUDIO):
sizeattr = 'size' if self.scan_type == SCAN_TYPE_CONTENT else 'audiosize'
return engine.getmatches_by_contents(files, sizeattr, partial=self.scan_type==SCAN_TYPE_CONTENT_AUDIO, j=j)
else:
j = j.start_subjob([2, 8])
kw = {}
kw['match_similar_words'] = self.match_similar_words
kw['weight_words'] = self.word_weighting
kw['min_match_percentage'] = self.min_match_percentage
if self.scan_type == SCAN_TYPE_FIELDS_NO_ORDER:
self.scan_type = SCAN_TYPE_FIELDS
kw['no_field_order'] = True
func = {
SCAN_TYPE_FILENAME: lambda f: engine.getwords(rem_file_ext(f.name)),
SCAN_TYPE_FIELDS: lambda f: engine.getfields(rem_file_ext(f.name)),
SCAN_TYPE_TAG: lambda f: [engine.getwords(unicode(getattr(f, attrname))) for attrname in SCANNABLE_TAGS if attrname in self.scanned_tags],
}[self.scan_type]
for f in j.iter_with_progress(files, 'Read metadata of %d/%d files'):
f.words = func(f)
return engine.getmatches(files, j=j, **kw)
@staticmethod
def _key_func(dupe):
return (not dupe.is_ref, -dupe.size)
@staticmethod
def _tie_breaker(ref, dupe):
refname = rem_file_ext(ref.name).lower()
dupename = rem_file_ext(dupe.name).lower()
if 'copy' in refname and 'copy' not in dupename:
return True
if refname.startswith(dupename) and (refname[len(dupename):].strip().isdigit()):
return True
return len(dupe.path) > len(ref.path)
def GetDupeGroups(self, files, j=job.nulljob):
j = j.start_subjob([8, 2])
for f in [f for f in files if not hasattr(f, 'is_ref')]:
f.is_ref = False
logging.info('Getting matches')
matches = self._getmatches(files, j)
logging.info('Found %d matches' % len(matches))
j.set_progress(100, 'Removing false matches')
if not self.mix_file_kind:
matches = [m for m in matches if get_file_ext(m.first.name) == get_file_ext(m.second.name)]
matches = [m for m in matches if io.exists(m.first.path) and io.exists(m.second.path)]
if self.ignore_list:
j = j.start_subjob(2)
iter_matches = j.iter_with_progress(matches, 'Processed %d/%d matches against the ignore list')
matches = [m for m in iter_matches
if not self.ignore_list.AreIgnored(unicode(m.first.path), unicode(m.second.path))]
logging.info('Grouping matches')
groups = engine.get_groups(matches, j)
matched_files = dedupe([m.first for m in matches] + [m.second for m in matches])
self.discarded_file_count = len(matched_files) - sum(len(g) for g in groups)
groups = [g for g in groups if any(not f.is_ref for f in g)]
logging.info('Created %d groups' % len(groups))
j.set_progress(100, 'Doing group prioritization')
for g in groups:
g.prioritize(self._key_func, self._tie_breaker)
return groups
match_similar_words = False
min_match_percentage = 80
mix_file_kind = True
scan_type = SCAN_TYPE_FILENAME
scanned_tags = set(['artist', 'title'])
size_threshold = 0
word_weighting = False
# Created By: Virgil Dupras
# Created On: 2006/03/03
# Copyright 2010 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" 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/hs_license
import logging
import re
from hscommon import job
from hsutil import io
from hsutil.misc import dedupe
from hsutil.str import get_file_ext, rem_file_ext
from . import engine
from .ignore import IgnoreList
class ScanType:
Filename = 0
Fields = 1
FieldsNoOrder = 2
Tag = 3
# number 4 is obsolete
Contents = 5
ContentsAudio = 6
SCANNABLE_TAGS = ['track', 'artist', 'album', 'title', 'genre', 'year']
RE_DIGIT_ENDING = re.compile(r'\d+|\(\d+\)|\[\d+\]|{\d+}')
def is_same_with_digit(name, refname):
# Returns True if name is the same as refname, but with digits (with brackets or not) at the end
if not name.startswith(refname):
return False
end = name[len(refname):].strip()
return RE_DIGIT_ENDING.match(end) is not None
class Scanner(object):
def __init__(self):
self.ignore_list = IgnoreList()
self.discarded_file_count = 0
def _getmatches(self, files, j):
if self.size_threshold:
j = j.start_subjob([2, 8])
for f in j.iter_with_progress(files, 'Read size of %d/%d files'):
f.size # pre-read, makes a smoother progress if read here (especially for bundles)
files = [f for f in files if f.size >= self.size_threshold]
if self.scan_type in (ScanType.Contents, ScanType.ContentsAudio):
sizeattr = 'size' if self.scan_type == ScanType.Contents else 'audiosize'
return engine.getmatches_by_contents(files, sizeattr, partial=self.scan_type==ScanType.ContentsAudio, j=j)
else:
j = j.start_subjob([2, 8])
kw = {}
kw['match_similar_words'] = self.match_similar_words
kw['weight_words'] = self.word_weighting
kw['min_match_percentage'] = self.min_match_percentage
if self.scan_type == ScanType.FieldsNoOrder:
self.scan_type = ScanType.Fields
kw['no_field_order'] = True
func = {
ScanType.Filename: lambda f: engine.getwords(rem_file_ext(f.name)),
ScanType.Fields: lambda f: engine.getfields(rem_file_ext(f.name)),
ScanType.Tag: lambda f: [engine.getwords(str(getattr(f, attrname))) for attrname in SCANNABLE_TAGS if attrname in self.scanned_tags],
}[self.scan_type]
for f in j.iter_with_progress(files, 'Read metadata of %d/%d files'):
f.words = func(f)
return engine.getmatches(files, j=j, **kw)
@staticmethod
def _key_func(dupe):
return (not dupe.is_ref, -dupe.size)
@staticmethod
def _tie_breaker(ref, dupe):
refname = rem_file_ext(ref.name).lower()
dupename = rem_file_ext(dupe.name).lower()
if 'copy' in dupename:
return False
if 'copy' in refname:
return True
if is_same_with_digit(dupename, refname):
return False
if is_same_with_digit(refname, dupename):
return True
return len(dupe.path) > len(ref.path)
def GetDupeGroups(self, files, j=job.nulljob):
j = j.start_subjob([8, 2])
for f in [f for f in files if not hasattr(f, 'is_ref')]:
f.is_ref = False
logging.info('Getting matches')
matches = self._getmatches(files, j)
logging.info('Found %d matches' % len(matches))
j.set_progress(100, 'Removing false matches')
if not self.mix_file_kind:
matches = [m for m in matches if get_file_ext(m.first.name) == get_file_ext(m.second.name)]
matches = [m for m in matches if io.exists(m.first.path) and io.exists(m.second.path)]
if self.ignore_list:
j = j.start_subjob(2)
iter_matches = j.iter_with_progress(matches, 'Processed %d/%d matches against the ignore list')
matches = [m for m in iter_matches
if not self.ignore_list.AreIgnored(str(m.first.path), str(m.second.path))]
logging.info('Grouping matches')
groups = engine.get_groups(matches, j)
matched_files = dedupe([m.first for m in matches] + [m.second for m in matches])
self.discarded_file_count = len(matched_files) - sum(len(g) for g in groups)
groups = [g for g in groups if any(not f.is_ref for f in g)]
logging.info('Created %d groups' % len(groups))
j.set_progress(100, 'Doing group prioritization')
for g in groups:
g.prioritize(self._key_func, self._tie_breaker)
return groups
match_similar_words = False
min_match_percentage = 80
mix_file_kind = True
scan_type = ScanType.Filename
scanned_tags = set(['artist', 'title'])
size_threshold = 0
word_weighting = False

View File

@@ -109,7 +109,7 @@ class TCDupeGuru(TestCase):
def test_Scan_with_objects_evaluating_to_false(self):
class FakeFile(fs.File):
def __nonzero__(self):
def __bool__(self):
return False
@@ -118,7 +118,7 @@ class TCDupeGuru(TestCase):
f1, f2 = [FakeFile('foo') for i in range(2)]
f1.is_ref, f2.is_ref = (False, False)
assert not (bool(f1) and bool(f2))
app.directories.get_files = lambda: [f1, f2]
app.directories.get_files = lambda: iter([f1, f2])
app.directories._dirs.append('this is just so Scan() doesnt return 3')
app.start_scanning() # no exception
@@ -200,11 +200,11 @@ class TCDupeGuruWithResults(TestCase):
if expected is not None:
expected = set(expected)
not_called = expected - calls
assert not not_called, u"These calls haven't been made: {0}".format(not_called)
assert not not_called, "These calls haven't been made: {0}".format(not_called)
if not_expected is not None:
not_expected = set(not_expected)
called = not_expected & calls
assert not called, u"These calls shouldn't have been made: {0}".format(called)
assert not called, "These calls shouldn't have been made: {0}".format(called)
gui.clear_calls()
def clear_gui_calls(self):
@@ -409,9 +409,9 @@ class TCDupeGuruWithResults(TestCase):
def test_only_unicode_is_added_to_ignore_list(self):
def FakeIgnore(first,second):
if not isinstance(first,unicode):
if not isinstance(first,str):
self.fail()
if not isinstance(second,unicode):
if not isinstance(second,str):
self.fail()
app = self.app
@@ -423,11 +423,11 @@ class TCDupeGuruWithResults(TestCase):
class TCDupeGuru_renameSelected(TestCase):
def setUp(self):
p = self.tmppath()
fp = open(unicode(p + 'foo bar 1'),mode='w')
fp = open(str(p + 'foo bar 1'),mode='w')
fp.close()
fp = open(unicode(p + 'foo bar 2'),mode='w')
fp = open(str(p + 'foo bar 2'),mode='w')
fp.close()
fp = open(unicode(p + 'foo bar 3'),mode='w')
fp = open(str(p + 'foo bar 3'),mode='w')
fp.close()
files = fs.get_files(p)
matches = engine.getmatches(files)

View File

@@ -82,8 +82,8 @@ class TCDirectories(TestCase):
def test_AddPath_non_latin(self):
p = Path(self.tmpdir())
to_add = p + u'unicode\u201a'
os.mkdir(unicode(to_add))
to_add = p + 'unicode\u201a'
os.mkdir(str(to_add))
d = Directories()
try:
d.add_path(to_add)
@@ -111,7 +111,7 @@ class TCDirectories(TestCase):
self.assertEqual(STATE_REFERENCE,d.get_state(p))
self.assertEqual(STATE_REFERENCE,d.get_state(p + 'dir1'))
self.assertEqual(1,len(d.states))
self.assertEqual(p,d.states.keys()[0])
self.assertEqual(p,list(d.states.keys())[0])
self.assertEqual(STATE_REFERENCE,d.states[p])
def test_get_state_with_path_not_there(self):
@@ -213,11 +213,11 @@ class TCDirectories(TestCase):
def test_unicode_save(self):
d = Directories()
p1 = self.tmppath() + u'hello\xe9'
p1 = self.tmppath() + 'hello\xe9'
io.mkdir(p1)
io.mkdir(p1 + u'foo\xe9')
io.mkdir(p1 + 'foo\xe9')
d.add_path(p1)
d.set_state(p1 + u'foo\xe9', STATE_EXCLUDED)
d.set_state(p1 + 'foo\xe9', STATE_EXCLUDED)
tmpxml = op.join(self.tmpdir(), 'directories_testunit.xml')
try:
d.save_to_file(tmpxml)

View File

@@ -62,12 +62,12 @@ class TCgetwords(TestCase):
def test_splitter_chars(self):
self.assertEqual(
[chr(i) for i in xrange(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")
)
def test_joiner_chars(self):
self.assertEqual(["aec"], getwords(u"a'e\u0301c"))
self.assertEqual(["aec"], getwords("a'e\u0301c"))
def test_empty(self):
self.assertEqual([], getwords(''))
@@ -76,7 +76,7 @@ class TCgetwords(TestCase):
self.assertEqual(['foo', 'bar'], getwords('FOO BAR'))
def test_decompose_unicode(self):
self.assertEqual(getwords(u'foo\xe9bar'), ['fooebar'])
self.assertEqual(getwords('foo\xe9bar'), ['fooebar'])
class TCgetfields(TestCase):
@@ -768,7 +768,7 @@ class TCget_groups(TestCase):
self.assert_(o3 in g)
def test_four_sized_group(self):
l = [NamedObject("foobar") for i in xrange(4)]
l = [NamedObject("foobar") for i in range(4)]
m = getmatches(l)
r = get_groups(m)
self.assertEqual(1,len(r))

View File

@@ -6,8 +6,8 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
import cStringIO
from lxml import etree
import io
from xml.etree import ElementTree as ET
from hsutil.testutil import eq_
@@ -59,10 +59,10 @@ def test_save_to_xml():
il.Ignore('foo','bar')
il.Ignore('foo','bleh')
il.Ignore('bleh','bar')
f = cStringIO.StringIO()
f = io.BytesIO()
il.save_to_xml(f)
f.seek(0)
doc = etree.parse(f)
doc = ET.parse(f)
root = doc.getroot()
eq_(root.tag, 'ignore_list')
eq_(len(root), 2)
@@ -76,19 +76,18 @@ def test_SaveThenLoad():
il.Ignore('foo', 'bar')
il.Ignore('foo', 'bleh')
il.Ignore('bleh', 'bar')
il.Ignore(u'\u00e9', 'bar')
f = cStringIO.StringIO()
il.Ignore('\u00e9', 'bar')
f = io.BytesIO()
il.save_to_xml(f)
f.seek(0)
f.seek(0)
il = IgnoreList()
il.load_from_xml(f)
eq_(4,len(il))
assert il.AreIgnored(u'\u00e9','bar')
assert il.AreIgnored('\u00e9','bar')
def test_LoadXML_with_empty_file_tags():
f = cStringIO.StringIO()
f.write('<?xml version="1.0" encoding="utf-8"?><ignore_list><file><file/></file></ignore_list>')
f = io.BytesIO()
f.write(b'<?xml version="1.0" encoding="utf-8"?><ignore_list><file><file/></file></ignore_list>')
f.seek(0)
il = IgnoreList()
il.load_from_xml(f)
@@ -130,12 +129,12 @@ def test_filter():
def test_save_with_non_ascii_items():
il = IgnoreList()
il.Ignore(u'\xac', u'\xbf')
f = cStringIO.StringIO()
il.Ignore('\xac', '\xbf')
f = io.BytesIO()
try:
il.save_to_xml(f)
except Exception as e:
raise AssertionError(unicode(e))
raise AssertionError(str(e))
def test_len():
il = IgnoreList()

View File

@@ -7,10 +7,10 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
import StringIO
import io
import os.path as op
from lxml import etree
from xml.etree import ElementTree as ET
from hsutil.path import Path
from hsutil.testutil import eq_
@@ -25,7 +25,7 @@ class NamedObject(engine_test.NamedObject):
path = property(lambda x:Path('basepath') + x.name)
is_ref = False
def __nonzero__(self):
def __bool__(self):
return False #Make sure that operations are made correctly when the bool value of files is false.
# Returns a group set that looks like that:
@@ -54,21 +54,24 @@ class TCResultsEmpty(TestCase):
self.test_stat_line() # make sure that the stats line isn't saying we applied a '[' filter
def test_stat_line(self):
self.assertEqual("0 / 0 (0.00 B / 0.00 B) duplicates marked.",self.results.stat_line)
eq_("0 / 0 (0.00 B / 0.00 B) duplicates marked.",self.results.stat_line)
def test_groups(self):
self.assertEqual(0,len(self.results.groups))
eq_(0,len(self.results.groups))
def test_get_group_of_duplicate(self):
self.assert_(self.results.get_group_of_duplicate('foo') is None)
assert self.results.get_group_of_duplicate('foo') is None
def test_save_to_xml(self):
f = StringIO.StringIO()
f = io.BytesIO()
self.results.save_to_xml(f)
f.seek(0)
doc = etree.parse(f)
doc = ET.parse(f)
root = doc.getroot()
self.assertEqual('results', root.tag)
eq_('results', root.tag)
def test_is_modified(self):
assert not self.results.is_modified
class TCResultsWithSomeGroups(TestCase):
@@ -78,57 +81,57 @@ class TCResultsWithSomeGroups(TestCase):
self.results.groups = self.groups
def test_stat_line(self):
self.assertEqual("0 / 3 (0.00 B / 1.01 KB) duplicates marked.",self.results.stat_line)
eq_("0 / 3 (0.00 B / 1.01 KB) duplicates marked.",self.results.stat_line)
def test_groups(self):
self.assertEqual(2,len(self.results.groups))
eq_(2,len(self.results.groups))
def test_get_group_of_duplicate(self):
for o in self.objects:
g = self.results.get_group_of_duplicate(o)
self.assert_(isinstance(g, engine.Group))
self.assert_(o in g)
self.assert_(self.results.get_group_of_duplicate(self.groups[0]) is None)
assert isinstance(g, engine.Group)
assert o in g
assert self.results.get_group_of_duplicate(self.groups[0]) is None
def test_remove_duplicates(self):
g1,g2 = self.results.groups
self.results.remove_duplicates([g1.dupes[0]])
self.assertEqual(2,len(g1))
self.assert_(g1 in self.results.groups)
eq_(2,len(g1))
assert g1 in self.results.groups
self.results.remove_duplicates([g1.ref])
self.assertEqual(2,len(g1))
self.assert_(g1 in self.results.groups)
eq_(2,len(g1))
assert g1 in self.results.groups
self.results.remove_duplicates([g1.dupes[0]])
self.assertEqual(0,len(g1))
self.assert_(g1 not in self.results.groups)
eq_(0,len(g1))
assert g1 not in self.results.groups
self.results.remove_duplicates([g2.dupes[0]])
self.assertEqual(0,len(g2))
self.assert_(g2 not in self.results.groups)
self.assertEqual(0,len(self.results.groups))
eq_(0,len(g2))
assert g2 not in self.results.groups
eq_(0,len(self.results.groups))
def test_remove_duplicates_with_ref_files(self):
g1,g2 = self.results.groups
self.objects[0].is_ref = True
self.objects[1].is_ref = True
self.results.remove_duplicates([self.objects[2]])
self.assertEqual(0,len(g1))
self.assert_(g1 not in self.results.groups)
eq_(0,len(g1))
assert g1 not in self.results.groups
def test_make_ref(self):
g = self.results.groups[0]
d = g.dupes[0]
self.results.make_ref(d)
self.assert_(d is g.ref)
assert d is g.ref
def test_sort_groups(self):
self.results.make_ref(self.objects[1]) #We want to make the 1024 sized object to go ref.
g1,g2 = self.groups
self.results.sort_groups(2) #2 is the key for size
self.assert_(self.results.groups[0] is g2)
self.assert_(self.results.groups[1] is g1)
assert self.results.groups[0] is g2
assert self.results.groups[1] is g1
self.results.sort_groups(2,False)
self.assert_(self.results.groups[0] is g1)
self.assert_(self.results.groups[1] is g2)
assert self.results.groups[0] is g1
assert self.results.groups[1] is g2
def test_set_groups_when_sorted(self):
self.results.make_ref(self.objects[1]) #We want to make the 1024 sized object to go ref.
@@ -137,24 +140,24 @@ class TCResultsWithSomeGroups(TestCase):
g1,g2 = groups
g1.switch_ref(objects[1])
self.results.groups = groups
self.assert_(self.results.groups[0] is g2)
self.assert_(self.results.groups[1] is g1)
assert self.results.groups[0] is g2
assert self.results.groups[1] is g1
def test_get_dupe_list(self):
self.assertEqual([self.objects[1],self.objects[2],self.objects[4]],self.results.dupes)
eq_([self.objects[1],self.objects[2],self.objects[4]],self.results.dupes)
def test_dupe_list_is_cached(self):
self.assert_(self.results.dupes is self.results.dupes)
assert self.results.dupes is self.results.dupes
def test_dupe_list_cache_is_invalidated_when_needed(self):
o1,o2,o3,o4,o5 = self.objects
self.assertEqual([o2,o3,o5],self.results.dupes)
eq_([o2,o3,o5],self.results.dupes)
self.results.make_ref(o2)
self.assertEqual([o1,o3,o5],self.results.dupes)
eq_([o1,o3,o5],self.results.dupes)
objects,matches,groups = GetTestGroups()
o1,o2,o3,o4,o5 = objects
self.results.groups = groups
self.assertEqual([o2,o3,o5],self.results.dupes)
eq_([o2,o3,o5],self.results.dupes)
def test_dupe_list_sort(self):
o1,o2,o3,o4,o5 = self.objects
@@ -164,9 +167,9 @@ class TCResultsWithSomeGroups(TestCase):
o4.size = 2
o5.size = 1
self.results.sort_dupes(2)
self.assertEqual([o5,o3,o2],self.results.dupes)
eq_([o5,o3,o2],self.results.dupes)
self.results.sort_dupes(2,False)
self.assertEqual([o2,o3,o5],self.results.dupes)
eq_([o2,o3,o5],self.results.dupes)
def test_dupe_list_remember_sort(self):
o1,o2,o3,o4,o5 = self.objects
@@ -177,7 +180,7 @@ class TCResultsWithSomeGroups(TestCase):
o5.size = 1
self.results.sort_dupes(2)
self.results.make_ref(o2)
self.assertEqual([o5,o3,o1],self.results.dupes)
eq_([o5,o3,o1],self.results.dupes)
def test_dupe_list_sort_delta_values(self):
o1,o2,o3,o4,o5 = self.objects
@@ -187,19 +190,69 @@ class TCResultsWithSomeGroups(TestCase):
o4.size = 20
o5.size = 1 #-19
self.results.sort_dupes(2,delta=True)
self.assertEqual([o5,o2,o3],self.results.dupes)
eq_([o5,o2,o3],self.results.dupes)
def test_sort_empty_list(self):
#There was an infinite loop when sorting an empty list.
r = Results(data)
r.sort_dupes(0)
self.assertEqual([],r.dupes)
eq_([],r.dupes)
def test_dupe_list_update_on_remove_duplicates(self):
o1,o2,o3,o4,o5 = self.objects
self.assertEqual(3,len(self.results.dupes))
eq_(3,len(self.results.dupes))
self.results.remove_duplicates([o2])
self.assertEqual(2,len(self.results.dupes))
eq_(2,len(self.results.dupes))
def test_is_modified(self):
# Changing the groups sets the modified flag
assert self.results.is_modified
def test_is_modified_after_save_and_load(self):
# Saving/Loading a file sets the modified flag back to False
def get_file(path):
return [f for f in self.objects if str(f.path) == path][0]
f = io.BytesIO()
self.results.save_to_xml(f)
assert not self.results.is_modified
self.results.groups = self.groups # sets the flag back
f.seek(0)
self.results.load_from_xml(f, get_file)
assert not self.results.is_modified
class ResultsWithSavedResults(TestCase):
def setUp(self):
self.results = Results(data)
self.objects,self.matches,self.groups = GetTestGroups()
self.results.groups = self.groups
self.f = io.BytesIO()
self.results.save_to_xml(self.f)
self.f.seek(0)
def test_is_modified(self):
# Saving a file sets the modified flag back to False
assert not self.results.is_modified
def test_is_modified_after_load(self):
# Loading a file sets the modified flag back to False
def get_file(path):
return [f for f in self.objects if str(f.path) == path][0]
self.results.groups = self.groups # sets the flag back
self.results.load_from_xml(self.f, get_file)
assert not self.results.is_modified
def test_is_modified_after_remove(self):
# Removing dupes sets the modified flag
self.results.remove_duplicates([self.results.groups[0].dupes[0]])
assert self.results.is_modified
def test_is_modified_after_make_ref(self):
# Making a dupe ref sets the modified flag
self.results.make_ref(self.results.groups[0].dupes[0])
assert self.results.is_modified
class TCResultsMarkings(TestCase):
@@ -209,27 +262,27 @@ class TCResultsMarkings(TestCase):
self.results.groups = self.groups
def test_stat_line(self):
self.assertEqual("0 / 3 (0.00 B / 1.01 KB) duplicates marked.",self.results.stat_line)
eq_("0 / 3 (0.00 B / 1.01 KB) duplicates marked.",self.results.stat_line)
self.results.mark(self.objects[1])
self.assertEqual("1 / 3 (1.00 KB / 1.01 KB) duplicates marked.",self.results.stat_line)
eq_("1 / 3 (1.00 KB / 1.01 KB) duplicates marked.",self.results.stat_line)
self.results.mark_invert()
self.assertEqual("2 / 3 (2.00 B / 1.01 KB) duplicates marked.",self.results.stat_line)
eq_("2 / 3 (2.00 B / 1.01 KB) duplicates marked.",self.results.stat_line)
self.results.mark_invert()
self.results.unmark(self.objects[1])
self.results.mark(self.objects[2])
self.results.mark(self.objects[4])
self.assertEqual("2 / 3 (2.00 B / 1.01 KB) duplicates marked.",self.results.stat_line)
eq_("2 / 3 (2.00 B / 1.01 KB) duplicates marked.",self.results.stat_line)
self.results.mark(self.objects[0]) #this is a ref, it can't be counted
self.assertEqual("2 / 3 (2.00 B / 1.01 KB) duplicates marked.",self.results.stat_line)
eq_("2 / 3 (2.00 B / 1.01 KB) duplicates marked.",self.results.stat_line)
self.results.groups = self.groups
self.assertEqual("0 / 3 (0.00 B / 1.01 KB) duplicates marked.",self.results.stat_line)
eq_("0 / 3 (0.00 B / 1.01 KB) duplicates marked.",self.results.stat_line)
def test_with_ref_duplicate(self):
self.objects[1].is_ref = True
self.results.groups = self.groups
self.assert_(not self.results.mark(self.objects[1]))
assert not self.results.mark(self.objects[1])
self.results.mark(self.objects[2])
self.assertEqual("1 / 2 (1.00 B / 2.00 B) duplicates marked.",self.results.stat_line)
eq_("1 / 2 (1.00 B / 2.00 B) duplicates marked.",self.results.stat_line)
def test_perform_on_marked(self):
def log_object(o):
@@ -239,17 +292,17 @@ class TCResultsMarkings(TestCase):
log = []
self.results.mark_all()
self.results.perform_on_marked(log_object,False)
self.assert_(self.objects[1] in log)
self.assert_(self.objects[2] in log)
self.assert_(self.objects[4] in log)
self.assertEqual(3,len(log))
assert self.objects[1] in log
assert self.objects[2] in log
assert self.objects[4] in log
eq_(3,len(log))
log = []
self.results.mark_none()
self.results.mark(self.objects[4])
self.results.perform_on_marked(log_object,True)
self.assertEqual(1,len(log))
self.assert_(self.objects[4] in log)
self.assertEqual(1,len(self.results.groups))
eq_(1,len(log))
assert self.objects[4] in log
eq_(1,len(self.results.groups))
def test_perform_on_marked_with_problems(self):
def log_object(o):
@@ -282,61 +335,61 @@ class TCResultsMarkings(TestCase):
self.objects[1].is_ref = True
self.results.mark_all()
self.results.perform_on_marked(log_object,True)
self.assert_(self.objects[1] not in log)
self.assert_(self.objects[2] in log)
self.assert_(self.objects[4] in log)
self.assertEqual(2,len(log))
self.assertEqual(0,len(self.results.groups))
assert self.objects[1] not in log
assert self.objects[2] in log
assert self.objects[4] in log
eq_(2,len(log))
eq_(0,len(self.results.groups))
def test_perform_on_marked_remove_objects_only_at_the_end(self):
def check_groups(o):
self.assertEqual(3,len(g1))
self.assertEqual(2,len(g2))
eq_(3,len(g1))
eq_(2,len(g2))
return True
g1,g2 = self.results.groups
self.results.mark_all()
self.results.perform_on_marked(check_groups,True)
self.assertEqual(0,len(g1))
self.assertEqual(0,len(g2))
self.assertEqual(0,len(self.results.groups))
eq_(0,len(g1))
eq_(0,len(g2))
eq_(0,len(self.results.groups))
def test_remove_duplicates(self):
g1 = self.results.groups[0]
g2 = self.results.groups[1]
self.results.mark(g1.dupes[0])
self.assertEqual("1 / 3 (1.00 KB / 1.01 KB) duplicates marked.",self.results.stat_line)
eq_("1 / 3 (1.00 KB / 1.01 KB) duplicates marked.",self.results.stat_line)
self.results.remove_duplicates([g1.dupes[1]])
self.assertEqual("1 / 2 (1.00 KB / 1.01 KB) duplicates marked.",self.results.stat_line)
eq_("1 / 2 (1.00 KB / 1.01 KB) duplicates marked.",self.results.stat_line)
self.results.remove_duplicates([g1.dupes[0]])
self.assertEqual("0 / 1 (0.00 B / 1.00 B) duplicates marked.",self.results.stat_line)
eq_("0 / 1 (0.00 B / 1.00 B) duplicates marked.",self.results.stat_line)
def test_make_ref(self):
g = self.results.groups[0]
d = g.dupes[0]
self.results.mark(d)
self.assertEqual("1 / 3 (1.00 KB / 1.01 KB) duplicates marked.",self.results.stat_line)
eq_("1 / 3 (1.00 KB / 1.01 KB) duplicates marked.",self.results.stat_line)
self.results.make_ref(d)
self.assertEqual("0 / 3 (0.00 B / 3.00 B) duplicates marked.",self.results.stat_line)
eq_("0 / 3 (0.00 B / 3.00 B) duplicates marked.",self.results.stat_line)
self.results.make_ref(d)
self.assertEqual("0 / 3 (0.00 B / 3.00 B) duplicates marked.",self.results.stat_line)
eq_("0 / 3 (0.00 B / 3.00 B) duplicates marked.",self.results.stat_line)
def test_SaveXML(self):
self.results.mark(self.objects[1])
self.results.mark_invert()
f = StringIO.StringIO()
f = io.BytesIO()
self.results.save_to_xml(f)
f.seek(0)
doc = etree.parse(f)
doc = ET.parse(f)
root = doc.getroot()
g1, g2 = root.iterchildren('group')
d1, d2, d3 = g1.iterchildren('file')
self.assertEqual('n', d1.get('marked'))
self.assertEqual('n', d2.get('marked'))
self.assertEqual('y', d3.get('marked'))
d1, d2 = g2.iterchildren('file')
self.assertEqual('n', d1.get('marked'))
self.assertEqual('y', d2.get('marked'))
g1, g2 = root.getiterator('group')
d1, d2, d3 = g1.getiterator('file')
eq_('n', d1.get('marked'))
eq_('n', d2.get('marked'))
eq_('y', d3.get('marked'))
d1, d2 = g2.getiterator('file')
eq_('n', d1.get('marked'))
eq_('y', d2.get('marked'))
def test_LoadXML(self):
def get_file(path):
@@ -345,16 +398,16 @@ class TCResultsMarkings(TestCase):
self.objects[4].name = 'ibabtu 2' #we can't have 2 files with the same path
self.results.mark(self.objects[1])
self.results.mark_invert()
f = StringIO.StringIO()
f = io.BytesIO()
self.results.save_to_xml(f)
f.seek(0)
r = Results(data)
r.load_from_xml(f,get_file)
self.assert_(not r.is_marked(self.objects[0]))
self.assert_(not r.is_marked(self.objects[1]))
self.assert_(r.is_marked(self.objects[2]))
self.assert_(not r.is_marked(self.objects[3]))
self.assert_(r.is_marked(self.objects[4]))
assert not r.is_marked(self.objects[0])
assert not r.is_marked(self.objects[1])
assert r.is_marked(self.objects[2])
assert not r.is_marked(self.objects[3])
assert r.is_marked(self.objects[4])
class TCResultsXML(TestCase):
@@ -369,38 +422,38 @@ class TCResultsXML(TestCase):
def test_save_to_xml(self):
self.objects[0].is_ref = True
self.objects[0].words = [['foo','bar']]
f = StringIO.StringIO()
f = io.BytesIO()
self.results.save_to_xml(f)
f.seek(0)
doc = etree.parse(f)
doc = ET.parse(f)
root = doc.getroot()
self.assertEqual('results', root.tag)
self.assertEqual(2, len(root))
self.assertEqual(2, len([c for c in root if c.tag == 'group']))
eq_('results', root.tag)
eq_(2, len(root))
eq_(2, len([c for c in root if c.tag == 'group']))
g1, g2 = root
self.assertEqual(6,len(g1))
self.assertEqual(3,len([c for c in g1 if c.tag == 'file']))
self.assertEqual(3,len([c for c in g1 if c.tag == 'match']))
eq_(6,len(g1))
eq_(3,len([c for c in g1 if c.tag == 'file']))
eq_(3,len([c for c in g1 if c.tag == 'match']))
d1, d2, d3 = [c for c in g1 if c.tag == 'file']
self.assertEqual(op.join('basepath','foo bar'),d1.get('path'))
self.assertEqual(op.join('basepath','bar bleh'),d2.get('path'))
self.assertEqual(op.join('basepath','foo bleh'),d3.get('path'))
self.assertEqual('y',d1.get('is_ref'))
self.assertEqual('n',d2.get('is_ref'))
self.assertEqual('n',d3.get('is_ref'))
self.assertEqual('foo,bar',d1.get('words'))
self.assertEqual('bar,bleh',d2.get('words'))
self.assertEqual('foo,bleh',d3.get('words'))
self.assertEqual(3,len(g2))
self.assertEqual(2,len([c for c in g2 if c.tag == 'file']))
self.assertEqual(1,len([c for c in g2 if c.tag == 'match']))
eq_(op.join('basepath','foo bar'),d1.get('path'))
eq_(op.join('basepath','bar bleh'),d2.get('path'))
eq_(op.join('basepath','foo bleh'),d3.get('path'))
eq_('y',d1.get('is_ref'))
eq_('n',d2.get('is_ref'))
eq_('n',d3.get('is_ref'))
eq_('foo,bar',d1.get('words'))
eq_('bar,bleh',d2.get('words'))
eq_('foo,bleh',d3.get('words'))
eq_(3,len(g2))
eq_(2,len([c for c in g2 if c.tag == 'file']))
eq_(1,len([c for c in g2 if c.tag == 'match']))
d1, d2 = [c for c in g2 if c.tag == 'file']
self.assertEqual(op.join('basepath','ibabtu'),d1.get('path'))
self.assertEqual(op.join('basepath','ibabtu'),d2.get('path'))
self.assertEqual('n',d1.get('is_ref'))
self.assertEqual('n',d2.get('is_ref'))
self.assertEqual('ibabtu',d1.get('words'))
self.assertEqual('ibabtu',d2.get('words'))
eq_(op.join('basepath','ibabtu'),d1.get('path'))
eq_(op.join('basepath','ibabtu'),d2.get('path'))
eq_('n',d1.get('is_ref'))
eq_('n',d2.get('is_ref'))
eq_('ibabtu',d1.get('words'))
eq_('ibabtu',d2.get('words'))
def test_LoadXML(self):
def get_file(path):
@@ -408,30 +461,30 @@ class TCResultsXML(TestCase):
self.objects[0].is_ref = True
self.objects[4].name = 'ibabtu 2' #we can't have 2 files with the same path
f = StringIO.StringIO()
f = io.BytesIO()
self.results.save_to_xml(f)
f.seek(0)
r = Results(data)
r.load_from_xml(f,get_file)
self.assertEqual(2,len(r.groups))
eq_(2,len(r.groups))
g1,g2 = r.groups
self.assertEqual(3,len(g1))
self.assert_(g1[0].is_ref)
self.assert_(not g1[1].is_ref)
self.assert_(not g1[2].is_ref)
self.assert_(g1[0] is self.objects[0])
self.assert_(g1[1] is self.objects[1])
self.assert_(g1[2] is self.objects[2])
self.assertEqual(['foo','bar'],g1[0].words)
self.assertEqual(['bar','bleh'],g1[1].words)
self.assertEqual(['foo','bleh'],g1[2].words)
self.assertEqual(2,len(g2))
self.assert_(not g2[0].is_ref)
self.assert_(not g2[1].is_ref)
self.assert_(g2[0] is self.objects[3])
self.assert_(g2[1] is self.objects[4])
self.assertEqual(['ibabtu'],g2[0].words)
self.assertEqual(['ibabtu'],g2[1].words)
eq_(3,len(g1))
assert g1[0].is_ref
assert not g1[1].is_ref
assert not g1[2].is_ref
assert g1[0] is self.objects[0]
assert g1[1] is self.objects[1]
assert g1[2] is self.objects[2]
eq_(['foo','bar'],g1[0].words)
eq_(['bar','bleh'],g1[1].words)
eq_(['foo','bleh'],g1[2].words)
eq_(2,len(g2))
assert not g2[0].is_ref
assert not g2[1].is_ref
assert g2[0] is self.objects[3]
assert g2[1] is self.objects[4]
eq_(['ibabtu'],g2[0].words)
eq_(['ibabtu'],g2[1].words)
def test_LoadXML_with_filename(self):
def get_file(path):
@@ -442,7 +495,7 @@ class TCResultsXML(TestCase):
self.results.save_to_xml(filename)
r = Results(data)
r.load_from_xml(filename,get_file)
self.assertEqual(2,len(r.groups))
eq_(2,len(r.groups))
def test_LoadXML_with_some_files_that_dont_exist_anymore(self):
def get_file(path):
@@ -451,84 +504,84 @@ class TCResultsXML(TestCase):
return [f for f in self.objects if str(f.path) == path][0]
self.objects[4].name = 'ibabtu 2' #we can't have 2 files with the same path
f = StringIO.StringIO()
f = io.BytesIO()
self.results.save_to_xml(f)
f.seek(0)
r = Results(data)
r.load_from_xml(f,get_file)
self.assertEqual(1,len(r.groups))
self.assertEqual(3,len(r.groups[0]))
eq_(1,len(r.groups))
eq_(3,len(r.groups[0]))
def test_LoadXML_missing_attributes_and_bogus_elements(self):
def get_file(path):
return [f for f in self.objects if str(f.path) == path][0]
root = etree.Element('foobar') #The root element shouldn't matter, really.
group_node = etree.SubElement(root, 'group')
dupe_node = etree.SubElement(group_node, 'file') #Perfectly correct file
root = ET.Element('foobar') #The root element shouldn't matter, really.
group_node = ET.SubElement(root, 'group')
dupe_node = ET.SubElement(group_node, 'file') #Perfectly correct file
dupe_node.set('path', op.join('basepath','foo bar'))
dupe_node.set('is_ref', 'y')
dupe_node.set('words', 'foo,bar')
dupe_node = etree.SubElement(group_node, 'file') #is_ref missing, default to 'n'
dupe_node = ET.SubElement(group_node, 'file') #is_ref missing, default to 'n'
dupe_node.set('path',op.join('basepath','foo bleh'))
dupe_node.set('words','foo,bleh')
dupe_node = etree.SubElement(group_node, 'file') #words are missing, valid.
dupe_node = ET.SubElement(group_node, 'file') #words are missing, valid.
dupe_node.set('path',op.join('basepath','bar bleh'))
dupe_node = etree.SubElement(group_node, 'file') #path is missing, invalid.
dupe_node = ET.SubElement(group_node, 'file') #path is missing, invalid.
dupe_node.set('words','foo,bleh')
dupe_node = etree.SubElement(group_node, 'foobar') #Invalid element name
dupe_node = ET.SubElement(group_node, 'foobar') #Invalid element name
dupe_node.set('path',op.join('basepath','bar bleh'))
dupe_node.set('is_ref','y')
dupe_node.set('words','bar,bleh')
match_node = etree.SubElement(group_node, 'match') # match pointing to a bad index
match_node = ET.SubElement(group_node, 'match') # match pointing to a bad index
match_node.set('first', '42')
match_node.set('second', '45')
match_node = etree.SubElement(group_node, 'match') # match with missing attrs
match_node = etree.SubElement(group_node, 'match') # match with non-int values
match_node = ET.SubElement(group_node, 'match') # match with missing attrs
match_node = ET.SubElement(group_node, 'match') # match with non-int values
match_node.set('first', 'foo')
match_node.set('second', 'bar')
match_node.set('percentage', 'baz')
group_node = etree.SubElement(root, 'foobar') #invalid group
group_node = etree.SubElement(root, 'group') #empty group
f = StringIO.StringIO()
tree = etree.ElementTree(root)
group_node = ET.SubElement(root, 'foobar') #invalid group
group_node = ET.SubElement(root, 'group') #empty group
f = io.BytesIO()
tree = ET.ElementTree(root)
tree.write(f, encoding='utf-8')
f.seek(0)
r = Results(data)
r.load_from_xml(f, get_file)
self.assertEqual(1,len(r.groups))
self.assertEqual(3,len(r.groups[0]))
eq_(1,len(r.groups))
eq_(3,len(r.groups[0]))
def test_xml_non_ascii(self):
def get_file(path):
if path == op.join('basepath',u'\xe9foo bar'):
if path == op.join('basepath','\xe9foo bar'):
return objects[0]
if path == op.join('basepath',u'bar bleh'):
if path == op.join('basepath','bar bleh'):
return objects[1]
objects = [NamedObject(u"\xe9foo bar",True),NamedObject("bar bleh",True)]
objects = [NamedObject("\xe9foo bar",True),NamedObject("bar bleh",True)]
matches = engine.getmatches(objects) #we should have 5 matches
groups = engine.get_groups(matches) #We should have 2 groups
for g in groups:
g.prioritize(lambda x:objects.index(x)) #We want the dupes to be in the same order as the list is
results = Results(data)
results.groups = groups
f = StringIO.StringIO()
f = io.BytesIO()
results.save_to_xml(f)
f.seek(0)
r = Results(data)
r.load_from_xml(f,get_file)
g = r.groups[0]
self.assertEqual(u"\xe9foo bar",g[0].name)
self.assertEqual(['efoo','bar'],g[0].words)
eq_("\xe9foo bar",g[0].name)
eq_(['efoo','bar'],g[0].words)
def test_load_invalid_xml(self):
f = StringIO.StringIO()
f.write('<this is invalid')
f = io.BytesIO()
f.write(b'<this is invalid')
f.seek(0)
r = Results(data)
r.load_from_xml(f,None)
self.assertEqual(0,len(r.groups))
eq_(0,len(r.groups))
def test_load_non_existant_xml(self):
r = Results(data)
@@ -536,7 +589,7 @@ class TCResultsXML(TestCase):
r.load_from_xml('does_not_exist.xml', None)
except IOError:
self.fail()
self.assertEqual(0,len(r.groups))
eq_(0,len(r.groups))
def test_remember_match_percentage(self):
group = self.groups[0]
@@ -546,7 +599,7 @@ class TCResultsXML(TestCase):
fake_matches.add(engine.Match(d1, d3, 43))
fake_matches.add(engine.Match(d2, d3, 46))
group.matches = fake_matches
f = StringIO.StringIO()
f = io.BytesIO()
results = self.results
results.save_to_xml(f)
f.seek(0)
@@ -555,16 +608,16 @@ class TCResultsXML(TestCase):
group = results.groups[0]
d1, d2, d3 = group
match = group.get_match_of(d2) #d1 - d2
self.assertEqual(42, match[2])
eq_(42, match[2])
match = group.get_match_of(d3) #d1 - d3
self.assertEqual(43, match[2])
eq_(43, match[2])
group.switch_ref(d2)
match = group.get_match_of(d3) #d2 - d3
self.assertEqual(46, match[2])
eq_(46, match[2])
def test_save_and_load(self):
# previously, when reloading matches, they wouldn't be reloaded as namedtuples
f = StringIO.StringIO()
f = io.BytesIO()
self.results.save_to_xml(f)
f.seek(0)
self.results.load_from_xml(f, self.get_file)
@@ -572,13 +625,13 @@ class TCResultsXML(TestCase):
def test_apply_filter_works_on_paths(self):
# apply_filter() searches on the whole path, not just on the filename.
self.results.apply_filter(u'basepath')
self.results.apply_filter('basepath')
eq_(len(self.results.groups), 2)
def test_save_xml_with_invalid_characters(self):
# Don't crash when saving files that have invalid xml characters in their path
self.objects[0].name = u'foo\x19'
self.results.save_to_xml(StringIO.StringIO()) # don't crash
self.objects[0].name = 'foo\x19'
self.results.save_to_xml(io.BytesIO()) # don't crash
class TCResultsFilter(TestCase):
@@ -589,47 +642,47 @@ class TCResultsFilter(TestCase):
self.results.apply_filter(r'foo')
def test_groups(self):
self.assertEqual(1, len(self.results.groups))
self.assert_(self.results.groups[0] is self.groups[0])
eq_(1, len(self.results.groups))
assert self.results.groups[0] is self.groups[0]
def test_dupes(self):
# There are 2 objects matching. The first one is ref. Only the 3rd one is supposed to be in dupes.
self.assertEqual(1, len(self.results.dupes))
self.assert_(self.results.dupes[0] is self.objects[2])
eq_(1, len(self.results.dupes))
assert self.results.dupes[0] is self.objects[2]
def test_cancel_filter(self):
self.results.apply_filter(None)
self.assertEqual(3, len(self.results.dupes))
self.assertEqual(2, len(self.results.groups))
eq_(3, len(self.results.dupes))
eq_(2, len(self.results.groups))
def test_dupes_reconstructed_filtered(self):
# make_ref resets self.__dupes to None. When it's reconstructed, we want it filtered
dupe = self.results.dupes[0] #3rd object
self.results.make_ref(dupe)
self.assertEqual(1, len(self.results.dupes))
self.assert_(self.results.dupes[0] is self.objects[0])
eq_(1, len(self.results.dupes))
assert self.results.dupes[0] is self.objects[0]
def test_include_ref_dupes_in_filter(self):
# When only the ref of a group match the filter, include it in the group
self.results.apply_filter(None)
self.results.apply_filter(r'foo bar')
self.assertEqual(1, len(self.results.groups))
self.assertEqual(0, len(self.results.dupes))
eq_(1, len(self.results.groups))
eq_(0, len(self.results.dupes))
def test_filters_build_on_one_another(self):
self.results.apply_filter(r'bar')
self.assertEqual(1, len(self.results.groups))
self.assertEqual(0, len(self.results.dupes))
eq_(1, len(self.results.groups))
eq_(0, len(self.results.dupes))
def test_stat_line(self):
expected = '0 / 1 (0.00 B / 1.00 B) duplicates marked. filter: foo'
self.assertEqual(expected, self.results.stat_line)
eq_(expected, self.results.stat_line)
self.results.apply_filter(r'bar')
expected = '0 / 0 (0.00 B / 0.00 B) duplicates marked. filter: foo --> bar'
self.assertEqual(expected, self.results.stat_line)
eq_(expected, self.results.stat_line)
self.results.apply_filter(None)
expected = '0 / 3 (0.00 B / 1.01 KB) duplicates marked.'
self.assertEqual(expected, self.results.stat_line)
eq_(expected, self.results.stat_line)
def test_mark_count_is_filtered_as_well(self):
self.results.apply_filter(None)
@@ -638,7 +691,7 @@ class TCResultsFilter(TestCase):
self.results.mark(dupe)
self.results.apply_filter(r'foo')
expected = '1 / 1 (1.00 B / 1.00 B) duplicates marked. filter: foo'
self.assertEqual(expected, self.results.stat_line)
eq_(expected, self.results.stat_line)
def test_sort_groups(self):
self.results.apply_filter(None)
@@ -646,22 +699,22 @@ class TCResultsFilter(TestCase):
g1,g2 = self.groups
self.results.apply_filter('a') # Matches both group
self.results.sort_groups(2) #2 is the key for size
self.assert_(self.results.groups[0] is g2)
self.assert_(self.results.groups[1] is g1)
assert self.results.groups[0] is g2
assert self.results.groups[1] is g1
self.results.apply_filter(None)
self.assert_(self.results.groups[0] is g2)
self.assert_(self.results.groups[1] is g1)
assert self.results.groups[0] is g2
assert self.results.groups[1] is g1
self.results.sort_groups(2, False)
self.results.apply_filter('a')
self.assert_(self.results.groups[1] is g2)
self.assert_(self.results.groups[0] is g1)
assert self.results.groups[1] is g2
assert self.results.groups[0] is g1
def test_set_group(self):
#We want the new group to be filtered
self.objects, self.matches, self.groups = GetTestGroups()
self.results.groups = self.groups
self.assertEqual(1, len(self.results.groups))
self.assert_(self.results.groups[0] is self.groups[0])
eq_(1, len(self.results.groups))
assert self.results.groups[0] is self.groups[0]
def test_load_cancels_filter(self):
def get_file(path):
@@ -673,23 +726,23 @@ class TCResultsFilter(TestCase):
r = Results(data)
r.apply_filter('foo')
r.load_from_xml(filename,get_file)
self.assertEqual(2,len(r.groups))
eq_(2,len(r.groups))
def test_remove_dupe(self):
self.results.remove_duplicates([self.results.dupes[0]])
self.results.apply_filter(None)
self.assertEqual(2,len(self.results.groups))
self.assertEqual(2,len(self.results.dupes))
eq_(2,len(self.results.groups))
eq_(2,len(self.results.dupes))
self.results.apply_filter('ibabtu')
self.results.remove_duplicates([self.results.dupes[0]])
self.results.apply_filter(None)
self.assertEqual(1,len(self.results.groups))
self.assertEqual(1,len(self.results.dupes))
eq_(1,len(self.results.groups))
eq_(1,len(self.results.dupes))
def test_filter_is_case_insensitive(self):
self.results.apply_filter(None)
self.results.apply_filter('FOO')
self.assertEqual(1, len(self.results.dupes))
eq_(1, len(self.results.dupes))
def test_make_ref_on_filtered_out_doesnt_mess_stats(self):
# When filtered, a group containing filtered out dupes will display them as being reference.
@@ -700,10 +753,10 @@ class TCResultsFilter(TestCase):
self.results.make_ref(bar_bleh)
# Now the stats should display *2* markable dupes (instead of 1)
expected = '0 / 2 (0.00 B / 2.00 B) duplicates marked. filter: foo'
self.assertEqual(expected, self.results.stat_line)
eq_(expected, self.results.stat_line)
self.results.apply_filter(None) # Now let's make sure our unfiltered results aren't fucked up
expected = '0 / 3 (0.00 B / 3.00 B) duplicates marked.'
self.assertEqual(expected, self.results.stat_line)
eq_(expected, self.results.stat_line)
class TCResultsRefFile(TestCase):
@@ -716,15 +769,15 @@ class TCResultsRefFile(TestCase):
def test_stat_line(self):
expected = '0 / 2 (0.00 B / 2.00 B) duplicates marked.'
self.assertEqual(expected, self.results.stat_line)
eq_(expected, self.results.stat_line)
def test_make_ref(self):
d = self.results.groups[0].dupes[1] #non-ref
r = self.results.groups[0].ref
self.results.make_ref(d)
expected = '0 / 1 (0.00 B / 1.00 B) duplicates marked.'
self.assertEqual(expected, self.results.stat_line)
eq_(expected, self.results.stat_line)
self.results.make_ref(r)
expected = '0 / 2 (0.00 B / 2.00 B) duplicates marked.'
self.assertEqual(expected, self.results.stat_line)
eq_(expected, self.results.stat_line)

View File

@@ -25,6 +25,9 @@ class NamedObject(object):
self.path = Path('')
self.words = getwords(name)
def __repr__(self):
return '<NamedObject %r>' % self.name
no = NamedObject
@@ -43,7 +46,7 @@ class ScannerTestFakeFiles(TestCase):
def test_default_settings(self):
s = Scanner()
eq_(s.min_match_percentage, 80)
eq_(s.scan_type, SCAN_TYPE_FILENAME)
eq_(s.scan_type, ScanType.Filename)
eq_(s.mix_file_kind, True)
eq_(s.word_weighting, False)
eq_(s.match_similar_words, False)
@@ -95,7 +98,7 @@ class ScannerTestFakeFiles(TestCase):
def test_content_scan(self):
s = Scanner()
s.scan_type = SCAN_TYPE_CONTENT
s.scan_type = ScanType.Contents
f = [no('foo'), no('bar'), no('bleh')]
f[0].md5 = f[0].md5partial = 'foobar'
f[1].md5 = f[1].md5partial = 'foobar'
@@ -112,13 +115,13 @@ class ScannerTestFakeFiles(TestCase):
raise AssertionError()
s = Scanner()
s.scan_type = SCAN_TYPE_CONTENT
s.scan_type = ScanType.Contents
f = [MyFile('foo', 1), MyFile('bar', 2)]
eq_(len(s.GetDupeGroups(f)), 0)
def test_min_match_perc_doesnt_matter_for_content_scan(self):
s = Scanner()
s.scan_type = SCAN_TYPE_CONTENT
s.scan_type = ScanType.Contents
f = [no('foo'), no('bar'), no('bleh')]
f[0].md5 = f[0].md5partial = 'foobar'
f[1].md5 = f[1].md5partial = 'foobar'
@@ -134,7 +137,7 @@ class ScannerTestFakeFiles(TestCase):
def test_content_scan_doesnt_put_md5_in_words_at_the_end(self):
s = Scanner()
s.scan_type = SCAN_TYPE_CONTENT
s.scan_type = ScanType.Contents
f = [no('foo'),no('bar')]
f[0].md5 = f[0].md5partial = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f'
f[1].md5 = f[1].md5partial = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f'
@@ -188,21 +191,21 @@ class ScannerTestFakeFiles(TestCase):
def test_fields(self):
s = Scanner()
s.scan_type = SCAN_TYPE_FIELDS
s.scan_type = ScanType.Fields
f = [no('The White Stripes - Little Ghost'), no('The White Stripes - Little Acorn')]
r = s.GetDupeGroups(f)
eq_(len(r), 0)
def test_fields_no_order(self):
s = Scanner()
s.scan_type = SCAN_TYPE_FIELDS_NO_ORDER
s.scan_type = ScanType.FieldsNoOrder
f = [no('The White Stripes - Little Ghost'), no('Little Ghost - The White Stripes')]
r = s.GetDupeGroups(f)
eq_(len(r), 1)
def test_tag_scan(self):
s = Scanner()
s.scan_type = SCAN_TYPE_TAG
s.scan_type = ScanType.Tag
o1 = no('foo')
o2 = no('bar')
o1.artist = 'The White Stripes'
@@ -214,7 +217,7 @@ class ScannerTestFakeFiles(TestCase):
def test_tag_with_album_scan(self):
s = Scanner()
s.scan_type = SCAN_TYPE_TAG
s.scan_type = ScanType.Tag
s.scanned_tags = set(['artist', 'album', 'title'])
o1 = no('foo')
o2 = no('bar')
@@ -233,7 +236,7 @@ class ScannerTestFakeFiles(TestCase):
def test_that_dash_in_tags_dont_create_new_fields(self):
s = Scanner()
s.scan_type = SCAN_TYPE_TAG
s.scan_type = ScanType.Tag
s.scanned_tags = set(['artist', 'album', 'title'])
s.min_match_percentage = 50
o1 = no('foo')
@@ -249,7 +252,7 @@ class ScannerTestFakeFiles(TestCase):
def test_tag_scan_with_different_scanned(self):
s = Scanner()
s.scan_type = SCAN_TYPE_TAG
s.scan_type = ScanType.Tag
s.scanned_tags = set(['track', 'year'])
o1 = no('foo')
o2 = no('bar')
@@ -266,7 +269,7 @@ class ScannerTestFakeFiles(TestCase):
def test_tag_scan_only_scans_existing_tags(self):
s = Scanner()
s.scan_type = SCAN_TYPE_TAG
s.scan_type = ScanType.Tag
s.scanned_tags = set(['artist', 'foo'])
o1 = no('foo')
o2 = no('bar')
@@ -279,7 +282,7 @@ class ScannerTestFakeFiles(TestCase):
def test_tag_scan_converts_to_str(self):
s = Scanner()
s.scan_type = SCAN_TYPE_TAG
s.scan_type = ScanType.Tag
s.scanned_tags = set(['track'])
o1 = no('foo')
o2 = no('bar')
@@ -293,12 +296,12 @@ class ScannerTestFakeFiles(TestCase):
def test_tag_scan_non_ascii(self):
s = Scanner()
s.scan_type = SCAN_TYPE_TAG
s.scan_type = ScanType.Tag
s.scanned_tags = set(['title'])
o1 = no('foo')
o2 = no('bar')
o1.title = u'foobar\u00e9'
o2.title = u'foobar\u00e9'
o1.title = 'foobar\u00e9'
o2.title = 'foobar\u00e9'
try:
r = s.GetDupeGroups([o1, o2])
except UnicodeEncodeError:
@@ -307,7 +310,7 @@ class ScannerTestFakeFiles(TestCase):
def test_audio_content_scan(self):
s = Scanner()
s.scan_type = SCAN_TYPE_CONTENT_AUDIO
s.scan_type = ScanType.ContentsAudio
f = [no('foo'), no('bar'), no('bleh')]
f[0].md5 = 'foo'
f[1].md5 = 'bar'
@@ -329,7 +332,7 @@ class ScannerTestFakeFiles(TestCase):
raise AssertionError()
s = Scanner()
s.scan_type = SCAN_TYPE_CONTENT_AUDIO
s.scan_type = ScanType.ContentsAudio
f = [MyFile('foo'), MyFile('bar')]
f[0].audiosize = 1
f[1].audiosize = 2
@@ -362,11 +365,11 @@ class ScannerTestFakeFiles(TestCase):
f1 = no('foobar')
f2 = no('foobar')
f3 = no('foobar')
f1.path = Path(u'foo1\u00e9')
f2.path = Path(u'foo2\u00e9')
f3.path = Path(u'foo3\u00e9')
s.ignore_list.Ignore(unicode(f1.path),unicode(f2.path))
s.ignore_list.Ignore(unicode(f1.path),unicode(f3.path))
f1.path = Path('foo1\u00e9')
f2.path = Path('foo2\u00e9')
f3.path = Path('foo3\u00e9')
s.ignore_list.Ignore(str(f1.path),str(f2.path))
s.ignore_list.Ignore(str(f1.path),str(f3.path))
r = s.GetDupeGroups([f1,f2,f3])
eq_(len(r), 1)
g = r[0]
@@ -379,7 +382,7 @@ class ScannerTestFakeFiles(TestCase):
# A very wrong way to use any() was added at some point, causing resulting group list
# to be empty.
class FalseNamedObject(NamedObject):
def __nonzero__(self):
def __bool__(self):
return False
@@ -426,11 +429,20 @@ class ScannerTestFakeFiles(TestCase):
# if ref has the same words as dupe, but has some just one extra word which is a digit, it
# becomes a dupe
s = Scanner()
o1, o2 = no('foo bar 42'), no('foo bar')
o1 = no('foo bar 42')
o2 = no('foo bar [42]')
o3 = no('foo bar (42)')
o4 = no('foo bar {42}')
o5 = no('foo bar')
# all numbered names have deeper paths, so they'll end up ref if the digits aren't correctly
# used as tie breakers
o1.path = Path('deeper/path')
o2.path = Path('foo')
[group] = s.GetDupeGroups([o1, o2])
assert group.ref is o2
o2.path = Path('deeper/path')
o3.path = Path('deeper/path')
o4.path = Path('deeper/path')
o5.path = Path('foo')
[group] = s.GetDupeGroups([o1, o2, o3, o4, o5])
assert group.ref is o5
def test_partial_group_match(self):
# Count the number od discarded matches (when a file doesn't match all other dupes of the
@@ -453,7 +465,7 @@ class ScannerTest(TestCase):
# In this test, we have to delete one of the files between the get_matches() part and the
# get_groups() part.
s = Scanner()
s.scan_type = SCAN_TYPE_CONTENT
s.scan_type = ScanType.Contents
p = self.tmppath()
io.open(p + 'file1', 'w').write('foo')
io.open(p + 'file2', 'w').write('foo')

View File

@@ -41,7 +41,7 @@ class DupeGuruME(DupeGuruBase):
try:
track.delete(timeout=0)
except CommandError as e:
logging.warning('Error while trying to remove a track from iTunes: %s' % unicode(e))
logging.warning('Error while trying to remove a track from iTunes: %s' % str(e))
self._start_job(JOB_REMOVE_DEAD_TRACKS, do)

View File

@@ -18,7 +18,6 @@ COLUMNS = [
{'attr':'bitrate','display':'Bitrate'},
{'attr':'samplerate','display':'Sample Rate'},
{'attr':'extension','display':'Kind'},
{'attr':'ctime','display':'Creation'},
{'attr':'mtime','display':'Modification'},
{'attr':'title','display':'Title'},
{'attr':'artist','display':'Artist'},
@@ -32,7 +31,7 @@ COLUMNS = [
{'attr':'dupe_count','display':'Dupe Count'},
]
METADATA_TO_READ = ['size', 'ctime', 'mtime', 'duration', 'bitrate', 'samplerate', 'title', 'artist',
METADATA_TO_READ = ['size', 'mtime', 'duration', 'bitrate', 'samplerate', 'title', 'artist',
'album', 'genre', 'year', 'track', 'comment']
def GetDisplayInfo(dupe, group, delta):
@@ -40,7 +39,6 @@ def GetDisplayInfo(dupe, group, delta):
duration = dupe.duration
bitrate = dupe.bitrate
samplerate = dupe.samplerate
ctime = dupe.ctime
mtime = dupe.mtime
m = group.get_match_of(dupe)
if m:
@@ -52,7 +50,6 @@ def GetDisplayInfo(dupe, group, delta):
duration -= r.duration
bitrate -= r.bitrate
samplerate -= r.samplerate
ctime -= r.ctime
mtime -= r.mtime
else:
percentage = group.percentage
@@ -65,7 +62,6 @@ def GetDisplayInfo(dupe, group, delta):
str(bitrate),
str(samplerate),
dupe.extension,
format_timestamp(ctime,delta and m),
format_timestamp(mtime,delta and m),
dupe.title,
dupe.artist,

View File

@@ -42,12 +42,12 @@ class Mp3File(MusicFile):
HANDLED_EXTS = set(['mp3'])
def _read_info(self, field):
if field == 'md5partial':
fileinfo = mpeg.Mpeg(unicode(self.path))
fileinfo = mpeg.Mpeg(str(self.path))
self._md5partial_offset = fileinfo.audio_offset
self._md5partial_size = fileinfo.audio_size
MusicFile._read_info(self, field)
if field in TAG_FIELDS:
fileinfo = mpeg.Mpeg(unicode(self.path))
fileinfo = mpeg.Mpeg(str(self.path))
self.audiosize = fileinfo.audio_size
self.bitrate = fileinfo.bitrate
self.duration = fileinfo.duration
@@ -70,12 +70,12 @@ class WmaFile(MusicFile):
HANDLED_EXTS = set(['wma'])
def _read_info(self, field):
if field == 'md5partial':
dec = wma.WMADecoder(unicode(self.path))
dec = wma.WMADecoder(str(self.path))
self._md5partial_offset = dec.audio_offset
self._md5partial_size = dec.audio_size
MusicFile._read_info(self, field)
if field in TAG_FIELDS:
dec = wma.WMADecoder(unicode(self.path))
dec = wma.WMADecoder(str(self.path))
self.audiosize = dec.audio_size
self.bitrate = dec.bitrate
self.duration = dec.duration
@@ -92,13 +92,13 @@ class Mp4File(MusicFile):
HANDLED_EXTS = set(['m4a', 'm4p'])
def _read_info(self, field):
if field == 'md5partial':
dec = mp4.File(unicode(self.path))
dec = mp4.File(str(self.path))
self._md5partial_offset = dec.audio_offset
self._md5partial_size = dec.audio_size
dec.close()
MusicFile._read_info(self, field)
if field in TAG_FIELDS:
dec = mp4.File(unicode(self.path))
dec = mp4.File(str(self.path))
self.audiosize = dec.audio_size
self.bitrate = dec.bitrate
self.duration = dec.duration
@@ -116,12 +116,12 @@ class OggFile(MusicFile):
HANDLED_EXTS = set(['ogg'])
def _read_info(self, field):
if field == 'md5partial':
dec = ogg.Vorbis(unicode(self.path))
dec = ogg.Vorbis(str(self.path))
self._md5partial_offset = dec.audio_offset
self._md5partial_size = dec.audio_size
MusicFile._read_info(self, field)
if field in TAG_FIELDS:
dec = ogg.Vorbis(unicode(self.path))
dec = ogg.Vorbis(str(self.path))
self.audiosize = dec.audio_size
self.bitrate = dec.bitrate
self.duration = dec.duration
@@ -138,12 +138,12 @@ class FlacFile(MusicFile):
HANDLED_EXTS = set(['flac'])
def _read_info(self, field):
if field == 'md5partial':
dec = flac.FLAC(unicode(self.path))
dec = flac.FLAC(str(self.path))
self._md5partial_offset = dec.audio_offset
self._md5partial_size = dec.audio_size
MusicFile._read_info(self, field)
if field in TAG_FIELDS:
dec = flac.FLAC(unicode(self.path))
dec = flac.FLAC(str(self.path))
self.audiosize = dec.audio_size
self.bitrate = dec.bitrate
self.duration = dec.duration
@@ -160,12 +160,12 @@ class AiffFile(MusicFile):
HANDLED_EXTS = set(['aif', 'aiff', 'aifc'])
def _read_info(self, field):
if field == 'md5partial':
dec = aiff.File(unicode(self.path))
dec = aiff.File(str(self.path))
self._md5partial_offset = dec.audio_offset
self._md5partial_size = dec.audio_size
MusicFile._read_info(self, field)
if field in TAG_FIELDS:
dec = aiff.File(unicode(self.path))
dec = aiff.File(str(self.path))
self.audiosize = dec.audio_size
self.bitrate = dec.bitrate
self.duration = dec.duration

View File

@@ -8,12 +8,13 @@
import os.path as op
import plistlib
import logging
import re
from lxml import etree
from appscript import app, k, CommandError
from hsutil import io
from hsutil.str import get_file_ext
from hsutil.str import get_file_ext, remove_invalid_xml
from hsutil.path import Path
from hscommon.cocoa import as_fetch
from hscommon.cocoa.objcmin import NSUserDefaults, NSURL
@@ -37,15 +38,15 @@ class Photo(fs.File):
def _read_info(self, field):
fs.File._read_info(self, field)
if field == 'dimensions':
self.dimensions = _block_osx.get_image_size(unicode(self.path))
self.dimensions = _block_osx.get_image_size(str(self.path))
def get_blocks(self, block_count_per_side):
try:
blocks = _block_osx.getblocks(unicode(self.path), block_count_per_side)
blocks = _block_osx.getblocks(str(self.path), block_count_per_side)
except Exception as e:
raise IOError('The reading of "%s" failed with "%s"' % (unicode(self.path), unicode(e)))
raise IOError('The reading of "%s" failed with "%s"' % (str(self.path), str(e)))
if not blocks:
raise IOError('The picture %s could not be read' % unicode(self.path))
raise IOError('The picture %s could not be read' % str(self.path))
return blocks
@@ -67,11 +68,16 @@ def get_iphoto_database_path():
def get_iphoto_pictures(plistpath):
if not io.exists(plistpath):
return []
# We make the xml go through lxml so that it can fix broken xml which iPhoto sometimes produces.
parser = etree.XMLParser(recover=True)
root = etree.parse(io.open(plistpath), parser=parser).getroot()
s = etree.tostring(root)
plist = plistlib.readPlistFromString(s)
s = io.open(plistpath, 'rt', encoding='utf-8').read()
# There was a case where a guy had 0x10 chars in his plist, causing expat errors on loading
s = remove_invalid_xml(s, replace_with='')
# It seems that iPhoto sometimes doesn't properly escape & chars. The regexp below is to find
# any & char that is not a &-based entity (&amp;, &quot;, etc.). based on TextMate's XML
# bundle's regexp
s, count = re.subn(r'&(?![a-zA-Z0-9_-]+|#[0-9]+|#x[0-9a-fA-F]+;)', '', s)
if count:
logging.warning("%d invalid XML entities replacement made", count)
plist = plistlib.readPlistFromBytes(s.encode('utf-8'))
result = []
for photo_data in plist['Master Image List'].values():
if photo_data['MediaType'] != 'Image':
@@ -116,6 +122,15 @@ class Directories(directories.Directories):
else:
directories.Directories.add_path(self, path)
def has_any_file(self):
# If we don't do that, it causes a hangup in the GUI when we click Start Scanning because
# checking if there's any file to scan involves reading the whole library. If we have the
# iPhoto library, we assume we have at least one file.
if any(path == Path('iPhoto Library') for path in self._dirs):
return True
else:
return directories.Directories.has_any_file(self)
class DupeGuruPE(app_cocoa.DupeGuru):
def __init__(self):
@@ -140,7 +155,7 @@ class DupeGuruPE(app_cocoa.DupeGuru):
photos = as_fetch(a.photo_library_album().photos, k.item)
for photo in j.iter_with_progress(photos):
try:
self.path2iphoto[unicode(photo.image_path(timeout=0))] = photo
self.path2iphoto[str(photo.image_path(timeout=0))] = photo
except CommandError:
pass
except (CommandError, RuntimeError):
@@ -151,23 +166,19 @@ class DupeGuruPE(app_cocoa.DupeGuru):
def _do_delete_dupe(self, dupe):
if isinstance(dupe, IPhoto):
if unicode(dupe.path) in self.path2iphoto:
photo = self.path2iphoto[unicode(dupe.path)]
if str(dupe.path) in self.path2iphoto:
photo = self.path2iphoto[str(dupe.path)]
try:
a = app('iPhoto')
a.remove(photo, timeout=0)
except (CommandError, RuntimeError) as e:
raise EnvironmentError(unicode(e))
raise EnvironmentError(str(e))
else:
msg = u"Could not find photo %s in iPhoto Library" % unicode(dupe.path)
msg = "Could not find photo %s in iPhoto Library" % str(dupe.path)
raise EnvironmentError(msg)
else:
app_cocoa.DupeGuru._do_delete_dupe(self, dupe)
def _do_load(self, j):
self.directories.load_from_file(op.join(self.appdata, 'last_directories.xml'))
self.results.load_from_xml(op.join(self.appdata, 'last_results.xml'), self._get_file, j)
def _get_file(self, str_path):
p = Path(str_path)
if (self.directories.iphoto_libpath is not None) and (p in self.directories.iphoto_libpath[:-1]):

View File

@@ -6,7 +6,7 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from _block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2
from ._block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2
# Converted to C
# def getblock(image):

View File

@@ -10,7 +10,7 @@ import os
import logging
import sqlite3 as sqlite
from _cache import string_to_colors
from ._cache import string_to_colors
def colors_to_string(colors):
"""Transform the 3 sized tuples 'colors' into a hex string.
@@ -82,7 +82,7 @@ class Cache(object):
self.con.execute(sql, [value, key])
except sqlite.OperationalError:
logging.warning('Picture cache could not set %r for key %r', value, key)
except sqlite.DatabaseError, e:
except sqlite.DatabaseError as e:
logging.warning('DatabaseError while setting %r for key %r: %s', value, key, str(e))
def _create_con(self, second_try=False):
@@ -97,7 +97,7 @@ class Cache(object):
self.con.execute("select * from pictures where 1=2")
except sqlite.OperationalError: # new db
create_tables()
except sqlite.DatabaseError, e: # corrupted db
except sqlite.DatabaseError as e: # corrupted db
if second_try:
raise # Something really strange is happening
logging.warning('Could not create picture cache because of an error: %s', str(e))

View File

@@ -18,19 +18,17 @@ COLUMNS = [
{'attr':'size','display':'Size (KB)'},
{'attr':'extension','display':'Kind'},
{'attr':'dimensions','display':'Dimensions'},
{'attr':'ctime','display':'Creation'},
{'attr':'mtime','display':'Modification'},
{'attr':'percentage','display':'Match %'},
{'attr':'dupe_count','display':'Dupe Count'},
]
METADATA_TO_READ = ['size', 'ctime', 'mtime', 'dimensions']
METADATA_TO_READ = ['size', 'mtime', 'dimensions']
def GetDisplayInfo(dupe,group,delta=False):
if (dupe is None) or (group is None):
return ['---'] * len(COLUMNS)
size = dupe.size
ctime = dupe.ctime
mtime = dupe.mtime
m = group.get_match_of(dupe)
if m:
@@ -39,7 +37,6 @@ def GetDisplayInfo(dupe,group,delta=False):
if delta:
r = group.ref
size -= r.size
ctime -= r.ctime
mtime -= r.mtime
else:
percentage = group.percentage
@@ -51,7 +48,6 @@ def GetDisplayInfo(dupe,group,delta=False):
format_size(size, 0, 1, False),
dupe.extension,
format_dimensions(dupe.dimensions),
format_timestamp(ctime, delta and m),
format_timestamp(mtime, delta and m),
format_perc(percentage),
format_dupe_count(dupe_count)

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env python
# Created By: Virgil Dupras
# Created On: 2009-05-26
# Copyright 2010 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" 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/hs_license
import os
import os.path as op
def move(src, dst):
if not op.exists(src):
return
if op.exists(dst):
os.remove(dst)
print 'Moving %s --> %s' % (src, dst)
os.rename(src, dst)
os.chdir('modules')
os.system('python setup.py build_ext --inplace')
os.chdir('..')
move(op.join('modules', '_block.so'), '_block.so')
move(op.join('modules', '_block.pyd'), '_block.pyd')
move(op.join('modules', '_block_osx.so'), '_block_osx.so')
move(op.join('modules', '_cache.so'), '_cache.so')
move(op.join('modules', '_cache.pyd'), '_cache.pyd')

View File

@@ -34,16 +34,16 @@ def prepare_pictures(pictures, cache_path, j=job.nulljob):
try:
for picture in j.iter_with_progress(pictures, 'Analyzed %d/%d pictures'):
picture.dimensions
picture.unicode_path = unicode(picture.path)
picture.unicode_path = str(picture.path)
try:
if picture.unicode_path not in cache:
blocks = picture.get_blocks(BLOCK_COUNT_PER_SIDE)
cache[picture.unicode_path] = blocks
prepared.append(picture)
except (IOError, ValueError) as e:
logging.warning(unicode(e))
logging.warning(str(e))
except MemoryError:
logging.warning(u'Ran out of memory while reading %s of size %d' % (picture.unicode_path, picture.size))
logging.warning('Ran out of memory while reading %s of size %d' % (picture.unicode_path, picture.size))
if picture.size < 10 * 1024 * 1024: # We're really running out of memory
raise
except MemoryError:

View File

@@ -39,9 +39,9 @@ static PyObject* getblock(PyObject *image)
pg = PySequence_ITEM(ppixel, 1);
pb = PySequence_ITEM(ppixel, 2);
Py_DECREF(ppixel);
r = PyInt_AsSsize_t(pr);
g = PyInt_AsSsize_t(pg);
b = PyInt_AsSsize_t(pb);
r = PyLong_AsLong(pr);
g = PyLong_AsLong(pg);
b = PyLong_AsLong(pb);
Py_DECREF(pr);
Py_DECREF(pg);
Py_DECREF(pb);
@@ -67,14 +67,14 @@ static PyObject* getblock(PyObject *image)
*/
static int diff(PyObject *first, PyObject *second)
{
Py_ssize_t r1, g1, b1, r2, b2, g2;
int r1, g1, b1, r2, b2, g2;
PyObject *pr, *pg, *pb;
pr = PySequence_ITEM(first, 0);
pg = PySequence_ITEM(first, 1);
pb = PySequence_ITEM(first, 2);
r1 = PyInt_AsSsize_t(pr);
g1 = PyInt_AsSsize_t(pg);
b1 = PyInt_AsSsize_t(pb);
r1 = PyLong_AsLong(pr);
g1 = PyLong_AsLong(pg);
b1 = PyLong_AsLong(pb);
Py_DECREF(pr);
Py_DECREF(pg);
Py_DECREF(pb);
@@ -82,9 +82,9 @@ static int diff(PyObject *first, PyObject *second)
pr = PySequence_ITEM(second, 0);
pg = PySequence_ITEM(second, 1);
pb = PySequence_ITEM(second, 2);
r2 = PyInt_AsSsize_t(pr);
g2 = PyInt_AsSsize_t(pg);
b2 = PyInt_AsSsize_t(pb);
r2 = PyLong_AsLong(pr);
g2 = PyLong_AsLong(pg);
b2 = PyLong_AsLong(pb);
Py_DECREF(pr);
Py_DECREF(pg);
Py_DECREF(pb);
@@ -115,8 +115,8 @@ static PyObject* block_getblocks2(PyObject *self, PyObject *args)
pimage_size = PyObject_GetAttrString(image, "size");
pwidth = PySequence_ITEM(pimage_size, 0);
pheight = PySequence_ITEM(pimage_size, 1);
width = PyInt_AsSsize_t(pwidth);
height = PyInt_AsSsize_t(pheight);
width = PyLong_AsLong(pwidth);
height = PyLong_AsLong(pheight);
Py_DECREF(pimage_size);
Py_DECREF(pwidth);
Py_DECREF(pheight);
@@ -147,8 +147,8 @@ static PyObject* block_getblocks2(PyObject *self, PyObject *args)
left = min(iw*block_width, width-block_width);
right = left + block_width;
pbox = inttuple(4, left, top, right, bottom);
pmethodname = PyString_FromString("crop");
pcrop = PyObject_CallMethodObjArgs(image, pmethodname, pbox);
pmethodname = PyUnicode_FromString("crop");
pcrop = PyObject_CallMethodObjArgs(image, pmethodname, pbox, NULL);
Py_DECREF(pmethodname);
Py_DECREF(pbox);
if (pcrop == NULL) {
@@ -207,7 +207,7 @@ static PyObject* block_avgdiff(PyObject *self, PyObject *args)
Py_DECREF(item1);
Py_DECREF(item2);
if ((sum > limit*iteration_count) && (iteration_count >= min_iterations)) {
return PyInt_FromSsize_t(limit + 1);
return PyLong_FromLong(limit + 1);
}
}
@@ -215,7 +215,7 @@ static PyObject* block_avgdiff(PyObject *self, PyObject *args)
if (!result && sum) {
result = 1;
}
return PyInt_FromSsize_t(result);
return PyLong_FromLong(result);
}
static PyMethodDef BlockMethods[] = {
@@ -224,16 +224,30 @@ static PyMethodDef BlockMethods[] = {
{NULL, NULL, 0, NULL} /* Sentinel */
};
PyMODINIT_FUNC
init_block(void)
static struct PyModuleDef BlockDef = {
PyModuleDef_HEAD_INIT,
"_block",
NULL,
-1,
BlockMethods,
NULL,
NULL,
NULL,
NULL
};
PyObject *
PyInit__block(void)
{
PyObject *m = Py_InitModule("_block", BlockMethods);
PyObject *m = PyModule_Create(&BlockDef);
if (m == NULL) {
return;
return NULL;
}
NoBlocksError = PyErr_NewException("_block.NoBlocksError", NULL, NULL);
PyModule_AddObject(m, "NoBlocksError", NoBlocksError);
DifferentBlockCountError = PyErr_NewException("_block.DifferentBlockCountError", NULL, NULL);
PyModule_AddObject(m, "DifferentBlockCountError", DifferentBlockCountError);
return m;
}

View File

@@ -29,8 +29,8 @@ pystring2cfstring(PyObject *pystring)
Py_INCREF(encoded);
}
s = (UInt8*)PyString_AS_STRING(encoded);
size = PyString_GET_SIZE(encoded);
s = (UInt8*)PyBytes_AS_STRING(encoded);
size = PyUnicode_GET_SIZE(encoded);
result = CFStringCreateWithBytes(NULL, s, size, kCFStringEncodingUTF8, FALSE);
Py_DECREF(encoded);
return result;
@@ -43,7 +43,7 @@ static PyObject* block_osx_get_image_size(PyObject *self, PyObject *args)
CFURLRef image_url;
CGImageSourceRef source;
CGImageRef image;
size_t width, height;
long width, height;
PyObject *pwidth, *pheight;
PyObject *result;
@@ -72,11 +72,11 @@ static PyObject* block_osx_get_image_size(PyObject *self, PyObject *args)
CFRelease(source);
}
pwidth = PyInt_FromSsize_t(width);
pwidth = PyLong_FromLong(width);
if (pwidth == NULL) {
return NULL;
}
pheight = PyInt_FromSsize_t(height);
pheight = PyLong_FromLong(height);
if (pheight == NULL) {
return NULL;
}
@@ -228,8 +228,24 @@ static PyMethodDef BlockOsxMethods[] = {
{NULL, NULL, 0, NULL} /* Sentinel */
};
PyMODINIT_FUNC
init_block_osx(void)
static struct PyModuleDef BlockOsxDef = {
PyModuleDef_HEAD_INIT,
"_block_osx",
NULL,
-1,
BlockOsxMethods,
NULL,
NULL,
NULL,
NULL
};
PyObject *
PyInit__block_osx(void)
{
Py_InitModule("_block_osx", BlockOsxMethods);
PyObject *m = PyModule_Create(&BlockOsxDef);
if (m == NULL) {
return NULL;
}
return m;
}

View File

@@ -72,8 +72,24 @@ static PyMethodDef CacheMethods[] = {
{NULL, NULL, 0, NULL} /* Sentinel */
};
PyMODINIT_FUNC
init_cache(void)
static struct PyModuleDef CacheDef = {
PyModuleDef_HEAD_INIT,
"_cache",
NULL,
-1,
CacheMethods,
NULL,
NULL,
NULL,
NULL
};
PyObject *
PyInit__cache(void)
{
(void)Py_InitModule("_cache", CacheMethods);
}
PyObject *m = PyModule_Create(&CacheDef);
if (m == NULL) {
return NULL;
}
return m;
}

View File

@@ -32,7 +32,7 @@ PyObject* inttuple(int n, ...)
result = PyTuple_New(n);
for (i=0; i<n; i++) {
pnumber = PyInt_FromLong(va_arg(numbers, int));
pnumber = PyLong_FromLong(va_arg(numbers, long));
if (pnumber == NULL) {
Py_DECREF(result);
return NULL;

View File

@@ -1,30 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2009-04-23
# Copyright 2010 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" 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/hs_license
import sys
from distutils.core import setup
from distutils.extension import Extension
exts = []
exts.append(Extension("_block", ["block.c", "common.c"]))
exts.append(Extension("_cache", ["cache.c", "common.c"]))
if sys.platform == 'darwin':
exts.append(Extension(
"_block_osx", ["block_osx.m", "common.c"],
extra_link_args=[
"-framework", "CoreFoundation",
"-framework", "Foundation",
"-framework", "ApplicationServices",
]))
setup(
ext_modules = exts,
)

View File

@@ -258,8 +258,8 @@ class TCavgdiff(unittest.TestCase):
def test_return_at_least_1_at_the_slightest_difference(self):
ref = (0,0,0)
b1 = (1,0,0)
blocks1 = [ref for i in xrange(250)]
blocks2 = [ref for i in xrange(250)]
blocks1 = [ref for i in range(250)]
blocks2 = [ref for i in range(250)]
blocks2[0] = b1
self.assertEqual(1,my_avgdiff(blocks1,blocks2))

View File

@@ -6,10 +6,8 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from StringIO import StringIO
import os.path as op
import os
import threading
from hsutil.testcase import TestCase

View File

@@ -6,7 +6,7 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from __future__ import unicode_literals
import logging
@@ -24,17 +24,17 @@ def is_bundle(str_path):
sw = NSWorkspace.sharedWorkspace()
uti, error = sw.typeOfFile_error_(str_path, None)
if error is not None:
logging.warning(u'There was an error trying to detect the UTI of %s', str_path)
logging.warning('There was an error trying to detect the UTI of %s', str_path)
return sw.type_conformsToType_(uti, 'com.apple.bundle') or sw.type_conformsToType_(uti, 'com.apple.package')
class Bundle(BundleBase):
@classmethod
def can_handle(cls, path):
return not io.islink(path) and io.isdir(path) and is_bundle(unicode(path))
return not io.islink(path) and io.isdir(path) and is_bundle(str(path))
class Directories(DirectoriesBase):
ROOT_PATH_TO_EXCLUDE = map(Path, ['/Library', '/Volumes', '/System', '/bin', '/sbin', '/opt', '/private', '/dev'])
ROOT_PATH_TO_EXCLUDE = list(map(Path, ['/Library', '/Volumes', '/System', '/bin', '/sbin', '/opt', '/private', '/dev']))
HOME_PATH_TO_EXCLUDE = [Path('Library')]
def __init__(self):
DirectoriesBase.__init__(self, fileclasses=[Bundle, fs.File])

View File

@@ -15,18 +15,16 @@ COLUMNS = [
{'attr':'path','display':'Directory'},
{'attr':'size','display':'Size (KB)'},
{'attr':'extension','display':'Kind'},
{'attr':'ctime','display':'Creation'},
{'attr':'mtime','display':'Modification'},
{'attr':'percentage','display':'Match %'},
{'attr':'words','display':'Words Used'},
{'attr':'dupe_count','display':'Dupe Count'},
]
METADATA_TO_READ = ['size', 'ctime', 'mtime']
METADATA_TO_READ = ['size', 'mtime']
def GetDisplayInfo(dupe, group, delta):
size = dupe.size
ctime = dupe.ctime
mtime = dupe.mtime
m = group.get_match_of(dupe)
if m:
@@ -35,7 +33,6 @@ def GetDisplayInfo(dupe, group, delta):
if delta:
r = group.ref
size -= r.size
ctime -= r.ctime
mtime -= r.mtime
else:
percentage = group.percentage
@@ -45,7 +42,6 @@ def GetDisplayInfo(dupe, group, delta):
format_path(dupe.path),
format_size(size, 0, 1, False),
dupe.extension,
format_timestamp(ctime, delta and m),
format_timestamp(mtime, delta and m),
format_perc(percentage),
format_words(dupe.words) if hasattr(dupe, 'words') else '',

View File

@@ -20,12 +20,11 @@ class Bundle(fs.File):
to see them as files.
"""
def _read_info(self, field):
if field in ('size', 'ctime', 'mtime'):
if field in ('size', 'mtime'):
files = fs.get_all_files(self.path)
size = sum((file.size for file in files), 0)
self.size = size
stats = io.stat(self.path)
self.ctime = nonone(stats.st_ctime, 0)
self.mtime = nonone(stats.st_mtime, 0)
elif field in ('md5', 'md5partial'):
# What's sensitive here is that we must make sure that subfiles'
@@ -35,7 +34,7 @@ class Bundle(fs.File):
files = fs.get_all_files(self.path)
files.sort(key=lambda f:f.path)
md5s = [getattr(f, field) for f in files]
return ''.join(md5s)
return b''.join(md5s)
md5 = hashlib.md5(get_dir_md5_concat())
digest = md5.digest()

View File

@@ -38,9 +38,8 @@ class TCBundle(TestCase):
eq_(b.md5, md5.digest())
def test_has_file_attrs(self):
#a Bundle must behave like a file, so it must have ctime and mtime attributes
#a Bundle must behave like a file, so it must have mtime attributes
b = fs.Bundle(self.tmppath())
assert b.mtime > 0
assert b.ctime > 0
eq_(b.extension, '')

View File

@@ -8,5 +8,5 @@ Homepage: http://www.hardcoded.net
Package: dupeguru-me
Architecture: any
Depends: python (>= 2.6), python-qt4 (>= 4.6), python-lxml (>= 2.1)
Depends: python (>= 2.6), python-qt4 (>= 4.6)
Description: dupeGuru Music Edition

View File

@@ -8,5 +8,5 @@ Homepage: http://www.hardcoded.net
Package: dupeguru-pe
Architecture: any
Depends: python (>= 2.6), python-qt4 (>= 4.6), python-lxml (>= 2.1), python-imaging (>= 1.1.6)
Depends: python (>= 2.6), python-qt4 (>= 4.6), python-imaging (>= 1.1.6)
Description: dupeGuru Picture Edition

1
debian_se/compat Normal file
View File

@@ -0,0 +1 @@
7

View File

@@ -8,5 +8,5 @@ Homepage: http://www.hardcoded.net
Package: dupeguru-se
Architecture: any
Depends: python (>= 2.6), python-qt4 (>= 4.6), python-lxml (>= 2.1)
Depends: python3 (>= 3.1)
Description: dupeGuru

View File

@@ -42,9 +42,9 @@ install: build
dh_installdirs
chmod +x src/start.py
cp -R src/ $(CURDIR)/debian/tmp/usr/local/share/dupeguru_se
cp $(CURDIR)/debian/dupeguru_se.desktop $(CURDIR)/debian/tmp/usr/share/applications
ln -s /usr/local/share/dupeguru_se/start.py $(CURDIR)/debian/tmp/usr/local/bin/dupeguru_se
cp -R src/ $(CURDIR)/debian/dupeguru-se/usr/local/share/dupeguru_se
cp $(CURDIR)/debian/dupeguru_se.desktop $(CURDIR)/debian/dupeguru-se/usr/share/applications
ln -s /usr/local/share/dupeguru_se/start.py $(CURDIR)/debian/dupeguru-se/usr/local/bin/dupeguru_se
# Build architecture-independent files here.

View File

@@ -1,3 +1,7 @@
- date: 2010-07-16
version: 5.8.1
description: |
* Fixed a couple of crashes. (#95, #97, #100)
- date: 2010-04-14
version: 5.8.0
description: |

View File

@@ -1,3 +1,7 @@
- date: 2010-07-17
version: 1.9.1
description: |
* Fixed a couple of crashes. (#95, #96, #97, #100)
- date: 2010-04-15
version: 1.9.0
description: |

View File

@@ -1,3 +1,12 @@
- date: 2010-08-18
version: 2.11.0
description: |
* Added the ability to save results (and reload them) at arbitrary locations.
* Improved the way reference files in dupe groups are chosen. (#15)
* Remember size/position of all windows between launches. (#102)
* Fixed a bug sometimes preventing dupeGuru from reloading previous results.
* Fixed a bug sometimes causing the progress dialog to be stuck there. [Mac OS X] (#103)
* Removed the Creation Date column, which wasn't displaying the correct value anyway. (#101)
- date: 2010-07-15
version: 2.10.1
description: |

View File

@@ -30,10 +30,11 @@ def package_windows(edition, dev):
# On Windows, PyInstaller is used to build an exe (py2exe creates a very bad looking icon)
# The release version is outdated. Use at least r672 on http://svn.pyinstaller.org/trunk
if sys.platform != "win32":
print "Qt packaging only works under Windows."
print("Qt packaging only works under Windows.")
return
add_to_pythonpath('.')
add_to_pythonpath('qt')
add_to_pythonpath(op.join('qt', 'base'))
add_to_pythonpath(op.join('qt', edition))
os.chdir(op.join('qt', edition))
from app import DupeGuru
@@ -60,7 +61,7 @@ def package_windows(edition, dev):
help_basedir = '..\\..\\help_{0}'.format(edition)
help_dir = 'dupeguru_{0}_help'.format(edition) if edition != 'se' else 'dupeguru_help'
help_path = op.join(help_basedir, help_dir)
print "Copying {0} to dist\\help".format(help_path)
print("Copying {0} to dist\\help".format(help_path))
shutil.copytree(help_path, 'dist\\help')
# AdvancedInstaller.com has to be in your PATH
@@ -73,6 +74,7 @@ def package_windows(edition, dev):
def package_debian(edition):
add_to_pythonpath('qt')
add_to_pythonpath(op.join('qt', 'base'))
add_to_pythonpath(op.join('qt', edition))
from app import DupeGuru
@@ -88,6 +90,15 @@ def package_debian(edition):
if edition == 'me':
packages.append('hsaudiotag')
copy_packages(packages, srcpath)
import sip, PyQt4
shutil.copy(sip.__file__, srcpath)
qtsrcpath = op.dirname(PyQt4.__file__)
qtdestpath = op.join(srcpath, 'PyQt4')
os.makedirs(qtdestpath)
shutil.copy(op.join(qtsrcpath, '__init__.py'), qtdestpath)
shutil.copy(op.join(qtsrcpath, 'Qt.so'), qtdestpath)
shutil.copy(op.join(qtsrcpath, 'QtCore.so'), qtdestpath)
shutil.copy(op.join(qtsrcpath, 'QtGui.so'), qtdestpath)
shutil.copytree(ed('debian_{0}'), op.join(destpath, 'debian'))
yaml_path = op.join(help_src, 'changelog.yaml')
changelog_dest = op.join(destpath, 'debian', 'changelog')
@@ -106,7 +117,7 @@ def main():
edition = conf['edition']
ui = conf['ui']
dev = conf['dev']
print "Packaging dupeGuru {0} with UI {1}".format(edition.upper(), ui)
print("Packaging dupeGuru {0} with UI {1}".format(edition.upper(), ui))
if ui == 'cocoa':
package_cocoa(edition)
elif ui == 'qt':
@@ -115,7 +126,7 @@ def main():
elif sys.platform == "linux2":
package_debian(edition)
else:
print "Qt packaging only works under Windows or Linux."
print("Qt packaging only works under Windows or Linux.")
if __name__ == '__main__':
main()

View File

@@ -6,13 +6,13 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from __future__ import unicode_literals
import logging
import os
import os.path as op
from PyQt4.QtCore import QTimer, QObject, QCoreApplication, QUrl, SIGNAL
from PyQt4.QtCore import QTimer, QObject, QCoreApplication, QUrl, SIGNAL, pyqtSignal
from PyQt4.QtGui import QDesktopServices, QFileDialog, QDialog, QMessageBox
from hscommon import job
@@ -54,7 +54,7 @@ class DupeGuru(DupeGuruBase, QObject):
DELTA_COLUMNS = frozenset()
def __init__(self, data_module, appid):
appdata = unicode(QDesktopServices.storageLocation(QDesktopServices.DataLocation))
appdata = str(QDesktopServices.storageLocation(QDesktopServices.DataLocation))
if not op.exists(appdata):
os.makedirs(appdata)
# For basicConfig() to work, we have to be sure that no logging has taken place before this call.
@@ -86,7 +86,10 @@ class DupeGuru(DupeGuruBase, QObject):
self._nagTimer = QTimer()
self.connect(self._nagTimer, SIGNAL('timeout()'), self.mustShowNag)
self._nagTimer.start(0)
self.main_window.show()
if self.prefs.mainWindowIsMaximized:
self.main_window.showMaximized()
else:
self.main_window.show()
self.load()
self.connect(QCoreApplication.instance(), SIGNAL('aboutToQuit()'), self.application_will_terminate)
@@ -120,7 +123,7 @@ class DupeGuru(DupeGuruBase, QObject):
#--- Override
@staticmethod
def _open_path(path):
url = QUrl.fromLocalFile(unicode(path))
url = QUrl.fromLocalFile(str(path))
QDesktopServices.openUrl(url)
@staticmethod
@@ -150,7 +153,7 @@ class DupeGuru(DupeGuruBase, QObject):
opname = 'copy' if copy else 'move'
title = "Select a directory to {0} marked files to".format(opname)
flags = QFileDialog.ShowDirsOnly
destination = unicode(QFileDialog.getExistingDirectory(self.main_window, title, '', flags))
destination = str(QFileDialog.getExistingDirectory(self.main_window, title, '', flags))
if not destination:
return
recreate_path = self.prefs.destination_type
@@ -205,8 +208,13 @@ class DupeGuru(DupeGuruBase, QObject):
self.prefs.save()
self._update_options()
#--- Signals
willSavePrefs = pyqtSignal()
#--- Events
def application_will_terminate(self):
self.willSavePrefs.emit()
self.prefs.save()
self.save()
self.save_ignore_list()

View File

@@ -1,7 +1,7 @@
# cxfreeze has some problems detecting all dependencies.
# This modules explicitly import those problematic modules.
import lxml._elementpath
import xml.etree.ElementPath
import gzip
import os

View File

@@ -20,15 +20,23 @@ class DetailsDialog(QDialog):
self.app = app
self.model = DetailsPanel(self, app)
self._setupUi()
if self.app.prefs.detailsWindowRect is not None:
self.setGeometry(self.app.prefs.detailsWindowRect)
self.tableModel = DetailsModel(self.model)
# tableView is defined in subclasses
self.tableView.setModel(self.tableModel)
self.model.connect()
self.app.willSavePrefs.connect(self.appWillSavePrefs)
def _setupUi(self): # Virtual
pass
# model --> view
#--- Events
def appWillSavePrefs(self):
self.app.prefs.detailsWindowRect = self.geometry()
#--- model --> view
def refresh(self):
self.tableModel.reset()

View File

@@ -26,6 +26,7 @@ class DirectoriesDialog(QDialog, Ui_DirectoriesDialog):
self.connect(self.addButton, SIGNAL('clicked()'), self.addButtonClicked)
self.connect(self.removeButton, SIGNAL('clicked()'), self.removeButtonClicked)
self.connect(self.treeView.selectionModel(), SIGNAL('selectionChanged(QItemSelection,QItemSelection)'), self.selectionChanged)
self.app.willSavePrefs.connect(self.appWillSavePrefs)
def _setupUi(self):
self.setupUi(self)
@@ -40,6 +41,9 @@ class DirectoriesDialog(QDialog, Ui_DirectoriesDialog):
header.setResizeMode(0, QHeaderView.Stretch)
header.setResizeMode(1, QHeaderView.Fixed)
header.resizeSection(1, 100)
if self.app.prefs.directoriesWindowRect is not None:
self.setGeometry(self.app.prefs.directoriesWindowRect)
def _updateRemoveButton(self):
indexes = self.treeView.selectedIndexes()
@@ -51,15 +55,19 @@ class DirectoriesDialog(QDialog, Ui_DirectoriesDialog):
node = index.internalPointer()
# label = 'Remove' if node.parent is None else 'Exclude'
#--- Events
def addButtonClicked(self):
title = u"Select a directory to add to the scanning list"
title = "Select a directory to add to the scanning list"
flags = QFileDialog.ShowDirsOnly
dirpath = unicode(QFileDialog.getExistingDirectory(self, title, self.lastAddedFolder, flags))
dirpath = str(QFileDialog.getExistingDirectory(self, title, self.lastAddedFolder, flags))
if not dirpath:
return
self.lastAddedFolder = dirpath
self.app.add_directory(dirpath)
def appWillSavePrefs(self):
self.app.prefs.directoriesWindowRect = self.geometry()
def doneButtonClicked(self):
self.hide()

View File

@@ -6,7 +6,7 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
import urllib
import urllib.parse
from PyQt4.QtCore import QModelIndex, Qt, QRect, QEvent, QPoint, QUrl
from PyQt4.QtGui import (QComboBox, QStyledItemDelegate, QMouseEvent, QApplication, QBrush, QStyle,
@@ -101,9 +101,9 @@ class DirectoriesModel(TreeModel):
if not mimeData.hasFormat('text/uri-list'):
return False
data = str(mimeData.data('text/uri-list'))
unquoted = urllib.unquote(data)
urls = unicode(unquoted, 'utf-8').split('\r\n')
paths = [unicode(QUrl(url).toLocalFile()) for url in urls if url]
unquoted = urllib.parse.unquote(data)
urls = str(unquoted, 'utf-8').split('\r\n')
paths = [str(QUrl(url).toLocalFile()) for url in urls if url]
for path in paths:
self.model.add_directory(path)
self.reset()

View File

@@ -10,16 +10,16 @@ import sys
from PyQt4.QtCore import Qt, QCoreApplication, QProcess, SIGNAL, QUrl
from PyQt4.QtGui import (QMainWindow, QMenu, QPixmap, QIcon, QToolButton, QLabel, QHeaderView,
QMessageBox, QInputDialog, QLineEdit, QDesktopServices)
QMessageBox, QInputDialog, QLineEdit, QDesktopServices, QFileDialog)
from hsutil.misc import nonone
from core.app import NoScannableFileError, AllFilesAreRefError
from core.app import NoScannableFileError
import dg_rc
from main_window_ui import Ui_MainWindow
from results_model import ResultsModel
from stats_label import StatsLabel
from . import dg_rc
from .main_window_ui import Ui_MainWindow
from .results_model import ResultsModel
from .stats_label import StatsLabel
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self, app):
@@ -34,10 +34,16 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.connect(self.actionQuit, SIGNAL('triggered()'), QCoreApplication.instance().quit)
self.connect(self.menuColumns, SIGNAL('triggered(QAction*)'), self.columnToggled)
self.connect(QCoreApplication.instance(), SIGNAL('aboutToQuit()'), self.application_will_terminate)
self.connect(self.resultsView, SIGNAL('doubleClicked()'), self.resultsDoubleClicked)
self.connect(self.resultsView, SIGNAL('spacePressed()'), self.resultsSpacePressed)
self.app.willSavePrefs.connect(self.appWillSavePrefs)
# Actions (the vast majority of them are connected in the UI file, but I'm trying to
# phase away from those, and these connections are harder to maintain than through simple
# code
self.actionInvokeCustomCommand.triggered.connect(self.app.invokeCustomCommand)
self.actionLoadResults.triggered.connect(self.loadResultsTriggered)
self.actionSaveResults.triggered.connect(self.saveResultsTriggered)
def _setupUi(self):
self.setupUi(self)
@@ -90,6 +96,9 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.statusLabel = QLabel(self)
self.statusbar.addPermanentWidget(self.statusLabel, 1)
if self.app.prefs.mainWindowRect is not None and not self.app.prefs.mainWindowIsMaximized:
self.setGeometry(self.app.prefs.mainWindowRect)
# Linux setup
if sys.platform == 'linux2':
self.actionCheckForUpdate.setVisible(False) # This only works on Windows
@@ -104,24 +113,12 @@ class MainWindow(QMainWindow, Ui_MainWindow):
h = self.resultsView.header()
h.setResizeMode(QHeaderView.Interactive)
prefs = self.app.prefs
attrs = zip(prefs.columns_width, prefs.columns_visible)
attrs = list(zip(prefs.columns_width, prefs.columns_visible))
for index, (width, visible) in enumerate(attrs):
h.resizeSection(index, width)
h.setSectionHidden(index, not visible)
h.setResizeMode(0, QHeaderView.Stretch)
def _save_columns(self):
h = self.resultsView.header()
widths = []
visible = []
for i in range(len(self.app.data.COLUMNS)):
widths.append(h.sectionSize(i))
visible.append(not h.isSectionHidden(i))
prefs = self.app.prefs
prefs.columns_width = widths
prefs.columns_visible = visible
prefs.save()
def _update_column_actions_status(self):
h = self.resultsView.header()
for action in self._column_actions:
@@ -145,7 +142,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
answer, ok = QInputDialog.getText(self, title, msg, QLineEdit.Normal, text)
if not ok:
return
answer = unicode(answer)
answer = str(answer)
self.app.apply_filter(answer)
self._last_filter = answer
@@ -196,7 +193,14 @@ class MainWindow(QMainWindow, Ui_MainWindow):
exported_path = self.app.export_to_xhtml(column_ids)
url = QUrl.fromLocalFile(exported_path)
QDesktopServices.openUrl(url)
def loadResultsTriggered(self):
title = "Select a results file to load"
files = "dupeGuru Results (*.dupeguru)"
destination = QFileDialog.getOpenFileName(self, title, '', files)
if destination:
self.app.load_from(destination)
def makeReferenceTriggered(self):
self.app.make_selected_reference()
@@ -248,6 +252,13 @@ class MainWindow(QMainWindow, Ui_MainWindow):
def revealTriggered(self):
self.app.reveal_selected()
def saveResultsTriggered(self):
title = "Select a file to save your results to"
files = "dupeGuru Results (*.dupeguru)"
destination = QFileDialog.getSaveFileName(self, title, '', files)
if destination:
self.app.save_as(destination)
def scanTriggered(self):
title = "Start a new scan"
if len(self.app.results.groups) > 0:
@@ -260,16 +271,23 @@ class MainWindow(QMainWindow, Ui_MainWindow):
msg = "The selected directories contain no scannable file."
QMessageBox.warning(self, title, msg)
self.app.show_directories()
except AllFilesAreRefError:
msg = "You cannot make a duplicate scan with only reference directories."
QMessageBox.warning(self, title, msg)
def showHelpTriggered(self):
self.app.show_help()
#--- Events
def application_will_terminate(self):
self._save_columns()
def appWillSavePrefs(self):
prefs = self.app.prefs
h = self.resultsView.header()
widths = []
visible = []
for i in range(len(self.app.data.COLUMNS)):
widths.append(h.sectionSize(i))
visible.append(not h.isSectionHidden(i))
prefs.columns_width = widths
prefs.columns_visible = visible
prefs.mainWindowIsMaximized = self.isMaximized()
prefs.mainWindowRect = self.geometry()
def columnToggled(self, action):
colid = action.column_index

View File

@@ -126,6 +126,8 @@
</property>
<addaction name="actionScan"/>
<addaction name="separator"/>
<addaction name="actionLoadResults"/>
<addaction name="actionSaveResults"/>
<addaction name="actionExport"/>
<addaction name="actionClearIgnoreList"/>
<addaction name="separator"/>
@@ -183,7 +185,7 @@
<string>Start scanning for duplicates</string>
</property>
<property name="shortcut">
<string>Ctrl+S</string>
<string>Ctrl+T</string>
</property>
</action>
<action name="actionDirectories">
@@ -427,6 +429,22 @@
<string>Export To XHTML</string>
</property>
</action>
<action name="actionLoadResults">
<property name="text">
<string>Load Results...</string>
</property>
<property name="shortcut">
<string>Ctrl+L</string>
</property>
</action>
<action name="actionSaveResults">
<property name="text">
<string>Save Results...</string>
</property>
<property name="shortcut">
<string>Ctrl+S</string>
</property>
</action>
<action name="actionOpenDebugLog">
<property name="text">
<string>Open Debug Log</string>

View File

@@ -11,10 +11,10 @@ import logging
import sys
if sys.platform == 'win32':
from platform_win import *
from .platform_win import *
elif sys.platform == 'darwin':
from platform_osx import *
from .platform_osx import *
elif sys.platform == 'linux2':
from platform_lnx import *
from .platform_lnx import *
else:
pass # unsupported platform

View File

@@ -7,5 +7,5 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
INITIAL_FOLDER_IN_DIALOGS = u'/'
INITIAL_FOLDER_IN_DIALOGS = '/'
HELP_PATH = '/usr/local/share/dupeguru_{0}/help'

View File

@@ -9,5 +9,5 @@
# dummy unit to allow the app to run under OSX during development
INITIAL_FOLDER_IN_DIALOGS = u'/'
INITIAL_FOLDER_IN_DIALOGS = '/'
HELP_PATH = ''

View File

@@ -7,5 +7,5 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
INITIAL_FOLDER_IN_DIALOGS = u'C:\\'
INITIAL_FOLDER_IN_DIALOGS = 'C:\\'
HELP_PATH = 'help'

View File

@@ -16,11 +16,12 @@ class Preferences(PreferencesBase):
PreferencesBase.__init__(self)
self.reset_columns()
def _load_specific(self, settings, get):
def _load_specific(self, settings):
# load prefs specific to the dg edition
pass
def _load_values(self, settings, get):
def _load_values(self, settings):
get = self.get_value
self.filter_hardness = get('FilterHardness', self.filter_hardness)
self.mix_file_kind = get('MixFileKind', self.mix_file_kind)
self.use_regexp = get('UseRegexp', self.use_regexp)
@@ -33,9 +34,15 @@ class Preferences(PreferencesBase):
if width > 0:
self.columns_width[index] = width
self.columns_visible = get('ColumnsVisible', self.columns_visible)
self.mainWindowIsMaximized = get('MainWindowIsMaximized', self.mainWindowIsMaximized)
self.mainWindowRect = self.get_rect('MainWindowRect', self.mainWindowRect)
self.detailsWindowRect = self.get_rect('DetailsWindowRect', self.detailsWindowRect)
self.directoriesWindowRect = self.get_rect('DirectoriesWindowRect', self.directoriesWindowRect)
self.registration_code = get('RegistrationCode', self.registration_code)
self.registration_email = get('RegistrationEmail', self.registration_email)
self._load_specific(settings, get)
self._load_specific(settings)
def _reset_specific(self):
# reset prefs specific to the dg edition
@@ -48,6 +55,12 @@ class Preferences(PreferencesBase):
self.remove_empty_folders = False
self.destination_type = 1
self.custom_command = ''
self.mainWindowIsMaximized = False
self.mainWindowRect = None
self.detailsWindowRect = None
self.directoriesWindowRect = None
self.registration_code = ''
self.registration_email = ''
self._reset_specific()
@@ -56,11 +69,12 @@ class Preferences(PreferencesBase):
self.columns_width = [width for width, _ in self.COLUMNS_DEFAULT_ATTRS]
self.columns_visible = [visible for _, visible in self.COLUMNS_DEFAULT_ATTRS]
def _save_specific(self, settings, set_):
def _save_specific(self, settings):
# save prefs specific to the dg edition
pass
def _save_values(self, settings, set_):
def _save_values(self, settings):
set_ = self.set_value
set_('FilterHardness', self.filter_hardness)
set_('MixFileKind', self.mix_file_kind)
set_('UseRegexp', self.use_regexp)
@@ -69,7 +83,13 @@ class Preferences(PreferencesBase):
set_('CustomCommand', self.custom_command)
set_('ColumnsWidth', self.columns_width)
set_('ColumnsVisible', self.columns_visible)
set_('MainWindowIsMaximized', self.mainWindowIsMaximized)
self.set_rect('MainWindowRect', self.mainWindowRect)
self.set_rect('DetailsWindowRect', self.detailsWindowRect)
self.set_rect('DirectoriesWindowRect', self.directoriesWindowRect)
set_('RegistrationCode', self.registration_code)
set_('RegistrationEmail', self.registration_email)
self._save_specific(settings, set_)
self._save_specific(settings)

View File

@@ -112,7 +112,7 @@ class ResultsModel(TreeModel):
return True
if role == Qt.EditRole:
if index.column() == 0:
value = unicode(value.toString())
value = str(value.toString())
return self.model.rename_selected(value)
return False

View File

@@ -17,8 +17,8 @@ class DupeGuru(DupeGuruBase):
EDITION = 'me'
LOGO_NAME = 'logo_me'
NAME = 'dupeGuru Music Edition'
VERSION = '5.8.0'
DELTA_COLUMNS = frozenset([2, 3, 4, 5, 7, 8])
VERSION = '5.8.1'
DELTA_COLUMNS = frozenset([2, 3, 4, 5, 7])
def __init__(self):
DupeGuruBase.__init__(self, data, appid=1)

View File

@@ -6,8 +6,7 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from core.scanner import (SCAN_TYPE_FILENAME, SCAN_TYPE_FIELDS, SCAN_TYPE_FIELDS_NO_ORDER,
SCAN_TYPE_TAG, SCAN_TYPE_CONTENT, SCAN_TYPE_CONTENT_AUDIO)
from core.scanner import ScanType
from base.preferences import Preferences as PreferencesBase
@@ -21,7 +20,6 @@ class Preferences(PreferencesBase):
(50, True), # Bitrate
(60, False), # Sample Rate
(40, False), # Kind
(120, False), # creation
(120, False), # modification
(120, False), # Title
(120, False), # Artist
@@ -35,7 +33,8 @@ class Preferences(PreferencesBase):
(80, False), # dupe count
]
def _load_specific(self, settings, get):
def _load_specific(self, settings):
get = self.get_value
self.scan_type = get('ScanType', self.scan_type)
self.word_weighting = get('WordWeighting', self.word_weighting)
self.match_similar = get('MatchSimilar', self.match_similar)
@@ -48,7 +47,7 @@ class Preferences(PreferencesBase):
def _reset_specific(self):
self.filter_hardness = 80
self.scan_type = SCAN_TYPE_TAG
self.scan_type = ScanType.Tag
self.word_weighting = True
self.match_similar = False
self.scan_tag_track = False
@@ -58,7 +57,8 @@ class Preferences(PreferencesBase):
self.scan_tag_genre = False
self.scan_tag_year = False
def _save_specific(self, settings, set_):
def _save_specific(self, settings):
set_ = self.set_value
set_('ScanType', self.scan_type)
set_('WordWeighting', self.word_weighting)
set_('MatchSimilar', self.match_similar)

View File

@@ -9,19 +9,18 @@
from PyQt4.QtCore import SIGNAL, Qt
from PyQt4.QtGui import QDialog, QDialogButtonBox
from core.scanner import (SCAN_TYPE_FILENAME, SCAN_TYPE_FIELDS, SCAN_TYPE_FIELDS_NO_ORDER,
SCAN_TYPE_TAG, SCAN_TYPE_CONTENT, SCAN_TYPE_CONTENT_AUDIO)
from core.scanner import ScanType
from preferences_dialog_ui import Ui_PreferencesDialog
import preferences
SCAN_TYPE_ORDER = [
SCAN_TYPE_FILENAME,
SCAN_TYPE_FIELDS,
SCAN_TYPE_FIELDS_NO_ORDER,
SCAN_TYPE_TAG,
SCAN_TYPE_CONTENT,
SCAN_TYPE_CONTENT_AUDIO,
ScanType.Filename,
ScanType.Fields,
ScanType.FieldsNoOrder,
ScanType.Tag,
ScanType.Contents,
ScanType.ContentsAudio,
]
class PreferencesDialog(QDialog, Ui_PreferencesDialog):
@@ -76,7 +75,7 @@ class PreferencesDialog(QDialog, Ui_PreferencesDialog):
prefs.use_regexp = ischecked(self.useRegexpBox)
prefs.remove_empty_folders = ischecked(self.removeEmptyFoldersBox)
prefs.destination_type = self.copyMoveDestinationComboBox.currentIndex()
prefs.custom_command = unicode(self.customCommandEdit.text())
prefs.custom_command = str(self.customCommandEdit.text())
def resetToDefaults(self):
self.load(preferences.Preferences())
@@ -89,9 +88,9 @@ class PreferencesDialog(QDialog, Ui_PreferencesDialog):
def scanTypeChanged(self, index):
scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()]
word_based = scan_type in [SCAN_TYPE_FILENAME, SCAN_TYPE_FIELDS, SCAN_TYPE_FIELDS_NO_ORDER,
SCAN_TYPE_TAG]
tag_based = scan_type == SCAN_TYPE_TAG
word_based = scan_type in (ScanType.Filename, ScanType.Fields, ScanType.FieldsNoOrder,
ScanType.Tag)
tag_based = scan_type == ScanType.Tag
self.filterHardnessSlider.setEnabled(word_based)
self.matchSimilarBox.setEnabled(word_based)
self.wordWeightingBox.setEnabled(word_based)

View File

@@ -6,6 +6,8 @@
# http://www.hardcoded.net/licenses/hs_license
import sys
import sip
sip.setapi('QVariant', 1)
from PyQt4.QtCore import QCoreApplication
from PyQt4.QtGui import QApplication, QIcon, QPixmap

View File

@@ -9,8 +9,7 @@
import os.path as op
import logging
from PyQt4.QtGui import QImage
import PIL.Image
from PyQt4.QtGui import QImage, QImageReader
from hsutil.str import get_file_ext
@@ -41,14 +40,18 @@ class File(fs.File):
fs.File._read_info(self, field)
if field == 'dimensions':
try:
im = PIL.Image.open(unicode(self.path))
self.dimensions = im.size
except IOError:
ir = QImageReader(str(self.path))
size = ir.size()
if size.isValid():
self.dimensions = (size.width(), size.height())
else:
self.dimensions = (0, 0)
except EnvironmentError:
self.dimensions = (0, 0)
logging.warning(u"Could not read image '%s'", unicode(self.path))
logging.warning("Could not read image '%s'", str(self.path))
def get_blocks(self, block_count_per_side):
image = QImage(unicode(self.path))
image = QImage(str(self.path))
image = image.convertToFormat(QImage.Format_RGB888)
return getblocks(image, block_count_per_side)
@@ -57,8 +60,8 @@ class DupeGuru(DupeGuruBase):
EDITION = 'pe'
LOGO_NAME = 'logo_pe'
NAME = 'dupeGuru Picture Edition'
VERSION = '1.9.0'
DELTA_COLUMNS = frozenset([2, 5, 6])
VERSION = '1.9.1'
DELTA_COLUMNS = frozenset([2, 5])
def __init__(self):
DupeGuruBase.__init__(self, data_pe, appid=5)

View File

@@ -28,11 +28,11 @@ class DetailsDialog(DetailsDialogBase, Ui_DetailsDialog):
group = self.app.results.get_group_of_duplicate(dupe)
ref = group.ref
self.selectedPixmap = QPixmap(unicode(dupe.path))
self.selectedPixmap = QPixmap(str(dupe.path))
if ref is dupe:
self.referencePixmap = None
else:
self.referencePixmap = QPixmap(unicode(ref.path))
self.referencePixmap = QPixmap(str(ref.path))
self._updateImages()
def _updateImages(self):

View File

@@ -1,25 +0,0 @@
#!/usr/bin/env python
# Created By: Virgil Dupras
# Created On: 2009-05-22
# Copyright 2010 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" 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/hs_license
import os
import os.path as op
def move(src, dst):
if not op.exists(src):
return
if op.exists(dst):
os.remove(dst)
print 'Moving %s --> %s' % (src, dst)
os.rename(src, dst)
os.chdir('modules')
os.system('python setup.py build_ext --inplace')
os.chdir('..')
move(op.join('modules', '_block.so'), op.join('.', '_block.so'))
move(op.join('modules', '_block.pyd'), op.join('.', '_block.pyd'))

View File

@@ -40,14 +40,14 @@ getblock(PyObject *image, int width, int height)
int i;
pi = PyObject_CallMethod(image, "bytesPerLine", NULL);
bytes_per_line = PyInt_AsSsize_t(pi);
bytes_per_line = PyLong_AsLong(pi);
Py_DECREF(pi);
sipptr = PyObject_CallMethod(image, "bits", NULL);
/* int(sipptr) returns the address of the pointer */
pi = PyObject_CallMethod(sipptr, "__int__", NULL);
Py_DECREF(sipptr);
s = (char *)PyInt_AsSsize_t(pi);
s = (char *)PyLong_AsLong(pi);
Py_DECREF(pi);
/* Qt aligns all its lines on 32bit, which means that if the number of bytes per
* line for image is not divisible by 4, there's going to be crap inserted in "s"
@@ -74,9 +74,9 @@ getblock(PyObject *image, int width, int height)
blue /= pixel_count;
}
pred = PyInt_FromSsize_t(red);
pgreen = PyInt_FromSsize_t(green);
pblue = PyInt_FromSsize_t(blue);
pred = PyLong_FromLong(red);
pgreen = PyLong_FromLong(green);
pblue = PyLong_FromLong(blue);
result = PyTuple_Pack(3, pred, pgreen, pblue);
Py_DECREF(pred);
Py_DECREF(pgreen);
@@ -107,10 +107,10 @@ block_getblocks(PyObject *self, PyObject *args)
}
pi = PyObject_CallMethod(image, "width", NULL);
width = PyInt_AsSsize_t(pi);
width = PyLong_AsLong(pi);
Py_DECREF(pi);
pi = PyObject_CallMethod(image, "height", NULL);
height = PyInt_AsSsize_t(pi);
height = PyLong_AsLong(pi);
Py_DECREF(pi);
if (!(width && height)) {
@@ -157,11 +157,24 @@ static PyMethodDef BlockMethods[] = {
{NULL, NULL, 0, NULL} /* Sentinel */
};
PyMODINIT_FUNC
init_block(void)
static struct PyModuleDef BlockDef = {
PyModuleDef_HEAD_INIT,
"_block",
NULL,
-1,
BlockMethods,
NULL,
NULL,
NULL,
NULL
};
PyObject *
PyInit__block(void)
{
PyObject *m = Py_InitModule("_block", BlockMethods);
PyObject *m = PyModule_Create(&BlockDef);
if (m == NULL) {
return;
return NULL;
}
return m;
}

View File

@@ -1,12 +0,0 @@
# Copyright 2010 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" 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/hs_license
from distutils.core import setup
from distutils.extension import Extension
setup(
ext_modules = [Extension("_block", ["block.c"])]
)

View File

@@ -18,19 +18,18 @@ class Preferences(PreferencesBase):
(60, True), # size
(40, False), # kind
(100, True), # dimensions
(120, False), # creation
(120, False), # modification
(60, True), # match %
(80, False), # dupe count
]
def _load_specific(self, settings, get):
self.match_scaled = get('MatchScaled', self.match_scaled)
def _load_specific(self, settings):
self.match_scaled = self.get_value('MatchScaled', self.match_scaled)
def _reset_specific(self):
self.filter_hardness = 95
self.match_scaled = False
def _save_specific(self, settings, set_):
set_('MatchScaled', self.match_scaled)
def _save_specific(self, settings):
self.set_value('MatchScaled', self.match_scaled)

View File

@@ -46,7 +46,7 @@ class PreferencesDialog(QDialog, Ui_PreferencesDialog):
prefs.use_regexp = ischecked(self.useRegexpBox)
prefs.remove_empty_folders = ischecked(self.removeEmptyFoldersBox)
prefs.destination_type = self.copyMoveDestinationComboBox.currentIndex()
prefs.custom_command = unicode(self.customCommandEdit.text())
prefs.custom_command = str(self.customCommandEdit.text())
def resetToDefaults(self):
self.load(preferences.Preferences())

View File

@@ -6,6 +6,8 @@
# http://www.hardcoded.net/licenses/hs_license
import sys
import sip
sip.setapi('QVariant', 1)
from PyQt4.QtCore import QCoreApplication
from PyQt4.QtGui import QApplication, QIcon, QPixmap
@@ -16,8 +18,6 @@ from app import DupeGuru
if sys.platform == 'win32':
import base.cxfreeze_fix
# This is a workaround for a cxfreeze problem where compiled dupeguru can't read tiff files
from PIL import TiffImagePlugin, TiffTags
if __name__ == "__main__":
app = QApplication(sys.argv)

View File

@@ -27,8 +27,8 @@ class DupeGuru(DupeGuruBase):
EDITION = 'se'
LOGO_NAME = 'logo_se'
NAME = 'dupeGuru'
VERSION = '2.10.1'
DELTA_COLUMNS = frozenset([2, 4, 5])
VERSION = '2.11.0'
DELTA_COLUMNS = frozenset([2, 4])
def __init__(self):
DupeGuruBase.__init__(self, data, appid=4)

View File

@@ -6,7 +6,7 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from core.scanner import SCAN_TYPE_FILENAME, SCAN_TYPE_CONTENT
from core.scanner import ScanType
from base.preferences import Preferences as PreferencesBase
@@ -17,14 +17,14 @@ class Preferences(PreferencesBase):
(180, True), # path
(60, True), # size
(40, False), # Kind
(120, False), # creation
(120, False), # modification
(60, True), # match %
(120, False), # Words Used
(80, False), # dupe count
]
def _load_specific(self, settings, get):
def _load_specific(self, settings):
get = self.get_value
self.scan_type = get('ScanType', self.scan_type)
self.word_weighting = get('WordWeighting', self.word_weighting)
self.match_similar = get('MatchSimilar', self.match_similar)
@@ -33,13 +33,14 @@ class Preferences(PreferencesBase):
def _reset_specific(self):
self.filter_hardness = 80
self.scan_type = SCAN_TYPE_CONTENT
self.scan_type = ScanType.Contents
self.word_weighting = True
self.match_similar = False
self.ignore_small_files = True
self.small_file_threshold = 10 # KB
def _save_specific(self, settings, set_):
def _save_specific(self, settings):
set_ = self.set_value
set_('ScanType', self.scan_type)
set_('WordWeighting', self.word_weighting)
set_('MatchSimilar', self.match_similar)

Some files were not shown because too many files have changed in this diff Show More