"""
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_ALBUMS,
_PHOTOS_4_VERSION,
_PHOTOS_5_ALBUM_KIND,
_PHOTOS_5_FOLDER_KIND,
_PHOTOS_5_VERSION,
TIME_DELTA,
AlbumSortOrder,
)
from .datetime_utils import get_local_tz
from .query_builder import get_query
__all__ = [
"sort_list_by_keys",
"AlbumInfoBaseClass",
"AlbumInfo",
"ImportInfo",
"ProjectInfo",
"FolderInfo",
]
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, uuid):
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 []
@property
def owner(self):
"""Return name of photo owner for shared album (Photos 5+ only), or None if not shared"""
if self._db._db_version <= _PHOTOS_4_VERSION:
return None
try:
return self._owner
except AttributeError:
try:
personid = self._db._dbalbum_details[self.uuid][
"cloudownerhashedpersonid"
]
self._owner = (
self._db._db_hashed_person_id[personid]["full_name"]
if personid
else None
)
except KeyError:
self._owner = None
return self._owner
def asdict(self):
"""Return album info as a dict"""
return {
"uuid": self.uuid,
"creation_date": self.creation_date,
"start_date": self.start_date,
"end_date": self.end_date,
"owner": self.owner,
"photos": [p.uuid for p in self.photos],
}
def __len__(self):
"""return number of photos contained in album"""
return len(self.photos)
[docs]class AlbumInfo(AlbumInfoBaseClass):
"""
Info about a specific Album, contains all the details about the album
including folders, photos, etc.
"""
def __init__(self, db, uuid):
super().__init__(db=db, uuid=uuid)
self._title = self._db._dbalbum_details[uuid]["title"]
@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)
photos = self._db.photos_by_uuid(sorted_uuid)
sort_order = self.sort_order
if sort_order == AlbumSortOrder.NEWEST_FIRST:
self._photos = sorted(photos, key=lambda p: p.date, reverse=True)
elif sort_order == AlbumSortOrder.OLDEST_FIRST:
self._photos = sorted(photos, key=lambda p: p.date)
elif sort_order == AlbumSortOrder.TITLE:
self._photos = sorted(photos, key=lambda p: p.title or "")
else:
# assume AlbumSortOrder.MANUAL
self._photos = photos
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", ...]
or 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):
"""Returns list of FolderInfo objects for each folder the album is contained in
or 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 not in _PHOTOS_4_TOP_LEVEL_ALBUMS
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 is not None and parent_pk != self._db._folder_root_pk
else None
)
return self._parent
@property
def sort_order(self):
"""return sort order of album"""
if self._db._db_version <= _PHOTOS_4_VERSION:
return AlbumSortOrder.MANUAL
details = self._db._dbalbum_details[self._uuid]
if details["customsortkey"] == 1:
if details["customsortascending"] == 0:
return AlbumSortOrder.NEWEST_FIRST
elif details["customsortascending"] == 1:
return AlbumSortOrder.OLDEST_FIRST
else:
return AlbumSortOrder.UNKNOWN
elif details["customsortkey"] == 5:
return AlbumSortOrder.TITLE
elif details["customsortkey"] == 0:
return AlbumSortOrder.MANUAL
else:
return AlbumSortOrder.UNKNOWN
[docs] def photo_index(self, photo):
"""return index of photo in album (based on album sort order)"""
for index, p in enumerate(self.photos):
if p.uuid == photo.uuid:
return index
raise ValueError(
f"Photo with uuid {photo.uuid} does not appear to be in this album"
)
[docs] def asdict(self):
"""Return album info as a dict"""
dict_data = super().asdict()
dict_data["title"] = self.title
dict_data["folder_names"] = self.folder_names
dict_data["folder_list"] = [f.uuid for f in self.folder_list]
dict_data["sort_order"] = self.sort_order
dict_data["parent"] = self.parent.uuid if self.parent else None
return dict_data
[docs]class ImportInfo(AlbumInfoBaseClass):
"""Information about import sessions"""
def __init__(self, db, uuid):
self._uuid = uuid
self._db = db
if self._db._db_version >= _PHOTOS_5_VERSION:
return super().__init__(db=db, uuid=uuid)
import_session = self._db._db_import_group[self._uuid]
try:
self._creation_date_timestamp = import_session[3]
except (ValueError, TypeError, KeyError):
self._creation_date_timestamp = datetime(1970, 1, 1)
self._start_date_timestamp = self._creation_date_timestamp
self._end_date_timestamp = self._creation_date_timestamp
self._title = import_session[2]
self._local_tz = get_local_tz(
datetime.fromtimestamp(self._creation_date_timestamp + TIME_DELTA)
)
@property
def title(self):
"""return title / name of import session"""
return self._title
@property
def photos(self):
"""return list of photos contained in import session"""
try:
return self._photos
except AttributeError:
if self._db._db_version >= _PHOTOS_5_VERSION:
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)
else:
import_photo_uuids = [
u
for u in self._db._dbphotos
if self._db._dbphotos[u]["import_uuid"] == self.uuid
]
self._photos = self._db.photos_by_uuid(import_photo_uuids)
return self._photos
[docs] def asdict(self):
"""Return import info as a dict"""
return {
"uuid": self.uuid,
"creation_date": self.creation_date,
"start_date": self.start_date,
"end_date": self.end_date,
"title": self.title,
"photos": [p.uuid for p in self.photos],
}
def __bool__(self):
"""Always returns True
A photo without an import session will return None for import_info,
thus if import_info is not None, it must be a valid import_info object (#820)
"""
return True
[docs]class ProjectInfo(AlbumInfo):
"""
ProjectInfo with info about projects
Projects are cards, calendars, slideshows, etc.
"""
...
[docs]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 not in _PHOTOS_4_TOP_LEVEL_ALBUMS
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 is not None and 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
[docs] def asdict(self):
"""Return folder info as a dict"""
return {
"title": self.title,
"uuid": self.uuid,
"parent": self.parent.uuid if self.parent is not None else None,
"subfolders": [f.uuid for f in self.subfolders],
"albums": [a.uuid for a in self.album_info],
}
def __len__(self):
"""returns count of folders + albums contained in the folder"""
return len(self.subfolders) + len(self.album_info)