Compare commits

...

48 Commits

Author SHA1 Message Date
Rhet Turnbull
51317a607c Added initial support for macOS Ventura/13.0 beta 2022-06-17 12:26:22 -07:00
Rhet Turnbull
5f63cccc7c Initial support for Ventura developer preview (#715) 2022-06-17 12:11:17 -07:00
Rhet Turnbull
b3a935bd90 Added missing import 2022-06-15 08:26:01 -07:00
Rhet Turnbull
56435b101f Updated examples [skip ci] 2022-06-13 06:02:04 -07:00
Rhet Turnbull
04c2f6121a Updated examples [skip ci] 2022-06-12 21:56:46 -07:00
Rhet Turnbull
f47aa72165 Updated examples [skip ci] 2022-06-12 21:50:51 -07:00
Rhet Turnbull
561c6846e4 Added example [skip ci] 2022-06-12 21:33:53 -07:00
Rhet Turnbull
0d7e324f02 Added metadata_changed to ExportResults docs 2022-06-03 09:36:29 -07:00
Rhet Turnbull
c2f02c3b7b Added --template to inspect command 2022-05-29 09:39:40 -07:00
Rhet Turnbull
04e1149cad Fixed docs 2022-05-29 08:18:18 -07:00
Rhet Turnbull
bb7a81f9ed Updated CHANGELOG.md [skip ci] 2022-05-28 23:20:11 -07:00
Rhet Turnbull
203dccb39f Fixed shortuuid docs 2022-05-28 23:14:05 -07:00
Rhet Turnbull
75568269bb Added shortuuid, #314 2022-05-28 23:03:04 -07:00
Rhet Turnbull
9e9266ec9c Added slice, sslice filters 2022-05-28 22:07:31 -07:00
Rhet Turnbull
6f1e81b038 Updated join so join() works correctly, #706 2022-05-28 19:12:46 -07:00
Rhet Turnbull
0122f9b2d8 Updated CHANGELOG.md [skip ci] 2022-05-28 19:04:45 -07:00
Rhet Turnbull
b6e7a75a81 Updated docs [skip ci] 2022-05-28 19:02:13 -07:00
Rhet Turnbull
43512240b3 Updated README.md, fixed broken test 2022-05-28 18:49:25 -07:00
Rhet Turnbull
a049b99b0e Updated README.md, #707 [skip ci] 2022-05-28 18:43:19 -07:00
Rhet Turnbull
6c1650b7cf Version 0.50.1 with --delete-file, --delete-uuid exportdb commands 2022-05-28 18:18:23 -07:00
Rhet Turnbull
49821d377c Updated CHANGELOG.md [skip ci] 2022-05-28 08:41:22 -07:00
Rhet Turnbull
175d7ea223 Version 0.50.0 with updated template engine 2022-05-28 08:12:16 -07:00
Rhet Turnbull
0a973d67f9 Updated template language to match autofile
* Added field_arg to template grammar

* Updated template grammar to match autofile MTL

* Added format and filter args from autofile

* Added cloud_metadata to PhotoInfo

* Version bump, updated docs

* Added tests for filters
2022-05-27 18:33:07 -07:00
Rhet Turnbull
999b16e80f Version bump, updated docs 2022-05-26 20:16:41 -07:00
Rhet Turnbull
ed3473f602 Added cloud_metadata to PhotoInfo 2022-05-25 22:56:52 -07:00
Rhet Turnbull
8d020cbf09 Updated docs 2022-05-24 09:14:01 -07:00
Rhet Turnbull
dae710b836 Implemented retry for export db, #569 2022-05-24 09:13:44 -07:00
Rhet Turnbull
1daf18ad9f Updated CHANGELOG.md [skip ci] 2022-05-23 23:35:59 -07:00
Rhet Turnbull
7926c8d676 Bug fix, #695 2022-05-23 23:33:40 -07:00
Rhet Turnbull
9d7a5e22d9 Removed screencast, [skip ci] 2022-05-23 23:15:15 -07:00
Rhet Turnbull
e9cc6ce137 Bug fix, #695 2022-05-23 22:53:39 -07:00
Rhet Turnbull
bc32b1827f Removed screencast, [skip ci] 2022-05-22 15:55:52 -07:00
Rhet Turnbull
206ad8c33c Added screencast [skip ci] 2022-05-22 15:50:54 -07:00
Rhet Turnbull
65e8be7ed7 Added screencast [skip ci] 2022-05-22 15:43:40 -07:00
Rhet Turnbull
6477292a13 Updated CHANGELOG.md [skip ci] 2022-05-22 15:43:21 -07:00
Rhet Turnbull
7a52d413a3 Bug fix 2022-05-22 15:19:48 -07:00
Rhet Turnbull
ab8b7b4b19 Fixed detected_text for videos 2022-05-22 15:06:31 -07:00
Rhet Turnbull
25d4459efd Updated CHANGELOG.md [skip ci] 2022-05-22 13:33:59 -07:00
Rhet Turnbull
128e84c7a4 Added inspect command 2022-05-22 13:32:20 -07:00
Rhet Turnbull
4771207595 Feature inspect command (#701)
* Initial implementation of inspect command

* Updated help for inspect
2022-05-22 12:23:40 -07:00
Rhet Turnbull
0eca3b9c13 Initial implementation of inspect command (#700) 2022-05-22 12:18:40 -07:00
Rhet Turnbull
afec845e1b Updated CHANGELOG.md [skip ci] 2022-05-21 11:13:55 -07:00
Rhet Turnbull
64002044d2 Added timestamp to export_data in exportdb, #697 2022-05-21 11:00:52 -07:00
Rhet Turnbull
4e40d4b74e Added warning on hardlinks to exiftool command 2022-05-21 08:48:59 -07:00
Rhet Turnbull
63d515646b Updated CHANGELOG.md [skip ci] 2022-05-21 08:20:21 -07:00
Rhet Turnbull
0a7575b889 Updated docs 2022-05-21 08:12:32 -07:00
Rhet Turnbull
c776f3070d Added --uuid-info, --uuid-files to exportdb 2022-05-21 08:08:25 -07:00
Rhet Turnbull
3b789242aa Updated CHANGELOG.md [skip ci] 2022-05-21 07:29:12 -07:00
257 changed files with 6355 additions and 3290 deletions

View File

@@ -2,7 +2,7 @@
In addition to a command line interface, OSXPhotos provides a access to a Python API that allows you to easily access a Photos library, often with just a few lines of code.
# Table of Contents
## Table of Contents
* [Example uses of the Python package](#example-uses-of-the-python-package)
* [Package Interface](#package-interface)
@@ -12,6 +12,7 @@ In addition to a command line interface, OSXPhotos provides a access to a Python
* [AlbumInfo](#albuminfo)
* [ImportInfo](#importinfo)
* [ProjectInfo](#projectinfo)
* [MomentInfo](#momentinfo)
* [FolderInfo](#folderinfo)
* [PlaceInfo](#placeinfo)
* [ScoreInfo](#scoreinfo)
@@ -266,6 +267,10 @@ Returns a list of [ImportInfo](#importinfo) objects representing the import sess
Returns a list of [ProjectInfo](#projectinfo) objects representing the projects/creations (cards, calendars, etc.) in the database.
#### `moment_info`
Returns the [MomentInfo](#momentinfo) object for the photo or `None` if the photo does not have an associated moment.
#### `folder_info`
```python
@@ -969,6 +974,10 @@ Returns a [ScoreInfo](#scoreinfo) data class object which provides access to the
Returns list of PhotoInfo objects for *possible* duplicates or empty list if no matching duplicates. Photos are considered possible duplicates if the photo's original file size, date created, height, and width match another those of another photo. This does not do a byte-for-byte comparison or compute a hash which makes it fast and allows for identification of possible duplicates even if originals are not downloaded from iCloud. The signature-based approach should be robust enough to match duplicates created either through the "duplicate photo" menu item or imported twice into the library but you should not rely on this 100% for identification of all duplicates.
#### `hexdigest`
Returns a unique digest of the photo's properties and metadata; useful for detecting changes in any property/metadata of the photo.
#### `json()`
Returns a JSON representation of all photo info.
@@ -1212,6 +1221,50 @@ Returns a list of [PhotoInfo](#PhotoInfo) objects representing each photo contai
Returns the creation date as a timezone aware datetime.datetime object of the project.
### MomentInfo
PhotoInfo.moment_info return the MomentInfo object for the photo. The MomentInfo object contains information about the photo's moment as assigned by Photos. The MomentInfo object contains the following properties:
#### `pk`
Returns the primary key of the moment in the Photos database.
#### `location`
Returns the location of the moment as a tuple of (latitude, longitude).
#### `title`
Returns the title of the moment.
#### `subtitle`
Returns the subtitle of the moment.
#### `start_date`
Returns the start date of the moment as a timezone aware datetime.datetime object.
#### `end_date`
Returns the end date of the moment as a timezone aware datetime.datetime object.
#### `date`
Returns the date of the moment as a timezone aware datetime.datetime object.
#### `modification_date`
Returns the modification date of the moment as a timezone aware datetime.datetime object.
#### `photos`
Returns a list of [PhotoInfo] objects representing the photos in the moment.
#### `asdict()`
Returns a dictionary representation of the moment.
### FolderInfo
PhotosDB.folder_info returns a list of FolderInfo objects representing the top level folders in the library. Each FolderInfo object represents a single folder in the Photos library.
@@ -1573,18 +1626,6 @@ Returns list of x, y coordinates as tuples `[(x0, y0), (x1, y1)]` representing t
Coordinates as (x, y) tuple for the center of the detected face.
#### `mouth`
Coordinates as (x, y) tuple for the mouth of the detected face.
#### `left_eye`
Coordinates as (x, y) tuple for the left eye of the detected face.
#### `right_eye`
Coordinates as (x, y) tuple for the right eye of the detected face.
#### `size_pixels`
Diameter of detected face region in pixels.
@@ -1601,29 +1642,25 @@ Roll of face region in radians.
Pitch of face region in radians.
**Note**: Only valid on Photos version <= 4, otherwise returns 0
#### yaw
Yaw of face region in radians.
**Note**: Only valid on Photos version <= 4, otherwise returns 0
#### `Additional properties`
The following additional properties are also available but are not yet fully documented.
* `center_x`: x coordinate of center of face in Photos' internal reference frame
* `center_y`: y coordinate of center of face in Photos' internal reference frame
* `mouth_x`: x coordinate of mouth in Photos' internal reference frame
* `mouth_y`: y coordinate of mouth in Photos' internal reference frame
* `left_eye_x`: x coordinate of left eye in Photos' internal reference frame
* `left_eye_y`: y coordinate of left eye in Photos' internal reference frame
* `right_eye_x`: x coordinate of right eye in Photos' internal reference frame
* `right_eye_y`: y coordinate of right eye in Photos' internal reference frame
* `size`: size of face region in Photos' internal reference frame
* `quality`: quality measure of detected face
* `source_width`: width in pixels of photo
* `source_height`: height in pixels of photo
* `has_smile`:
* `left_eye_closed`:
* `right_eye_closed`:
* `manual`:
* `face_type`:
* `age_type`:
@@ -1727,7 +1764,7 @@ In its simplest form, a template statement has the form: `"{template_field}"`, f
Template statements may contain one or more modifiers. The full syntax is:
`"pretext{delim+template_field:subfield|filter(path_sep)[find,replace] conditional?bool_value,default}posttext"`
`"pretext{delim+template_field:subfield(field_arg)|filter[find,replace] conditional?bool_value,default}posttext"`
Template statements are white-space sensitive meaning that white space (spaces, tabs) changes the meaning of the template statement.
@@ -1748,6 +1785,8 @@ e.g. if Photo keywords are `["foo","bar"]`:
`:subfield`: Some templates have sub-fields, For example, `{exiftool:IPTC:Make}`; the template_field is `exiftool` and the sub-field is `IPTC:Make`.
`(field_arg)`: optional arguments to pass to the field; for example, with `{folder_album}` this is used to pass the path separator used for joining folders and albums when rendering the field (default is "/" for `{folder_album}`).
`|filter`: You may optionally append one or more filter commands to the end of the template field using the vertical pipe ('|') symbol. Filters may be combined, separated by '|' as in: `{keyword|capitalize|parens}`.
Valid filters are:
@@ -1762,6 +1801,20 @@ Valid filters are:
- `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().
e.g. if Photo keywords are `["FOO","bar"]`:
@@ -1774,8 +1827,6 @@ e.g. if Photo description is "my description":
- `"{descr|titlecase}"` renders to: `"My Description"`
`(path_sep)`: optional path separator to use when joining path-like fields, for example `{folder_album}`. Default is "/".
e.g. If Photo is in `Album1` in `Folder1`:
- `"{folder_album}"` renders to `["Folder1/Album1"]`
@@ -1818,7 +1869,7 @@ This can be used to rename files as well, for example:
This renames any photo that is a favorite as 'Favorite-ImageName.jpg' (where 'ImageName.jpg' is the original name of the photo) and all other photos with the unmodified original name.
`?bool_value`: Template fields may be evaluated as boolean (True/False) by appending "?" after the field name (and following "(path_sep)" or "[find/replace]". If a field is True (e.g. photo is HDR and field is `"{hdr}"`) or has any value, the value following the "?" will be used to render the template instead of the actual field value. If the template field evaluates to False (e.g. in above example, photo is not HDR) or has no value (e.g. photo has no title and field is `"{title}"`) then the default value following a "," will be used.
`?bool_value`: Template fields may be evaluated as boolean (True/False) by appending "?" after the field name (and following "(field_arg)" or "[find/replace]". If a field is True (e.g. photo is HDR and field is `"{hdr}"`) or has any value, the value following the "?" will be used to render the template instead of the actual field value. If the template field evaluates to False (e.g. in above example, photo is not HDR) or has no value (e.g. photo has no title and field is `"{title}"`) then the default value following a "," will be used.
e.g. if photo is an HDR image,
@@ -1848,6 +1899,16 @@ Either or both bool_value or default (False value) may be empty which would resu
If you want to include "{" or "}" in the output, use "{openbrace}" or "{closebrace}" template substitution.
e.g. `"{created.year}/{openbrace}{title}{closebrace}"` would result in `"2020/{Photo Title}"`.
**Variables**
You can define variables for later use in the template string using the format `{var:NAME,VALUE}`. Variables may then be referenced using the format `%NAME`. For example: `{var:foo,bar}` defines the variable `%foo` to have value `bar`. This can be useful if you want to re-use a complex template value in multiple places within your template string or for allowing the use of characters that would otherwise be prohibited in a template string. For example, the "pipe" (`|`) character is not allowed in a find/replace pair but you can get around this limitation like so: `{var:pipe,{pipe}}{title[-,%pipe]}` which replaces the `-` character with `|` (the value of `%pipe`).
Variables can also be referenced as fields in the template string, for example: `{var:year,created.year}{original_name}-{%year}`. In some cases, use of variables can make your template string more readable. Variables can be used as template fields, as values for filters, as values for conditional operations, or as default values. When used as a conditional value or default value, variables should be treated like any other field and enclosed in braces as conditional and default values are evaluated as template strings. For example: `{var:name,Katie}{person contains {%name}?{%name},Not-{%name}}`.
If you need to use a `%` (percent sign character), you can escape the percent sign by using `%%`. You can also use the `{percent}` template field where a template field is required. For example:
`{title[:,%%]}` replaces the `:` with `%` and `{title contains Foo?{title}{percent},{title}}` adds `%` to the title if it contains `Foo`.
<!--[[[end]]] -->
The following template field substitutions are availabe for use the templating system.
@@ -1926,9 +1987,10 @@ cog.out(get_template_field_table())
|{exif.lens_model}|Lens model from original photo's EXIF information as imported by Photos, e.g. 'iPhone 6s back camera 4.15mm f/2.2'|
|{moment}|The moment title of the photo|
|{uuid}|Photo's internal universally unique identifier (UUID) for the photo, a 36-character string unique to the photo, e.g. '128FB4C6-0B16-4E7D-9108-FB2E90DA1546'|
|{shortuuid}|A shorter representation of photo's internal universally unique identifier (UUID) for the photo, a 22-character string unique to the photo, e.g. 'JYsxugP9UjetmCbBCHXcmu'|
|{id}|A unique number for the photo based on its primary key in the Photos database. A sequential integer, e.g. 1, 2, 3...etc. Each asset associated with a photo (e.g. an image and Live Photo preview) will share the same id. May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use '{id:05d}' which results in 00001, 00002, 00003...etc. |
|{album_seq}|An integer, starting at 0, indicating the photo's index (sequence) in the containing album. Only valid when used in a '--filename' template and only when '{album}' or '{folder_album}' is used in the '--directory' template. For example '--directory "{folder_album}" --filename "{album_seq}_{original_name}"'. To start counting at a value other than 0, append append a period and the starting value to the field name. For example, to start counting at 1 instead of 0: '{album_seq.1}'. May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use '{album_seq:05d}' which results in 00000, 00001, 00002...etc. This may result in incorrect sequences if you have duplicate albums with the same name; see also '{folder_album_seq}'.|
|{folder_album_seq}|An integer, starting at 0, indicating the photo's index (sequence) in the containing album and folder path. Only valid when used in a '--filename' template and only when '{folder_album}' is used in the '--directory' template. For example '--directory "{folder_album}" --filename "{folder_album_seq}_{original_name}"'. To start counting at a value other than 0, append append a period and the starting value to the field name. For example, to start counting at 1 instead of 0: '{folder_album_seq.1}' May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use '{folder_album_seq:05d}' which results in 00000, 00001, 00002...etc. This may result in incorrect sequences if you have duplicate albums with the same name in the same folder; see also '{album_seq}'.|
|{album_seq}|An integer, starting at 0, indicating the photo's index (sequence) in the containing album. Only valid when used in a '--filename' template and only when '{album}' or '{folder_album}' is used in the '--directory' template. For example '--directory "{folder_album}" --filename "{album_seq}_{original_name}"'. To start counting at a value other than 0, append append '(starting_value)' to the field name. For example, to start counting at 1 instead of 0: '{album_seq(1)}'. May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use '{album_seq:05d}' which results in 00000, 00001, 00002...etc. To format while also using a starting value: '{album_seq:05d(1)}' which results in 0001, 00002...etc.This may result in incorrect sequences if you have duplicate albums with the same name; see also '{folder_album_seq}'.|
|{folder_album_seq}|An integer, starting at 0, indicating the photo's index (sequence) in the containing album and folder path. Only valid when used in a '--filename' template and only when '{folder_album}' is used in the '--directory' template. For example '--directory "{folder_album}" --filename "{folder_album_seq}_{original_name}"'. To start counting at a value other than 0, append '(starting_value)' to the field name. For example, to start counting at 1 instead of 0: '{folder_album_seq(1)}' May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use '{folder_album_seq:05d}' which results in 00000, 00001, 00002...etc. To format while also using a starting value: '{folder_album_seq:05d(1)}' which results in 0001, 00002...etc.This may result in incorrect sequences if you have duplicate albums with the same name in the same folder; see also '{album_seq}'. |
|{comma}|A comma: ','|
|{semicolon}|A semicolon: ';'|
|{questionmark}|A question mark: '?'|
@@ -1943,7 +2005,7 @@ cog.out(get_template_field_table())
|{lf}|A line feed: '\n', alias for {newline}|
|{cr}|A carriage return: '\r'|
|{crlf}|a carriage return + line feed: '\r\n'|
|{osxphotos_version}|The osxphotos version, e.g. '0.49.2'|
|{osxphotos_version}|The osxphotos version, e.g. '0.50.4'|
|{osxphotos_cmd_line}|The full command line used to run osxphotos|
|{album}|Album(s) photo is contained in|
|{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|
@@ -1964,6 +2026,7 @@ cog.out(get_template_field_table())
|{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.|
<!--[[[end]]] -->
@@ -2112,6 +2175,7 @@ Attributes:
* touched: list of files touched during export (e.g. file date/time updated with touch_file=True)
* to_touch: Reserved for internal use of export
* converted_to_jpeg: list of files converted to jpeg when convert_to_jpeg=True
* metadata_changed: list of filenames that had metadata changes since last export
* sidecar_json_written: list of JSON sidecars written
* sidecar_json_skipped: list of JSON sidecars skipped when update=True
* sidecar_exiftool_written: list of exiftool sidecars written

View File

@@ -4,6 +4,90 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [v0.50.2](https://github.com/RhetTbull/osxphotos/compare/v0.50.1...v0.50.2)
> 28 May 2022
- Added shortuuid, #314 [`7556826`](https://github.com/RhetTbull/osxphotos/commit/75568269bbd7b05a22f9fd00acd6f59691f1a507)
- Added slice, sslice filters [`9e9266e`](https://github.com/RhetTbull/osxphotos/commit/9e9266ec9c890ed6fb09d61b1a075be954bef7c1)
- Fixed shortuuid docs [`203dccb`](https://github.com/RhetTbull/osxphotos/commit/203dccb39fbe68b996d53126b52fd3fcedc5f0a1)
#### [v0.50.1](https://github.com/RhetTbull/osxphotos/compare/v0.50.0...v0.50.1)
> 28 May 2022
- Updated README.md, #707 [skip ci] [`a049b99`](https://github.com/RhetTbull/osxphotos/commit/a049b99b0ef67803da917f92ecb24b18fbe6a6c9)
- Version 0.50.1 with --delete-file, --delete-uuid exportdb commands [`6c1650b`](https://github.com/RhetTbull/osxphotos/commit/6c1650b7cffefc223374f66012393f14d443fa72)
- Updated docs [skip ci] [`b6e7a75`](https://github.com/RhetTbull/osxphotos/commit/b6e7a75a8110e7f71ae615e50e91419d92f5b59e)
#### [v0.50.0](https://github.com/RhetTbull/osxphotos/compare/v0.49.9...v0.50.0)
> 28 May 2022
- Version 0.50.0 with updated template engine [`175d7ea`](https://github.com/RhetTbull/osxphotos/commit/175d7ea223dcb650ad3f642b0ae23f0ed1cf2a37)
- Updated template language to match autofile [`0a973d6`](https://github.com/RhetTbull/osxphotos/commit/0a973d67f94a5c59ee7dbbf4fb18892c61666d5d)
#### [v0.49.9](https://github.com/RhetTbull/osxphotos/compare/v0.49.8...v0.49.9)
> 26 May 2022
- Updated docs [`8d020cb`](https://github.com/RhetTbull/osxphotos/commit/8d020cbf09fcd2921cfb388a187bb6e891b263b2)
- Implemented retry for export db, #569 [`dae710b`](https://github.com/RhetTbull/osxphotos/commit/dae710b836a8ff0d3553052d9d7f9af6e1a94f02)
- Bug fix, #695 [`7926c8d`](https://github.com/RhetTbull/osxphotos/commit/7926c8d676d0ff38b60eda7c88409a932801f115)
#### [v0.49.8](https://github.com/RhetTbull/osxphotos/compare/v0.49.7...v0.49.8)
> 23 May 2022
- Removed screencast, [skip ci] [`9d7a5e2`](https://github.com/RhetTbull/osxphotos/commit/9d7a5e22d92cd889c2a54b4c58828f6e499995de)
#### [v0.49.7](https://github.com/RhetTbull/osxphotos/compare/v0.49.6...v0.49.7)
> 23 May 2022
- Bug fix, #695 [`e9cc6ce`](https://github.com/RhetTbull/osxphotos/commit/e9cc6ce137927664a42a3dfb1d93d7ba44d74e4d)
- Removed screencast, [skip ci] [`bc32b18`](https://github.com/RhetTbull/osxphotos/commit/bc32b1827f55b4481a49ea2db0530e5a77719028)
- Added screencast [skip ci] [`206ad8c`](https://github.com/RhetTbull/osxphotos/commit/206ad8c33c04e4b3074e8b32942266cb043aebd4)
#### [v0.49.6](https://github.com/RhetTbull/osxphotos/compare/v0.49.5...v0.49.6)
> 22 May 2022
- Bug fix [`7a52d41`](https://github.com/RhetTbull/osxphotos/commit/7a52d413a3cb130a6d11413775fad88933249f36)
- Fixed detected_text for videos [`ab8b7b4`](https://github.com/RhetTbull/osxphotos/commit/ab8b7b4b198c00569b3852b9a21b0b032fad1940)
#### [v0.49.5](https://github.com/RhetTbull/osxphotos/compare/v0.49.4...v0.49.5)
> 22 May 2022
- Feature inspect command [`#701`](https://github.com/RhetTbull/osxphotos/pull/701)
- Initial implementation of inspect command [`#700`](https://github.com/RhetTbull/osxphotos/pull/700)
- Added inspect command [`128e84c`](https://github.com/RhetTbull/osxphotos/commit/128e84c7a4b9745d70e71e3bb1c48c9952431dec)
#### [v0.49.4](https://github.com/RhetTbull/osxphotos/compare/v0.49.3...v0.49.4)
> 21 May 2022
- Added timestamp to export_data in exportdb, #697 [`6400204`](https://github.com/RhetTbull/osxphotos/commit/64002044d2cbbd6fb3c2f0ab69ff6cd5ee2e8e25)
- Added warning on hardlinks to exiftool command [`4e40d4b`](https://github.com/RhetTbull/osxphotos/commit/4e40d4b74e9b244b8eee602f839e595af4f99dfb)
#### [v0.49.3](https://github.com/RhetTbull/osxphotos/compare/v0.49.2...v0.49.3)
> 21 May 2022
- Updated docs [`0a7575b`](https://github.com/RhetTbull/osxphotos/commit/0a7575b889949f9e74ad716bc316e87f2599d4ad)
- Added --uuid-info, --uuid-files to exportdb [`c776f30`](https://github.com/RhetTbull/osxphotos/commit/c776f3070d40ed960eabe3c21c57c354108ff5e4)
#### [v0.49.2](https://github.com/RhetTbull/osxphotos/compare/v0.49.1...v0.49.2)
> 21 May 2022
- Initial implementation of exiftool command, #691 [`#696`](https://github.com/RhetTbull/osxphotos/pull/696)
- Added exiftool command [`8e9f279`](https://github.com/RhetTbull/osxphotos/commit/8e9f27995b56489da8968e77018a8a3d1bbe76bd)
- Added example [skip ci] [`6d5af5c`](https://github.com/RhetTbull/osxphotos/commit/6d5af5c5e87aa0699da7291376cbf05c4237f6ec)
- Updated docs [skip ci] [`3473c2e`](https://github.com/RhetTbull/osxphotos/commit/3473c2ece2b6eea9885893c85aa9eceb16921b94)
- Updated test [`dfcb99f`](https://github.com/RhetTbull/osxphotos/commit/dfcb99f3774cb519c30a9fcf403ca9fdc3fed993)
#### [v0.49.1](https://github.com/RhetTbull/osxphotos/compare/v0.49.0...v0.49.1)
> 17 May 2022

1973
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -107,6 +107,7 @@ Alternatively, you can also run the command line utility like this: ``python3 -m
exportdb Utilities for working with the osxphotos export database
help Print help; for help on commands: help <command>.
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...

View File

@@ -35,6 +35,7 @@ echo "Building docs"
# copy docs to osxphotos/docs/docs.zip for use with `osxphotos docs` command
echo "Zipping docs to osxphotos/docs/docs.zip"
rm osxphotos/docs/docs.zip
zip -r osxphotos/docs/docs.zip docs/*
# build the package

View File

@@ -1,4 +1,4 @@
# Sphinx build info version 1
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
config: 3a7efae54b6e07cdaf59f7d2e9729b6e
config: ad1b9f346384c93c414bf29025529641
tags: 645f666f9bcd5a90fca523b33c5a78b7

View File

@@ -5,7 +5,7 @@
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../genindex.html" /><link rel="search" title="Search" href="../search.html" />
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/>
<title>Overview: module code - osxphotos 0.49.2 documentation</title>
<title>Overview: module code - osxphotos 0.50.4 documentation</title>
<link rel="stylesheet" type="text/css" href="../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
<link rel="stylesheet" type="text/css" href="../_static/copybutton.css" />
@@ -123,7 +123,7 @@
</label>
</div>
<div class="header-center">
<a href="../index.html"><div class="brand">osxphotos 0.49.2 documentation</div></a>
<a href="../index.html"><div class="brand">osxphotos 0.50.4 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -146,7 +146,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="../index.html">
<span class="sidebar-brand-text">osxphotos 0.49.2 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.50.4 documentation</span>
</a><form class="sidebar-search-container" method="get" action="../search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">

View File

@@ -5,7 +5,7 @@
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../genindex.html" /><link rel="search" title="Search" href="../../search.html" />
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/>
<title>osxphotos._constants - osxphotos 0.48.1 documentation</title>
<title>osxphotos._constants - osxphotos 0.50.4 documentation</title>
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css" />
@@ -123,7 +123,7 @@
</label>
</div>
<div class="header-center">
<a href="../../index.html"><div class="brand">osxphotos 0.48.1 documentation</div></a>
<a href="../../index.html"><div class="brand">osxphotos 0.50.4 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -146,7 +146,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../index.html">
<span class="sidebar-brand-text">osxphotos 0.48.1 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.50.4 documentation</span>
</a><form class="sidebar-search-container" method="get" action="../../search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
@@ -240,10 +240,8 @@
<span class="c1"># Ranges for model version by Photos version</span>
<span class="n">_PHOTOS_5_MODEL_VERSION</span> <span class="o">=</span> <span class="p">[</span><span class="mi">13000</span><span class="p">,</span> <span class="mi">13999</span><span class="p">]</span>
<span class="n">_PHOTOS_6_MODEL_VERSION</span> <span class="o">=</span> <span class="p">[</span><span class="mi">14000</span><span class="p">,</span> <span class="mi">14999</span><span class="p">]</span>
<span class="n">_PHOTOS_7_MODEL_VERSION</span> <span class="o">=</span> <span class="p">[</span>
<span class="mi">15000</span><span class="p">,</span>
<span class="mi">15999</span><span class="p">,</span>
<span class="p">]</span> <span class="c1"># Monterey developer preview is 15134, 12.1 is 15331</span>
<span class="n">_PHOTOS_7_MODEL_VERSION</span> <span class="o">=</span> <span class="p">[</span><span class="mi">15000</span><span class="p">,</span> <span class="mi">15999</span><span class="p">]</span> <span class="c1"># Dev preview: 15134, 12.1: 15331</span>
<span class="n">_PHOTOS_8_MODEL_VERSION</span> <span class="o">=</span> <span class="p">[</span><span class="mi">16000</span><span class="p">,</span> <span class="mi">16999</span><span class="p">]</span> <span class="c1"># Ventura dev preview: 16119</span>
<span class="c1"># some table names differ between Photos 5 and Photos 6</span>
<span class="n">_DB_TABLE_NAMES</span> <span class="o">=</span> <span class="p">{</span>
@@ -283,6 +281,18 @@
<span class="s2">"ASSET_ALBUM_TABLE"</span><span class="p">:</span> <span class="s2">"Z_27ASSETS"</span><span class="p">,</span>
<span class="s2">"HDR_TYPE"</span><span class="p">:</span> <span class="s2">"ZHDRTYPE"</span><span class="p">,</span>
<span class="p">},</span>
<span class="mi">8</span><span class="p">:</span> <span class="p">{</span>
<span class="s2">"ASSET"</span><span class="p">:</span> <span class="s2">"ZASSET"</span><span class="p">,</span>
<span class="s2">"KEYWORD_JOIN"</span><span class="p">:</span> <span class="s2">"Z_1KEYWORDS.Z_40KEYWORDS"</span><span class="p">,</span>
<span class="s2">"ALBUM_JOIN"</span><span class="p">:</span> <span class="s2">"Z_28ASSETS.Z_3ASSETS"</span><span class="p">,</span>
<span class="s2">"ALBUM_SORT_ORDER"</span><span class="p">:</span> <span class="s2">"Z_28ASSETS.Z_FOK_3ASSETS"</span><span class="p">,</span>
<span class="s2">"IMPORT_FOK"</span><span class="p">:</span> <span class="s2">"null"</span><span class="p">,</span>
<span class="s2">"DEPTH_STATE"</span><span class="p">:</span> <span class="s2">"ZASSET.ZDEPTHTYPE"</span><span class="p">,</span>
<span class="s2">"UTI_ORIGINAL"</span><span class="p">:</span> <span class="s2">"ZINTERNALRESOURCE.ZCOMPACTUTI"</span><span class="p">,</span>
<span class="s2">"ASSET_ALBUM_JOIN"</span><span class="p">:</span> <span class="s2">"Z_28ASSETS.Z_28ALBUMS"</span><span class="p">,</span>
<span class="s2">"ASSET_ALBUM_TABLE"</span><span class="p">:</span> <span class="s2">"Z_28ASSETS"</span><span class="p">,</span>
<span class="s2">"HDR_TYPE"</span><span class="p">:</span> <span class="s2">"ZHDRTYPE"</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">}</span>
<span class="c1"># which version operating systems have been tested</span>
@@ -303,6 +313,7 @@
<span class="p">(</span><span class="s2">"12"</span><span class="p">,</span> <span class="s2">"1"</span><span class="p">),</span>
<span class="p">(</span><span class="s2">"12"</span><span class="p">,</span> <span class="s2">"2"</span><span class="p">),</span>
<span class="p">(</span><span class="s2">"12"</span><span class="p">,</span> <span class="s2">"3"</span><span class="p">),</span>
<span class="p">(</span><span class="s2">"12"</span><span class="p">,</span> <span class="s2">"4"</span><span class="p">),</span>
<span class="p">]</span>
<span class="c1"># Photos 5 has persons who are empty string if unidentified face</span>

View File

@@ -5,7 +5,7 @@
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../genindex.html" /><link rel="search" title="Search" href="../../search.html" />
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/>
<title>osxphotos.export_db - osxphotos 0.49.2 documentation</title>
<title>osxphotos.export_db - osxphotos 0.50.3 documentation</title>
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css" />
@@ -123,7 +123,7 @@
</label>
</div>
<div class="header-center">
<a href="../../index.html"><div class="brand">osxphotos 0.49.2 documentation</div></a>
<a href="../../index.html"><div class="brand">osxphotos 0.50.3 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -146,7 +146,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../index.html">
<span class="sidebar-brand-text">osxphotos 0.49.2 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.50.3 documentation</span>
</a><form class="sidebar-search-container" method="get" action="../../search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
@@ -202,8 +202,10 @@
<span class="kn">import</span> <span class="nn">json</span>
<span class="kn">import</span> <span class="nn">logging</span>
<span class="kn">import</span> <span class="nn">os</span>
<span class="kn">import</span> <span class="nn">os.path</span>
<span class="kn">import</span> <span class="nn">pathlib</span>
<span class="kn">import</span> <span class="nn">pickle</span>
<span class="kn">import</span> <span class="nn">re</span>
<span class="kn">import</span> <span class="nn">sqlite3</span>
<span class="kn">import</span> <span class="nn">sys</span>
<span class="kn">import</span> <span class="nn">time</span>
@@ -211,9 +213,9 @@
<span class="kn">from</span> <span class="nn">io</span> <span class="kn">import</span> <span class="n">StringIO</span>
<span class="kn">from</span> <span class="nn">sqlite3</span> <span class="kn">import</span> <span class="n">Error</span>
<span class="kn">from</span> <span class="nn">tempfile</span> <span class="kn">import</span> <span class="n">TemporaryDirectory</span>
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Any</span><span class="p">,</span> <span class="n">Optional</span><span class="p">,</span> <span class="n">Tuple</span><span class="p">,</span> <span class="n">Union</span>
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Any</span><span class="p">,</span> <span class="n">List</span><span class="p">,</span> <span class="n">Optional</span><span class="p">,</span> <span class="n">Tuple</span><span class="p">,</span> <span class="n">Union</span>
<span class="kn">from</span> <span class="nn">tenacity</span> <span class="kn">import</span> <span class="n">retry</span><span class="p">,</span> <span class="n">stop_after_attempt</span>
<span class="kn">from</span> <span class="nn">tenacity</span> <span class="kn">import</span> <span class="n">retry</span><span class="p">,</span> <span class="n">retry_if_not_exception_type</span><span class="p">,</span> <span class="n">stop_after_attempt</span>
<span class="kn">from</span> <span class="nn">._constants</span> <span class="kn">import</span> <span class="n">OSXPHOTOS_EXPORT_DB</span>
<span class="kn">from</span> <span class="nn">._version</span> <span class="kn">import</span> <span class="n">__version__</span>
@@ -226,11 +228,11 @@
<span class="s2">"ExportDBTemp"</span><span class="p">,</span>
<span class="p">]</span>
<span class="n">OSXPHOTOS_EXPORTDB_VERSION</span> <span class="o">=</span> <span class="s2">"7.0"</span>
<span class="n">OSXPHOTOS_EXPORTDB_VERSION</span> <span class="o">=</span> <span class="s2">"7.1"</span>
<span class="n">OSXPHOTOS_ABOUT_STRING</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"Created by osxphotos version </span><span class="si">{</span><span class="n">__version__</span><span class="si">}</span><span class="s2"> (https://github.com/RhetTbull/osxphotos) on </span><span class="si">{</span><span class="n">datetime</span><span class="o">.</span><span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">()</span><span class="si">}</span><span class="s2">"</span>
<span class="c1"># max retry attempts for methods which use tenacity.retry</span>
<span class="n">MAX_RETRY_ATTEMPTS</span> <span class="o">=</span> <span class="mi">5</span>
<span class="n">MAX_RETRY_ATTEMPTS</span> <span class="o">=</span> <span class="mi">3</span>
<span class="c1"># maximum number of export results rows to save</span>
<span class="n">MAX_EXPORT_RESULTS_DATA_ROWS</span> <span class="o">=</span> <span class="mi">10</span>
@@ -295,7 +297,8 @@
<span class="sd">"""returns path to export directory"""</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_path</span>
<div class="viewcode-block" id="ExportDB.get_file_record"><a class="viewcode-back" href="../../reference.html#osxphotos.ExportDB.get_file_record">[docs]</a> <span class="k">def</span> <span class="nf">get_file_record</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">filename</span><span class="p">:</span> <span class="n">Union</span><span class="p">[</span><span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">,</span> <span class="nb">str</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="s2">"ExportRecord"</span><span class="p">:</span>
<div class="viewcode-block" id="ExportDB.get_file_record"><a class="viewcode-back" href="../../reference.html#osxphotos.ExportDB.get_file_record">[docs]</a> <span class="nd">@retry</span><span class="p">(</span><span class="n">stop</span><span class="o">=</span><span class="n">stop_after_attempt</span><span class="p">(</span><span class="n">MAX_RETRY_ATTEMPTS</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">get_file_record</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">filename</span><span class="p">:</span> <span class="n">Union</span><span class="p">[</span><span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">,</span> <span class="nb">str</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="s2">"ExportRecord"</span><span class="p">:</span>
<span class="sd">"""get info for filename and uuid</span>
<span class="sd"> Returns: an ExportRecord object or None if filename not found</span>
@@ -312,7 +315,11 @@
<span class="k">return</span> <span class="n">ExportRecord</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="n">filename_normalized</span><span class="p">)</span>
<span class="k">return</span> <span class="kc">None</span></div>
<div class="viewcode-block" id="ExportDB.create_file_record"><a class="viewcode-back" href="../../reference.html#osxphotos.ExportDB.create_file_record">[docs]</a> <span class="k">def</span> <span class="nf">create_file_record</span><span class="p">(</span>
<div class="viewcode-block" id="ExportDB.create_file_record"><a class="viewcode-back" href="../../reference.html#osxphotos.ExportDB.create_file_record">[docs]</a> <span class="nd">@retry</span><span class="p">(</span>
<span class="n">stop</span><span class="o">=</span><span class="n">stop_after_attempt</span><span class="p">(</span><span class="n">MAX_RETRY_ATTEMPTS</span><span class="p">),</span>
<span class="n">retry</span><span class="o">=</span><span class="n">retry_if_not_exception_type</span><span class="p">(</span><span class="n">sqlite3</span><span class="o">.</span><span class="n">IntegrityError</span><span class="p">),</span>
<span class="p">)</span>
<span class="k">def</span> <span class="nf">create_file_record</span><span class="p">(</span>
<span class="bp">self</span><span class="p">,</span> <span class="n">filename</span><span class="p">:</span> <span class="n">Union</span><span class="p">[</span><span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">,</span> <span class="nb">str</span><span class="p">],</span> <span class="n">uuid</span><span class="p">:</span> <span class="nb">str</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="s2">"ExportRecord"</span><span class="p">:</span>
<span class="sd">"""create a new record for filename and uuid</span>
@@ -330,7 +337,11 @@
<span class="n">conn</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="k">return</span> <span class="n">ExportRecord</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="n">filename_normalized</span><span class="p">)</span></div>
<div class="viewcode-block" id="ExportDB.create_or_get_file_record"><a class="viewcode-back" href="../../reference.html#osxphotos.ExportDB.create_or_get_file_record">[docs]</a> <span class="k">def</span> <span class="nf">create_or_get_file_record</span><span class="p">(</span>
<div class="viewcode-block" id="ExportDB.create_or_get_file_record"><a class="viewcode-back" href="../../reference.html#osxphotos.ExportDB.create_or_get_file_record">[docs]</a> <span class="nd">@retry</span><span class="p">(</span>
<span class="n">stop</span><span class="o">=</span><span class="n">stop_after_attempt</span><span class="p">(</span><span class="n">MAX_RETRY_ATTEMPTS</span><span class="p">),</span>
<span class="n">retry</span><span class="o">=</span><span class="n">retry_if_not_exception_type</span><span class="p">(</span><span class="n">sqlite3</span><span class="o">.</span><span class="n">IntegrityError</span><span class="p">),</span>
<span class="p">)</span>
<span class="k">def</span> <span class="nf">create_or_get_file_record</span><span class="p">(</span>
<span class="bp">self</span><span class="p">,</span> <span class="n">filename</span><span class="p">:</span> <span class="n">Union</span><span class="p">[</span><span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">,</span> <span class="nb">str</span><span class="p">],</span> <span class="n">uuid</span><span class="p">:</span> <span class="nb">str</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="s2">"ExportRecord"</span><span class="p">:</span>
<span class="sd">"""create a new record for filename and uuid or return existing record</span>
@@ -348,102 +359,138 @@
<span class="n">conn</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="k">return</span> <span class="n">ExportRecord</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="n">filename_normalized</span><span class="p">)</span></div>
<div class="viewcode-block" id="ExportDB.get_uuid_for_file"><a class="viewcode-back" href="../../reference.html#osxphotos.ExportDB.get_uuid_for_file">[docs]</a> <span class="k">def</span> <span class="nf">get_uuid_for_file</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">filename</span><span class="p">):</span>
<div class="viewcode-block" id="ExportDB.get_uuid_for_file"><a class="viewcode-back" href="../../reference.html#osxphotos.ExportDB.get_uuid_for_file">[docs]</a> <span class="nd">@retry</span><span class="p">(</span><span class="n">stop</span><span class="o">=</span><span class="n">stop_after_attempt</span><span class="p">(</span><span class="n">MAX_RETRY_ATTEMPTS</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">get_uuid_for_file</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">filename</span><span class="p">):</span>
<span class="sd">"""query database for filename and return UUID</span>
<span class="sd"> returns None if filename not found in database</span>
<span class="sd"> """</span>
<span class="n">filepath_normalized</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_normalize_filepath_relative</span><span class="p">(</span><span class="n">filename</span><span class="p">)</span>
<span class="n">conn</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_conn</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="s2">"SELECT uuid FROM export_data WHERE filepath_normalized = ?"</span><span class="p">,</span>
<span class="p">(</span><span class="n">filepath_normalized</span><span class="p">,),</span>
<span class="p">)</span>
<span class="n">results</span> <span class="o">=</span> <span class="n">c</span><span class="o">.</span><span class="n">fetchone</span><span class="p">()</span>
<span class="n">uuid</span> <span class="o">=</span> <span class="n">results</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">if</span> <span class="n">results</span> <span class="k">else</span> <span class="kc">None</span>
<span class="k">except</span> <span class="n">Error</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="n">e</span><span class="p">)</span>
<span class="n">uuid</span> <span class="o">=</span> <span class="kc">None</span>
<span class="k">return</span> <span class="n">uuid</span></div>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="s2">"SELECT uuid FROM export_data WHERE filepath_normalized = ?"</span><span class="p">,</span>
<span class="p">(</span><span class="n">filepath_normalized</span><span class="p">,),</span>
<span class="p">)</span>
<span class="n">results</span> <span class="o">=</span> <span class="n">c</span><span class="o">.</span><span class="n">fetchone</span><span class="p">()</span>
<span class="k">return</span> <span class="n">results</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">if</span> <span class="n">results</span> <span class="k">else</span> <span class="kc">None</span></div>
<div class="viewcode-block" id="ExportDB.get_photoinfo_for_uuid"><a class="viewcode-back" href="../../reference.html#osxphotos.ExportDB.get_photoinfo_for_uuid">[docs]</a> <span class="k">def</span> <span class="nf">get_photoinfo_for_uuid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">uuid</span><span class="p">):</span>
<div class="viewcode-block" id="ExportDB.get_files_for_uuid"><a class="viewcode-back" href="../../reference.html#osxphotos.ExportDB.get_files_for_uuid">[docs]</a> <span class="nd">@retry</span><span class="p">(</span><span class="n">stop</span><span class="o">=</span><span class="n">stop_after_attempt</span><span class="p">(</span><span class="n">MAX_RETRY_ATTEMPTS</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">get_files_for_uuid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">uuid</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">List</span><span class="p">:</span>
<span class="sd">"""query database for UUID and return list of files associated with UUID or empty list"""</span>
<span class="n">conn</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_conn</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="s2">"SELECT filepath FROM export_data WHERE uuid = ?"</span><span class="p">,</span>
<span class="p">(</span><span class="n">uuid</span><span class="p">,),</span>
<span class="p">)</span>
<span class="n">results</span> <span class="o">=</span> <span class="n">c</span><span class="o">.</span><span class="n">fetchall</span><span class="p">()</span>
<span class="k">return</span> <span class="p">[</span><span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">export_dir</span><span class="p">,</span> <span class="n">r</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span> <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">results</span><span class="p">]</span></div>
<div class="viewcode-block" id="ExportDB.get_photoinfo_for_uuid"><a class="viewcode-back" href="../../reference.html#osxphotos.ExportDB.get_photoinfo_for_uuid">[docs]</a> <span class="nd">@retry</span><span class="p">(</span><span class="n">stop</span><span class="o">=</span><span class="n">stop_after_attempt</span><span class="p">(</span><span class="n">MAX_RETRY_ATTEMPTS</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">get_photoinfo_for_uuid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">uuid</span><span class="p">):</span>
<span class="sd">"""returns the photoinfo JSON struct for a UUID"""</span>
<span class="n">conn</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_conn</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"SELECT photoinfo FROM photoinfo WHERE uuid = ?"</span><span class="p">,</span> <span class="p">(</span><span class="n">uuid</span><span class="p">,))</span>
<span class="n">results</span> <span class="o">=</span> <span class="n">c</span><span class="o">.</span><span class="n">fetchone</span><span class="p">()</span>
<span class="n">info</span> <span class="o">=</span> <span class="n">results</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">if</span> <span class="n">results</span> <span class="k">else</span> <span class="kc">None</span>
<span class="k">except</span> <span class="n">Error</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="n">e</span><span class="p">)</span>
<span class="n">info</span> <span class="o">=</span> <span class="kc">None</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"SELECT photoinfo FROM photoinfo WHERE uuid = ?"</span><span class="p">,</span> <span class="p">(</span><span class="n">uuid</span><span class="p">,))</span>
<span class="n">results</span> <span class="o">=</span> <span class="n">c</span><span class="o">.</span><span class="n">fetchone</span><span class="p">()</span>
<span class="k">return</span> <span class="n">results</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">if</span> <span class="n">results</span> <span class="k">else</span> <span class="kc">None</span></div>
<span class="k">return</span> <span class="n">info</span></div>
<div class="viewcode-block" id="ExportDB.set_photoinfo_for_uuid"><a class="viewcode-back" href="../../reference.html#osxphotos.ExportDB.set_photoinfo_for_uuid">[docs]</a> <span class="k">def</span> <span class="nf">set_photoinfo_for_uuid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">uuid</span><span class="p">,</span> <span class="n">info</span><span class="p">):</span>
<div class="viewcode-block" id="ExportDB.set_photoinfo_for_uuid"><a class="viewcode-back" href="../../reference.html#osxphotos.ExportDB.set_photoinfo_for_uuid">[docs]</a> <span class="nd">@retry</span><span class="p">(</span>
<span class="n">stop</span><span class="o">=</span><span class="n">stop_after_attempt</span><span class="p">(</span><span class="n">MAX_RETRY_ATTEMPTS</span><span class="p">),</span>
<span class="n">retry</span><span class="o">=</span><span class="n">retry_if_not_exception_type</span><span class="p">(</span><span class="n">sqlite3</span><span class="o">.</span><span class="n">IntegrityError</span><span class="p">),</span>
<span class="p">)</span>
<span class="k">def</span> <span class="nf">set_photoinfo_for_uuid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">uuid</span><span class="p">,</span> <span class="n">info</span><span class="p">):</span>
<span class="sd">"""sets the photoinfo JSON struct for a UUID"""</span>
<span class="n">conn</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_conn</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="s2">"INSERT OR REPLACE INTO photoinfo(uuid, photoinfo) VALUES (?, ?);"</span><span class="p">,</span>
<span class="p">(</span><span class="n">uuid</span><span class="p">,</span> <span class="n">info</span><span class="p">),</span>
<span class="p">)</span>
<span class="n">conn</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="k">except</span> <span class="n">Error</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="n">e</span><span class="p">)</span></div>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="s2">"INSERT OR REPLACE INTO photoinfo(uuid, photoinfo) VALUES (?, ?);"</span><span class="p">,</span>
<span class="p">(</span><span class="n">uuid</span><span class="p">,</span> <span class="n">info</span><span class="p">),</span>
<span class="p">)</span>
<span class="n">conn</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span></div>
<div class="viewcode-block" id="ExportDB.get_previous_uuids"><a class="viewcode-back" href="../../reference.html#osxphotos.ExportDB.get_previous_uuids">[docs]</a> <span class="k">def</span> <span class="nf">get_previous_uuids</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<div class="viewcode-block" id="ExportDB.get_target_for_file"><a class="viewcode-back" href="../../reference.html#osxphotos.ExportDB.get_target_for_file">[docs]</a> <span class="nd">@retry</span><span class="p">(</span><span class="n">stop</span><span class="o">=</span><span class="n">stop_after_attempt</span><span class="p">(</span><span class="n">MAX_RETRY_ATTEMPTS</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">get_target_for_file</span><span class="p">(</span>
<span class="bp">self</span><span class="p">,</span> <span class="n">uuid</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">filename</span><span class="p">:</span> <span class="n">Union</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">]</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]:</span>
<span class="sd">"""query database for file matching file name and return the matching filename if there is one;</span>
<span class="sd"> otherwise return None; looks for file.ext, file (1).ext, file (2).ext and so on to find the</span>
<span class="sd"> actual target name that was used to export filename</span>
<span class="sd"> Returns: the matching filename or None if no match found</span>
<span class="sd"> """</span>
<span class="n">conn</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_conn</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">filepath_normalized</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_normalize_filepath_relative</span><span class="p">(</span><span class="n">filename</span><span class="p">)</span>
<span class="n">filepath_stem</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">splitext</span><span class="p">(</span><span class="n">filepath_normalized</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="s2">"SELECT uuid, filepath, filepath_normalized FROM export_data WHERE uuid = ? AND filepath_normalized LIKE ?"</span><span class="p">,</span>
<span class="p">(</span>
<span class="n">uuid</span><span class="p">,</span>
<span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">filepath_stem</span><span class="si">}</span><span class="s2">%"</span><span class="p">,</span>
<span class="p">),</span>
<span class="p">)</span>
<span class="n">results</span> <span class="o">=</span> <span class="n">c</span><span class="o">.</span><span class="n">fetchall</span><span class="p">()</span>
<span class="k">for</span> <span class="n">result</span> <span class="ow">in</span> <span class="n">results</span><span class="p">:</span>
<span class="n">filepath_normalized</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">splitext</span><span class="p">(</span><span class="n">result</span><span class="p">[</span><span class="mi">2</span><span class="p">])[</span><span class="mi">0</span><span class="p">]</span>
<span class="k">if</span> <span class="n">re</span><span class="o">.</span><span class="n">match</span><span class="p">(</span>
<span class="n">re</span><span class="o">.</span><span class="n">escape</span><span class="p">(</span><span class="n">filepath_stem</span><span class="p">)</span> <span class="o">+</span> <span class="sa">r</span><span class="s2">"(\s\(\d+\))?$"</span><span class="p">,</span> <span class="n">filepath_normalized</span>
<span class="p">):</span>
<span class="k">return</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">export_dir</span><span class="p">,</span> <span class="n">result</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span>
<span class="k">return</span> <span class="kc">None</span></div>
<div class="viewcode-block" id="ExportDB.get_previous_uuids"><a class="viewcode-back" href="../../reference.html#osxphotos.ExportDB.get_previous_uuids">[docs]</a> <span class="nd">@retry</span><span class="p">(</span><span class="n">stop</span><span class="o">=</span><span class="n">stop_after_attempt</span><span class="p">(</span><span class="n">MAX_RETRY_ATTEMPTS</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">get_previous_uuids</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">"""returns list of UUIDs of previously exported photos found in export database"""</span>
<span class="n">conn</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_conn</span>
<span class="n">previous_uuids</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"SELECT DISTINCT uuid FROM export_data"</span><span class="p">)</span>
<span class="n">results</span> <span class="o">=</span> <span class="n">c</span><span class="o">.</span><span class="n">fetchall</span><span class="p">()</span>
<span class="n">previous_uuids</span> <span class="o">=</span> <span class="p">[</span><span class="n">row</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">results</span><span class="p">]</span>
<span class="k">except</span> <span class="n">Error</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="n">e</span><span class="p">)</span>
<span class="k">return</span> <span class="n">previous_uuids</span></div>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"SELECT DISTINCT uuid FROM export_data"</span><span class="p">)</span>
<span class="n">results</span> <span class="o">=</span> <span class="n">c</span><span class="o">.</span><span class="n">fetchall</span><span class="p">()</span>
<span class="k">return</span> <span class="p">[</span><span class="n">row</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">results</span><span class="p">]</span></div>
<div class="viewcode-block" id="ExportDB.set_config"><a class="viewcode-back" href="../../reference.html#osxphotos.ExportDB.set_config">[docs]</a> <span class="k">def</span> <span class="nf">set_config</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">config_data</span><span class="p">):</span>
<div class="viewcode-block" id="ExportDB.set_config"><a class="viewcode-back" href="../../reference.html#osxphotos.ExportDB.set_config">[docs]</a> <span class="nd">@retry</span><span class="p">(</span>
<span class="n">stop</span><span class="o">=</span><span class="n">stop_after_attempt</span><span class="p">(</span><span class="n">MAX_RETRY_ATTEMPTS</span><span class="p">),</span>
<span class="n">retry</span><span class="o">=</span><span class="n">retry_if_not_exception_type</span><span class="p">(</span><span class="n">sqlite3</span><span class="o">.</span><span class="n">IntegrityError</span><span class="p">),</span>
<span class="p">)</span>
<span class="k">def</span> <span class="nf">set_config</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">config_data</span><span class="p">):</span>
<span class="sd">"""set config in the database"""</span>
<span class="n">conn</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_conn</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">dt</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">()</span><span class="o">.</span><span class="n">isoformat</span><span class="p">()</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="s2">"INSERT OR REPLACE INTO config(datetime, config) VALUES (?, ?);"</span><span class="p">,</span>
<span class="p">(</span><span class="n">dt</span><span class="p">,</span> <span class="n">config_data</span><span class="p">),</span>
<span class="p">)</span>
<span class="n">conn</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="k">except</span> <span class="n">Error</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="n">e</span><span class="p">)</span></div>
<span class="n">dt</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">()</span><span class="o">.</span><span class="n">isoformat</span><span class="p">()</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="s2">"INSERT OR REPLACE INTO config(datetime, config) VALUES (?, ?);"</span><span class="p">,</span>
<span class="p">(</span><span class="n">dt</span><span class="p">,</span> <span class="n">config_data</span><span class="p">),</span>
<span class="p">)</span>
<span class="n">conn</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span></div>
<div class="viewcode-block" id="ExportDB.set_export_results"><a class="viewcode-back" href="../../reference.html#osxphotos.ExportDB.set_export_results">[docs]</a> <span class="k">def</span> <span class="nf">set_export_results</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">results</span><span class="p">):</span>
<div class="viewcode-block" id="ExportDB.set_export_results"><a class="viewcode-back" href="../../reference.html#osxphotos.ExportDB.set_export_results">[docs]</a> <span class="nd">@retry</span><span class="p">(</span>
<span class="n">stop</span><span class="o">=</span><span class="n">stop_after_attempt</span><span class="p">(</span><span class="n">MAX_RETRY_ATTEMPTS</span><span class="p">),</span>
<span class="n">retry</span><span class="o">=</span><span class="n">retry_if_not_exception_type</span><span class="p">(</span><span class="n">sqlite3</span><span class="o">.</span><span class="n">IntegrityError</span><span class="p">),</span>
<span class="p">)</span>
<span class="k">def</span> <span class="nf">set_export_results</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">results</span><span class="p">):</span>
<span class="sd">"""Store export results in database; data is pickled and gzipped for storage"""</span>
<span class="n">results_data</span> <span class="o">=</span> <span class="n">pickle_and_zip</span><span class="p">(</span><span class="n">results</span><span class="p">)</span>
<span class="n">conn</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_conn</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">dt</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">()</span><span class="o">.</span><span class="n">isoformat</span><span class="p">()</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">"""</span>
<span class="sd"> UPDATE export_results_data</span>
<span class="sd"> SET datetime = ?,</span>
<span class="sd"> export_results = ?</span>
<span class="sd"> WHERE datetime = (SELECT MIN(datetime) FROM export_results_data);</span>
<span class="sd"> """</span><span class="p">,</span>
<span class="p">(</span><span class="n">dt</span><span class="p">,</span> <span class="n">results_data</span><span class="p">),</span>
<span class="p">)</span>
<span class="n">conn</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="k">except</span> <span class="n">Error</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="n">e</span><span class="p">)</span></div>
<span class="n">dt</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">()</span><span class="o">.</span><span class="n">isoformat</span><span class="p">()</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">"""</span>
<span class="sd"> UPDATE export_results_data</span>
<span class="sd"> SET datetime = ?,</span>
<span class="sd"> export_results = ?</span>
<span class="sd"> WHERE datetime = (SELECT MIN(datetime) FROM export_results_data);</span>
<span class="sd"> """</span><span class="p">,</span>
<span class="p">(</span><span class="n">dt</span><span class="p">,</span> <span class="n">results_data</span><span class="p">),</span>
<span class="p">)</span>
<span class="n">conn</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span></div>
<div class="viewcode-block" id="ExportDB.get_export_results"><a class="viewcode-back" href="../../reference.html#osxphotos.ExportDB.get_export_results">[docs]</a> <span class="k">def</span> <span class="nf">get_export_results</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">run</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">0</span><span class="p">):</span>
<div class="viewcode-block" id="ExportDB.get_export_results"><a class="viewcode-back" href="../../reference.html#osxphotos.ExportDB.get_export_results">[docs]</a> <span class="nd">@retry</span><span class="p">(</span><span class="n">stop</span><span class="o">=</span><span class="n">stop_after_attempt</span><span class="p">(</span><span class="n">MAX_RETRY_ATTEMPTS</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">get_export_results</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">run</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">0</span><span class="p">):</span>
<span class="sd">"""Retrieve export results from database</span>
<span class="sd"> Args:</span>
@@ -458,47 +505,67 @@
<span class="n">run</span> <span class="o">=</span> <span class="o">-</span><span class="n">run</span>
<span class="n">conn</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_conn</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">"""</span>
<span class="sd"> SELECT export_results</span>
<span class="sd"> FROM export_results_data</span>
<span class="sd"> ORDER BY datetime DESC</span>
<span class="sd"> """</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">rows</span> <span class="o">=</span> <span class="n">c</span><span class="o">.</span><span class="n">fetchall</span><span class="p">()</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">"""</span>
<span class="sd"> SELECT export_results</span>
<span class="sd"> FROM export_results_data</span>
<span class="sd"> ORDER BY datetime DESC</span>
<span class="sd"> """</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">rows</span> <span class="o">=</span> <span class="n">c</span><span class="o">.</span><span class="n">fetchall</span><span class="p">()</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">data</span> <span class="o">=</span> <span class="n">rows</span><span class="p">[</span><span class="n">run</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span>
<span class="n">results</span> <span class="o">=</span> <span class="n">unzip_and_unpickle</span><span class="p">(</span><span class="n">data</span><span class="p">)</span> <span class="k">if</span> <span class="n">data</span> <span class="k">else</span> <span class="kc">None</span>
<span class="k">except</span> <span class="ne">IndexError</span><span class="p">:</span>
<span class="n">results</span> <span class="o">=</span> <span class="kc">None</span>
<span class="k">except</span> <span class="n">Error</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="n">e</span><span class="p">)</span>
<span class="n">data</span> <span class="o">=</span> <span class="n">rows</span><span class="p">[</span><span class="n">run</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span>
<span class="n">results</span> <span class="o">=</span> <span class="n">unzip_and_unpickle</span><span class="p">(</span><span class="n">data</span><span class="p">)</span> <span class="k">if</span> <span class="n">data</span> <span class="k">else</span> <span class="kc">None</span>
<span class="k">except</span> <span class="ne">IndexError</span><span class="p">:</span>
<span class="n">results</span> <span class="o">=</span> <span class="kc">None</span>
<span class="k">return</span> <span class="n">results</span></div>
<div class="viewcode-block" id="ExportDB.get_exported_files"><a class="viewcode-back" href="../../reference.html#osxphotos.ExportDB.get_exported_files">[docs]</a> <span class="k">def</span> <span class="nf">get_exported_files</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<div class="viewcode-block" id="ExportDB.get_exported_files"><a class="viewcode-back" href="../../reference.html#osxphotos.ExportDB.get_exported_files">[docs]</a> <span class="nd">@retry</span><span class="p">(</span><span class="n">stop</span><span class="o">=</span><span class="n">stop_after_attempt</span><span class="p">(</span><span class="n">MAX_RETRY_ATTEMPTS</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">get_exported_files</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">"""Returns tuple of (uuid, filepath) for all paths of all exported files tracked in the database"""</span>
<span class="n">conn</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_conn</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"SELECT uuid, filepath FROM export_data"</span><span class="p">)</span>
<span class="k">except</span> <span class="n">Error</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="n">e</span><span class="p">)</span>
<span class="k">return</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"SELECT uuid, filepath FROM export_data"</span><span class="p">)</span>
<span class="k">while</span> <span class="n">row</span> <span class="o">:=</span> <span class="n">c</span><span class="o">.</span><span class="n">fetchone</span><span class="p">():</span>
<span class="k">yield</span> <span class="n">row</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">export_dir</span><span class="p">,</span> <span class="n">row</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span>
<span class="k">return</span></div>
<div class="viewcode-block" id="ExportDB.close"><a class="viewcode-back" href="../../reference.html#osxphotos.ExportDB.close">[docs]</a> <span class="k">def</span> <span class="nf">close</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">"""close the database connection"""</span>
<span class="k">try</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_conn</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
<span class="k">except</span> <span class="n">Error</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="n">e</span><span class="p">)</span></div>
<div class="viewcode-block" id="ExportDB.delete_data_for_uuid"><a class="viewcode-back" href="../../reference.html#osxphotos.ExportDB.delete_data_for_uuid">[docs]</a> <span class="nd">@retry</span><span class="p">(</span><span class="n">stop</span><span class="o">=</span><span class="n">stop_after_attempt</span><span class="p">(</span><span class="n">MAX_RETRY_ATTEMPTS</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">delete_data_for_uuid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">uuid</span><span class="p">):</span>
<span class="sd">"""Delete all exportdb data for given UUID"""</span>
<span class="n">conn</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_conn</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">count</span> <span class="o">=</span> <span class="mi">0</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"DELETE FROM export_data WHERE uuid = ?;"</span><span class="p">,</span> <span class="p">(</span><span class="n">uuid</span><span class="p">,))</span>
<span class="n">count</span> <span class="o">+=</span> <span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"SELECT CHANGES();"</span><span class="p">)</span><span class="o">.</span><span class="n">fetchone</span><span class="p">()[</span><span class="mi">0</span><span class="p">]</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"DELETE FROM photoinfo WHERE uuid = ?;"</span><span class="p">,</span> <span class="p">(</span><span class="n">uuid</span><span class="p">,))</span>
<span class="n">count</span> <span class="o">+=</span> <span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"SELECT CHANGES();"</span><span class="p">)</span><span class="o">.</span><span class="n">fetchone</span><span class="p">()[</span><span class="mi">0</span><span class="p">]</span>
<span class="n">conn</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="k">return</span> <span class="n">count</span></div>
<div class="viewcode-block" id="ExportDB.delete_data_for_filepath"><a class="viewcode-back" href="../../reference.html#osxphotos.ExportDB.delete_data_for_filepath">[docs]</a> <span class="nd">@retry</span><span class="p">(</span><span class="n">stop</span><span class="o">=</span><span class="n">stop_after_attempt</span><span class="p">(</span><span class="n">MAX_RETRY_ATTEMPTS</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">delete_data_for_filepath</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">filepath</span><span class="p">):</span>
<span class="sd">"""Delete all exportdb data for given filepath"""</span>
<span class="n">conn</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_conn</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">filepath_normalized</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_normalize_filepath_relative</span><span class="p">(</span><span class="n">filepath</span><span class="p">)</span>
<span class="n">results</span> <span class="o">=</span> <span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="s2">"SELECT uuid FROM export_data WHERE filepath_normalized = ?;"</span><span class="p">,</span>
<span class="p">(</span><span class="n">filepath_normalized</span><span class="p">,),</span>
<span class="p">)</span><span class="o">.</span><span class="n">fetchall</span><span class="p">()</span>
<span class="n">count</span> <span class="o">=</span> <span class="mi">0</span>
<span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">results</span><span class="p">:</span>
<span class="n">count</span> <span class="o">+=</span> <span class="bp">self</span><span class="o">.</span><span class="n">delete_data_for_uuid</span><span class="p">(</span><span class="n">row</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
<span class="k">return</span> <span class="n">count</span></div>
<div class="viewcode-block" id="ExportDB.close"><a class="viewcode-back" href="../../reference.html#osxphotos.ExportDB.close">[docs]</a> <span class="nd">@retry</span><span class="p">(</span><span class="n">stop</span><span class="o">=</span><span class="n">stop_after_attempt</span><span class="p">(</span><span class="n">MAX_RETRY_ATTEMPTS</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">close</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">"""close the database connection"""</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_conn</span><span class="o">.</span><span class="n">close</span><span class="p">()</span></div>
<span class="nd">@retry</span><span class="p">(</span><span class="n">stop</span><span class="o">=</span><span class="n">stop_after_attempt</span><span class="p">(</span><span class="n">MAX_RETRY_ATTEMPTS</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">_open_export_db</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">dbfile</span><span class="p">):</span>
<span class="sd">"""open export database and return a db connection</span>
<span class="sd"> if dbfile does not exist, will create and initialize the database</span>
@@ -508,8 +575,6 @@
<span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">dbfile</span><span class="p">):</span>
<span class="n">conn</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_db_connection</span><span class="p">(</span><span class="n">dbfile</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">conn</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">Exception</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Error getting connection to database </span><span class="si">{</span><span class="n">dbfile</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_create_or_migrate_db_tables</span><span class="p">(</span><span class="n">conn</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">was_created</span> <span class="o">=</span> <span class="kc">True</span>
<span class="bp">self</span><span class="o">.</span><span class="n">was_upgraded</span> <span class="o">=</span> <span class="p">()</span>
@@ -533,16 +598,12 @@
<span class="k">return</span> <span class="n">conn</span>
<span class="nd">@retry</span><span class="p">(</span><span class="n">stop</span><span class="o">=</span><span class="n">stop_after_attempt</span><span class="p">(</span><span class="n">MAX_RETRY_ATTEMPTS</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">_get_db_connection</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">dbfile</span><span class="p">):</span>
<span class="sd">"""return db connection to dbname"""</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">conn</span> <span class="o">=</span> <span class="n">sqlite3</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="n">dbfile</span><span class="p">)</span>
<span class="k">except</span> <span class="n">Error</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="n">e</span><span class="p">)</span>
<span class="n">conn</span> <span class="o">=</span> <span class="kc">None</span>
<span class="k">return</span> <span class="n">conn</span>
<span class="k">return</span> <span class="n">sqlite3</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="n">dbfile</span><span class="p">)</span>
<span class="nd">@retry</span><span class="p">(</span><span class="n">stop</span><span class="o">=</span><span class="n">stop_after_attempt</span><span class="p">(</span><span class="n">MAX_RETRY_ATTEMPTS</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">_get_database_version</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">conn</span><span class="p">):</span>
<span class="sd">"""return tuple of (osxphotos, exportdb) versions for database connection conn"""</span>
<span class="n">version_info</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
@@ -638,18 +699,15 @@
<span class="sd">""" CREATE UNIQUE INDEX IF NOT EXISTS idx_detected_text on detected_text (uuid);"""</span><span class="p">,</span>
<span class="p">]</span>
<span class="c1"># create the tables if needed</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="k">for</span> <span class="n">cmd</span> <span class="ow">in</span> <span class="n">sql_commands</span><span class="p">:</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="n">cmd</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="s2">"INSERT INTO version(osxphotos, exportdb) VALUES (?, ?);"</span><span class="p">,</span>
<span class="p">(</span><span class="n">__version__</span><span class="p">,</span> <span class="n">OSXPHOTOS_EXPORTDB_VERSION</span><span class="p">),</span>
<span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"INSERT INTO about(about) VALUES (?);"</span><span class="p">,</span> <span class="p">(</span><span class="n">OSXPHOTOS_ABOUT_STRING</span><span class="p">,))</span>
<span class="n">conn</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="k">except</span> <span class="n">Error</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="n">e</span><span class="p">)</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="k">for</span> <span class="n">cmd</span> <span class="ow">in</span> <span class="n">sql_commands</span><span class="p">:</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="n">cmd</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="s2">"INSERT INTO version(osxphotos, exportdb) VALUES (?, ?);"</span><span class="p">,</span>
<span class="p">(</span><span class="n">__version__</span><span class="p">,</span> <span class="n">OSXPHOTOS_EXPORTDB_VERSION</span><span class="p">),</span>
<span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"INSERT INTO about(about) VALUES (?);"</span><span class="p">,</span> <span class="p">(</span><span class="n">OSXPHOTOS_ABOUT_STRING</span><span class="p">,))</span>
<span class="n">conn</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="c1"># perform needed migrations</span>
<span class="k">if</span> <span class="n">version</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">&lt;</span> <span class="s2">"4.3"</span><span class="p">:</span>
@@ -666,6 +724,10 @@
<span class="c1"># create report_data table</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_migrate_6_0_to_7_0</span><span class="p">(</span><span class="n">conn</span><span class="p">)</span>
<span class="k">if</span> <span class="n">version</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">&lt;</span> <span class="s2">"7.1"</span><span class="p">:</span>
<span class="c1"># add timestamp to export_data</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_migrate_7_0_to_7_1</span><span class="p">(</span><span class="n">conn</span><span class="p">)</span>
<span class="n">conn</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"VACUUM;"</span><span class="p">)</span>
<span class="n">conn</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
@@ -674,6 +736,7 @@
<span class="k">with</span> <span class="n">suppress</span><span class="p">(</span><span class="ne">Exception</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_conn</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
<span class="nd">@retry</span><span class="p">(</span><span class="n">stop</span><span class="o">=</span><span class="n">stop_after_attempt</span><span class="p">(</span><span class="n">MAX_RETRY_ATTEMPTS</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">_insert_run_info</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">dt</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">(</span><span class="n">datetime</span><span class="o">.</span><span class="n">timezone</span><span class="o">.</span><span class="n">utc</span><span class="p">)</span><span class="o">.</span><span class="n">isoformat</span><span class="p">()</span>
<span class="n">python_path</span> <span class="o">=</span> <span class="n">sys</span><span class="o">.</span><span class="n">executable</span>
@@ -681,16 +744,13 @@
<span class="n">args</span> <span class="o">=</span> <span class="s2">" "</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">:])</span> <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">1</span> <span class="k">else</span> <span class="s2">""</span>
<span class="n">cwd</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">getcwd</span><span class="p">()</span>
<span class="n">conn</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_conn</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="s2">"INSERT INTO runs (datetime, python_path, script_name, args, cwd) VALUES (?, ?, ?, ?, ?)"</span><span class="p">,</span>
<span class="p">(</span><span class="n">dt</span><span class="p">,</span> <span class="n">python_path</span><span class="p">,</span> <span class="n">cmd</span><span class="p">,</span> <span class="n">args</span><span class="p">,</span> <span class="n">cwd</span><span class="p">),</span>
<span class="p">)</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="s2">"INSERT INTO runs (datetime, python_path, script_name, args, cwd) VALUES (?, ?, ?, ?, ?)"</span><span class="p">,</span>
<span class="p">(</span><span class="n">dt</span><span class="p">,</span> <span class="n">python_path</span><span class="p">,</span> <span class="n">cmd</span><span class="p">,</span> <span class="n">args</span><span class="p">,</span> <span class="n">cwd</span><span class="p">),</span>
<span class="p">)</span>
<span class="n">conn</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="k">except</span> <span class="n">Error</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="n">e</span><span class="p">)</span>
<span class="n">conn</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">_relative_filepath</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">filepath</span><span class="p">:</span> <span class="n">Union</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
<span class="sd">"""return filepath relative to self._path"""</span>
@@ -746,158 +806,169 @@
<span class="k">def</span> <span class="nf">_migrate_4_3_to_5_0</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">conn</span><span class="p">):</span>
<span class="sd">"""Migrate database from version 4.3 to 5.0"""</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="c1"># add metadata column to files to support --force-update</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"ALTER TABLE files ADD COLUMN metadata TEXT;"</span><span class="p">)</span>
<span class="n">conn</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="k">except</span> <span class="n">Error</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="n">e</span><span class="p">)</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="c1"># add metadata column to files to support --force-update</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"ALTER TABLE files ADD COLUMN metadata TEXT;"</span><span class="p">)</span>
<span class="n">conn</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">_migrate_5_0_to_6_0</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">conn</span><span class="p">):</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="c1"># add export_data table</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">""" CREATE TABLE IF NOT EXISTS export_data(</span>
<span class="sd"> id INTEGER PRIMARY KEY,</span>
<span class="sd"> filepath_normalized TEXT NOT NULL,</span>
<span class="sd"> filepath TEXT NOT NULL,</span>
<span class="sd"> uuid TEXT NOT NULL,</span>
<span class="sd"> src_mode INTEGER,</span>
<span class="sd"> src_size INTEGER,</span>
<span class="sd"> src_mtime REAL,</span>
<span class="sd"> dest_mode INTEGER,</span>
<span class="sd"> dest_size INTEGER,</span>
<span class="sd"> dest_mtime REAL,</span>
<span class="sd"> digest TEXT,</span>
<span class="sd"> exifdata JSON,</span>
<span class="sd"> export_options INTEGER,</span>
<span class="sd"> UNIQUE(filepath_normalized)</span>
<span class="sd"> ); """</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">""" CREATE UNIQUE INDEX IF NOT EXISTS idx_export_data_filepath_normalized on export_data (filepath_normalized); """</span><span class="p">,</span>
<span class="p">)</span>
<span class="c1"># add export_data table</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">""" CREATE TABLE IF NOT EXISTS export_data(</span>
<span class="sd"> id INTEGER PRIMARY KEY,</span>
<span class="sd"> filepath_normalized TEXT NOT NULL,</span>
<span class="sd"> filepath TEXT NOT NULL,</span>
<span class="sd"> uuid TEXT NOT NULL,</span>
<span class="sd"> src_mode INTEGER,</span>
<span class="sd"> src_size INTEGER,</span>
<span class="sd"> src_mtime REAL,</span>
<span class="sd"> dest_mode INTEGER,</span>
<span class="sd"> dest_size INTEGER,</span>
<span class="sd"> dest_mtime REAL,</span>
<span class="sd"> digest TEXT,</span>
<span class="sd"> exifdata JSON,</span>
<span class="sd"> export_options INTEGER,</span>
<span class="sd"> UNIQUE(filepath_normalized)</span>
<span class="sd"> ); """</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">""" CREATE UNIQUE INDEX IF NOT EXISTS idx_export_data_filepath_normalized on export_data (filepath_normalized); """</span><span class="p">,</span>
<span class="p">)</span>
<span class="c1"># migrate data</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">""" INSERT INTO export_data (filepath_normalized, filepath, uuid) SELECT filepath_normalized, filepath, uuid FROM files;"""</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">""" UPDATE export_data </span>
<span class="sd"> SET (src_mode, src_size, src_mtime) = </span>
<span class="sd"> (SELECT mode, size, mtime </span>
<span class="sd"> FROM edited </span>
<span class="sd"> WHERE export_data.filepath_normalized = edited.filepath_normalized);</span>
<span class="sd"> """</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">""" UPDATE export_data </span>
<span class="sd"> SET (dest_mode, dest_size, dest_mtime) = </span>
<span class="sd"> (SELECT orig_mode, orig_size, orig_mtime </span>
<span class="sd"> FROM files </span>
<span class="sd"> WHERE export_data.filepath_normalized = files.filepath_normalized);</span>
<span class="sd"> """</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">""" UPDATE export_data SET digest = </span>
<span class="sd"> (SELECT metadata FROM files </span>
<span class="sd"> WHERE files.filepath_normalized = export_data.filepath_normalized</span>
<span class="sd"> ); """</span>
<span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">""" UPDATE export_data SET exifdata = </span>
<span class="sd"> (SELECT json_exifdata FROM exifdata </span>
<span class="sd"> WHERE exifdata.filepath_normalized = export_data.filepath_normalized</span>
<span class="sd"> ); """</span>
<span class="p">)</span>
<span class="c1"># migrate data</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">""" INSERT INTO export_data (filepath_normalized, filepath, uuid) SELECT filepath_normalized, filepath, uuid FROM files;"""</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">""" UPDATE export_data </span>
<span class="sd"> SET (src_mode, src_size, src_mtime) = </span>
<span class="sd"> (SELECT mode, size, mtime </span>
<span class="sd"> FROM edited </span>
<span class="sd"> WHERE export_data.filepath_normalized = edited.filepath_normalized);</span>
<span class="sd"> """</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">""" UPDATE export_data </span>
<span class="sd"> SET (dest_mode, dest_size, dest_mtime) = </span>
<span class="sd"> (SELECT orig_mode, orig_size, orig_mtime </span>
<span class="sd"> FROM files </span>
<span class="sd"> WHERE export_data.filepath_normalized = files.filepath_normalized);</span>
<span class="sd"> """</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">""" UPDATE export_data SET digest = </span>
<span class="sd"> (SELECT metadata FROM files </span>
<span class="sd"> WHERE files.filepath_normalized = export_data.filepath_normalized</span>
<span class="sd"> ); """</span>
<span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">""" UPDATE export_data SET exifdata = </span>
<span class="sd"> (SELECT json_exifdata FROM exifdata </span>
<span class="sd"> WHERE exifdata.filepath_normalized = export_data.filepath_normalized</span>
<span class="sd"> ); """</span>
<span class="p">)</span>
<span class="c1"># create config table</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">""" CREATE TABLE IF NOT EXISTS config (</span>
<span class="sd"> id INTEGER PRIMARY KEY,</span>
<span class="sd"> datetime TEXT,</span>
<span class="sd"> config TEXT </span>
<span class="sd"> ); """</span>
<span class="p">)</span>
<span class="c1"># create config table</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">""" CREATE TABLE IF NOT EXISTS config (</span>
<span class="sd"> id INTEGER PRIMARY KEY,</span>
<span class="sd"> datetime TEXT,</span>
<span class="sd"> config TEXT </span>
<span class="sd"> ); """</span>
<span class="p">)</span>
<span class="c1"># create photoinfo table</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">""" CREATE TABLE IF NOT EXISTS photoinfo (</span>
<span class="sd"> id INTEGER PRIMARY KEY,</span>
<span class="sd"> uuid TEXT NOT NULL,</span>
<span class="sd"> photoinfo JSON,</span>
<span class="sd"> UNIQUE(uuid)</span>
<span class="sd"> ); """</span>
<span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">"""CREATE UNIQUE INDEX IF NOT EXISTS idx_photoinfo_uuid on photoinfo (uuid);"""</span>
<span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">""" INSERT INTO photoinfo (uuid, photoinfo) SELECT uuid, json_info FROM info;"""</span>
<span class="p">)</span>
<span class="c1"># create photoinfo table</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">""" CREATE TABLE IF NOT EXISTS photoinfo (</span>
<span class="sd"> id INTEGER PRIMARY KEY,</span>
<span class="sd"> uuid TEXT NOT NULL,</span>
<span class="sd"> photoinfo JSON,</span>
<span class="sd"> UNIQUE(uuid)</span>
<span class="sd"> ); """</span>
<span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">"""CREATE UNIQUE INDEX IF NOT EXISTS idx_photoinfo_uuid on photoinfo (uuid);"""</span>
<span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">""" INSERT INTO photoinfo (uuid, photoinfo) SELECT uuid, json_info FROM info;"""</span>
<span class="p">)</span>
<span class="c1"># drop indexes no longer needed</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"DROP INDEX IF EXISTS idx_files_filepath_normalized;"</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"DROP INDEX IF EXISTS idx_exifdata_filename;"</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"DROP INDEX IF EXISTS idx_edited_filename;"</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"DROP INDEX IF EXISTS idx_converted_filename;"</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"DROP INDEX IF EXISTS idx_sidecar_filename;"</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"DROP INDEX IF EXISTS idx_detected_text;"</span><span class="p">)</span>
<span class="c1"># drop indexes no longer needed</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"DROP INDEX IF EXISTS idx_files_filepath_normalized;"</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"DROP INDEX IF EXISTS idx_exifdata_filename;"</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"DROP INDEX IF EXISTS idx_edited_filename;"</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"DROP INDEX IF EXISTS idx_converted_filename;"</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"DROP INDEX IF EXISTS idx_sidecar_filename;"</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"DROP INDEX IF EXISTS idx_detected_text;"</span><span class="p">)</span>
<span class="c1"># drop tables no longer needed</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"DROP TABLE IF EXISTS files;"</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"DROP TABLE IF EXISTS info;"</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"DROP TABLE IF EXISTS exifdata;"</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"DROP TABLE IF EXISTS edited;"</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"DROP TABLE IF EXISTS converted;"</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"DROP TABLE IF EXISTS sidecar;"</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"DROP TABLE IF EXISTS detected_text;"</span><span class="p">)</span>
<span class="c1"># drop tables no longer needed</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"DROP TABLE IF EXISTS files;"</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"DROP TABLE IF EXISTS info;"</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"DROP TABLE IF EXISTS exifdata;"</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"DROP TABLE IF EXISTS edited;"</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"DROP TABLE IF EXISTS converted;"</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"DROP TABLE IF EXISTS sidecar;"</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"DROP TABLE IF EXISTS detected_text;"</span><span class="p">)</span>
<span class="n">conn</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="k">except</span> <span class="n">Error</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="n">e</span><span class="p">)</span>
<span class="n">conn</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">_migrate_6_0_to_7_0</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">conn</span><span class="p">):</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">"""CREATE TABLE IF NOT EXISTS export_results_data (</span>
<span class="sd"> id INTEGER PRIMARY KEY,</span>
<span class="sd"> datetime TEXT,</span>
<span class="sd"> export_results BLOB</span>
<span class="sd"> );"""</span>
<span class="p">)</span>
<span class="c1"># pre-populate report_data table with blank fields</span>
<span class="c1"># ExportDB will use these as circular buffer always writing to the oldest record</span>
<span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">MAX_EXPORT_RESULTS_DATA_ROWS</span><span class="p">):</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">"""CREATE TABLE IF NOT EXISTS export_results_data (</span>
<span class="sd"> id INTEGER PRIMARY KEY,</span>
<span class="sd"> datetime TEXT,</span>
<span class="sd"> export_results BLOB</span>
<span class="sd"> );"""</span>
<span class="sd">"""INSERT INTO export_results_data (datetime, export_results) VALUES (?, ?);"""</span><span class="p">,</span>
<span class="p">(</span><span class="n">datetime</span><span class="o">.</span><span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">()</span><span class="o">.</span><span class="n">isoformat</span><span class="p">(),</span> <span class="sa">b</span><span class="s2">""</span><span class="p">),</span>
<span class="p">)</span>
<span class="c1"># pre-populate report_data table with blank fields</span>
<span class="c1"># ExportDB will use these as circular buffer always writing to the oldest record</span>
<span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">MAX_EXPORT_RESULTS_DATA_ROWS</span><span class="p">):</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">"""INSERT INTO export_results_data (datetime, export_results) VALUES (?, ?);"""</span><span class="p">,</span>
<span class="p">(</span><span class="n">datetime</span><span class="o">.</span><span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">()</span><span class="o">.</span><span class="n">isoformat</span><span class="p">(),</span> <span class="sa">b</span><span class="s2">""</span><span class="p">),</span>
<span class="p">)</span>
<span class="c1"># sleep a tiny bit just to ensure time stamps increment</span>
<span class="n">time</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mf">0.001</span><span class="p">)</span>
<span class="n">conn</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="k">except</span> <span class="n">Error</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="n">e</span><span class="p">)</span>
<span class="c1"># sleep a tiny bit just to ensure time stamps increment</span>
<span class="n">time</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mf">0.001</span><span class="p">)</span>
<span class="n">conn</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">_migrate_7_0_to_7_1</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">conn</span><span class="p">):</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"""ALTER TABLE export_data ADD COLUMN timestamp DATETIME;"""</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">"""</span>
<span class="sd"> CREATE TRIGGER insert_timestamp_trigger</span>
<span class="sd"> AFTER INSERT ON export_data</span>
<span class="sd"> BEGIN</span>
<span class="sd"> UPDATE export_data SET timestamp = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') WHERE id = NEW.id;</span>
<span class="sd"> END;</span>
<span class="sd"> """</span>
<span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">"""</span>
<span class="sd"> CREATE TRIGGER update_timestamp_trigger</span>
<span class="sd"> AFTER UPDATE On export_data</span>
<span class="sd"> BEGIN</span>
<span class="sd"> UPDATE export_data SET timestamp = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') WHERE id = NEW.id;</span>
<span class="sd"> END;</span>
<span class="sd"> """</span>
<span class="p">)</span>
<span class="n">conn</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">_perform_db_maintenace</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">conn</span><span class="p">):</span>
<span class="sd">"""Perform database maintenance"""</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">"""DELETE FROM config</span>
<span class="sd"> WHERE id &lt; (</span>
<span class="sd"> SELECT MIN(id)</span>
<span class="sd"> FROM (SELECT id FROM config ORDER BY id DESC LIMIT 9)</span>
<span class="sd"> );</span>
<span class="sd"> """</span>
<span class="p">)</span>
<span class="n">conn</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="k">except</span> <span class="n">Error</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="n">e</span><span class="p">)</span></div>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sd">"""DELETE FROM config</span>
<span class="sd"> WHERE id &lt; (</span>
<span class="sd"> SELECT MIN(id)</span>
<span class="sd"> FROM (SELECT id FROM config ORDER BY id DESC LIMIT 9)</span>
<span class="sd"> );</span>
<span class="sd"> """</span>
<span class="p">)</span>
<span class="n">conn</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span></div>
<span class="k">class</span> <span class="nc">ExportDBInMemory</span><span class="p">(</span><span class="n">ExportDB</span><span class="p">):</span>
@@ -949,14 +1020,13 @@
<span class="n">conn_on_disk</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="n">conn_on_disk</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
<span class="nd">@retry</span><span class="p">(</span><span class="n">stop</span><span class="o">=</span><span class="n">stop_after_attempt</span><span class="p">(</span><span class="n">MAX_RETRY_ATTEMPTS</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">close</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">"""close the database connection"""</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_conn</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_conn</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
<span class="k">except</span> <span class="n">Error</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="n">e</span><span class="p">)</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_conn</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_conn</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
<span class="nd">@retry</span><span class="p">(</span><span class="n">stop</span><span class="o">=</span><span class="n">stop_after_attempt</span><span class="p">(</span><span class="n">MAX_RETRY_ATTEMPTS</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">_open_export_db</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">dbfile</span><span class="p">):</span> <span class="c1"># sourcery skip: raise-specific-error</span>
<span class="sd">"""open export database and return a db connection</span>
<span class="sd"> returns: connection to the database</span>
@@ -990,13 +1060,7 @@
<span class="k">def</span> <span class="nf">_get_db_connection</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">"""return db connection to in memory database"""</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">conn</span> <span class="o">=</span> <span class="n">sqlite3</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="s2">":memory:"</span><span class="p">)</span>
<span class="k">except</span> <span class="n">Error</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="n">e</span><span class="p">)</span>
<span class="n">conn</span> <span class="o">=</span> <span class="kc">None</span>
<span class="k">return</span> <span class="n">conn</span>
<span class="k">return</span> <span class="n">sqlite3</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="s2">":memory:"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">_dump_db</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">conn</span><span class="p">:</span> <span class="n">sqlite3</span><span class="o">.</span><span class="n">Connection</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">StringIO</span><span class="p">:</span>
<span class="sd">"""dump sqlite db to a string buffer"""</span>
@@ -1012,7 +1076,7 @@
<span class="bp">self</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
<span class="k">class</span> <span class="nc">ExportDBTemp</span><span class="p">(</span><span class="n">ExportDBInMemory</span><span class="p">):</span>
<div class="viewcode-block" id="ExportDBTemp"><a class="viewcode-back" href="../../reference.html#osxphotos.ExportDBTemp">[docs]</a><span class="k">class</span> <span class="nc">ExportDBTemp</span><span class="p">(</span><span class="n">ExportDBInMemory</span><span class="p">):</span>
<span class="sd">"""Temporary in-memory version of ExportDB"""</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
@@ -1027,7 +1091,7 @@
<span class="n">filepath</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">filepath</span><span class="p">)</span>
<span class="k">if</span> <span class="n">filepath</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="s2">"/"</span><span class="p">:</span>
<span class="k">return</span> <span class="n">filepath</span><span class="p">[</span><span class="mi">1</span><span class="p">:]</span>
<span class="k">return</span> <span class="n">filepath</span>
<span class="k">return</span> <span class="n">filepath</span></div>
<span class="k">class</span> <span class="nc">ExportRecord</span><span class="p">:</span>
@@ -1244,6 +1308,21 @@
<span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">_context_manager</span><span class="p">:</span>
<span class="n">conn</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">timestamp</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">"""returns the timestamp value"""</span>
<span class="n">conn</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_conn</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="k">if</span> <span class="n">row</span> <span class="o">:=</span> <span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="s2">"SELECT timestamp FROM export_data WHERE filepath_normalized = ?;"</span><span class="p">,</span>
<span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_filepath_normalized</span><span class="p">,),</span>
<span class="p">)</span><span class="o">.</span><span class="n">fetchone</span><span class="p">():</span>
<span class="k">return</span> <span class="n">row</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span>
<span class="sa">f</span><span class="s2">"No timestamp found in database for </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_filepath_normalized</span><span class="si">}</span><span class="s2">"</span>
<span class="p">)</span>
<span class="k">def</span> <span class="nf">asdict</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">"""Return dict of self"""</span>
<span class="n">exifdata</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">exifdata</span><span class="p">)</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">exifdata</span> <span class="k">else</span> <span class="kc">None</span>
@@ -1252,6 +1331,7 @@
<span class="s2">"filepath"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">filepath</span><span class="p">,</span>
<span class="s2">"filepath_normalized"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">filepath_normalized</span><span class="p">,</span>
<span class="s2">"uuid"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">,</span>
<span class="s2">"timestamp"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">timestamp</span><span class="p">,</span>
<span class="s2">"digest"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">digest</span><span class="p">,</span>
<span class="s2">"src_sig"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">src_sig</span><span class="p">,</span>
<span class="s2">"dest_sig"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">dest_sig</span><span class="p">,</span>

View File

@@ -1,37 +1,200 @@
<!doctype html>
<html class="no-js">
<head><meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../genindex.html" /><link rel="search" title="Search" href="../../search.html" />
<!DOCTYPE html>
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/>
<title>osxphotos.personinfo - osxphotos 0.50.4 documentation</title>
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css" />
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo-extensions.css?digest=30d1aed668e5c3a91c3e3bf6a60b675221979f0e" />
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.personinfo &#8212; osxphotos 0.47.9 documentation</title>
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../../_static/alabaster.css" />
<script data-url_root="../../" id="documentation_options" src="../../_static/documentation_options.js"></script>
<script src="../../_static/jquery.js"></script>
<script src="../../_static/underscore.js"></script>
<script src="../../_static/doctools.js"></script>
<link rel="index" title="Index" href="../../genindex.html" />
<link rel="search" title="Search" href="../../search.html" />
<link rel="stylesheet" href="../../_static/custom.css" type="text/css" />
<style>
body {
--color-code-background: #f8f8f8;
--color-code-foreground: black;
}
@media not print {
body[data-theme="dark"] {
--color-code-background: #202020;
--color-code-foreground: #d0d0d0;
}
@media (prefers-color-scheme: dark) {
body:not([data-theme="light"]) {
--color-code-background: #202020;
--color-code-foreground: #d0d0d0;
}
}
}
</style></head>
<body>
<script>
document.body.dataset.theme = localStorage.getItem("theme") || "auto";
</script>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="svg-toc" viewBox="0 0 24 24">
<title>Contents</title>
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 1024 1024">
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM115.4 518.9L271.7 642c5.8 4.6 14.4.5 14.4-6.9V388.9c0-7.4-8.5-11.5-14.4-6.9L115.4 505.1a8.74 8.74 0 0 0 0 13.8z"/>
</svg>
</symbol>
<symbol id="svg-menu" viewBox="0 0 24 24">
<title>Menu</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-menu">
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
</symbol>
<symbol id="svg-arrow-right" viewBox="0 0 24 24">
<title>Expand</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-chevron-right">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</symbol>
<symbol id="svg-sun" viewBox="0 0 24 24">
<title>Light mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="feather-sun">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
</symbol>
<symbol id="svg-moon" viewBox="0 0 24 24">
<title>Dark mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z" />
</svg>
</symbol>
<symbol id="svg-sun-half" viewBox="0 0 24 24">
<title>Auto light/dark mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-shadow">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<circle cx="12" cy="12" r="9" />
<path d="M13 12h5" />
<path d="M13 15h4" />
<path d="M13 18h1" />
<path d="M13 9h4" />
<path d="M13 6h1" />
</svg>
</symbol>
</svg>
<input type="checkbox" class="sidebar-toggle" name="__navigation" id="__navigation">
<input type="checkbox" class="sidebar-toggle" name="__toc" id="__toc">
<label class="overlay sidebar-overlay" for="__navigation">
<div class="visually-hidden">Hide navigation sidebar</div>
</label>
<label class="overlay toc-overlay" for="__toc">
<div class="visually-hidden">Hide table of contents sidebar</div>
</label>
<div class="page">
<header class="mobile-header">
<div class="header-left">
<label class="nav-overlay-icon" for="__navigation">
<div class="visually-hidden">Toggle site navigation sidebar</div>
<i class="icon"><svg><use href="#svg-menu"></use></svg></i>
</label>
</div>
<div class="header-center">
<a href="../../index.html"><div class="brand">osxphotos 0.50.4 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
<button class="theme-toggle">
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
</button>
</div>
<label class="toc-overlay-icon toc-header-icon no-toc" for="__toc">
<div class="visually-hidden">Toggle table of contents sidebar</div>
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
</label>
</div>
</header>
<aside class="sidebar-drawer">
<div class="sidebar-container">
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../index.html">
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9" />
</head><body>
<span class="sidebar-brand-text">osxphotos 0.50.4 documentation</span>
</a><form class="sidebar-search-container" method="get" action="../../search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
<input type="hidden" name="check_keywords" value="yes">
<input type="hidden" name="area" value="default">
</form>
<div id="searchbox"></div><div class="sidebar-scroll"><div class="sidebar-tree">
<ul>
<li class="toctree-l1"><a class="reference internal" href="../../overview.html">OSXPhotos</a></li>
<li class="toctree-l1"><a class="reference internal" href="../../tutorial.html">OSXPhotos Tutorial</a></li>
<li class="toctree-l1"><a class="reference internal" href="../../cli.html">OSXPhotos Command Line Interface (CLI)</a></li>
<li class="toctree-l1"><a class="reference internal" href="../../template_help.html">OSXPhotos Template System</a></li>
<li class="toctree-l1"><a class="reference internal" href="../../package_overview.html">OSXPhotos Python Package Overview</a></li>
<li class="toctree-l1"><a class="reference internal" href="../../reference.html">OSXPhotos python API</a></li>
</ul>
<div class="document">
<div class="documentwrapper">
<div class="bodywrapper">
</div>
</div>
<div class="body" role="main">
<h1>Source code for osxphotos.personinfo</h1><div class="highlight"><pre>
<span></span><span class="sd">&quot;&quot;&quot; PhotoInfo and FaceInfo classes to expose info about persons and faces in the Photos library &quot;&quot;&quot;</span>
</div>
</div>
</aside>
<div class="main">
<div class="content">
<div class="article-container">
<a href="#" class="back-to-top muted-link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8v12z"></path>
</svg>
<span>Back to top</span>
</a>
<div class="content-icon-container"><div class="theme-toggle-container theme-toggle-content">
<button class="theme-toggle">
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
</button>
</div>
<label class="toc-overlay-icon toc-content-icon no-toc" for="__toc">
<div class="visually-hidden">Toggle table of contents sidebar</div>
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
</label>
</div>
<article role="main">
<h1>Source code for osxphotos.personinfo</h1><div class="highlight"><pre>
<span></span><span class="sd">""" PhotoInfo and FaceInfo classes to expose info about persons and faces in the Photos library """</span>
<span class="kn">import</span> <span class="nn">json</span>
<span class="kn">import</span> <span class="nn">logging</span>
@@ -39,17 +202,17 @@
<span class="kn">from</span> <span class="nn">collections</span> <span class="kn">import</span> <span class="n">namedtuple</span>
<span class="n">__all__</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&quot;PersonInfo&quot;</span><span class="p">,</span> <span class="s2">&quot;FaceInfo&quot;</span><span class="p">,</span> <span class="s2">&quot;rotate_image_point&quot;</span><span class="p">]</span>
<span class="n">__all__</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"PersonInfo"</span><span class="p">,</span> <span class="s2">"FaceInfo"</span><span class="p">,</span> <span class="s2">"rotate_image_point"</span><span class="p">]</span>
<span class="n">MWG_RS_Area</span> <span class="o">=</span> <span class="n">namedtuple</span><span class="p">(</span><span class="s2">&quot;MWG_RS_Area&quot;</span><span class="p">,</span> <span class="p">[</span><span class="s2">&quot;x&quot;</span><span class="p">,</span> <span class="s2">&quot;y&quot;</span><span class="p">,</span> <span class="s2">&quot;h&quot;</span><span class="p">,</span> <span class="s2">&quot;w&quot;</span><span class="p">])</span>
<span class="n">MPRI_Reg_Rect</span> <span class="o">=</span> <span class="n">namedtuple</span><span class="p">(</span><span class="s2">&quot;MPRI_Reg_Rect&quot;</span><span class="p">,</span> <span class="p">[</span><span class="s2">&quot;x&quot;</span><span class="p">,</span> <span class="s2">&quot;y&quot;</span><span class="p">,</span> <span class="s2">&quot;h&quot;</span><span class="p">,</span> <span class="s2">&quot;w&quot;</span><span class="p">])</span>
<span class="n">MWG_RS_Area</span> <span class="o">=</span> <span class="n">namedtuple</span><span class="p">(</span><span class="s2">"MWG_RS_Area"</span><span class="p">,</span> <span class="p">[</span><span class="s2">"x"</span><span class="p">,</span> <span class="s2">"y"</span><span class="p">,</span> <span class="s2">"h"</span><span class="p">,</span> <span class="s2">"w"</span><span class="p">])</span>
<span class="n">MPRI_Reg_Rect</span> <span class="o">=</span> <span class="n">namedtuple</span><span class="p">(</span><span class="s2">"MPRI_Reg_Rect"</span><span class="p">,</span> <span class="p">[</span><span class="s2">"x"</span><span class="p">,</span> <span class="s2">"y"</span><span class="p">,</span> <span class="s2">"h"</span><span class="p">,</span> <span class="s2">"w"</span><span class="p">])</span>
<div class="viewcode-block" id="PersonInfo"><a class="viewcode-back" href="../../reference.html#osxphotos.PersonInfo">[docs]</a><span class="k">class</span> <span class="nc">PersonInfo</span><span class="p">:</span>
<span class="sd">&quot;&quot;&quot;Info about a person in the Photos library&quot;&quot;&quot;</span>
<span class="sd">"""Info about a person in the Photos library"""</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">db</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">pk</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Creates a new PersonInfo instance</span>
<span class="sd">"""Creates a new PersonInfo instance</span>
<span class="sd"> Arguments:</span>
<span class="sd"> db: instance of PhotosDB object</span>
@@ -57,16 +220,16 @@
<span class="sd"> Returns:</span>
<span class="sd"> PersonInfo instance</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="sd"> """</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_db</span> <span class="o">=</span> <span class="n">db</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_pk</span> <span class="o">=</span> <span class="n">pk</span>
<span class="n">person</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_dbpersons_pk</span><span class="p">[</span><span class="n">pk</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">uuid</span> <span class="o">=</span> <span class="n">person</span><span class="p">[</span><span class="s2">&quot;uuid&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">name</span> <span class="o">=</span> <span class="n">person</span><span class="p">[</span><span class="s2">&quot;fullname&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">display_name</span> <span class="o">=</span> <span class="n">person</span><span class="p">[</span><span class="s2">&quot;displayname&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">keyface</span> <span class="o">=</span> <span class="n">person</span><span class="p">[</span><span class="s2">&quot;keyface&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">facecount</span> <span class="o">=</span> <span class="n">person</span><span class="p">[</span><span class="s2">&quot;facecount&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">uuid</span> <span class="o">=</span> <span class="n">person</span><span class="p">[</span><span class="s2">"uuid"</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">name</span> <span class="o">=</span> <span class="n">person</span><span class="p">[</span><span class="s2">"fullname"</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">display_name</span> <span class="o">=</span> <span class="n">person</span><span class="p">[</span><span class="s2">"displayname"</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">keyface</span> <span class="o">=</span> <span class="n">person</span><span class="p">[</span><span class="s2">"keyface"</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">facecount</span> <span class="o">=</span> <span class="n">person</span><span class="p">[</span><span class="s2">"facecount"</span><span class="p">]</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">keyphoto</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
@@ -74,9 +237,9 @@
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_keyphoto</span>
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
<span class="n">person</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_dbpersons_pk</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">_pk</span><span class="p">]</span>
<span class="k">if</span> <span class="n">person</span><span class="p">[</span><span class="s2">&quot;photo_uuid&quot;</span><span class="p">]:</span>
<span class="k">if</span> <span class="n">person</span><span class="p">[</span><span class="s2">"photo_uuid"</span><span class="p">]:</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">key_photo</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">get_photo</span><span class="p">(</span><span class="n">person</span><span class="p">[</span><span class="s2">&quot;photo_uuid&quot;</span><span class="p">])</span>
<span class="n">key_photo</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">get_photo</span><span class="p">(</span><span class="n">person</span><span class="p">[</span><span class="s2">"photo_uuid"</span><span class="p">])</span>
<span class="k">except</span> <span class="ne">IndexError</span><span class="p">:</span>
<span class="n">key_photo</span> <span class="o">=</span> <span class="kc">None</span>
<span class="k">else</span><span class="p">:</span>
@@ -86,14 +249,14 @@
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">photos</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Returns list of PhotoInfo objects associated with this person&quot;&quot;&quot;</span>
<span class="sd">"""Returns list of PhotoInfo objects associated with this person"""</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">photos_by_uuid</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_dbfaces_pk</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">_pk</span><span class="p">])</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">face_info</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Returns a list of FaceInfo objects associated with this person sorted by quality score</span>
<span class="sd">"""Returns a list of FaceInfo objects associated with this person sorted by quality score</span>
<span class="sd"> Highest quality face is result[0] and lowest quality face is result[n]</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="sd"> """</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">faces</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_faceinfo_person</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">_pk</span><span class="p">]</span>
<span class="k">return</span> <span class="nb">sorted</span><span class="p">(</span>
@@ -106,30 +269,32 @@
<span class="k">return</span> <span class="p">[]</span>
<div class="viewcode-block" id="PersonInfo.asdict"><a class="viewcode-back" href="../../reference.html#osxphotos.PersonInfo.asdict">[docs]</a> <span class="k">def</span> <span class="nf">asdict</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Returns dictionary representation of class instance&quot;&quot;&quot;</span>
<span class="sd">"""Returns dictionary representation of class instance"""</span>
<span class="n">keyphoto</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">keyphoto</span><span class="o">.</span><span class="n">uuid</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">keyphoto</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="k">else</span> <span class="kc">None</span>
<span class="k">return</span> <span class="p">{</span>
<span class="s2">&quot;uuid&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">,</span>
<span class="s2">&quot;name&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">name</span><span class="p">,</span>
<span class="s2">&quot;displayname&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">display_name</span><span class="p">,</span>
<span class="s2">&quot;keyface&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">keyface</span><span class="p">,</span>
<span class="s2">&quot;facecount&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">facecount</span><span class="p">,</span>
<span class="s2">&quot;keyphoto&quot;</span><span class="p">:</span> <span class="n">keyphoto</span><span class="p">,</span>
<span class="s2">"uuid"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">,</span>
<span class="s2">"name"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">name</span><span class="p">,</span>
<span class="s2">"displayname"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">display_name</span><span class="p">,</span>
<span class="s2">"keyface"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">keyface</span><span class="p">,</span>
<span class="s2">"facecount"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">facecount</span><span class="p">,</span>
<span class="s2">"keyphoto"</span><span class="p">:</span> <span class="n">keyphoto</span><span class="p">,</span>
<span class="p">}</span></div>
<div class="viewcode-block" id="PersonInfo.json"><a class="viewcode-back" href="../../reference.html#osxphotos.PersonInfo.json">[docs]</a> <span class="k">def</span> <span class="nf">json</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Returns JSON representation of class instance&quot;&quot;&quot;</span>
<span class="sd">"""Returns JSON representation of class instance"""</span>
<span class="k">return</span> <span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">asdict</span><span class="p">())</span></div>
<span class="k">def</span> <span class="fm">__str__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="sa">f</span><span class="s2">&quot;PersonInfo(name=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">name</span><span class="si">}</span><span class="s2">, display_name=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">display_name</span><span class="si">}</span><span class="s2">, uuid=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="si">}</span><span class="s2">, facecount=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">facecount</span><span class="si">}</span><span class="s2">)&quot;</span>
<span class="k">return</span> <span class="sa">f</span><span class="s2">"PersonInfo(name=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">name</span><span class="si">}</span><span class="s2">, display_name=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">display_name</span><span class="si">}</span><span class="s2">, uuid=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="si">}</span><span class="s2">, facecount=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">facecount</span><span class="si">}</span><span class="s2">)"</span>
<span class="k">def</span> <span class="fm">__eq__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">other</span><span class="p">):</span>
<span class="k">if</span> <span class="ow">not</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">other</span><span class="p">,</span> <span class="nb">type</span><span class="p">(</span><span class="bp">self</span><span class="p">)):</span>
<span class="k">return</span> <span class="kc">False</span>
<span class="k">return</span> <span class="nb">all</span><span class="p">(</span>
<span class="nb">getattr</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">field</span><span class="p">)</span> <span class="o">==</span> <span class="nb">getattr</span><span class="p">(</span><span class="n">other</span><span class="p">,</span> <span class="n">field</span><span class="p">)</span> <span class="k">for</span> <span class="n">field</span> <span class="ow">in</span> <span class="p">[</span><span class="s2">&quot;_db&quot;</span><span class="p">,</span> <span class="s2">&quot;_pk&quot;</span><span class="p">]</span>
<span class="k">return</span> <span class="p">(</span>
<span class="nb">all</span><span class="p">(</span>
<span class="nb">getattr</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">field</span><span class="p">)</span> <span class="o">==</span> <span class="nb">getattr</span><span class="p">(</span><span class="n">other</span><span class="p">,</span> <span class="n">field</span><span class="p">)</span>
<span class="k">for</span> <span class="n">field</span> <span class="ow">in</span> <span class="p">[</span><span class="s2">"_db"</span><span class="p">,</span> <span class="s2">"_pk"</span><span class="p">]</span>
<span class="p">)</span>
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">other</span><span class="p">,</span> <span class="nb">type</span><span class="p">(</span><span class="bp">self</span><span class="p">))</span>
<span class="k">else</span> <span class="kc">False</span>
<span class="p">)</span>
<span class="k">def</span> <span class="fm">__ne__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">other</span><span class="p">):</span>
@@ -137,10 +302,10 @@
<span class="k">class</span> <span class="nc">FaceInfo</span><span class="p">:</span>
<span class="sd">&quot;&quot;&quot;Info about a face in the Photos library&quot;&quot;&quot;</span>
<span class="sd">"""Info about a face in the Photos library"""</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">db</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">pk</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Creates a new FaceInfo instance</span>
<span class="sd">"""Creates a new FaceInfo instance</span>
<span class="sd"> Arguments:</span>
<span class="sd"> db: instance of PhotosDB object</span>
@@ -148,95 +313,59 @@
<span class="sd"> Returns:</span>
<span class="sd"> FaceInfo instance</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="sd"> """</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_db</span> <span class="o">=</span> <span class="n">db</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_pk</span> <span class="o">=</span> <span class="n">pk</span>
<span class="n">face</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_faceinfo_pk</span><span class="p">[</span><span class="n">pk</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_info</span> <span class="o">=</span> <span class="n">face</span>
<span class="bp">self</span><span class="o">.</span><span class="n">uuid</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">&quot;uuid&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">name</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">&quot;fullname&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">asset_uuid</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">&quot;asset_uuid&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_person_pk</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">&quot;person&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">center_x</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">&quot;centerx&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">center_y</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">&quot;centery&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">mouth_x</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">&quot;mouthx&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">mouth_y</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">&quot;mouthy&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">left_eye_x</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">&quot;lefteyex&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">left_eye_y</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">&quot;lefteyey&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">right_eye_x</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">&quot;righteyex&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">right_eye_y</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">&quot;righteyey&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">size</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">&quot;size&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">quality</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">&quot;quality&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">source_width</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">&quot;sourcewidth&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">source_height</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">&quot;sourceheight&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">has_smile</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">&quot;has_smile&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">left_eye_closed</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">&quot;left_eye_closed&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">right_eye_closed</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">&quot;right_eye_closed&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">manual</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">&quot;manual&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">face_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">&quot;facetype&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">age_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">&quot;agetype&quot;</span><span class="p">]</span>
<span class="c1"># self.bald_type = face[&quot;baldtype&quot;]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">eye_makeup_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">&quot;eyemakeuptype&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">eye_state</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">&quot;eyestate&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">facial_hair_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">&quot;facialhairtype&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">gender_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">&quot;gendertype&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">glasses_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">&quot;glassestype&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">hair_color_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">&quot;haircolortype&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">intrash</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">&quot;intrash&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">lip_makeup_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">&quot;lipmakeuptype&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">smile_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">&quot;smiletype&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">uuid</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"uuid"</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">name</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"fullname"</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">asset_uuid</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"asset_uuid"</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_person_pk</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"person"</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">center_x</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"centerx"</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">center_y</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"centery"</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">size</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"size"</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">quality</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"quality"</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">source_width</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"sourcewidth"</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">source_height</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"sourceheight"</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">has_smile</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"has_smile"</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">manual</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"manual"</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">face_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"facetype"</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">age_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"agetype"</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">eye_makeup_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"eyemakeuptype"</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">eye_state</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"eyestate"</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">facial_hair_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"facialhairtype"</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">gender_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"gendertype"</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">glasses_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"glassestype"</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">hair_color_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"haircolortype"</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">intrash</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"intrash"</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">lip_makeup_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"lipmakeuptype"</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">smile_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"smiletype"</span><span class="p">]</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">center</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Coordinates, in PIL format, for center of face</span>
<span class="sd">"""Coordinates, in PIL format, for center of face</span>
<span class="sd"> Returns:</span>
<span class="sd"> tuple of coordinates in form (x, y)</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="sd"> """</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_make_point</span><span class="p">((</span><span class="bp">self</span><span class="o">.</span><span class="n">center_x</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">center_y</span><span class="p">))</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">size_pixels</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Size of face in pixels (centered around center_x, center_y)</span>
<span class="sd">"""Size of face in pixels (centered around center_x, center_y)</span>
<span class="sd"> Returns:</span>
<span class="sd"> size, in int pixels, of a circle drawn around the center of the face</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="sd"> """</span>
<span class="n">photo</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">photo</span>
<span class="n">size_reference</span> <span class="o">=</span> <span class="n">photo</span><span class="o">.</span><span class="n">width</span> <span class="k">if</span> <span class="n">photo</span><span class="o">.</span><span class="n">width</span> <span class="o">&gt;</span> <span class="n">photo</span><span class="o">.</span><span class="n">height</span> <span class="k">else</span> <span class="n">photo</span><span class="o">.</span><span class="n">height</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">size</span> <span class="o">*</span> <span class="n">size_reference</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">mouth</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Coordinates, in PIL format, for mouth position</span>
<span class="sd"> Returns:</span>
<span class="sd"> tuple of coordinates in form (x, y)</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_make_point_with_rotation</span><span class="p">((</span><span class="bp">self</span><span class="o">.</span><span class="n">mouth_x</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">mouth_y</span><span class="p">))</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">left_eye</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Coordinates, in PIL format, for left eye position</span>
<span class="sd"> Returns:</span>
<span class="sd"> tuple of coordinates in form (x, y)</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_make_point_with_rotation</span><span class="p">((</span><span class="bp">self</span><span class="o">.</span><span class="n">left_eye_x</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">left_eye_y</span><span class="p">))</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">right_eye</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Coordinates, in PIL format, for right eye position</span>
<span class="sd"> Returns:</span>
<span class="sd"> tuple of coordinates in form (x, y)</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_make_point_with_rotation</span><span class="p">((</span><span class="bp">self</span><span class="o">.</span><span class="n">right_eye_x</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">right_eye_y</span><span class="p">))</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">person_info</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;PersonInfo instance for person associated with this face&quot;&quot;&quot;</span>
<span class="sd">"""PersonInfo instance for person associated with this face"""</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_person</span>
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
@@ -245,18 +374,18 @@
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">photo</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;PhotoInfo instance associated with this face&quot;&quot;&quot;</span>
<span class="sd">"""PhotoInfo instance associated with this face"""</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_photo</span>
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_photo</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">get_photo</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">asset_uuid</span><span class="p">)</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_photo</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Could not get photo for uuid: </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">asset_uuid</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Could not get photo for uuid: </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">asset_uuid</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_photo</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">mwg_rs_area</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Get coordinates for Metadata Working Group Region Area.</span>
<span class="sd">"""Get coordinates for Metadata Working Group Region Area.</span>
<span class="sd"> Returns:</span>
<span class="sd"> MWG_RS_Area named tuple with x, y, h, w where:</span>
@@ -267,7 +396,7 @@
<span class="sd"> Reference:</span>
<span class="sd"> https://photo.stackexchange.com/questions/106410/how-does-xmp-define-the-face-region</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="sd"> """</span>
<span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">center_x</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">center_y</span>
<span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_fix_orientation</span><span class="p">((</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">))</span>
@@ -282,7 +411,7 @@
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">mpri_reg_rect</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Get coordinates for Microsoft Photo Region Rectangle.</span>
<span class="sd">"""Get coordinates for Microsoft Photo Region Rectangle.</span>
<span class="sd"> Returns:</span>
<span class="sd"> MPRI_Reg_Rect named tuple with x, y, h, w where:</span>
@@ -293,7 +422,7 @@
<span class="sd"> Reference:</span>
<span class="sd"> https://docs.microsoft.com/en-us/windows/win32/wic/-wic-people-tagging</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="sd"> """</span>
<span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">center_x</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">center_y</span>
<span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_fix_orientation</span><span class="p">((</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">))</span>
@@ -311,13 +440,13 @@
<span class="k">return</span> <span class="n">MPRI_Reg_Rect</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">h</span><span class="p">,</span> <span class="n">w</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">face_rect</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Get face rectangle coordinates for current version of the associated image</span>
<span class="sd">"""Get face rectangle coordinates for current version of the associated image</span>
<span class="sd"> If image has been edited, rectangle applies to edited version, otherwise original version</span>
<span class="sd"> Coordinates in format and reference frame used by PIL</span>
<span class="sd"> Returns:</span>
<span class="sd"> list [(x0, x1), (y0, y1)] of coordinates in reference frame used by PIL</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="sd"> """</span>
<span class="n">photo</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">photo</span>
<span class="n">size_reference</span> <span class="o">=</span> <span class="n">photo</span><span class="o">.</span><span class="n">width</span> <span class="k">if</span> <span class="n">photo</span><span class="o">.</span><span class="n">width</span> <span class="o">&gt;</span> <span class="n">photo</span><span class="o">.</span><span class="n">height</span> <span class="k">else</span> <span class="n">photo</span><span class="o">.</span><span class="n">height</span>
<span class="n">radius</span> <span class="o">=</span> <span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">size</span> <span class="o">/</span> <span class="mi">2</span><span class="p">)</span> <span class="o">*</span> <span class="n">size_reference</span>
@@ -327,34 +456,34 @@
<span class="k">return</span> <span class="p">[(</span><span class="n">x0</span><span class="p">,</span> <span class="n">y0</span><span class="p">),</span> <span class="p">(</span><span class="n">x1</span><span class="p">,</span> <span class="n">y1</span><span class="p">)]</span>
<span class="k">def</span> <span class="nf">roll_pitch_yaw</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Roll, pitch, yaw of face in radians as tuple&quot;&quot;&quot;</span>
<span class="sd">"""Roll, pitch, yaw of face in radians as tuple"""</span>
<span class="n">info</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span>
<span class="n">roll</span> <span class="o">=</span> <span class="mi">0</span> <span class="k">if</span> <span class="n">info</span><span class="p">[</span><span class="s2">&quot;roll&quot;</span><span class="p">]</span> <span class="ow">is</span> <span class="kc">None</span> <span class="k">else</span> <span class="n">info</span><span class="p">[</span><span class="s2">&quot;roll&quot;</span><span class="p">]</span>
<span class="n">pitch</span> <span class="o">=</span> <span class="mi">0</span> <span class="k">if</span> <span class="n">info</span><span class="p">[</span><span class="s2">&quot;pitch&quot;</span><span class="p">]</span> <span class="ow">is</span> <span class="kc">None</span> <span class="k">else</span> <span class="n">info</span><span class="p">[</span><span class="s2">&quot;pitch&quot;</span><span class="p">]</span>
<span class="n">yaw</span> <span class="o">=</span> <span class="mi">0</span> <span class="k">if</span> <span class="n">info</span><span class="p">[</span><span class="s2">&quot;yaw&quot;</span><span class="p">]</span> <span class="ow">is</span> <span class="kc">None</span> <span class="k">else</span> <span class="n">info</span><span class="p">[</span><span class="s2">&quot;yaw&quot;</span><span class="p">]</span>
<span class="n">roll</span> <span class="o">=</span> <span class="mi">0</span> <span class="k">if</span> <span class="n">info</span><span class="p">[</span><span class="s2">"roll"</span><span class="p">]</span> <span class="ow">is</span> <span class="kc">None</span> <span class="k">else</span> <span class="n">info</span><span class="p">[</span><span class="s2">"roll"</span><span class="p">]</span>
<span class="n">pitch</span> <span class="o">=</span> <span class="mi">0</span> <span class="k">if</span> <span class="n">info</span><span class="p">[</span><span class="s2">"pitch"</span><span class="p">]</span> <span class="ow">is</span> <span class="kc">None</span> <span class="k">else</span> <span class="n">info</span><span class="p">[</span><span class="s2">"pitch"</span><span class="p">]</span>
<span class="n">yaw</span> <span class="o">=</span> <span class="mi">0</span> <span class="k">if</span> <span class="n">info</span><span class="p">[</span><span class="s2">"yaw"</span><span class="p">]</span> <span class="ow">is</span> <span class="kc">None</span> <span class="k">else</span> <span class="n">info</span><span class="p">[</span><span class="s2">"yaw"</span><span class="p">]</span>
<span class="k">return</span> <span class="p">(</span><span class="n">roll</span><span class="p">,</span> <span class="n">pitch</span><span class="p">,</span> <span class="n">yaw</span><span class="p">)</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">roll</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Return roll angle in radians of the face region&quot;&quot;&quot;</span>
<span class="sd">"""Return roll angle in radians of the face region"""</span>
<span class="n">roll</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">roll_pitch_yaw</span><span class="p">()</span>
<span class="k">return</span> <span class="n">roll</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">pitch</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Return pitch angle in radians of the face region&quot;&quot;&quot;</span>
<span class="sd">"""Return pitch angle in radians of the face region"""</span>
<span class="n">_</span><span class="p">,</span> <span class="n">pitch</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">roll_pitch_yaw</span><span class="p">()</span>
<span class="k">return</span> <span class="n">pitch</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">yaw</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Return yaw angle in radians of the face region&quot;&quot;&quot;</span>
<span class="sd">"""Return yaw angle in radians of the face region"""</span>
<span class="n">_</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">yaw</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">roll_pitch_yaw</span><span class="p">()</span>
<span class="k">return</span> <span class="n">yaw</span>
<span class="k">def</span> <span class="nf">_fix_orientation</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">xy</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Translate an (x, y) tuple based on image orientation</span>
<span class="sd">"""Translate an (x, y) tuple based on image orientation</span>
<span class="sd"> Arguments:</span>
<span class="sd"> xy: tuple of (x, y) coordinates for point to translate</span>
@@ -362,7 +491,7 @@
<span class="sd"> Returns:</span>
<span class="sd"> (x, y) tuple of translated coordinates</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="sd"> """</span>
<span class="c1"># Reference: https://github.com/neilpa/phace/blob/7594776480505d0c389688a42099c94ac5d34f3f/cmd/phace/draw.go#L79-L94</span>
<span class="n">orientation</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="o">.</span><span class="n">orientation</span>
@@ -386,15 +515,15 @@
<span class="k">elif</span> <span class="n">orientation</span> <span class="o">==</span> <span class="mi">8</span><span class="p">:</span>
<span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="n">y</span><span class="p">,</span> <span class="n">x</span>
<span class="k">elif</span> <span class="n">orientation</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
<span class="c1"># set by osxphotos if adjusted orientation cannot be read, assume it&#39;s 1</span>
<span class="c1"># set by osxphotos if adjusted orientation cannot be read, assume it's 1</span>
<span class="n">y</span> <span class="o">=</span> <span class="mf">1.0</span> <span class="o">-</span> <span class="n">y</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Unhandled orientation: </span><span class="si">{</span><span class="n">orientation</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Unhandled orientation: </span><span class="si">{</span><span class="n">orientation</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="k">return</span> <span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">_make_point</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">xy</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Translate an (x, y) tuple based on image orientation</span>
<span class="sd">"""Translate an (x, y) tuple based on image orientation</span>
<span class="sd"> and convert to image coordinates</span>
<span class="sd"> Arguments:</span>
@@ -403,7 +532,7 @@
<span class="sd"> Returns:</span>
<span class="sd"> (x, y) tuple of translated coordinates in pixels in PIL format/reference frame</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="sd"> """</span>
<span class="c1"># Reference: https://github.com/neilpa/phace/blob/7594776480505d0c389688a42099c94ac5d34f3f/cmd/phace/draw.go#L79-L94</span>
<span class="n">orientation</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="o">.</span><span class="n">orientation</span>
@@ -415,7 +544,7 @@
<span class="k">return</span> <span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">x</span> <span class="o">*</span> <span class="n">dx</span><span class="p">),</span> <span class="nb">int</span><span class="p">(</span><span class="n">y</span> <span class="o">*</span> <span class="n">dy</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">_make_point_with_rotation</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">xy</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Translate an (x, y) tuple based on image orientation and rotation</span>
<span class="sd">"""Translate an (x, y) tuple based on image orientation and rotation</span>
<span class="sd"> and convert to image coordinates</span>
<span class="sd"> Arguments:</span>
@@ -424,7 +553,7 @@
<span class="sd"> Returns:</span>
<span class="sd"> (x, y) tuple of translated coordinates in pixels in PIL format/reference frame</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="sd"> """</span>
<span class="c1"># convert to image coordinates</span>
<span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_make_point</span><span class="p">(</span><span class="n">xy</span><span class="p">)</span>
@@ -437,70 +566,58 @@
<span class="k">return</span> <span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">xr</span><span class="p">),</span> <span class="nb">int</span><span class="p">(</span><span class="n">yr</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">asdict</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Returns dict representation of class instance&quot;&quot;&quot;</span>
<span class="sd">"""Returns dict representation of class instance"""</span>
<span class="n">roll</span><span class="p">,</span> <span class="n">pitch</span><span class="p">,</span> <span class="n">yaw</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">roll_pitch_yaw</span><span class="p">()</span>
<span class="k">return</span> <span class="p">{</span>
<span class="s2">&quot;_pk&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">_pk</span><span class="p">,</span>
<span class="s2">&quot;uuid&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">,</span>
<span class="s2">&quot;name&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">name</span><span class="p">,</span>
<span class="s2">&quot;asset_uuid&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">asset_uuid</span><span class="p">,</span>
<span class="s2">&quot;_person_pk&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">_person_pk</span><span class="p">,</span>
<span class="s2">&quot;center_x&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">center_x</span><span class="p">,</span>
<span class="s2">&quot;center_y&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">center_y</span><span class="p">,</span>
<span class="s2">&quot;center&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">center</span><span class="p">,</span>
<span class="s2">&quot;mouth_x&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">mouth_x</span><span class="p">,</span>
<span class="s2">&quot;mouth_y&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">mouth_y</span><span class="p">,</span>
<span class="s2">&quot;mouth&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">mouth</span><span class="p">,</span>
<span class="s2">&quot;left_eye_x&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">left_eye_x</span><span class="p">,</span>
<span class="s2">&quot;left_eye_y&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">left_eye_y</span><span class="p">,</span>
<span class="s2">&quot;left_eye&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">left_eye</span><span class="p">,</span>
<span class="s2">&quot;right_eye_x&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">right_eye_x</span><span class="p">,</span>
<span class="s2">&quot;right_eye_y&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">right_eye_y</span><span class="p">,</span>
<span class="s2">&quot;right_eye&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">right_eye</span><span class="p">,</span>
<span class="s2">&quot;size&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">size</span><span class="p">,</span>
<span class="s2">&quot;face_rect&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">face_rect</span><span class="p">(),</span>
<span class="s2">&quot;mpri_reg_rect&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">mpri_reg_rect</span><span class="o">.</span><span class="n">_asdict</span><span class="p">(),</span>
<span class="s2">&quot;mwg_rs_area&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">mwg_rs_area</span><span class="o">.</span><span class="n">_asdict</span><span class="p">(),</span>
<span class="s2">&quot;roll&quot;</span><span class="p">:</span> <span class="n">roll</span><span class="p">,</span>
<span class="s2">&quot;pitch&quot;</span><span class="p">:</span> <span class="n">pitch</span><span class="p">,</span>
<span class="s2">&quot;yaw&quot;</span><span class="p">:</span> <span class="n">yaw</span><span class="p">,</span>
<span class="s2">&quot;quality&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">quality</span><span class="p">,</span>
<span class="s2">&quot;source_width&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">source_width</span><span class="p">,</span>
<span class="s2">&quot;source_height&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">source_height</span><span class="p">,</span>
<span class="s2">&quot;has_smile&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">has_smile</span><span class="p">,</span>
<span class="s2">&quot;left_eye_closed&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">left_eye_closed</span><span class="p">,</span>
<span class="s2">&quot;right_eye_closed&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">right_eye_closed</span><span class="p">,</span>
<span class="s2">&quot;manual&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">manual</span><span class="p">,</span>
<span class="s2">&quot;face_type&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">face_type</span><span class="p">,</span>
<span class="s2">&quot;age_type&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">age_type</span><span class="p">,</span>
<span class="c1"># &quot;bald_type&quot;: self.bald_type,</span>
<span class="s2">&quot;eye_makeup_type&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">eye_makeup_type</span><span class="p">,</span>
<span class="s2">&quot;eye_state&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">eye_state</span><span class="p">,</span>
<span class="s2">&quot;facial_hair_type&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">facial_hair_type</span><span class="p">,</span>
<span class="s2">&quot;gender_type&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">gender_type</span><span class="p">,</span>
<span class="s2">&quot;glasses_type&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">glasses_type</span><span class="p">,</span>
<span class="s2">&quot;hair_color_type&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">hair_color_type</span><span class="p">,</span>
<span class="s2">&quot;intrash&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">intrash</span><span class="p">,</span>
<span class="s2">&quot;lip_makeup_type&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">lip_makeup_type</span><span class="p">,</span>
<span class="s2">&quot;smile_type&quot;</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">smile_type</span><span class="p">,</span>
<span class="s2">"_pk"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">_pk</span><span class="p">,</span>
<span class="s2">"uuid"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">,</span>
<span class="s2">"name"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">name</span><span class="p">,</span>
<span class="s2">"asset_uuid"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">asset_uuid</span><span class="p">,</span>
<span class="s2">"_person_pk"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">_person_pk</span><span class="p">,</span>
<span class="s2">"center_x"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">center_x</span><span class="p">,</span>
<span class="s2">"center_y"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">center_y</span><span class="p">,</span>
<span class="s2">"center"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">center</span><span class="p">,</span>
<span class="s2">"size"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">size</span><span class="p">,</span>
<span class="s2">"face_rect"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">face_rect</span><span class="p">(),</span>
<span class="s2">"mpri_reg_rect"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">mpri_reg_rect</span><span class="o">.</span><span class="n">_asdict</span><span class="p">(),</span>
<span class="s2">"mwg_rs_area"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">mwg_rs_area</span><span class="o">.</span><span class="n">_asdict</span><span class="p">(),</span>
<span class="s2">"roll"</span><span class="p">:</span> <span class="n">roll</span><span class="p">,</span>
<span class="s2">"pitch"</span><span class="p">:</span> <span class="n">pitch</span><span class="p">,</span>
<span class="s2">"yaw"</span><span class="p">:</span> <span class="n">yaw</span><span class="p">,</span>
<span class="s2">"quality"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">quality</span><span class="p">,</span>
<span class="s2">"source_width"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">source_width</span><span class="p">,</span>
<span class="s2">"source_height"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">source_height</span><span class="p">,</span>
<span class="s2">"has_smile"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">has_smile</span><span class="p">,</span>
<span class="s2">"manual"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">manual</span><span class="p">,</span>
<span class="s2">"face_type"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">face_type</span><span class="p">,</span>
<span class="s2">"age_type"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">age_type</span><span class="p">,</span>
<span class="s2">"eye_makeup_type"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">eye_makeup_type</span><span class="p">,</span>
<span class="s2">"eye_state"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">eye_state</span><span class="p">,</span>
<span class="s2">"facial_hair_type"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">facial_hair_type</span><span class="p">,</span>
<span class="s2">"gender_type"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">gender_type</span><span class="p">,</span>
<span class="s2">"glasses_type"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">glasses_type</span><span class="p">,</span>
<span class="s2">"hair_color_type"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">hair_color_type</span><span class="p">,</span>
<span class="s2">"intrash"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">intrash</span><span class="p">,</span>
<span class="s2">"lip_makeup_type"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">lip_makeup_type</span><span class="p">,</span>
<span class="s2">"smile_type"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">smile_type</span><span class="p">,</span>
<span class="p">}</span>
<span class="k">def</span> <span class="nf">json</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Return JSON representation of FaceInfo instance&quot;&quot;&quot;</span>
<span class="sd">"""Return JSON representation of FaceInfo instance"""</span>
<span class="k">return</span> <span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">asdict</span><span class="p">())</span>
<span class="k">def</span> <span class="fm">__str__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="sa">f</span><span class="s2">&quot;FaceInfo(uuid=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="si">}</span><span class="s2">, center_x=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">center_x</span><span class="si">}</span><span class="s2">, center_y = </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">center_y</span><span class="si">}</span><span class="s2">, size=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">size</span><span class="si">}</span><span class="s2">, person=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">name</span><span class="si">}</span><span class="s2">, asset_uuid=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">asset_uuid</span><span class="si">}</span><span class="s2">)&quot;</span>
<span class="k">return</span> <span class="sa">f</span><span class="s2">"FaceInfo(uuid=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="si">}</span><span class="s2">, center_x=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">center_x</span><span class="si">}</span><span class="s2">, center_y = </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">center_y</span><span class="si">}</span><span class="s2">, size=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">size</span><span class="si">}</span><span class="s2">, person=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">name</span><span class="si">}</span><span class="s2">, asset_uuid=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">asset_uuid</span><span class="si">}</span><span class="s2">)"</span>
<span class="k">def</span> <span class="fm">__repr__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="sa">f</span><span class="s2">&quot;FaceInfo(db=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="si">}</span><span class="s2">, pk=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_pk</span><span class="si">}</span><span class="s2">)&quot;</span>
<span class="k">return</span> <span class="sa">f</span><span class="s2">"FaceInfo(db=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="si">}</span><span class="s2">, pk=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_pk</span><span class="si">}</span><span class="s2">)"</span>
<span class="k">def</span> <span class="fm">__eq__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">other</span><span class="p">):</span>
<span class="k">if</span> <span class="ow">not</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">other</span><span class="p">,</span> <span class="nb">type</span><span class="p">(</span><span class="bp">self</span><span class="p">)):</span>
<span class="k">return</span> <span class="kc">False</span>
<span class="k">return</span> <span class="nb">all</span><span class="p">(</span>
<span class="nb">getattr</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">field</span><span class="p">)</span> <span class="o">==</span> <span class="nb">getattr</span><span class="p">(</span><span class="n">other</span><span class="p">,</span> <span class="n">field</span><span class="p">)</span> <span class="k">for</span> <span class="n">field</span> <span class="ow">in</span> <span class="p">[</span><span class="s2">&quot;_db&quot;</span><span class="p">,</span> <span class="s2">&quot;_pk&quot;</span><span class="p">]</span>
<span class="nb">getattr</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">field</span><span class="p">)</span> <span class="o">==</span> <span class="nb">getattr</span><span class="p">(</span><span class="n">other</span><span class="p">,</span> <span class="n">field</span><span class="p">)</span> <span class="k">for</span> <span class="n">field</span> <span class="ow">in</span> <span class="p">[</span><span class="s2">"_db"</span><span class="p">,</span> <span class="s2">"_pk"</span><span class="p">]</span>
<span class="p">)</span>
<span class="k">def</span> <span class="fm">__ne__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">other</span><span class="p">):</span>
@@ -508,7 +625,7 @@
<span class="k">def</span> <span class="nf">rotate_image_point</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">xmid</span><span class="p">,</span> <span class="n">ymid</span><span class="p">,</span> <span class="n">angle</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;rotate image point about xm, ym by angle in radians</span>
<span class="sd">"""rotate image point about xm, ym by angle in radians</span>
<span class="sd"> Arguments:</span>
<span class="sd"> x: x coordinate of point to rotate</span>
@@ -520,7 +637,7 @@
<span class="sd"> Returns:</span>
<span class="sd"> tuple of rotated points (xr, yr)</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="sd"> """</span>
<span class="c1"># translate point relative to the mid point</span>
<span class="n">x</span> <span class="o">=</span> <span class="n">x</span> <span class="o">-</span> <span class="n">xmid</span>
<span class="n">y</span> <span class="o">=</span> <span class="n">y</span> <span class="o">-</span> <span class="n">ymid</span>
@@ -535,72 +652,45 @@
<span class="k">return</span> <span class="p">(</span><span class="n">xr</span><span class="p">,</span> <span class="n">yr</span><span class="p">)</span>
</pre></div>
</div>
</article>
</div>
<footer>
<div class="related-pages">
</div>
</div>
<div class="sphinxsidebar" role="navigation" aria-label="main navigation">
<div class="sphinxsidebarwrapper">
<h1 class="logo"><a href="../../index.html">osxphotos</a></h1>
<h3>Navigation</h3>
<ul>
<li class="toctree-l1"><a class="reference internal" href="../../overview.html">osxphotos</a></li>
<li class="toctree-l1"><a class="reference internal" href="../../tutorial.html">Tutorial</a></li>
<li class="toctree-l1"><a class="reference internal" href="../../cli.html">osxphotos command line interface (CLI)</a></li>
<li class="toctree-l1"><a class="reference internal" href="../../reference.html">osxphotos package</a></li>
</ul>
<div class="relations">
<h3>Related Topics</h3>
<ul>
<li><a href="../../index.html">Documentation overview</a><ul>
<li><a href="../index.html">Module code</a><ul>
</ul></li>
</ul></li>
</ul>
</div>
<div id="searchbox" style="display: none" role="search">
<h3 id="searchlabel">Quick search</h3>
<div class="searchformwrapper">
<form class="search" action="../../search.html" method="get">
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
<input type="submit" value="Go" />
</form>
</div>
</div>
<script>$('#searchbox').show(0);</script>
<div class="bottom-of-page">
<div class="left-details">
<div class="copyright">
Copyright &#169; 2021, Rhet Turnbull
</div>
Made with <a href="https://www.sphinx-doc.org/">Sphinx</a> and <a class="muted-link" href="https://pradyunsg.me">@pradyunsg</a>'s
<a href="https://github.com/pradyunsg/furo">Furo</a>
</div>
<div class="right-details">
<div class="icons">
</div>
</div>
</div>
</div>
<div class="clearer"></div>
</footer>
</div>
<div class="footer">
&copy;2021, Rhet Turnbull.
<aside class="toc-drawer no-toc">
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.4.0</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div>
</body>
</aside>
</div>
</div><script data-url_root="../../" id="documentation_options" src="../../_static/documentation_options.js"></script>
<script src="../../_static/jquery.js"></script>
<script src="../../_static/underscore.js"></script>
<script src="../../_static/doctools.js"></script>
<script src="../../_static/scripts/furo.js"></script>
<script src="../../_static/clipboard.min.js"></script>
<script src="../../_static/copybutton.js"></script>
</body>
</html>

View File

@@ -5,7 +5,7 @@
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../genindex.html" /><link rel="search" title="Search" href="../../search.html" />
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/>
<title>osxphotos.photoexporter - osxphotos 0.49.2 documentation</title>
<title>osxphotos.photoexporter - osxphotos 0.50.1 documentation</title>
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css" />
@@ -123,7 +123,7 @@
</label>
</div>
<div class="header-center">
<a href="../../index.html"><div class="brand">osxphotos 0.49.2 documentation</div></a>
<a href="../../index.html"><div class="brand">osxphotos 0.50.1 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -146,7 +146,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../index.html">
<span class="sidebar-brand-text">osxphotos 0.49.2 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.50.1 documentation</span>
</a><form class="sidebar-search-container" method="get" action="../../search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
@@ -240,7 +240,13 @@
<span class="kn">from</span> <span class="nn">.phototemplate</span> <span class="kn">import</span> <span class="n">RenderOptions</span>
<span class="kn">from</span> <span class="nn">.rich_utils</span> <span class="kn">import</span> <span class="n">add_rich_markup_tag</span>
<span class="kn">from</span> <span class="nn">.uti</span> <span class="kn">import</span> <span class="n">get_preferred_uti_extension</span>
<span class="kn">from</span> <span class="nn">.utils</span> <span class="kn">import</span> <span class="n">hexdigest</span><span class="p">,</span> <span class="n">increment_filename</span><span class="p">,</span> <span class="n">lineno</span><span class="p">,</span> <span class="n">list_directory</span>
<span class="kn">from</span> <span class="nn">.utils</span> <span class="kn">import</span> <span class="p">(</span>
<span class="n">hexdigest</span><span class="p">,</span>
<span class="n">increment_filename</span><span class="p">,</span>
<span class="n">increment_filename_with_count</span><span class="p">,</span>
<span class="n">lineno</span><span class="p">,</span>
<span class="n">list_directory</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">__all__</span> <span class="o">=</span> <span class="p">[</span>
<span class="s2">"ExportError"</span><span class="p">,</span>
@@ -684,7 +690,7 @@
<span class="n">src</span> <span class="o">=</span> <span class="n">staged_files</span><span class="o">.</span><span class="n">edited</span> <span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">edited</span> <span class="k">else</span> <span class="n">staged_files</span><span class="o">.</span><span class="n">original</span>
<span class="c1"># get the right destination path depending on options.update, etc.</span>
<span class="n">dest</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_dest_path</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">dest</span><span class="p">,</span> <span class="n">options</span><span class="p">)</span>
<span class="n">dest</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_dest_path</span><span class="p">(</span><span class="n">dest</span><span class="p">,</span> <span class="n">options</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_render_options</span><span class="o">.</span><span class="n">filepath</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">dest</span><span class="p">)</span>
<span class="n">all_results</span> <span class="o">=</span> <span class="n">ExportResults</span><span class="p">()</span>
@@ -838,12 +844,11 @@
<span class="k">return</span> <span class="n">edited_filename</span>
<span class="k">def</span> <span class="nf">_get_dest_path</span><span class="p">(</span>
<span class="bp">self</span><span class="p">,</span> <span class="n">src</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">dest</span><span class="p">:</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">,</span> <span class="n">options</span><span class="p">:</span> <span class="n">ExportOptions</span>
<span class="bp">self</span><span class="p">,</span> <span class="n">dest</span><span class="p">:</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">,</span> <span class="n">options</span><span class="p">:</span> <span class="n">ExportOptions</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">:</span>
<span class="sd">"""If destination exists find match in ExportDB, on disk, or add (1), (2), and so on to filename to get a valid destination</span>
<span class="sd"> Args:</span>
<span class="sd"> src (str): source file path</span>
<span class="sd"> dest (str): destination path</span>
<span class="sd"> options (ExportOptions): Export options</span>
@@ -859,6 +864,10 @@
<span class="sa">f</span><span class="s2">"destination exists (</span><span class="si">{</span><span class="n">dest</span><span class="si">}</span><span class="s2">); overwrite=</span><span class="si">{</span><span class="n">options</span><span class="o">.</span><span class="n">overwrite</span><span class="si">}</span><span class="s2">, increment=</span><span class="si">{</span><span class="n">options</span><span class="o">.</span><span class="n">increment</span><span class="si">}</span><span class="s2">"</span>
<span class="p">)</span>
<span class="c1"># if overwrite, we don't care if the file exists or not</span>
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">overwrite</span><span class="p">:</span>
<span class="k">return</span> <span class="n">dest</span>
<span class="c1"># if not update or overwrite, check to see if file exists and if so, add (1), (2), etc</span>
<span class="c1"># until we find one that works</span>
<span class="c1"># Photos checks the stem and adds (1), (2), etc which avoids collision with sidecars</span>
@@ -871,29 +880,36 @@
<span class="k">return</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">increment_filename</span><span class="p">(</span><span class="n">dest</span><span class="p">))</span>
<span class="c1"># if update and file exists, need to check to see if it's the right file by checking export db</span>
<span class="k">if</span> <span class="p">(</span><span class="n">options</span><span class="o">.</span><span class="n">update</span> <span class="ow">or</span> <span class="n">options</span><span class="o">.</span><span class="n">force_update</span><span class="p">)</span> <span class="ow">and</span> <span class="n">dest</span><span class="o">.</span><span class="n">exists</span><span class="p">()</span> <span class="ow">and</span> <span class="n">src</span><span class="p">:</span>
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">update</span> <span class="ow">or</span> <span class="n">options</span><span class="o">.</span><span class="n">force_update</span><span class="p">:</span>
<span class="n">export_db</span> <span class="o">=</span> <span class="n">options</span><span class="o">.</span><span class="n">export_db</span>
<span class="c1"># destination exists, check to see if destination is the right UUID</span>
<span class="n">dest_uuid</span> <span class="o">=</span> <span class="n">export_db</span><span class="o">.</span><span class="n">get_uuid_for_file</span><span class="p">(</span><span class="n">dest</span><span class="p">)</span>
<span class="k">if</span> <span class="n">dest_uuid</span> <span class="o">!=</span> <span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="o">.</span><span class="n">uuid</span><span class="p">:</span>
<span class="c1"># not the right file, find the right one</span>
<span class="c1"># find files that match "dest_name (*.ext" (e.g. "dest_name (1).jpg", "dest_name (2).jpg)", ...)</span>
<span class="n">dest_files</span> <span class="o">=</span> <span class="n">list_directory</span><span class="p">(</span>
<span class="n">dest</span><span class="o">.</span><span class="n">parent</span><span class="p">,</span>
<span class="n">startswith</span><span class="o">=</span><span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">dest</span><span class="o">.</span><span class="n">stem</span><span class="si">}</span><span class="s2"> ("</span><span class="p">,</span>
<span class="n">endswith</span><span class="o">=</span><span class="n">dest</span><span class="o">.</span><span class="n">suffix</span><span class="p">,</span>
<span class="n">include_path</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">for</span> <span class="n">file_</span> <span class="ow">in</span> <span class="n">dest_files</span><span class="p">:</span>
<span class="n">dest_uuid</span> <span class="o">=</span> <span class="n">export_db</span><span class="o">.</span><span class="n">get_uuid_for_file</span><span class="p">(</span><span class="n">file_</span><span class="p">)</span>
<span class="k">if</span> <span class="n">dest_uuid</span> <span class="o">==</span> <span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="o">.</span><span class="n">uuid</span><span class="p">:</span>
<span class="n">dest</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">file_</span><span class="p">)</span>
<span class="k">break</span>
<span class="k">else</span><span class="p">:</span>
<span class="c1"># increment the destination file</span>
<span class="n">dest</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">increment_filename</span><span class="p">(</span><span class="n">dest</span><span class="p">))</span>
<span class="k">if</span> <span class="n">dest_uuid</span> <span class="ow">is</span> <span class="kc">None</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">dest</span><span class="o">.</span><span class="n">exists</span><span class="p">():</span>
<span class="c1"># destination doesn't exist in export db and doesn't exist on disk</span>
<span class="c1"># so we can just use it</span>
<span class="k">return</span> <span class="n">dest</span>
<span class="c1"># either dest was updated in the if clause above or not updated at all</span>
<span class="k">if</span> <span class="n">dest_uuid</span> <span class="o">==</span> <span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="o">.</span><span class="n">uuid</span><span class="p">:</span>
<span class="c1"># destination is the right file</span>
<span class="k">return</span> <span class="n">dest</span>
<span class="c1"># either dest_uuid is wrong or file exists and there's no associated UUID, so find a name that matches</span>
<span class="c1"># or create a new name if no match</span>
<span class="c1"># find files that match "dest_name (*.ext" (e.g. "dest_name (1).jpg", "dest_name (2).jpg)", ...)</span>
<span class="c1"># first, find all matching files in export db and see if there's a match</span>
<span class="k">if</span> <span class="n">dest_target</span> <span class="o">:=</span> <span class="n">export_db</span><span class="o">.</span><span class="n">get_target_for_file</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="o">.</span><span class="n">uuid</span><span class="p">,</span> <span class="n">dest</span><span class="p">):</span>
<span class="c1"># there's a match so use that</span>
<span class="k">return</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">dest_target</span><span class="p">)</span>
<span class="c1"># no match so need to create a new name</span>
<span class="c1"># increment the destination file until we find one that doesn't exist and doesn't match another uuid in the database</span>
<span class="n">count</span> <span class="o">=</span> <span class="mi">0</span>
<span class="n">dest</span><span class="p">,</span> <span class="n">count</span> <span class="o">=</span> <span class="n">increment_filename_with_count</span><span class="p">(</span><span class="n">dest</span><span class="p">,</span> <span class="n">count</span><span class="p">)</span>
<span class="n">count</span> <span class="o">+=</span> <span class="mi">1</span>
<span class="k">while</span> <span class="n">export_db</span><span class="o">.</span><span class="n">get_uuid_for_file</span><span class="p">(</span><span class="n">dest</span><span class="p">)</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">dest</span><span class="p">,</span> <span class="n">count</span> <span class="o">=</span> <span class="n">increment_filename_with_count</span><span class="p">(</span><span class="n">dest</span><span class="p">,</span> <span class="n">count</span><span class="p">)</span>
<span class="k">return</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">dest</span><span class="p">)</span>
<span class="c1"># fail safe...I can't think of a case that gets here</span>
<span class="k">return</span> <span class="n">dest</span>
<span class="k">def</span> <span class="nf">_should_update_photo</span><span class="p">(</span>
@@ -1232,9 +1248,9 @@
<span class="k">def</span> <span class="nf">_export_photo</span><span class="p">(</span>
<span class="bp">self</span><span class="p">,</span>
<span class="n">src</span><span class="p">,</span>
<span class="n">dest</span><span class="p">,</span>
<span class="n">options</span><span class="p">,</span>
<span class="n">src</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
<span class="n">dest</span><span class="p">:</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">,</span>
<span class="n">options</span><span class="p">:</span> <span class="n">ExportOptions</span><span class="p">,</span>
<span class="p">):</span>
<span class="sd">"""Helper function for export()</span>
<span class="sd"> Does the actual copy or hardlink taking the appropriate</span>
@@ -1256,6 +1272,7 @@
<span class="sd"> ValueError if export_as_hardlink and convert_to_jpeg both True</span>
<span class="sd"> """</span>
<span class="n">verbose</span> <span class="o">=</span> <span class="n">options</span><span class="o">.</span><span class="n">verbose</span> <span class="ow">or</span> <span class="bp">self</span><span class="o">.</span><span class="n">_verbose</span>
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">export_as_hardlink</span> <span class="ow">and</span> <span class="n">options</span><span class="o">.</span><span class="n">convert_to_jpeg</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span>
<span class="s2">"export_as_hardlink and convert_to_jpeg cannot both be True"</span>
@@ -1345,6 +1362,9 @@
<span class="k">try</span><span class="p">:</span>
<span class="n">fileutil</span><span class="o">.</span><span class="n">copy</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">dest_str</span><span class="p">)</span>
<span class="n">verbose</span><span class="p">(</span>
<span class="sa">f</span><span class="s2">"Exported </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_filename</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="o">.</span><span class="n">original_filename</span><span class="p">)</span><span class="si">}</span><span class="s2"> to </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_filepath</span><span class="p">(</span><span class="n">dest_str</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span>
<span class="p">)</span>
<span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">ExportError</span><span class="p">(</span>
<span class="sa">f</span><span class="s2">"Error copying file </span><span class="si">{</span><span class="n">src</span><span class="si">}</span><span class="s2"> to </span><span class="si">{</span><span class="n">dest_str</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2"> (</span><span class="si">{</span><span class="n">lineno</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="si">}</span><span class="s2">)"</span>
@@ -1640,7 +1660,7 @@
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">ExportResults</span><span class="p">:</span>
<span class="sd">"""Write exif metadata to src file using exiftool</span>
<span class="sd"> Caution: This method modifies *src*, not *dest*, </span>
<span class="sd"> Caution: This method modifies *src*, not *dest*,</span>
<span class="sd"> so src must be a copy of the original file if you don't want the source modified;</span>
<span class="sd"> it also does not write to dest (dest is the intended destination for purposes of</span>
<span class="sd"> referencing the export database. This allows the exiftool update to be done on the</span>

View File

@@ -5,7 +5,7 @@
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../genindex.html" /><link rel="search" title="Search" href="../../search.html" />
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/>
<title>osxphotos.photoinfo - osxphotos 0.48.7 documentation</title>
<title>osxphotos.photoinfo - osxphotos 0.50.0 documentation</title>
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css" />
@@ -123,7 +123,7 @@
</label>
</div>
<div class="header-center">
<a href="../../index.html"><div class="brand">osxphotos 0.48.7 documentation</div></a>
<a href="../../index.html"><div class="brand">osxphotos 0.50.0 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -146,7 +146,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../index.html">
<span class="sidebar-brand-text">osxphotos 0.48.7 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.50.0 documentation</span>
</a><form class="sidebar-search-container" method="get" action="../../search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
@@ -199,6 +199,8 @@
<span class="sd">Represents a single photo in the Photos library and provides access to the photo's attributes</span>
<span class="sd">PhotosDB.photos() returns a list of PhotoInfo objects</span>
<span class="sd">"""</span>
<span class="kn">import</span> <span class="nn">contextlib</span>
<span class="kn">import</span> <span class="nn">dataclasses</span>
<span class="kn">import</span> <span class="nn">datetime</span>
<span class="kn">import</span> <span class="nn">json</span>
@@ -206,14 +208,16 @@
<span class="kn">import</span> <span class="nn">os</span>
<span class="kn">import</span> <span class="nn">os.path</span>
<span class="kn">import</span> <span class="nn">pathlib</span>
<span class="kn">import</span> <span class="nn">plistlib</span>
<span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">timedelta</span><span class="p">,</span> <span class="n">timezone</span>
<span class="kn">from</span> <span class="nn">functools</span> <span class="kn">import</span> <span class="n">cached_property</span>
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Optional</span>
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Dict</span><span class="p">,</span> <span class="n">Optional</span>
<span class="kn">import</span> <span class="nn">yaml</span>
<span class="kn">from</span> <span class="nn">osxmetadata</span> <span class="kn">import</span> <span class="n">OSXMetaData</span>
<span class="kn">from</span> <span class="nn">._constants</span> <span class="kn">import</span> <span class="p">(</span>
<span class="n">_DB_TABLE_NAMES</span><span class="p">,</span>
<span class="n">_MOVIE_TYPE</span><span class="p">,</span>
<span class="n">_PHOTO_TYPE</span><span class="p">,</span>
<span class="n">_PHOTOS_4_ALBUM_KIND</span><span class="p">,</span>
@@ -1568,6 +1572,30 @@
<span class="sd"> useful for detecting changes in any property/metadata of the photo"""</span>
<span class="k">return</span> <span class="n">hexdigest</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">json</span><span class="p">())</span>
<span class="nd">@cached_property</span>
<span class="k">def</span> <span class="nf">cloud_metadata</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Dict</span><span class="p">:</span>
<span class="sd">"""Returns contents of ZCLOUDMASTERMEDIAMETADATA as dict"""</span>
<span class="c1"># This is a large blob of data so don't load it unless requested</span>
<span class="n">asset_table</span> <span class="o">=</span> <span class="n">_DB_TABLE_NAMES</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_photos_ver</span><span class="p">][</span><span class="s2">"ASSET"</span><span class="p">]</span>
<span class="n">sql_cloud_metadata</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"""</span>
<span class="s2"> SELECT ZCLOUDMASTERMEDIAMETADATA.ZDATA</span>
<span class="s2"> FROM ZCLOUDMASTERMEDIAMETADATA</span>
<span class="s2"> JOIN ZCLOUDMASTER ON ZCLOUDMASTER.Z_PK = ZCLOUDMASTERMEDIAMETADATA.ZCLOUDMASTER</span>
<span class="s2"> JOIN </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2"> on </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZMASTER = ZCLOUDMASTER.Z_PK</span>
<span class="s2"> WHERE </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZUUID = ?</span>
<span class="s2"> """</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">&lt;=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"cloud_metadata not implemented for this database version"</span><span class="p">)</span>
<span class="k">return</span> <span class="p">{}</span>
<span class="n">_</span><span class="p">,</span> <span class="n">cursor</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">get_db_connection</span><span class="p">()</span>
<span class="n">metadata</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">if</span> <span class="n">results</span> <span class="o">:=</span> <span class="n">cursor</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="n">sql_cloud_metadata</span><span class="p">,</span> <span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">,))</span><span class="o">.</span><span class="n">fetchone</span><span class="p">():</span>
<span class="k">with</span> <span class="n">contextlib</span><span class="o">.</span><span class="n">suppress</span><span class="p">(</span><span class="ne">Exception</span><span class="p">):</span>
<span class="n">metadata</span> <span class="o">=</span> <span class="n">plistlib</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">results</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
<span class="k">return</span> <span class="n">metadata</span>
<div class="viewcode-block" id="PhotoInfo.detected_text"><a class="viewcode-back" href="../../reference.html#osxphotos.PhotoInfo.detected_text">[docs]</a> <span class="k">def</span> <span class="nf">detected_text</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">confidence_threshold</span><span class="o">=</span><span class="n">TEXT_DETECTION_CONFIDENCE_THRESHOLD</span><span class="p">):</span>
<span class="sd">"""Detects text in photo and returns lists of results as (detected text, confidence)</span>

View File

@@ -5,7 +5,7 @@
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../../genindex.html" /><link rel="search" title="Search" href="../../../search.html" />
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/>
<title>osxphotos.photosdb.photosdb - osxphotos 0.48.7 documentation</title>
<title>osxphotos.photosdb.photosdb - osxphotos 0.50.4 documentation</title>
<link rel="stylesheet" type="text/css" href="../../../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../../../_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
<link rel="stylesheet" type="text/css" href="../../../_static/copybutton.css" />
@@ -123,7 +123,7 @@
</label>
</div>
<div class="header-center">
<a href="../../../index.html"><div class="brand">osxphotos 0.48.7 documentation</div></a>
<a href="../../../index.html"><div class="brand">osxphotos 0.50.4 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -146,7 +146,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../../index.html">
<span class="sidebar-brand-text">osxphotos 0.48.7 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.50.4 documentation</span>
</a><form class="sidebar-search-container" method="get" action="../../../search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
@@ -210,7 +210,7 @@
<span class="kn">from</span> <span class="nn">collections</span> <span class="kn">import</span> <span class="n">OrderedDict</span>
<span class="kn">from</span> <span class="nn">collections.abc</span> <span class="kn">import</span> <span class="n">Iterable</span>
<span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">datetime</span><span class="p">,</span> <span class="n">timedelta</span><span class="p">,</span> <span class="n">timezone</span>
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">List</span>
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">List</span><span class="p">,</span> <span class="n">Optional</span>
<span class="kn">from</span> <span class="nn">unicodedata</span> <span class="kn">import</span> <span class="n">normalize</span>
<span class="kn">import</span> <span class="nn">bitmath</span>
@@ -3054,16 +3054,16 @@
<div class="viewcode-block" id="PhotosDB.photos"><a class="viewcode-back" href="../../../reference.html#osxphotos.PhotosDB.photos">[docs]</a> <span class="k">def</span> <span class="nf">photos</span><span class="p">(</span>
<span class="bp">self</span><span class="p">,</span>
<span class="n">keywords</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">uuid</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">persons</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">albums</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">images</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">movies</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">from_date</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">to_date</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">intrash</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="p">):</span>
<span class="n">keywords</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">]]</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span>
<span class="n">uuid</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">]]</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span>
<span class="n">persons</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">]]</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span>
<span class="n">albums</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">]]</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span>
<span class="n">images</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="kc">True</span><span class="p">,</span>
<span class="n">movies</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="kc">True</span><span class="p">,</span>
<span class="n">from_date</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="n">datetime</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span>
<span class="n">to_date</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="n">datetime</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span>
<span class="n">intrash</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="kc">False</span><span class="p">,</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">List</span><span class="p">[</span><span class="n">PhotoInfo</span><span class="p">]:</span>
<span class="sd">"""Return a list of PhotoInfo objects</span>
<span class="sd"> If called with no args, returns the entire database of photos</span>
<span class="sd"> If called with args, returns photos matching the args (e.g. keywords, persons, etc.)</span>

View File

@@ -5,7 +5,7 @@
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../genindex.html" /><link rel="search" title="Search" href="../../search.html" />
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/>
<title>osxphotos.phototemplate - osxphotos 0.49.0 documentation</title>
<title>osxphotos.phototemplate - osxphotos 0.50.3 documentation</title>
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css" />
@@ -123,7 +123,7 @@
</label>
</div>
<div class="header-center">
<a href="../../index.html"><div class="brand">osxphotos 0.49.0 documentation</div></a>
<a href="../../index.html"><div class="brand">osxphotos 0.50.3 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -146,7 +146,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../index.html">
<span class="sidebar-brand-text">osxphotos 0.49.0 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.50.3 documentation</span>
</a><form class="sidebar-search-container" method="get" action="../../search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
@@ -201,11 +201,12 @@
<span class="kn">import</span> <span class="nn">locale</span>
<span class="kn">import</span> <span class="nn">os</span>
<span class="kn">import</span> <span class="nn">pathlib</span>
<span class="kn">import</span> <span class="nn">re</span>
<span class="kn">import</span> <span class="nn">shlex</span>
<span class="kn">import</span> <span class="nn">sys</span>
<span class="kn">from</span> <span class="nn">contextlib</span> <span class="kn">import</span> <span class="n">suppress</span>
<span class="kn">from</span> <span class="nn">dataclasses</span> <span class="kn">import</span> <span class="n">dataclass</span>
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Optional</span>
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">List</span><span class="p">,</span> <span class="n">Optional</span><span class="p">,</span> <span class="n">Tuple</span>
<span class="kn">from</span> <span class="nn">textx</span> <span class="kn">import</span> <span class="n">TextXSyntaxError</span><span class="p">,</span> <span class="n">metamodel_from_file</span>
@@ -215,7 +216,7 @@
<span class="kn">from</span> <span class="nn">.exiftool</span> <span class="kn">import</span> <span class="n">ExifToolCaching</span>
<span class="kn">from</span> <span class="nn">.path_utils</span> <span class="kn">import</span> <span class="n">sanitize_dirname</span><span class="p">,</span> <span class="n">sanitize_filename</span><span class="p">,</span> <span class="n">sanitize_pathpart</span>
<span class="kn">from</span> <span class="nn">.text_detection</span> <span class="kn">import</span> <span class="n">detect_text</span>
<span class="kn">from</span> <span class="nn">.utils</span> <span class="kn">import</span> <span class="n">expand_and_validate_filepath</span><span class="p">,</span> <span class="n">load_function</span>
<span class="kn">from</span> <span class="nn">.utils</span> <span class="kn">import</span> <span class="n">expand_and_validate_filepath</span><span class="p">,</span> <span class="n">load_function</span><span class="p">,</span> <span class="n">uuid_to_shortuuid</span>
<span class="n">__all__</span> <span class="o">=</span> <span class="p">[</span>
<span class="s2">"RenderOptions"</span><span class="p">,</span>
@@ -336,6 +337,8 @@
<span class="s2">"</span><span class="si">{exif.lens_model}</span><span class="s2">"</span><span class="p">:</span> <span class="s2">"Lens model from original photo's EXIF information as imported by Photos, e.g. 'iPhone 6s back camera 4.15mm f/2.2'"</span><span class="p">,</span>
<span class="s2">"</span><span class="si">{moment}</span><span class="s2">"</span><span class="p">:</span> <span class="s2">"The moment title of the photo"</span><span class="p">,</span>
<span class="s2">"</span><span class="si">{uuid}</span><span class="s2">"</span><span class="p">:</span> <span class="s2">"Photo's internal universally unique identifier (UUID) for the photo, a 36-character string unique to the photo, e.g. '128FB4C6-0B16-4E7D-9108-FB2E90DA1546'"</span><span class="p">,</span>
<span class="s2">"</span><span class="si">{shortuuid}</span><span class="s2">"</span><span class="p">:</span> <span class="s2">"A shorter representation of photo's internal universally unique identifier (UUID) for the photo, "</span>
<span class="o">+</span> <span class="s2">"a 22-character string unique to the photo, e.g. 'JYsxugP9UjetmCbBCHXcmu'"</span><span class="p">,</span>
<span class="s2">"</span><span class="si">{id}</span><span class="s2">"</span><span class="p">:</span> <span class="s2">"A unique number for the photo based on its primary key in the Photos database. "</span>
<span class="o">+</span> <span class="s2">"A sequential integer, e.g. 1, 2, 3...etc. Each asset associated with a photo (e.g. an image and Live Photo preview) will share the same id. "</span>
<span class="o">+</span> <span class="s2">"May be formatted using a python string format code. "</span>
@@ -344,21 +347,23 @@
<span class="s2">"</span><span class="si">{album_seq}</span><span class="s2">"</span><span class="p">:</span> <span class="s2">"An integer, starting at 0, indicating the photo's index (sequence) in the containing album. "</span>
<span class="o">+</span> <span class="s2">"Only valid when used in a '--filename' template and only when '</span><span class="si">{album}</span><span class="s2">' or '</span><span class="si">{folder_album}</span><span class="s2">' is used in the '--directory' template. "</span>
<span class="o">+</span> <span class="s1">'For example </span><span class="se">\'</span><span class="s1">--directory "</span><span class="si">{folder_album}</span><span class="s1">" --filename "</span><span class="si">{album_seq}</span><span class="s1">_</span><span class="si">{original_name}</span><span class="s1">"</span><span class="se">\'</span><span class="s1">. '</span>
<span class="o">+</span> <span class="s2">"To start counting at a value other than 0, append append a period and the starting value to the field name. "</span>
<span class="o">+</span> <span class="s2">"For example, to start counting at 1 instead of 0: '</span><span class="si">{album_seq.1}</span><span class="s2">'. "</span>
<span class="o">+</span> <span class="s2">"To start counting at a value other than 0, append append '(starting_value)' to the field name. "</span>
<span class="o">+</span> <span class="s2">"For example, to start counting at 1 instead of 0: '{album_seq(1)}'. "</span>
<span class="o">+</span> <span class="s2">"May be formatted using a python string format code. "</span>
<span class="o">+</span> <span class="s2">"For example, to format as a 5-digit integer and pad with zeros, use '</span><span class="si">{album_seq:05d}</span><span class="s2">' which results in "</span>
<span class="o">+</span> <span class="s2">"00000, 00001, 00002...etc. "</span>
<span class="o">+</span> <span class="s2">"To format while also using a starting value: '{album_seq:05d(1)}' which results in 0001, 00002...etc."</span>
<span class="o">+</span> <span class="s2">"This may result in incorrect sequences if you have duplicate albums with the same name; see also '</span><span class="si">{folder_album_seq}</span><span class="s2">'."</span><span class="p">,</span>
<span class="s2">"</span><span class="si">{folder_album_seq}</span><span class="s2">"</span><span class="p">:</span> <span class="s2">"An integer, starting at 0, indicating the photo's index (sequence) in the containing album and folder path. "</span>
<span class="o">+</span> <span class="s2">"Only valid when used in a '--filename' template and only when '</span><span class="si">{folder_album}</span><span class="s2">' is used in the '--directory' template. "</span>
<span class="o">+</span> <span class="s1">'For example </span><span class="se">\'</span><span class="s1">--directory "</span><span class="si">{folder_album}</span><span class="s1">" --filename "</span><span class="si">{folder_album_seq}</span><span class="s1">_</span><span class="si">{original_name}</span><span class="s1">"</span><span class="se">\'</span><span class="s1">. '</span>
<span class="o">+</span> <span class="s2">"To start counting at a value other than 0, append append a period and the starting value to the field name. "</span>
<span class="o">+</span> <span class="s2">"For example, to start counting at 1 instead of 0: '</span><span class="si">{folder_album_seq.1}</span><span class="s2">' "</span>
<span class="o">+</span> <span class="s2">"To start counting at a value other than 0, append '(starting_value)' to the field name. "</span>
<span class="o">+</span> <span class="s2">"For example, to start counting at 1 instead of 0: '{folder_album_seq(1)}' "</span>
<span class="o">+</span> <span class="s2">"May be formatted using a python string format code. "</span>
<span class="o">+</span> <span class="s2">"For example, to format as a 5-digit integer and pad with zeros, use '</span><span class="si">{folder_album_seq:05d}</span><span class="s2">' which results in "</span>
<span class="o">+</span> <span class="s2">"00000, 00001, 00002...etc. "</span>
<span class="o">+</span> <span class="s2">"This may result in incorrect sequences if you have duplicate albums with the same name in the same folder; see also '</span><span class="si">{album_seq}</span><span class="s2">'."</span><span class="p">,</span>
<span class="o">+</span> <span class="s2">"To format while also using a starting value: '{folder_album_seq:05d(1)}' which results in 0001, 00002...etc."</span>
<span class="o">+</span> <span class="s2">"This may result in incorrect sequences if you have duplicate albums with the same name in the same folder; see also '</span><span class="si">{album_seq}</span><span class="s2">'. "</span><span class="p">,</span>
<span class="s2">"</span><span class="si">{comma}</span><span class="s2">"</span><span class="p">:</span> <span class="s2">"A comma: ','"</span><span class="p">,</span>
<span class="s2">"</span><span class="si">{semicolon}</span><span class="s2">"</span><span class="p">:</span> <span class="s2">"A semicolon: ';'"</span><span class="p">,</span>
<span class="s2">"</span><span class="si">{questionmark}</span><span class="s2">"</span><span class="p">:</span> <span class="s2">"A question mark: '?'"</span><span class="p">,</span>
@@ -418,6 +423,9 @@
<span class="o">+</span> <span class="s2">"Note: this feature is not the same thing as Live Text in macOS Monterey, which osxphotos does not yet support."</span><span class="p">,</span>
<span class="s2">"</span><span class="si">{shell_quote}</span><span class="s2">"</span><span class="p">:</span> <span class="s2">"Use in form '{shell_quote,TEMPLATE}'; quotes the rendered TEMPLATE value(s) for safe usage in the shell, e.g. My file.jpeg =&gt; 'My file.jpeg'; only adds quotes if needed."</span><span class="p">,</span>
<span class="s2">"</span><span class="si">{strip}</span><span class="s2">"</span><span class="p">:</span> <span class="s2">"Use in form '{strip,TEMPLATE}'; strips whitespace from begining and end of rendered TEMPLATE value(s)."</span><span class="p">,</span>
<span class="s2">"</span><span class="si">{format}</span><span class="s2">"</span><span class="p">:</span> <span class="s2">"Use in form, '{format:TYPE:FORMAT,TEMPLATE}'; converts TEMPLATE value to TYPE then formats the value "</span>
<span class="o">+</span> <span class="s2">"using Python string formatting codes specified by FORMAT; TYPE is one of: 'int', 'float', or 'str'. "</span>
<span class="s2">"For example, '{format:float:.1f,{exiftool:EXIF:FocalLength}}' will format focal length to 1 decimal place (e.g. '100.0'). "</span><span class="p">,</span>
<span class="s2">"</span><span class="si">{function}</span><span class="s2">"</span><span class="p">:</span> <span class="s2">"Execute a python function from an external file and use return value as template substitution. "</span>
<span class="o">+</span> <span class="s2">"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. "</span>
<span class="o">+</span> <span class="s2">"The function will be passed the PhotoInfo object for the photo. "</span>
@@ -435,6 +443,27 @@
<span class="s2">"brackets"</span><span class="p">:</span> <span class="s2">"Enclose value in brackets, e.g. 'value' =&gt; '[value]'"</span><span class="p">,</span>
<span class="s2">"shell_quote"</span><span class="p">:</span> <span class="s2">"Quotes the value for safe usage in the shell, e.g. My file.jpeg =&gt; 'My file.jpeg'; only adds quotes if needed."</span><span class="p">,</span>
<span class="s2">"function"</span><span class="p">:</span> <span class="s2">"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"</span><span class="p">,</span>
<span class="s2">"split(x)"</span><span class="p">:</span> <span class="s2">"Split value into a list of values using x as delimiter, e.g. 'value1;value2' =&gt; ['value1', 'value2'] if used with split(;)."</span><span class="p">,</span>
<span class="s2">"autosplit"</span><span class="p">:</span> <span class="s2">"Automatically split delimited string into separate values; will split strings delimited by comma, semicolon, or space, e.g. 'value1,value2' =&gt; ['value1', 'value2']."</span><span class="p">,</span>
<span class="s2">"chop(x)"</span><span class="p">:</span> <span class="s2">"Remove x characters off the end of value, e.g. chop(1): 'Value' =&gt; 'Valu'; when applied to a list, chops characters from each list value, e.g. chop(1): ['travel', 'beach']=&gt; ['trave', 'beac']."</span><span class="p">,</span>
<span class="s2">"chomp(x)"</span><span class="p">:</span> <span class="s2">"Remove x characters from the beginning of value, e.g. chomp(1): ['Value'] =&gt; ['alue']; when applied to a list, removes characters from each list value, e.g. chomp(1): ['travel', 'beach']=&gt; ['ravel', 'each']."</span><span class="p">,</span>
<span class="s2">"sort"</span><span class="p">:</span> <span class="s2">"Sort list of values, e.g. ['c', 'b', 'a'] =&gt; ['a', 'b', 'c']."</span><span class="p">,</span>
<span class="s2">"rsort"</span><span class="p">:</span> <span class="s2">"Sort list of values in reverse order, e.g. ['a', 'b', 'c'] =&gt; ['c', 'b', 'a']."</span><span class="p">,</span>
<span class="s2">"reverse"</span><span class="p">:</span> <span class="s2">"Reverse order of values, e.g. ['a', 'b', 'c'] =&gt; ['c', 'b', 'a']."</span><span class="p">,</span>
<span class="s2">"uniq"</span><span class="p">:</span> <span class="s2">"Remove duplicate values, e.g. ['a', 'b', 'c', 'b', 'a'] =&gt; ['a', 'b', 'c']."</span><span class="p">,</span>
<span class="s2">"join(x)"</span><span class="p">:</span> <span class="s2">"Join list of values with delimiter x, e.g. join(,): ['a', 'b', 'c'] =&gt; 'a,b,c'; "</span>
<span class="o">+</span> <span class="s2">"the DELIM option functions similar to join(x) but with DELIM, the join happens before being passed to any filters."</span>
<span class="o">+</span> <span class="s2">"May optionally be used without an argument, that is 'join()' which joins values together with no delimiter. "</span>
<span class="o">+</span> <span class="s2">"e.g. join(): ['a', 'b', 'c'] =&gt; 'abc'."</span><span class="p">,</span>
<span class="s2">"append(x)"</span><span class="p">:</span> <span class="s2">"Append x to list of values, e.g. append(d): ['a', 'b', 'c'] =&gt; ['a', 'b', 'c', 'd']."</span><span class="p">,</span>
<span class="s2">"prepend(x)"</span><span class="p">:</span> <span class="s2">"Prepend x to list of values, e.g. prepend(d): ['a', 'b', 'c'] =&gt; ['d', 'a', 'b', 'c']."</span><span class="p">,</span>
<span class="s2">"remove(x)"</span><span class="p">:</span> <span class="s2">"Remove x from list of values, e.g. remove(b): ['a', 'b', 'c'] =&gt; ['a', 'c']."</span><span class="p">,</span>
<span class="s2">"slice(start:stop:step)"</span><span class="p">:</span> <span class="s2">"Slice list using same semantics as Python's list slicing, "</span>
<span class="o">+</span> <span class="s2">"e.g. slice(1:3): ['a', 'b', 'c', 'd'] =&gt; ['b', 'c']; slice(1:4:2): ['a', 'b', 'c', 'd'] =&gt; ['b', 'd']; "</span>
<span class="o">+</span> <span class="s2">"slice(1:): ['a', 'b', 'c', 'd'] =&gt; ['b', 'c', 'd']; slice(:-1): ['a', 'b', 'c', 'd'] =&gt; ['a', 'b', 'c']; "</span>
<span class="o">+</span> <span class="s2">"slice(::-1): ['a', 'b', 'c', 'd'] =&gt; ['d', 'c', 'b', 'a']. See also sslice()."</span><span class="p">,</span>
<span class="s2">"sslice(start:stop:step)"</span><span class="p">:</span> <span class="s2">"[s(tring) slice] Slice values in a list using same semantics as Python's string slicing, "</span>
<span class="o">+</span> <span class="s2">"e.g. sslice(1:3):'abcd =&gt; 'bc'; sslice(1:4:2): 'abcd' =&gt; 'bd', etc. See also slice()."</span><span class="p">,</span>
<span class="p">}</span>
<span class="c1"># Just the substitutions without the braces</span>
@@ -578,6 +607,7 @@
<span class="bp">self</span><span class="o">.</span><span class="n">filepath</span> <span class="o">=</span> <span class="n">options</span><span class="o">.</span><span class="n">filepath</span>
<span class="bp">self</span><span class="o">.</span><span class="n">quote</span> <span class="o">=</span> <span class="n">options</span><span class="o">.</span><span class="n">quote</span>
<span class="bp">self</span><span class="o">.</span><span class="n">dest_path</span> <span class="o">=</span> <span class="n">options</span><span class="o">.</span><span class="n">dest_path</span>
<span class="bp">self</span><span class="o">.</span><span class="n">variables</span> <span class="o">=</span> <span class="p">{}</span>
<div class="viewcode-block" id="PhotoTemplate.render"><a class="viewcode-back" href="../../reference.html#osxphotos.PhotoTemplate.render">[docs]</a> <span class="k">def</span> <span class="nf">render</span><span class="p">(</span>
<span class="bp">self</span><span class="p">,</span>
@@ -626,14 +656,13 @@
<span class="k">def</span> <span class="nf">_render_statement</span><span class="p">(</span>
<span class="bp">self</span><span class="p">,</span>
<span class="n">statement</span><span class="p">,</span>
<span class="n">path_sep</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">field_arg</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="p">):</span>
<span class="n">path_sep</span> <span class="o">=</span> <span class="n">path_sep</span> <span class="ow">or</span> <span class="bp">self</span><span class="o">.</span><span class="n">path_sep</span>
<span class="n">results</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">unmatched</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">ts</span> <span class="ow">in</span> <span class="n">statement</span><span class="o">.</span><span class="n">template_strings</span><span class="p">:</span>
<span class="n">results</span><span class="p">,</span> <span class="n">unmatched</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_render_template_string</span><span class="p">(</span>
<span class="n">ts</span><span class="p">,</span> <span class="n">results</span><span class="o">=</span><span class="n">results</span><span class="p">,</span> <span class="n">unmatched</span><span class="o">=</span><span class="n">unmatched</span><span class="p">,</span> <span class="n">path_sep</span><span class="o">=</span><span class="n">path_sep</span>
<span class="n">ts</span><span class="p">,</span> <span class="n">results</span><span class="o">=</span><span class="n">results</span><span class="p">,</span> <span class="n">unmatched</span><span class="o">=</span><span class="n">unmatched</span><span class="p">,</span> <span class="n">field_arg</span><span class="o">=</span><span class="n">field_arg</span>
<span class="p">)</span>
<span class="n">rendered_strings</span> <span class="o">=</span> <span class="n">results</span>
@@ -653,7 +682,7 @@
<span class="k">def</span> <span class="nf">_render_template_string</span><span class="p">(</span>
<span class="bp">self</span><span class="p">,</span>
<span class="n">ts</span><span class="p">,</span>
<span class="n">path_sep</span><span class="p">,</span>
<span class="n">field_arg</span><span class="p">,</span>
<span class="n">results</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">unmatched</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="p">):</span>
@@ -665,11 +694,6 @@
<span class="k">if</span> <span class="n">ts</span><span class="o">.</span><span class="n">template</span><span class="p">:</span>
<span class="c1"># have a template field to process</span>
<span class="n">field</span> <span class="o">=</span> <span class="n">ts</span><span class="o">.</span><span class="n">template</span><span class="o">.</span><span class="n">field</span>
<span class="n">field_part</span> <span class="o">=</span> <span class="n">field</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">"."</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
<span class="k">if</span> <span class="n">field</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">FIELD_NAMES</span> <span class="ow">and</span> <span class="n">field_part</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">FIELD_NAMES</span><span class="p">:</span>
<span class="n">unmatched</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">field</span><span class="p">)</span>
<span class="k">return</span> <span class="p">[],</span> <span class="n">unmatched</span>
<span class="n">subfield</span> <span class="o">=</span> <span class="n">ts</span><span class="o">.</span><span class="n">template</span><span class="o">.</span><span class="n">subfield</span>
<span class="c1"># process filters</span>
@@ -677,14 +701,15 @@
<span class="k">if</span> <span class="n">ts</span><span class="o">.</span><span class="n">template</span><span class="o">.</span><span class="n">filter</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">filters</span> <span class="o">=</span> <span class="n">ts</span><span class="o">.</span><span class="n">template</span><span class="o">.</span><span class="n">filter</span><span class="o">.</span><span class="n">value</span>
<span class="c1"># process path_sep</span>
<span class="k">if</span> <span class="n">ts</span><span class="o">.</span><span class="n">template</span><span class="o">.</span><span class="n">pathsep</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">path_sep</span> <span class="o">=</span> <span class="n">ts</span><span class="o">.</span><span class="n">template</span><span class="o">.</span><span class="n">pathsep</span><span class="o">.</span><span class="n">value</span>
<span class="c1"># process field arguments</span>
<span class="k">if</span> <span class="n">ts</span><span class="o">.</span><span class="n">template</span><span class="o">.</span><span class="n">fieldarg</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">field_arg</span> <span class="o">=</span> <span class="n">ts</span><span class="o">.</span><span class="n">template</span><span class="o">.</span><span class="n">fieldarg</span><span class="o">.</span><span class="n">value</span>
<span class="c1"># process delim</span>
<span class="k">if</span> <span class="n">ts</span><span class="o">.</span><span class="n">template</span><span class="o">.</span><span class="n">delim</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
<span class="c1"># if value is None, means format was {+field}</span>
<span class="n">delim</span> <span class="o">=</span> <span class="n">ts</span><span class="o">.</span><span class="n">template</span><span class="o">.</span><span class="n">delim</span><span class="o">.</span><span class="n">value</span> <span class="ow">or</span> <span class="s2">""</span>
<span class="n">delim</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">expand_variables_to_str</span><span class="p">(</span><span class="n">delim</span><span class="p">,</span> <span class="s2">"delim"</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">delim</span> <span class="o">=</span> <span class="kc">None</span>
@@ -693,7 +718,7 @@
<span class="k">if</span> <span class="n">ts</span><span class="o">.</span><span class="n">template</span><span class="o">.</span><span class="n">bool</span><span class="o">.</span><span class="n">value</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">bool_val</span><span class="p">,</span> <span class="n">u</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_render_statement</span><span class="p">(</span>
<span class="n">ts</span><span class="o">.</span><span class="n">template</span><span class="o">.</span><span class="n">bool</span><span class="o">.</span><span class="n">value</span><span class="p">,</span>
<span class="n">path_sep</span><span class="o">=</span><span class="n">path_sep</span><span class="p">,</span>
<span class="n">field_arg</span><span class="o">=</span><span class="n">field_arg</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">unmatched</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">u</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
@@ -709,7 +734,7 @@
<span class="k">if</span> <span class="n">ts</span><span class="o">.</span><span class="n">template</span><span class="o">.</span><span class="n">default</span><span class="o">.</span><span class="n">value</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">default</span><span class="p">,</span> <span class="n">u</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_render_statement</span><span class="p">(</span>
<span class="n">ts</span><span class="o">.</span><span class="n">template</span><span class="o">.</span><span class="n">default</span><span class="o">.</span><span class="n">value</span><span class="p">,</span>
<span class="n">path_sep</span><span class="o">=</span><span class="n">path_sep</span><span class="p">,</span>
<span class="n">field_arg</span><span class="o">=</span><span class="n">field_arg</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">unmatched</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">u</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
@@ -724,11 +749,11 @@
<span class="n">negation</span> <span class="o">=</span> <span class="n">ts</span><span class="o">.</span><span class="n">template</span><span class="o">.</span><span class="n">conditional</span><span class="o">.</span><span class="n">negation</span>
<span class="k">if</span> <span class="n">ts</span><span class="o">.</span><span class="n">template</span><span class="o">.</span><span class="n">conditional</span><span class="o">.</span><span class="n">value</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
<span class="c1"># conditional value is also a TemplateString</span>
<span class="n">conditional_value</span><span class="p">,</span> <span class="n">u</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_render_statement</span><span class="p">(</span>
<span class="n">ts</span><span class="o">.</span><span class="n">template</span><span class="o">.</span><span class="n">conditional</span><span class="o">.</span><span class="n">value</span><span class="p">,</span>
<span class="n">path_sep</span><span class="o">=</span><span class="n">path_sep</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">unmatched</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">u</span><span class="p">)</span>
<span class="n">conditional_value</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">cv</span> <span class="ow">in</span> <span class="n">ts</span><span class="o">.</span><span class="n">template</span><span class="o">.</span><span class="n">conditional</span><span class="o">.</span><span class="n">value</span><span class="p">:</span>
<span class="n">value</span><span class="p">,</span> <span class="n">u</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_render_statement</span><span class="p">(</span><span class="n">cv</span><span class="p">)</span>
<span class="n">conditional_value</span> <span class="o">+=</span> <span class="n">value</span>
<span class="n">unmatched</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">u</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="c1"># this shouldn't happen</span>
<span class="n">conditional_value</span> <span class="o">=</span> <span class="p">[</span><span class="s2">""</span><span class="p">]</span>
@@ -737,43 +762,23 @@
<span class="n">negation</span> <span class="o">=</span> <span class="kc">None</span>
<span class="n">conditional_value</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">vals</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">if</span> <span class="p">(</span>
<span class="n">field</span> <span class="ow">in</span> <span class="n">SINGLE_VALUE_SUBSTITUTIONS</span>
<span class="ow">or</span> <span class="n">field</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">"."</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span> <span class="ow">in</span> <span class="n">SINGLE_VALUE_SUBSTITUTIONS</span>
<span class="p">):</span>
<span class="n">vals</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_template_value</span><span class="p">(</span>
<span class="n">field</span><span class="p">,</span>
<span class="n">default</span><span class="o">=</span><span class="n">default</span><span class="p">,</span>
<span class="n">subfield</span><span class="o">=</span><span class="n">subfield</span><span class="p">,</span>
<span class="c1"># delim=delim or self.inplace_sep,</span>
<span class="c1"># path_sep=path_sep,</span>
<span class="p">)</span>
<span class="k">elif</span> <span class="n">field</span> <span class="o">==</span> <span class="s2">"exiftool"</span><span class="p">:</span>
<span class="k">if</span> <span class="n">subfield</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span>
<span class="s2">"SyntaxError: GROUP:NAME subfield must not be null with {exiftool:GROUP:NAME}'"</span>
<span class="k">if</span> <span class="n">field</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">"%"</span><span class="p">):</span>
<span class="c1"># variable in form {%var}</span>
<span class="n">vals</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">variables</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">field</span><span class="p">[</span><span class="mi">1</span><span class="p">:],</span> <span class="kc">None</span><span class="p">)</span>
<span class="k">if</span> <span class="n">vals</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">SyntaxError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Variable '</span><span class="si">{</span><span class="n">field</span><span class="p">[</span><span class="mi">1</span><span class="p">:]</span><span class="si">}</span><span class="s2">' is not defined."</span><span class="p">)</span>
<span class="k">elif</span> <span class="n">field</span> <span class="o">==</span> <span class="s2">"var"</span><span class="p">:</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">subfield</span> <span class="ow">or</span> <span class="ow">not</span> <span class="n">default</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">SyntaxError</span><span class="p">(</span>
<span class="s2">"var must have a subfield and value in form {var:subfield,value}"</span>
<span class="p">)</span>
<span class="n">vals</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_template_value_exiftool</span><span class="p">(</span>
<span class="n">subfield</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">elif</span> <span class="n">field</span> <span class="o">==</span> <span class="s2">"function"</span><span class="p">:</span>
<span class="k">if</span> <span class="n">subfield</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span>
<span class="s2">"SyntaxError: filename and function must not be null with {function::filename.py:function_name}"</span>
<span class="p">)</span>
<span class="n">vals</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_template_value_function</span><span class="p">(</span>
<span class="n">subfield</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">elif</span> <span class="n">field</span> <span class="ow">in</span> <span class="n">MULTI_VALUE_SUBSTITUTIONS</span> <span class="ow">or</span> <span class="n">field</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">"photo"</span><span class="p">):</span>
<span class="n">vals</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_template_value_multi</span><span class="p">(</span>
<span class="n">field</span><span class="p">,</span> <span class="n">subfield</span><span class="p">,</span> <span class="n">path_sep</span><span class="o">=</span><span class="n">path_sep</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="n">default</span>
<span class="p">)</span>
<span class="k">elif</span> <span class="n">field</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">"."</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span> <span class="ow">in</span> <span class="n">PATHLIB_SUBSTITUTIONS</span><span class="p">:</span>
<span class="n">vals</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_template_value_pathlib</span><span class="p">(</span><span class="n">field</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">variables</span><span class="p">[</span><span class="n">subfield</span><span class="p">]</span> <span class="o">=</span> <span class="n">default</span>
<span class="n">vals</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">unmatched</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">field</span><span class="p">)</span>
<span class="k">return</span> <span class="p">[],</span> <span class="n">unmatched</span>
<span class="n">vals</span><span class="p">,</span> <span class="n">u</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_field_values</span><span class="p">(</span><span class="n">field</span><span class="p">,</span> <span class="n">subfield</span><span class="p">,</span> <span class="n">field_arg</span><span class="p">,</span> <span class="n">default</span><span class="p">)</span>
<span class="k">if</span> <span class="n">u</span><span class="p">:</span>
<span class="n">unmatched</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">u</span><span class="p">)</span>
<span class="k">return</span> <span class="p">[],</span> <span class="n">unmatched</span>
<span class="n">vals</span> <span class="o">=</span> <span class="p">[</span><span class="n">val</span> <span class="k">for</span> <span class="n">val</span> <span class="ow">in</span> <span class="n">vals</span> <span class="k">if</span> <span class="n">val</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">]</span>
@@ -782,7 +787,7 @@
<span class="n">vals</span> <span class="o">=</span> <span class="p">[</span><span class="n">sep</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="nb">sorted</span><span class="p">(</span><span class="n">vals</span><span class="p">))]</span> <span class="k">if</span> <span class="n">vals</span> <span class="k">else</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">filter_</span> <span class="ow">in</span> <span class="n">filters</span><span class="p">:</span>
<span class="n">vals</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_template_value_filter</span><span class="p">(</span><span class="n">filter_</span><span class="p">,</span> <span class="n">vals</span><span class="p">)</span>
<span class="n">vals</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_filter_values</span><span class="p">(</span><span class="n">filter_</span><span class="p">,</span> <span class="n">vals</span><span class="p">)</span>
<span class="c1"># process find/replace</span>
<span class="k">if</span> <span class="n">ts</span><span class="o">.</span><span class="n">template</span><span class="o">.</span><span class="n">findreplace</span><span class="p">:</span>
@@ -790,7 +795,9 @@
<span class="k">for</span> <span class="n">val</span> <span class="ow">in</span> <span class="n">vals</span><span class="p">:</span>
<span class="k">for</span> <span class="n">pair</span> <span class="ow">in</span> <span class="n">ts</span><span class="o">.</span><span class="n">template</span><span class="o">.</span><span class="n">findreplace</span><span class="o">.</span><span class="n">pairs</span><span class="p">:</span>
<span class="n">find</span> <span class="o">=</span> <span class="n">pair</span><span class="o">.</span><span class="n">find</span> <span class="ow">or</span> <span class="s2">""</span>
<span class="n">find</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">expand_variables_to_str</span><span class="p">(</span><span class="n">find</span><span class="p">,</span> <span class="s2">"find/replace"</span><span class="p">)</span>
<span class="n">repl</span> <span class="o">=</span> <span class="n">pair</span><span class="o">.</span><span class="n">replace</span> <span class="ow">or</span> <span class="s2">""</span>
<span class="n">repl</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">expand_variables_to_str</span><span class="p">(</span><span class="n">repl</span><span class="p">,</span> <span class="s2">"find/replace"</span><span class="p">)</span>
<span class="n">val</span> <span class="o">=</span> <span class="n">val</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="n">find</span><span class="p">,</span> <span class="n">repl</span><span class="p">)</span>
<span class="n">new_vals</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">val</span><span class="p">)</span>
<span class="n">vals</span> <span class="o">=</span> <span class="n">new_vals</span>
@@ -817,22 +824,23 @@
<span class="k">def</span> <span class="nf">comparison_test</span><span class="p">(</span><span class="n">test_function</span><span class="p">):</span>
<span class="sd">"""Perform numerical comparisons using test_function; closure to capture conditional_val, vals, negation"""</span>
<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">vals</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">1</span> <span class="ow">or</span> <span class="nb">len</span><span class="p">(</span><span class="n">conditional_value</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">1</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span>
<span class="sa">f</span><span class="s2">"comparison operators may only be used with a single value: </span><span class="si">{</span><span class="n">vals</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">conditional_value</span><span class="si">}</span><span class="s2">"</span>
<span class="c1"># returns True if any of the values match the condition</span>
<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">conditional_value</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">1</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">SyntaxError</span><span class="p">(</span>
<span class="sa">f</span><span class="s2">"comparison operators may only be used with a single conditional value: </span><span class="si">{</span><span class="n">conditional_value</span><span class="si">}</span><span class="s2">"</span>
<span class="p">)</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">match</span> <span class="o">=</span> <span class="nb">bool</span><span class="p">(</span>
<span class="n">test_function</span><span class="p">(</span><span class="nb">float</span><span class="p">(</span><span class="n">vals</span><span class="p">[</span><span class="mi">0</span><span class="p">]),</span> <span class="nb">float</span><span class="p">(</span><span class="n">conditional_value</span><span class="p">[</span><span class="mi">0</span><span class="p">]))</span>
<span class="n">match</span> <span class="o">=</span> <span class="nb">any</span><span class="p">(</span>
<span class="nb">bool</span><span class="p">(</span><span class="n">test_function</span><span class="p">(</span><span class="nb">float</span><span class="p">(</span><span class="n">v</span><span class="p">),</span> <span class="nb">float</span><span class="p">(</span><span class="n">conditional_value</span><span class="p">[</span><span class="mi">0</span><span class="p">])))</span>
<span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">vals</span>
<span class="p">)</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p">[</span><span class="s2">"True"</span><span class="p">]</span>
<span class="k">if</span> <span class="p">(</span><span class="n">match</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">negation</span><span class="p">)</span> <span class="ow">or</span> <span class="p">(</span><span class="n">negation</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">match</span><span class="p">)</span>
<span class="k">else</span> <span class="p">[]</span>
<span class="p">)</span>
<span class="k">except</span> <span class="ne">ValueError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span>
<span class="k">raise</span> <span class="ne">SyntaxError</span><span class="p">(</span>
<span class="sa">f</span><span class="s2">"comparison operators may only be used with values that can be converted to numbers: </span><span class="si">{</span><span class="n">vals</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">conditional_value</span><span class="si">}</span><span class="s2">"</span>
<span class="p">)</span> <span class="kn">from</span> <span class="nn">e</span>
@@ -874,7 +882,8 @@
<span class="k">if</span> <span class="n">is_bool</span><span class="p">:</span>
<span class="n">vals</span> <span class="o">=</span> <span class="n">default</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">vals</span> <span class="k">else</span> <span class="n">bool_val</span>
<span class="k">elif</span> <span class="ow">not</span> <span class="n">vals</span><span class="p">:</span>
<span class="k">elif</span> <span class="ow">not</span> <span class="n">vals</span> <span class="ow">and</span> <span class="n">field</span> <span class="o">!=</span> <span class="s2">"var"</span><span class="p">:</span>
<span class="c1"># don't assign default value if the template was variable assignment</span>
<span class="n">vals</span> <span class="o">=</span> <span class="n">default</span> <span class="ow">or</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">none_str</span><span class="p">]</span>
<span class="n">pre</span> <span class="o">=</span> <span class="n">ts</span><span class="o">.</span><span class="n">pre</span> <span class="ow">or</span> <span class="s2">""</span>
@@ -896,14 +905,103 @@
<span class="k">return</span> <span class="n">results</span><span class="p">,</span> <span class="n">unmatched</span>
<div class="viewcode-block" id="PhotoTemplate.expand_variables_to_str"><a class="viewcode-back" href="../../reference.html#osxphotos.PhotoTemplate.expand_variables_to_str">[docs]</a> <span class="k">def</span> <span class="nf">expand_variables_to_str</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">value</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">name</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
<span class="sd">"""</span>
<span class="sd"> Expand variables in value and return a str of the expanded value.</span>
<span class="sd"> Enforce that the expanded value is a single value, raises ValueError if not.</span>
<span class="sd"> Args:</span>
<span class="sd"> value: the value to expand</span>
<span class="sd"> name: the name of the value being expanded (used in error messages)</span>
<span class="sd"> """</span>
<span class="n">expanded</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">expand_variables</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">expanded</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">1</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">SyntaxError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s2"> must have a single value, not </span><span class="si">{</span><span class="n">expanded</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="k">return</span> <span class="n">expanded</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span></div>
<div class="viewcode-block" id="PhotoTemplate.expand_variables"><a class="viewcode-back" href="../../reference.html#osxphotos.PhotoTemplate.expand_variables">[docs]</a> <span class="k">def</span> <span class="nf">expand_variables</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">value</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">]:</span>
<span class="sd">"""Expand variables in value"""</span>
<span class="c1"># replace any variables with their values</span>
<span class="n">values</span> <span class="o">=</span> <span class="p">[</span><span class="n">value</span><span class="p">]</span>
<span class="n">new_values</span> <span class="o">=</span> <span class="p">[]</span>
<span class="c1"># allow %% to escape %, match variables in form %var</span>
<span class="n">variable_match</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="sa">r</span><span class="s2">"(?:</span><span class="si">%%</span><span class="s2">)*(%[\w]+)?"</span><span class="p">)</span>
<span class="k">while</span> <span class="kc">True</span><span class="p">:</span>
<span class="k">for</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">values</span><span class="p">:</span>
<span class="n">match</span> <span class="o">=</span> <span class="n">variable_match</span><span class="o">.</span><span class="n">search</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">match</span> <span class="ow">or</span> <span class="ow">not</span> <span class="n">match</span><span class="p">[</span><span class="mi">1</span><span class="p">]:</span>
<span class="k">break</span>
<span class="n">var</span> <span class="o">=</span> <span class="n">match</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
<span class="n">var_name</span> <span class="o">=</span> <span class="n">var</span><span class="p">[</span><span class="mi">1</span><span class="p">:]</span>
<span class="k">if</span> <span class="n">var_name</span> <span class="ow">not</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">variables</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">SyntaxError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Variable '</span><span class="si">{</span><span class="n">var_name</span><span class="si">}</span><span class="s2">' is not defined."</span><span class="p">)</span>
<span class="k">for</span> <span class="n">val</span> <span class="ow">in</span> <span class="n">values</span><span class="p">:</span>
<span class="k">for</span> <span class="n">var_val</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">variables</span><span class="p">[</span><span class="n">var_name</span><span class="p">]:</span>
<span class="n">new_values</span><span class="o">.</span><span class="n">append</span><span class="p">(</span>
<span class="n">re</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="sa">f</span><span class="s2">"(%%)*</span><span class="si">{</span><span class="n">var</span><span class="si">}</span><span class="s2">"</span><span class="p">,</span> <span class="sa">r</span><span class="s2">"\g&lt;1&gt;"</span> <span class="o">+</span> <span class="n">var_val</span><span class="p">,</span> <span class="n">val</span><span class="p">)</span>
<span class="p">)</span>
<span class="k">if</span> <span class="n">new_values</span> <span class="o">==</span> <span class="n">values</span> <span class="ow">or</span> <span class="ow">not</span> <span class="n">new_values</span><span class="p">:</span>
<span class="k">break</span>
<span class="n">values</span> <span class="o">=</span> <span class="n">new_values</span><span class="o">.</span><span class="n">copy</span><span class="p">()</span>
<span class="n">new_values</span> <span class="o">=</span> <span class="p">[]</span>
<span class="c1"># replace %% with %</span>
<span class="c1"># any %% left in the string will be replaced with %</span>
<span class="n">values</span> <span class="o">=</span> <span class="p">[</span><span class="n">value</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">"</span><span class="si">%%</span><span class="s2">"</span><span class="p">,</span> <span class="s2">"%"</span><span class="p">)</span> <span class="k">for</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span>
<span class="k">return</span> <span class="n">values</span></div>
<div class="viewcode-block" id="PhotoTemplate.get_field_values"><a class="viewcode-back" href="../../reference.html#osxphotos.PhotoTemplate.get_field_values">[docs]</a> <span class="k">def</span> <span class="nf">get_field_values</span><span class="p">(</span>
<span class="bp">self</span><span class="p">,</span>
<span class="n">field</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
<span class="n">subfield</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">],</span>
<span class="n">field_arg</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">],</span>
<span class="n">default</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">],</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Tuple</span><span class="p">[</span><span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">],</span> <span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">]]:</span>
<span class="sd">"""Get the values for a field"""</span>
<span class="n">vals</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">unmatched</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">if</span> <span class="p">(</span>
<span class="n">field</span> <span class="ow">in</span> <span class="n">SINGLE_VALUE_SUBSTITUTIONS</span>
<span class="ow">or</span> <span class="n">field</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">"."</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span> <span class="ow">in</span> <span class="n">SINGLE_VALUE_SUBSTITUTIONS</span>
<span class="p">):</span>
<span class="n">vals</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_template_value</span><span class="p">(</span>
<span class="n">field</span><span class="p">,</span>
<span class="n">default</span><span class="o">=</span><span class="n">default</span><span class="p">,</span>
<span class="n">subfield</span><span class="o">=</span><span class="n">subfield</span><span class="p">,</span>
<span class="n">field_arg</span><span class="o">=</span><span class="n">field_arg</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">elif</span> <span class="n">field</span> <span class="o">==</span> <span class="s2">"exiftool"</span><span class="p">:</span>
<span class="k">if</span> <span class="n">subfield</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span>
<span class="s2">"SyntaxError: GROUP:NAME subfield must not be null with {exiftool:GROUP:NAME}'"</span>
<span class="p">)</span>
<span class="n">vals</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_template_value_exiftool</span><span class="p">(</span>
<span class="n">subfield</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">elif</span> <span class="n">field</span> <span class="o">==</span> <span class="s2">"function"</span><span class="p">:</span>
<span class="k">if</span> <span class="n">subfield</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span>
<span class="s2">"SyntaxError: filename and function must not be null with {function::filename.py:function_name}"</span>
<span class="p">)</span>
<span class="n">vals</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_template_value_function</span><span class="p">(</span><span class="n">subfield</span><span class="p">,</span> <span class="n">field_arg</span><span class="p">)</span>
<span class="k">elif</span> <span class="n">field</span> <span class="ow">in</span> <span class="n">MULTI_VALUE_SUBSTITUTIONS</span> <span class="ow">or</span> <span class="n">field</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">"photo"</span><span class="p">):</span>
<span class="n">vals</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_template_value_multi</span><span class="p">(</span>
<span class="n">field</span><span class="p">,</span> <span class="n">subfield</span><span class="p">,</span> <span class="n">path_sep</span><span class="o">=</span><span class="n">field_arg</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="n">default</span>
<span class="p">)</span>
<span class="k">elif</span> <span class="n">field</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">"."</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span> <span class="ow">in</span> <span class="n">PATHLIB_SUBSTITUTIONS</span><span class="p">:</span>
<span class="n">vals</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_template_value_pathlib</span><span class="p">(</span><span class="n">field</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">unmatched</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">field</span><span class="p">)</span>
<span class="k">return</span> <span class="p">[],</span> <span class="n">unmatched</span>
<span class="k">return</span> <span class="n">vals</span><span class="p">,</span> <span class="n">unmatched</span></div>
<div class="viewcode-block" id="PhotoTemplate.get_template_value"><a class="viewcode-back" href="../../reference.html#osxphotos.PhotoTemplate.get_template_value">[docs]</a> <span class="k">def</span> <span class="nf">get_template_value</span><span class="p">(</span>
<span class="bp">self</span><span class="p">,</span>
<span class="n">field</span><span class="p">,</span>
<span class="n">default</span><span class="p">,</span>
<span class="n">subfield</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="c1"># bool_val=None,</span>
<span class="c1"># delim=None,</span>
<span class="c1"># path_sep=None,</span>
<span class="n">subfield</span><span class="p">,</span>
<span class="n">field_arg</span><span class="p">,</span>
<span class="p">):</span>
<span class="sd">"""lookup value for template field (single-value template substitutions)</span>
@@ -1187,6 +1285,8 @@
<span class="n">value</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="o">.</span><span class="n">moment_info</span><span class="o">.</span><span class="n">title</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="o">.</span><span class="n">moment_info</span> <span class="k">else</span> <span class="kc">None</span>
<span class="k">elif</span> <span class="n">field</span> <span class="o">==</span> <span class="s2">"uuid"</span><span class="p">:</span>
<span class="n">value</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="o">.</span><span class="n">uuid</span>
<span class="k">elif</span> <span class="n">field</span> <span class="o">==</span> <span class="s2">"shortuuid"</span><span class="p">:</span>
<span class="n">value</span> <span class="o">=</span> <span class="n">uuid_to_shortuuid</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="o">.</span><span class="n">uuid</span><span class="p">)</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="o">.</span><span class="n">uuid</span> <span class="k">else</span> <span class="kc">None</span>
<span class="k">elif</span> <span class="n">field</span> <span class="o">==</span> <span class="s2">"id"</span><span class="p">:</span>
<span class="n">value</span> <span class="o">=</span> <span class="n">format_str_value</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"pk"</span><span class="p">],</span> <span class="n">subfield</span><span class="p">)</span>
<span class="k">elif</span> <span class="n">field</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">"album_seq"</span><span class="p">)</span> <span class="ow">or</span> <span class="n">field</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">"folder_album_seq"</span><span class="p">):</span>
@@ -1200,9 +1300,8 @@
<span class="k">else</span><span class="p">:</span>
<span class="n">value</span> <span class="o">=</span> <span class="kc">None</span>
<span class="k">if</span> <span class="n">value</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
<span class="k">with</span> <span class="n">suppress</span><span class="p">(</span><span class="ne">IndexError</span><span class="p">):</span>
<span class="n">start_id</span> <span class="o">=</span> <span class="n">field</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">"."</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
<span class="n">value</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="o">+</span> <span class="nb">int</span><span class="p">(</span><span class="n">start_id</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span>
<span class="n">start_id</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">field_arg</span><span class="p">)</span> <span class="k">if</span> <span class="n">field_arg</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="k">else</span> <span class="mi">0</span>
<span class="n">value</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="o">+</span> <span class="n">start_id</span>
<span class="n">value</span> <span class="o">=</span> <span class="n">format_str_value</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">subfield</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="c1"># if here, didn't get a match</span>
@@ -1250,57 +1349,124 @@
<span class="k">return</span> <span class="p">[</span><span class="n">value</span><span class="p">]</span></div>
<span class="k">def</span> <span class="nf">get_template_value_filter</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">filter_</span><span class="p">,</span> <span class="n">values</span><span class="p">):</span>
<div class="viewcode-block" id="PhotoTemplate.get_filter_values"><a class="viewcode-back" href="../../reference.html#osxphotos.PhotoTemplate.get_filter_values">[docs]</a> <span class="k">def</span> <span class="nf">get_filter_values</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">filter_</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">values</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">]:</span>
<span class="sd">"""Return filtered values"""</span>
<span class="c1"># extract args, if any</span>
<span class="k">if</span> <span class="n">re</span><span class="o">.</span><span class="n">search</span><span class="p">(</span><span class="sa">r</span><span class="s2">"\(.*\)"</span><span class="p">,</span> <span class="n">filter_</span><span class="p">):</span>
<span class="n">filter_</span><span class="p">,</span> <span class="n">args</span> <span class="o">=</span> <span class="n">filter_</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">"("</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
<span class="n">args</span> <span class="o">=</span> <span class="n">args</span><span class="o">.</span><span class="n">rstrip</span><span class="p">(</span><span class="s2">")"</span><span class="p">)</span>
<span class="n">args</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">expand_variables_to_str</span><span class="p">(</span><span class="n">args</span><span class="p">,</span> <span class="s2">"Filter arguments"</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">args</span> <span class="o">=</span> <span class="kc">None</span>
<span class="c1"># check that filter name (without subfields or arguments) is valid</span>
<span class="n">valid_filters</span> <span class="o">=</span> <span class="p">[</span><span class="n">f</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">"("</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span> <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">FILTER_VALUES</span><span class="p">]</span>
<span class="k">if</span> <span class="n">filter_</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">":"</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">valid_filters</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">SyntaxError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Unknown filter: </span><span class="si">{</span><span class="n">filter_</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="k">if</span> <span class="n">filter_</span> <span class="ow">in</span> <span class="p">[</span>
<span class="s2">"split"</span><span class="p">,</span>
<span class="s2">"chop"</span><span class="p">,</span>
<span class="s2">"chomp"</span><span class="p">,</span>
<span class="s2">"append"</span><span class="p">,</span>
<span class="s2">"prepend"</span><span class="p">,</span>
<span class="s2">"remove"</span><span class="p">,</span>
<span class="s2">"slice"</span><span class="p">,</span>
<span class="s2">"sslice"</span><span class="p">,</span>
<span class="p">]</span> <span class="ow">and</span> <span class="p">(</span><span class="n">args</span> <span class="ow">is</span> <span class="kc">None</span> <span class="ow">or</span> <span class="ow">not</span> <span class="nb">len</span><span class="p">(</span><span class="n">args</span><span class="p">)):</span>
<span class="k">raise</span> <span class="ne">SyntaxError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">filter_</span><span class="si">}</span><span class="s2"> requires arguments"</span><span class="p">)</span>
<span class="k">if</span> <span class="n">filter_</span> <span class="o">==</span> <span class="s2">"lower"</span><span class="p">:</span>
<span class="k">if</span> <span class="n">values</span> <span class="ow">and</span> <span class="nb">type</span><span class="p">(</span><span class="n">values</span><span class="p">)</span> <span class="o">==</span> <span class="nb">list</span><span class="p">:</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="n">v</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="n">values</span><span class="o">.</span><span class="n">lower</span><span class="p">()]</span> <span class="k">if</span> <span class="n">values</span> <span class="k">else</span> <span class="p">[]</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="n">v</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span>
<span class="k">elif</span> <span class="n">filter_</span> <span class="o">==</span> <span class="s2">"upper"</span><span class="p">:</span>
<span class="k">if</span> <span class="n">values</span> <span class="ow">and</span> <span class="nb">type</span><span class="p">(</span><span class="n">values</span><span class="p">)</span> <span class="o">==</span> <span class="nb">list</span><span class="p">:</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="n">v</span><span class="o">.</span><span class="n">upper</span><span class="p">()</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="n">values</span><span class="o">.</span><span class="n">upper</span><span class="p">()]</span> <span class="k">if</span> <span class="n">values</span> <span class="k">else</span> <span class="p">[]</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="n">v</span><span class="o">.</span><span class="n">upper</span><span class="p">()</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span>
<span class="k">elif</span> <span class="n">filter_</span> <span class="o">==</span> <span class="s2">"strip"</span><span class="p">:</span>
<span class="k">if</span> <span class="n">values</span> <span class="ow">and</span> <span class="nb">type</span><span class="p">(</span><span class="n">values</span><span class="p">)</span> <span class="o">==</span> <span class="nb">list</span><span class="p">:</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="n">v</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="n">values</span><span class="o">.</span><span class="n">strip</span><span class="p">()]</span> <span class="k">if</span> <span class="n">values</span> <span class="k">else</span> <span class="p">[]</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="n">v</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span>
<span class="k">elif</span> <span class="n">filter_</span> <span class="o">==</span> <span class="s2">"capitalize"</span><span class="p">:</span>
<span class="k">if</span> <span class="n">values</span> <span class="ow">and</span> <span class="nb">type</span><span class="p">(</span><span class="n">values</span><span class="p">)</span> <span class="o">==</span> <span class="nb">list</span><span class="p">:</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="n">v</span><span class="o">.</span><span class="n">capitalize</span><span class="p">()</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="n">values</span><span class="o">.</span><span class="n">capitalize</span><span class="p">()]</span> <span class="k">if</span> <span class="n">values</span> <span class="k">else</span> <span class="p">[]</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="n">v</span><span class="o">.</span><span class="n">capitalize</span><span class="p">()</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span>
<span class="k">elif</span> <span class="n">filter_</span> <span class="o">==</span> <span class="s2">"titlecase"</span><span class="p">:</span>
<span class="k">if</span> <span class="n">values</span> <span class="ow">and</span> <span class="nb">type</span><span class="p">(</span><span class="n">values</span><span class="p">)</span> <span class="o">==</span> <span class="nb">list</span><span class="p">:</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="n">v</span><span class="o">.</span><span class="n">title</span><span class="p">()</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="n">values</span><span class="o">.</span><span class="n">title</span><span class="p">()]</span> <span class="k">if</span> <span class="n">values</span> <span class="k">else</span> <span class="p">[]</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="n">v</span><span class="o">.</span><span class="n">title</span><span class="p">()</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span>
<span class="k">elif</span> <span class="n">filter_</span> <span class="o">==</span> <span class="s2">"braces"</span><span class="p">:</span>
<span class="k">if</span> <span class="n">values</span> <span class="ow">and</span> <span class="nb">type</span><span class="p">(</span><span class="n">values</span><span class="p">)</span> <span class="o">==</span> <span class="nb">list</span><span class="p">:</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"{"</span> <span class="o">+</span> <span class="n">v</span> <span class="o">+</span> <span class="s2">"}"</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"{"</span> <span class="o">+</span> <span class="n">values</span> <span class="o">+</span> <span class="s2">"}"</span><span class="p">]</span> <span class="k">if</span> <span class="n">values</span> <span class="k">else</span> <span class="p">[]</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"{"</span> <span class="o">+</span> <span class="n">v</span> <span class="o">+</span> <span class="s2">"}"</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span>
<span class="k">elif</span> <span class="n">filter_</span> <span class="o">==</span> <span class="s2">"parens"</span><span class="p">:</span>
<span class="k">if</span> <span class="n">values</span> <span class="ow">and</span> <span class="nb">type</span><span class="p">(</span><span class="n">values</span><span class="p">)</span> <span class="o">==</span> <span class="nb">list</span><span class="p">:</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="sa">f</span><span class="s2">"(</span><span class="si">{</span><span class="n">v</span><span class="si">}</span><span class="s2">)"</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="sa">f</span><span class="s2">"(</span><span class="si">{</span><span class="n">values</span><span class="si">}</span><span class="s2">)"</span><span class="p">]</span> <span class="k">if</span> <span class="n">values</span> <span class="k">else</span> <span class="p">[]</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"("</span> <span class="o">+</span> <span class="n">v</span> <span class="o">+</span> <span class="s2">")"</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span>
<span class="k">elif</span> <span class="n">filter_</span> <span class="o">==</span> <span class="s2">"brackets"</span><span class="p">:</span>
<span class="k">if</span> <span class="n">values</span> <span class="ow">and</span> <span class="nb">type</span><span class="p">(</span><span class="n">values</span><span class="p">)</span> <span class="o">==</span> <span class="nb">list</span><span class="p">:</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="sa">f</span><span class="s2">"[</span><span class="si">{</span><span class="n">v</span><span class="si">}</span><span class="s2">]"</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="sa">f</span><span class="s2">"[</span><span class="si">{</span><span class="n">values</span><span class="si">}</span><span class="s2">]"</span><span class="p">]</span> <span class="k">if</span> <span class="n">values</span> <span class="k">else</span> <span class="p">[]</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"["</span> <span class="o">+</span> <span class="n">v</span> <span class="o">+</span> <span class="s2">"]"</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span>
<span class="k">elif</span> <span class="n">filter_</span> <span class="o">==</span> <span class="s2">"shell_quote"</span><span class="p">:</span>
<span class="k">if</span> <span class="n">values</span> <span class="ow">and</span> <span class="nb">type</span><span class="p">(</span><span class="n">values</span><span class="p">)</span> <span class="o">==</span> <span class="nb">list</span><span class="p">:</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="n">shlex</span><span class="o">.</span><span class="n">quote</span><span class="p">(</span><span class="n">v</span><span class="p">)</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="n">shlex</span><span class="o">.</span><span class="n">quote</span><span class="p">(</span><span class="n">v</span><span class="p">)</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span>
<span class="k">elif</span> <span class="n">filter_</span> <span class="o">==</span> <span class="s2">"split"</span><span class="p">:</span>
<span class="c1"># split on delimiter</span>
<span class="n">delim</span> <span class="o">=</span> <span class="n">args</span>
<span class="k">if</span> <span class="n">delim</span><span class="p">:</span>
<span class="n">new_values</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">:</span>
<span class="n">new_values</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">v</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="n">delim</span><span class="p">))</span>
<span class="n">value</span> <span class="o">=</span> <span class="n">new_values</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="n">shlex</span><span class="o">.</span><span class="n">quote</span><span class="p">(</span><span class="n">values</span><span class="p">)]</span> <span class="k">if</span> <span class="n">values</span> <span class="k">else</span> <span class="p">[]</span>
<span class="n">value</span> <span class="o">=</span> <span class="n">values</span>
<span class="k">elif</span> <span class="n">filter_</span> <span class="o">==</span> <span class="s2">"chop"</span><span class="p">:</span>
<span class="c1"># chop off characters from the end</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">chop</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">args</span><span class="p">)</span>
<span class="k">except</span> <span class="ne">ValueError</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">SyntaxError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Invalid value for chop: </span><span class="si">{</span><span class="n">args</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="n">v</span><span class="p">[:</span><span class="o">-</span><span class="n">chop</span><span class="p">]</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span> <span class="k">if</span> <span class="n">chop</span> <span class="k">else</span> <span class="n">values</span>
<span class="k">elif</span> <span class="n">filter_</span> <span class="o">==</span> <span class="s2">"chomp"</span><span class="p">:</span>
<span class="c1"># chop off characters from the beginning</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">chomp</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">args</span><span class="p">)</span>
<span class="k">except</span> <span class="ne">ValueError</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">SyntaxError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Invalid value for chomp: </span><span class="si">{</span><span class="n">args</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="n">v</span><span class="p">[</span><span class="n">chomp</span><span class="p">:]</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span> <span class="k">if</span> <span class="n">chomp</span> <span class="k">else</span> <span class="n">values</span>
<span class="k">elif</span> <span class="n">filter_</span> <span class="o">==</span> <span class="s2">"autosplit"</span><span class="p">:</span>
<span class="c1"># try to split keyword strings automatically</span>
<span class="n">temp_values</span> <span class="o">=</span> <span class="p">[</span><span class="n">v</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">","</span><span class="p">,</span> <span class="s2">" "</span><span class="p">)</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span>
<span class="n">temp_values</span> <span class="o">=</span> <span class="p">[</span><span class="n">v</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">";"</span><span class="p">,</span> <span class="s2">" "</span><span class="p">)</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">temp_values</span><span class="p">]</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">val</span> <span class="ow">in</span> <span class="n">temp_values</span><span class="p">:</span>
<span class="n">value</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">val</span><span class="o">.</span><span class="n">split</span><span class="p">())</span>
<span class="k">elif</span> <span class="n">filter_</span> <span class="o">==</span> <span class="s2">"sort"</span><span class="p">:</span>
<span class="c1"># sort list of values</span>
<span class="n">value</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">values</span><span class="p">)</span>
<span class="k">elif</span> <span class="n">filter_</span> <span class="o">==</span> <span class="s2">"rsort"</span><span class="p">:</span>
<span class="c1"># reverse sort list of values</span>
<span class="n">value</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">values</span><span class="p">,</span> <span class="n">reverse</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="k">elif</span> <span class="n">filter_</span> <span class="o">==</span> <span class="s2">"reverse"</span><span class="p">:</span>
<span class="c1"># reverse list of values</span>
<span class="n">value</span> <span class="o">=</span> <span class="n">values</span><span class="p">[::</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span>
<span class="k">elif</span> <span class="n">filter_</span> <span class="o">==</span> <span class="s2">"uniq"</span><span class="p">:</span>
<span class="c1"># remove duplicate values from list</span>
<span class="n">temp_values</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">:</span>
<span class="k">if</span> <span class="n">v</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">temp_values</span><span class="p">:</span>
<span class="n">temp_values</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">v</span><span class="p">)</span>
<span class="n">value</span> <span class="o">=</span> <span class="n">temp_values</span>
<span class="k">elif</span> <span class="n">filter_</span> <span class="o">==</span> <span class="s2">"join"</span><span class="p">:</span>
<span class="c1"># join list of values with delimiter</span>
<span class="n">delim</span> <span class="o">=</span> <span class="n">args</span> <span class="ow">or</span> <span class="s2">""</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="n">delim</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">values</span><span class="p">)]</span>
<span class="k">elif</span> <span class="n">filter_</span> <span class="o">==</span> <span class="s2">"append"</span><span class="p">:</span>
<span class="c1"># append value to list</span>
<span class="n">value</span> <span class="o">=</span> <span class="n">values</span> <span class="o">+</span> <span class="p">[</span><span class="n">args</span><span class="p">]</span>
<span class="k">elif</span> <span class="n">filter_</span> <span class="o">==</span> <span class="s2">"prepend"</span><span class="p">:</span>
<span class="c1"># prepend value to list</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="n">args</span><span class="p">]</span> <span class="o">+</span> <span class="n">values</span>
<span class="k">elif</span> <span class="n">filter_</span> <span class="o">==</span> <span class="s2">"remove"</span><span class="p">:</span>
<span class="c1"># remove value from list</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="n">v</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span> <span class="k">if</span> <span class="n">v</span> <span class="o">!=</span> <span class="n">args</span><span class="p">]</span>
<span class="k">elif</span> <span class="n">filter_</span> <span class="o">==</span> <span class="s2">"slice"</span><span class="p">:</span>
<span class="c1"># slice list of values</span>
<span class="n">value</span> <span class="o">=</span> <span class="n">values</span><span class="p">[</span><span class="n">create_slice</span><span class="p">(</span><span class="n">args</span><span class="p">)]</span>
<span class="k">elif</span> <span class="n">filter_</span> <span class="o">==</span> <span class="s2">"sslice"</span><span class="p">:</span>
<span class="c1"># slice each value in a list</span>
<span class="n">slice_</span> <span class="o">=</span> <span class="n">create_slice</span><span class="p">(</span><span class="n">args</span><span class="p">)</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="n">v</span><span class="p">[</span><span class="n">slice_</span><span class="p">]</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span>
<span class="k">elif</span> <span class="n">filter_</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">"function:"</span><span class="p">):</span>
<span class="n">value</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_template_value_filter_function</span><span class="p">(</span><span class="n">filter_</span><span class="p">,</span> <span class="n">values</span><span class="p">)</span>
<span class="n">value</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_template_value_filter_function</span><span class="p">(</span><span class="n">filter_</span><span class="p">,</span> <span class="n">args</span><span class="p">,</span> <span class="n">values</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">value</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">return</span> <span class="n">value</span>
<span class="k">return</span> <span class="n">value</span></div>
<div class="viewcode-block" id="PhotoTemplate.get_template_value_multi"><a class="viewcode-back" href="../../reference.html#osxphotos.PhotoTemplate.get_template_value_multi">[docs]</a> <span class="k">def</span> <span class="nf">get_template_value_multi</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">field</span><span class="p">,</span> <span class="n">subfield</span><span class="p">,</span> <span class="n">path_sep</span><span class="p">,</span> <span class="n">default</span><span class="p">):</span>
<span class="sd">"""lookup value for template field (multi-value template substitutions)</span>
@@ -1320,6 +1486,8 @@
<span class="sd">""" return list of values for a multi-valued template field """</span>
<span class="n">path_sep</span> <span class="o">=</span> <span class="n">path_sep</span> <span class="ow">or</span> <span class="bp">self</span><span class="o">.</span><span class="n">path_sep</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="o">.</span><span class="n">uuid</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="k">return</span> <span class="p">[]</span>
@@ -1385,6 +1553,8 @@
<span class="n">values</span> <span class="o">=</span> <span class="p">[</span><span class="n">shlex</span><span class="o">.</span><span class="n">quote</span><span class="p">(</span><span class="n">v</span><span class="p">)</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">default</span> <span class="k">if</span> <span class="n">v</span><span class="p">]</span>
<span class="k">elif</span> <span class="n">field</span> <span class="o">==</span> <span class="s2">"strip"</span><span class="p">:</span>
<span class="n">values</span> <span class="o">=</span> <span class="p">[</span><span class="n">v</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">default</span><span class="p">]</span>
<span class="k">elif</span> <span class="n">field</span> <span class="o">==</span> <span class="s2">"format"</span><span class="p">:</span>
<span class="n">values</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_format_values</span><span class="p">(</span><span class="n">field</span><span class="p">,</span> <span class="n">subfield</span><span class="p">,</span> <span class="n">default</span><span class="p">)</span>
<span class="k">elif</span> <span class="n">field</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">"photo"</span><span class="p">):</span>
<span class="c1"># provide access to PhotoInfo object</span>
<span class="n">properties</span> <span class="o">=</span> <span class="n">field</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">"."</span><span class="p">)</span>
@@ -1399,10 +1569,11 @@
<span class="n">obj</span> <span class="o">=</span> <span class="nb">getattr</span><span class="p">(</span><span class="n">obj</span><span class="p">,</span> <span class="n">property_</span><span class="p">)</span>
<span class="k">if</span> <span class="n">obj</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="k">break</span>
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
<span class="k">except</span> <span class="ne">AttributeError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span>
<span class="s2">"Invalid property for </span><span class="si">{photo}</span><span class="s2"> template: "</span> <span class="o">+</span> <span class="sa">f</span><span class="s2">"'</span><span class="si">{</span><span class="n">property_</span><span class="si">}</span><span class="s2">'"</span>
<span class="p">)</span>
<span class="p">)</span> <span class="kn">from</span> <span class="nn">e</span>
<span class="k">if</span> <span class="n">obj</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">values</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">elif</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">obj</span><span class="p">,</span> <span class="nb">bool</span><span class="p">):</span>
@@ -1427,6 +1598,31 @@
<span class="n">values</span> <span class="o">=</span> <span class="n">values</span> <span class="ow">or</span> <span class="p">[]</span>
<span class="k">return</span> <span class="n">values</span></div>
<div class="viewcode-block" id="PhotoTemplate.get_format_values"><a class="viewcode-back" href="../../reference.html#osxphotos.PhotoTemplate.get_format_values">[docs]</a> <span class="k">def</span> <span class="nf">get_format_values</span><span class="p">(</span>
<span class="bp">self</span><span class="p">,</span> <span class="n">field</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">subfield</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">default</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Optional</span><span class="p">[</span><span class="n">List</span><span class="p">[</span><span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]]]:</span>
<span class="sd">"""Return values for {format} templates"""</span>
<span class="k">if</span> <span class="n">field</span> <span class="o">!=</span> <span class="s2">"format"</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Unhandled template value in get_format_values: </span><span class="si">{</span><span class="n">field</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">subfield</span> <span class="ow">or</span> <span class="s2">":"</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">subfield</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">SyntaxError</span><span class="p">(</span><span class="s2">"</span><span class="si">{format}</span><span class="s2"> requires subfield in form TYPE:FORMAT"</span><span class="p">)</span>
<span class="n">type_</span><span class="p">,</span> <span class="n">format_str</span> <span class="o">=</span> <span class="n">subfield</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">":"</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
<span class="k">if</span> <span class="n">type_</span> <span class="ow">not</span> <span class="ow">in</span> <span class="p">(</span><span class="s2">"int"</span><span class="p">,</span> <span class="s2">"float"</span><span class="p">,</span> <span class="s2">"str"</span><span class="p">):</span>
<span class="k">raise</span> <span class="ne">SyntaxError</span><span class="p">(</span>
<span class="sa">f</span><span class="s2">"'</span><span class="si">{</span><span class="n">type_</span><span class="si">}</span><span class="s2">' is not a valid type for </span><span class="si">{</span><span class="nb">format</span><span class="si">}</span><span class="s2">: must be one of 'int', 'float', 'str'"</span>
<span class="p">)</span>
<span class="k">if</span> <span class="n">type_</span> <span class="o">==</span> <span class="s2">"int"</span><span class="p">:</span>
<span class="c1"># convert to float then int to avoid error when converting a string float to int</span>
<span class="n">default_</span> <span class="o">=</span> <span class="p">[</span><span class="nb">int</span><span class="p">(</span><span class="nb">float</span><span class="p">(</span><span class="n">v</span><span class="p">))</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">default</span><span class="p">]</span>
<span class="k">elif</span> <span class="n">type_</span> <span class="o">==</span> <span class="s2">"float"</span><span class="p">:</span>
<span class="n">default_</span> <span class="o">=</span> <span class="p">[</span><span class="nb">float</span><span class="p">(</span><span class="n">v</span><span class="p">)</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">default</span><span class="p">]</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">default_</span> <span class="o">=</span> <span class="n">default</span>
<span class="n">format_str</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">expand_variables_to_str</span><span class="p">(</span><span class="n">format_str</span><span class="p">,</span> <span class="s2">"format string"</span><span class="p">)</span>
<span class="k">return</span> <span class="p">[</span><span class="n">format_str_value</span><span class="p">(</span><span class="n">v</span><span class="p">,</span> <span class="n">format_str</span><span class="p">)</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">default_</span><span class="p">]</span></div>
<div class="viewcode-block" id="PhotoTemplate.get_template_value_exiftool"><a class="viewcode-back" href="../../reference.html#osxphotos.PhotoTemplate.get_template_value_exiftool">[docs]</a> <span class="k">def</span> <span class="nf">get_template_value_exiftool</span><span class="p">(</span>
<span class="bp">self</span><span class="p">,</span>
<span class="n">subfield</span><span class="p">,</span>
@@ -1460,6 +1656,7 @@
<div class="viewcode-block" id="PhotoTemplate.get_template_value_function"><a class="viewcode-back" href="../../reference.html#osxphotos.PhotoTemplate.get_template_value_function">[docs]</a> <span class="k">def</span> <span class="nf">get_template_value_function</span><span class="p">(</span>
<span class="bp">self</span><span class="p">,</span>
<span class="n">subfield</span><span class="p">,</span>
<span class="n">field_arg</span><span class="p">,</span>
<span class="p">):</span>
<span class="sd">"""Get template value from external function"""</span>
@@ -1475,7 +1672,13 @@
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"'</span><span class="si">{</span><span class="n">filename</span><span class="si">}</span><span class="s2">' does not appear to be a file"</span><span class="p">)</span>
<span class="n">template_func</span> <span class="o">=</span> <span class="n">load_function</span><span class="p">(</span><span class="n">filename_validated</span><span class="p">,</span> <span class="n">funcname</span><span class="p">)</span>
<span class="n">values</span> <span class="o">=</span> <span class="n">template_func</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="p">,</span> <span class="n">options</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="p">)</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="o">.</span><span class="n">uuid</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="c1"># must be a PhotoInfoNone instance</span>
<span class="c1"># if no uuid, then template is being validated but not actually run</span>
<span class="c1"># so don't run the function</span>
<span class="n">values</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">values</span> <span class="o">=</span> <span class="n">template_func</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="p">,</span> <span class="n">options</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">options</span><span class="p">,</span> <span class="n">args</span><span class="o">=</span><span class="n">field_arg</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">values</span><span class="p">,</span> <span class="p">(</span><span class="nb">str</span><span class="p">,</span> <span class="nb">list</span><span class="p">)):</span>
<span class="k">raise</span> <span class="ne">TypeError</span><span class="p">(</span>
@@ -1493,8 +1696,9 @@
<span class="k">return</span> <span class="n">values</span></div>
<div class="viewcode-block" id="PhotoTemplate.get_template_value_filter_function"><a class="viewcode-back" href="../../reference.html#osxphotos.PhotoTemplate.get_template_value_filter_function">[docs]</a> <span class="k">def</span> <span class="nf">get_template_value_filter_function</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">filter_</span><span class="p">,</span> <span class="n">values</span><span class="p">):</span>
<div class="viewcode-block" id="PhotoTemplate.get_template_value_filter_function"><a class="viewcode-back" href="../../reference.html#osxphotos.PhotoTemplate.get_template_value_filter_function">[docs]</a> <span class="k">def</span> <span class="nf">get_template_value_filter_function</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">filter_</span><span class="p">,</span> <span class="n">args</span><span class="p">,</span> <span class="n">values</span><span class="p">):</span>
<span class="sd">"""Filter template value from external function"""</span>
<span class="c1"># TODO: add args to filter function call? Would change signature of function</span>
<span class="n">filter_</span> <span class="o">=</span> <span class="n">filter_</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">"function:"</span><span class="p">,</span> <span class="s2">""</span><span class="p">)</span>
@@ -1513,7 +1717,11 @@
<span class="k">if</span> <span class="ow">not</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">values</span><span class="p">,</span> <span class="p">(</span><span class="nb">list</span><span class="p">,</span> <span class="nb">tuple</span><span class="p">)):</span>
<span class="n">values</span> <span class="o">=</span> <span class="p">[</span><span class="n">values</span><span class="p">]</span>
<span class="n">values</span> <span class="o">=</span> <span class="n">template_func</span><span class="p">(</span><span class="n">values</span><span class="p">)</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="o">.</span><span class="n">uuid</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
<span class="c1"># if uuid is None, it's a PhotoInfoNone instance and template is being validated</span>
<span class="c1"># so don't run the function</span>
<span class="n">values</span> <span class="o">=</span> <span class="n">template_func</span><span class="p">(</span><span class="n">values</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">values</span><span class="p">,</span> <span class="nb">list</span><span class="p">):</span>
<span class="k">raise</span> <span class="ne">TypeError</span><span class="p">(</span>
@@ -1525,10 +1733,7 @@
<div class="viewcode-block" id="PhotoTemplate.get_photo_video_type"><a class="viewcode-back" href="../../reference.html#osxphotos.PhotoTemplate.get_photo_video_type">[docs]</a> <span class="k">def</span> <span class="nf">get_photo_video_type</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">default</span><span class="p">):</span>
<span class="sd">"""return media type, e.g. photo or video"""</span>
<span class="n">default_dict</span> <span class="o">=</span> <span class="n">parse_default_kv</span><span class="p">(</span><span class="n">default</span><span class="p">,</span> <span class="n">PHOTO_VIDEO_TYPE_DEFAULTS</span><span class="p">)</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="o">.</span><span class="n">isphoto</span><span class="p">:</span>
<span class="k">return</span> <span class="n">default_dict</span><span class="p">[</span><span class="s2">"photo"</span><span class="p">]</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="n">default_dict</span><span class="p">[</span><span class="s2">"video"</span><span class="p">]</span></div>
<span class="k">return</span> <span class="n">default_dict</span><span class="p">[</span><span class="s2">"photo"</span><span class="p">]</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="o">.</span><span class="n">isphoto</span> <span class="k">else</span> <span class="n">default_dict</span><span class="p">[</span><span class="s2">"video"</span><span class="p">]</span></div>
<div class="viewcode-block" id="PhotoTemplate.get_media_type"><a class="viewcode-back" href="../../reference.html#osxphotos.PhotoTemplate.get_media_type">[docs]</a> <span class="k">def</span> <span class="nf">get_media_type</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">default</span><span class="p">):</span>
<span class="sd">"""return special media type, e.g. slow_mo, panorama, etc., defaults to photo or video if no special type"""</span>
@@ -1555,13 +1760,9 @@
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="n">default_dict</span><span class="p">[</span><span class="s2">"photo"</span><span class="p">]</span></div>
<span class="k">def</span> <span class="nf">get_photo_bool_attribute</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">attr</span><span class="p">,</span> <span class="n">default</span><span class="p">,</span> <span class="n">bool_val</span><span class="p">):</span>
<span class="c1"># get value for a PhotoInfo bool attribute</span>
<span class="n">val</span> <span class="o">=</span> <span class="nb">getattr</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="p">,</span> <span class="n">attr</span><span class="p">)</span>
<span class="k">if</span> <span class="n">val</span><span class="p">:</span>
<span class="k">return</span> <span class="n">bool_val</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="n">default</span></div>
<div class="viewcode-block" id="PhotoTemplate.get_photo_bool_attribute"><a class="viewcode-back" href="../../reference.html#osxphotos.PhotoTemplate.get_photo_bool_attribute">[docs]</a> <span class="k">def</span> <span class="nf">get_photo_bool_attribute</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">attr</span><span class="p">,</span> <span class="n">default</span><span class="p">,</span> <span class="n">bool_val</span><span class="p">):</span>
<span class="sd">"""Return the boolean value for a photo attribute"""</span>
<span class="k">return</span> <span class="n">bool_val</span> <span class="k">if</span> <span class="p">(</span><span class="n">val</span> <span class="o">:=</span> <span class="nb">getattr</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="p">,</span> <span class="n">attr</span><span class="p">))</span> <span class="k">else</span> <span class="n">default</span></div></div>
<span class="k">def</span> <span class="nf">parse_default_kv</span><span class="p">(</span><span class="n">default</span><span class="p">,</span> <span class="n">default_dict</span><span class="p">):</span>
@@ -1685,6 +1886,28 @@
<span class="c1"># so the first time this gets called is slow but repeated accesses are fast</span>
<span class="n">detected_text</span> <span class="o">=</span> <span class="n">photo</span><span class="o">.</span><span class="n">_detected_text</span><span class="p">()</span>
<span class="k">return</span> <span class="p">[</span><span class="n">text</span> <span class="k">for</span> <span class="n">text</span><span class="p">,</span> <span class="n">conf</span> <span class="ow">in</span> <span class="n">detected_text</span> <span class="k">if</span> <span class="n">conf</span> <span class="o">&gt;=</span> <span class="n">confidence</span><span class="p">]</span>
<span class="k">def</span> <span class="nf">create_slice</span><span class="p">(</span><span class="n">args</span><span class="p">):</span>
<span class="sd">"""Create a slice object from a string of args in form "start:end:step" """</span>
<span class="n">slice_args</span> <span class="o">=</span> <span class="n">args</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">":"</span><span class="p">)</span>
<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">slice_args</span><span class="p">)</span> <span class="o">==</span> <span class="mi">1</span><span class="p">:</span>
<span class="n">start</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">slice_args</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="ow">or</span> <span class="mi">0</span><span class="p">)</span>
<span class="n">end</span> <span class="o">=</span> <span class="kc">None</span>
<span class="n">step</span> <span class="o">=</span> <span class="kc">None</span>
<span class="k">elif</span> <span class="nb">len</span><span class="p">(</span><span class="n">slice_args</span><span class="p">)</span> <span class="o">==</span> <span class="mi">2</span><span class="p">:</span>
<span class="n">start</span><span class="p">,</span> <span class="n">end</span> <span class="o">=</span> <span class="n">slice_args</span>
<span class="n">start</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">start</span><span class="p">)</span> <span class="k">if</span> <span class="n">start</span> <span class="o">!=</span> <span class="s2">""</span> <span class="k">else</span> <span class="kc">None</span>
<span class="n">end</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">end</span><span class="p">)</span> <span class="k">if</span> <span class="n">end</span> <span class="o">!=</span> <span class="s2">""</span> <span class="k">else</span> <span class="kc">None</span>
<span class="n">step</span> <span class="o">=</span> <span class="kc">None</span>
<span class="k">elif</span> <span class="nb">len</span><span class="p">(</span><span class="n">slice_args</span><span class="p">)</span> <span class="o">==</span> <span class="mi">3</span><span class="p">:</span>
<span class="n">start</span><span class="p">,</span> <span class="n">end</span><span class="p">,</span> <span class="n">step</span> <span class="o">=</span> <span class="n">slice_args</span>
<span class="n">start</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">start</span><span class="p">)</span> <span class="k">if</span> <span class="n">start</span> <span class="o">!=</span> <span class="s2">""</span> <span class="k">else</span> <span class="kc">None</span>
<span class="n">end</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">end</span><span class="p">)</span> <span class="k">if</span> <span class="n">end</span> <span class="o">!=</span> <span class="s2">""</span> <span class="k">else</span> <span class="kc">None</span>
<span class="n">step</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">step</span><span class="p">)</span> <span class="k">if</span> <span class="n">step</span> <span class="o">!=</span> <span class="s2">""</span> <span class="k">else</span> <span class="kc">None</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">SyntaxError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Invalid slice: </span><span class="si">{</span><span class="n">args</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="k">return</span> <span class="nb">slice</span><span class="p">(</span><span class="n">start</span><span class="p">,</span> <span class="n">end</span><span class="p">,</span> <span class="n">step</span><span class="p">)</span>
</pre></div>
</article>
</div>

View File

@@ -8,7 +8,7 @@ In its simplest form, a template statement has the form: ``"{template_field}"``\
Template statements may contain one or more modifiers. The full syntax is:
``"pretext{delim+template_field:subfield|filter(path_sep)[find,replace] conditional?bool_value,default}posttext"``
``"pretext{delim+template_field:subfield(field_arg)|filter[find,replace] conditional?bool_value,default}posttext"``
Template statements are white-space sensitive meaning that white space (spaces, tabs) changes the meaning of the template statement.
@@ -30,6 +30,8 @@ e.g. if Photo keywords are ``["foo","bar"]``\ :
`:subfield`: Some templates have sub-fields, For example, `{exiftool:IPTC:Make}\ ``; the template_field is``\ exiftool\ ``and the sub-field is``\ IPTC:Make`.
``(field_arg)``\ : optional arguments to pass to the field; for example, with ``{folder_album}`` this is used to pass the path separator used for joining folders and albums when rendering the field (default is "/" for ``{folder_album}``\ ).
`|filter`: You may optionally append one or more filter commands to the end of the template field using the vertical pipe ('|') symbol. Filters may be combined, separated by '|' as in: ``{keyword|capitalize|parens}``.
Valid filters are:
@@ -45,6 +47,20 @@ Valid filters are:
* ``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().
e.g. if Photo keywords are ``["FOO","bar"]``\ :
@@ -59,8 +75,6 @@ e.g. if Photo description is "my description":
* ``"{descr|titlecase}"`` renders to: ``"My Description"``
``(path_sep)``\ : optional path separator to use when joining path-like fields, for example ``{folder_album}``. Default is "/".
e.g. If Photo is in ``Album1`` in ``Folder1``\ :
@@ -106,7 +120,7 @@ This can be used to rename files as well, for example:
This renames any photo that is a favorite as 'Favorite-ImageName.jpg' (where 'ImageName.jpg' is the original name of the photo) and all other photos with the unmodified original name.
``?bool_value``\ : Template fields may be evaluated as boolean (True/False) by appending "?" after the field name (and following "(path_sep)" or "[find/replace]". If a field is True (e.g. photo is HDR and field is ``"{hdr}"``\ ) or has any value, the value following the "?" will be used to render the template instead of the actual field value. If the template field evaluates to False (e.g. in above example, photo is not HDR) or has no value (e.g. photo has no title and field is ``"{title}"``\ ) then the default value following a "," will be used.
``?bool_value``\ : Template fields may be evaluated as boolean (True/False) by appending "?" after the field name (and following "(field_arg)" or "[find/replace]". If a field is True (e.g. photo is HDR and field is ``"{hdr}"``\ ) or has any value, the value following the "?" will be used to render the template instead of the actual field value. If the template field evaluates to False (e.g. in above example, photo is not HDR) or has no value (e.g. photo has no title and field is ``"{title}"``\ ) then the default value following a "," will be used.
e.g. if photo is an HDR image,
@@ -141,6 +155,16 @@ If you want to include "{" or "}" in the output, use "{openbrace}" or "{closebra
e.g. ``"{created.year}/{openbrace}{title}{closebrace}"`` would result in ``"2020/{Photo Title}"``.
**Variables**
You can define variables for later use in the template string using the format ``{var:NAME,VALUE}``. Variables may then be referenced using the format ``%NAME``. For example: ``{var:foo,bar}`` defines the variable ``%foo`` to have value ``bar``. This can be useful if you want to re-use a complex template value in multiple places within your template string or for allowing the use of characters that would otherwise be prohibited in a template string. For example, the "pipe" (\ ``|``\ ) character is not allowed in a find/replace pair but you can get around this limitation like so: ``{var:pipe,{pipe}}{title[-,%pipe]}`` which replaces the ``-`` character with ``|`` (the value of ``%pipe``\ ).
Variables can also be referenced as fields in the template string, for example: ``{var:year,created.year}{original_name}-{%year}``. In some cases, use of variables can make your template string more readable. Variables can be used as template fields, as values for filters, as values for conditional operations, or as default values. When used as a conditional value or default value, variables should be treated like any other field and enclosed in braces as conditional and default values are evaluated as template strings. For example: ``{var:name,Katie}{person contains {%name}?{%name},Not-{%name}}``.
If you need to use a ``%`` (percent sign character), you can escape the percent sign by using ``%%``. You can also use the ``{percent}`` template field where a template field is required. For example:
``{title[:,%%]}`` replaces the ``:`` with ``%`` and ``{title contains Foo?{title}{percent},{title}}`` adds ``%`` to the title if it contains ``Foo``.
Template Substitutions
----------------------
@@ -285,12 +309,14 @@ Template Substitutions
- The moment title of the photo
* - {uuid}
- Photo's internal universally unique identifier (UUID) for the photo, a 36-character string unique to the photo, e.g. '128FB4C6-0B16-4E7D-9108-FB2E90DA1546'
* - {shortuuid}
- A shorter representation of photo's internal universally unique identifier (UUID) for the photo, a 22-character string unique to the photo, e.g. 'JYsxugP9UjetmCbBCHXcmu'
* - {id}
- A unique number for the photo based on its primary key in the Photos database. A sequential integer, e.g. 1, 2, 3...etc. Each asset associated with a photo (e.g. an image and Live Photo preview) will share the same id. May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use '{id:05d}' which results in 00001, 00002, 00003...etc.
* - {album_seq}
- An integer, starting at 0, indicating the photo's index (sequence) in the containing album. Only valid when used in a '--filename' template and only when '{album}' or '{folder_album}' is used in the '--directory' template. For example '--directory "{folder_album}" --filename "{album\ *seq}*\ {original_name}"'. To start counting at a value other than 0, append append a period and the starting value to the field name. For example, to start counting at 1 instead of 0: '{album_seq.1}'. May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use '{album_seq:05d}' which results in 00000, 00001, 00002...etc. This may result in incorrect sequences if you have duplicate albums with the same name; see also '{folder_album_seq}'.
- An integer, starting at 0, indicating the photo's index (sequence) in the containing album. Only valid when used in a '--filename' template and only when '{album}' or '{folder_album}' is used in the '--directory' template. For example '--directory "{folder_album}" --filename "{album\ *seq}*\ {original_name}"'. To start counting at a value other than 0, append append '(starting_value)' to the field name. For example, to start counting at 1 instead of 0: '{album_seq(1)}'. May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use '{album_seq:05d}' which results in 00000, 00001, 00002...etc. To format while also using a starting value: '{album_seq:05d(1)}' which results in 0001, 00002...etc.This may result in incorrect sequences if you have duplicate albums with the same name; see also '{folder_album_seq}'.
* - {folder_album_seq}
- An integer, starting at 0, indicating the photo's index (sequence) in the containing album and folder path. Only valid when used in a '--filename' template and only when '{folder_album}' is used in the '--directory' template. For example '--directory "{folder_album}" --filename "{folder_album\ *seq}*\ {original_name}"'. To start counting at a value other than 0, append append a period and the starting value to the field name. For example, to start counting at 1 instead of 0: '{folder_album_seq.1}' May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use '{folder_album_seq:05d}' which results in 00000, 00001, 00002...etc. This may result in incorrect sequences if you have duplicate albums with the same name in the same folder; see also '{album_seq}'.
- An integer, starting at 0, indicating the photo's index (sequence) in the containing album and folder path. Only valid when used in a '--filename' template and only when '{folder_album}' is used in the '--directory' template. For example '--directory "{folder_album}" --filename "{folder_album\ *seq}*\ {original_name}"'. To start counting at a value other than 0, append '(starting_value)' to the field name. For example, to start counting at 1 instead of 0: '{folder_album_seq(1)}' May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use '{folder_album_seq:05d}' which results in 00000, 00001, 00002...etc. To format while also using a starting value: '{folder_album_seq:05d(1)}' which results in 0001, 00002...etc.This may result in incorrect sequences if you have duplicate albums with the same name in the same folder; see also '{album_seq}'.
* - {comma}
- A comma: ','
* - {semicolon}
@@ -320,7 +346,7 @@ Template Substitutions
* - {crlf}
- a carriage return + line feed: '\r\n'
* - {osxphotos_version}
- The osxphotos version, e.g. '0.49.2'
- The osxphotos version, e.g. '0.50.4'
* - {osxphotos_cmd_line}
- The full command line used to run osxphotos
* - {album}
@@ -361,6 +387,8 @@ Template Substitutions
- 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.

View File

@@ -218,6 +218,15 @@ A powerful feature of Photos is that it uses machine learning algorithms to auto
``osxphotos export /path/to/export --exiftool --keyword-template "{label}"``
Removing a keyword during export
--------------------------------
If some of your photos contain a keyword you do not want to be added to the exported file with ``--exiftool``\ , you can use the template system to remove the keyword from the exported file. For example, if you want to remove the keyword "MyKeyword" from all your photos:
``osxphotos export /path/to/export --exiftool --keyword-template "{keyword|remove(MyKeyword)}" --replace-keywords``
In this example, ``|remove(MyKeyword)`` is a filter which removes ``MyKeyword`` from the keyword list of every photo being processed. The ``--replace-keywords`` option instructs osxphotos to replace the keywords in the exported file with the filtered keywords from ``--keyword-template``.
**Note**\ : When evaluating templates for ``--directory`` and ``--filename``\ , osxphotos inserts the automatic default value "_" for any template field which is null (empty or blank). This is to ensure that there's never a null directory or filename created. For metadata templates such as ``--keyword-template``\ , osxphotos does not provide an automatic default value thus if the template field is null, no keyword would be created. Of course, you can provide a default value if desired and osxphotos will use this. For example, to add "nolabel" as a keyword for any photo that doesn't have labels:
``osxphotos export /path/to/export --exiftool --keyword-template "{label,nolabel}"``

View File

@@ -1,6 +1,6 @@
var DOCUMENTATION_OPTIONS = {
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
VERSION: '0.49.2',
VERSION: '0.50.4',
LANGUAGE: 'None',
COLLAPSE_INDEX: false,
BUILDER: 'html',

View File

@@ -6,7 +6,7 @@
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="OSXPhotos Template System" href="template_help.html" /><link rel="prev" title="OSXPhotos Tutorial" href="tutorial.html" />
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/>
<title>OSXPhotos Command Line Interface (CLI) - osxphotos 0.49.2 documentation</title>
<title>OSXPhotos Command Line Interface (CLI) - osxphotos 0.50.4 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
@@ -124,7 +124,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">osxphotos 0.49.2 documentation</div></a>
<a href="index.html"><div class="brand">osxphotos 0.50.4 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -147,7 +147,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
<span class="sidebar-brand-text">osxphotos 0.49.2 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.50.4 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
@@ -1278,6 +1278,26 @@ to modify this behavior.</p>
<dd><p>Print information about FILE_PATH contained in the database.</p>
</dd></dl>
<dl class="std option">
<dt class="sig sig-object std" id="cmdoption-osxphotos-exportdb-uuid-files">
<span class="sig-name descname"><span class="pre">--uuid-files</span></span><span class="sig-prename descclassname"> <span class="pre">&lt;UUID&gt;</span></span><a class="headerlink" href="#cmdoption-osxphotos-exportdb-uuid-files" title="Permalink to this definition">#</a></dt>
<dd><p>List exported files associated with UUID.</p>
</dd></dl>
<dl class="std option">
<dt class="sig sig-object std" id="cmdoption-osxphotos-exportdb-uuid-info">
<span class="sig-name descname"><span class="pre">--uuid-info</span></span><span class="sig-prename descclassname"> <span class="pre">&lt;UUID&gt;</span></span><a class="headerlink" href="#cmdoption-osxphotos-exportdb-uuid-info" title="Permalink to this definition">#</a></dt>
<dd><p>Print information about UUID contained in the database.</p>
</dd></dl>
<dl class="std option">
<dt class="sig sig-object std" id="cmdoption-osxphotos-exportdb-delete-uuid">
<span class="sig-name descname"><span class="pre">--delete-uuid</span></span><span class="sig-prename descclassname"> <span class="pre">&lt;UUID&gt;</span></span><a class="headerlink" href="#cmdoption-osxphotos-exportdb-delete-uuid" title="Permalink to this definition">#</a></dt>
<dd><p>Delete all data associated with UUID from the database.</p>
</dd></dl>
<dl class="std option">
<dt class="sig sig-object std" id="cmdoption-osxphotos-exportdb-delete-file">
<span class="sig-name descname"><span class="pre">--delete-file</span></span><span class="sig-prename descclassname"> <span class="pre">&lt;FILE_PATH&gt;</span></span><a class="headerlink" href="#cmdoption-osxphotos-exportdb-delete-file" title="Permalink to this definition">#</a></dt>
<dd><p>Delete all data associated with FILE_PATH from the database; does not delete the actual exported file if it exists, only the data in the database.</p>
</dd></dl>
<dl class="std option">
<dt class="sig sig-object std" id="cmdoption-osxphotos-exportdb-report">
<span class="sig-name descname"><span class="pre">--report</span></span><span class="sig-prename descclassname"> <span class="pre">&lt;REPORT_FILE</span> <span class="pre">RUN_ID&gt;</span></span><a class="headerlink" href="#cmdoption-osxphotos-exportdb-report" title="Permalink to this definition">#</a></dt>
<dd><p>Generate an export report as <cite>osxphotos export … report REPORT_FILE</cite> would have done. This allows you to re-create an export report if you didnt use the report option when running <cite>osxphotos export</cite>. The extension of the report file is used to determine the format. Valid extensions are: .csv (CSV file), .json (JSON), .db and .sqlite (SQLite database). RUN_ID may be any integer from -10 to 0 specifying which run to use. For example, <cite>report report.csv 0</cite> will generate a CSV report for the last run and <cite>report report.json -1</cite> will generate a JSON report for the second-to-last run (one run prior to last run). REPORT_FILE may be a template string (see Templating System), for example, report export_{today.date}.csv will write a CSV report file named with todays date. See also append.</p>
@@ -1361,6 +1381,43 @@ to modify this behavior.</p>
<dd><p>Optional argument(s)</p>
</dd></dl>
</section>
<section id="osxphotos-inspect">
<h3>inspect<a class="headerlink" href="#osxphotos-inspect" title="Permalink to this headline">#</a></h3>
<p>Interactively inspect photos selected in Photos.</p>
<p>Open Photos then run <cite>osxphotos inspect</cite> in the terminal.
As you select a photo in Photos, inspect will display metadata about the photo.
Press Ctrl+C to exit when done.
Works best with a modern terminal like iTerm2 or Kitty.</p>
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span>osxphotos inspect <span class="o">[</span>OPTIONS<span class="o">]</span>
</pre></div>
</div>
<p class="rubric">Options</p>
<dl class="std option">
<dt class="sig sig-object std" id="cmdoption-osxphotos-inspect-t">
<span id="cmdoption-osxphotos-inspect-detect-text"></span><span class="sig-name descname"><span class="pre">-t</span></span><span class="sig-prename descclassname"></span><span class="sig-prename descclassname"><span class="pre">,</span> </span><span class="sig-name descname"><span class="pre">--detect-text</span></span><span class="sig-prename descclassname"></span><a class="headerlink" href="#cmdoption-osxphotos-inspect-t" title="Permalink to this definition">#</a></dt>
<dd><p>Detect text in photos</p>
</dd></dl>
<dl class="std option">
<dt class="sig sig-object std" id="cmdoption-osxphotos-inspect-T">
<span id="cmdoption-osxphotos-inspect-template"></span><span class="sig-name descname"><span class="pre">-T</span></span><span class="sig-prename descclassname"></span><span class="sig-prename descclassname"><span class="pre">,</span> </span><span class="sig-name descname"><span class="pre">--template</span></span><span class="sig-prename descclassname"> <span class="pre">&lt;TEMPLATE&gt;</span></span><a class="headerlink" href="#cmdoption-osxphotos-inspect-T" title="Permalink to this definition">#</a></dt>
<dd><p>Template string to render for each photo using template preview mode. Useful for testing templates for export; may be repeated to test multiple templates. If template/-T is used, other inspection data will not be displayed.</p>
</dd></dl>
<dl class="std option">
<dt class="sig sig-object std" id="cmdoption-osxphotos-inspect-theme">
<span class="sig-name descname"><span class="pre">--theme</span></span><span class="sig-prename descclassname"> <span class="pre">&lt;THEME&gt;</span></span><a class="headerlink" href="#cmdoption-osxphotos-inspect-theme" title="Permalink to this definition">#</a></dt>
<dd><p>Specify the color theme to use for verbose output. Valid themes are dark, light, mono, and plain. Defaults to dark or light depending on system dark mode setting.</p>
<dl class="field-list simple">
<dt class="field-odd">Options</dt>
<dd class="field-odd"><p>dark | light | mono | plain</p>
</dd>
</dl>
</dd></dl>
<dl class="std option">
<dt class="sig sig-object std" id="cmdoption-osxphotos-inspect-db">
<span class="sig-name descname"><span class="pre">--db</span></span><span class="sig-prename descclassname"> <span class="pre">&lt;PHOTOS_LIBRARY_PATH&gt;</span></span><a class="headerlink" href="#cmdoption-osxphotos-inspect-db" title="Permalink to this definition">#</a></dt>
<dd><p>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</p>
</dd></dl>
</section>
<section id="osxphotos-install">
<h3>install<a class="headerlink" href="#osxphotos-install" title="Permalink to this headline">#</a></h3>
<p>Install Python packages into the same environment as osxphotos</p>
@@ -2705,6 +2762,7 @@ Commands:
<li><a class="reference internal" href="#osxphotos-exportdb">exportdb</a></li>
<li><a class="reference internal" href="#osxphotos-help">help</a></li>
<li><a class="reference internal" href="#osxphotos-info">info</a></li>
<li><a class="reference internal" href="#osxphotos-inspect">inspect</a></li>
<li><a class="reference internal" href="#osxphotos-install">install</a></li>
<li><a class="reference internal" href="#osxphotos-keywords">keywords</a></li>
<li><a class="reference internal" href="#osxphotos-labels">labels</a></li>

View File

@@ -4,7 +4,7 @@
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="#" /><link rel="search" title="Search" href="search.html" />
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/><title>Index - osxphotos 0.49.2 documentation</title>
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/><title>Index - osxphotos 0.50.4 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
@@ -122,7 +122,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">osxphotos 0.49.2 documentation</div></a>
<a href="index.html"><div class="brand">osxphotos 0.50.4 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -145,7 +145,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
<span class="sidebar-brand-text">osxphotos 0.49.2 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.50.4 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
@@ -402,6 +402,8 @@
<li><a href="cli.html#cmdoption-osxphotos-export-db">osxphotos-export command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-info-db">osxphotos-info command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-inspect-db">osxphotos-inspect command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-keywords-db">osxphotos-keywords command line option</a>
</li>
@@ -437,6 +439,20 @@
<ul>
<li><a href="cli.html#cmdoption-osxphotos-theme-delete">osxphotos-theme command line option</a>
</li>
</ul></li>
<li>
--delete-file
<ul>
<li><a href="cli.html#cmdoption-osxphotos-exportdb-delete-file">osxphotos-exportdb command line option</a>
</li>
</ul></li>
<li>
--delete-uuid
<ul>
<li><a href="cli.html#cmdoption-osxphotos-exportdb-delete-uuid">osxphotos-exportdb command line option</a>
</li>
</ul></li>
<li>
@@ -483,6 +499,13 @@
<li><a href="cli.html#cmdoption-osxphotos-exiftool-description-template">osxphotos-exiftool command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-description-template">osxphotos-export command line option</a>
</li>
</ul></li>
<li>
--detect-text
<ul>
<li><a href="cli.html#cmdoption-osxphotos-inspect-t">osxphotos-inspect command line option</a>
</li>
</ul></li>
<li>
@@ -1705,6 +1728,13 @@
<ul>
<li><a href="cli.html#cmdoption-osxphotos-diff-s">osxphotos-diff command line option</a>
</li>
</ul></li>
<li>
--template
<ul>
<li><a href="cli.html#cmdoption-osxphotos-inspect-T">osxphotos-inspect command line option</a>
</li>
</ul></li>
<li>
@@ -1714,6 +1744,8 @@
<li><a href="cli.html#cmdoption-osxphotos-exiftool-theme">osxphotos-exiftool command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-theme">osxphotos-export command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-inspect-theme">osxphotos-inspect command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-theme">osxphotos-timewarp command line option</a>
</li>
@@ -1872,6 +1904,13 @@
<li><a href="cli.html#cmdoption-osxphotos-query-uuid">osxphotos-query command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-repl-uuid">osxphotos-repl command line option</a>
</li>
</ul></li>
<li>
--uuid-files
<ul>
<li><a href="cli.html#cmdoption-osxphotos-exportdb-uuid-files">osxphotos-exportdb command line option</a>
</li>
</ul></li>
<li>
@@ -1883,6 +1922,13 @@
<li><a href="cli.html#cmdoption-osxphotos-query-uuid-from-file">osxphotos-query command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-repl-uuid-from-file">osxphotos-repl command line option</a>
</li>
</ul></li>
<li>
--uuid-info
<ul>
<li><a href="cli.html#cmdoption-osxphotos-exportdb-uuid-info">osxphotos-exportdb command line option</a>
</li>
</ul></li>
<li>
@@ -2065,6 +2111,8 @@
-T
<ul>
<li><a href="cli.html#cmdoption-osxphotos-inspect-T">osxphotos-inspect command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-T">osxphotos-timewarp command line option</a>
</li>
</ul></li>
@@ -2072,6 +2120,8 @@
-t
<ul>
<li><a href="cli.html#cmdoption-osxphotos-inspect-t">osxphotos-inspect command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-timewarp-t">osxphotos-timewarp command line option</a>
</li>
</ul></li>
@@ -2238,6 +2288,8 @@
<li><a href="reference.html#osxphotos.SearchInfo.city">city (osxphotos.SearchInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.ExportDB.close">close() (osxphotos.ExportDB method)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.cloud_metadata">cloud_metadata (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.QueryOptions.cloudasset">cloudasset (osxphotos.QueryOptions attribute)</a>
</li>
@@ -2290,10 +2342,14 @@
</li>
<li><a href="reference.html#osxphotos.PhotosDB.db_version">db_version (osxphotos.PhotosDB property)</a>
</li>
<li><a href="reference.html#osxphotos.QueryOptions.deleted">deleted (osxphotos.QueryOptions attribute)</a>
<li><a href="reference.html#osxphotos.ExportDB.delete_data_for_filepath">delete_data_for_filepath() (osxphotos.ExportDB method)</a>
</li>
<li><a href="reference.html#osxphotos.ExportDB.delete_data_for_uuid">delete_data_for_uuid() (osxphotos.ExportDB method)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.QueryOptions.deleted">deleted (osxphotos.QueryOptions attribute)</a>
</li>
<li><a href="reference.html#osxphotos.QueryOptions.deleted_only">deleted_only (osxphotos.QueryOptions attribute)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.description">description (osxphotos.PhotoInfo property)</a>
@@ -2357,14 +2413,18 @@
</li>
<li><a href="reference.html#osxphotos.PhotoExporter.exiftool_json_sidecar">exiftool_json_sidecar() (osxphotos.PhotoExporter method)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoTemplate.expand_variables">expand_variables() (osxphotos.PhotoTemplate method)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoTemplate.expand_variables_to_str">expand_variables_to_str() (osxphotos.PhotoTemplate method)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoExporter.export">export() (osxphotos.PhotoExporter method)</a>
<ul>
<li><a href="reference.html#osxphotos.PhotoInfo.export">(osxphotos.PhotoInfo method)</a>
</li>
</ul></li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.ExportOptions.export_as_hardlink">export_as_hardlink (osxphotos.ExportOptions attribute)</a>
</li>
<li>
@@ -2386,6 +2446,8 @@
</li>
</ul></li>
<li><a href="reference.html#osxphotos.ExportDB">ExportDB (class in osxphotos)</a>
</li>
<li><a href="reference.html#osxphotos.ExportDBTemp">ExportDBTemp (class in osxphotos)</a>
</li>
<li><a href="reference.html#osxphotos.ExportOptions">ExportOptions (class in osxphotos)</a>
</li>
@@ -2462,20 +2524,32 @@
<li><a href="reference.html#osxphotos.ExportDB.get_export_results">get_export_results() (osxphotos.ExportDB method)</a>
</li>
<li><a href="reference.html#osxphotos.ExportDB.get_exported_files">get_exported_files() (osxphotos.ExportDB method)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoTemplate.get_field_values">get_field_values() (osxphotos.PhotoTemplate method)</a>
</li>
<li><a href="reference.html#osxphotos.ExportDB.get_file_record">get_file_record() (osxphotos.ExportDB method)</a>
</li>
<li><a href="reference.html#osxphotos.ExportDB.get_files_for_uuid">get_files_for_uuid() (osxphotos.ExportDB method)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoTemplate.get_filter_values">get_filter_values() (osxphotos.PhotoTemplate method)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoTemplate.get_format_values">get_format_values() (osxphotos.PhotoTemplate method)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoTemplate.get_media_type">get_media_type() (osxphotos.PhotoTemplate method)</a>
</li>
<li><a href="reference.html#osxphotos.PhotosDB.get_photo">get_photo() (osxphotos.PhotosDB method)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoTemplate.get_photo_bool_attribute">get_photo_bool_attribute() (osxphotos.PhotoTemplate method)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoTemplate.get_photo_video_type">get_photo_video_type() (osxphotos.PhotoTemplate method)</a>
</li>
<li><a href="reference.html#osxphotos.ExportDB.get_photoinfo_for_uuid">get_photoinfo_for_uuid() (osxphotos.ExportDB method)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.ExportDB.get_previous_uuids">get_previous_uuids() (osxphotos.ExportDB method)</a>
</li>
<li><a href="reference.html#osxphotos.ExportDB.get_target_for_file">get_target_for_file() (osxphotos.ExportDB method)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoTemplate.get_template_value">get_template_value() (osxphotos.PhotoTemplate method)</a>
</li>
@@ -3228,6 +3302,10 @@
<li><a href="cli.html#cmdoption-osxphotos-exportdb-append">--append</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-exportdb-check-signatures">--check-signatures</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-exportdb-delete-file">--delete-file</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-exportdb-delete-uuid">--delete-uuid</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-exportdb-dry-run">--dry-run</a>
</li>
@@ -3248,6 +3326,10 @@
<li><a href="cli.html#cmdoption-osxphotos-exportdb-touch-file">--touch-file</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-exportdb-update-signatures">--update-signatures</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-exportdb-uuid-files">--uuid-files</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-exportdb-uuid-info">--uuid-info</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-exportdb-vacuum">--vacuum</a>
</li>
@@ -3278,6 +3360,23 @@
<li><a href="cli.html#cmdoption-osxphotos-info-json">--json</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-info-arg-PHOTOS_LIBRARY">PHOTOS_LIBRARY</a>
</li>
</ul></li>
<li>
osxphotos-inspect command line option
<ul>
<li><a href="cli.html#cmdoption-osxphotos-inspect-db">--db</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-inspect-t">--detect-text</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-inspect-T">--template</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-inspect-theme">--theme</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-inspect-t">-t</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-inspect-T">-T</a>
</li>
</ul></li>
<li>
@@ -3313,6 +3412,8 @@
<li><a href="cli.html#cmdoption-osxphotos-labels-arg-PHOTOS_LIBRARY">PHOTOS_LIBRARY</a>
</li>
</ul></li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li>
osxphotos-list command line option
@@ -3320,8 +3421,6 @@
<li><a href="cli.html#cmdoption-osxphotos-list-json">--json</a>
</li>
</ul></li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li>
osxphotos-persons command line option

View File

@@ -6,7 +6,7 @@
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="OSXPhotos" href="overview.html" />
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/>
<title>osxphotos 0.49.2 documentation</title>
<title>osxphotos 0.50.4 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
@@ -124,7 +124,7 @@
</label>
</div>
<div class="header-center">
<a href="#"><div class="brand">osxphotos 0.49.2 documentation</div></a>
<a href="#"><div class="brand">osxphotos 0.50.4 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -147,7 +147,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="#">
<span class="sidebar-brand-text">osxphotos 0.49.2 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.50.4 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
@@ -216,6 +216,7 @@
<li class="toctree-l2"><a class="reference internal" href="tutorial.html#missing-photos">Missing photos</a></li>
<li class="toctree-l2"><a class="reference internal" href="tutorial.html#exporting-to-external-disks">Exporting to external disks</a></li>
<li class="toctree-l2"><a class="reference internal" href="tutorial.html#exporting-metadata-with-exported-photos">Exporting metadata with exported photos</a></li>
<li class="toctree-l2"><a class="reference internal" href="tutorial.html#removing-a-keyword-during-export">Removing a keyword during export</a></li>
<li class="toctree-l2"><a class="reference internal" href="tutorial.html#sidecar-files">Sidecar files</a></li>
<li class="toctree-l2"><a class="reference internal" href="tutorial.html#updating-a-previous-export">Updating a previous export</a></li>
<li class="toctree-l2"><a class="reference internal" href="tutorial.html#dry-run">Dry Run</a></li>
@@ -242,6 +243,7 @@
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-exportdb">exportdb</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-help">help</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-info">info</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-inspect">inspect</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-install">install</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-keywords">keywords</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-labels">labels</a></li>

Binary file not shown.

View File

@@ -6,7 +6,7 @@
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="OSXPhotos Tutorial" href="tutorial.html" /><link rel="prev" title="Welcome to OSXPhotoss documentation!" href="index.html" />
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/>
<title>OSXPhotos - osxphotos 0.49.2 documentation</title>
<title>OSXPhotos - osxphotos 0.50.4 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
@@ -124,7 +124,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">osxphotos 0.49.2 documentation</div></a>
<a href="index.html"><div class="brand">osxphotos 0.50.4 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -147,7 +147,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
<span class="sidebar-brand-text">osxphotos 0.49.2 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.50.4 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">

View File

@@ -6,7 +6,7 @@
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="OSXPhotos python API" href="reference.html" /><link rel="prev" title="OSXPhotos Template System" href="template_help.html" />
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/>
<title>OSXPhotos Python Package Overview - osxphotos 0.49.2 documentation</title>
<title>OSXPhotos Python Package Overview - osxphotos 0.50.4 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
@@ -124,7 +124,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">osxphotos 0.49.2 documentation</div></a>
<a href="index.html"><div class="brand">osxphotos 0.50.4 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -147,7 +147,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
<span class="sidebar-brand-text">osxphotos 0.49.2 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.50.4 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">

View File

@@ -4,7 +4,7 @@
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" />
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/><title>Python Module Index - osxphotos 0.49.2 documentation</title>
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/><title>Python Module Index - osxphotos 0.50.4 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
@@ -122,7 +122,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">osxphotos 0.49.2 documentation</div></a>
<a href="index.html"><div class="brand">osxphotos 0.50.4 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -145,7 +145,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
<span class="sidebar-brand-text">osxphotos 0.49.2 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.50.4 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">

File diff suppressed because one or more lines are too long

View File

@@ -4,7 +4,7 @@
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="#" />
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/><title>Search - osxphotos 0.49.2 documentation</title><link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/><title>Search - osxphotos 0.50.4 documentation</title><link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?digest=30d1aed668e5c3a91c3e3bf6a60b675221979f0e" />
@@ -121,7 +121,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">osxphotos 0.49.2 documentation</div></a>
<a href="index.html"><div class="brand">osxphotos 0.50.4 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -144,7 +144,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
<span class="sidebar-brand-text">osxphotos 0.49.2 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.50.4 documentation</span>
</a><form class="sidebar-search-container" method="get" action="#" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,7 @@
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="OSXPhotos Python Package Overview" href="package_overview.html" /><link rel="prev" title="OSXPhotos Command Line Interface (CLI)" href="cli.html" />
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/>
<title>OSXPhotos Template System - osxphotos 0.49.2 documentation</title>
<title>OSXPhotos Template System - osxphotos 0.50.4 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
@@ -124,7 +124,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">osxphotos 0.49.2 documentation</div></a>
<a href="index.html"><div class="brand">osxphotos 0.50.4 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -147,7 +147,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
<span class="sidebar-brand-text">osxphotos 0.49.2 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.50.4 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
@@ -199,7 +199,7 @@
<p>The templating system converts one or template statements, written in osxphotos metadata templating language, to one or more rendered values using information from the photo being processed.</p>
<p>In its simplest form, a template statement has the form: <code class="docutils literal notranslate"><span class="pre">"{template_field}"</span></code>, for example <code class="docutils literal notranslate"><span class="pre">"{title}"</span></code> which would resolve to the title of the photo.</p>
<p>Template statements may contain one or more modifiers. The full syntax is:</p>
<p><code class="docutils literal notranslate"><span class="pre">"pretext{delim+template_field:subfield|filter(path_sep)[find,replace]</span> <span class="pre">conditional?bool_value,default}posttext"</span></code></p>
<p><code class="docutils literal notranslate"><span class="pre">"pretext{delim+template_field:subfield(field_arg)|filter[find,replace]</span> <span class="pre">conditional?bool_value,default}posttext"</span></code></p>
<p>Template statements are white-space sensitive meaning that white space (spaces, tabs) changes the meaning of the template statement.</p>
<p><code class="docutils literal notranslate"><span class="pre">pretext</span></code> and <code class="docutils literal notranslate"><span class="pre">posttext</span></code> are free form text. For example, if a photo has title “My Photo Title” the template statement <code class="docutils literal notranslate"><span class="pre">"The</span> <span class="pre">title</span> <span class="pre">of</span> <span class="pre">the</span> <span class="pre">photo</span> <span class="pre">is</span> <span class="pre">{title}"</span></code>, resolves to <code class="docutils literal notranslate"><span class="pre">"The</span> <span class="pre">title</span> <span class="pre">of</span> <span class="pre">the</span> <span class="pre">photo</span> <span class="pre">is</span> <span class="pre">My</span> <span class="pre">Photo</span> <span class="pre">Title"</span></code>. The <code class="docutils literal notranslate"><span class="pre">pretext</span></code> in this example is <code class="docutils literal notranslate"><span class="pre">"The</span> <span class="pre">title</span> <span class="pre">if</span> <span class="pre">the</span> <span class="pre">photo</span> <span class="pre">is</span> <span class="pre">"</span></code> and the template_field is <code class="docutils literal notranslate"><span class="pre">{title}</span></code>.</p>
<p><code class="docutils literal notranslate"><span class="pre">delim</span></code>: optional delimiter string to use when expanding multi-valued template values in-place</p>
@@ -213,6 +213,7 @@
</ul>
<p><code class="docutils literal notranslate"><span class="pre">template_field</span></code>: The template field to resolve. See <a class="reference external" href="#template-substitutions">Template Substitutions</a> for full list of template fields.</p>
<p><cite>:subfield</cite>: Some templates have sub-fields, For example, <cite>{exiftool:IPTC:Make}`</cite>; the template_field is``exiftool<code class="docutils literal notranslate"><span class="pre">and</span> <span class="pre">the</span> <span class="pre">sub-field</span> <span class="pre">is</span></code>IPTC:Make`.</p>
<p><code class="docutils literal notranslate"><span class="pre">(field_arg)</span></code>: optional arguments to pass to the field; for example, with <code class="docutils literal notranslate"><span class="pre">{folder_album}</span></code> this is used to pass the path separator used for joining folders and albums when rendering the field (default is “/” for <code class="docutils literal notranslate"><span class="pre">{folder_album}</span></code>).</p>
<p><cite>|filter</cite>: You may optionally append one or more filter commands to the end of the template field using the vertical pipe (|) symbol. Filters may be combined, separated by | as in: <code class="docutils literal notranslate"><span class="pre">{keyword|capitalize|parens}</span></code>.</p>
<p>Valid filters are:</p>
<ul class="simple">
@@ -226,6 +227,20 @@
<li><p><code class="docutils literal notranslate"><span class="pre">brackets</span></code>: Enclose value in brackets, e.g. value =&gt; [value]</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">shell_quote</span></code>: Quotes the value for safe usage in the shell, e.g. My file.jpeg =&gt; My file.jpeg; only adds quotes if needed.</p></li>
<li><p><cite>function</cite>: 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</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">split(x)</span></code>: Split value into a list of values using x as delimiter, e.g. value1;value2 =&gt; [value1, value2] if used with split(;).</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">autosplit</span></code>: Automatically split delimited string into separate values; will split strings delimited by comma, semicolon, or space, e.g. value1,value2 =&gt; [value1, value2].</p></li>
<li><p><cite>chop(x)</cite>: Remove x characters off the end of value, e.g. chop(1): Value =&gt; Valu; when applied to a list, chops characters from each list value, e.g. chop(1): [travel, beach]=&gt; [trave, beac].</p></li>
<li><p><cite>chomp(x)</cite>: Remove x characters from the beginning of value, e.g. chomp(1): [Value] =&gt; [alue]; when applied to a list, removes characters from each list value, e.g. chomp(1): [travel, beach]=&gt; [ravel, each].</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">sort</span></code>: Sort list of values, e.g. [c, b, a] =&gt; [a, b, c].</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">rsort</span></code>: Sort list of values in reverse order, e.g. [a, b, c] =&gt; [c, b, a].</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">reverse</span></code>: Reverse order of values, e.g. [a, b, c] =&gt; [c, b, a].</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">uniq</span></code>: Remove duplicate values, e.g. [a, b, c, b, a] =&gt; [a, b, c].</p></li>
<li><p><cite>join(x)</cite>: Join list of values with delimiter x, e.g. join(,): [a, b, c] =&gt; 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] =&gt; abc.</p></li>
<li><p><cite>append(x)</cite>: Append x to list of values, e.g. append(d): [a, b, c] =&gt; [a, b, c, d].</p></li>
<li><p><cite>prepend(x)</cite>: Prepend x to list of values, e.g. prepend(d): [a, b, c] =&gt; [d, a, b, c].</p></li>
<li><p><cite>remove(x)</cite>: Remove x from list of values, e.g. remove(b): [a, b, c] =&gt; [a, c].</p></li>
<li><p><cite>slice(start:stop:step)</cite>: Slice list using same semantics as Pythons list slicing, e.g. slice(1:3): [a, b, c, d] =&gt; [b, c]; slice(1:4:2): [a, b, c, d] =&gt; [b, d]; slice(1:): [a, b, c, d] =&gt; [b, c, d]; slice(:-1): [a, b, c, d] =&gt; [a, b, c]; slice(::-1): [a, b, c, d] =&gt; [d, c, b, a]. See also sslice().</p></li>
<li><p><cite>sslice(start:stop:step)</cite>: [s(tring) slice] Slice values in a list using same semantics as Pythons string slicing, e.g. sslice(1:3):abcd =&gt; bc; sslice(1:4:2): abcd =&gt; bd, etc. See also slice().</p></li>
</ul>
<p>e.g. if Photo keywords are <code class="docutils literal notranslate"><span class="pre">["FOO","bar"]</span></code>:</p>
<ul class="simple">
@@ -238,7 +253,6 @@
<ul class="simple">
<li><p><code class="docutils literal notranslate"><span class="pre">"{descr|titlecase}"</span></code> renders to: <code class="docutils literal notranslate"><span class="pre">"My</span> <span class="pre">Description"</span></code></p></li>
</ul>
<p><code class="docutils literal notranslate"><span class="pre">(path_sep)</span></code>: optional path separator to use when joining path-like fields, for example <code class="docutils literal notranslate"><span class="pre">{folder_album}</span></code>. Default is “/”.</p>
<p>e.g. If Photo is in <code class="docutils literal notranslate"><span class="pre">Album1</span></code> in <code class="docutils literal notranslate"><span class="pre">Folder1</span></code>:</p>
<ul class="simple">
<li><p><code class="docutils literal notranslate"><span class="pre">"{folder_album}"</span></code> renders to <code class="docutils literal notranslate"><span class="pre">["Folder1/Album1"]</span></code></p></li>
@@ -274,7 +288,7 @@
<p>This can be used to rename files as well, for example:
<code class="docutils literal notranslate"><span class="pre">--filename</span> <span class="pre">"{favorite?Favorite-{original_name},{original_name}}"</span></code></p>
<p>This renames any photo that is a favorite as Favorite-ImageName.jpg (where ImageName.jpg is the original name of the photo) and all other photos with the unmodified original name.</p>
<p><code class="docutils literal notranslate"><span class="pre">?bool_value</span></code>: Template fields may be evaluated as boolean (True/False) by appending “?” after the field name (and following “(path_sep)” or “[find/replace]”. If a field is True (e.g. photo is HDR and field is <code class="docutils literal notranslate"><span class="pre">"{hdr}"</span></code>) or has any value, the value following the “?” will be used to render the template instead of the actual field value. If the template field evaluates to False (e.g. in above example, photo is not HDR) or has no value (e.g. photo has no title and field is <code class="docutils literal notranslate"><span class="pre">"{title}"</span></code>) then the default value following a “,” will be used.</p>
<p><code class="docutils literal notranslate"><span class="pre">?bool_value</span></code>: Template fields may be evaluated as boolean (True/False) by appending “?” after the field name (and following “(field_arg)” or “[find/replace]”. If a field is True (e.g. photo is HDR and field is <code class="docutils literal notranslate"><span class="pre">"{hdr}"</span></code>) or has any value, the value following the “?” will be used to render the template instead of the actual field value. If the template field evaluates to False (e.g. in above example, photo is not HDR) or has no value (e.g. photo has no title and field is <code class="docutils literal notranslate"><span class="pre">"{title}"</span></code>) then the default value following a “,” will be used.</p>
<p>e.g. if photo is an HDR image,</p>
<ul class="simple">
<li><p><code class="docutils literal notranslate"><span class="pre">"{hdr?ISHDR,NOTHDR}"</span></code> renders to <code class="docutils literal notranslate"><span class="pre">"ISHDR"</span></code></p></li>
@@ -298,6 +312,11 @@
<p>Either or both bool_value or default (False value) may be empty which would result in empty string <code class="docutils literal notranslate"><span class="pre">""</span></code> when rendered.</p>
<p>If you want to include “{” or “}” in the output, use “{openbrace}” or “{closebrace}” template substitution.</p>
<p>e.g. <code class="docutils literal notranslate"><span class="pre">"{created.year}/{openbrace}{title}{closebrace}"</span></code> would result in <code class="docutils literal notranslate"><span class="pre">"2020/{Photo</span> <span class="pre">Title}"</span></code>.</p>
<p><strong>Variables</strong></p>
<p>You can define variables for later use in the template string using the format <code class="docutils literal notranslate"><span class="pre">{var:NAME,VALUE}</span></code>. Variables may then be referenced using the format <code class="docutils literal notranslate"><span class="pre">%NAME</span></code>. For example: <code class="docutils literal notranslate"><span class="pre">{var:foo,bar}</span></code> defines the variable <code class="docutils literal notranslate"><span class="pre">%foo</span></code> to have value <code class="docutils literal notranslate"><span class="pre">bar</span></code>. This can be useful if you want to re-use a complex template value in multiple places within your template string or for allowing the use of characters that would otherwise be prohibited in a template string. For example, the “pipe” (<code class="docutils literal notranslate"><span class="pre">|</span></code>) character is not allowed in a find/replace pair but you can get around this limitation like so: <code class="docutils literal notranslate"><span class="pre">{var:pipe,{pipe}}{title[-,%pipe]}</span></code> which replaces the <code class="docutils literal notranslate"><span class="pre">-</span></code> character with <code class="docutils literal notranslate"><span class="pre">|</span></code> (the value of <code class="docutils literal notranslate"><span class="pre">%pipe</span></code>).</p>
<p>Variables can also be referenced as fields in the template string, for example: <code class="docutils literal notranslate"><span class="pre">{var:year,created.year}{original_name}-{%year}</span></code>. In some cases, use of variables can make your template string more readable. Variables can be used as template fields, as values for filters, as values for conditional operations, or as default values. When used as a conditional value or default value, variables should be treated like any other field and enclosed in braces as conditional and default values are evaluated as template strings. For example: <code class="docutils literal notranslate"><span class="pre">{var:name,Katie}{person</span> <span class="pre">contains</span> <span class="pre">{%name}?{%name},Not-{%name}}</span></code>.</p>
<p>If you need to use a <code class="docutils literal notranslate"><span class="pre">%</span></code> (percent sign character), you can escape the percent sign by using <code class="docutils literal notranslate"><span class="pre">%%</span></code>. You can also use the <code class="docutils literal notranslate"><span class="pre">{percent}</span></code> template field where a template field is required. For example:</p>
<p><code class="docutils literal notranslate"><span class="pre">{title[:,%%]}</span></code> replaces the <code class="docutils literal notranslate"><span class="pre">:</span></code> with <code class="docutils literal notranslate"><span class="pre">%</span></code> and <code class="docutils literal notranslate"><span class="pre">{title</span> <span class="pre">contains</span> <span class="pre">Foo?{title}{percent},{title}}</span></code> adds <code class="docutils literal notranslate"><span class="pre">%</span></code> to the title if it contains <code class="docutils literal notranslate"><span class="pre">Foo</span></code>.</p>
<section id="id1">
<h2>Template Substitutions<a class="headerlink" href="#id1" title="Permalink to this headline">#</a></h2>
<div class="table-wrapper"><table class="docutils align-default">
@@ -515,120 +534,126 @@
<tr class="row-odd"><td><p>{uuid}</p></td>
<td><p>Photos internal universally unique identifier (UUID) for the photo, a 36-character string unique to the photo, e.g. 128FB4C6-0B16-4E7D-9108-FB2E90DA1546</p></td>
</tr>
<tr class="row-even"><td><p>{id}</p></td>
<tr class="row-even"><td><p>{shortuuid}</p></td>
<td><p>A shorter representation of photos internal universally unique identifier (UUID) for the photo, a 22-character string unique to the photo, e.g. JYsxugP9UjetmCbBCHXcmu</p></td>
</tr>
<tr class="row-odd"><td><p>{id}</p></td>
<td><p>A unique number for the photo based on its primary key in the Photos database. A sequential integer, e.g. 1, 2, 3…etc. Each asset associated with a photo (e.g. an image and Live Photo preview) will share the same id. May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use {id:05d} which results in 00001, 00002, 00003…etc.</p></td>
</tr>
<tr class="row-odd"><td><p>{album_seq}</p></td>
<td><p>An integer, starting at 0, indicating the photos index (sequence) in the containing album. Only valid when used in a filename template and only when {album} or {folder_album} is used in the directory template. For example directory “{folder_album}” filename “{album<em>seq}</em>{original_name}”’. To start counting at a value other than 0, append append a period and the starting value to the field name. For example, to start counting at 1 instead of 0: {album_seq.1}. May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use {album_seq:05d} which results in 00000, 00001, 00002…etc. This may result in incorrect sequences if you have duplicate albums with the same name; see also {folder_album_seq}.</p></td>
<tr class="row-even"><td><p>{album_seq}</p></td>
<td><p>An integer, starting at 0, indicating the photos index (sequence) in the containing album. Only valid when used in a filename template and only when {album} or {folder_album} is used in the directory template. For example directory “{folder_album}” filename “{album<em>seq}</em>{original_name}”’. To start counting at a value other than 0, append append (starting_value) to the field name. For example, to start counting at 1 instead of 0: {album_seq(1)}. May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use {album_seq:05d} which results in 00000, 00001, 00002…etc. To format while also using a starting value: {album_seq:05d(1)} which results in 0001, 00002…etc.This may result in incorrect sequences if you have duplicate albums with the same name; see also {folder_album_seq}.</p></td>
</tr>
<tr class="row-even"><td><p>{folder_album_seq}</p></td>
<td><p>An integer, starting at 0, indicating the photos index (sequence) in the containing album and folder path. Only valid when used in a filename template and only when {folder_album} is used in the directory template. For example directory “{folder_album}” filename “{folder_album<em>seq}</em>{original_name}”’. To start counting at a value other than 0, append append a period and the starting value to the field name. For example, to start counting at 1 instead of 0: {folder_album_seq.1} May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use {folder_album_seq:05d} which results in 00000, 00001, 00002…etc. This may result in incorrect sequences if you have duplicate albums with the same name in the same folder; see also {album_seq}.</p></td>
<tr class="row-odd"><td><p>{folder_album_seq}</p></td>
<td><p>An integer, starting at 0, indicating the photos index (sequence) in the containing album and folder path. Only valid when used in a filename template and only when {folder_album} is used in the directory template. For example directory “{folder_album}” filename “{folder_album<em>seq}</em>{original_name}”’. To start counting at a value other than 0, append (starting_value) to the field name. For example, to start counting at 1 instead of 0: {folder_album_seq(1)} May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use {folder_album_seq:05d} which results in 00000, 00001, 00002…etc. To format while also using a starting value: {folder_album_seq:05d(1)} which results in 0001, 00002…etc.This may result in incorrect sequences if you have duplicate albums with the same name in the same folder; see also {album_seq}.</p></td>
</tr>
<tr class="row-odd"><td><p>{comma}</p></td>
<tr class="row-even"><td><p>{comma}</p></td>
<td><p>A comma: ,</p></td>
</tr>
<tr class="row-even"><td><p>{semicolon}</p></td>
<tr class="row-odd"><td><p>{semicolon}</p></td>
<td><p>A semicolon: ;</p></td>
</tr>
<tr class="row-odd"><td><p>{questionmark}</p></td>
<tr class="row-even"><td><p>{questionmark}</p></td>
<td><p>A question mark: ?</p></td>
</tr>
<tr class="row-even"><td><p>{pipe}</p></td>
<tr class="row-odd"><td><p>{pipe}</p></td>
<td><p>A vertical pipe: |</p></td>
</tr>
<tr class="row-odd"><td><p>{openbrace}</p></td>
<tr class="row-even"><td><p>{openbrace}</p></td>
<td><p>An open brace: {</p></td>
</tr>
<tr class="row-even"><td><p>{closebrace}</p></td>
<tr class="row-odd"><td><p>{closebrace}</p></td>
<td><p>A close brace: }</p></td>
</tr>
<tr class="row-odd"><td><p>{openparens}</p></td>
<tr class="row-even"><td><p>{openparens}</p></td>
<td><p>An open parentheses: (</p></td>
</tr>
<tr class="row-even"><td><p>{closeparens}</p></td>
<tr class="row-odd"><td><p>{closeparens}</p></td>
<td><p>A close parentheses: )</p></td>
</tr>
<tr class="row-odd"><td><p>{openbracket}</p></td>
<tr class="row-even"><td><p>{openbracket}</p></td>
<td><p>An open bracket: [</p></td>
</tr>
<tr class="row-even"><td><p>{closebracket}</p></td>
<tr class="row-odd"><td><p>{closebracket}</p></td>
<td><p>A close bracket: ]</p></td>
</tr>
<tr class="row-odd"><td><p>{newline}</p></td>
<tr class="row-even"><td><p>{newline}</p></td>
<td><p>A newline: n</p></td>
</tr>
<tr class="row-even"><td><p>{lf}</p></td>
<tr class="row-odd"><td><p>{lf}</p></td>
<td><p>A line feed: n, alias for {newline}</p></td>
</tr>
<tr class="row-odd"><td><p>{cr}</p></td>
<tr class="row-even"><td><p>{cr}</p></td>
<td><p>A carriage return: r</p></td>
</tr>
<tr class="row-even"><td><p>{crlf}</p></td>
<tr class="row-odd"><td><p>{crlf}</p></td>
<td><p>a carriage return + line feed: rn</p></td>
</tr>
<tr class="row-odd"><td><p>{osxphotos_version}</p></td>
<td><p>The osxphotos version, e.g. 0.49.2</p></td>
<tr class="row-even"><td><p>{osxphotos_version}</p></td>
<td><p>The osxphotos version, e.g. 0.50.4</p></td>
</tr>
<tr class="row-even"><td><p>{osxphotos_cmd_line}</p></td>
<tr class="row-odd"><td><p>{osxphotos_cmd_line}</p></td>
<td><p>The full command line used to run osxphotos</p></td>
</tr>
<tr class="row-odd"><td><p>{album}</p></td>
<tr class="row-even"><td><p>{album}</p></td>
<td><p>Album(s) photo is contained in</p></td>
</tr>
<tr class="row-even"><td><p>{folder_album}</p></td>
<tr class="row-odd"><td><p>{folder_album}</p></td>
<td><p>Folder path + album photo is contained in. e.g. Folder/Subfolder/Album or just Album if no enclosing folder</p></td>
</tr>
<tr class="row-odd"><td><p>{project}</p></td>
<tr class="row-even"><td><p>{project}</p></td>
<td><p>Project(s) photo is contained in (such as greeting cards, calendars, slideshows)</p></td>
</tr>
<tr class="row-even"><td><p>{album_project}</p></td>
<tr class="row-odd"><td><p>{album_project}</p></td>
<td><p>Album(s) and project(s) photo is contained in; treats projects as regular albums</p></td>
</tr>
<tr class="row-odd"><td><p>{folder_album_project}</p></td>
<tr class="row-even"><td><p>{folder_album_project}</p></td>
<td><p>Folder path + album (includes projects as albums) photo is contained in. e.g. Folder/Subfolder/Album or just Album if no enclosing folder</p></td>
</tr>
<tr class="row-even"><td><p>{keyword}</p></td>
<tr class="row-odd"><td><p>{keyword}</p></td>
<td><p>Keyword(s) assigned to photo</p></td>
</tr>
<tr class="row-odd"><td><p>{person}</p></td>
<tr class="row-even"><td><p>{person}</p></td>
<td><p>Person(s) / face(s) in a photo</p></td>
</tr>
<tr class="row-even"><td><p>{label}</p></td>
<tr class="row-odd"><td><p>{label}</p></td>
<td><p>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.</p></td>
</tr>
<tr class="row-odd"><td><p>{label_normalized}</p></td>
<tr class="row-even"><td><p>{label_normalized}</p></td>
<td><p>All lower case version of label (Photos 5+ only)</p></td>
</tr>
<tr class="row-even"><td><p>{comment}</p></td>
<tr class="row-odd"><td><p>{comment}</p></td>
<td><p>Comment(s) on shared Photos; format is Person name: comment text (Photos 5+ only)</p></td>
</tr>
<tr class="row-odd"><td><p>{exiftool}</p></td>
<tr class="row-even"><td><p>{exiftool}</p></td>
<td><p>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 <code class="docutils literal notranslate"><span class="pre">exiftool</span> <span class="pre">-G</span></code>. exiftool must be installed in the path to use this template.</p></td>
</tr>
<tr class="row-even"><td><p>{searchinfo.holiday}</p></td>
<tr class="row-odd"><td><p>{searchinfo.holiday}</p></td>
<td><p>Holiday names associated with a photo, e.g. Christmas Day; (Photos 5+ only, applied automatically by Photos image categorization algorithms).</p></td>
</tr>
<tr class="row-odd"><td><p>{searchinfo.activity}</p></td>
<tr class="row-even"><td><p>{searchinfo.activity}</p></td>
<td><p>Activities associated with a photo, e.g. Sporting Event; (Photos 5+ only, applied automatically by Photos image categorization algorithms).</p></td>
</tr>
<tr class="row-even"><td><p>{searchinfo.venue}</p></td>
<tr class="row-odd"><td><p>{searchinfo.venue}</p></td>
<td><p>Venues associated with a photo, e.g. name of restaurant; (Photos 5+ only, applied automatically by Photos image categorization algorithms).</p></td>
</tr>
<tr class="row-odd"><td><p>{searchinfo.venue_type}</p></td>
<tr class="row-even"><td><p>{searchinfo.venue_type}</p></td>
<td><p>Venue types associated with a photo, e.g. Restaurant; (Photos 5+ only, applied automatically by Photos image categorization algorithms).</p></td>
</tr>
<tr class="row-even"><td><p>{photo}</p></td>
<tr class="row-odd"><td><p>{photo}</p></td>
<td><p>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 <a class="reference external" href="https://rhettbull.github.io/osxphotos/">https://rhettbull.github.io/osxphotos/</a> for additional documentation on the PhotoInfo class.</p></td>
</tr>
<tr class="row-odd"><td><p>{detected_text}</p></td>
<tr class="row-even"><td><p>{detected_text}</p></td>
<td><p>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.</p></td>
</tr>
<tr class="row-even"><td><p>{shell_quote}</p></td>
<tr class="row-odd"><td><p>{shell_quote}</p></td>
<td><p>Use in form {shell_quote,TEMPLATE}; quotes the rendered TEMPLATE value(s) for safe usage in the shell, e.g. My file.jpeg =&gt; My file.jpeg; only adds quotes if needed.</p></td>
</tr>
<tr class="row-odd"><td><p>{strip}</p></td>
<tr class="row-even"><td><p>{strip}</p></td>
<td><p>Use in form {strip,TEMPLATE}; strips whitespace from begining and end of rendered TEMPLATE value(s).</p></td>
</tr>
<tr class="row-odd"><td><p>{format}</p></td>
<td><p>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).</p></td>
</tr>
<tr class="row-even"><td><p>{function}</p></td>
<td><p>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.</p></td>
</tr>

View File

@@ -6,7 +6,7 @@
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="OSXPhotos Command Line Interface (CLI)" href="cli.html" /><link rel="prev" title="OSXPhotos" href="overview.html" />
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/>
<title>OSXPhotos Tutorial - osxphotos 0.49.2 documentation</title>
<title>OSXPhotos Tutorial - osxphotos 0.50.4 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
@@ -124,7 +124,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">osxphotos 0.49.2 documentation</div></a>
<a href="index.html"><div class="brand">osxphotos 0.50.4 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -147,7 +147,7 @@
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
<span class="sidebar-brand-text">osxphotos 0.49.2 documentation</span>
<span class="sidebar-brand-text">osxphotos 0.50.4 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
@@ -331,6 +331,12 @@
<p>The above command will write all the regular metadata that <code class="docutils literal notranslate"><span class="pre">--exiftool</span></code> normally writes to the file upon export but will also add an additional keyword in the exported metadata in the form “Folder1&gt;Folder2&gt;Album”. If you did not include the <code class="docutils literal notranslate"><span class="pre">(&gt;)</span></code> in the template string (e.g. <code class="docutils literal notranslate"><span class="pre">{folder_album}</span></code>), folder_album would render in form “Folder1/Folder2/Album”.</p>
<p>A powerful feature of Photos is that it uses machine learning algorithms to automatically classify or label photos. These labels are used when you search for images in Photos but are not otherwise available to the user. osxphotos is able to read all the labels associated with a photo and makes those available through the template system via the <code class="docutils literal notranslate"><span class="pre">{label}</span></code>. Think of these as automatic keywords as opposed to the keywords you assign manually in Photos. One common use case is to use the automatic labels to create new keywords when exporting images so that these labels are embedded in the images metadata:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--exiftool</span> <span class="pre">--keyword-template</span> <span class="pre">"{label}"</span></code></p>
</section>
<section id="removing-a-keyword-during-export">
<h2>Removing a keyword during export<a class="headerlink" href="#removing-a-keyword-during-export" title="Permalink to this headline">#</a></h2>
<p>If some of your photos contain a keyword you do not want to be added to the exported file with <code class="docutils literal notranslate"><span class="pre">--exiftool</span></code>, you can use the template system to remove the keyword from the exported file. For example, if you want to remove the keyword “MyKeyword” from all your photos:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--exiftool</span> <span class="pre">--keyword-template</span> <span class="pre">"{keyword|remove(MyKeyword)}"</span> <span class="pre">--replace-keywords</span></code></p>
<p>In this example, <code class="docutils literal notranslate"><span class="pre">|remove(MyKeyword)</span></code> is a filter which removes <code class="docutils literal notranslate"><span class="pre">MyKeyword</span></code> from the keyword list of every photo being processed. The <code class="docutils literal notranslate"><span class="pre">--replace-keywords</span></code> option instructs osxphotos to replace the keywords in the exported file with the filtered keywords from <code class="docutils literal notranslate"><span class="pre">--keyword-template</span></code>.</p>
<p><strong>Note</strong>: When evaluating templates for <code class="docutils literal notranslate"><span class="pre">--directory</span></code> and <code class="docutils literal notranslate"><span class="pre">--filename</span></code>, osxphotos inserts the automatic default value “_” for any template field which is null (empty or blank). This is to ensure that theres never a null directory or filename created. For metadata templates such as <code class="docutils literal notranslate"><span class="pre">--keyword-template</span></code>, osxphotos does not provide an automatic default value thus if the template field is null, no keyword would be created. Of course, you can provide a default value if desired and osxphotos will use this. For example, to add “nolabel” as a keyword for any photo that doesnt have labels:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">/path/to/export</span> <span class="pre">--exiftool</span> <span class="pre">--keyword-template</span> <span class="pre">"{label,nolabel}"</span></code></p>
</section>
@@ -568,6 +574,7 @@ template fields.
<li><a class="reference internal" href="#missing-photos">Missing photos</a></li>
<li><a class="reference internal" href="#exporting-to-external-disks">Exporting to external disks</a></li>
<li><a class="reference internal" href="#exporting-metadata-with-exported-photos">Exporting metadata with exported photos</a></li>
<li><a class="reference internal" href="#removing-a-keyword-during-export">Removing a keyword during export</a></li>
<li><a class="reference internal" href="#sidecar-files">Sidecar files</a></li>
<li><a class="reference internal" href="#updating-a-previous-export">Updating a previous export</a></li>
<li><a class="reference internal" href="#dry-run">Dry Run</a></li>

View File

@@ -8,7 +8,7 @@ In its simplest form, a template statement has the form: ``"{template_field}"``\
Template statements may contain one or more modifiers. The full syntax is:
``"pretext{delim+template_field:subfield|filter(path_sep)[find,replace] conditional?bool_value,default}posttext"``
``"pretext{delim+template_field:subfield(field_arg)|filter[find,replace] conditional?bool_value,default}posttext"``
Template statements are white-space sensitive meaning that white space (spaces, tabs) changes the meaning of the template statement.
@@ -30,6 +30,8 @@ e.g. if Photo keywords are ``["foo","bar"]``\ :
`:subfield`: Some templates have sub-fields, For example, `{exiftool:IPTC:Make}\ ``; the template_field is``\ exiftool\ ``and the sub-field is``\ IPTC:Make`.
``(field_arg)``\ : optional arguments to pass to the field; for example, with ``{folder_album}`` this is used to pass the path separator used for joining folders and albums when rendering the field (default is "/" for ``{folder_album}``\ ).
`|filter`: You may optionally append one or more filter commands to the end of the template field using the vertical pipe ('|') symbol. Filters may be combined, separated by '|' as in: ``{keyword|capitalize|parens}``.
Valid filters are:
@@ -45,6 +47,20 @@ Valid filters are:
* ``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().
e.g. if Photo keywords are ``["FOO","bar"]``\ :
@@ -59,8 +75,6 @@ e.g. if Photo description is "my description":
* ``"{descr|titlecase}"`` renders to: ``"My Description"``
``(path_sep)``\ : optional path separator to use when joining path-like fields, for example ``{folder_album}``. Default is "/".
e.g. If Photo is in ``Album1`` in ``Folder1``\ :
@@ -106,7 +120,7 @@ This can be used to rename files as well, for example:
This renames any photo that is a favorite as 'Favorite-ImageName.jpg' (where 'ImageName.jpg' is the original name of the photo) and all other photos with the unmodified original name.
``?bool_value``\ : Template fields may be evaluated as boolean (True/False) by appending "?" after the field name (and following "(path_sep)" or "[find/replace]". If a field is True (e.g. photo is HDR and field is ``"{hdr}"``\ ) or has any value, the value following the "?" will be used to render the template instead of the actual field value. If the template field evaluates to False (e.g. in above example, photo is not HDR) or has no value (e.g. photo has no title and field is ``"{title}"``\ ) then the default value following a "," will be used.
``?bool_value``\ : Template fields may be evaluated as boolean (True/False) by appending "?" after the field name (and following "(field_arg)" or "[find/replace]". If a field is True (e.g. photo is HDR and field is ``"{hdr}"``\ ) or has any value, the value following the "?" will be used to render the template instead of the actual field value. If the template field evaluates to False (e.g. in above example, photo is not HDR) or has no value (e.g. photo has no title and field is ``"{title}"``\ ) then the default value following a "," will be used.
e.g. if photo is an HDR image,
@@ -141,6 +155,16 @@ If you want to include "{" or "}" in the output, use "{openbrace}" or "{closebra
e.g. ``"{created.year}/{openbrace}{title}{closebrace}"`` would result in ``"2020/{Photo Title}"``.
**Variables**
You can define variables for later use in the template string using the format ``{var:NAME,VALUE}``. Variables may then be referenced using the format ``%NAME``. For example: ``{var:foo,bar}`` defines the variable ``%foo`` to have value ``bar``. This can be useful if you want to re-use a complex template value in multiple places within your template string or for allowing the use of characters that would otherwise be prohibited in a template string. For example, the "pipe" (\ ``|``\ ) character is not allowed in a find/replace pair but you can get around this limitation like so: ``{var:pipe,{pipe}}{title[-,%pipe]}`` which replaces the ``-`` character with ``|`` (the value of ``%pipe``\ ).
Variables can also be referenced as fields in the template string, for example: ``{var:year,created.year}{original_name}-{%year}``. In some cases, use of variables can make your template string more readable. Variables can be used as template fields, as values for filters, as values for conditional operations, or as default values. When used as a conditional value or default value, variables should be treated like any other field and enclosed in braces as conditional and default values are evaluated as template strings. For example: ``{var:name,Katie}{person contains {%name}?{%name},Not-{%name}}``.
If you need to use a ``%`` (percent sign character), you can escape the percent sign by using ``%%``. You can also use the ``{percent}`` template field where a template field is required. For example:
``{title[:,%%]}`` replaces the ``:`` with ``%`` and ``{title contains Foo?{title}{percent},{title}}`` adds ``%`` to the title if it contains ``Foo``.
Template Substitutions
----------------------
@@ -285,12 +309,14 @@ Template Substitutions
- The moment title of the photo
* - {uuid}
- Photo's internal universally unique identifier (UUID) for the photo, a 36-character string unique to the photo, e.g. '128FB4C6-0B16-4E7D-9108-FB2E90DA1546'
* - {shortuuid}
- A shorter representation of photo's internal universally unique identifier (UUID) for the photo, a 22-character string unique to the photo, e.g. 'JYsxugP9UjetmCbBCHXcmu'
* - {id}
- A unique number for the photo based on its primary key in the Photos database. A sequential integer, e.g. 1, 2, 3...etc. Each asset associated with a photo (e.g. an image and Live Photo preview) will share the same id. May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use '{id:05d}' which results in 00001, 00002, 00003...etc.
* - {album_seq}
- An integer, starting at 0, indicating the photo's index (sequence) in the containing album. Only valid when used in a '--filename' template and only when '{album}' or '{folder_album}' is used in the '--directory' template. For example '--directory "{folder_album}" --filename "{album\ *seq}*\ {original_name}"'. To start counting at a value other than 0, append append a period and the starting value to the field name. For example, to start counting at 1 instead of 0: '{album_seq.1}'. May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use '{album_seq:05d}' which results in 00000, 00001, 00002...etc. This may result in incorrect sequences if you have duplicate albums with the same name; see also '{folder_album_seq}'.
- An integer, starting at 0, indicating the photo's index (sequence) in the containing album. Only valid when used in a '--filename' template and only when '{album}' or '{folder_album}' is used in the '--directory' template. For example '--directory "{folder_album}" --filename "{album\ *seq}*\ {original_name}"'. To start counting at a value other than 0, append append '(starting_value)' to the field name. For example, to start counting at 1 instead of 0: '{album_seq(1)}'. May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use '{album_seq:05d}' which results in 00000, 00001, 00002...etc. To format while also using a starting value: '{album_seq:05d(1)}' which results in 0001, 00002...etc.This may result in incorrect sequences if you have duplicate albums with the same name; see also '{folder_album_seq}'.
* - {folder_album_seq}
- An integer, starting at 0, indicating the photo's index (sequence) in the containing album and folder path. Only valid when used in a '--filename' template and only when '{folder_album}' is used in the '--directory' template. For example '--directory "{folder_album}" --filename "{folder_album\ *seq}*\ {original_name}"'. To start counting at a value other than 0, append append a period and the starting value to the field name. For example, to start counting at 1 instead of 0: '{folder_album_seq.1}' May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use '{folder_album_seq:05d}' which results in 00000, 00001, 00002...etc. This may result in incorrect sequences if you have duplicate albums with the same name in the same folder; see also '{album_seq}'.
- An integer, starting at 0, indicating the photo's index (sequence) in the containing album and folder path. Only valid when used in a '--filename' template and only when '{folder_album}' is used in the '--directory' template. For example '--directory "{folder_album}" --filename "{folder_album\ *seq}*\ {original_name}"'. To start counting at a value other than 0, append '(starting_value)' to the field name. For example, to start counting at 1 instead of 0: '{folder_album_seq(1)}' May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use '{folder_album_seq:05d}' which results in 00000, 00001, 00002...etc. To format while also using a starting value: '{folder_album_seq:05d(1)}' which results in 0001, 00002...etc.This may result in incorrect sequences if you have duplicate albums with the same name in the same folder; see also '{album_seq}'.
* - {comma}
- A comma: ','
* - {semicolon}
@@ -320,7 +346,7 @@ Template Substitutions
* - {crlf}
- a carriage return + line feed: '\r\n'
* - {osxphotos_version}
- The osxphotos version, e.g. '0.49.2'
- The osxphotos version, e.g. '0.50.4'
* - {osxphotos_cmd_line}
- The full command line used to run osxphotos
* - {album}
@@ -361,6 +387,8 @@ Template Substitutions
- 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.

View File

@@ -218,6 +218,15 @@ A powerful feature of Photos is that it uses machine learning algorithms to auto
``osxphotos export /path/to/export --exiftool --keyword-template "{label}"``
Removing a keyword during export
--------------------------------
If some of your photos contain a keyword you do not want to be added to the exported file with ``--exiftool``\ , you can use the template system to remove the keyword from the exported file. For example, if you want to remove the keyword "MyKeyword" from all your photos:
``osxphotos export /path/to/export --exiftool --keyword-template "{keyword|remove(MyKeyword)}" --replace-keywords``
In this example, ``|remove(MyKeyword)`` is a filter which removes ``MyKeyword`` from the keyword list of every photo being processed. The ``--replace-keywords`` option instructs osxphotos to replace the keywords in the exported file with the filtered keywords from ``--keyword-template``.
**Note**\ : When evaluating templates for ``--directory`` and ``--filename``\ , osxphotos inserts the automatic default value "_" for any template field which is null (empty or blank). This is to ensure that there's never a null directory or filename created. For metadata templates such as ``--keyword-template``\ , osxphotos does not provide an automatic default value thus if the template field is null, no keyword would be created. Of course, you can provide a default value if desired and osxphotos will use this. For example, to add "nolabel" as a keyword for any photo that doesn't have labels:
``osxphotos export /path/to/export --exiftool --keyword-template "{label,nolabel}"``

View File

@@ -0,0 +1,40 @@
""" Find photos that had an EXIF or XMP rating of 5 and mark them as favorites in Photos
To use this script, save it to a file, e.g. `rating_to_favorites.py` then
run it with osxphotos (https://github.com/RhetTbull/osxphotos) via
`osxphotos run rating_to_favorites.py`
You'll also need exiftool (https://exiftool.org/)
"""
import photoscript
import osxphotos
# only find photos taken with SONY cameras, adjust to suit your use case
CAMERA_MAKE = "SONY"
def main():
"""Find all photos with EXIF or XMP rating of 5 and mark them as favorites"""
photosdb = osxphotos.PhotosDB()
for photo in photosdb.photos():
# extracting the rating data takes a while so
# skip photos not taken with the camera we're looking for
if photo.exif_info.camera_make != CAMERA_MAKE:
continue
# Photos stores some data in photo.exif but the rating data must be extracted with exiftool
if exif := photo.exiftool:
exif_data = exif.asdict()
# I think SONY uses XMP:Rating but also check EXIF:Rating
xmp_rating = exif_data.get("XMP:Rating", 0)
exif_rating = exif_data.get("EXIF:Rating", 0)
rating = max(xmp_rating, exif_rating)
if rating == 5:
print(f"Marking {photo.original_filename} ({photo.uuid}) as favorite")
photoscript.Photo(photo.uuid).favorite = True
if __name__ == "__main__":
main()

View File

@@ -5,16 +5,20 @@
"""
import pathlib
from typing import List, Union
from typing import List, Optional, Union
import osxphotos
from osxphotos import ExportOptions, PhotoInfo
def example(photo: osxphotos.PhotoInfo, **kwargs) -> Union[List, str]:
""" example function for {function} template; adds suffix of # if photo has adjustments and ! if photo is a favorite
def example(
photo: PhotoInfo, options: ExportOptions, args: Optional[str] = None, **kwargs
) -> Union[List, str]:
"""example function for {function} template; adds suffix of # if photo has adjustments and ! if photo is a favorite
Args:
photo: osxphotos.PhotoInfo object
options: osxphotos.ExportOptions object
args: optional str of arguments passed to template function
**kwargs: not currently used, placeholder to keep functions compatible with possible changes to {function}
Returns:

View File

@@ -2,11 +2,11 @@ import logging
from ._constants import AlbumSortOrder
from ._version import __version__
from .albuminfo import AlbumInfo, ImportInfo, ProjectInfo, FolderInfo
from .albuminfo import AlbumInfo, FolderInfo, ImportInfo, ProjectInfo
from .debug import is_debug, set_debug
from .exifinfo import ExifInfo
from .exiftool import ExifTool
from .export_db import ExportDB
from .export_db import ExportDB, ExportDBTemp
from .fileutil import FileUtil, FileUtilNoOp
from .momentinfo import MomentInfo
from .personinfo import PersonInfo

View File

@@ -44,10 +44,8 @@ _PHOTOS_5_VERSION = "5000" # I've seen both 5001 and 6000. 6000 is most common
# Ranges for model version by Photos version
_PHOTOS_5_MODEL_VERSION = [13000, 13999]
_PHOTOS_6_MODEL_VERSION = [14000, 14999]
_PHOTOS_7_MODEL_VERSION = [
15000,
15999,
] # Monterey developer preview is 15134, 12.1 is 15331
_PHOTOS_7_MODEL_VERSION = [15000, 15999] # Dev preview: 15134, 12.1: 15331
_PHOTOS_8_MODEL_VERSION = [16000, 16999] # Ventura dev preview: 16119
# some table names differ between Photos 5 and Photos 6
_DB_TABLE_NAMES = {
@@ -87,6 +85,18 @@ _DB_TABLE_NAMES = {
"ASSET_ALBUM_TABLE": "Z_27ASSETS",
"HDR_TYPE": "ZHDRTYPE",
},
8: {
"ASSET": "ZASSET",
"KEYWORD_JOIN": "Z_1KEYWORDS.Z_40KEYWORDS",
"ALBUM_JOIN": "Z_28ASSETS.Z_3ASSETS",
"ALBUM_SORT_ORDER": "Z_28ASSETS.Z_FOK_3ASSETS",
"IMPORT_FOK": "null",
"DEPTH_STATE": "ZASSET.ZDEPTHTYPE",
"UTI_ORIGINAL": "ZINTERNALRESOURCE.ZCOMPACTUTI",
"ASSET_ALBUM_JOIN": "Z_28ASSETS.Z_28ALBUMS",
"ASSET_ALBUM_TABLE": "Z_28ASSETS",
"HDR_TYPE": "ZHDRTYPE",
},
}
# which version operating systems have been tested
@@ -107,6 +117,7 @@ _TESTED_OS_VERSIONS = [
("12", "1"),
("12", "2"),
("12", "3"),
("12", "4"),
]
# Photos 5 has persons who are empty string if unidentified face

View File

@@ -1,3 +1,3 @@
""" version info """
__version__ = "0.49.2"
__version__ = "0.50.4"

View File

@@ -59,6 +59,7 @@ from .keywords import keywords
from .labels import labels
from .list import _list_libraries, list_libraries
from .persons import persons
from .photo_inspect import photo_inspect
from .places import places
from .query import query
from .repl import repl
@@ -88,6 +89,7 @@ __all__ = [
"list_libraries",
"load_uuid_from_file",
"persons",
"photo_inspect",
"places",
"query",
"repl",

View File

@@ -21,6 +21,7 @@ from .keywords import keywords
from .labels import labels
from .list import list_libraries
from .persons import persons
from .photo_inspect import photo_inspect
from .places import places
from .query import query
from .repl import repl
@@ -79,6 +80,7 @@ for command in [
labels,
list_libraries,
persons,
photo_inspect,
places,
query,
repl,

View File

@@ -1,5 +1,6 @@
"""exiftool command for osxphotos CLI to update an previous export with exiftool metadata"""
import os
import pathlib
import sys
from typing import Callable
@@ -18,7 +19,6 @@ from osxphotos.utils import pluralize
from .click_rich_echo import (
rich_click_echo,
rich_echo,
rich_echo_error,
set_rich_console,
set_rich_theme,
@@ -343,6 +343,8 @@ def process_files(
total = len(files)
count = 1
all_results = ExportResults()
# process files that are hardlinked? Requires user confirmation
hardlink_ok = False
with rich_progress(console=get_verbose_console(), mock=options.verbose) as progress:
task = progress.add_task("Processing files", total=total)
for uuid, file in files:
@@ -350,7 +352,14 @@ def process_files(
verbose(f"Skipping missing file [filepath]{file}[/]")
report_writer.write(ExportResults(missing=[file]))
continue
# zzz put in check for hardlink
if not hardlink_ok and os.stat(file).st_nlink > 1:
rich_click_echo(
f"[warning]:warning-emoji: Warning: file [filepath]{file}[/] is hardlinked.\n"
"You may be modifying linked original files in your Photos library. "
"This is inadvisable.[/]",
)
click.confirm("Continue processing hardlinks?", abort=True)
hardlink_ok = True
verbose(f"Processing file [filepath]{file}[/] ([num]{count}/{total}[/num])")
photo = photosdb.get_photo(uuid)
export_options = ExportOptions(

View File

@@ -1920,7 +1920,7 @@ def export_photo(
original_filename = str(original_filename)
verbose_(
f"Exporting [filename]{photo.original_filename}[/] ([filename]{photo.filename}[/]) as [filepath]{original_filename}[/] ([count]{photo_num}/{num_photos}[/])"
f"Exporting [filename]{photo.original_filename}[/] ([filename]{photo.filename}[/]) ([count]{photo_num}/{num_photos}[/])"
)
results += export_photo_to_directory(
@@ -2034,7 +2034,7 @@ def export_photo(
)
verbose_(
f"Exporting edited version of [filename]{photo.original_filename}[/filename] ([filename]{photo.filename}[/filename]) as [filepath]{edited_filename}[/filepath]"
f"Exporting edited version of [filename]{photo.original_filename}[/filename] ([filename]{photo.filename}[/filename])"
)
results += export_photo_to_directory(
@@ -2728,4 +2728,3 @@ def render_and_validate_report(report: str, exiftool_path: str, export_dir: str)
sys.exit(1)
return report

View File

@@ -1,5 +1,6 @@
"""exportdb command for osxphotos CLI"""
import json
import pathlib
import sys
@@ -22,6 +23,7 @@ from osxphotos.export_db_utils import (
export_db_update_signatures,
export_db_vacuum,
)
from osxphotos.utils import pluralize
from .export import render_and_validate_report
from .param_types import TemplateString
@@ -63,6 +65,33 @@ from .verbose import verbose_print
nargs=1,
help="Print information about FILE_PATH contained in the database.",
)
@click.option(
"--uuid-files",
metavar="UUID",
nargs=1,
help="List exported files associated with UUID.",
)
@click.option(
"--uuid-info",
metavar="UUID",
nargs=1,
help="Print information about UUID contained in the database.",
)
@click.option(
"--delete-uuid",
metavar="UUID",
nargs=1,
multiple=True,
help="Delete all data associated with UUID from the database.",
)
@click.option(
"--delete-file",
metavar="FILE_PATH",
nargs=1,
multiple=True,
help="Delete all data associated with FILE_PATH from the database; "
"does not delete the actual exported file if it exists, only the data in the database.",
)
@click.option(
"--report",
metavar="REPORT_FILE RUN_ID",
@@ -110,22 +139,26 @@ from .verbose import verbose_print
)
@click.argument("export_db", metavar="EXPORT_DATABASE", type=click.Path(exists=True))
def exportdb(
version,
vacuum,
check_signatures,
update_signatures,
touch_file,
last_run,
save_config,
info,
report,
migrate,
sql,
export_dir,
append,
verbose,
check_signatures,
dry_run,
export_db,
export_dir,
info,
last_run,
migrate,
report,
save_config,
sql,
touch_file,
update_signatures,
uuid_files,
uuid_info,
delete_uuid,
delete_file,
vacuum,
verbose,
version,
):
"""Utilities for working with the osxphotos export database"""
@@ -163,6 +196,8 @@ def exportdb(
sql,
touch_file,
update_signatures,
uuid_files,
uuid_info,
vacuum,
version,
]
@@ -273,6 +308,55 @@ def exportdb(
print(f"[red]File '{info}' not found in export database[/red]")
sys.exit(0)
if uuid_info:
# get photoinfo record for a uuid
exportdb = ExportDB(export_db, export_dir)
try:
info_rec = exportdb.get_photoinfo_for_uuid(uuid_info)
except Exception as e:
print(f"[red]Error: {e}[/red]")
sys.exit(1)
else:
if info_rec:
print(json.dumps(json.loads(info_rec), sort_keys=True, indent=2))
else:
print(f"[red]UUID '{uuid_info}' not found in export database[/red]")
sys.exit(0)
if uuid_files:
# list files associated with a uuid
exportdb = ExportDB(export_db, export_dir)
try:
file_list = exportdb.get_files_for_uuid(uuid_files)
except Exception as e:
print(f"[red]Error: {e}[/red]")
sys.exit(1)
else:
if file_list:
for f in file_list:
print(f)
else:
print(f"[red]UUID '{uuid_files}' not found in export database[/red]")
sys.exit(0)
if delete_uuid:
# delete a uuid from the export database
exportdb = ExportDB(export_db, export_dir)
for uuid in delete_uuid:
print(f"Deleting uuid {uuid} from database.")
count = exportdb.delete_data_for_uuid(uuid)
print(f"Deleted {count} {pluralize(count, 'record', 'records')}.")
sys.exit(0)
if delete_file:
# delete information associated with a file from the export database
exportdb = ExportDB(export_db, export_dir)
for filepath in delete_file:
print(f"Deleting file {filepath} from database.")
count = exportdb.delete_data_for_filepath(filepath)
print(f"Deleted {count} {pluralize(count, 'record', 'records')}.")
sys.exit(0)
if report:
exportdb = ExportDB(export_db, export_dir)
report_template, run_id = report

View File

@@ -0,0 +1,517 @@
"""Inspect photos selected in Photos """
import functools
import re
from fractions import Fraction
from multiprocessing import Process, Queue
from queue import Empty
from time import gmtime, sleep, strftime
from typing import List, Optional, Tuple
import pathlib
import bitmath
import click
from applescript import ScriptError
from photoscript import PhotosLibrary
from rich.console import Console
from rich.layout import Layout
from rich.live import Live
from rich.panel import Panel
from osxphotos import PhotoInfo, PhotosDB
from osxphotos._constants import _UNKNOWN_PERSON
from osxphotos.rich_utils import add_rich_markup_tag
from osxphotos.text_detection import detect_text as detect_text_in_photo
from osxphotos.utils import dd_to_dms_str
from .color_themes import get_theme
from .common import DB_OPTION, THEME_OPTION, get_photos_db
# global that tracks UUID being inspected
CURRENT_UUID = None
# helpers for markup
bold = add_rich_markup_tag("bold")
dim = add_rich_markup_tag("dim")
def extract_uuid(text: str) -> str:
"""Extract a UUID from a string"""
if match := re.search(
r"([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})",
text,
):
return match[1]
return None
def trim(text: str, pad: str = "") -> str:
"""Truncate a string to a fit in console, - len(pad) - 4; also removes new lines"""
width = Console().width - len(pad) - 4
text = text.replace("\n", " ")
return text if len(text) <= width else f"{text[: width- 3]}..."
def inspect_photo(
photo: PhotoInfo,
detected_text: Optional[str] = None,
templates: Optional[List[str]] = None,
) -> str:
"""Get info about an osxphotos PhotoInfo object formatted for printing"""
if templates:
return inspect_photo_templates(photo, templates)
properties = [
bold("Filename: ") + f"[filename]{photo.original_filename}[/]",
bold("Type: ") + get_photo_type(photo),
bold("UUID: ") + f"[uuid]{photo.uuid}[/]",
bold("Date: ") + f"[time]{photo.date.isoformat()}[/]",
bold("Date added: ") + f"[time]{photo.date_added.isoformat()}[/]",
]
if photo.date_modified:
properties.append(
bold("Date modified: ") + f"[time]{photo.date_modified.isoformat()}[/]"
)
if photo.intrash and photo.date_trashed:
properties.append(
bold("Date deleted: ") + f"[time]{photo.date_trashed.isoformat()}[/]"
)
if photo.import_info and photo.import_info.creation_date:
properties.append(
bold("Date imported: ")
+ f"[time]{photo.import_info.creation_date.isoformat()}[/]"
)
properties.extend(
[
bold("Dimensions: ")
+ f"[num]{photo.width}[/] x [num]{photo.height}[/] "
+ bold("Orientation: ")
+ f"[num]{photo.orientation}[/]",
bold("File size: ")
+ f"[num]{float(bitmath.Byte(photo.original_filesize).to_MB()):.2f} MB[/]",
bold("Title: ") + f"{photo.title or '-'}",
bold("Description: ")
+ f"{trim(photo.description or '-', 'Description: ')}",
bold("Edited: ")
+ f"{'' if photo.hasadjustments else '-'} "
+ bold("External edits: ")
+ f"{'' if photo.external_edit else '-'}",
bold("Keywords: ") + f"{', '.join(photo.keywords) or '-'}",
bold("Persons: ")
+ f"{', '.join(p for p in photo.persons if p != _UNKNOWN_PERSON) or '-'}",
bold("Location: ")
+ f"{', '.join(dd_to_dms_str(*photo.location)) if photo.location[0] else '-'}",
bold("Place: ") + f"{photo.place.name if photo.place else '-'}",
bold("Categories: ") + f"{', '.join(photo.labels) or '-'}",
]
)
properties.append(format_flags(photo))
properties.append(format_albums(photo))
if photo.project_info:
properties.append(
bold("Projects: ")
+ f"{', '.join(p.title for p in photo.project_info) or '-'}"
)
if photo.moment_info:
properties.append(bold("Moment: ") + f"{photo.moment_info.title or '-'}")
if photo.comments:
comments = [f"{c.user}: {c.text}" for c in photo.comments]
properties.append(
bold("Comments: ") + trim(f"{', '.join(comments)}", "Comments: ")
)
if photo.likes:
properties.append(
bold("Likes: ")
+ trim(f"{', '.join(l.user for l in photo.likes)}", "Likes: ")
)
properties.append(format_exif_info(photo))
properties.append(format_score_info(photo))
if detected_text:
# have detected text for this photo
properties.append(
bold("Detected text: ") + trim(detected_text, "Detected text: ")
)
properties.append(format_paths(photo))
return "\n".join(properties)
def inspect_photo_templates(
photo: PhotoInfo, templates: Optional[List[str]] = None
) -> str:
"""Render and display photo templates"""
properties = [
bold("Filename: ") + f"[filename]{photo.original_filename}[/]",
bold("Type: ") + get_photo_type(photo),
bold("UUID: ") + f"[uuid]{photo.uuid}[/]",
]
properties.append(bold("Templates: "))
properties.append(format_templates(photo, templates))
return "\n".join(properties)
def format_templates(photo: PhotoInfo, templates: List[str]) -> str:
"""Format templates for a photo"""
formatted_templates = []
for template in templates:
template_str, _ = photo.render_template(template)
formatted_templates.append((template, template_str))
return "\n".join(f"{t[0]} = {t[1]}" for t in formatted_templates)
def format_score_info(photo: PhotoInfo) -> str:
"""Format score_info"""
score_str = bold("Score: ")
if photo.score:
score_str += f"[num]{photo.score.overall}[/]" if photo.score else "-"
return score_str
def format_flags(photo: PhotoInfo) -> str:
"""Format special properties"""
flag_str = bold("Flags: ")
flags = []
if photo.favorite:
flags.append("favorite")
if photo.visible:
flags.append("visible")
if photo.hidden:
flags.append("hidden")
if photo.ismissing:
flags.append("missing")
if photo.intrash:
flags.append("in trash")
if photo.iscloudasset:
flags.append("cloud asset")
if photo.incloud:
flags.append("in cloud")
if photo.shared:
flags.append("shared")
flag_str += f"{', '.join(flags) or '-'}"
return flag_str
def format_albums(photo: PhotoInfo) -> str:
"""Format albums for inspect_photo"""
album_str = bold("Albums: ")
album_names = []
for album in photo.album_info:
if album.folder_names:
folder_str = "/".join(album.folder_names)
album_names.append(f"{folder_str}/{album.title}")
else:
album_names.append(album.title)
album_str += f"{', '.join(album_names) or '-'}"
return album_str
def format_path_link(path: str) -> str:
"""Format a path as URI for display in terminal"""
return f"[link={pathlib.Path(path).as_uri()}]{path}[/link]"
def format_paths(photo: PhotoInfo) -> str:
"""format photo paths for inspect_photo"""
path_str = bold("Path original: ")
path_str += f"[filepath]{format_path_link(photo.path)}[/]" if photo.path else "-"
if photo.path_edited:
path_str += "\n"
path_str += bold("Path edited: ")
path_str += f"[filepath]{format_path_link(photo.path_edited)}[/]"
if photo.path_raw:
path_str += "\n"
path_str += bold("Path raw: ")
path_str += f"[filepath]{format_path_link(photo.path_raw)}[/]"
if photo.path_derivatives:
path_str += "\n"
path_str += bold("Path preview: ")
path_str += f"[filepath]{format_path_link(photo.path_derivatives[0])}[/]"
return path_str
def format_exif_info(photo: PhotoInfo) -> str:
"""Format exif_info for inspect_photo"""
exif_str = bold("EXIF: ")
exif = photo.exif_info
if not exif:
return f"{exif_str}-"
if exif.camera_make:
exif_str += f"{exif.camera_make} "
if exif.camera_model:
exif_str += f"{exif.camera_model} "
if exif.focal_length:
exif_str += f"{exif.focal_length:.2f}mm "
if exif.iso:
exif_str += f"ISO {exif.iso} "
if exif.flash_fired:
exif_str += "Flash "
if exif.exposure_bias is not None:
exif_str += (
f"{int(exif.exposure_bias)} ev "
if exif.exposure_bias == int(exif.exposure_bias)
else f"{exif.exposure_bias} ev "
)
if exif.aperture:
exif_str += (
f"ƒ{int(exif.aperture)} "
if exif.aperture == int(exif.aperture)
else f"ƒ{exif.aperture:.1f} "
)
if exif.shutter_speed:
exif_str += f"{Fraction(exif.shutter_speed).limit_denominator(100_000)}s "
if exif.bit_rate:
exif_str += f"{exif.bit_rate} bit rate"
if exif.sample_rate:
exif_str += f"{exif.sample_rate} sample rate"
if exif.fps:
exif_str += f"{exif.fps or 0:.1f}FPS "
if exif.duration:
exif_str += f"{strftime('%H:%M:%S', gmtime(exif.duration or 0))} "
if exif.codec:
exif_str += f"{exif.codec}"
if exif.track_format:
exif_str += f"{exif.track_format}"
return exif_str
def get_photo_type(photo: PhotoInfo) -> str:
"""Return a string describing the type of photo"""
photo_type = "video" if photo.ismovie else "photo"
if photo.has_raw:
photo_type += " RAW+JPEG"
if photo.israw:
photo_type += " RAW"
if photo.burst:
photo_type += " burst"
if photo.live_photo:
photo_type += " live"
if photo.selfie:
photo_type += " selfie"
if photo.panorama:
photo_type += " panorama"
if photo.hdr:
photo_type += " HDR"
if photo.screenshot:
photo_type += " screenshot"
if photo.slow_mo:
photo_type += " slow-mo"
if photo.time_lapse:
photo_type += " time-lapse"
if photo.portrait:
photo_type += " portrait"
return photo_type
def start_text_detection(photo: PhotoInfo) -> Tuple[Process, Queue]:
"""Start text detection process for a photo"""
path_preview = photo.path_derivatives[0] if photo.path_derivatives else None
path = photo.path_edited or photo.path or path_preview
if not path:
raise ValueError("No path to photo")
queue = Queue()
p = Process(
target=_get_detected_text,
args=(photo.uuid, path, photo.orientation, queue),
)
p.start()
return (p, queue)
def _get_detected_text(uuid: str, path: str, orientation: int, queue: Queue) -> None:
"""Called by start_text_detection to run text detection in separate process"""
try:
if text := detect_text_in_photo(path, orientation):
queue.put([uuid, " ".join(t[0] for t in text if t[1] > 0.5)])
except Exception as e:
queue.put([None, str(e)])
def get_uuid_for_photos_selection() -> List[str]:
"""Get the uuid for the first photo selected in Photos
Returns: tuple of (uuid, total_selected_photos)"""
photoslib = PhotosLibrary()
try:
if photos := photoslib.selection:
return photos[0].uuid, len(photos)
except (ValueError, ScriptError) as e:
if uuid := extract_uuid(str(e)):
return uuid, 1
else:
raise e
return None, 0
def make_layout() -> Layout:
"""Define the layout."""
layout = Layout(name="root")
layout.split(
Layout(name="main", ratio=1),
Layout(name="status", size=1),
Layout(name="footer", size=1),
)
return layout
@click.command(name="inspect")
@click.option("--detect-text", "-t", is_flag=True, help="Detect text in photos")
@click.option(
"--template",
"-T",
metavar="TEMPLATE",
multiple=True,
help="Template string to render for each photo using template preview mode. "
"Useful for testing templates for export; may be repeated to test multiple templates. "
"If --template/-T is used, other inspection data will not be displayed. ",
)
@THEME_OPTION
@DB_OPTION
def photo_inspect(db, theme, detect_text, template):
"""Interactively inspect photos selected in Photos.
Open Photos then run `osxphotos inspect` in the terminal.
As you select a photo in Photos, inspect will display metadata about the photo.
Press Ctrl+C to exit when done.
Works best with a modern terminal like iTerm2 or Kitty.
"""
db = get_photos_db(db)
if not db:
raise click.UsageError(
"Did not locate Photos database. Try running with --db option."
)
theme = get_theme(theme)
console = Console(theme=theme)
layout = make_layout()
layout["footer"].update("Press Ctrl+C to quit")
layout["main"].update(
Panel("Loading Photos database, hold on...", title="Loading...")
)
def loading_status(status: str):
layout["status"].update(f"Loading database: {status}")
def update_status(status: str):
layout["status"].update(status)
def update_detected_text(photo: PhotoInfo, uuid: str, text: str):
global CURRENT_UUID
if uuid == CURRENT_UUID:
layout["main"].update(
Panel(
inspect_photo(photo, detected_text=text),
title=photo.title or photo.original_filename,
)
)
with Live(layout, console=console, refresh_per_second=10, screen=True):
photosdb = PhotosDB(dbfile=db, verbose=loading_status)
layout["main"].update(
Panel("Select a photo in Photos to inspect", title="Select a photo")
)
processes = []
global CURRENT_UUID
detected_text_cache = {}
last_uuid = None
uuid = None
total = 0
while True:
try:
uuid, total = get_uuid_for_photos_selection()
except Exception as e:
layout["main"].update(Panel(f"Error: {e}", title="Error"))
except KeyboardInterrupt:
# allow Ctrl+C to quit
break
finally:
if uuid and uuid != last_uuid:
if total > 1:
update_status(
f"{total} photos selected; inspecting uuid={uuid}"
)
else:
update_status(f"Inspecting uuid={uuid}")
CURRENT_UUID = uuid
if photo := photosdb.get_photo(uuid):
layout["main"].update(
Panel(
inspect_photo(
photo,
detected_text=detected_text_cache.get(uuid, None),
templates=template,
),
title=photo.title or photo.original_filename,
)
)
# start text detection if requested (but not if in template preview mode)
if (
detect_text
and not template
and photo.isphoto
and (
photo.path
or photo.path_edited
or photo.path_derivatives
)
and uuid not in detected_text_cache
):
# can only detect text on photos if on disk
# start text detection in separate process as it can take a few seconds
update_detected_text_callback = functools.partial(
update_detected_text, photo
)
process, queue = start_text_detection(photo)
processes.append(
[True, process, queue, update_detected_text_callback]
)
else:
layout["main"].update(
Panel(
f"No photo found in database for uuid={uuid}",
title="Error",
)
)
last_uuid = uuid
if not uuid:
last_uuid = None
layout["main"].update(
Panel("Select a photo in Photos to inspect", title="Select a photo")
)
update_status("Select a photo in Photos to inspect")
if detect_text:
# check on text detection processes
for _, values in enumerate(processes):
alive, process, queue, update_detected_text_callback = values
if alive:
# process hasn't been marked as dead yet
try:
uuid, text = queue.get(False)
update_detected_text_callback(uuid, text)
detected_text_cache[uuid] = text
process.join()
# set alive = False
values[0] = False
except Empty:
if not process.is_alive():
# process has finished, nothing in the queue
process.join()
# set alive = False
values[0] = False
sleep(0.100)

Binary file not shown.

View File

@@ -6,8 +6,10 @@ import gzip
import json
import logging
import os
import os.path
import pathlib
import pickle
import re
import sqlite3
import sys
import time
@@ -15,9 +17,9 @@ from contextlib import suppress
from io import StringIO
from sqlite3 import Error
from tempfile import TemporaryDirectory
from typing import Any, Optional, Tuple, Union
from typing import Any, List, Optional, Tuple, Union
from tenacity import retry, stop_after_attempt
from tenacity import retry, retry_if_not_exception_type, stop_after_attempt
from ._constants import OSXPHOTOS_EXPORT_DB
from ._version import __version__
@@ -30,11 +32,11 @@ __all__ = [
"ExportDBTemp",
]
OSXPHOTOS_EXPORTDB_VERSION = "7.0"
OSXPHOTOS_EXPORTDB_VERSION = "7.1"
OSXPHOTOS_ABOUT_STRING = f"Created by osxphotos version {__version__} (https://github.com/RhetTbull/osxphotos) on {datetime.datetime.now()}"
# max retry attempts for methods which use tenacity.retry
MAX_RETRY_ATTEMPTS = 5
MAX_RETRY_ATTEMPTS = 3
# maximum number of export results rows to save
MAX_EXPORT_RESULTS_DATA_ROWS = 10
@@ -99,6 +101,7 @@ class ExportDB:
"""returns path to export directory"""
return self._path
@retry(stop=stop_after_attempt(MAX_RETRY_ATTEMPTS))
def get_file_record(self, filename: Union[pathlib.Path, str]) -> "ExportRecord":
"""get info for filename and uuid
@@ -116,6 +119,10 @@ class ExportDB:
return ExportRecord(conn, filename_normalized)
return None
@retry(
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
retry=retry_if_not_exception_type(sqlite3.IntegrityError),
)
def create_file_record(
self, filename: Union[pathlib.Path, str], uuid: str
) -> "ExportRecord":
@@ -134,6 +141,10 @@ class ExportDB:
conn.commit()
return ExportRecord(conn, filename_normalized)
@retry(
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
retry=retry_if_not_exception_type(sqlite3.IntegrityError),
)
def create_or_get_file_record(
self, filename: Union[pathlib.Path, str], uuid: str
) -> "ExportRecord":
@@ -152,101 +163,137 @@ class ExportDB:
conn.commit()
return ExportRecord(conn, filename_normalized)
@retry(stop=stop_after_attempt(MAX_RETRY_ATTEMPTS))
def get_uuid_for_file(self, filename):
"""query database for filename and return UUID
returns None if filename not found in database
"""
filepath_normalized = self._normalize_filepath_relative(filename)
conn = self._conn
try:
c = conn.cursor()
c.execute(
"SELECT uuid FROM export_data WHERE filepath_normalized = ?",
(filepath_normalized,),
)
results = c.fetchone()
uuid = results[0] if results else None
except Error as e:
logging.warning(e)
uuid = None
return uuid
c = conn.cursor()
c.execute(
"SELECT uuid FROM export_data WHERE filepath_normalized = ?",
(filepath_normalized,),
)
results = c.fetchone()
return results[0] if results else None
@retry(stop=stop_after_attempt(MAX_RETRY_ATTEMPTS))
def get_files_for_uuid(self, uuid: str) -> List:
"""query database for UUID and return list of files associated with UUID or empty list"""
conn = self._conn
c = conn.cursor()
c.execute(
"SELECT filepath FROM export_data WHERE uuid = ?",
(uuid,),
)
results = c.fetchall()
return [os.path.join(self.export_dir, r[0]) for r in results]
@retry(stop=stop_after_attempt(MAX_RETRY_ATTEMPTS))
def get_photoinfo_for_uuid(self, uuid):
"""returns the photoinfo JSON struct for a UUID"""
conn = self._conn
try:
c = conn.cursor()
c.execute("SELECT photoinfo FROM photoinfo WHERE uuid = ?", (uuid,))
results = c.fetchone()
info = results[0] if results else None
except Error as e:
logging.warning(e)
info = None
return info
c = conn.cursor()
c.execute("SELECT photoinfo FROM photoinfo WHERE uuid = ?", (uuid,))
results = c.fetchone()
return results[0] if results else None
@retry(
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
retry=retry_if_not_exception_type(sqlite3.IntegrityError),
)
def set_photoinfo_for_uuid(self, uuid, info):
"""sets the photoinfo JSON struct for a UUID"""
conn = self._conn
try:
c = conn.cursor()
c.execute(
"INSERT OR REPLACE INTO photoinfo(uuid, photoinfo) VALUES (?, ?);",
(uuid, info),
)
conn.commit()
except Error as e:
logging.warning(e)
c = conn.cursor()
c.execute(
"INSERT OR REPLACE INTO photoinfo(uuid, photoinfo) VALUES (?, ?);",
(uuid, info),
)
conn.commit()
@retry(stop=stop_after_attempt(MAX_RETRY_ATTEMPTS))
def get_target_for_file(
self, uuid: str, filename: Union[str, pathlib.Path]
) -> Optional[str]:
"""query database for file matching file name and return the matching filename if there is one;
otherwise return None; looks for file.ext, file (1).ext, file (2).ext and so on to find the
actual target name that was used to export filename
Returns: the matching filename or None if no match found
"""
conn = self._conn
c = conn.cursor()
filepath_normalized = self._normalize_filepath_relative(filename)
filepath_stem = os.path.splitext(filepath_normalized)[0]
c.execute(
"SELECT uuid, filepath, filepath_normalized FROM export_data WHERE uuid = ? AND filepath_normalized LIKE ?",
(
uuid,
f"{filepath_stem}%",
),
)
results = c.fetchall()
for result in results:
filepath_normalized = os.path.splitext(result[2])[0]
if re.match(
re.escape(filepath_stem) + r"(\s\(\d+\))?$", filepath_normalized
):
return os.path.join(self.export_dir, result[1])
return None
@retry(stop=stop_after_attempt(MAX_RETRY_ATTEMPTS))
def get_previous_uuids(self):
"""returns list of UUIDs of previously exported photos found in export database"""
conn = self._conn
previous_uuids = []
try:
c = conn.cursor()
c.execute("SELECT DISTINCT uuid FROM export_data")
results = c.fetchall()
previous_uuids = [row[0] for row in results]
except Error as e:
logging.warning(e)
return previous_uuids
c = conn.cursor()
c.execute("SELECT DISTINCT uuid FROM export_data")
results = c.fetchall()
return [row[0] for row in results]
@retry(
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
retry=retry_if_not_exception_type(sqlite3.IntegrityError),
)
def set_config(self, config_data):
"""set config in the database"""
conn = self._conn
try:
dt = datetime.datetime.now().isoformat()
c = conn.cursor()
c.execute(
"INSERT OR REPLACE INTO config(datetime, config) VALUES (?, ?);",
(dt, config_data),
)
conn.commit()
except Error as e:
logging.warning(e)
dt = datetime.datetime.now().isoformat()
c = conn.cursor()
c.execute(
"INSERT OR REPLACE INTO config(datetime, config) VALUES (?, ?);",
(dt, config_data),
)
conn.commit()
@retry(
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
retry=retry_if_not_exception_type(sqlite3.IntegrityError),
)
def set_export_results(self, results):
"""Store export results in database; data is pickled and gzipped for storage"""
results_data = pickle_and_zip(results)
conn = self._conn
try:
dt = datetime.datetime.now().isoformat()
c = conn.cursor()
c.execute(
"""
UPDATE export_results_data
SET datetime = ?,
export_results = ?
WHERE datetime = (SELECT MIN(datetime) FROM export_results_data);
""",
(dt, results_data),
)
conn.commit()
except Error as e:
logging.warning(e)
dt = datetime.datetime.now().isoformat()
c = conn.cursor()
c.execute(
"""
UPDATE export_results_data
SET datetime = ?,
export_results = ?
WHERE datetime = (SELECT MIN(datetime) FROM export_results_data);
""",
(dt, results_data),
)
conn.commit()
@retry(stop=stop_after_attempt(MAX_RETRY_ATTEMPTS))
def get_export_results(self, run: int = 0):
"""Retrieve export results from database
@@ -262,47 +309,67 @@ class ExportDB:
run = -run
conn = self._conn
c = conn.cursor()
c.execute(
"""
SELECT export_results
FROM export_results_data
ORDER BY datetime DESC
""",
)
rows = c.fetchall()
try:
c = conn.cursor()
c.execute(
"""
SELECT export_results
FROM export_results_data
ORDER BY datetime DESC
""",
)
rows = c.fetchall()
try:
data = rows[run][0]
results = unzip_and_unpickle(data) if data else None
except IndexError:
results = None
except Error as e:
logging.warning(e)
data = rows[run][0]
results = unzip_and_unpickle(data) if data else None
except IndexError:
results = None
return results
@retry(stop=stop_after_attempt(MAX_RETRY_ATTEMPTS))
def get_exported_files(self):
"""Returns tuple of (uuid, filepath) for all paths of all exported files tracked in the database"""
conn = self._conn
try:
c = conn.cursor()
c.execute("SELECT uuid, filepath FROM export_data")
except Error as e:
logging.warning(e)
return
c = conn.cursor()
c.execute("SELECT uuid, filepath FROM export_data")
while row := c.fetchone():
yield row[0], os.path.join(self.export_dir, row[1])
return
@retry(stop=stop_after_attempt(MAX_RETRY_ATTEMPTS))
def delete_data_for_uuid(self, uuid):
"""Delete all exportdb data for given UUID"""
conn = self._conn
c = conn.cursor()
count = 0
c.execute("DELETE FROM export_data WHERE uuid = ?;", (uuid,))
count += c.execute("SELECT CHANGES();").fetchone()[0]
c.execute("DELETE FROM photoinfo WHERE uuid = ?;", (uuid,))
count += c.execute("SELECT CHANGES();").fetchone()[0]
conn.commit()
return count
@retry(stop=stop_after_attempt(MAX_RETRY_ATTEMPTS))
def delete_data_for_filepath(self, filepath):
"""Delete all exportdb data for given filepath"""
conn = self._conn
c = conn.cursor()
filepath_normalized = self._normalize_filepath_relative(filepath)
results = c.execute(
"SELECT uuid FROM export_data WHERE filepath_normalized = ?;",
(filepath_normalized,),
).fetchall()
count = 0
for row in results:
count += self.delete_data_for_uuid(row[0])
return count
@retry(stop=stop_after_attempt(MAX_RETRY_ATTEMPTS))
def close(self):
"""close the database connection"""
try:
self._conn.close()
except Error as e:
logging.warning(e)
self._conn.close()
@retry(stop=stop_after_attempt(MAX_RETRY_ATTEMPTS))
def _open_export_db(self, dbfile):
"""open export database and return a db connection
if dbfile does not exist, will create and initialize the database
@@ -312,8 +379,6 @@ class ExportDB:
if not os.path.isfile(dbfile):
conn = self._get_db_connection(dbfile)
if not conn:
raise Exception(f"Error getting connection to database {dbfile}")
self._create_or_migrate_db_tables(conn)
self.was_created = True
self.was_upgraded = ()
@@ -337,16 +402,12 @@ class ExportDB:
return conn
@retry(stop=stop_after_attempt(MAX_RETRY_ATTEMPTS))
def _get_db_connection(self, dbfile):
"""return db connection to dbname"""
try:
conn = sqlite3.connect(dbfile)
except Error as e:
logging.warning(e)
conn = None
return conn
return sqlite3.connect(dbfile)
@retry(stop=stop_after_attempt(MAX_RETRY_ATTEMPTS))
def _get_database_version(self, conn):
"""return tuple of (osxphotos, exportdb) versions for database connection conn"""
version_info = conn.execute(
@@ -442,18 +503,15 @@ class ExportDB:
""" CREATE UNIQUE INDEX IF NOT EXISTS idx_detected_text on detected_text (uuid);""",
]
# create the tables if needed
try:
c = conn.cursor()
for cmd in sql_commands:
c.execute(cmd)
c.execute(
"INSERT INTO version(osxphotos, exportdb) VALUES (?, ?);",
(__version__, OSXPHOTOS_EXPORTDB_VERSION),
)
c.execute("INSERT INTO about(about) VALUES (?);", (OSXPHOTOS_ABOUT_STRING,))
conn.commit()
except Error as e:
logging.warning(e)
c = conn.cursor()
for cmd in sql_commands:
c.execute(cmd)
c.execute(
"INSERT INTO version(osxphotos, exportdb) VALUES (?, ?);",
(__version__, OSXPHOTOS_EXPORTDB_VERSION),
)
c.execute("INSERT INTO about(about) VALUES (?);", (OSXPHOTOS_ABOUT_STRING,))
conn.commit()
# perform needed migrations
if version[1] < "4.3":
@@ -470,6 +528,10 @@ class ExportDB:
# create report_data table
self._migrate_6_0_to_7_0(conn)
if version[1] < "7.1":
# add timestamp to export_data
self._migrate_7_0_to_7_1(conn)
conn.execute("VACUUM;")
conn.commit()
@@ -478,6 +540,7 @@ class ExportDB:
with suppress(Exception):
self._conn.close()
@retry(stop=stop_after_attempt(MAX_RETRY_ATTEMPTS))
def _insert_run_info(self):
dt = datetime.datetime.now(datetime.timezone.utc).isoformat()
python_path = sys.executable
@@ -485,16 +548,13 @@ class ExportDB:
args = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else ""
cwd = os.getcwd()
conn = self._conn
try:
c = conn.cursor()
c.execute(
"INSERT INTO runs (datetime, python_path, script_name, args, cwd) VALUES (?, ?, ?, ?, ?)",
(dt, python_path, cmd, args, cwd),
)
c = conn.cursor()
c.execute(
"INSERT INTO runs (datetime, python_path, script_name, args, cwd) VALUES (?, ?, ?, ?, ?)",
(dt, python_path, cmd, args, cwd),
)
conn.commit()
except Error as e:
logging.warning(e)
conn.commit()
def _relative_filepath(self, filepath: Union[str, pathlib.Path]) -> str:
"""return filepath relative to self._path"""
@@ -550,158 +610,169 @@ class ExportDB:
def _migrate_4_3_to_5_0(self, conn):
"""Migrate database from version 4.3 to 5.0"""
try:
c = conn.cursor()
# add metadata column to files to support --force-update
c.execute("ALTER TABLE files ADD COLUMN metadata TEXT;")
conn.commit()
except Error as e:
logging.warning(e)
c = conn.cursor()
# add metadata column to files to support --force-update
c.execute("ALTER TABLE files ADD COLUMN metadata TEXT;")
conn.commit()
def _migrate_5_0_to_6_0(self, conn):
try:
c = conn.cursor()
c = conn.cursor()
# add export_data table
c.execute(
""" CREATE TABLE IF NOT EXISTS export_data(
id INTEGER PRIMARY KEY,
filepath_normalized TEXT NOT NULL,
filepath TEXT NOT NULL,
uuid TEXT NOT NULL,
src_mode INTEGER,
src_size INTEGER,
src_mtime REAL,
dest_mode INTEGER,
dest_size INTEGER,
dest_mtime REAL,
digest TEXT,
exifdata JSON,
export_options INTEGER,
UNIQUE(filepath_normalized)
); """,
)
c.execute(
""" CREATE UNIQUE INDEX IF NOT EXISTS idx_export_data_filepath_normalized on export_data (filepath_normalized); """,
)
# add export_data table
c.execute(
""" CREATE TABLE IF NOT EXISTS export_data(
id INTEGER PRIMARY KEY,
filepath_normalized TEXT NOT NULL,
filepath TEXT NOT NULL,
uuid TEXT NOT NULL,
src_mode INTEGER,
src_size INTEGER,
src_mtime REAL,
dest_mode INTEGER,
dest_size INTEGER,
dest_mtime REAL,
digest TEXT,
exifdata JSON,
export_options INTEGER,
UNIQUE(filepath_normalized)
); """,
)
c.execute(
""" CREATE UNIQUE INDEX IF NOT EXISTS idx_export_data_filepath_normalized on export_data (filepath_normalized); """,
)
# migrate data
c.execute(
""" INSERT INTO export_data (filepath_normalized, filepath, uuid) SELECT filepath_normalized, filepath, uuid FROM files;""",
)
c.execute(
""" UPDATE export_data
SET (src_mode, src_size, src_mtime) =
(SELECT mode, size, mtime
FROM edited
WHERE export_data.filepath_normalized = edited.filepath_normalized);
""",
)
c.execute(
""" UPDATE export_data
SET (dest_mode, dest_size, dest_mtime) =
(SELECT orig_mode, orig_size, orig_mtime
FROM files
WHERE export_data.filepath_normalized = files.filepath_normalized);
""",
)
c.execute(
""" UPDATE export_data SET digest =
(SELECT metadata FROM files
WHERE files.filepath_normalized = export_data.filepath_normalized
); """
)
c.execute(
""" UPDATE export_data SET exifdata =
(SELECT json_exifdata FROM exifdata
WHERE exifdata.filepath_normalized = export_data.filepath_normalized
); """
)
# migrate data
c.execute(
""" INSERT INTO export_data (filepath_normalized, filepath, uuid) SELECT filepath_normalized, filepath, uuid FROM files;""",
)
c.execute(
""" UPDATE export_data
SET (src_mode, src_size, src_mtime) =
(SELECT mode, size, mtime
FROM edited
WHERE export_data.filepath_normalized = edited.filepath_normalized);
""",
)
c.execute(
""" UPDATE export_data
SET (dest_mode, dest_size, dest_mtime) =
(SELECT orig_mode, orig_size, orig_mtime
FROM files
WHERE export_data.filepath_normalized = files.filepath_normalized);
""",
)
c.execute(
""" UPDATE export_data SET digest =
(SELECT metadata FROM files
WHERE files.filepath_normalized = export_data.filepath_normalized
); """
)
c.execute(
""" UPDATE export_data SET exifdata =
(SELECT json_exifdata FROM exifdata
WHERE exifdata.filepath_normalized = export_data.filepath_normalized
); """
)
# create config table
c.execute(
""" CREATE TABLE IF NOT EXISTS config (
id INTEGER PRIMARY KEY,
datetime TEXT,
config TEXT
); """
)
# create config table
c.execute(
""" CREATE TABLE IF NOT EXISTS config (
id INTEGER PRIMARY KEY,
datetime TEXT,
config TEXT
); """
)
# create photoinfo table
c.execute(
""" CREATE TABLE IF NOT EXISTS photoinfo (
id INTEGER PRIMARY KEY,
uuid TEXT NOT NULL,
photoinfo JSON,
UNIQUE(uuid)
); """
)
c.execute(
"""CREATE UNIQUE INDEX IF NOT EXISTS idx_photoinfo_uuid on photoinfo (uuid);"""
)
c.execute(
""" INSERT INTO photoinfo (uuid, photoinfo) SELECT uuid, json_info FROM info;"""
)
# create photoinfo table
c.execute(
""" CREATE TABLE IF NOT EXISTS photoinfo (
id INTEGER PRIMARY KEY,
uuid TEXT NOT NULL,
photoinfo JSON,
UNIQUE(uuid)
); """
)
c.execute(
"""CREATE UNIQUE INDEX IF NOT EXISTS idx_photoinfo_uuid on photoinfo (uuid);"""
)
c.execute(
""" INSERT INTO photoinfo (uuid, photoinfo) SELECT uuid, json_info FROM info;"""
)
# drop indexes no longer needed
c.execute("DROP INDEX IF EXISTS idx_files_filepath_normalized;")
c.execute("DROP INDEX IF EXISTS idx_exifdata_filename;")
c.execute("DROP INDEX IF EXISTS idx_edited_filename;")
c.execute("DROP INDEX IF EXISTS idx_converted_filename;")
c.execute("DROP INDEX IF EXISTS idx_sidecar_filename;")
c.execute("DROP INDEX IF EXISTS idx_detected_text;")
# drop indexes no longer needed
c.execute("DROP INDEX IF EXISTS idx_files_filepath_normalized;")
c.execute("DROP INDEX IF EXISTS idx_exifdata_filename;")
c.execute("DROP INDEX IF EXISTS idx_edited_filename;")
c.execute("DROP INDEX IF EXISTS idx_converted_filename;")
c.execute("DROP INDEX IF EXISTS idx_sidecar_filename;")
c.execute("DROP INDEX IF EXISTS idx_detected_text;")
# drop tables no longer needed
c.execute("DROP TABLE IF EXISTS files;")
c.execute("DROP TABLE IF EXISTS info;")
c.execute("DROP TABLE IF EXISTS exifdata;")
c.execute("DROP TABLE IF EXISTS edited;")
c.execute("DROP TABLE IF EXISTS converted;")
c.execute("DROP TABLE IF EXISTS sidecar;")
c.execute("DROP TABLE IF EXISTS detected_text;")
# drop tables no longer needed
c.execute("DROP TABLE IF EXISTS files;")
c.execute("DROP TABLE IF EXISTS info;")
c.execute("DROP TABLE IF EXISTS exifdata;")
c.execute("DROP TABLE IF EXISTS edited;")
c.execute("DROP TABLE IF EXISTS converted;")
c.execute("DROP TABLE IF EXISTS sidecar;")
c.execute("DROP TABLE IF EXISTS detected_text;")
conn.commit()
except Error as e:
logging.warning(e)
conn.commit()
def _migrate_6_0_to_7_0(self, conn):
try:
c = conn.cursor()
c = conn.cursor()
c.execute(
"""CREATE TABLE IF NOT EXISTS export_results_data (
id INTEGER PRIMARY KEY,
datetime TEXT,
export_results BLOB
);"""
)
# pre-populate report_data table with blank fields
# ExportDB will use these as circular buffer always writing to the oldest record
for _ in range(MAX_EXPORT_RESULTS_DATA_ROWS):
c.execute(
"""CREATE TABLE IF NOT EXISTS export_results_data (
id INTEGER PRIMARY KEY,
datetime TEXT,
export_results BLOB
);"""
"""INSERT INTO export_results_data (datetime, export_results) VALUES (?, ?);""",
(datetime.datetime.now().isoformat(), b""),
)
# pre-populate report_data table with blank fields
# ExportDB will use these as circular buffer always writing to the oldest record
for _ in range(MAX_EXPORT_RESULTS_DATA_ROWS):
c.execute(
"""INSERT INTO export_results_data (datetime, export_results) VALUES (?, ?);""",
(datetime.datetime.now().isoformat(), b""),
)
# sleep a tiny bit just to ensure time stamps increment
time.sleep(0.001)
conn.commit()
except Error as e:
logging.warning(e)
# sleep a tiny bit just to ensure time stamps increment
time.sleep(0.001)
conn.commit()
def _migrate_7_0_to_7_1(self, conn):
c = conn.cursor()
c.execute("""ALTER TABLE export_data ADD COLUMN timestamp DATETIME;""")
c.execute(
"""
CREATE TRIGGER insert_timestamp_trigger
AFTER INSERT ON export_data
BEGIN
UPDATE export_data SET timestamp = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') WHERE id = NEW.id;
END;
"""
)
c.execute(
"""
CREATE TRIGGER update_timestamp_trigger
AFTER UPDATE On export_data
BEGIN
UPDATE export_data SET timestamp = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') WHERE id = NEW.id;
END;
"""
)
conn.commit()
def _perform_db_maintenace(self, conn):
"""Perform database maintenance"""
try:
c = conn.cursor()
c.execute(
"""DELETE FROM config
WHERE id < (
SELECT MIN(id)
FROM (SELECT id FROM config ORDER BY id DESC LIMIT 9)
);
"""
)
conn.commit()
except Error as e:
logging.warning(e)
c = conn.cursor()
c.execute(
"""DELETE FROM config
WHERE id < (
SELECT MIN(id)
FROM (SELECT id FROM config ORDER BY id DESC LIMIT 9)
);
"""
)
conn.commit()
class ExportDBInMemory(ExportDB):
@@ -753,14 +824,13 @@ class ExportDBInMemory(ExportDB):
conn_on_disk.commit()
conn_on_disk.close()
@retry(stop=stop_after_attempt(MAX_RETRY_ATTEMPTS))
def close(self):
"""close the database connection"""
try:
if self._conn:
self._conn.close()
except Error as e:
logging.warning(e)
if self._conn:
self._conn.close()
@retry(stop=stop_after_attempt(MAX_RETRY_ATTEMPTS))
def _open_export_db(self, dbfile): # sourcery skip: raise-specific-error
"""open export database and return a db connection
returns: connection to the database
@@ -794,13 +864,7 @@ class ExportDBInMemory(ExportDB):
def _get_db_connection(self):
"""return db connection to in memory database"""
try:
conn = sqlite3.connect(":memory:")
except Error as e:
logging.warning(e)
conn = None
return conn
return sqlite3.connect(":memory:")
def _dump_db(self, conn: sqlite3.Connection) -> StringIO:
"""dump sqlite db to a string buffer"""
@@ -1048,6 +1112,21 @@ class ExportRecord:
if not self._context_manager:
conn.commit()
@property
def timestamp(self):
"""returns the timestamp value"""
conn = self._conn
c = conn.cursor()
if row := c.execute(
"SELECT timestamp FROM export_data WHERE filepath_normalized = ?;",
(self._filepath_normalized,),
).fetchone():
return row[0]
raise ValueError(
f"No timestamp found in database for {self._filepath_normalized}"
)
def asdict(self):
"""Return dict of self"""
exifdata = json.loads(self.exifdata) if self.exifdata else None
@@ -1056,6 +1135,7 @@ class ExportRecord:
"filepath": self.filepath,
"filepath_normalized": self.filepath_normalized,
"uuid": self.uuid,
"timestamp": self.timestamp,
"digest": self.digest,
"src_sig": self.src_sig,
"dest_sig": self.dest_sig,

View File

@@ -92,11 +92,13 @@ class PersonInfo:
return f"PersonInfo(name={self.name}, display_name={self.display_name}, uuid={self.uuid}, facecount={self.facecount})"
def __eq__(self, other):
if not isinstance(other, type(self)):
return False
return all(
getattr(self, field) == getattr(other, field) for field in ["_db", "_pk"]
return (
all(
getattr(self, field) == getattr(other, field)
for field in ["_db", "_pk"]
)
if isinstance(other, type(self))
else False
)
def __ne__(self, other):
@@ -127,23 +129,14 @@ class FaceInfo:
self._person_pk = face["person"]
self.center_x = face["centerx"]
self.center_y = face["centery"]
self.mouth_x = face["mouthx"]
self.mouth_y = face["mouthy"]
self.left_eye_x = face["lefteyex"]
self.left_eye_y = face["lefteyey"]
self.right_eye_x = face["righteyex"]
self.right_eye_y = face["righteyey"]
self.size = face["size"]
self.quality = face["quality"]
self.source_width = face["sourcewidth"]
self.source_height = face["sourceheight"]
self.has_smile = face["has_smile"]
self.left_eye_closed = face["left_eye_closed"]
self.right_eye_closed = face["right_eye_closed"]
self.manual = face["manual"]
self.face_type = face["facetype"]
self.age_type = face["agetype"]
# self.bald_type = face["baldtype"]
self.eye_makeup_type = face["eyemakeuptype"]
self.eye_state = face["eyestate"]
self.facial_hair_type = face["facialhairtype"]
@@ -174,33 +167,6 @@ class FaceInfo:
size_reference = photo.width if photo.width > photo.height else photo.height
return self.size * size_reference
@property
def mouth(self):
"""Coordinates, in PIL format, for mouth position
Returns:
tuple of coordinates in form (x, y)
"""
return self._make_point_with_rotation((self.mouth_x, self.mouth_y))
@property
def left_eye(self):
"""Coordinates, in PIL format, for left eye position
Returns:
tuple of coordinates in form (x, y)
"""
return self._make_point_with_rotation((self.left_eye_x, self.left_eye_y))
@property
def right_eye(self):
"""Coordinates, in PIL format, for right eye position
Returns:
tuple of coordinates in form (x, y)
"""
return self._make_point_with_rotation((self.right_eye_x, self.right_eye_y))
@property
def person_info(self):
"""PersonInfo instance for person associated with this face"""
@@ -415,15 +381,6 @@ class FaceInfo:
"center_x": self.center_x,
"center_y": self.center_y,
"center": self.center,
"mouth_x": self.mouth_x,
"mouth_y": self.mouth_y,
"mouth": self.mouth,
"left_eye_x": self.left_eye_x,
"left_eye_y": self.left_eye_y,
"left_eye": self.left_eye,
"right_eye_x": self.right_eye_x,
"right_eye_y": self.right_eye_y,
"right_eye": self.right_eye,
"size": self.size,
"face_rect": self.face_rect(),
"mpri_reg_rect": self.mpri_reg_rect._asdict(),
@@ -435,12 +392,9 @@ class FaceInfo:
"source_width": self.source_width,
"source_height": self.source_height,
"has_smile": self.has_smile,
"left_eye_closed": self.left_eye_closed,
"right_eye_closed": self.right_eye_closed,
"manual": self.manual,
"face_type": self.face_type,
"age_type": self.age_type,
# "bald_type": self.bald_type,
"eye_makeup_type": self.eye_makeup_type,
"eye_state": self.eye_state,
"facial_hair_type": self.facial_hair_type,

View File

@@ -44,7 +44,13 @@ from .photokit import (
from .phototemplate import RenderOptions
from .rich_utils import add_rich_markup_tag
from .uti import get_preferred_uti_extension
from .utils import hexdigest, increment_filename, lineno, list_directory
from .utils import (
hexdigest,
increment_filename,
increment_filename_with_count,
lineno,
list_directory,
)
__all__ = [
"ExportError",
@@ -488,7 +494,7 @@ class PhotoExporter:
src = staged_files.edited if options.edited else staged_files.original
# get the right destination path depending on options.update, etc.
dest = self._get_dest_path(src, dest, options)
dest = self._get_dest_path(dest, options)
self._render_options.filepath = str(dest)
all_results = ExportResults()
@@ -642,12 +648,11 @@ class PhotoExporter:
return edited_filename
def _get_dest_path(
self, src: str, dest: pathlib.Path, options: ExportOptions
self, dest: pathlib.Path, options: ExportOptions
) -> pathlib.Path:
"""If destination exists find match in ExportDB, on disk, or add (1), (2), and so on to filename to get a valid destination
Args:
src (str): source file path
dest (str): destination path
options (ExportOptions): Export options
@@ -663,6 +668,10 @@ class PhotoExporter:
f"destination exists ({dest}); overwrite={options.overwrite}, increment={options.increment}"
)
# if overwrite, we don't care if the file exists or not
if options.overwrite:
return dest
# if not update or overwrite, check to see if file exists and if so, add (1), (2), etc
# until we find one that works
# Photos checks the stem and adds (1), (2), etc which avoids collision with sidecars
@@ -675,29 +684,36 @@ class PhotoExporter:
return pathlib.Path(increment_filename(dest))
# if update and file exists, need to check to see if it's the right file by checking export db
if (options.update or options.force_update) and dest.exists() and src:
if options.update or options.force_update:
export_db = options.export_db
# destination exists, check to see if destination is the right UUID
dest_uuid = export_db.get_uuid_for_file(dest)
if dest_uuid != self.photo.uuid:
# not the right file, find the right one
# find files that match "dest_name (*.ext" (e.g. "dest_name (1).jpg", "dest_name (2).jpg)", ...)
dest_files = list_directory(
dest.parent,
startswith=f"{dest.stem} (",
endswith=dest.suffix,
include_path=True,
)
for file_ in dest_files:
dest_uuid = export_db.get_uuid_for_file(file_)
if dest_uuid == self.photo.uuid:
dest = pathlib.Path(file_)
break
else:
# increment the destination file
dest = pathlib.Path(increment_filename(dest))
if dest_uuid is None and not dest.exists():
# destination doesn't exist in export db and doesn't exist on disk
# so we can just use it
return dest
# either dest was updated in the if clause above or not updated at all
if dest_uuid == self.photo.uuid:
# destination is the right file
return dest
# either dest_uuid is wrong or file exists and there's no associated UUID, so find a name that matches
# or create a new name if no match
# find files that match "dest_name (*.ext" (e.g. "dest_name (1).jpg", "dest_name (2).jpg)", ...)
# first, find all matching files in export db and see if there's a match
if dest_target := export_db.get_target_for_file(self.photo.uuid, dest):
# there's a match so use that
return pathlib.Path(dest_target)
# no match so need to create a new name
# increment the destination file until we find one that doesn't exist and doesn't match another uuid in the database
count = 0
dest, count = increment_filename_with_count(dest, count)
count += 1
while export_db.get_uuid_for_file(dest) is not None:
dest, count = increment_filename_with_count(dest, count)
return pathlib.Path(dest)
# fail safe...I can't think of a case that gets here
return dest
def _should_update_photo(
@@ -1036,9 +1052,9 @@ class PhotoExporter:
def _export_photo(
self,
src,
dest,
options,
src: str,
dest: pathlib.Path,
options: ExportOptions,
):
"""Helper function for export()
Does the actual copy or hardlink taking the appropriate
@@ -1060,6 +1076,7 @@ class PhotoExporter:
ValueError if export_as_hardlink and convert_to_jpeg both True
"""
verbose = options.verbose or self._verbose
if options.export_as_hardlink and options.convert_to_jpeg:
raise ValueError(
"export_as_hardlink and convert_to_jpeg cannot both be True"
@@ -1149,6 +1166,9 @@ class PhotoExporter:
try:
fileutil.copy(src, dest_str)
verbose(
f"Exported {self._filename(self.photo.original_filename)} to {self._filepath(dest_str)}"
)
except Exception as e:
raise ExportError(
f"Error copying file {src} to {dest_str}: {e} ({lineno(__file__)})"
@@ -1444,7 +1464,7 @@ class PhotoExporter:
) -> ExportResults:
"""Write exif metadata to src file using exiftool
Caution: This method modifies *src*, not *dest*,
Caution: This method modifies *src*, not *dest*,
so src must be a copy of the original file if you don't want the source modified;
it also does not write to dest (dest is the intended destination for purposes of
referencing the export database. This allows the exiftool update to be done on the

View File

@@ -3,6 +3,8 @@ PhotoInfo class
Represents a single photo in the Photos library and provides access to the photo's attributes
PhotosDB.photos() returns a list of PhotoInfo objects
"""
import contextlib
import dataclasses
import datetime
import json
@@ -10,14 +12,16 @@ import logging
import os
import os.path
import pathlib
import plistlib
from datetime import timedelta, timezone
from functools import cached_property
from typing import Optional
from typing import Dict, Optional
import yaml
from osxmetadata import OSXMetaData
from ._constants import (
_DB_TABLE_NAMES,
_MOVIE_TYPE,
_PHOTO_TYPE,
_PHOTOS_4_ALBUM_KIND,
@@ -1372,6 +1376,30 @@ class PhotoInfo:
useful for detecting changes in any property/metadata of the photo"""
return hexdigest(self.json())
@cached_property
def cloud_metadata(self) -> Dict:
"""Returns contents of ZCLOUDMASTERMEDIAMETADATA as dict"""
# This is a large blob of data so don't load it unless requested
asset_table = _DB_TABLE_NAMES[self._db._photos_ver]["ASSET"]
sql_cloud_metadata = f"""
SELECT ZCLOUDMASTERMEDIAMETADATA.ZDATA
FROM ZCLOUDMASTERMEDIAMETADATA
JOIN ZCLOUDMASTER ON ZCLOUDMASTER.Z_PK = ZCLOUDMASTERMEDIAMETADATA.ZCLOUDMASTER
JOIN {asset_table} on {asset_table}.ZMASTER = ZCLOUDMASTER.Z_PK
WHERE {asset_table}.ZUUID = ?
"""
if self._db._db_version <= _PHOTOS_4_VERSION:
logging.debug(f"cloud_metadata not implemented for this database version")
return {}
_, cursor = self._db.get_db_connection()
metadata = {}
if results := cursor.execute(sql_cloud_metadata, (self.uuid,)).fetchone():
with contextlib.suppress(Exception):
metadata = plistlib.loads(results[0])
return metadata
def detected_text(self, confidence_threshold=TEXT_DETECTION_CONFIDENCE_THRESHOLD):
"""Detects text in photo and returns lists of results as (detected text, confidence)

View File

@@ -1,13 +1,11 @@
""" Methods for PhotosDB to add Photos face info
"""
import logging
from .._constants import _DB_TABLE_NAMES, _PHOTOS_4_VERSION
from ..utils import _open_sql_file, normalize_unicode
from .photosdb_utils import get_db_version
"""
This module should be imported in the class defintion of PhotosDB in photosdb.py
Do not import this module directly
@@ -202,8 +200,8 @@ def _process_faceinfo_5(photosdb):
ZDETECTEDFACE.ZHASSMILE,
ZDETECTEDFACE.ZHIDDEN,
ZDETECTEDFACE.ZISINTRASH,
ZDETECTEDFACE.ZISLEFTEYECLOSED,
ZDETECTEDFACE.ZISRIGHTEYECLOSED,
NULL, -- ZDETECTEDFACE.ZISLEFTEYECLOSED
NULL, -- ZDETECTEDFACE.ZISRIGHTEYECLOSED
ZDETECTEDFACE.ZLIPMAKEUPTYPE,
ZDETECTEDFACE.ZMANUAL,
ZDETECTEDFACE.ZQUALITYMEASURE,
@@ -213,17 +211,17 @@ def _process_faceinfo_5(photosdb):
ZDETECTEDFACE.ZBLURSCORE,
ZDETECTEDFACE.ZCENTERX,
ZDETECTEDFACE.ZCENTERY,
ZDETECTEDFACE.ZLEFTEYEX,
ZDETECTEDFACE.ZLEFTEYEY,
ZDETECTEDFACE.ZMOUTHX,
ZDETECTEDFACE.ZMOUTHY,
NULL, -- ZDETECTEDFACE.ZLEFTEYEX,
NULL, -- ZDETECTEDFACE.ZLEFTEYEY,
NULL, -- ZDETECTEDFACE.ZMOUTHX,
NULL, -- ZDETECTEDFACE.ZMOUTHY,
ZDETECTEDFACE.ZPOSEYAW,
ZDETECTEDFACE.ZQUALITY,
ZDETECTEDFACE.ZRIGHTEYEX,
ZDETECTEDFACE.ZRIGHTEYEY,
NULL, -- ZDETECTEDFACE.ZRIGHTEYEX,
NULL, -- ZDETECTEDFACE.ZRIGHTEYEY,
ZDETECTEDFACE.ZROLL,
ZDETECTEDFACE.ZSIZE,
ZDETECTEDFACE.ZYAW,
NULL, -- ZDETECTEDFACE.ZYAW,
ZDETECTEDFACE.ZMASTERIDENTIFIER
FROM ZDETECTEDFACE
JOIN {asset_table} ON {asset_table}.Z_PK = ZDETECTEDFACE.ZASSET
@@ -237,7 +235,7 @@ def _process_faceinfo_5(photosdb):
# 3 ZDETECTEDFACE.ZPERSON,
# 4 ZPERSON.ZFULLNAME,
# 5 ZDETECTEDFACE.ZAGETYPE,
# 6 ZDETECTEDFACE.ZBALDTYPE, (Not available on Monterey)
# 6 NULL -- ZDETECTEDFACE.ZBALDTYPE, (Not available on Monterey)
# 7 ZDETECTEDFACE.ZEYEMAKEUPTYPE,
# 8 ZDETECTEDFACE.ZEYESSTATE,
# 9 ZDETECTEDFACE.ZFACIALHAIRTYPE,
@@ -247,8 +245,8 @@ def _process_faceinfo_5(photosdb):
# 13 ZDETECTEDFACE.ZHASSMILE,
# 14 ZDETECTEDFACE.ZHIDDEN,
# 15 ZDETECTEDFACE.ZISINTRASH,
# 16 ZDETECTEDFACE.ZISLEFTEYECLOSED,
# 17 ZDETECTEDFACE.ZISRIGHTEYECLOSED,
# 16 NULL -- ZDETECTEDFACE.ZISLEFTEYECLOSED,
# 17 NULL -- ZDETECTEDFACE.ZISRIGHTEYECLOSED,
# 18 ZDETECTEDFACE.ZLIPMAKEUPTYPE,
# 19 ZDETECTEDFACE.ZMANUAL,
# 20 ZDETECTEDFACE.ZQUALITYMEASURE,
@@ -258,17 +256,17 @@ def _process_faceinfo_5(photosdb):
# 24 ZDETECTEDFACE.ZBLURSCORE,
# 25 ZDETECTEDFACE.ZCENTERX,
# 26 ZDETECTEDFACE.ZCENTERY,
# 27 ZDETECTEDFACE.ZLEFTEYEX,
# 28 ZDETECTEDFACE.ZLEFTEYEY,
# 29 ZDETECTEDFACE.ZMOUTHX,
# 30 ZDETECTEDFACE.ZMOUTHY,
# 27 NULL -- ZDETECTEDFACE.ZLEFTEYEX, (Not available on Ventura)
# 28 NULL -- ZDETECTEDFACE.ZLEFTEYEY, (Not available on Ventura)
# 29 NULL -- ZDETECTEDFACE.ZMOUTHX, (Not available on Ventura)
# 30 NULL -- ZDETECTEDFACE.ZMOUTHY, (Not available on Ventura)
# 31 ZDETECTEDFACE.ZPOSEYAW,
# 32 ZDETECTEDFACE.ZQUALITY,
# 33 ZDETECTEDFACE.ZRIGHTEYEX,
# 34 ZDETECTEDFACE.ZRIGHTEYEY,
# 33 NULL -- ZDETECTEDFACE.ZRIGHTEYEX, (Not available on Ventura)
# 34 NULL -- ZDETECTEDFACE.ZRIGHTEYEY, (Not available on Ventura)
# 35 ZDETECTEDFACE.ZROLL,
# 36 ZDETECTEDFACE.ZSIZE,
# 37 ZDETECTEDFACE.ZYAW,
# 37 NULL -- ZDETECTEDFACE.ZYAW, (Not available on Ventura)
# 38 ZDETECTEDFACE.ZMASTERIDENTIFIER
for row in result:
@@ -310,8 +308,8 @@ def _process_faceinfo_5(photosdb):
face["righteyey"] = row[34]
face["roll"] = row[35]
face["size"] = row[36]
face["yaw"] = row[37]
face["pitch"] = 0.0 # not defined in Photos 5
face["yaw"] = 0 # Photos 4 only (this is in Photos 5-7, but dropped in Ventura so just don't support it)
face["pitch"] = 0 # not defined in Photos 5
photosdb._db_faceinfo_pk[pk] = face

View File

@@ -14,7 +14,7 @@ import tempfile
from collections import OrderedDict
from collections.abc import Iterable
from datetime import datetime, timedelta, timezone
from typing import List
from typing import List, Optional
from unicodedata import normalize
import bitmath
@@ -2858,16 +2858,16 @@ class PhotosDB:
def photos(
self,
keywords=None,
uuid=None,
persons=None,
albums=None,
images=True,
movies=True,
from_date=None,
to_date=None,
intrash=False,
):
keywords: Optional[List[str]] = None,
uuid: Optional[List[str]] = None,
persons: Optional[List[str]] = None,
albums: Optional[List[str]] = None,
images: bool = True,
movies: bool = True,
from_date: Optional[datetime] = None,
to_date: Optional[datetime] = None,
intrash: bool = False,
) -> List[PhotoInfo]:
"""Return a list of PhotoInfo objects
If called with no args, returns the entire database of photos
If called with args, returns photos matching the args (e.g. keywords, persons, etc.)

View File

@@ -12,6 +12,7 @@ from .._constants import (
_PHOTOS_5_VERSION,
_PHOTOS_6_MODEL_VERSION,
_PHOTOS_7_MODEL_VERSION,
_PHOTOS_8_MODEL_VERSION,
_TESTED_DB_VERSIONS,
)
from ..utils import _open_sql_file
@@ -92,10 +93,12 @@ def get_db_model_version(db_file):
return 6
elif _PHOTOS_7_MODEL_VERSION[0] <= model_ver <= _PHOTOS_7_MODEL_VERSION[1]:
return 7
elif _PHOTOS_8_MODEL_VERSION[0] <= model_ver <= _PHOTOS_8_MODEL_VERSION[1]:
return 8
else:
logging.warning(f"Unknown model version: {model_ver}")
# cross our fingers and try latest version
return 7
return 8
class UnknownLibraryVersion(Exception):

View File

@@ -10,7 +10,7 @@ In its simplest form, a template statement has the form: `"{template_field}"`, f
Template statements may contain one or more modifiers. The full syntax is:
`"pretext{delim+template_field:subfield|filter(path_sep)[find,replace] conditional?bool_value,default}posttext"`
`"pretext{delim+template_field:subfield(field_arg)|filter[find,replace] conditional?bool_value,default}posttext"`
Template statements are white-space sensitive meaning that white space (spaces, tabs) changes the meaning of the template statement.
@@ -31,6 +31,8 @@ e.g. if Photo keywords are `["foo","bar"]`:
`:subfield`: Some templates have sub-fields, For example, `{exiftool:IPTC:Make}`; the template_field is `exiftool` and the sub-field is `IPTC:Make`.
`(field_arg)`: optional arguments to pass to the field; for example, with `{folder_album}` this is used to pass the path separator used for joining folders and albums when rendering the field (default is "/" for `{folder_album}`).
`|filter`: You may optionally append one or more filter commands to the end of the template field using the vertical pipe ('|') symbol. Filters may be combined, separated by '|' as in: `{keyword|capitalize|parens}`.
Valid filters are:
@@ -40,16 +42,6 @@ from osxphotos.phototemplate import FILTER_VALUES
filter_help = "\n".join(f"- `{f}`: {descr}" for f, descr in FILTER_VALUES.items())
cog.out(filter_help)
]]]-->
- `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>
<!--[[[end]]]-->
e.g. if Photo keywords are `["FOO","bar"]`:
@@ -63,8 +55,6 @@ e.g. if Photo description is "my description":
- `"{descr|titlecase}"` renders to: `"My Description"`
`(path_sep)`: optional path separator to use when joining path-like fields, for example `{folder_album}`. Default is "/".
e.g. If Photo is in `Album1` in `Folder1`:
- `"{folder_album}"` renders to `["Folder1/Album1"]`
@@ -107,7 +97,7 @@ This can be used to rename files as well, for example:
This renames any photo that is a favorite as 'Favorite-ImageName.jpg' (where 'ImageName.jpg' is the original name of the photo) and all other photos with the unmodified original name.
`?bool_value`: Template fields may be evaluated as boolean (True/False) by appending "?" after the field name (and following "(path_sep)" or "[find/replace]". If a field is True (e.g. photo is HDR and field is `"{hdr}"`) or has any value, the value following the "?" will be used to render the template instead of the actual field value. If the template field evaluates to False (e.g. in above example, photo is not HDR) or has no value (e.g. photo has no title and field is `"{title}"`) then the default value following a "," will be used.
`?bool_value`: Template fields may be evaluated as boolean (True/False) by appending "?" after the field name (and following "(field_arg)" or "[find/replace]". If a field is True (e.g. photo is HDR and field is `"{hdr}"`) or has any value, the value following the "?" will be used to render the template instead of the actual field value. If the template field evaluates to False (e.g. in above example, photo is not HDR) or has no value (e.g. photo has no title and field is `"{title}"`) then the default value following a "," will be used.
e.g. if photo is an HDR image,
@@ -137,3 +127,13 @@ Either or both bool_value or default (False value) may be empty which would resu
If you want to include "{" or "}" in the output, use "{openbrace}" or "{closebrace}" template substitution.
e.g. `"{created.year}/{openbrace}{title}{closebrace}"` would result in `"2020/{Photo Title}"`.
**Variables**
You can define variables for later use in the template string using the format `{var:NAME,VALUE}`. Variables may then be referenced using the format `%NAME`. For example: `{var:foo,bar}` defines the variable `%foo` to have value `bar`. This can be useful if you want to re-use a complex template value in multiple places within your template string or for allowing the use of characters that would otherwise be prohibited in a template string. For example, the "pipe" (`|`) character is not allowed in a find/replace pair but you can get around this limitation like so: `{var:pipe,{pipe}}{title[-,%pipe]}` which replaces the `-` character with `|` (the value of `%pipe`).
Variables can also be referenced as fields in the template string, for example: `{var:year,created.year}{original_name}-{%year}`. In some cases, use of variables can make your template string more readable. Variables can be used as template fields, as values for filters, as values for conditional operations, or as default values. When used as a conditional value or default value, variables should be treated like any other field and enclosed in braces as conditional and default values are evaluated as template strings. For example: `{var:name,Katie}{person contains {%name}?{%name},Not-{%name}}`.
If you need to use a `%` (percent sign character), you can escape the percent sign by using `%%`. You can also use the `{percent}` template field where a template field is required. For example:
`{title[:,%%]}` replaces the `:` with `%` and `{title contains Foo?{title}{percent},{title}}` adds `%` to the title if it contains `Foo`.

View File

@@ -6,7 +6,7 @@ In its simplest form, a template statement has the form: `"{template_field}"`, f
Template statements may contain one or more modifiers. The full syntax is:
`"pretext{delim+template_field:subfield|filter(path_sep)[find,replace] conditional?bool_value,default}posttext"`
`"pretext{delim+template_field:subfield(field_arg)|filter[find,replace] conditional?bool_value,default}posttext"`
Template statements are white-space sensitive meaning that white space (spaces, tabs) changes the meaning of the template statement.
@@ -27,6 +27,8 @@ e.g. if Photo keywords are `["foo","bar"]`:
`:subfield`: Some templates have sub-fields, For example, `{exiftool:IPTC:Make}`; the template_field is `exiftool` and the sub-field is `IPTC:Make`.
`(field_arg)`: optional arguments to pass to the field; for example, with `{folder_album}` this is used to pass the path separator used for joining folders and albums when rendering the field (default is "/" for `{folder_album}`).
`|filter`: You may optionally append one or more filter commands to the end of the template field using the vertical pipe ('|') symbol. Filters may be combined, separated by '|' as in: `{keyword|capitalize|parens}`.
Valid filters are:
@@ -41,6 +43,20 @@ Valid filters are:
- `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().
e.g. if Photo keywords are `["FOO","bar"]`:
@@ -53,8 +69,6 @@ e.g. if Photo description is "my description":
- `"{descr|titlecase}"` renders to: `"My Description"`
`(path_sep)`: optional path separator to use when joining path-like fields, for example `{folder_album}`. Default is "/".
e.g. If Photo is in `Album1` in `Folder1`:
- `"{folder_album}"` renders to `["Folder1/Album1"]`
@@ -97,7 +111,7 @@ This can be used to rename files as well, for example:
This renames any photo that is a favorite as 'Favorite-ImageName.jpg' (where 'ImageName.jpg' is the original name of the photo) and all other photos with the unmodified original name.
`?bool_value`: Template fields may be evaluated as boolean (True/False) by appending "?" after the field name (and following "(path_sep)" or "[find/replace]". If a field is True (e.g. photo is HDR and field is `"{hdr}"`) or has any value, the value following the "?" will be used to render the template instead of the actual field value. If the template field evaluates to False (e.g. in above example, photo is not HDR) or has no value (e.g. photo has no title and field is `"{title}"`) then the default value following a "," will be used.
`?bool_value`: Template fields may be evaluated as boolean (True/False) by appending "?" after the field name (and following "(field_arg)" or "[find/replace]". If a field is True (e.g. photo is HDR and field is `"{hdr}"`) or has any value, the value following the "?" will be used to render the template instead of the actual field value. If the template field evaluates to False (e.g. in above example, photo is not HDR) or has no value (e.g. photo has no title and field is `"{title}"`) then the default value following a "," will be used.
e.g. if photo is an HDR image,
@@ -127,3 +141,13 @@ Either or both bool_value or default (False value) may be empty which would resu
If you want to include "{" or "}" in the output, use "{openbrace}" or "{closebrace}" template substitution.
e.g. `"{created.year}/{openbrace}{title}{closebrace}"` would result in `"2020/{Photo Title}"`.
**Variables**
You can define variables for later use in the template string using the format `{var:NAME,VALUE}`. Variables may then be referenced using the format `%NAME`. For example: `{var:foo,bar}` defines the variable `%foo` to have value `bar`. This can be useful if you want to re-use a complex template value in multiple places within your template string or for allowing the use of characters that would otherwise be prohibited in a template string. For example, the "pipe" (`|`) character is not allowed in a find/replace pair but you can get around this limitation like so: `{var:pipe,{pipe}}{title[-,%pipe]}` which replaces the `-` character with `|` (the value of `%pipe`).
Variables can also be referenced as fields in the template string, for example: `{var:year,created.year}{original_name}-{%year}`. In some cases, use of variables can make your template string more readable. Variables can be used as template fields, as values for filters, as values for conditional operations, or as default values. When used as a conditional value or default value, variables should be treated like any other field and enclosed in braces as conditional and default values are evaluated as template strings. For example: `{var:name,Katie}{person contains {%name}?{%name},Not-{%name}}`.
If you need to use a `%` (percent sign character), you can escape the percent sign by using `%%`. You can also use the `{percent}` template field where a template field is required. For example:
`{title[:,%%]}` replaces the `:` with `%` and `{title contains Foo?{title}{percent},{title}}` adds `%` to the title if it contains `Foo`.

View File

@@ -5,11 +5,12 @@ import datetime
import locale
import os
import pathlib
import re
import shlex
import sys
from contextlib import suppress
from dataclasses import dataclass
from typing import Optional
from typing import List, Optional, Tuple
from textx import TextXSyntaxError, metamodel_from_file
@@ -19,7 +20,7 @@ from .datetime_formatter import DateTimeFormatter
from .exiftool import ExifToolCaching
from .path_utils import sanitize_dirname, sanitize_filename, sanitize_pathpart
from .text_detection import detect_text
from .utils import expand_and_validate_filepath, load_function
from .utils import expand_and_validate_filepath, load_function, uuid_to_shortuuid
__all__ = [
"RenderOptions",
@@ -140,6 +141,8 @@ TEMPLATE_SUBSTITUTIONS = {
"{exif.lens_model}": "Lens model from original photo's EXIF information as imported by Photos, e.g. 'iPhone 6s back camera 4.15mm f/2.2'",
"{moment}": "The moment title of the photo",
"{uuid}": "Photo's internal universally unique identifier (UUID) for the photo, a 36-character string unique to the photo, e.g. '128FB4C6-0B16-4E7D-9108-FB2E90DA1546'",
"{shortuuid}": "A shorter representation of photo's internal universally unique identifier (UUID) for the photo, "
+ "a 22-character string unique to the photo, e.g. 'JYsxugP9UjetmCbBCHXcmu'",
"{id}": "A unique number for the photo based on its primary key in the Photos database. "
+ "A sequential integer, e.g. 1, 2, 3...etc. Each asset associated with a photo (e.g. an image and Live Photo preview) will share the same id. "
+ "May be formatted using a python string format code. "
@@ -148,21 +151,23 @@ TEMPLATE_SUBSTITUTIONS = {
"{album_seq}": "An integer, starting at 0, indicating the photo's index (sequence) in the containing album. "
+ "Only valid when used in a '--filename' template and only when '{album}' or '{folder_album}' is used in the '--directory' template. "
+ 'For example \'--directory "{folder_album}" --filename "{album_seq}_{original_name}"\'. '
+ "To start counting at a value other than 0, append append a period and the starting value to the field name. "
+ "For example, to start counting at 1 instead of 0: '{album_seq.1}'. "
+ "To start counting at a value other than 0, append append '(starting_value)' to the field name. "
+ "For example, to start counting at 1 instead of 0: '{album_seq(1)}'. "
+ "May be formatted using a python string format code. "
+ "For example, to format as a 5-digit integer and pad with zeros, use '{album_seq:05d}' which results in "
+ "00000, 00001, 00002...etc. "
+ "To format while also using a starting value: '{album_seq:05d(1)}' which results in 0001, 00002...etc."
+ "This may result in incorrect sequences if you have duplicate albums with the same name; see also '{folder_album_seq}'.",
"{folder_album_seq}": "An integer, starting at 0, indicating the photo's index (sequence) in the containing album and folder path. "
+ "Only valid when used in a '--filename' template and only when '{folder_album}' is used in the '--directory' template. "
+ 'For example \'--directory "{folder_album}" --filename "{folder_album_seq}_{original_name}"\'. '
+ "To start counting at a value other than 0, append append a period and the starting value to the field name. "
+ "For example, to start counting at 1 instead of 0: '{folder_album_seq.1}' "
+ "To start counting at a value other than 0, append '(starting_value)' to the field name. "
+ "For example, to start counting at 1 instead of 0: '{folder_album_seq(1)}' "
+ "May be formatted using a python string format code. "
+ "For example, to format as a 5-digit integer and pad with zeros, use '{folder_album_seq:05d}' which results in "
+ "00000, 00001, 00002...etc. "
+ "This may result in incorrect sequences if you have duplicate albums with the same name in the same folder; see also '{album_seq}'.",
+ "To format while also using a starting value: '{folder_album_seq:05d(1)}' which results in 0001, 00002...etc."
+ "This may result in incorrect sequences if you have duplicate albums with the same name in the same folder; see also '{album_seq}'. ",
"{comma}": "A comma: ','",
"{semicolon}": "A semicolon: ';'",
"{questionmark}": "A question mark: '?'",
@@ -222,6 +227,9 @@ TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
+ "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. "
@@ -239,6 +247,27 @@ FILTER_VALUES = {
"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().",
}
# Just the substitutions without the braces
@@ -382,6 +411,7 @@ class PhotoTemplate:
self.filepath = options.filepath
self.quote = options.quote
self.dest_path = options.dest_path
self.variables = {}
def render(
self,
@@ -430,14 +460,13 @@ class PhotoTemplate:
def _render_statement(
self,
statement,
path_sep=None,
field_arg=None,
):
path_sep = path_sep or self.path_sep
results = []
unmatched = []
for ts in statement.template_strings:
results, unmatched = self._render_template_string(
ts, results=results, unmatched=unmatched, path_sep=path_sep
ts, results=results, unmatched=unmatched, field_arg=field_arg
)
rendered_strings = results
@@ -457,7 +486,7 @@ class PhotoTemplate:
def _render_template_string(
self,
ts,
path_sep,
field_arg,
results=None,
unmatched=None,
):
@@ -469,11 +498,6 @@ class PhotoTemplate:
if ts.template:
# have a template field to process
field = ts.template.field
field_part = field.split(".")[0]
if field not in FIELD_NAMES and field_part not in FIELD_NAMES:
unmatched.append(field)
return [], unmatched
subfield = ts.template.subfield
# process filters
@@ -481,14 +505,15 @@ class PhotoTemplate:
if ts.template.filter is not None:
filters = ts.template.filter.value
# process path_sep
if ts.template.pathsep is not None:
path_sep = ts.template.pathsep.value
# process field arguments
if ts.template.fieldarg is not None:
field_arg = ts.template.fieldarg.value
# process delim
if ts.template.delim is not None:
# if value is None, means format was {+field}
delim = ts.template.delim.value or ""
delim = self.expand_variables_to_str(delim, "delim")
else:
delim = None
@@ -497,7 +522,7 @@ class PhotoTemplate:
if ts.template.bool.value is not None:
bool_val, u = self._render_statement(
ts.template.bool.value,
path_sep=path_sep,
field_arg=field_arg,
)
unmatched.extend(u)
else:
@@ -513,7 +538,7 @@ class PhotoTemplate:
if ts.template.default.value is not None:
default, u = self._render_statement(
ts.template.default.value,
path_sep=path_sep,
field_arg=field_arg,
)
unmatched.extend(u)
else:
@@ -528,11 +553,11 @@ class PhotoTemplate:
negation = ts.template.conditional.negation
if ts.template.conditional.value is not None:
# conditional value is also a TemplateString
conditional_value, u = self._render_statement(
ts.template.conditional.value,
path_sep=path_sep,
)
unmatched.extend(u)
conditional_value = []
for cv in ts.template.conditional.value:
value, u = self._render_statement(cv)
conditional_value += value
unmatched.extend(u)
else:
# this shouldn't happen
conditional_value = [""]
@@ -541,43 +566,23 @@ class PhotoTemplate:
negation = None
conditional_value = []
vals = []
if (
field in SINGLE_VALUE_SUBSTITUTIONS
or field.split(".")[0] in SINGLE_VALUE_SUBSTITUTIONS
):
vals = self.get_template_value(
field,
default=default,
subfield=subfield,
# delim=delim or self.inplace_sep,
# path_sep=path_sep,
)
elif field == "exiftool":
if subfield is None:
raise ValueError(
"SyntaxError: GROUP:NAME subfield must not be null with {exiftool:GROUP:NAME}'"
if field.startswith("%"):
# variable in form {%var}
vals = self.variables.get(field[1:], None)
if vals is None:
raise SyntaxError(f"Variable '{field[1:]}' is not defined.")
elif field == "var":
if not subfield or not default:
raise SyntaxError(
"var must have a subfield and value in form {var:subfield,value}"
)
vals = self.get_template_value_exiftool(
subfield,
)
elif field == "function":
if subfield is None:
raise ValueError(
"SyntaxError: filename and function must not be null with {function::filename.py:function_name}"
)
vals = self.get_template_value_function(
subfield,
)
elif field in MULTI_VALUE_SUBSTITUTIONS or field.startswith("photo"):
vals = self.get_template_value_multi(
field, subfield, path_sep=path_sep, default=default
)
elif field.split(".")[0] in PATHLIB_SUBSTITUTIONS:
vals = self.get_template_value_pathlib(field)
self.variables[subfield] = default
vals = []
else:
unmatched.append(field)
return [], unmatched
vals, u = self.get_field_values(field, subfield, field_arg, default)
if u:
unmatched.extend(u)
return [], unmatched
vals = [val for val in vals if val is not None]
@@ -586,7 +591,7 @@ class PhotoTemplate:
vals = [sep.join(sorted(vals))] if vals else []
for filter_ in filters:
vals = self.get_template_value_filter(filter_, vals)
vals = self.get_filter_values(filter_, vals)
# process find/replace
if ts.template.findreplace:
@@ -594,7 +599,9 @@ class PhotoTemplate:
for val in vals:
for pair in ts.template.findreplace.pairs:
find = pair.find or ""
find = self.expand_variables_to_str(find, "find/replace")
repl = pair.replace or ""
repl = self.expand_variables_to_str(repl, "find/replace")
val = val.replace(find, repl)
new_vals.append(val)
vals = new_vals
@@ -621,22 +628,23 @@ class PhotoTemplate:
def comparison_test(test_function):
"""Perform numerical comparisons using test_function; closure to capture conditional_val, vals, negation"""
if len(vals) != 1 or len(conditional_value) != 1:
raise ValueError(
f"comparison operators may only be used with a single value: {vals} {conditional_value}"
# returns True if any of the values match the condition
if len(conditional_value) != 1:
raise SyntaxError(
f"comparison operators may only be used with a single conditional value: {conditional_value}"
)
try:
match = bool(
test_function(float(vals[0]), float(conditional_value[0]))
match = any(
bool(test_function(float(v), float(conditional_value[0])))
for v in vals
)
return (
["True"]
if (match and not negation) or (negation and not match)
else []
)
except ValueError as e:
raise ValueError(
raise SyntaxError(
f"comparison operators may only be used with values that can be converted to numbers: {vals} {conditional_value}"
) from e
@@ -678,7 +686,8 @@ class PhotoTemplate:
if is_bool:
vals = default if not vals else bool_val
elif not vals:
elif not vals and field != "var":
# don't assign default value if the template was variable assignment
vals = default or [self.none_str]
pre = ts.pre or ""
@@ -700,14 +709,103 @@ class PhotoTemplate:
return results, unmatched
def expand_variables_to_str(self, value: str, name: str) -> str:
"""
Expand variables in value and return a str of the expanded value.
Enforce that the expanded value is a single value, raises ValueError if not.
Args:
value: the value to expand
name: the name of the value being expanded (used in error messages)
"""
expanded = self.expand_variables(value)
if len(expanded) != 1:
raise SyntaxError(f"{name} must have a single value, not {expanded}")
return expanded[0]
def expand_variables(self, value: str) -> List[str]:
"""Expand variables in value"""
# replace any variables with their values
values = [value]
new_values = []
# allow %% to escape %, match variables in form %var
variable_match = re.compile(r"(?:%%)*(%[\w]+)?")
while True:
for value in values:
match = variable_match.search(value)
if not match or not match[1]:
break
var = match[1]
var_name = var[1:]
if var_name not in self.variables:
raise SyntaxError(f"Variable '{var_name}' is not defined.")
for val in values:
for var_val in self.variables[var_name]:
new_values.append(
re.sub(f"(%%)*{var}", r"\g<1>" + var_val, val)
)
if new_values == values or not new_values:
break
values = new_values.copy()
new_values = []
# replace %% with %
# any %% left in the string will be replaced with %
values = [value.replace("%%", "%") for value in values]
return values
def get_field_values(
self,
field: str,
subfield: Optional[str],
field_arg: Optional[str],
default: List[str],
) -> Tuple[List[str], List[str]]:
"""Get the values for a field"""
vals = []
unmatched = []
if (
field in SINGLE_VALUE_SUBSTITUTIONS
or field.split(".")[0] in SINGLE_VALUE_SUBSTITUTIONS
):
vals = self.get_template_value(
field,
default=default,
subfield=subfield,
field_arg=field_arg,
)
elif field == "exiftool":
if subfield is None:
raise ValueError(
"SyntaxError: GROUP:NAME subfield must not be null with {exiftool:GROUP:NAME}'"
)
vals = self.get_template_value_exiftool(
subfield,
)
elif field == "function":
if subfield is None:
raise ValueError(
"SyntaxError: filename and function must not be null with {function::filename.py:function_name}"
)
vals = self.get_template_value_function(subfield, field_arg)
elif field in MULTI_VALUE_SUBSTITUTIONS or field.startswith("photo"):
vals = self.get_template_value_multi(
field, subfield, path_sep=field_arg, default=default
)
elif field.split(".")[0] in PATHLIB_SUBSTITUTIONS:
vals = self.get_template_value_pathlib(field)
else:
unmatched.append(field)
return [], unmatched
return vals, unmatched
def get_template_value(
self,
field,
default,
subfield=None,
# bool_val=None,
# delim=None,
# path_sep=None,
subfield,
field_arg,
):
"""lookup value for template field (single-value template substitutions)
@@ -991,6 +1089,8 @@ class PhotoTemplate:
value = self.photo.moment_info.title if self.photo.moment_info else None
elif field == "uuid":
value = self.photo.uuid
elif field == "shortuuid":
value = uuid_to_shortuuid(self.photo.uuid) if self.photo.uuid else None
elif field == "id":
value = format_str_value(self.photo._info["pk"], subfield)
elif field.startswith("album_seq") or field.startswith("folder_album_seq"):
@@ -1004,9 +1104,8 @@ class PhotoTemplate:
else:
value = None
if value is not None:
with suppress(IndexError):
start_id = field.split(".", 1)
value = int(value) + int(start_id[1])
start_id = int(field_arg) if field_arg is not None else 0
value = int(value) + start_id
value = format_str_value(value, subfield)
else:
# if here, didn't get a match
@@ -1054,54 +1153,121 @@ class PhotoTemplate:
return [value]
def get_template_value_filter(self, filter_, values):
def get_filter_values(self, filter_: str, values: List[str]) -> List[str]:
"""Return filtered values"""
# extract args, if any
if re.search(r"\(.*\)", filter_):
filter_, args = filter_.split("(", 1)
args = args.rstrip(")")
args = self.expand_variables_to_str(args, "Filter arguments")
else:
args = None
# check that filter name (without subfields or arguments) is valid
valid_filters = [f.split("(")[0] for f in FILTER_VALUES]
if filter_.split(":")[0] not in valid_filters:
raise SyntaxError(f"Unknown filter: {filter_}")
if filter_ in [
"split",
"chop",
"chomp",
"append",
"prepend",
"remove",
"slice",
"sslice",
] and (args is None or not len(args)):
raise SyntaxError(f"{filter_} requires arguments")
if filter_ == "lower":
if values and type(values) == list:
value = [v.lower() for v in values]
else:
value = [values.lower()] if values else []
value = [v.lower() for v in values]
elif filter_ == "upper":
if values and type(values) == list:
value = [v.upper() for v in values]
else:
value = [values.upper()] if values else []
value = [v.upper() for v in values]
elif filter_ == "strip":
if values and type(values) == list:
value = [v.strip() for v in values]
else:
value = [values.strip()] if values else []
value = [v.strip() for v in values]
elif filter_ == "capitalize":
if values and type(values) == list:
value = [v.capitalize() for v in values]
else:
value = [values.capitalize()] if values else []
value = [v.capitalize() for v in values]
elif filter_ == "titlecase":
if values and type(values) == list:
value = [v.title() for v in values]
else:
value = [values.title()] if values else []
value = [v.title() for v in values]
elif filter_ == "braces":
if values and type(values) == list:
value = ["{" + v + "}" for v in values]
else:
value = ["{" + values + "}"] if values else []
value = ["{" + v + "}" for v in values]
elif filter_ == "parens":
if values and type(values) == list:
value = [f"({v})" for v in values]
else:
value = [f"({values})"] if values else []
value = ["(" + v + ")" for v in values]
elif filter_ == "brackets":
if values and type(values) == list:
value = [f"[{v}]" for v in values]
else:
value = [f"[{values}]"] if values else []
value = ["[" + v + "]" for v in values]
elif filter_ == "shell_quote":
if values and type(values) == list:
value = [shlex.quote(v) for v in values]
value = [shlex.quote(v) for v in values]
elif filter_ == "split":
# split on delimiter
delim = args
if delim:
new_values = []
for v in values:
new_values.extend(v.split(delim))
value = new_values
else:
value = [shlex.quote(values)] if values else []
value = values
elif filter_ == "chop":
# chop off characters from the end
try:
chop = int(args)
except ValueError:
raise SyntaxError(f"Invalid value for chop: {args}")
value = [v[:-chop] for v in values] if chop else values
elif filter_ == "chomp":
# chop off characters from the beginning
try:
chomp = int(args)
except ValueError:
raise SyntaxError(f"Invalid value for chomp: {args}")
value = [v[chomp:] for v in values] if chomp else values
elif filter_ == "autosplit":
# try to split keyword strings automatically
temp_values = [v.replace(",", " ") for v in values]
temp_values = [v.replace(";", " ") for v in temp_values]
value = []
for val in temp_values:
value.extend(val.split())
elif filter_ == "sort":
# sort list of values
value = sorted(values)
elif filter_ == "rsort":
# reverse sort list of values
value = sorted(values, reverse=True)
elif filter_ == "reverse":
# reverse list of values
value = values[::-1]
elif filter_ == "uniq":
# remove duplicate values from list
temp_values = []
for v in values:
if v not in temp_values:
temp_values.append(v)
value = temp_values
elif filter_ == "join":
# join list of values with delimiter
delim = args or ""
value = [delim.join(values)]
elif filter_ == "append":
# append value to list
value = values + [args]
elif filter_ == "prepend":
# prepend value to list
value = [args] + values
elif filter_ == "remove":
# remove value from list
value = [v for v in values if v != args]
elif filter_ == "slice":
# slice list of values
value = values[create_slice(args)]
elif filter_ == "sslice":
# slice each value in a list
slice_ = create_slice(args)
value = [v[slice_] for v in values]
elif filter_.startswith("function:"):
value = self.get_template_value_filter_function(filter_, values)
value = self.get_template_value_filter_function(filter_, args, values)
else:
value = []
return value
@@ -1124,6 +1290,8 @@ class PhotoTemplate:
""" return list of values for a multi-valued template field """
path_sep = path_sep or self.path_sep
if self.photo.uuid is None:
return []
@@ -1189,6 +1357,8 @@ class PhotoTemplate:
values = [shlex.quote(v) for v in default if v]
elif field == "strip":
values = [v.strip() for v in default]
elif field == "format":
values = self.get_format_values(field, subfield, default)
elif field.startswith("photo"):
# provide access to PhotoInfo object
properties = field.split(".")
@@ -1203,10 +1373,11 @@ class PhotoTemplate:
obj = getattr(obj, property_)
if obj is None:
break
except AttributeError:
except AttributeError as e:
raise ValueError(
"Invalid property for {photo} template: " + f"'{property_}'"
)
) from e
if obj is None:
values = []
elif isinstance(obj, bool):
@@ -1231,6 +1402,31 @@ class PhotoTemplate:
values = values or []
return values
def get_format_values(
self, field: str, subfield: str, default: List[str]
) -> Optional[List[Optional[str]]]:
"""Return values for {format} templates"""
if field != "format":
raise ValueError(f"Unhandled template value in get_format_values: {field}")
if not subfield or ":" not in subfield:
raise SyntaxError("{format} requires subfield in form TYPE:FORMAT")
type_, format_str = subfield.split(":", 1)
if type_ not in ("int", "float", "str"):
raise SyntaxError(
f"'{type_}' is not a valid type for {format}: must be one of 'int', 'float', 'str'"
)
if type_ == "int":
# convert to float then int to avoid error when converting a string float to int
default_ = [int(float(v)) for v in default]
elif type_ == "float":
default_ = [float(v) for v in default]
else:
default_ = default
format_str = self.expand_variables_to_str(format_str, "format string")
return [format_str_value(v, format_str) for v in default_]
def get_template_value_exiftool(
self,
subfield,
@@ -1264,6 +1460,7 @@ class PhotoTemplate:
def get_template_value_function(
self,
subfield,
field_arg,
):
"""Get template value from external function"""
@@ -1279,7 +1476,13 @@ class PhotoTemplate:
raise ValueError(f"'{filename}' does not appear to be a file")
template_func = load_function(filename_validated, funcname)
values = template_func(self.photo, options=self.options)
if self.photo.uuid is None:
# must be a PhotoInfoNone instance
# if no uuid, then template is being validated but not actually run
# so don't run the function
values = []
else:
values = template_func(self.photo, options=self.options, args=field_arg)
if not isinstance(values, (str, list)):
raise TypeError(
@@ -1297,8 +1500,9 @@ class PhotoTemplate:
return values
def get_template_value_filter_function(self, filter_, values):
def get_template_value_filter_function(self, filter_, args, values):
"""Filter template value from external function"""
# TODO: add args to filter function call? Would change signature of function
filter_ = filter_.replace("function:", "")
@@ -1317,7 +1521,11 @@ class PhotoTemplate:
if not isinstance(values, (list, tuple)):
values = [values]
values = template_func(values)
if self.photo.uuid is not None:
# if uuid is None, it's a PhotoInfoNone instance and template is being validated
# so don't run the function
values = template_func(values)
if not isinstance(values, list):
raise TypeError(
@@ -1329,10 +1537,7 @@ class PhotoTemplate:
def get_photo_video_type(self, default):
"""return media type, e.g. photo or video"""
default_dict = parse_default_kv(default, PHOTO_VIDEO_TYPE_DEFAULTS)
if self.photo.isphoto:
return default_dict["photo"]
else:
return default_dict["video"]
return default_dict["photo"] if self.photo.isphoto else default_dict["video"]
def get_media_type(self, default):
"""return special media type, e.g. slow_mo, panorama, etc., defaults to photo or video if no special type"""
@@ -1360,12 +1565,8 @@ class PhotoTemplate:
return default_dict["photo"]
def get_photo_bool_attribute(self, attr, default, bool_val):
# get value for a PhotoInfo bool attribute
val = getattr(self.photo, attr)
if val:
return bool_val
else:
return default
"""Return the boolean value for a photo attribute"""
return bool_val if (val := getattr(self.photo, attr)) else default
def parse_default_kv(default, default_dict):
@@ -1489,3 +1690,25 @@ def _get_detected_text(photo, confidence=TEXT_DETECTION_CONFIDENCE_THRESHOLD):
# so the first time this gets called is slow but repeated accesses are fast
detected_text = photo._detected_text()
return [text for text, conf in detected_text if conf >= confidence]
def create_slice(args):
"""Create a slice object from a string of args in form "start:end:step" """
slice_args = args.split(":")
if len(slice_args) == 1:
start = int(slice_args[0] or 0)
end = None
step = None
elif len(slice_args) == 2:
start, end = slice_args
start = int(start) if start != "" else None
end = int(end) if end != "" else None
step = None
elif len(slice_args) == 3:
start, end, step = slice_args
start = int(start) if start != "" else None
end = int(end) if end != "" else None
step = int(step) if step != "" else None
else:
raise SyntaxError(f"Invalid slice: {args}")
return slice(start, end, step)

View File

@@ -1,6 +1,6 @@
// OSXPhotos Metadata Template Language (MTL)
// a TemplateString has format:
// pre{delim+template_field:subfield|filter(path_sep)[find,replace] conditional?bool_value,default}post
// pre{delim+template_field:subfield(field_arg)|filter[find,replace] conditional?bool_value,default}post
// a TemplateStatement may contain zero or more TemplateStrings
// The pre and post are optional strings
// The template itself (inside the {}) is also optional but if present
@@ -22,8 +22,8 @@ Template:
delim=Delim
field=Field
subfield=SubField
fieldarg=FieldArg
filter=Filter
pathsep=PathSep
findreplace=FindReplace
conditional=Conditional
bool=Boolean
@@ -52,7 +52,7 @@ Field:
;
FIELD_WORD:
/[\.\w]+/
/[\%]?[\.\w]+/
;
SubField:
@@ -70,21 +70,22 @@ SUBFIELD_WORD:
Filter:
(
"|"-
(value+=FILTER_WORD['|'])?
(value+=FILTER_FUNCTION['|'])?
)?
;
FILTER_WORD:
/[\.\w:\/]+/
FILTER_FUNCTION:
/[\.\w:\/]+(\([^\)]*\))?/
;
Conditional:
(
(" "+)-
(negation=NEGATION)?
(operator=OPERATOR)
(" "+)-
(value=Statement)
(value+=Statement['|'])
)?
;
@@ -96,7 +97,7 @@ OPERATOR:
"contains" | "matches" | "startswith" | "endswith" | "<=" | ">=" | "<" | ">" | "==" | "!="
;
PathSep:
FieldArg:
(
"("
(value=/[^\(\)\{\}]+/)?

View File

@@ -32,7 +32,7 @@ def detect_text(img_path: str, orientation: Optional[int] = None) -> List:
orientation: optional EXIF orientation (if known, passing orientation may improve quality of results)
"""
if not vision:
logging.warning(f"detect_text not implemented for this version of macOS")
logging.warning("detect_text not implemented for this version of macOS")
return []
with objc.autorelease_pool():
@@ -47,18 +47,18 @@ def detect_text(img_path: str, orientation: Optional[int] = None) -> List:
input_image = Quartz.CIImage.imageWithContentsOfURL_(input_url)
vision_options = NSDictionary.dictionaryWithDictionary_({})
if orientation is not None:
if not 1 <= orientation <= 8:
raise ValueError("orientation must be between 1 and 8")
vision_handler = Vision.VNImageRequestHandler.alloc().initWithCIImage_orientation_options_(
input_image, orientation, vision_options
)
else:
if orientation is None:
vision_handler = (
Vision.VNImageRequestHandler.alloc().initWithCIImage_options_(
input_image, vision_options
)
)
elif 1 <= orientation <= 8:
vision_handler = Vision.VNImageRequestHandler.alloc().initWithCIImage_orientation_options_(
input_image, orientation, vision_options
)
else:
raise ValueError("orientation must be between 1 and 8")
results = []
handler = make_request_handler(results)
vision_request = (

View File

@@ -178,6 +178,14 @@ A powerful feature of Photos is that it uses machine learning algorithms to auto
`osxphotos export /path/to/export --exiftool --keyword-template "{label}"`
## Removing a keyword during export
If some of your photos contain a keyword you do not want to be added to the exported file with `--exiftool`, you can use the template system to remove the keyword from the exported file. For example, if you want to remove the keyword "MyKeyword" from all your photos:
`osxphotos export /path/to/export --exiftool --keyword-template "{keyword|remove(MyKeyword)}" --replace-keywords`
In this example, `|remove(MyKeyword)` is a filter which removes `MyKeyword` from the keyword list of every photo being processed. The `--replace-keywords` option instructs osxphotos to replace the keywords in the exported file with the filtered keywords from `--keyword-template`.
**Note**: When evaluating templates for `--directory` and `--filename`, osxphotos inserts the automatic default value "_" for any template field which is null (empty or blank). This is to ensure that there's never a null directory or filename created. For metadata templates such as `--keyword-template`, osxphotos does not provide an automatic default value thus if the template field is null, no keyword would be created. Of course, you can provide a default value if desired and osxphotos will use this. For example, to add "nolabel" as a keyword for any photo that doesn't have labels:
`osxphotos export /path/to/export --exiftool --keyword-template "{label,nolabel}"`

View File

@@ -19,9 +19,11 @@ import unicodedata
import urllib.parse
from plistlib import load as plistload
from typing import Callable, List, Optional, Tuple, Union
from uuid import UUID
import CoreFoundation
import requests
import shortuuid
from ._constants import UNICODE_FORMAT
@@ -41,6 +43,8 @@ __all__ = [
"normalize_fs_path",
"normalize_unicode",
"pluralize",
"shortuuid_to_uuid",
"uuid_to_shortuuid",
]
@@ -411,7 +415,7 @@ def normalize_unicode(value):
def increment_filename_with_count(
filepath: Union[str, pathlib.Path], count: int = 0
) -> str:
) -> Tuple[str, int]:
"""Return filename (1).ext, etc if filename.ext exists
If file exists in filename's parent folder with same stem as filename,
@@ -447,6 +451,7 @@ def increment_filename(filepath: Union[str, pathlib.Path]) -> str:
Args:
filepath: str or pathlib.Path; full path, including file name
force: force the file count to increment by at least 1 even if filepath doesn't exist
Returns:
new filepath (or same if not incremented)
@@ -457,6 +462,13 @@ def increment_filename(filepath: Union[str, pathlib.Path]) -> str:
return new_filepath
def extract_increment_count_from_filename(filepath: Union[str, pathlib.Path]) -> int:
"""Extract a count from end of file name if it exists or 0 if not; count takes forms file (1).ext, file (2).ext, etc."""
filepath = str(filepath)
match = re.search(r"(?s:.*)\((\d+)\)", filepath)
return int(match[1]) if match else 0
def expand_and_validate_filepath(path: str) -> str:
"""validate and expand ~ in filepath, also un-escapes spaces
@@ -526,3 +538,13 @@ def hexdigest(strval: str) -> str:
h = hashlib.blake2b(digest_size=20)
h.update(bytes(strval, "utf-8"))
return h.hexdigest()
def uuid_to_shortuuid(uuid: str) -> str:
"""Convert uuid to shortuuid"""
return str(shortuuid.encode(UUID(uuid)))
def shortuuid_to_uuid(short_uuid: str) -> str:
"""Convert shortuuid to uuid"""
return str(shortuuid.decode(short_uuid)).upper()

View File

@@ -24,6 +24,7 @@ PyYAML>=5.4.1,<6.0.0
requests>=2.27.1,<3.0.0
rich_theme_manager>=0.11.0
rich>=11.2.0,<13.0.0
shortuuid==1.0.9
tenacity>=8.0.1,<9.0.0
textx>=2.3.0,<2.4.0
toml>=0.10.2,<0.11.0

View File

@@ -99,6 +99,7 @@ setup(
"requests>=2.27.1,<3.0.0",
"rich>=11.2.0,<13.0.0",
"rich_theme_manager>=0.11.0",
"shortuuid==1.0.9",
"tenacity>=8.0.1,<9.0.0",
"textx>=2.3.0,<3.0.0",
"toml>=0.10.2,<0.11.0",

View File

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

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>hostname</key>
<string>tc1.local</string>
<key>hostuuid</key>
<string>A22F6630-733E-54D7-99F1-B3805B38E11B</string>
<key>pid</key>
<integer>548</integer>
<key>processname</key>
<string>photolibraryd</string>
<key>uid</key>
<integer>501</integer>
</dict>
</plist>

View File

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

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>embeddingVersion</key>
<string>1</string>
<key>featureFlags</key>
<string>63</string>
<key>featuredContentAllowed</key>
<string>1</string>
<key>localeIdentifier</key>
<string>en_GB</string>
<key>sceneTaxonomySHA</key>
<string>35f76559cb0770342bcea46fce0e3f562844ab7506e0e4b4ad1256abb0a07be2,4afa5d3c45c08a664cf73cff957aaeeae3a325d2970aada51268407b9ad0f03e</string>
<key>searchIndexVersion</key>
<string>16016</string>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 KiB

View File

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

View File

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

View File

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

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