""" AlbumInfo and FolderInfo classes for dealing with albums and folders AlbumInfo class Represents a single Album in the Photos library and provides access to the album's attributes PhotosDB.albums() returns a list of AlbumInfo objects FolderInfo class Represents a single Folder in the Photos library and provides access to the folders attributes PhotosDB.folders() returns a list of FolderInfo objects """ from datetime import datetime, timedelta, timezone from ._constants import ( _PHOTOS_4_ALBUM_KIND, _PHOTOS_4_TOP_LEVEL_ALBUM, _PHOTOS_4_VERSION, _PHOTOS_5_ALBUM_KIND, _PHOTOS_5_FOLDER_KIND, TIME_DELTA, ) from .datetime_utils import get_local_tz def sort_list_by_keys(values, sort_keys): """ Sorts list values by a second list sort_keys e.g. given ["a","c","b"], [1, 3, 2], returns ["a", "b", "c"] Args: values: a list of values to be sorted sort_keys: a list of keys to sort values by Returns: list of values, sorted by sort_keys Raises: ValueError: raised if len(values) != len(sort_keys) """ if len(values) != len(sort_keys): return ValueError("values and sort_keys must have same length") return list(zip(*sorted(zip(sort_keys, values))))[1] class AlbumInfoBaseClass: """ Base class for AlbumInfo, ImportInfo Info about a specific Album, contains all the details about the album including folders, photos, etc. """ def __init__(self, db=None, uuid=None): self._uuid = uuid self._db = db self._title = self._db._dbalbum_details[uuid]["title"] self._creation_date_timestamp = self._db._dbalbum_details[uuid]["creation_date"] self._start_date_timestamp = self._db._dbalbum_details[uuid]["start_date"] self._end_date_timestamp = self._db._dbalbum_details[uuid]["end_date"] self._local_tz = get_local_tz( datetime.fromtimestamp(self._creation_date_timestamp + TIME_DELTA) ) @property def uuid(self): """ return uuid of album """ return self._uuid @property def creation_date(self): """ return creation date of album """ try: return self._creation_date except AttributeError: try: self._creation_date = ( datetime.fromtimestamp( self._creation_date_timestamp + TIME_DELTA ).astimezone(tz=self._local_tz) if self._creation_date_timestamp else datetime(1970, 1, 1, 0, 0, 0).astimezone( tz=timezone(timedelta(0)) ) ) except ValueError: self._creation_date = datetime(1970, 1, 1, 0, 0, 0).astimezone( tz=timezone(timedelta(0)) ) return self._creation_date @property def start_date(self): """ For Albums, return start date (earliest image) of album or None for albums with no images For Import Sessions, return start date of import session (when import began) """ try: return self._start_date except AttributeError: try: self._start_date = ( datetime.fromtimestamp( self._start_date_timestamp + TIME_DELTA ).astimezone(tz=self._local_tz) if self._start_date_timestamp else None ) except ValueError: self._start_date = None return self._start_date @property def end_date(self): """ For Albums, return end date (most recent image) of album or None for albums with no images For Import Sessions, return end date of import sessions (when import was completed) """ try: return self._end_date except AttributeError: try: self._end_date = ( datetime.fromtimestamp( self._end_date_timestamp + TIME_DELTA ).astimezone(tz=self._local_tz) if self._end_date_timestamp else None ) except ValueError: self._end_date = None return self._end_date @property def photos(self): return [] def __len__(self): """ return number of photos contained in album """ return len(self.photos) class AlbumInfo(AlbumInfoBaseClass): """ Base class for AlbumInfo, ImportInfo Info about a specific Album, contains all the details about the album including folders, photos, etc. """ @property def title(self): """ return title / name of album """ return self._title @property def photos(self): """ return list of photos contained in album sorted in same sort order as Photos """ try: return self._photos except AttributeError: if self.uuid in self._db._dbalbums_album: uuid, sort_order = zip(*self._db._dbalbums_album[self.uuid]) sorted_uuid = sort_list_by_keys(uuid, sort_order) self._photos = self._db.photos_by_uuid(sorted_uuid) else: self._photos = [] return self._photos @property def folder_names(self): """ return hierarchical list of folders the album is contained in the folder list is in form: ["Top level folder", "sub folder 1", "sub folder 2", ...] returns empty list if album is not in any folders """ try: return self._folder_names except AttributeError: self._folder_names = self._db._album_folder_hierarchy_list(self._uuid) return self._folder_names @property def folder_list(self): """ return hierarchical list of folders the album is contained in as list of FolderInfo objects in form ["Top level folder", "sub folder 1", "sub folder 2", ...] returns empty list if album is not in any folders """ try: return self._folders except AttributeError: self._folders = self._db._album_folder_hierarchy_folderinfo(self._uuid) return self._folders @property def parent(self): """ returns FolderInfo object for parent folder or None if no parent (e.g. top-level album) """ try: return self._parent except AttributeError: if self._db._db_version <= _PHOTOS_4_VERSION: parent_uuid = self._db._dbalbum_details[self._uuid]["folderUuid"] self._parent = ( FolderInfo(db=self._db, uuid=parent_uuid) if parent_uuid != _PHOTOS_4_TOP_LEVEL_ALBUM else None ) else: parent_pk = self._db._dbalbum_details[self._uuid]["parentfolder"] self._parent = ( FolderInfo(db=self._db, uuid=self._db._dbalbums_pk[parent_pk]) if parent_pk != self._db._folder_root_pk else None ) return self._parent class ImportInfo(AlbumInfoBaseClass): @property def photos(self): """ return list of photos contained in import session """ try: return self._photos except AttributeError: uuid_list, sort_order = zip( *[ (uuid, self._db._dbphotos[uuid]["fok_import_session"]) for uuid in self._db._dbphotos if self._db._dbphotos[uuid]["import_uuid"] == self.uuid ] ) sorted_uuid = sort_list_by_keys(uuid_list, sort_order) self._photos = self._db.photos_by_uuid(sorted_uuid) return self._photos class FolderInfo: """ Info about a specific folder, contains all the details about the folder including folders, albums, etc """ def __init__(self, db=None, uuid=None): self._uuid = uuid self._db = db if self._db._db_version <= _PHOTOS_4_VERSION: self._pk = None self._title = self._db._dbfolder_details[uuid]["name"] else: self._pk = self._db._dbalbum_details[uuid]["pk"] self._title = self._db._dbalbum_details[uuid]["title"] @property def title(self): """ return title / name of folder""" return self._title @property def uuid(self): """ return uuid of folder """ return self._uuid @property def album_info(self): """ return list of albums (as AlbumInfo objects) contained in the folder """ try: return self._albums except AttributeError: if self._db._db_version <= _PHOTOS_4_VERSION: albums = [ AlbumInfo(db=self._db, uuid=album) for album, detail in self._db._dbalbum_details.items() if not detail["intrash"] and detail["albumSubclass"] == _PHOTOS_4_ALBUM_KIND and detail["folderUuid"] == self._uuid ] else: albums = [ AlbumInfo(db=self._db, uuid=album) for album, detail in self._db._dbalbum_details.items() if not detail["intrash"] and detail["kind"] == _PHOTOS_5_ALBUM_KIND and detail["parentfolder"] == self._pk ] self._albums = albums return self._albums @property def parent(self): """ returns FolderInfo object for parent or None if no parent (e.g. top-level folder) """ try: return self._parent except AttributeError: if self._db._db_version <= _PHOTOS_4_VERSION: parent_uuid = self._db._dbfolder_details[self._uuid]["parentFolderUuid"] self._parent = ( FolderInfo(db=self._db, uuid=parent_uuid) if parent_uuid != _PHOTOS_4_TOP_LEVEL_ALBUM else None ) else: parent_pk = self._db._dbalbum_details[self._uuid]["parentfolder"] self._parent = ( FolderInfo(db=self._db, uuid=self._db._dbalbums_pk[parent_pk]) if parent_pk != self._db._folder_root_pk else None ) return self._parent @property def subfolders(self): """ return list of folders (as FolderInfo objects) contained in the folder """ try: return self._folders except AttributeError: if self._db._db_version <= _PHOTOS_4_VERSION: folders = [ FolderInfo(db=self._db, uuid=folder) for folder, detail in self._db._dbfolder_details.items() if not detail["intrash"] and not detail["isMagic"] and detail["parentFolderUuid"] == self._uuid ] else: folders = [ FolderInfo(db=self._db, uuid=album) for album, detail in self._db._dbalbum_details.items() if not detail["intrash"] and detail["kind"] == _PHOTOS_5_FOLDER_KIND and detail["parentfolder"] == self._pk ] self._folders = folders return self._folders def __len__(self): """ returns count of folders + albums contained in the folder """ return len(self.subfolders) + len(self.album_info)