Compare commits

...

5 Commits

Author SHA1 Message Date
Rhet Turnbull
3bac106eb7 test library update 2020-04-18 12:24:03 -07:00
Rhet Turnbull
47d1c82c03 Added folder support for Photos <= 4, closes #93 2020-04-18 12:21:08 -07:00
Rhet Turnbull
6f281711e2 cleaned up SQL statements in _process_database4 2020-04-18 08:05:43 -07:00
Rhet Turnbull
4b30b3b426 Fixed suffix check on export to be case insensitive 2020-04-18 07:59:04 -07:00
Rhet Turnbull
1fa9583ea6 Updated CHANGELOG.md 2020-04-17 23:33:17 -07:00
46 changed files with 452 additions and 212 deletions

View File

@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [v0.28.1](https://github.com/RhetTbull/osxphotos/compare/v0.27.4...v0.28.1)
> 18 April 2020
- Initial work on suppport for associated RAW images [`7e42ebb`](https://github.com/RhetTbull/osxphotos/commit/7e42ebb2402d45cd5d20bdd55bddddaa9db4679f)
- Initial support for RAW photos in Photos 4 to address issue #101 [`9d15147`](https://github.com/RhetTbull/osxphotos/commit/9d151478d610291b8d482aafae3d445dfd391fca)
- replaced CLI option --original-name with --current-name [`36c2821`](https://github.com/RhetTbull/osxphotos/commit/36c2821a0fa62eaaa54cf1edc2d9c6da98155354)
#### [v0.27.4](https://github.com/RhetTbull/osxphotos/compare/v0.27.3...v0.27.4)
> 12 April 2020
@@ -265,11 +273,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Moved PhotosDB attributes to properties instead of methods [`d95acdf`](https://github.com/RhetTbull/osxphotos/commit/d95acdf9f8764a1720bcba71a6dad29bf668eaf9)
- changed interface for export, prepped for exiftool_json_sidecar [`1fe8859`](https://github.com/RhetTbull/osxphotos/commit/1fe885962e8a9a420e776bdd3dc640ca143224b2)
#### [v0.15.1](https://github.com/RhetTbull/osxphotos/compare/v0.15.0...v0.15.1)
> 13 April 2020
#### [v0.15.0](https://github.com/RhetTbull/osxphotos/compare/v0.14.21...v0.15.0)
#### [v0.15.1](https://github.com/RhetTbull/osxphotos/compare/v0.14.21...v0.15.1)
> 14 December 2019

View File

@@ -17,8 +17,8 @@ _TESTED_DB_VERSIONS = ["6000", "4025", "4016", "3301", "2622"]
_PHOTOS_3_VERSION = "3301"
# versions 5.0 and later have a different database structure
_PHOTOS_4_VERSION = "4025" # latest Mojove version on 10.14.6
_PHOTOS_5_VERSION = "6000" # seems to be current on 10.15.1 through 10.14.4
_PHOTOS_4_VERSION = "4025" # latest Mojove version on 10.14.6
_PHOTOS_5_VERSION = "6000" # seems to be current on 10.15.1 through 10.15.4
# which major version operating systems have been tested
_TESTED_OS_VERSIONS = ["12", "13", "14", "15"]
@@ -47,3 +47,7 @@ _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
_PHOTOS_4_ALBUM_KIND = 3 # RKAlbum.albumSubclass
_PHOTOS_4_TOP_LEVEL_ALBUM = "TopLevelAlbums"
_PHOTOS_4_ROOT_FOLDER = "LibraryFolder"

View File

@@ -1,3 +1,3 @@
""" version info """
__version__ = "0.28.1"
__version__ = "0.28.2"

View File

@@ -12,7 +12,13 @@ PhotosDB.folders() returns a list of FolderInfo objects
import logging
from ._constants import _PHOTOS_5_ALBUM_KIND, _PHOTOS_5_FOLDER_KIND, _PHOTOS_5_VERSION
from ._constants import (
_PHOTOS_4_ALBUM_KIND,
_PHOTOS_4_TOP_LEVEL_ALBUM,
_PHOTOS_4_VERSION,
_PHOTOS_5_ALBUM_KIND,
_PHOTOS_5_FOLDER_KIND,
)
class AlbumInfo:
@@ -53,14 +59,13 @@ class AlbumInfo:
["Top level folder", "sub folder 1", "sub folder 2", ...]
returns empty list if album is not in any folders """
if self._db._db_version < _PHOTOS_5_VERSION:
logging.warning("Folders not yet implemented for this DB version")
return []
try:
return self._folder_names
except AttributeError:
self._folder_names = self._db._album_folder_hierarchy_list(self._uuid)
if self._db._db_version <= _PHOTOS_4_VERSION:
self._folder_names = self._db._album_folder_hierarchy_list(self._uuid)
else:
self._folder_names = self._db._album_folder_hierarchy_list(self._uuid)
return self._folder_names
@property
@@ -70,10 +75,6 @@ class AlbumInfo:
["Top level folder", "sub folder 1", "sub folder 2", ...]
returns empty list if album is not in any folders """
if self._db._db_version < _PHOTOS_5_VERSION:
logging.warning("Folders not yet implemented for this DB version")
return []
try:
return self._folders
except AttributeError:
@@ -83,19 +84,23 @@ class AlbumInfo:
@property
def parent(self):
""" returns FolderInfo object for parent folder or None if no parent (e.g. top-level album) """
if self._db._db_version < _PHOTOS_5_VERSION:
logging.warning("Folders not yet implemented for this DB version")
return None
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
)
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
def __len__(self):
@@ -112,8 +117,12 @@ class FolderInfo:
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"]
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):
@@ -131,13 +140,22 @@ class FolderInfo:
try:
return self._albums
except AttributeError:
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
]
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
@@ -147,12 +165,20 @@ class FolderInfo:
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
)
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
@@ -161,13 +187,22 @@ class FolderInfo:
try:
return self._folders
except AttributeError:
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
]
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

View File

@@ -21,8 +21,8 @@ from mako.template import Template
from ._constants import (
_MOVIE_TYPE,
_PHOTO_TYPE,
_PHOTOS_4_VERSION,
_PHOTOS_5_SHARED_PHOTO_PATH,
_PHOTOS_5_VERSION,
_TEMPLATE_DIR,
_XMP_TEMPLATE_NAME,
)
@@ -98,7 +98,7 @@ class PhotoInfo:
if self._info["isMissing"] == 1:
return photopath # path would be meaningless until downloaded
if self._db._db_version < _PHOTOS_5_VERSION:
if self._db._db_version <= _PHOTOS_4_VERSION:
vol = self._info["volume"]
if vol is not None:
photopath = os.path.join("/Volumes", vol, self._info["imagePath"])
@@ -137,7 +137,7 @@ class PhotoInfo:
photopath = None
if self._db._db_version < _PHOTOS_5_VERSION:
if self._db._db_version <= _PHOTOS_4_VERSION:
if self._info["hasAdjustments"]:
edit_id = self._info["edit_resource_id"]
if edit_id is not None:
@@ -269,7 +269,7 @@ class PhotoInfo:
# )
# return photopath
if self._db._db_version < _PHOTOS_5_VERSION:
if self._db._db_version <= _PHOTOS_4_VERSION:
vol = self._info["raw_info"]["volume"]
if vol is not None:
photopath = os.path.join(
@@ -400,7 +400,7 @@ class PhotoInfo:
def shared(self):
""" returns True if photos is in a shared iCloud album otherwise false
Only valid on Photos 5; returns None on older versions """
if self._db._db_version >= _PHOTOS_5_VERSION:
if self._db._db_version > _PHOTOS_4_VERSION:
return self._info["shared"]
else:
return None
@@ -445,7 +445,7 @@ class PhotoInfo:
""" Returns True if photo is a cloud asset (in an iCloud library),
otherwise False
"""
if self._db._db_version < _PHOTOS_5_VERSION:
if self._db._db_version <= _PHOTOS_4_VERSION:
return (
True
if self._info["cloudLibraryState"] is not None
@@ -488,7 +488,7 @@ class PhotoInfo:
If photo is missing, returns None """
photopath = None
if self._db._db_version < _PHOTOS_5_VERSION:
if self._db._db_version <= _PHOTOS_4_VERSION:
if self.live_photo and not self.ismissing:
live_model_id = self._info["live_model_id"]
if live_model_id == None:
@@ -579,7 +579,7 @@ class PhotoInfo:
# implementation note: doesn't create the PlaceInfo object until requested
# then memoizes the object in self._place to avoid recreating the object
if self._db._db_version < _PHOTOS_5_VERSION:
if self._db._db_version <= _PHOTOS_4_VERSION:
try:
return self._place # pylint: disable=access-member-before-definition
except AttributeError:
@@ -717,8 +717,11 @@ class PhotoInfo:
# warn if suffixes don't match but ignore .JPG / .jpeg as
# Photo's often converts .JPG to .jpeg
suffixes = sorted([x.lower() for x in [dest.suffix, actual_suffix]])
if dest.suffix != actual_suffix and suffixes != [".jpeg", ".jpg"]:
logging.debug(
if dest.suffix.lower() != actual_suffix.lower() and suffixes != [
".jpeg",
".jpg",
]:
logging.warning(
f"Invalid destination suffix: {dest.suffix}, should be {actual_suffix}"
)

View File

@@ -24,6 +24,8 @@ from ._constants import (
_TESTED_DB_VERSIONS,
_TESTED_OS_VERSIONS,
_UNKNOWN_PERSON,
_PHOTOS_4_ALBUM_KIND,
_PHOTOS_4_TOP_LEVEL_ALBUM,
_PHOTOS_5_ROOT_FOLDER_KIND,
_PHOTOS_5_FOLDER_KIND,
_PHOTOS_5_ALBUM_KIND,
@@ -173,6 +175,9 @@ class PhotosDB:
# e.g. {'AA4145F5-098C-496E-9197-B7584958FF9B': {'99D24D3E-59E7-465F-B386-A48A94B00BC1': {'F2246D82-1A12-4994-9654-3DC6FE38A7A8': None}}, }
self._dbalbum_folders = {}
# Dict with information about folders
self._dbfolder_details = {}
# Will hold the primary key of root folder
self._folder_root_pk = None
@@ -312,7 +317,7 @@ class PhotosDB:
valid only on Photos 5; on Photos <= 4, prints warning and returns empty dict """
# if _dbalbum_details[key]["cloudownerhashedpersonid"] is not None, then it's a shared album
if self._db_version < _PHOTOS_5_VERSION:
if self._db_version <= _PHOTOS_4_VERSION:
logging.warning(
f"albums_shared not implemented for Photos versions < {_PHOTOS_5_VERSION}"
)
@@ -348,33 +353,43 @@ class PhotosDB:
@property
def folder_info(self):
""" return list FolderInfo objects representing top-level folders in the photos database """
if self._db_version < _PHOTOS_5_VERSION:
logging.warning("Folders not yet implemented for this DB version")
return []
folders = [
FolderInfo(db=self, uuid=album)
for album, detail in self._dbalbum_details.items()
if not detail["intrash"]
and detail["kind"] == _PHOTOS_5_FOLDER_KIND
and detail["parentfolder"] == self._folder_root_pk
]
if self._db_version <= _PHOTOS_4_VERSION:
folders = [
FolderInfo(db=self, uuid=folder)
for folder, detail in self._dbfolder_details.items()
if not detail["intrash"]
and not detail["isMagic"]
and detail["parentFolderUuid"] == _PHOTOS_4_TOP_LEVEL_ALBUM
]
else:
folders = [
FolderInfo(db=self, uuid=album)
for album, detail in self._dbalbum_details.items()
if not detail["intrash"]
and detail["kind"] == _PHOTOS_5_FOLDER_KIND
and detail["parentfolder"] == self._folder_root_pk
]
return folders
@property
def folders(self):
""" return list of top-level folder names in the photos database """
if self._db_version < _PHOTOS_5_VERSION:
logging.warning("Folders not yet implemented for this DB version")
return []
folder_names = [
detail["title"]
for detail in self._dbalbum_details.values()
if not detail["intrash"]
and detail["kind"] == _PHOTOS_5_FOLDER_KIND
and detail["parentfolder"] == self._folder_root_pk
]
if self._db_version <= _PHOTOS_4_VERSION:
folder_names = [
folder["name"]
for folder in self._dbfolder_details.values()
if not folder["intrash"]
and not folder["isMagic"]
and folder["parentFolderUuid"] == _PHOTOS_4_TOP_LEVEL_ALBUM
]
else:
folder_names = [
detail["title"]
for detail in self._dbalbum_details.values()
if not detail["intrash"]
and detail["kind"] == _PHOTOS_5_FOLDER_KIND
and detail["parentfolder"] == self._folder_root_pk
]
return folder_names
@property
@@ -395,7 +410,7 @@ class PhotosDB:
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:
if self._db_version <= _PHOTOS_4_VERSION:
logging.warning(
f"albums_shared not implemented for Photos versions < {_PHOTOS_5_VERSION}"
)
@@ -434,7 +449,7 @@ class PhotosDB:
# if _dbalbum_details[key]["cloudownerhashedpersonid"] is not None, then it's a shared album
if self._db_version < _PHOTOS_5_VERSION:
if self._db_version <= _PHOTOS_4_VERSION:
logging.warning(
f"album_names_shared not implemented for Photos versions < {_PHOTOS_5_VERSION}"
)
@@ -521,10 +536,10 @@ class PhotosDB:
# Look for all combinations of persons and pictures
c.execute(
"select RKPerson.name, RKVersion.uuid from RKFace, RKPerson, RKVersion, RKMaster "
+ "where RKFace.personID = RKperson.modelID and RKVersion.modelId = RKFace.ImageModelId "
+ "and RKVersion.masterUuid = RKMaster.uuid and "
+ "RKVersion.filename not like '%.pdf' and RKVersion.isInTrash = 0"
""" select RKPerson.name, RKVersion.uuid from RKFace, RKPerson, RKVersion, RKMaster
where RKFace.personID = RKperson.modelID and RKVersion.modelId = RKFace.ImageModelId
and RKVersion.masterUuid = RKMaster.uuid
and RKVersion.isInTrash = 0 """
)
for person in c:
if person[0] is None:
@@ -538,10 +553,13 @@ class PhotosDB:
# Get info on albums
c.execute(
"select RKAlbum.uuid, RKVersion.uuid from RKAlbum, RKVersion, RKAlbumVersion "
+ "where RKAlbum.modelID = RKAlbumVersion.albumId and "
+ "RKAlbumVersion.versionID = RKVersion.modelId and "
+ "RKVersion.filename not like '%.pdf' and RKVersion.isInTrash = 0"
""" select
RKAlbum.uuid,
RKVersion.uuid
from RKAlbum, RKVersion, RKAlbumVersion
where RKAlbum.modelID = RKAlbumVersion.albumId and
RKAlbumVersion.versionID = RKVersion.modelId
and RKVersion.isInTrash = 0 """
)
for album in c:
# store by uuid in _dbalbums_uuid and by album in _dbalbums_album
@@ -554,17 +572,31 @@ class PhotosDB:
# now get additional details about albums
c.execute(
"SELECT "
"uuid, " # 0
"name, " # 1
"cloudLibraryState, " # 2
"cloudIdentifier, " # 3
"isInTrash " # 4
"FROM RKAlbum "
""" SELECT
uuid,
name,
cloudLibraryState,
cloudIdentifier,
isInTrash,
folderUuid,
albumType,
albumSubclass
FROM RKAlbum """
)
# Order of results
# 0: uuid
# 1: name
# 2: cloudLibraryState
# 3: cloudIdentifier
# 4: isInTrash
# 5: folderUuid
# 6: albumType
# 7: albumSubclass -- if 3, normal user album
for album in c:
self._dbalbum_details[album[0]] = {
"_uuid": album[0],
"title": album[1],
"cloudlibrarystate": album[2],
"cloudidentifier": album[3],
@@ -573,22 +605,80 @@ class PhotosDB:
"cloudownerfirstname": None, # Photos 5
"cloudownderlastname": None, # Photos 5
"cloudownerhashedpersonid": None, # Photos 5
"folderUuid": album[5],
"albumType": album[6],
"albumSubclass": album[7],
}
# get details about folders
c.execute(
""" SELECT
uuid,
modelId,
name,
isMagic,
isInTrash,
folderType,
parentFolderUuid,
folderPath
FROM RKFolder """
)
# Order of results
# 0 uuid,
# 1 modelId,
# 2 name,
# 3 isMagic,
# 4 isInTrash,
# 5 folderType,
# 6 parentFolderUuid,
# 7 folderPath
for row in c:
uuid = row[0]
self._dbfolder_details[uuid] = {
"_uuid": row[0],
"modelId": row[1],
"name": row[2],
"isMagic": row[3],
"intrash": row[4],
"folderType": row[5],
"parentFolderUuid": row[6],
"folderPath": row[7],
}
# build _dbalbum_folders in form uuid: [parent uuid] to be consistent with _process_database5
for album, details in self._dbalbum_details.items():
# album can be in a single folder
parent = details["folderUuid"]
self._dbalbum_parent_folders[album] = [parent]
# build folder hierarchy
for album, details in self._dbalbum_details.items():
parent_folder = details["folderUuid"]
if parent_folder != _PHOTOS_4_TOP_LEVEL_ALBUM:
# logging.warning(f"album = {details['title']}, parent = {parent_folder}")
folder_hierarchy = self._build_album_folder_hierarchy_4(parent_folder)
self._dbalbum_folders[album] = folder_hierarchy
else:
self._dbalbum_folders[album] = {}
if _debug():
logging.debug(f"Finished walking through albums")
logging.debug(pformat(self._dbalbums_album))
logging.debug(pformat(self._dbalbums_uuid))
logging.debug(pformat(self._dbalbum_details))
logging.debug(pformat(self._dbalbum_folders))
logging.debug(pformat(self._dbfolder_details))
# Get info on keywords
c.execute(
"select RKKeyword.name, RKVersion.uuid, RKMaster.uuid from "
+ "RKKeyword, RKKeywordForVersion, RKVersion, RKMaster "
+ "where RKKeyword.modelId = RKKeyWordForVersion.keywordID and "
+ "RKVersion.modelID = RKKeywordForVersion.versionID "
+ "and RKMaster.uuid = RKVersion.masterUuid "
+ "and RKVersion.filename not like '%.pdf' and RKVersion.isInTrash = 0"
""" select RKKeyword.name, RKVersion.uuid, RKMaster.uuid from
RKKeyword, RKKeywordForVersion, RKVersion, RKMaster
where RKKeyword.modelId = RKKeyWordForVersion.keywordID and
RKVersion.modelID = RKKeywordForVersion.versionID and
RKMaster.uuid = RKVersion.masterUuid and
RKVersion.isInTrash = 0 """
)
for keyword in c:
if not keyword[1] in self._dbkeywords_uuid:
@@ -899,13 +989,13 @@ class PhotosDB:
# get details on external edits
c.execute(
"SELECT RKVersion.uuid, "
"RKVersion.adjustmentUuid, "
"RKAdjustmentData.originator, "
"RKAdjustmentData.format "
"FROM RKVersion, RKAdjustmentData "
"WHERE RKVersion.adjustmentUuid = RKAdjustmentData.uuid "
"AND RKVersion.isInTrash = 0"
""" SELECT RKVersion.uuid,
RKVersion.adjustmentUuid,
RKAdjustmentData.originator,
RKAdjustmentData.format
FROM RKVersion, RKAdjustmentData
WHERE RKVersion.adjustmentUuid = RKAdjustmentData.uuid
AND RKVersion.isInTrash = 0 """
)
for row in c:
@@ -1122,6 +1212,30 @@ class PhotosDB:
logging.debug("Burst Photos (dbphotos_burst:")
logging.debug(pformat(self._dbphotos_burst))
def _build_album_folder_hierarchy_4(self, uuid, folders=None):
""" recursively build folder/album hierarchy
uuid: uuid of the album/folder being processed
folders: dict holding the folder hierarchy """
parent_uuid = self._dbfolder_details[uuid]["parentFolderUuid"]
# logging.warning(f"uuid = {uuid}, parent = {parent_uuid}, folders = {folders}")
if parent_uuid is None:
return folders
if parent_uuid == _PHOTOS_4_TOP_LEVEL_ALBUM:
# at top of hierarchy, we're done
return folders
# recurse to keep building
if not folders:
# first time building
folders = {uuid: None}
folders = {parent_uuid: folders}
folders = self._build_album_folder_hierarchy_4(parent_uuid, folders=folders)
return folders
def _process_database5(self):
""" process the Photos database to extract info """
""" works on Photos version >= 5.0 """
@@ -1228,6 +1342,8 @@ class PhotosDB:
self._folder_root_pk = self._dbalbum_details[root_uuid[0]]["pk"]
# build _dbalbum_folders which is in form uuid: [list of parent uuids]
# TODO: look at this code...it works but I think I album can only be in a single folder
# which means there's a code path that will never get executed
for album, details in self._dbalbum_details.items():
pk_parent = details["parentfolder"]
if pk_parent is None:
@@ -1246,10 +1362,10 @@ class PhotosDB:
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)
folder_hierarchy = self._build_album_folder_hierarchy_5(album)
self._dbalbum_folders[album] = folder_hierarchy
elif details["kind"] == _PHOTOS_5_SHARED_ALBUM_KIND:
# shared albums can be in folders
# shared albums can't be in folders
self._dbalbum_folders[album] = []
if _debug():
@@ -1257,6 +1373,7 @@ class PhotosDB:
logging.debug(pformat(self._dbalbums_album))
logging.debug(pformat(self._dbalbums_uuid))
logging.debug(pformat(self._dbalbum_details))
logging.debug(pformat(self._dbalbum_folders))
# get details on keywords
c.execute(
@@ -1747,14 +1864,11 @@ class PhotosDB:
logging.debug("Burst Photos (dbphotos_burst:")
logging.debug(pformat(self._dbphotos_burst))
def _build_album_folder_hierarchy(self, uuid, folders=None):
def _build_album_folder_hierarchy_5(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"]
@@ -1770,10 +1884,50 @@ class PhotosDB:
# recurse to keep building
folders = {parent_uuid: folders}
folders = self._build_album_folder_hierarchy(parent_uuid, folders=folders)
folders = self._build_album_folder_hierarchy_5(parent_uuid, folders=folders)
return folders
def _album_folder_hierarchy_list(self, album_uuid):
if self._db_version <= _PHOTOS_4_VERSION:
return self._album_folder_hierarchy_list_4(album_uuid)
else:
return self._album_folder_hierarchy_list_5(album_uuid)
def _album_folder_hierarchy_list_4(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._dbfolder_details[folder_uuid]["name"]
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_list_5(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"]
@@ -1808,6 +1962,46 @@ class PhotosDB:
return hierarchy
def _album_folder_hierarchy_folderinfo(self, album_uuid):
if self._db_version <= _PHOTOS_4_VERSION:
return self._album_folder_hierarchy_folderinfo_4(album_uuid)
else:
return self._album_folder_hierarchy_folderinfo_5(album_uuid)
def _album_folder_hierarchy_folderinfo_4(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]
# logging.warning(f"uuid = {album_uuid}, folder = {folders}")
def _recurse_folder_hierarchy(folders, hierarchy=[]):
""" recursively walk the folders dict to build list of folder hierarchy """
# logging.warning(f"folders={folders},hierarchy = {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)
# logging.warning(f"hierarchy = {hierarchy}")
return hierarchy
def _album_folder_hierarchy_folderinfo_5(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 """

View File

@@ -9,12 +9,14 @@
<key>ExpandedSidebarItemIdentifiers</key>
<array>
<string>TopLevelAlbums</string>
<string>QtSnVvTkQ%i2z3hB834M1A</string>
<string>TopLevelSlideshows</string>
<string>N7eQ4VhfTfeHFp9PPHaJDw</string>
</array>
<key>IPXWorkspaceControllerZoomLevelsKey</key>
<dict>
<key>kZoomLevelIdentifierAlbums</key>
<integer>10</integer>
<integer>5</integer>
<key>kZoomLevelIdentifierVersions</key>
<integer>7</integer>
</dict>
@@ -23,11 +25,11 @@
<key>key</key>
<integer>1</integer>
<key>lastKnownDisplayName</key>
<string>Test Album (1)</string>
<string>Pumpkin Farm (1)</string>
<key>type</key>
<string>album</string>
<key>uuid</key>
<string>Uq6qsKihRRSjMHTiD+0Azg</string>
<string>xJ8ya3NBRWC24gKhcwwNeQ</string>
</dict>
<key>lastKnownItemCounts</key>
<dict>

View File

@@ -3,8 +3,8 @@
<plist version="1.0">
<dict>
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
<date>2020-03-27T04:00:09Z</date>
<date>2020-04-18T18:01:02Z</date>
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
<date>2020-03-27T04:00:10Z</date>
<date>2020-04-18T17:22:55Z</date>
</dict>
</plist>

View File

@@ -5,7 +5,7 @@
<key>LithiumMessageTracer</key>
<dict>
<key>LastReportedDate</key>
<date>2020-03-15T20:19:24Z</date>
<date>2020-04-17T17:51:16Z</date>
</dict>
</dict>
</plist>

View File

@@ -11,6 +11,6 @@
<key>PLLastRevGeoForcedProviderOutOfDateCheckVersionKey</key>
<integer>1</integer>
<key>PLLastRevGeoVerFileFetchDateKey</key>
<date>2020-03-27T03:59:54Z</date>
<date>2020-04-17T17:49:52Z</date>
</dict>
</plist>

View File

@@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>LastHistoryRowId</key>
<integer>575</integer>
<integer>606</integer>
<key>LibraryBuildTag</key>
<string>D8C4AAA1-3AB6-4A65-BEBD-99CC3E5D433E</string>
<key>LibrarySchemaVersion</key>

View File

@@ -9,7 +9,7 @@
<key>HistoricalMarker</key>
<dict>
<key>LastHistoryRowId</key>
<integer>575</integer>
<integer>606</integer>
<key>LibraryBuildTag</key>
<string>D8C4AAA1-3AB6-4A65-BEBD-99CC3E5D433E</string>
<key>LibrarySchemaVersion</key>
@@ -24,7 +24,7 @@
<key>SnapshotCompletedDate</key>
<date>2019-07-27T13:16:43Z</date>
<key>SnapshotLastValidated</key>
<date>2020-03-27T04:02:59Z</date>
<date>2020-04-17T17:51:16Z</date>
<key>SnapshotTables</key>
<dict/>
</dict>

View File

@@ -3,24 +3,24 @@
<plist version="1.0">
<dict>
<key>BackgroundHighlightCollection</key>
<date>2020-04-16T17:31:22Z</date>
<date>2020-04-17T14:33:32Z</date>
<key>BackgroundHighlightEnrichment</key>
<date>2020-04-16T17:31:21Z</date>
<date>2020-04-17T14:33:32Z</date>
<key>BackgroundJobAssetRevGeocode</key>
<date>2020-04-16T17:31:22Z</date>
<date>2020-04-17T14:33:33Z</date>
<key>BackgroundJobSearch</key>
<date>2020-04-16T17:31:22Z</date>
<date>2020-04-17T14:33:33Z</date>
<key>BackgroundPeopleSuggestion</key>
<date>2020-04-16T17:31:21Z</date>
<date>2020-04-17T14:33:31Z</date>
<key>BackgroundUserBehaviorProcessor</key>
<date>2020-04-16T06:52:39Z</date>
<date>2020-04-17T07:32:04Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphConsistencyUpdateJobDateKey</key>
<date>2020-04-16T17:38:41Z</date>
<date>2020-04-17T14:33:37Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
<date>2020-04-16T06:52:38Z</date>
<date>2020-04-17T07:32:00Z</date>
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
<date>2020-04-16T17:31:22Z</date>
<date>2020-04-17T14:33:34Z</date>
<key>SiriPortraitDonation</key>
<date>2020-04-16T06:52:39Z</date>
<date>2020-04-17T07:32:04Z</date>
</dict>
</plist>

View File

@@ -3,8 +3,8 @@
<plist version="1.0">
<dict>
<key>FaceIDModelLastGenerationKey</key>
<date>2020-04-16T06:52:40Z</date>
<date>2020-04-17T07:32:07Z</date>
<key>LastContactClassificationKey</key>
<date>2020-04-16T06:52:42Z</date>
<date>2020-04-17T07:32:12Z</date>
</dict>
</plist>

View File

@@ -4,32 +4,33 @@ from osxphotos._constants import _UNKNOWN_PERSON
PHOTOS_DB = "./tests/Test-10.14.6.photoslibrary/database/photos.db"
# TOP_LEVEL_FOLDERS = ["Folder1"]
TOP_LEVEL_FOLDERS = ["Folder1"]
# TOP_LEVEL_CHILDREN = ["SubFolder1", "SubFolder2"]
TOP_LEVEL_CHILDREN = ["SubFolder1", "SubFolder2"]
# FOLDER_ALBUM_DICT = {"Folder1": [], "SubFolder1": [], "SubFolder2": ["AlbumInFolder"]}
FOLDER_ALBUM_DICT = {"Folder1": [], "SubFolder1": [], "SubFolder2": ["AlbumInFolder"]}
# ALBUM_NAMES = ["Pumpkin Farm", "AlbumInFolder", "Test Album", "Test Album"]
ALBUM_NAMES = ["Pumpkin Farm", "Test Album", "Test Album (1)"]
ALBUM_NAMES = ["Pumpkin Farm", "AlbumInFolder", "Test Album", "Test Album (1)"]
# ALBUM_PARENT_DICT = {
# "Pumpkin Farm": None,
# "AlbumInFolder": "SubFolder2",
# "Test Album": None,
# }
ALBUM_PARENT_DICT = {
"Pumpkin Farm": None,
"AlbumInFolder": "SubFolder2",
"Test Album": None,
"Test Album (1)": None,
}
# ALBUM_FOLDER_NAMES_DICT = {
# "Pumpkin Farm": [],
# "AlbumInFolder": ["Folder1", "SubFolder2"],
# "Test Album": [],
# }
ALBUM_FOLDER_NAMES_DICT = {
"Pumpkin Farm": [],
"AlbumInFolder": ["Folder1", "SubFolder2"],
"Test Album": [],
"Test Album (1)": [],
}
ALBUM_LEN_DICT = {
"Pumpkin Farm": 3,
"Test Album": 1,
"Test Album (1)": 1,
# "AlbumInFolder": 2,
"AlbumInFolder": 1,
}
ALBUM_PHOTO_UUID_DICT = {
@@ -40,10 +41,7 @@ ALBUM_PHOTO_UUID_DICT = {
],
"Test Album": ["8SOE9s0XQVGsuq4ONohTng"],
"Test Album (1)": ["15uNd7%8RguTEgNPKHfTWw"],
# "AlbumInFolder": [
# "3DD2C897-F19E-4CA6-8C22-B027D5A71907",
# "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51",
# ],
"AlbumInFolder": ["15uNd7%8RguTEgNPKHfTWw"],
}
UUID_DICT = {"two_albums": "8SOE9s0XQVGsuq4ONohTng"}
@@ -51,62 +49,57 @@ UUID_DICT = {"two_albums": "8SOE9s0XQVGsuq4ONohTng"}
######### Test FolderInfo ##########
def test_folders_1(caplog):
def test_folders_1():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
folders = photosdb.folders
assert folders == []
assert "Folders not yet implemented for this DB version" in caplog.text
# # top level folders
# folders = photosdb.folders
# assert len(folders) == 1
# top level folders
folders = photosdb.folder_info
assert len(folders) == 1
# # check folder names
# folder_names = [f.title for f in folders]
# assert sorted(folder_names) == sorted(TOP_LEVEL_FOLDERS)
# check folder names
folder_names = [f.title for f in folders]
assert sorted(folder_names) == sorted(TOP_LEVEL_FOLDERS)
def test_folder_names(caplog):
def test_folder_names():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
# check folder names
folder_names = photosdb.folders
assert folder_names == []
assert "Folders not yet implemented for this DB version" in caplog.text
# assert sorted(folder_names) == sorted(TOP_LEVEL_FOLDERS)
assert folder_names == TOP_LEVEL_FOLDERS
assert sorted(folder_names) == sorted(TOP_LEVEL_FOLDERS)
@pytest.mark.skip(reason="Folders not yet impleted in Photos < 5")
def test_folders_len():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
# top level folders
folders = photosdb.folders
folders = photosdb.folder_info
assert len(folders[0]) == len(TOP_LEVEL_CHILDREN)
@pytest.mark.skip(reason="Folders not yet impleted in Photos < 5")
def test_folders_children():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
# top level folders
folders = photosdb.folders
folders = photosdb.folder_info
# children of top level folder
children = folders[0].folders
children = folders[0].subfolders
children_names = [f.title for f in children]
assert sorted(children_names) == sorted(TOP_LEVEL_CHILDREN)
for child in folders[0].folders:
for child in folders[0].subfolders:
# check valid children FolderInfo
assert child.parent
assert child.parent.uuid == folders[0].uuid
@@ -116,38 +109,36 @@ def test_folders_children():
assert sorted(folder_names) == sorted(TOP_LEVEL_FOLDERS)
@pytest.mark.skip(reason="Folders not yet impleted in Photos < 5")
def test_folders_parent():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
# top level folders
folders = photosdb.folders
folders = photosdb.folder_info
# parent of top level folder should be none
for folder in folders:
assert folder.parent is None
for child in folder.folders:
for child in folder.subfolders:
# children's parent uuid should match folder uuid
assert child.parent
assert child.parent.uuid == folder.uuid
@pytest.mark.skip(reason="Folders not yet impleted in Photos < 5")
def test_folders_albums():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
# top level folders
folders = photosdb.folders
folders = photosdb.folder_info
for folder in folders:
name = folder.title
albums = [a.title for a in folder.album_info]
assert sorted(albums) == sorted(FOLDER_ALBUM_DICT[name])
for child in folder.folders:
for child in folder.subfolders:
name = child.title
albums = [a.title for a in child.album_info]
assert sorted(albums) == sorted(FOLDER_ALBUM_DICT[name])
@@ -162,14 +153,14 @@ def test_albums_1():
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
albums = photosdb.album_info
assert len(albums) == 3
assert len(albums) == 4
# check names
album_names = [a.title for a in albums]
assert sorted(album_names) == sorted(ALBUM_NAMES)
def test_albums_parent(caplog):
def test_albums_parent():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
@@ -178,11 +169,10 @@ def test_albums_parent(caplog):
for album in albums:
parent = album.parent.title if album.parent else None
assert "Folders not yet implemented for this DB version" in caplog.text
# assert parent == ALBUM_PARENT_DICT[album.title]
assert parent == ALBUM_PARENT_DICT[album.title]
def test_albums_folder_names(caplog):
def test_albums_folder_names():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
@@ -191,11 +181,10 @@ def test_albums_folder_names(caplog):
for album in albums:
folder_names = album.folder_names
assert "Folders not yet implemented for this DB version" in caplog.text
# assert folder_names == ALBUM_FOLDER_NAMES_DICT[album.title]
assert folder_names == ALBUM_FOLDER_NAMES_DICT[album.title]
def test_albums_folders(caplog):
def test_albums_folders():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
@@ -204,9 +193,8 @@ def test_albums_folders(caplog):
for album in albums:
folders = album.folder_list
assert "Folders not yet implemented for this DB version" in caplog.text
# folder_names = [f.title for f in folders]
# assert folder_names == ALBUM_FOLDER_NAMES_DICT[album.title]
folder_names = [f.title for f in folders]
assert folder_names == ALBUM_FOLDER_NAMES_DICT[album.title]
def test_albums_len():

View File

@@ -780,7 +780,7 @@ def test_no_folder_2_15():
assert item["albums"] == ["AlbumInFolder"]
def test_no_folder_1_14(caplog):
def test_no_folder_1_14():
# test --folder on 10.14
import json
import os
@@ -797,6 +797,5 @@ def test_no_folder_1_14(caplog):
)
assert result.exit_code == 0
json_got = json.loads(result.output)
assert len(json_got) == 0 # single element
assert "not yet implemented" in caplog.text
assert len(json_got) == 1 # single element
assert json_got[0]["uuid"] == "15uNd7%8RguTEgNPKHfTWw"

View File

@@ -18,7 +18,7 @@ KEYWORDS = [
"United Kingdom",
]
PERSONS = ["Katie", "Suzy", "Maria"]
ALBUMS = ["Pumpkin Farm", "Test Album", "Test Album (1)"]
ALBUMS = ["Pumpkin Farm", "AlbumInFolder", "Test Album", "Test Album (1)"]
KEYWORDS_DICT = {
"Kids": 4,
"wedding": 2,
@@ -31,7 +31,12 @@ KEYWORDS_DICT = {
"United Kingdom": 1,
}
PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 1}
ALBUM_DICT = {"Pumpkin Farm": 3, "Test Album": 1, "Test Album (1)": 1}
ALBUM_DICT = {
"Pumpkin Farm": 3,
"AlbumInFolder": 1,
"Test Album": 1,
"Test Album (1)": 1,
}
UUID_DICT = {
"favorite": "6bxcNnzRQKGnK4uPrCJ9UQ",
@@ -131,7 +136,9 @@ def test_attributes():
)
assert p.description == "Girl holding pumpkin"
assert p.title == "I found one!"
assert p.albums == ["Pumpkin Farm", "Test Album (1)"]
assert sorted(p.albums) == sorted(
["Pumpkin Farm", "AlbumInFolder", "Test Album (1)"]
)
assert p.persons == ["Katie"]
assert p.path.endswith(
"/tests/Test-10.14.6.photoslibrary/Masters/2019/07/27/20190727-131650/Pumkins2.jpg"

View File

@@ -7,8 +7,13 @@ PHOTOS_DB = "./tests/Test-10.14.6.photoslibrary/database/photos.db"
PHOTOS_DB_PATH = "/Test-10.14.6.photoslibrary/database/photos.db"
PHOTOS_LIBRARY_PATH = "/Test-10.14.6.photoslibrary"
ALBUMS = ["Pumpkin Farm", "Test Album", "Test Album (1)"]
ALBUM_DICT = {"Pumpkin Farm": 3, "Test Album": 1, "Test Album (1)": 1}
ALBUMS = ["Pumpkin Farm", "AlbumInFolder", "Test Album", "Test Album (1)"]
ALBUM_DICT = {
"Pumpkin Farm": 3,
"AlbumInFolder": 1,
"Test Album": 1,
"Test Album (1)": 1,
}
def test_album_names():

View File

@@ -15,7 +15,7 @@ UUID_DICT = {
"0_2_0": "6191423D-8DB8-4D4C-92BE-9BBBA308AAC4",
"folder_album_1": "3DD2C897-F19E-4CA6-8C22-B027D5A71907",
"folder_album_no_folder": "D79B8D77-BFFC-460B-9312-034F2877D35B",
"mojave_no_folder": "15uNd7%8RguTEgNPKHfTWw",
"mojave_album_1": "15uNd7%8RguTEgNPKHfTWw",
}
TEMPLATE_VALUES = {
@@ -341,17 +341,16 @@ def test_subst_multi_folder_albums_2():
def test_subst_multi_folder_albums_3(caplog):
""" Test substitutions for folder_album on < Photos 5 (not implemented) """
""" Test substitutions for folder_album on < Photos 5 """
import osxphotos
from osxphotos.template import render_filepath_template
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_14_6)
# photo in an album in a folder
photo = photosdb.photos(uuid=[UUID_DICT["mojave_no_folder"]])[0]
photo = photosdb.photos(uuid=[UUID_DICT["mojave_album_1"]])[0]
template = "{folder_album}"
expected = ["Pumpkin Farm", "Test Album (1)"]
expected = ["Folder1/SubFolder2/AlbumInFolder", "Pumpkin Farm", "Test Album (1)"]
rendered, unknown = render_filepath_template(template, photo)
assert sorted(rendered) == sorted(expected)
assert unknown == []
assert "not yet implemented" in caplog.text