diff --git a/osxphotos/cli.py b/osxphotos/cli.py index b900bede..4139261e 100644 --- a/osxphotos/cli.py +++ b/osxphotos/cli.py @@ -79,7 +79,6 @@ from .sqlgrep import sqlgrep from .uti import get_preferred_uti_extension from .utils import ( expand_and_validate_filepath, - list_directory, load_function, normalize_fs_path, ) @@ -3071,8 +3070,7 @@ def export_photo_to_directory( results.missing.append(str(pathlib.Path(dest_path) / filename)) return results - render_options = RenderOptions( - export_dir=export_dir, dest_path=dest_path) + render_options = RenderOptions(export_dir=export_dir, dest_path=dest_path) tries = 0 while tries <= retry: @@ -3211,7 +3209,7 @@ def get_filenames_from_template( filename=True, edited_version=edited, export_dir=export_dir, - dest_path=dest_path + dest_path=dest_path, ) filenames, unmatched = photo.render_template(filename_template, options) except ValueError as e: @@ -3277,8 +3275,7 @@ def get_dirnames_from_template( elif directory: # got a directory template, render it and check results are valid try: - options = RenderOptions( - dirname=True, edited_version=edited) + options = RenderOptions(dirname=True, edited_version=edited) dirnames, unmatched = photo.render_template(directory, options) except ValueError as e: raise click.BadOptionUsage( @@ -3607,7 +3604,7 @@ def write_finder_tags( options = RenderOptions( none_str=_OSXPHOTOS_NONE_SENTINEL, path_sep="/", - export_dir=export_dir + export_dir=export_dir, ) rendered, unmatched = photo.render_template(template_str, options) except ValueError as e: @@ -3671,9 +3668,7 @@ def write_extended_attributes( for xattr, template_str in xattr_template: try: options = RenderOptions( - none_str=_OSXPHOTOS_NONE_SENTINEL, - path_sep="/", - export_dir=export_dir + none_str=_OSXPHOTOS_NONE_SENTINEL, path_sep="/", export_dir=export_dir ) rendered, unmatched = photo.render_template(template_str, options) except ValueError as e: @@ -3740,8 +3735,7 @@ def run_post_command( # some categories, like error, return a tuple of (file, error str) if isinstance(f, tuple): f = f[0] - render_options = RenderOptions( - export_dir=export_dir, filepath=f) + render_options = RenderOptions(export_dir=export_dir, filepath=f) template = PhotoTemplate(photo, exiftool_path=exiftool_path) command, _ = template.render(command_template, options=render_options) command = command[0] if command else None diff --git a/osxphotos/photoexporter.py b/osxphotos/photoexporter.py index b5bff836..42bdcf06 100644 --- a/osxphotos/photoexporter.py +++ b/osxphotos/photoexporter.py @@ -601,6 +601,7 @@ class PhotoExporter: if dest_uuid != self.photo.uuid: # not the right file, find the right one glob_str = str(dest.parent / f"{dest.stem} (*{dest.suffix}") + # TODO: use the normalized code in utils dest_files = glob.glob(glob_str) for file_ in dest_files: dest_uuid = export_db.get_uuid_for_file(file_) diff --git a/osxphotos/utils.py b/osxphotos/utils.py index 47d259f3..cedf8136 100644 --- a/osxphotos/utils.py +++ b/osxphotos/utils.py @@ -20,7 +20,7 @@ from typing import Callable, List, Union import CoreFoundation import objc -from Foundation import NSFileManager, NSString +from Foundation import NSFileManager, NSPredicate, NSString from ._constants import UNICODE_FORMAT @@ -265,7 +265,7 @@ def list_photo_libraries(): # On older MacOS versions, mdfind appears to ignore some libraries # glob to find libraries in ~/Pictures then mdfind to find all the others # TODO: make this more robust - lib_list = glob.glob(f"{str(pathlib.Path.home())}/Pictures/*.photoslibrary") + lib_list = glob.glob(f"{pathlib.Path.home()}/Pictures/*.photoslibrary") # On older OS, may not get all libraries so make sure we get the last one last_lib = get_last_library_path() @@ -284,35 +284,32 @@ def list_photo_libraries(): def normalize_fs_path(path: str) -> str: """Normalize filesystem paths with unicode in them""" - with objc.autorelease_pool(): - normalized_path = NSString.fileSystemRepresentation(path) - return normalized_path.decode("utf8") + # macOS HFS+ uses NFD, APFS doesn't normalize but stick with NFD + # ref: https://eclecticlight.co/2021/05/08/explainer-unicode-normalization-and-apfs/ + return unicodedata.normalize("NFD", path) -def findfiles(pattern, path_): - """Returns list of filenames from path_ matched by pattern +def findfiles(pattern, path): + """Returns list of filenames from path matched by pattern shell pattern. Matching is case-insensitive. If 'path_' is invalid/doesn't exist, returns [].""" - if not os.path.isdir(path_): + if not os.path.isdir(path): return [] - # See: https://gist.github.com/techtonik/5694830 # paths need to be normalized for unicode as filesystem returns unicode in NFD form pattern = normalize_fs_path(pattern) rule = re.compile(fnmatch.translate(pattern), re.IGNORECASE) - files = list_directory(path_) + files = os.listdir(path) return [name for name in files if rule.match(name)] -def list_directory(directory_path: str) -> List[str]: - """List directory contents using NSFileManager""" - """[[NSFileManager defaultManager] contentsOfDirectoryAtPath:@"directoryName" error:nil]""" - with objc.autorelease_pool(): - manager = NSFileManager.defaultManager() - contents, error = manager.contentsOfDirectoryAtPath_error_(directory_path, None) - if error: - raise OSError(f"Error listing directory {directory_path}: {error}") - return [str(path) for path in contents] +def list_directory_startswith(directory_path: str, startswith: str) -> List[str]: + """List directory contents and return list of files starting with startswith; returns [] if directory doesn't exist""" + if not os.path.isdir(directory_path): + return [] + startswith = normalize_fs_path(startswith) + files = [normalize_fs_path(f) for f in os.listdir(directory_path)] + return [f for f in files if f.startswith(startswith)] def _open_sql_file(dbname): @@ -353,44 +350,16 @@ def _db_is_locked(dbname): return locked -# OSXPHOTOS_XATTR_UUID = "com.osxphotos.uuid" - -# def get_uuid_for_file(filepath): -# """ returns UUID associated with an exported file -# filepath: path to exported photo -# """ -# attr = xattr.xattr(filepath) -# try: -# uuid_bytes = attr[OSXPHOTOS_XATTR_UUID] -# uuid_str = uuid_bytes.decode('utf-8') -# except KeyError: -# uuid_str = None -# return uuid_str - -# def set_uuid_for_file(filepath, uuid): -# """ sets the UUID associated with an exported file -# filepath: path to exported photo -# uuid: uuid string for photo -# """ -# if not os.path.exists(filepath): -# raise FileNotFoundError(f"Missing file: {filepath}") - -# attr = xattr.xattr(filepath) -# uuid_bytes = bytes(uuid, 'utf-8') -# attr.set(OSXPHOTOS_XATTR_UUID, uuid_bytes) - - def normalize_unicode(value): """normalize unicode data""" - if value is not None: - if isinstance(value, (tuple, list)): - return tuple(unicodedata.normalize(UNICODE_FORMAT, v) for v in value) - elif isinstance(value, str): - return unicodedata.normalize(UNICODE_FORMAT, value) - else: - return value - else: + if value is None: return None + if isinstance(value, (tuple, list)): + return tuple(unicodedata.normalize(UNICODE_FORMAT, v) for v in value) + elif isinstance(value, str): + return unicodedata.normalize(UNICODE_FORMAT, value) + else: + return value def increment_filename_with_count( @@ -411,7 +380,7 @@ def increment_filename_with_count( Note: This obviously is subject to race condition so using with caution. """ dest = filepath if isinstance(filepath, pathlib.Path) else pathlib.Path(filepath) - dest_files = findfiles(f"{dest.stem}*", str(dest.parent)) + dest_files = list_directory_startswith(str(dest.parent), dest.stem) dest_files = [pathlib.Path(f).stem.lower() for f in dest_files] dest_new = f"{dest.stem} ({count})" if count else dest.stem dest_new = normalize_fs_path(dest_new)