Feature timewarp parse date 867 (#951)

* Working on tests for timewarp --parse-date

* Test working for --parse-date

* Refactored date utils out of timewarp.py

* Added timezone to --parse-date, updated tests

* Added cog to README
This commit is contained in:
Rhet Turnbull 2023-01-22 22:35:05 -08:00 committed by GitHub
parent 29968269ff
commit 369fa553e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1419 additions and 430 deletions

View File

@ -1797,10 +1797,10 @@ Template statements are white-space sensitive meaning that white space (spaces,
e.g. if Photo keywords are `["foo","bar"]`:
* `"{keyword}"` renders to `"foo", "bar"`
* `"{,+keyword}"` renders to: `"foo,bar"`
* `"{; +keyword}"` renders to: `"foo; bar"`
* `"{+keyword}"` renders to `"foobar"`
- `"{keyword}"` renders to `"foo", "bar"`
- `"{,+keyword}"` renders to: `"foo,bar"`
- `"{; +keyword}"` renders to: `"foo; bar"`
- `"{+keyword}"` renders to `"foobar"`
`template_field`: The template field to resolve. See [Template Substitutions](#template-substitutions) for full list of template fields.
@ -1812,75 +1812,75 @@ e.g. if Photo keywords are `["foo","bar"]`:
Valid filters are:
* `lower`: Convert value to lower case, e.g. 'Value' => 'value'.
* `upper`: Convert value to upper case, e.g. 'Value' => 'VALUE'.
* `strip`: Strip whitespace from beginning/end of value, e.g. ' Value ' => 'Value'.
* `titlecase`: Convert value to title case, e.g. 'my value' => 'My Value'.
* `capitalize`: Capitalize first word of value and convert other words to lower case, e.g. 'MY VALUE' => 'My value'.
* `braces`: Enclose value in curly braces, e.g. 'value => '{value}'.
* `parens`: Enclose value in parentheses, e.g. 'value' => '(value')
* `brackets`: Enclose value in brackets, e.g. 'value' => '[value]'
* `shell_quote`: Quotes the value for safe usage in the shell, e.g. My file.jpeg => 'My file.jpeg'; only adds quotes if needed.
* `function`: Run custom python function to filter value; use in format 'function:/path/to/file.py::function_name'. See example at <https://github.com/RhetTbull/osxphotos/blob/master/examples/template_filter.py>
* `split(x)`: Split value into a list of values using x as delimiter, e.g. 'value1;value2' => ['value1', 'value2'] if used with split(;).
* `autosplit`: Automatically split delimited string into separate values; will split strings delimited by comma, semicolon, or space, e.g. 'value1,value2' => ['value1', 'value2'].
* `chop(x)`: Remove x characters off the end of value, e.g. chop(1): 'Value' => 'Valu'; when applied to a list, chops characters from each list value, e.g. chop(1): ['travel', 'beach']=> ['trave', 'beac'].
* `chomp(x)`: Remove x characters from the beginning of value, e.g. chomp(1): ['Value'] => ['alue']; when applied to a list, removes characters from each list value, e.g. chomp(1): ['travel', 'beach']=> ['ravel', 'each'].
* `sort`: Sort list of values, e.g. ['c', 'b', 'a'] => ['a', 'b', 'c'].
* `rsort`: Sort list of values in reverse order, e.g. ['a', 'b', 'c'] => ['c', 'b', 'a'].
* `reverse`: Reverse order of values, e.g. ['a', 'b', 'c'] => ['c', 'b', 'a'].
* `uniq`: Remove duplicate values, e.g. ['a', 'b', 'c', 'b', 'a'] => ['a', 'b', 'c'].
* `join(x)`: Join list of values with delimiter x, e.g. join(,): ['a', 'b', 'c'] => 'a,b,c'; the DELIM option functions similar to join(x) but with DELIM, the join happens before being passed to any filters.May optionally be used without an argument, that is 'join()' which joins values together with no delimiter. e.g. join(): ['a', 'b', 'c'] => 'abc'.
* `append(x)`: Append x to list of values, e.g. append(d): ['a', 'b', 'c'] => ['a', 'b', 'c', 'd'].
* `prepend(x)`: Prepend x to list of values, e.g. prepend(d): ['a', 'b', 'c'] => ['d', 'a', 'b', 'c'].
* `remove(x)`: Remove x from list of values, e.g. remove(b): ['a', 'b', 'c'] => ['a', 'c'].
* `slice(start:stop:step)`: Slice list using same semantics as Python's list slicing, e.g. slice(1:3): ['a', 'b', 'c', 'd'] => ['b', 'c']; slice(1:4:2): ['a', 'b', 'c', 'd'] => ['b', 'd']; slice(1:): ['a', 'b', 'c', 'd'] => ['b', 'c', 'd']; slice(:-1): ['a', 'b', 'c', 'd'] => ['a', 'b', 'c']; slice(::-1): ['a', 'b', 'c', 'd'] => ['d', 'c', 'b', 'a']. See also sslice().
* `sslice(start:stop:step)`: [s(tring) slice] Slice values in a list using same semantics as Python's string slicing, e.g. sslice(1:3):'abcd => 'bc'; sslice(1:4:2): 'abcd' => 'bd', etc. See also slice().
* `filter(x)`: Filter list of values using predicate x; for example, `{folder_album|filter(contains Events)}` returns only folders/albums containing the word 'Events' in their path.
* `int`: Convert values in list to integer, e.g. 1.0 => 1. If value cannot be converted to integer, remove value from list. ['1.1', 'x'] => ['1']. See also float.
* `float`: Convert values in list to floating point number, e.g. 1 => 1.0. If value cannot be converted to float, remove value from list. ['1', 'x'] => ['1.0']. See also int.
- `lower`: Convert value to lower case, e.g. 'Value' => 'value'.
- `upper`: Convert value to upper case, e.g. 'Value' => 'VALUE'.
- `strip`: Strip whitespace from beginning/end of value, e.g. ' Value ' => 'Value'.
- `titlecase`: Convert value to title case, e.g. 'my value' => 'My Value'.
- `capitalize`: Capitalize first word of value and convert other words to lower case, e.g. 'MY VALUE' => 'My value'.
- `braces`: Enclose value in curly braces, e.g. 'value => '{value}'.
- `parens`: Enclose value in parentheses, e.g. 'value' => '(value')
- `brackets`: Enclose value in brackets, e.g. 'value' => '[value]'
- `shell_quote`: Quotes the value for safe usage in the shell, e.g. My file.jpeg => 'My file.jpeg'; only adds quotes if needed.
- `function`: Run custom python function to filter value; use in format 'function:/path/to/file.py::function_name'. See example at https://github.com/RhetTbull/osxphotos/blob/master/examples/template_filter.py
- `split(x)`: Split value into a list of values using x as delimiter, e.g. 'value1;value2' => ['value1', 'value2'] if used with split(;).
- `autosplit`: Automatically split delimited string into separate values; will split strings delimited by comma, semicolon, or space, e.g. 'value1,value2' => ['value1', 'value2'].
- `chop(x)`: Remove x characters off the end of value, e.g. chop(1): 'Value' => 'Valu'; when applied to a list, chops characters from each list value, e.g. chop(1): ['travel', 'beach']=> ['trave', 'beac'].
- `chomp(x)`: Remove x characters from the beginning of value, e.g. chomp(1): ['Value'] => ['alue']; when applied to a list, removes characters from each list value, e.g. chomp(1): ['travel', 'beach']=> ['ravel', 'each'].
- `sort`: Sort list of values, e.g. ['c', 'b', 'a'] => ['a', 'b', 'c'].
- `rsort`: Sort list of values in reverse order, e.g. ['a', 'b', 'c'] => ['c', 'b', 'a'].
- `reverse`: Reverse order of values, e.g. ['a', 'b', 'c'] => ['c', 'b', 'a'].
- `uniq`: Remove duplicate values, e.g. ['a', 'b', 'c', 'b', 'a'] => ['a', 'b', 'c'].
- `join(x)`: Join list of values with delimiter x, e.g. join(,): ['a', 'b', 'c'] => 'a,b,c'; the DELIM option functions similar to join(x) but with DELIM, the join happens before being passed to any filters.May optionally be used without an argument, that is 'join()' which joins values together with no delimiter. e.g. join(): ['a', 'b', 'c'] => 'abc'.
- `append(x)`: Append x to list of values, e.g. append(d): ['a', 'b', 'c'] => ['a', 'b', 'c', 'd'].
- `prepend(x)`: Prepend x to list of values, e.g. prepend(d): ['a', 'b', 'c'] => ['d', 'a', 'b', 'c'].
- `remove(x)`: Remove x from list of values, e.g. remove(b): ['a', 'b', 'c'] => ['a', 'c'].
- `slice(start:stop:step)`: Slice list using same semantics as Python's list slicing, e.g. slice(1:3): ['a', 'b', 'c', 'd'] => ['b', 'c']; slice(1:4:2): ['a', 'b', 'c', 'd'] => ['b', 'd']; slice(1:): ['a', 'b', 'c', 'd'] => ['b', 'c', 'd']; slice(:-1): ['a', 'b', 'c', 'd'] => ['a', 'b', 'c']; slice(::-1): ['a', 'b', 'c', 'd'] => ['d', 'c', 'b', 'a']. See also sslice().
- `sslice(start:stop:step)`: [s(tring) slice] Slice values in a list using same semantics as Python's string slicing, e.g. sslice(1:3):'abcd => 'bc'; sslice(1:4:2): 'abcd' => 'bd', etc. See also slice().
- `filter(x)`: Filter list of values using predicate x; for example, `{folder_album|filter(contains Events)}` returns only folders/albums containing the word 'Events' in their path.
- `int`: Convert values in list to integer, e.g. 1.0 => 1. If value cannot be converted to integer, remove value from list. ['1.1', 'x'] => ['1']. See also float.
- `float`: Convert values in list to floating point number, e.g. 1 => 1.0. If value cannot be converted to float, remove value from list. ['1', 'x'] => ['1.0']. See also int.
e.g. if Photo keywords are `["FOO","bar"]`:
* `"{keyword|lower}"` renders to `"foo", "bar"`
* `"{keyword|upper}"` renders to: `"FOO", "BAR"`
* `"{keyword|capitalize}"` renders to: `"Foo", "Bar"`
* `"{keyword|lower|parens}"` renders to: `"(foo)", "(bar)"`
- `"{keyword|lower}"` renders to `"foo", "bar"`
- `"{keyword|upper}"` renders to: `"FOO", "BAR"`
- `"{keyword|capitalize}"` renders to: `"Foo", "Bar"`
- `"{keyword|lower|parens}"` renders to: `"(foo)", "(bar)"`
e.g. if Photo description is "my description":
* `"{descr|titlecase}"` renders to: `"My Description"`
- `"{descr|titlecase}"` renders to: `"My Description"`
e.g. If Photo is in `Album1` in `Folder1`:
* `"{folder_album}"` renders to `["Folder1/Album1"]`
* `"{folder_album(>)}"` renders to `["Folder1>Album1"]`
* `"{folder_album()}"` renders to `["Folder1Album1"]`
- `"{folder_album}"` renders to `["Folder1/Album1"]`
- `"{folder_album(>)}"` renders to `["Folder1>Album1"]`
- `"{folder_album()}"` renders to `["Folder1Album1"]`
`[find,replace]`: optional text replacement to perform on rendered template value. For example, to replace "/" in an album name, you could use the template `"{album[/,-]}"`. Multiple replacements can be made by appending "|" and adding another find|replace pair. e.g. to replace both "/" and ":" in album name: `"{album[/,-|:,-]}"`. find/replace pairs are not limited to single characters. The "|" character cannot be used in a find/replace pair.
`conditional`: optional conditional expression that is evaluated as boolean (True/False) for use with the `?bool_value` modifier. Conditional expressions take the form '`not operator value`' where `not` is an optional modifier that negates the `operator`. Note: the space before the conditional expression is required if you use a conditional expression. Valid comparison operators are:
* `contains`: template field contains value, similar to python's `in`
* `matches`: template field contains exactly value, unlike `contains`: does not match partial matches
* `startswith`: template field starts with value
* `endswith`: template field ends with value
* `<=`: template field is less than or equal to value
* `>=`: template field is greater than or equal to value
* `<`: template field is less than value
* `>`: template field is greater than value
* `==`: template field equals value
* `!=`: template field does not equal value
- `contains`: template field contains value, similar to python's `in`
- `matches`: template field contains exactly value, unlike `contains`: does not match partial matches
- `startswith`: template field starts with value
- `endswith`: template field ends with value
- `<=`: template field is less than or equal to value
- `>=`: template field is greater than or equal to value
- `<`: template field is less than value
- `>`: template field is greater than value
- `==`: template field equals value
- `!=`: template field does not equal value
The `value` part of the conditional expression is treated as a bare (unquoted) word/phrase. Multiple values may be separated by '|' (the pipe symbol). `value` is itself a template statement so you can use one or more template fields in `value` which will be resolved before the comparison occurs.
For example:
* `{keyword matches Beach}` resolves to True if 'Beach' is a keyword. It would not match keyword 'BeachDay'.
* `{keyword contains Beach}` resolves to True if any keyword contains the word 'Beach' so it would match both 'Beach' and 'BeachDay'.
* `{photo.score.overall > 0.7}` resolves to True if the photo's overall aesthetic score is greater than 0.7.
* `{keyword|lower contains beach}` uses the lower case filter to do case-insensitive matching to match any keyword that contains the word 'beach'.
* `{keyword|lower not contains beach}` uses the `not` modifier to negate the comparison so this resolves to True if there is no keyword that matches 'beach'.
- `{keyword matches Beach}` resolves to True if 'Beach' is a keyword. It would not match keyword 'BeachDay'.
- `{keyword contains Beach}` resolves to True if any keyword contains the word 'Beach' so it would match both 'Beach' and 'BeachDay'.
- `{photo.score.overall > 0.7}` resolves to True if the photo's overall aesthetic score is greater than 0.7.
- `{keyword|lower contains beach}` uses the lower case filter to do case-insensitive matching to match any keyword that contains the word 'beach'.
- `{keyword|lower not contains beach}` uses the `not` modifier to negate the comparison so this resolves to True if there is no keyword that matches 'beach'.
Examples: to export photos that contain certain keywords with the `osxphotos export` command's `--directory` option:
@ -1897,24 +1897,24 @@ This renames any photo that is a favorite as 'Favorite-ImageName.jpg' (where 'Im
e.g. if photo is an HDR image,
* `"{hdr?ISHDR,NOTHDR}"` renders to `"ISHDR"`
- `"{hdr?ISHDR,NOTHDR}"` renders to `"ISHDR"`
and if it is not an HDR image,
* `"{hdr?ISHDR,NOTHDR}"` renders to `"NOTHDR"`
- `"{hdr?ISHDR,NOTHDR}"` renders to `"NOTHDR"`
`,default`: optional default value to use if the template name has no value. This modifier is also used for the value if False for boolean-type fields (see above) as well as to hold a sub-template for values like `{created.strftime}`. If no default value provided, "_" is used.
e.g., if photo has no title set,
* `"{title}"` renders to "_"
* `"{title,I have no title}"` renders to `"I have no title"`
- `"{title}"` renders to "_"
- `"{title,I have no title}"` renders to `"I have no title"`
Template fields such as `created.strftime` use the default value to pass the template to use for `strftime`.
e.g., if photo date is 4 February 2020, 19:07:38,
* `"{created.strftime,%Y-%m-%d-%H%M%S}"` renders to `"2020-02-04-190738"`
- `"{created.strftime,%Y-%m-%d-%H%M%S}"` renders to `"2020-02-04-190738"`
Some template fields such as `"{media_type}"` use the default value to allow customization of the output. For example, `"{media_type}"` resolves to the special media type of the photo such as `panorama` or `selfie`. You may use the default value to override these in form: `"{media_type,video=vidéo;time_lapse=vidéo_accélérée}"`. In this example, if photo was a time_lapse photo, `media_type` would resolve to `vidéo_accélérée` instead of `time_lapse`.
@ -1966,7 +1966,7 @@ cog.out(get_template_field_table())
|{created.hour}|2-digit hour of the photo creation time|
|{created.min}|2-digit minute of the photo creation time|
|{created.sec}|2-digit second of the photo creation time|
|{created.strftime}|Apply strftime template to file creation date/time. Should be used in form {created.strftime,TEMPLATE} where TEMPLATE is a valid strftime template, e.g. {created.strftime,%Y-%U} would result in year-week number of year: '2020-23'. If used with no template will return null value. See <https://strftime.org/> for help on strftime templates.|
|{created.strftime}|Apply strftime template to file creation date/time. Should be used in form {created.strftime,TEMPLATE} where TEMPLATE is a valid strftime template, e.g. {created.strftime,%Y-%U} would result in year-week number of year: '2020-23'. If used with no template will return null value. See https://strftime.org/ for help on strftime templates.|
|{modified}|Photo's modification date in ISO format, e.g. '2020-03-22'; uses creation date if photo is not modified|
|{modified.date}|Photo's modification date in ISO format, e.g. '2020-03-22'; uses creation date if photo is not modified|
|{modified.year}|4-digit year of photo modification time; uses creation date if photo is not modified|
@ -1980,7 +1980,7 @@ cog.out(get_template_field_table())
|{modified.hour}|2-digit hour of the photo modification time; uses creation date if photo is not modified|
|{modified.min}|2-digit minute of the photo modification time; uses creation date if photo is not modified|
|{modified.sec}|2-digit second of the photo modification time; uses creation date if photo is not modified|
|{modified.strftime}|Apply strftime template to file modification date/time. Should be used in form {modified.strftime,TEMPLATE} where TEMPLATE is a valid strftime template, e.g. {modified.strftime,%Y-%U} would result in year-week number of year: '2020-23'. If used with no template will return null value. Uses creation date if photo is not modified. See <https://strftime.org/> for help on strftime templates.|
|{modified.strftime}|Apply strftime template to file modification date/time. Should be used in form {modified.strftime,TEMPLATE} where TEMPLATE is a valid strftime template, e.g. {modified.strftime,%Y-%U} would result in year-week number of year: '2020-23'. If used with no template will return null value. Uses creation date if photo is not modified. See https://strftime.org/ for help on strftime templates.|
|{today}|Current date in iso format, e.g. '2020-03-22'|
|{today.date}|Current date in iso format, e.g. '2020-03-22'|
|{today.year}|4-digit year of current date|
@ -1994,7 +1994,7 @@ cog.out(get_template_field_table())
|{today.hour}|2-digit hour of the current date|
|{today.min}|2-digit minute of the current date|
|{today.sec}|2-digit second of the current date|
|{today.strftime}|Apply strftime template to current date/time. Should be used in form {today.strftime,TEMPLATE} where TEMPLATE is a valid strftime template, e.g. {today.strftime,%Y-%U} would result in year-week number of year: '2020-23'. If used with no template will return null value. See <https://strftime.org/> for help on strftime templates.|
|{today.strftime}|Apply strftime template to current date/time. Should be used in form {today.strftime,TEMPLATE} where TEMPLATE is a valid strftime template, e.g. {today.strftime,%Y-%U} would result in year-week number of year: '2020-23'. If used with no template will return null value. See https://strftime.org/ for help on strftime templates.|
|{place.name}|Place name from the photo's reverse geolocation data, as displayed in Photos|
|{place.country_code}|The ISO country code from the photo's reverse geolocation data|
|{place.name.country}|Country name from the photo's reverse geolocation data|
@ -2045,17 +2045,17 @@ cog.out(get_template_field_table())
|{label}|Image categorization label associated with a photo (Photos 5+ only). Labels are added automatically by Photos using machine learning algorithms to categorize images. These are not the same as {keyword} which refers to the user-defined keywords/tags applied in Photos.|
|{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)|
|{exiftool}|Format: '{exiftool:GROUP:TAGNAME}'; use exiftool (<https://exiftool.org>) to extract metadata, in form GROUP:TAGNAME, from image. E.g. '{exiftool:EXIF:Make}' to get camera make, or {exiftool:IPTC:Keywords} to extract keywords. See <https://exiftool.org/TagNames/> for list of valid tag names. You must specify group (e.g. EXIF, IPTC, etc) as used in `exiftool -G`. exiftool must be installed in the path to use this template.|
|{exiftool}|Format: '{exiftool:GROUP:TAGNAME}'; use exiftool (https://exiftool.org) to extract metadata, in form GROUP:TAGNAME, from image. E.g. '{exiftool:EXIF:Make}' to get camera make, or {exiftool:IPTC:Keywords} to extract keywords. See https://exiftool.org/TagNames/ for list of valid tag names. You must specify group (e.g. EXIF, IPTC, etc) as used in `exiftool -G`. exiftool must be installed in the path to use this template.|
|{searchinfo.holiday}|Holiday names associated with a photo, e.g. 'Christmas Day'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|{searchinfo.activity}|Activities associated with a photo, e.g. 'Sporting Event'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|{searchinfo.venue}|Venues associated with a photo, e.g. name of restaurant; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|{searchinfo.venue_type}|Venue types associated with a photo, e.g. 'Restaurant'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|{photo}|Provides direct access to the PhotoInfo object for the photo. Must be used in format '{photo.property}' where 'property' represents a PhotoInfo property. For example: '{photo.favorite}' is the same as '{favorite}' and '{photo.place.name}' is the same as '{place.name}'. '{photo}' provides access to properties that are not available as separate template fields but it assumes some knowledge of the underlying PhotoInfo class. See <https://rhettbull.github.io/osxphotos/> for additional documentation on the PhotoInfo class.|
|{photo}|Provides direct access to the PhotoInfo object for the photo. Must be used in format '{photo.property}' where 'property' represents a PhotoInfo property. For example: '{photo.favorite}' is the same as '{favorite}' and '{photo.place.name}' is the same as '{place.name}'. '{photo}' provides access to properties that are not available as separate template fields but it assumes some knowledge of the underlying PhotoInfo class. See https://rhettbull.github.io/osxphotos/ for additional documentation on the PhotoInfo class.|
|{detected_text}|List of text strings found in the image after performing text detection. Using '{detected_text}' will cause osxphotos to perform text detection on your photos using the built-in macOS text detection algorithms which will slow down your export. The results for each photo will be cached in the export database so that future exports with '--update' do not need to reprocess each photo. You may pass a confidence threshold value between 0.0 and 1.0 after a colon as in '{detected_text:0.5}'; The default confidence threshold is 0.75. '{detected_text}' works only on macOS Catalina (10.15) or later. Note: this feature is not the same thing as Live Text in macOS Monterey, which osxphotos does not yet support.|
|{shell_quote}|Use in form '{shell_quote,TEMPLATE}'; quotes the rendered TEMPLATE value(s) for safe usage in the shell, e.g. My file.jpeg => 'My file.jpeg'; only adds quotes if needed.|
|{strip}|Use in form '{strip,TEMPLATE}'; strips whitespace from begining and end of rendered TEMPLATE value(s).|
|{format}|Use in form, '{format:TYPE:FORMAT,TEMPLATE}'; converts TEMPLATE value to TYPE then formats the value using Python string formatting codes specified by FORMAT; TYPE is one of: 'int', 'float', or 'str'. For example, '{format:float:.1f,{exiftool:EXIF:FocalLength}}' will format focal length to 1 decimal place (e.g. '100.0'). |
|{function}|Execute a python function from an external file and use return value as template substitution. Use in format: {function:file.py::function_name} where 'file.py' is the name of the python file and 'function_name' is the name of the function to call. The function will be passed the PhotoInfo object for the photo. See <https://github.com/RhetTbull/osxphotos/blob/master/examples/template_function.py> for an example of how to implement a template function.|
|{function}|Execute a python function from an external file and use return value as template substitution. Use in format: {function:file.py::function_name} where 'file.py' is the name of the python file and 'function_name' is the name of the function to call. The function will be passed the PhotoInfo object for the photo. See https://github.com/RhetTbull/osxphotos/blob/master/examples/template_function.py for an example of how to implement a template function.|
<!--[[[end]]] -->
### <a name="exiftoolExifTool">ExifTool</a>

View File

@ -112,56 +112,68 @@ If you have questions, would like to show off projects created with OSXPhotos, o
This package will install a command line utility called `osxphotos` that allows you to query the Photos database. Alternatively, you can also run the command line utility like this: `python3 -m osxphotos`
<!--[[[cog
from osxphotos.cli import cli_main
from click.testing import CliRunner
runner = CliRunner()
result = runner.invoke(cli_main, ["--help"])
help = result.output.replace("Usage: cli-main", "Usage: osxphotos")
cog.out(
"```\n{}\n```".format(help)
)
]]] -->
```
> osxphotos
Usage: osxphotos [OPTIONS] COMMAND [ARGS]...
osxphotos: query and export your Photos library
osxphotos: the multi-tool for your Photos library
Options:
--db PHOTOS_LIBRARY_PATH Specify Photos database path. Path to Photos
library/database can be specified using either
--db 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
--json Print output in JSON format.
-v, --version Show the version and exit.
--db PHOTOS_LIBRARY_PATH Specify Photos database path. Path to Photos
library/database can be specified using either --db
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
--json Print output in JSON format.
-h, --help Show this message and exit.
Commands:
about Print information about osxphotos including license.
albums Print out albums found in the Photos library.
diff Compare two Photos databases and print out differences
docs Open osxphotos documentation in your browser.
dump Print list of all photos & associated info from the Photos...
exiftool Run exiftool on previously exported files to update metadata.
export Export photos from the Photos database.
exportdb Utilities for working with the osxphotos export database
help Print help; for help on commands: help <command>.
import Import photos and videos into Photos.
info Print out descriptive info of the Photos library database.
inspect Interactively inspect photos selected in Photos.
install Install Python packages into the same environment as osxphotos
keywords Print out keywords found in the Photos library.
labels Print out image classification labels found in the Photos...
list Print list of Photos libraries found on the system.
orphans Find orphaned photos in a Photos library
persons Print out persons (faces) found in the Photos library.
places Print out places found in the Photos library.
query Query the Photos database using 1 or more search options; if...
repl Run interactive osxphotos REPL shell (useful for debugging,...
run Run a python file using same environment as osxphotos.
snap Create snapshot of Photos database to use with diff command
theme Manage osxphotos color themes.
timewarp Adjust date/time/timezone of photos in Apple Photos.
tutorial Display osxphotos tutorial.
uninstall Uninstall Python packages from the osxphotos environment
uuid Print out unique IDs (UUID) of photos selected in Photos
version Check for new version of osxphotos.
about Print information about osxphotos including license.
add-locations Add missing location data to photos in Photos.app using...
albums Print out albums found in the Photos library.
diff Compare two Photos databases and print out differences
docs Open osxphotos documentation in your browser.
dump Print list of all photos & associated info from the Photos...
exiftool Run exiftool on previously exported files to update metadata.
export Export photos from the Photos database.
exportdb Utilities for working with the osxphotos export database
help Print help; for help on commands: help <command>.
import Import photos and videos into Photos.
info Print out descriptive info of the Photos library database.
inspect Interactively inspect photos selected in Photos.
install Install Python packages into the same environment as...
keywords Print out keywords found in the Photos library.
labels Print out image classification labels found in the Photos...
list Print list of Photos libraries found on the system.
orphans Find orphaned photos in a Photos library
persons Print out persons (faces) found in the Photos library.
places Print out places found in the Photos library.
query Query the Photos database using 1 or more search options;...
repl Run interactive osxphotos REPL shell (useful for...
run Run a python file using same environment as osxphotos.
snap Create snapshot of Photos database to use with diff command
sync Sync metadata and albums between Photos libraries.
theme Manage osxphotos color themes.
timewarp Adjust date/time/timezone of photos in Apple Photos.
tutorial Display osxphotos tutorial.
uninstall Uninstall Python packages from the osxphotos environment
uuid Print out unique IDs (UUID) of photos selected in Photos
version Check for new version of osxphotos.
```
<!--[[[end]]] -->
To get help on a specific command, use `osxphotos help command_name`, for example, `osxphotos help export` to get help on the `export` command.

View File

@ -12,6 +12,7 @@ cog -d -o osxphotos/phototemplate.md osxphotos/phototemplate.cog.md
echo "Updating README.md"
python3 utils/update_readme.py
cog -r README.md
echo "Updating API_README.md"
cog -r API_README.md
@ -53,4 +54,4 @@ ARCHSTR=$(uname -m)
ZIPNAME=osxphotos_MacOS_exe_darwin_${ARCHSTR}_v${OSXPHOTOSVERSION}.zip
echo "Zipping CLI executable to $ZIPNAME"
cd dist && zip $ZIPNAME osxphotos && cd ..
rm dist/osxphotos
rm dist/osxphotos

View File

@ -1,14 +1,13 @@
""" Fix time / date / timezone for photos in Apple Photos """
import datetime
import os
from __future__ import annotations
import sys
from functools import partial
from textwrap import dedent
from typing import Callable, Optional
import click
from photoscript import Photo, PhotosLibrary
from photoscript import PhotosLibrary
from rich.console import Console
from osxphotos._constants import APP_NAME
@ -16,10 +15,14 @@ from osxphotos.compare_exif import PhotoCompare
from osxphotos.datetime_utils import datetime_naive_to_local, datetime_to_new_tz
from osxphotos.exif_datetime_updater import ExifDateTimeUpdater
from osxphotos.exiftool import get_exiftool_path
from osxphotos.photodates import (
set_photo_date_from_filename,
update_photo_date_time,
update_photo_from_function,
update_photo_time_for_new_timezone,
)
from osxphotos.photosalbum import PhotosAlbumPhotoScript
from osxphotos.phototz import PhotoTimeZone, PhotoTimeZoneUpdater
from osxphotos.timeutils import update_datetime
from osxphotos.timezones import Timezone
from osxphotos.utils import noop, pluralize
from .click_rich_echo import (
@ -38,6 +41,7 @@ from .param_types import (
DateOffset,
DateTimeISO8601,
FunctionCall,
StrpDateTimePattern,
TimeOffset,
TimeString,
UTCOffset,
@ -49,110 +53,6 @@ from .verbose import get_verbose_console, verbose_print
DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S%z"
def update_photo_date_time(
photo: Photo,
date,
time,
date_delta,
time_delta,
verbose_print: Callable,
):
"""Update date, time in photo"""
photo_date = photo.date
new_photo_date = update_datetime(
photo_date, date=date, time=time, date_delta=date_delta, time_delta=time_delta
)
filename = photo.filename
uuid = photo.uuid
if new_photo_date != photo_date:
photo.date = new_photo_date
verbose_print(
f"Updated date/time for photo [filename]{filename}[/filename] "
f"([uuid]{uuid}[/uuid]) from: [time]{photo_date}[/time] to [time]{new_photo_date}[/time]"
)
else:
verbose_print(
f"Skipped date/time update for photo [filename]{filename}[/filename] "
f"([uuid]{uuid}[/uuid]): nothing to do"
)
def update_photo_time_for_new_timezone(
library_path: str,
photo: Photo,
new_timezone: Timezone,
verbose_print: Callable,
):
"""Update time in photo to keep it the same time but in a new timezone
For example, photo time is 12:00+0100 and new timezone is +0200,
so adjust photo time by 1 hour so it will now be 12:00+0200 instead of
13:00+0200 as it would be with no adjustment to the time"""
old_timezone = PhotoTimeZone(library_path=library_path).get_timezone(photo)[0]
# need to move time in opposite direction of timezone offset so that
# photo time is the same time but in the new timezone
delta = old_timezone - new_timezone.offset
photo_date = photo.date
new_photo_date = update_datetime(
dt=photo_date, time_delta=datetime.timedelta(seconds=delta)
)
filename = photo.filename
uuid = photo.uuid
if photo_date != new_photo_date:
photo.date = new_photo_date
verbose_print(
f"Adjusted date/time for photo [filename]{filename}[/filename] ([uuid]{uuid}[/uuid]) to match "
f"previous time [time]{photo_date}[time] but in new timezone [tz]{new_timezone}[/tz]."
)
else:
verbose_print(
f"Skipping date/time update for photo [filename]{filename}[/filename] ([uuid]{photo.uuid}[/uuid]), "
f"already matches new timezone [tz]{new_timezone}[/tz]"
)
def update_photo_from_function(
library_path: str,
function: Callable,
verbose_print: Callable,
photo: Photo,
path: Optional[str],
):
"""Update photo from function call"""
photo_tz_sec, _, photo_tz_name = PhotoTimeZone(
library_path=library_path
).get_timezone(photo)
dt_new, tz_new = function(
photo=photo,
path=path,
tz_sec=photo_tz_sec,
tz_name=photo_tz_name,
verbose=verbose_print,
)
if dt_new != photo.date:
old_date = photo.date
photo.date = dt_new
verbose_print(
f"Updated date/time for photo [filename]{photo.filename}[/filename] "
f"([uuid]{photo.uuid}[/uuid]) from: [time]{old_date}[/time] to [time]{dt_new}[/time]"
)
else:
verbose_print(
f"Skipped date/time update for photo [filename]{photo.filename}[/filename] "
f"([uuid]{photo.uuid}[/uuid]): nothing to do"
)
if tz_new != photo_tz_sec:
tz_updater = PhotoTimeZoneUpdater(
timezone=Timezone(tz_new), verbose=verbose_print, library_path=library_path
)
tz_updater.update_photo(photo)
else:
verbose_print(
f"Skipped timezone update for photo [filename]{photo.filename}[/filename] "
f"([uuid]{photo.uuid}[/uuid]): nothing to do"
)
class TimeWarpCommand(click.Command):
"""Custom click.Command that overrides get_help() to show additional help info for export"""
@ -223,6 +123,40 @@ if the EXIF data is missing, use the file modification date/time; show verbose o
`osxphotos timewarp --pull-exif --use-file-time --verbose`
## Parsing Dates/Times from Filenames
The --parse-date option allows you to parse dates/times from the original filename of the photo.
This is useful if you files with dates/times embedded in the filename but not in the metadata.
The argument to `--parse-date` is a pattern string that is used to parse the date/time
from the filename. The pattern string is a superset of the python `strftime/strptime`
format with the following additions:
- *: Match any number of characters
- ^: Match the beginning of the string
- $: Match the end of the string
- {n}: Match exactly n characters
- {n,}: Match at least n characters
- {n,m}: Match at least n characters and at most m characters
- In addition to `%%` for a literal `%`, the following format codes are supported:
`%^`, `%$`, `%*`, `%|`, `%{`, `%}` for `^`, `$`, `*`, `|`, `{`, `}` respectively
- |: join multiple format codes; each code is tried in order until one matches
- Unlike the standard library, the leading zero is not optional for
%d, %m, %H, %I, %M, %S, %j, %U, %W, and %V
- For optional leading zero, use %-d, %-m, %-H, %-I, %-M, %-S, %-j, %-U, %-W, and %-V
For more information on strptime format codes, see:
https://docs.python.org/3/library/datetime.html?highlight=strptime#strftime-and-strptime-format-codes
**Note**: The time zone of the parsed date/time is assumed to be the local time zone.
If the parse pattern includes a time zone, the photo's time will be converted from
the specified time zone to the local time zone. osxphotos import does not
currently support setting the time zone of imported photos.
See also `osxphotos help timewarp` for more information on the timewarp
command which can be used to change the time zone of photos after import.
"""
),
width=formatter.width,
@ -322,6 +256,19 @@ if the EXIF data is missing, use the file modification date/time; show verbose o
"Requires the third-party exiftool utility be installed (see https://exiftool.org/). "
"See also --push-exif.",
)
@click.option(
"--parse-date",
"-M",
metavar="DATE_PATTERN",
type=StrpDateTimePattern(),
help="Parse date from filename using DATE_PATTERN and set photo's date to match. "
"If file does not match DATE_PATTERN, the date will not be changed. "
"DATE_PATTERN is a strptime-compatible pattern with extensions as pattern described below. "
"If DATE_PATTERN matches time zone information, the photo's timezone will be set to match. "
"For example, if your photos are named 'IMG_1234_2022_11_23_12_34_56.jpg' where the date/time is "
"'2022-11-23 12:34:56', you could use the pattern '%Y_%m_%d_%H_%M_%S' or "
"'IMG_*_%Y_%m_%d_%H_%M_%S' to further narrow the pattern to only match files with 'IMG_xxxx_' in the name.",
)
@click.option(
"--function",
"-F",
@ -363,7 +310,7 @@ if the EXIF data is missing, use the file modification date/time; show verbose o
help="When used with --compare-exif, adds any photos with date/time/timezone differences "
"between Photos/EXIF to album ALBUM. If ALBUM does not exist, it will be created.",
)
@click.option("--verbose", "-V", "verbose", is_flag=True, help="Show verbose output.")
@click.option("--verbose", "-V", "verbose_", is_flag=True, help="Show verbose output.")
@click.option(
"--library",
"-L",
@ -419,9 +366,10 @@ def timewarp(
use_file_time,
add_to_album,
exiftool_path,
verbose,
verbose_,
library,
theme,
parse_date,
plain,
output_file,
terminal_width,
@ -446,6 +394,7 @@ def timewarp(
timezone,
inspect,
compare_exif,
parse_date,
push_exif,
pull_exif,
function,
@ -453,7 +402,8 @@ def timewarp(
):
raise click.UsageError(
"At least one of --date, --date-delta, --time, --time-delta, "
"--timezone, --inspect, --compare-exif, --push-exif, --pull-exif, --function "
"--timezone, --inspect, --compare-exif, --push-exif, --pull-exif, "
"--parse-date, --function "
"must be specified."
)
@ -472,8 +422,8 @@ def timewarp(
# configure colored rich output
# TODO: this is all a little hacky, find a better way to do this
color_theme = get_theme(theme)
verbose_ = verbose_print(
verbose,
verbose = verbose_print(
verbose_,
timestamp,
rich=True,
theme=color_theme,
@ -499,7 +449,7 @@ def timewarp(
if any([compare_exif, push_exif, pull_exif]):
exiftool_path = exiftool_path or get_exiftool_path()
verbose_(f"exiftool path: [filename]{exiftool_path}[/filename]")
verbose(f"exiftool path: [filename]{exiftool_path}[/filename]")
try:
photos = PhotosLibrary().selection
@ -533,6 +483,7 @@ def timewarp(
push_exif,
pull_exif,
function,
parse_date,
]
)
and not force
@ -556,13 +507,19 @@ def timewarp(
time=time,
date_delta=date_delta,
time_delta=time_delta,
verbose_print=verbose_,
verbose=verbose,
)
update_photo_time_for_new_timezone_ = partial(
update_photo_time_for_new_timezone,
library_path=library,
verbose_print=verbose_,
verbose=verbose,
)
set_photo_date_from_filename_ = partial(
set_photo_date_from_filename,
library_path=library,
verbose=verbose,
)
if function:
@ -570,7 +527,7 @@ def timewarp(
update_photo_from_function,
library_path=library,
function=function[0],
verbose_print=verbose_,
verbose=verbose,
)
else:
update_photo_from_function_ = noop
@ -579,14 +536,20 @@ def timewarp(
tzinfo = PhotoTimeZone(library_path=library)
if photos:
rich_echo(
"[filename]filename[/filename], [uuid]uuid[/uuid], [time]photo time (local)[/time], [time]photo time[/time], [tz]timezone offset[/tz], [tz]timezone name[/tz]"
"[filename]filename[/filename], [uuid]uuid[/uuid], "
"[time]photo time (local)[/time], "
"[time]photo time[/time], "
"[tz]timezone offset[/tz], [tz]timezone name[/tz]"
)
for photo in photos:
tz_seconds, tz_str, tz_name = tzinfo.get_timezone(photo)
photo_date_local = datetime_naive_to_local(photo.date)
photo_date_tz = datetime_to_new_tz(photo_date_local, tz_seconds)
rich_echo(
f"[filename]{photo.filename}[/filename], [uuid]{photo.uuid}[/uuid], [time]{photo_date_local.strftime(DATETIME_FORMAT)}[/time], [time]{photo_date_tz.strftime(DATETIME_FORMAT)}[/time], [tz]{tz_str}[/tz], [tz]{tz_name}[/tz]"
f"[filename]{photo.filename}[/filename], [uuid]{photo.uuid}[/uuid], "
f"[time]{photo_date_local.strftime(DATETIME_FORMAT)}[/time], "
f"[time]{photo_date_tz.strftime(DATETIME_FORMAT)}[/time], "
f"[tz]{tz_str}[/tz], [tz]{tz_name}[/tz]"
)
sys.exit(0)
@ -596,7 +559,7 @@ def timewarp(
if photos:
photocomp = PhotoCompare(
library_path=library,
verbose=verbose_,
verbose=verbose,
exiftool_path=exiftool_path,
)
if not album:
@ -622,12 +585,12 @@ def timewarp(
if album:
if diff_results.diff:
different_photos += 1
verbose_(
verbose(
f"Photo {filename} ({uuid}) has different date/time/timezone, adding to album '{album.name}'"
)
album.add(photo)
else:
verbose_(f"Photo {filename} ({uuid}) has same date/time/timezone")
verbose(f"Photo {filename} ({uuid}) has same date/time/timezone")
else:
rich_echo(
f"{filename}, {uuid}, "
@ -644,14 +607,14 @@ def timewarp(
if timezone:
tz_updater = PhotoTimeZoneUpdater(
timezone, verbose=verbose_, library_path=library
timezone, verbose=verbose, library_path=library
)
if any([push_exif, pull_exif, function]):
# ExifDateTimeUpdater used to get photo path for --function
exif_updater = ExifDateTimeUpdater(
library_path=library,
verbose=verbose_,
verbose=verbose,
exiftool_path=exiftool_path,
plain=plain,
)
@ -663,6 +626,8 @@ def timewarp(
total=num_photos,
)
for p in photos:
if parse_date:
set_photo_date_from_filename_(p, p.filename, parse_date)
if pull_exif:
exif_updater.update_photos_from_exif(
p, use_file_modify_date=use_file_time
@ -676,7 +641,7 @@ def timewarp(
if timezone:
tz_updater.update_photo(p)
if function:
verbose_(f"Calling function [bold]{function[1]}")
verbose(f"Calling function [bold]{function[1]}")
photo_path = exif_updater.get_photo_path(p)
update_photo_from_function_(photo=p, path=photo_path)
if push_exif:

181
osxphotos/photodates.py Normal file
View File

@ -0,0 +1,181 @@
"""Utilities for working with Photo dates in Apple Photos; used by osxphotos timewarp command"""
from __future__ import annotations
import datetime
import pathlib
from typing import Callable
import photoscript
from strpdatetime import strpdatetime
from .datetime_utils import (
datetime_has_tz,
datetime_remove_tz,
datetime_tz_to_utc,
datetime_utc_to_local,
utc_offset_seconds,
)
from .phototz import PhotoTimeZone, PhotoTimeZoneUpdater
from .timeutils import update_datetime
from .timezones import Timezone
def update_photo_date_time(
photo: photoscript.Photo,
date,
time,
date_delta,
time_delta,
verbose: Callable,
):
"""Update date, time in photo"""
photo_date = photo.date
new_photo_date = update_datetime(
photo_date, date=date, time=time, date_delta=date_delta, time_delta=time_delta
)
filename = photo.filename
uuid = photo.uuid
if new_photo_date != photo_date:
photo.date = new_photo_date
verbose(
f"Updated date/time for photo [filename]{filename}[/filename] "
f"([uuid]{uuid}[/uuid]) from: [time]{photo_date}[/time] to [time]{new_photo_date}[/time]"
)
else:
verbose(
f"Skipped date/time update for photo [filename]{filename}[/filename] "
f"([uuid]{uuid}[/uuid]): nothing to do"
)
def update_photo_from_function(
library_path: str,
function: Callable,
verbose: Callable[..., None],
photo: photoscript.Photo,
path: str | None,
):
"""Update photo from function call"""
photo_tz_sec, _, photo_tz_name = PhotoTimeZone(
library_path=library_path
).get_timezone(photo)
dt_new, tz_new = function(
photo=photo,
path=path,
tz_sec=photo_tz_sec,
tz_name=photo_tz_name,
verbose=verbose,
)
if dt_new != photo.date:
old_date = photo.date
photo.date = dt_new
verbose(
f"Updated date/time for photo [filename]{photo.filename}[/filename] "
f"([uuid]{photo.uuid}[/uuid]) from: [time]{old_date}[/time] to [time]{dt_new}[/time]"
)
else:
verbose(
f"Skipped date/time update for photo [filename]{photo.filename}[/filename] "
f"([uuid]{photo.uuid}[/uuid]): nothing to do"
)
if tz_new != photo_tz_sec:
tz_updater = PhotoTimeZoneUpdater(
timezone=Timezone(tz_new), verbose=verbose, library_path=library_path
)
tz_updater.update_photo(photo)
else:
verbose(
f"Skipped timezone update for photo [filename]{photo.filename}[/filename] "
f"([uuid]{photo.uuid}[/uuid]): nothing to do"
)
def update_photo_time_for_new_timezone(
library_path: str,
photo: photoscript.Photo,
new_timezone: Timezone,
verbose: Callable[..., None],
):
"""Update time in photo to keep it the same time but in a new timezone
For example, photo time is 12:00+0100 and new timezone is +0200,
so adjust photo time by 1 hour so it will now be 12:00+0200 instead of
13:00+0200 as it would be with no adjustment to the time"""
old_timezone = PhotoTimeZone(library_path=library_path).get_timezone(photo)[0]
# need to move time in opposite direction of timezone offset so that
# photo time is the same time but in the new timezone
delta = old_timezone - new_timezone.offset
photo_date = photo.date
new_photo_date = update_datetime(
dt=photo_date, time_delta=datetime.timedelta(seconds=delta)
)
filename = photo.filename
uuid = photo.uuid
if photo_date != new_photo_date:
photo.date = new_photo_date
verbose(
f"Adjusted date/time for photo [filename]{filename}[/] ([uuid]{uuid}[/]) to [time]{new_photo_date}[/] "
f"to match previous time [time]{photo_date}[/] but in new timezone [tz]{new_timezone}[/]."
)
else:
verbose(
f"Skipping date/time update for photo [filename]{filename}[/] ([uuid]{photo.uuid}[/]), "
f"already matches new timezone [tz]{new_timezone}[/]"
)
def set_photo_date_from_filename(
photo: photoscript.Photo,
filepath: pathlib.Path | str,
parse_date: str,
verbose: Callable[..., None],
library_path: str | None = None,
) -> datetime.datetime | None:
"""Set date of photo from filename
Args:
photo: Photo to set date
filepath: Path to photo's original file
parse_date: strptime format string to parse date from filename
verbose: verbose function to use for logging
library_path: Path to Photos library; if not provided, will attempt to determine automatically
Returns:
datetime.datetime: date set on photo or None if date could not be parsed
"""
if not isinstance(filepath, pathlib.Path):
filepath = pathlib.Path(filepath)
try:
date = strpdatetime(filepath.name, parse_date)
except ValueError:
verbose(
f"[warning]Could not parse date from filename [filename]{filepath.name}[/][/]"
)
return None
# first, set date on photo without timezone (Photos will assume local timezone)
date_no_tz = datetime_remove_tz(date) if datetime_has_tz(date) else date
verbose(
f"Setting date of photo [filename]{filepath.name}[/] to [time]{date_no_tz.strftime('%Y-%m-%d %H:%M:%S')}[/]"
)
photo.date = date_no_tz
if datetime_has_tz(date):
# if timezone, need to update timezone and also the date/time to match
photo_tz_sec, _, photo_tz_name = PhotoTimeZone(
library_path=library_path
).get_timezone(photo)
tz_new_secs = int(utc_offset_seconds(date))
if photo_tz_sec != tz_new_secs:
tz_new = Timezone(tz_new_secs)
update_photo_time_for_new_timezone(library_path, photo, tz_new, verbose)
tz_updater = PhotoTimeZoneUpdater(
timezone=tz_new,
verbose=verbose,
library_path=library_path,
)
tz_updater.update_photo(photo)
return date

View File

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

View File

@ -22,5 +22,7 @@
<array/>
<key>removeMoment</key>
<array/>
<key>renamePerson</key>
<array/>
</dict>
</plist>

View File

@ -4,11 +4,15 @@
<dict>
<key>embeddingVersion</key>
<string>1</string>
<key>featureFlags</key>
<string>319</string>
<key>featuredContentAllowed</key>
<string>1</string>
<key>localeIdentifier</key>
<string>en_US</string>
<key>sceneTaxonomySHA</key>
<string>87914a047c69fbe8013fad2c70fa70c6c03b08b56190fe4054c880e6b9f57cc3</string>
<string>64d078bafc0035e1ec26dfa565c2ac0479fcbab329fda1c16cd17e0fdbf2f4c0,4afa5d3c45c08a664cf73cff957aaeeae3a325d2970aada51268407b9ad0f03e</string>
<key>searchIndexVersion</key>
<string>10</string>
<string>16025</string>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

View File

@ -4,5 +4,10 @@
<dict>
<key>CollapsedSidebarSectionIdentifiers</key>
<array/>
<key>IPXWorkspaceControllerZoomLevelsKey</key>
<dict>
<key>kZoomLevelIdentifierPhotosGrid</key>
<integer>2</integer>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,910 @@
<?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>firstSeenDates</key>
<dict>
<key>com.apple.photos.CPAnalytics.addAssetsToLibrary</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.appleMusicPreparationFailed</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.appleMusicPreparationSucceeded</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.appleMusicPreparedToPlay</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.appleMusicSongDownloaded</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.assetCollectionBlocked</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.assetCollectionDeleted</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.assetCollectionFavorited</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.assetCollectionMoviePlayed</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.assetCollectionUnfavorited</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.assetCollectionViewed</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.assetExportItemPreparationCompleted</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.assetExportPreparationCanceled</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.assetExportPreparationCompleted</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.assetExportPreparationFailed</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.assetFavorited</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.assetSharedStandardSelectionSize</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.assetUnfavorited</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.assetViewed</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.assetsDeleted</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.assetsDeletedFromTrash</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.assetsRestored</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.cmmAddAssetToLibrary</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.cmmPublishFromDetailViewMenuAction</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.cmmPublishFromForYouSendBackSuggestions</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.cmmPublishFromForYouSuggestion</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.cmmPublishFromMessagesSuggestion</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.cmmPublishFromShareSheet</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.cmmPublishFromUnknown</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.cmmReceivedSharesOpened</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.cmmReceivedSharesSeen</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.cmmSentSharesOpened</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.cmmSentSharesSeen</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.cmmShareBackOpened</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.cmmShareBackPresentable</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.cmmShareBackShown</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.cmmSuggestionComposeFlowOpened</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.cmmSuggestionOpened</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.cmmSuggestionSeen</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.deduplicationEventFailed</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.deduplicationEventOneUpSelect</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.deduplicationEventSkip</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.deduplicationFromAlbumSection</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.deduplicationFromAlbumSelection</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.deduplicationFromOneUpMergeAll</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.deduplicationFromOneUpSelection</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.deduplicationFromUnspecified</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.deduplicationMerge1</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.deduplicationMerge11+</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.deduplicationMerge2</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.deduplicationMerge3-5</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.deduplicationMerge6-10</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.deduplicationMergeAll</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.deduplicationSkipPerceptualMerge</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.filterAllItemsSelected</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.filterDuplicatesSelected</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.filterEditedSelected</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.filterFavoritedSelected</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.filterHeaderSelected</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.filterIncludeSharedWithYouSelected</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.filterKeywordHeaderSelected</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.filterKeywordManagerSelected</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.filterKeywordsSelected</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.filterPhotosSelected</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.filterSavedItemsOnlySelected</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.filterSharedLibraryAllDisplayed</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.filterSharedLibraryAllSelected</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.filterSharedLibraryDismissed</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.filterSharedLibraryMineDisplayed</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.filterSharedLibraryMineSelected</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.filterSharedLibraryPresented</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.filterSharedLibrarySharedDisplayed</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.filterSharedLibrarySharedSelected</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.filterUnsavedItemsOnlySelected</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.filterVideosSelected</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.firstTimeExperienceNotReady</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.firstTimeExperienceReady</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.flexMusicDownloadRequiredAtPlaybackTime</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.flexMusicSongArtworkDownloaded</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.flexMusicSongAudioDownloaded</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.forYouInboxItemSelected</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.forYouInboxItemSelectedWithTypeCMMPublishedShare</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.forYouInboxItemSelectedWithTypeCMMReceivedShare</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.forYouInboxItemSelectedWithTypeSharedAlbumAccept</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.forYouInboxItemSelectedWithTypeSharedAlbumCoalescedWithComments</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.forYouInboxItemSelectedWithTypeSharedAlbumCoalescedWithCommentsAndLikes</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.forYouInboxItemSelectedWithTypeSharedAlbumComment</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.forYouInboxItemSelectedWithTypeSharedAlbumDecline</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.forYouInboxItemSelectedWithTypeSharedAlbumInvitation</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.forYouInboxItemSelectedWithTypeSharedAlbumLike</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.forYouInboxItemSelectedWithTypeSharedAlbumPost</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.forYouTabOpenedFromInAppUserNavigation</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.forYouTabOpenedFromNotification</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.forYouTabOpenedFromOtherURL</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.forYouTabOpenedFromStateRestoration</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.forYouTabOpenedFromUndefined</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.forYouTabOpenedFromWidget</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.gadgetSeenWithTypeAlbumGadget</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.gadgetSeenWithTypeAlbumListGadget</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.gadgetSeenWithTypeCMMInviteGadget</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.gadgetSeenWithTypeCMMSuggestionGadget</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.gadgetSeenWithTypeCPLCMMSuggestionGadget</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.gadgetSeenWithTypeContentSyndication</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.gadgetSeenWithTypeDebugGadget</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.gadgetSeenWithTypeFooterGadget</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.gadgetSeenWithTypeInboxGadget</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.gadgetSeenWithTypeListViewGadget</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.gadgetSeenWithTypeMemoryRowGadget</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.gadgetSeenWithTypeNoContentGadget</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.gadgetSeenWithTypeRecentPhotosGadget</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.gadgetSeenWithTypeRecentSearchesGadget</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.gadgetSeenWithTypeSearchZeroKeywordGadget</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.gadgetSeenWithTypeSettingsAdvisory</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.gadgetSeenWithTypeSharedAlbumActivityGadget</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.gadgetSeenWithTypeSharedAlbumGadget</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.gadgetSeenWithTypeSharedAlbumInviteGadget</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.gadgetSeenWithTypeSuggestedEditGadget</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.gadgetSeenWithTypeSuggestionGadget</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.gadgetSeenWithTypeSurveyCongratulations</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.gadgetSeenWithTypeSurveyQuestionGadget</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.gadgetSeenWithTypeTapToRadarGadget</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.gadgetWithTypeMemoryRowGadgetSelectedAccessoryButtonWithTypeSeeAll</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.gadgetWithTypeSharedAlbumGadgetSelectedAccessoryButtonWithTypeSeeAll</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.infoPanelAddLocationTapped</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.infoPanelAdjustLocationTapped</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.infoPanelLocationTapped</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.mapViewAdjustLocationTapped</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.mediaViewed</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.memoryCreatedViaAddToMemories</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.memoryNotificationResponded</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.memoryNotificationSeen</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.navigateToFailedToUploadItemsAlbumFailed</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.navigateToFailedToUploadItemsAlbumSucceeded</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.navigatedToSharedAlbumActivityViewFromCollageView</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.navigatedToSharedAlbumActivityViewFromRecentActivityEntry</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.navigatedToSharedAlbumActivityViewFromSeeAllButton</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.navigationList.favorites</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.navigationList.hidden</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.navigationList.imports</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.navigationList.mediaTypes.animated</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.navigationList.mediaTypes.bursts</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.navigationList.mediaTypes.cinematicVideos</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.navigationList.mediaTypes.depthEffect</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.navigationList.mediaTypes.livePhotos</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.navigationList.mediaTypes.longExposures</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.navigationList.mediaTypes.panoramas</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.navigationList.mediaTypes.proRes</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.navigationList.mediaTypes.raw</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.navigationList.mediaTypes.screenrecordings</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.navigationList.mediaTypes.screenshots</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.navigationList.mediaTypes.selfPortraits</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.navigationList.mediaTypes.slomoVideos</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.navigationList.mediaTypes.timelapses</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.navigationList.mediaTypes.videos</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.navigationList.people</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.navigationList.places</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.navigationList.recentlyedited</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.navigationList.recentlysaved</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.navigationList.trashBin</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.oneUpCaptionEditExited</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.oneUpCaptionEditExitedWith1-10Characters</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.oneUpCaptionEditExitedWith101+Characters</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.oneUpCaptionEditExitedWith11+Hashtags</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.oneUpCaptionEditExitedWith11-20Characters</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.oneUpCaptionEditExitedWith1Hashtags</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.oneUpCaptionEditExitedWith21-30Characters</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.oneUpCaptionEditExitedWith2Hashtags</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.oneUpCaptionEditExitedWith31-50Characters</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.oneUpCaptionEditExitedWith3Hashtags</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.oneUpCaptionEditExitedWith4-5Hashtags</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.oneUpCaptionEditExitedWith51-100Characters</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.oneUpCaptionEditExitedWith6-10Hashtags</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.oneUpLivePhotoEffectApplied</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.people.addToPeopleHome</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.people.bootstrap.inlineControl.dismiss</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.people.bootstrap.inlineControl.review</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.people.bootstrap.mergeCandidates.confirmedAndRejectedCounts</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.people.bootstrap.speedbump.advance</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.people.bootstrap.speedbump.cancel</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.people.confirmAdditional.mergeCandidates.confirmedAndRejectedCounts</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.people.confirmAdditional.personSuggestions.confirmedAndRejectedCounts</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.people.detailView.keyPhotoChanged</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.people.detailView.notThisPerson</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.people.detailView.showFaces</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.people.manageTags.commitTapped</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.people.me.confirmed</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.people.me.rejected</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.people.naming.contactChosen</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.people.naming.personChosen</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.people.naming.skipped</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.people.naming.stringChosen</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.people.removeFromPeopleHome</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.people.untag.cancelTapped</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.people.untag.newTagTapped</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.people.untag.untagTapped</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.photosCloudQuotaOfferAdded</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.photosCloudQuotaOfferRemoved</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.photosCloudQuotaOfferReplaced</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.photosDetailsAllAssetsCounted</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.photosDetailsNavigatedToDepth1</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.photosDetailsNavigatedToDepth10</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.photosDetailsNavigatedToDepth11-12</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.photosDetailsNavigatedToDepth13-15</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.photosDetailsNavigatedToDepth16-20</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.photosDetailsNavigatedToDepth2</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.photosDetailsNavigatedToDepth21-30</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.photosDetailsNavigatedToDepth3</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.photosDetailsNavigatedToDepth31-50</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.photosDetailsNavigatedToDepth4</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.photosDetailsNavigatedToDepth5</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.photosDetailsNavigatedToDepth51+</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.photosDetailsNavigatedToDepth6</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.photosDetailsNavigatedToDepth7</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.photosDetailsNavigatedToDepth8</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.photosDetailsNavigatedToDepth9</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.photosDetailsShowMoreButtonSelected</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.photosDetailsSummaryAssetsCounted</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.photosDetailsSummaryButtonSelected</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.places.locationAction</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.rendering.livePhotoEffectFailed</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.rendering.livePhotoEffectPreviewRenderingDuration</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.search.session</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.search.siri</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.selectModeEntered</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.shareCanceled</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.shareCompleted</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.shareFailed</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.sharedAlbumInvitationAccepted</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.sharedAlbumInvitationDeclined</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.sharedAlbumInvitationReportedAsJunk</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.sharedAlbumsActivityFeedPostLiked</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.sharedAlbumsActivityFeedPostUnliked</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.sharedLibrary.suggestionsBanner.dismissed</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.sharedLibrary.suggestionsBanner.presented</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.sharedLibrary.suggestionsBanner.reviewed</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowExportCancelledAfter10.0-60.0Seconds</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowExportCancelledAfter2.0-10.0Seconds</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowExportCancelledAfter&lt;2.0Seconds</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowExportCancelledAfter&gt;60.0Seconds</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowExportEndedSuccessfullyAfter0.0-1.0Seconds</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowExportEndedSuccessfullyAfter1.0-2.0Seconds</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowExportEndedSuccessfullyAfter10.0-20.0Seconds</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowExportEndedSuccessfullyAfter120.0-300.0Seconds</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowExportEndedSuccessfullyAfter2.0-5.0Seconds</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowExportEndedSuccessfullyAfter20.0-60.0Seconds</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowExportEndedSuccessfullyAfter300.0-600.0Seconds</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowExportEndedSuccessfullyAfter5.0-10.0Seconds</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowExportEndedSuccessfullyAfter60.0-120.0Seconds</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowExportEndedSuccessfullyAfter600.0-3600.0Seconds</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowExportEndedSuccessfullyAfter&lt;0.0Seconds</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowExportEndedSuccessfullyAfter&gt;3600.0Seconds</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowExportFailed</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowExportStarted</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackEndedAfterTimeFraction0.0-0.1</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackEndedAfterTimeFraction0.1-0.2</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackEndedAfterTimeFraction0.2-0.2</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackEndedAfterTimeFraction0.3-0.4</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackEndedAfterTimeFraction0.4-0.5</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackEndedAfterTimeFraction0.5-0.6</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackEndedAfterTimeFraction0.6-0.7</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackEndedAfterTimeFraction0.7-0.8</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackEndedAfterTimeFraction0.8-0.9</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackEndedAfterTimeFraction0.9-1.0</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackEndedAfterTimeFraction1.0-1.5</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackEndedAfterTimeFraction1.5-2.0</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackEndedAfterTimeFraction2.0-3.0</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackEndedAfterTimeFraction3.0-5.0</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackEndedAfterTimeFraction&lt;0.0</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackEndedAfterTimeFraction&gt;5.0</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackEndedByEndReached</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackEndedByUndefined</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackEndedByUser</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackStarted</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackStartedWith0-Assets</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackStartedWith1-5Assets</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackStartedWith1001+Assets</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackStartedWith101-200Assets</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackStartedWith11-15Assets</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackStartedWith16-20Assets</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackStartedWith201-300Assets</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackStartedWith21-30Assets</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackStartedWith301-500Assets</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackStartedWith31-50Assets</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackStartedWith501-1000Assets</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackStartedWith51-100Assets</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.slideshowPlaybackStartedWith6-10Assets</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.syndicatedAssetsFilterTipAnchorButtonTapped</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.syndicatedAssetsFilterTipDismissed</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.syndicatedAssetsFilterTipPresented</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.syndicatedAssetsSaved</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.syndicationSuggestionRemoved</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.tabIdentifierChanged</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.toggleMineAndSharedSelected</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.toggleMyPhotosOnlySelected</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.userChoices.livePhotoEffectBounceSelected</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.userChoices.livePhotoEffectLongExposureSelected</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.userChoices.livePhotoEffectLoopSelected</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.userChoices.livePhotoEffectsAppeared</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.viewScrolledToBottom</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.viewScrolledToInitialPosition</key>
<string>1/22/23</string>
<key>com.apple.photos.CPAnalytics.widget.open</key>
<string>1/22/23</string>
<key>com.apple.photos.cpa.active_user_feature.actionOnSharedLibrarySuggestions</key>
<string>1/22/23</string>
<key>com.apple.photos.cpa.active_user_feature.interactiveMemoryExport</key>
<string>1/22/23</string>
<key>com.apple.photos.cpa.active_user_feature.interactiveMemoryPlayback</key>
<string>1/22/23</string>
<key>com.apple.photos.cpa.active_user_feature.memoryMoviePlayed</key>
<string>1/22/23</string>
<key>com.apple.photos.cpa.active_user_feature.memoryViewed</key>
<string>1/22/23</string>
<key>com.apple.photos.cpa.active_user_feature.mobileSlideShowActive</key>
<string>1/22/23</string>
<key>com.apple.photos.cpa.active_user_feature.slideshowExported</key>
<string>1/22/23</string>
<key>com.apple.photos.cpa.active_user_feature.slideshowPlayed</key>
<string>1/22/23</string>
<key>com.apple.photos.edit.perf.exitedit</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.appleMusicFixation</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryErrorOccurred</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryErrorOccurredInAdditionalAutoEditDecisionLists</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryErrorOccurredInAssets</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryErrorOccurredInAssetsPreloading</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryErrorOccurredInAutoEditClip</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryErrorOccurredInAutoEditDecisionLists</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryErrorOccurredInAutoEditTransitionInfo</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryErrorOccurredInColorNormalization</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryErrorOccurredInCuratedSongs</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryErrorOccurredInInitialStyle</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryErrorOccurredInMovieHighlights</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryErrorOccurredInMusicPlayback</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryErrorOccurredInPersistableRecipe</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryErrorOccurredInPersistence</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryErrorOccurredInRecipePersistence</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryErrorOccurredInStoryModel</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryErrorOccurredInStyleManager</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryErrorOccurredInStyles</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryErrorOccurredInTargetDurationCuration</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryErrorOccurredInTimelineManager</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryErrorOccurredInTimelineValidation</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryErrorOccurredInTransitions</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryExportCancelled</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryExportFailed</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryExported</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryFullscreenPlaybackStarted</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryFullscreenPlaybackStartedByAutoplayedRelated</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryFullscreenPlaybackStartedByUserSelectingRelated</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryFullscreenPlaybackStartedWith1-10CuratedAssets</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryFullscreenPlaybackStartedWith10001-20000CuratedAssets</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryFullscreenPlaybackStartedWith1001-2000CuratedAssets</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryFullscreenPlaybackStartedWith101-200CuratedAssets</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryFullscreenPlaybackStartedWith11-50CuratedAssets</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryFullscreenPlaybackStartedWith20000+CuratedAssets</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryFullscreenPlaybackStartedWith2001-5000CuratedAssets</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryFullscreenPlaybackStartedWith201-300CuratedAssets</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryFullscreenPlaybackStartedWith301-500CuratedAssets</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryFullscreenPlaybackStartedWith5001-10000CuratedAssets</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryFullscreenPlaybackStartedWith501-1000CuratedAssets</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryFullscreenPlaybackStartedWith51-100CuratedAssets</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryMusicFellBackToLocalFlexSong</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryMusicUsedFlexMusic</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryMusicUseedAppleMusic</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryPlaybackFinishedMuted</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryPlaybackFinishedTruncated</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryPlaybackFinishedUnmuted</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryPlaybackFinishedWith0Pauses</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryPlaybackFinishedWith1-2Pauses</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryPlaybackFinishedWith101+Pauses</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryPlaybackFinishedWith11-50Pauses</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryPlaybackFinishedWith3-5Pauses</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryPlaybackFinishedWith51-100Pauses</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryPlaybackFinishedWith6-10Pauses</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryPlaybackMutedDueToExplicitUserAction</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryPlaybackMutedDueToSilentModeSwitch</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryPlaybackUnmutedDueToExplicitUserAction</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryPlaybackUnmutedDueToSilentModeSwitch</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryPlaybackUnmutedDueToSongPick</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryPlaybackUnmutedDueToVolumeIncrease</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryRecipeErrorOccurred</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemorySessionBegan</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemorySessionEnded</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemorySessionPaused</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemorySessionPlayed</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryTimelinePlayedWith0-49PercentTransitionsOnBar</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryTimelinePlayedWith50-74PercentTransitionsOnBar</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryTimelinePlayedWith75-84PercentTransitionsOnBar</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryTimelinePlayedWith85-94PercentTransitionsOnBar</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.interactiveMemoryTimelinePlayedWith95-100PercentTransitionsOnBar</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.playbackLaunchPerformance</key>
<string>1/22/23</string>
<key>com.apple.photos.memory.playbackReliability</key>
<string>1/22/23</string>
<key>screen_CuratedLibrary_AllPhotos</key>
<string>1/22/23</string>
<key>screen_CuratedLibrary_Days</key>
<string>1/22/23</string>
<key>screen_CuratedLibrary_Months</key>
<string>1/22/23</string>
<key>screen_CuratedLibrary_Years</key>
<string>1/22/23</string>
<key>screen_DuplicatesAlbum</key>
<string>1/22/23</string>
<key>screen_FavoriteMemories</key>
<string>1/22/23</string>
<key>screen_IPXFeedViewController</key>
<string>1/22/23</string>
<key>screen_InteractiveMemory</key>
<string>1/22/23</string>
<key>screen_InteractiveMemoryBrowserGrid</key>
<string>1/22/23</string>
<key>screen_InteractiveMemoryStyleSwitcher</key>
<string>1/22/23</string>
<key>screen_MFMailComposeViewController</key>
<string>1/22/23</string>
<key>screen_MFMessageComposeViewController</key>
<string>1/22/23</string>
<key>screen_Memories</key>
<string>1/22/23</string>
<key>screen_MemoriesFeed</key>
<string>1/22/23</string>
<key>screen_MemoriesWidget</key>
<string>1/22/23</string>
<key>screen_ObjectManipulationViewController</key>
<string>1/22/23</string>
<key>screen_PMEditorNavigationController</key>
<string>1/22/23</string>
<key>screen_PMiOSMainViewController</key>
<string>1/22/23</string>
<key>screen_PXAssistantController</key>
<string>1/22/23</string>
<key>screen_PXCMMAssetsViewController</key>
<string>1/22/23</string>
<key>screen_PXEducationalTipViewController</key>
<string>1/22/23</string>
<key>screen_PXFeedViewController</key>
<string>1/22/23</string>
<key>screen_PXFeedbackTapToRadarViewController</key>
<string>1/22/23</string>
<key>screen_PXFloatingCardViewController</key>
<string>1/22/23</string>
<key>screen_PXForYouGadgetViewController</key>
<string>1/22/23</string>
<key>screen_PXPeopleBootstrapConfirmationViewController</key>
<string>1/22/23</string>
<key>screen_PXPeopleBootstrapSummaryViewController</key>
<string>1/22/23</string>
<key>screen_PXPeopleCollectionViewController</key>
<string>1/22/23</string>
<key>screen_PXPeopleConfirmationSummaryViewController</key>
<string>1/22/23</string>
<key>screen_PXPeopleDetailViewController</key>
<string>1/22/23</string>
<key>screen_PXPeopleNamePickerViewController</key>
<string>1/22/23</string>
<key>screen_PXPeopleRecoCollectionViewController</key>
<string>1/22/23</string>
<key>screen_PXPlacesMapInfoViewController</key>
<string>1/22/23</string>
<key>screen_PXPlacesMapViewController</key>
<string>1/22/23</string>
<key>screen_PXSharedLibraryAssistantCameraViewController</key>
<string>1/22/23</string>
<key>screen_PXSharedLibraryAssistantHowToViewController</key>
<string>1/22/23</string>
<key>screen_PXSharedLibraryAssistantReviewParticipantsViewController</key>
<string>1/22/23</string>
<key>screen_PXSharedLibraryAssistantRulesViewController</key>
<string>1/22/23</string>
<key>screen_PXSharedLibraryAssistantSummaryViewController</key>
<string>1/22/23</string>
<key>screen_PXSurveyRadarReporterViewController</key>
<string>1/22/23</string>
<key>screen_PXUIPeopleBootstrapNamingViewController</key>
<string>1/22/23</string>
<key>screen_PhotosDetails_Collection</key>
<string>1/22/23</string>
<key>screen_PhotosDetails_CuratedLibraryDays</key>
<string>1/22/23</string>
<key>screen_PhotosDetails_CuratedLibraryMonths</key>
<string>1/22/23</string>
<key>screen_PhotosDetails_CuratedLibraryYears</key>
<string>1/22/23</string>
<key>screen_PhotosDetails_Memories</key>
<string>1/22/23</string>
<key>screen_PhotosDetails_Moment</key>
<string>1/22/23</string>
<key>screen_PhotosDetails_NonTracking</key>
<string>1/22/23</string>
<key>screen_PhotosDetails_OneUpAccessory</key>
<string>1/22/23</string>
<key>screen_PhotosDetails_OtherAlbums</key>
<string>1/22/23</string>
<key>screen_PhotosDetails_People</key>
<string>1/22/23</string>
<key>screen_PhotosDetails_Places</key>
<string>1/22/23</string>
<key>screen_PhotosDetails_RelatedWidget</key>
<string>1/22/23</string>
<key>screen_PhotosDetails_Search</key>
<string>1/22/23</string>
<key>screen_PhotosDetails_Year</key>
<string>1/22/23</string>
<key>screen_PhotosUIApps.PUXStoryColorGradeEditorViewController</key>
<string>1/22/23</string>
<key>screen_PhotosUIApps.PUXStoryExportActivityPreviewViewController</key>
<string>1/22/23</string>
<key>screen_PhotosUIApps.PUXStoryExportActivityViewController</key>
<string>1/22/23</string>
<key>screen_PhotosUIApps.PUXStoryMusicEditorViewController</key>
<string>1/22/23</string>
<key>screen_PhotosView</key>
<string>1/22/23</string>
<key>screen_SBSUIWallpaperPreviewViewController</key>
<string>1/22/23</string>
<key>screen_SLComposeViewController</key>
<string>1/22/23</string>
<key>screen_TPKContentPopoverViewController</key>
<string>1/22/23</string>
<key>screen_UIImagePickerController</key>
<string>1/22/23</string>
<key>screen__UIActivityUserDefaultsViewController</key>
<string>1/22/23</string>
</dict>
<key>lastSeenDates</key>
<dict>
<key>screen_CuratedLibrary_AllPhotos</key>
<string>1/22/23</string>
<key>screen_PhotosView</key>
<string>1/22/23</string>
</dict>
</dict>
</plist>

View File

@ -1,7 +1,11 @@
""" Test data for timewarp command on Catalina/Photos 5 """
""" Test data for timewarp command on Ventura/Photos 8 """
# NOTE: The data may be wrong if test not run in US Pacific time zone
# I've not tried to test this for other timezones
import datetime
import pathlib
import time
from tests.parse_timewarp_output import CompareValues, InspectValues
@ -15,6 +19,11 @@ def get_file_timestamp(file: str) -> str:
)
def is_dst() -> bool:
"""Return True if daylight savings time is in effect"""
return bool(time.localtime().tm_isdst)
VENTURA_PHOTOS_5 = {
"filenames": {
"pumpkins": "IMG_6522.jpeg",
@ -24,6 +33,7 @@ VENTURA_PHOTOS_5 = {
"marigold flowers": "IMG_6517.jpeg",
"multi-colored zinnia flowers": "IMG_6506.jpeg",
"sunset": "IMG_6551.mov",
"palm tree": "20230120_010203-0400.jpg",
},
"inspect": {
# IMG_6501.jpeg
@ -271,7 +281,7 @@ VENTURA_PHOTOS_5 = {
f"{TEST_LIBRARY_TIMEWARP}/originals/7/7E9DF2EE-A5B0-4077-80EC-30565221A3B9.jpeg"
),
"",
"-0700",
"-0700" if is_dst() else "-0800",
"",
),
},
@ -362,4 +372,28 @@ VENTURA_PHOTOS_5 = {
"GMT-0700",
),
},
"parse_date": {
# 20230120_010203-0400.jpg
"uuid": "5285C4E2-BB1A-49DF-AEF5-246AA337ACAB",
"expected": InspectValues(
"20230120_010203-0400.jpg",
"5285C4E2-BB1A-49DF-AEF5-246AA337ACAB",
"2023-01-20 01:02:03-0800" if not is_dst() else "2023-01-20 00:02:03-0700",
"2023-01-20 01:02:03-0800" if not is_dst() else "2023-01-20 00:02:03-0700",
"-0800",
"GMT-0800",
),
},
"parse_date_tz": {
# 20230120_010203-0400.jpg
"uuid": "5285C4E2-BB1A-49DF-AEF5-246AA337ACAB",
"expected": InspectValues(
"20230120_010203-0400.jpg",
"5285C4E2-BB1A-49DF-AEF5-246AA337ACAB",
"2023-01-19 21:02:03-0800" if not is_dst() else "2023-01-19 20:02:03-0700",
"2023-01-20 01:02:03-0400",
"-0400",
"GMT-0400",
),
},
}

View File

@ -5,15 +5,12 @@ import time
import pytest
from click.testing import CliRunner
from osxphotos import PhotosDB
from osxphotos.exiftool import ExifTool
from tests.conftest import (
get_os_version,
)
from tests.conftest import get_os_version
from tests.parse_timewarp_output import parse_compare_exif, parse_inspect_output
# set timezone to avoid issues with comparing dates
os.environ["TZ"] = "US/Pacific"
time.tzset()
@ -1007,3 +1004,69 @@ def test_function(photoslib, suspend_capture, output_file):
)
output_values = parse_inspect_output(output_file)
assert output_values[0] == expected
@pytest.mark.timewarp
def test_select_palm_tree_1(photoslib, suspend_capture):
"""Force user to select the right photo for following tests"""
assert ask_user_to_make_selection(photoslib, suspend_capture, "palm tree")
@pytest.mark.timewarp
@pytest.mark.skipif(get_os_version()[0] != "13", reason="test requires macOS 13")
def test_parse_date(photoslib, suspend_capture, output_file):
"""Test --parse-date"""
from osxphotos.cli.timewarp import timewarp
expected = TEST_DATA["parse_date"]["expected"]
runner = CliRunner()
result = runner.invoke(
timewarp,
[
"--parse-date",
"^%Y%m%d_%H%M%S",
"--force",
],
terminal_width=TERMINAL_WIDTH,
)
assert result.exit_code == 0
result = runner.invoke(
timewarp,
["--inspect", "--plain", "--force", "-o", output_file],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_inspect_output(output_file)
assert output_values[0].date_local == expected.date_local
assert output_values[0].date_tz == expected.date_tz
assert output_values[0].tz_offset == expected.tz_offset
@pytest.mark.timewarp
@pytest.mark.skipif(get_os_version()[0] != "13", reason="test requires macOS 13")
def test_parse_date_tz(photoslib, suspend_capture, output_file):
"""Test --parse-date with a timezone"""
from osxphotos.cli.timewarp import timewarp
expected = TEST_DATA["parse_date_tz"]["expected"]
runner = CliRunner()
result = runner.invoke(
timewarp,
[
"--parse-date",
"^%Y%m%d_%H%M%S%z",
"--force",
],
terminal_width=TERMINAL_WIDTH,
)
assert result.exit_code == 0
result = runner.invoke(
timewarp,
["--inspect", "--plain", "--force", "-o", output_file],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_inspect_output(output_file)
assert output_values[0].date_local == expected.date_local
assert output_values[0].date_tz == expected.date_tz
assert output_values[0].tz_offset == expected.tz_offset