1
0
mirror of https://github.com/arsenetar/dupeguru.git synced 2025-03-09 21:24:36 +00:00

[#42 state:fixed] Added Aperture support in dupeGuru PE.

This commit is contained in:
Virgil Dupras 2012-06-05 13:56:28 -04:00
parent 04056c1597
commit dccffd9516
3 changed files with 121 additions and 17 deletions

View File

@ -11,16 +11,17 @@ import plistlib
import logging import logging
import re import re
from appscript import app, its, CommandError, ApplicationNotFoundError from appscript import app, its, k, CommandError, ApplicationNotFoundError
from hscommon import io from hscommon import io
from hscommon.util import remove_invalid_xml from hscommon.util import remove_invalid_xml, first
from hscommon.path import Path from hscommon.path import Path
from hscommon.trans import trget from hscommon.trans import trget
from cocoa import proxy from cocoa import proxy
from core.scanner import ScanType from core.scanner import ScanType
from core import directories from core import directories
from core.app import JobType
from core_pe import _block_osx from core_pe import _block_osx
from core_pe.photo import Photo as PhotoBase from core_pe.photo import Photo as PhotoBase
from core_pe.app import DupeGuru as DupeGuruBase from core_pe.app import DupeGuru as DupeGuruBase
@ -29,6 +30,7 @@ from .app import PyDupeGuruBase
tr = trget('ui') tr = trget('ui')
IPHOTO_PATH = Path('iPhoto Library') IPHOTO_PATH = Path('iPhoto Library')
APERTURE_PATH = Path('Aperture Library')
class Photo(PhotoBase): class Photo(PhotoBase):
HANDLED_EXTS = PhotoBase.HANDLED_EXTS.copy() HANDLED_EXTS = PhotoBase.HANDLED_EXTS.copy()
@ -48,18 +50,25 @@ class Photo(PhotoBase):
class IPhoto(Photo): class IPhoto(Photo):
def __init__(self, path, db_id):
# In IPhoto, we don't care about the db_id, we find photos by path.
Photo.__init__(self, path)
@property @property
def display_folder_path(self): def display_folder_path(self):
return IPHOTO_PATH return IPHOTO_PATH
def get_iphoto_database_path():
plisturls = proxy.prefValue_inDomain_('iPhotoRecentDatabases', 'com.apple.iApps')
if not plisturls:
raise directories.InvalidPathError()
plistpath = proxy.url2path_(plisturls[0])
return Path(plistpath)
def get_iphoto_pictures(plistpath): class AperturePhoto(Photo):
def __init__(self, path, db_id):
Photo.__init__(self, path)
self.db_id = db_id
@property
def display_folder_path(self):
return APERTURE_PATH
def get_iphoto_or_aperture_pictures(plistpath, photo_class):
# The structure of iPhoto and Aperture libraries for the base photo list are excactly the same.
if not io.exists(plistpath): if not io.exists(plistpath):
return [] return []
s = io.open(plistpath, 'rt', encoding='utf-8').read() s = io.open(plistpath, 'rt', encoding='utf-8').read()
@ -73,14 +82,33 @@ def get_iphoto_pictures(plistpath):
logging.warning("%d invalid XML entities replacement made", count) logging.warning("%d invalid XML entities replacement made", count)
plist = plistlib.readPlistFromBytes(s.encode('utf-8')) plist = plistlib.readPlistFromBytes(s.encode('utf-8'))
result = [] result = []
for photo_data in plist['Master Image List'].values(): for key, photo_data in plist['Master Image List'].items():
if photo_data['MediaType'] != 'Image': if photo_data['MediaType'] != 'Image':
continue continue
photo_path = Path(photo_data['ImagePath']) photo_path = Path(photo_data['ImagePath'])
photo = IPhoto(photo_path) photo = photo_class(photo_path, key)
result.append(photo) result.append(photo)
return result return result
def get_iphoto_pictures(plistpath):
return get_iphoto_or_aperture_pictures(plistpath, IPhoto)
def get_aperture_pictures(plistpath):
return get_iphoto_or_aperture_pictures(plistpath, AperturePhoto)
def get_iapps_database_path(prefname):
plisturls = proxy.prefValue_inDomain_(prefname, 'com.apple.iApps')
if not plisturls:
raise directories.InvalidPathError()
plistpath = proxy.url2path_(plisturls[0])
return Path(plistpath)
def get_iphoto_database_path():
return get_iapps_database_path('iPhotoRecentDatabases')
def get_aperture_database_path():
return get_iapps_database_path('ApertureLibraries')
class Directories(directories.Directories): class Directories(directories.Directories):
def __init__(self): def __init__(self):
directories.Directories.__init__(self, fileclasses=[Photo]) directories.Directories.__init__(self, fileclasses=[Photo])
@ -89,6 +117,11 @@ class Directories(directories.Directories):
self.set_state(self.iphoto_libpath[:-1], directories.DirectoryState.Excluded) self.set_state(self.iphoto_libpath[:-1], directories.DirectoryState.Excluded)
except directories.InvalidPathError: except directories.InvalidPathError:
self.iphoto_libpath = None self.iphoto_libpath = None
try:
self.aperture_libpath = get_aperture_database_path()
self.set_state(self.aperture_libpath[:-1], directories.DirectoryState.Excluded)
except directories.InvalidPathError:
self.aperture_libpath = None
def _get_files(self, from_path, j): def _get_files(self, from_path, j):
if from_path == IPHOTO_PATH: if from_path == IPHOTO_PATH:
@ -99,25 +132,33 @@ class Directories(directories.Directories):
for photo in photos: for photo in photos:
photo.is_ref = is_ref photo.is_ref = is_ref
return photos return photos
elif from_path == APERTURE_PATH:
if self.aperture_libpath is None:
return []
is_ref = self.get_state(from_path) == directories.DirectoryState.Reference
photos = get_aperture_pictures(self.aperture_libpath)
for photo in photos:
photo.is_ref = is_ref
return photos
else: else:
return directories.Directories._get_files(self, from_path, j) return directories.Directories._get_files(self, from_path, j)
@staticmethod @staticmethod
def get_subfolders(path): def get_subfolders(path):
if path == IPHOTO_PATH: if path in {IPHOTO_PATH, APERTURE_PATH}:
return [] return []
else: else:
return directories.Directories.get_subfolders(path) return directories.Directories.get_subfolders(path)
def add_path(self, path): def add_path(self, path):
if path == IPHOTO_PATH: if path in {IPHOTO_PATH, APERTURE_PATH}:
if path not in self: if path not in self:
self._dirs.append(path) self._dirs.append(path)
else: else:
directories.Directories.add_path(self, path) directories.Directories.add_path(self, path)
def has_iphoto_path(self): def has_iphoto_path(self):
return any(path == IPHOTO_PATH for path in self._dirs) return any(path in {IPHOTO_PATH, APERTURE_PATH} for path in self._dirs)
def has_any_file(self): def has_any_file(self):
# If we don't do that, it causes a hangup in the GUI when we click Start Scanning because # If we don't do that, it causes a hangup in the GUI when we click Start Scanning because
@ -140,6 +181,7 @@ class DupeGuruPE(DupeGuruBase):
j.add_progress() j.add_progress()
return self._do_delete_dupe(dupe, replace_with_hardlinks, direct_deletion) return self._do_delete_dupe(dupe, replace_with_hardlinks, direct_deletion)
self.deleted_aperture_photos = False
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)]
j.start_job(self.results.mark_count, tr("Sending dupes to the Trash")) 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):
@ -150,6 +192,14 @@ class DupeGuruPE(DupeGuruBase):
a.select(a.photo_library_album(timeout=0), timeout=0) a.select(a.photo_library_album(timeout=0), timeout=0)
except (CommandError, RuntimeError, ApplicationNotFoundError): except (CommandError, RuntimeError, ApplicationNotFoundError):
pass pass
if any(isinstance(dupe, AperturePhoto) for dupe in marked):
self.deleted_aperture_photos = True
j.add_progress(0, desc=tr("Talking to Aperture. Don't touch it!"))
try:
a = app('Aperture')
a.activate(timeout=0)
except (CommandError, RuntimeError, ApplicationNotFoundError):
pass
self.results.perform_on_marked(op, True) self.results.perform_on_marked(op, True)
def _do_delete_dupe(self, dupe, replace_with_hardlinks, direct_deletion): def _do_delete_dupe(self, dupe, replace_with_hardlinks, direct_deletion):
@ -167,16 +217,61 @@ class DupeGuruPE(DupeGuruBase):
raise EnvironmentError(msg) raise EnvironmentError(msg)
except (CommandError, RuntimeError) as e: except (CommandError, RuntimeError) as e:
raise EnvironmentError(str(e)) raise EnvironmentError(str(e))
if isinstance(dupe, AperturePhoto):
try:
a = app('Aperture')
# I'm flying blind here. In my own test library, all photos are in an album with the
# id "LibraryFolder", so I'm going to guess that it's the case at least most of the
# time. As a safeguard, if we don't find any library with that id, we'll use the
# first album.
# Now, about deleting: All attempts I've made at sending photos to trash failed,
# even with normal applescript. So, what we're going to do here is to create a
# "dupeGuru Trash" project and tell the user to manually send those photos to trash.
libraries = a.libraries()
library = first(l for l in libraries if l.id == 'LibraryFolder')
if library is None:
library = libraries[0]
trash_project = a.projects["dupeGuru Trash"]
if trash_project.exists():
trash_project = trash_project()
else:
trash_project = library.make(new=k.project, with_properties={k.name: "dupeGuru Trash"})
[photo] = library.image_versions[its.id == dupe.db_id]()
photo.move(to=trash_project)
except (IndexError, ValueError):
msg = "Could not find photo '{}' in Aperture Library".format(str(dupe.path))
raise EnvironmentError(msg)
except (CommandError, RuntimeError) as e:
raise EnvironmentError(str(e))
else: else:
DupeGuruBase._do_delete_dupe(self, dupe, replace_with_hardlinks, direct_deletion) DupeGuruBase._do_delete_dupe(self, dupe, replace_with_hardlinks, direct_deletion)
def _create_file(self, path): def _create_file(self, path):
if (self.directories.iphoto_libpath is not None) and (path in self.directories.iphoto_libpath[:-1]): if (self.directories.iphoto_libpath is not None) and (path in self.directories.iphoto_libpath[:-1]):
return IPhoto(path) if not hasattr(self, 'path2iphoto'):
photos = get_iphoto_pictures(self.directories.iphoto_libpath)
self.path2iphoto = {p.path: p for p in photos}
return self.path2iphoto.get(path)
if (self.directories.aperture_libpath is not None) and (path in self.directories.aperture_libpath[:-1]):
if not hasattr(self, 'path2aperture'):
photos = get_aperture_pictures(self.directories.aperture_libpath)
self.path2aperture = {p.path: p for p in photos}
return self.path2aperture.get(path)
return DupeGuruBase._create_file(self, path) return DupeGuruBase._create_file(self, path)
def _job_completed(self, jobid, exc):
DupeGuruBase._job_completed(self, jobid, exc)
if jobid == JobType.Load:
if hasattr(self, 'path2iphoto'):
del self.path2iphoto
if hasattr(self, 'path2aperture'):
del self.path2aperture
if jobid == JobType.Delete and self.deleted_aperture_photos:
msg = tr("Deleted Aperture photos were sent to a project called \"dupeGuru Trash\".")
self.view.show_message(msg)
def copy_or_move(self, dupe, copy, destination, dest_type): def copy_or_move(self, dupe, copy, destination, dest_type):
if isinstance(dupe, IPhoto): if isinstance(dupe, (IPhoto, AperturePhoto)):
copy = True copy = True
return DupeGuruBase.copy_or_move(self, dupe, copy, destination, dest_type) return DupeGuruBase.copy_or_move(self, dupe, copy, destination, dest_type)

View File

@ -13,4 +13,5 @@ http://www.hardcoded.net/licenses/bsd_license
{ {
} }
- (IBAction)addiPhoto:(id)sender; - (IBAction)addiPhoto:(id)sender;
- (IBAction)addAperture:(id)sender;
@end @end

View File

@ -24,10 +24,18 @@ http://www.hardcoded.net/licenses/bsd_license
NSMenuItem *mi = [m insertItemWithTitle:TR(@"Add iPhoto Library") action:@selector(addiPhoto:) NSMenuItem *mi = [m insertItemWithTitle:TR(@"Add iPhoto Library") action:@selector(addiPhoto:)
keyEquivalent:@"" atIndex:1]; keyEquivalent:@"" atIndex:1];
[mi setTarget:self]; [mi setTarget:self];
mi = [m insertItemWithTitle:TR(@"Add Aperture Library") action:@selector(addAperture:)
keyEquivalent:@"" atIndex:2];
[mi setTarget:self];
} }
- (IBAction)addiPhoto:(id)sender - (IBAction)addiPhoto:(id)sender
{ {
[self addDirectory:@"iPhoto Library"]; [self addDirectory:@"iPhoto Library"];
} }
- (IBAction)addAperture:(id)sender
{
[self addDirectory:@"Aperture Library"];
}
@end @end