mirror of
https://github.com/arsenetar/dupeguru.git
synced 2026-01-25 16:11:39 +00:00
Compare commits
8 Commits
pe2.0.0
...
before-plu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4249c528e9 | ||
|
|
084068852e | ||
|
|
c524a85897 | ||
|
|
d39d46be5a | ||
|
|
b8980b4667 | ||
|
|
e0adec7b2b | ||
|
|
eb8b9d663f | ||
|
|
fa4b0cf9ec |
2
.hgtags
2
.hgtags
@@ -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
|
||||||
|
|||||||
@@ -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.";
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
15
core/app.py
15
core/app.py
@@ -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):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
__version__ = '5.10.4'
|
__version__ = '6.0.0'
|
||||||
__appname__ = 'dupeGuru Music Edition'
|
__appname__ = 'dupeGuru Music Edition'
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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:
|
||||||
|
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):
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user