Compare commits

...

35 Commits

Author SHA1 Message Date
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
Rhet Turnbull
a941f66d62 Fixed albums for burst images, closes #401, #403, #404 2021-03-27 08:11:33 -07:00
dependabot[bot]
d77eba12b2 Bump pyyaml from 5.1.2 to 5.4 (#402)
Bumps [pyyaml](https://github.com/yaml/pyyaml) from 5.1.2 to 5.4.
- [Release notes](https://github.com/yaml/pyyaml/releases)
- [Changelog](https://github.com/yaml/pyyaml/blob/master/CHANGES)
- [Commits](https://github.com/yaml/pyyaml/compare/5.1.2...5.4)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-25 21:06:24 -07:00
Rhet Turnbull
de94fd76de Updated CHANGELOG.md, [skip ci] 2021-03-21 23:18:41 -07:00
Rhet Turnbull
1026473684 Added --from-time, --to-time, closes #400 2021-03-21 22:57:18 -07:00
dependabot[bot]
3f9c9893c3 Bump pillow from 7.2.0 to 8.1.1 (#399)
Bumps [pillow](https://github.com/python-pillow/Pillow) from 7.2.0 to 8.1.1.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/7.2.0...8.1.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-19 08:15:36 -07:00
Rhet Turnbull
574cdd65a3 Updated CHANGELOG.md, [skip ci] 2021-03-16 07:02:35 -07:00
Rhet Turnbull
5b9547669e Add --cleanup files to report, #395 2021-03-14 14:58:35 -07:00
allcontributors[bot]
35b5bbd13d docs: add AaronVanGeffen as a contributor (#398)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-03-14 12:19:26 -07:00
Aaron van Geffen
6870ad0d8e Use original filename to export photos by default (#396)
* Use original filename to export photos by default, #176

* Fixed edited filename to also use original_filename if none provided.

* Rolled back previous change due to test failures

* Use original filename for edited file exports as well

Co-authored-by: Rhet Turnbull <rturnbull@gmail.com>
2021-03-14 12:19:05 -07:00
Rhet Turnbull
17ac5949e1 Updated docs for --cleanup, #394 2021-03-14 12:16:26 -07:00
56 changed files with 4508 additions and 791 deletions

View File

@@ -184,6 +184,24 @@
"contributions": [ "contributions": [
"code" "code"
] ]
},
{
"login": "AaronVanGeffen",
"name": "Aaron van Geffen",
"avatar_url": "https://avatars.githubusercontent.com/u/604665?v=4",
"profile": "https://aaronweb.net/",
"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, "contributorsPerLine": 7,

View File

@@ -4,6 +4,90 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [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
- Bump pillow from 7.2.0 to 8.1.1 [`#399`](https://github.com/RhetTbull/osxphotos/pull/399)
- Added --from-time, --to-time, closes #400 [`#400`](https://github.com/RhetTbull/osxphotos/issues/400)
#### [v0.41.3](https://github.com/RhetTbull/osxphotos/compare/v0.41.2...v0.41.3)
> 14 March 2021
- docs: add AaronVanGeffen as a contributor [`#398`](https://github.com/RhetTbull/osxphotos/pull/398)
- Use original filename to export photos by default [`#396`](https://github.com/RhetTbull/osxphotos/pull/396)
- Updated docs for --cleanup, #394 [`17ac594`](https://github.com/RhetTbull/osxphotos/commit/17ac5949e15057379eb13b979d4d7498bbb94d67)
- Add --cleanup files to report, #395 [`5b95476`](https://github.com/RhetTbull/osxphotos/commit/5b9547669ed6622ae06607e024315e383c0b2d98)
#### [v0.41.2](https://github.com/RhetTbull/osxphotos/compare/v0.41.1...v0.41.2)
> 14 March 2021
- Fix for long descriptions with exiftool, #393 [`ffb9af1`](https://github.com/RhetTbull/osxphotos/commit/ffb9af1965668bcfc2422f08b2462964a7dae3e2)
#### [v0.41.1](https://github.com/RhetTbull/osxphotos/compare/v0.41.0...v0.41.1)
> 5 March 2021
- Bug fix, convert PosixPath to str, #392 [`595307a`](https://github.com/RhetTbull/osxphotos/commit/595307a003c8ae5d3bee3ad161bb880d884b3cc3)
#### [v0.41.0](https://github.com/RhetTbull/osxphotos/compare/v0.40.19...v0.41.0) #### [v0.41.0](https://github.com/RhetTbull/osxphotos/compare/v0.40.19...v0.41.0)
> 22 February 2021 > 22 February 2021

352
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) [![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) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/osxphotos)
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section --> <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-19-orange.svg?style=flat)](#contributors) [![All Contributors](https://img.shields.io/badge/all_contributors-21-orange.svg?style=flat)](#contributors)
<!-- ALL-CONTRIBUTORS-BADGE:END --> <!-- 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. 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.
@@ -183,41 +183,57 @@ Options:
use in the following order: 1. last opened use in the following order: 1. last opened
library, 2. system library, 3. library, 2. system library, 3.
~/Pictures/Photos Library.photoslibrary ~/Pictures/Photos Library.photoslibrary
-V, --verbose Print verbose output. -V, --verbose Print verbose output.
--keyword KEYWORD Search for photos with keyword KEYWORD. If --keyword KEYWORD Search for photos with keyword KEYWORD. If
more than one keyword, treated as "OR", e.g. more than one keyword, treated as "OR", e.g.
find photos matching any keyword find photos matching any keyword
--person PERSON Search for photos with person PERSON. If more --person PERSON Search for photos with person PERSON. If more
than one person, treated as "OR", e.g. find than one person, treated as "OR", e.g. find
photos matching any person photos matching any person
--album ALBUM Search for photos in album ALBUM. If more than --album ALBUM Search for photos in album ALBUM. If more than
one album, treated as "OR", e.g. find photos one album, treated as "OR", e.g. find photos
matching any album matching any album
--folder FOLDER Search for photos in an album in folder --folder FOLDER Search for photos in an album in folder
FOLDER. If more than one folder, treated as FOLDER. If more than one folder, treated as
"OR", e.g. find photos in any FOLDER. Only "OR", e.g. find photos in any FOLDER. Only
searches top level folders (e.g. does not look searches top level folders (e.g. does not look
at subfolders) 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 UUID Search for photos with UUID(s).
--uuid-from-file FILE Search for photos with UUID(s) loaded from --uuid-from-file FILE Search for photos with UUID(s) loaded from
FILE. Format is a single UUID per line. Lines FILE. Format is a single UUID per line. Lines
preceded with # are ignored. preceded with # are ignored.
--title TITLE Search for TITLE in title of photo. --title TITLE Search for TITLE in title of photo.
--no-title Search for photos with no title. --no-title Search for photos with no title.
--description DESC Search for DESC in description of photo. --description DESC Search for DESC in description of photo.
--no-description Search for photos with no description. --no-description Search for photos with no description.
--place PLACE Search for PLACE in photo's reverse --place PLACE Search for PLACE in photo's reverse
geolocation info geolocation info
--no-place Search for photos with no associated place --no-place Search for photos with no associated place
name info (no reverse geolocation info) name info (no reverse geolocation info)
--label LABEL Search for photos with image classification --label LABEL Search for photos with image classification
label LABEL (Photos 5 only). If more than one label LABEL (Photos 5 only). If more than one
label, treated as "OR", e.g. find photos label, treated as "OR", e.g. find photos
matching any label matching any label
--uti UTI Search for photos whose uniform type --uti UTI Search for photos whose uniform type
identifier (UTI) matches UTI identifier (UTI) matches UTI
-i, --ignore-case Case insensitive search for title, -i, --ignore-case Case insensitive search for title,
description, place, keyword, person, or album. description, place, keyword, person, or album.
--edited Search for photos that have been edited. --edited Search for photos that have been edited.
--external-edit Search for photos edited in external editor. --external-edit Search for photos edited in external editor.
--favorite Search for photos marked favorite. --favorite Search for photos marked favorite.
@@ -226,47 +242,67 @@ Options:
--not-hidden Search for photos not marked hidden. --not-hidden Search for photos not marked hidden.
--shared Search for photos in shared iCloud album --shared Search for photos in shared iCloud album
(Photos 5 only). (Photos 5 only).
--not-shared Search for photos not in shared iCloud album --not-shared Search for photos not in shared iCloud album
(Photos 5 only). (Photos 5 only).
--burst Search for photos that were taken in a burst. --burst Search for photos that were taken in a burst.
--not-burst Search for photos that are not part of a --not-burst Search for photos that are not part of a
burst. burst.
--live Search for Apple live photos --live Search for Apple live photos
--not-live Search for photos that are not Apple live --not-live Search for photos that are not Apple live
photos. photos.
--portrait Search for Apple portrait mode photos. --portrait Search for Apple portrait mode photos.
--not-portrait Search for photos that are not Apple portrait --not-portrait Search for photos that are not Apple portrait
mode photos. mode photos.
--screenshot Search for screenshot photos. --screenshot Search for screenshot photos.
--not-screenshot Search for photos that are not screenshot --not-screenshot Search for photos that are not screenshot
photos. photos.
--slow-mo Search for slow motion videos. --slow-mo Search for slow motion videos.
--not-slow-mo Search for photos that are not slow motion --not-slow-mo Search for photos that are not slow motion
videos. videos.
--time-lapse Search for time lapse videos. --time-lapse Search for time lapse videos.
--not-time-lapse Search for photos that are not time lapse --not-time-lapse Search for photos that are not time lapse
videos. videos.
--hdr Search for high dynamic range (HDR) photos. --hdr Search for high dynamic range (HDR) photos.
--not-hdr Search for photos that are not HDR photos. --not-hdr Search for photos that are not HDR photos.
--selfie Search for selfies (photos taken with front- --selfie Search for selfies (photos taken with front-
facing cameras). facing cameras).
--not-selfie Search for photos that are not selfies. --not-selfie Search for photos that are not selfies.
--panorama Search for panorama photos. --panorama Search for panorama photos.
--not-panorama Search for photos that are not panoramas. --not-panorama Search for photos that are not panoramas.
--has-raw Search for photos with both a jpeg and raw --has-raw Search for photos with both a jpeg and raw
version version
--only-movies Search only for movies (default searches both --only-movies Search only for movies (default searches both
images and movies). images and movies).
--only-photos Search only for photos/images (default --only-photos Search only for photos/images (default
searches both images and movies). searches both images and movies).
--from-date DATETIME Search by start item date, e.g.
--from-date DATETIME Search by item start date, e.g.
2000-01-12T12:00:00, 2000-01-12T12:00:00,
2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO
8601). 8601 with/without timezone).
--to-date DATETIME Search by end item date, e.g.
--to-date DATETIME Search by item end date, e.g.
2000-01-12T12:00:00, 2000-01-12T12:00:00,
2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO
8601). 8601 with/without timezone).
--from-time TIME Search by item start time of day, e.g. 12:00,
or 12:00:00.
--to-time TIME Search by item end time of day, e.g. 12:00 or
12:00:00.
--has-comment Search for photos that have comments. --has-comment Search for photos that have comments.
--no-comment Search for photos with no comments. --no-comment Search for photos with no comments.
--has-likes Search for photos that have likes. --has-likes Search for photos that have likes.
@@ -274,17 +310,38 @@ Options:
--is-reference Search for photos that were imported as --is-reference Search for photos that were imported as
referenced files (not copied into Photos referenced files (not copied into Photos
library). library).
--in-album Search for photos that are in one or more --in-album Search for photos that are in one or more
albums. albums.
--not-in-album Search for photos that are not in any albums. --not-in-album Search for photos that are not in any albums.
--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 --missing Export only photos missing from the Photos
library; must be used with --download-missing. library; must be used with --download-missing.
--deleted Include photos from the 'Recently Deleted' --deleted Include photos from the 'Recently Deleted'
folder. folder.
--deleted-only Include only photos from the 'Recently --deleted-only Include only photos from the 'Recently
Deleted' folder. Deleted' folder.
--update Only export new or updated files. See notes --update Only export new or updated files. See notes
below on export and --update. below on export and --update.
--ignore-signature When used with '--update', ignores file --ignore-signature When used with '--update', ignores file
signature when updating files. This is useful signature when updating files. This is useful
if you have processed or edited exported if you have processed or edited exported
@@ -303,12 +360,15 @@ Options:
not; 3) if a sidecar does not exist for the not; 3) if a sidecar does not exist for the
photo, a sidecar will be written whether or photo, a sidecar will be written whether or
not the photo file was written or updated. not the photo file was written or updated.
--only-new If used with --update, ignores any previously --only-new If used with --update, ignores any previously
exported files, even if missing from the exported files, even if missing from the
export folder and only exports new files that export folder and only exports new files that
haven't previously been exported. haven't previously been exported.
--dry-run Dry run (test) the export but don't actually --dry-run Dry run (test) the export but don't actually
export any files; most useful with --verbose. export any files; most useful with --verbose.
--export-as-hardlink Hardlink files instead of copying them. Cannot --export-as-hardlink Hardlink files instead of copying them. Cannot
be used with --exiftool which creates copies be used with --exiftool which creates copies
of the files with embedded EXIF data. Note: on of the files with embedded EXIF data. Note: on
@@ -316,41 +376,58 @@ Options:
giving many of the same advantages as giving many of the same advantages as
hardlinks without having to use --export-as- hardlinks without having to use --export-as-
hardlink. hardlink.
--touch-file Sets the file's modification time to match --touch-file Sets the file's modification time to match
photo date. photo date.
--overwrite Overwrite existing files. Default behavior is --overwrite Overwrite existing files. Default behavior is
to add (1), (2), etc to filename if file to add (1), (2), etc to filename if file
already exists. Use this with caution as it already exists. Use this with caution as it
may create name collisions on export. (e.g. if may create name collisions on export. (e.g. if
two files happen to have the same name) 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 --export-by-date Automatically create output folders to
organize photos by date created (e.g. organize photos by date created (e.g.
DEST/2019/12/20/photoname.jpg). DEST/2019/12/20/photoname.jpg).
--skip-edited Do not export edited version of photo if an --skip-edited Do not export edited version of photo if an
edited version exists. edited version exists.
--skip-original-if-edited Do not export original if there is an edited --skip-original-if-edited Do not export original if there is an edited
version (exports only the edited version). version (exports only the edited version).
--skip-bursts Do not export all associated burst images in --skip-bursts Do not export all associated burst images in
the library if a photo is a burst photo. the library if a photo is a burst photo.
--skip-live Do not export the associated live video --skip-live Do not export the associated live video
component of a live photo. component of a live photo.
--skip-raw Do not export associated raw images of a --skip-raw Do not export associated raw images of a
RAW+JPEG pair. Note: this does not skip raw RAW+JPEG pair. Note: this does not skip raw
photos if the raw photo does not have an photos if the raw photo does not have an
associated jpeg image (e.g. the raw file was associated jpeg image (e.g. the raw file was
imported to Photos without a jpeg preview). imported to Photos without a jpeg preview).
--current-name Use photo's current filename instead of --current-name Use photo's current filename instead of
original filename for export. Note: Starting original filename for export. Note: Starting
with Photos 5, all photos are renamed upon with Photos 5, all photos are renamed upon
import. By default, photos are exported with import. By default, photos are exported with
the the original name they had before import. the the original name they had before import.
--convert-to-jpeg Convert all non-jpeg images (e.g. raw, HEIC, --convert-to-jpeg Convert all non-jpeg images (e.g. raw, HEIC,
PNG, etc) to JPEG upon export. Only works if PNG, etc) to JPEG upon export. Only works if
your Mac has a GPU. your Mac has a GPU.
--jpeg-quality FLOAT RANGE Value in range 0.0 to 1.0 to use with --jpeg-quality FLOAT RANGE Value in range 0.0 to 1.0 to use with
--convert-to-jpeg. A value of 1.0 specifies --convert-to-jpeg. A value of 1.0 specifies
best quality, a value of 0.0 specifies maximum best quality, a value of 0.0 specifies maximum
compression. Defaults to 1.0 compression. Defaults to 1.0
--download-missing Attempt to download missing photos from --download-missing Attempt to download missing photos from
iCloud. The current implementation uses iCloud. The current implementation uses
Applescript to interact with Photos to export Applescript to interact with Photos to export
@@ -363,6 +440,7 @@ Options:
export all burst images; only the primary export all burst images; only the primary
photo will be exported--associated burst photo will be exported--associated burst
images will be skipped. images will be skipped.
--sidecar FORMAT Create sidecar for each photo exported; valid --sidecar FORMAT Create sidecar for each photo exported; valid
FORMAT values: xmp, json, exiftool; --sidecar FORMAT values: xmp, json, exiftool; --sidecar
xmp: create XMP sidecar used by Digikam, Adobe xmp: create XMP sidecar used by Digikam, Adobe
@@ -389,6 +467,7 @@ Options:
tags exported in the JSON and exiftool tags exported in the JSON and exiftool
sidecar, see '--exiftool'. See also '--ignore- sidecar, see '--exiftool'. See also '--ignore-
signature'. signature'.
--sidecar-drop-ext Drop the photo's extension when naming sidecar --sidecar-drop-ext Drop the photo's extension when naming sidecar
files. By default, sidecar files are named in files. By default, sidecar files are named in
format 'photo_filename.photo_ext.sidecar_ext', format 'photo_filename.photo_ext.sidecar_ext',
@@ -400,6 +479,7 @@ Options:
of different types but the same name in the of different types but the same name in the
output directory, e.g. 'IMG_1234.JPG' and output directory, e.g. 'IMG_1234.JPG' and
'IMG_1234.MOV'. 'IMG_1234.MOV'.
--exiftool Use exiftool to write metadata directly to --exiftool Use exiftool to write metadata directly to
exported photos. To use this option, exiftool exported photos. To use this option, exiftool
must be installed and in the path. exiftool must be installed and in the path. exiftool
@@ -421,8 +501,10 @@ Options:
QuickTime:ModifyDate (see also --ignore-date- QuickTime:ModifyDate (see also --ignore-date-
modified); QuickTime:GPSCoordinates; modified); QuickTime:GPSCoordinates;
UserData:GPSCoordinates. UserData:GPSCoordinates.
--exiftool-path EXIFTOOL_PATH Optionally specify path to exiftool; if not --exiftool-path EXIFTOOL_PATH Optionally specify path to exiftool; if not
provided, will look for exiftool in $PATH. provided, will look for exiftool in $PATH.
--exiftool-option OPTION Optional flag/option to pass to exiftool when --exiftool-option OPTION Optional flag/option to pass to exiftool when
using --exiftool. For example, --exiftool- using --exiftool. For example, --exiftool-
option '-m' to ignore minor warnings. Specify option '-m' to ignore minor warnings. Specify
@@ -432,21 +514,27 @@ Options:
full list of options. More than one option may full list of options. More than one option may
be specified by repeating the option, e.g. be specified by repeating the option, e.g.
--exiftool-option '-m' --exiftool-option '-F'. --exiftool-option '-m' --exiftool-option '-F'.
--exiftool-merge-keywords Merge any keywords found in the original file --exiftool-merge-keywords Merge any keywords found in the original file
with keywords used for '--exiftool' and '-- with keywords used for '--exiftool' and '--
sidecar'. sidecar'.
--exiftool-merge-persons Merge any persons found in the original file --exiftool-merge-persons Merge any persons found in the original file
with persons used for '--exiftool' and '-- with persons used for '--exiftool' and '--
sidecar'. sidecar'.
--ignore-date-modified If used with --exiftool or --sidecar, will --ignore-date-modified If used with --exiftool or --sidecar, will
ignore the photo modification date and set ignore the photo modification date and set
EXIF:ModifyDate to EXIF:DateTimeOriginal; this EXIF:ModifyDate to EXIF:DateTimeOriginal; this
is consistent with how Photos handles the is consistent with how Photos handles the
EXIF:ModifyDate tag. EXIF:ModifyDate tag.
--person-keyword Use person in image as keyword/tag when --person-keyword Use person in image as keyword/tag when
exporting metadata. exporting metadata.
--album-keyword Use album name as keyword/tag when exporting --album-keyword Use album name as keyword/tag when exporting
metadata. metadata.
--keyword-template TEMPLATE For use with --exiftool, --sidecar; specify a --keyword-template TEMPLATE For use with --exiftool, --sidecar; specify a
template string to use as keyword in the form template string to use as keyword in the form
'{name,DEFAULT}' This is the same format as '{name,DEFAULT}' This is the same format as
@@ -459,6 +547,7 @@ Options:
"{folder_album}" --keyword-template "{folder_album}" --keyword-template
"{created.year}". See '--replace-keywords' and "{created.year}". See '--replace-keywords' and
Templating System below. Templating System below.
--replace-keywords Replace keywords with any values specified --replace-keywords Replace keywords with any values specified
with --keyword-template. By default, with --keyword-template. By default,
--keyword-template will add keywords to any --keyword-template will add keywords to any
@@ -467,6 +556,7 @@ Options:
from --keyword-template will replace any from --keyword-template will replace any
existing keywords instead of adding additional existing keywords instead of adding additional
keywords. keywords.
--description-template TEMPLATE --description-template TEMPLATE
For use with --exiftool, --sidecar; specify a For use with --exiftool, --sidecar; specify a
template string to use as description in the template string to use as description in the
@@ -477,6 +567,7 @@ Options:
--description-template "{descr} exported with --description-template "{descr} exported with
osxphotos on {today.date}" See Templating osxphotos on {today.date}" See Templating
System below. System below.
--finder-tag-template TEMPLATE Set MacOS Finder tags to TEMPLATE. These tags --finder-tag-template TEMPLATE Set MacOS Finder tags to TEMPLATE. These tags
can be searched in the Finder or Spotlight can be searched in the Finder or Spotlight
with 'tag:tagname' format. For example, '-- with 'tag:tagname' format. For example, '--
@@ -485,11 +576,13 @@ Options:
TEMPLATE values by using '--finder-tag- TEMPLATE values by using '--finder-tag-
template' multiple times. See also '--finder- template' multiple times. See also '--finder-
tag-keywords and Extended Attributes below.'. tag-keywords and Extended Attributes below.'.
--finder-tag-keywords Set MacOS Finder tags to keywords; any --finder-tag-keywords Set MacOS Finder tags to keywords; any
keywords specified via '--keyword-template', ' keywords specified via '--keyword-template', '
--person-keyword', etc. will also be used as --person-keyword', etc. will also be used as
Finder tags. See also '--finder-tag-template Finder tags. See also '--finder-tag-template
and Extended Attributes below.'. and Extended Attributes below.'.
--xattr-template ATTRIBUTE TEMPLATE --xattr-template ATTRIBUTE TEMPLATE
Set extended attribute ATTRIBUTE to TEMPLATE Set extended attribute ATTRIBUTE to TEMPLATE
value. Valid attributes are: 'authors', value. Valid attributes are: 'authors',
@@ -500,16 +593,19 @@ Options:
findercomment "{title}; {descr}" See Extended findercomment "{title}; {descr}" See Extended
Attributes below for additional details on Attributes below for additional details on
this option. this option.
--directory DIRECTORY Optional template for specifying name of --directory DIRECTORY Optional template for specifying name of
output directory in the form '{name,DEFAULT}'. output directory in the form '{name,DEFAULT}'.
See below for additional details on templating See below for additional details on templating
system. system.
--filename FILENAME Optional template for specifying name of --filename FILENAME Optional template for specifying name of
output file in the form '{name,DEFAULT}'. File output file in the form '{name,DEFAULT}'. File
extension will be added automatically--do not extension will be added automatically--do not
include an extension in the FILENAME template. include an extension in the FILENAME template.
See below for additional details on templating See below for additional details on templating
system. system.
--jpeg-ext EXTENSION Specify file extension for JPEG files. Photos --jpeg-ext EXTENSION Specify file extension for JPEG files. Photos
uses .jpeg for edited images but many images uses .jpeg for edited images but many images
are imported with .jpg or .JPG which can are imported with .jpg or .JPG which can
@@ -519,12 +615,14 @@ Options:
exported JPEG images. Valid values are jpeg, exported JPEG images. Valid values are jpeg,
jpg, JPEG, JPG; e.g. '--jpeg-ext jpg' to use jpg, JPEG, JPG; e.g. '--jpeg-ext jpg' to use
'.jpg' for all JPEGs. '.jpg' for all JPEGs.
--strip Optionally strip leading and trailing --strip Optionally strip leading and trailing
whitespace from any rendered templates. For whitespace from any rendered templates. For
example, if --filename template is "{title,} example, if --filename template is "{title,}
{original_name}" and image has no title, {original_name}" and image has no title,
resulting file would have a leading space but resulting file would have a leading space but
if used with --strip, this will be removed. if used with --strip, this will be removed.
--edited-suffix SUFFIX Optional suffix template for naming edited --edited-suffix SUFFIX Optional suffix template for naming edited
photos. Default name for edited photos is in photos. Default name for edited photos is in
form 'photoname_edited.ext'. For example, with form 'photoname_edited.ext'. For example, with
@@ -534,6 +632,7 @@ Options:
suffix is '_edited'. Multi-value templates suffix is '_edited'. Multi-value templates
(see Templating System) are not permitted with (see Templating System) are not permitted with
--edited-suffix. --edited-suffix.
--original-suffix SUFFIX Optional suffix template for naming original --original-suffix SUFFIX Optional suffix template for naming original
photos. Default name for original photos is photos. Default name for original photos is
in form 'filename.ext'. For example, with '-- in form 'filename.ext'. For example, with '--
@@ -542,9 +641,11 @@ Options:
default suffix is '' (no suffix). Multi-value default suffix is '' (no suffix). Multi-value
templates (see Templating System) are not templates (see Templating System) are not
permitted with --original-suffix. permitted with --original-suffix.
--use-photos-export Force the use of AppleScript or PhotoKit to --use-photos-export Force the use of AppleScript or PhotoKit to
export even if not missing (see also '-- export even if not missing (see also '--
download-missing' and '--use-photokit'). download-missing' and '--use-photokit').
--use-photokit Use with '--download-missing' or '--use- --use-photokit Use with '--download-missing' or '--use-
photos-export' to use direct Photos interface photos-export' to use direct Photos interface
instead of AppleScript to export. Highly instead of AppleScript to export. Highly
@@ -552,14 +653,23 @@ Options:
iTerm2 (use with Terminal.app). This is faster iTerm2 (use with Terminal.app). This is faster
and more reliable than the default AppleScript and more reliable than the default AppleScript
interface. interface.
--report <path to export report> --report <path to export report>
Write a CSV formatted report of all files that Write a CSV formatted report of all files that
were exported. were exported.
--cleanup Cleanup export directory by deleting any files --cleanup Cleanup export directory by deleting any files
which were not included in this export set. which were not included in this export set.
For example, photos which had previously been For example, photos which had previously been
exported and were subsequently deleted in exported and were subsequently deleted in
Photos. Photos. WARNING: --cleanup will delete *any*
files in the export directory that were not
exported by osxphotos, for example, your own
scripts or other files. Be sure this is what
you intend before using --cleanup. Use --dry-
run with --cleanup first if you're not
certain.
--exportdb EXPORTDB_FILE Specify alternate name for database file which --exportdb EXPORTDB_FILE Specify alternate name for database file which
stores state information for export and stores state information for export and
--update. If --exportdb is not specified, --update. If --exportdb is not specified,
@@ -568,6 +678,7 @@ Options:
directory. Must be specified as filename directory. Must be specified as filename
only, not a path, as export database will be only, not a path, as export database will be
saved in export directory. saved in export directory.
--load-config <config file path> --load-config <config file path>
Load options from file as written with --save- Load options from file as written with --save-
config. This allows you to save a complex config. This allows you to save a complex
@@ -579,9 +690,11 @@ Options:
line options are used in conjunction with line options are used in conjunction with
--load-config, they will override the --load-config, they will override the
corresponding values in the config file. corresponding values in the config file.
--save-config <config file path> --save-config <config file path>
Save options to file for use with --load- Save options to file for use with --load-
config. File format is TOML. config. File format is TOML.
--help Show this message and exit. --help Show this message and exit.
** Export ** ** Export **
@@ -636,25 +749,32 @@ The following attributes may be used with '--xattr-template':
authors The author, or authors, of the contents of the file. A list of authors The author, or authors, of the contents of the file. A list of
strings. (com.apple.metadata:kMDItemAuthors) strings. (com.apple.metadata:kMDItemAuthors)
comment A comment related to the file. This differs from the Finder comment A comment related to the file. This differs from the Finder
comment, kMDItemFinderComment. A string. comment, kMDItemFinderComment. A string.
(com.apple.metadata:kMDItemComment) (com.apple.metadata:kMDItemComment)
copyright The copyright owner of the file contents. A string. copyright The copyright owner of the file contents. A string.
(com.apple.metadata:kMDItemCopyright) (com.apple.metadata:kMDItemCopyright)
description A description of the content of the resource. The description description A description of the content of the resource. The description
may include an abstract, table of contents, reference to a may include an abstract, table of contents, reference to a
graphical representation of content or a free-text account of graphical representation of content or a free-text account of
the content. A string. (com.apple.metadata:kMDItemDescription) the content. A string. (com.apple.metadata:kMDItemDescription)
findercomment Finder comments for this file. A string. findercomment Finder comments for this file. A string.
(com.apple.metadata:kMDItemFinderComment) (com.apple.metadata:kMDItemFinderComment)
headline A publishable entry providing a synopsis of the contents of the headline A publishable entry providing a synopsis of the contents of the
file. A string. (com.apple.metadata:kMDItemHeadline) file. A string. (com.apple.metadata:kMDItemHeadline)
keywords Keywords associated with this file. For example, “Birthday”, keywords Keywords associated with this file. For example, “Birthday”,
“Important”, etc. This differs from Finder tags “Important”, etc. This differs from Finder tags
(_kMDItemUserTags) which are keywords/tags shown in the Finder (_kMDItemUserTags) which are keywords/tags shown in the Finder
and searchable in Spotlight using "tag:tag_name". A list of and searchable in Spotlight using "tag:tag_name". A list of
strings. (com.apple.metadata:kMDItemKeywords) strings. (com.apple.metadata:kMDItemKeywords)
For additional information on extended attributes see: https://developer.apple.c For additional information on extended attributes see: https://developer.apple.c
om/documentation/coreservices/file_metadata/mditem/common_metadata_attribute_key om/documentation/coreservices/file_metadata/mditem/common_metadata_attribute_key
s s
@@ -671,8 +791,8 @@ example "{title}" which would resolve to the title of the photo.
Template statements may contain one or more modifiers. The full syntax is: Template statements may contain one or more modifiers. The full syntax is:
"pretext{delim+template_field:subfield|filter(path_sep)[find,replace]?bool_value "pretext{delim+template_field:subfield|filter(path_sep)[find,replace]
,default}posttext" conditional?bool_value,default}posttext"
Template statements are white-space sensitive meaning that white space (spaces, Template statements are white-space sensitive meaning that white space (spaces,
tabs) changes the meaning of the template statement. tabs) changes the meaning of the template statement.
@@ -738,20 +858,75 @@ e.g. If Photo is in Album1 in Folder1:
• "{folder_album(>)}" renders to ["Folder1>Album1"] • "{folder_album(>)}" renders to ["Folder1>Album1"]
• "{folder_album()}" renders to ["Folder1Album1"] • "{folder_album()}" renders to ["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 For example, to replace "/" in an album name, you could use the template
"{album[/,-]}". Multiple replacements can be made by appending "|" and adding "{album[/,-]}". Multiple replacements can be made by appending "|" and adding
another find|replace pair. e.g. to replace both "/" and ":" in album name: another find|replace pair. e.g. to replace both "/" and ":" in album name:
"{album[/,-|:,-]}". find/replace pairs are not limited to single characters. "{album[/,-|:,-]}". find/replace pairs are not limited to single characters.
The "|" character cannot be used in a find/replace pair. The "|" character cannot be used in a find/replace pair.
?bool_value: Template fields may be evaluated as boolean by appending "?" after conditional: optional conditional expression that is evaluated as boolean
the field name (and following "(path_sep)" or "[find/replace]". If a field is (True/False) for use with the ?bool_value modifier. Conditional expressions
True (e.g. photo is HDR and field is "{hdr}") or has any value, the value take the form ' not operator value' where not is an optional modifier that
following the "?" will be used to render the template instead of the actual negates the operator. Note: the space before the conditional expression is
field value. If the template field evaluates to False (e.g. in above example, required if you use a conditional expression. Valid comparison operators are:
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. • 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, e.g. if photo is an HDR image,
@@ -820,6 +995,7 @@ Substitution Description
{name} Current filename of the photo {name} Current filename of the photo
{original_name} Photo's original filename when imported to {original_name} Photo's original filename when imported to
Photos Photos
{title} Title of the photo {title} Title of the photo
{descr} Description of the photo {descr} Description of the photo
{media_type} Special media type resolved in this {media_type} Special media type resolved in this
@@ -829,30 +1005,45 @@ Substitution Description
'video' if no special type. Customize one or 'video' if no special type. Customize one or
more media types using format: '{media_type,vi more media types using format: '{media_type,vi
deo=vidéo;time_lapse=vidéo_accélérée}' deo=vidéo;time_lapse=vidéo_accélérée}'
{photo_or_video} 'photo' or 'video' depending on what type the {photo_or_video} 'photo' or 'video' depending on what type the
image is. To customize, use default value as image is. To customize, use default value as
in '{photo_or_video,photo=fotos;video=videos}' in '{photo_or_video,photo=fotos;video=videos}'
{hdr} Photo is HDR?; True/False value, use in format {hdr} Photo is HDR?; True/False value, use in format
'{hdr?VALUE_IF_TRUE,VALUE_IF_FALSE}' '{hdr?VALUE_IF_TRUE,VALUE_IF_FALSE}'
{edited} Photo has been edited (has adjustments)?; {edited} Photo has been edited (has adjustments)?;
True/False value, use in format True/False value, use in format
'{edited?VALUE_IF_TRUE,VALUE_IF_FALSE}' '{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. {created.date} Photo's creation date in ISO format, e.g.
'2020-03-22' '2020-03-22'
{created.year} 4-digit year of photo creation time {created.year} 4-digit year of photo creation time
{created.yy} 2-digit year of photo creation time {created.yy} 2-digit year of photo creation time
{created.mm} 2-digit month of the photo creation time (zero {created.mm} 2-digit month of the photo creation time (zero
padded) padded)
{created.month} Month name in user's locale of the photo {created.month} Month name in user's locale of the photo
creation time creation time
{created.mon} Month abbreviation in the user's locale of the {created.mon} Month abbreviation in the user's locale of the
photo creation time photo creation time
{created.dd} 2-digit day of the month (zero padded) of {created.dd} 2-digit day of the month (zero padded) of
photo creation time photo creation time
{created.dow} Day of week in user's locale of the photo {created.dow} Day of week in user's locale of the photo
creation time creation time
{created.doy} 3-digit day of year (e.g Julian day) of photo {created.doy} 3-digit day of year (e.g Julian day) of photo
creation time, starting from 1 (zero padded) creation time, starting from 1 (zero padded)
{created.hour} 2-digit hour of the photo creation time {created.hour} 2-digit hour of the photo creation time
{created.min} 2-digit minute of the photo creation time {created.min} 2-digit minute of the photo creation time
{created.sec} 2-digit second of the photo creation time {created.sec} 2-digit second of the photo creation time
@@ -865,38 +1056,51 @@ Substitution Description
no template will return null value. See no template will return null value. See
https://strftime.org/ for help on strftime https://strftime.org/ for help on strftime
templates. templates.
{modified.date} Photo's modification date in ISO format, e.g. {modified.date} Photo's modification date in ISO format, e.g.
'2020-03-22'; uses creation date if photo is '2020-03-22'; uses creation date if photo is
not modified not modified
{modified.year} 4-digit year of photo modification time; uses {modified.year} 4-digit year of photo modification time; uses
creation date if photo is not modified creation date if photo is not modified
{modified.yy} 2-digit year of photo modification time; uses {modified.yy} 2-digit year of photo modification time; uses
creation date if photo is not modified creation date if photo is not modified
{modified.mm} 2-digit month of the photo modification time {modified.mm} 2-digit month of the photo modification time
(zero padded); uses creation date if photo is (zero padded); uses creation date if photo is
not modified not modified
{modified.month} Month name in user's locale of the photo {modified.month} Month name in user's locale of the photo
modification time; uses creation date if photo modification time; uses creation date if photo
is not modified is not modified
{modified.mon} Month abbreviation in the user's locale of the {modified.mon} Month abbreviation in the user's locale of the
photo modification time; uses creation date if photo modification time; uses creation date if
photo is not modified photo is not modified
{modified.dd} 2-digit day of the month (zero padded) of the {modified.dd} 2-digit day of the month (zero padded) of the
photo modification time; uses creation date if photo modification time; uses creation date if
photo is not modified photo is not modified
{modified.dow} Day of week in user's locale of the photo {modified.dow} Day of week in user's locale of the photo
modification time; uses creation date if photo modification time; uses creation date if photo
is not modified is not modified
{modified.doy} 3-digit day of year (e.g Julian day) of photo {modified.doy} 3-digit day of year (e.g Julian day) of photo
modification time, starting from 1 (zero modification time, starting from 1 (zero
padded); uses creation date if photo is not padded); uses creation date if photo is not
modified modified
{modified.hour} 2-digit hour of the photo modification time; {modified.hour} 2-digit hour of the photo modification time;
uses creation date if photo is not modified uses creation date if photo is not modified
{modified.min} 2-digit minute of the photo modification time; {modified.min} 2-digit minute of the photo modification time;
uses creation date if photo is not modified uses creation date if photo is not modified
{modified.sec} 2-digit second of the photo modification time; {modified.sec} 2-digit second of the photo modification time;
uses creation date if photo is not modified uses creation date if photo is not modified
{modified.strftime} Apply strftime template to file modification {modified.strftime} Apply strftime template to file modification
date/time. Should be used in form date/time. Should be used in form
{modified.strftime,TEMPLATE} where TEMPLATE is {modified.strftime,TEMPLATE} where TEMPLATE is
@@ -907,21 +1111,28 @@ Substitution Description
creation date if photo is not modified. See creation date if photo is not modified. See
https://strftime.org/ for help on strftime https://strftime.org/ for help on strftime
templates. templates.
{today.date} Current date in iso format, e.g. '2020-03-22' {today.date} Current date in iso format, e.g. '2020-03-22'
{today.year} 4-digit year of current date {today.year} 4-digit year of current date
{today.yy} 2-digit year of current date {today.yy} 2-digit year of current date
{today.mm} 2-digit month of the current date (zero {today.mm} 2-digit month of the current date (zero
padded) padded)
{today.month} Month name in user's locale of the current {today.month} Month name in user's locale of the current
date date
{today.mon} Month abbreviation in the user's locale of the {today.mon} Month abbreviation in the user's locale of the
current date current date
{today.dd} 2-digit day of the month (zero padded) of {today.dd} 2-digit day of the month (zero padded) of
current date current date
{today.dow} Day of week in user's locale of the current {today.dow} Day of week in user's locale of the current
date date
{today.doy} 3-digit day of year (e.g Julian day) of {today.doy} 3-digit day of year (e.g Julian day) of
current date, starting from 1 (zero padded) current date, starting from 1 (zero padded)
{today.hour} 2-digit hour of the current date {today.hour} 2-digit hour of the current date
{today.min} 2-digit minute of the current date {today.min} 2-digit minute of the current date
{today.sec} 2-digit second of the current date {today.sec} 2-digit second of the current date
@@ -934,53 +1145,73 @@ Substitution Description
no template will return null value. See no template will return null value. See
https://strftime.org/ for help on strftime https://strftime.org/ for help on strftime
templates. templates.
{place.name} Place name from the photo's reverse {place.name} Place name from the photo's reverse
geolocation data, as displayed in Photos geolocation data, as displayed in Photos
{place.country_code} The ISO country code from the photo's reverse {place.country_code} The ISO country code from the photo's reverse
geolocation data geolocation data
{place.name.country} Country name from the photo's reverse {place.name.country} Country name from the photo's reverse
geolocation data geolocation data
{place.name.state_province} State or province name from the photo's {place.name.state_province} State or province name from the photo's
reverse geolocation data reverse geolocation data
{place.name.city} City or locality name from the photo's reverse {place.name.city} City or locality name from the photo's reverse
geolocation data geolocation data
{place.name.area_of_interest} Area of interest name (e.g. landmark or public {place.name.area_of_interest} Area of interest name (e.g. landmark or public
place) from the photo's reverse geolocation place) from the photo's reverse geolocation
data data
{place.address} Postal address from the photo's reverse {place.address} Postal address from the photo's reverse
geolocation data, e.g. '2007 18th St NW, geolocation data, e.g. '2007 18th St NW,
Washington, DC 20009, United States' Washington, DC 20009, United States'
{place.address.street} Street part of the postal address, e.g. '2007 {place.address.street} Street part of the postal address, e.g. '2007
18th St NW' 18th St NW'
{place.address.city} City part of the postal address, e.g. {place.address.city} City part of the postal address, e.g.
'Washington' 'Washington'
{place.address.state_province} State/province part of the postal address, {place.address.state_province} State/province part of the postal address,
e.g. 'DC' e.g. 'DC'
{place.address.postal_code} Postal code part of the postal address, e.g. {place.address.postal_code} Postal code part of the postal address, e.g.
'20009' '20009'
{place.address.country} Country name of the postal address, e.g. {place.address.country} Country name of the postal address, e.g.
'United States' 'United States'
{place.address.country_code} ISO country code of the postal address, e.g. {place.address.country_code} ISO country code of the postal address, e.g.
'US' 'US'
{searchinfo.season} Season of the year associated with a photo, {searchinfo.season} Season of the year associated with a photo,
e.g. 'Summer'; (Photos 5+ only, applied e.g. 'Summer'; (Photos 5+ only, applied
automatically by Photos' image categorization automatically by Photos' image categorization
algorithms). algorithms).
{exif.camera_make} Camera make from original photo's EXIF {exif.camera_make} Camera make from original photo's EXIF
information as imported by Photos, e.g. information as imported by Photos, e.g.
'Apple' 'Apple'
{exif.camera_model} Camera model from original photo's EXIF {exif.camera_model} Camera model from original photo's EXIF
information as imported by Photos, e.g. information as imported by Photos, e.g.
'iPhone 6s' 'iPhone 6s'
{exif.lens_model} Lens model from original photo's EXIF {exif.lens_model} Lens model from original photo's EXIF
information as imported by Photos, e.g. information as imported by Photos, e.g.
'iPhone 6s back camera 4.15mm f/2.2' 'iPhone 6s back camera 4.15mm f/2.2'
{uuid} Photo's internal universally unique identifier {uuid} Photo's internal universally unique identifier
(UUID) for the photo, a 36-character string (UUID) for the photo, a 36-character string
unique to the photo, e.g. unique to the photo, e.g.
'128FB4C6-0B16-4E7D-9108-FB2E90DA1546' '128FB4C6-0B16-4E7D-9108-FB2E90DA1546'
{comma} A comma: ',' {comma} A comma: ','
{semicolon} A semicolon: ';' {semicolon} A semicolon: ';'
{questionmark} A question mark: '?'
{pipe} A vertical pipe: '|' {pipe} A vertical pipe: '|'
{openbrace} An open brace: '{' {openbrace} An open brace: '{'
{closebrace} A close brace: '}' {closebrace} A close brace: '}'
@@ -1001,13 +1232,16 @@ Substitution Description
{folder_album} Folder path + album photo is contained in. e.g. {folder_album} Folder path + album photo is contained in. e.g.
'Folder/Subfolder/Album' or just 'Album' if no 'Folder/Subfolder/Album' or just 'Album' if no
enclosing folder enclosing folder
{keyword} Keyword(s) assigned to photo {keyword} Keyword(s) assigned to photo
{person} Person(s) / face(s) in a photo {person} Person(s) / face(s) in a photo
{label} Image categorization label associated with a photo {label} Image categorization label associated with a photo
(Photos 5+ only) (Photos 5+ only)
{label_normalized} All lower case version of 'label' (Photos 5+ only) {label_normalized} All lower case version of 'label' (Photos 5+ only)
{comment} Comment(s) on shared Photos; format is 'Person name: {comment} Comment(s) on shared Photos; format is 'Person name:
comment text' (Photos 5+ only) comment text' (Photos 5+ only)
{exiftool} Format: '{exiftool:GROUP:TAGNAME}'; use exiftool {exiftool} Format: '{exiftool:GROUP:TAGNAME}'; use exiftool
(https://exiftool.org) to extract metadata, in form (https://exiftool.org) to extract metadata, in form
GROUP:TAGNAME, from image. E.g. GROUP:TAGNAME, from image. E.g.
@@ -1017,20 +1251,47 @@ Substitution Description
names. You must specify group (e.g. EXIF, IPTC, etc) names. You must specify group (e.g. EXIF, IPTC, etc)
as used in `exiftool -G`. exiftool must be installed as used in `exiftool -G`. exiftool must be installed
in the path to use this template. in the path to use this template.
{searchinfo.holiday} Holiday names associated with a photo, e.g. {searchinfo.holiday} Holiday names associated with a photo, e.g.
'Christmas Day'; (Photos 5+ only, applied 'Christmas Day'; (Photos 5+ only, applied
automatically by Photos' image categorization automatically by Photos' image categorization
algorithms). algorithms).
{searchinfo.activity} Activities associated with a photo, e.g. 'Sporting {searchinfo.activity} Activities associated with a photo, e.g. 'Sporting
Event'; (Photos 5+ only, applied automatically by Event'; (Photos 5+ only, applied automatically by
Photos' image categorization algorithms). Photos' image categorization algorithms).
{searchinfo.venue} Venues associated with a photo, e.g. name of {searchinfo.venue} Venues associated with a photo, e.g. name of
restaurant; (Photos 5+ only, applied automatically by restaurant; (Photos 5+ only, applied automatically by
Photos' image categorization algorithms). Photos' image categorization algorithms).
{searchinfo.venue_type} Venue types associated with a photo, e.g. {searchinfo.venue_type} Venue types associated with a photo, e.g.
'Restaurant'; (Photos 5+ only, applied automatically 'Restaurant'; (Photos 5+ only, applied automatically
by Photos' image categorization algorithms). 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-EXPORT-USAGE:END --> <!-- OSXPHOTOS-EXPORT-USAGE:END -->
@@ -1240,7 +1501,7 @@ Returns a list of the keywords found in the Photos library
albums = photosdb.album_info albums = photosdb.album_info
``` ```
Returns a list of [AlbumInfo](#AlbumInfo) objects representing albums in the database or empty list if there are no albums. See also [albums](#albums). Returns a list of [AlbumInfo](#AlbumInfo) objects representing albums in the database or empty list if there are no albums. See also [albums](#albums) and [burst_album_info](#burst_album_info).
#### `albums` #### `albums`
```python ```python
@@ -1248,7 +1509,7 @@ Returns a list of [AlbumInfo](#AlbumInfo) objects representing albums in the dat
album_names = photosdb.albums album_names = photosdb.albums
``` ```
Returns a list of the album names found in the Photos library. Returns a list of the album names found in the Photos library. See also [burst_albums](#burst_albums).
**Note**: In Photos 5.0 (MacOS 10.15/Catalina), It is possible to have more than one album with the same name in Photos. Albums with duplicate names are treated as a single album and the photos in each are combined. For example, if you have two albums named "Wedding" and each has 2 photos, osxphotos will treat this as a single album named "Wedding" with 4 photos in it. **Note**: In Photos 5.0 (MacOS 10.15/Catalina), It is possible to have more than one album with the same name in Photos. Albums with duplicate names are treated as a single album and the photos in each are combined. For example, if you have two albums named "Wedding" and each has 2 photos, osxphotos will treat this as a single album named "Wedding" with 4 photos in it.
@@ -1684,6 +1945,9 @@ Returns Uniform Type Identifier (UTI) for the associated raw image, if there is
Returns True if photos is a burst image (e.g. part of a set of burst images), otherwise False. Returns True if photos is a burst image (e.g. part of a set of burst images), otherwise False.
See [burst_photos](#burst_photos) See [burst_photos](#burst_photos)
#### `burst_selected`
Returns True if photo is a burst photo and has been selected from the burst set by the user, otherwise False.
#### `burst_photos` #### `burst_photos`
If photo is a burst image (see [burst](#burst)), returns a list of PhotoInfo objects for all other photos in the same burst set. If not a burst image, returns empty list. If photo is a burst image (see [burst](#burst)), returns a list of PhotoInfo objects for all other photos in the same burst set. If not a burst image, returns empty list.
@@ -1707,6 +1971,12 @@ IMG_9854.JPG
IMG_9855.JPG IMG_9855.JPG
``` ```
#### `burst_albums`
If photo is a non-selected burst photo, returns a list of albums any other photos in the same burst set, are contained in. Otherwise, returns `PhotoInfo.albums`. If a burst photo which has unselected burst images (e.g. the burst images are in the library but haven't been selected by the user using the "Make a selection" feature) is placed in a an album, Photos treats only the selected "key" photo as in the album. The unselected burst images, while associated with the photo in the album, are not technically in the album. If you are handling one of these unselected burst photos and want to know which album it would be in based on which albums it's selected key images are in, use `burst_albums`. See also [burst_album_info](#burst_album_info) and [albums](#albums).
#### `burst_album_info`
If photo is non-selected burst photo, teturns a list of [AlbumInfo](#AlbumInfo) objects representing the albums any other photos in the same burst set are contained in. Otherwise, returns `PhotoInfo.album_info`. See also [burst_albums](#burst_albums) and [album_info](#album_info).
#### `live_photo` #### `live_photo`
Returns True if photo is an Apple live photo (ie. it has an associated "live" video component), otherwise returns False. See [path_live_photo](#path_live_photo). Returns True if photo is an Apple live photo (ie. it has an associated "live" video component), otherwise returns False. See [path_live_photo](#path_live_photo).
@@ -1897,7 +2167,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: 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. Template statements are white-space sensitive meaning that white space (spaces, tabs) changes the meaning of the template statement.
@@ -1953,9 +2223,43 @@ e.g. If Photo is in `Album1` in `Folder1`:
- `"{folder_album(>)}"` renders to `["Folder1>Album1"]` - `"{folder_album(>)}"` renders to `["Folder1>Album1"]`
- `"{folder_album()}"` renders to `["Folder1Album1"]` - `"{folder_album()}"` renders to `["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, e.g. if photo is an HDR image,
@@ -2517,6 +2821,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}'| |{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}'| |{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}'| |{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.date}|Photo's creation date in ISO format, e.g. '2020-03-22'|
|{created.year}|4-digit year of photo creation time| |{created.year}|4-digit year of photo creation time|
|{created.yy}|2-digit year of photo creation time| |{created.yy}|2-digit year of photo creation time|
@@ -2576,6 +2881,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'| |{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: ','| |{comma}|A comma: ','|
|{semicolon}|A semicolon: ';'| |{semicolon}|A semicolon: ';'|
|{questionmark}|A question mark: '?'|
|{pipe}|A vertical pipe: '|'| |{pipe}|A vertical pipe: '|'|
|{openbrace}|An open brace: '{'| |{openbrace}|An open brace: '{'|
|{closebrace}|A close brace: '}'| |{closebrace}|A close brace: '}'|
@@ -2595,6 +2901,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.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}|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).| |{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 --> <!-- OSXPHOTOS-TEMPLATE-TABLE:END -->
### Utility Functions ### Utility Functions
@@ -2728,6 +3036,8 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center"><a href="https://github.com/martinhrpi"><img src="https://avatars2.githubusercontent.com/u/19407684?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Martin</b></sub></a><br /><a href="#research-martinhrpi" title="Research">🔬</a> <a href="#userTesting-martinhrpi" title="User Testing">📓</a></td> <td align="center"><a href="https://github.com/martinhrpi"><img src="https://avatars2.githubusercontent.com/u/19407684?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Martin</b></sub></a><br /><a href="#research-martinhrpi" title="Research">🔬</a> <a href="#userTesting-martinhrpi" title="User Testing">📓</a></td>
<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://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://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> </tr>
</table> </table>

View File

@@ -1,4 +1,4 @@
# Sphinx build info version 1 # 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. # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
config: d0470550c1fa9feae481cebbbbc126af config: 456ab9bf5a36a59a5d7b68a97d5f6583
tags: 645f666f9bcd5a90fca523b33c5a78b7 tags: 645f666f9bcd5a90fca523b33c5a78b7

View File

@@ -5,7 +5,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Overview: module code &#8212; osxphotos 0.41.0 documentation</title> <title>Overview: module code &#8212; osxphotos 0.42.1 documentation</title>
<link rel="stylesheet" href="../_static/pygments.css" type="text/css" /> <link rel="stylesheet" href="../_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="../_static/alabaster.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> <script id="documentation_options" data-url_root="../" src="../_static/documentation_options.js"></script>
@@ -93,7 +93,7 @@
&copy;2021, Rhet Turnbull. &copy;2021, Rhet Turnbull.
| |
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.4.3</a> Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a> &amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div> </div>

View File

@@ -5,7 +5,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.photoinfo._photoinfo_exifinfo &#8212; osxphotos 0.41.0 documentation</title> <title>osxphotos.photoinfo._photoinfo_exifinfo &#8212; osxphotos 0.41.4 documentation</title>
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" /> <link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="../../../_static/alabaster.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> <script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
@@ -183,7 +183,7 @@
&copy;2021, Rhet Turnbull. &copy;2021, Rhet Turnbull.
| |
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.4.3</a> Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a> &amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div> </div>

View File

@@ -5,7 +5,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.photoinfo._photoinfo_export &#8212; osxphotos 0.41.0 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/pygments.css" type="text/css" />
<link rel="stylesheet" href="../../../_static/alabaster.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> <script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
@@ -120,6 +120,8 @@
<span class="n">exiftool_error</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">exiftool_error</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">xattr_written</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">xattr_written</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">xattr_skipped</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">xattr_skipped</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">deleted_files</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">deleted_directories</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="p">):</span> <span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">exported</span> <span class="o">=</span> <span class="n">exported</span> <span class="ow">or</span> <span class="p">[]</span> <span class="bp">self</span><span class="o">.</span><span class="n">exported</span> <span class="o">=</span> <span class="n">exported</span> <span class="ow">or</span> <span class="p">[]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">new</span> <span class="o">=</span> <span class="n">new</span> <span class="ow">or</span> <span class="p">[]</span> <span class="bp">self</span><span class="o">.</span><span class="n">new</span> <span class="o">=</span> <span class="n">new</span> <span class="ow">or</span> <span class="p">[]</span>
@@ -140,6 +142,8 @@
<span class="bp">self</span><span class="o">.</span><span class="n">exiftool_error</span> <span class="o">=</span> <span class="n">exiftool_error</span> <span class="ow">or</span> <span class="p">[]</span> <span class="bp">self</span><span class="o">.</span><span class="n">exiftool_error</span> <span class="o">=</span> <span class="n">exiftool_error</span> <span class="ow">or</span> <span class="p">[]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">xattr_written</span> <span class="o">=</span> <span class="n">xattr_written</span> <span class="ow">or</span> <span class="p">[]</span> <span class="bp">self</span><span class="o">.</span><span class="n">xattr_written</span> <span class="o">=</span> <span class="n">xattr_written</span> <span class="ow">or</span> <span class="p">[]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">xattr_skipped</span> <span class="o">=</span> <span class="n">xattr_skipped</span> <span class="ow">or</span> <span class="p">[]</span> <span class="bp">self</span><span class="o">.</span><span class="n">xattr_skipped</span> <span class="o">=</span> <span class="n">xattr_skipped</span> <span class="ow">or</span> <span class="p">[]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">deleted_files</span> <span class="o">=</span> <span class="n">deleted_files</span> <span class="ow">or</span> <span class="p">[]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">deleted_directories</span> <span class="o">=</span> <span class="n">deleted_directories</span> <span class="ow">or</span> <span class="p">[]</span>
<span class="k">def</span> <span class="nf">all_files</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">def</span> <span class="nf">all_files</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; return all filenames contained in results &quot;&quot;&quot;</span> <span class="sd">&quot;&quot;&quot; return all filenames contained in results &quot;&quot;&quot;</span>
@@ -184,6 +188,8 @@
<span class="bp">self</span><span class="o">.</span><span class="n">error</span> <span class="o">+=</span> <span class="n">other</span><span class="o">.</span><span class="n">error</span> <span class="bp">self</span><span class="o">.</span><span class="n">error</span> <span class="o">+=</span> <span class="n">other</span><span class="o">.</span><span class="n">error</span>
<span class="bp">self</span><span class="o">.</span><span class="n">exiftool_warning</span> <span class="o">+=</span> <span class="n">other</span><span class="o">.</span><span class="n">exiftool_warning</span> <span class="bp">self</span><span class="o">.</span><span class="n">exiftool_warning</span> <span class="o">+=</span> <span class="n">other</span><span class="o">.</span><span class="n">exiftool_warning</span>
<span class="bp">self</span><span class="o">.</span><span class="n">exiftool_error</span> <span class="o">+=</span> <span class="n">other</span><span class="o">.</span><span class="n">exiftool_error</span> <span class="bp">self</span><span class="o">.</span><span class="n">exiftool_error</span> <span class="o">+=</span> <span class="n">other</span><span class="o">.</span><span class="n">exiftool_error</span>
<span class="bp">self</span><span class="o">.</span><span class="n">deleted_files</span> <span class="o">+=</span> <span class="n">other</span><span class="o">.</span><span class="n">deleted_files</span>
<span class="bp">self</span><span class="o">.</span><span class="n">deleted_directories</span> <span class="o">+=</span> <span class="n">other</span><span class="o">.</span><span class="n">deleted_directories</span>
<span class="k">return</span> <span class="bp">self</span> <span class="k">return</span> <span class="bp">self</span>
<span class="k">def</span> <span class="fm">__str__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">def</span> <span class="fm">__str__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
@@ -206,6 +212,8 @@
<span class="o">+</span> <span class="sa">f</span><span class="s2">&quot;,error=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">error</span><span class="si">}</span><span class="s2">&quot;</span> <span class="o">+</span> <span class="sa">f</span><span class="s2">&quot;,error=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">error</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="o">+</span> <span class="sa">f</span><span class="s2">&quot;,exiftool_warning=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">exiftool_warning</span><span class="si">}</span><span class="s2">&quot;</span> <span class="o">+</span> <span class="sa">f</span><span class="s2">&quot;,exiftool_warning=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">exiftool_warning</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="o">+</span> <span class="sa">f</span><span class="s2">&quot;,exiftool_error=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">exiftool_error</span><span class="si">}</span><span class="s2">&quot;</span> <span class="o">+</span> <span class="sa">f</span><span class="s2">&quot;,exiftool_error=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">exiftool_error</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="o">+</span> <span class="sa">f</span><span class="s2">&quot;,deleted_files=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">deleted_files</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="o">+</span> <span class="sa">f</span><span class="s2">&quot;,deleted_directories=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">deleted_directories</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="o">+</span> <span class="s2">&quot;)&quot;</span> <span class="o">+</span> <span class="s2">&quot;)&quot;</span>
<span class="p">)</span> <span class="p">)</span>
@@ -508,6 +516,9 @@
<span class="n">merge_exif_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">merge_exif_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">merge_exif_persons</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">merge_exif_persons</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">jpeg_ext</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">jpeg_ext</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">persons</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="p">):</span> <span class="p">):</span>
<span class="sd">&quot;&quot;&quot;export photo, like export but with update and dry_run options</span> <span class="sd">&quot;&quot;&quot;export photo, like export but with update and dry_run options</span>
<span class="sd"> dest: must be valid destination path or exception raised</span> <span class="sd"> dest: must be valid destination path or exception raised</span>
@@ -560,6 +571,9 @@
<span class="sd"> merge_exif_keywords: boolean; if True, merged keywords found in file&#39;s exif data (requires exiftool)</span> <span class="sd"> merge_exif_keywords: boolean; if True, merged keywords found in file&#39;s exif data (requires exiftool)</span>
<span class="sd"> merge_exif_persons: boolean; if True, merged persons found in file&#39;s exif data (requires exiftool)</span> <span class="sd"> merge_exif_persons: boolean; if True, merged persons found in file&#39;s exif data (requires exiftool)</span>
<span class="sd"> jpeg_ext: if set, will use this value for extension on jpegs converted to jpeg with convert_to_jpeg; if not set, uses jpeg; do not include the leading &quot;.&quot;</span> <span class="sd"> jpeg_ext: if set, will use this value for extension on jpegs converted to jpeg with convert_to_jpeg; if not set, uses jpeg; do not include the leading &quot;.&quot;</span>
<span class="sd"> persons: if True, include persons in exported metadata</span>
<span class="sd"> location: if True, include location in exported metadata</span>
<span class="sd"> replace_keywords: if True, keyword_template replaces any keywords, otherwise it&#39;s additive</span>
<span class="sd"> Returns: ExportResults class </span> <span class="sd"> Returns: ExportResults class </span>
<span class="sd"> ExportResults has attributes: </span> <span class="sd"> ExportResults has attributes: </span>
@@ -640,9 +654,9 @@
<span class="p">)</span> <span class="p">)</span>
<span class="n">edited_name</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">path_edited</span><span class="p">)</span><span class="o">.</span><span class="n">name</span> <span class="n">edited_name</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">path_edited</span><span class="p">)</span><span class="o">.</span><span class="n">name</span>
<span class="n">edited_suffix</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">edited_name</span><span class="p">)</span><span class="o">.</span><span class="n">suffix</span> <span class="n">edited_suffix</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">edited_name</span><span class="p">)</span><span class="o">.</span><span class="n">suffix</span>
<span class="n">fname</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">filename</span><span class="p">)</span><span class="o">.</span><span class="n">stem</span> <span class="o">+</span> <span class="n">edited_identifier</span> <span class="o">+</span> <span class="n">edited_suffix</span> <span class="n">fname</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">original_filename</span><span class="p">)</span><span class="o">.</span><span class="n">stem</span> <span class="o">+</span> <span class="n">edited_identifier</span> <span class="o">+</span> <span class="n">edited_suffix</span>
<span class="k">else</span><span class="p">:</span> <span class="k">else</span><span class="p">:</span>
<span class="n">fname</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">filename</span> <span class="n">fname</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">original_filename</span>
<span class="n">uti</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">uti</span> <span class="k">if</span> <span class="n">edited</span> <span class="k">else</span> <span class="bp">self</span><span class="o">.</span><span class="n">uti_original</span> <span class="n">uti</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">uti</span> <span class="k">if</span> <span class="n">edited</span> <span class="k">else</span> <span class="bp">self</span><span class="o">.</span><span class="n">uti_original</span>
<span class="k">if</span> <span class="n">convert_to_jpeg</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">isphoto</span> <span class="ow">and</span> <span class="n">uti</span> <span class="o">!=</span> <span class="s2">&quot;public.jpeg&quot;</span><span class="p">:</span> <span class="k">if</span> <span class="n">convert_to_jpeg</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">isphoto</span> <span class="ow">and</span> <span class="n">uti</span> <span class="o">!=</span> <span class="s2">&quot;public.jpeg&quot;</span><span class="p">:</span>
@@ -974,6 +988,9 @@
<span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span> <span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span>
<span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span> <span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span>
<span class="n">filename</span><span class="o">=</span><span class="n">dest</span><span class="o">.</span><span class="n">name</span><span class="p">,</span> <span class="n">filename</span><span class="o">=</span><span class="n">dest</span><span class="o">.</span><span class="n">name</span><span class="p">,</span>
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="p">)</span> <span class="p">)</span>
<span class="n">sidecars</span><span class="o">.</span><span class="n">append</span><span class="p">(</span> <span class="n">sidecars</span><span class="o">.</span><span class="n">append</span><span class="p">(</span>
<span class="p">(</span> <span class="p">(</span>
@@ -997,6 +1014,9 @@
<span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span> <span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span>
<span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span> <span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span>
<span class="n">filename</span><span class="o">=</span><span class="n">dest</span><span class="o">.</span><span class="n">name</span><span class="p">,</span> <span class="n">filename</span><span class="o">=</span><span class="n">dest</span><span class="o">.</span><span class="n">name</span><span class="p">,</span>
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="p">)</span> <span class="p">)</span>
<span class="n">sidecars</span><span class="o">.</span><span class="n">append</span><span class="p">(</span> <span class="n">sidecars</span><span class="o">.</span><span class="n">append</span><span class="p">(</span>
<span class="p">(</span> <span class="p">(</span>
@@ -1016,6 +1036,9 @@
<span class="n">keyword_template</span><span class="o">=</span><span class="n">keyword_template</span><span class="p">,</span> <span class="n">keyword_template</span><span class="o">=</span><span class="n">keyword_template</span><span class="p">,</span>
<span class="n">description_template</span><span class="o">=</span><span class="n">description_template</span><span class="p">,</span> <span class="n">description_template</span><span class="o">=</span><span class="n">description_template</span><span class="p">,</span>
<span class="n">extension</span><span class="o">=</span><span class="n">dest</span><span class="o">.</span><span class="n">suffix</span><span class="p">[</span><span class="mi">1</span><span class="p">:]</span> <span class="k">if</span> <span class="n">dest</span><span class="o">.</span><span class="n">suffix</span> <span class="k">else</span> <span class="kc">None</span><span class="p">,</span> <span class="n">extension</span><span class="o">=</span><span class="n">dest</span><span class="o">.</span><span class="n">suffix</span><span class="p">[</span><span class="mi">1</span><span class="p">:]</span> <span class="k">if</span> <span class="n">dest</span><span class="o">.</span><span class="n">suffix</span> <span class="k">else</span> <span class="kc">None</span><span class="p">,</span>
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="p">)</span> <span class="p">)</span>
<span class="n">sidecars</span><span class="o">.</span><span class="n">append</span><span class="p">(</span> <span class="n">sidecars</span><span class="o">.</span><span class="n">append</span><span class="p">(</span>
<span class="p">(</span> <span class="p">(</span>
@@ -1083,6 +1106,9 @@
<span class="n">ignore_date_modified</span><span class="o">=</span><span class="n">ignore_date_modified</span><span class="p">,</span> <span class="n">ignore_date_modified</span><span class="o">=</span><span class="n">ignore_date_modified</span><span class="p">,</span>
<span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span> <span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span>
<span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span> <span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span>
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="p">)</span> <span class="p">)</span>
<span class="p">)[</span><span class="mi">0</span><span class="p">]</span> <span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
<span class="k">if</span> <span class="n">old_data</span> <span class="o">!=</span> <span class="n">current_data</span><span class="p">:</span> <span class="k">if</span> <span class="n">old_data</span> <span class="o">!=</span> <span class="n">current_data</span><span class="p">:</span>
@@ -1103,6 +1129,9 @@
<span class="n">flags</span><span class="o">=</span><span class="n">exiftool_flags</span><span class="p">,</span> <span class="n">flags</span><span class="o">=</span><span class="n">exiftool_flags</span><span class="p">,</span>
<span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span> <span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span>
<span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span> <span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span>
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="p">)</span> <span class="p">)</span>
<span class="k">if</span> <span class="n">warning_</span><span class="p">:</span> <span class="k">if</span> <span class="n">warning_</span><span class="p">:</span>
<span class="n">all_results</span><span class="o">.</span><span class="n">exiftool_warning</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="n">exported_file</span><span class="p">,</span> <span class="n">warning_</span><span class="p">))</span> <span class="n">all_results</span><span class="o">.</span><span class="n">exiftool_warning</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="n">exported_file</span><span class="p">,</span> <span class="n">warning_</span><span class="p">))</span>
@@ -1120,6 +1149,9 @@
<span class="n">ignore_date_modified</span><span class="o">=</span><span class="n">ignore_date_modified</span><span class="p">,</span> <span class="n">ignore_date_modified</span><span class="o">=</span><span class="n">ignore_date_modified</span><span class="p">,</span>
<span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span> <span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span>
<span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span> <span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span>
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="p">),</span> <span class="p">),</span>
<span class="p">)</span> <span class="p">)</span>
<span class="n">export_db</span><span class="o">.</span><span class="n">set_stat_exif_for_file</span><span class="p">(</span> <span class="n">export_db</span><span class="o">.</span><span class="n">set_stat_exif_for_file</span><span class="p">(</span>
@@ -1142,6 +1174,9 @@
<span class="n">flags</span><span class="o">=</span><span class="n">exiftool_flags</span><span class="p">,</span> <span class="n">flags</span><span class="o">=</span><span class="n">exiftool_flags</span><span class="p">,</span>
<span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span> <span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span>
<span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span> <span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span>
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="p">)</span> <span class="p">)</span>
<span class="k">if</span> <span class="n">warning_</span><span class="p">:</span> <span class="k">if</span> <span class="n">warning_</span><span class="p">:</span>
<span class="n">all_results</span><span class="o">.</span><span class="n">exiftool_warning</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="n">exported_file</span><span class="p">,</span> <span class="n">warning_</span><span class="p">))</span> <span class="n">all_results</span><span class="o">.</span><span class="n">exiftool_warning</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="n">exported_file</span><span class="p">,</span> <span class="n">warning_</span><span class="p">))</span>
@@ -1159,6 +1194,9 @@
<span class="n">ignore_date_modified</span><span class="o">=</span><span class="n">ignore_date_modified</span><span class="p">,</span> <span class="n">ignore_date_modified</span><span class="o">=</span><span class="n">ignore_date_modified</span><span class="p">,</span>
<span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span> <span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span>
<span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span> <span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span>
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="p">),</span> <span class="p">),</span>
<span class="p">)</span> <span class="p">)</span>
<span class="n">export_db</span><span class="o">.</span><span class="n">set_stat_exif_for_file</span><span class="p">(</span> <span class="n">export_db</span><span class="o">.</span><span class="n">set_stat_exif_for_file</span><span class="p">(</span>
@@ -1378,6 +1416,9 @@
<span class="n">flags</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">flags</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">merge_exif_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">merge_exif_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">merge_exif_persons</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">merge_exif_persons</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">persons</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="p">):</span> <span class="p">):</span>
<span class="sd">&quot;&quot;&quot;write exif data to image file at filepath</span> <span class="sd">&quot;&quot;&quot;write exif data to image file at filepath</span>
@@ -1388,6 +1429,9 @@
<span class="sd"> keyword_template: (list of strings); list of template strings to render as keywords</span> <span class="sd"> keyword_template: (list of strings); list of template strings to render as keywords</span>
<span class="sd"> ignore_date_modified: if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set</span> <span class="sd"> ignore_date_modified: if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set</span>
<span class="sd"> flags: optional list of exiftool flags to prepend to exiftool command when writing metadata (e.g. -m or -F)</span> <span class="sd"> flags: optional list of exiftool flags to prepend to exiftool command when writing metadata (e.g. -m or -F)</span>
<span class="sd"> persons: if True, write person data to metadata</span>
<span class="sd"> location: if True, write location data to metadata</span>
<span class="sd"> replace_keywords: if True, keyword_template replaces any keywords, otherwise it&#39;s additive</span>
<span class="sd"> Returns:</span> <span class="sd"> Returns:</span>
<span class="sd"> (warning, error) of warning and error strings if exiftool produces warnings or errors</span> <span class="sd"> (warning, error) of warning and error strings if exiftool produces warnings or errors</span>
@@ -1402,6 +1446,9 @@
<span class="n">ignore_date_modified</span><span class="o">=</span><span class="n">ignore_date_modified</span><span class="p">,</span> <span class="n">ignore_date_modified</span><span class="o">=</span><span class="n">ignore_date_modified</span><span class="p">,</span>
<span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span> <span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span>
<span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span> <span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span>
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="p">)</span> <span class="p">)</span>
<span class="k">with</span> <span class="n">ExifTool</span><span class="p">(</span><span class="n">filepath</span><span class="p">,</span> <span class="n">flags</span><span class="o">=</span><span class="n">flags</span><span class="p">,</span> <span class="n">exiftool</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_exiftool_path</span><span class="p">)</span> <span class="k">as</span> <span class="n">exiftool</span><span class="p">:</span> <span class="k">with</span> <span class="n">ExifTool</span><span class="p">(</span><span class="n">filepath</span><span class="p">,</span> <span class="n">flags</span><span class="o">=</span><span class="n">flags</span><span class="p">,</span> <span class="n">exiftool</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_exiftool_path</span><span class="p">)</span> <span class="k">as</span> <span class="n">exiftool</span><span class="p">:</span>
@@ -1424,6 +1471,9 @@
<span class="n">merge_exif_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">merge_exif_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">merge_exif_persons</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">merge_exif_persons</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">filename</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">filename</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">persons</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="p">):</span> <span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool.</span> <span class="sd">&quot;&quot;&quot;Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool.</span>
<span class="sd"> Does not include all the EXIF fields as those are likely already in the image.</span> <span class="sd"> Does not include all the EXIF fields as those are likely already in the image.</span>
@@ -1437,6 +1487,9 @@
<span class="sd"> ignore_date_modified: if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set</span> <span class="sd"> ignore_date_modified: if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set</span>
<span class="sd"> merge_exif_keywords: merge keywords in the file&#39;s exif metadata (requires exiftool)</span> <span class="sd"> merge_exif_keywords: merge keywords in the file&#39;s exif metadata (requires exiftool)</span>
<span class="sd"> merge_exif_persons: merge persons in the file&#39;s exif metadata (requires exiftool)</span> <span class="sd"> merge_exif_persons: merge persons in the file&#39;s exif metadata (requires exiftool)</span>
<span class="sd"> persons: if True, include person data</span>
<span class="sd"> location: if True, include location data</span>
<span class="sd"> replace_keywords: if True, keyword_template replaces any keywords, otherwise it&#39;s additive</span>
<span class="sd"> Returns: dict with exiftool tags / values</span> <span class="sd"> Returns: dict with exiftool tags / values</span>
@@ -1444,8 +1497,10 @@
<span class="sd"> EXIF:ImageDescription (may include template)</span> <span class="sd"> EXIF:ImageDescription (may include template)</span>
<span class="sd"> XMP:Description (may include template)</span> <span class="sd"> XMP:Description (may include template)</span>
<span class="sd"> XMP:Title</span> <span class="sd"> XMP:Title</span>
<span class="sd"> IPTC:ObjectName</span>
<span class="sd"> XMP:TagsList (may include album name, person name, or template)</span> <span class="sd"> XMP:TagsList (may include album name, person name, or template)</span>
<span class="sd"> IPTC:Keywords (may include album name, person name, or template)</span> <span class="sd"> IPTC:Keywords (may include album name, person name, or template)</span>
<span class="sd"> IPTC:Caption-Abstract</span>
<span class="sd"> XMP:Subject (set to keywords + persons)</span> <span class="sd"> XMP:Subject (set to keywords + persons)</span>
<span class="sd"> XMP:PersonInImage</span> <span class="sd"> XMP:PersonInImage</span>
<span class="sd"> EXIF:GPSLatitudeRef, EXIF:GPSLongitudeRef</span> <span class="sd"> EXIF:GPSLatitudeRef, EXIF:GPSLongitudeRef</span>
@@ -1461,6 +1516,9 @@
<span class="sd"> QuickTime:ModifyDate (UTC)</span> <span class="sd"> QuickTime:ModifyDate (UTC)</span>
<span class="sd"> QuickTime:GPSCoordinates</span> <span class="sd"> QuickTime:GPSCoordinates</span>
<span class="sd"> UserData:GPSCoordinates</span> <span class="sd"> UserData:GPSCoordinates</span>
<span class="sd"> Reference: </span>
<span class="sd"> https://iptc.org/std/photometadata/specification/IPTC-PhotoMetadata-201610_1.pdf</span>
<span class="sd"> &quot;&quot;&quot;</span> <span class="sd"> &quot;&quot;&quot;</span>
<span class="n">exif</span> <span class="o">=</span> <span class="p">(</span> <span class="n">exif</span> <span class="o">=</span> <span class="p">(</span>
@@ -1480,30 +1538,34 @@
<span class="n">description</span> <span class="o">=</span> <span class="s2">&quot; &quot;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">rendered</span><span class="p">)</span> <span class="k">if</span> <span class="n">rendered</span> <span class="k">else</span> <span class="s2">&quot;&quot;</span> <span class="n">description</span> <span class="o">=</span> <span class="s2">&quot; &quot;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">rendered</span><span class="p">)</span> <span class="k">if</span> <span class="n">rendered</span> <span class="k">else</span> <span class="s2">&quot;&quot;</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;EXIF:ImageDescription&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">description</span> <span class="n">exif</span><span class="p">[</span><span class="s2">&quot;EXIF:ImageDescription&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">description</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;XMP:Description&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">description</span> <span class="n">exif</span><span class="p">[</span><span class="s2">&quot;XMP:Description&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">description</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;IPTC:Caption-Abstract&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">description</span>
<span class="k">elif</span> <span class="bp">self</span><span class="o">.</span><span class="n">description</span><span class="p">:</span> <span class="k">elif</span> <span class="bp">self</span><span class="o">.</span><span class="n">description</span><span class="p">:</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;EXIF:ImageDescription&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">description</span> <span class="n">exif</span><span class="p">[</span><span class="s2">&quot;EXIF:ImageDescription&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">description</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;XMP:Description&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">description</span> <span class="n">exif</span><span class="p">[</span><span class="s2">&quot;XMP:Description&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">description</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;IPTC:Caption-Abstract&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">description</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">title</span><span class="p">:</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">title</span><span class="p">:</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;XMP:Title&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">title</span> <span class="n">exif</span><span class="p">[</span><span class="s2">&quot;XMP:Title&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">title</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;IPTC:ObjectName&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">title</span>
<span class="n">keyword_list</span> <span class="o">=</span> <span class="p">[]</span> <span class="n">keyword_list</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">if</span> <span class="n">merge_exif_keywords</span><span class="p">:</span> <span class="k">if</span> <span class="n">merge_exif_keywords</span><span class="p">:</span>
<span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_get_exif_keywords</span><span class="p">())</span> <span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_get_exif_keywords</span><span class="p">())</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">keywords</span><span class="p">:</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">keywords</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">replace_keywords</span><span class="p">:</span>
<span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">keywords</span><span class="p">)</span> <span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">keywords</span><span class="p">)</span>
<span class="n">person_list</span> <span class="o">=</span> <span class="p">[]</span> <span class="n">person_list</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">if</span> <span class="n">merge_exif_persons</span><span class="p">:</span> <span class="k">if</span> <span class="n">persons</span><span class="p">:</span>
<span class="n">person_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_get_exif_persons</span><span class="p">())</span> <span class="k">if</span> <span class="n">merge_exif_persons</span><span class="p">:</span>
<span class="n">person_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_get_exif_persons</span><span class="p">())</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">persons</span><span class="p">:</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">persons</span><span class="p">:</span>
<span class="c1"># filter out _UNKNOWN_PERSON</span> <span class="c1"># filter out _UNKNOWN_PERSON</span>
<span class="n">person_list</span><span class="o">.</span><span class="n">extend</span><span class="p">([</span><span class="n">p</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">persons</span> <span class="k">if</span> <span class="n">p</span> <span class="o">!=</span> <span class="n">_UNKNOWN_PERSON</span><span class="p">])</span> <span class="n">person_list</span><span class="o">.</span><span class="n">extend</span><span class="p">([</span><span class="n">p</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">persons</span> <span class="k">if</span> <span class="n">p</span> <span class="o">!=</span> <span class="n">_UNKNOWN_PERSON</span><span class="p">])</span>
<span class="k">if</span> <span class="n">use_persons_as_keywords</span> <span class="ow">and</span> <span class="n">person_list</span><span class="p">:</span> <span class="k">if</span> <span class="n">use_persons_as_keywords</span> <span class="ow">and</span> <span class="n">person_list</span><span class="p">:</span>
<span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">person_list</span><span class="p">)</span> <span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">person_list</span><span class="p">)</span>
<span class="k">if</span> <span class="n">use_albums_as_keywords</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">albums</span><span class="p">:</span> <span class="k">if</span> <span class="n">use_albums_as_keywords</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">albums</span><span class="p">:</span>
<span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">albums</span><span class="p">)</span> <span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">albums</span><span class="p">)</span>
@@ -1547,25 +1609,26 @@
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;XMP:Subject&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">keyword_list</span><span class="o">.</span><span class="n">copy</span><span class="p">()</span> <span class="n">exif</span><span class="p">[</span><span class="s2">&quot;XMP:Subject&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">keyword_list</span><span class="o">.</span><span class="n">copy</span><span class="p">()</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;XMP:TagsList&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">keyword_list</span><span class="o">.</span><span class="n">copy</span><span class="p">()</span> <span class="n">exif</span><span class="p">[</span><span class="s2">&quot;XMP:TagsList&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">keyword_list</span><span class="o">.</span><span class="n">copy</span><span class="p">()</span>
<span class="k">if</span> <span class="n">person_list</span><span class="p">:</span> <span class="k">if</span> <span class="n">persons</span> <span class="ow">and</span> <span class="n">person_list</span><span class="p">:</span>
<span class="n">person_list</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="nb">list</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">person_list</span><span class="p">)))</span> <span class="n">person_list</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="nb">list</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">person_list</span><span class="p">)))</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;XMP:PersonInImage&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">person_list</span><span class="o">.</span><span class="n">copy</span><span class="p">()</span> <span class="n">exif</span><span class="p">[</span><span class="s2">&quot;XMP:PersonInImage&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">person_list</span><span class="o">.</span><span class="n">copy</span><span class="p">()</span>
<span class="c1"># if self.favorite():</span> <span class="c1"># if self.favorite():</span>
<span class="c1"># exif[&quot;Rating&quot;] = 5</span> <span class="c1"># exif[&quot;Rating&quot;] = 5</span>
<span class="p">(</span><span class="n">lat</span><span class="p">,</span> <span class="n">lon</span><span class="p">)</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">location</span> <span class="k">if</span> <span class="n">location</span><span class="p">:</span>
<span class="k">if</span> <span class="n">lat</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="ow">and</span> <span class="n">lon</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span> <span class="p">(</span><span class="n">lat</span><span class="p">,</span> <span class="n">lon</span><span class="p">)</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">location</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">isphoto</span><span class="p">:</span> <span class="k">if</span> <span class="n">lat</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="ow">and</span> <span class="n">lon</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;EXIF:GPSLatitude&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">lat</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">isphoto</span><span class="p">:</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;EXIF:GPSLongitude&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">lon</span> <span class="n">exif</span><span class="p">[</span><span class="s2">&quot;EXIF:GPSLatitude&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">lat</span>
<span class="n">lat_ref</span> <span class="o">=</span> <span class="s2">&quot;N&quot;</span> <span class="k">if</span> <span class="n">lat</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="k">else</span> <span class="s2">&quot;S&quot;</span> <span class="n">exif</span><span class="p">[</span><span class="s2">&quot;EXIF:GPSLongitude&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">lon</span>
<span class="n">lon_ref</span> <span class="o">=</span> <span class="s2">&quot;E&quot;</span> <span class="k">if</span> <span class="n">lon</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="k">else</span> <span class="s2">&quot;W&quot;</span> <span class="n">lat_ref</span> <span class="o">=</span> <span class="s2">&quot;N&quot;</span> <span class="k">if</span> <span class="n">lat</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="k">else</span> <span class="s2">&quot;S&quot;</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;EXIF:GPSLatitudeRef&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">lat_ref</span> <span class="n">lon_ref</span> <span class="o">=</span> <span class="s2">&quot;E&quot;</span> <span class="k">if</span> <span class="n">lon</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="k">else</span> <span class="s2">&quot;W&quot;</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;EXIF:GPSLongitudeRef&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">lon_ref</span> <span class="n">exif</span><span class="p">[</span><span class="s2">&quot;EXIF:GPSLatitudeRef&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">lat_ref</span>
<span class="k">elif</span> <span class="bp">self</span><span class="o">.</span><span class="n">ismovie</span><span class="p">:</span> <span class="n">exif</span><span class="p">[</span><span class="s2">&quot;EXIF:GPSLongitudeRef&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">lon_ref</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;Keys:GPSCoordinates&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">lat</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">lon</span><span class="si">}</span><span class="s2">&quot;</span> <span class="k">elif</span> <span class="bp">self</span><span class="o">.</span><span class="n">ismovie</span><span class="p">:</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;UserData:GPSCoordinates&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">lat</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">lon</span><span class="si">}</span><span class="s2">&quot;</span> <span class="n">exif</span><span class="p">[</span><span class="s2">&quot;Keys:GPSCoordinates&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">lat</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">lon</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;UserData:GPSCoordinates&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">lat</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">lon</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="c1"># process date/time and timezone offset</span> <span class="c1"># process date/time and timezone offset</span>
<span class="c1"># Photos exports the following fields and sets modify date to creation date</span> <span class="c1"># Photos exports the following fields and sets modify date to creation date</span>
@@ -1624,6 +1687,13 @@
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;QuickTime:ModifyDate&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">datetime_tz_to_utc</span><span class="p">(</span> <span class="n">exif</span><span class="p">[</span><span class="s2">&quot;QuickTime:ModifyDate&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">datetime_tz_to_utc</span><span class="p">(</span>
<span class="bp">self</span><span class="o">.</span><span class="n">date_modified</span> <span class="bp">self</span><span class="o">.</span><span class="n">date_modified</span>
<span class="p">)</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">&quot;%Y:%m:</span><span class="si">%d</span><span class="s2"> %H:%M:%S&quot;</span><span class="p">)</span> <span class="p">)</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">&quot;%Y:%m:</span><span class="si">%d</span><span class="s2"> %H:%M:%S&quot;</span><span class="p">)</span>
<span class="c1"># remove any new lines in any fields</span>
<span class="k">for</span> <span class="n">field</span><span class="p">,</span> <span class="n">val</span> <span class="ow">in</span> <span class="n">exif</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
<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="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> <span class="k">return</span> <span class="n">exif</span>
@@ -1673,6 +1743,9 @@
<span class="n">merge_exif_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">merge_exif_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">merge_exif_persons</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">merge_exif_persons</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">filename</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">filename</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">persons</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="p">):</span> <span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool.</span> <span class="sd">&quot;&quot;&quot;Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool.</span>
<span class="sd"> Does not include all the EXIF fields as those are likely already in the image.</span> <span class="sd"> Does not include all the EXIF fields as those are likely already in the image.</span>
@@ -1687,13 +1760,18 @@
<span class="sd"> merge_exif_keywords: boolean; if True, merged keywords found in file&#39;s exif data (requires exiftool)</span> <span class="sd"> merge_exif_keywords: boolean; if True, merged keywords found in file&#39;s exif data (requires exiftool)</span>
<span class="sd"> merge_exif_persons: boolean; if True, merged persons found in file&#39;s exif data (requires exiftool)</span> <span class="sd"> merge_exif_persons: boolean; if True, merged persons found in file&#39;s exif data (requires exiftool)</span>
<span class="sd"> filename: filename of the destination image file for including in exiftool signature in JSON sidecar</span> <span class="sd"> filename: filename of the destination image file for including in exiftool signature in JSON sidecar</span>
<span class="sd"> persons: if True, include person data</span>
<span class="sd"> location: if True, include location data</span>
<span class="sd"> replace_keywords: if True, keyword_template replaces any keywords, otherwise it&#39;s additive</span>
<span class="sd"> Returns: dict with exiftool tags / values</span> <span class="sd"> Returns: dict with exiftool tags / values</span>
<span class="sd"> Exports the following:</span> <span class="sd"> Exports the following:</span>
<span class="sd"> EXIF:ImageDescription</span> <span class="sd"> EXIF:ImageDescription</span>
<span class="sd"> XMP:Description (may include template)</span> <span class="sd"> XMP:Description (may include template)</span>
<span class="sd"> IPTC:CaptionAbstract</span>
<span class="sd"> XMP:Title</span> <span class="sd"> XMP:Title</span>
<span class="sd"> IPTC:ObjectName</span>
<span class="sd"> XMP:TagsList</span> <span class="sd"> XMP:TagsList</span>
<span class="sd"> IPTC:Keywords (may include album name, person name, or template)</span> <span class="sd"> IPTC:Keywords (may include album name, person name, or template)</span>
<span class="sd"> XMP:Subject (set to keywords + person)</span> <span class="sd"> XMP:Subject (set to keywords + person)</span>
@@ -1721,6 +1799,9 @@
<span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span> <span class="n">merge_exif_keywords</span><span class="o">=</span><span class="n">merge_exif_keywords</span><span class="p">,</span>
<span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span> <span class="n">merge_exif_persons</span><span class="o">=</span><span class="n">merge_exif_persons</span><span class="p">,</span>
<span class="n">filename</span><span class="o">=</span><span class="n">filename</span><span class="p">,</span> <span class="n">filename</span><span class="o">=</span><span class="n">filename</span><span class="p">,</span>
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="p">)</span> <span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">tag_groups</span><span class="p">:</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">tag_groups</span><span class="p">:</span>
@@ -1743,6 +1824,9 @@
<span class="n">extension</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">extension</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">merge_exif_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">merge_exif_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">merge_exif_persons</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">merge_exif_persons</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">persons</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="p">):</span> <span class="p">):</span>
<span class="sd">&quot;&quot;&quot;returns string for XMP sidecar</span> <span class="sd">&quot;&quot;&quot;returns string for XMP sidecar</span>
<span class="sd"> use_albums_as_keywords: treat album names as keywords</span> <span class="sd"> use_albums_as_keywords: treat album names as keywords</span>
@@ -1752,6 +1836,9 @@
<span class="sd"> extension: which extension to use for SidecarForExtension property</span> <span class="sd"> extension: which extension to use for SidecarForExtension property</span>
<span class="sd"> merge_exif_keywords: boolean; if True, merged keywords found in file&#39;s exif data (requires exiftool)</span> <span class="sd"> merge_exif_keywords: boolean; if True, merged keywords found in file&#39;s exif data (requires exiftool)</span>
<span class="sd"> merge_exif_persons: boolean; if True, merged persons found in file&#39;s exif data (requires exiftool)</span> <span class="sd"> merge_exif_persons: boolean; if True, merged persons found in file&#39;s exif data (requires exiftool)</span>
<span class="sd"> persons: if True, include person data</span>
<span class="sd"> location: if True, include location data</span>
<span class="sd"> replace_keywords: if True, keyword_template replaces any keywords, otherwise it&#39;s additive</span>
<span class="sd"> &quot;&quot;&quot;</span> <span class="sd"> &quot;&quot;&quot;</span>
<span class="n">xmp_template_file</span> <span class="o">=</span> <span class="p">(</span> <span class="n">xmp_template_file</span> <span class="o">=</span> <span class="p">(</span>
@@ -1775,22 +1862,23 @@
<span class="k">if</span> <span class="n">merge_exif_keywords</span><span class="p">:</span> <span class="k">if</span> <span class="n">merge_exif_keywords</span><span class="p">:</span>
<span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_get_exif_keywords</span><span class="p">())</span> <span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_get_exif_keywords</span><span class="p">())</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">keywords</span><span class="p">:</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">keywords</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">replace_keywords</span><span class="p">:</span>
<span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">keywords</span><span class="p">)</span> <span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">keywords</span><span class="p">)</span>
<span class="c1"># TODO: keyword handling in this and _exiftool_json_sidecar is</span> <span class="c1"># TODO: keyword handling in this and _exiftool_json_sidecar is</span>
<span class="c1"># good candidate for pulling out in a function</span> <span class="c1"># good candidate for pulling out in a function</span>
<span class="n">person_list</span> <span class="o">=</span> <span class="p">[]</span> <span class="n">person_list</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">if</span> <span class="n">merge_exif_persons</span><span class="p">:</span> <span class="k">if</span> <span class="n">persons</span><span class="p">:</span>
<span class="n">person_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_get_exif_persons</span><span class="p">())</span> <span class="k">if</span> <span class="n">merge_exif_persons</span><span class="p">:</span>
<span class="n">person_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_get_exif_persons</span><span class="p">())</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">persons</span><span class="p">:</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">persons</span><span class="p">:</span>
<span class="c1"># filter out _UNKNOWN_PERSON</span> <span class="c1"># filter out _UNKNOWN_PERSON</span>
<span class="n">person_list</span><span class="o">.</span><span class="n">extend</span><span class="p">([</span><span class="n">p</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">persons</span> <span class="k">if</span> <span class="n">p</span> <span class="o">!=</span> <span class="n">_UNKNOWN_PERSON</span><span class="p">])</span> <span class="n">person_list</span><span class="o">.</span><span class="n">extend</span><span class="p">([</span><span class="n">p</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">persons</span> <span class="k">if</span> <span class="n">p</span> <span class="o">!=</span> <span class="n">_UNKNOWN_PERSON</span><span class="p">])</span>
<span class="k">if</span> <span class="n">use_persons_as_keywords</span> <span class="ow">and</span> <span class="n">person_list</span><span class="p">:</span> <span class="k">if</span> <span class="n">use_persons_as_keywords</span> <span class="ow">and</span> <span class="n">person_list</span><span class="p">:</span>
<span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">person_list</span><span class="p">)</span> <span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">person_list</span><span class="p">)</span>
<span class="k">if</span> <span class="n">use_albums_as_keywords</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">albums</span><span class="p">:</span> <span class="k">if</span> <span class="n">use_albums_as_keywords</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">albums</span><span class="p">:</span>
<span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">albums</span><span class="p">)</span> <span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">albums</span><span class="p">)</span>
@@ -1820,11 +1908,14 @@
<span class="c1"># sorted mainly to make testing the XMP file easier</span> <span class="c1"># sorted mainly to make testing the XMP file easier</span>
<span class="k">if</span> <span class="n">keyword_list</span><span class="p">:</span> <span class="k">if</span> <span class="n">keyword_list</span><span class="p">:</span>
<span class="n">keyword_list</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="nb">list</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">keyword_list</span><span class="p">)))</span> <span class="n">keyword_list</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="nb">list</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">keyword_list</span><span class="p">)))</span>
<span class="k">if</span> <span class="n">person_list</span><span class="p">:</span> <span class="k">if</span> <span class="n">persons</span> <span class="ow">and</span> <span class="n">person_list</span><span class="p">:</span>
<span class="n">person_list</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="nb">list</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">person_list</span><span class="p">)))</span> <span class="n">person_list</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="nb">list</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">person_list</span><span class="p">)))</span>
<span class="n">subject_list</span> <span class="o">=</span> <span class="n">keyword_list</span> <span class="n">subject_list</span> <span class="o">=</span> <span class="n">keyword_list</span>
<span class="k">if</span> <span class="n">location</span><span class="p">:</span>
<span class="n">latlon</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">location</span>
<span class="n">xmp_str</span> <span class="o">=</span> <span class="n">xmp_template</span><span class="o">.</span><span class="n">render</span><span class="p">(</span> <span class="n">xmp_str</span> <span class="o">=</span> <span class="n">xmp_template</span><span class="o">.</span><span class="n">render</span><span class="p">(</span>
<span class="n">photo</span><span class="o">=</span><span class="bp">self</span><span class="p">,</span> <span class="n">photo</span><span class="o">=</span><span class="bp">self</span><span class="p">,</span>
<span class="n">description</span><span class="o">=</span><span class="n">description</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="n">description</span><span class="p">,</span>
@@ -1832,6 +1923,7 @@
<span class="n">persons</span><span class="o">=</span><span class="n">person_list</span><span class="p">,</span> <span class="n">persons</span><span class="o">=</span><span class="n">person_list</span><span class="p">,</span>
<span class="n">subjects</span><span class="o">=</span><span class="n">subject_list</span><span class="p">,</span> <span class="n">subjects</span><span class="o">=</span><span class="n">subject_list</span><span class="p">,</span>
<span class="n">extension</span><span class="o">=</span><span class="n">extension</span><span class="p">,</span> <span class="n">extension</span><span class="o">=</span><span class="n">extension</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">latlon</span><span class="p">,</span>
<span class="n">version</span><span class="o">=</span><span class="n">__version__</span><span class="p">,</span> <span class="n">version</span><span class="o">=</span><span class="n">__version__</span><span class="p">,</span>
<span class="p">)</span> <span class="p">)</span>
@@ -1912,7 +2004,7 @@
&copy;2021, Rhet Turnbull. &copy;2021, Rhet Turnbull.
| |
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.4.3</a> Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a> &amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div> </div>

View File

@@ -5,7 +5,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.photoinfo._photoinfo_scoreinfo &#8212; osxphotos 0.41.0 documentation</title> <title>osxphotos.photoinfo._photoinfo_scoreinfo &#8212; osxphotos 0.41.4 documentation</title>
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" /> <link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="../../../_static/alabaster.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> <script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
@@ -208,7 +208,7 @@
&copy;2021, Rhet Turnbull. &copy;2021, Rhet Turnbull.
| |
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.4.3</a> Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a> &amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div> </div>

View File

@@ -5,7 +5,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.photoinfo._photoinfo_searchinfo &#8212; osxphotos 0.41.0 documentation</title> <title>osxphotos.photoinfo._photoinfo_searchinfo &#8212; osxphotos 0.41.4 documentation</title>
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" /> <link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="../../../_static/alabaster.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> <script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
@@ -366,7 +366,7 @@
&copy;2021, Rhet Turnbull. &copy;2021, Rhet Turnbull.
| |
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.4.3</a> Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a> &amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div> </div>

View File

@@ -5,7 +5,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.photoinfo.photoinfo &#8212; osxphotos 0.41.0 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/pygments.css" type="text/css" />
<link rel="stylesheet" href="../../../_static/alabaster.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> <script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
@@ -60,6 +60,7 @@
<span class="n">_PHOTOS_5_SHARED_PHOTO_PATH</span><span class="p">,</span> <span class="n">_PHOTOS_5_SHARED_PHOTO_PATH</span><span class="p">,</span>
<span class="n">_PHOTOS_5_VERSION</span><span class="p">,</span> <span class="n">_PHOTOS_5_VERSION</span><span class="p">,</span>
<span class="p">)</span> <span class="p">)</span>
<span class="kn">from</span> <span class="nn">..adjustmentsinfo</span> <span class="kn">import</span> <span class="n">AdjustmentsInfo</span>
<span class="kn">from</span> <span class="nn">..albuminfo</span> <span class="kn">import</span> <span class="n">AlbumInfo</span><span class="p">,</span> <span class="n">ImportInfo</span> <span class="kn">from</span> <span class="nn">..albuminfo</span> <span class="kn">import</span> <span class="n">AlbumInfo</span><span class="p">,</span> <span class="n">ImportInfo</span>
<span class="kn">from</span> <span class="nn">..personinfo</span> <span class="kn">import</span> <span class="n">FaceInfo</span><span class="p">,</span> <span class="n">PersonInfo</span> <span class="kn">from</span> <span class="nn">..personinfo</span> <span class="kn">import</span> <span class="n">FaceInfo</span><span class="p">,</span> <span class="n">PersonInfo</span>
<span class="kn">from</span> <span class="nn">..phototemplate</span> <span class="kn">import</span> <span class="n">PhotoTemplate</span> <span class="kn">from</span> <span class="nn">..phototemplate</span> <span class="kn">import</span> <span class="n">PhotoTemplate</span>
@@ -485,9 +486,24 @@
<span class="p">)</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">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="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="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">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">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> <span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
@@ -497,6 +513,21 @@
<span class="p">]</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">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="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="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> <span class="sd">&quot;&quot;&quot; ImportInfo object representing import session for the photo or None if no import session &quot;&quot;&quot;</span>
@@ -543,6 +574,30 @@
<span class="sd">&quot;&quot;&quot; True if picture has adjustments / edits &quot;&quot;&quot;</span> <span class="sd">&quot;&quot;&quot; True if picture has adjustments / edits &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;hasAdjustments&quot;</span><span class="p">]</span> <span class="o">==</span> <span class="mi">1</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;hasAdjustments&quot;</span><span class="p">]</span> <span class="o">==</span> <span class="mi">1</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">adjustments</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; Returns AdjustmentsInfo class for adjustment data or None if no adjustments; Photos 5+ only &quot;&quot;&quot;</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">&lt;=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
<span class="k">return</span> <span class="kc">None</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">hasadjustments</span><span class="p">:</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">_adjustmentinfo</span>
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
<span class="n">library</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_library_path</span>
<span class="n">directory</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="c1"># first char of uuid</span>
<span class="n">plist_file</span> <span class="o">=</span> <span class="p">(</span>
<span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">library</span><span class="p">)</span>
<span class="o">/</span> <span class="s2">&quot;resources&quot;</span>
<span class="o">/</span> <span class="s2">&quot;renders&quot;</span>
<span class="o">/</span> <span class="n">directory</span>
<span class="o">/</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="si">}</span><span class="s2">.plist&quot;</span>
<span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">plist_file</span><span class="o">.</span><span class="n">is_file</span><span class="p">():</span>
<span class="k">return</span> <span class="kc">None</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_adjustmentinfo</span> <span class="o">=</span> <span class="n">AdjustmentsInfo</span><span class="p">(</span><span class="n">plist_file</span><span class="p">)</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_adjustmentinfo</span>
<span class="nd">@property</span> <span class="nd">@property</span>
<span class="k">def</span> <span class="nf">external_edit</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">def</span> <span class="nf">external_edit</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; Returns True if picture was edited outside of Photos using external editor &quot;&quot;&quot;</span> <span class="sd">&quot;&quot;&quot; Returns True if picture was edited outside of Photos using external editor &quot;&quot;&quot;</span>
@@ -688,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="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="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="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="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> <span class="sd">&quot;&quot;&quot;If photo is a burst photo, returns list of PhotoInfo objects</span>
@@ -856,8 +916,19 @@
<span class="nd">@property</span> <span class="nd">@property</span>
<span class="k">def</span> <span class="nf">orientation</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">def</span> <span class="nf">orientation</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; returns EXIF orientation of the current photo version as int &quot;&quot;&quot;</span> <span class="sd">&quot;&quot;&quot; returns EXIF orientation of the current photo version as int or 0 if current orientation cannot be determined &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;orientation&quot;</span><span class="p">]</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">&lt;=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
<span class="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;orientation&quot;</span><span class="p">]</span>
<span class="c1"># For Photos 5+, try to get the adjusted orientation</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">hasadjustments</span><span class="p">:</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">adjustments</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">adjustments</span><span class="o">.</span><span class="n">adj_orientation</span>
<span class="k">else</span><span class="p">:</span>
<span class="c1"># can&#39;t reliably determine orientation for edited photo if adjustmentinfo not available</span>
<span class="k">return</span> <span class="mi">0</span>
<span class="k">else</span><span class="p">:</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;orientation&quot;</span><span class="p">]</span>
<span class="nd">@property</span> <span class="nd">@property</span>
<span class="k">def</span> <span class="nf">original_height</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">def</span> <span class="nf">original_height</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
@@ -1185,7 +1256,7 @@
&copy;2021, Rhet Turnbull. &copy;2021, Rhet Turnbull.
| |
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.4.3</a> Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a> &amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div> </div>

View File

@@ -5,7 +5,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.photosdb.photosdb &#8212; osxphotos 0.41.0 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/pygments.css" type="text/css" />
<link rel="stylesheet" href="../../../_static/alabaster.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> <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="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">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="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="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> <span class="s2"> ZADDITIONALASSETATTRIBUTES.ZMASTERFINGERPRINT, </span>
@@ -2074,6 +2073,7 @@
<span class="c1"># &gt; 6 = portrait (sometimes, see ZDEPTHSTATE/ZDEPTHTYPE)</span> <span class="c1"># &gt; 6 = portrait (sometimes, see ZDEPTHSTATE/ZDEPTHTYPE)</span>
<span class="n">info</span><span class="p">[</span><span class="s2">&quot;customRenderedValue&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">22</span><span class="p">]</span> <span class="n">info</span><span class="p">[</span><span class="s2">&quot;customRenderedValue&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">22</span><span class="p">]</span>
<span class="n">info</span><span class="p">[</span><span class="s2">&quot;hdr&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="kc">True</span> <span class="k">if</span> <span class="n">row</span><span class="p">[</span><span class="mi">22</span><span class="p">]</span> <span class="o">==</span> <span class="mi">3</span> <span class="k">else</span> <span class="kc">False</span> <span class="n">info</span><span class="p">[</span><span class="s2">&quot;hdr&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="kc">True</span> <span class="k">if</span> <span class="n">row</span><span class="p">[</span><span class="mi">22</span><span class="p">]</span> <span class="o">==</span> <span class="mi">3</span> <span class="k">else</span> <span class="kc">False</span>
<span class="n">info</span><span class="p">[</span><span class="s2">&quot;depth_state&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">36</span><span class="p">]</span>
<span class="n">info</span><span class="p">[</span><span class="s2">&quot;portrait&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="kc">True</span> <span class="k">if</span> <span class="n">row</span><span class="p">[</span><span class="mi">36</span><span class="p">]</span> <span class="o">!=</span> <span class="mi">0</span> <span class="k">else</span> <span class="kc">False</span> <span class="n">info</span><span class="p">[</span><span class="s2">&quot;portrait&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="kc">True</span> <span class="k">if</span> <span class="n">row</span><span class="p">[</span><span class="mi">36</span><span class="p">]</span> <span class="o">!=</span> <span class="mi">0</span> <span class="k">else</span> <span class="kc">False</span>
<span class="c1"># Set panorama from either KindSubType or RenderedValue</span> <span class="c1"># Set panorama from either KindSubType or RenderedValue</span>
@@ -2768,8 +2768,6 @@
<span class="c1"># an empty album will be in _dbalbum_titles but not _dbalbums_album</span> <span class="c1"># an empty album will be in _dbalbum_titles but not _dbalbums_album</span>
<span class="k">pass</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="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="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> <span class="k">if</span> <span class="n">uuid</span><span class="p">:</span>
@@ -2777,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">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="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="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="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> <span class="k">if</span> <span class="n">keywords</span><span class="p">:</span>
@@ -2786,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">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="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="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="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> <span class="k">if</span> <span class="n">persons</span><span class="p">:</span>
@@ -2800,8 +2794,6 @@
<span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span> <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="c1"># some persons have zero photos so they won&#39;t be in _dbfaces_pk</span>
<span class="k">pass</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="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> <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>
@@ -2812,14 +2804,10 @@
<span class="n">dsel</span> <span class="o">=</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">&gt;=</span> <span class="n">from_date</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="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="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="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">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">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">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> <span class="n">photoinfo</span> <span class="o">=</span> <span class="p">[]</span>
@@ -2951,7 +2939,7 @@
&copy;2021, Rhet Turnbull. &copy;2021, Rhet Turnbull.
| |
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.4.3</a> Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a> &amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div> </div>

View File

@@ -29,9 +29,14 @@ if (!window.console || !console.firebug) {
/** /**
* small helper function to urldecode strings * small helper function to urldecode strings
*
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL
*/ */
jQuery.urldecode = function(x) { jQuery.urldecode = function(x) {
return decodeURIComponent(x).replace(/\+/g, ' '); if (!x) {
return x
}
return decodeURIComponent(x.replace(/\+/g, ' '));
}; };
/** /**

View File

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

View File

@@ -13,7 +13,8 @@
var stopwords = ["a","and","are","as","at","be","but","by","for","if","in","into","is","it","near","no","not","of","on","or","such","that","the","their","then","there","these","they","this","to","was","will","with"]; var stopwords = ["a","and","are","as","at","be","but","by","for","if","in","into","is","it","near","no","not","of","on","or","such","that","the","their","then","there","these","they","this","to","was","will","with"];
/* Non-minified version JS is _stemmer.js if file is provided */ /* Non-minified version is copied as a separate JS file, is available */
/** /**
* Porter Stemmer * Porter Stemmer
*/ */
@@ -199,7 +200,6 @@ var Stemmer = function() {
var splitChars = (function() { var splitChars = (function() {
var result = {}; var result = {};
var singles = [96, 180, 187, 191, 215, 247, 749, 885, 903, 907, 909, 930, 1014, 1648, var singles = [96, 180, 187, 191, 215, 247, 749, 885, 903, 907, 909, 930, 1014, 1648,

View File

@@ -1,7 +1,7 @@
pre { line-height: 125%; } pre { line-height: 125%; }
td.linenos pre { color: #000000; background-color: #f0f0f0; padding-left: 5px; padding-right: 5px; } td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: #000000; background-color: #f0f0f0; padding-left: 5px; padding-right: 5px; } span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos pre.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc } .highlight .hll { background-color: #ffffcc }
.highlight { background: #f8f8f8; } .highlight { background: #f8f8f8; }

View File

@@ -248,7 +248,7 @@ var Search = {
// results left, load the summary and display it // results left, load the summary and display it
if (results.length) { if (results.length) {
var item = results.pop(); var item = results.pop();
var listItem = $('<li style="display:none"></li>'); var listItem = $('<li></li>');
var requestUrl = ""; var requestUrl = "";
var linkUrl = ""; var linkUrl = "";
if (DOCUMENTATION_OPTIONS.BUILDER === 'dirhtml') { if (DOCUMENTATION_OPTIONS.BUILDER === 'dirhtml') {
@@ -273,9 +273,9 @@ var Search = {
if (item[3]) { if (item[3]) {
listItem.append($('<span> (' + item[3] + ')</span>')); listItem.append($('<span> (' + item[3] + ')</span>'));
Search.output.append(listItem); Search.output.append(listItem);
listItem.slideDown(5, function() { setTimeout(function() {
displayNextItem(); displayNextItem();
}); }, 5);
} else if (DOCUMENTATION_OPTIONS.HAS_SOURCE) { } else if (DOCUMENTATION_OPTIONS.HAS_SOURCE) {
$.ajax({url: requestUrl, $.ajax({url: requestUrl,
dataType: "text", dataType: "text",
@@ -285,16 +285,16 @@ var Search = {
listItem.append(Search.makeSearchSummary(data, searchterms, hlterms)); listItem.append(Search.makeSearchSummary(data, searchterms, hlterms));
} }
Search.output.append(listItem); Search.output.append(listItem);
listItem.slideDown(5, function() { setTimeout(function() {
displayNextItem(); displayNextItem();
}); }, 5);
}}); }});
} else { } else {
// no source available, just display title // no source available, just display title
Search.output.append(listItem); Search.output.append(listItem);
listItem.slideDown(5, function() { setTimeout(function() {
displayNextItem(); displayNextItem();
}); }, 5);
} }
} }
// search finished, update title and status message // search finished, update title and status message
@@ -379,6 +379,13 @@ var Search = {
return results; return results;
}, },
/**
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
*/
escapeRegExp : function(string) {
return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
},
/** /**
* search for full-text terms in the index * search for full-text terms in the index
*/ */
@@ -402,13 +409,14 @@ var Search = {
]; ];
// add support for partial matches // add support for partial matches
if (word.length > 2) { if (word.length > 2) {
var word_regex = this.escapeRegExp(word);
for (var w in terms) { for (var w in terms) {
if (w.match(word) && !terms[word]) { if (w.match(word_regex) && !terms[word]) {
_o.push({files: terms[w], score: Scorer.partialTerm}) _o.push({files: terms[w], score: Scorer.partialTerm})
} }
} }
for (var w in titleterms) { for (var w in titleterms) {
if (w.match(word) && !titleterms[word]) { if (w.match(word_regex) && !titleterms[word]) {
_o.push({files: titleterms[w], score: Scorer.partialTitle}) _o.push({files: titleterms[w], score: Scorer.partialTitle})
} }
} }

2027
docs/_static/underscore-1.12.0.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Index &#8212; osxphotos 0.41.0 documentation</title> <title>Index &#8212; osxphotos 0.42.1 documentation</title>
<link rel="stylesheet" href="_static/pygments.css" type="text/css" /> <link rel="stylesheet" href="_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="_static/alabaster.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> <script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
@@ -327,6 +327,15 @@
<li><a href="cli.html#cmdoption-osxphotos-export-from-date">osxphotos-export command line option</a> <li><a href="cli.html#cmdoption-osxphotos-export-from-date">osxphotos-export command line option</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-query-from-date">osxphotos-query command line option</a> <li><a href="cli.html#cmdoption-osxphotos-query-from-date">osxphotos-query command line option</a>
</li>
</ul></li>
<li>
--from-time &lt;from_time&gt;
<ul>
<li><a href="cli.html#cmdoption-osxphotos-export-from-time">osxphotos-export command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-from-time">osxphotos-query command line option</a>
</li> </li>
</ul></li> </ul></li>
<li> <li>
@@ -509,6 +518,15 @@
<li><a href="cli.html#cmdoption-osxphotos-export-missing">osxphotos-export command line option</a> <li><a href="cli.html#cmdoption-osxphotos-export-missing">osxphotos-export command line option</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-query-missing">osxphotos-query command line option</a> <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> </li>
</ul></li> </ul></li>
<li> <li>
@@ -529,8 +547,6 @@
<li><a href="cli.html#cmdoption-osxphotos-query-no-description">osxphotos-query command line option</a> <li><a href="cli.html#cmdoption-osxphotos-query-no-description">osxphotos-query command line option</a>
</li> </li>
</ul></li> </ul></li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li> <li>
--no-likes --no-likes
@@ -540,6 +556,8 @@
<li><a href="cli.html#cmdoption-osxphotos-query-no-likes">osxphotos-query command line option</a> <li><a href="cli.html#cmdoption-osxphotos-query-no-likes">osxphotos-query command line option</a>
</li> </li>
</ul></li> </ul></li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li> <li>
--no-place --no-place
@@ -776,6 +794,22 @@
<li><a href="cli.html#cmdoption-osxphotos-export-portrait">osxphotos-export command line option</a> <li><a href="cli.html#cmdoption-osxphotos-export-portrait">osxphotos-export command line option</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-query-portrait">osxphotos-query command line option</a> <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>
--replace-keywords
<ul>
<li><a href="cli.html#cmdoption-osxphotos-export-replace-keywords">osxphotos-export command line option</a>
</li> </li>
</ul></li> </ul></li>
<li> <li>
@@ -783,6 +817,13 @@
<ul> <ul>
<li><a href="cli.html#cmdoption-osxphotos-export-report">osxphotos-export command line option</a> <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> </li>
</ul></li> </ul></li>
<li> <li>
@@ -909,6 +950,15 @@
<li><a href="cli.html#cmdoption-osxphotos-export-to-date">osxphotos-export command line option</a> <li><a href="cli.html#cmdoption-osxphotos-export-to-date">osxphotos-export command line option</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-query-to-date">osxphotos-query command line option</a> <li><a href="cli.html#cmdoption-osxphotos-query-to-date">osxphotos-query command line option</a>
</li>
</ul></li>
<li>
--to-time &lt;to_time&gt;
<ul>
<li><a href="cli.html#cmdoption-osxphotos-export-to-time">osxphotos-export command line option</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-to-time">osxphotos-query command line option</a>
</li> </li>
</ul></li> </ul></li>
<li> <li>
@@ -1017,6 +1067,8 @@
<table style="width: 100%" class="indextable genindextable"><tr> <table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul> <td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.SearchInfo.activities">activities() (osxphotos.PhotoInfo.SearchInfo property)</a> <li><a href="reference.html#osxphotos.PhotoInfo.SearchInfo.activities">activities() (osxphotos.PhotoInfo.SearchInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.adjustments">adjustments() (osxphotos.PhotoInfo property)</a>
</li> </li>
<li><a href="reference.html#osxphotos.PhotoInfo.album_info">album_info() (osxphotos.PhotoInfo property)</a> <li><a href="reference.html#osxphotos.PhotoInfo.album_info">album_info() (osxphotos.PhotoInfo property)</a>
@@ -1032,10 +1084,10 @@
<li><a href="reference.html#osxphotos.PhotosDB.albums">(osxphotos.PhotosDB property)</a> <li><a href="reference.html#osxphotos.PhotosDB.albums">(osxphotos.PhotosDB property)</a>
</li> </li>
</ul></li> </ul></li>
<li><a href="reference.html#osxphotos.PhotosDB.albums_as_dict">albums_as_dict() (osxphotos.PhotosDB property)</a>
</li>
</ul></td> </ul></td>
<td style="width: 33%; vertical-align: top;"><ul> <td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotosDB.albums_as_dict">albums_as_dict() (osxphotos.PhotosDB property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotosDB.albums_shared">albums_shared() (osxphotos.PhotosDB property)</a> <li><a href="reference.html#osxphotos.PhotosDB.albums_shared">albums_shared() (osxphotos.PhotosDB property)</a>
</li> </li>
<li><a href="reference.html#osxphotos.PhotosDB.albums_shared_as_dict">albums_shared_as_dict() (osxphotos.PhotosDB property)</a> <li><a href="reference.html#osxphotos.PhotosDB.albums_shared_as_dict">albums_shared_as_dict() (osxphotos.PhotosDB property)</a>
@@ -1062,13 +1114,19 @@
</li> </li>
<li><a href="reference.html#osxphotos.PhotoInfo.ExifInfo.bit_rate">bit_rate (osxphotos.PhotoInfo.ExifInfo attribute)</a> <li><a href="reference.html#osxphotos.PhotoInfo.ExifInfo.bit_rate">bit_rate (osxphotos.PhotoInfo.ExifInfo attribute)</a>
</li> </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><a href="reference.html#osxphotos.PhotoInfo.SearchInfo.bodies_of_water">bodies_of_water() (osxphotos.PhotoInfo.SearchInfo property)</a>
</li> </li>
<li><a href="reference.html#osxphotos.PhotoInfo.burst">burst() (osxphotos.PhotoInfo property)</a> <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>
<li><a href="reference.html#osxphotos.PhotoInfo.burst_photos">burst_photos() (osxphotos.PhotoInfo property)</a> <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> </li>
</ul></td> </ul></td>
</tr></table> </tr></table>
@@ -1461,6 +1519,8 @@
<li><a href="cli.html#cmdoption-osxphotos-export-folder">--folder &lt;FOLDER&gt;</a> <li><a href="cli.html#cmdoption-osxphotos-export-folder">--folder &lt;FOLDER&gt;</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-export-from-date">--from-date &lt;from_date&gt;</a> <li><a href="cli.html#cmdoption-osxphotos-export-from-date">--from-date &lt;from_date&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-from-time">--from-time &lt;from_time&gt;</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-export-has-comment">--has-comment</a> <li><a href="cli.html#cmdoption-osxphotos-export-has-comment">--has-comment</a>
</li> </li>
@@ -1497,6 +1557,8 @@
<li><a href="cli.html#cmdoption-osxphotos-export-load-config">--load-config &lt;config file path&gt;</a> <li><a href="cli.html#cmdoption-osxphotos-export-load-config">--load-config &lt;config file path&gt;</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-export-missing">--missing</a> <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>
<li><a href="cli.html#cmdoption-osxphotos-export-no-comment">--no-comment</a> <li><a href="cli.html#cmdoption-osxphotos-export-no-comment">--no-comment</a>
</li> </li>
@@ -1553,8 +1615,14 @@
<li><a href="cli.html#cmdoption-osxphotos-export-place">--place &lt;PLACE&gt;</a> <li><a href="cli.html#cmdoption-osxphotos-export-place">--place &lt;PLACE&gt;</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-export-portrait">--portrait</a> <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>
<li><a href="cli.html#cmdoption-osxphotos-export-report">--report &lt;path to export report&gt;</a> <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>
<li><a href="cli.html#cmdoption-osxphotos-export-save-config">--save-config &lt;config file path&gt;</a> <li><a href="cli.html#cmdoption-osxphotos-export-save-config">--save-config &lt;config file path&gt;</a>
</li> </li>
@@ -1587,6 +1655,8 @@
<li><a href="cli.html#cmdoption-osxphotos-export-title">--title &lt;TITLE&gt;</a> <li><a href="cli.html#cmdoption-osxphotos-export-title">--title &lt;TITLE&gt;</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-export-to-date">--to-date &lt;to_date&gt;</a> <li><a href="cli.html#cmdoption-osxphotos-export-to-date">--to-date &lt;to_date&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-export-to-time">--to-time &lt;to_time&gt;</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-export-touch-file">--touch-file</a> <li><a href="cli.html#cmdoption-osxphotos-export-touch-file">--touch-file</a>
</li> </li>
@@ -1713,6 +1783,8 @@
<li><a href="cli.html#cmdoption-osxphotos-query-folder">--folder &lt;FOLDER&gt;</a> <li><a href="cli.html#cmdoption-osxphotos-query-folder">--folder &lt;FOLDER&gt;</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-query-from-date">--from-date &lt;from_date&gt;</a> <li><a href="cli.html#cmdoption-osxphotos-query-from-date">--from-date &lt;from_date&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-from-time">--from-time &lt;from_time&gt;</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-query-has-comment">--has-comment</a> <li><a href="cli.html#cmdoption-osxphotos-query-has-comment">--has-comment</a>
</li> </li>
@@ -1741,6 +1813,8 @@
<li><a href="cli.html#cmdoption-osxphotos-query-live">--live</a> <li><a href="cli.html#cmdoption-osxphotos-query-live">--live</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-query-missing">--missing</a> <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>
<li><a href="cli.html#cmdoption-osxphotos-query-no-comment">--no-comment</a> <li><a href="cli.html#cmdoption-osxphotos-query-no-comment">--no-comment</a>
</li> </li>
@@ -1795,6 +1869,8 @@
<li><a href="cli.html#cmdoption-osxphotos-query-place">--place &lt;PLACE&gt;</a> <li><a href="cli.html#cmdoption-osxphotos-query-place">--place &lt;PLACE&gt;</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-query-portrait">--portrait</a> <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>
<li><a href="cli.html#cmdoption-osxphotos-query-screenshot">--screenshot</a> <li><a href="cli.html#cmdoption-osxphotos-query-screenshot">--screenshot</a>
</li> </li>
@@ -1809,6 +1885,8 @@
<li><a href="cli.html#cmdoption-osxphotos-query-title">--title &lt;TITLE&gt;</a> <li><a href="cli.html#cmdoption-osxphotos-query-title">--title &lt;TITLE&gt;</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-query-to-date">--to-date &lt;to_date&gt;</a> <li><a href="cli.html#cmdoption-osxphotos-query-to-date">--to-date &lt;to_date&gt;</a>
</li>
<li><a href="cli.html#cmdoption-osxphotos-query-to-time">--to-time &lt;to_time&gt;</a>
</li> </li>
<li><a href="cli.html#cmdoption-osxphotos-query-uti">--uti &lt;UTI&gt;</a> <li><a href="cli.html#cmdoption-osxphotos-query-uti">--uti &lt;UTI&gt;</a>
</li> </li>
@@ -2108,7 +2186,7 @@
&copy;2021, Rhet Turnbull. &copy;2021, Rhet Turnbull.
| |
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.4.3</a> Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a> &amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div> </div>

View File

@@ -5,7 +5,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Welcome to osxphotoss documentation! &#8212; osxphotos 0.41.0 documentation</title> <title>Welcome to osxphotoss documentation! &#8212; osxphotos 0.42.1 documentation</title>
<link rel="stylesheet" href="_static/pygments.css" type="text/css" /> <link rel="stylesheet" href="_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="_static/alabaster.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> <script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
@@ -365,7 +365,7 @@ Alternatively, you can also run the command line utility like this: <code class=
&copy;2021, Rhet Turnbull. &copy;2021, Rhet Turnbull.
| |
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.4.3</a> Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a> &amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
| |

View File

@@ -5,7 +5,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos &#8212; osxphotos 0.41.0 documentation</title> <title>osxphotos &#8212; osxphotos 0.42.1 documentation</title>
<link rel="stylesheet" href="_static/pygments.css" type="text/css" /> <link rel="stylesheet" href="_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="_static/alabaster.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> <script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
@@ -91,7 +91,7 @@
&copy;2021, Rhet Turnbull. &copy;2021, Rhet Turnbull.
| |
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.4.3</a> Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a> &amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
| |

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -5,7 +5,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Search &#8212; osxphotos 0.41.0 documentation</title> <title>Search &#8212; osxphotos 0.42.1 documentation</title>
<link rel="stylesheet" href="_static/pygments.css" type="text/css" /> <link rel="stylesheet" href="_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" /> <link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
@@ -102,7 +102,7 @@
&copy;2021, Rhet Turnbull. &copy;2021, Rhet Turnbull.
| |
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.4.3</a> Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a> &amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div> </div>

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

@@ -1,3 +1,3 @@
""" version info """ """ version info """
__version__ = "0.41.2" __version__ = "0.42.2"

View File

@@ -131,6 +131,21 @@ class DateTimeISO8601(click.ParamType):
) )
class TimeISO8601(click.ParamType):
name = "TIME"
def convert(self, value, param, ctx):
try:
return datetime.time.fromisoformat(value).replace(tzinfo=None)
except Exception:
self.fail(
f"Invalid value for --{param.name}: invalid time format {value}. "
"Valid format: HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]] "
"however, note that timezone will be ignored."
)
# Click CLI object & context settings # Click CLI object & context settings
class CLI_Obj: class CLI_Obj:
def __init__(self, db=None, json=False, debug=False): def __init__(self, db=None, json=False, debug=False):
@@ -223,6 +238,15 @@ def query_options(f):
'If more than one folder, treated as "OR", e.g. find photos in any 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)", "Only searches top level folders (e.g. does not look at subfolders)",
), ),
o(
"--name",
metavar="FILENAME",
default=None,
multiple=True,
help="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. ",
),
o( o(
"--uuid", "--uuid",
metavar="UUID", metavar="UUID",
@@ -386,14 +410,24 @@ def query_options(f):
), ),
o( o(
"--from-date", "--from-date",
help="Search by start item date, e.g. 2000-01-12T12:00:00, 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 8601).", help="Search by item start date, e.g. 2000-01-12T12:00:00, 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 8601 with/without timezone).",
type=DateTimeISO8601(), type=DateTimeISO8601(),
), ),
o( o(
"--to-date", "--to-date",
help="Search by end item date, e.g. 2000-01-12T12:00:00, 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 8601).", help="Search by item end date, e.g. 2000-01-12T12:00:00, 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 8601 with/without timezone).",
type=DateTimeISO8601(), type=DateTimeISO8601(),
), ),
o(
"--from-time",
help="Search by item start time of day, e.g. 12:00, or 12:00:00.",
type=TimeISO8601(),
),
o(
"--to-time",
help="Search by item end time of day, e.g. 12:00 or 12:00:00.",
type=TimeISO8601(),
),
o("--has-comment", is_flag=True, help="Search for photos that have comments."), o("--has-comment", is_flag=True, help="Search for photos that have comments."),
o("--no-comment", is_flag=True, help="Search for photos with no comments."), o("--no-comment", is_flag=True, help="Search for photos with no comments."),
o("--has-likes", is_flag=True, help="Search for photos that have likes."), o("--has-likes", is_flag=True, help="Search for photos that have likes."),
@@ -413,6 +447,20 @@ def query_options(f):
is_flag=True, is_flag=True,
help="Search for photos that are not in any albums.", help="Search for photos that are not in any albums.",
), ),
o(
"--query-eval",
metavar="CRITERIA",
multiple=True,
help="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.",
),
] ]
for o in options[::-1]: for o in options[::-1]:
f = o(f) f = o(f)
@@ -492,6 +540,13 @@ def cli(ctx, db, json_, debug):
"Use this with caution as it may create name collisions on export. " "Use this with caution as it may create name collisions on export. "
"(e.g. if two files happen to have the same name)", "(e.g. if two files happen to have the same name)",
) )
@click.option(
"--retry",
metavar="RETRY",
type=click.INT,
help="Automatically retry export up to RETRY times if an error occurs during export. "
"This may be useful with network drives that experience intermittent errors.",
)
@click.option( @click.option(
"--export-by-date", "--export-by-date",
is_flag=True, is_flag=True,
@@ -782,7 +837,10 @@ def cli(ctx, db, json_, debug):
"--cleanup", "--cleanup",
is_flag=True, is_flag=True,
help="Cleanup export directory by deleting any files which were not included in this export set. " help="Cleanup export directory by deleting any files which were not included in this export set. "
"For example, photos which had previously been exported and were subsequently deleted in Photos.", "For example, photos which had previously been exported and were subsequently deleted in Photos. "
"WARNING: --cleanup will delete *any* files in the export directory that were not exported by osxphotos, "
"for example, your own scripts or other files. Be sure this is what you intend before using "
"--cleanup. Use --dry-run with --cleanup first if you're not certain.",
) )
@click.option( @click.option(
"--exportdb", "--exportdb",
@@ -836,6 +894,7 @@ def export(
album, album,
folder, folder,
uuid, uuid,
name,
uuid_from_file, uuid_from_file,
title, title,
no_title, no_title,
@@ -853,6 +912,8 @@ def export(
not_shared, not_shared,
from_date, from_date,
to_date, to_date,
from_time,
to_time,
verbose, verbose,
missing, missing,
update, update,
@@ -862,6 +923,7 @@ def export(
export_as_hardlink, export_as_hardlink,
touch_file, touch_file,
overwrite, overwrite,
retry,
export_by_date, export_by_date,
skip_edited, skip_edited,
skip_original_if_edited, skip_original_if_edited,
@@ -936,6 +998,7 @@ def export(
beta, beta,
in_album, in_album,
not_in_album, not_in_album,
query_eval,
): ):
"""Export photos from the Photos database. """Export photos from the Photos database.
Export path DEST is required. Export path DEST is required.
@@ -982,6 +1045,7 @@ def export(
person = cfg.person person = cfg.person
album = cfg.album album = cfg.album
folder = cfg.folder folder = cfg.folder
name = cfg.name
uuid = cfg.uuid uuid = cfg.uuid
uuid_from_file = cfg.uuid_from_file uuid_from_file = cfg.uuid_from_file
title = cfg.title title = cfg.title
@@ -1000,6 +1064,8 @@ def export(
not_shared = cfg.not_shared not_shared = cfg.not_shared
from_date = cfg.from_date from_date = cfg.from_date
to_date = cfg.to_date to_date = cfg.to_date
from_time = cfg.from_time
to_time = cfg.to_time
verbose = cfg.verbose verbose = cfg.verbose
missing = cfg.missing missing = cfg.missing
update = cfg.update update = cfg.update
@@ -1008,6 +1074,7 @@ def export(
export_as_hardlink = cfg.export_as_hardlink export_as_hardlink = cfg.export_as_hardlink
touch_file = cfg.touch_file touch_file = cfg.touch_file
overwrite = cfg.overwrite overwrite = cfg.overwrite
retry = cfg.retry
export_by_date = cfg.export_by_date export_by_date = cfg.export_by_date
skip_edited = cfg.skip_edited skip_edited = cfg.skip_edited
skip_original_if_edited = cfg.skip_original_if_edited skip_original_if_edited = cfg.skip_original_if_edited
@@ -1079,6 +1146,7 @@ def export(
only_new = cfg.only_new only_new = cfg.only_new
in_album = cfg.in_album in_album = cfg.in_album
not_in_album = cfg.not_in_album not_in_album = cfg.not_in_album
query_eval = cfg.query_eval
# config file might have changed verbose # config file might have changed verbose
VERBOSE = bool(verbose) VERBOSE = bool(verbose)
@@ -1167,6 +1235,7 @@ def export(
original_suffix = ( original_suffix = (
DEFAULT_ORIGINAL_SUFFIX if original_suffix is None else original_suffix DEFAULT_ORIGINAL_SUFFIX if original_suffix is None else original_suffix
) )
retry = 0 if not retry else retry
if not os.path.isdir(dest): if not os.path.isdir(dest):
click.echo( click.echo(
@@ -1347,6 +1416,8 @@ def export(
not_incloud=False, not_incloud=False,
from_date=from_date, from_date=from_date,
to_date=to_date, to_date=to_date,
from_time=from_time,
to_time=to_time,
portrait=portrait, portrait=portrait,
not_portrait=not_portrait, not_portrait=not_portrait,
screenshot=screenshot, screenshot=screenshot,
@@ -1374,6 +1445,11 @@ def export(
is_reference=is_reference, is_reference=is_reference,
in_album=in_album, in_album=in_album,
not_in_album=not_in_album, not_in_album=not_in_album,
burst_photos=export_bursts,
# skip missing bursts if using --download-missing by itself as AppleScript otherwise causes errors
missing_bursts=(download_missing and use_photokit) or not download_missing,
name=name,
query_eval=query_eval,
) )
if photos: if photos:
@@ -1382,13 +1458,6 @@ def export(
previous_uuids = {uuid: 1 for uuid in export_db.get_previous_uuids()} previous_uuids = {uuid: 1 for uuid in export_db.get_previous_uuids()}
photos = [p for p in photos if p.uuid not in previous_uuids] photos = [p for p in photos if p.uuid not in previous_uuids]
if export_bursts:
# add the burst_photos to the export set
photos_burst = [p for p in photos if p.burst]
for burst in photos_burst:
burst_set = [p for p in burst.burst_photos if not p.ismissing]
photos.extend(burst_set)
num_photos = len(photos) num_photos = len(photos)
# TODO: photos or photo appears several times, pull into a separate function # TODO: photos or photo appears several times, pull into a separate function
photo_str = "photos" if num_photos > 1 else "photo" photo_str = "photos" if num_photos > 1 else "photo"
@@ -1445,6 +1514,7 @@ def export(
strip=strip, strip=strip,
jpeg_ext=jpeg_ext, jpeg_ext=jpeg_ext,
replace_keywords=replace_keywords, replace_keywords=replace_keywords,
retry=retry,
) )
results += export_results results += export_results
@@ -1506,10 +1576,14 @@ def export(
+ [str(pathlib.Path(export_db_path).resolve())] + [str(pathlib.Path(export_db_path).resolve())]
) )
click.echo(f"Cleaning up {dest}") click.echo(f"Cleaning up {dest}")
(cleaned_files, cleaned_dirs) = cleanup_files(dest, all_files, fileutil) cleaned_files, cleaned_dirs = cleanup_files(dest, all_files, fileutil)
file_str = "files" if cleaned_files != 1 else "file" file_str = "files" if len(cleaned_files) != 1 else "file"
dir_str = "directories" if cleaned_dirs != 1 else "directory" dir_str = "directories" if len(cleaned_dirs) != 1 else "directory"
click.echo(f"Deleted: {cleaned_files} {file_str}, {cleaned_dirs} {dir_str}") click.echo(
f"Deleted: {len(cleaned_files)} {file_str}, {len(cleaned_dirs)} {dir_str}"
)
results.deleted_files = cleaned_files
results.deleted_directories = cleaned_dirs
if report: if report:
verbose_(f"Writing export report to {report}") verbose_(f"Writing export report to {report}")
@@ -1600,6 +1674,7 @@ def query(
person, person,
album, album,
folder, folder,
name,
uuid, uuid,
uuid_from_file, uuid_from_file,
title, title,
@@ -1631,6 +1706,8 @@ def query(
not_incloud, not_incloud,
from_date, from_date,
to_date, to_date,
from_time,
to_time,
portrait, portrait,
not_portrait, not_portrait,
screenshot, screenshot,
@@ -1658,6 +1735,7 @@ def query(
is_reference, is_reference,
in_album, in_album,
not_in_album, not_in_album,
query_eval,
): ):
"""Query the Photos database using 1 or more search options; """Query the Photos database using 1 or more search options;
if more than one option is provided, they are treated as "AND" if more than one option is provided, they are treated as "AND"
@@ -1671,6 +1749,7 @@ def query(
person, person,
album, album,
folder, folder,
name,
uuid, uuid,
uuid_from_file, uuid_from_file,
edited, edited,
@@ -1679,8 +1758,11 @@ def query(
has_raw, has_raw,
from_date, from_date,
to_date, to_date,
from_time,
to_time,
label, label,
is_reference, is_reference,
query_eval,
] ]
exclusive = [ exclusive = [
(favorite, not_favorite), (favorite, not_favorite),
@@ -1773,6 +1855,8 @@ def query(
not_incloud=not_incloud, not_incloud=not_incloud,
from_date=from_date, from_date=from_date,
to_date=to_date, to_date=to_date,
from_time=from_time,
to_time=to_time,
portrait=portrait, portrait=portrait,
not_portrait=not_portrait, not_portrait=not_portrait,
screenshot=screenshot, screenshot=screenshot,
@@ -1800,6 +1884,8 @@ def query(
is_reference=is_reference, is_reference=is_reference,
in_album=in_album, in_album=in_album,
not_in_album=not_in_album, not_in_album=not_in_album,
name=name,
query_eval=query_eval,
) )
# below needed for to make CliRunner work for testing # below needed for to make CliRunner work for testing
@@ -1946,6 +2032,8 @@ def _query(
not_incloud=None, not_incloud=None,
from_date=None, from_date=None,
to_date=None, to_date=None,
from_time=None,
to_time=None,
portrait=None, portrait=None,
not_portrait=None, not_portrait=None,
screenshot=None, screenshot=None,
@@ -1973,6 +2061,10 @@ def _query(
is_reference=False, is_reference=False,
in_album=False, in_album=False,
not_in_album=False, not_in_album=False,
burst_photos=None,
missing_bursts=None,
name=None,
query_eval=None,
): ):
"""Run a query against PhotosDB to extract the photos based on user supply criteria used by query and export commands """Run a query against PhotosDB to extract the photos based on user supply criteria used by query and export commands
@@ -2039,30 +2131,38 @@ def _query(
if title: if title:
# search title field for text # search title field for text
# if more than one, find photos with all title values in title # if more than one, find photos with all title values in title
photo_list = []
if ignore_case: if ignore_case:
# case-insensitive # case-insensitive
for t in title: for t in title:
t = t.lower() t = t.lower()
photos = [p for p in photos if p.title and t in p.title.lower()] photo_list.extend(
[p for p in photos if p.title and t in p.title.lower()]
)
else: else:
for t in title: for t in title:
photos = [p for p in photos if p.title and t in p.title] photo_list.extend([p for p in photos if p.title and t in p.title])
photos = photo_list
elif no_title: elif no_title:
photos = [p for p in photos if not p.title] photos = [p for p in photos if not p.title]
if description: if description:
# search description field for text # search description field for text
# if more than one, find photos with all description values in description # if more than one, find photos with all description values in description
photo_list = []
if ignore_case: if ignore_case:
# case-insensitive # case-insensitive
for d in description: for d in description:
d = d.lower() d = d.lower()
photos = [ photo_list.extend(
p for p in photos if p.description and d in p.description.lower() [p for p in photos if p.description and d in p.description.lower()]
] )
else: else:
for d in description: for d in description:
photos = [p for p in photos if p.description and d in p.description] photo_list.extend(
[p for p in photos if p.description and d in p.description]
)
photos = photo_list
elif no_description: elif no_description:
photos = [p for p in photos if not p.description] photos = [p for p in photos if not p.description]
@@ -2210,6 +2310,65 @@ def _query(
elif not_in_album: elif not_in_album:
photos = [p for p in photos if not p.albums] photos = [p for p in photos if not p.albums]
if from_time:
photos = [p for p in photos if p.date.time() >= from_time]
if to_time:
photos = [p for p in photos if p.date.time() <= to_time]
if 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 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 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 query_eval:
for q in query_eval:
query_string = f"[photo for photo in photos if {q}]"
try:
photos = eval(query_string)
except Exception as e:
raise click.BadOptionUsage(
"query_eval", f"Invalid query-eval CRITERIA: {e}"
)
return photos return photos
@@ -2282,6 +2441,7 @@ def export_photo(
strip=False, strip=False,
jpeg_ext=None, jpeg_ext=None,
replace_keywords=False, replace_keywords=False,
retry=0,
): ):
"""Helper function for export that does the actual export """Helper function for export that does the actual export
@@ -2321,6 +2481,7 @@ def export_photo(
exiftool_merge_persons: boolean; if True, merged persons found in file's exif data (requires exiftool) exiftool_merge_persons: boolean; if True, merged persons found in file's exif data (requires exiftool)
jpeg_ext: if not None, specify the extension to use for all JPEG images on export jpeg_ext: if not None, specify the extension to use for all JPEG images on export
replace_keywords: if True, --keyword-template replaces keywords instead of adding keywords replace_keywords: if True, --keyword-template replaces keywords instead of adding keywords
retry: retry up to retry # of times if there's an error
Returns: Returns:
list of path(s) of exported photo or None if photo was missing list of path(s) of exported photo or None if photo was missing
@@ -2470,72 +2631,88 @@ def export_photo(
str(pathlib.Path(dest_path) / original_filename) str(pathlib.Path(dest_path) / original_filename)
) )
else: else:
try: tries = 0
export_results = photo.export2( while tries <= retry:
dest_path, tries += 1
original_filename, error = 0
sidecar=sidecar_flags, try:
sidecar_drop_ext=sidecar_drop_ext, export_results = photo.export2(
live_photo=export_live, dest_path,
raw_photo=export_raw, original_filename,
export_as_hardlink=export_as_hardlink, sidecar=sidecar_flags,
overwrite=overwrite, sidecar_drop_ext=sidecar_drop_ext,
use_photos_export=use_photos_export, live_photo=export_live,
exiftool=exiftool, raw_photo=export_raw,
merge_exif_keywords=exiftool_merge_keywords, export_as_hardlink=export_as_hardlink,
merge_exif_persons=exiftool_merge_persons, overwrite=overwrite,
use_albums_as_keywords=album_keyword, use_photos_export=use_photos_export,
use_persons_as_keywords=person_keyword, exiftool=exiftool,
keyword_template=keyword_template, merge_exif_keywords=exiftool_merge_keywords,
description_template=description_template, merge_exif_persons=exiftool_merge_persons,
update=update, use_albums_as_keywords=album_keyword,
ignore_signature=ignore_signature, use_persons_as_keywords=person_keyword,
export_db=export_db, keyword_template=keyword_template,
fileutil=fileutil, description_template=description_template,
dry_run=dry_run, update=update,
touch_file=touch_file, ignore_signature=ignore_signature,
convert_to_jpeg=convert_to_jpeg, export_db=export_db,
jpeg_quality=jpeg_quality, fileutil=fileutil,
ignore_date_modified=ignore_date_modified, dry_run=dry_run,
use_photokit=use_photokit, touch_file=touch_file,
verbose=verbose_, convert_to_jpeg=convert_to_jpeg,
exiftool_flags=exiftool_option, jpeg_quality=jpeg_quality,
jpeg_ext=jpeg_ext, ignore_date_modified=ignore_date_modified,
replace_keywords=replace_keywords, use_photokit=use_photokit,
) verbose=verbose_,
results += export_results exiftool_flags=exiftool_option,
for warning_ in export_results.exiftool_warning: jpeg_ext=jpeg_ext,
verbose_( replace_keywords=replace_keywords,
f"exiftool warning for file {warning_[0]}: {warning_[1]}"
) )
for error_ in export_results.exiftool_error: for warning_ in export_results.exiftool_warning:
verbose_(
f"exiftool warning for file {warning_[0]}: {warning_[1]}"
)
for error_ in export_results.exiftool_error:
click.echo(
click.style(
f"exiftool error for file {error_[0]}: {error_[1]}",
fg=CLI_COLOR_ERROR,
),
err=True,
)
for error_ in export_results.error:
click.echo(
click.style(
f"Error exporting photo ({photo.uuid}: {photo.original_filename}) as {error_[0]}: {error_[1]}",
fg=CLI_COLOR_ERROR,
),
err=True,
)
error += 1
if not error or tries > retry:
results += export_results
break
else:
click.echo(
"Retrying export for photo ({photo.uuid}: {photo.original_filename})"
)
except Exception as e:
click.echo( click.echo(
click.style( click.style(
f"exiftool error for file {error_[0]}: {error_[1]}", f"Error exporting photo ({photo.uuid}: {photo.original_filename}) as {original_filename}: {e}",
fg=CLI_COLOR_ERROR, fg=CLI_COLOR_ERROR,
), ),
err=True, err=True,
) )
for error_ in export_results.error: if tries > retry:
click.echo( results.error.append(
click.style( (str(pathlib.Path(dest) / original_filename), e)
f"Error exporting photo ({photo.uuid}: {photo.original_filename}) as {error_[0]}: {error_[1]}", )
fg=CLI_COLOR_ERROR, break
), else:
err=True, click.echo(
) f"Retrying export for photo ({photo.uuid}: {photo.original_filename})"
)
except Exception as e:
click.echo(
click.style(
f"Error exporting photo ({photo.uuid}: {photo.original_filename}) as {original_filename}: {e}",
fg=CLI_COLOR_ERROR,
),
err=True,
)
results.error.append(
(str(pathlib.Path(dest) / original_filename), e)
)
else: else:
verbose_(f"Skipping original version of {photo.original_filename}") verbose_(f"Skipping original version of {photo.original_filename}")
@@ -2619,70 +2796,87 @@ def export_photo(
) )
else: else:
try: tries = 0
export_results_edited = photo.export2( while tries <= retry:
dest_path, tries += 1
edited_filename, error = 0
sidecar=sidecar_flags, try:
sidecar_drop_ext=sidecar_drop_ext, export_results_edited = photo.export2(
export_as_hardlink=export_as_hardlink, dest_path,
overwrite=overwrite, edited_filename,
edited=True, sidecar=sidecar_flags,
use_photos_export=use_photos_export, sidecar_drop_ext=sidecar_drop_ext,
exiftool=exiftool, export_as_hardlink=export_as_hardlink,
merge_exif_keywords=exiftool_merge_keywords, overwrite=overwrite,
merge_exif_persons=exiftool_merge_persons, edited=True,
use_albums_as_keywords=album_keyword, use_photos_export=use_photos_export,
use_persons_as_keywords=person_keyword, exiftool=exiftool,
keyword_template=keyword_template, merge_exif_keywords=exiftool_merge_keywords,
description_template=description_template, merge_exif_persons=exiftool_merge_persons,
update=update, use_albums_as_keywords=album_keyword,
ignore_signature=ignore_signature, use_persons_as_keywords=person_keyword,
export_db=export_db, keyword_template=keyword_template,
fileutil=fileutil, description_template=description_template,
dry_run=dry_run, update=update,
touch_file=touch_file, ignore_signature=ignore_signature,
convert_to_jpeg=convert_to_jpeg, export_db=export_db,
jpeg_quality=jpeg_quality, fileutil=fileutil,
ignore_date_modified=ignore_date_modified, dry_run=dry_run,
use_photokit=use_photokit, touch_file=touch_file,
verbose=verbose_, convert_to_jpeg=convert_to_jpeg,
exiftool_flags=exiftool_option, jpeg_quality=jpeg_quality,
jpeg_ext=jpeg_ext, ignore_date_modified=ignore_date_modified,
replace_keywords=replace_keywords, use_photokit=use_photokit,
) verbose=verbose_,
results += export_results_edited exiftool_flags=exiftool_option,
for warning_ in export_results_edited.exiftool_warning: jpeg_ext=jpeg_ext,
verbose_( replace_keywords=replace_keywords,
f"exiftool warning for file {warning_[0]}: {warning_[1]}"
) )
for error_ in export_results_edited.exiftool_error: for warning_ in export_results_edited.exiftool_warning:
verbose_(
f"exiftool warning for file {warning_[0]}: {warning_[1]}"
)
for error_ in export_results_edited.exiftool_error:
click.echo(
click.style(
f"exiftool error for file {error_[0]}: {error_[1]}",
fg=CLI_COLOR_ERROR,
),
err=True,
)
for error_ in export_results_edited.error:
click.echo(
click.style(
f"Error exporting edited photo ({photo.uuid}: {photo.original_filename}) as {error_[0]}: {error_[1]}",
fg=CLI_COLOR_ERROR,
),
err=True,
)
error += 1
if not error or tries > retry:
results += export_results_edited
break
else:
click.echo(
"Retrying export for photo ({photo.uuid}: {photo.original_filename})"
)
except Exception as e:
click.echo( click.echo(
click.style( click.style(
f"exiftool error for file {error_[0]}: {error_[1]}", f"Error exporting edited photo ({photo.uuid}: {photo.original_filename}) {filename} as {edited_filename}: {e}",
fg=CLI_COLOR_ERROR, fg=CLI_COLOR_ERROR,
), ),
err=True, err=True,
) )
for error_ in export_results_edited.error: if tries > retry:
click.echo( results.error.append(
click.style( (str(pathlib.Path(dest) / edited_filename), e)
f"Error exporting edited photo ({photo.uuid}: {photo.original_filename}) as {error_[0]}: {error_[1]}", )
fg=CLI_COLOR_ERROR, break
), else:
err=True, click.echo(
) f"Retrying export for photo ({photo.uuid}: {photo.original_filename})"
except Exception as e: )
click.echo(
click.style(
f"Error exporting edited photo ({photo.uuid}: {photo.original_filename}) {filename} as {edited_filename}: {e}",
fg=CLI_COLOR_ERROR,
),
err=True,
)
results.error.append(
(str(pathlib.Path(dest) / edited_filename), e)
)
if verbose: if verbose:
if update: if update:
@@ -2893,8 +3087,12 @@ def write_export_report(report_file, results):
"exiftool_error": "", "exiftool_error": "",
"extended_attributes_written": 0, "extended_attributes_written": 0,
"extended_attributes_skipped": 0, "extended_attributes_skipped": 0,
"cleanup_deleted_file": 0,
"cleanup_deleted_directory": 0,
} }
for result in results.all_files() for result in results.all_files()
+ results.deleted_files
+ results.deleted_directories
} }
for result in results.exported: for result in results.exported:
@@ -2960,6 +3158,12 @@ def write_export_report(report_file, results):
for result in results.xattr_skipped: for result in results.xattr_skipped:
all_results[result]["extended_attributes_skipped"] = 1 all_results[result]["extended_attributes_skipped"] = 1
for result in results.deleted_files:
all_results[result]["cleanup_deleted_file"] = 1
for result in results.deleted_directories:
all_results[result]["cleanup_deleted_directory"] = 1
report_columns = [ report_columns = [
"filename", "filename",
"exported", "exported",
@@ -2978,6 +3182,8 @@ def write_export_report(report_file, results):
"exiftool_error", "exiftool_error",
"extended_attributes_written", "extended_attributes_written",
"extended_attributes_skipped", "extended_attributes_skipped",
"cleanup_deleted_file",
"cleanup_deleted_directory",
] ]
try: try:
@@ -3004,27 +3210,27 @@ def cleanup_files(dest_path, files_to_keep, fileutil):
fileutile: FileUtil object fileutile: FileUtil object
Returns: Returns:
tuple of (number of files deleted, number of directories deleted) tuple of (list of files deleted, list of directories deleted)
""" """
keepers = {str(filename).lower(): 1 for filename in files_to_keep} keepers = {str(filename).lower(): 1 for filename in files_to_keep}
deleted_files = 0 deleted_files = []
for p in pathlib.Path(dest_path).rglob("*"): for p in pathlib.Path(dest_path).rglob("*"):
path = str(p).lower() path = str(p).lower()
if p.is_file() and path not in keepers: if p.is_file() and path not in keepers:
verbose_(f"Deleting {p}") verbose_(f"Deleting {p}")
fileutil.unlink(p) fileutil.unlink(p)
deleted_files += 1 deleted_files.append(str(p))
# delete empty directories # delete empty directories
deleted_dirs = 0 deleted_dirs = []
for p in pathlib.Path(dest_path).rglob("*"): for p in pathlib.Path(dest_path).rglob("*"):
path = str(p).lower() path = str(p).lower()
# if directory and directory is empty # if directory and directory is empty
if p.is_dir() and not next(p.iterdir(), False): if p.is_dir() and not next(p.iterdir(), False):
verbose_(f"Deleting empty directory {p}") verbose_(f"Deleting empty directory {p}")
fileutil.rmdir(p) fileutil.rmdir(p)
deleted_dirs += 1 deleted_dirs.append(str(p))
return (deleted_files, deleted_dirs) return (deleted_files, deleted_dirs)
@@ -3101,7 +3307,7 @@ def write_finder_tags(
# filter out any template values that didn't match by looking for sentinel # filter out any template values that didn't match by looking for sentinel
rendered_tags = [ rendered_tags = [
tag for tag in rendered_tags if _OSXPHOTOS_NONE_SENTINEL not in tag value.replace(_OSXPHOTOS_NONE_SENTINEL, "") for value in rendered_tags
] ]
tags.extend(rendered_tags) tags.extend(rendered_tags)
@@ -3152,10 +3358,10 @@ def write_extended_attributes(photo, files, xattr_template, strip=False):
), ),
err=True, err=True,
) )
# filter out any template values that didn't match by looking for sentinel # filter out any template values that didn't match by looking for sentinel
rendered = [ rendered = [value.replace(_OSXPHOTOS_NONE_SENTINEL, "") for value in rendered]
value for value in rendered if _OSXPHOTOS_NONE_SENTINEL not in value
]
try: try:
attributes[xattr].extend(rendered) attributes[xattr].extend(rendered)
except KeyError: except KeyError:

View File

@@ -87,6 +87,8 @@ class ExportResults:
exiftool_error=None, exiftool_error=None,
xattr_written=None, xattr_written=None,
xattr_skipped=None, xattr_skipped=None,
deleted_files=None,
deleted_directories=None,
): ):
self.exported = exported or [] self.exported = exported or []
self.new = new or [] self.new = new or []
@@ -107,6 +109,8 @@ class ExportResults:
self.exiftool_error = exiftool_error or [] self.exiftool_error = exiftool_error or []
self.xattr_written = xattr_written or [] self.xattr_written = xattr_written or []
self.xattr_skipped = xattr_skipped or [] self.xattr_skipped = xattr_skipped or []
self.deleted_files = deleted_files or []
self.deleted_directories = deleted_directories or []
def all_files(self): def all_files(self):
""" return all filenames contained in results """ """ return all filenames contained in results """
@@ -151,6 +155,8 @@ class ExportResults:
self.error += other.error self.error += other.error
self.exiftool_warning += other.exiftool_warning self.exiftool_warning += other.exiftool_warning
self.exiftool_error += other.exiftool_error self.exiftool_error += other.exiftool_error
self.deleted_files += other.deleted_files
self.deleted_directories += other.deleted_directories
return self return self
def __str__(self): def __str__(self):
@@ -173,6 +179,8 @@ class ExportResults:
+ f",error={self.error}" + f",error={self.error}"
+ f",exiftool_warning={self.exiftool_warning}" + f",exiftool_warning={self.exiftool_warning}"
+ f",exiftool_error={self.exiftool_error}" + f",exiftool_error={self.exiftool_error}"
+ f",deleted_files={self.deleted_files}"
+ f",deleted_directories={self.deleted_directories}"
+ ")" + ")"
) )
@@ -613,9 +621,9 @@ def export2(
) )
edited_name = pathlib.Path(self.path_edited).name edited_name = pathlib.Path(self.path_edited).name
edited_suffix = pathlib.Path(edited_name).suffix edited_suffix = pathlib.Path(edited_name).suffix
fname = pathlib.Path(self.filename).stem + edited_identifier + edited_suffix fname = pathlib.Path(self.original_filename).stem + edited_identifier + edited_suffix
else: else:
fname = self.filename fname = self.original_filename
uti = self.uti if edited else self.uti_original uti = self.uti if edited else self.uti_original
if convert_to_jpeg and self.isphoto and uti != "public.jpeg": if convert_to_jpeg and self.isphoto and uti != "public.jpeg":
@@ -1652,7 +1660,7 @@ def _exiftool_dict(
if type(val) == str: if type(val) == str:
exif[field] = val.replace("\n", " ") exif[field] = val.replace("\n", " ")
elif type(val) == list: 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 return exif

View File

@@ -453,9 +453,24 @@ class PhotoInfo:
) )
return self._albums return self._albums
@property
def burst_albums(self):
"""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"""
if self.burst_selected or not self.burst:
return self.albums
try:
return self._burst_albums
except AttributeError:
burst_albums = []
for photo in self.burst_photos:
burst_albums.extend(photo.albums)
self._burst_albums = list(set(burst_albums))
return self._burst_albums
@property @property
def album_info(self): def album_info(self):
""" list of AlbumInfo objects representing albums the photos is contained in """ """ list of AlbumInfo objects representing albums the photo is contained in """
try: try:
return self._album_info return self._album_info
except AttributeError: except AttributeError:
@@ -465,6 +480,21 @@ class PhotoInfo:
] ]
return self._album_info return self._album_info
@property
def burst_album_info(self):
""" 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 """
if self.burst_selected or not self.burst:
return self.album_info
try:
return self._burst_album_info
except AttributeError:
burst_album_info = []
for photo in self.burst_photos:
burst_album_info.extend(photo.album_info)
self._burst_album_info = list(set(burst_album_info))
return self._burst_album_info
@property @property
def import_info(self): def import_info(self):
""" ImportInfo object representing import session for the photo or None if no import session """ """ ImportInfo object representing import session for the photo or None if no import session """
@@ -680,6 +710,11 @@ class PhotoInfo:
""" Returns True if photo is part of a Burst photo set, otherwise False """ """ Returns True if photo is part of a Burst photo set, otherwise False """
return self._info["burst"] return self._info["burst"]
@property
def burst_selected(self):
""" Returns True if photo is a burst photo and has been selected from the burst set by the user, otherwise False """
return self._info["burst_key"]
@property @property
def burst_photos(self): def burst_photos(self):
"""If photo is a burst photo, returns list of PhotoInfo objects """If photo is a burst photo, returns list of PhotoInfo objects

View File

@@ -1828,7 +1828,6 @@ class PhotosDB:
# get details about photos # get details about photos
verbose("Processing photo details.") verbose("Processing photo details.")
logging.debug(f"Getting information about photos")
c.execute( c.execute(
f"""SELECT {asset_table}.ZUUID, f"""SELECT {asset_table}.ZUUID,
ZADDITIONALASSETATTRIBUTES.ZMASTERFINGERPRINT, ZADDITIONALASSETATTRIBUTES.ZMASTERFINGERPRINT,
@@ -2736,8 +2735,6 @@ class PhotosDB:
# an empty album will be in _dbalbum_titles but not _dbalbums_album # an empty album will be in _dbalbum_titles but not _dbalbums_album
pass pass
album_set.update(title_set) album_set.update(title_set)
else:
logging.debug(f"Could not find album '{album}' in database")
photos_sets.append(album_set) photos_sets.append(album_set)
if uuid: if uuid:
@@ -2745,8 +2742,6 @@ class PhotosDB:
for u in uuid: for u in uuid:
if u in self._dbphotos: if u in self._dbphotos:
uuid_set.update([u]) uuid_set.update([u])
else:
logging.debug(f"Could not find uuid '{u}' in database")
photos_sets.append(uuid_set) photos_sets.append(uuid_set)
if keywords: if keywords:
@@ -2754,8 +2749,6 @@ class PhotosDB:
for keyword in keywords: for keyword in keywords:
if keyword in self._dbkeywords_keyword: if keyword in self._dbkeywords_keyword:
keyword_set.update(self._dbkeywords_keyword[keyword]) keyword_set.update(self._dbkeywords_keyword[keyword])
else:
logging.debug(f"Could not find keyword '{keyword}' in database")
photos_sets.append(keyword_set) photos_sets.append(keyword_set)
if persons: if persons:
@@ -2768,8 +2761,6 @@ class PhotosDB:
except KeyError: except KeyError:
# some persons have zero photos so they won't be in _dbfaces_pk # some persons have zero photos so they won't be in _dbfaces_pk
pass pass
else:
logging.debug(f"Could not find person '{person}' in database")
photos_sets.append(person_set) photos_sets.append(person_set)
if from_date or to_date: # sourcery off if from_date or to_date: # sourcery off
@@ -2780,14 +2771,10 @@ class PhotosDB:
dsel = { dsel = {
k: v for k, v in dsel.items() if v["imageDate"] >= from_date 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 to_date:
if not datetime_has_tz(to_date): if not datetime_has_tz(to_date):
to_date = datetime_naive_to_local(to_date) to_date = datetime_naive_to_local(to_date)
dsel = {k: v for k, v in dsel.items() if v["imageDate"] <= 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())) photos_sets.append(set(dsel.keys()))
photoinfo = [] photoinfo = []

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: 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. 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 `["Folder1>Album1"]`
- `"{folder_album()}"` renders to `["Folder1Album1"]` - `"{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, 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 .datetime_formatter import DateTimeFormatter
from .exiftool import ExifTool from .exiftool import ExifTool
from .path_utils import sanitize_dirname, sanitize_filename, sanitize_pathpart from .path_utils import sanitize_dirname, sanitize_filename, sanitize_pathpart
from .utils import load_function
# ensure locale set to user's locale # ensure locale set to user's locale
locale.setlocale(locale.LC_ALL, "") 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}'", "{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}'", "{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}'", "{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.date}": "Photo's creation date in ISO format, e.g. '2020-03-22'",
"{created.year}": "4-digit year of photo creation time", "{created.year}": "4-digit year of photo creation time",
"{created.yy}": "2-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'", "{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: ','", "{comma}": "A comma: ','",
"{semicolon}": "A semicolon: ';'", "{semicolon}": "A semicolon: ';'",
"{questionmark}": "A question mark: '?'",
"{pipe}": "A vertical pipe: '|'", "{pipe}": "A vertical pipe: '|'",
"{openbrace}": "An open brace: '{'", "{openbrace}": "An open brace: '{'",
"{closebrace}": "A close brace: '}'", "{closebrace}": "A close brace: '}'",
@@ -145,6 +148,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.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}": "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).", "{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 = { FILTER_VALUES = {
@@ -185,6 +197,7 @@ PUNCTUATION = {
"closeparens": ")", "closeparens": ")",
"openbracket": "[", "openbracket": "[",
"closebracket": "]", "closebracket": "]",
"questionmark": "?",
} }
@@ -317,17 +330,6 @@ class PhotoTemplate:
unmatched=unmatched, 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 rendered_strings = results
if filename: if filename:
@@ -362,7 +364,7 @@ class PhotoTemplate:
if ts.template: if ts.template:
# have a template field to process # have a template field to process
field = ts.template.field field = ts.template.field
if field not in FIELD_NAMES: if field not in FIELD_NAMES and not field.startswith("photo"):
unmatched.append(field) unmatched.append(field)
return [], unmatched return [], unmatched
@@ -424,6 +426,30 @@ class PhotoTemplate:
else: else:
default = [] 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 = [] vals = []
if field in SINGLE_VALUE_SUBSTITUTIONS: if field in SINGLE_VALUE_SUBSTITUTIONS:
vals = self.get_template_value( vals = self.get_template_value(
@@ -442,7 +468,15 @@ class PhotoTemplate:
vals = self.get_template_value_exiftool( vals = self.get_template_value_exiftool(
subfield, filename=filename, dirname=dirname 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( vals = self.get_template_value_multi(
field, path_sep=path_sep, filename=filename, dirname=dirname field, path_sep=path_sep, filename=filename, dirname=dirname
) )
@@ -452,14 +486,6 @@ class PhotoTemplate:
vals = [val for val in vals if val is not None] 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: if expand_inplace or delim is not None:
sep = delim if delim is not None else inplace_sep sep = delim if delim is not None else inplace_sep
vals = [sep.join(sorted(vals))] vals = [sep.join(sorted(vals))]
@@ -467,6 +493,99 @@ class PhotoTemplate:
for filter_ in filters: for filter_ in filters:
vals = self.get_template_value_filter(filter_, vals) 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 "" pre = ts.pre or ""
post = ts.post or "" post = ts.post or ""
@@ -539,6 +658,8 @@ class PhotoTemplate:
value = "hdr" if self.photo.hdr else None value = "hdr" if self.photo.hdr else None
elif field == "edited": elif field == "edited":
value = "edited" if self.photo.hasadjustments else None value = "edited" if self.photo.hasadjustments else None
elif field == "favorite":
value = "favorite" if self.photo.favorite else None
elif field == "created.date": elif field == "created.date":
value = DateTimeFormatter(self.photo.date).date value = DateTimeFormatter(self.photo.date).date
elif field == "created.year": elif field == "created.year":
@@ -840,7 +961,7 @@ class PhotoTemplate:
""" return list of values for a multi-valued template field """ """ return list of values for a multi-valued template field """
values = [] values = []
if field == "album": if field == "album":
values = self.photo.albums values = self.photo.burst_albums if self.photo.burst else self.photo.albums
elif field == "keyword": elif field == "keyword":
values = self.photo.keywords values = self.photo.keywords
elif field == "person": elif field == "person":
@@ -854,7 +975,11 @@ class PhotoTemplate:
elif field == "folder_album": elif field == "folder_album":
values = [] values = []
# photos must be in an album to be in a folder # photos must be in an album to be in a folder
for album in self.photo.album_info: if self.photo.burst:
album_info = self.photo.burst_album_info
else:
album_info = self.photo.album_info
for album in album_info:
if album.folder_names: if album.folder_names:
# album in folder # album in folder
if dirname: if dirname:
@@ -887,6 +1012,32 @@ class PhotoTemplate:
values = ( values = (
self.photo.search_info.venue_types if self.photo.search_info else [] 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: else:
raise ValueError(f"Unhandled template value: {field}") raise ValueError(f"Unhandled template value: {field}")
@@ -914,6 +1065,7 @@ class PhotoTemplate:
if subfield in exifdict: if subfield in exifdict:
values = exifdict[subfield] values = exifdict[subfield]
values = [values] if not isinstance(values, list) else values values = [values] if not isinstance(values, list) else values
values = [str(v) for v in values]
# sanitize directory names if needed # sanitize directory names if needed
if filename: if filename:
@@ -925,6 +1077,39 @@ class PhotoTemplate:
return values 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): def get_photo_video_type(self, default):
""" return media type, e.g. photo or video """ """ return media type, e.g. photo or video """
default_dict = parse_default_kv(default, PHOTO_VIDEO_TYPE_DEFAULTS) default_dict = parse_default_kv(default, PHOTO_VIDEO_TYPE_DEFAULTS)

View File

@@ -1,6 +1,6 @@
// OSXPhotos Template Language (OTL) // OSXPhotos Template Language (OTL)
// a TemplateString has format: // 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 // a TemplateStatement may contain zero or more TemplateStrings
// The pre and post are optional strings // The pre and post are optional strings
// The template itself (inside the {}) is also optional but if present // The template itself (inside the {}) is also optional but if present
@@ -25,6 +25,7 @@ Template:
filter=Filter filter=Filter
pathsep=PathSep pathsep=PathSep
findreplace=FindReplace findreplace=FindReplace
conditional=Conditional
bool=Boolean bool=Boolean
default=Default default=Default
"}" "}"
@@ -32,7 +33,7 @@ Template:
; ;
NON_TEMPLATE_STRING: NON_TEMPLATE_STRING:
/[^\{\},]*/ /[^\{\},\?]*/
; ;
Delim: Delim:
@@ -50,6 +51,10 @@ Field:
FIELD_WORD+ FIELD_WORD+
; ;
FIELD_WORD:
/[\.\w]+/
;
SubField: SubField:
( (
":"- ":"-
@@ -57,12 +62,8 @@ SubField:
)? )?
; ;
FIELD_WORD:
/[\.\w]+/
;
SUBFIELD_WORD: SUBFIELD_WORD:
/[\.\w:]+/ /[\.\w:\/]+/
; ;
Filter: Filter:
@@ -76,6 +77,24 @@ FILTER_WORD:
/[\.\w]+/ /[\.\w]+/
; ;
Conditional:
(
(" "+)-
(negation=NEGATION)?
(operator=OPERATOR)
(" "+)-
(value=Statement)
)?
;
NEGATION:
"not "
;
OPERATOR:
"contains" | "matches" | "startswith" | "endswith" | "<=" | ">=" | "<" | ">" | "==" | "!="
;
PathSep: PathSep:
( (
"(" "("

View File

@@ -1,5 +1,8 @@
""" Utility functions used in osxphotos """
import fnmatch import fnmatch
import glob import glob
import importlib
import inspect import inspect
import logging import logging
import os import os
@@ -13,6 +16,7 @@ import sys
import unicodedata import unicodedata
import urllib.parse import urllib.parse
from plistlib import load as plistload from plistlib import load as plistload
from typing import Callable
import CoreFoundation import CoreFoundation
import CoreServices import CoreServices
@@ -401,3 +405,28 @@ def increment_filename(filepath):
count += 1 count += 1
dest = dest.parent / f"{dest_new}{dest.suffix}" dest = dest.parent / f"{dest_new}{dest.suffix}"
return str(dest) 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

@@ -49,7 +49,7 @@ pathvalidate==2.2.1
pexpect==4.8.0 pexpect==4.8.0
photoscript==0.1.0 photoscript==0.1.0
pickleshare==0.7.5 pickleshare==0.7.5
Pillow==7.2.0 Pillow==8.1.1
pkginfo==1.5.0.1 pkginfo==1.5.0.1
pluggy==0.12.0 pluggy==0.12.0
prompt-toolkit==3.0.4 prompt-toolkit==3.0.4
@@ -59,7 +59,7 @@ py==1.8.0
py2app==0.21 py2app==0.21
pycparser==2.20 pycparser==2.20
pyfiglet==0.8.post1 pyfiglet==0.8.post1
Pygments==2.6.1 Pygments==2.7.4
PyInstaller==3.6 PyInstaller==3.6
pyinstaller-setuptools==2019.3 pyinstaller-setuptools==2019.3
pylint==2.3.1 pylint==2.3.1
@@ -181,7 +181,7 @@ pyobjc-framework-Vision==6.2.2
pyobjc-framework-WebKit==6.2.2 pyobjc-framework-WebKit==6.2.2
pyparsing==2.4.1.1 pyparsing==2.4.1.1
python-dateutil==2.8.1 python-dateutil==2.8.1
PyYAML==5.1.2 PyYAML==5.4
pyzmq==18.1.1 pyzmq==18.1.1
readme-renderer==25.0 readme-renderer==25.0
regex==2020.2.20 regex==2020.2.20

View File

@@ -23,6 +23,14 @@ class PhotoInfoMock(PhotoInfo):
else self._photo.hdr 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 @property
def hasadjustments(self): def hasadjustments(self):
return ( 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

@@ -731,7 +731,7 @@ def test_export_1():
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]]) photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename filename = photos[0].original_filename
expected_dest = os.path.join(dest, filename) expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest)[0] got_dest = photos[0].export(dest)[0]
@@ -776,7 +776,7 @@ def test_export_3():
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]]) photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename filename = photos[0].original_filename
filename2 = pathlib.Path(filename) filename2 = pathlib.Path(filename)
filename2 = f"{filename2.stem} (1){filename2.suffix}" filename2 = f"{filename2.stem} (1){filename2.suffix}"
expected_dest_2 = os.path.join(dest, filename2) expected_dest_2 = os.path.join(dest, filename2)
@@ -829,7 +829,7 @@ def test_export_5():
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]]) photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename filename = photos[0].original_filename
expected_dest = os.path.join(dest, filename) expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest)[0] got_dest = photos[0].export(dest)[0]
@@ -994,7 +994,7 @@ def test_export_12():
edited_name = pathlib.Path(photos[0].path_edited).name edited_name = pathlib.Path(photos[0].path_edited).name
edited_suffix = pathlib.Path(edited_name).suffix edited_suffix = pathlib.Path(edited_name).suffix
filename = pathlib.Path(photos[0].filename).stem + "_edited" + edited_suffix filename = pathlib.Path(photos[0].original_filename).stem + "_edited" + edited_suffix
expected_dest = os.path.join(dest, filename) expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest, edited=True)[0] got_dest = photos[0].export(dest, edited=True)[0]

View File

@@ -618,7 +618,7 @@ def test_export_1(photosdb):
dest = tempdir.name dest = tempdir.name
photos = photosdb.photos(uuid=[UUID_DICT["export"]]) photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename filename = photos[0].original_filename
expected_dest = os.path.join(dest, filename) expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest)[0] got_dest = photos[0].export(dest)[0]
@@ -657,7 +657,7 @@ def test_export_3(photosdb):
dest = tempdir.name dest = tempdir.name
photos = photosdb.photos(uuid=[UUID_DICT["export"]]) photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename filename = photos[0].original_filename
filename2 = pathlib.Path(filename) filename2 = pathlib.Path(filename)
filename2 = f"{filename2.stem} (1){filename2.suffix}" filename2 = f"{filename2.stem} (1){filename2.suffix}"
expected_dest_2 = os.path.join(dest, filename2) expected_dest_2 = os.path.join(dest, filename2)
@@ -704,7 +704,7 @@ def test_export_5(photosdb):
dest = tempdir.name dest = tempdir.name
photos = photosdb.photos(uuid=[UUID_DICT["export"]]) photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename filename = photos[0].original_filename
expected_dest = os.path.join(dest, filename) expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest)[0] got_dest = photos[0].export(dest)[0]
@@ -848,7 +848,7 @@ def test_export_12(photosdb):
edited_name = pathlib.Path(photos[0].path_edited).name edited_name = pathlib.Path(photos[0].path_edited).name
edited_suffix = pathlib.Path(edited_name).suffix edited_suffix = pathlib.Path(edited_name).suffix
filename = pathlib.Path(photos[0].filename).stem + "_edited" + edited_suffix filename = pathlib.Path(photos[0].original_filename).stem + "_edited" + edited_suffix
expected_dest = os.path.join(dest, filename) expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest, edited=True)[0] got_dest = photos[0].export(dest, edited=True)[0]

View File

@@ -418,7 +418,7 @@ def test_export_1():
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]]) photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename filename = photos[0].original_filename
expected_dest = os.path.join(dest, filename) expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest)[0] got_dest = photos[0].export(dest)[0]
@@ -463,7 +463,7 @@ def test_export_3():
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]]) photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename filename = photos[0].original_filename
filename2 = pathlib.Path(filename) filename2 = pathlib.Path(filename)
filename2 = f"{filename2.stem} (1){filename2.suffix}" filename2 = f"{filename2.stem} (1){filename2.suffix}"
expected_dest_2 = os.path.join(dest, filename2) expected_dest_2 = os.path.join(dest, filename2)
@@ -516,7 +516,7 @@ def test_export_5():
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]]) photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename filename = photos[0].original_filename
expected_dest = os.path.join(dest, filename) expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest)[0] got_dest = photos[0].export(dest)[0]
@@ -681,7 +681,7 @@ def test_export_12():
edited_name = pathlib.Path(photos[0].path_edited).name edited_name = pathlib.Path(photos[0].path_edited).name
edited_suffix = pathlib.Path(edited_name).suffix edited_suffix = pathlib.Path(edited_name).suffix
filename = pathlib.Path(photos[0].filename).stem + "_edited" + edited_suffix filename = pathlib.Path(photos[0].original_filename).stem + "_edited" + edited_suffix
expected_dest = os.path.join(dest, filename) expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest, edited=True)[0] got_dest = photos[0].export(dest, edited=True)[0]

View File

@@ -426,7 +426,7 @@ def test_export_1():
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]]) photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename filename = photos[0].original_filename
expected_dest = os.path.join(dest, filename) expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest)[0] got_dest = photos[0].export(dest)[0]
@@ -471,7 +471,7 @@ def test_export_3():
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]]) photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename filename = photos[0].original_filename
filename2 = pathlib.Path(filename) filename2 = pathlib.Path(filename)
filename2 = f"{filename2.stem} (1){filename2.suffix}" filename2 = f"{filename2.stem} (1){filename2.suffix}"
expected_dest_2 = os.path.join(dest, filename2) expected_dest_2 = os.path.join(dest, filename2)
@@ -524,7 +524,7 @@ def test_export_5():
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]]) photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename filename = photos[0].original_filename
expected_dest = os.path.join(dest, filename) expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest)[0] got_dest = photos[0].export(dest)[0]
@@ -689,7 +689,7 @@ def test_export_12():
edited_name = pathlib.Path(photos[0].path_edited).name edited_name = pathlib.Path(photos[0].path_edited).name
edited_suffix = pathlib.Path(edited_name).suffix edited_suffix = pathlib.Path(edited_name).suffix
filename = pathlib.Path(photos[0].filename).stem + "_edited" + edited_suffix filename = pathlib.Path(photos[0].original_filename).stem + "_edited" + edited_suffix
expected_dest = os.path.join(dest, filename) expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest, edited=True)[0] got_dest = photos[0].export(dest, edited=True)[0]

View File

@@ -640,7 +640,7 @@ def test_export_1():
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]]) photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename filename = photos[0].original_filename
expected_dest = os.path.join(dest, filename) expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest)[0] got_dest = photos[0].export(dest)[0]
@@ -685,7 +685,7 @@ def test_export_3():
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]]) photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename filename = photos[0].original_filename
filename2 = pathlib.Path(filename) filename2 = pathlib.Path(filename)
filename2 = f"{filename2.stem} (1){filename2.suffix}" filename2 = f"{filename2.stem} (1){filename2.suffix}"
expected_dest_2 = os.path.join(dest, filename2) expected_dest_2 = os.path.join(dest, filename2)
@@ -738,7 +738,7 @@ def test_export_5():
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["export"]]) photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename filename = photos[0].original_filename
expected_dest = os.path.join(dest, filename) expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest)[0] got_dest = photos[0].export(dest)[0]
@@ -903,7 +903,7 @@ def test_export_12():
edited_name = pathlib.Path(photos[0].path_edited).name edited_name = pathlib.Path(photos[0].path_edited).name
edited_suffix = pathlib.Path(edited_name).suffix edited_suffix = pathlib.Path(edited_name).suffix
filename = pathlib.Path(photos[0].filename).stem + "_edited" + edited_suffix filename = pathlib.Path(photos[0].original_filename).stem + "_edited" + edited_suffix
expected_dest = os.path.join(dest, filename) expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest, edited=True)[0] got_dest = photos[0].export(dest, edited=True)[0]

View File

@@ -635,7 +635,7 @@ def test_export_1(photosdb):
dest = tempdir.name dest = tempdir.name
photos = photosdb.photos(uuid=[UUID_DICT["export"]]) photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename filename = photos[0].original_filename
expected_dest = os.path.join(dest, filename) expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest)[0] got_dest = photos[0].export(dest)[0]
@@ -666,7 +666,7 @@ def test_export_3(photosdb):
dest = tempdir.name dest = tempdir.name
photos = photosdb.photos(uuid=[UUID_DICT["export"]]) photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename filename = photos[0].original_filename
filename2 = pathlib.Path(filename) filename2 = pathlib.Path(filename)
filename2 = f"{filename2.stem} (1){filename2.suffix}" filename2 = f"{filename2.stem} (1){filename2.suffix}"
expected_dest_2 = os.path.join(dest, filename2) expected_dest_2 = os.path.join(dest, filename2)
@@ -705,7 +705,7 @@ def test_export_5(photosdb):
dest = tempdir.name dest = tempdir.name
photos = photosdb.photos(uuid=[UUID_DICT["export"]]) photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename filename = photos[0].original_filename
expected_dest = os.path.join(dest, filename) expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest)[0] got_dest = photos[0].export(dest)[0]
@@ -823,7 +823,7 @@ def test_export_12(photosdb):
edited_name = pathlib.Path(photos[0].path_edited).name edited_name = pathlib.Path(photos[0].path_edited).name
edited_suffix = pathlib.Path(edited_name).suffix edited_suffix = pathlib.Path(edited_name).suffix
filename = pathlib.Path(photos[0].filename).stem + "_edited" + edited_suffix filename = pathlib.Path(photos[0].original_filename).stem + "_edited" + edited_suffix
expected_dest = os.path.join(dest, filename) expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest, edited=True)[0] got_dest = photos[0].export(dest, edited=True)[0]

View File

@@ -702,7 +702,7 @@ def test_export_1(photosdb):
dest = tempdir.name dest = tempdir.name
photos = photosdb.photos(uuid=[UUID_DICT["export"]]) photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename filename = photos[0].original_filename
expected_dest = os.path.join(dest, filename) expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest)[0] got_dest = photos[0].export(dest)[0]
@@ -733,7 +733,7 @@ def test_export_3(photosdb):
dest = tempdir.name dest = tempdir.name
photos = photosdb.photos(uuid=[UUID_DICT["export"]]) photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename filename = photos[0].original_filename
filename2 = pathlib.Path(filename) filename2 = pathlib.Path(filename)
filename2 = f"{filename2.stem} (1){filename2.suffix}" filename2 = f"{filename2.stem} (1){filename2.suffix}"
expected_dest_2 = os.path.join(dest, filename2) expected_dest_2 = os.path.join(dest, filename2)
@@ -772,7 +772,7 @@ def test_export_5(photosdb):
dest = tempdir.name dest = tempdir.name
photos = photosdb.photos(uuid=[UUID_DICT["export"]]) photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename filename = photos[0].original_filename
expected_dest = os.path.join(dest, filename) expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest)[0] got_dest = photos[0].export(dest)[0]
@@ -890,7 +890,7 @@ def test_export_12(photosdb):
edited_name = pathlib.Path(photos[0].path_edited).name edited_name = pathlib.Path(photos[0].path_edited).name
edited_suffix = pathlib.Path(edited_name).suffix edited_suffix = pathlib.Path(edited_name).suffix
filename = pathlib.Path(photos[0].filename).stem + "_edited" + edited_suffix filename = pathlib.Path(photos[0].original_filename).stem + "_edited" + edited_suffix
expected_dest = os.path.join(dest, filename) expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest, edited=True)[0] got_dest = photos[0].export(dest, edited=True)[0]

View File

@@ -22,6 +22,17 @@ PHOTOS_DB_TOUCH = PHOTOS_DB_15_6
PHOTOS_DB_14_6 = "tests/Test-10.14.6.photoslibrary" PHOTOS_DB_14_6 = "tests/Test-10.14.6.photoslibrary"
PHOTOS_DB_MOVIES = "tests/Test-Movie-5_0.photoslibrary" PHOTOS_DB_MOVIES = "tests/Test-Movie-5_0.photoslibrary"
# my personal library which some tests require
PHOTOS_DB_RHET = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
UUID_BURST_ALBUM = "9F90DC00-AAAF-4A05-9A65-61FEEE0D67F2" # in my personal library
BURST_ALBUM_FILES = [
"IMG_9812.JPG",
"IMG_9813.JPG",
"IMG_9814.JPG",
"IMG_9815.JPG",
"IMG_9816.JPG",
]
UUID_FILE = "tests/uuid_from_file.txt" UUID_FILE = "tests/uuid_from_file.txt"
CLI_OUTPUT_NO_SUBCOMMAND = [ CLI_OUTPUT_NO_SUBCOMMAND = [
@@ -451,7 +462,17 @@ CLI_FINDER_TAGS = {
"XMP:Description": "Girl holding pumpkin", "XMP:Description": "Girl holding pumpkin",
"XMP:PersonInImage": "Katie", "XMP:PersonInImage": "Katie",
"XMP:Subject": "Kids", "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 = { LABELS_JSON = {
@@ -1846,6 +1867,36 @@ def test_query_date_timezone():
assert len(json_got) == 4 assert len(json_got) == 4
def test_query_time():
""" Test --from-time, --to-time"""
import json
import osxphotos
import os
import os.path
import time
from osxphotos.cli import query
os.environ["TZ"] = "US/Pacific"
time.tzset()
runner = CliRunner()
cwd = os.getcwd()
result = runner.invoke(
query,
[
"--json",
"--db",
os.path.join(cwd, CLI_PHOTOS_DB),
"--from-time=16:00",
"--to-time=17:00",
],
)
assert result.exit_code == 0
json_got = json.loads(result.output)
assert len(json_got) == 3
def test_query_keyword_1(): def test_query_keyword_1():
"""Test query --keyword """ """Test query --keyword """
import json import json
@@ -5289,7 +5340,7 @@ def test_export_finder_tag_template_multiple():
keywords = [keywords] if type(keywords) != list else keywords keywords = [keywords] if type(keywords) != list else keywords
persons = CLI_FINDER_TAGS[uuid]["XMP:PersonInImage"] persons = CLI_FINDER_TAGS[uuid]["XMP:PersonInImage"]
persons = [persons] if type(persons) != list else persons 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) assert sorted(md.tags) == sorted(expected)
@@ -5327,7 +5378,42 @@ def test_export_finder_tag_template_keywords():
keywords = [keywords] if type(keywords) != list else keywords keywords = [keywords] if type(keywords) != list else keywords
persons = CLI_FINDER_TAGS[uuid]["XMP:PersonInImage"] persons = CLI_FINDER_TAGS[uuid]["XMP:PersonInImage"]
persons = [persons] if type(persons) != list else persons 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) assert sorted(md.tags) == sorted(expected)
@@ -5356,7 +5442,7 @@ def test_export_xattr_template():
"{person}", "{person}",
"--xattr-template", "--xattr-template",
"comment", "comment",
"{title}", "{title};{descr}",
"--uuid", "--uuid",
f"{uuid}", f"{uuid}",
], ],
@@ -5367,7 +5453,9 @@ def test_export_xattr_template():
expected = CLI_FINDER_TAGS[uuid]["XMP:PersonInImage"] expected = CLI_FINDER_TAGS[uuid]["XMP:PersonInImage"]
expected = [expected] if type(expected) != list else expected expected = [expected] if type(expected) != list else expected
assert sorted(md.keywords) == sorted(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 # run again with --update, should skip writing extended attributes
result = runner.invoke( result = runner.invoke(
@@ -5381,7 +5469,7 @@ def test_export_xattr_template():
"{person}", "{person}",
"--xattr-template", "--xattr-template",
"comment", "comment",
"{title}", "{title};{descr}",
"--uuid", "--uuid",
f"{uuid}", f"{uuid}",
"--update", "--update",
@@ -5619,3 +5707,157 @@ def test_export_jpeg_ext_convert_to_jpeg_movie():
assert f"{filename}.jpg".lower() not in files assert f"{filename}.jpg".lower() not in files
assert f"{filename}.{ext}".lower() in files assert f"{filename}.{ext}".lower() in files
assert f"{filename}_edited.{ext}".lower() in files assert f"{filename}_edited.{ext}".lower() in files
@pytest.mark.skipif(
"OSXPHOTOS_TEST_EXPORT" not in os.environ,
reason="Skip if not running on author's personal library.",
)
def test_export_burst_folder_album():
""" test non-selected burst photos are exported with the album their key photo is in, issue #401 """
import glob
import os
import os.path
import pathlib
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_RHET),
".",
"-V",
"--directory",
"{folder_album}",
"--uuid",
UUID_BURST_ALBUM,
"--download-missing",
"--use-photokit",
],
)
assert result.exit_code == 0
folder_album = pathlib.Path("TestBurst")
assert folder_album.is_dir()
for filename in BURST_ALBUM_FILES:
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

View File

@@ -95,7 +95,7 @@ def test_export_1(photosdb):
dest = tempdir.name dest = tempdir.name
photos = photosdb.photos(uuid=[UUID_DICT["export"]]) photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename filename = photos[0].original_filename
expected_dest = os.path.join(dest, filename) expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest)[0] got_dest = photos[0].export(dest)[0]
@@ -134,7 +134,7 @@ def test_export_3(photosdb):
dest = tempdir.name dest = tempdir.name
photos = photosdb.photos(uuid=[UUID_DICT["export"]]) photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename filename = photos[0].original_filename
filename2 = pathlib.Path(filename) filename2 = pathlib.Path(filename)
filename2 = f"{filename2.stem} (1){filename2.suffix}" filename2 = f"{filename2.stem} (1){filename2.suffix}"
expected_dest_2 = os.path.join(dest, filename2) expected_dest_2 = os.path.join(dest, filename2)
@@ -181,7 +181,7 @@ def test_export_5(photosdb):
dest = tempdir.name dest = tempdir.name
photos = photosdb.photos(uuid=[UUID_DICT["export"]]) photos = photosdb.photos(uuid=[UUID_DICT["export"]])
filename = photos[0].filename filename = photos[0].original_filename
expected_dest = os.path.join(dest, filename) expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest)[0] got_dest = photos[0].export(dest)[0]
@@ -326,7 +326,7 @@ def test_export_12(photosdb):
edited_name = pathlib.Path(photos[0].path_edited).name edited_name = pathlib.Path(photos[0].path_edited).name
edited_suffix = pathlib.Path(edited_name).suffix edited_suffix = pathlib.Path(edited_name).suffix
filename = pathlib.Path(photos[0].filename).stem + "_edited" + edited_suffix filename = pathlib.Path(photos[0].original_filename).stem + "_edited" + edited_suffix
expected_dest = os.path.join(dest, filename) expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest, edited=True)[0] got_dest = photos[0].export(dest, edited=True)[0]

View File

@@ -17,6 +17,34 @@ UUID_DICT = {
"live": "BFF29EBD-22DF-4FCF-9817-317E7104EA50", "live": "BFF29EBD-22DF-4FCF-9817-317E7104EA50",
} }
UUID_BURSTS = {
"9F90DC00-AAAF-4A05-9A65-61FEEE0D67F2": {
"selected": True,
"filename": "IMAGE_9812.JPG",
"albums": ["TestBurst"],
},
"964F457D-5FFC-47B9-BEAD-56B0A83FEF63": {
"selected": True,
"filename": "IMG_9816.JPG",
"albums": [],
},
"A385FA13-DF8E-482F-A8C5-970EDDF54C2F": {
"selected": False,
"filename": "IMG_9813.JPG",
"albums": ["TestBurst", "TestBurst2"],
},
"38F8F30C-FF6D-49DA-8092-18497F1D6628": {
"selected": True,
"filename": "IMG_9814.JPG",
"albums": ["TestBurst2"],
},
"E3863443-9EA8-417F-A90B-8F7086623DAD": {
"selected": False,
"filename": "IMG_9815.JPG",
"albums": ["TestBurst", "TestBurst2"],
},
}
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def photosdb(): def photosdb():
@@ -29,7 +57,7 @@ def test_export_default_name(photosdb):
# test basic export # test basic export
# get an unedited image and export it using default filename # get an unedited image and export it using default filename
import os import os
import os.path import pathlib
import tempfile import tempfile
import osxphotos import osxphotos
@@ -38,11 +66,12 @@ def test_export_default_name(photosdb):
dest = tempdir.name dest = tempdir.name
photos = photosdb.photos(uuid=[UUID_DICT["no_adjustments"]]) photos = photosdb.photos(uuid=[UUID_DICT["no_adjustments"]])
filename = photos[0].filename filename = photos[0].original_filename
expected_dest = os.path.join(dest, filename) expected_dest = pathlib.Path(dest) / filename
expected_dest = expected_dest.parent / f"{expected_dest.stem}.jpeg"
got_dest = photos[0].export(dest, use_photos_export=True)[0] got_dest = photos[0].export(dest, use_photos_export=True)[0]
assert got_dest == expected_dest assert got_dest == str(expected_dest)
assert os.path.isfile(got_dest) assert os.path.isfile(got_dest)
@@ -82,7 +111,7 @@ def test_export_edited(photosdb):
photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]]) photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]])
suffix = pathlib.Path(photos[0].path_edited).suffix suffix = pathlib.Path(photos[0].path_edited).suffix
filename = f"{pathlib.Path(photos[0].filename).stem}_edited{suffix}" filename = f"{pathlib.Path(photos[0].original_filename).stem}_edited{suffix}"
expected_dest = os.path.join(dest, filename) expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest, use_photos_export=True, edited=True)[0] got_dest = photos[0].export(dest, use_photos_export=True, edited=True)[0]
@@ -155,3 +184,13 @@ def test_export_edited_no_edit(photosdb):
with pytest.raises(Exception) as e: with pytest.raises(Exception) as e:
assert photos[0].export(dest, use_photos_export=True, edited=True) assert photos[0].export(dest, use_photos_export=True, edited=True)
assert e.type == ValueError assert e.type == ValueError
def test_burst_albums(photosdb):
"""Test burst_selected, burst_albums"""
for uuid in UUID_BURSTS:
photo = photosdb.get_photo(uuid)
assert photo.burst
assert photo.burst_selected == UUID_BURSTS[uuid]["selected"]
assert sorted(photo.burst_albums) == sorted(UUID_BURSTS[uuid]["albums"])

View File

@@ -16,14 +16,14 @@ UUID_DICT = {
} }
NAMES_DICT = { NAMES_DICT = {
"raw": "D05A5FE3-15FB-49A1-A15D-AB3DA6F8B068.jpeg", "raw": "DSC03584.jpeg",
"heic": "7783E8E6-9CAC-40F3-BE22-81FB7051C266.jpeg", "heic": "IMG_3092.jpeg"
} }
UUID_LIVE_HEIC = "612CE30B-3D8F-417A-9B14-EC42CBA10ACC" UUID_LIVE_HEIC = "612CE30B-3D8F-417A-9B14-EC42CBA10ACC"
NAMES_LIVE_HEIC = [ NAMES_LIVE_HEIC = [
"612CE30B-3D8F-417A-9B14-EC42CBA10ACC.jpeg", "IMG_3259.jpeg",
"612CE30B-3D8F-417A-9B14-EC42CBA10ACC.mov", "IMG_3259.mov"
] ]

View File

@@ -29,13 +29,13 @@ def test_export_1():
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]]) photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]])
filename = photos[0].filename filename = photos[0].original_filename
expected_dest = os.path.join(dest, filename) expected_dest = os.path.join(dest, filename)
got_dest = photos[0].export(dest)[0] got_dest = photos[0].export(dest)[0]
assert got_dest == expected_dest assert got_dest == expected_dest
assert os.path.isfile(got_dest) assert os.path.isfile(got_dest)
assert pathlib.Path(got_dest).name == FILENAME_DICT["current"] assert pathlib.Path(got_dest).name == FILENAME_DICT["original"]
def test_export_2(): def test_export_2():
@@ -101,7 +101,7 @@ def test_export_edited_default():
photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]]) photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]])
got_dest = photos[0].export(dest, edited=True)[0] got_dest = photos[0].export(dest, edited=True)[0]
assert pathlib.Path(got_dest).name == FILENAME_DICT["current_edited"] assert pathlib.Path(got_dest).name == FILENAME_DICT["original_edited"]
def test_export_edited_wrong_suffix(): def test_export_edited_wrong_suffix():

View File

@@ -21,6 +21,8 @@ EXPORT_RESULT_ATTRIBUTES = [
"error", "error",
"exiftool_warning", "exiftool_warning",
"exiftool_error", "exiftool_error",
"deleted_files",
"deleted_directories",
] ]
@@ -43,6 +45,8 @@ def test_exportresults_init():
assert results.error == [] assert results.error == []
assert results.exiftool_warning == [] assert results.exiftool_warning == []
assert results.exiftool_error == [] assert results.exiftool_error == []
assert results.deleted_files == []
assert results.deleted_directories == []
def test_exportresults_iadd(): def test_exportresults_iadd():
@@ -64,6 +68,12 @@ def test_exportresults_iadd():
results1.exiftool_error = [("exiftool_error1", "foo")] results1.exiftool_error = [("exiftool_error1", "foo")]
results2.exiftool_error = [("exiftool_error2", "bar")] results2.exiftool_error = [("exiftool_error2", "bar")]
results1.deleted_files = [("foo1")]
results2.deleted_files = [("foo2")]
results1.deleted_directories = [("bar1")]
results2.deleted_directories = [("bar2")]
results1 += results2 results1 += results2
assert results1.exiftool_warning == [ assert results1.exiftool_warning == [
@@ -75,6 +85,9 @@ def test_exportresults_iadd():
("exiftool_error2", "bar"), ("exiftool_error2", "bar"),
] ]
assert results1.deleted_files == ["foo1", "foo2"]
assert results1.deleted_directories == ["bar1", "bar2"]
def test_all_files(): def test_all_files():
""" test ExportResults.all_files() """ """ test ExportResults.all_files() """
@@ -84,10 +97,12 @@ def test_all_files():
results.exiftool_warning = [("exiftool_warning1", "foo")] results.exiftool_warning = [("exiftool_warning1", "foo")]
results.exiftool_error = [("exiftool_error1", "foo")] results.exiftool_error = [("exiftool_error1", "foo")]
results.error = [("error1", "foo")] results.error = [("error1", "foo")]
results.deleted_files = ["deleted_files1"]
results.deleted_directories = ["deleted_directories1"]
assert sorted(results.all_files()) == sorted( assert sorted(
[f"{x}1" for x in EXPORT_RESULT_ATTRIBUTES] results.all_files() + results.deleted_files + results.deleted_directories
) ) == sorted([f"{x}1" for x in EXPORT_RESULT_ATTRIBUTES])
def test_str(): def test_str():
@@ -95,6 +110,6 @@ def test_str():
results = ExportResults() results = ExportResults()
assert ( assert (
str(results) str(results)
== "ExportResults(exported=[],new=[],updated=[],skipped=[],exif_updated=[],touched=[],converted_to_jpeg=[],sidecar_json_written=[],sidecar_json_skipped=[],sidecar_exiftool_written=[],sidecar_exiftool_skipped=[],sidecar_xmp_written=[],sidecar_xmp_skipped=[],missing=[],error=[],exiftool_warning=[],exiftool_error=[])" == "ExportResults(exported=[],new=[],updated=[],skipped=[],exif_updated=[],touched=[],converted_to_jpeg=[],sidecar_json_written=[],sidecar_json_skipped=[],sidecar_exiftool_written=[],sidecar_exiftool_skipped=[],sidecar_xmp_written=[],sidecar_xmp_skipped=[],missing=[],error=[],exiftool_warning=[],exiftool_error=[],deleted_files=[],deleted_directories=[])"
) )

View File

@@ -44,7 +44,7 @@ def test_export_live_1():
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["live"]]) photos = photosdb.photos(uuid=[UUID_DICT["live"]])
filename = photos[0].filename filename = photos[0].original_filename
expected_dest = os.path.join(dest.name, filename) expected_dest = os.path.join(dest.name, filename)
got_dest = photos[0].export(dest.name, live_photo=True)[0] got_dest = photos[0].export(dest.name, live_photo=True)[0]
got_movie = f"{pathlib.Path(got_dest).parent / pathlib.Path(got_dest).stem}.mov" got_movie = f"{pathlib.Path(got_dest).parent / pathlib.Path(got_dest).stem}.mov"
@@ -70,7 +70,7 @@ def test_export_live_2():
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["live"]]) photos = photosdb.photos(uuid=[UUID_DICT["live"]])
filename = photos[0].filename filename = photos[0].original_filename
expected_dest = os.path.join(dest.name, filename) expected_dest = os.path.join(dest.name, filename)
got_dest = photos[0].export(dest.name, live_photo=False)[0] got_dest = photos[0].export(dest.name, live_photo=False)[0]
got_movie = f"{pathlib.Path(got_dest).parent / pathlib.Path(got_dest).stem}.mov" got_movie = f"{pathlib.Path(got_dest).parent / pathlib.Path(got_dest).stem}.mov"
@@ -97,7 +97,7 @@ def test_export_live_3():
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["live"]]) photos = photosdb.photos(uuid=[UUID_DICT["live"]])
filename = photos[0].filename filename = photos[0].original_filename
expected_dest = os.path.join(dest.name, filename) expected_dest = os.path.join(dest.name, filename)
expected_mov = f"{dest.name}/{pathlib.Path(expected_dest).stem}.mov" expected_mov = f"{dest.name}/{pathlib.Path(expected_dest).stem}.mov"
got_files = photos[0].export(dest.name, live_photo=True) got_files = photos[0].export(dest.name, live_photo=True)

View File

@@ -28,6 +28,7 @@ UUID_DICT = {
"mojave_album_1": "15uNd7%8RguTEgNPKHfTWw", "mojave_album_1": "15uNd7%8RguTEgNPKHfTWw",
"date_modified": "A9B73E13-A6F2-4915-8D67-7213B39BAE9F", "date_modified": "A9B73E13-A6F2-4915-8D67-7213B39BAE9F",
"date_not_modified": "128FB4C6-0B16-4E7D-9108-FB2E90DA1546", "date_not_modified": "128FB4C6-0B16-4E7D-9108-FB2E90DA1546",
"favorite": "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51",
} }
UUID_MEDIA_TYPE = { UUID_MEDIA_TYPE = {
@@ -133,6 +134,9 @@ UUID_EXIFTOOL = {
"England,London,London 2018,St. James's Park,UK,United Kingdom" "England,London,London 2018,St. James's Park,UK,United Kingdom"
], ],
}, },
"1EB2B765-0765-43BA-A90C-0D0580E6172C": {
"{exiftool:EXIF:SubSecTimeOriginal}": ["22"]
},
} }
TEMPLATE_VALUES = { TEMPLATE_VALUES = {
@@ -173,6 +177,8 @@ TEMPLATE_VALUES = {
"{exif.lens_model}": "iPhone 6s back camera 4.15mm f/2.2", "{exif.lens_model}": "iPhone 6s back camera 4.15mm f/2.2",
"{album?{folder_album},{created.year}/{created.mm}}": "2020/02", "{album?{folder_album},{created.year}/{created.mm}}": "2020/02",
"{title?Title is '{title} - {descr}',No Title}": "Title is 'Glen Ord - Jack Rose Dining Saloon'", "{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"], "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") @pytest.fixture(scope="module")
def photosdb_places(): def photosdb_places():
@@ -304,7 +381,7 @@ def test_lookup_multi(photosdb_places):
for subst in TEMPLATE_SUBSTITUTIONS_MULTI_VALUED: for subst in TEMPLATE_SUBSTITUTIONS_MULTI_VALUED:
lookup_str = re.match(r"\{([^\\,}]+)\}", subst).group(1) lookup_str = re.match(r"\{([^\\,}]+)\}", subst).group(1)
if subst == "{exiftool}": if subst in ["{exiftool}", "{photo}", "{function}"]:
continue continue
lookup = template.get_template_value_multi(lookup_str, path_sep=os.path.sep) lookup = template.get_template_value_multi(lookup_str, path_sep=os.path.sep)
assert isinstance(lookup, list) assert isinstance(lookup, list)
@@ -828,6 +905,14 @@ def test_edited(photosdb):
assert rendered == ["edited"] 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): def test_nested_template_bool(photosdb):
photo = photosdb.get_photo(UUID_MULTI_KEYWORDS) photo = photosdb.get_photo(UUID_MULTI_KEYWORDS)
template = "{hdr?{edited?HDR_EDITED,HDR_NOT_EDITED},{edited?NOT_HDR_EDITED,NOT_HDR_NOT_EDITED}}" 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 + "}") rendered, _ = photo.render_template("{" + punc + "}")
assert rendered[0] == PUNCTUATION[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}"
)