""" PhotosAlbum class to create an album in default Photos library and add photos to it """
from __future__ import annotations
import unicodedata
from typing import List, Optional
from more_itertools import chunked
from .photoinfo import PhotoInfo
from .platform import assert_macos
from .utils import noop, pluralize
assert_macos()
import photoscript
from photoscript import Album, Folder, Photo, PhotosLibrary
__all__ = ["PhotosAlbum", "PhotosAlbumPhotoScript"]
def get_unicode_variants(s: str) -> list[str]:
"""Get all unicode variants of string"""
variants = []
for form in ["NFC", "NFD", "NFKC", "NFKD"]:
normalized = unicodedata.normalize(form, s)
variants.append(normalized)
return variants
def folder_by_path(folders: List[str], verbose: Optional[callable] = None) -> Folder:
"""Get (and create if necessary) a Photos Folder by path (passed as list of folder names)"""
library = PhotosLibrary()
verbose = verbose or noop
top_folder_name = folders.pop(0)
for folder_variant in get_unicode_variants(top_folder_name):
top_folder = library.folder(folder_variant, top_level=True)
if top_folder is not None:
break
else:
verbose(f"Creating folder '{top_folder_name}'")
top_folder = library.create_folder(top_folder_name)
current_folder = top_folder
for folder_name in folders:
for folder_variant in get_unicode_variants(folder_name):
folder = current_folder.folder(folder_variant)
if folder is not None:
break
else:
verbose(f"Creating folder '{folder_name}'")
folder = current_folder.create_folder(folder_name)
current_folder = folder
return current_folder
def album_by_path(
folders_album: List[str], verbose: Optional[callable] = None
) -> Album:
"""Get (and create if necessary) a Photos Album by path (pass as list of folders, album name)"""
library = PhotosLibrary()
verbose = verbose or noop
if len(folders_album) > 1:
# have folders
album_name = folders_album.pop()
folder = folder_by_path(folders_album, verbose)
for album_variant in get_unicode_variants(album_name):
# Get album if it exists
# need to check every unicode variant to avoid creating duplicate albums with same visual representation (#1085)
album = folder.album(album_variant)
if album is not None:
break
else:
verbose(f"Creating album '{album_name}'")
album = folder.create_album(album_name)
else:
# only have album name
album_name = folders_album[0]
for album_variant in get_unicode_variants(album_name):
album = library.album(album_variant, top_level=True)
if album is not None:
break
else:
# album doesn't exist, create it
verbose(f"Creating album '{album_name}'")
album = library.create_album(album_name)
return album
[docs]
class PhotosAlbum:
"""Add osxphotos.photoinfo.PhotoInfo objects to album"""
def __init__(
self,
name: str,
verbose: Optional[callable] = None,
split_folder: Optional[str] = None,
rich: bool = False,
):
"""Return a PhotosAlbum object, creating the album if necessary
Args:
name: Name of album
verbose: optional callable to print verbose output
split_folder: if set, split album name on value of split_folder to create folders if necessary,
e.g. if name = 'folder1/folder2/album' and split_folder='/',
then folders 'folder1' and 'folder2' will be created and album 'album' will be created in 'folder2';
if not set, album 'folder1/folder2/album' will be created
rich: if True, use rich themes for verbose output
"""
self.verbose = verbose or noop
self.library = photoscript.PhotosLibrary()
folders_album = name.split(split_folder) if split_folder else [name]
self.album = album_by_path(folders_album, verbose=verbose)
self.name = name
self.rich = rich
def add(self, photo: PhotoInfo):
photo_ = photoscript.Photo(photo.uuid)
self.album.add([photo_])
self.verbose(
f"Added {self._format_name(photo.original_filename)} ({self._format_uuid(photo.uuid)}) to album {self._format_album(self.name)}"
)
def add_list(self, photo_list: List[PhotoInfo]):
photos = []
for p in photo_list:
try:
photos.append(photoscript.Photo(p.uuid))
except Exception as e:
self.verbose(
f"Error creating Photo object for photo {self._format_uuid(p.uuid)}: {e}"
)
for photolist in chunked(photos, 10):
self.album.add(photolist)
photo_len = len(photo_list)
self.verbose(
f"Added {self._format_num(photo_len)} {pluralize(photo_len, 'photo', 'photos')} to album {self._format_album(self.name)}"
)
def photos(self):
return self.album.photos()
def _format_uuid(self, uuid: str) -> str:
""" "Format uuid for verbose output"""
return f"[uuid]{uuid}[/uuid]" if self.rich else uuid
def _format_album(self, album: str) -> str:
""" "Format album name for verbose output"""
return f"[filepath]{album}[/filepath]" if self.rich else album
def _format_name(self, name: str) -> str:
""" "Format name for verbose output"""
return f"[filename]{name}[/filename]" if self.rich else name
def _format_num(self, num: int) -> str:
""" "Format number for verbose output"""
return f"[num]{num}[/num]" if self.rich else str(num)
[docs]
class PhotosAlbumPhotoScript(PhotosAlbum):
"""Add photoscript.Photo objects to album"""
def add(self, photo: Photo):
self.album.add([photo])
self.verbose(
f"Added {self._format_name(photo.filename)} ({self._format_uuid(photo.uuid)}) to album {self._format_album(self.name)}"
)
def add_list(self, photo_list: List[Photo]):
for photolist in chunked(photo_list, 10):
self.album.add(photolist)
photo_len = len(photo_list)
self.verbose(
f"Added {self._format_num(photo_len)} {pluralize(photo_len, 'photo', 'photos')} to album {self._format_album(self.name)}"
)