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). 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) #### [v0.27.4](https://github.com/RhetTbull/osxphotos/compare/v0.27.3...v0.27.4)
> 12 April 2020 > 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) - 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) - 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) #### [v0.15.1](https://github.com/RhetTbull/osxphotos/compare/v0.14.21...v0.15.1)
> 13 April 2020
#### [v0.15.0](https://github.com/RhetTbull/osxphotos/compare/v0.14.21...v0.15.0)
> 14 December 2019 > 14 December 2019

View File

@@ -17,8 +17,8 @@ _TESTED_DB_VERSIONS = ["6000", "4025", "4016", "3301", "2622"]
_PHOTOS_3_VERSION = "3301" _PHOTOS_3_VERSION = "3301"
# versions 5.0 and later have a different database structure # versions 5.0 and later have a different database structure
_PHOTOS_4_VERSION = "4025" # latest Mojove version on 10.14.6 _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_5_VERSION = "6000" # seems to be current on 10.15.1 through 10.15.4
# which major version operating systems have been tested # which major version operating systems have been tested
_TESTED_OS_VERSIONS = ["12", "13", "14", "15"] _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_SHARED_ALBUM_KIND = 1505 # shared album
_PHOTOS_5_FOLDER_KIND = 4000 # user folder _PHOTOS_5_FOLDER_KIND = 4000 # user folder
_PHOTOS_5_ROOT_FOLDER_KIND = 3999 # root 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 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 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: class AlbumInfo:
@@ -53,14 +59,13 @@ class AlbumInfo:
["Top level folder", "sub folder 1", "sub folder 2", ...] ["Top level folder", "sub folder 1", "sub folder 2", ...]
returns empty list if album is not in any folders """ 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: try:
return self._folder_names return self._folder_names
except AttributeError: 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 return self._folder_names
@property @property
@@ -70,10 +75,6 @@ class AlbumInfo:
["Top level folder", "sub folder 1", "sub folder 2", ...] ["Top level folder", "sub folder 1", "sub folder 2", ...]
returns empty list if album is not in any folders """ 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: try:
return self._folders return self._folders
except AttributeError: except AttributeError:
@@ -83,19 +84,23 @@ class AlbumInfo:
@property @property
def parent(self): def parent(self):
""" returns FolderInfo object for parent folder or None if no parent (e.g. top-level album) """ """ 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: try:
return self._parent return self._parent
except AttributeError: except AttributeError:
parent_pk = self._db._dbalbum_details[self._uuid]["parentfolder"] if self._db._db_version <= _PHOTOS_4_VERSION:
self._parent = ( parent_uuid = self._db._dbalbum_details[self._uuid]["folderUuid"]
FolderInfo(db=self._db, uuid=self._db._dbalbums_pk[parent_pk]) self._parent = (
if parent_pk != self._db._folder_root_pk FolderInfo(db=self._db, uuid=parent_uuid)
else None 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 return self._parent
def __len__(self): def __len__(self):
@@ -112,8 +117,12 @@ class FolderInfo:
def __init__(self, db=None, uuid=None): def __init__(self, db=None, uuid=None):
self._uuid = uuid self._uuid = uuid
self._db = db self._db = db
self._pk = self._db._dbalbum_details[uuid]["pk"] if self._db._db_version <= _PHOTOS_4_VERSION:
self._title = self._db._dbalbum_details[uuid]["title"] 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 @property
def title(self): def title(self):
@@ -131,13 +140,22 @@ class FolderInfo:
try: try:
return self._albums return self._albums
except AttributeError: except AttributeError:
albums = [ if self._db._db_version <= _PHOTOS_4_VERSION:
AlbumInfo(db=self._db, uuid=album) albums = [
for album, detail in self._db._dbalbum_details.items() AlbumInfo(db=self._db, uuid=album)
if not detail["intrash"] for album, detail in self._db._dbalbum_details.items()
and detail["kind"] == _PHOTOS_5_ALBUM_KIND if not detail["intrash"]
and detail["parentfolder"] == self._pk 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 self._albums = albums
return self._albums return self._albums
@@ -147,12 +165,20 @@ class FolderInfo:
try: try:
return self._parent return self._parent
except AttributeError: except AttributeError:
parent_pk = self._db._dbalbum_details[self._uuid]["parentfolder"] if self._db._db_version <= _PHOTOS_4_VERSION:
self._parent = ( parent_uuid = self._db._dbfolder_details[self._uuid]["parentFolderUuid"]
FolderInfo(db=self._db, uuid=self._db._dbalbums_pk[parent_pk]) self._parent = (
if parent_pk != self._db._folder_root_pk FolderInfo(db=self._db, uuid=parent_uuid)
else None 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 return self._parent
@property @property
@@ -161,13 +187,22 @@ class FolderInfo:
try: try:
return self._folders return self._folders
except AttributeError: except AttributeError:
folders = [ if self._db._db_version <= _PHOTOS_4_VERSION:
FolderInfo(db=self._db, uuid=album) folders = [
for album, detail in self._db._dbalbum_details.items() FolderInfo(db=self._db, uuid=folder)
if not detail["intrash"] for folder, detail in self._db._dbfolder_details.items()
and detail["kind"] == _PHOTOS_5_FOLDER_KIND if not detail["intrash"]
and detail["parentfolder"] == self._pk 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 self._folders = folders
return self._folders return self._folders

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,7 +15,7 @@ UUID_DICT = {
"0_2_0": "6191423D-8DB8-4D4C-92BE-9BBBA308AAC4", "0_2_0": "6191423D-8DB8-4D4C-92BE-9BBBA308AAC4",
"folder_album_1": "3DD2C897-F19E-4CA6-8C22-B027D5A71907", "folder_album_1": "3DD2C897-F19E-4CA6-8C22-B027D5A71907",
"folder_album_no_folder": "D79B8D77-BFFC-460B-9312-034F2877D35B", "folder_album_no_folder": "D79B8D77-BFFC-460B-9312-034F2877D35B",
"mojave_no_folder": "15uNd7%8RguTEgNPKHfTWw", "mojave_album_1": "15uNd7%8RguTEgNPKHfTWw",
} }
TEMPLATE_VALUES = { TEMPLATE_VALUES = {
@@ -341,17 +341,16 @@ def test_subst_multi_folder_albums_2():
def test_subst_multi_folder_albums_3(caplog): 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 import osxphotos
from osxphotos.template import render_filepath_template from osxphotos.template import render_filepath_template
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_14_6) photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_14_6)
# photo in an album in a folder # 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}" 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) rendered, unknown = render_filepath_template(template, photo)
assert sorted(rendered) == sorted(expected) assert sorted(rendered) == sorted(expected)
assert unknown == [] assert unknown == []
assert "not yet implemented" in caplog.text