Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cce234a8c | ||
|
|
c5dba8c89b | ||
|
|
603dabb8f4 | ||
|
|
091f1d9bb4 | ||
|
|
d16932d0fd | ||
|
|
23de6b5890 | ||
|
|
4fe58bf2af | ||
|
|
d87b8f30a4 | ||
|
|
667c89e32c | ||
|
|
f9cac05f0d | ||
|
|
48f29e138e | ||
|
|
7f2701f6ee |
28
CHANGELOG.md
@@ -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).
|
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)
|
#### [v0.35.4](https://github.com/RhetTbull/osxphotos/compare/v0.35.3...v0.35.4)
|
||||||
|
|
||||||
> 18 October 2020
|
> 18 October 2020
|
||||||
|
|||||||
106
README.md
@@ -21,6 +21,8 @@
|
|||||||
+ [ScoreInfo](#scoreinfo)
|
+ [ScoreInfo](#scoreinfo)
|
||||||
+ [PersonInfo](#personinfo)
|
+ [PersonInfo](#personinfo)
|
||||||
+ [FaceInfo](#faceinfo)
|
+ [FaceInfo](#faceinfo)
|
||||||
|
+ [CommentInfo](#commentinfo)
|
||||||
|
+ [LikeInfo](#likeinfo)
|
||||||
+ [Raw Photos](#raw-photos)
|
+ [Raw Photos](#raw-photos)
|
||||||
+ [Template Substitutions](#template-substitutions)
|
+ [Template Substitutions](#template-substitutions)
|
||||||
+ [Utility Functions](#utility-functions)
|
+ [Utility Functions](#utility-functions)
|
||||||
@@ -219,6 +221,10 @@ Options:
|
|||||||
2000-01-12T12:00:00,
|
2000-01-12T12:00:00,
|
||||||
2001-01-12T12:00:00-07:00, or 2000-12-31
|
2001-01-12T12:00:00-07:00, or 2000-12-31
|
||||||
(ISO 8601).
|
(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'
|
--deleted Include photos from the 'Recently Deleted'
|
||||||
folder.
|
folder.
|
||||||
--deleted-only Include only photos from the 'Recently
|
--deleted-only Include only photos from the 'Recently
|
||||||
@@ -421,23 +427,24 @@ Substitution Description
|
|||||||
{descr} Description of the photo
|
{descr} Description of the photo
|
||||||
{created.date} Photo's creation date in ISO format, e.g.
|
{created.date} Photo's creation date in ISO format, e.g.
|
||||||
'2020-03-22'
|
'2020-03-22'
|
||||||
{created.year} 4-digit year of file creation time
|
{created.year} 4-digit year of photo creation time
|
||||||
{created.yy} 2-digit year of file creation time
|
{created.yy} 2-digit year of photo creation time
|
||||||
{created.mm} 2-digit month of the file creation time
|
{created.mm} 2-digit month of the photo creation time
|
||||||
(zero padded)
|
(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
|
creation time
|
||||||
{created.mon} Month abbreviation in the user's locale of
|
{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
|
{created.dd} 2-digit day of the month (zero padded) of
|
||||||
file creation time
|
photo creation time
|
||||||
{created.dow} Day of week in user's locale of the file
|
{created.dow} Day of week in user's locale of the photo
|
||||||
creation time
|
creation time
|
||||||
{created.doy} 3-digit day of year (e.g Julian day) of file
|
{created.doy} 3-digit day of year (e.g Julian day) of
|
||||||
creation time, starting from 1 (zero padded)
|
photo creation time, starting from 1 (zero
|
||||||
{created.hour} 2-digit hour of the file creation time
|
padded)
|
||||||
{created.min} 2-digit minute of the file creation time
|
{created.hour} 2-digit hour of the photo creation time
|
||||||
{created.sec} 2-digit second of the file 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
|
{created.strftime} Apply strftime template to file creation
|
||||||
date/time. Should be used in form
|
date/time. Should be used in form
|
||||||
{created.strftime,TEMPLATE} where TEMPLATE
|
{created.strftime,TEMPLATE} where TEMPLATE
|
||||||
@@ -449,22 +456,26 @@ Substitution Description
|
|||||||
templates.
|
templates.
|
||||||
{modified.date} Photo's modification date in ISO format,
|
{modified.date} Photo's modification date in ISO format,
|
||||||
e.g. '2020-03-22'
|
e.g. '2020-03-22'
|
||||||
{modified.year} 4-digit year of file modification time
|
{modified.year} 4-digit year of photo modification time
|
||||||
{modified.yy} 2-digit year of file modification time
|
{modified.yy} 2-digit year of photo modification time
|
||||||
{modified.mm} 2-digit month of the file modification time
|
{modified.mm} 2-digit month of the photo modification time
|
||||||
(zero padded)
|
(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
|
modification time
|
||||||
{modified.mon} Month abbreviation in the user's locale of
|
{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
|
{modified.dd} 2-digit day of the month (zero padded) of
|
||||||
the file modification time
|
the photo modification time
|
||||||
{modified.doy} 3-digit day of year (e.g Julian day) of file
|
{modified.dow} Day of week in user's locale of the photo
|
||||||
modification time, starting from 1 (zero
|
modification time
|
||||||
padded)
|
{modified.doy} 3-digit day of year (e.g Julian day) of
|
||||||
{modified.hour} 2-digit hour of the file modification time
|
photo modification time, starting from 1
|
||||||
{modified.min} 2-digit minute of the file modification time
|
(zero padded)
|
||||||
{modified.sec} 2-digit second of the file modification time
|
{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.
|
{today.date} Current date in iso format, e.g.
|
||||||
'2020-03-22'
|
'2020-03-22'
|
||||||
{today.year} 4-digit year of current date
|
{today.year} 4-digit year of current date
|
||||||
@@ -539,6 +550,8 @@ Substitution Description
|
|||||||
{label} Image categorization label associated with a photo
|
{label} Image categorization label associated with a photo
|
||||||
(Photos 5 only)
|
(Photos 5 only)
|
||||||
{label_normalized} All lower case version of 'label' (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
|
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.
|
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.
|
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`
|
#### `shared`
|
||||||
Returns True if photo is in a shared album, otherwise False.
|
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`
|
#### `isphoto`
|
||||||
Returns True if type is photo/still image, otherwise False
|
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:
|
`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
|
```python
|
||||||
{'Composite:Aperture': 2.2,
|
{'Composite:Aperture': 2.2,
|
||||||
'Composite:GPSPosition': '-34.9188916666667 138.596861111111',
|
'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:
|
- `setvalue(tag, value)`: write to the EXIF data in the photo file. To delete a tag, use setvalue with value = `None`. For example:
|
||||||
```python
|
```python
|
||||||
@@ -1305,7 +1329,7 @@ photo.exiftool.setvalue("XMP:Title", "Title of photo")
|
|||||||
photo.exiftool.addvalues("IPTC:Keywords", "vacation", "beach")
|
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`
|
#### `score`
|
||||||
Returns a [ScoreInfo](#scoreinfo) data class object which provides access to the computed aesthetic scores for each photo.
|
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.
|
**Note**: Valid only for Photos 5; returns None for earlier Photos versions.
|
||||||
|
|
||||||
#### `json()`
|
#### `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()`
|
||||||
`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)`
|
`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()`
|
#### `json()`
|
||||||
Returns a json string representation of the PersonInfo instance.
|
Returns a json string representation of the PersonInfo instance.
|
||||||
|
|
||||||
|
#### `asdict()`
|
||||||
|
Returns a dictionary representation of the PersonInfo instance.
|
||||||
|
|
||||||
### FaceInfo
|
### 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.
|
[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()`
|
#### `json()`
|
||||||
Returns a JSON representation of the FaceInfo instance.
|
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
|
### 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.
|
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|
|
|{person}|Person(s) / face(s) in a photo|
|
||||||
|{label}|Image categorization label associated with a photo (Photos 5 only)|
|
|{label}|Image categorization label associated with a photo (Photos 5 only)|
|
||||||
|{label_normalized}|All lower case version of 'label' (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
|
### Utility Functions
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ def main():
|
|||||||
if db:
|
if db:
|
||||||
print("loading database")
|
print("loading database")
|
||||||
tic = time.perf_counter()
|
tic = time.perf_counter()
|
||||||
photosdb = osxphotos.PhotosDB(dbfile=db)
|
photosdb = osxphotos.PhotosDB(dbfile=db, verbose=print)
|
||||||
toc = time.perf_counter()
|
toc = time.perf_counter()
|
||||||
print(f"done: took {toc-tic} seconds")
|
print(f"done: took {toc-tic} seconds")
|
||||||
return photosdb
|
return photosdb
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
from ._version import __version__
|
from ._version import __version__
|
||||||
from .photoinfo import PhotoInfo
|
from .photoinfo import PhotoInfo
|
||||||
from .photosdb import PhotosDB
|
from .photosdb import PhotosDB
|
||||||
|
from .photosdb._photosdb_process_comments import CommentInfo, LikeInfo
|
||||||
from .phototemplate import PhotoTemplate
|
from .phototemplate import PhotoTemplate
|
||||||
from .utils import _debug, _get_logger, _set_debug
|
from .utils import _debug, _get_logger, _set_debug
|
||||||
|
|
||||||
|
|||||||
@@ -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).",
|
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(),
|
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]:
|
for o in options[::-1]:
|
||||||
f = o(f)
|
f = o(f)
|
||||||
@@ -521,10 +525,15 @@ def cli(ctx, db, json_, debug):
|
|||||||
help="Use with '--dump photos' to dump only certain UUIDs",
|
help="Use with '--dump photos' to dump only certain UUIDs",
|
||||||
multiple=True,
|
multiple=True,
|
||||||
)
|
)
|
||||||
|
@click.option("--verbose", "-V", "verbose_", is_flag=True, help="Print verbose output.")
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
@click.pass_context
|
@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 """
|
""" Print out debug info """
|
||||||
|
|
||||||
|
global VERBOSE
|
||||||
|
VERBOSE = bool(verbose_)
|
||||||
|
|
||||||
db = get_photos_db(*photos_library, db, cli_obj.db)
|
db = get_photos_db(*photos_library, db, cli_obj.db)
|
||||||
if db is None:
|
if db is None:
|
||||||
click.echo(cli.commands["debug-dump"].get_help(ctx), err=True)
|
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()
|
start_t = time.perf_counter()
|
||||||
print(f"Opening database: {db}")
|
print(f"Opening database: {db}")
|
||||||
photosdb = osxphotos.PhotosDB(dbfile=db)
|
photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose)
|
||||||
stop_t = time.perf_counter()
|
stop_t = time.perf_counter()
|
||||||
print(f"Done; took {(stop_t-start_t):.2f} seconds")
|
print(f"Done; took {(stop_t-start_t):.2f} seconds")
|
||||||
|
|
||||||
@@ -977,6 +986,10 @@ def query(
|
|||||||
label,
|
label,
|
||||||
deleted,
|
deleted,
|
||||||
deleted_only,
|
deleted_only,
|
||||||
|
has_comment,
|
||||||
|
no_comment,
|
||||||
|
has_likes,
|
||||||
|
no_likes,
|
||||||
):
|
):
|
||||||
""" Query the Photos database using 1 or more search options;
|
""" Query the Photos database using 1 or more search options;
|
||||||
if more than one option is provided, they are treated as "AND"
|
if more than one option is provided, they are treated as "AND"
|
||||||
@@ -1021,6 +1034,8 @@ def query(
|
|||||||
(any(place), no_place),
|
(any(place), no_place),
|
||||||
(deleted, deleted_only),
|
(deleted, deleted_only),
|
||||||
(shared, not_shared),
|
(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
|
# 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(
|
if any(all(bb) for bb in exclusive) or not any(
|
||||||
@@ -1107,6 +1122,10 @@ def query(
|
|||||||
label=label,
|
label=label,
|
||||||
deleted=deleted,
|
deleted=deleted,
|
||||||
deleted_only=deleted_only,
|
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
|
# below needed for to make CliRunner work for testing
|
||||||
@@ -1390,6 +1409,10 @@ def export(
|
|||||||
edited_suffix,
|
edited_suffix,
|
||||||
place,
|
place,
|
||||||
no_place,
|
no_place,
|
||||||
|
has_comment,
|
||||||
|
no_comment,
|
||||||
|
has_likes,
|
||||||
|
no_likes,
|
||||||
no_extended_attributes,
|
no_extended_attributes,
|
||||||
label,
|
label,
|
||||||
deleted,
|
deleted,
|
||||||
@@ -1437,6 +1460,8 @@ def export(
|
|||||||
(skip_edited, skip_original_if_edited),
|
(skip_edited, skip_original_if_edited),
|
||||||
(export_as_hardlink, convert_to_jpeg),
|
(export_as_hardlink, convert_to_jpeg),
|
||||||
(shared, not_shared),
|
(shared, not_shared),
|
||||||
|
(has_comment, no_comment),
|
||||||
|
(has_likes, no_likes),
|
||||||
]
|
]
|
||||||
if any(all(bb) for bb in exclusive):
|
if any(all(bb) for bb in exclusive):
|
||||||
click.echo("Incompatible export options", err=True)
|
click.echo("Incompatible export options", err=True)
|
||||||
@@ -1575,6 +1600,10 @@ def export(
|
|||||||
label=label,
|
label=label,
|
||||||
deleted=deleted,
|
deleted=deleted,
|
||||||
deleted_only=deleted_only,
|
deleted_only=deleted_only,
|
||||||
|
has_comment=has_comment,
|
||||||
|
no_comment=no_comment,
|
||||||
|
has_likes=has_likes,
|
||||||
|
no_likes=no_likes,
|
||||||
)
|
)
|
||||||
|
|
||||||
if photos:
|
if photos:
|
||||||
@@ -1894,13 +1923,17 @@ def _query(
|
|||||||
label=None,
|
label=None,
|
||||||
deleted=False,
|
deleted=False,
|
||||||
deleted_only=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
|
""" run a query against PhotosDB to extract the photos based on user supply criteria
|
||||||
used by query and export commands
|
used by query and export commands
|
||||||
arguments must be passed in same order as query and export
|
arguments must be passed in same order as query and export
|
||||||
if either is modified, need to ensure all three functions are updated """
|
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:
|
if deleted or deleted_only:
|
||||||
photos = photosdb.photos(
|
photos = photosdb.photos(
|
||||||
uuid=uuid,
|
uuid=uuid,
|
||||||
@@ -2113,6 +2146,16 @@ def _query(
|
|||||||
if has_raw:
|
if has_raw:
|
||||||
photos = [p for p in photos if p.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
|
return photos
|
||||||
|
|
||||||
|
|
||||||
@@ -2230,7 +2273,7 @@ def export_photo(
|
|||||||
f"skipping {photo.original_filename}"
|
f"skipping {photo.original_filename}"
|
||||||
)
|
)
|
||||||
return ExportResults([], [], [], [], [], [])
|
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(
|
verbose(
|
||||||
f"Skipping missing {photo.original_filename}: not iCloud asset or missing from cloud"
|
f"Skipping missing {photo.original_filename}: not iCloud asset or missing from cloud"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
""" version info """
|
""" version info """
|
||||||
|
|
||||||
__version__ = "0.35.6"
|
__version__ = "0.36.2"
|
||||||
|
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ class ExifTool:
|
|||||||
ver = self.run_commands("-ver", no_file=True)
|
ver = self.run_commands("-ver", no_file=True)
|
||||||
return ver.decode("utf-8")
|
return ver.decode("utf-8")
|
||||||
|
|
||||||
def as_dict(self):
|
def asdict(self):
|
||||||
""" return dictionary of all EXIF tags and values from exiftool
|
""" return dictionary of all EXIF tags and values from exiftool
|
||||||
returns empty dict if no tags
|
returns empty dict if no tags
|
||||||
"""
|
"""
|
||||||
@@ -245,7 +245,7 @@ class ExifTool:
|
|||||||
|
|
||||||
def _read_exif(self):
|
def _read_exif(self):
|
||||||
""" read exif data from file """
|
""" read exif data from file """
|
||||||
data = self.as_dict()
|
data = self.asdict()
|
||||||
self.data = {k: v for k, v in data.items()}
|
self.data = {k: v for k, v in data.items()}
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|||||||
@@ -66,10 +66,10 @@ class PersonInfo:
|
|||||||
# no faces
|
# no faces
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def json(self):
|
def asdict(self):
|
||||||
""" Returns JSON representation of class instance """
|
""" Returns dictionary representation of class instance """
|
||||||
keyphoto = self.keyphoto.uuid if self.keyphoto is not None else None
|
keyphoto = self.keyphoto.uuid if self.keyphoto is not None else None
|
||||||
person = {
|
return {
|
||||||
"uuid": self.uuid,
|
"uuid": self.uuid,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"displayname": self.display_name,
|
"displayname": self.display_name,
|
||||||
@@ -77,7 +77,10 @@ class PersonInfo:
|
|||||||
"facecount": self.facecount,
|
"facecount": self.facecount,
|
||||||
"keyphoto": keyphoto,
|
"keyphoto": keyphoto,
|
||||||
}
|
}
|
||||||
return json.dumps(person)
|
|
||||||
|
def json(self):
|
||||||
|
""" Returns JSON representation of class instance """
|
||||||
|
return json.dumps(self.asdict())
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"PersonInfo(name={self.name}, display_name={self.display_name}, uuid={self.uuid}, facecount={self.facecount})"
|
return f"PersonInfo(name={self.name}, display_name={self.display_name}, uuid={self.uuid}, facecount={self.facecount})"
|
||||||
|
|||||||
17
osxphotos/photoinfo/_photoinfo_comments.py
Normal 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 []
|
||||||
@@ -636,8 +636,11 @@ def export2(
|
|||||||
exported = []
|
exported = []
|
||||||
# export live_photo .mov file?
|
# export live_photo .mov file?
|
||||||
live_photo = True if live_photo and self.live_photo else False
|
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
|
# 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:
|
if filename:
|
||||||
# use filename stem provided
|
# use filename stem provided
|
||||||
filestem = dest.stem
|
filestem = dest.stem
|
||||||
@@ -671,7 +674,6 @@ def export2(
|
|||||||
burst=self.burst,
|
burst=self.burst,
|
||||||
dry_run=dry_run,
|
dry_run=dry_run,
|
||||||
)
|
)
|
||||||
|
|
||||||
if exported:
|
if exported:
|
||||||
if touch_file:
|
if touch_file:
|
||||||
for exported_file in exported:
|
for exported_file in exported:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ PhotosDB.photos() returns a list of PhotoInfo objects
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
import datetime
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@@ -59,6 +60,7 @@ class PhotoInfo:
|
|||||||
ExportResults,
|
ExportResults,
|
||||||
)
|
)
|
||||||
from ._photoinfo_scoreinfo import score, ScoreInfo
|
from ._photoinfo_scoreinfo import score, ScoreInfo
|
||||||
|
from ._photoinfo_comments import comments, likes
|
||||||
|
|
||||||
def __init__(self, db=None, uuid=None, info=None):
|
def __init__(self, db=None, uuid=None, info=None):
|
||||||
self._uuid = uuid
|
self._uuid = uuid
|
||||||
@@ -949,22 +951,23 @@ class PhotoInfo:
|
|||||||
}
|
}
|
||||||
return yaml.dump(info, sort_keys=False)
|
return yaml.dump(info, sort_keys=False)
|
||||||
|
|
||||||
def json(self):
|
def asdict(self):
|
||||||
""" return JSON representation """
|
""" 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}
|
folders = {album.title: album.folder_names for album in self.album_info}
|
||||||
exif = dataclasses.asdict(self.exif_info) if self.exif_info else {}
|
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 {}
|
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,
|
"uuid": self.uuid,
|
||||||
"filename": self.filename,
|
"filename": self.filename,
|
||||||
"original_filename": self.original_filename,
|
"original_filename": self.original_filename,
|
||||||
"date": self.date.isoformat(),
|
"date": self.date,
|
||||||
"description": self.description,
|
"description": self.description,
|
||||||
"title": self.title,
|
"title": self.title,
|
||||||
"keywords": self.keywords,
|
"keywords": self.keywords,
|
||||||
@@ -973,6 +976,7 @@ class PhotoInfo:
|
|||||||
"albums": self.albums,
|
"albums": self.albums,
|
||||||
"folders": folders,
|
"folders": folders,
|
||||||
"persons": self.persons,
|
"persons": self.persons,
|
||||||
|
"faces": faces,
|
||||||
"path": self.path,
|
"path": self.path,
|
||||||
"ismissing": self.ismissing,
|
"ismissing": self.ismissing,
|
||||||
"hasadjustments": self.hasadjustments,
|
"hasadjustments": self.hasadjustments,
|
||||||
@@ -986,12 +990,13 @@ class PhotoInfo:
|
|||||||
"isphoto": self.isphoto,
|
"isphoto": self.isphoto,
|
||||||
"ismovie": self.ismovie,
|
"ismovie": self.ismovie,
|
||||||
"uti": self.uti,
|
"uti": self.uti,
|
||||||
|
"uti_original": self.uti_original,
|
||||||
"burst": self.burst,
|
"burst": self.burst,
|
||||||
"live_photo": self.live_photo,
|
"live_photo": self.live_photo,
|
||||||
"path_live_photo": self.path_live_photo,
|
"path_live_photo": self.path_live_photo,
|
||||||
"iscloudasset": self.iscloudasset,
|
"iscloudasset": self.iscloudasset,
|
||||||
"incloud": self.incloud,
|
"incloud": self.incloud,
|
||||||
"date_modified": date_modified_iso,
|
"date_modified": self.date_modified,
|
||||||
"portrait": self.portrait,
|
"portrait": self.portrait,
|
||||||
"screenshot": self.screenshot,
|
"screenshot": self.screenshot,
|
||||||
"slow_mo": self.slow_mo,
|
"slow_mo": self.slow_mo,
|
||||||
@@ -1000,6 +1005,8 @@ class PhotoInfo:
|
|||||||
"selfie": self.selfie,
|
"selfie": self.selfie,
|
||||||
"panorama": self.panorama,
|
"panorama": self.panorama,
|
||||||
"has_raw": self.has_raw,
|
"has_raw": self.has_raw,
|
||||||
|
"israw": self.israw,
|
||||||
|
"raw_original": self.raw_original,
|
||||||
"uti_raw": self.uti_raw,
|
"uti_raw": self.uti_raw,
|
||||||
"path_raw": self.path_raw,
|
"path_raw": self.path_raw,
|
||||||
"place": place,
|
"place": place,
|
||||||
@@ -1013,8 +1020,17 @@ class PhotoInfo:
|
|||||||
"original_width": self.original_width,
|
"original_width": self.original_width,
|
||||||
"original_orientation": self.original_orientation,
|
"original_orientation": self.original_orientation,
|
||||||
"original_filesize": self.original_filesize,
|
"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):
|
def __eq__(self, other):
|
||||||
""" Compare two PhotoInfo objects for equality """
|
""" Compare two PhotoInfo objects for equality """
|
||||||
|
|||||||
157
osxphotos/photosdb/_photosdb_process_comments.py
Normal 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()
|
||||||
@@ -44,6 +44,7 @@ from ..utils import (
|
|||||||
_get_os_version,
|
_get_os_version,
|
||||||
_open_sql_file,
|
_open_sql_file,
|
||||||
get_last_library_path,
|
get_last_library_path,
|
||||||
|
noop,
|
||||||
normalize_unicode,
|
normalize_unicode,
|
||||||
)
|
)
|
||||||
from .photosdb_utils import get_db_model_version, get_db_version
|
from .photosdb_utils import get_db_model_version, get_db_version
|
||||||
@@ -67,12 +68,19 @@ class PhotosDB:
|
|||||||
labels_normalized_as_dict,
|
labels_normalized_as_dict,
|
||||||
)
|
)
|
||||||
from ._photosdb_process_scoreinfo import _process_scoreinfo
|
from ._photosdb_process_scoreinfo import _process_scoreinfo
|
||||||
|
from ._photosdb_process_comments import _process_comments
|
||||||
|
|
||||||
def __init__(self, *dbfile_, dbfile=None):
|
def __init__(self, dbfile=None, verbose=None):
|
||||||
""" create a new PhotosDB object
|
""" 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
|
Args:
|
||||||
specify path to photos library or photos.db using named argument dbfile=path """
|
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
|
# Check OS version
|
||||||
system = platform.system()
|
system = platform.system()
|
||||||
@@ -84,6 +92,12 @@ class PhotosDB:
|
|||||||
f"you have {system}, OS version: {major}"
|
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
|
# create a temporary directory
|
||||||
# tempfile.TemporaryDirectory gets cleaned up when the object does
|
# tempfile.TemporaryDirectory gets cleaned up when the object does
|
||||||
self._tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
self._tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||||
@@ -216,25 +230,7 @@ class PhotosDB:
|
|||||||
if _debug():
|
if _debug():
|
||||||
logging.debug(f"dbfile = {dbfile}")
|
logging.debug(f"dbfile = {dbfile}")
|
||||||
|
|
||||||
# get the path to photos library database
|
if dbfile is None:
|
||||||
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:
|
|
||||||
dbfile = get_last_library_path()
|
dbfile = get_last_library_path()
|
||||||
if dbfile is None:
|
if dbfile is None:
|
||||||
# get_last_library_path must have failed to find library
|
# get_last_library_path must have failed to find library
|
||||||
@@ -262,11 +258,14 @@ class PhotosDB:
|
|||||||
# or photosanalysisd
|
# or photosanalysisd
|
||||||
self._dbfile = self._dbfile_actual = self._tmp_db = os.path.abspath(dbfile)
|
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
|
# 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
|
# Photos maintains an exclusive lock on the database file while Photos is open
|
||||||
# photoanalysisd sometimes maintains this lock even after Photos is closed
|
# photoanalysisd sometimes maintains this lock even after Photos is closed
|
||||||
# In those cases, make a temp copy of the file for sqlite3 to read
|
# In those cases, make a temp copy of the file for sqlite3 to read
|
||||||
if _db_is_locked(self._dbfile):
|
if _db_is_locked(self._dbfile):
|
||||||
|
verbose(f"Database locked, creating temporary copy.")
|
||||||
self._tmp_db = self._copy_db_file(self._dbfile)
|
self._tmp_db = self._copy_db_file(self._dbfile)
|
||||||
|
|
||||||
self._db_version = get_db_version(self._tmp_db)
|
self._db_version = get_db_version(self._tmp_db)
|
||||||
@@ -279,8 +278,10 @@ class PhotosDB:
|
|||||||
raise FileNotFoundError(f"dbfile {dbfile} does not exist", dbfile)
|
raise FileNotFoundError(f"dbfile {dbfile} does not exist", dbfile)
|
||||||
else:
|
else:
|
||||||
self._dbfile_actual = self._tmp_db = dbfile
|
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 database is exclusively locked, make a copy of it and use the copy
|
||||||
if _db_is_locked(self._dbfile_actual):
|
if _db_is_locked(self._dbfile_actual):
|
||||||
|
verbose(f"Database locked, creating temporary copy.")
|
||||||
self._tmp_db = self._copy_db_file(self._dbfile_actual)
|
self._tmp_db = self._copy_db_file(self._dbfile_actual)
|
||||||
|
|
||||||
if _debug():
|
if _debug():
|
||||||
@@ -549,10 +550,15 @@ class PhotosDB:
|
|||||||
""" process the Photos database to extract info
|
""" process the Photos database to extract info
|
||||||
works on Photos version <= 4.0 """
|
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)
|
(conn, c) = _open_sql_file(self._tmp_db)
|
||||||
|
|
||||||
# get info to associate persons with photos
|
# get info to associate persons with photos
|
||||||
# then get detected faces in each photo and link to persons
|
# then get detected faces in each photo and link to persons
|
||||||
|
verbose("Processing persons in photos.")
|
||||||
c.execute(
|
c.execute(
|
||||||
""" SELECT
|
""" SELECT
|
||||||
RKPerson.modelID,
|
RKPerson.modelID,
|
||||||
@@ -618,6 +624,7 @@ class PhotosDB:
|
|||||||
logging.debug(f"Unexpected KeyError _dbpersons_pk[{pk}]")
|
logging.debug(f"Unexpected KeyError _dbpersons_pk[{pk}]")
|
||||||
|
|
||||||
# get information on detected faces
|
# get information on detected faces
|
||||||
|
verbose("Processing detected faces in photos.")
|
||||||
c.execute(
|
c.execute(
|
||||||
""" SELECT
|
""" SELECT
|
||||||
RKPerson.modelID,
|
RKPerson.modelID,
|
||||||
@@ -655,6 +662,7 @@ class PhotosDB:
|
|||||||
logging.debug(pformat(self._dbfaces_uuid))
|
logging.debug(pformat(self._dbfaces_uuid))
|
||||||
|
|
||||||
# Get info on albums
|
# Get info on albums
|
||||||
|
verbose("Processing albums.")
|
||||||
c.execute(
|
c.execute(
|
||||||
""" SELECT
|
""" SELECT
|
||||||
RKAlbum.uuid,
|
RKAlbum.uuid,
|
||||||
@@ -797,6 +805,7 @@ class PhotosDB:
|
|||||||
logging.debug(pformat(self._dbfolder_details))
|
logging.debug(pformat(self._dbfolder_details))
|
||||||
|
|
||||||
# Get info on keywords
|
# Get info on keywords
|
||||||
|
verbose("Processing keywords.")
|
||||||
c.execute(
|
c.execute(
|
||||||
""" SELECT
|
""" SELECT
|
||||||
RKKeyword.name,
|
RKKeyword.name,
|
||||||
@@ -824,6 +833,7 @@ class PhotosDB:
|
|||||||
self._dbvolumes[vol[0]] = vol[1]
|
self._dbvolumes[vol[0]] = vol[1]
|
||||||
|
|
||||||
# Get photo details
|
# Get photo details
|
||||||
|
verbose("Processing photo details.")
|
||||||
if self._db_version < _PHOTOS_3_VERSION:
|
if self._db_version < _PHOTOS_3_VERSION:
|
||||||
# Photos < 3.0 doesn't have RKVersion.selfPortrait (selfie)
|
# Photos < 3.0 doesn't have RKVersion.selfPortrait (selfie)
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -1113,6 +1123,7 @@ class PhotosDB:
|
|||||||
self._dbphotos[uuid]["fok_import_session"] = None
|
self._dbphotos[uuid]["fok_import_session"] = None
|
||||||
|
|
||||||
# get additional details from RKMaster, needed for RAW processing
|
# get additional details from RKMaster, needed for RAW processing
|
||||||
|
verbose("Processing additional photo details.")
|
||||||
c.execute(
|
c.execute(
|
||||||
""" SELECT
|
""" SELECT
|
||||||
RKMaster.uuid,
|
RKMaster.uuid,
|
||||||
@@ -1286,6 +1297,7 @@ class PhotosDB:
|
|||||||
self._dbphotos[uuid]["incloud"] = True if row[2] == 1 else False
|
self._dbphotos[uuid]["incloud"] = True if row[2] == 1 else False
|
||||||
|
|
||||||
# get location data
|
# get location data
|
||||||
|
verbose("Processing location data.")
|
||||||
# get the country codes
|
# get the country codes
|
||||||
country_codes = c.execute(
|
country_codes = c.execute(
|
||||||
"SELECT modelID, countryCode "
|
"SELECT modelID, countryCode "
|
||||||
@@ -1372,6 +1384,7 @@ class PhotosDB:
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
# process faces
|
# process faces
|
||||||
|
verbose("Processing face details.")
|
||||||
self._process_faceinfo()
|
self._process_faceinfo()
|
||||||
|
|
||||||
# add faces and keywords to photo data
|
# add faces and keywords to photo data
|
||||||
@@ -1408,6 +1421,7 @@ class PhotosDB:
|
|||||||
self._dbphotos[uuid]["volume"] = None
|
self._dbphotos[uuid]["volume"] = None
|
||||||
|
|
||||||
# done processing, dump debug data if requested
|
# done processing, dump debug data if requested
|
||||||
|
verbose("Done processing details from Photos library.")
|
||||||
if _debug():
|
if _debug():
|
||||||
logging.debug("Faces (_dbfaces_uuid):")
|
logging.debug("Faces (_dbfaces_uuid):")
|
||||||
logging.debug(pformat(self._dbfaces_uuid))
|
logging.debug(pformat(self._dbfaces_uuid))
|
||||||
@@ -1483,12 +1497,14 @@ class PhotosDB:
|
|||||||
|
|
||||||
if _debug():
|
if _debug():
|
||||||
logging.debug(f"_process_database5")
|
logging.debug(f"_process_database5")
|
||||||
|
verbose = self._verbose
|
||||||
|
verbose(f"Processing database.")
|
||||||
(conn, c) = _open_sql_file(self._tmp_db)
|
(conn, c) = _open_sql_file(self._tmp_db)
|
||||||
|
|
||||||
# some of the tables/columns have different names in different versions of Photos
|
# some of the tables/columns have different names in different versions of Photos
|
||||||
photos_ver = get_db_model_version(self._tmp_db)
|
photos_ver = get_db_model_version(self._tmp_db)
|
||||||
self._photos_ver = photos_ver
|
self._photos_ver = photos_ver
|
||||||
|
verbose(f"Database version: {self._db_version}, {photos_ver}.")
|
||||||
asset_table = _DB_TABLE_NAMES[photos_ver]["ASSET"]
|
asset_table = _DB_TABLE_NAMES[photos_ver]["ASSET"]
|
||||||
keyword_join = _DB_TABLE_NAMES[photos_ver]["KEYWORD_JOIN"]
|
keyword_join = _DB_TABLE_NAMES[photos_ver]["KEYWORD_JOIN"]
|
||||||
album_join = _DB_TABLE_NAMES[photos_ver]["ALBUM_JOIN"]
|
album_join = _DB_TABLE_NAMES[photos_ver]["ALBUM_JOIN"]
|
||||||
@@ -1502,6 +1518,7 @@ class PhotosDB:
|
|||||||
|
|
||||||
# get info to associate persons with photos
|
# get info to associate persons with photos
|
||||||
# then get detected faces in each photo and link to persons
|
# then get detected faces in each photo and link to persons
|
||||||
|
verbose("Processing persons in photos.")
|
||||||
c.execute(
|
c.execute(
|
||||||
""" SELECT
|
""" SELECT
|
||||||
ZPERSON.Z_PK,
|
ZPERSON.Z_PK,
|
||||||
@@ -1567,6 +1584,7 @@ class PhotosDB:
|
|||||||
logging.debug(f"Unexpected KeyError _dbpersons_pk[{pk}]")
|
logging.debug(f"Unexpected KeyError _dbpersons_pk[{pk}]")
|
||||||
|
|
||||||
# get information on detected faces
|
# get information on detected faces
|
||||||
|
verbose("Processing detected faces in photos.")
|
||||||
c.execute(
|
c.execute(
|
||||||
f""" SELECT
|
f""" SELECT
|
||||||
ZPERSON.Z_PK,
|
ZPERSON.Z_PK,
|
||||||
@@ -1601,6 +1619,7 @@ class PhotosDB:
|
|||||||
logging.debug(pformat(self._dbfaces_uuid))
|
logging.debug(pformat(self._dbfaces_uuid))
|
||||||
|
|
||||||
# get details about albums
|
# get details about albums
|
||||||
|
verbose("Processing albums.")
|
||||||
c.execute(
|
c.execute(
|
||||||
f""" SELECT
|
f""" SELECT
|
||||||
ZGENERICALBUM.ZUUID,
|
ZGENERICALBUM.ZUUID,
|
||||||
@@ -1719,6 +1738,7 @@ class PhotosDB:
|
|||||||
logging.debug(pformat(self._dbalbum_folders))
|
logging.debug(pformat(self._dbalbum_folders))
|
||||||
|
|
||||||
# get details on keywords
|
# get details on keywords
|
||||||
|
verbose("Processing keywords.")
|
||||||
c.execute(
|
c.execute(
|
||||||
f"""SELECT ZKEYWORD.ZTITLE, {asset_table}.ZUUID
|
f"""SELECT ZKEYWORD.ZTITLE, {asset_table}.ZUUID
|
||||||
FROM {asset_table}
|
FROM {asset_table}
|
||||||
@@ -1750,6 +1770,7 @@ class PhotosDB:
|
|||||||
logging.debug(self._dbvolumes)
|
logging.debug(self._dbvolumes)
|
||||||
|
|
||||||
# get details about photos
|
# get details about photos
|
||||||
|
verbose("Processing photo details.")
|
||||||
logging.debug(f"Getting information about photos")
|
logging.debug(f"Getting information about photos")
|
||||||
c.execute(
|
c.execute(
|
||||||
f"""SELECT {asset_table}.ZUUID,
|
f"""SELECT {asset_table}.ZUUID,
|
||||||
@@ -1788,7 +1809,8 @@ class PhotosDB:
|
|||||||
ZADDITIONALASSETATTRIBUTES.ZORIGINALWIDTH,
|
ZADDITIONALASSETATTRIBUTES.ZORIGINALWIDTH,
|
||||||
ZADDITIONALASSETATTRIBUTES.ZORIGINALORIENTATION,
|
ZADDITIONALASSETATTRIBUTES.ZORIGINALORIENTATION,
|
||||||
ZADDITIONALASSETATTRIBUTES.ZORIGINALFILESIZE,
|
ZADDITIONALASSETATTRIBUTES.ZORIGINALFILESIZE,
|
||||||
{depth_state}
|
{depth_state},
|
||||||
|
{asset_table}.ZADJUSTMENTTIMESTAMP
|
||||||
FROM {asset_table}
|
FROM {asset_table}
|
||||||
JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = {asset_table}.Z_PK
|
JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = {asset_table}.Z_PK
|
||||||
ORDER BY {asset_table}.ZUUID """
|
ORDER BY {asset_table}.ZUUID """
|
||||||
@@ -1832,6 +1854,7 @@ class PhotosDB:
|
|||||||
# 34 ZADDITIONALASSETATTRIBUTES.ZORIGINALORIENTATION,
|
# 34 ZADDITIONALASSETATTRIBUTES.ZORIGINALORIENTATION,
|
||||||
# 35 ZADDITIONALASSETATTRIBUTES.ZORIGINALFILESIZE
|
# 35 ZADDITIONALASSETATTRIBUTES.ZORIGINALFILESIZE
|
||||||
# 36 ZGENERICASSET.ZDEPTHSTATES / ZASSET.ZDEPTHTYPE
|
# 36 ZGENERICASSET.ZDEPTHSTATES / ZASSET.ZDEPTHTYPE
|
||||||
|
# 37 ZGENERICASSET.ZADJUSTMENTTIMESTAMP -- when was photo edited?
|
||||||
|
|
||||||
for row in c:
|
for row in c:
|
||||||
uuid = row[0]
|
uuid = row[0]
|
||||||
@@ -1845,9 +1868,9 @@ class PhotosDB:
|
|||||||
# There are sometimes negative values for lastmodifieddate in the database
|
# 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
|
# I don't know what these mean but they will raise exception in datetime if
|
||||||
# not accounted for
|
# not accounted for
|
||||||
info["lastmodifieddate_timestamp"] = row[4]
|
info["lastmodifieddate_timestamp"] = row[37]
|
||||||
try:
|
try:
|
||||||
info["lastmodifieddate"] = datetime.fromtimestamp(row[4] + TIME_DELTA)
|
info["lastmodifieddate"] = datetime.fromtimestamp(row[37] + TIME_DELTA)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
info["lastmodifieddate"] = None
|
info["lastmodifieddate"] = None
|
||||||
except TypeError:
|
except TypeError:
|
||||||
@@ -1908,7 +1931,7 @@ class PhotosDB:
|
|||||||
info["type"] = None
|
info["type"] = None
|
||||||
|
|
||||||
info["UTI"] = row[18]
|
info["UTI"] = row[18]
|
||||||
info["UTI_original"] = None # filled in later
|
info["UTI_original"] = None # filled in later
|
||||||
|
|
||||||
# handle burst photos
|
# handle burst photos
|
||||||
# if burst photo, determine whether or not it's a selected burst photo
|
# if burst photo, determine whether or not it's a selected burst photo
|
||||||
@@ -2040,6 +2063,7 @@ class PhotosDB:
|
|||||||
# 1 ZGENERICASSET.ZIMPORTSESSION
|
# 1 ZGENERICASSET.ZIMPORTSESSION
|
||||||
# 2 ZGENERICASSET.Z_FOK_IMPORTSESSION
|
# 2 ZGENERICASSET.Z_FOK_IMPORTSESSION
|
||||||
# 3 ZGENERICALBUM.ZUUID,
|
# 3 ZGENERICALBUM.ZUUID,
|
||||||
|
verbose("Processing import sessions.")
|
||||||
c.execute(
|
c.execute(
|
||||||
f"""SELECT
|
f"""SELECT
|
||||||
{asset_table}.ZUUID,
|
{asset_table}.ZUUID,
|
||||||
@@ -2062,6 +2086,7 @@ class PhotosDB:
|
|||||||
logging.debug(f"No info record for uuid {uuid} for import session")
|
logging.debug(f"No info record for uuid {uuid} for import session")
|
||||||
|
|
||||||
# Get extended description
|
# Get extended description
|
||||||
|
verbose("Processing additional photo details.")
|
||||||
c.execute(
|
c.execute(
|
||||||
f"""SELECT {asset_table}.ZUUID,
|
f"""SELECT {asset_table}.ZUUID,
|
||||||
ZASSETDESCRIPTION.ZLONGDESCRIPTION
|
ZASSETDESCRIPTION.ZLONGDESCRIPTION
|
||||||
@@ -2241,18 +2266,27 @@ class PhotosDB:
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
# process face info
|
# process face info
|
||||||
|
verbose("Processing face details.")
|
||||||
self._process_faceinfo()
|
self._process_faceinfo()
|
||||||
|
|
||||||
# process search info
|
# process search info
|
||||||
|
verbose("Processing photo labels.")
|
||||||
self._process_searchinfo()
|
self._process_searchinfo()
|
||||||
|
|
||||||
# process exif info
|
# process exif info
|
||||||
|
verbose("Processing EXIF details.")
|
||||||
self._process_exifinfo()
|
self._process_exifinfo()
|
||||||
|
|
||||||
# process computed scores
|
# process computed scores
|
||||||
|
verbose("Processing computed aesthetic scores.")
|
||||||
self._process_scoreinfo()
|
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
|
# done processing, dump debug data if requested
|
||||||
|
verbose("Done processing details from Photos library.")
|
||||||
if _debug():
|
if _debug():
|
||||||
logging.debug("Faces (_dbfaces_uuid):")
|
logging.debug("Faces (_dbfaces_uuid):")
|
||||||
logging.debug(pformat(self._dbfaces_uuid))
|
logging.debug(pformat(self._dbfaces_uuid))
|
||||||
|
|||||||
@@ -30,33 +30,34 @@ TEMPLATE_SUBSTITUTIONS = {
|
|||||||
"{title}": "Title of the photo",
|
"{title}": "Title of the photo",
|
||||||
"{descr}": "Description of the photo",
|
"{descr}": "Description of the photo",
|
||||||
"{created.date}": "Photo's creation date in ISO format, e.g. '2020-03-22'",
|
"{created.date}": "Photo's creation date in ISO format, e.g. '2020-03-22'",
|
||||||
"{created.year}": "4-digit year of file creation time",
|
"{created.year}": "4-digit year of photo creation time",
|
||||||
"{created.yy}": "2-digit year of file creation time",
|
"{created.yy}": "2-digit year of photo creation time",
|
||||||
"{created.mm}": "2-digit month of the file creation time (zero padded)",
|
"{created.mm}": "2-digit month of the photo creation time (zero padded)",
|
||||||
"{created.month}": "Month name in user's locale of the file creation time",
|
"{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",
|
"{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 file 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 file 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.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 file creation time",
|
"{created.hour}": "2-digit hour of the photo creation time",
|
||||||
"{created.min}": "2-digit minute of the file creation time",
|
"{created.min}": "2-digit minute of the photo creation time",
|
||||||
"{created.sec}": "2-digit second of the file 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}": "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,TEMPLATE} where TEMPLATE is a valid strftime template, e.g. "
|
||||||
+ "{created.strftime,%Y-%U} would result in year-week number of year: '2020-23'. "
|
+ "{created.strftime,%Y-%U} would result in year-week number of year: '2020-23'. "
|
||||||
+ "If used with no template will return null value. "
|
+ "If used with no template will return null value. "
|
||||||
+ "See https://strftime.org/ for help on strftime templates.",
|
+ "See https://strftime.org/ for help on strftime templates.",
|
||||||
"{modified.date}": "Photo's modification date in ISO format, e.g. '2020-03-22'",
|
"{modified.date}": "Photo's modification date in ISO format, e.g. '2020-03-22'",
|
||||||
"{modified.year}": "4-digit year of file modification time",
|
"{modified.year}": "4-digit year of photo modification time",
|
||||||
"{modified.yy}": "2-digit year of file modification time",
|
"{modified.yy}": "2-digit year of photo modification time",
|
||||||
"{modified.mm}": "2-digit month of the file modification time (zero padded)",
|
"{modified.mm}": "2-digit month of the photo modification time (zero padded)",
|
||||||
"{modified.month}": "Month name in user's locale of the file modification time",
|
"{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",
|
"{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 file modification time",
|
"{modified.dd}": "2-digit day of the month (zero padded) of the photo modification time",
|
||||||
"{modified.doy}": "3-digit day of year (e.g Julian day) of file modification time, starting from 1 (zero padded)",
|
"{modified.dow}": "Day of week in user's locale of the photo modification time",
|
||||||
"{modified.hour}": "2-digit hour of the file modification time",
|
"{modified.doy}": "3-digit day of year (e.g Julian day) of photo modification time, starting from 1 (zero padded)",
|
||||||
"{modified.min}": "2-digit minute of the file modification time",
|
"{modified.hour}": "2-digit hour of the photo modification time",
|
||||||
"{modified.sec}": "2-digit second of the file 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}": "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,TEMPLATE} where TEMPLATE is a valid strftime template, e.g. "
|
||||||
# + "{modified.strftime,%Y-%U} would result in year-week number of year: '2020-23'. "
|
# + "{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",
|
"{person}": "Person(s) / face(s) in a photo",
|
||||||
"{label}": "Image categorization label associated with a photo (Photos 5 only)",
|
"{label}": "Image categorization label associated with a photo (Photos 5 only)",
|
||||||
"{label_normalized}": "All lower case version of 'label' (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
|
# Just the multi-valued substitution names without the braces
|
||||||
@@ -244,14 +246,14 @@ class PhotoTemplate:
|
|||||||
# '2011/Album2/keyword1/person1',
|
# '2011/Album2/keyword1/person1',
|
||||||
# '2011/Album2/keyword2/person1',]
|
# '2011/Album2/keyword2/person1',]
|
||||||
|
|
||||||
rendered_strings = set([rendered])
|
rendered_strings = [rendered]
|
||||||
for field in MULTI_VALUE_SUBSTITUTIONS:
|
for field in MULTI_VALUE_SUBSTITUTIONS:
|
||||||
# Build a regex that matches only the field being processed
|
# Build a regex that matches only the field being processed
|
||||||
re_str = r"(?<!\\)\{(" + field + r")(,(([\w\-\%. ]{0,})))?\}"
|
re_str = r"(?<!\\)\{(" + field + r")(,(([\w\-\%. ]{0,})))?\}"
|
||||||
regex_multi = re.compile(re_str)
|
regex_multi = re.compile(re_str)
|
||||||
|
|
||||||
# holds each of the new rendered_strings, set() to avoid duplicates
|
# holds each of the new rendered_strings, dict to avoid repeats (dict.keys())
|
||||||
new_strings = set()
|
new_strings = {}
|
||||||
|
|
||||||
for str_template in rendered_strings:
|
for str_template in rendered_strings:
|
||||||
if regex_multi.search(str_template):
|
if regex_multi.search(str_template):
|
||||||
@@ -307,10 +309,10 @@ class PhotoTemplate:
|
|||||||
self, none_str, get_func=lookup_template_value_multi
|
self, none_str, get_func=lookup_template_value_multi
|
||||||
)
|
)
|
||||||
new_string = regex_multi.sub(subst, str_template)
|
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
|
# 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
|
# find any {fields} that weren't replaced
|
||||||
unmatched = []
|
unmatched = []
|
||||||
@@ -444,6 +446,12 @@ class PhotoTemplate:
|
|||||||
if self.photo.date_modified
|
if self.photo.date_modified
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
elif field == "modified.dow":
|
||||||
|
value = (
|
||||||
|
DateTimeFormatter(self.photo.date_modified).dow
|
||||||
|
if self.photo.date_modified
|
||||||
|
else None
|
||||||
|
)
|
||||||
elif field == "modified.doy":
|
elif field == "modified.doy":
|
||||||
value = (
|
value = (
|
||||||
DateTimeFormatter(self.photo.date_modified).doy
|
DateTimeFormatter(self.photo.date_modified).doy
|
||||||
@@ -637,6 +645,10 @@ class PhotoTemplate:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
values.append(album.title)
|
values.append(album.title)
|
||||||
|
elif field == "comment":
|
||||||
|
values = [
|
||||||
|
f"{comment.user}: {comment.text}" for comment in self.photo.comments
|
||||||
|
]
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unhandled template value: {field}")
|
raise ValueError(f"Unhandled template value: {field}")
|
||||||
|
|
||||||
|
|||||||
@@ -491,7 +491,7 @@ class PlaceInfo4(PlaceInfo):
|
|||||||
}
|
}
|
||||||
return "PlaceInfo(" + ", ".join([f"{k}='{v}'" for k, v in info.items()]) + ")"
|
return "PlaceInfo(" + ", ".join([f"{k}='{v}'" for k, v in info.items()]) + ")"
|
||||||
|
|
||||||
def as_dict(self):
|
def asdict(self):
|
||||||
return {
|
return {
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"names": self.names._asdict(),
|
"names": self.names._asdict(),
|
||||||
@@ -634,7 +634,7 @@ class PlaceInfo5(PlaceInfo):
|
|||||||
}
|
}
|
||||||
return "PlaceInfo(" + ", ".join([f"{k}='{v}'" for k, v in info.items()]) + ")"
|
return "PlaceInfo(" + ", ".join([f"{k}='{v}'" for k, v in info.items()]) + ")"
|
||||||
|
|
||||||
def as_dict(self):
|
def asdict(self):
|
||||||
return {
|
return {
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"names": self.names._asdict(),
|
"names": self.names._asdict(),
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ def _debug():
|
|||||||
""" returns True if debugging turned on (via _set_debug), otherwise, false """
|
""" returns True if debugging turned on (via _set_debug), otherwise, false """
|
||||||
return _DEBUG
|
return _DEBUG
|
||||||
|
|
||||||
|
def noop(*args, **kwargs):
|
||||||
|
""" do nothing (no operation) """
|
||||||
|
pass
|
||||||
|
|
||||||
def _get_os_version():
|
def _get_os_version():
|
||||||
# returns tuple containing OS version
|
# returns tuple containing OS version
|
||||||
|
|||||||
@@ -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>
|
||||||
BIN
tests/Test-10.15.7.photoslibrary/database/Photos.sqlite
Normal file
BIN
tests/Test-10.15.7.photoslibrary/database/Photos.sqlite-shm
Normal file
BIN
tests/Test-10.15.7.photoslibrary/database/Photos.sqlite-wal
Normal file
16
tests/Test-10.15.7.photoslibrary/database/Photos.sqlite.lock
Normal 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>
|
||||||
BIN
tests/Test-10.15.7.photoslibrary/database/metaSchema.db
Normal file
BIN
tests/Test-10.15.7.photoslibrary/database/photos.db
Normal file
BIN
tests/Test-10.15.7.photoslibrary/database/search/psi.sqlite
Normal file
BIN
tests/Test-10.15.7.photoslibrary/database/search/psi.sqlite-shm
Normal 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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
After Width: | Height: | Size: 574 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 2.9 MiB |
|
After Width: | Height: | Size: 500 KiB |
|
After Width: | Height: | Size: 524 KiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 2.8 MiB |
|
After Width: | Height: | Size: 528 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 450 KiB |
|
After Width: | Height: | Size: 541 KiB |
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||