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

Compare commits

...

8 Commits

14 changed files with 78 additions and 32 deletions

View File

@@ -42,3 +42,5 @@ ca93352ce35184853ad9fcb881935a43a8b1e249 me5.10.3
0056293b0dade8b8230f68c1fe6f0c2d1e0b74d8 se2.12.3 0056293b0dade8b8230f68c1fe6f0c2d1e0b74d8 se2.12.3
8d12cab3b12b723e3a86d02cf8002731a0f73f95 se3.0.0 8d12cab3b12b723e3a86d02cf8002731a0f73f95 se3.0.0
778876a8a9787658aa6adf6944b53aebcb7faeea se3.0.1 778876a8a9787658aa6adf6944b53aebcb7faeea se3.0.1
f1d40b556c01f32c58f9ef9f9acac5b78e01ba7a pe2.0.0
2fd901a516f8cb6b4438491f63f2ebfd52a57c13 me6.0.0

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

@@ -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']:
@@ -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

@@ -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

@@ -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

@@ -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: try:
photo = self.path2iphoto[str(dupe.path)] a = app('iPhoto')
try: [photo] = a.photo_library_album().photos[its.image_path == str(dupe.path)]()
a = app('iPhoto') a.remove(photo, timeout=0)
a.remove(photo, timeout=0) except ValueError:
except (CommandError, RuntimeError) as e: msg = "Could not find photo '{}' in iPhoto Library".format(str(dupe.path))
raise EnvironmentError(str(e))
else:
msg = "Could not find photo %s in iPhoto Library" % str(dupe.path)
raise EnvironmentError(msg) raise EnvironmentError(msg)
except (CommandError, RuntimeError) as e:
raise EnvironmentError(str(e))
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

@@ -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

@@ -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,6 +39,11 @@ 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>'
@@ -49,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
@@ -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:
self.setGeometry(self.app.prefs.resultWindowRect) if self.app.prefs.resultWindowRect is not None:
self.setGeometry(self.app.prefs.resultWindowRect)
else:
moveToScreenCenter(self)
#--- Private #--- Private
def _load_columns(self): def _load_columns(self):

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>