Added albuminfo.py for AlbumInfo and FolderInfo classes

This commit is contained in:
Rhet Turnbull 2020-04-11 10:28:50 -07:00
parent c01f713f00
commit 96365728c2
34 changed files with 392 additions and 30 deletions

View File

@ -40,3 +40,9 @@ _MOVIE_TYPE = 1
# Name of XMP template file
_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "templates")
_XMP_TEMPLATE_NAME = "xmp_sidecar.mako"
# Constants used for processing folders and albums
_PHOTOS_5_ALBUM_KIND = 2 # normal user album
_PHOTOS_5_SHARED_ALBUM_KIND = 1505 # shared album
_PHOTOS_5_FOLDER_KIND = 4000 # user folder
_PHOTOS_5_ROOT_FOLDER_KIND = 3999 # root folder

160
osxphotos/albuminfo.py Normal file
View File

@ -0,0 +1,160 @@
"""
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 ._constants import _PHOTOS_5_ALBUM_KIND, _PHOTOS_5_FOLDER_KIND
class AlbumInfo:
"""
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"]
@property
def title(self):
""" return title / name of album """
return self._title
@property
def uuid(self):
""" return uuid of album """
return self._uuid
@property
def photos(self):
""" return list of photos contained in album """
try:
return self._photos
except AttributeError:
uuid = self._db._dbalbums_album[self._uuid]
self._photos = self._db.photos(uuid=uuid)
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 folders(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:
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
def __len__(self):
""" return number of photos contained in album """
return len(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
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 albums(self):
""" return list of albums (as AlbumInfo objects) contained in the folder """
try:
return self._albums
except AttributeError:
albums = [
AlbumInfo(db=self._db, uuid=album)
for album, detail in self._db._dbalbum_details.items()
if detail["intrash"] == 0
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:
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 folders(self):
""" return list of folders (as FolderInfo objects) contained in the folder """
try:
return self._folders
except AttributeError:
folders = [
FolderInfo(db=self._db, uuid=album)
for album, detail in self._db._dbalbum_details.items()
if detail["intrash"] == 0
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.folders) + len(self.albums)

View File

@ -23,8 +23,13 @@ from ._constants import (
_TESTED_DB_VERSIONS,
_TESTED_OS_VERSIONS,
_UNKNOWN_PERSON,
_PHOTOS_5_ROOT_FOLDER_KIND,
_PHOTOS_5_FOLDER_KIND,
_PHOTOS_5_ALBUM_KIND,
_PHOTOS_5_SHARED_ALBUM_KIND,
)
from ._version import __version__
from .albuminfo import AlbumInfo, FolderInfo
from .photoinfo import PhotoInfo
from .utils import (
_check_file_exists,
@ -151,6 +156,20 @@ class PhotosDB:
# Used to find path of photos imported but not copied to the Photos library
self._dbvolumes = {}
# Dict with information about parent folders for folders and albums
# key is album or folder UUID and value is list of UUIDs of parent folder
# e.g. {'0C514A98-7B77-4E4F-801B-364B7B65EAFA': ['92D68107-B6C7-453B-96D2-97B0F26D5B8B'],}
self._dbalbum_parent_folders = {}
# Dict with information about folder hierarchy for each album / folder
# key is uuid of album / folder, value is dict with uuid of descendant folder / album
# structure is recursive as a descendant may itself have descendants
# e.g. {'AA4145F5-098C-496E-9197-B7584958FF9B': {'99D24D3E-59E7-465F-B386-A48A94B00BC1': {'F2246D82-1A12-4994-9654-3DC6FE38A7A8': None}}, }
self._dbalbum_folders = {}
# Will hold the primary key of root folder
self._folder_root_pk = None
if _debug():
logging.debug(f"dbfile = {dbfile}")
@ -320,6 +339,51 @@ class PhotosDB:
persons = self._dbfaces_person.keys()
return list(persons)
@property
def folders(self):
""" return list of top-level folders in the photos database """
if self._db_version < _PHOTOS_5_VERSION:
raise AttributeError("Not yet implemented for this DB version")
folders = [
FolderInfo(db=self, uuid=album)
for album, detail in self._dbalbum_details.items()
if detail["intrash"] == 0
and detail["kind"] == _PHOTOS_5_FOLDER_KIND
and detail["parentfolder"] == self._folder_root_pk
]
return folders
@property
def albums(self):
""" return list of AlbumInfo objects for each album in the photos database """
albums = [
AlbumInfo(db=self, uuid=album)
for album in self._dbalbums_album.keys()
if self._dbalbum_details[album]["cloudownerhashedpersonid"] is None
]
return albums
@property
def albums_shared(self):
""" return list of AlbumInfo objects for each shared album in the photos database
only valid for Photos 5; on Photos <= 4, prints warning and returns empty list """
# if _dbalbum_details[key]["cloudownerhashedpersonid"] is not None, then it's a shared album
if self._db_version < _PHOTOS_5_VERSION:
logging.warning(
f"albums_shared not implemented for Photos versions < {_PHOTOS_5_VERSION}"
)
return []
albums_shared = [
AlbumInfo(db=self, uuid=album)
for album in self._dbalbums_album.keys()
if self._dbalbum_details[album]["cloudownerhashedpersonid"] is not None
]
return albums_shared
@property
def album_names(self):
""" return list of albums found in photos database """
@ -327,14 +391,11 @@ class PhotosDB:
# Could be more than one album with same name
# Right now, they are treated as same album and photos are combined from albums with same name
albums = set()
album_keys = [
k
for k in self._dbalbums_album.keys()
if self._dbalbum_details[k]["cloudownerhashedpersonid"] is None
]
for album in album_keys:
albums.add(self._dbalbum_details[album]["title"])
albums = {
self._dbalbum_details[album]["title"]
for album in self._dbalbums_album.keys()
if self._dbalbum_details[album]["cloudownerhashedpersonid"] is None
}
return list(albums)
@property
@ -349,18 +410,15 @@ class PhotosDB:
if self._db_version < _PHOTOS_5_VERSION:
logging.warning(
f"albums_shared not implemented for Photos versions < {_PHOTOS_5_VERSION}"
f"album_names_shared not implemented for Photos versions < {_PHOTOS_5_VERSION}"
)
return []
albums = set()
album_keys = [
k
for k in self._dbalbums_album.keys()
if self._dbalbum_details[k]["cloudownerhashedpersonid"] is not None
]
for album in album_keys:
albums.add(self._dbalbum_details[album]["title"])
albums = {
self._dbalbum_details[album]["title"]
for album in self._dbalbums_album.keys()
if self._dbalbum_details[album]["cloudownerhashedpersonid"] is not None
}
return list(albums)
@property
@ -1029,6 +1087,7 @@ class PhotosDB:
)
for album in c:
self._dbalbum_details[album[0]] = {
"_uuid": album[0],
"title": album[1],
"cloudlocalstate": album[2],
"cloudownerfirstname": album[3],
@ -1047,6 +1106,42 @@ class PhotosDB:
# in Photos >= 5, folders are special albums
self._dbalbums_pk[album[8]] = album[0]
# get pk of root folder
root_uuid = [
album
for album, details in self._dbalbum_details.items()
if details["kind"] == _PHOTOS_5_ROOT_FOLDER_KIND
]
if len(root_uuid) != 1:
raise ValueError(f"Error finding root folder: {root_uuid}")
else:
self._folder_root_pk = self._dbalbum_details[root_uuid[0]]["pk"]
# build _dbalbum_folders which is in form uuid: [list of parent uuids]
for album, details in self._dbalbum_details.items():
pk_parent = details["parentfolder"]
if pk_parent is None:
continue
try:
parent = self._dbalbums_pk[pk_parent]
except KeyError:
raise ValueError(f"Did not find uuid for album {album} pk {pk_parent}")
try:
self._dbalbum_parent_folders[album].append(parent)
except KeyError:
self._dbalbum_parent_folders[album] = [parent]
for album, details in self._dbalbum_details.items():
# if details["kind"] in [_PHOTOS_5_ALBUM_KIND, _PHOTOS_5_FOLDER_KIND]:
if details["kind"] == _PHOTOS_5_ALBUM_KIND:
folder_hierarchy = self._build_album_folder_hierarchy(album)
self._dbalbum_folders[album] = folder_hierarchy
elif details["kind"] == _PHOTOS_5_SHARED_ALBUM_KIND:
# shared albums can be in folders
self._dbalbum_folders[album] = []
if _debug():
logging.debug(f"Finished walking through albums")
logging.debug(pformat(self._dbalbums_album))
@ -1482,6 +1577,15 @@ class PhotosDB:
logging.debug("Album titles (_dbalbum_titles):")
logging.debug(pformat(self._dbalbum_titles))
logging.debug("Album folders (_dbalbum_folders):")
logging.debug(pformat(self._dbalbum_folders))
logging.debug("Album parent folders (_dbalbum_parent_folders):")
logging.debug(pformat(self._dbalbum_parent_folders))
logging.debug("Albums pk (_dbalbums_pk):")
logging.debug(pformat(self._dbalbums_pk))
logging.debug("Volumes (_dbvolumes):")
logging.debug(pformat(self._dbvolumes))
@ -1491,6 +1595,98 @@ class PhotosDB:
logging.debug("Burst Photos (dbphotos_burst:")
logging.debug(pformat(self._dbphotos_burst))
def _build_album_folder_hierarchy(self, uuid, folders=None):
""" recursively build folder/album hierarchy
uuid: uuid of the album/folder being processed
folders: dict holding the folder hierarchy """
if self._db_version < _PHOTOS_5_VERSION:
raise AttributeError("Not yet implemented for this DB version")
# get parent uuid
parent = self._dbalbum_details[uuid]["parentfolder"]
if parent is not None:
parent_uuid = self._dbalbums_pk[parent]
else:
# folder with no parent (e.g. shared iCloud folders)
return folders
if self._db_version >= _PHOTOS_5_VERSION and parent == self._folder_root_pk:
# at the top of the folder hierarchy, we're done
return folders
# recurse to keep building
folders = {parent_uuid: folders}
folders = self._build_album_folder_hierarchy(parent_uuid, folders=folders)
return folders
def _album_folder_hierarchy_list(self, album_uuid):
""" return hierarchical list of folder names album_uuid is contained in
the folder list is in form:
["Top level folder", "sub folder 1", "sub folder 2"]
returns empty list of album is not in any folders """
# title = photosdb._dbalbum_details[album_uuid]["title"]
folders = self._dbalbum_folders[album_uuid]
def _recurse_folder_hierarchy(folders, hierarchy=[]):
""" recursively walk the folders dict to build list of folder hierarchy """
if not folders:
# empty folder dict (album has no folder hierarchy)
return []
if len(folders) != 1:
raise ValueError("Expected only a single key in folders dict")
folder_uuid = list(folders)[0] # first and only key of dict
parent_title = self._dbalbum_details[folder_uuid]["title"]
hierarchy.append(parent_title)
folders = folders[folder_uuid]
if folders:
# still have elements left to recurse
hierarchy = _recurse_folder_hierarchy(folders, hierarchy=hierarchy)
return hierarchy
# no elements left to recurse
return hierarchy
hierarchy = _recurse_folder_hierarchy(folders)
return hierarchy
def _album_folder_hierarchy_folderinfo(self, album_uuid):
""" return hierarchical list of FolderInfo objects album_uuid is contained in
["Top level folder", "sub folder 1", "sub folder 2"]
returns empty list of album is not in any folders """
# title = photosdb._dbalbum_details[album_uuid]["title"]
folders = self._dbalbum_folders[album_uuid]
def _recurse_folder_hierarchy(folders, hierarchy=[]):
""" recursively walk the folders dict to build list of folder hierarchy """
if not folders:
# empty folder dict (album has no folder hierarchy)
return []
if len(folders) != 1:
raise ValueError("Expected only a single key in folders dict")
folder_uuid = list(folders)[0] # first and only key of dict
hierarchy.append(FolderInfo(db=self, uuid=folder_uuid))
folders = folders[folder_uuid]
if folders:
# still have elements left to recurse
hierarchy = _recurse_folder_hierarchy(folders, hierarchy=hierarchy)
return hierarchy
# no elements left to recurse
return hierarchy
hierarchy = _recurse_folder_hierarchy(folders)
return hierarchy
def _process_database5X(self):
""" ALPHA: TESTING using SimpleNamespace to clean up code for info, DO NOT CALL THIS METHOD """
""" Needs to be updated for changes in process_database5 due to adding PlaceInfo """

View File

@ -3,24 +3,24 @@
<plist version="1.0">
<dict>
<key>BackgroundHighlightCollection</key>
<date>2020-04-11T01:34:25Z</date>
<date>2020-04-11T13:55:22Z</date>
<key>BackgroundHighlightEnrichment</key>
<date>2020-04-11T01:34:24Z</date>
<date>2020-04-11T13:55:21Z</date>
<key>BackgroundJobAssetRevGeocode</key>
<date>2020-04-11T01:34:26Z</date>
<date>2020-04-11T16:03:49Z</date>
<key>BackgroundJobSearch</key>
<date>2020-04-11T01:34:26Z</date>
<date>2020-04-11T13:55:22Z</date>
<key>BackgroundPeopleSuggestion</key>
<date>2020-04-11T01:34:23Z</date>
<date>2020-04-11T13:55:21Z</date>
<key>BackgroundUserBehaviorProcessor</key>
<date>2020-04-10T06:04:05Z</date>
<date>2020-04-11T06:27:26Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphConsistencyUpdateJobDateKey</key>
<date>2020-04-11T01:34:33Z</date>
<date>2020-04-11T16:17:41Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
<date>2020-04-10T06:04:04Z</date>
<date>2020-04-11T06:27:24Z</date>
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
<date>2020-04-11T01:34:26Z</date>
<date>2020-04-11T16:03:50Z</date>
<key>SiriPortraitDonation</key>
<date>2020-04-10T06:04:05Z</date>
<date>2020-04-11T06:27:26Z</date>
</dict>
</plist>

View File

@ -3,8 +3,8 @@
<plist version="1.0">
<dict>
<key>FaceIDModelLastGenerationKey</key>
<date>2020-04-10T06:04:06Z</date>
<date>2020-04-11T06:27:27Z</date>
<key>LastContactClassificationKey</key>
<date>2020-04-10T06:04:08Z</date>
<date>2020-04-11T06:27:29Z</date>
</dict>
</plist>

View File

@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>coalesceDate</key>
<date>2019-12-08T18:06:37Z</date>
<date>2020-04-11T16:34:16Z</date>
<key>coalescePayloadVersion</key>
<integer>1</integer>
<key>currentPayloadVersion</key>