Compare commits

..

13 Commits

Author SHA1 Message Date
Rhet Turnbull
8544667c72 Update README.md TOC 2020-04-11 14:20:05 -07:00
Rhet Turnbull
d6a22b765a Added tests and README for AlbumInfo and FolderInfo 2020-04-11 14:07:39 -07:00
Rhet Turnbull
96365728c2 Added albuminfo.py for AlbumInfo and FolderInfo classes 2020-04-11 10:28:50 -07:00
Rhet Turnbull
c01f713f00 Merge pull request #95 from jystervinou/patch-2
Update README.md
2020-04-11 06:53:58 -07:00
Jean-Yves Stervinou
1aa3838c38 Update README.md
just some typos fixes
- packge/package
- the copy then read can take => the copy read then can take
2020-04-11 11:34:17 +02:00
Rhet Turnbull
cde56e9d13 Updated CHANGELOG.md 2020-04-10 18:56:22 -07:00
Rhet Turnbull
1c9da5ed6f Bug fix for PhotosDB.photos() query 2020-04-10 18:50:58 -07:00
Rhet Turnbull
d74f7f499b Updated test library 2020-04-10 17:58:45 -07:00
Rhet Turnbull
c85bb02304 Updated CHANGELOG.md 2020-04-10 17:35:23 -07:00
Rhet Turnbull
3e5062684a Changed PhotosDB albums interface as prep for adding folders 2020-04-10 17:30:37 -07:00
Rhet Turnbull
626e460aab Update README.md 2020-04-06 07:24:20 -07:00
Rhet Turnbull
1820715849 Added test for 10.15.4 2020-04-05 22:57:05 -07:00
Rhet Turnbull
a6ca3f453c Updated CHANGELOG.md 2020-04-05 09:17:27 -07:00
206 changed files with 2671 additions and 96 deletions

View File

@@ -4,6 +4,41 @@ 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.26.1](https://github.com/RhetTbull/osxphotos/compare/v0.26.0...v0.26.1)
> 11 April 2020
- Bug fix for PhotosDB.photos() query [`1c9da5e`](https://github.com/RhetTbull/osxphotos/commit/1c9da5ed6ffa21f0577906b65b7da08951725d1f)
- Updated test library [`d74f7f4`](https://github.com/RhetTbull/osxphotos/commit/d74f7f499bf59f37ec81cfa9d49cbbf3aafb5961)
- Updated CHANGELOG.md [`c85bb02`](https://github.com/RhetTbull/osxphotos/commit/c85bb023042e072d6688060eb259156c2fa579b9)
#### [v0.26.0](https://github.com/RhetTbull/osxphotos/compare/v0.25.1...v0.26.0)
> 11 April 2020
- Added test for 10.15.4 [`1820715`](https://github.com/RhetTbull/osxphotos/commit/182071584904d001a9b199eef5febfb79e00696e)
- Changed PhotosDB albums interface as prep for adding folders [`3e50626`](https://github.com/RhetTbull/osxphotos/commit/3e5062684ab6d706d91d4abeb4e3b0ca47867b70)
- Updated CHANGELOG.md [`a6ca3f4`](https://github.com/RhetTbull/osxphotos/commit/a6ca3f453ce0fae4e8d13c7c256ed69a16d2e3f2)
#### [v0.25.1](https://github.com/RhetTbull/osxphotos/compare/v0.25.0...v0.25.1)
> 5 April 2020
- Added --no-extended-attributes option to CLI, closes #85 [`#85`](https://github.com/RhetTbull/osxphotos/issues/85)
- Fixed CLI help for invalid topic, closes #76 [`#76`](https://github.com/RhetTbull/osxphotos/issues/76)
- Updated test library [`bae0283`](https://github.com/RhetTbull/osxphotos/commit/bae0283441f04d71aa78dbd1cf014f376ef1f91a)
#### [v0.25.0](https://github.com/RhetTbull/osxphotos/compare/v0.24.2...v0.25.0)
> 4 April 2020
- Added places, --place, --no-place to CLI, closes #87, #88 [`#87`](https://github.com/RhetTbull/osxphotos/issues/87)
- Updated render_filepath_template to support multiple values [`6a89888`](https://github.com/RhetTbull/osxphotos/commit/6a898886ddadc9d5bc9dbad6ee7365270dd0a26d)
- Added {album}, {keyword}, and {person} to template system [`507c4a3`](https://github.com/RhetTbull/osxphotos/commit/507c4a374014f999ca19789bce0df0c14332e021)
- Added places command to CLI [`fd5e748`](https://github.com/RhetTbull/osxphotos/commit/fd5e748dca759ea1c3a7329d447f363afe8418b7)
- Updated export example [`01cd7fe`](https://github.com/RhetTbull/osxphotos/commit/01cd7fed6d7fc0c61c171a05319c211eb0a9f7c1)
- Updated CHANGELOG.md [`daea30f`](https://github.com/RhetTbull/osxphotos/commit/daea30f1626a208209ab6854cbd3b12f4b0a3405)
#### [v0.24.2](https://github.com/RhetTbull/osxphotos/compare/v0.24.1...v0.24.2)
> 28 March 2020

125
README.md
View File

@@ -13,6 +13,8 @@
* [Package Interface](#package-interface)
+ [PhotosDB](#photosdb)
+ [PhotoInfo](#photoinfo)
+ [AlbumInfo](#albuminfo)
+ [FolderInfo](#folderinfo)
+ [PlaceInfo](#placeinfo)
+ [Template Functions](#template-functions)
+ [Utility Functions](#utility-functions)
@@ -368,7 +370,7 @@ def main():
photosdb = osxphotos.PhotosDB(db)
print(photosdb.keywords)
print(photosdb.persons)
print(photosdb.albums)
print(photosdb.album_names)
print(photosdb.keywords_as_dict)
print(photosdb.persons_as_dict)
@@ -560,16 +562,44 @@ Returns a list of the keywords found in the Photos library
albums = photosdb.albums
```
Returns a list of the albums found in the Photos library.
Returns a list of [AlbumInfo](#AlbumInfo) objects representing albums in the database or empty list if there are no albums. See also [album_names](#album_names).
#### `album_names`
```python
# assumes photosdb is a PhotosDB object (see above)
album_names = photosdb.album_names
```
Returns a list of the album names found in the Photos library.
**Note**: In Photos 5.0 (MacOS 10.15/Catalina), It is possible to have more than one album with the same name in Photos. Albums with duplicate names are treated as a single album and the photos in each are combined. For example, if you have two albums named "Wedding" and each has 2 photos, osxphotos will treat this as a single album named "Wedding" with 4 photos in it.
#### `albums_shared`
#### `album_names_shared`
Returns list of shared albums found in photos database (e.g. albums shared via iCloud photo sharing)
Returns list of shared album names found in photos database (e.g. albums shared via iCloud photo sharing)
**Note**: *Only valid for Photos 5 / MacOS 10.15*; on Photos <= 4, prints warning and returns empty list.
#### `folders`
```python
# assumes photosdb is a PhotosDB object (see above)
folders = photosdb.folders
```
Returns a list of [FolderInfo](#FolderInfo) objects representing top level folders in the database or empty list if there are no folders. See also [folder_names](#folder_names).
**Note**: Currently folders is only implemented for Photos 5 (Catalina); will return empty list and output warning if called on earlier database versions.
#### `folder_names`
```python
# assumes photosdb is a PhotosDB object (see above)
folder_names = photosdb.folder_names
```
Returns a list names of top level folder names in the database.
**Note**: Currently folders is only implemented for Photos 5 (Catalina); will return empty list and output warning if called on earlier database versions.
#### `persons`
```python
# assumes photosdb is a PhotosDB object (see above)
@@ -928,6 +958,82 @@ If overwrite=False and increment=False, export will fail if destination file alr
**Implementation Note**: Because the usual python file copy methods don't preserve all the metadata available on MacOS, export uses /usr/bin/ditto to do the copy for export. ditto preserves most metadata such as extended attributes, permissions, ACLs, etc.
### AlbumInfo
PhotosDB.albums returns a list of AlbumInfo objects. Each AlbumInfo object represents a single album in the Photos library.
#### `uuid`
Returns the universally unique identifier (uuid) of the album. This is how Photos keeps track of individual objects within the database.
#### `title`
Returns the title or name of the album.
#### `photos`
Returns a list of [PhotoInfo](#PhotoInfo) objects representing each photo contained in the album.
#### `folder_list`
Returns a hierarchical list of [FolderInfo](#FolderInfo) objects representing the folders the album is contained in. For example, if album "AlbumInFolder" is in SubFolder1 of Folder1 as illustrated below, would return a list of `FolderInfo` objects representing ["Folder1", "SubFolder2"]
```txt
Photos Library
├── Folder1
    ├── SubFolder1
  ├── SubFolder2
     └── AlbumInFolder
```
#### `folder_names`
Returns a hierarchical list of names of the folders the album is contained in. For example, if album is in SubFolder2 of Folder1 as illustrated below, would return ["Folder1", "SubFolder2"].
```txt
Photos Library
├── Folder1
    ├── SubFolder1
  ├── SubFolder2
     └── AlbumInFolder
```
#### `parent`
Returns a [FolderInfo](#FolderInfo) object representing the albums parent folder or `None` if album is not a in a folder.
### FolderInfo
PhotosDB.folders returns a list of FolderInfo objects representing the top level folders in the library. Each FolderInfo object represents a single folder in the Photos library.
#### `uuid`
Returns the universally unique identifier (uuid) of the folder. This is how Photos keeps track of individual objects within the database.
#### `title`
Returns the title or name of the folder.
#### `albums`
Returns a list of [AlbumInfo](#AlbumInfo) objects representing each album contained in the folder.
#### `folders`
Returns a list of [FolderInfo](#FolderInfo) objects representing the sub-folders of the folder.
#### `parent`
Returns a [FolderInfo](#FolderInfo) object representing the folder's parent folder or `None` if album is not a in a folder.
**Note**: FolderInfo and AlbumInfo objects effectively work as a linked list. The children of a folder are contained in `folders` and `albums` and the parent object of both `AlbumInfo` and `FolderInfo` is represented by `parent`. For example:
```python
>>> import osxphotos
>>> photosdb = osxphotos.PhotosDB()
>>> photosdb.folders
[<osxphotos.albuminfo.FolderInfo object at 0x10fcc0160>]
>>> photosdb.folders[0].title
'Folder1'
>>> photosdb.folders[0].folders[1].title
'SubFolder2'
>>> photosdb.folders[0].folders[1].albums[0].title
'AlbumInFolder'
>>> photosdb.folders[0].folders[1].albums[0].parent.title
'SubFolder2'
>>> photosdb.folders[0].folders[1].albums[0].parent.albums[0].title
'AlbumInFolder'
```
### PlaceInfo
[PhotoInfo.place](#place) returns a PlaceInfo object if the photo contains valid reverse geolocation information. PlaceInfo has the following properties.
@@ -1009,11 +1115,14 @@ Render template string for photo. none_str is used if template substitution res
Returns a tuple of (rendered, unmatched) where rendered is a list of rendered strings with all substitutions made and unmatched is a list of any strings that resembled a template substitution but did not match a known substitution. E.g. if template contained "{foo}", unmatched would be ["foo"].
e.g. `render_filepath_template("{created.year}/{foo}", photo)` would return `("2020/{foo}",["foo"])`
e.g. `render_filepath_template("{created.year}/{foo}", photo)` would return `(["2020/{foo}"],["foo"])`
If you want to include "{" or "}" in the output, use "{{" or "}}"
e.g. `render_filepath_template("{created.year}/{{foo}}", photo)` would return `("2020/{foo}",[])`
e.g. `render_filepath_template("{created.year}/{{foo}}", photo)` would return `(["2020/{foo}"],[])`
Some substitutions, notably `album`, `keyword`, and `person` could return multiple values, hence a new string will be return for each possible substitution (hence why a list of rendered strings is returned). For example, a photo in 2 albums: 'Vacation' and 'Family' would result in the following rendered values if template was "{created.year}/{album}" and created.year == 2020: `["2020/Vacation","2020/Family"]`
| Substitution | Description |
|--------------|-------------|
@@ -1110,7 +1219,7 @@ def main():
print(photosdb.keywords)
print(photosdb.persons)
print(photosdb.albums)
print(photosdb.album_names)
print(photosdb.keywords_as_dict)
print(photosdb.persons_as_dict)
@@ -1171,7 +1280,7 @@ Testing against "real world" Photos libraries would be especially helpful. If y
## Implementation Notes
This packge works by creating a copy of the sqlite3 database that photos uses to store data about the photos library. the class photosdb then queries this database to extract information about the photos such as persons (faces identified in the photos), albums, keywords, etc. If your library is large, the database can be hundreds of MB in size and the copy then read can take many 10s of seconds to complete. Once copied, the entire database is processed and an in-memory data structure is created meaning all subsequent accesses of the PhotosDB object occur much more quickly.
This package works by creating a copy of the sqlite3 database that photos uses to store data about the photos library. the class photosdb then queries this database to extract information about the photos such as persons (faces identified in the photos), albums, keywords, etc. If your library is large, the database can be hundreds of MB in size and the copy read then can take many 10s of seconds to complete. Once copied, the entire database is processed and an in-memory data structure is created meaning all subsequent accesses of the PhotosDB object occur much more quickly.
If apple changes the database format this will likely break.

View File

@@ -14,7 +14,7 @@ def main():
print(photosdb.keywords)
print(photosdb.persons)
print(photosdb.albums)
print(photosdb.album_names)
print(photosdb.keywords_as_dict)
print(photosdb.persons_as_dict)

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

View File

@@ -1,3 +1,3 @@
""" version info """
__version__ = "0.25.1"
__version__ = "0.27.0"

176
osxphotos/albuminfo.py Normal file
View File

@@ -0,0 +1,176 @@
"""
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
"""
import logging
from ._constants import _PHOTOS_5_ALBUM_KIND, _PHOTOS_5_FOLDER_KIND, _PHOTOS_5_VERSION
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 """
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)
return self._folder_names
@property
def folder_list(self):
""" return hierarchical list of folders the album is contained in
as list of FolderInfo objects in form
["Top level folder", "sub folder 1", "sub folder 2", ...]
returns empty list if album is not in any folders """
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:
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) """
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
)
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,
@@ -120,6 +125,12 @@ class PhotosDB:
# e.g. {'1EB2B765-0765-43BA-A90C-0D0580E6172C': ['0C514A98-7B77-4E4F-801B-364B7B65EAFA']}
self._dbalbums_uuid = {}
# Dict with information about all albums/photos by primary key in the album database
# key is album pk, value is album uuid
# e.g. {'43': '0C514A98-7B77-4E4F-801B-364B7B65EAFA'}
# specific to Photos versions >= 5
self._dbalbums_pk = {}
# Dict with information about all albums/photos by album
# key is album UUID, value is list of photo UUIDs contained in that album
# e.g. {'0C514A98-7B77-4E4F-801B-364B7B65EAFA': ['1EB2B765-0765-43BA-A90C-0D0580E6172C']}
@@ -145,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}")
@@ -264,6 +289,7 @@ class PhotosDB:
k
for k in self._dbalbums_album.keys()
if self._dbalbum_details[k]["cloudownerhashedpersonid"] is None
and self._dbalbum_details[k]["intrash"] == 0
]
for k in album_keys:
title = self._dbalbum_details[k]["title"]
@@ -313,25 +339,88 @@ 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:
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 detail["intrash"] == 0
and detail["kind"] == _PHOTOS_5_FOLDER_KIND
and detail["parentfolder"] == self._folder_root_pk
]
return folders
@property
def folder_names(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 detail["intrash"] == 0
and detail["kind"] == _PHOTOS_5_FOLDER_KIND
and detail["parentfolder"] == self._folder_root_pk
]
return folder_names
@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
and self._dbalbum_details[album]["intrash"] == 0
]
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
and self._dbalbum_details[album]["intrash"] == 0
]
return albums_shared
@property
def album_names(self):
""" return list of albums found in photos database """
# 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
and self._dbalbum_details[album]["intrash"] == 0
}
return list(albums)
@property
def albums_shared(self):
def album_names_shared(self):
""" return list of shared albums found in photos database
only valid for Photos 5; on Photos <= 4, prints warning and returns empty list """
@@ -342,18 +431,16 @@ 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
and self._dbalbum_details[album]["intrash"] == 0
}
return list(albums)
@property
@@ -466,9 +553,9 @@ class PhotosDB:
"uuid, " # 0
"name, " # 1
"cloudLibraryState, " # 2
"cloudIdentifier " # 3
"cloudIdentifier, " # 3
"isInTrash " # 4
"FROM RKAlbum "
"WHERE isInTrash = 0"
)
for album in c:
@@ -476,6 +563,7 @@ class PhotosDB:
"title": album[1],
"cloudlibrarystate": album[2],
"cloudidentifier": album[3],
"intrash": album[4],
"cloudlocalstate": None, # Photos 5
"cloudownerfirstname": None, # Photos 5
"cloudownderlastname": None, # Photos 5
@@ -846,12 +934,10 @@ class PhotosDB:
"FROM RKPlaceForVersion "
f"WHERE versionId = '{self._dbphotos[uuid]['modelID']}'"
)
place_ids = [id[0] for id in place_ids_query.fetchall()]
self._dbphotos[uuid]["placeIDs"] = place_ids
country_code = [
countries[x] for x in place_ids if x in countries
]
self._dbphotos[uuid]["placeIDs"] = place_ids
country_code = [countries[x] for x in place_ids if x in countries]
if len(country_code) > 1:
logging.warning(f"Found more than one country code for uuid: {uuid}")
@@ -860,7 +946,7 @@ class PhotosDB:
else:
self._dbphotos[uuid]["countryCode"] = None
# get the place info that matches the RKPlace modelIDs for this photo
# get the place info that matches the RKPlace modelIDs for this photo
# (place_ids), sort by area (element 3 of the place_data tuple in places)
place_names = [
pname
@@ -986,6 +1072,7 @@ class PhotosDB:
logging.debug(pformat(self._dbfaces_person))
logging.debug(self._dbfaces_uuid)
# get details about albums
c.execute(
"SELECT ZGENERICALBUM.ZUUID, ZGENERICASSET.ZUUID "
"FROM ZGENERICASSET "
@@ -995,12 +1082,15 @@ class PhotosDB:
)
for album in c:
# store by uuid in _dbalbums_uuid and by album in _dbalbums_album
if not album[1] in self._dbalbums_uuid:
self._dbalbums_uuid[album[1]] = []
if not album[0] in self._dbalbums_album:
self._dbalbums_album[album[0]] = []
self._dbalbums_uuid[album[1]].append(album[0])
self._dbalbums_album[album[0]].append(album[1])
try:
self._dbalbums_uuid[album[1]].append(album[0])
except KeyError:
self._dbalbums_uuid[album[1]] = [album[0]]
try:
self._dbalbums_album[album[0]].append(album[1])
except KeyError:
self._dbalbums_album[album[0]] = [album[1]]
# now get additional details about albums
c.execute(
@@ -1010,20 +1100,70 @@ class PhotosDB:
"ZCLOUDLOCALSTATE, " # 2
"ZCLOUDOWNERFIRSTNAME, " # 3
"ZCLOUDOWNERLASTNAME, " # 4
"ZCLOUDOWNERHASHEDPERSONID " # 5
"FROM ZGENERICALBUM"
"ZCLOUDOWNERHASHEDPERSONID, " # 5
"ZKIND, " # 6
"ZPARENTFOLDER, " # 7
"Z_PK, " # 8
"ZTRASHEDSTATE " # 9
"FROM ZGENERICALBUM "
)
for album in c:
self._dbalbum_details[album[0]] = {
"_uuid": album[0],
"title": album[1],
"cloudlocalstate": album[2],
"cloudownerfirstname": album[3],
"cloudownderlastname": album[4],
"cloudownerhashedpersonid": album[5],
"cloudlibrarystate": None, # Photos 4
"cloudidentifier": None, # Photos4
"cloudidentifier": None, # Photos 4
"kind": album[6],
"parentfolder": album[7],
"pk": album[8],
"intrash": album[9],
}
# add cross-reference by pk to uuid
# needed to extract folder hierarchy
# 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))
@@ -1459,6 +1599,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))
@@ -1468,6 +1617,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 """
@@ -2066,37 +2307,45 @@ class PhotosDB:
photos_sets.append(set(self._dbphotos.keys()))
else:
if albums:
album_set = set()
for album in albums:
# TODO: can have >1 album with same name. This globs them together.
# Need a way to select which album?
if album in self._dbalbum_titles:
album_set = set()
title_set = set()
for album_id in self._dbalbum_titles[album]:
album_set.update(self._dbalbums_album[album_id])
photos_sets.append(album_set)
title_set.update(self._dbalbums_album[album_id])
album_set.update(title_set)
else:
logging.debug(f"Could not find album '{album}' in database")
photos_sets.append(album_set)
if uuid:
uuid_set = set()
for u in uuid:
if u in self._dbphotos:
photos_sets.append(set([u]))
uuid_set.update([u])
else:
logging.debug(f"Could not find uuid '{u}' in database")
photos_sets.append(uuid_set)
if keywords:
keyword_set = set()
for keyword in keywords:
if keyword in self._dbkeywords_keyword:
photos_sets.append(set(self._dbkeywords_keyword[keyword]))
keyword_set.update(self._dbkeywords_keyword[keyword])
else:
logging.debug(f"Could not find keyword '{keyword}' in database")
photos_sets.append(keyword_set)
if persons:
person_set = set()
for person in persons:
if person in self._dbfaces_person:
photos_sets.append(set(self._dbfaces_person[person]))
person_set.update(self._dbfaces_person[person])
else:
logging.debug(f"Could not find person '{person}' in database")
photos_sets.append(person_set)
if from_date or to_date:
dsel = self._dbphotos

View File

@@ -7,7 +7,7 @@
<key>hostuuid</key>
<string>9575E48B-8D5F-5654-ABAC-4431B1167324</string>
<key>pid</key>
<integer>1526</integer>
<integer>685</integer>
<key>processname</key>
<string>photolibraryd</string>
<key>uid</key>

View File

@@ -3,24 +3,24 @@
<plist version="1.0">
<dict>
<key>BackgroundHighlightCollection</key>
<date>2020-04-04T17:34:35Z</date>
<date>2020-04-11T13:55:22Z</date>
<key>BackgroundHighlightEnrichment</key>
<date>2020-04-04T17:34:34Z</date>
<date>2020-04-11T13:55:21Z</date>
<key>BackgroundJobAssetRevGeocode</key>
<date>2020-04-04T20:16:33Z</date>
<date>2020-04-11T16:03:49Z</date>
<key>BackgroundJobSearch</key>
<date>2020-04-04T17:34:35Z</date>
<date>2020-04-11T13:55:22Z</date>
<key>BackgroundPeopleSuggestion</key>
<date>2020-04-04T17:34:34Z</date>
<date>2020-04-11T13:55:21Z</date>
<key>BackgroundUserBehaviorProcessor</key>
<date>2020-04-04T13:56:56Z</date>
<date>2020-04-11T06:27:26Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphConsistencyUpdateJobDateKey</key>
<date>2020-04-04T20:16:40Z</date>
<date>2020-04-11T16:17:41Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
<date>2020-04-04T13:56:49Z</date>
<date>2020-04-11T06:27:24Z</date>
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
<date>2020-04-04T20:16:33Z</date>
<date>2020-04-11T16:03:50Z</date>
<key>SiriPortraitDonation</key>
<date>2020-04-04T13:56:56Z</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-04T13:56:59Z</date>
<date>2020-04-11T06:27:27Z</date>
<key>LastContactClassificationKey</key>
<date>2020-04-04T13:57:02Z</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>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LibrarySchemaVersion</key>
<integer>5001</integer>
<key>MetaSchemaVersion</key>
<integer>3</integer>
</dict>
</plist>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>hostname</key>
<string>Rhets-MacBook-Pro.local</string>
<key>hostuuid</key>
<string>9575E48B-8D5F-5654-ABAC-4431B1167324</string>
<key>pid</key>
<integer>3212</integer>
<key>processname</key>
<string>photolibraryd</string>
<key>uid</key>
<integer>501</integer>
</dict>
</plist>

Binary file not shown.

View File

@@ -0,0 +1,188 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BlacklistedMeaningsByMeaning</key>
<dict/>
<key>MePersonUUID</key>
<string>39488755-78C0-40B2-B378-EDA280E1823C</string>
<key>SceneWhitelist</key>
<array>
<string>Graduation</string>
<string>Aquarium</string>
<string>Food</string>
<string>Ice Skating</string>
<string>Mountain</string>
<string>Cliff</string>
<string>Basketball</string>
<string>Tennis</string>
<string>Jewelry</string>
<string>Cheese</string>
<string>Softball</string>
<string>Football</string>
<string>Circus</string>
<string>Jet Ski</string>
<string>Playground</string>
<string>Carousel</string>
<string>Paint Ball</string>
<string>Windsurfing</string>
<string>Sailboat</string>
<string>Sunbathing</string>
<string>Dam</string>
<string>Fireplace</string>
<string>Flower</string>
<string>Scuba</string>
<string>Hiking</string>
<string>Cetacean</string>
<string>Pier</string>
<string>Bowling</string>
<string>Snowboarding</string>
<string>Zoo</string>
<string>Snowmobile</string>
<string>Theater</string>
<string>Boat</string>
<string>Casino</string>
<string>Car</string>
<string>Diving</string>
<string>Cycling</string>
<string>Musical Instrument</string>
<string>Board Game</string>
<string>Castle</string>
<string>Sunset Sunrise</string>
<string>Martial Arts</string>
<string>Motocross</string>
<string>Submarine</string>
<string>Cat</string>
<string>Snow</string>
<string>Kiteboarding</string>
<string>Squash</string>
<string>Geyser</string>
<string>Music</string>
<string>Archery</string>
<string>Desert</string>
<string>Blackjack</string>
<string>Fireworks</string>
<string>Sportscar</string>
<string>Feline</string>
<string>Soccer</string>
<string>Museum</string>
<string>Baby</string>
<string>Fencing</string>
<string>Railroad</string>
<string>Nascar</string>
<string>Sky Surfing</string>
<string>Bird</string>
<string>Games</string>
<string>Baseball</string>
<string>Dressage</string>
<string>Snorkeling</string>
<string>Pyramid</string>
<string>Kite</string>
<string>Rowboat</string>
<string>Golf</string>
<string>Watersports</string>
<string>Lightning</string>
<string>Canyon</string>
<string>Auditorium</string>
<string>Night Sky</string>
<string>Karaoke</string>
<string>Skiing</string>
<string>Parade</string>
<string>Forest</string>
<string>Hot Air Balloon</string>
<string>Dragon Parade</string>
<string>Easter Egg</string>
<string>Monument</string>
<string>Jungle</string>
<string>Thanksgiving</string>
<string>Jockey Horse</string>
<string>Stadium</string>
<string>Airplane</string>
<string>Ballet</string>
<string>Yoga</string>
<string>Coral Reef</string>
<string>Skating</string>
<string>Wrestling</string>
<string>Bicycle</string>
<string>Tattoo</string>
<string>Amusement Park</string>
<string>Canoe</string>
<string>Cheerleading</string>
<string>Ping Pong</string>
<string>Fishing</string>
<string>Magic</string>
<string>Reptile</string>
<string>Winter Sport</string>
<string>Waterfall</string>
<string>Train</string>
<string>Bonsai</string>
<string>Surfing</string>
<string>Dog</string>
<string>Cake</string>
<string>Sledding</string>
<string>Sandcastle</string>
<string>Glacier</string>
<string>Lighthouse</string>
<string>Equestrian</string>
<string>Rafting</string>
<string>Shore</string>
<string>Hockey</string>
<string>Santa Claus</string>
<string>Formula One Car</string>
<string>Sport</string>
<string>Vehicle</string>
<string>Boxing</string>
<string>Rollerskating</string>
<string>Underwater</string>
<string>Orchestra</string>
<string>Carnival</string>
<string>Rocket</string>
<string>Skateboarding</string>
<string>Helicopter</string>
<string>Performance</string>
<string>Oktoberfest</string>
<string>Water Polo</string>
<string>Skate Park</string>
<string>Animal</string>
<string>Nightclub</string>
<string>String Instrument</string>
<string>Dinosaur</string>
<string>Gymnastics</string>
<string>Cricket</string>
<string>Volcano</string>
<string>Lake</string>
<string>Aurora</string>
<string>Dancing</string>
<string>Concert</string>
<string>Rock Climbing</string>
<string>Hang Glider</string>
<string>Rodeo</string>
<string>Fish</string>
<string>Art</string>
<string>Motorcycle</string>
<string>Volleyball</string>
<string>Wake Boarding</string>
<string>Badminton</string>
<string>Motor Sport</string>
<string>Sumo</string>
<string>Parasailing</string>
<string>Skydiving</string>
<string>Kickboxing</string>
<string>Pinata</string>
<string>Foosball</string>
<string>Go Kart</string>
<string>Poker</string>
<string>Kayak</string>
<string>Swimming</string>
<string>Atv</string>
<string>Beach</string>
<string>Dartboard</string>
<string>Athletics</string>
<string>Camping</string>
<string>Tornado</string>
<string>Billiards</string>
<string>Rugby</string>
<string>Airshow</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>insertAlbum</key>
<array/>
<key>insertAsset</key>
<array/>
<key>insertHighlight</key>
<array/>
<key>insertMemory</key>
<array/>
<key>insertMoment</key>
<array/>
<key>removeAlbum</key>
<array/>
<key>removeAsset</key>
<array/>
<key>removeHighlight</key>
<array/>
<key>removeMemory</key>
<array/>
<key>removeMoment</key>
<array/>
</dict>
</plist>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>embeddingVersion</key>
<string>1</string>
<key>localeIdentifier</key>
<string>en_US</string>
<key>sceneTaxonomySHA</key>
<string>87914a047c69fbe8013fad2c70fa70c6c03b08b56190fe4054c880e6b9f57cc3</string>
<key>searchIndexVersion</key>
<string>10</string>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 KiB

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>MigrationService</key>
<dict>
<key>State</key>
<integer>4</integer>
</dict>
<key>MigrationService.LastCompletedTask</key>
<integer>12</integer>
<key>MigrationService.ValidationCounts</key>
<dict>
<key>MigrationDetectedFaceprint</key>
<integer>6</integer>
<key>MigrationManagedAsset</key>
<integer>0</integer>
<key>MigrationSceneClassification</key>
<integer>44</integer>
<key>MigrationUnmanagedAdjustment</key>
<integer>0</integer>
<key>RDVersion.cloudLocalState.CPLIsNotPushed</key>
<integer>7</integer>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CollapsedSidebarSectionIdentifiers</key>
<array/>
<key>ExpandedSidebarItemIdentifiers</key>
<array>
<string>88A5F8B8-5B9A-43C7-BB85-3952B81580EB/L0/020</string>
<string>92D68107-B6C7-453B-96D2-97B0F26D5B8B/L0/020</string>
<string>29EF7A97-7E76-4D5F-A5E0-CC0A93E8524C/L0/020</string>
</array>
<key>Photos</key>
<dict>
<key>CollapsedSidebarSectionIdentifiers</key>
<array/>
<key>ExpandedSidebarItemIdentifiers</key>
<array>
<string>TopLevelAlbums</string>
<string>TopLevelSlideshows</string>
</array>
<key>IPXWorkspaceControllerZoomLevelsKey</key>
<dict>
<key>kZoomLevelIdentifierAlbums</key>
<integer>7</integer>
<key>kZoomLevelIdentifierVersions</key>
<integer>7</integer>
</dict>
<key>lastAddToDestination</key>
<dict>
<key>key</key>
<integer>1</integer>
<key>lastKnownDisplayName</key>
<string>September 28, 2018</string>
<key>type</key>
<string>album</string>
<key>uuid</key>
<string>DFFKmHt3Tk+AGzZLe2Xq+g</string>
</dict>
<key>lastKnownItemCounts</key>
<dict>
<key>other</key>
<integer>0</integer>
<key>photos</key>
<integer>7</integer>
<key>videos</key>
<integer>0</integer>
</dict>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BackgroundHighlightCollection</key>
<date>2020-04-11T20:00:25Z</date>
<key>BackgroundHighlightEnrichment</key>
<date>2020-04-11T20:00:25Z</date>
<key>BackgroundJobAssetRevGeocode</key>
<date>2020-04-11T20:00:25Z</date>
<key>BackgroundJobSearch</key>
<date>2020-04-11T20:00:25Z</date>
<key>BackgroundPeopleSuggestion</key>
<date>2020-04-11T20:00:24Z</date>
<key>BackgroundUserBehaviorProcessor</key>
<date>2020-04-11T20:00:25Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphConsistencyUpdateJobDateKey</key>
<date>2020-04-11T20:10:27Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
<date>2020-04-11T20:00:24Z</date>
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
<date>2020-04-11T20:00:25Z</date>
<key>SiriPortraitDonation</key>
<date>2020-04-11T20:00:25Z</date>
</dict>
</plist>

Some files were not shown because too many files have changed in this diff Show More