Skip to content

Commit

Permalink
Got Folder.__init__ working
Browse files Browse the repository at this point in the history
  • Loading branch information
RhetTbull committed Nov 27, 2022
1 parent 711e540 commit 2077c1f
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 52 deletions.
125 changes: 88 additions & 37 deletions photoscript/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
""" Provides PhotosLibrary, Photo, Album classes to interact with Photos App """

from __future__ import annotations

import datetime
import glob
import os
Expand All @@ -14,6 +16,9 @@
from photoscript.utils import ditto, findfiles

from .script_loader import run_script
from .utils import get_os_version

MACOS_VERSION = get_os_version()

""" In Catalina / Photos 5+, UUIDs in AppleScript have suffix that doesn't
appear in actual database value. These need to be dropped to be compatible
Expand All @@ -23,6 +28,29 @@
UUID_SUFFIX_FOLDER = "/L0/020"


def uuid_to_id(uuid: str, suffix: str) -> tuple[str, str]:
"""Converts UUID betweens formats used by osxphotos and Photos app
Args:
uuid: UUID as string
suffix: suffix to add to UUID if needed to form id
Returns:
tuple of (uuid, id)
"""
id_ = uuid
if MACOS_VERSION >= (10, 15, 0):
# In Photos 5+ (Catalina/10.15), UUIDs in AppleScript have suffix that doesn't
# appear in actual database value. Suffix needs to be added to be compatible
# with AppleScript (id_) and dropped for osxphotos (uuid)
if len(uuid.split("/")) == 1:
# osxphotos style UUID without the suffix
id_ = f"{uuid}{suffix}"
else:
uuid = uuid.split("/")[0]
return uuid, id_


class AppleScriptError(Exception):
def __init__(self, *message):
super().__init__(*message)
Expand Down Expand Up @@ -553,17 +581,9 @@ def _export_photo(

class Album:
def __init__(self, uuid):
id_ = uuid
# check to see if we need to add UUID suffix
if float(PhotosLibrary().version) >= 5.0:
if len(uuid.split("/")) == 1:
# osxphotos style UUID without the suffix
id_ = f"{uuid}{UUID_SUFFIX_ALBUM}"
else:
uuid = uuid.split("/")[0]

valuuidalbum = run_script("albumExists", id_)
if valuuidalbum:
uuid, id_ = uuid_to_id(uuid, UUID_SUFFIX_ALBUM)
if valuuidalbum := run_script("albumExists", id_):
self.id = id_
self._uuid = uuid
else:
Expand Down Expand Up @@ -761,39 +781,76 @@ def __len__(self):


class Folder:
def __init__(self, uuid):
id_ = uuid
# check to see if we need to add UUID suffix
if float(PhotosLibrary().version) >= 5.0:
if len(uuid.split("/")) == 1:
# osxphotos style UUID without the suffix
id_ = f"{uuid}{UUID_SUFFIX_FOLDER}"
else:
uuid = uuid.split("/")[0]
def __init__(
self,
path: list[str] | None = None,
uuid: str | None = None,
idstring: str | None = None,
):
"""Create a Folder object; only one of path, uuid, or idstring should be specified
valid_folder = run_script("folderExists", id_)
if valid_folder:
self.id = id_
self._uuid = uuid
Args:
path: list of folder names in descending order from parent to child: ["Folder", "SubFolder"]
uuid: uuid of folder: "E0CD4B6C-CB43-46A6-B8A3-67D1FB4D0F3D/L0/020" or "E0CD4B6C-CB43-46A6-B8A3-67D1FB4D0F3D"
idstring: idstring of folder:
"folder id(\"E0CD4B6C-CB43-46A6-B8A3-67D1FB4D0F3D/L0/020\") of folder id(\"CB051A4C-2CB7-4B90-B59B-08CC4D0C2823/L0/020\")"
"""

if sum(bool(x) for x in (path, uuid, idstring)) != 1:
raise ValueError(
"One (and only one) of path, uuid, or idstring must be specified"
)

if uuid is not None:
uuid, _id = uuid_to_id(uuid, UUID_SUFFIX_FOLDER)
else:
raise ValueError(f"Invalid folder id: {uuid}")
_id = None

self._path, self._uuid, self._id, self._idstring = path, uuid, _id, idstring

# if initialized with path or uuid, need to initialize idstring
if self._path is not None:
self._idstring = run_script("folderGetIDStringFromPath", self._path)
elif self._id is not None:
# if uuid was passed, _id will have been initialized above
self._idstring = run_script("photosLibraryGetFolderIDStringForID", self._id)

@property
def idstring(self) -> str:
"""idstring of folder"""
return self._idstring

@property
def uuid(self):
"""UUID of folder"""
if self._uuid is not None:
return self._uuid
self._uuid, self._id = uuid_to_id(
run_script("folderUUID", self._idstring), UUID_SUFFIX_FOLDER
)
return self._uuid

@property
def id(self):
"""ID of folder"""
if self._id is not None:
return self._id
self._uuid, self._id = uuid_to_id(
run_script("folderUUID", self._idstring), UUID_SUFFIX_FOLDER
)
return self._id

@property
def name(self):
"""name of folder (read/write)"""
name = run_script("folderName", self.id)
name = run_script("folderName", self._idstring)
return name if name != kMissingValue else ""

@name.setter
def name(self, name):
"""set name of photo"""
name = "" if name is None else name
return run_script("folderSetName", self.id, name)
return run_script("folderSetName", self._idstring, name)

@property
def title(self):
Expand All @@ -804,17 +861,18 @@ def title(self):
def title(self, title):
"""set title of folder (alias for name)"""
name = "" if title is None else title
return run_script("folderSetName", self.id, name)
return run_script("folderSetName", self._idstring, name)

@property
def parent_id(self):
"""parent container id"""
return run_script("folderParent", self.id)
return run_script("folderParent", self._idstring)

# TODO: if no parent should return a "My Albums" object that contains all top-level folders/albums?
@property
def parent(self):
"""Return parent Folder object"""
# ZZZ
parent_id = self.parent_id
if parent_id != 0:
return Folder(parent_id)
Expand Down Expand Up @@ -909,16 +967,9 @@ def __len__(self):

class Photo:
def __init__(self, uuid):
id_ = uuid
# check to see if we need to add UUID suffix
if float(PhotosLibrary().version) >= 5.0:
if len(uuid.split("/")) == 1:
# osxphotos style UUID without the suffix
id_ = f"{uuid}{UUID_SUFFIX_PHOTO}"
else:
uuid = uuid.split("/")[0]
valid = run_script("photoExists", uuid)
if valid:
uuid, id_ = uuid_to_id(uuid, UUID_SUFFIX_PHOTO)
if valid := run_script("photoExists", uuid):
self.id = id_
self._uuid = uuid
else:
Expand Down
8 changes: 4 additions & 4 deletions photoscript/photoscript.applescript
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ on _walkFoldersLookingForID(theFolderID, theFolder, folderString)
tell application "Photos"
set subFolders to theFolder's folders
repeat with aFolder in subFolders
set folderString to "folder (\"" & (id of aFolder as text) & "\") of " & folderString
set folderString to "folder id(\"" & (id of aFolder as text) & "\") of " & folderString
if id of aFolder is equal to theFolderID then
return folderString
end if
Expand All @@ -291,7 +291,7 @@ on photosLibraryGetFolderIDStringForID(theFolderID)
tell application "Photos"
set theFolders to folders
repeat with aFolder in theFolders
set folderString to "folder (\"" & (id of aFolder as text) & "\")"
set folderString to "folder id(\"" & (id of aFolder as text) & "\")"
if id of aFolder is equal to theFolderID then
return folderString
end if
Expand Down Expand Up @@ -326,7 +326,7 @@ on _walkFoldersLookingForName(theFolderName, theFolder, folderString)
tell application "Photos"
set subFolders to theFolder's folders
repeat with aFolder in subFolders
set folderString to "folder (\"" & (id of aFolder as text) & "\") of " & folderString
set folderString to "folder id(\"" & (id of aFolder as text) & "\") of " & folderString
if aFolder's name is equal to theFolderName then
return folderString
end if
Expand All @@ -342,7 +342,7 @@ on photosLibraryGetFolderIDStringForName(theFolderName)
tell application "Photos"
set theFolders to folders
repeat with aFolder in theFolders
set folderString to "folder (\"" & (id of aFolder as text) & "\")"
set folderString to "folder id(\"" & (id of aFolder as text) & "\")"
if aFolder's name is equal to theFolderName then
return folderString
end if
Expand Down
43 changes: 32 additions & 11 deletions photoscript/utils.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
""" Utility functions for photoscript """

from __future__ import annotations

import fnmatch
import os
import platform
import re
import subprocess


def ditto(src, dest, norsrc=False):
""" Copies a file or directory tree from src path to dest path
src: source path as string
dest: destination path as string
norsrc: (bool) if True, uses --norsrc flag with ditto so it will not copy
resource fork or extended attributes. May be useful on volumes that
don't work with extended attributes (likely only certain SMB mounts)
default is False
Uses ditto to perform copy; will silently overwrite dest if it exists
Raises exception if copy fails or either path is None """
"""Copies a file or directory tree from src path to dest path
src: source path as string
dest: destination path as string
norsrc: (bool) if True, uses --norsrc flag with ditto so it will not copy
resource fork or extended attributes. May be useful on volumes that
don't work with extended attributes (likely only certain SMB mounts)
default is False
Uses ditto to perform copy; will silently overwrite dest if it exists
Raises exception if copy fails or either path is None"""

if src is None or dest is None:
raise ValueError("src and dest must not be None", src, dest)
Expand All @@ -33,11 +36,29 @@ def ditto(src, dest, norsrc=False):

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 []."""
shell pattern. Matching is case-insensitive.
If 'path_' is invalid/doesn't exist, returns []."""
if not os.path.isdir(path_):
return []
# See: https://gist.github.com/techtonik/5694830

rule = re.compile(fnmatch.translate(pattern), re.IGNORECASE)
return [name for name in os.listdir(path_) if rule.match(name)]


def get_os_version() -> tuple[int, int, int]:
# returns tuple of str containing OS version
# e.g. 10.13.6 = ("10", "13", "6")
version = platform.mac_ver()[0].split(".")
if len(version) == 2:
(ver, major) = version
minor = 0
elif len(version) == 3:
(ver, major, minor) = version
else:
raise (
ValueError(
f"Could not parse version string: {platform.mac_ver()} {version}"
)
)
return tuple(map(int, (ver, major, minor)))

0 comments on commit 2077c1f

Please sign in to comment.