Compare commits

..

40 Commits

Author SHA1 Message Date
Rhet Turnbull
e3c40bcbaa Cleaned up comments and unneeded test code 2020-02-08 07:28:47 -08:00
Rhet Turnbull
69addc3464 removed commented out code 2020-02-07 22:26:11 -08:00
Rhet Turnbull
c654e3dc61 Fixed bug in --download-missing to fix issue #64 2020-02-07 22:20:05 -08:00
Rhet Turnbull
1e013b6802 Updated CHANGELOG.md 2020-02-01 08:25:36 -08:00
Rhet Turnbull
640471eba9 Updated README.md 2020-02-01 08:22:30 -08:00
Rhet Turnbull
c346003059 Updated README.md 2020-02-01 08:19:58 -08:00
Rhet Turnbull
46d3c7dbda Added PhotosDB() behavior to open last library if no args passed but also added cautionary note to README 2020-02-01 08:16:20 -08:00
Rhet Turnbull
476e094365 cleanup 2020-02-01 07:48:30 -08:00
Rhet Turnbull
0bb579ee87 Updated help strings 2020-02-01 07:46:30 -08:00
Rhet Turnbull
91d5729bea Slight refactor to PhotosDB.photos() 2020-02-01 07:35:55 -08:00
Rhet Turnbull
fdf636ac88 Updated photos_repl.py 2020-02-01 07:09:36 -08:00
Rhet Turnbull
b6fe2b55e0 Updated documentation 2020-01-31 20:15:52 -08:00
Rhet Turnbull
6e563e214c Test library updates 2020-01-30 05:40:24 -08:00
Rhet Turnbull
ac8be51156 Updated PhotosDB to only copy database if locked, speed improvement for cases where DB not locked; closes #34 2020-01-30 05:38:11 -08:00
Rhet Turnbull
27994c9fd3 Removed _tmp_file code that's no longer needed 2020-01-29 05:33:47 -08:00
Rhet Turnbull
b9c360cd20 Updated _open_sql_file to use URI and read-only mode 2020-01-28 22:20:29 -08:00
Rhet Turnbull
f50cdd5403 Changed temp file handling to use tempfile.TemporaryDirectory, closes #59 2020-01-28 05:20:17 -08:00
Rhet Turnbull
1c792a371f Updated test for bad db 2020-01-28 05:08:53 -08:00
Rhet Turnbull
8e11e237ef Documentation update 2020-01-26 21:38:14 -08:00
Rhet Turnbull
675867a3d3 Updated README.md Dependencies 2020-01-26 21:26:24 -08:00
Rhet Turnbull
f910124fe1 Updated CHANGELOG.md 2020-01-26 20:22:56 -08:00
Rhet Turnbull
e79cb92693 Updated CLI options with more descriptive metavar names 2020-01-26 20:16:51 -08:00
Rhet Turnbull
f7c8b457a5 Added XMP sidecar option to export, closes #51 2020-01-26 19:58:56 -08:00
Rhet Turnbull
4dfb131a21 Added XMP sidecar to export 2020-01-26 09:34:53 -08:00
Rhet Turnbull
ba224af3fb Added Subject to JSON sidecar to match info Photo exports in XMP 2020-01-26 08:09:17 -08:00
Rhet Turnbull
4a1adaa156 Test library updates, closes #52 2020-01-25 10:25:06 -08:00
Rhet Turnbull
9ee96c55e0 changed a few warnings to debug messages for missing paths 2020-01-25 08:40:42 -08:00
Rhet Turnbull
1261425d00 Bug fixes for path_live_photo and lastmodifieddate 2020-01-25 08:37:49 -08:00
Rhet Turnbull
06cefcc7d2 Merge branch 'master' of https://github.com/RhetTbull/osxphotos 2020-01-25 08:10:41 -08:00
Rhet Turnbull
67b0ae0bf6 Added date_modified to PhotoInfo 2020-01-25 08:10:27 -08:00
Rhet Turnbull
4d36b3b31f Added date_modified to PhotoInfo 2020-01-25 08:07:50 -08:00
Rhet Turnbull
fd8d466e05 Test DB updates 2020-01-22 22:12:39 -08:00
Rhet Turnbull
898d3afc08 Merge branch 'master' of https://github.com/RhetTbull/osxphotos 2020-01-22 22:10:10 -08:00
Rhet Turnbull
a0fd52deea Merge pull request #58 from hshore29/master
Corrected Panorama Flag
2020-01-22 22:08:52 -08:00
hshore29
9ce4566d0f Panorama from CustomRenderedValue 2020-01-22 08:43:35 -05:00
hshore29
46c6b6d130 Merge pull request #1 from RhetTbull/master
Jan 20 Updates
2020-01-21 22:03:07 -05:00
Rhet Turnbull
ab1d95e458 Added instructions to photos_repl.py 2020-01-20 22:05:29 -08:00
Rhet Turnbull
db5effde52 Added photos_repl.py to examples 2020-01-20 08:59:52 -08:00
Rhet Turnbull
50b7e6920a CLI now looks for photos library to use if non specified by user 2020-01-20 08:26:32 -08:00
Rhet Turnbull
d37e6d9725 Updated CHANGELOG.md 2020-01-20 08:05:32 -08:00
358 changed files with 2448 additions and 402 deletions

View File

@@ -4,6 +4,44 @@ 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.22.9](https://github.com/RhetTbull/osxphotos/compare/v0.22.7...v0.22.9)
> 1 February 2020
- Updated PhotosDB to only copy database if locked, speed improvement for cases where DB not locked; closes #34 [`#34`](https://github.com/RhetTbull/osxphotos/issues/34)
- Changed temp file handling to use tempfile.TemporaryDirectory, closes #59 [`#59`](https://github.com/RhetTbull/osxphotos/issues/59)
- Slight refactor to PhotosDB.photos() [`91d5729`](https://github.com/RhetTbull/osxphotos/commit/91d5729beaa0f0c2583e6320b18d958429e66075)
- Test library updates [`6e563e2`](https://github.com/RhetTbull/osxphotos/commit/6e563e214c569ba7838f7464de9258c3bba5db23)
- Removed _tmp_file code that's no longer needed [`27994c9`](https://github.com/RhetTbull/osxphotos/commit/27994c9fd372303833a5794f1de9815f425c762e)
- Updated photos_repl.py [`fdf636a`](https://github.com/RhetTbull/osxphotos/commit/fdf636ac8864ebb2cc324b1f9d3c6c82ee3910f9)
- Updated CHANGELOG.md [`f910124`](https://github.com/RhetTbull/osxphotos/commit/f910124fe1fbf75d44c09c79607374bf000733a1)
#### [v0.22.7](https://github.com/RhetTbull/osxphotos/compare/v0.22.4...v0.22.7)
> 27 January 2020
- Corrected Panorama Flag [`#58`](https://github.com/RhetTbull/osxphotos/pull/58)
- Jan 20 Updates [`#1`](https://github.com/RhetTbull/osxphotos/pull/1)
- Added XMP sidecar option to export, closes #51 [`#51`](https://github.com/RhetTbull/osxphotos/issues/51)
- Test library updates, closes #52 [`#52`](https://github.com/RhetTbull/osxphotos/issues/52)
- Added XMP sidecar to export [`4dfb131`](https://github.com/RhetTbull/osxphotos/commit/4dfb131a21b1b1efefe3b918ecb06fc6fcb03f2c)
- Added date_modified to PhotoInfo [`67b0ae0`](https://github.com/RhetTbull/osxphotos/commit/67b0ae0bf679815372d415c3064e21d46a5b8718)
- Added date_modified to PhotoInfo [`4d36b3b`](https://github.com/RhetTbull/osxphotos/commit/4d36b3b31f3e0e74d9d111b6b691771e19f94086)
- Updated CLI options with more descriptive metavar names [`e79cb92`](https://github.com/RhetTbull/osxphotos/commit/e79cb92693758c984dc789d5fa5d2e87e381e921)
- CLI now looks for photos library to use if non specified by user [`50b7e69`](https://github.com/RhetTbull/osxphotos/commit/50b7e6920a694aa45f478d1131868525c9147919)
#### [v0.22.4](https://github.com/RhetTbull/osxphotos/compare/v0.22.0...v0.22.4)
> 20 January 2020
- Add --from-date and --to-date to query and export command [`#57`](https://github.com/RhetTbull/osxphotos/pull/57)
- Refactor CLI [`#55`](https://github.com/RhetTbull/osxphotos/pull/55)
- Refactor cli: singular --db, --json and query options. [`e214746`](https://github.com/RhetTbull/osxphotos/commit/e214746063271e6f9f586286103ed051ada49d85)
- Implement from_date and to_date in PhotosDB as well as query and export command. Some refactoring of CLI as well. [`cfa2b4a`](https://github.com/RhetTbull/osxphotos/commit/cfa2b4a828facf0aff5bc19f777457ad776c4a05)
- Refactored _query. Still hairy, but less so. [`b9dee49`](https://github.com/RhetTbull/osxphotos/commit/b9dee4995c6d89fadb3d2482374b7098f2ab5ed9)
- Updated README.md [`0aff83f`](https://github.com/RhetTbull/osxphotos/commit/0aff83ff21c20e293c0b75bacf2863090a0fb725)
- Started adding tests for CLI [`f0b18c3`](https://github.com/RhetTbull/osxphotos/commit/f0b18c3d29b2141d348be0495013c51c072c6251)
#### [v0.22.0](https://github.com/RhetTbull/osxphotos/compare/v0.21.5...v0.22.0)
> 18 January 2020

View File

@@ -87,17 +87,22 @@ Options:
--db <Photos database path> Specify Photos database path. Path to Photos
library/database can be specified using
either --db or directly as PHOTOS_LIBRARY
positional argument.
--keyword TEXT Search for keyword(s).
--person TEXT Search for person(s).
--album TEXT Search for album(s).
--uuid TEXT Search for UUID(s).
--title TEXT Search for TEXT in title of photo.
positional argument. If neither --db or
PHOTOS_LIBRARY provided, will attempt to
find the library to use in the following
order: 1. last opened library, 2. system
library, 3. ~/Pictures/Photos
Library.photoslibrary
--keyword KEYWORD Search for keyword(s).
--person PERSON Search for person(s).
--album ALBUM Search for album(s).
--uuid UUID Search for UUID(s).
--title TITLE Search for TITLE in title of photo.
--no-title Search for photos with no title.
--description TEXT Search for TEXT in description of photo.
--description DESC Search for DESC in description of photo.
--no-description Search for photos with no description.
--uti TEXT Search for photos whose uniform type
identifier (UTI) matches TEXT
--uti UTI Search for photos whose uniform type
identifier (UTI) matches UTI
-i, --ignore-case Case insensitive search for title or
description. Does not apply to keyword,
person, or album.
@@ -150,17 +155,17 @@ Options:
extension.
--original-name Use photo's original filename instead of
current filename for export.
--sidecar Create JSON sidecar for each photo exported
in format useable by exiftool
(https://exiftool.org/) The sidecar file can
be used to apply metadata to the file with
exiftool, for example: "exiftool
-j=photoname.jpg.json photoname.jpg" The
sidecar file is named in format
photoname.ext.json where ext is extension of
the photo (e.g. jpg). Note: this does not
create an XMP sidecar as used by Lightroom,
etc.
--sidecar FORMAT Create sidecar for each photo exported;
valid FORMAT values: xmp, json; --sidecar
json: create JSON sidecar useable by
exiftool (https://exiftool.org/) The sidecar
file can be used to apply metadata to the
file with exiftool, for example: "exiftool
-j=photoname.json photoname.jpg" The sidecar
file is named in format photoname.json
--sidecar xmp: create XMP sidecar used by
Adobe Lightroom, etc. The sidecar file is
named in format photoname.xmp
--download-missing Attempt to download missing photos from
iCloud. The current implementation uses
Applescript to interact with Photos to
@@ -263,6 +268,7 @@ if __name__ == "__main__":
#### Read a Photos library database
```python
osxphotos.PhotosDB() # not recommended, see Note below
osxphotos.PhotosDB(path)
osxphotos.PhotosDB(dbfile=path)
```
@@ -271,9 +277,13 @@ 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)
If an invalid path is passed, PhotosDB will raise `ValueError` exception.
If an invalid path is passed, PhotosDB will raise `FileNotFoundError` exception.
Open the default (last opened) Photos library. (E.g. this is the library that would open if the user opened Photos.app)
**Note**: If neither path or dbfile is passed, PhotosDB will use get_last_library_path to open the last opened Photos library. This usually works but is not 100% reliable. It can also lead to loading a different library than expected if the user has held down *option* key when opening Photos to switch libraries. It is therefore recommended you explicitely pass the path to `PhotosDB()`.
#### Open the default (last opened) Photos library
The default library is the library that would open if the user opened Photos.app.
```python
import osxphotos
@@ -539,7 +549,10 @@ Returns the current filename of the photo on disk. See also [original_filename]
Returns the original filename of the photo when it was imported to Photos. **Note**: Photos 5.0+ renames the photo when it adds the file to the library using UUID. See also [filename](#filename)
#### `date`
Returns the date of the photo as a datetime.datetime object
Returns the create date of the photo as a datetime.datetime object
#### `date_modified`
Returns the modification date of the photo as a datetime.datetime object or None if photo has no modification date
#### `description`
Returns the description of the photo
@@ -642,15 +655,17 @@ Returns the path to the live video component of a [live photo](#live_photo). If
#### `json()`
Returns a JSON representation of all photo info
#### `export(dest, *filename, edited=False, overwrite=False, increment=True, sidecar=False, use_photos_export=False, timeout=120)`
#### `export(dest, *filename, edited=False, live_photo=False, overwrite=False, increment=True, sidecar_json=False, sidecar_xmp=False, use_photos_export=False, timeout=120,)`
Export photo from the Photos library to another destination on disk.
- dest: must be valid destination path as str (or exception raised).
- *filename (optional): name of picture as str; if not provided, will use current filename
- edited: boolean; if True (default=False), will export the edited version of the photo (or raise exception if no edited version)
- overwrite: boolean; if True (default=False), will overwrite files if they alreay exist
- increment: boolean; if True (default=True), will increment file name until a non-existant name is found
- sidecar: boolean; if True (default=False) will also write a json sidecar file with EXIF data in format readable by [exiftool](https://exiftool.org/); filename will be dest/filename.ext.json where ext is suffix of the image file (e.g. jpeg or jpg). Note: this is not an XMP sidecar.
- live_photo: boolean; if True (default=False), will also export the associted .mov for live photos; exported live photo will be named filename.mov
- increment: boolean; if True (default=True), will increment file name until a non-existent name is found
- sidecar_json: (boolean, default = False); if True will also write a json sidecar with IPTC data in format readable by exiftool; sidecar filename will be dest/filename.json where filename is the stem of the photo name
- sidecar_xmp: (boolean, default = False); if True will also write a XMP sidecar with IPTC data; sidecar filename will be dest/filename.xmp where filename is the stem of the photo name
- use_photos_export: boolean; (default=False), if True will attempt to export photo via applescript interaction with Photos; useful for forcing download of missing photos. This only works if the Photos library being used is the default library (last opened by Photos) as applescript will directly interact with whichever library Photos is currently using.
- timeout: (int, default=120) timeout in seconds used with use_photos_export
@@ -661,12 +676,12 @@ import osxphotos
photosdb = osxphotos.PhotosDB("/Users/smith/Pictures/Photos Library.photoslibrary")
photos = photosdb.photos()
photos[0].export("/tmp","photo_name.jpg",sidecar=True)
photos[0].export("/tmp","photo_name.jpg",sidecar_json=True)
```
Then
`exiftool -j=photo_name.jpg.json photo_name.jpg`
`exiftool -j=photo_name.json photo_name.jpg`
If overwrite=False and increment=False, export will fail if destination file already exists
@@ -781,6 +796,7 @@ Apple does provide a framework ([PhotoKit](https://developer.apple.com/documenta
- [PyObjC](https://pythonhosted.org/pyobjc/)
- [PyYAML](https://pypi.org/project/PyYAML/)
- [Click](https://pypi.org/project/click/)
- [Mako](https://www.makotemplates.org/)
## Acknowledgements
This project was originally inspired by [photo-export](https://github.com/patrikhson/photo-export) by Patrick Fältström, Copyright (c) 2015 Patrik Fältström paf@frobbit.se

48
examples/photos_repl.py Executable file
View File

@@ -0,0 +1,48 @@
#!/usr/bin/env python3 -i
# Open an interactive REPL with photosdb and photos defined
# as osxphotos.PhotosDB() and PhotosDB.photos respectively
# useful for debugging or exploring the Photos database
# If you run this using python from command line, do so with -i flag:
# python3 -i examples/photos_repl.py
import sys
import time
# click needed since this uses a couple of functions from CLI (__main__.py)
import click
import osxphotos
from osxphotos.__main__ import get_photos_db, _list_libraries
def main():
db = None
if len(sys.argv) > 1:
db = sys.argv[1]
else:
db = get_photos_db()
if db:
print("loading database")
tic = time.perf_counter()
photosdb = osxphotos.PhotosDB(dbfile=db)
toc = time.perf_counter()
print(f"done: took {toc-tic} seconds")
return photosdb
else:
_list_libraries()
sys.exit()
if __name__ == "__main__":
print(f"osxphotos version: {osxphotos._version.__version__}")
photosdb = main()
print(f"database version: {photosdb.db_version}")
print("getting photos")
tic = time.perf_counter()
photos = photosdb.photos(images=True, movies=True)
toc = time.perf_counter()
print(f"found {len(photos)} photos in {toc-tic} seconds")

View File

@@ -17,38 +17,36 @@ from .utils import create_path_by_date, _copy_file
def get_photos_db(*db_options):
""" Return path to photos db, select first non-None arg
""" Return path to photos db, select first non-None db_options
If no db_options are non-None, try to find library to use in
the following order:
- last library opened
- system library
- ~/Pictures/Photos Library.photoslibrary
- failing above, returns None
"""
if db_options:
for db in db_options:
if db is not None:
return db
# _list_libraries()
return None
# if get here, no valid database paths passed, so try to figure out which to use
db = osxphotos.utils.get_last_library_path()
if db is not None:
click.echo(f"Using last opened Photos library: {db}", err=True)
return db
# if get here, no valid database paths passed, so ask user
db = osxphotos.utils.get_system_library_path()
if db is not None:
click.echo(f"Using system Photos library: {db}", err=True)
return db
# _, major, _ = osxphotos.utils._get_os_version()
# last_lib = osxphotos.utils.get_last_library_path()
# if last_lib is not None:
# db = last_lib
# return db
# sys_lib = None
# if int(major) >= 15:
# sys_lib = osxphotos.utils.get_system_library_path()
# if sys_lib is not None:
# db = sys_lib
# return db
# db = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
# if os.path.isdir(db):
# return db
# else:
# return None ### TODO: put list here
db = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
if os.path.isdir(db):
click.echo(f"Using Photos library: {db}", err=True)
return db
else:
return None
# Click CLI object & context settings
@@ -69,7 +67,9 @@ DB_OPTION = click.option(
help=(
"Specify Photos database path. "
"Path to Photos library/database can be specified using either --db "
"or directly as PHOTOS_LIBRARY positional argument."
"or directly as PHOTOS_LIBRARY positional argument. "
"If neither --db or PHOTOS_LIBRARY provided, will attempt to find the library "
"to use in the following order: 1. last opened library, 2. system library, 3. ~/Pictures/Photos Library.photoslibrary"
),
type=click.Path(exists=True),
)
@@ -89,22 +89,51 @@ JSON_OPTION = click.option(
def query_options(f):
o = click.option
options = [
o("--keyword", default=None, multiple=True, help="Search for keyword(s)."),
o("--person", default=None, multiple=True, help="Search for person(s)."),
o("--album", default=None, multiple=True, help="Search for album(s)."),
o("--uuid", default=None, multiple=True, help="Search for UUID(s)."),
o(
"--title",
"--keyword",
metavar="KEYWORD",
default=None,
multiple=True,
help="Search for TEXT in title of photo.",
help="Search for keyword KEYWORD. "
'If more than one keyword, treated as "OR", e.g. find photos match any keyword',
),
o(
"--person",
metavar="PERSON",
default=None,
multiple=True,
help="Search for person PERSON. "
'If more than one person, treated as "OR", e.g. find photos match any person',
),
o(
"--album",
metavar="ALBUM",
default=None,
multiple=True,
help="Search for album ALBUM. "
'If more than one album, treated as "OR", e.g. find photos match any album',
),
o(
"--uuid",
metavar="UUID",
default=None,
multiple=True,
help="Search for UUID(s).",
),
o(
"--title",
metavar="TITLE",
default=None,
multiple=True,
help="Search for TITLE in title of photo.",
),
o("--no-title", is_flag=True, help="Search for photos with no title."),
o(
"--description",
metavar="DESC",
default=None,
multiple=True,
help="Search for TEXT in description of photo.",
help="Search for DESC in description of photo.",
),
o(
"--no-description",
@@ -113,9 +142,10 @@ def query_options(f):
),
o(
"--uti",
metavar="UTI",
default=None,
multiple=False,
help="Search for photos whose uniform type identifier (UTI) matches TEXT",
help="Search for photos whose uniform type identifier (UTI) matches UTI",
),
o(
"-i",
@@ -328,16 +358,6 @@ def info(ctx, cli_obj, db, json_, photos_library):
persons = pdb.persons_as_dict
# handle empty person names (added by Photos 5.0+ when face detected but not identified)
# TODO: remove this
# noperson = "UNKNOWN"
# if "" in persons:
# if noperson in persons:
# persons[noperson].append(persons[""])
# else:
# persons[noperson] = persons[""]
# persons.pop("", None)
info["persons_count"] = len(persons)
info["persons"] = persons
@@ -622,13 +642,17 @@ def query(
)
@click.option(
"--sidecar",
is_flag=True,
help="Create JSON sidecar for each photo exported "
f"in format useable by exiftool ({_EXIF_TOOL_URL}) "
default=None,
multiple=True,
metavar="FORMAT",
type=click.Choice(["xmp", "json"], case_sensitive=False),
help="Create sidecar for each photo exported; valid FORMAT values: xmp, json; "
f"--sidecar json: create JSON sidecar useable by exiftool ({_EXIF_TOOL_URL}) "
"The sidecar file can be used to apply metadata to the file with exiftool, for example: "
'"exiftool -j=photoname.jpg.json photoname.jpg" '
"The sidecar file is named in format photoname.ext.json where ext is extension of the photo (e.g. jpg). "
"Note: this does not create an XMP sidecar as used by Lightroom, etc.",
'"exiftool -j=photoname.json photoname.jpg" '
"The sidecar file is named in format photoname.json "
"--sidecar xmp: create XMP sidecar used by Adobe Lightroom, etc."
"The sidecar file is named in format photoname.xmp",
)
@click.option(
"--download-missing",
@@ -863,9 +887,11 @@ def print_photo_info(photos, json=False):
"path_live_photo",
"iscloudasset",
"incloud",
"date_modified",
]
)
for p in photos:
date_modified_iso = p.date_modified.isoformat() if p.date_modified else None
dump.append(
[
p.uuid,
@@ -895,6 +921,7 @@ def print_photo_info(photos, json=False):
p.path_live_photo,
p.iscloudasset,
p.incloud,
date_modified_iso,
]
)
for row in dump:
@@ -1057,7 +1084,7 @@ def export_photo(
dest: destination path as string
verbose: boolean; print verbose output
export_by_date: boolean; create export folder in form dest/YYYY/MM/DD
sidecar: boolean; create json sidecar file with export
sidecar: list zero, 1 or 2 of ["json","xmp"] of sidecar variety to export
overwrite: boolean; overwrite dest file if it already exists
original_name: boolean; use original filename instead of current filename
export_live: boolean; also export live video component if photo is a live photo
@@ -1097,10 +1124,19 @@ def export_photo(
date_created = photo.date.timetuple()
dest = create_path_by_date(dest, date_created)
sidecar = [s.lower() for s in sidecar]
sidecar_json = sidecar_xmp = False
if "json" in sidecar:
sidecar_json = True
if "xmp" in sidecar:
sidecar_xmp = True
photo_path = photo.export(
dest,
filename,
sidecar=sidecar,
sidecar_json=sidecar_json,
sidecar_xmp=sidecar_xmp,
live_photo=export_live,
overwrite=overwrite,
use_photos_export=download_missing,
)
@@ -1116,7 +1152,8 @@ def export_photo(
photo.export(
dest,
edited_name,
sidecar=sidecar,
sidecar_json=sidecar_json,
sidecar_xmp=sidecar_xmp,
overwrite=overwrite,
edited=True,
use_photos_export=download_missing,
@@ -1124,23 +1161,6 @@ def export_photo(
else:
click.echo(f"Skipping missing edited photo for {filename}")
if export_live and photo.live_photo and photo.path_live_photo is not None:
# if destination exists, will be overwritten regardless of overwrite
# so that name matches name of live photo
live_name = pathlib.Path(photo_path)
live_name = f"{live_name.stem}.mov"
src_live = photo.path_live_photo
dest_live = pathlib.Path(photo_path).parent / pathlib.Path(live_name)
if src_live is not None:
if verbose:
click.echo(f"Exporting live photo video of {filename} as {live_name}")
_copy_file(src_live, str(dest_live))
else:
click.echo(f"Skipping missing live movie for {filename}")
return photo_path

View File

@@ -2,6 +2,7 @@
Constants used by osxphotos
"""
import os.path
# which Photos library database versions have been tested
# Photos 2.0 (10.12.6) == 2622
@@ -30,3 +31,6 @@ _PHOTOS_5_SHARED_PHOTO_PATH = "resources/cloudsharing/data"
_PHOTO_TYPE = 0
_MOVIE_TYPE = 1
# Name of XMP template file
_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "templates")
_XMP_TEMPLATE_NAME = "xmp_sidecar.mako"

View File

@@ -1,3 +1,3 @@
""" version info """
__version__ = "0.22.4"
__version__ = "0.22.10"

View File

@@ -4,6 +4,7 @@ Represents a single photo in the Photos library and provides access to the photo
PhotosDB.photos() returns a list of PhotoInfo objects
"""
import glob
import json
import logging
import os.path
@@ -15,22 +16,23 @@ from datetime import timedelta, timezone
from pprint import pformat
import yaml
from mako.template import Template
from ._constants import (
_MOVIE_TYPE,
_PHOTO_TYPE,
_PHOTOS_5_SHARED_PHOTO_PATH,
_PHOTOS_5_VERSION,
_TEMPLATE_DIR,
_XMP_TEMPLATE_NAME,
)
from .utils import (
_copy_file,
_export_photo_uuid_applescript,
_get_resource_loc,
dd_to_dms_str,
_export_photo_uuid_applescript,
)
# TODO: check pylint output
class PhotoInfo:
"""
@@ -64,6 +66,20 @@ class PhotoInfo:
imagedate_utc = imagedate.astimezone(tz=tz)
return imagedate_utc
@property
def date_modified(self):
""" image modification date as timezone aware datetime object
or None if no modification date set """
imagedate = self._info["lastmodifieddate"]
if imagedate:
seconds = self._info["imageTimeZoneOffsetSeconds"] or 0
delta = timedelta(seconds=seconds)
tz = timezone(delta)
imagedate_utc = imagedate.astimezone(tz=tz)
return imagedate_utc
else:
return None
@property
def tzoffset(self):
""" timezone offset from UTC in seconds """
@@ -170,12 +186,12 @@ class PhotoInfo:
# check again to see if we found a valid file
if not os.path.isfile(photopath):
logging.warning(
logging.debug(
f"MISSING PATH: edited file for UUID {self._uuid} should be at {photopath} but does not appear to exist"
)
photopath = None
else:
logging.warning(
logging.debug(
f"{self.uuid} hasAdjustments but edit_resource_id is None"
)
photopath = None
@@ -214,7 +230,7 @@ class PhotoInfo:
)
if not os.path.isfile(photopath):
logging.warning(
logging.debug(
f"edited file for UUID {self._uuid} should be at {photopath} but does not appear to exist"
)
photopath = None
@@ -423,6 +439,7 @@ class PhotoInfo:
if self.live_photo and not self.ismissing:
filename = pathlib.Path(self.path)
photopath = filename.parent.joinpath(f"{filename.stem}_3.mov")
photopath = str(photopath)
if not os.path.isfile(photopath):
# In testing, I've seen occasional missing movie for live photo
# these appear to be valid -- e.g. video component not yet downloaded from iCloud
@@ -441,9 +458,11 @@ class PhotoInfo:
dest,
*filename,
edited=False,
live_photo=False,
overwrite=False,
increment=True,
sidecar=False,
sidecar_json=False,
sidecar_xmp=False,
use_photos_export=False,
timeout=120,
):
@@ -452,11 +471,14 @@ class PhotoInfo:
filename: (optional): name of picture; if not provided, will use current filename
edited: (boolean, default=False); if True will export the edited version of the photo
(or raise exception if no edited version)
live_photo: (boolean, default=False); if True, will also export the associted .mov for live photos
overwrite: (boolean, default=False); if True will overwrite files if they alreay exist
increment: (boolean, default=True); if True, will increment file name until a non-existant name is found
if overwrite=False and increment=False, export will fail if destination file already exists
sidecar: (boolean, default = False); if True will also write a json sidecar with EXIF data in format readable by exiftool
sidecar filename will be dest/filename.ext.json where ext is suffix of the image file (e.g. jpeg or jpg)
sidecar_json: (boolean, default = False); if True will also write a json sidecar with IPTC data in format readable by exiftool
sidecar filename will be dest/filename.json
sidecar_xmp: (boolean, default = False); if True will also write a XMP sidecar with IPTC data
sidecar filename will be dest/filename.xmp
use_photos_export: (boolean, default=False); if True will attempt to export photo via applescript interaction with Photos
timeout: (int, default=120) timeout in seconds used with use_photos_export
returns the full path to the exported file """
@@ -483,7 +505,7 @@ class PhotoInfo:
# no filename provided so use the default
# if edited file requested, use filename but add _edited
# need to use file extension from edited file as Photos saves a jpeg once edited
if edited:
if edited and not use_photos_export:
# verify we have a valid path_edited and use that to get filename
if not self.path_edited:
raise FileNotFoundError(
@@ -504,13 +526,20 @@ class PhotoInfo:
dest = dest / filename
# check to see if file exists and if so, add (1), (2), etc until we find one that works
# Photos checks the stem and adds (1), (2), etc which avoids collision with sidecars
# e.g. exporting sidecar for file1.png and file1.jpeg
# if file1.png exists and exporting file1.jpeg,
# dest will be file1 (1).jpeg even though file1.jpeg doesn't exist to prevent sidecar collision
if increment and not overwrite:
count = 1
dest_new = dest
while dest_new.exists():
dest_new = dest.parent / f"{dest.stem} ({count}){dest.suffix}"
glob_str = str(dest.parent / f"{dest.stem}*")
dest_files = glob.glob(glob_str)
dest_files = [pathlib.Path(f).stem for f in dest_files]
dest_new = dest.stem
while dest_new in dest_files:
dest_new = f"{dest.stem} ({count})"
count += 1
dest = dest_new
dest = dest.parent / f"{dest_new}{dest.suffix}"
# if overwrite==False and #increment==False, export should fail if file exists
if dest.exists() and not overwrite and not increment:
@@ -557,38 +586,100 @@ class PhotoInfo:
# copy the file, _copy_file uses ditto to preserve Mac extended attributes
_copy_file(src, dest)
# copy live photo associated .mov if requested
if live_photo and self.live_photo:
live_name = dest.parent / f"{dest.stem}.mov"
src_live = self.path_live_photo
if src_live is not None:
logging.debug(
f"Exporting live photo video of {filename} as {live_name.name}"
)
_copy_file(src_live, str(live_name))
else:
logging.warning(f"Skipping missing live movie for {filename}")
else:
# use_photo_export
exported = None
# export live_photo .mov file?
live_photo = True if live_photo and self.live_photo else False
if edited:
# exported edited version and not original
exported = _export_photo_uuid_applescript(
self.uuid, dest, original=False, edited=True, timeout=timeout
)
if filename:
# use filename stem provided
filestem = dest.stem
else:
# didn't get passed a filename, add _edited
filestem = f"{dest.stem}_edited"
exported = _export_photo_uuid_applescript(
self.uuid,
dest.parent,
filestem=filestem,
original=False,
edited=True,
live_photo=live_photo,
timeout=timeout,
)
else:
# export original version and not edited
filestem = dest.stem
exported = _export_photo_uuid_applescript(
self.uuid, dest, original=True, edited=False, timeout=timeout
self.uuid,
dest.parent,
filestem=filestem,
original=True,
edited=False,
live_photo=live_photo,
timeout=timeout,
)
if exported is None:
logging.warning(f"Error exporting photo {self.uuid} to {dest}")
if sidecar:
if sidecar_json:
logging.debug("writing exiftool_json_sidecar")
sidecar_filename = f"{dest}.json"
json_sidecar_str = self._exiftool_json_sidecar()
sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}.json")
sidecar_str = self._exiftool_json_sidecar()
try:
self._write_sidecar_car(sidecar_filename, json_sidecar_str)
self._write_sidecar(sidecar_filename, sidecar_str)
except Exception as e:
logging.critical(f"Error writing json sidecar to {sidecar_filename}")
logging.warning(f"Error writing json sidecar to {sidecar_filename}")
raise e
if sidecar_xmp:
logging.debug("writing xmp_sidecar")
sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}.xmp")
sidecar_str = self._xmp_sidecar()
try:
self._write_sidecar(sidecar_filename, sidecar_str)
except Exception as e:
logging.warning(f"Error writing xmp sidecar to {sidecar_filename}")
raise e
return str(dest)
def _exiftool_json_sidecar(self):
""" return json string of EXIF details in exiftool sidecar format """
""" return json string of EXIF details in exiftool sidecar format
Does not include all the EXIF fields as those are likely already in the image
Exports the following:
FileName
ImageDescription
Description
Title
TagsList
Keywords
Subject
PersonInImage
GPSLatitude, GPSLongitude
GPSPosition
GPSLatitudeRef, GPSLongitudeRef
DateTimeOriginal
OffsetTimeOriginal
ModifyDate """
exif = {}
exif["_CreatedBy"] = "osxphotos, https://github.com/RhetTbull/osxphotos"
exif["FileName"] = self.filename
if self.description:
@@ -599,10 +690,17 @@ class PhotoInfo:
exif["Title"] = self.title
if self.keywords:
exif["TagsList"] = exif["Keywords"] = self.keywords
exif["TagsList"] = exif["Keywords"] = list(self.keywords)
# Photos puts both keywords and persons in Subject when using "Export IPTC as XMP"
exif["Subject"] = list(self.keywords)
if self.persons:
exif["PersonInImage"] = self.persons
# Photos puts both keywords and persons in Subject when using "Export IPTC as XMP"
if "Subject" in exif:
exif["Subject"].extend(self.persons)
else:
exif["Subject"] = self.persons
# if self.favorite():
# exif["Rating"] = 5
@@ -630,20 +728,39 @@ class PhotoInfo:
exif["DateTimeOriginal"] = datetimeoriginal
exif["OffsetTimeOriginal"] = offsettime
if self.date_modified is not None:
exif["ModifyDate"] = self.date_modified.strftime("%Y:%m:%d %H:%M:%S")
json_str = json.dumps([exif])
return json_str
def _write_sidecar_car(self, filename, json_str):
if not filename and not json_str:
def _xmp_sidecar(self):
""" returns string for XMP sidecar """
# TODO: add additional fields to XMP file?
xmp_template = Template(
filename=os.path.join(_TEMPLATE_DIR, _XMP_TEMPLATE_NAME)
)
xmp_str = xmp_template.render(photo=self)
# remove extra lines that mako inserts from template
xmp_str = "\n".join(
[line for line in xmp_str.split("\n") if line.strip() != ""]
)
return xmp_str
def _write_sidecar(self, filename, sidecar_str):
""" write sidecar_str to filename
used for exporting sidecar info """
if not filename and not sidecar_str:
raise (
ValueError(
f"filename {filename} and json_str {json_str} must not be None"
f"filename {filename} and sidecar_str {sidecar_str} must not be None"
)
)
# TODO: catch exception?
f = open(filename, "w")
f.write(json_str)
f.write(sidecar_str)
f.close()
@property
@@ -660,11 +777,18 @@ class PhotoInfo:
return f"osxphotos.{self.__class__.__name__}(db={self._db}, uuid='{self._uuid}', info={self._info})"
def __str__(self):
""" string representation of PhotoInfo object """
date_iso = self.date.isoformat()
date_modified_iso = (
self.date_modified.isoformat() if self.date_modified else None
)
info = {
"uuid": self.uuid,
"filename": self.filename,
"original_filename": self.original_filename,
"date": str(self.date),
"date": date_iso,
"description": self.description,
"title": self.title,
"keywords": self.keywords,
@@ -688,11 +812,17 @@ class PhotoInfo:
"path_live_photo": self.path_live_photo,
"iscloudasset": self.iscloudasset,
"incloud": self.incloud,
"date_modified": date_modified_iso,
}
return yaml.dump(info, sort_keys=False)
def json(self):
""" return JSON representation """
date_modified_iso = (
self.date_modified.isoformat() if self.date_modified else None
)
pic = {
"uuid": self.uuid,
"filename": self.filename,
@@ -721,6 +851,7 @@ class PhotoInfo:
"path_live_photo": self.path_live_photo,
"iscloudasset": self.iscloudasset,
"incloud": self.incloud,
"date_modified": date_modified_iso,
}
return json.dumps(pic)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,99 @@
<!-- Created with osxphotos https://github.com/RhetTbull/osxphotos -->
<%def name="dc_description(desc)">
% if desc is None:
<dc:description></dc:description>
% else:
<dc:description>${desc}</dc:description>
% endif
</%def>
<%def name="dc_title(title)">
% if title is None:
<dc:title></dc:title>
% else:
<dc:title>${title}</dc:title>
% endif
</%def>
<%def name="dc_subject(subject)">
% if subject:
<!-- keywords and persons listed in <dc:subject> as Photos does -->
<dc:subject>
<rdf:Seq>
% for subj in subject:
<rdf:li>${subj}</rdf:li>
% endfor
</rdf:Seq>
</dc:subject>
% endif
</%def>
<%def name="dc_datecreated(date)">
% if date is not None:
<photoshop:DateCreated>${date.isoformat()}</photoshop:DateCreated>
% endif
</%def>
<%def name="iptc_personinimage(persons)">
% if persons:
<Iptc4xmpExt:PersonInImage>
<rdf:Bag>
% for person in persons:
<rdf:li>${person}</rdf:li>
% endfor
</rdf:Bag>
</Iptc4xmpExt:PersonInImage>
% endif
</%def>
<%def name="dk_tagslist(keywords)">
% if keywords:
<digiKam:TagsList>
<rdf:Seq>
% for keyword in keywords:
<rdf:li>${keyword}</rdf:li>
% endfor
</rdf:Seq>
</digiKam:TagsList>
% endif
</%def>
<%def name="adobe_createdate(date)">
% if date is not None:
<xmp:CreateDate>${date.strftime("%Y-%m-%dT%H:%M:%S")}</xmp:CreateDate>
% endif
</%def>
<%def name="adobe_modifydate(date)">
% if date is not None:
<xmp:ModifyDate>${date.strftime("%Y-%m-%dT%H:%M:%S")}</xmp:ModifyDate>
% endif
</%def>
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 5.4.0">
<!-- mirrors Photos 5 "Export IPTC as XMP" option -->
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/">
${dc_description(photo.description)}
${dc_title(photo.title)}
${dc_subject(photo.keywords + photo.persons)}
${dc_datecreated(photo.date)}
</rdf:Description>
<rdf:Description rdf:about=''
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
${iptc_personinimage(photo.persons)}
</rdf:Description>
<rdf:Description rdf:about=''
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
${dk_tagslist(photo.keywords)}
</rdf:Description>
<rdf:Description rdf:about=''
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
${adobe_createdate(photo.date)}
${adobe_modifydate(photo.date)}
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>

View File

@@ -2,10 +2,11 @@ import glob
import logging
import os.path
import platform
import sqlite3
import subprocess
import tempfile
import urllib.parse
from pathlib import Path
import pathlib
from plistlib import load as plistload
import CoreFoundation
@@ -177,8 +178,8 @@ def get_system_library_path():
)
return None
plist_file = Path(
str(Path.home())
plist_file = pathlib.Path(
str(pathlib.Path.home())
+ "/Library/Containers/com.apple.photolibraryd/Data/Library/Preferences/com.apple.photolibraryd.plist"
)
if plist_file.is_file():
@@ -200,8 +201,8 @@ def get_system_library_path():
def get_last_library_path():
""" returns the path to the last opened Photos library
If a library has never been opened, returns None """
plist_file = Path(
str(Path.home())
plist_file = pathlib.Path(
str(pathlib.Path.home())
+ "/Library/Containers/com.apple.Photos/Data/Library/Preferences/com.apple.Photos.plist"
)
if plist_file.is_file():
@@ -308,17 +309,32 @@ def create_path_by_date(dest, dt):
def _export_photo_uuid_applescript(
uuid, dest, original=True, edited=False, timeout=120
uuid,
dest,
filestem=None,
original=True,
edited=False,
live_photo=False,
timeout=120,
):
""" Export photo to dest path using applescript to control Photos
If photo is a live photo, exports both the photo and associated .mov file
uuid: UUID of photo to export
dest: destination path to export to; may be either a directory or a filename
if filename provided and file exists, exiting file will be overwritten
dest: destination path to export to
filestem: (string) if provided, exported filename will be named stem.ext
where ext is extension of the file exported by photos (e.g. .jpeg, .mov, etc)
If not provided, file will be named with whatever name Photos uses
If filestem.ext exists, it wil be overwritten
original: (boolean) if True, export original image; default = True
edited: (boolean) if True, export edited photo; default = False
will produce an error if image does not have edits/adjustments
will produce an error if image does not have edits/adjustments
*Note*: must be called with either edited or original but not both,
will raise error if called with both edited and original = True
live_photo: (boolean) if True, export associated .mov live photo; default = False
timeout: timeout value in seconds; export will fail if applescript run time exceeds timeout
Returns: path to exported file or None if export failed
Returns: list of paths to exported file(s) or None if export failed
Note: For Live Photos, if edited=True, will export a jpeg but not the movie, even if photo
has not been edited. This is due to how Photos Applescript interface works.
"""
# setup the applescript to do the export
@@ -351,6 +367,13 @@ def _export_photo_uuid_applescript(
"""
)
dest = pathlib.Path(dest)
if not dest.is_dir:
raise ValueError(f"dest {dest} must be a directory")
if not original ^ edited:
raise ValueError(f"edited or original must be True but not both")
tmpdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
# export original
@@ -365,14 +388,63 @@ def _export_photo_uuid_applescript(
if filename is not None:
# need to find actual filename as sometimes Photos renames JPG to jpeg on export
# this assumes only a single file in export folder, which should be true as
# may be more than one file exported (e.g. if Live Photo, Photos exports both .jpeg and .mov)
# TemporaryDirectory will cleanup on return
path = glob.glob(os.path.join(tmpdir.name, "*"))[0]
_copy_file(path, dest)
if os.path.isdir(dest):
new_path = os.path.join(dest, filename)
else:
new_path = dest
return new_path
files = glob.glob(os.path.join(tmpdir.name, "*"))
exported_paths = []
for fname in files:
path = pathlib.Path(fname)
if len(files) > 1 and not live_photo and path.suffix.lower() == ".mov":
# it's the .mov part of live photo but not requested, so don't export
logging.debug(f"Skipping live photo file {path}")
continue
if filestem:
# rename the file based on filestem, keeping original extension
dest_new = dest / f"{filestem}{path.suffix}"
else:
# use the name Photos provided
dest_new = dest / path.name
logging.debug(f"exporting {path} to dest_new: {dest_new}")
_copy_file(str(path), str(dest_new))
exported_paths.append(str(dest_new))
return exported_paths
else:
return None
def _open_sql_file(dbname):
""" opens sqlite file dbname in read-only mode
returns tuple of (connection, cursor) """
try:
dbpath = pathlib.Path(dbname).resolve()
conn = sqlite3.connect(f"{dbpath.as_uri()}?mode=ro", timeout=1, uri=True)
c = conn.cursor()
except sqlite3.Error as e:
sys.exit(f"An error occurred opening sqlite file: {e.args[0]} {dbname}")
return (conn, c)
def _db_is_locked(dbname):
""" check to see if a sqlite3 db is locked
returns True if database is locked, otherwise False
dbname: name of database to test """
# first, check to see if lock file exists, if so, assume the file is locked
lock_name = f"{dbname}.lock"
if os.path.exists(lock_name):
logging.debug(f"{dbname} is locked")
return True
# no lock file so try to read from the database to see if it's locked
locked = None
try:
(conn, c) = _open_sql_file(dbname)
c.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;")
conn.close()
logging.debug(f"{dbname} is not locked")
locked = False
except Exception as e:
logging.debug(f"{dbname} is locked")
locked = True
return locked

View File

@@ -143,3 +143,4 @@ termcolor==1.1.0
wcwidth==0.1.7
wrapt==1.11.1
zipp==0.5.2
Mako==1.1.1

View File

@@ -61,6 +61,6 @@ setup(
"Programming Language :: Python :: 3.6",
"Topic :: Software Development :: Libraries :: Python Modules",
],
install_requires=["pyobjc>=6.0.1", "Click>=7", "PyYAML>=5.1.2"],
install_requires=["pyobjc>=6.0.1", "Click>=7", "PyYAML>=5.1.2", "Mako>=1.1.1"],
entry_points={"console_scripts": ["osxphotos=osxphotos.__main__:cli"]},
)

View File

@@ -3,8 +3,8 @@
<plist version="1.0">
<dict>
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
<date>2020-01-11T03:40:24Z</date>
<date>2020-01-22T02:10:26Z</date>
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
<date>2020-01-12T02:07:27Z</date>
<date>2020-01-22T02:10:27Z</date>
</dict>
</plist>

View File

@@ -11,6 +11,6 @@
<key>PLLastRevGeoForcedProviderOutOfDateCheckVersionKey</key>
<integer>1</integer>
<key>PLLastRevGeoVerFileFetchDateKey</key>
<date>2020-01-11T03:42:50Z</date>
<date>2020-01-19T17:29:28Z</date>
</dict>
</plist>

View File

@@ -3,8 +3,8 @@
<plist version="1.0">
<dict>
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
<date>2020-01-11T16:41:00Z</date>
<date>2020-01-29T06:24:15Z</date>
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
<date>2020-01-12T08:20:23Z</date>
<date>2020-01-29T13:44:20Z</date>
</dict>
</plist>

View File

@@ -5,7 +5,7 @@
<key>LithiumMessageTracer</key>
<dict>
<key>LastReportedDate</key>
<date>2020-01-04T18:29:59Z</date>
<date>2020-01-19T14:48:46Z</date>
</dict>
</dict>
</plist>

View File

@@ -11,6 +11,6 @@
<key>PLLastRevGeoForcedProviderOutOfDateCheckVersionKey</key>
<integer>1</integer>
<key>PLLastRevGeoVerFileFetchDateKey</key>
<date>2020-01-11T16:40:56Z</date>
<date>2020-01-29T06:24:08Z</date>
</dict>
</plist>

View File

@@ -24,7 +24,7 @@
<key>SnapshotCompletedDate</key>
<date>2019-07-27T13:16:43Z</date>
<key>SnapshotLastValidated</key>
<date>2020-01-12T06:04:41Z</date>
<date>2020-01-29T06:26:14Z</date>
<key>SnapshotTables</key>
<dict/>
</dict>

View File

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

View File

@@ -3,24 +3,24 @@
<plist version="1.0">
<dict>
<key>BackgroundHighlightCollection</key>
<date>2020-01-11T15:41:21Z</date>
<date>2020-01-30T02:33:23Z</date>
<key>BackgroundHighlightEnrichment</key>
<date>2020-01-11T15:41:20Z</date>
<date>2020-01-30T02:33:23Z</date>
<key>BackgroundJobAssetRevGeocode</key>
<date>2020-01-11T15:41:24Z</date>
<date>2020-01-30T04:13:27Z</date>
<key>BackgroundJobSearch</key>
<date>2020-01-11T15:41:23Z</date>
<date>2020-01-30T02:33:24Z</date>
<key>BackgroundPeopleSuggestion</key>
<date>2020-01-11T15:41:20Z</date>
<date>2020-01-30T02:33:23Z</date>
<key>BackgroundUserBehaviorProcessor</key>
<date>2020-01-11T15:41:24Z</date>
<date>2020-01-30T02:33:24Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphConsistencyUpdateJobDateKey</key>
<date>2020-01-11T15:44:27Z</date>
<date>2020-01-30T04:13:27Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
<date>2020-01-11T15:41:20Z</date>
<date>2020-01-30T02:33:23Z</date>
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
<date>2020-01-11T15:41:26Z</date>
<date>2020-01-30T02:33:24Z</date>
<key>SiriPortraitDonation</key>
<date>2020-01-11T15:41:25Z</date>
<date>2020-01-30T02:33:24Z</date>
</dict>
</plist>

View File

@@ -3,8 +3,8 @@
<plist version="1.0">
<dict>
<key>FaceIDModelLastGenerationKey</key>
<date>2020-01-11T15:41:26Z</date>
<date>2020-01-30T02:33:24Z</date>
<key>LastContactClassificationKey</key>
<date>2020-01-11T15:41:31Z</date>
<date>2020-01-30T02:33:26Z</date>
</dict>
</plist>

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@@ -7,9 +7,14 @@
<key>ExpandedSidebarItemIdentifiers</key>
<array>
<string>PXSharedAlbumsVirtualCollection</string>
<string>13A08E24-F8C0-458F-9D29-BDDCAE13BB00/L0/020</string>
<string>PXMediaTypesVirtualCollection</string>
<string>13A08E24-F8C0-458F-9D29-BDDCAE13BB00/L0/020</string>
</array>
<key>IPXWorkspaceControllerZoomLevelsKey</key>
<dict>
<key>kZoomLevelIdentifierAlbums</key>
<integer>10</integer>
</dict>
<key>lastAddToDestination</key>
<data>
YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMS

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