diff --git a/pe/cocoa/py/dg_cocoa.py b/pe/cocoa/py/dg_cocoa.py index 296178ce..32c5575f 100644 --- a/pe/cocoa/py/dg_cocoa.py +++ b/pe/cocoa/py/dg_cocoa.py @@ -12,7 +12,6 @@ from dupeguru_pe import app_cocoa as app_pe_cocoa # Fix py2app imports which chokes on relative imports from dupeguru import app, app_cocoa, data, directories, engine, export, ignore, results, scanner from dupeguru_pe import block, cache, matchbase, data -from hsfs import auto, stats, tree from hsutil import conflict class PyApp(NSObject): diff --git a/pe/py/app_cocoa.py b/pe/py/app_cocoa.py index fa619838..74e89701 100644 --- a/pe/py/app_cocoa.py +++ b/pe/py/app_cocoa.py @@ -7,25 +7,21 @@ # 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 import logging import plistlib import re -import objc from Foundation import * from AppKit import * from appscript import app, k -from hsutil import job, io -import hsfs as fs -from hsfs import phys, InvalidPath -from hsutil import files +from hsutil import io from hsutil.str import get_file_ext from hsutil.path import Path from hsutil.cocoa import as_fetch +from dupeguru import fs from dupeguru import app_cocoa, directories from . import data from .cache import string_to_colors, Cache @@ -35,14 +31,19 @@ mainBundle = NSBundle.mainBundle() PictureBlocks = mainBundle.classNamed_('PictureBlocks') assert PictureBlocks is not None -class Photo(phys.File): - INITIAL_INFO = phys.File.INITIAL_INFO.copy() +class Photo(fs.File): + INITIAL_INFO = fs.File.INITIAL_INFO.copy() INITIAL_INFO.update({ 'dimensions': (0,0), }) + HANDLED_EXTS = set(['png', 'jpg', 'jpeg', 'gif', 'psd', 'bmp', 'tiff', 'nef', 'cr2']) + + @classmethod + def can_handle(cls, path): + return fs.File.can_handle(path) and get_file_ext(path[-1]) in cls.HANDLED_EXTS def _read_info(self, field): - super(Photo, self)._read_info(field) + fs.File._read_info(self, field) if field == 'dimensions': size = PictureBlocks.getImageSize_(unicode(self.path)) self.dimensions = (size.width, size.height) @@ -50,7 +51,7 @@ class Photo(phys.File): def get_blocks(self, block_count_per_side): try: blocks = PictureBlocks.getBlocksFromImagePath_blockCount_(unicode(self.path), block_count_per_side) - except Exception, e: + except Exception as e: raise IOError('The reading of "%s" failed with "%s"' % (unicode(self.path), unicode(e))) if not blocks: raise IOError('The picture %s could not be read' % unicode(self.path)) @@ -58,90 +59,80 @@ class Photo(phys.File): class IPhoto(Photo): - def __init__(self, parent, whole_path): - super(IPhoto, self).__init__(parent, whole_path[-1]) - self.whole_path = whole_path - - def _build_path(self): - return self.whole_path - @property def display_path(self): - return super(IPhoto, self)._build_path() + return Path(('iPhoto Library', self.name)) +def get_iphoto_database_path(): + ud = NSUserDefaults.standardUserDefaults() + prefs = ud.persistentDomainForName_('com.apple.iApps') + if 'iPhotoRecentDatabases' not in prefs: + raise directories.InvalidPathError() + plisturl = NSURL.URLWithString_(prefs['iPhotoRecentDatabases'][0]) + return Path(plisturl.path()) -class Directory(phys.Directory): - cls_file_class = Photo - cls_supported_exts = ('png', 'jpg', 'jpeg', 'gif', 'psd', 'bmp', 'tiff', 'nef', 'cr2') - - def _fetch_subitems(self): - subdirs, subfiles = super(Directory,self)._fetch_subitems() - return subdirs, [name for name in subfiles if get_file_ext(name) in self.cls_supported_exts] - - -class IPhotoLibrary(fs.Directory): - def __init__(self, plistpath): - self.plistpath = plistpath - self.refpath = plistpath[:-1] - # the AlbumData.xml file lives right in the library path - super(IPhotoLibrary, self).__init__(None, 'iPhoto Library') - if not io.exists(plistpath): - raise InvalidPath(self) - - def _update_photo(self, photo_data): +def get_iphoto_pictures(plistpath): + if not io.exists(plistpath): + raise InvalidPath(self) + s = io.open(plistpath).read() + # There was a case where a guy had 0x10 chars in his plist, causing expat errors on loading + s = s.replace('\x10', '') + # It seems that iPhoto sometimes doesn't properly escape & chars. The regexp below is to find + # any & char that is not a &-based entity (&, ", 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.readPlistFromString(s) + result = [] + for photo_data in plist['Master Image List'].values(): if photo_data['MediaType'] != 'Image': - return + continue photo_path = Path(photo_data['ImagePath']) - subpath = photo_path[len(self.refpath):-1] - subdir = self - for element in subpath: - try: - subdir = subdir[element] - except KeyError: - subdir = fs.Directory(subdir, element) - try: - IPhoto(subdir, photo_path) - except fs.AlreadyExistsError: - # it's possible for 2 entries in the plist to point to the same path. Ignore one of them. - pass + photo = IPhoto(photo_path) + result.append(photo) + return result + +class Directories(directories.Directories): + def __init__(self): + directories.Directories.__init__(self, fileclasses=[Photo]) + self.iphoto_libpath = get_iphoto_database_path() + self.set_state(self.iphoto_libpath[:-1], directories.STATE_EXCLUDED) - def update(self): - self.clear() - s = open(unicode(self.plistpath)).read() - # There was a case where a guy had 0x10 chars in his plist, causing expat errors on loading - s = s.replace('\x10', '') - # It seems that iPhoto sometimes doesn't properly escape & chars. The regexp below is to find - # any & char that is not a &-based entity (&, ", 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.readPlistFromString(s) - for photo_data in plist['Master Image List'].values(): - self._update_photo(photo_data) + def _get_files(self, from_path): + if from_path == Path('iPhoto Library'): + is_ref = self.get_state(from_path) == directories.STATE_REFERENCE + photos = get_iphoto_pictures(self.iphoto_libpath) + for photo in photos: + photo.is_ref = is_ref + return photos + else: + return directories.Directories._get_files(self, from_path) - def force_update(self): # Don't update - pass + @staticmethod + def get_subfolders(path): + if path == Path('iPhoto Library'): + return [] + else: + return directories.Directories.get_subfolders(path) + + def add_path(self, path): + if path == Path('iPhoto Library'): + if path in self: + raise AlreadyThereError() + self._dirs.append(path) + else: + directories.Directories.add_path(self, path) class DupeGuruPE(app_cocoa.DupeGuru): def __init__(self): app_cocoa.DupeGuru.__init__(self, data, 'dupeGuru Picture Edition', appid=5) self.scanner = ScannerPE() - self.directories.dirclass = Directory - self.directories.special_dirclasses[Path('iPhoto Library')] = lambda _, __: self._create_iphoto_library() + self.directories = Directories() p = op.join(self.appdata, 'cached_pictures.db') self.scanner.cached_blocks = Cache(p) - def _create_iphoto_library(self): - ud = NSUserDefaults.standardUserDefaults() - prefs = ud.persistentDomainForName_('com.apple.iApps') - if 'iPhotoRecentDatabases' not in prefs: - raise directories.InvalidPathError - plisturl = NSURL.URLWithString_(prefs['iPhotoRecentDatabases'][0]) - plistpath = Path(plisturl.path()) - return IPhotoLibrary(plistpath) - def _do_delete(self, j): def op(dupe): j.add_progress() @@ -175,40 +166,19 @@ class DupeGuruPE(app_cocoa.DupeGuru): def _do_load(self, j): self.directories.load_from_file(op.join(self.appdata, 'last_directories.xml')) - for d in self.directories: - if isinstance(d, IPhotoLibrary): - d.update() 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) - for d in self.directories: - result = None - if p in d.path: - result = d.find_path(p[d.path:]) - if isinstance(d, IPhotoLibrary) and p in d.refpath: - result = d.find_path(p[d.refpath:]) - if result is not None: - return result - - def add_directory(self, d): - result = app_cocoa.DupeGuru.add_directory(self, d) - if (result == 0) and (d == 'iPhoto Library'): - [iphotolib] = [dir for dir in self.directories if dir.path == d] - iphotolib.update() - return result + if p in self.directories.iphoto_libpath[:-1]: + return IPhoto(p) + return app_cocoa.DupeGuru._get_file(self, str_path) def copy_or_move(self, dupe, copy, destination, dest_type): if isinstance(dupe, IPhoto): copy = True return app_cocoa.DupeGuru.copy_or_move(self, dupe, copy, destination, dest_type) - def start_scanning(self): - for directory in self.directories: - if isinstance(directory, IPhotoLibrary): - self.directories.set_state(directory.refpath, directories.STATE_EXCLUDED) - return app_cocoa.DupeGuru.start_scanning(self) - def selected_dupe_path(self): if not self.selected_dupes: return None