Compare commits

...

35 Commits

Author SHA1 Message Date
Rhet Turnbull
c8ea0b0452 Added re to photosdb for use with query_eval 2021-04-18 13:24:02 -07:00
Rhet Turnbull
81fd51c793 Cleaned up queryoptions.py 2021-04-18 09:07:24 -07:00
Rhet Turnbull
648d399524 Updated CHANGELOG.md, [skip ci] 2021-04-18 09:05:28 -07:00
Rhet Turnbull
345c052353 Refactored _query to PhotosDB.query() 2021-04-18 08:32:13 -07:00
Rhet Turnbull
952f1a6c3c Fixed setup.py 2021-04-17 17:58:15 -07:00
Rhet Turnbull
7ae5b8aae7 Added --min-size, --max-size query options, #425 2021-04-17 17:56:48 -07:00
Rhet Turnbull
2e189d771e Updated docs, added build.sh 2021-04-17 10:00:34 -07:00
Rhet Turnbull
7fa7de1563 Added {newline}, #426 2021-04-17 09:29:11 -07:00
Rhet Turnbull
70d68a25ba Updated docs, closes #424 2021-04-17 03:03:23 -07:00
Rhet Turnbull
bfc4371d9e Updated CHANGELOG.md, [skip ci] 2021-04-17 02:57:55 -07:00
Rhet Turnbull
6a288676a1 Fixed bug for multi-field templates and --xattr-template, #422 2021-04-17 02:41:29 -07:00
Rhet Turnbull
874ad2fa34 Add @ubrandes as a contributor 2021-04-15 06:46:10 -07:00
Rhet Turnbull
a233167471 Updated CHANGELOG.md, [skip ci] 2021-04-14 22:17:00 -07:00
Rhet Turnbull
21dc0d388f Added {function} template, #419 2021-04-14 22:00:04 -07:00
Rhet Turnbull
eff8e7a63f Added template_function.py to examples 2021-04-14 20:20:46 -07:00
Rhet Turnbull
03f8b2bc6e Implements conditional expressions for template system, #417 2021-04-13 06:20:56 -07:00
Rhet Turnbull
e215c200c7 Updated CHANGELOG.md, [skip ci] 2021-04-11 23:54:40 -07:00
Rhet Turnbull
ae5b02f563 Added additional test for {photo} template 2021-04-11 23:49:54 -07:00
Rhet Turnbull
aa1a96d201 Added {photo} template, partial fix for issue #417 2021-04-11 23:36:17 -07:00
Rhet Turnbull
d9f24307ac Added {favorite} template, partial fix for #289 2021-04-11 19:45:50 -07:00
Rhet Turnbull
958f8c343a Doc updates 2021-04-09 07:36:02 -07:00
Rhet Turnbull
70cf4c9f92 Updated CHANGELOG.md, [skip ci] 2021-04-09 06:51:35 -07:00
Rhet Turnbull
2d3344ee34 Updated docs for --query-eval 2021-04-09 06:37:51 -07:00
Rhet Turnbull
b4bc906b6a Added --query-eval, implements #280 2021-04-08 22:15:58 -07:00
Rhet Turnbull
520a15fac6 Updated CHANGELOG.md, [skip ci] 2021-04-08 18:32:53 -07:00
Rhet Turnbull
032dff8967 Bug fix for #414, exiftool str replace 2021-04-05 13:11:17 -07:00
Rhet Turnbull
3c36b0fb33 Updated CHANGELOG.md, [skip ci] 2021-04-03 21:07:41 -07:00
Rhet Turnbull
d51d7a41e4 Added --name to search filename, closes #249, #412 2021-04-03 20:23:03 -07:00
Rhet Turnbull
60c926fea5 Updated CHANGELOG.md, [skip ci] 2021-04-03 08:03:10 -07:00
Rhet Turnbull
db27aac14b Added test for #409 2021-04-02 21:44:45 -07:00
Rhet Turnbull
d17454772c Update phototemplate.py
Fix for non-str values in exiftool template (#409)
2021-03-30 07:51:34 -06:00
dependabot[bot]
9c9e73ba96 Bump pygments from 2.6.1 to 2.7.4 (#408)
Bumps [pygments](https://github.com/pygments/pygments) from 2.6.1 to 2.7.4.
- [Release notes](https://github.com/pygments/pygments/releases)
- [Changelog](https://github.com/pygments/pygments/blob/master/CHANGES)
- [Commits](https://github.com/pygments/pygments/compare/2.6.1...2.7.4)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-30 07:37:28 -06:00
Rhet Turnbull
e21a78c2b3 Removed logging.debug code 2021-03-28 07:05:29 -07:00
Rhet Turnbull
de0fbf2bb9 Updated CHANGELOG.md, [skip ci] 2021-03-28 06:44:53 -07:00
Rhet Turnbull
b330e27fb8 Added --retry, issue #406 2021-03-27 22:40:56 -07:00
38 changed files with 2023 additions and 620 deletions

View File

@@ -193,6 +193,15 @@
"contributions": [
"code"
]
},
{
"login": "ubrandes",
"name": "ubrandes ",
"avatar_url": "https://avatars.githubusercontent.com/u/59647284?v=4",
"profile": "https://github.com/ubrandes",
"contributions": [
"ideas"
]
}
],
"contributorsPerLine": 7,

View File

@@ -4,6 +4,84 @@ 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.42.4](https://github.com/RhetTbull/osxphotos/compare/v0.42.3...v0.42.4)
> 18 April 2021
- Added --min-size, --max-size query options, #425 [`7ae5b8a`](https://github.com/RhetTbull/osxphotos/commit/7ae5b8aae78621c5b7501f9faa5e0f7f4d815ba1)
- Updated docs, added build.sh [`2e189d7`](https://github.com/RhetTbull/osxphotos/commit/2e189d771edaf18c1ebffd558e3e84e43bff2f08)
- Fixed setup.py [`952f1a6`](https://github.com/RhetTbull/osxphotos/commit/952f1a6c3c3f3c7a55c0a270e73a13c4da6d2375)
#### [v0.42.3](https://github.com/RhetTbull/osxphotos/compare/v0.42.2...v0.42.3)
> 17 April 2021
- Updated docs, closes #424 [`#424`](https://github.com/RhetTbull/osxphotos/issues/424)
- Added {newline}, #426 [`7fa7de1`](https://github.com/RhetTbull/osxphotos/commit/7fa7de15631958a973514fe1a9c2cbf4301b6301)
#### [v0.42.2](https://github.com/RhetTbull/osxphotos/compare/v0.42.1...v0.42.2)
> 17 April 2021
- Fixed bug for multi-field templates and --xattr-template, #422 [`6a28867`](https://github.com/RhetTbull/osxphotos/commit/6a288676a14ce23380181d43db19128afdda7731)
- Add @ubrandes as a contributor [`874ad2f`](https://github.com/RhetTbull/osxphotos/commit/874ad2fa34d8306c071cd479625a9aa97f6488b2)
#### [v0.42.1](https://github.com/RhetTbull/osxphotos/compare/v0.41.11...v0.42.1)
> 15 April 2021
- Implements conditional expressions for template system, #417 [`03f8b2b`](https://github.com/RhetTbull/osxphotos/commit/03f8b2bc6ed53d3176f9d1ac51c3e4469db3e94b)
- Added {function} template, #419 [`21dc0d3`](https://github.com/RhetTbull/osxphotos/commit/21dc0d388f508c33526ba7510d78c71abd1151a9)
- Added template_function.py to examples [`eff8e7a`](https://github.com/RhetTbull/osxphotos/commit/eff8e7a63ff77e80fff0ce53fe56f5a010f55ab5)
#### [v0.41.11](https://github.com/RhetTbull/osxphotos/compare/v0.41.10...v0.41.11)
> 12 April 2021
- Doc updates [`958f8c3`](https://github.com/RhetTbull/osxphotos/commit/958f8c343a93ba60c1182df32727143a750f7b15)
- Added {photo} template, partial fix for issue #417 [`aa1a96d`](https://github.com/RhetTbull/osxphotos/commit/aa1a96d20118916a558b08e7f8ec87c43abf789b)
- Added {favorite} template, partial fix for #289 [`d9f2430`](https://github.com/RhetTbull/osxphotos/commit/d9f24307acc9f3f7cfa01c5e47f161b3aa390a81)
#### [v0.41.10](https://github.com/RhetTbull/osxphotos/compare/v0.41.9...v0.41.10)
> 9 April 2021
- Added --query-eval, implements #280 [`b4bc906`](https://github.com/RhetTbull/osxphotos/commit/b4bc906b6a1c3444c5f5a5d9d908ab8c955c8f7e)
#### [v0.41.9](https://github.com/RhetTbull/osxphotos/compare/v0.41.8...v0.41.9)
> 5 April 2021
- Bug fix for #414, exiftool str replace [`032dff8`](https://github.com/RhetTbull/osxphotos/commit/032dff89677f049a234d9f498951b8b402d1b31c)
#### [v0.41.8](https://github.com/RhetTbull/osxphotos/compare/v0.41.7...v0.41.8)
> 4 April 2021
- Added --name to search filename, closes #249, #412 [`#249`](https://github.com/RhetTbull/osxphotos/issues/249)
#### [v0.41.7](https://github.com/RhetTbull/osxphotos/compare/v0.41.6...v0.41.7)
> 3 April 2021
- Bump pygments from 2.6.1 to 2.7.4 [`#408`](https://github.com/RhetTbull/osxphotos/pull/408)
- Removed logging.debug code [`e21a78c`](https://github.com/RhetTbull/osxphotos/commit/e21a78c2b39ee82610394b447a9aa697e489c3e4)
- Added test for #409 [`db27aac`](https://github.com/RhetTbull/osxphotos/commit/db27aac14bbaff0b2db44f8b2d41022ebcad18a7)
- Update phototemplate.py [`d174547`](https://github.com/RhetTbull/osxphotos/commit/d17454772cebbd6edd5d8e0f04e80feecbdb2355)
#### [v0.41.6](https://github.com/RhetTbull/osxphotos/compare/v0.41.5...v0.41.6)
> 28 March 2021
- Added --retry, issue #406 [`b330e27`](https://github.com/RhetTbull/osxphotos/commit/b330e27fb838b702cefcbdb588c2fbb924b4cbc4)
#### [v0.41.5](https://github.com/RhetTbull/osxphotos/compare/v0.41.4...v0.41.5)
> 27 March 2021
- Bump pyyaml from 5.1.2 to 5.4 [`#402`](https://github.com/RhetTbull/osxphotos/pull/402)
- Fixed albums for burst images, closes #401, #403, #404 [`#401`](https://github.com/RhetTbull/osxphotos/issues/401)
#### [v0.41.4](https://github.com/RhetTbull/osxphotos/compare/v0.41.3...v0.41.4)
> 22 March 2021

206
README.md
View File

@@ -4,7 +4,7 @@
[![tests](https://github.com/RhetTbull/osxphotos/workflows/Tests/badge.svg)](https://github.com/RhetTbull/osxphotos/workflows/Tests/badge.svg)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/osxphotos)
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-20-orange.svg?style=flat)](#contributors)
[![All Contributors](https://img.shields.io/badge/all_contributors-21-orange.svg?style=flat)](#contributors)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
OSXPhotos provides the ability to interact with and query Apple's Photos.app library on macOS. You can query the Photos library database — for example, file name, file path, and metadata such as keywords/tags, persons/faces, albums, etc. You can also easily export both the original and edited photos.
@@ -203,6 +203,11 @@ Options:
searches top level folders (e.g. does not look
at subfolders)
--name FILENAME Search for photos with filename matching
FILENAME. If more than one --name options is
specified, they are treated as "OR", e.g. find
photos matching any FILENAME.
--uuid UUID Search for photos with UUID(s).
--uuid-from-file FILE Search for photos with UUID(s) loaded from
FILE. Format is a single UUID per line. Lines
@@ -310,6 +315,37 @@ Options:
albums.
--not-in-album Search for photos that are not in any albums.
--min-size SIZE Search for photos with size >= SIZE bytes. The
size evaluated is the photo's original size
(when imported to Photos). Size may be
specified as integer bytes or using SI or NIST
units. For example, the following are all
valid and equivalent sizes: '1048576'
'1.048576MB', '1 MiB'.
--max-size SIZE Search for photos with size <= SIZE bytes. The
size evaluated is the photo's original size
(when imported to Photos). Size may be
specified as integer bytes or using SI or NIST
units. For example, the following are all
valid and equivalent sizes: '1048576'
'1.048576MB', '1 MiB'.
--query-eval CRITERIA Evaluate CRITERIA to filter photos. CRITERIA
will be evaluated in context of the following
python list comprehension: `photos = [photo
for photo in photos if CRITERIA]` where photo
represents a PhotoInfo object. For example:
`--query-eval photo.favorite` returns all
photos that have been favorited and is
equivalent to --favorite. You may specify more
than one CRITERIA by using --query-eval
multiple times. CRITERIA must be a valid
python expression. See
https://rhettbull.github.io/osxphotos/ for
additional documentation on the PhotoInfo
class.
--missing Export only photos missing from the Photos
library; must be used with --download-missing.
@@ -366,6 +402,11 @@ Options:
may create name collisions on export. (e.g. if
two files happen to have the same name)
--retry RETRY Automatically retry export up to RETRY times
if an error occurs during export. This may be
useful with network drives that experience
intermittent errors.
--export-by-date Automatically create output folders to
organize photos by date created (e.g.
DEST/2019/12/20/photoname.jpg).
@@ -766,8 +807,8 @@ example "{title}" which would resolve to the title of the photo.
Template statements may contain one or more modifiers. The full syntax is:
"pretext{delim+template_field:subfield|filter(path_sep)[find,replace]?bool_value
,default}posttext"
"pretext{delim+template_field:subfield|filter(path_sep)[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.
@@ -833,20 +874,75 @@ e.g. If Photo is in Album1 in Folder1:
• "{folder_album(>)}" renders to ["Folder1>Album1"]
• "{folder_album()}" renders to ["Folder1Album1"]
[find|replace]: optional text replacement to perform on rendered template value.
[find,replace]: optional text replacement to perform on rendered template value.
For example, to replace "/" in an album name, you could use the template
"{album[/,-]}". Multiple replacements can be made by appending "|" and adding
another find|replace pair. e.g. to replace both "/" and ":" in album name:
"{album[/,-|:,-]}". find/replace pairs are not limited to single characters.
The "|" character cannot be used in a find/replace pair.
?bool_value: Template fields may be evaluated as boolean 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.
conditional: optional conditional expression that is evaluated as boolean
(True/False) for use with the ?bool_value modifier. Conditional expressions
take the form ' not operator value' where not is an optional modifier that
negates the operator. Note: the space before the conditional expression is
required if you use a conditional expression. Valid comparison operators are:
• contains: template field contains value, similar to python's in
• matches: template field contains exactly value, unlike contains: does not
match partial matches
• startswith: template field starts with value
• endswith: template field ends with value
• <=: template field is less than or equal to value
• >=: template field is greater than or equal to value
• <: template field is less than value
• >: template field is greater than value
• ==: template field equals value
• !=: template field does not equal value
The value part of the conditional expression is treated as a bare (unquoted)
word/phrase. Multiple values may be separated by '|' (the pipe symbol). value
is itself a template statement so you can use one or more template fields in
value which will be resolved before the comparison occurs.
For example:
• {keyword matches Beach} resolves to True if 'Beach' is a keyword. It would
not match keyword 'BeachDay'.
• {keyword contains Beach} resolves to True if any keyword contains the word
'Beach' so it would match both 'Beach' and 'BeachDay'.
• {photo.score.overall > 0.7} resolves to True if the photo's overall aesthetic
score is greater than 0.7.
• {keyword|lower contains beach} uses the lower case filter to do
case-insensitive matching to match any keyword that contains the word
'beach'.
• {keyword|lower not contains beach} uses the not modifier to negate the
comparison so this resolves to True if there is no keyword that matches
'beach'.
Examples: to export photos that contain certain keywords with the osxphotos
export command's --directory option:
--directory "{keyword|lower matches
travel|vacation?Travel-Photos,Not-Travel-Photos}"
This exports any photo that has keywords 'travel' or 'vacation' into a directory
'Travel-Photos' and all other photos into directory 'Not-Travel-Photos'.
This can be used to rename files as well, for example: --filename
"{favorite?Favorite-{original_name},{original_name}}"
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.
e.g. if photo is an HDR image,
@@ -937,6 +1033,10 @@ Substitution Description
True/False value, use in format
'{edited?VALUE_IF_TRUE,VALUE_IF_FALSE}'
{favorite} Photo has been marked as favorite?; True/False
value, use in format
'{favorite?VALUE_IF_TRUE,VALUE_IF_FALSE}'
{created.date} Photo's creation date in ISO format, e.g.
'2020-03-22'
@@ -1127,6 +1227,7 @@ Substitution Description
{comma} A comma: ','
{semicolon} A semicolon: ';'
{questionmark} A question mark: '?'
{pipe} A vertical pipe: '|'
{openbrace} An open brace: '{'
{closebrace} A close brace: '}'
@@ -1134,6 +1235,10 @@ Substitution Description
{closeparens} A close parentheses: ')'
{openbracket} An open bracket: '['
{closebracket} A close bracket: ']'
{newline} A newline: '\n'
{lf} A line feed: '\n', alias for {newline}
{cr} A carriage return: '\r'
{crlf} a carriage return + line feed: '\r\n'
The following substitutions may result in multiple values. Thus if specified for
--directory these could result in multiple copies of a photo being being
@@ -1151,7 +1256,11 @@ Substitution Description
{keyword} Keyword(s) assigned to photo
{person} Person(s) / face(s) in a photo
{label} Image categorization label associated with a photo
(Photos 5+ only)
(Photos 5+ only). Labels are added automatically by
Photos using machine learning algorithms to
categorize images. These are not the same as
{keyword} which refers to the user-defined
keywords/tags applied in Photos.
{label_normalized} All lower case version of 'label' (Photos 5+ only)
{comment} Comment(s) on shared Photos; format is 'Person name:
@@ -1184,6 +1293,28 @@ Substitution Description
'Restaurant'; (Photos 5+ only, applied automatically
by Photos' image categorization algorithms).
{photo} Provides direct access to the PhotoInfo object for
the photo. Must be used in format '{photo.property}'
where 'property' represents a PhotoInfo property. For
example: '{photo.favorite}' is the same as
'{favorite}' and '{photo.place.name}' is the same as
'{place.name}'. '{photo}' provides access to
properties that are not available as separate
template fields but it assumes some knowledge of the
underlying PhotoInfo class. See
https://rhettbull.github.io/osxphotos/ for additional
documentation on the PhotoInfo class.
{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.
```
@@ -2060,7 +2191,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]?bool_value,default}posttext"`
`"pretext{delim+template_field:subfield|filter(path_sep)[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.
@@ -2116,9 +2247,43 @@ e.g. If Photo is in `Album1` in `Folder1`:
- `"{folder_album(>)}"` renders to `["Folder1>Album1"]`
- `"{folder_album()}"` renders to `["Folder1Album1"]`
`[find|replace]`: optional text replacement to perform on rendered template value. For example, to replace "/" in an album name, you could use the template `"{album[/,-]}"`. Multiple replacements can be made by appending "|" and adding another find|replace pair. e.g. to replace both "/" and ":" in album name: `"{album[/,-|:,-]}"`. find/replace pairs are not limited to single characters. The "|" character cannot be used in a find/replace pair.
`[find,replace]`: optional text replacement to perform on rendered template value. For example, to replace "/" in an album name, you could use the template `"{album[/,-]}"`. Multiple replacements can be made by appending "|" and adding another find|replace pair. e.g. to replace both "/" and ":" in album name: `"{album[/,-|:,-]}"`. find/replace pairs are not limited to single characters. The "|" character cannot be used in a find/replace pair.
`?bool_value`: Template fields may be evaluated as boolean 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.
`conditional`: optional conditional expression that is evaluated as boolean (True/False) for use with the `?bool_value` modifier. Conditional expressions take the form '` not operator value`' where `not` is an optional modifier that negates the `operator`. Note: the space before the conditional expression is required if you use a conditional expression. Valid comparison operators are:
- `contains`: template field contains value, similar to python's `in`
- `matches`: template field contains exactly value, unlike `contains`: does not match partial matches
- `startswith`: template field starts with value
- `endswith`: template field ends with value
- `<=`: template field is less than or equal to value
- `>=`: template field is greater than or equal to value
- `<`: template field is less than value
- `>`: template field is greater than value
- `==`: template field equals value
- `!=`: template field does not equal value
The `value` part of the conditional expression is treated as a bare (unquoted) word/phrase. Multiple values may be separated by '|' (the pipe symbol). `value` is itself a template statement so you can use one or more template fields in `value` which will be resolved before the comparison occurs.
For example:
- `{keyword matches Beach}` resolves to True if 'Beach' is a keyword. It would not match keyword 'BeachDay'.
- `{keyword contains Beach}` resolves to True if any keyword contains the word 'Beach' so it would match both 'Beach' and 'BeachDay'.
- `{photo.score.overall > 0.7}` resolves to True if the photo's overall aesthetic score is greater than 0.7.
- `{keyword|lower contains beach}` uses the lower case filter to do case-insensitive matching to match any keyword that contains the word 'beach'.
- `{keyword|lower not contains beach}` uses the `not` modifier to negate the comparison so this resolves to True if there is no keyword that matches 'beach'.
Examples: to export photos that contain certain keywords with the `osxphotos export` command's `--directory` option:
`--directory "{keyword|lower matches travel|vacation?Travel-Photos,Not-Travel-Photos}"`
This exports any photo that has keywords 'travel' or 'vacation' into a directory 'Travel-Photos' and all other photos into directory 'Not-Travel-Photos'.
This can be used to rename files as well, for example:
`--filename "{favorite?Favorite-{original_name},{original_name}}"`
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.
e.g. if photo is an HDR image,
@@ -2680,6 +2845,7 @@ The following template field substitutions are availabe for use with `PhotoInfo.
|{photo_or_video}|'photo' or 'video' depending on what type the image is. To customize, use default value as in '{photo_or_video,photo=fotos;video=videos}'|
|{hdr}|Photo is HDR?; True/False value, use in format '{hdr?VALUE_IF_TRUE,VALUE_IF_FALSE}'|
|{edited}|Photo has been edited (has adjustments)?; True/False value, use in format '{edited?VALUE_IF_TRUE,VALUE_IF_FALSE}'|
|{favorite}|Photo has been marked as favorite?; True/False value, use in format '{favorite?VALUE_IF_TRUE,VALUE_IF_FALSE}'|
|{created.date}|Photo's creation date in ISO format, e.g. '2020-03-22'|
|{created.year}|4-digit year of photo creation time|
|{created.yy}|2-digit year of photo creation time|
@@ -2739,6 +2905,7 @@ The following template field substitutions are availabe for use with `PhotoInfo.
|{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'|
|{comma}|A comma: ','|
|{semicolon}|A semicolon: ';'|
|{questionmark}|A question mark: '?'|
|{pipe}|A vertical pipe: '|'|
|{openbrace}|An open brace: '{'|
|{closebrace}|A close brace: '}'|
@@ -2746,11 +2913,15 @@ The following template field substitutions are availabe for use with `PhotoInfo.
|{closeparens}|A close parentheses: ')'|
|{openbracket}|An open bracket: '['|
|{closebracket}|A close bracket: ']'|
|{newline}|A newline: '\n'|
|{lf}|A line feed: '\n', alias for {newline}|
|{cr}|A carriage return: '\r'|
|{crlf}|a carriage return + line feed: '\r\n'|
|{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|
|{keyword}|Keyword(s) assigned to photo|
|{person}|Person(s) / face(s) in a photo|
|{label}|Image categorization label associated with a photo (Photos 5+ only)|
|{label}|Image categorization label associated with a photo (Photos 5+ only). Labels are added automatically by Photos using machine learning algorithms to categorize images. These are not the same as {keyword} which refers to the user-defined keywords/tags applied in Photos.|
|{label_normalized}|All lower case version of 'label' (Photos 5+ only)|
|{comment}|Comment(s) on shared Photos; format is 'Person name: comment text' (Photos 5+ only)|
|{exiftool}|Format: '{exiftool:GROUP:TAGNAME}'; use exiftool (https://exiftool.org) to extract metadata, in form GROUP:TAGNAME, from image. E.g. '{exiftool:EXIF:Make}' to get camera make, or {exiftool:IPTC:Keywords} to extract keywords. See https://exiftool.org/TagNames/ for list of valid tag names. You must specify group (e.g. EXIF, IPTC, etc) as used in `exiftool -G`. exiftool must be installed in the path to use this template.|
@@ -2758,6 +2929,8 @@ The following template field substitutions are availabe for use with `PhotoInfo.
|{searchinfo.activity}|Activities associated with a photo, e.g. 'Sporting Event'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|{searchinfo.venue}|Venues associated with a photo, e.g. name of restaurant; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|{searchinfo.venue_type}|Venue types associated with a photo, e.g. 'Restaurant'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|{photo}|Provides direct access to the PhotoInfo object for the photo. Must be used in format '{photo.property}' where 'property' represents a PhotoInfo property. For example: '{photo.favorite}' is the same as '{favorite}' and '{photo.place.name}' is the same as '{place.name}'. '{photo}' provides access to properties that are not available as separate template fields but it assumes some knowledge of the underlying PhotoInfo class. See https://rhettbull.github.io/osxphotos/ for additional documentation on the PhotoInfo class.|
|{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.|
<!-- OSXPHOTOS-TEMPLATE-TABLE:END -->
### Utility Functions
@@ -2892,6 +3065,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center"><a href="https://github.com/davidjroos"><img src="https://avatars.githubusercontent.com/u/15630844?v=4?s=75" width="75px;" alt=""/><br /><sub><b>davidjroos </b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=davidjroos" title="Documentation">📖</a></td>
<td align="center"><a href="https://neilpa.me"><img src="https://avatars.githubusercontent.com/u/42419?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Neil Pankey</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=neilpa" title="Code">💻</a></td>
<td align="center"><a href="https://aaronweb.net/"><img src="https://avatars.githubusercontent.com/u/604665?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Aaron van Geffen</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=AaronVanGeffen" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/ubrandes"><img src="https://avatars.githubusercontent.com/u/59647284?v=4?s=75" width="75px;" alt=""/><br /><sub><b>ubrandes </b></sub></a><br /><a href="#ideas-ubrandes" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
</table>

11
build.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/sh
# script to help build osxphotos release
# this is unique to my own dev setup
activate osxphotos
rm -rf dist; rm -rf build
python3 utils/update_readme.py
(cd docsrc && make github && make pdf)
python3 setup.py sdist bdist_wheel
./make_cli_exe.sh

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: c43f566654ff6a66a64cd55da2e67fef
config: 041dede95604f7f8f5de01377fc1b414
tags: 645f666f9bcd5a90fca523b33c5a78b7

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Overview: module code &#8212; osxphotos 0.41.4 documentation</title>
<title>Overview: module code &#8212; osxphotos 0.42.4 documentation</title>
<link rel="stylesheet" href="../_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="../_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="../" src="../_static/documentation_options.js"></script>

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.photoinfo._photoinfo_export &#8212; osxphotos 0.41.4 documentation</title>
<title>osxphotos.photoinfo._photoinfo_export &#8212; osxphotos 0.41.10 documentation</title>
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
@@ -1693,7 +1693,7 @@
<span class="k">if</span> <span class="nb">type</span><span class="p">(</span><span class="n">val</span><span class="p">)</span> <span class="o">==</span> <span class="nb">str</span><span class="p">:</span>
<span class="n">exif</span><span class="p">[</span><span class="n">field</span><span class="p">]</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="s2">&quot;</span><span class="se">\n</span><span class="s2">&quot;</span><span class="p">,</span> <span class="s2">&quot; &quot;</span><span class="p">)</span>
<span class="k">elif</span> <span class="nb">type</span><span class="p">(</span><span class="n">val</span><span class="p">)</span> <span class="o">==</span> <span class="nb">list</span><span class="p">:</span>
<span class="n">exif</span><span class="p">[</span><span class="n">field</span><span class="p">]</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">&quot;</span><span class="se">\n</span><span class="s2">&quot;</span><span class="p">,</span> <span class="s2">&quot; &quot;</span><span class="p">)</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">val</span><span class="p">]</span>
<span class="n">exif</span><span class="p">[</span><span class="n">field</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="nb">str</span><span class="p">(</span><span class="n">v</span><span class="p">)</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">&quot;</span><span class="se">\n</span><span class="s2">&quot;</span><span class="p">,</span> <span class="s2">&quot; &quot;</span><span class="p">)</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">val</span> <span class="k">if</span> <span class="n">v</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">]</span>
<span class="k">return</span> <span class="n">exif</span>

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.photoinfo.photoinfo &#8212; osxphotos 0.41.4 documentation</title>
<title>osxphotos.photoinfo.photoinfo &#8212; osxphotos 0.41.6 documentation</title>
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
@@ -486,9 +486,24 @@
<span class="p">)</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_albums</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">burst_albums</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;If photo is non-selected burst photo, list of albums any other images in the same burst set are contained in, otherwise returns self.albums&quot;&quot;&quot;</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst_selected</span> <span class="ow">or</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">albums</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">_burst_albums</span>
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
<span class="n">burst_albums</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">photo</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst_photos</span><span class="p">:</span>
<span class="n">burst_albums</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">photo</span><span class="o">.</span><span class="n">albums</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_burst_albums</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">burst_albums</span><span class="p">))</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_burst_albums</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">album_info</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; list of AlbumInfo objects representing albums the photos is contained in &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot; list of AlbumInfo objects representing albums the photo is contained in &quot;&quot;&quot;</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">_album_info</span>
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
@@ -498,6 +513,21 @@
<span class="p">]</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_album_info</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">burst_album_info</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; If photo is a non-selected burst photo, returns list of AlbumInfo objects representing albums any other photos in the same burst set are contained in, otherwise returns self.album_info &quot;&quot;&quot;</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst_selected</span> <span class="ow">or</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">album_info</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">_burst_album_info</span>
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
<span class="n">burst_album_info</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">photo</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst_photos</span><span class="p">:</span>
<span class="n">burst_album_info</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">photo</span><span class="o">.</span><span class="n">album_info</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_burst_album_info</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">burst_album_info</span><span class="p">))</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_burst_album_info</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">import_info</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; ImportInfo object representing import session for the photo or None if no import session &quot;&quot;&quot;</span>
@@ -713,6 +743,11 @@
<span class="sd">&quot;&quot;&quot; Returns True if photo is part of a Burst photo set, otherwise False &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">&quot;burst&quot;</span><span class="p">]</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">burst_selected</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; Returns True if photo is a burst photo and has been selected from the burst set by the user, otherwise False &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">&quot;burst_key&quot;</span><span class="p">]</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">burst_photos</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;If photo is a burst photo, returns list of PhotoInfo objects</span>

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.photosdb.photosdb &#8212; osxphotos 0.41.4 documentation</title>
<title>osxphotos.photosdb.photosdb &#8212; osxphotos 0.41.10 documentation</title>
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
@@ -1861,7 +1861,6 @@
<span class="c1"># get details about photos</span>
<span class="n">verbose</span><span class="p">(</span><span class="s2">&quot;Processing photo details.&quot;</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">&quot;Getting information about photos&quot;</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="sa">f</span><span class="s2">&quot;&quot;&quot;SELECT </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZUUID, </span>
<span class="s2"> ZADDITIONALASSETATTRIBUTES.ZMASTERFINGERPRINT, </span>
@@ -2769,8 +2768,6 @@
<span class="c1"># an empty album will be in _dbalbum_titles but not _dbalbums_album</span>
<span class="k">pass</span>
<span class="n">album_set</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="n">title_set</span><span class="p">)</span>
<span class="k">else</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">&quot;Could not find album &#39;</span><span class="si">{</span><span class="n">album</span><span class="si">}</span><span class="s2">&#39; in database&quot;</span><span class="p">)</span>
<span class="n">photos_sets</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">album_set</span><span class="p">)</span>
<span class="k">if</span> <span class="n">uuid</span><span class="p">:</span>
@@ -2778,8 +2775,6 @@
<span class="k">for</span> <span class="n">u</span> <span class="ow">in</span> <span class="n">uuid</span><span class="p">:</span>
<span class="k">if</span> <span class="n">u</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">:</span>
<span class="n">uuid_set</span><span class="o">.</span><span class="n">update</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="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Could not find uuid &#39;</span><span class="si">{</span><span class="n">u</span><span class="si">}</span><span class="s2">&#39; in database&quot;</span><span class="p">)</span>
<span class="n">photos_sets</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">uuid_set</span><span class="p">)</span>
<span class="k">if</span> <span class="n">keywords</span><span class="p">:</span>
@@ -2787,8 +2782,6 @@
<span class="k">for</span> <span class="n">keyword</span> <span class="ow">in</span> <span class="n">keywords</span><span class="p">:</span>
<span class="k">if</span> <span class="n">keyword</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_keyword</span><span class="p">:</span>
<span class="n">keyword_set</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_keyword</span><span class="p">[</span><span class="n">keyword</span><span class="p">])</span>
<span class="k">else</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">&quot;Could not find keyword &#39;</span><span class="si">{</span><span class="n">keyword</span><span class="si">}</span><span class="s2">&#39; in database&quot;</span><span class="p">)</span>
<span class="n">photos_sets</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">keyword_set</span><span class="p">)</span>
<span class="k">if</span> <span class="n">persons</span><span class="p">:</span>
@@ -2801,8 +2794,6 @@
<span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
<span class="c1"># some persons have zero photos so they won&#39;t be in _dbfaces_pk</span>
<span class="k">pass</span>
<span class="k">else</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">&quot;Could not find person &#39;</span><span class="si">{</span><span class="n">person</span><span class="si">}</span><span class="s2">&#39; in database&quot;</span><span class="p">)</span>
<span class="n">photos_sets</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">person_set</span><span class="p">)</span>
<span class="k">if</span> <span class="n">from_date</span> <span class="ow">or</span> <span class="n">to_date</span><span class="p">:</span> <span class="c1"># sourcery off</span>
@@ -2813,14 +2804,10 @@
<span class="n">dsel</span> <span class="o">=</span> <span class="p">{</span>
<span class="n">k</span><span class="p">:</span> <span class="n">v</span> <span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">dsel</span><span class="o">.</span><span class="n">items</span><span class="p">()</span> <span class="k">if</span> <span class="n">v</span><span class="p">[</span><span class="s2">&quot;imageDate&quot;</span><span class="p">]</span> <span class="o">&gt;=</span> <span class="n">from_date</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">&quot;Found %i items with from_date </span><span class="si">{</span><span class="n">from_date</span><span class="si">}</span><span class="s2">&quot;</span> <span class="o">%</span> <span class="nb">len</span><span class="p">(</span><span class="n">dsel</span><span class="p">)</span>
<span class="p">)</span>
<span class="k">if</span> <span class="n">to_date</span><span class="p">:</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">datetime_has_tz</span><span class="p">(</span><span class="n">to_date</span><span class="p">):</span>
<span class="n">to_date</span> <span class="o">=</span> <span class="n">datetime_naive_to_local</span><span class="p">(</span><span class="n">to_date</span><span class="p">)</span>
<span class="n">dsel</span> <span class="o">=</span> <span class="p">{</span><span class="n">k</span><span class="p">:</span> <span class="n">v</span> <span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">dsel</span><span class="o">.</span><span class="n">items</span><span class="p">()</span> <span class="k">if</span> <span class="n">v</span><span class="p">[</span><span class="s2">&quot;imageDate&quot;</span><span class="p">]</span> <span class="o">&lt;=</span> <span class="n">to_date</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">&quot;Found %i items with to_date </span><span class="si">{</span><span class="n">to_date</span><span class="si">}</span><span class="s2">&quot;</span> <span class="o">%</span> <span class="nb">len</span><span class="p">(</span><span class="n">dsel</span><span class="p">))</span>
<span class="n">photos_sets</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">dsel</span><span class="o">.</span><span class="n">keys</span><span class="p">()))</span>
<span class="n">photoinfo</span> <span class="o">=</span> <span class="p">[]</span>

View File

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

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos command line interface (CLI) &#8212; osxphotos 0.41.4 documentation</title>
<title>osxphotos command line interface (CLI) &#8212; osxphotos 0.42.4 documentation</title>
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
@@ -183,6 +183,12 @@ to modify this behavior.</p>
<dd><p>Search for photos in an album in folder FOLDER. If more than one folder, treated as “OR”, e.g. find photos in any FOLDER. Only searches top level folders (e.g. does not look at subfolders)</p>
</dd></dl>
<dl class="std option">
<dt id="cmdoption-osxphotos-export-name">
<code class="sig-name descname"><span class="pre">--name</span></code><code class="sig-prename descclassname"> <span class="pre">&lt;FILENAME&gt;</span></code><a class="headerlink" href="#cmdoption-osxphotos-export-name" title="Permalink to this definition"></a></dt>
<dd><p>Search for photos with filename matching FILENAME. If more than one name options is specified, they are treated as “OR”, e.g. find photos matching any FILENAME.</p>
</dd></dl>
<dl class="std option">
<dt id="cmdoption-osxphotos-export-uuid">
<code class="sig-name descname"><span class="pre">--uuid</span></code><code class="sig-prename descclassname"> <span class="pre">&lt;UUID&gt;</span></code><a class="headerlink" href="#cmdoption-osxphotos-export-uuid" title="Permalink to this definition"></a></dt>
@@ -489,6 +495,24 @@ to modify this behavior.</p>
<dd><p>Search for photos that are not in any albums.</p>
</dd></dl>
<dl class="std option">
<dt id="cmdoption-osxphotos-export-min-size">
<code class="sig-name descname"><span class="pre">--min-size</span></code><code class="sig-prename descclassname"> <span class="pre">&lt;SIZE&gt;</span></code><a class="headerlink" href="#cmdoption-osxphotos-export-min-size" title="Permalink to this definition"></a></dt>
<dd><p>Search for photos with size &gt;= SIZE bytes. The size evaluated is the photos original size (when imported to Photos). Size may be specified as integer bytes or using SI or NIST units. For example, the following are all valid and equivalent sizes: 1048576 1.048576MB, 1 MiB.</p>
</dd></dl>
<dl class="std option">
<dt id="cmdoption-osxphotos-export-max-size">
<code class="sig-name descname"><span class="pre">--max-size</span></code><code class="sig-prename descclassname"> <span class="pre">&lt;SIZE&gt;</span></code><a class="headerlink" href="#cmdoption-osxphotos-export-max-size" title="Permalink to this definition"></a></dt>
<dd><p>Search for photos with size &lt;= SIZE bytes. The size evaluated is the photos original size (when imported to Photos). Size may be specified as integer bytes or using SI or NIST units. For example, the following are all valid and equivalent sizes: 1048576 1.048576MB, 1 MiB.</p>
</dd></dl>
<dl class="std option">
<dt id="cmdoption-osxphotos-export-query-eval">
<code class="sig-name descname"><span class="pre">--query-eval</span></code><code class="sig-prename descclassname"> <span class="pre">&lt;CRITERIA&gt;</span></code><a class="headerlink" href="#cmdoption-osxphotos-export-query-eval" title="Permalink to this definition"></a></dt>
<dd><p>Evaluate CRITERIA to filter photos. CRITERIA will be evaluated in context of the following python list comprehension: <cite>photos = [photo for photo in photos if CRITERIA]</cite> where photo represents a PhotoInfo object. For example: <cite>query-eval photo.favorite</cite> returns all photos that have been favorited and is equivalent to favorite. You may specify more than one CRITERIA by using query-eval multiple times. CRITERIA must be a valid python expression. 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>
</dd></dl>
<dl class="std option">
<dt id="cmdoption-osxphotos-export-missing">
<code class="sig-name descname"><span class="pre">--missing</span></code><code class="sig-prename descclassname"></code><a class="headerlink" href="#cmdoption-osxphotos-export-missing" title="Permalink to this definition"></a></dt>
@@ -549,6 +573,12 @@ to modify this behavior.</p>
<dd><p>Overwrite existing files. Default behavior is to add (1), (2), etc to filename if file already exists. Use this with caution as it may create name collisions on export. (e.g. if two files happen to have the same name)</p>
</dd></dl>
<dl class="std option">
<dt id="cmdoption-osxphotos-export-retry">
<code class="sig-name descname"><span class="pre">--retry</span></code><code class="sig-prename descclassname"> <span class="pre">&lt;RETRY&gt;</span></code><a class="headerlink" href="#cmdoption-osxphotos-export-retry" title="Permalink to this definition"></a></dt>
<dd><p>Automatically retry export up to RETRY times if an error occurs during export. This may be useful with network drives that experience intermittent errors.</p>
</dd></dl>
<dl class="std option">
<dt id="cmdoption-osxphotos-export-export-by-date">
<code class="sig-name descname"><span class="pre">--export-by-date</span></code><code class="sig-prename descclassname"></code><a class="headerlink" href="#cmdoption-osxphotos-export-export-by-date" title="Permalink to this definition"></a></dt>
@@ -1017,6 +1047,12 @@ if more than one option is provided, they are treated as “AND”
<dd><p>Search for photos in an album in folder FOLDER. If more than one folder, treated as “OR”, e.g. find photos in any FOLDER. Only searches top level folders (e.g. does not look at subfolders)</p>
</dd></dl>
<dl class="std option">
<dt id="cmdoption-osxphotos-query-name">
<code class="sig-name descname"><span class="pre">--name</span></code><code class="sig-prename descclassname"> <span class="pre">&lt;FILENAME&gt;</span></code><a class="headerlink" href="#cmdoption-osxphotos-query-name" title="Permalink to this definition"></a></dt>
<dd><p>Search for photos with filename matching FILENAME. If more than one name options is specified, they are treated as “OR”, e.g. find photos matching any FILENAME.</p>
</dd></dl>
<dl class="std option">
<dt id="cmdoption-osxphotos-query-uuid">
<code class="sig-name descname"><span class="pre">--uuid</span></code><code class="sig-prename descclassname"> <span class="pre">&lt;UUID&gt;</span></code><a class="headerlink" href="#cmdoption-osxphotos-query-uuid" title="Permalink to this definition"></a></dt>
@@ -1323,6 +1359,24 @@ if more than one option is provided, they are treated as “AND”
<dd><p>Search for photos that are not in any albums.</p>
</dd></dl>
<dl class="std option">
<dt id="cmdoption-osxphotos-query-min-size">
<code class="sig-name descname"><span class="pre">--min-size</span></code><code class="sig-prename descclassname"> <span class="pre">&lt;SIZE&gt;</span></code><a class="headerlink" href="#cmdoption-osxphotos-query-min-size" title="Permalink to this definition"></a></dt>
<dd><p>Search for photos with size &gt;= SIZE bytes. The size evaluated is the photos original size (when imported to Photos). Size may be specified as integer bytes or using SI or NIST units. For example, the following are all valid and equivalent sizes: 1048576 1.048576MB, 1 MiB.</p>
</dd></dl>
<dl class="std option">
<dt id="cmdoption-osxphotos-query-max-size">
<code class="sig-name descname"><span class="pre">--max-size</span></code><code class="sig-prename descclassname"> <span class="pre">&lt;SIZE&gt;</span></code><a class="headerlink" href="#cmdoption-osxphotos-query-max-size" title="Permalink to this definition"></a></dt>
<dd><p>Search for photos with size &lt;= SIZE bytes. The size evaluated is the photos original size (when imported to Photos). Size may be specified as integer bytes or using SI or NIST units. For example, the following are all valid and equivalent sizes: 1048576 1.048576MB, 1 MiB.</p>
</dd></dl>
<dl class="std option">
<dt id="cmdoption-osxphotos-query-query-eval">
<code class="sig-name descname"><span class="pre">--query-eval</span></code><code class="sig-prename descclassname"> <span class="pre">&lt;CRITERIA&gt;</span></code><a class="headerlink" href="#cmdoption-osxphotos-query-query-eval" title="Permalink to this definition"></a></dt>
<dd><p>Evaluate CRITERIA to filter photos. CRITERIA will be evaluated in context of the following python list comprehension: <cite>photos = [photo for photo in photos if CRITERIA]</cite> where photo represents a PhotoInfo object. For example: <cite>query-eval photo.favorite</cite> returns all photos that have been favorited and is equivalent to favorite. You may specify more than one CRITERIA by using query-eval multiple times. CRITERIA must be a valid python expression. 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>
</dd></dl>
<dl class="std option">
<dt id="cmdoption-osxphotos-query-deleted">
<code class="sig-name descname"><span class="pre">--deleted</span></code><code class="sig-prename descclassname"></code><a class="headerlink" href="#cmdoption-osxphotos-query-deleted" title="Permalink to this definition"></a></dt>

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Index &#8212; osxphotos 0.41.4 documentation</title>
<title>Index &#8212; osxphotos 0.42.4 documentation</title>
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
@@ -509,6 +509,24 @@
<ul>
<li><a href="cli.html#cmdoption-osxphotos-export-load-config">osxphotos-export command line option</a>
</li>
</ul></li>
<li>
--max-size &lt;SIZE&gt;
<ul>
<li><a href="cli.html#cmdoption-osxphotos-export-max-size">osxphotos-export command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-max-size">osxphotos-query command line option</a>
</li>
</ul></li>
<li>
--min-size &lt;SIZE&gt;
<ul>
<li><a href="cli.html#cmdoption-osxphotos-export-min-size">osxphotos-export command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-min-size">osxphotos-query command line option</a>
</li>
</ul></li>
<li>
@@ -518,6 +536,15 @@
<li><a href="cli.html#cmdoption-osxphotos-export-missing">osxphotos-export command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-missing">osxphotos-query command line option</a>
</li>
</ul></li>
<li>
--name &lt;FILENAME&gt;
<ul>
<li><a href="cli.html#cmdoption-osxphotos-export-name">osxphotos-export command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-name">osxphotos-query command line option</a>
</li>
</ul></li>
<li>
@@ -785,6 +812,15 @@
<li><a href="cli.html#cmdoption-osxphotos-export-portrait">osxphotos-export command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-portrait">osxphotos-query command line option</a>
</li>
</ul></li>
<li>
--query-eval &lt;CRITERIA&gt;
<ul>
<li><a href="cli.html#cmdoption-osxphotos-export-query-eval">osxphotos-export command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-query-eval">osxphotos-query command line option</a>
</li>
</ul></li>
<li>
@@ -799,6 +835,13 @@
<ul>
<li><a href="cli.html#cmdoption-osxphotos-export-report">osxphotos-export command line option</a>
</li>
</ul></li>
<li>
--retry &lt;RETRY&gt;
<ul>
<li><a href="cli.html#cmdoption-osxphotos-export-retry">osxphotos-export command line option</a>
</li>
</ul></li>
<li>
@@ -1089,13 +1132,19 @@
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.ExifInfo.bit_rate">bit_rate (osxphotos.PhotoInfo.ExifInfo attribute)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.SearchInfo.bodies_of_water">bodies_of_water() (osxphotos.PhotoInfo.SearchInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.burst">burst() (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.burst_album_info">burst_album_info() (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.burst_albums">burst_albums() (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.burst_photos">burst_photos() (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.burst_selected">burst_selected() (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
</tr></table>
@@ -1524,8 +1573,14 @@
<li><a href="cli.html#cmdoption-osxphotos-export-live">--live</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-load-config">--load-config &lt;config file path&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-max-size">--max-size &lt;SIZE&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-min-size">--min-size &lt;SIZE&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-missing">--missing</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-name">--name &lt;FILENAME&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-no-comment">--no-comment</a>
</li>
@@ -1582,10 +1637,14 @@
<li><a href="cli.html#cmdoption-osxphotos-export-place">--place &lt;PLACE&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-portrait">--portrait</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-query-eval">--query-eval &lt;CRITERIA&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-replace-keywords">--replace-keywords</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-report">--report &lt;path to export report&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-retry">--retry &lt;RETRY&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-save-config">--save-config &lt;config file path&gt;</a>
</li>
@@ -1774,8 +1833,14 @@
<li><a href="cli.html#cmdoption-osxphotos-query-label">--label &lt;LABEL&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-live">--live</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-max-size">--max-size &lt;SIZE&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-min-size">--min-size &lt;SIZE&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-missing">--missing</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-name">--name &lt;FILENAME&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-no-comment">--no-comment</a>
</li>
@@ -1830,6 +1895,8 @@
<li><a href="cli.html#cmdoption-osxphotos-query-place">--place &lt;PLACE&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-portrait">--portrait</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-query-eval">--query-eval &lt;CRITERIA&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-screenshot">--screenshot</a>
</li>

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Welcome to osxphotoss documentation! &#8212; osxphotos 0.41.4 documentation</title>
<title>Welcome to osxphotoss documentation! &#8212; osxphotos 0.42.4 documentation</title>
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos &#8212; osxphotos 0.41.4 documentation</title>
<title>osxphotos &#8212; osxphotos 0.42.4 documentation</title>
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>

Binary file not shown.

Binary file not shown.

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos package &#8212; osxphotos 0.41.4 documentation</title>
<title>osxphotos package &#8212; osxphotos 0.42.4 documentation</title>
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
@@ -650,7 +650,7 @@ including keywords, persons, albums, uuid, path, etc.</p>
<dl class="py method">
<dt id="osxphotos.PhotoInfo.album_info">
<em class="property"><span class="pre">property</span> </em><code class="sig-name descname"><span class="pre">album_info</span></code><a class="headerlink" href="#osxphotos.PhotoInfo.album_info" title="Permalink to this definition"></a></dt>
<dd><p>list of AlbumInfo objects representing albums the photos is contained in</p>
<dd><p>list of AlbumInfo objects representing albums the photo is contained in</p>
</dd></dl>
<dl class="py method">
@@ -671,6 +671,18 @@ including keywords, persons, albums, uuid, path, etc.</p>
<dd><p>Returns True if photo is part of a Burst photo set, otherwise False</p>
</dd></dl>
<dl class="py method">
<dt id="osxphotos.PhotoInfo.burst_album_info">
<em class="property"><span class="pre">property</span> </em><code class="sig-name descname"><span class="pre">burst_album_info</span></code><a class="headerlink" href="#osxphotos.PhotoInfo.burst_album_info" title="Permalink to this definition"></a></dt>
<dd><p>If photo is a non-selected burst photo, returns list of AlbumInfo objects representing albums any other photos in the same burst set are contained in, otherwise returns self.album_info</p>
</dd></dl>
<dl class="py method">
<dt id="osxphotos.PhotoInfo.burst_albums">
<em class="property"><span class="pre">property</span> </em><code class="sig-name descname"><span class="pre">burst_albums</span></code><a class="headerlink" href="#osxphotos.PhotoInfo.burst_albums" title="Permalink to this definition"></a></dt>
<dd><p>If photo is non-selected burst photo, list of albums any other images in the same burst set are contained in, otherwise returns self.albums</p>
</dd></dl>
<dl class="py method">
<dt id="osxphotos.PhotoInfo.burst_photos">
<em class="property"><span class="pre">property</span> </em><code class="sig-name descname"><span class="pre">burst_photos</span></code><a class="headerlink" href="#osxphotos.PhotoInfo.burst_photos" title="Permalink to this definition"></a></dt>
@@ -679,6 +691,12 @@ that are part of the same burst photo set; otherwise returns empty list.
self is not included in the returned list</p>
</dd></dl>
<dl class="py method">
<dt id="osxphotos.PhotoInfo.burst_selected">
<em class="property"><span class="pre">property</span> </em><code class="sig-name descname"><span class="pre">burst_selected</span></code><a class="headerlink" href="#osxphotos.PhotoInfo.burst_selected" title="Permalink to this definition"></a></dt>
<dd><p>Returns True if photo is a burst photo and has been selected from the burst set by the user, otherwise False</p>
</dd></dl>
<dl class="py method">
<dt id="osxphotos.PhotoInfo.comments">
<em class="property"><span class="pre">property</span> </em><code class="sig-name descname"><span class="pre">comments</span></code><a class="headerlink" href="#osxphotos.PhotoInfo.comments" title="Permalink to this definition"></a></dt>

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Search &#8212; osxphotos 0.41.4 documentation</title>
<title>Search &#8212; osxphotos 0.42.4 documentation</title>
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,30 @@
""" Example showing how to use a custom function for osxphotos {function} template
Use: osxphotos export /path/to/export --filename "{function:/path/to/template_function.py::example}"
You may place more than one template function in a single file as each is called by name using the {function:file.py::function_name} format
"""
import pathlib
from typing import List, Union
import osxphotos
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
Args:
photo: osxphotos.PhotoInfo object
**kwargs: not currently used, placeholder to keep functions compatible with possible changes to {function}
Returns:
str or list of str of values that should be substituted for the {function} template
"""
filename = pathlib.Path(photo.original_filename).stem
if photo.hasadjustments:
filename += "#"
if photo.favorite:
filename += "!"
return filename

View File

@@ -3,6 +3,7 @@ from .photoinfo import PhotoInfo
from .photosdb import PhotosDB
from .photosdb._photosdb_process_comments import CommentInfo, LikeInfo
from .phototemplate import PhotoTemplate
from .queryoptions import QueryOptions
from .utils import _debug, _get_logger, _set_debug
# TODO: Add test for imageTimeZoneOffsetSeconds = None

View File

@@ -1,3 +1,3 @@
""" version info """
__version__ = "0.41.5"
__version__ = "0.42.6"

File diff suppressed because it is too large Load Diff

View File

@@ -7,4 +7,4 @@ PhotosDB.photos() returns a list of PhotoInfo objects
from ._photoinfo_exifinfo import ExifInfo
from ._photoinfo_export import ExportResults
from ._photoinfo_scoreinfo import ScoreInfo
from .photoinfo import PhotoInfo
from .photoinfo import PhotoInfo

View File

@@ -1660,7 +1660,7 @@ def _exiftool_dict(
if type(val) == str:
exif[field] = val.replace("\n", " ")
elif type(val) == list:
exif[field] = [v.replace("\n", " ") for v in val]
exif[field] = [str(v).replace("\n", " ") for v in val if v is not None]
return exif

View File

@@ -8,10 +8,14 @@ import os
import os.path
import pathlib
import platform
import re
import sys
import tempfile
from datetime import datetime, timedelta, timezone
from pprint import pformat
from typing import List
import bitmath
from .._constants import (
_DB_TABLE_NAMES,
@@ -47,6 +51,7 @@ from ..utils import (
noop,
normalize_unicode,
)
from ..queryoptions import QueryOptions
from .photosdb_utils import get_db_model_version, get_db_version
# TODO: Add test for imageTimeZoneOffsetSeconds = None
@@ -1828,7 +1833,6 @@ class PhotosDB:
# get details about photos
verbose("Processing photo details.")
logging.debug(f"Getting information about photos")
c.execute(
f"""SELECT {asset_table}.ZUUID,
ZADDITIONALASSETATTRIBUTES.ZMASTERFINGERPRINT,
@@ -2736,8 +2740,6 @@ class PhotosDB:
# an empty album will be in _dbalbum_titles but not _dbalbums_album
pass
album_set.update(title_set)
else:
logging.debug(f"Could not find album '{album}' in database")
photos_sets.append(album_set)
if uuid:
@@ -2745,8 +2747,6 @@ class PhotosDB:
for u in uuid:
if u in self._dbphotos:
uuid_set.update([u])
else:
logging.debug(f"Could not find uuid '{u}' in database")
photos_sets.append(uuid_set)
if keywords:
@@ -2754,8 +2754,6 @@ class PhotosDB:
for keyword in keywords:
if keyword in self._dbkeywords_keyword:
keyword_set.update(self._dbkeywords_keyword[keyword])
else:
logging.debug(f"Could not find keyword '{keyword}' in database")
photos_sets.append(keyword_set)
if persons:
@@ -2768,8 +2766,6 @@ class PhotosDB:
except KeyError:
# some persons have zero photos so they won't be in _dbfaces_pk
pass
else:
logging.debug(f"Could not find person '{person}' in database")
photos_sets.append(person_set)
if from_date or to_date: # sourcery off
@@ -2780,14 +2776,10 @@ class PhotosDB:
dsel = {
k: v for k, v in dsel.items() if v["imageDate"] >= from_date
}
logging.debug(
f"Found %i items with from_date {from_date}" % len(dsel)
)
if to_date:
if not datetime_has_tz(to_date):
to_date = datetime_naive_to_local(to_date)
dsel = {k: v for k, v in dsel.items() if v["imageDate"] <= to_date}
logging.debug(f"Found %i items with to_date {to_date}" % len(dsel))
photos_sets.append(set(dsel.keys()))
photoinfo = []
@@ -2846,6 +2838,346 @@ class PhotosDB:
pass
return photos
def query(self, options: QueryOptions) -> List[PhotoInfo]:
"""Run a query against PhotosDB to extract the photos based on user supplied options
Args:
options: a QueryOptions instance
"""
if options.deleted or options.deleted_only:
photos = self.photos(
uuid=options.uuid,
images=options.photos,
movies=options.movies,
from_date=options.from_date,
to_date=options.to_date,
intrash=True,
)
else:
photos = []
if not options.deleted_only:
photos += self.photos(
uuid=options.uuid,
images=options.photos,
movies=options.movies,
from_date=options.from_date,
to_date=options.to_date,
)
person = normalize_unicode(options.person)
keyword = normalize_unicode(options.keyword)
album = normalize_unicode(options.album)
folder = normalize_unicode(options.folder)
title = normalize_unicode(options.title)
description = normalize_unicode(options.description)
place = normalize_unicode(options.place)
label = normalize_unicode(options.label)
name = normalize_unicode(options.name)
if album:
photos = _get_photos_by_attribute(
photos, "albums", album, options.ignore_case
)
if keyword:
photos = _get_photos_by_attribute(
photos, "keywords", keyword, options.ignore_case
)
if person:
photos = _get_photos_by_attribute(
photos, "persons", person, options.ignore_case
)
if label:
photos = _get_photos_by_attribute(
photos, "labels", label, options.ignore_case
)
if folder:
# search for photos in an album in folder
# finds photos that have albums whose top level folder matches folder
photo_list = []
for f in folder:
photo_list.extend(
[
p
for p in photos
if p.album_info
and f
in [a.folder_names[0] for a in p.album_info if a.folder_names]
]
)
photos = photo_list
if title:
# search title field for text
# if more than one, find photos with all title values in title
photo_list = []
if options.ignore_case:
# case-insensitive
for t in title:
t = t.lower()
photo_list.extend(
[p for p in photos if p.title and t in p.title.lower()]
)
else:
for t in title:
photo_list.extend([p for p in photos if p.title and t in p.title])
photos = photo_list
elif options.no_title:
photos = [p for p in photos if not p.title]
if description:
# search description field for text
# if more than one, find photos with all description values in description
photo_list = []
if options.ignore_case:
# case-insensitive
for d in description:
d = d.lower()
photo_list.extend(
[
p
for p in photos
if p.description and d in p.description.lower()
]
)
else:
for d in description:
photo_list.extend(
[p for p in photos if p.description and d in p.description]
)
photos = photo_list
elif options.no_description:
photos = [p for p in photos if not p.description]
if place:
# search place.names for text matching place
# if more than one place, find photos with all place values in description
if options.ignore_case:
# case-insensitive
for place_name in place:
place_name = place_name.lower()
photos = [
p
for p in photos
if p.place
and any(
pname
for pname in p.place.names
if any(
pvalue
for pvalue in pname
if place_name in pvalue.lower()
)
)
]
else:
for place_name in place:
photos = [
p
for p in photos
if p.place
and any(
pname
for pname in p.place.names
if any(pvalue for pvalue in pname if place_name in pvalue)
)
]
elif options.no_place:
photos = [p for p in photos if not p.place]
if options.edited:
photos = [p for p in photos if p.hasadjustments]
if options.external_edit:
photos = [p for p in photos if p.external_edit]
if options.favorite:
photos = [p for p in photos if p.favorite]
elif options.not_favorite:
photos = [p for p in photos if not p.favorite]
if options.hidden:
photos = [p for p in photos if p.hidden]
elif options.not_hidden:
photos = [p for p in photos if not p.hidden]
if options.missing:
photos = [p for p in photos if not p.path]
elif options.not_missing:
photos = [p for p in photos if p.path]
if options.shared:
photos = [p for p in photos if p.shared]
elif options.not_shared:
photos = [p for p in photos if not p.shared]
if options.shared:
photos = [p for p in photos if p.shared]
elif options.not_shared:
photos = [p for p in photos if not p.shared]
if options.uti:
photos = [p for p in photos if options.uti in p.uti_original]
if options.burst:
photos = [p for p in photos if p.burst]
elif options.not_burst:
photos = [p for p in photos if not p.burst]
if options.live:
photos = [p for p in photos if p.live_photo]
elif options.not_live:
photos = [p for p in photos if not p.live_photo]
if options.portrait:
photos = [p for p in photos if p.portrait]
elif options.not_portrait:
photos = [p for p in photos if not p.portrait]
if options.screenshot:
photos = [p for p in photos if p.screenshot]
elif options.not_screenshot:
photos = [p for p in photos if not p.screenshot]
if options.slow_mo:
photos = [p for p in photos if p.slow_mo]
elif options.not_slow_mo:
photos = [p for p in photos if not p.slow_mo]
if options.time_lapse:
photos = [p for p in photos if p.time_lapse]
elif options.not_time_lapse:
photos = [p for p in photos if not p.time_lapse]
if options.hdr:
photos = [p for p in photos if p.hdr]
elif options.not_hdr:
photos = [p for p in photos if not p.hdr]
if options.selfie:
photos = [p for p in photos if p.selfie]
elif options.not_selfie:
photos = [p for p in photos if not p.selfie]
if options.panorama:
photos = [p for p in photos if p.panorama]
elif options.not_panorama:
photos = [p for p in photos if not p.panorama]
if options.cloudasset:
photos = [p for p in photos if p.iscloudasset]
elif options.not_cloudasset:
photos = [p for p in photos if not p.iscloudasset]
if options.incloud:
photos = [p for p in photos if p.incloud]
elif options.not_incloud:
photos = [p for p in photos if not p.incloud]
if options.has_raw:
photos = [p for p in photos if p.has_raw]
if options.has_comment:
photos = [p for p in photos if p.comments]
elif options.no_comment:
photos = [p for p in photos if not p.comments]
if options.has_likes:
photos = [p for p in photos if p.likes]
elif options.no_likes:
photos = [p for p in photos if not p.likes]
if options.is_reference:
photos = [p for p in photos if p.isreference]
if options.in_album:
photos = [p for p in photos if p.albums]
elif options.not_in_album:
photos = [p for p in photos if not p.albums]
if options.from_time:
photos = [p for p in photos if p.date.time() >= options.from_time]
if options.to_time:
photos = [p for p in photos if p.date.time() <= options.to_time]
if options.burst_photos:
# add the burst_photos to the export set
photos_burst = [p for p in photos if p.burst]
for burst in photos_burst:
if options.missing_bursts:
# include burst photos that are missing
photos.extend(burst.burst_photos)
else:
# don't include missing burst images (these can't be downloaded with AppleScript)
photos.extend([p for p in burst.burst_photos if not p.ismissing])
# remove duplicates as each burst photo in the set that's selected would
# result in the entire set being added above
# can't use set() because PhotoInfo not hashable
seen_uuids = {}
for p in photos:
if p.uuid in seen_uuids:
continue
seen_uuids[p.uuid] = p
photos = list(seen_uuids.values())
if name:
# search filename fields for text
# if more than one, find photos with all title values in filename
photo_list = []
if options.ignore_case:
# case-insensitive
for n in name:
n = n.lower()
photo_list.extend(
[
p
for p in photos
if n in p.filename.lower()
or n in p.original_filename.lower()
]
)
else:
for n in name:
photo_list.extend(
[
p
for p in photos
if n in p.filename or n in p.original_filename
]
)
photos = photo_list
if options.min_size:
photos = [
p
for p in photos
if bitmath.Byte(p.original_filesize) >= options.min_size
]
if options.max_size:
photos = [
p
for p in photos
if bitmath.Byte(p.original_filesize) <= options.max_size
]
if options.query_eval:
for q in options.query_eval:
query_string = f"[photo for photo in photos if {q}]"
try:
photos = eval(query_string)
except Exception as e:
raise ValueError(f"Invalid query_eval CRITERIA: {e}")
return photos
def __repr__(self):
return f"osxphotos.{self.__class__.__name__}(dbfile='{self.db_path}')"
@@ -2861,3 +3193,32 @@ class PhotosDB:
Includes recently deleted photos and non-selected burst images
"""
return len(self._dbphotos)
def _get_photos_by_attribute(photos, attribute, values, ignore_case):
"""Search for photos based on values being in PhotoInfo.attribute
Args:
photos: a list of PhotoInfo objects
attribute: str, name of PhotoInfo attribute to search (e.g. keywords, persons, etc)
values: list of values to search in property
ignore_case: ignore case when searching
Returns:
list of PhotoInfo objects matching search criteria
"""
photos_search = []
if ignore_case:
# case-insensitive
for x in values:
x = x.lower()
photos_search.extend(
p
for p in photos
if x in [attr.lower() for attr in getattr(p, attribute)]
)
else:
for x in values:
photos_search.extend(p for p in photos if x in getattr(p, attribute))
return photos_search

View File

@@ -4,7 +4,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]?bool_value,default}posttext"`
`"pretext{delim+template_field:subfield|filter(path_sep)[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.
@@ -60,9 +60,43 @@ e.g. If Photo is in `Album1` in `Folder1`:
- `"{folder_album(>)}"` renders to `["Folder1>Album1"]`
- `"{folder_album()}"` renders to `["Folder1Album1"]`
`[find|replace]`: optional text replacement to perform on rendered template value. For example, to replace "/" in an album name, you could use the template `"{album[/,-]}"`. Multiple replacements can be made by appending "|" and adding another find|replace pair. e.g. to replace both "/" and ":" in album name: `"{album[/,-|:,-]}"`. find/replace pairs are not limited to single characters. The "|" character cannot be used in a find/replace pair.
`[find,replace]`: optional text replacement to perform on rendered template value. For example, to replace "/" in an album name, you could use the template `"{album[/,-]}"`. Multiple replacements can be made by appending "|" and adding another find|replace pair. e.g. to replace both "/" and ":" in album name: `"{album[/,-|:,-]}"`. find/replace pairs are not limited to single characters. The "|" character cannot be used in a find/replace pair.
`?bool_value`: Template fields may be evaluated as boolean 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.
`conditional`: optional conditional expression that is evaluated as boolean (True/False) for use with the `?bool_value` modifier. Conditional expressions take the form '` not operator value`' where `not` is an optional modifier that negates the `operator`. Note: the space before the conditional expression is required if you use a conditional expression. Valid comparison operators are:
- `contains`: template field contains value, similar to python's `in`
- `matches`: template field contains exactly value, unlike `contains`: does not match partial matches
- `startswith`: template field starts with value
- `endswith`: template field ends with value
- `<=`: template field is less than or equal to value
- `>=`: template field is greater than or equal to value
- `<`: template field is less than value
- `>`: template field is greater than value
- `==`: template field equals value
- `!=`: template field does not equal value
The `value` part of the conditional expression is treated as a bare (unquoted) word/phrase. Multiple values may be separated by '|' (the pipe symbol). `value` is itself a template statement so you can use one or more template fields in `value` which will be resolved before the comparison occurs.
For example:
- `{keyword matches Beach}` resolves to True if 'Beach' is a keyword. It would not match keyword 'BeachDay'.
- `{keyword contains Beach}` resolves to True if any keyword contains the word 'Beach' so it would match both 'Beach' and 'BeachDay'.
- `{photo.score.overall > 0.7}` resolves to True if the photo's overall aesthetic score is greater than 0.7.
- `{keyword|lower contains beach}` uses the lower case filter to do case-insensitive matching to match any keyword that contains the word 'beach'.
- `{keyword|lower not contains beach}` uses the `not` modifier to negate the comparison so this resolves to True if there is no keyword that matches 'beach'.
Examples: to export photos that contain certain keywords with the `osxphotos export` command's `--directory` option:
`--directory "{keyword|lower matches travel|vacation?Travel-Photos,Not-Travel-Photos}"`
This exports any photo that has keywords 'travel' or 'vacation' into a directory 'Travel-Photos' and all other photos into directory 'Not-Travel-Photos'.
This can be used to rename files as well, for example:
`--filename "{favorite?Favorite-{original_name},{original_name}}"`
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.
e.g. if photo is an HDR image,

View File

@@ -11,6 +11,7 @@ from ._constants import _UNKNOWN_PERSON
from .datetime_formatter import DateTimeFormatter
from .exiftool import ExifTool
from .path_utils import sanitize_dirname, sanitize_filename, sanitize_pathpart
from .utils import load_function
# ensure locale set to user's locale
locale.setlocale(locale.LC_ALL, "")
@@ -48,6 +49,7 @@ TEMPLATE_SUBSTITUTIONS = {
"{photo_or_video}": "'photo' or 'video' depending on what type the image is. To customize, use default value as in '{photo_or_video,photo=fotos;video=videos}'",
"{hdr}": "Photo is HDR?; True/False value, use in format '{hdr?VALUE_IF_TRUE,VALUE_IF_FALSE}'",
"{edited}": "Photo has been edited (has adjustments)?; True/False value, use in format '{edited?VALUE_IF_TRUE,VALUE_IF_FALSE}'",
"{favorite}": "Photo has been marked as favorite?; True/False value, use in format '{favorite?VALUE_IF_TRUE,VALUE_IF_FALSE}'",
"{created.date}": "Photo's creation date in ISO format, e.g. '2020-03-22'",
"{created.year}": "4-digit year of photo creation time",
"{created.yy}": "2-digit year of photo creation time",
@@ -119,6 +121,7 @@ TEMPLATE_SUBSTITUTIONS = {
"{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'",
"{comma}": "A comma: ','",
"{semicolon}": "A semicolon: ';'",
"{questionmark}": "A question mark: '?'",
"{pipe}": "A vertical pipe: '|'",
"{openbrace}": "An open brace: '{'",
"{closebrace}": "A close brace: '}'",
@@ -126,6 +129,10 @@ TEMPLATE_SUBSTITUTIONS = {
"{closeparens}": "A close parentheses: ')'",
"{openbracket}": "An open bracket: '['",
"{closebracket}": "A close bracket: ']'",
"{newline}": r"A newline: '\n'",
"{lf}": r"A line feed: '\n', alias for {newline}",
"{cr}": r"A carriage return: '\r'",
"{crlf}": r"a carriage return + line feed: '\r\n'",
}
# Permitted multi-value substitutions (each of these returns None or 1 or more values)
@@ -134,7 +141,9 @@ TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
"{folder_album}": "Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder",
"{keyword}": "Keyword(s) assigned to photo",
"{person}": "Person(s) / face(s) in a photo",
"{label}": "Image categorization label associated with a photo (Photos 5+ only)",
"{label}": "Image categorization label associated with a photo (Photos 5+ only). "
"Labels are added automatically by Photos using machine learning algorithms to categorize images. "
"These are not the same as {keyword} which refers to the user-defined keywords/tags applied in Photos.",
"{label_normalized}": "All lower case version of 'label' (Photos 5+ only)",
"{comment}": "Comment(s) on shared Photos; format is 'Person name: comment text' (Photos 5+ only)",
"{exiftool}": "Format: '{exiftool:GROUP:TAGNAME}'; use exiftool (https://exiftool.org) to extract metadata, in form GROUP:TAGNAME, from image. "
@@ -145,6 +154,15 @@ TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
"{searchinfo.activity}": "Activities associated with a photo, e.g. 'Sporting Event'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).",
"{searchinfo.venue}": "Venues associated with a photo, e.g. name of restaurant; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).",
"{searchinfo.venue_type}": "Venue types associated with a photo, e.g. 'Restaurant'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).",
"{photo}": "Provides direct access to the PhotoInfo object for the photo. "
+ "Must be used in format '{photo.property}' where 'property' represents a PhotoInfo property. "
+ "For example: '{photo.favorite}' is the same as '{favorite}' and '{photo.place.name}' is the same as '{place.name}'. "
+ "'{photo}' provides access to properties that are not available as separate template fields but it assumes some knowledge of "
+ "the underlying PhotoInfo class. See https://rhettbull.github.io/osxphotos/ for additional documentation on the PhotoInfo class.",
"{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.",
}
FILTER_VALUES = {
@@ -185,6 +203,11 @@ PUNCTUATION = {
"closeparens": ")",
"openbracket": "[",
"closebracket": "]",
"questionmark": "?",
"newline": "\n",
"lf": "\n",
"cr": "\r",
"crlf": "\r\n",
}
@@ -317,17 +340,6 @@ class PhotoTemplate:
unmatched=unmatched,
)
# process find/replace
if ts.template and ts.template.findreplace:
new_results = []
for result in results:
for pair in ts.template.findreplace.pairs:
find = pair.find or ""
repl = pair.replace or ""
result = result.replace(find, repl)
new_results.append(result)
results = new_results
rendered_strings = results
if filename:
@@ -362,7 +374,7 @@ class PhotoTemplate:
if ts.template:
# have a template field to process
field = ts.template.field
if field not in FIELD_NAMES:
if field not in FIELD_NAMES and not field.startswith("photo"):
unmatched.append(field)
return [], unmatched
@@ -424,6 +436,30 @@ class PhotoTemplate:
else:
default = []
# process conditional
if ts.template.conditional is not None:
operator = ts.template.conditional.operator
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,
none_str=none_str,
path_sep=path_sep,
expand_inplace=expand_inplace,
inplace_sep=inplace_sep,
filename=filename,
dirname=dirname,
)
unmatched.extend(u)
else:
# this shouldn't happen
conditional_value = [""]
else:
operator = None
negation = None
conditional_value = []
vals = []
if field in SINGLE_VALUE_SUBSTITUTIONS:
vals = self.get_template_value(
@@ -442,7 +478,15 @@ class PhotoTemplate:
vals = self.get_template_value_exiftool(
subfield, filename=filename, dirname=dirname
)
elif field in MULTI_VALUE_SUBSTITUTIONS:
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, filename=filename, dirname=dirname
)
elif field in MULTI_VALUE_SUBSTITUTIONS or field.startswith("photo"):
vals = self.get_template_value_multi(
field, path_sep=path_sep, filename=filename, dirname=dirname
)
@@ -452,14 +496,6 @@ class PhotoTemplate:
vals = [val for val in vals if val is not None]
if is_bool:
if not vals:
vals = default
else:
vals = bool_val
elif not vals:
vals = default or [none_str]
if expand_inplace or delim is not None:
sep = delim if delim is not None else inplace_sep
vals = [sep.join(sorted(vals))]
@@ -467,6 +503,99 @@ class PhotoTemplate:
for filter_ in filters:
vals = self.get_template_value_filter(filter_, vals)
# process find/replace
if ts.template.findreplace:
new_vals = []
for val in vals:
for pair in ts.template.findreplace.pairs:
find = pair.find or ""
repl = pair.replace or ""
val = val.replace(find, repl)
new_vals.append(val)
vals = new_vals
if operator:
# have a conditional operator
def string_test(test_function):
""" Perform string comparison using test_function; closure to capture conditional_value, vals, negation """
match = False
for c in conditional_value:
for v in vals:
if test_function(v, c):
match = True
break
if match:
break
if (match and not negation) or (negation and not match):
return ["True"]
else:
return []
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}"
)
try:
match = (
True
if test_function(
float(vals[0]), float(conditional_value[0])
)
else False
)
if (match and not negation) or (negation and not match):
return ["True"]
else:
return []
except ValueError as e:
raise ValueError(
f"comparison operators may only be used with values that can be converted to numbers: {vals} {conditional_value}"
)
if operator in ["contains", "matches", "startswith", "endswith"]:
# process any "or" values separated by "|"
temp_values = []
for c in conditional_value:
temp_values.extend(c.split("|"))
conditional_value = temp_values
if operator == "contains":
vals = string_test(lambda v, c: c in v)
elif operator == "matches":
vals = string_test(lambda v, c: v == c)
elif operator == "startswith":
vals = string_test(lambda v, c: v.startswith(c))
elif operator == "endswith":
vals = string_test(lambda v, c: v.endswith(c))
elif operator == "==":
match = sorted(vals) == sorted(conditional_value)
if (match and not negation) or (negation and not match):
vals = ["True"]
else:
vals = []
elif operator == "!=":
match = sorted(vals) != sorted(conditional_value)
if (match and not negation) or (negation and not match):
vals = ["True"]
else:
vals = []
elif operator == "<":
vals = comparison_test(lambda v, c: v < c)
elif operator == "<=":
vals = comparison_test(lambda v, c: v <= c)
elif operator == ">":
vals = comparison_test(lambda v, c: v > c)
elif operator == ">=":
vals = comparison_test(lambda v, c: v >= c)
if is_bool:
vals = default if not vals else bool_val
elif not vals:
vals = default or [none_str]
pre = ts.pre or ""
post = ts.post or ""
@@ -539,6 +668,8 @@ class PhotoTemplate:
value = "hdr" if self.photo.hdr else None
elif field == "edited":
value = "edited" if self.photo.hasadjustments else None
elif field == "favorite":
value = "favorite" if self.photo.favorite else None
elif field == "created.date":
value = DateTimeFormatter(self.photo.date).date
elif field == "created.year":
@@ -891,6 +1022,32 @@ class PhotoTemplate:
values = (
self.photo.search_info.venue_types if self.photo.search_info else []
)
elif field.startswith("photo"):
# provide access to PhotoInfo object
properties = field.split(".")
if len(properties) <= 1:
raise ValueError(
"Missing property in {photo} template. Use in form {photo.property}."
)
obj = self.photo
for i in range(1, len(properties)):
property_ = properties[i]
try:
obj = getattr(obj, property_)
if obj is None:
break
except AttributeError:
raise ValueError(
"Invalid property for {photo} template: " + f"'{property_}'"
)
if obj is None:
values = []
elif isinstance(obj, bool):
values = [property_] if obj else []
elif isinstance(obj, (str, int, float)):
values = [str(obj)]
else:
values = [val for val in obj]
else:
raise ValueError(f"Unhandled template value: {field}")
@@ -918,6 +1075,7 @@ class PhotoTemplate:
if subfield in exifdict:
values = exifdict[subfield]
values = [values] if not isinstance(values, list) else values
values = [str(v) for v in values]
# sanitize directory names if needed
if filename:
@@ -929,6 +1087,39 @@ class PhotoTemplate:
return values
def get_template_value_function(self, subfield, filename=None, dirname=None):
"""Get template value from external function """
if "::" not in subfield:
raise ValueError(
f"SyntaxError: could not parse function name from '{subfield}'"
)
filename, funcname = subfield.split("::")
print(filename, funcname)
if not pathlib.Path(filename).is_file():
raise ValueError(f"'{filename}' does not appear to be a file")
template_func = load_function(filename, funcname)
values = template_func(self.photo)
if not isinstance(values, (str, list)):
raise TypeError(
f"Invalid return type for function {funcname}: expected str or list"
)
if type(values) == str:
values = [values]
# sanitize directory names if needed
if filename:
values = [sanitize_pathpart(value) for value in values]
elif dirname:
values = [sanitize_dirname(value) for value in values]
return values
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)

View File

@@ -1,6 +1,6 @@
// OSXPhotos Template Language (OTL)
// a TemplateString has format:
// pre{delim+template_field:subfield|filter(path_sep)[find,replace]?bool_value,default}post
// pre{delim+template_field:subfield|filter(path_sep)[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
@@ -25,6 +25,7 @@ Template:
filter=Filter
pathsep=PathSep
findreplace=FindReplace
conditional=Conditional
bool=Boolean
default=Default
"}"
@@ -32,7 +33,7 @@ Template:
;
NON_TEMPLATE_STRING:
/[^\{\},]*/
/[^\{\},\?]*/
;
Delim:
@@ -50,6 +51,10 @@ Field:
FIELD_WORD+
;
FIELD_WORD:
/[\.\w]+/
;
SubField:
(
":"-
@@ -57,12 +62,8 @@ SubField:
)?
;
FIELD_WORD:
/[\.\w]+/
;
SUBFIELD_WORD:
/[\.\w:]+/
/[\.\w:\/]+/
;
Filter:
@@ -76,6 +77,24 @@ FILTER_WORD:
/[\.\w]+/
;
Conditional:
(
(" "+)-
(negation=NEGATION)?
(operator=OPERATOR)
(" "+)-
(value=Statement)
)?
;
NEGATION:
"not "
;
OPERATOR:
"contains" | "matches" | "startswith" | "endswith" | "<=" | ">=" | "<" | ">" | "==" | "!="
;
PathSep:
(
"("

82
osxphotos/queryoptions.py Normal file
View File

@@ -0,0 +1,82 @@
""" QueryOptions class for PhotosDB.query """
from dataclasses import dataclass
from typing import Optional, Iterable
import datetime
import bitmath
@dataclass
class QueryOptions:
keyword: Optional[Iterable[str]] = None
person: Optional[Iterable[str]] = None
album: Optional[Iterable[str]] = None
folder: Optional[Iterable[str]] = None
uuid: Optional[Iterable[str]] = None
title: Optional[Iterable[str]] = None
no_title: Optional[bool] = None
description: Optional[Iterable[str]] = None
no_description: Optional[bool] = None
ignore_case: Optional[bool] = None
edited: Optional[bool] = None
external_edit: Optional[bool] = None
favorite: Optional[bool] = None
not_favorite: Optional[bool] = None
hidden: Optional[bool] = None
not_hidden: Optional[bool] = None
missing: Optional[bool] = None
not_missing: Optional[bool] = None
shared: Optional[bool] = None
not_shared: Optional[bool] = None
photos: Optional[bool] = True
movies: Optional[bool] = True
uti: Optional[Iterable[str]] = None
burst: Optional[bool] = None
not_burst: Optional[bool] = None
live: Optional[bool] = None
not_live: Optional[bool] = None
cloudasset: Optional[bool] = None
not_cloudasset: Optional[bool] = None
incloud: Optional[bool] = None
not_incloud: Optional[bool] = None
from_date: Optional[datetime.datetime] = None
to_date: Optional[datetime.datetime] = None
from_time: Optional[datetime.time] = None
to_time: Optional[datetime.time] = None
portrait: Optional[bool] = None
not_portrait: Optional[bool] = None
screenshot: Optional[bool] = None
not_screenshot: Optional[bool] = None
slow_mo: Optional[bool] = None
not_slow_mo: Optional[bool] = None
time_lapse: Optional[bool] = None
not_time_lapse: Optional[bool] = None
hdr: Optional[bool] = None
not_hdr: Optional[bool] = None
selfie: Optional[bool] = None
not_selfie: Optional[bool] = None
panorama: Optional[bool] = None
not_panorama: Optional[bool] = None
has_raw: Optional[bool] = None
place: Optional[Iterable[str]] = None
no_place: Optional[bool] = None
label: Optional[Iterable[str]] = None
deleted: Optional[bool] = None
deleted_only: Optional[bool] = None
has_comment: Optional[bool] = None
no_comment: Optional[bool] = None
has_likes: Optional[bool] = None
no_likes: Optional[bool] = None
is_reference: Optional[bool] = None
in_album: Optional[bool] = None
not_in_album: Optional[bool] = None
burst_photos: Optional[bool] = None
missing_bursts: Optional[bool] = None
name: Optional[Iterable[str]] = None
min_size: Optional[bitmath.Byte] = None
max_size: Optional[bitmath.Byte] = None
query_eval: Optional[Iterable[str]] = None
def asdict(self):
return asdict(self)

View File

@@ -1,5 +1,8 @@
""" Utility functions used in osxphotos """
import fnmatch
import glob
import importlib
import inspect
import logging
import os
@@ -13,6 +16,7 @@ import sys
import unicodedata
import urllib.parse
from plistlib import load as plistload
from typing import Callable
import CoreFoundation
import CoreServices
@@ -369,13 +373,16 @@ def _db_is_locked(dbname):
def normalize_unicode(value):
""" normalize unicode data """
if value is None:
if value is not None:
if isinstance(value, (tuple, list)):
return tuple(unicodedata.normalize(UNICODE_FORMAT, v) for v in value)
elif isinstance(value, str):
return unicodedata.normalize(UNICODE_FORMAT, value)
else:
return value
else:
return None
if not isinstance(value, str):
raise ValueError("value must be str")
return unicodedata.normalize(UNICODE_FORMAT, value)
def increment_filename(filepath):
""" Return filename (1).ext, etc if filename.ext exists
@@ -401,3 +408,28 @@ def increment_filename(filepath):
count += 1
dest = dest.parent / f"{dest_new}{dest.suffix}"
return str(dest)
def load_function(pyfile: str, function_name: str) -> Callable:
""" Load function_name from python file pyfile """
module_file = pathlib.Path(pyfile)
if not module_file.is_file():
raise FileNotFoundError(f"module {pyfile} does not appear to exist")
module_dir = module_file.parent or pathlib.Path(os.getcwd())
module_name = module_file.stem
# store old sys.path and ensure module_dir at beginning of path
syspath = sys.path
sys.path = [str(module_dir)] + syspath
module = importlib.import_module(module_name)
try:
func = getattr(module, function_name)
except AttributeError:
raise ValueError(f"'{function_name}' not found in module '{module_name}'")
finally:
# restore sys.path
sys.path = syspath
return func

View File

@@ -9,6 +9,7 @@ atomicwrites==1.3.0
attrs==19.1.0
backcall==0.1.0
better-exceptions-fork==0.2.1.post6
bitmath==1.3.3.1
bleach==3.3.0
bpylist2==3.0.2
certifi==2020.4.5.1
@@ -59,7 +60,7 @@ py==1.8.0
py2app==0.21
pycparser==2.20
pyfiglet==0.8.post1
Pygments==2.6.1
Pygments==2.7.4
PyInstaller==3.6
pyinstaller-setuptools==2019.3
pylint==2.3.1

View File

@@ -86,6 +86,7 @@ setup(
"osxmetadata>=0.99.13",
"textx==2.3.0",
"rich>=9.11.1",
"bitmath==1.3.3.1",
],
entry_points={"console_scripts": ["osxphotos=osxphotos.__main__:cli"]},
include_package_data=True,

View File

@@ -23,6 +23,14 @@ class PhotoInfoMock(PhotoInfo):
else self._photo.hdr
)
@property
def favorite(self):
return (
self._mock_favorite
if getattr(self, "_mock_favorite", None) is not None
else self._photo.favorite
)
@property
def hasadjustments(self):
return (

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,20 @@
""" Example showing how to use a custom function for osxphotos {function} template """
import pathlib
from typing import List, Union
import osxphotos
def foo(photo: osxphotos.PhotoInfo, **kwargs) -> Union[List, str]:
""" example function for {function} template
Args:
photo: osxphotos.PhotoInfo object
**kwargs: not currently used, placeholder to keep functions compatible with possible changes to {function}
Returns:
str or list of str of values that should be substituted for the {function} template
"""
return photo.original_filename + "-FOO"

View File

@@ -462,7 +462,17 @@ CLI_FINDER_TAGS = {
"XMP:Description": "Girl holding pumpkin",
"XMP:PersonInImage": "Katie",
"XMP:Subject": "Kids",
}
},
"E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51": {
"File:FileName": "wedding.jpg",
"IPTC:Keywords": ["Maria", "wedding"],
"XMP:TagsList": ["Maria", "wedding"],
"XMP:Title": None,
"EXIF:ImageDescription": "Bride Wedding day",
"XMP:Description": "Bride Wedding day",
"XMP:PersonInImage": "Maria",
"XMP:Subject": ["Maria", "wedding"],
},
}
LABELS_JSON = {
@@ -5330,7 +5340,7 @@ def test_export_finder_tag_template_multiple():
keywords = [keywords] if type(keywords) != list else keywords
persons = CLI_FINDER_TAGS[uuid]["XMP:PersonInImage"]
persons = [persons] if type(persons) != list else persons
expected = [Tag(x) for x in keywords + persons]
expected = [Tag(x) for x in set(keywords + persons)]
assert sorted(md.tags) == sorted(expected)
@@ -5368,7 +5378,42 @@ def test_export_finder_tag_template_keywords():
keywords = [keywords] if type(keywords) != list else keywords
persons = CLI_FINDER_TAGS[uuid]["XMP:PersonInImage"]
persons = [persons] if type(persons) != list else persons
expected = [Tag(x) for x in keywords + persons]
expected = [Tag(x) for x in set(keywords + persons)]
assert sorted(md.tags) == sorted(expected)
def test_export_finder_tag_template_multi_field():
""" test --finder-tag-template with multiple fields (issue #422) """
import glob
import os
import os.path
from osxmetadata import OSXMetaData, Tag
from osxphotos.cli import export
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
for uuid in CLI_FINDER_TAGS:
result = runner.invoke(
export,
[
os.path.join(cwd, PHOTOS_DB_15_7),
".",
"-V",
"--finder-tag-template",
"{title};{descr}",
"--uuid",
f"{uuid}",
],
)
assert result.exit_code == 0
md = OSXMetaData(CLI_FINDER_TAGS[uuid]["File:FileName"])
title = CLI_FINDER_TAGS[uuid]["XMP:Title"] or ""
descr = CLI_FINDER_TAGS[uuid]["XMP:Description"] or ""
expected = [Tag(f"{title};{descr}")]
assert sorted(md.tags) == sorted(expected)
@@ -5397,7 +5442,7 @@ def test_export_xattr_template():
"{person}",
"--xattr-template",
"comment",
"{title}",
"{title};{descr}",
"--uuid",
f"{uuid}",
],
@@ -5408,7 +5453,9 @@ def test_export_xattr_template():
expected = CLI_FINDER_TAGS[uuid]["XMP:PersonInImage"]
expected = [expected] if type(expected) != list else expected
assert sorted(md.keywords) == sorted(expected)
assert md.comment == CLI_FINDER_TAGS[uuid]["XMP:Title"]
title = CLI_FINDER_TAGS[uuid]["XMP:Title"] or ""
descr = CLI_FINDER_TAGS[uuid]["XMP:Description"] or ""
assert md.comment == f"{title};{descr}"
# run again with --update, should skip writing extended attributes
result = runner.invoke(
@@ -5422,7 +5469,7 @@ def test_export_xattr_template():
"{person}",
"--xattr-template",
"comment",
"{title}",
"{title};{descr}",
"--uuid",
f"{uuid}",
"--update",
@@ -5699,3 +5746,250 @@ def test_export_burst_folder_album():
path = folder_album / filename
assert path.is_file()
def test_query_name():
""" test query --name """
import json
import os
import os.path
import osxphotos
from osxphotos.cli import query
runner = CliRunner()
cwd = os.getcwd()
result = runner.invoke(
query,
["--json", "--db", os.path.join(cwd, PHOTOS_DB_15_7), "--name", "DSC03584"],
)
assert result.exit_code == 0
json_got = json.loads(result.output)
assert len(json_got) == 1
assert json_got[0]["original_filename"] == "DSC03584.dng"
def test_query_name_i():
""" test query --name -i """
import json
import os
import os.path
import osxphotos
from osxphotos.cli import query
runner = CliRunner()
cwd = os.getcwd()
result = runner.invoke(
query,
[
"--json",
"--db",
os.path.join(cwd, PHOTOS_DB_15_7),
"--name",
"dsc03584",
"-i",
],
)
assert result.exit_code == 0
json_got = json.loads(result.output)
assert len(json_got) == 1
assert json_got[0]["original_filename"] == "DSC03584.dng"
def test_export_name():
""" test export --name """
import glob
import os
import os.path
import osxphotos
from osxphotos.cli import export
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
result = runner.invoke(
export, [os.path.join(cwd, PHOTOS_DB_15_7), ".", "-V", "--name", "DSC03584"]
)
assert result.exit_code == 0
files = glob.glob("*")
assert len(files) == 1
def test_query_eval():
""" test export --query-eval """
import glob
from osxphotos.cli import export
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
result = runner.invoke(
export,
[
os.path.join(cwd, PHOTOS_DB_15_7),
".",
"-V",
"--query-eval",
"'DSC03584' in photo.original_filename",
],
)
assert result.exit_code == 0
files = glob.glob("*")
assert len(files) == 1
def test_bad_query_eval():
""" test export --query-eval with bad input """
import glob
from osxphotos.cli import export
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
result = runner.invoke(
export,
[
os.path.join(cwd, PHOTOS_DB_15_7),
".",
"-V",
"--query-eval",
"'DSC03584' in photo.originalfilename",
],
)
assert result.exit_code != 0
assert "Error: Invalid query-eval CRITERIA" in result.output
def test_query_min_size_1():
""" test query --min-size """
import json
import os
import os.path
import osxphotos
from osxphotos.cli import query
runner = CliRunner()
cwd = os.getcwd()
result = runner.invoke(
query,
["--json", "--db", os.path.join(cwd, PHOTOS_DB_15_7), "--min-size", "10MB"],
)
assert result.exit_code == 0
json_got = json.loads(result.output)
assert len(json_got) == 2
def test_query_min_size_2():
""" test query --min-size """
import json
import os
import os.path
import osxphotos
from osxphotos.cli import query
runner = CliRunner()
cwd = os.getcwd()
result = runner.invoke(
query,
[
"--json",
"--db",
os.path.join(cwd, PHOTOS_DB_15_7),
"--min-size",
"10_000_000",
],
)
assert result.exit_code == 0
json_got = json.loads(result.output)
assert len(json_got) == 2
def test_query_max_size_1():
""" test query --max-size """
import json
import os
import os.path
import osxphotos
from osxphotos.cli import query
runner = CliRunner()
cwd = os.getcwd()
result = runner.invoke(
query,
["--json", "--db", os.path.join(cwd, PHOTOS_DB_15_7), "--max-size", "500 kB"],
)
assert result.exit_code == 0
json_got = json.loads(result.output)
assert len(json_got) == 1
def test_query_max_size_2():
""" test query --max-size """
import json
import os
import os.path
import osxphotos
from osxphotos.cli import query
runner = CliRunner()
cwd = os.getcwd()
result = runner.invoke(
query,
["--json", "--db", os.path.join(cwd, PHOTOS_DB_15_7), "--max-size", "500_000"],
)
assert result.exit_code == 0
json_got = json.loads(result.output)
assert len(json_got) == 1
def test_query_min_max_size():
""" test query --max-size with --min-size"""
import json
import os
import os.path
import osxphotos
from osxphotos.cli import query
runner = CliRunner()
cwd = os.getcwd()
result = runner.invoke(
query,
[
"--json",
"--db",
os.path.join(cwd, PHOTOS_DB_15_7),
"--min-size",
"48MB",
"--max-size",
"49MB",
],
)
assert result.exit_code == 0
json_got = json.loads(result.output)
assert len(json_got) == 1
def test_query_min_size_error():
""" test query --max-size with invalid size """
import json
import os
import os.path
import osxphotos
from osxphotos.cli import query
runner = CliRunner()
cwd = os.getcwd()
result = runner.invoke(
query,
["--json", "--db", os.path.join(cwd, PHOTOS_DB_15_7), "--min-size", "500 foo"],
)
assert result.exit_code != 0

View File

@@ -28,6 +28,7 @@ UUID_DICT = {
"mojave_album_1": "15uNd7%8RguTEgNPKHfTWw",
"date_modified": "A9B73E13-A6F2-4915-8D67-7213B39BAE9F",
"date_not_modified": "128FB4C6-0B16-4E7D-9108-FB2E90DA1546",
"favorite": "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51",
}
UUID_MEDIA_TYPE = {
@@ -133,6 +134,9 @@ UUID_EXIFTOOL = {
"England,London,London 2018,St. James's Park,UK,United Kingdom"
],
},
"1EB2B765-0765-43BA-A90C-0D0580E6172C": {
"{exiftool:EXIF:SubSecTimeOriginal}": ["22"]
},
}
TEMPLATE_VALUES = {
@@ -173,6 +177,8 @@ TEMPLATE_VALUES = {
"{exif.lens_model}": "iPhone 6s back camera 4.15mm f/2.2",
"{album?{folder_album},{created.year}/{created.mm}}": "2020/02",
"{title?Title is '{title} - {descr}',No Title}": "Title is 'Glen Ord - Jack Rose Dining Saloon'",
"{favorite}": "_",
"{favorite?FAV,NOTFAV}": "NOTFAV",
}
@@ -250,6 +256,77 @@ COMMENT_UUID_DICT = {
"4E4944A0-3E5C-4028-9600-A8709F2FA1DB": ["None: Nice trophy"],
}
UUID_PHOTO = {
"DC99FBDD-7A52-4100-A5BB-344131646C30": {
"{photo.title}": ["St. James's Park"],
"{photo.favorite?FAVORITE,NOTFAVORITE}": ["NOTFAVORITE"],
"{photo.hdr}": ["_"],
"{photo.keywords}": [
"England",
"London",
"London 2018",
"St. James's Park",
"UK",
"United Kingdom",
],
"{photo.keywords|lower}": [
"england",
"london",
"london 2018",
"st. james's park",
"uk",
"united kingdom",
],
},
"3DD2C897-F19E-4CA6-8C22-B027D5A71907": {"{photo.place.country_code}": ["AU"]},
"F12384F6-CD17-4151-ACBA-AE0E3688539E": {"{photo.place.name}": ["_"]},
"E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51": {"{photo.favorite}": ["favorite"]},
}
UUID_CONDITIONAL = {
"3DD2C897-F19E-4CA6-8C22-B027D5A71907": {
"{title matches Elder Park?YES,NO}": ["YES"],
"{title matches not Elder Park?YES,NO}": ["NO"],
"{title contains Park?YES,NO}": ["YES"],
"{title not contains Park?YES,NO}": ["NO"],
"{title matches Park?YES,NO}": ["NO"],
"{title matches Elder Park?YES,NO}": ["YES"],
"{title == Elder Park?YES,NO}": ["YES"],
"{title != Elder Park?YES,NO}": ["NO"],
"{title[ ,] == ElderPark?YES,NO}": ["YES"],
"{title not != Elder Park?YES,NO}": ["YES"],
"{title not == Elder Park?YES,NO}": ["NO"],
"{title endswith Park?YES,NO}": ["YES"],
"{title endswith Elder?YES,NO}": ["NO"],
"{title startswith Elder?YES,NO}": ["YES"],
"{title endswith Elder?YES,NO}": ["NO"],
"{photo.place.name contains Adelaide?YES,NO}": ["YES"],
"{photo.place.name|lower contains adelaide?YES,NO}": ["YES"],
"{photo.place.name|lower not contains adelaide?YES,NO}": ["NO"],
"{photo.score.overall < 0.7?YES,NO}": ["YES"],
"{photo.score.overall <= 0.7?YES,NO}": ["YES"],
"{photo.score.overall > 0.7?YES,NO}": ["NO"],
"{photo.score.overall >= 0.7?YES,NO}": ["NO"],
"{photo.score.overall not < 0.7?YES,NO}": ["NO"],
"{folder_album(-) contains Folder1-SubFolder2-AlbumInFolder?YES,NO}": ["YES"],
"{folder_album(-)[In,] contains Folder1-SubFolder2-AlbumFolder?YES,NO}": [
"YES"
],
},
"DC99FBDD-7A52-4100-A5BB-344131646C30": {
"{keyword == {keyword}?YES,NO}": ["YES"],
"{keyword contains England?YES,NO}": ["YES"],
"{keyword contains Eng?YES,NO}": ["YES"],
"{keyword contains Foo?YES,NO}": ["NO"],
"{keyword matches England?YES,NO}": ["YES"],
"{keyword matches Eng?YES,NO}": ["NO"],
"{keyword contains Foo|Bar|England?YES,NO}": ["YES"],
"{keyword contains Foo|Bar?YES,NO}": ["NO"],
"{keyword matches Foo|Bar|England?YES,NO}": ["YES"],
"{keyword matches Foo|Bar?YES,NO}": ["NO"],
},
}
@pytest.fixture(scope="module")
def photosdb_places():
@@ -304,7 +381,7 @@ def test_lookup_multi(photosdb_places):
for subst in TEMPLATE_SUBSTITUTIONS_MULTI_VALUED:
lookup_str = re.match(r"\{([^\\,}]+)\}", subst).group(1)
if subst == "{exiftool}":
if subst in ["{exiftool}", "{photo}", "{function}"]:
continue
lookup = template.get_template_value_multi(lookup_str, path_sep=os.path.sep)
assert isinstance(lookup, list)
@@ -828,6 +905,14 @@ def test_edited(photosdb):
assert rendered == ["edited"]
def test_favorite(photosdb):
""" Test favorite"""
photo = photosdb.get_photo(UUID_MULTI_KEYWORDS)
photomock = PhotoInfoMock(photo, favorite=True)
rendered, _ = photomock.render_template("{favorite}")
assert rendered == ["favorite"]
def test_nested_template_bool(photosdb):
photo = photosdb.get_photo(UUID_MULTI_KEYWORDS)
template = "{hdr?{edited?HDR_EDITED,HDR_NOT_EDITED},{edited?NOT_HDR_EDITED,NOT_HDR_NOT_EDITED}}"
@@ -865,3 +950,35 @@ def test_punctuation(photosdb):
rendered, _ = photo.render_template("{" + punc + "}")
assert rendered[0] == PUNCTUATION[punc]
def test_photo_template(photosdb):
for uuid in UUID_PHOTO:
photo = photosdb.get_photo(uuid)
for template in UUID_PHOTO[uuid]:
rendered, _ = photo.render_template(template)
assert sorted(rendered) == sorted(UUID_PHOTO[uuid][template])
def test_conditional(photosdb):
for uuid in UUID_CONDITIONAL:
photo = photosdb.get_photo(uuid)
for template in UUID_CONDITIONAL[uuid]:
rendered, _ = photo.render_template(template)
assert sorted(rendered) == sorted(UUID_CONDITIONAL[uuid][template])
def test_function(photosdb):
""" Test {function} """
photo = photosdb.get_photo(UUID_MULTI_KEYWORDS)
rendered, _ = photo.render_template("{function:tests/template_function.py::foo}")
assert rendered == [f"{photo.original_filename}-FOO"]
def test_function_bad(photosdb):
""" Test invalid {function} """
photo = photosdb.get_photo(UUID_MULTI_KEYWORDS)
with pytest.raises(ValueError):
rendered, _ = photo.render_template(
"{function:tests/template_function.py::foobar}"
)