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

Compare commits

..

13 Commits

Author SHA1 Message Date
Virgil Dupras
4249c528e9 [#144 state:fixed] Fixed crash on error handling under windows when sys.stderr is None. 2011-02-18 10:10:11 +00:00
Virgil Dupras
084068852e [#140 state:fixed] Fixed a crash on showing details panel in PE right after a mark-based removal (selection wasn't refreshed). 2011-02-18 10:37:40 +01:00
Virgil Dupras
c524a85897 Greatly improved iPhoto deletion process. 2011-02-17 15:08:23 +01:00
Virgil Dupras
d39d46be5a Added tag me6.0.0 for changeset 2fd901a516f8 2011-02-01 12:23:44 +01:00
Virgil Dupras
b8980b4667 Fixed a glitch with initial window position (they'd sometimes end up in awkward places on the screen). 2011-02-01 11:43:16 +01:00
Virgil Dupras
e0adec7b2b me v6.0.0 2011-02-01 10:03:56 +01:00
Virgil Dupras
eb8b9d663f Fixed a mistake in french translation. 2011-02-01 10:01:57 +01:00
Virgil Dupras
fa4b0cf9ec Added tag pe2.0.0 for changeset f1d40b556c01 2011-01-29 14:56:15 +01:00
Virgil Dupras
f72db8dd1d pe v2.0.0 2011-01-29 11:39:51 +01:00
Virgil Dupras
c5bf0f228a Changed error logging in core_pe.cache because it would sometimes result in huge logs of no value. Also, added debug logging during the analysis of pictures. 2011-01-29 11:31:17 +01:00
Virgil Dupras
e150b26cab Forgot a litteral --> constant conversion in the last commit. 2011-01-29 11:20:19 +01:00
Virgil Dupras
da41d07dae [#115 state:fixed] Re-factored the data columns (and delta columns) and made the Dimensions column a delta one. 2011-01-29 11:07:33 +01:00
Virgil Dupras
c885cb35d8 Added tag se3.0.1 for changeset 778876a8a978 2011-01-27 12:11:11 +01:00
34 changed files with 180 additions and 132 deletions

View File

@@ -41,3 +41,6 @@ ca93352ce35184853ad9fcb881935a43a8b1e249 me5.10.3
3f71a8f5bf8f6d0729748a27af9163e013723294 pe1.11.3 3f71a8f5bf8f6d0729748a27af9163e013723294 pe1.11.3
0056293b0dade8b8230f68c1fe6f0c2d1e0b74d8 se2.12.3 0056293b0dade8b8230f68c1fe6f0c2d1e0b74d8 se2.12.3
8d12cab3b12b723e3a86d02cf8002731a0f73f95 se3.0.0 8d12cab3b12b723e3a86d02cf8002731a0f73f95 se3.0.0
778876a8a9787658aa6adf6944b53aebcb7faeea se3.0.1
f1d40b556c01f32c58f9ef9f9acac5b78e01ba7a pe2.0.0
2fd901a516f8cb6b4438491f63f2ebfd52a57c13 me6.0.0

View File

@@ -45,6 +45,7 @@ http://www.hardcoded.net/licenses/bsd_license
- (NSNumber *)getMarkCount; - (NSNumber *)getMarkCount;
- (BOOL)scanWasProblematic; - (BOOL)scanWasProblematic;
- (BOOL)resultsAreModified; - (BOOL)resultsAreModified;
- (NSArray *)deltaColumns;
//Scanning options //Scanning options
- (void)setMinMatchPercentage:(NSNumber *)percentage; - (void)setMinMatchPercentage:(NSNumber *)percentage;

View File

@@ -29,6 +29,7 @@ http://www.hardcoded.net/licenses/bsd_license
[self fillColumnsMenu]; [self fillColumnsMenu];
[matches setTarget:self]; [matches setTarget:self];
[matches setDoubleAction:@selector(openClicked:)]; [matches setDoubleAction:@selector(openClicked:)];
[table setDeltaColumns:[Utils array2IndexSet:[py deltaColumns]]];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobCompleted:) name:JobCompletedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobCompleted:) name:JobCompletedNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobStarted:) name:JobStarted object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobStarted:) name:JobStarted object:nil];

View File

@@ -14,7 +14,7 @@
"56.title" = "Référence"; "56.title" = "Référence";
/* Class = "NSMenuItem"; title = "Excluded"; ObjectID = "57"; */ /* Class = "NSMenuItem"; title = "Excluded"; ObjectID = "57"; */
"57.title" = "Exclus"; "57.title" = "Exclu";
/* Class = "NSTextFieldCell"; title = "Select folders to scan and press \"Scan\"."; ObjectID = "71"; */ /* Class = "NSTextFieldCell"; title = "Select folders to scan and press \"Scan\"."; ObjectID = "71"; */
"71.title" = "Sélectionnez les dossiers à scanner et cliquez sur Scan."; "71.title" = "Sélectionnez les dossiers à scanner et cliquez sur Scan.";

View File

@@ -191,7 +191,7 @@
</object> </object>
<object class="NSMenuItem" id="142495353"> <object class="NSMenuItem" id="142495353">
<reference key="NSMenu" ref="104112446"/> <reference key="NSMenu" ref="104112446"/>
<string key="NSTitle">Exclus</string> <string key="NSTitle">Exclu</string>
<string key="NSKeyEquiv"/> <string key="NSKeyEquiv"/>
<int key="NSMnemonicLoc">2147483647</int> <int key="NSMnemonicLoc">2147483647</int>
<string key="NSAction">_popUpItemAction:</string> <string key="NSAction">_popUpItemAction:</string>

View File

@@ -14,15 +14,6 @@ http://www.hardcoded.net/licenses/bsd_license
@implementation ResultWindow @implementation ResultWindow
/* Override */ /* Override */
- (id)initWithParentApp:(AppDelegateBase *)aApp;
{
self = [super initWithParentApp:aApp];
NSMutableIndexSet *deltaColumns = [NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(2,6)];
[deltaColumns removeIndex:6];
[table setDeltaColumns:deltaColumns];
return self;
}
- (void)setScanOptions - (void)setScanOptions
{ {
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];

View File

@@ -14,15 +14,6 @@ http://www.hardcoded.net/licenses/bsd_license
@implementation ResultWindow @implementation ResultWindow
/* Override */ /* Override */
- (id)initWithParentApp:(AppDelegateBase *)aApp;
{
self = [super initWithParentApp:aApp];
NSMutableIndexSet *deltaColumns = [NSMutableIndexSet indexSetWithIndex:2];
[deltaColumns addIndex:5];
[table setDeltaColumns:deltaColumns];
return self;
}
- (void)initResultColumns - (void)initResultColumns
{ {
NSTableColumn *refCol = [matches tableColumnWithIdentifier:@"0"]; NSTableColumn *refCol = [matches tableColumnWithIdentifier:@"0"];

View File

@@ -13,15 +13,6 @@ http://www.hardcoded.net/licenses/bsd_license
@implementation ResultWindow @implementation ResultWindow
/* Override */ /* Override */
- (id)initWithParentApp:(AppDelegateBase *)aApp;
{
self = [super initWithParentApp:aApp];
NSMutableIndexSet *deltaColumns = [NSMutableIndexSet indexSetWithIndex:2];
[deltaColumns addIndex:4];
[table setDeltaColumns:deltaColumns];
return self;
}
- (void)initResultColumns - (void)initResultColumns
{ {
NSTableColumn *refCol = [matches tableColumnWithIdentifier:@"0"]; NSTableColumn *refCol = [matches tableColumnWithIdentifier:@"0"];

View File

@@ -98,12 +98,17 @@ class DupeGuru(RegistrableApplication, Broadcaster):
except EnvironmentError: except EnvironmentError:
return None return None
def _results_changed(self):
self.selected_dupes = [d for d in self.selected_dupes
if self.results.get_group_of_duplicate(d) is not None]
self.notify('results_changed')
def _job_completed(self, jobid): def _job_completed(self, jobid):
# Must be called by subclasses when they detect that an async job is completed. # Must be called by subclasses when they detect that an async job is completed.
if jobid == JOB_SCAN: if jobid == JOB_SCAN:
self.notify('results_changed') self._results_changed()
elif jobid in (JOB_LOAD, JOB_MOVE, JOB_DELETE): elif jobid in (JOB_LOAD, JOB_MOVE, JOB_DELETE):
self.notify('results_changed') self._results_changed()
self.notify('problems_changed') self.notify('problems_changed')
@staticmethod @staticmethod
@@ -171,7 +176,7 @@ class DupeGuru(RegistrableApplication, Broadcaster):
filter = escape(filter, set('()[]\\.|+?^')) filter = escape(filter, set('()[]\\.|+?^'))
filter = escape(filter, '*', '.') filter = escape(filter, '*', '.')
self.results.apply_filter(filter) self.results.apply_filter(filter)
self.notify('results_changed') self._results_changed()
def clean_empty_dirs(self, path): def clean_empty_dirs(self, path):
if self.options['clean_empty_dirs']: if self.options['clean_empty_dirs']:
@@ -221,7 +226,7 @@ class DupeGuru(RegistrableApplication, Broadcaster):
column_ids = [colid for colid in column_ids if colid.isdigit()] column_ids = [colid for colid in column_ids if colid.isdigit()]
column_ids = list(map(int, column_ids)) column_ids = list(map(int, column_ids))
column_ids.sort() column_ids.sort()
colnames = [col['display'] for i, col in enumerate(self.data.COLUMNS) if i in column_ids] colnames = [col.display for i, col in enumerate(self.data.COLUMNS) if i in column_ids]
rows = [] rows = []
for group in self.results.groups: for group in self.results.groups:
for dupe in group: for dupe in group:
@@ -316,7 +321,7 @@ class DupeGuru(RegistrableApplication, Broadcaster):
def remove_marked(self): def remove_marked(self):
self.results.perform_on_marked(lambda x:None, True) self.results.perform_on_marked(lambda x:None, True)
self.notify('results_changed') self._results_changed()
def remove_selected(self): def remove_selected(self):
self.remove_duplicates(self.selected_dupes) self.remove_duplicates(self.selected_dupes)
@@ -356,7 +361,7 @@ class DupeGuru(RegistrableApplication, Broadcaster):
if not self.directories.has_any_file(): if not self.directories.has_any_file():
raise NoScannableFileError() raise NoScannableFileError()
self.results.groups = [] self.results.groups = []
self.notify('results_changed') self._results_changed()
self._start_job(JOB_SCAN, do) self._start_job(JOB_SCAN, do)
def toggle_selected_mark_state(self): def toggle_selected_mark_state(self):

View File

@@ -112,6 +112,9 @@ class PyDupeGuruBase(PyFairware):
def resultsAreModified(self): def resultsAreModified(self):
return self.py.results.is_modified return self.py.results.is_modified
def deltaColumns(self):
return list(self.py.data.DELTA_COLUMNS)
#---Properties #---Properties
@signature('v@:c') @signature('v@:c')
def setMixFileKind_(self, mix_file_kind): def setMixFileKind_(self, mix_file_kind):

View File

@@ -6,10 +6,14 @@
# which should be included with this package. The terms are also available at # which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license # http://www.hardcoded.net/licenses/bsd_license
from collections import namedtuple
from hscommon.util import format_time_decimal, format_size from hscommon.util import format_time_decimal, format_size
import time import time
Column = namedtuple('Column', 'attr display')
def format_path(p): def format_path(p):
return str(p[:-1]) return str(p[:-1])

View File

@@ -31,7 +31,7 @@ class DetailsPanel(GUIObject):
# we don't want the two sides of the table to display the stats for the same file # we don't want the two sides of the table to display the stats for the same file
ref = group.ref if group is not None and group.ref is not dupe else None 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) l2 = self.app._get_display_info(ref, group, False)
names = [c['display'] for c in self.app.data.COLUMNS] names = [c.display for c in self.app.data.COLUMNS]
self._table = list(zip(names, l1, l2)) self._table = list(zip(names, l1, l2))
#--- Public #--- Public

View File

@@ -259,11 +259,14 @@ class Results(Markable):
group = self.get_group_of_duplicate(dupe) group = self.get_group_of_duplicate(dupe)
if dupe not in group.dupes: if dupe not in group.dupes:
return return
ref = group.ref
group.remove_dupe(dupe, False) group.remove_dupe(dupe, False)
del self.__group_of_duplicate[dupe]
self._remove_mark_flag(dupe) self._remove_mark_flag(dupe)
self.__total_count -= 1 self.__total_count -= 1
self.__total_size -= dupe.size self.__total_size -= dupe.size
if not group: if not group:
del self.__group_of_duplicate[ref]
self.__groups.remove(group) self.__groups.remove(group)
if self.__filtered_groups: if self.__filtered_groups:
self.__filtered_groups.remove(group) self.__filtered_groups.remove(group)

View File

@@ -391,6 +391,16 @@ class TestCaseDupeGuruWithResults:
app.start_scanning() # will be cancelled immediately app.start_scanning() # will be cancelled immediately
eq_(len(self.rtable), 0) eq_(len(self.rtable), 0)
def test_selected_dupes_after_removal(self, do_setup):
# Purge the app's `selected_dupes` attribute when removing dupes, or else it might cause a
# crash later with None refs.
app = self.app
app.results.mark_all()
self.rtable.select([0, 1, 2, 3, 4])
app.remove_marked()
eq_(len(self.rtable), 0)
eq_(app.selected_dupes, [])
class TestCaseDupeGuru_renameSelected: class TestCaseDupeGuru_renameSelected:
def pytest_funcarg__do_setup(self, request): def pytest_funcarg__do_setup(self, request):
tmpdir = request.getfuncargvalue('tmpdir') tmpdir = request.getfuncargvalue('tmpdir')

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Created By: Virgil Dupras # Created By: Virgil Dupras
# Created On: 2009-10-23 # Created On: 2009-10-23
# Copyright 2010 Hardcoded Software (http://www.hardcoded.net) # Copyright 2010 Hardcoded Software (http://www.hardcoded.net)
@@ -10,16 +9,17 @@
# data module for tests # data module for tests
from hscommon.util import format_size from hscommon.util import format_size
from ..data import format_path, cmp_value from ..data import format_path, cmp_value, Column
COLUMNS = [ COLUMNS = [
{'attr':'name','display':'Filename'}, Column('name', 'Filename'),
{'attr':'path','display':'Directory'}, Column('path', 'Directory'),
{'attr':'size','display':'Size (KB)'}, Column('size', 'Size (KB)'),
{'attr':'extension','display':'Kind'}, Column('extension', 'Kind'),
] ]
METADATA_TO_READ = ['size'] METADATA_TO_READ = ['size']
DELTA_COLUMNS = {2,}
def GetDisplayInfo(dupe, group, delta): def GetDisplayInfo(dupe, group, delta):
size = dupe.size size = dupe.size
@@ -35,10 +35,10 @@ def GetDisplayInfo(dupe, group, delta):
] ]
def GetDupeSortKey(dupe, get_group, key, delta): def GetDupeSortKey(dupe, get_group, key, delta):
r = cmp_value(getattr(dupe, COLUMNS[key]['attr'])) r = cmp_value(getattr(dupe, COLUMNS[key].attr))
if delta and (key == 2): if delta and (key in DELTA_COLUMNS):
r -= cmp_value(getattr(get_group().ref, COLUMNS[key]['attr'])) r -= cmp_value(getattr(get_group().ref, COLUMNS[key].attr))
return r return r
def GetGroupSortKey(group, key): def GetGroupSortKey(group, key):
return cmp_value(getattr(group.ref, COLUMNS[key]['attr'])) return cmp_value(getattr(group.ref, COLUMNS[key].attr))

View File

@@ -231,6 +231,15 @@ class TestCaseResultsWithSomeGroups:
self.results.perform_on_marked(lambda x:None, True) self.results.perform_on_marked(lambda x:None, True)
assert not self.results.is_modified assert not self.results.is_modified
def test_group_of_duplicate_after_removal(self):
# removing a duplicate also removes it from the dupe:group map.
dupe = self.results.groups[1].dupes[0]
ref = self.results.groups[1].ref
self.results.remove_duplicates([dupe])
assert self.results.get_group_of_duplicate(dupe) is None
# also remove group ref
assert self.results.get_group_of_duplicate(ref) is None
class TestCaseResultsWithSavedResults: class TestCaseResultsWithSavedResults:
def setup_method(self, method): def setup_method(self, method):

View File

@@ -1,2 +1,2 @@
__version__ = '5.10.4' __version__ = '6.0.0'
__appname__ = 'dupeGuru Music Edition' __appname__ = 'dupeGuru Music Edition'

View File

@@ -9,33 +9,34 @@
from hscommon.util import format_time, format_size from hscommon.util import format_time, format_size
from hscommon.trans import tr as trbase from hscommon.trans import tr as trbase
from core.data import (format_path, format_timestamp, format_words, format_perc, from core.data import (format_path, format_timestamp, format_words, format_perc,
format_dupe_count, cmp_value) format_dupe_count, cmp_value, Column)
tr = lambda s: trbase(s, 'columns') tr = lambda s: trbase(s, 'columns')
COLUMNS = [ COLUMNS = [
{'attr': 'name', 'display': tr("Filename")}, Column('name', tr("Filename")),
{'attr': 'path', 'display': tr("Folder")}, Column('path', tr("Folder")),
{'attr': 'size', 'display': tr("Size (MB)")}, Column('size', tr("Size (MB)")),
{'attr': 'duration', 'display': tr("Time")}, Column('duration', tr("Time")),
{'attr': 'bitrate', 'display': tr("Bitrate")}, Column('bitrate', tr("Bitrate")),
{'attr': 'samplerate', 'display': tr("Sample Rate")}, Column('samplerate', tr("Sample Rate")),
{'attr': 'extension', 'display': tr("Kind")}, Column('extension', tr("Kind")),
{'attr': 'mtime', 'display': tr("Modification")}, Column('mtime', tr("Modification")),
{'attr': 'title', 'display': tr("Title")}, Column('title', tr("Title")),
{'attr': 'artist', 'display': tr("Artist")}, Column('artist', tr("Artist")),
{'attr': 'album', 'display': tr("Album")}, Column('album', tr("Album")),
{'attr': 'genre', 'display': tr("Genre")}, Column('genre', tr("Genre")),
{'attr': 'year', 'display': tr("Year")}, Column('year', tr("Year")),
{'attr': 'track', 'display': tr("Track Number")}, Column('track', tr("Track Number")),
{'attr': 'comment', 'display': tr("Comment")}, Column('comment', tr("Comment")),
{'attr': 'percentage', 'display': tr("Match %")}, Column('percentage', tr("Match %")),
{'attr': 'words', 'display': tr("Words Used")}, Column('words', tr("Words Used")),
{'attr': 'dupe_count', 'display': tr("Dupe Count")}, Column('dupe_count', tr("Dupe Count")),
] ]
MATCHPERC_COL = 15 MATCHPERC_COL = 15
DUPECOUNT_COL = 17 DUPECOUNT_COL = 17
DELTA_COLUMNS = {2, 3, 4, 5, 7}
METADATA_TO_READ = ['size', 'mtime', 'duration', 'bitrate', 'samplerate', 'title', 'artist', METADATA_TO_READ = ['size', 'mtime', 'duration', 'bitrate', 'samplerate', 'title', 'artist',
'album', 'genre', 'year', 'track', 'comment'] 'album', 'genre', 'year', 'track', 'comment']
@@ -87,9 +88,9 @@ def GetDupeSortKey(dupe, get_group, key, delta):
return m.percentage return m.percentage
if key == DUPECOUNT_COL: if key == DUPECOUNT_COL:
return 0 return 0
r = cmp_value(getattr(dupe, COLUMNS[key]['attr'], '')) r = cmp_value(getattr(dupe, COLUMNS[key].attr, ''))
if delta and (key in {2, 3, 4, 7}): if delta and (key in DELTA_COLUMNS):
r -= cmp_value(getattr(get_group().ref, COLUMNS[key]['attr'], '')) r -= cmp_value(getattr(get_group().ref, COLUMNS[key].attr, ''))
return r return r
def GetGroupSortKey(group, key): def GetGroupSortKey(group, key):
@@ -97,4 +98,4 @@ def GetGroupSortKey(group, key):
return group.percentage return group.percentage
if key == DUPECOUNT_COL: if key == DUPECOUNT_COL:
return len(group) return len(group)
return cmp_value(getattr(group.ref, COLUMNS[key]['attr'], '')) return cmp_value(getattr(group.ref, COLUMNS[key].attr, ''))

View File

@@ -1,2 +1,2 @@
__version__ = '1.11.3' __version__ = '2.0.0'
__appname__ = 'dupeGuru Picture Edition' __appname__ = 'dupeGuru Picture Edition'

View File

@@ -11,12 +11,11 @@ import plistlib
import logging import logging
import re import re
from appscript import app, k, CommandError, ApplicationNotFoundError from appscript import app, its, CommandError, ApplicationNotFoundError
from hscommon import io from hscommon import io
from hscommon.util import get_file_ext, remove_invalid_xml from hscommon.util import get_file_ext, remove_invalid_xml
from hscommon.path import Path from hscommon.path import Path
from hscommon.cocoa import as_fetch
from hscommon.cocoa.objcmin import NSUserDefaults, NSURL from hscommon.cocoa.objcmin import NSUserDefaults, NSURL
from hscommon.trans import tr from hscommon.trans import tr
@@ -151,37 +150,28 @@ class DupeGuruPE(app_cocoa.DupeGuru):
return self._do_delete_dupe(dupe, replace_with_hardlinks) return self._do_delete_dupe(dupe, replace_with_hardlinks)
marked = [dupe for dupe in self.results.dupes if self.results.is_marked(dupe)] marked = [dupe for dupe in self.results.dupes if self.results.is_marked(dupe)]
self.path2iphoto = {} j.start_job(self.results.mark_count, tr("Sending dupes to the Trash"))
if any(isinstance(dupe, IPhoto) for dupe in marked): if any(isinstance(dupe, IPhoto) for dupe in marked):
j = j.start_subjob([6, 4], tr("Probing iPhoto. Don't touch it during the operation!")) j.add_progress(0, desc=tr("Talking to iPhoto. Don't touch it!"))
try: try:
a = app('iPhoto') a = app('iPhoto')
a.activate(timeout=0) a.activate(timeout=0)
a.select(a.photo_library_album(timeout=0), timeout=0) a.select(a.photo_library_album(timeout=0), timeout=0)
photos = as_fetch(a.photo_library_album().photos, k.item)
for photo in j.iter_with_progress(photos):
try:
self.path2iphoto[str(photo.image_path(timeout=0))] = photo
except CommandError:
pass
except (CommandError, RuntimeError, ApplicationNotFoundError): except (CommandError, RuntimeError, ApplicationNotFoundError):
pass pass
j.start_job(self.results.mark_count, tr("Sending dupes to the Trash"))
self.results.perform_on_marked(op, True) self.results.perform_on_marked(op, True)
del self.path2iphoto
def _do_delete_dupe(self, dupe, replace_with_hardlinks): def _do_delete_dupe(self, dupe, replace_with_hardlinks):
if isinstance(dupe, IPhoto): if isinstance(dupe, IPhoto):
if str(dupe.path) in self.path2iphoto:
photo = self.path2iphoto[str(dupe.path)]
try: try:
a = app('iPhoto') a = app('iPhoto')
[photo] = a.photo_library_album().photos[its.image_path == str(dupe.path)]()
a.remove(photo, timeout=0) a.remove(photo, timeout=0)
except ValueError:
msg = "Could not find photo '{}' in iPhoto Library".format(str(dupe.path))
raise EnvironmentError(msg)
except (CommandError, RuntimeError) as e: except (CommandError, RuntimeError) as e:
raise EnvironmentError(str(e)) raise EnvironmentError(str(e))
else:
msg = "Could not find photo %s in iPhoto Library" % str(dupe.path)
raise EnvironmentError(msg)
else: else:
app_cocoa.DupeGuru._do_delete_dupe(self, dupe, replace_with_hardlinks) app_cocoa.DupeGuru._do_delete_dupe(self, dupe, replace_with_hardlinks)

View File

@@ -81,9 +81,9 @@ class Cache(object):
try: try:
self.con.execute(sql, [value, key]) self.con.execute(sql, [value, key])
except sqlite.OperationalError: except sqlite.OperationalError:
logging.warning('Picture cache could not set %r for key %r', value, key) logging.warning('Picture cache could not set value for key %r', key)
except sqlite.DatabaseError as e: except sqlite.DatabaseError as e:
logging.warning('DatabaseError while setting %r for key %r: %s', value, key, str(e)) logging.warning('DatabaseError while setting value for key %r: %s', key, str(e))
def _create_con(self, second_try=False): def _create_con(self, second_try=False):
def create_tables(): def create_tables():

View File

@@ -8,7 +8,7 @@
from hscommon.util import format_size from hscommon.util import format_size
from hscommon.trans import tr as trbase from hscommon.trans import tr as trbase
from core.data import format_path, format_timestamp, format_perc, format_dupe_count, cmp_value from core.data import format_path, format_timestamp, format_perc, format_dupe_count, cmp_value, Column
tr = lambda s: trbase(s, 'columns') tr = lambda s: trbase(s, 'columns')
@@ -16,26 +16,31 @@ def format_dimensions(dimensions):
return '%d x %d' % (dimensions[0], dimensions[1]) return '%d x %d' % (dimensions[0], dimensions[1])
COLUMNS = [ COLUMNS = [
{'attr':'name', 'display': tr("Filename")}, Column('name', tr("Filename")),
{'attr':'path', 'display': tr("Folder")}, Column('path', tr("Folder")),
{'attr':'size', 'display': tr("Size (KB)")}, Column('size', tr("Size (KB)")),
{'attr':'extension', 'display': tr("Kind")}, Column('extension', tr("Kind")),
{'attr':'dimensions', 'display': tr("Dimensions")}, Column('dimensions', tr("Dimensions")),
{'attr':'mtime', 'display': tr("Modification")}, Column('mtime', tr("Modification")),
{'attr':'percentage', 'display': tr("Match %")}, Column('percentage', tr("Match %")),
{'attr':'dupe_count', 'display': tr("Dupe Count")}, Column('dupe_count', tr("Dupe Count")),
] ]
MATCHPERC_COL = 6 MATCHPERC_COL = 6
DUPECOUNT_COL = 7 DUPECOUNT_COL = 7
DELTA_COLUMNS = {2, 4, 5}
METADATA_TO_READ = ['size', 'mtime', 'dimensions'] METADATA_TO_READ = ['size', 'mtime', 'dimensions']
def get_delta_dimensions(value, ref_value):
return (value[0]-ref_value[0], value[1]-ref_value[1])
def GetDisplayInfo(dupe,group,delta=False): def GetDisplayInfo(dupe,group,delta=False):
if (dupe is None) or (group is None): if (dupe is None) or (group is None):
return ['---'] * len(COLUMNS) return ['---'] * len(COLUMNS)
size = dupe.size size = dupe.size
mtime = dupe.mtime mtime = dupe.mtime
dimensions = dupe.dimensions
m = group.get_match_of(dupe) m = group.get_match_of(dupe)
if m: if m:
percentage = m.percentage percentage = m.percentage
@@ -44,6 +49,7 @@ def GetDisplayInfo(dupe,group,delta=False):
r = group.ref r = group.ref
size -= r.size size -= r.size
mtime -= r.mtime mtime -= r.mtime
dimensions = get_delta_dimensions(dimensions, r.dimensions)
else: else:
percentage = group.percentage percentage = group.percentage
dupe_count = len(group.dupes) dupe_count = len(group.dupes)
@@ -53,7 +59,7 @@ def GetDisplayInfo(dupe,group,delta=False):
format_path(dupe_path), format_path(dupe_path),
format_size(size, 0, 1, False), format_size(size, 0, 1, False),
dupe.extension, dupe.extension,
format_dimensions(dupe.dimensions), format_dimensions(dimensions),
format_timestamp(mtime, delta and m), format_timestamp(mtime, delta and m),
format_perc(percentage), format_perc(percentage),
format_dupe_count(dupe_count) format_dupe_count(dupe_count)
@@ -65,9 +71,13 @@ def GetDupeSortKey(dupe, get_group, key, delta):
return m.percentage return m.percentage
if key == DUPECOUNT_COL: if key == DUPECOUNT_COL:
return 0 return 0
r = cmp_value(getattr(dupe, COLUMNS[key]['attr'], '')) r = cmp_value(getattr(dupe, COLUMNS[key].attr, ''))
if delta and (key in {2, 5}): if delta and (key in DELTA_COLUMNS):
r -= cmp_value(getattr(get_group().ref, COLUMNS[key]['attr'], '')) ref_value = cmp_value(getattr(get_group().ref, COLUMNS[key].attr, ''))
if key == 4: # dimensions
r = get_delta_dimensions(r, ref_value)
else:
r -= ref_value
return r return r
def GetGroupSortKey(group, key): def GetGroupSortKey(group, key):
@@ -75,5 +85,5 @@ def GetGroupSortKey(group, key):
return group.percentage return group.percentage
if key == DUPECOUNT_COL: if key == DUPECOUNT_COL:
return len(group) return len(group)
return cmp_value(getattr(group.ref, COLUMNS[key]['attr'], '')) return cmp_value(getattr(group.ref, COLUMNS[key].attr, ''))

View File

@@ -34,8 +34,9 @@ def prepare_pictures(pictures, cache_path, j=job.nulljob):
prepared = [] # only pictures for which there was no error getting blocks prepared = [] # only pictures for which there was no error getting blocks
try: try:
for picture in j.iter_with_progress(pictures, tr("Analyzed %d/%d pictures")): for picture in j.iter_with_progress(pictures, tr("Analyzed %d/%d pictures")):
picture.dimensions
picture.unicode_path = str(picture.path) picture.unicode_path = str(picture.path)
logging.debug("Analyzing picture at {}".format(picture.unicode_path))
picture.dimensions # pre-read dimensions
try: try:
if picture.unicode_path not in cache: if picture.unicode_path not in cache:
blocks = picture.get_blocks(BLOCK_COUNT_PER_SIDE) blocks = picture.get_blocks(BLOCK_COUNT_PER_SIDE)

View File

@@ -9,23 +9,24 @@
from hscommon.util import format_size from hscommon.util import format_size
from hscommon.trans import tr as trbase from hscommon.trans import tr as trbase
from core.data import (format_path, format_timestamp, format_words, format_perc, from core.data import (format_path, format_timestamp, format_words, format_perc,
format_dupe_count, cmp_value) format_dupe_count, cmp_value, Column)
tr = lambda s: trbase(s, 'columns') tr = lambda s: trbase(s, 'columns')
COLUMNS = [ COLUMNS = [
{'attr':'name', 'display': tr("Filename")}, Column('name', tr("Filename")),
{'attr':'path', 'display': tr("Folder")}, Column('path', tr("Folder")),
{'attr':'size', 'display': tr("Size (KB)")}, Column('size', tr("Size (KB)")),
{'attr':'extension', 'display': tr("Kind")}, Column('extension', tr("Kind")),
{'attr':'mtime', 'display': tr("Modification")}, Column('mtime', tr("Modification")),
{'attr':'percentage', 'display': tr("Match %")}, Column('percentage', tr("Match %")),
{'attr':'words', 'display': tr("Words Used")}, Column('words', tr("Words Used")),
{'attr':'dupe_count', 'display': tr("Dupe Count")}, Column('dupe_count', tr("Dupe Count")),
] ]
MATCHPERC_COL = 5 MATCHPERC_COL = 5
DUPECOUNT_COL = 7 DUPECOUNT_COL = 7
DELTA_COLUMNS = {2, 4}
METADATA_TO_READ = ['size', 'mtime'] METADATA_TO_READ = ['size', 'mtime']
@@ -60,9 +61,9 @@ def GetDupeSortKey(dupe, get_group, key, delta):
return m.percentage return m.percentage
if key == DUPECOUNT_COL: if key == DUPECOUNT_COL:
return 0 return 0
r = cmp_value(getattr(dupe, COLUMNS[key]['attr'], '')) r = cmp_value(getattr(dupe, COLUMNS[key].attr, ''))
if delta and (key in {2, 4}): if delta and (key in DELTA_COLUMNS):
r -= cmp_value(getattr(get_group().ref, COLUMNS[key]['attr'], '')) r -= cmp_value(getattr(get_group().ref, COLUMNS[key].attr, ''))
return r return r
def GetGroupSortKey(group, key): def GetGroupSortKey(group, key):
@@ -70,4 +71,4 @@ def GetGroupSortKey(group, key):
return group.percentage return group.percentage
if key == DUPECOUNT_COL: if key == DUPECOUNT_COL:
return len(group) return len(group)
return cmp_value(getattr(group.ref, COLUMNS[key]['attr'], '')) return cmp_value(getattr(group.ref, COLUMNS[key].attr, ''))

View File

@@ -1,3 +1,13 @@
=== 6.0.0 (2011-02-01)
* Re-designed the UI. (#129)
* Internationalized dupeGuru and localized it to french. (#32)
* Changed the format of the help file. (#130)
* Fixed crashes when reading malformed songs. (#127 #131)
* Removed focus from the cancel button in the progress dialog to avoid accidental cancellations. [Mac OS X] (#135)
* Folders added through drag and drop are added to the recent folders list. (#136)
* Added a debugging mode. (#132)
=== 5.10.4 (2010-12-30) === 5.10.4 (2010-12-30)
* Fixed bug causing results to be corrupted after a scan cancellation. (#120) * Fixed bug causing results to be corrupted after a scan cancellation. (#120)

View File

@@ -1,3 +1,13 @@
=== 2.0.0 (2011-01-29)
* Re-designed the UI. (#129)
* Internationalized dupeGuru and localized it to french. (#32)
* Changed the format of the help file. (#130)
* The "Dimensions" column is now affected by the delta mode. (#115)
* Removed focus from the cancel button in the progress dialog to avoid accidental cancellations. [Mac OS X] (#135)
* Folders added through drag and drop are added to the recent folders list. (#136)
* Added a debugging mode. (#132)
=== 1.11.3 (2010-12-31) === 1.11.3 (2010-12-31)
* Fixed bug causing results to be corrupted after a scan cancellation. (#120) * Fixed bug causing results to be corrupted after a scan cancellation. (#120)

View File

@@ -10,6 +10,7 @@ import sys
import logging import logging
import os import os
import os.path as op import os.path as op
import io
from PyQt4.QtCore import QTimer, QObject, QCoreApplication, QUrl, QProcess, SIGNAL, pyqtSignal from PyQt4.QtCore import QTimer, QObject, QCoreApplication, QUrl, QProcess, SIGNAL, pyqtSignal
from PyQt4.QtGui import QDesktopServices, QFileDialog, QDialog, QMessageBox, QApplication from PyQt4.QtGui import QDesktopServices, QFileDialog, QDialog, QMessageBox, QApplication
@@ -38,10 +39,14 @@ JOBID2TITLE = {
JOB_DELETE: tr("Sending files to the recycle bin"), JOB_DELETE: tr("Sending files to the recycle bin"),
} }
class SysWrapper(io.IOBase):
def write(self, s):
if s.strip(): # don't log empty stuff
logging.warning(s)
class DupeGuru(DupeGuruBase, QObject): class DupeGuru(DupeGuruBase, QObject):
LOGO_NAME = '<replace this>' LOGO_NAME = '<replace this>'
NAME = '<replace this>' NAME = '<replace this>'
DELTA_COLUMNS = frozenset()
def __init__(self, data_module): def __init__(self, data_module):
appdata = str(QDesktopServices.storageLocation(QDesktopServices.DataLocation)) appdata = str(QDesktopServices.storageLocation(QDesktopServices.DataLocation))
@@ -50,6 +55,10 @@ class DupeGuru(DupeGuruBase, QObject):
# For basicConfig() to work, we have to be sure that no logging has taken place before this call. # For basicConfig() to work, we have to be sure that no logging has taken place before this call.
logging.basicConfig(filename=op.join(appdata, 'debug.log'), level=logging.WARNING, logging.basicConfig(filename=op.join(appdata, 'debug.log'), level=logging.WARNING,
format='%(asctime)s - %(levelname)s - %(message)s') format='%(asctime)s - %(levelname)s - %(message)s')
if sys.stderr is None: # happens under a cx_freeze environment
sys.stderr = SysWrapper()
if sys.stdout is None:
sys.stdout = SysWrapper()
self.prefs = self._create_preferences() self.prefs = self._create_preferences()
self.prefs.load() self.prefs.load()
DupeGuruBase.__init__(self, data_module, appdata) DupeGuruBase.__init__(self, data_module, appdata)

View File

@@ -13,6 +13,7 @@ from PyQt4.QtGui import (QWidget, QFileDialog, QHeaderView, QVBoxLayout, QHBoxLa
from hscommon.trans import tr, trmsg from hscommon.trans import tr, trmsg
from qtlib.recent import Recent from qtlib.recent import Recent
from qtlib.util import moveToScreenCenter
from core.app import NoScannableFileError from core.app import NoScannableFileError
from . import platform from . import platform
@@ -144,6 +145,8 @@ class DirectoriesDialog(QMainWindow):
if self.app.prefs.directoriesWindowRect is not None: if self.app.prefs.directoriesWindowRect is not None:
self.setGeometry(self.app.prefs.directoriesWindowRect) self.setGeometry(self.app.prefs.directoriesWindowRect)
else:
moveToScreenCenter(self)
def _updateAddButton(self): def _updateAddButton(self):
if self.recentFolders.isEmpty(): if self.recentFolders.isEmpty():

View File

@@ -15,6 +15,7 @@ from PyQt4.QtGui import (QMainWindow, QMenu, QLabel, QHeaderView, QMessageBox, Q
from hscommon.trans import tr, trmsg from hscommon.trans import tr, trmsg
from hscommon.util import nonone from hscommon.util import nonone
from qtlib.util import moveToScreenCenter
from .results_model import ResultsModel, ResultsView from .results_model import ResultsModel, ResultsView
from .stats_label import StatsLabel from .stats_label import StatsLabel
@@ -137,7 +138,7 @@ class ResultWindow(QMainWindow):
menu = self.menuColumns menu = self.menuColumns
self._column_actions = [] self._column_actions = []
for index, column in enumerate(self.app.data.COLUMNS): for index, column in enumerate(self.app.data.COLUMNS):
action = menu.addAction(column['display']) action = menu.addAction(column.display)
action.setCheckable(True) action.setCheckable(True)
action.column_index = index action.column_index = index
self._column_actions.append(action) self._column_actions.append(action)
@@ -192,8 +193,11 @@ class ResultWindow(QMainWindow):
if self.app.prefs.resultWindowIsMaximized: if self.app.prefs.resultWindowIsMaximized:
self.setWindowState(self.windowState() | Qt.WindowMaximized) self.setWindowState(self.windowState() | Qt.WindowMaximized)
if self.app.prefs.resultWindowRect is not None and not self.app.prefs.resultWindowIsMaximized: else:
if self.app.prefs.resultWindowRect is not None:
self.setGeometry(self.app.prefs.resultWindowRect) self.setGeometry(self.app.prefs.resultWindowRect)
else:
moveToScreenCenter(self)
#--- Private #--- Private
def _load_columns(self): def _load_columns(self):

View File

@@ -18,7 +18,7 @@ class ResultsModel(Table):
model = ResultTableModel(self, app) model = ResultTableModel(self, app)
self._app = app self._app = app
self._data = app.data self._data = app.data
self._delta_columns = app.DELTA_COLUMNS self._delta_columns = app.data.DELTA_COLUMNS
Table.__init__(self, model, view) Table.__init__(self, model, view)
self.model.connect() self.model.connect()
@@ -63,7 +63,7 @@ class ResultsModel(Table):
def headerData(self, section, orientation, role): def headerData(self, section, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole and section < len(self._data.COLUMNS): if orientation == Qt.Horizontal and role == Qt.DisplayRole and section < len(self._data.COLUMNS):
return self._data.COLUMNS[section]['display'] return self._data.COLUMNS[section].display
return None return None
def setData(self, index, value, role): def setData(self, index, value, role):

View File

@@ -244,7 +244,7 @@
</message> </message>
<message> <message>
<source>Excluded</source> <source>Excluded</source>
<translation>Exclus</translation> <translation>Exclu</translation>
</message> </message>
<message> <message>
<source>Problems!</source> <source>Problems!</source>

View File

@@ -17,7 +17,6 @@ class DupeGuru(DupeGuruBase):
EDITION = 'me' EDITION = 'me'
LOGO_NAME = 'logo_me' LOGO_NAME = 'logo_me'
NAME = __appname__ NAME = __appname__
DELTA_COLUMNS = frozenset([2, 3, 4, 5, 7])
def __init__(self): def __init__(self):
DupeGuruBase.__init__(self, data) DupeGuruBase.__init__(self, data)

View File

@@ -59,7 +59,6 @@ class DupeGuru(DupeGuruBase):
EDITION = 'pe' EDITION = 'pe'
LOGO_NAME = 'logo_pe' LOGO_NAME = 'logo_pe'
NAME = __appname__ NAME = __appname__
DELTA_COLUMNS = frozenset([2, 5])
def __init__(self): def __init__(self):
DupeGuruBase.__init__(self, data_pe) DupeGuruBase.__init__(self, data_pe)

View File

@@ -27,7 +27,6 @@ class DupeGuru(DupeGuruBase):
EDITION = 'se' EDITION = 'se'
LOGO_NAME = 'logo_se' LOGO_NAME = 'logo_se'
NAME = __appname__ NAME = __appname__
DELTA_COLUMNS = frozenset([2, 4])
def __init__(self): def __init__(self):
DupeGuruBase.__init__(self, data) DupeGuruBase.__init__(self, data)