Compare commits

...

12 Commits

Author SHA1 Message Date
Rhet Turnbull
0cce234a8c Fixed handling of date_modified for Catalina, issue #247 2020-10-31 08:46:35 -07:00
Rhet Turnbull
c5dba8c89b Added --has-comment/--has-likes to CLI, issue #240 2020-10-29 21:34:15 -07:00
Rhet Turnbull
603dabb8f4 Cleaned up as_dict/asdict, issue #144, #188 2020-10-27 06:54:42 -07:00
Rhet Turnbull
091f1d9bb4 Updated CHANGELOG.md 2020-10-25 22:25:18 -07:00
Rhet Turnbull
d16932d0fd Updated README.md 2020-10-25 22:24:47 -07:00
Rhet Turnbull
23de6b5890 Added comments/likes, implements #214 2020-10-25 22:12:02 -07:00
Rhet Turnbull
4fe58bf2af fixed test 2020-10-25 09:18:20 -07:00
Rhet Turnbull
d87b8f30a4 Added verbose to PhotosDB(), partial fix for #110 2020-10-25 08:16:54 -07:00
Rhet Turnbull
667c89e32c Cleaned up constructor for PhotosDB 2020-10-24 17:20:46 -07:00
Rhet Turnbull
f9cac05f0d Updated CHANGELOG.md 2020-10-24 13:52:27 -07:00
Rhet Turnbull
48f29e138e Fix for issue #238 2020-10-24 13:45:10 -07:00
Rhet Turnbull
7f2701f6ee Updated CHANGELOG.md 2020-10-24 09:17:16 -07:00
328 changed files with 2468 additions and 723 deletions

View File

@@ -4,6 +4,34 @@ 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.36.0](https://github.com/RhetTbull/osxphotos/compare/v0.35.7...v0.36.0)
> 26 October 2020
- Added verbose to PhotosDB(), partial fix for #110 [`d87b8f3`](https://github.com/RhetTbull/osxphotos/commit/d87b8f30a45cbb6fdb315a12f8585e2bdc21be6b)
- Added comments/likes, implements #214 [`23de6b5`](https://github.com/RhetTbull/osxphotos/commit/23de6b58908371d9ca55d1d1999c6d56de454180)
- Cleaned up constructor for PhotosDB [`667c89e`](https://github.com/RhetTbull/osxphotos/commit/667c89e32c3f96baeafebc03e83517ea05693b00)
#### [v0.35.7](https://github.com/RhetTbull/osxphotos/compare/v0.35.6...v0.35.7)
> 24 October 2020
- Fix for issue #238 [`48f29e1`](https://github.com/RhetTbull/osxphotos/commit/48f29e138e4e9da3eba78f3681ee9b8cb28910df)
#### [v0.35.6](https://github.com/RhetTbull/osxphotos/compare/v0.35.5...v0.35.6)
> 24 October 2020
- Fixed shared, not_shared in cli [`8551981`](https://github.com/RhetTbull/osxphotos/commit/8551981f68f0cd2a3a081cc21ae287ff981b9b4b)
#### [v0.35.5](https://github.com/RhetTbull/osxphotos/compare/v0.35.4...v0.35.5)
> 22 October 2020
- Added get_shared_photo_comments.py to examples [`15e0914`](https://github.com/RhetTbull/osxphotos/commit/15e0914af6301a945bc751173aef6718487d9637)
- Fix for issue #237 [`a416de2`](https://github.com/RhetTbull/osxphotos/commit/a416de29e4ac39a5c323f7913b05a8c38ad205be)
- Added test for issue #235 [`ea68229`](https://github.com/RhetTbull/osxphotos/commit/ea68229ddac2e2301ac2d5607451cf7d00207d5d)
#### [v0.35.4](https://github.com/RhetTbull/osxphotos/compare/v0.35.3...v0.35.4)
> 18 October 2020

106
README.md
View File

@@ -21,6 +21,8 @@
+ [ScoreInfo](#scoreinfo)
+ [PersonInfo](#personinfo)
+ [FaceInfo](#faceinfo)
+ [CommentInfo](#commentinfo)
+ [LikeInfo](#likeinfo)
+ [Raw Photos](#raw-photos)
+ [Template Substitutions](#template-substitutions)
+ [Utility Functions](#utility-functions)
@@ -219,6 +221,10 @@ Options:
2000-01-12T12:00:00,
2001-01-12T12:00:00-07:00, or 2000-12-31
(ISO 8601).
--has-comment Search for photos that have comments.
--no-comment Search for photos with no comments.
--has-likes Search for photos that have likes.
--no-likes Search for photos with no likes.
--deleted Include photos from the 'Recently Deleted'
folder.
--deleted-only Include only photos from the 'Recently
@@ -421,23 +427,24 @@ Substitution Description
{descr} Description of the photo
{created.date} Photo's creation date in ISO format, e.g.
'2020-03-22'
{created.year} 4-digit year of file creation time
{created.yy} 2-digit year of file creation time
{created.mm} 2-digit month of the file creation time
{created.year} 4-digit year of photo creation time
{created.yy} 2-digit year of photo creation time
{created.mm} 2-digit month of the photo creation time
(zero padded)
{created.month} Month name in user's locale of the file
{created.month} Month name in user's locale of the photo
creation time
{created.mon} Month abbreviation in the user's locale of
the file creation time
the photo creation time
{created.dd} 2-digit day of the month (zero padded) of
file creation time
{created.dow} Day of week in user's locale of the file
photo creation time
{created.dow} Day of week in user's locale of the photo
creation time
{created.doy} 3-digit day of year (e.g Julian day) of file
creation time, starting from 1 (zero padded)
{created.hour} 2-digit hour of the file creation time
{created.min} 2-digit minute of the file creation time
{created.sec} 2-digit second of the file creation time
{created.doy} 3-digit day of year (e.g Julian day) of
photo creation time, starting from 1 (zero
padded)
{created.hour} 2-digit hour of the photo creation time
{created.min} 2-digit minute of the photo creation time
{created.sec} 2-digit second of the photo creation time
{created.strftime} Apply strftime template to file creation
date/time. Should be used in form
{created.strftime,TEMPLATE} where TEMPLATE
@@ -449,22 +456,26 @@ Substitution Description
templates.
{modified.date} Photo's modification date in ISO format,
e.g. '2020-03-22'
{modified.year} 4-digit year of file modification time
{modified.yy} 2-digit year of file modification time
{modified.mm} 2-digit month of the file modification time
{modified.year} 4-digit year of photo modification time
{modified.yy} 2-digit year of photo modification time
{modified.mm} 2-digit month of the photo modification time
(zero padded)
{modified.month} Month name in user's locale of the file
{modified.month} Month name in user's locale of the photo
modification time
{modified.mon} Month abbreviation in the user's locale of
the file modification time
the photo modification time
{modified.dd} 2-digit day of the month (zero padded) of
the file modification time
{modified.doy} 3-digit day of year (e.g Julian day) of file
modification time, starting from 1 (zero
padded)
{modified.hour} 2-digit hour of the file modification time
{modified.min} 2-digit minute of the file modification time
{modified.sec} 2-digit second of the file modification time
the photo modification time
{modified.dow} Day of week in user's locale of the photo
modification time
{modified.doy} 3-digit day of year (e.g Julian day) of
photo modification time, starting from 1
(zero padded)
{modified.hour} 2-digit hour of the photo modification time
{modified.min} 2-digit minute of the photo modification
time
{modified.sec} 2-digit second of the photo modification
time
{today.date} Current date in iso format, e.g.
'2020-03-22'
{today.year} 4-digit year of current date
@@ -539,6 +550,8 @@ Substitution Description
{label} Image categorization label associated with a photo
(Photos 5 only)
{label_normalized} All lower case version of 'label' (Photos 5 only)
{comment} Comment(s) on shared Photos; format is 'Person name:
comment text' (Photos 5 only)
```
Example: export all photos to ~/Desktop/export group in folders by date created
@@ -697,7 +710,7 @@ osxphotos.PhotosDB(dbfile=path)
Reads the Photos library database and returns a PhotosDB object.
Pass the path to a Photos library or to a specific database file (e.g. "/Users/smith/Pictures/Photos Library.photoslibrary" or "/Users/smith/Pictures/Photos Library.photoslibrary/database/photos.db"). Normally, it's recommended you pass the path the .photoslibrary folder, not the actual database path. The latter option is provided for debugging -- e.g. for reading a database file if you don't have the entire library. Path to photos library may be passed **either** as first argument **or** as named argument `dbfile`. **Note**: In Photos, users may specify a different library to open by holding down the *option* key while opening Photos.app. See also [get_last_library_path](#get_last_library_path) and [get_system_library_path](#get_system_library_path)
Pass the path to a Photos library or to a specific database file (e.g. "/Users/smith/Pictures/Photos Library.photoslibrary" or "/Users/smith/Pictures/Photos Library.photoslibrary/database/photos.db"). Normally, it's recommended you pass the path the .photoslibrary folder, not the actual database path. **Note**: In Photos, users may specify a different library to open by holding down the *option* key while opening Photos.app. See also [get_last_library_path](#get_last_library_path) and [get_system_library_path](#get_system_library_path)
If an invalid path is passed, PhotosDB will raise `FileNotFoundError` exception.
@@ -1157,7 +1170,17 @@ Returns a [PlaceInfo](#PlaceInfo) object with reverse geolocation data or None i
#### `shared`
Returns True if photo is in a shared album, otherwise False.
**Note**: *Only valid on Photos 5 / MacOS 10.15*; on Photos <= 4, returns None instead of True/False.
**Note**: *Only valid on Photos 5 / MacOS 10.15+; on Photos <= 4, returns None instead of True/False.
#### `comments`
Returns list of [CommentInfo](#commentinfo) objects for comments on shared photos or empty list if no comments.
**Note**: *Only valid on Photos 5 / MacOS 10.15+; on Photos <= 4, returns empty list.
#### `likes`
Returns list of [LikeInfo](#likeinfo) objects for likes on shared photos or empty list if no likes.
**Note**: *Only valid on Photos 5 / MacOS 10.15+; on Photos <= 4, returns empty list.
#### `isphoto`
Returns True if type is photo/still image, otherwise False
@@ -1281,7 +1304,8 @@ exiftool must be installed in the path for this to work. If exiftool cannot be
`ExifTool` provides the following methods:
- `as_dict()`: returns all EXIF metadata found in the file as a dictionary in following form (Note: this shows just a subset of available metadata). See [exiftool](https://exiftool.org/) documentation to understand which metadata keys are available.
- `asdict()`: returns all EXIF metadata found in the file as a dictionary in following form (Note: this shows just a subset of available metadata). See [exiftool](https://exiftool.org/) documentation to understand which metadata keys are available.
```python
{'Composite:Aperture': 2.2,
'Composite:GPSPosition': '-34.9188916666667 138.596861111111',
@@ -1294,7 +1318,7 @@ exiftool must be installed in the path for this to work. If exiftool cannot be
}
```
- `json()`: returns same information as `as_dict()` but as a serialized JSON string.
- `json()`: returns same information as `asdict()` but as a serialized JSON string.
- `setvalue(tag, value)`: write to the EXIF data in the photo file. To delete a tag, use setvalue with value = `None`. For example:
```python
@@ -1305,7 +1329,7 @@ photo.exiftool.setvalue("XMP:Title", "Title of photo")
photo.exiftool.addvalues("IPTC:Keywords", "vacation", "beach")
```
**Caution**: I caution against writing new EXIF data to photos in the Photos library because this will overwrite the original copy of the photo and could adversely affect how Photos behaves. `exiftool.as_dict()` is useful for getting access to all the photos information but if you want to write new EXIF data, I recommend you export the photo first then write the data. [PhotoInfo.export()](#export) does this if called with `exiftool=True`.
**Caution**: I caution against writing new EXIF data to photos in the Photos library because this will overwrite the original copy of the photo and could adversely affect how Photos behaves. `exiftool.asdict()` is useful for getting access to all the photos information but if you want to write new EXIF data, I recommend you export the photo first then write the data. [PhotoInfo.export()](#export) does this if called with `exiftool=True`.
#### `score`
Returns a [ScoreInfo](#scoreinfo) data class object which provides access to the computed aesthetic scores for each photo.
@@ -1313,7 +1337,10 @@ Returns a [ScoreInfo](#scoreinfo) data class object which provides access to the
**Note**: Valid only for Photos 5; returns None for earlier Photos versions.
#### `json()`
Returns a JSON representation of all photo info
Returns a JSON representation of all photo info.
#### `asdict()`
Returns a dictionary representation of all photo info.
#### `export()`
`export(dest, *filename, edited=False, live_photo=False, export_as_hardlink=False, overwrite=False, increment=True, sidecar_json=False, sidecar_xmp=False, use_photos_export=False, timeout=120, exiftool=False, no_xattr=False, use_albums_as_keywords=False, use_persons_as_keywords=False)`
@@ -1661,6 +1688,9 @@ Returns a list of [FaceInfo](#faceinfo) objects associated with this person sort
#### `json()`
Returns a json string representation of the PersonInfo instance.
#### `asdict()`
Returns a dictionary representation of the PersonInfo instance.
### FaceInfo
[PhotoInfo.face_info](#photofaceinfo) return a list of FaceInfo objects representing detected faces in a photo. The FaceInfo class has the following properties and methods.
@@ -1746,6 +1776,21 @@ Returns a dictionary representation of the FaceInfo instance.
#### `json()`
Returns a JSON representation of the FaceInfo instance.
### CommentInfo
[PhotoInfo.comments](#comments) returns a list of CommentInfo objects for comments on shared photos. (Photos 5/MacOS 10.15+ only). The list of CommentInfo objects will be sorted in ascending order by date comment was made. CommentInfo contains the following fields:
- `datetime`: `datetime.datetime`, date/time comment was made
- `user`: `str`, name of user who made the comment
- `ismine`: `bool`, True if comment was made by person who owns the Photos library being operated on
- `text`: `str`, text of the actual comment
### LikeInfo
[PhotoInfo.likes](#likes) returns a list of LikeInfo objects for "likes" on shared photos. (Photos 5/MacOS 10.15+ only). The list of LikeInfo objects will be sorted in ascending order by date like was made. LikeInfo contains the following fields:
- `datetime`: `datetime.datetime`, date/time like was made
- `user`: `str`, name of user who made the like
- `ismine`: `bool`, True if like was made by person who owns the Photos library being operated on
### Raw Photos
Handling raw photos in `osxphotos` requires a bit of extra work. Raw photos in Photos can be imported in two different ways: 1) a single raw photo with no associated JPEG image is imported 2) a raw+JPEG pair is imported -- two separate images with same file stem (e.g. `IMG_0001.CR2` and `IMG_001.JPG`) are imported.
@@ -1843,6 +1888,7 @@ The following substitutions are availabe for use with `PhotoInfo.render_template
|{person}|Person(s) / face(s) in a photo|
|{label}|Image categorization label associated with a photo (Photos 5 only)|
|{label_normalized}|All lower case version of 'label' (Photos 5 only)|
|{comment}|Comment(s) on shared Photos; format is 'Person name: comment text' (Photos 5 only)|
### Utility Functions

View File

@@ -42,7 +42,7 @@ def main():
if db:
print("loading database")
tic = time.perf_counter()
photosdb = osxphotos.PhotosDB(dbfile=db)
photosdb = osxphotos.PhotosDB(dbfile=db, verbose=print)
toc = time.perf_counter()
print(f"done: took {toc-tic} seconds")
return photosdb

View File

@@ -1,8 +1,7 @@
import logging
from ._version import __version__
from .photoinfo import PhotoInfo
from .photosdb import PhotosDB
from .photosdb._photosdb_process_comments import CommentInfo, LikeInfo
from .phototemplate import PhotoTemplate
from .utils import _debug, _get_logger, _set_debug

View File

@@ -489,6 +489,10 @@ def query_options(f):
help="Search by end item date, e.g. 2000-01-12T12:00:00, 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 8601).",
type=DateTimeISO8601(),
),
o("--has-comment", is_flag=True, help="Search for photos that have comments."),
o("--no-comment", is_flag=True, help="Search for photos with no comments."),
o("--has-likes", is_flag=True, help="Search for photos that have likes."),
o("--no-likes", is_flag=True, help="Search for photos with no likes."),
]
for o in options[::-1]:
f = o(f)
@@ -521,10 +525,15 @@ def cli(ctx, db, json_, debug):
help="Use with '--dump photos' to dump only certain UUIDs",
multiple=True,
)
@click.option("--verbose", "-V", "verbose_", is_flag=True, help="Print verbose output.")
@click.pass_obj
@click.pass_context
def debug_dump(ctx, cli_obj, db, photos_library, dump, uuid):
def debug_dump(ctx, cli_obj, db, photos_library, dump, uuid, verbose_):
""" Print out debug info """
global VERBOSE
VERBOSE = bool(verbose_)
db = get_photos_db(*photos_library, db, cli_obj.db)
if db is None:
click.echo(cli.commands["debug-dump"].get_help(ctx), err=True)
@@ -534,7 +543,7 @@ def debug_dump(ctx, cli_obj, db, photos_library, dump, uuid):
start_t = time.perf_counter()
print(f"Opening database: {db}")
photosdb = osxphotos.PhotosDB(dbfile=db)
photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose)
stop_t = time.perf_counter()
print(f"Done; took {(stop_t-start_t):.2f} seconds")
@@ -977,6 +986,10 @@ def query(
label,
deleted,
deleted_only,
has_comment,
no_comment,
has_likes,
no_likes,
):
""" Query the Photos database using 1 or more search options;
if more than one option is provided, they are treated as "AND"
@@ -1021,6 +1034,8 @@ def query(
(any(place), no_place),
(deleted, deleted_only),
(shared, not_shared),
(has_comment, no_comment),
(has_likes, no_likes),
]
# print help if no non-exclusive term or a double exclusive term is given
if any(all(bb) for bb in exclusive) or not any(
@@ -1107,6 +1122,10 @@ def query(
label=label,
deleted=deleted,
deleted_only=deleted_only,
has_comment=has_comment,
no_comment=no_comment,
has_likes=has_likes,
no_likes=no_likes,
)
# below needed for to make CliRunner work for testing
@@ -1390,6 +1409,10 @@ def export(
edited_suffix,
place,
no_place,
has_comment,
no_comment,
has_likes,
no_likes,
no_extended_attributes,
label,
deleted,
@@ -1437,6 +1460,8 @@ def export(
(skip_edited, skip_original_if_edited),
(export_as_hardlink, convert_to_jpeg),
(shared, not_shared),
(has_comment, no_comment),
(has_likes, no_likes),
]
if any(all(bb) for bb in exclusive):
click.echo("Incompatible export options", err=True)
@@ -1575,6 +1600,10 @@ def export(
label=label,
deleted=deleted,
deleted_only=deleted_only,
has_comment=has_comment,
no_comment=no_comment,
has_likes=has_likes,
no_likes=no_likes,
)
if photos:
@@ -1894,13 +1923,17 @@ def _query(
label=None,
deleted=False,
deleted_only=False,
has_comment=False,
no_comment=False,
has_likes=False,
no_likes=False,
):
""" run a query against PhotosDB to extract the photos based on user supply criteria
used by query and export commands
arguments must be passed in same order as query and export
if either is modified, need to ensure all three functions are updated """
photosdb = osxphotos.PhotosDB(dbfile=db)
photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose)
if deleted or deleted_only:
photos = photosdb.photos(
uuid=uuid,
@@ -2113,6 +2146,16 @@ def _query(
if has_raw:
photos = [p for p in photos if p.has_raw]
if has_comment:
photos = [p for p in photos if p.comments]
elif no_comment:
photos = [p for p in photos if not p.comments]
if has_likes:
photos = [p for p in photos if p.likes]
elif no_likes:
photos = [p for p in photos if not p.likes]
return photos
@@ -2230,7 +2273,7 @@ def export_photo(
f"skipping {photo.original_filename}"
)
return ExportResults([], [], [], [], [], [])
elif photo.ismissing and not photo.iscloudasset or not photo.incloud:
elif photo.ismissing and not photo.iscloudasset and not photo.incloud:
verbose(
f"Skipping missing {photo.original_filename}: not iCloud asset or missing from cloud"
)

View File

@@ -1,4 +1,4 @@
""" version info """
__version__ = "0.35.6"
__version__ = "0.36.2"

View File

@@ -228,7 +228,7 @@ class ExifTool:
ver = self.run_commands("-ver", no_file=True)
return ver.decode("utf-8")
def as_dict(self):
def asdict(self):
""" return dictionary of all EXIF tags and values from exiftool
returns empty dict if no tags
"""
@@ -245,7 +245,7 @@ class ExifTool:
def _read_exif(self):
""" read exif data from file """
data = self.as_dict()
data = self.asdict()
self.data = {k: v for k, v in data.items()}
def __str__(self):

View File

@@ -66,10 +66,10 @@ class PersonInfo:
# no faces
return []
def json(self):
""" Returns JSON representation of class instance """
def asdict(self):
""" Returns dictionary representation of class instance """
keyphoto = self.keyphoto.uuid if self.keyphoto is not None else None
person = {
return {
"uuid": self.uuid,
"name": self.name,
"displayname": self.display_name,
@@ -77,7 +77,10 @@ class PersonInfo:
"facecount": self.facecount,
"keyphoto": keyphoto,
}
return json.dumps(person)
def json(self):
""" Returns JSON representation of class instance """
return json.dumps(self.asdict())
def __str__(self):
return f"PersonInfo(name={self.name}, display_name={self.display_name}, uuid={self.uuid}, facecount={self.facecount})"

View File

@@ -0,0 +1,17 @@
""" PhotoInfo methods to expose comments and likes for shared photos """
@property
def comments(self):
""" Returns list of Comment objects for any comments on the photo (sorted by date) """
try:
return self._db._db_comments_uuid[self.uuid]["comments"]
except:
return []
@property
def likes(self):
""" Returns list of Like objects for any likes on the photo (sorted by date) """
try:
return self._db._db_comments_uuid[self.uuid]["likes"]
except:
return []

View File

@@ -636,8 +636,11 @@ def export2(
exported = []
# export live_photo .mov file?
live_photo = True if live_photo and self.live_photo else False
if edited:
if edited or self.shared:
# exported edited version and not original
# shared photos (in shared albums) show up as not having adjustments (not edited)
# but Photos is unable to export the "original" as only a jpeg copy is shared in iCloud
# so tell Photos to export the current version in this case
if filename:
# use filename stem provided
filestem = dest.stem
@@ -671,7 +674,6 @@ def export2(
burst=self.burst,
dry_run=dry_run,
)
if exported:
if touch_file:
for exported_file in exported:

View File

@@ -5,6 +5,7 @@ PhotosDB.photos() returns a list of PhotoInfo objects
"""
import dataclasses
import datetime
import json
import logging
import os
@@ -59,6 +60,7 @@ class PhotoInfo:
ExportResults,
)
from ._photoinfo_scoreinfo import score, ScoreInfo
from ._photoinfo_comments import comments, likes
def __init__(self, db=None, uuid=None, info=None):
self._uuid = uuid
@@ -949,22 +951,23 @@ class PhotoInfo:
}
return yaml.dump(info, sort_keys=False)
def json(self):
""" return JSON representation """
def asdict(self):
""" return dict representation """
date_modified_iso = (
self.date_modified.isoformat() if self.date_modified else None
)
folders = {album.title: album.folder_names for album in self.album_info}
exif = dataclasses.asdict(self.exif_info) if self.exif_info else {}
place = self.place.as_dict() if self.place else {}
place = self.place.asdict() if self.place else {}
score = dataclasses.asdict(self.score) if self.score else {}
comments = [comment.asdict() for comment in self.comments]
likes = [like.asdict() for like in self.likes]
faces = [face.asdict() for face in self.face_info]
pic = {
return {
"library": self._db._library_path,
"uuid": self.uuid,
"filename": self.filename,
"original_filename": self.original_filename,
"date": self.date.isoformat(),
"date": self.date,
"description": self.description,
"title": self.title,
"keywords": self.keywords,
@@ -973,6 +976,7 @@ class PhotoInfo:
"albums": self.albums,
"folders": folders,
"persons": self.persons,
"faces": faces,
"path": self.path,
"ismissing": self.ismissing,
"hasadjustments": self.hasadjustments,
@@ -986,12 +990,13 @@ class PhotoInfo:
"isphoto": self.isphoto,
"ismovie": self.ismovie,
"uti": self.uti,
"uti_original": self.uti_original,
"burst": self.burst,
"live_photo": self.live_photo,
"path_live_photo": self.path_live_photo,
"iscloudasset": self.iscloudasset,
"incloud": self.incloud,
"date_modified": date_modified_iso,
"date_modified": self.date_modified,
"portrait": self.portrait,
"screenshot": self.screenshot,
"slow_mo": self.slow_mo,
@@ -1000,6 +1005,8 @@ class PhotoInfo:
"selfie": self.selfie,
"panorama": self.panorama,
"has_raw": self.has_raw,
"israw": self.israw,
"raw_original": self.raw_original,
"uti_raw": self.uti_raw,
"path_raw": self.path_raw,
"place": place,
@@ -1013,8 +1020,17 @@ class PhotoInfo:
"original_width": self.original_width,
"original_orientation": self.original_orientation,
"original_filesize": self.original_filesize,
"comments": comments,
"likes": likes,
}
return json.dumps(pic)
def json(self):
""" Return JSON representation """
def default(o):
if isinstance(o, (datetime.date, datetime.datetime)):
return o.isoformat()
return json.dumps(self.asdict(), sort_keys=True, default=default)
def __eq__(self, other):
""" Compare two PhotoInfo objects for equality """

View File

@@ -0,0 +1,157 @@
""" PhotosDB method for processing comments and likes on shared photos.
Do not import this module directly """
import dataclasses
import datetime
from dataclasses import dataclass
from .._constants import _DB_TABLE_NAMES, _PHOTOS_4_VERSION, TIME_DELTA
from ..utils import _open_sql_file, normalize_unicode
def _process_comments(self):
""" load the comments and likes data from the database
this is a PhotosDB method that should be imported in
the PhotosDB class definition in photosdb.py
"""
self._db_hashed_person_id = {}
self._db_comments_uuid = {}
if self._db_version <= _PHOTOS_4_VERSION:
_process_comments_4(self)
else:
_process_comments_5(self)
@dataclass
class CommentInfo:
""" Class for shared photo comments """
datetime: datetime.datetime
user: str
ismine: bool
text: str
def asdict(self):
return dataclasses.asdict(self)
@dataclass
class LikeInfo:
""" Class for shared photo likes """
datetime: datetime.datetime
user: str
ismine: bool
def asdict(self):
return dataclasses.asdict(self)
# The following methods do not get imported into PhotosDB
# but will get called by _process_comments
def _process_comments_4(photosdb):
""" process comments and likes info for Photos <= 4
photosdb: PhotosDB instance """
raise NotImplementedError(
f"Not implemented for database version {photosdb._db_version}."
)
def _process_comments_5(photosdb):
""" process comments and likes info for Photos >= 5
photosdb: PhotosDB instance """
db = photosdb._tmp_db
asset_table = _DB_TABLE_NAMES[photosdb._photos_ver]["ASSET"]
(conn, cursor) = _open_sql_file(db)
results = conn.execute(
"""
SELECT DISTINCT
ZINVITEEHASHEDPERSONID,
ZINVITEEFIRSTNAME,
ZINVITEELASTNAME,
ZINVITEEFULLNAME
FROM
ZCLOUDSHAREDALBUMINVITATIONRECORD
"""
)
# order of results
# 0: ZINVITEEHASHEDPERSONID,
# 1: ZINVITEEFIRSTNAME,
# 2: ZINVITEELASTNAME,
# 3: ZINVITEEFULLNAME
photosdb._db_hashed_person_id = {}
for row in results.fetchall():
person_id = row[0]
photosdb._db_hashed_person_id[person_id] = {
"first_name": normalize_unicode(row[1]),
"last_name": normalize_unicode(row[2]),
"full_name": normalize_unicode(row[3]),
}
results = conn.execute(
f"""
SELECT
{asset_table}.ZUUID, -- UUID of the photo
ZCLOUDSHAREDCOMMENT.ZISLIKE, -- comment is actually a "like"
ZCLOUDSHAREDCOMMENT.ZCOMMENTDATE, -- date of comment
ZCLOUDSHAREDCOMMENT.ZCOMMENTTEXT, -- text of comment
ZCLOUDSHAREDCOMMENT.ZCOMMENTERHASHEDPERSONID, -- hashed ID of person who made comment/like
ZCLOUDSHAREDCOMMENT.ZISMYCOMMENT -- is my (this user's) comment
FROM ZCLOUDSHAREDCOMMENT
JOIN {asset_table} ON
{asset_table}.Z_PK = ZCLOUDSHAREDCOMMENT.ZCOMMENTEDASSET
OR
{asset_table}.Z_PK = ZCLOUDSHAREDCOMMENT.ZLIKEDASSET
"""
)
# order of results
# 0: ZGENERICASSET.ZUUID, -- UUID of the photo
# 1: ZCLOUDSHAREDCOMMENT.ZISLIKE, -- comment is actually a "like"
# 2: ZCLOUDSHAREDCOMMENT.ZCOMMENTDATE, -- date of comment
# 3: ZCLOUDSHAREDCOMMENT.ZCOMMENTTEXT, -- text of comment
# 4: ZCLOUDSHAREDCOMMENT.ZCOMMENTERHASHEDPERSONID, -- hashed ID of person who made comment/like
# 5: ZCLOUDSHAREDCOMMENT.ZISMYCOMMENT -- is my (this user's) comment
photosdb._db_comments_uuid = {}
for row in results:
uuid = row[0]
is_like = bool(row[1])
text = normalize_unicode(row[3])
try:
user_name = photosdb._db_hashed_person_id[row[4]]["full_name"]
except KeyError:
user_name = None
try:
dt = datetime.datetime.fromtimestamp(row[2] + TIME_DELTA)
except:
dt = datetime.datetime(1970, 1, 1)
ismine = bool(row[5])
try:
db_comments = photosdb._db_comments_uuid[uuid]
except KeyError:
photosdb._db_comments_uuid[uuid] = {"likes": [], "comments": []}
db_comments = photosdb._db_comments_uuid[uuid]
if is_like:
db_comments["likes"].append(LikeInfo(dt, user_name, ismine))
elif text:
db_comments["comments"].append(CommentInfo(dt, user_name, ismine, text))
# sort results
for uuid in photosdb._db_comments_uuid:
if photosdb._db_comments_uuid[uuid]["likes"]:
photosdb._db_comments_uuid[uuid]["likes"].sort(key=lambda x: x.datetime)
if photosdb._db_comments_uuid[uuid]["comments"]:
photosdb._db_comments_uuid[uuid]["comments"].sort(key=lambda x: x.datetime)
conn.close()

View File

@@ -44,6 +44,7 @@ from ..utils import (
_get_os_version,
_open_sql_file,
get_last_library_path,
noop,
normalize_unicode,
)
from .photosdb_utils import get_db_model_version, get_db_version
@@ -67,12 +68,19 @@ class PhotosDB:
labels_normalized_as_dict,
)
from ._photosdb_process_scoreinfo import _process_scoreinfo
from ._photosdb_process_comments import _process_comments
def __init__(self, *dbfile_, dbfile=None):
""" create a new PhotosDB object
path to photos library or database may be specified EITHER as first argument or as named argument dbfile=path
specify full path to photos library or photos.db as first argument
specify path to photos library or photos.db using named argument dbfile=path """
def __init__(self, dbfile=None, verbose=None):
""" Create a new PhotosDB object.
Args:
dbfile: specify full path to photos library or photos.db; if None, will attempt to locate last library opened by Photos.
verbose: optional callable function to use for printing verbose text during processing; if None (default), does not print output.
Raises:
FileNotFoundError if dbfile is not a valid Photos library.
TypeError if verbose is not None and not callable.
"""
# Check OS version
system = platform.system()
@@ -84,6 +92,12 @@ class PhotosDB:
f"you have {system}, OS version: {major}"
)
if verbose is None:
verbose = noop
elif not callable(verbose):
raise TypeError("verbose must be callable")
self._verbose = verbose
# create a temporary directory
# tempfile.TemporaryDirectory gets cleaned up when the object does
self._tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
@@ -216,25 +230,7 @@ class PhotosDB:
if _debug():
logging.debug(f"dbfile = {dbfile}")
# get the path to photos library database
if dbfile_:
# got a library path as argument
if dbfile:
# shouldn't pass via both *args and dbfile=
raise TypeError(
f"photos database path must be specified as argument or "
f"named parameter dbfile but not both: args: {dbfile_}, dbfile: {dbfile}",
dbfile_,
dbfile,
)
elif len(dbfile_) == 1:
dbfile = dbfile_[0]
else:
raise TypeError(
f"__init__ takes only a single argument (photos database path): {dbfile_}",
dbfile_,
)
elif dbfile is None:
if dbfile is None:
dbfile = get_last_library_path()
if dbfile is None:
# get_last_library_path must have failed to find library
@@ -262,11 +258,14 @@ class PhotosDB:
# or photosanalysisd
self._dbfile = self._dbfile_actual = self._tmp_db = os.path.abspath(dbfile)
verbose(f"Processing database {self._dbfile}")
# if database is exclusively locked, make a copy of it and use the copy
# Photos maintains an exclusive lock on the database file while Photos is open
# photoanalysisd sometimes maintains this lock even after Photos is closed
# In those cases, make a temp copy of the file for sqlite3 to read
if _db_is_locked(self._dbfile):
verbose(f"Database locked, creating temporary copy.")
self._tmp_db = self._copy_db_file(self._dbfile)
self._db_version = get_db_version(self._tmp_db)
@@ -279,8 +278,10 @@ class PhotosDB:
raise FileNotFoundError(f"dbfile {dbfile} does not exist", dbfile)
else:
self._dbfile_actual = self._tmp_db = dbfile
verbose(f"Processing database {self._dbfile_actual}")
# if database is exclusively locked, make a copy of it and use the copy
if _db_is_locked(self._dbfile_actual):
verbose(f"Database locked, creating temporary copy.")
self._tmp_db = self._copy_db_file(self._dbfile_actual)
if _debug():
@@ -549,10 +550,15 @@ class PhotosDB:
""" process the Photos database to extract info
works on Photos version <= 4.0 """
verbose = self._verbose
verbose("Processing database.")
verbose(f"Database version: {self._db_version}.")
(conn, c) = _open_sql_file(self._tmp_db)
# get info to associate persons with photos
# then get detected faces in each photo and link to persons
verbose("Processing persons in photos.")
c.execute(
""" SELECT
RKPerson.modelID,
@@ -618,6 +624,7 @@ class PhotosDB:
logging.debug(f"Unexpected KeyError _dbpersons_pk[{pk}]")
# get information on detected faces
verbose("Processing detected faces in photos.")
c.execute(
""" SELECT
RKPerson.modelID,
@@ -655,6 +662,7 @@ class PhotosDB:
logging.debug(pformat(self._dbfaces_uuid))
# Get info on albums
verbose("Processing albums.")
c.execute(
""" SELECT
RKAlbum.uuid,
@@ -797,6 +805,7 @@ class PhotosDB:
logging.debug(pformat(self._dbfolder_details))
# Get info on keywords
verbose("Processing keywords.")
c.execute(
""" SELECT
RKKeyword.name,
@@ -824,6 +833,7 @@ class PhotosDB:
self._dbvolumes[vol[0]] = vol[1]
# Get photo details
verbose("Processing photo details.")
if self._db_version < _PHOTOS_3_VERSION:
# Photos < 3.0 doesn't have RKVersion.selfPortrait (selfie)
c.execute(
@@ -1113,6 +1123,7 @@ class PhotosDB:
self._dbphotos[uuid]["fok_import_session"] = None
# get additional details from RKMaster, needed for RAW processing
verbose("Processing additional photo details.")
c.execute(
""" SELECT
RKMaster.uuid,
@@ -1286,6 +1297,7 @@ class PhotosDB:
self._dbphotos[uuid]["incloud"] = True if row[2] == 1 else False
# get location data
verbose("Processing location data.")
# get the country codes
country_codes = c.execute(
"SELECT modelID, countryCode "
@@ -1372,6 +1384,7 @@ class PhotosDB:
conn.close()
# process faces
verbose("Processing face details.")
self._process_faceinfo()
# add faces and keywords to photo data
@@ -1408,6 +1421,7 @@ class PhotosDB:
self._dbphotos[uuid]["volume"] = None
# done processing, dump debug data if requested
verbose("Done processing details from Photos library.")
if _debug():
logging.debug("Faces (_dbfaces_uuid):")
logging.debug(pformat(self._dbfaces_uuid))
@@ -1483,12 +1497,14 @@ class PhotosDB:
if _debug():
logging.debug(f"_process_database5")
verbose = self._verbose
verbose(f"Processing database.")
(conn, c) = _open_sql_file(self._tmp_db)
# some of the tables/columns have different names in different versions of Photos
photos_ver = get_db_model_version(self._tmp_db)
self._photos_ver = photos_ver
verbose(f"Database version: {self._db_version}, {photos_ver}.")
asset_table = _DB_TABLE_NAMES[photos_ver]["ASSET"]
keyword_join = _DB_TABLE_NAMES[photos_ver]["KEYWORD_JOIN"]
album_join = _DB_TABLE_NAMES[photos_ver]["ALBUM_JOIN"]
@@ -1502,6 +1518,7 @@ class PhotosDB:
# get info to associate persons with photos
# then get detected faces in each photo and link to persons
verbose("Processing persons in photos.")
c.execute(
""" SELECT
ZPERSON.Z_PK,
@@ -1567,6 +1584,7 @@ class PhotosDB:
logging.debug(f"Unexpected KeyError _dbpersons_pk[{pk}]")
# get information on detected faces
verbose("Processing detected faces in photos.")
c.execute(
f""" SELECT
ZPERSON.Z_PK,
@@ -1601,6 +1619,7 @@ class PhotosDB:
logging.debug(pformat(self._dbfaces_uuid))
# get details about albums
verbose("Processing albums.")
c.execute(
f""" SELECT
ZGENERICALBUM.ZUUID,
@@ -1719,6 +1738,7 @@ class PhotosDB:
logging.debug(pformat(self._dbalbum_folders))
# get details on keywords
verbose("Processing keywords.")
c.execute(
f"""SELECT ZKEYWORD.ZTITLE, {asset_table}.ZUUID
FROM {asset_table}
@@ -1750,6 +1770,7 @@ class PhotosDB:
logging.debug(self._dbvolumes)
# get details about photos
verbose("Processing photo details.")
logging.debug(f"Getting information about photos")
c.execute(
f"""SELECT {asset_table}.ZUUID,
@@ -1788,7 +1809,8 @@ class PhotosDB:
ZADDITIONALASSETATTRIBUTES.ZORIGINALWIDTH,
ZADDITIONALASSETATTRIBUTES.ZORIGINALORIENTATION,
ZADDITIONALASSETATTRIBUTES.ZORIGINALFILESIZE,
{depth_state}
{depth_state},
{asset_table}.ZADJUSTMENTTIMESTAMP
FROM {asset_table}
JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = {asset_table}.Z_PK
ORDER BY {asset_table}.ZUUID """
@@ -1832,6 +1854,7 @@ class PhotosDB:
# 34 ZADDITIONALASSETATTRIBUTES.ZORIGINALORIENTATION,
# 35 ZADDITIONALASSETATTRIBUTES.ZORIGINALFILESIZE
# 36 ZGENERICASSET.ZDEPTHSTATES / ZASSET.ZDEPTHTYPE
# 37 ZGENERICASSET.ZADJUSTMENTTIMESTAMP -- when was photo edited?
for row in c:
uuid = row[0]
@@ -1845,9 +1868,9 @@ class PhotosDB:
# There are sometimes negative values for lastmodifieddate in the database
# I don't know what these mean but they will raise exception in datetime if
# not accounted for
info["lastmodifieddate_timestamp"] = row[4]
info["lastmodifieddate_timestamp"] = row[37]
try:
info["lastmodifieddate"] = datetime.fromtimestamp(row[4] + TIME_DELTA)
info["lastmodifieddate"] = datetime.fromtimestamp(row[37] + TIME_DELTA)
except ValueError:
info["lastmodifieddate"] = None
except TypeError:
@@ -1908,7 +1931,7 @@ class PhotosDB:
info["type"] = None
info["UTI"] = row[18]
info["UTI_original"] = None # filled in later
info["UTI_original"] = None # filled in later
# handle burst photos
# if burst photo, determine whether or not it's a selected burst photo
@@ -2040,6 +2063,7 @@ class PhotosDB:
# 1 ZGENERICASSET.ZIMPORTSESSION
# 2 ZGENERICASSET.Z_FOK_IMPORTSESSION
# 3 ZGENERICALBUM.ZUUID,
verbose("Processing import sessions.")
c.execute(
f"""SELECT
{asset_table}.ZUUID,
@@ -2062,6 +2086,7 @@ class PhotosDB:
logging.debug(f"No info record for uuid {uuid} for import session")
# Get extended description
verbose("Processing additional photo details.")
c.execute(
f"""SELECT {asset_table}.ZUUID,
ZASSETDESCRIPTION.ZLONGDESCRIPTION
@@ -2241,18 +2266,27 @@ class PhotosDB:
conn.close()
# process face info
verbose("Processing face details.")
self._process_faceinfo()
# process search info
verbose("Processing photo labels.")
self._process_searchinfo()
# process exif info
verbose("Processing EXIF details.")
self._process_exifinfo()
# process computed scores
verbose("Processing computed aesthetic scores.")
self._process_scoreinfo()
# process shared comments/likes
verbose("Processing comments and likes for shared photos.")
self._process_comments()
# done processing, dump debug data if requested
verbose("Done processing details from Photos library.")
if _debug():
logging.debug("Faces (_dbfaces_uuid):")
logging.debug(pformat(self._dbfaces_uuid))

View File

@@ -30,33 +30,34 @@ TEMPLATE_SUBSTITUTIONS = {
"{title}": "Title of the photo",
"{descr}": "Description of the photo",
"{created.date}": "Photo's creation date in ISO format, e.g. '2020-03-22'",
"{created.year}": "4-digit year of file creation time",
"{created.yy}": "2-digit year of file creation time",
"{created.mm}": "2-digit month of the file creation time (zero padded)",
"{created.month}": "Month name in user's locale of the file creation time",
"{created.mon}": "Month abbreviation in the user's locale of the file creation time",
"{created.dd}": "2-digit day of the month (zero padded) of file creation time",
"{created.dow}": "Day of week in user's locale of the file creation time",
"{created.doy}": "3-digit day of year (e.g Julian day) of file creation time, starting from 1 (zero padded)",
"{created.hour}": "2-digit hour of the file creation time",
"{created.min}": "2-digit minute of the file creation time",
"{created.sec}": "2-digit second of the file creation time",
"{created.year}": "4-digit year of photo creation time",
"{created.yy}": "2-digit year of photo creation time",
"{created.mm}": "2-digit month of the photo creation time (zero padded)",
"{created.month}": "Month name in user's locale of the photo creation time",
"{created.mon}": "Month abbreviation in the user's locale of the photo creation time",
"{created.dd}": "2-digit day of the month (zero padded) of photo creation time",
"{created.dow}": "Day of week in user's locale of the photo creation time",
"{created.doy}": "3-digit day of year (e.g Julian day) of photo creation time, starting from 1 (zero padded)",
"{created.hour}": "2-digit hour of the photo creation time",
"{created.min}": "2-digit minute of the photo creation time",
"{created.sec}": "2-digit second of the photo creation time",
"{created.strftime}": "Apply strftime template to file creation date/time. Should be used in form "
+ "{created.strftime,TEMPLATE} where TEMPLATE is a valid strftime template, e.g. "
+ "{created.strftime,%Y-%U} would result in year-week number of year: '2020-23'. "
+ "If used with no template will return null value. "
+ "See https://strftime.org/ for help on strftime templates.",
"{modified.date}": "Photo's modification date in ISO format, e.g. '2020-03-22'",
"{modified.year}": "4-digit year of file modification time",
"{modified.yy}": "2-digit year of file modification time",
"{modified.mm}": "2-digit month of the file modification time (zero padded)",
"{modified.month}": "Month name in user's locale of the file modification time",
"{modified.mon}": "Month abbreviation in the user's locale of the file modification time",
"{modified.dd}": "2-digit day of the month (zero padded) of the file modification time",
"{modified.doy}": "3-digit day of year (e.g Julian day) of file modification time, starting from 1 (zero padded)",
"{modified.hour}": "2-digit hour of the file modification time",
"{modified.min}": "2-digit minute of the file modification time",
"{modified.sec}": "2-digit second of the file modification time",
"{modified.year}": "4-digit year of photo modification time",
"{modified.yy}": "2-digit year of photo modification time",
"{modified.mm}": "2-digit month of the photo modification time (zero padded)",
"{modified.month}": "Month name in user's locale of the photo modification time",
"{modified.mon}": "Month abbreviation in the user's locale of the photo modification time",
"{modified.dd}": "2-digit day of the month (zero padded) of the photo modification time",
"{modified.dow}": "Day of week in user's locale of the photo modification time",
"{modified.doy}": "3-digit day of year (e.g Julian day) of photo modification time, starting from 1 (zero padded)",
"{modified.hour}": "2-digit hour of the photo modification time",
"{modified.min}": "2-digit minute of the photo modification time",
"{modified.sec}": "2-digit second of the photo modification time",
# "{modified.strftime}": "Apply strftime template to file modification date/time. Should be used in form "
# + "{modified.strftime,TEMPLATE} where TEMPLATE is a valid strftime template, e.g. "
# + "{modified.strftime,%Y-%U} would result in year-week number of year: '2020-23'. "
@@ -102,6 +103,7 @@ TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
"{person}": "Person(s) / face(s) in a photo",
"{label}": "Image categorization label associated with a photo (Photos 5 only)",
"{label_normalized}": "All lower case version of 'label' (Photos 5 only)",
"{comment}": "Comment(s) on shared Photos; format is 'Person name: comment text' (Photos 5 only)",
}
# Just the multi-valued substitution names without the braces
@@ -244,14 +246,14 @@ class PhotoTemplate:
# '2011/Album2/keyword1/person1',
# '2011/Album2/keyword2/person1',]
rendered_strings = set([rendered])
rendered_strings = [rendered]
for field in MULTI_VALUE_SUBSTITUTIONS:
# Build a regex that matches only the field being processed
re_str = r"(?<!\\)\{(" + field + r")(,(([\w\-\%. ]{0,})))?\}"
regex_multi = re.compile(re_str)
# holds each of the new rendered_strings, set() to avoid duplicates
new_strings = set()
# holds each of the new rendered_strings, dict to avoid repeats (dict.keys())
new_strings = {}
for str_template in rendered_strings:
if regex_multi.search(str_template):
@@ -307,10 +309,10 @@ class PhotoTemplate:
self, none_str, get_func=lookup_template_value_multi
)
new_string = regex_multi.sub(subst, str_template)
new_strings.add(new_string)
new_strings[new_string] = 1
# update rendered_strings for the next field to process
rendered_strings = new_strings
rendered_strings = list(new_strings.keys())
# find any {fields} that weren't replaced
unmatched = []
@@ -444,6 +446,12 @@ class PhotoTemplate:
if self.photo.date_modified
else None
)
elif field == "modified.dow":
value = (
DateTimeFormatter(self.photo.date_modified).dow
if self.photo.date_modified
else None
)
elif field == "modified.doy":
value = (
DateTimeFormatter(self.photo.date_modified).doy
@@ -637,6 +645,10 @@ class PhotoTemplate:
)
else:
values.append(album.title)
elif field == "comment":
values = [
f"{comment.user}: {comment.text}" for comment in self.photo.comments
]
else:
raise ValueError(f"Unhandled template value: {field}")

View File

@@ -491,7 +491,7 @@ class PlaceInfo4(PlaceInfo):
}
return "PlaceInfo(" + ", ".join([f"{k}='{v}'" for k, v in info.items()]) + ")"
def as_dict(self):
def asdict(self):
return {
"name": self.name,
"names": self.names._asdict(),
@@ -634,7 +634,7 @@ class PlaceInfo5(PlaceInfo):
}
return "PlaceInfo(" + ", ".join([f"{k}='{v}'" for k, v in info.items()]) + ")"
def as_dict(self):
def asdict(self):
return {
"name": self.name,
"names": self.names._asdict(),

View File

@@ -57,6 +57,9 @@ def _debug():
""" returns True if debugging turned on (via _set_debug), otherwise, false """
return _DEBUG
def noop(*args, **kwargs):
""" do nothing (no operation) """
pass
def _get_os_version():
# returns tuple containing OS version

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>1797</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: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

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,53 @@
<?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>92D68107-B6C7-453B-96D2-97B0F26D5B8B/L0/020</string>
<string>88A5F8B8-5B9A-43C7-BB85-3952B81580EB/L0/020</string>
<string>29EF7A97-7E76-4D5F-A5E0-CC0A93E8524C/L0/020</string>
<string>2C2AF115-BD1D-4434-A747-D1C8BD8E2045/L0/020</string>
<string>CB051A4C-2CB7-4B90-B59B-08CC4D0C2823/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-10-17T23:45:25Z</date>
<key>BackgroundHighlightEnrichment</key>
<date>2020-10-17T23:45:25Z</date>
<key>BackgroundJobAssetRevGeocode</key>
<date>2020-10-17T23:45:25Z</date>
<key>BackgroundJobSearch</key>
<date>2020-10-17T23:45:25Z</date>
<key>BackgroundPeopleSuggestion</key>
<date>2020-10-17T23:45:25Z</date>
<key>BackgroundUserBehaviorProcessor</key>
<date>2020-10-17T23:45:25Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphConsistencyUpdateJobDateKey</key>
<date>2020-10-17T23:45:33Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
<date>2020-10-17T23:45:24Z</date>
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
<date>2020-10-17T23:45:26Z</date>
<key>SiriPortraitDonation</key>
<date>2020-10-17T23:45:25Z</date>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?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>revgeoprovider</key>
<string>7618</string>
</dict>
</plist>

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