Source code for osxphotos.photosalbum

""" PhotosAlbum class to create an album in default Photos library and add photos to it """

from __future__ import annotations

import unicodedata
from collections.abc import Iterable
from typing import Any, Callable, 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[..., Any]] = 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[..., Any]] = 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
    folders_album = folders_album.copy()
    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[..., Any]] = 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 update(self, photos: Iterable[PhotoInfo]): photoscript_photos = [] for p in photos: try: photoscript_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(photoscript_photos, 10): self.album.add(photolist) photo_len = len(photos) self.verbose( f"Added {self._format_num(photo_len)} {pluralize(photo_len, 'photo', 'photos')} to album {self._format_album(self.name)}" )
[docs] def append(self, photo: PhotoInfo): """Add photo to album""" self.add(photo)
[docs] def extend(self, photos: Iterable[PhotoInfo]): """Add list of photos to album""" self.update(photos)
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 update(self, photos: Iterable[Photo]): for photolist in chunked(photos, 10): self.album.add(photolist) photo_len = len(photos) self.verbose( f"Added {self._format_num(photo_len)} {pluralize(photo_len, 'photo', 'photos')} to album {self._format_album(self.name)}" )
[docs] def append(self, photo: Photo): """Add photo to album""" self.add(photo)
[docs] def extend(self, photos: Iterable[Photo]): """Add list of photos to album""" self.update(photos)
class PhotosAlbumPhotoScriptByPath(PhotosAlbumPhotoScript): """Add photoscript.Photo objects to album""" def __init__( self, album_path: list[str], verbose: Optional[callable] = None, rich: bool = False, ): """Return a PhotosAlbum object, creating the album if necessary Args: album_path: Path of album, e.g. ["Folder", "Album"] or ["Album"] verbose: optional callable to print verbose output rich: if True, use rich themes for verbose output """ self.verbose = verbose or noop self.library = photoscript.PhotosLibrary() self.album = album_by_path(album_path, verbose=verbose) self.name = album_path[-1] self.rich = rich