Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fa7de1563 | ||
|
|
70d68a25ba | ||
|
|
bfc4371d9e | ||
|
|
6a288676a1 | ||
|
|
874ad2fa34 | ||
|
|
a233167471 | ||
|
|
21dc0d388f | ||
|
|
eff8e7a63f | ||
|
|
03f8b2bc6e | ||
|
|
e215c200c7 | ||
|
|
ae5b02f563 | ||
|
|
aa1a96d201 | ||
|
|
d9f24307ac | ||
|
|
958f8c343a | ||
|
|
70cf4c9f92 | ||
|
|
2d3344ee34 | ||
|
|
b4bc906b6a | ||
|
|
520a15fac6 | ||
|
|
032dff8967 | ||
|
|
3c36b0fb33 | ||
|
|
d51d7a41e4 | ||
|
|
60c926fea5 | ||
|
|
db27aac14b | ||
|
|
d17454772c | ||
|
|
9c9e73ba96 | ||
|
|
e21a78c2b3 | ||
|
|
de0fbf2bb9 | ||
|
|
b330e27fb8 | ||
|
|
a941f66d62 | ||
|
|
d77eba12b2 | ||
|
|
de94fd76de | ||
|
|
1026473684 | ||
|
|
3f9c9893c3 | ||
|
|
574cdd65a3 | ||
|
|
5b9547669e | ||
|
|
35b5bbd13d | ||
|
|
6870ad0d8e | ||
|
|
17ac5949e1 |
@@ -184,6 +184,24 @@
|
||||
"contributions": [
|
||||
"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,
|
||||
|
||||
91
CHANGELOG.md
91
CHANGELOG.md
@@ -4,6 +4,97 @@ All notable changes to this project will be documented in this file. Dates are d
|
||||
|
||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
|
||||
#### [v0.42.2](https://github.com/RhetTbull/osxphotos/compare/v0.42.1...v0.42.2)
|
||||
|
||||
> 17 April 2021
|
||||
|
||||
- Fixed bug for multi-field templates and --xattr-template, #422 [`6a28867`](https://github.com/RhetTbull/osxphotos/commit/6a288676a14ce23380181d43db19128afdda7731)
|
||||
- Add @ubrandes as a contributor [`874ad2f`](https://github.com/RhetTbull/osxphotos/commit/874ad2fa34d8306c071cd479625a9aa97f6488b2)
|
||||
|
||||
#### [v0.42.1](https://github.com/RhetTbull/osxphotos/compare/v0.41.11...v0.42.1)
|
||||
|
||||
> 15 April 2021
|
||||
|
||||
- Implements conditional expressions for template system, #417 [`03f8b2b`](https://github.com/RhetTbull/osxphotos/commit/03f8b2bc6ed53d3176f9d1ac51c3e4469db3e94b)
|
||||
- Added {function} template, #419 [`21dc0d3`](https://github.com/RhetTbull/osxphotos/commit/21dc0d388f508c33526ba7510d78c71abd1151a9)
|
||||
- Added template_function.py to examples [`eff8e7a`](https://github.com/RhetTbull/osxphotos/commit/eff8e7a63ff77e80fff0ce53fe56f5a010f55ab5)
|
||||
|
||||
#### [v0.41.11](https://github.com/RhetTbull/osxphotos/compare/v0.41.10...v0.41.11)
|
||||
|
||||
> 12 April 2021
|
||||
|
||||
- Doc updates [`958f8c3`](https://github.com/RhetTbull/osxphotos/commit/958f8c343a93ba60c1182df32727143a750f7b15)
|
||||
- Added {photo} template, partial fix for issue #417 [`aa1a96d`](https://github.com/RhetTbull/osxphotos/commit/aa1a96d20118916a558b08e7f8ec87c43abf789b)
|
||||
- Added {favorite} template, partial fix for #289 [`d9f2430`](https://github.com/RhetTbull/osxphotos/commit/d9f24307acc9f3f7cfa01c5e47f161b3aa390a81)
|
||||
|
||||
#### [v0.41.10](https://github.com/RhetTbull/osxphotos/compare/v0.41.9...v0.41.10)
|
||||
|
||||
> 9 April 2021
|
||||
|
||||
- Added --query-eval, implements #280 [`b4bc906`](https://github.com/RhetTbull/osxphotos/commit/b4bc906b6a1c3444c5f5a5d9d908ab8c955c8f7e)
|
||||
|
||||
#### [v0.41.9](https://github.com/RhetTbull/osxphotos/compare/v0.41.8...v0.41.9)
|
||||
|
||||
> 5 April 2021
|
||||
|
||||
- Bug fix for #414, exiftool str replace [`032dff8`](https://github.com/RhetTbull/osxphotos/commit/032dff89677f049a234d9f498951b8b402d1b31c)
|
||||
|
||||
#### [v0.41.8](https://github.com/RhetTbull/osxphotos/compare/v0.41.7...v0.41.8)
|
||||
|
||||
> 4 April 2021
|
||||
|
||||
- Added --name to search filename, closes #249, #412 [`#249`](https://github.com/RhetTbull/osxphotos/issues/249)
|
||||
|
||||
#### [v0.41.7](https://github.com/RhetTbull/osxphotos/compare/v0.41.6...v0.41.7)
|
||||
|
||||
> 3 April 2021
|
||||
|
||||
- Bump pygments from 2.6.1 to 2.7.4 [`#408`](https://github.com/RhetTbull/osxphotos/pull/408)
|
||||
- Removed logging.debug code [`e21a78c`](https://github.com/RhetTbull/osxphotos/commit/e21a78c2b39ee82610394b447a9aa697e489c3e4)
|
||||
- Added test for #409 [`db27aac`](https://github.com/RhetTbull/osxphotos/commit/db27aac14bbaff0b2db44f8b2d41022ebcad18a7)
|
||||
- Update phototemplate.py [`d174547`](https://github.com/RhetTbull/osxphotos/commit/d17454772cebbd6edd5d8e0f04e80feecbdb2355)
|
||||
|
||||
#### [v0.41.6](https://github.com/RhetTbull/osxphotos/compare/v0.41.5...v0.41.6)
|
||||
|
||||
> 28 March 2021
|
||||
|
||||
- Added --retry, issue #406 [`b330e27`](https://github.com/RhetTbull/osxphotos/commit/b330e27fb838b702cefcbdb588c2fbb924b4cbc4)
|
||||
|
||||
#### [v0.41.5](https://github.com/RhetTbull/osxphotos/compare/v0.41.4...v0.41.5)
|
||||
|
||||
> 27 March 2021
|
||||
|
||||
- Bump pyyaml from 5.1.2 to 5.4 [`#402`](https://github.com/RhetTbull/osxphotos/pull/402)
|
||||
- Fixed albums for burst images, closes #401, #403, #404 [`#401`](https://github.com/RhetTbull/osxphotos/issues/401)
|
||||
|
||||
#### [v0.41.4](https://github.com/RhetTbull/osxphotos/compare/v0.41.3...v0.41.4)
|
||||
|
||||
> 22 March 2021
|
||||
|
||||
- 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)
|
||||
|
||||
> 22 February 2021
|
||||
|
||||
360
README.md
360
README.md
@@ -4,7 +4,7 @@
|
||||
[](https://github.com/RhetTbull/osxphotos/workflows/Tests/badge.svg)
|
||||

|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
[](#contributors)
|
||||
[](#contributors)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
OSXPhotos provides the ability to interact with and query Apple's Photos.app library on macOS. You can query the Photos library database — for example, file name, file path, and metadata such as keywords/tags, persons/faces, albums, etc. You can also easily export both the original and edited photos.
|
||||
@@ -183,41 +183,57 @@ Options:
|
||||
use in the following order: 1. last opened
|
||||
library, 2. system library, 3.
|
||||
~/Pictures/Photos Library.photoslibrary
|
||||
|
||||
-V, --verbose Print verbose output.
|
||||
--keyword KEYWORD Search for photos with keyword KEYWORD. If
|
||||
more than one keyword, treated as "OR", e.g.
|
||||
find photos matching any keyword
|
||||
|
||||
--person PERSON Search for photos with person PERSON. If more
|
||||
than one person, treated as "OR", e.g. find
|
||||
photos matching any person
|
||||
|
||||
--album ALBUM Search for photos in album ALBUM. If more than
|
||||
one album, treated as "OR", e.g. find photos
|
||||
matching any album
|
||||
|
||||
--folder FOLDER Search for photos in an album in folder
|
||||
FOLDER. If more than one folder, treated as
|
||||
"OR", e.g. find photos in any FOLDER. Only
|
||||
searches top level folders (e.g. does not look
|
||||
at subfolders)
|
||||
|
||||
--name FILENAME Search for photos with filename matching
|
||||
FILENAME. If more than one --name options is
|
||||
specified, they are treated as "OR", e.g. find
|
||||
photos matching any FILENAME.
|
||||
|
||||
--uuid UUID Search for photos with UUID(s).
|
||||
--uuid-from-file FILE Search for photos with UUID(s) loaded from
|
||||
FILE. Format is a single UUID per line. Lines
|
||||
preceded with # are ignored.
|
||||
|
||||
--title TITLE Search for TITLE in title of photo.
|
||||
--no-title Search for photos with no title.
|
||||
--description DESC Search for DESC in description of photo.
|
||||
--no-description Search for photos with no description.
|
||||
--place PLACE Search for PLACE in photo's reverse
|
||||
geolocation info
|
||||
|
||||
--no-place Search for photos with no associated place
|
||||
name info (no reverse geolocation info)
|
||||
|
||||
--label LABEL Search for photos with image classification
|
||||
label LABEL (Photos 5 only). If more than one
|
||||
label, treated as "OR", e.g. find photos
|
||||
matching any label
|
||||
|
||||
--uti UTI Search for photos whose uniform type
|
||||
identifier (UTI) matches UTI
|
||||
|
||||
-i, --ignore-case Case insensitive search for title,
|
||||
description, place, keyword, person, or album.
|
||||
|
||||
--edited Search for photos that have been edited.
|
||||
--external-edit Search for photos edited in external editor.
|
||||
--favorite Search for photos marked favorite.
|
||||
@@ -226,47 +242,67 @@ Options:
|
||||
--not-hidden Search for photos not marked hidden.
|
||||
--shared Search for photos in shared iCloud album
|
||||
(Photos 5 only).
|
||||
|
||||
--not-shared Search for photos not in shared iCloud album
|
||||
(Photos 5 only).
|
||||
|
||||
--burst Search for photos that were taken in a burst.
|
||||
--not-burst Search for photos that are not part of a
|
||||
burst.
|
||||
|
||||
--live Search for Apple live photos
|
||||
--not-live Search for photos that are not Apple live
|
||||
photos.
|
||||
|
||||
--portrait Search for Apple portrait mode photos.
|
||||
--not-portrait Search for photos that are not Apple portrait
|
||||
mode photos.
|
||||
|
||||
--screenshot Search for screenshot photos.
|
||||
--not-screenshot Search for photos that are not screenshot
|
||||
photos.
|
||||
|
||||
--slow-mo Search for slow motion videos.
|
||||
--not-slow-mo Search for photos that are not slow motion
|
||||
videos.
|
||||
|
||||
--time-lapse Search for time lapse videos.
|
||||
--not-time-lapse Search for photos that are not time lapse
|
||||
videos.
|
||||
|
||||
--hdr Search for high dynamic range (HDR) photos.
|
||||
--not-hdr Search for photos that are not HDR photos.
|
||||
--selfie Search for selfies (photos taken with front-
|
||||
facing cameras).
|
||||
|
||||
--not-selfie Search for photos that are not selfies.
|
||||
--panorama Search for panorama photos.
|
||||
--not-panorama Search for photos that are not panoramas.
|
||||
--has-raw Search for photos with both a jpeg and raw
|
||||
version
|
||||
|
||||
--only-movies Search only for movies (default searches both
|
||||
images and movies).
|
||||
|
||||
--only-photos Search only for photos/images (default
|
||||
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,
|
||||
2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO
|
||||
8601).
|
||||
--to-date DATETIME Search by end item date, e.g.
|
||||
8601 with/without timezone).
|
||||
|
||||
--to-date DATETIME 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).
|
||||
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.
|
||||
--no-comment Search for photos with no comments.
|
||||
--has-likes Search for photos that have likes.
|
||||
@@ -274,17 +310,38 @@ Options:
|
||||
--is-reference Search for photos that were imported as
|
||||
referenced files (not copied into Photos
|
||||
library).
|
||||
|
||||
--in-album Search for photos that are in one or more
|
||||
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
|
||||
library; must be used with --download-missing.
|
||||
|
||||
--deleted Include photos from the 'Recently Deleted'
|
||||
folder.
|
||||
|
||||
--deleted-only Include only photos from the 'Recently
|
||||
Deleted' folder.
|
||||
|
||||
--update Only export new or updated files. See notes
|
||||
below on export and --update.
|
||||
|
||||
--ignore-signature When used with '--update', ignores file
|
||||
signature when updating files. This is useful
|
||||
if you have processed or edited exported
|
||||
@@ -303,12 +360,15 @@ Options:
|
||||
not; 3) if a sidecar does not exist for the
|
||||
photo, a sidecar will be written whether or
|
||||
not the photo file was written or updated.
|
||||
|
||||
--only-new If used with --update, ignores any previously
|
||||
exported files, even if missing from the
|
||||
export folder and only exports new files that
|
||||
haven't previously been exported.
|
||||
|
||||
--dry-run Dry run (test) the export but don't actually
|
||||
export any files; most useful with --verbose.
|
||||
|
||||
--export-as-hardlink Hardlink files instead of copying them. Cannot
|
||||
be used with --exiftool which creates copies
|
||||
of the files with embedded EXIF data. Note: on
|
||||
@@ -316,41 +376,58 @@ Options:
|
||||
giving many of the same advantages as
|
||||
hardlinks without having to use --export-as-
|
||||
hardlink.
|
||||
|
||||
--touch-file Sets the file's modification time to match
|
||||
photo date.
|
||||
|
||||
--overwrite Overwrite existing files. Default behavior is
|
||||
to add (1), (2), etc to filename if file
|
||||
already exists. Use this with caution as it
|
||||
may create name collisions on export. (e.g. if
|
||||
two files happen to have the same name)
|
||||
|
||||
--retry RETRY Automatically retry export up to RETRY times
|
||||
if an error occurs during export. This may be
|
||||
useful with network drives that experience
|
||||
intermittent errors.
|
||||
|
||||
--export-by-date Automatically create output folders to
|
||||
organize photos by date created (e.g.
|
||||
DEST/2019/12/20/photoname.jpg).
|
||||
|
||||
--skip-edited Do not export edited version of photo if an
|
||||
edited version exists.
|
||||
|
||||
--skip-original-if-edited Do not export original if there is an edited
|
||||
version (exports only the edited version).
|
||||
|
||||
--skip-bursts Do not export all associated burst images in
|
||||
the library if a photo is a burst photo.
|
||||
|
||||
--skip-live Do not export the associated live video
|
||||
component of a live photo.
|
||||
|
||||
--skip-raw Do not export associated raw images of a
|
||||
RAW+JPEG pair. Note: this does not skip raw
|
||||
photos if the raw photo does not have an
|
||||
associated jpeg image (e.g. the raw file was
|
||||
imported to Photos without a jpeg preview).
|
||||
|
||||
--current-name Use photo's current filename instead of
|
||||
original filename for export. Note: Starting
|
||||
with Photos 5, all photos are renamed upon
|
||||
import. By default, photos are exported with
|
||||
the the original name they had before import.
|
||||
|
||||
--convert-to-jpeg Convert all non-jpeg images (e.g. raw, HEIC,
|
||||
PNG, etc) to JPEG upon export. Only works if
|
||||
your Mac has a GPU.
|
||||
|
||||
--jpeg-quality FLOAT RANGE Value in range 0.0 to 1.0 to use with
|
||||
--convert-to-jpeg. A value of 1.0 specifies
|
||||
best quality, a value of 0.0 specifies maximum
|
||||
compression. Defaults to 1.0
|
||||
|
||||
--download-missing Attempt to download missing photos from
|
||||
iCloud. The current implementation uses
|
||||
Applescript to interact with Photos to export
|
||||
@@ -363,6 +440,7 @@ Options:
|
||||
export all burst images; only the primary
|
||||
photo will be exported--associated burst
|
||||
images will be skipped.
|
||||
|
||||
--sidecar FORMAT Create sidecar for each photo exported; valid
|
||||
FORMAT values: xmp, json, exiftool; --sidecar
|
||||
xmp: create XMP sidecar used by Digikam, Adobe
|
||||
@@ -389,6 +467,7 @@ Options:
|
||||
tags exported in the JSON and exiftool
|
||||
sidecar, see '--exiftool'. See also '--ignore-
|
||||
signature'.
|
||||
|
||||
--sidecar-drop-ext Drop the photo's extension when naming sidecar
|
||||
files. By default, sidecar files are named in
|
||||
format 'photo_filename.photo_ext.sidecar_ext',
|
||||
@@ -400,6 +479,7 @@ Options:
|
||||
of different types but the same name in the
|
||||
output directory, e.g. 'IMG_1234.JPG' and
|
||||
'IMG_1234.MOV'.
|
||||
|
||||
--exiftool Use exiftool to write metadata directly to
|
||||
exported photos. To use this option, exiftool
|
||||
must be installed and in the path. exiftool
|
||||
@@ -421,8 +501,10 @@ Options:
|
||||
QuickTime:ModifyDate (see also --ignore-date-
|
||||
modified); QuickTime:GPSCoordinates;
|
||||
UserData:GPSCoordinates.
|
||||
|
||||
--exiftool-path EXIFTOOL_PATH Optionally specify path to exiftool; if not
|
||||
provided, will look for exiftool in $PATH.
|
||||
|
||||
--exiftool-option OPTION Optional flag/option to pass to exiftool when
|
||||
using --exiftool. For example, --exiftool-
|
||||
option '-m' to ignore minor warnings. Specify
|
||||
@@ -432,21 +514,27 @@ Options:
|
||||
full list of options. More than one option may
|
||||
be specified by repeating the option, e.g.
|
||||
--exiftool-option '-m' --exiftool-option '-F'.
|
||||
|
||||
--exiftool-merge-keywords Merge any keywords found in the original file
|
||||
with keywords used for '--exiftool' and '--
|
||||
sidecar'.
|
||||
|
||||
--exiftool-merge-persons Merge any persons found in the original file
|
||||
with persons used for '--exiftool' and '--
|
||||
sidecar'.
|
||||
|
||||
--ignore-date-modified If used with --exiftool or --sidecar, will
|
||||
ignore the photo modification date and set
|
||||
EXIF:ModifyDate to EXIF:DateTimeOriginal; this
|
||||
is consistent with how Photos handles the
|
||||
EXIF:ModifyDate tag.
|
||||
|
||||
--person-keyword Use person in image as keyword/tag when
|
||||
exporting metadata.
|
||||
|
||||
--album-keyword Use album name as keyword/tag when exporting
|
||||
metadata.
|
||||
|
||||
--keyword-template TEMPLATE For use with --exiftool, --sidecar; specify a
|
||||
template string to use as keyword in the form
|
||||
'{name,DEFAULT}' This is the same format as
|
||||
@@ -459,6 +547,7 @@ Options:
|
||||
"{folder_album}" --keyword-template
|
||||
"{created.year}". See '--replace-keywords' and
|
||||
Templating System below.
|
||||
|
||||
--replace-keywords Replace keywords with any values specified
|
||||
with --keyword-template. By default,
|
||||
--keyword-template will add keywords to any
|
||||
@@ -467,6 +556,7 @@ Options:
|
||||
from --keyword-template will replace any
|
||||
existing keywords instead of adding additional
|
||||
keywords.
|
||||
|
||||
--description-template TEMPLATE
|
||||
For use with --exiftool, --sidecar; specify a
|
||||
template string to use as description in the
|
||||
@@ -477,6 +567,7 @@ Options:
|
||||
--description-template "{descr} exported with
|
||||
osxphotos on {today.date}" See Templating
|
||||
System below.
|
||||
|
||||
--finder-tag-template TEMPLATE Set MacOS Finder tags to TEMPLATE. These tags
|
||||
can be searched in the Finder or Spotlight
|
||||
with 'tag:tagname' format. For example, '--
|
||||
@@ -485,11 +576,13 @@ Options:
|
||||
TEMPLATE values by using '--finder-tag-
|
||||
template' multiple times. See also '--finder-
|
||||
tag-keywords and Extended Attributes below.'.
|
||||
|
||||
--finder-tag-keywords Set MacOS Finder tags to keywords; any
|
||||
keywords specified via '--keyword-template', '
|
||||
--person-keyword', etc. will also be used as
|
||||
Finder tags. See also '--finder-tag-template
|
||||
and Extended Attributes below.'.
|
||||
|
||||
--xattr-template ATTRIBUTE TEMPLATE
|
||||
Set extended attribute ATTRIBUTE to TEMPLATE
|
||||
value. Valid attributes are: 'authors',
|
||||
@@ -500,16 +593,19 @@ Options:
|
||||
findercomment "{title}; {descr}" See Extended
|
||||
Attributes below for additional details on
|
||||
this option.
|
||||
|
||||
--directory DIRECTORY Optional template for specifying name of
|
||||
output directory in the form '{name,DEFAULT}'.
|
||||
See below for additional details on templating
|
||||
system.
|
||||
|
||||
--filename FILENAME Optional template for specifying name of
|
||||
output file in the form '{name,DEFAULT}'. File
|
||||
extension will be added automatically--do not
|
||||
include an extension in the FILENAME template.
|
||||
See below for additional details on templating
|
||||
system.
|
||||
|
||||
--jpeg-ext EXTENSION Specify file extension for JPEG files. Photos
|
||||
uses .jpeg for edited images but many images
|
||||
are imported with .jpg or .JPG which can
|
||||
@@ -519,12 +615,14 @@ Options:
|
||||
exported JPEG images. Valid values are jpeg,
|
||||
jpg, JPEG, JPG; e.g. '--jpeg-ext jpg' to use
|
||||
'.jpg' for all JPEGs.
|
||||
|
||||
--strip Optionally strip leading and trailing
|
||||
whitespace from any rendered templates. For
|
||||
example, if --filename template is "{title,}
|
||||
{original_name}" and image has no title,
|
||||
resulting file would have a leading space but
|
||||
if used with --strip, this will be removed.
|
||||
|
||||
--edited-suffix SUFFIX Optional suffix template for naming edited
|
||||
photos. Default name for edited photos is in
|
||||
form 'photoname_edited.ext'. For example, with
|
||||
@@ -534,6 +632,7 @@ Options:
|
||||
suffix is '_edited'. Multi-value templates
|
||||
(see Templating System) are not permitted with
|
||||
--edited-suffix.
|
||||
|
||||
--original-suffix SUFFIX Optional suffix template for naming original
|
||||
photos. Default name for original photos is
|
||||
in form 'filename.ext'. For example, with '--
|
||||
@@ -542,9 +641,11 @@ Options:
|
||||
default suffix is '' (no suffix). Multi-value
|
||||
templates (see Templating System) are not
|
||||
permitted with --original-suffix.
|
||||
|
||||
--use-photos-export Force the use of AppleScript or PhotoKit to
|
||||
export even if not missing (see also '--
|
||||
download-missing' and '--use-photokit').
|
||||
|
||||
--use-photokit Use with '--download-missing' or '--use-
|
||||
photos-export' to use direct Photos interface
|
||||
instead of AppleScript to export. Highly
|
||||
@@ -552,14 +653,23 @@ Options:
|
||||
iTerm2 (use with Terminal.app). This is faster
|
||||
and more reliable than the default AppleScript
|
||||
interface.
|
||||
|
||||
--report <path to export report>
|
||||
Write a CSV formatted report of all files that
|
||||
were exported.
|
||||
|
||||
--cleanup 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.
|
||||
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
|
||||
stores state information for export and
|
||||
--update. If --exportdb is not specified,
|
||||
@@ -568,6 +678,7 @@ Options:
|
||||
directory. Must be specified as filename
|
||||
only, not a path, as export database will be
|
||||
saved in export directory.
|
||||
|
||||
--load-config <config file path>
|
||||
Load options from file as written with --save-
|
||||
config. This allows you to save a complex
|
||||
@@ -579,9 +690,11 @@ Options:
|
||||
line options are used in conjunction with
|
||||
--load-config, they will override the
|
||||
corresponding values in the config file.
|
||||
|
||||
--save-config <config file path>
|
||||
Save options to file for use with --load-
|
||||
config. File format is TOML.
|
||||
|
||||
--help Show this message and exit.
|
||||
|
||||
** 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
|
||||
strings. (com.apple.metadata:kMDItemAuthors)
|
||||
|
||||
comment A comment related to the file. This differs from the Finder
|
||||
comment, kMDItemFinderComment. A string.
|
||||
(com.apple.metadata:kMDItemComment)
|
||||
|
||||
copyright The copyright owner of the file contents. A string.
|
||||
(com.apple.metadata:kMDItemCopyright)
|
||||
|
||||
description A description of the content of the resource. The description
|
||||
may include an abstract, table of contents, reference to a
|
||||
graphical representation of content or a free-text account of
|
||||
the content. A string. (com.apple.metadata:kMDItemDescription)
|
||||
|
||||
findercomment Finder comments for this file. A string.
|
||||
(com.apple.metadata:kMDItemFinderComment)
|
||||
|
||||
headline A publishable entry providing a synopsis of the contents of the
|
||||
file. A string. (com.apple.metadata:kMDItemHeadline)
|
||||
|
||||
keywords Keywords associated with this file. For example, “Birthday”,
|
||||
“Important”, etc. This differs from Finder tags
|
||||
(_kMDItemUserTags) which are keywords/tags shown in the Finder
|
||||
and searchable in Spotlight using "tag:tag_name". A list of
|
||||
strings. (com.apple.metadata:kMDItemKeywords)
|
||||
|
||||
|
||||
For additional information on extended attributes see: https://developer.apple.c
|
||||
om/documentation/coreservices/file_metadata/mditem/common_metadata_attribute_key
|
||||
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:
|
||||
|
||||
"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.
|
||||
@@ -738,20 +858,75 @@ e.g. If Photo is in Album1 in Folder1:
|
||||
• "{folder_album(>)}" renders to ["Folder1>Album1"]
|
||||
• "{folder_album()}" renders to ["Folder1Album1"]
|
||||
|
||||
[find|replace]: optional text replacement to perform on rendered template value.
|
||||
[find,replace]: optional text replacement to perform on rendered template value.
|
||||
For example, to replace "/" in an album name, you could use the template
|
||||
"{album[/,-]}". Multiple replacements can be made by appending "|" and adding
|
||||
another find|replace pair. e.g. to replace both "/" and ":" in album name:
|
||||
"{album[/,-|:,-]}". find/replace pairs are not limited to single characters.
|
||||
The "|" character cannot be used in a find/replace pair.
|
||||
|
||||
?bool_value: Template fields may be evaluated as boolean by appending "?" after
|
||||
the field name (and following "(path_sep)" or "[find/replace]". If a field is
|
||||
True (e.g. photo is HDR and field is "{hdr}") or has any value, the value
|
||||
following the "?" will be used to render the template instead of the actual
|
||||
field value. If the template field evaluates to False (e.g. in above example,
|
||||
photo is not HDR) or has no value (e.g. photo has no title and field is
|
||||
"{title}") then the default value following a "," will be used.
|
||||
conditional: optional conditional expression that is evaluated as boolean
|
||||
(True/False) for use with the ?bool_value modifier. Conditional expressions
|
||||
take the form ' not operator value' where not is an optional modifier that
|
||||
negates the operator. Note: the space before the conditional expression is
|
||||
required if you use a conditional expression. Valid comparison operators are:
|
||||
|
||||
• contains: template field contains value, similar to python's in
|
||||
• matches: template field contains exactly value, unlike contains: does not
|
||||
match partial matches
|
||||
• startswith: template field starts with value
|
||||
• endswith: template field ends with value
|
||||
• <=: template field is less than or equal to value
|
||||
• >=: template field is greater than or equal to value
|
||||
• <: template field is less than value
|
||||
• >: template field is greater than value
|
||||
• ==: template field equals value
|
||||
• !=: template field does not equal value
|
||||
|
||||
The value part of the conditional expression is treated as a bare (unquoted)
|
||||
word/phrase. Multiple values may be separated by '|' (the pipe symbol). value
|
||||
is itself a template statement so you can use one or more template fields in
|
||||
value which will be resolved before the comparison occurs.
|
||||
|
||||
For example:
|
||||
|
||||
• {keyword matches Beach} resolves to True if 'Beach' is a keyword. It would
|
||||
not match keyword 'BeachDay'.
|
||||
• {keyword contains Beach} resolves to True if any keyword contains the word
|
||||
'Beach' so it would match both 'Beach' and 'BeachDay'.
|
||||
• {photo.score.overall > 0.7} resolves to True if the photo's overall aesthetic
|
||||
score is greater than 0.7.
|
||||
• {keyword|lower contains beach} uses the lower case filter to do
|
||||
case-insensitive matching to match any keyword that contains the word
|
||||
'beach'.
|
||||
• {keyword|lower not contains beach} uses the not modifier to negate the
|
||||
comparison so this resolves to True if there is no keyword that matches
|
||||
'beach'.
|
||||
|
||||
Examples: to export photos that contain certain keywords with the osxphotos
|
||||
export command's --directory option:
|
||||
|
||||
--directory "{keyword|lower matches
|
||||
travel|vacation?Travel-Photos,Not-Travel-Photos}"
|
||||
|
||||
This exports any photo that has keywords 'travel' or 'vacation' into a directory
|
||||
'Travel-Photos' and all other photos into directory 'Not-Travel-Photos'.
|
||||
|
||||
This can be used to rename files as well, for example: --filename
|
||||
"{favorite?Favorite-{original_name},{original_name}}"
|
||||
|
||||
This renames any photo that is a favorite as 'Favorite-ImageName.jpg' (where
|
||||
'ImageName.jpg' is the original name of the photo) and all other photos with the
|
||||
unmodified original name.
|
||||
|
||||
?bool_value: Template fields may be evaluated as boolean (True/False) by
|
||||
appending "?" after the field name (and following "(path_sep)" or
|
||||
"[find/replace]". If a field is True (e.g. photo is HDR and field is "{hdr}")
|
||||
or has any value, the value following the "?" will be used to render the
|
||||
template instead of the actual field value. If the template field evaluates to
|
||||
False (e.g. in above example, photo is not HDR) or has no value (e.g. photo has
|
||||
no title and field is "{title}") then the default value following a "," will be
|
||||
used.
|
||||
|
||||
e.g. if photo is an HDR image,
|
||||
|
||||
@@ -820,6 +995,7 @@ Substitution Description
|
||||
{name} Current filename of the photo
|
||||
{original_name} Photo's original filename when imported to
|
||||
Photos
|
||||
|
||||
{title} Title of the photo
|
||||
{descr} Description of the photo
|
||||
{media_type} Special media type resolved in this
|
||||
@@ -829,30 +1005,45 @@ Substitution Description
|
||||
'video' if no special type. Customize one or
|
||||
more media types using format: '{media_type,vi
|
||||
deo=vidéo;time_lapse=vidéo_accélérée}'
|
||||
|
||||
{photo_or_video} 'photo' or 'video' depending on what type the
|
||||
image is. To customize, use default value as
|
||||
in '{photo_or_video,photo=fotos;video=videos}'
|
||||
|
||||
{hdr} Photo is HDR?; True/False value, use in format
|
||||
'{hdr?VALUE_IF_TRUE,VALUE_IF_FALSE}'
|
||||
|
||||
{edited} Photo has been edited (has adjustments)?;
|
||||
True/False value, use in format
|
||||
'{edited?VALUE_IF_TRUE,VALUE_IF_FALSE}'
|
||||
|
||||
{favorite} Photo has been marked as favorite?; True/False
|
||||
value, use in format
|
||||
'{favorite?VALUE_IF_TRUE,VALUE_IF_FALSE}'
|
||||
|
||||
{created.date} Photo's creation date in ISO format, e.g.
|
||||
'2020-03-22'
|
||||
|
||||
{created.year} 4-digit year of photo creation time
|
||||
{created.yy} 2-digit year of photo creation time
|
||||
{created.mm} 2-digit month of the photo creation time (zero
|
||||
padded)
|
||||
|
||||
{created.month} Month name in user's locale of the photo
|
||||
creation time
|
||||
|
||||
{created.mon} Month abbreviation in the user's locale of the
|
||||
photo creation time
|
||||
|
||||
{created.dd} 2-digit day of the month (zero padded) of
|
||||
photo creation time
|
||||
|
||||
{created.dow} Day of week in user's locale of the photo
|
||||
creation time
|
||||
|
||||
{created.doy} 3-digit day of year (e.g Julian day) of photo
|
||||
creation time, starting from 1 (zero padded)
|
||||
|
||||
{created.hour} 2-digit hour of the photo creation time
|
||||
{created.min} 2-digit minute of the photo creation time
|
||||
{created.sec} 2-digit second of the photo creation time
|
||||
@@ -865,38 +1056,51 @@ Substitution Description
|
||||
no template will return null value. See
|
||||
https://strftime.org/ for help on strftime
|
||||
templates.
|
||||
|
||||
{modified.date} Photo's modification date in ISO format, e.g.
|
||||
'2020-03-22'; uses creation date if photo is
|
||||
not modified
|
||||
|
||||
{modified.year} 4-digit year of photo modification time; uses
|
||||
creation date if photo is not modified
|
||||
|
||||
{modified.yy} 2-digit year of photo modification time; uses
|
||||
creation date if photo is not modified
|
||||
|
||||
{modified.mm} 2-digit month of the photo modification time
|
||||
(zero padded); uses creation date if photo is
|
||||
not modified
|
||||
|
||||
{modified.month} Month name in user's locale of the photo
|
||||
modification time; uses creation date if photo
|
||||
is not modified
|
||||
|
||||
{modified.mon} Month abbreviation in the user's locale of the
|
||||
photo modification time; uses creation date if
|
||||
photo is not modified
|
||||
|
||||
{modified.dd} 2-digit day of the month (zero padded) of the
|
||||
photo modification time; uses creation date if
|
||||
photo is not modified
|
||||
|
||||
{modified.dow} Day of week in user's locale of the photo
|
||||
modification time; uses creation date if photo
|
||||
is not modified
|
||||
|
||||
{modified.doy} 3-digit day of year (e.g Julian day) of photo
|
||||
modification time, starting from 1 (zero
|
||||
padded); uses creation date if photo is not
|
||||
modified
|
||||
|
||||
{modified.hour} 2-digit hour of the photo modification time;
|
||||
uses creation date if photo is not modified
|
||||
|
||||
{modified.min} 2-digit minute of the photo modification time;
|
||||
uses creation date if photo is not modified
|
||||
|
||||
{modified.sec} 2-digit second of the photo modification time;
|
||||
uses creation date if photo is not modified
|
||||
|
||||
{modified.strftime} Apply strftime template to file modification
|
||||
date/time. Should be used in form
|
||||
{modified.strftime,TEMPLATE} where TEMPLATE is
|
||||
@@ -907,21 +1111,28 @@ Substitution Description
|
||||
creation date if photo is not modified. See
|
||||
https://strftime.org/ for help on strftime
|
||||
templates.
|
||||
|
||||
{today.date} Current date in iso format, e.g. '2020-03-22'
|
||||
{today.year} 4-digit year of current date
|
||||
{today.yy} 2-digit year of current date
|
||||
{today.mm} 2-digit month of the current date (zero
|
||||
padded)
|
||||
|
||||
{today.month} Month name in user's locale of the current
|
||||
date
|
||||
|
||||
{today.mon} Month abbreviation in the user's locale of the
|
||||
current date
|
||||
|
||||
{today.dd} 2-digit day of the month (zero padded) of
|
||||
current date
|
||||
|
||||
{today.dow} Day of week in user's locale of the current
|
||||
date
|
||||
|
||||
{today.doy} 3-digit day of year (e.g Julian day) of
|
||||
current date, starting from 1 (zero padded)
|
||||
|
||||
{today.hour} 2-digit hour of the current date
|
||||
{today.min} 2-digit minute of the current date
|
||||
{today.sec} 2-digit second of the current date
|
||||
@@ -934,53 +1145,73 @@ Substitution Description
|
||||
no template will return null value. See
|
||||
https://strftime.org/ for help on strftime
|
||||
templates.
|
||||
|
||||
{place.name} Place name from the photo's reverse
|
||||
geolocation data, as displayed in Photos
|
||||
|
||||
{place.country_code} The ISO country code from the photo's reverse
|
||||
geolocation data
|
||||
|
||||
{place.name.country} Country name from the photo's reverse
|
||||
geolocation data
|
||||
|
||||
{place.name.state_province} State or province name from the photo's
|
||||
reverse geolocation data
|
||||
|
||||
{place.name.city} City or locality name from the photo's reverse
|
||||
geolocation data
|
||||
|
||||
{place.name.area_of_interest} Area of interest name (e.g. landmark or public
|
||||
place) from the photo's reverse geolocation
|
||||
data
|
||||
|
||||
{place.address} Postal address from the photo's reverse
|
||||
geolocation data, e.g. '2007 18th St NW,
|
||||
Washington, DC 20009, United States'
|
||||
|
||||
{place.address.street} Street part of the postal address, e.g. '2007
|
||||
18th St NW'
|
||||
|
||||
{place.address.city} City part of the postal address, e.g.
|
||||
'Washington'
|
||||
|
||||
{place.address.state_province} State/province part of the postal address,
|
||||
e.g. 'DC'
|
||||
|
||||
{place.address.postal_code} Postal code part of the postal address, e.g.
|
||||
'20009'
|
||||
|
||||
{place.address.country} Country name of the postal address, e.g.
|
||||
'United States'
|
||||
|
||||
{place.address.country_code} ISO country code of the postal address, e.g.
|
||||
'US'
|
||||
|
||||
{searchinfo.season} Season of the year associated with a photo,
|
||||
e.g. 'Summer'; (Photos 5+ only, applied
|
||||
automatically by Photos' image categorization
|
||||
algorithms).
|
||||
|
||||
{exif.camera_make} Camera make from original photo's EXIF
|
||||
information as imported by Photos, e.g.
|
||||
'Apple'
|
||||
|
||||
{exif.camera_model} Camera model from original photo's EXIF
|
||||
information as imported by Photos, e.g.
|
||||
'iPhone 6s'
|
||||
|
||||
{exif.lens_model} Lens model from original photo's EXIF
|
||||
information as imported by Photos, e.g.
|
||||
'iPhone 6s back camera 4.15mm f/2.2'
|
||||
|
||||
{uuid} Photo's internal universally unique identifier
|
||||
(UUID) for the photo, a 36-character string
|
||||
unique to the photo, e.g.
|
||||
'128FB4C6-0B16-4E7D-9108-FB2E90DA1546'
|
||||
|
||||
{comma} A comma: ','
|
||||
{semicolon} A semicolon: ';'
|
||||
{questionmark} A question mark: '?'
|
||||
{pipe} A vertical pipe: '|'
|
||||
{openbrace} An open brace: '{'
|
||||
{closebrace} A close brace: '}'
|
||||
@@ -1001,13 +1232,20 @@ Substitution Description
|
||||
{folder_album} Folder path + album photo is contained in. e.g.
|
||||
'Folder/Subfolder/Album' or just 'Album' if no
|
||||
enclosing folder
|
||||
|
||||
{keyword} Keyword(s) assigned to photo
|
||||
{person} Person(s) / face(s) in a photo
|
||||
{label} Image categorization label associated with a photo
|
||||
(Photos 5+ only)
|
||||
(Photos 5+ only). Labels are added automatically by
|
||||
Photos using machine learning algorithms to
|
||||
categorize images. These are not the same as
|
||||
{keyword} which refers to the user-defined
|
||||
keywords/tags applied in Photos.
|
||||
|
||||
{label_normalized} All lower case version of 'label' (Photos 5+ only)
|
||||
{comment} Comment(s) on shared Photos; format is 'Person name:
|
||||
comment text' (Photos 5+ only)
|
||||
|
||||
{exiftool} Format: '{exiftool:GROUP:TAGNAME}'; use exiftool
|
||||
(https://exiftool.org) to extract metadata, in form
|
||||
GROUP:TAGNAME, from image. E.g.
|
||||
@@ -1017,20 +1255,47 @@ Substitution Description
|
||||
names. You must specify group (e.g. EXIF, IPTC, etc)
|
||||
as used in `exiftool -G`. exiftool must be installed
|
||||
in the path to use this template.
|
||||
|
||||
{searchinfo.holiday} Holiday names associated with a photo, e.g.
|
||||
'Christmas Day'; (Photos 5+ only, applied
|
||||
automatically by Photos' image categorization
|
||||
algorithms).
|
||||
|
||||
{searchinfo.activity} Activities associated with a photo, e.g. 'Sporting
|
||||
Event'; (Photos 5+ only, applied automatically by
|
||||
Photos' image categorization algorithms).
|
||||
|
||||
{searchinfo.venue} Venues associated with a photo, e.g. name of
|
||||
restaurant; (Photos 5+ only, applied automatically by
|
||||
Photos' image categorization algorithms).
|
||||
|
||||
{searchinfo.venue_type} Venue types associated with a photo, e.g.
|
||||
'Restaurant'; (Photos 5+ only, applied automatically
|
||||
by Photos' image categorization algorithms).
|
||||
|
||||
{photo} Provides direct access to the PhotoInfo object for
|
||||
the photo. Must be used in format '{photo.property}'
|
||||
where 'property' represents a PhotoInfo property. For
|
||||
example: '{photo.favorite}' is the same as
|
||||
'{favorite}' and '{photo.place.name}' is the same as
|
||||
'{place.name}'. '{photo}' provides access to
|
||||
properties that are not available as separate
|
||||
template fields but it assumes some knowledge of the
|
||||
underlying PhotoInfo class. See
|
||||
https://rhettbull.github.io/osxphotos/ for additional
|
||||
documentation on the PhotoInfo class.
|
||||
|
||||
{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 -->
|
||||
@@ -1240,7 +1505,7 @@ Returns a list of the keywords found in the Photos library
|
||||
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`
|
||||
```python
|
||||
@@ -1248,7 +1513,7 @@ Returns a list of [AlbumInfo](#AlbumInfo) objects representing albums in the dat
|
||||
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.
|
||||
|
||||
@@ -1684,6 +1949,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.
|
||||
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`
|
||||
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 +1975,12 @@ IMG_9854.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`
|
||||
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 +2171,7 @@ In its simplest form, a template statement has the form: `"{template_field}"`, f
|
||||
|
||||
Template statements may contain one or more modifiers. The full syntax is:
|
||||
|
||||
`"pretext{delim+template_field:subfield|filter(path_sep)[find,replace]?bool_value,default}posttext"`
|
||||
`"pretext{delim+template_field:subfield|filter(path_sep)[find,replace] conditional?bool_value,default}posttext"`
|
||||
|
||||
Template statements are white-space sensitive meaning that white space (spaces, tabs) changes the meaning of the template statement.
|
||||
|
||||
@@ -1953,9 +2227,43 @@ e.g. If Photo is in `Album1` in `Folder1`:
|
||||
- `"{folder_album(>)}"` renders to `["Folder1>Album1"]`
|
||||
- `"{folder_album()}"` renders to `["Folder1Album1"]`
|
||||
|
||||
`[find|replace]`: optional text replacement to perform on rendered template value. For example, to replace "/" in an album name, you could use the template `"{album[/,-]}"`. Multiple replacements can be made by appending "|" and adding another find|replace pair. e.g. to replace both "/" and ":" in album name: `"{album[/,-|:,-]}"`. find/replace pairs are not limited to single characters. The "|" character cannot be used in a find/replace pair.
|
||||
`[find,replace]`: optional text replacement to perform on rendered template value. For example, to replace "/" in an album name, you could use the template `"{album[/,-]}"`. Multiple replacements can be made by appending "|" and adding another find|replace pair. e.g. to replace both "/" and ":" in album name: `"{album[/,-|:,-]}"`. find/replace pairs are not limited to single characters. The "|" character cannot be used in a find/replace pair.
|
||||
|
||||
`?bool_value`: Template fields may be evaluated as boolean by appending "?" after the field name (and following "(path_sep)" or "[find/replace]". If a field is True (e.g. photo is HDR and field is `"{hdr}"`) or has any value, the value following the "?" will be used to render the template instead of the actual field value. If the template field evaluates to False (e.g. in above example, photo is not HDR) or has no value (e.g. photo has no title and field is `"{title}"`) then the default value following a "," will be used.
|
||||
`conditional`: optional conditional expression that is evaluated as boolean (True/False) for use with the `?bool_value` modifier. Conditional expressions take the form '` not operator value`' where `not` is an optional modifier that negates the `operator`. Note: the space before the conditional expression is required if you use a conditional expression. Valid comparison operators are:
|
||||
|
||||
- `contains`: template field contains value, similar to python's `in`
|
||||
- `matches`: template field contains exactly value, unlike `contains`: does not match partial matches
|
||||
- `startswith`: template field starts with value
|
||||
- `endswith`: template field ends with value
|
||||
- `<=`: template field is less than or equal to value
|
||||
- `>=`: template field is greater than or equal to value
|
||||
- `<`: template field is less than value
|
||||
- `>`: template field is greater than value
|
||||
- `==`: template field equals value
|
||||
- `!=`: template field does not equal value
|
||||
|
||||
The `value` part of the conditional expression is treated as a bare (unquoted) word/phrase. Multiple values may be separated by '|' (the pipe symbol). `value` is itself a template statement so you can use one or more template fields in `value` which will be resolved before the comparison occurs.
|
||||
|
||||
For example:
|
||||
|
||||
- `{keyword matches Beach}` resolves to True if 'Beach' is a keyword. It would not match keyword 'BeachDay'.
|
||||
- `{keyword contains Beach}` resolves to True if any keyword contains the word 'Beach' so it would match both 'Beach' and 'BeachDay'.
|
||||
- `{photo.score.overall > 0.7}` resolves to True if the photo's overall aesthetic score is greater than 0.7.
|
||||
- `{keyword|lower contains beach}` uses the lower case filter to do case-insensitive matching to match any keyword that contains the word 'beach'.
|
||||
- `{keyword|lower not contains beach}` uses the `not` modifier to negate the comparison so this resolves to True if there is no keyword that matches 'beach'.
|
||||
|
||||
Examples: to export photos that contain certain keywords with the `osxphotos export` command's `--directory` option:
|
||||
|
||||
`--directory "{keyword|lower matches travel|vacation?Travel-Photos,Not-Travel-Photos}"`
|
||||
|
||||
This exports any photo that has keywords 'travel' or 'vacation' into a directory 'Travel-Photos' and all other photos into directory 'Not-Travel-Photos'.
|
||||
|
||||
This can be used to rename files as well, for example:
|
||||
`--filename "{favorite?Favorite-{original_name},{original_name}}"`
|
||||
|
||||
This renames any photo that is a favorite as 'Favorite-ImageName.jpg' (where 'ImageName.jpg' is the original name of the photo) and all other photos with the unmodified original name.
|
||||
|
||||
`?bool_value`: Template fields may be evaluated as boolean (True/False) by appending "?" after the field name (and following "(path_sep)" or "[find/replace]". If a field is True (e.g. photo is HDR and field is `"{hdr}"`) or has any value, the value following the "?" will be used to render the template instead of the actual field value. If the template field evaluates to False (e.g. in above example, photo is not HDR) or has no value (e.g. photo has no title and field is `"{title}"`) then the default value following a "," will be used.
|
||||
|
||||
e.g. if photo is an HDR image,
|
||||
|
||||
@@ -2517,6 +2825,7 @@ The following template field substitutions are availabe for use with `PhotoInfo.
|
||||
|{photo_or_video}|'photo' or 'video' depending on what type the image is. To customize, use default value as in '{photo_or_video,photo=fotos;video=videos}'|
|
||||
|{hdr}|Photo is HDR?; True/False value, use in format '{hdr?VALUE_IF_TRUE,VALUE_IF_FALSE}'|
|
||||
|{edited}|Photo has been edited (has adjustments)?; True/False value, use in format '{edited?VALUE_IF_TRUE,VALUE_IF_FALSE}'|
|
||||
|{favorite}|Photo has been marked as favorite?; True/False value, use in format '{favorite?VALUE_IF_TRUE,VALUE_IF_FALSE}'|
|
||||
|{created.date}|Photo's creation date in ISO format, e.g. '2020-03-22'|
|
||||
|{created.year}|4-digit year of photo creation time|
|
||||
|{created.yy}|2-digit year of photo creation time|
|
||||
@@ -2576,6 +2885,7 @@ The following template field substitutions are availabe for use with `PhotoInfo.
|
||||
|{uuid}|Photo's internal universally unique identifier (UUID) for the photo, a 36-character string unique to the photo, e.g. '128FB4C6-0B16-4E7D-9108-FB2E90DA1546'|
|
||||
|{comma}|A comma: ','|
|
||||
|{semicolon}|A semicolon: ';'|
|
||||
|{questionmark}|A question mark: '?'|
|
||||
|{pipe}|A vertical pipe: '|'|
|
||||
|{openbrace}|An open brace: '{'|
|
||||
|{closebrace}|A close brace: '}'|
|
||||
@@ -2587,7 +2897,7 @@ The following template field substitutions are availabe for use with `PhotoInfo.
|
||||
|{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|
|
||||
|{keyword}|Keyword(s) assigned to photo|
|
||||
|{person}|Person(s) / face(s) in a photo|
|
||||
|{label}|Image categorization label associated with a photo (Photos 5+ only)|
|
||||
|{label}|Image categorization label associated with a photo (Photos 5+ only). Labels are added automatically by Photos using machine learning algorithms to categorize images. These are not the same as {keyword} which refers to the user-defined keywords/tags applied in Photos.|
|
||||
|{label_normalized}|All lower case version of 'label' (Photos 5+ only)|
|
||||
|{comment}|Comment(s) on shared Photos; format is 'Person name: comment text' (Photos 5+ only)|
|
||||
|{exiftool}|Format: '{exiftool:GROUP:TAGNAME}'; use exiftool (https://exiftool.org) to extract metadata, in form GROUP:TAGNAME, from image. E.g. '{exiftool:EXIF:Make}' to get camera make, or {exiftool:IPTC:Keywords} to extract keywords. See https://exiftool.org/TagNames/ for list of valid tag names. You must specify group (e.g. EXIF, IPTC, etc) as used in `exiftool -G`. exiftool must be installed in the path to use this template.|
|
||||
@@ -2595,6 +2905,8 @@ The following template field substitutions are availabe for use with `PhotoInfo.
|
||||
|{searchinfo.activity}|Activities associated with a photo, e.g. 'Sporting Event'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|
||||
|{searchinfo.venue}|Venues associated with a photo, e.g. name of restaurant; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|
||||
|{searchinfo.venue_type}|Venue types associated with a photo, e.g. 'Restaurant'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|
||||
|{photo}|Provides direct access to the PhotoInfo object for the photo. Must be used in format '{photo.property}' where 'property' represents a PhotoInfo property. For example: '{photo.favorite}' is the same as '{favorite}' and '{photo.place.name}' is the same as '{place.name}'. '{photo}' provides access to properties that are not available as separate template fields but it assumes some knowledge of the underlying PhotoInfo class. See https://rhettbull.github.io/osxphotos/ for additional documentation on the PhotoInfo class.|
|
||||
|{function}|Execute a python function from an external file and use return value as template substitution. Use in format: {function:file.py::function_name} where 'file.py' is the name of the python file and 'function_name' is the name of the function to call. The function will be passed the PhotoInfo object for the photo. See https://github.com/RhetTbull/osxphotos/blob/master/examples/template_function.py for an example of how to implement a template function.|
|
||||
<!-- OSXPHOTOS-TEMPLATE-TABLE:END -->
|
||||
|
||||
### Utility Functions
|
||||
@@ -2728,6 +3040,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/davidjroos"><img src="https://avatars.githubusercontent.com/u/15630844?v=4?s=75" width="75px;" alt=""/><br /><sub><b>davidjroos </b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=davidjroos" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://neilpa.me"><img src="https://avatars.githubusercontent.com/u/42419?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Neil Pankey</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=neilpa" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://aaronweb.net/"><img src="https://avatars.githubusercontent.com/u/604665?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Aaron van Geffen</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=AaronVanGeffen" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/ubrandes"><img src="https://avatars.githubusercontent.com/u/59647284?v=4?s=75" width="75px;" alt=""/><br /><sub><b>ubrandes </b></sub></a><br /><a href="#ideas-ubrandes" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Sphinx build info version 1
|
||||
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
|
||||
config: d0470550c1fa9feae481cebbbbc126af
|
||||
config: d8381561e4b55688141d3bad9c7d6dc7
|
||||
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Overview: module code — osxphotos 0.41.0 documentation</title>
|
||||
<title>Overview: module code — osxphotos 0.42.2 documentation</title>
|
||||
<link rel="stylesheet" href="../_static/pygments.css" type="text/css" />
|
||||
<link rel="stylesheet" href="../_static/alabaster.css" type="text/css" />
|
||||
<script id="documentation_options" data-url_root="../" src="../_static/documentation_options.js"></script>
|
||||
@@ -93,7 +93,7 @@
|
||||
©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>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>osxphotos.photoinfo._photoinfo_exifinfo — osxphotos 0.41.0 documentation</title>
|
||||
<title>osxphotos.photoinfo._photoinfo_exifinfo — osxphotos 0.41.4 documentation</title>
|
||||
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
|
||||
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
|
||||
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
|
||||
@@ -183,7 +183,7 @@
|
||||
©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>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>osxphotos.photoinfo._photoinfo_export — osxphotos 0.41.0 documentation</title>
|
||||
<title>osxphotos.photoinfo._photoinfo_export — osxphotos 0.41.10 documentation</title>
|
||||
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
|
||||
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
|
||||
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
|
||||
@@ -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">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">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="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>
|
||||
@@ -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">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">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="sd">""" return all filenames contained in results """</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">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">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">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">",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">"</span>
|
||||
<span class="o">+</span> <span class="sa">f</span><span class="s2">",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">"</span>
|
||||
<span class="o">+</span> <span class="sa">f</span><span class="s2">",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">"</span>
|
||||
<span class="o">+</span> <span class="sa">f</span><span class="s2">",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">"</span>
|
||||
<span class="o">+</span> <span class="sa">f</span><span class="s2">",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">"</span>
|
||||
<span class="o">+</span> <span class="s2">")"</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_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">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="sd">"""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>
|
||||
@@ -560,6 +571,9 @@
|
||||
<span class="sd"> merge_exif_keywords: boolean; if True, merged keywords found in file's exif data (requires exiftool)</span>
|
||||
<span class="sd"> merge_exif_persons: boolean; if True, merged persons found in file'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 "."</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's additive</span>
|
||||
|
||||
<span class="sd"> Returns: ExportResults class </span>
|
||||
<span class="sd"> ExportResults has attributes: </span>
|
||||
@@ -640,9 +654,9 @@
|
||||
<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_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="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="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">"public.jpeg"</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_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">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="n">sidecars</span><span class="o">.</span><span class="n">append</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_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">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="n">sidecars</span><span class="o">.</span><span class="n">append</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">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">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="n">sidecars</span><span class="o">.</span><span class="n">append</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">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">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="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>
|
||||
@@ -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">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">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="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>
|
||||
@@ -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">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">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">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">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">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="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>
|
||||
@@ -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">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">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">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">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">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="sd">"""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"> 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"> 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's additive</span>
|
||||
|
||||
<span class="sd"> Returns:</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">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">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="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_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">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="sd">"""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>
|
||||
@@ -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"> merge_exif_keywords: merge keywords in the file's exif metadata (requires exiftool)</span>
|
||||
<span class="sd"> merge_exif_persons: merge persons in the file'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's additive</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"> XMP:Description (may include template)</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"> 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:PersonInImage</span>
|
||||
<span class="sd"> EXIF:GPSLatitudeRef, EXIF:GPSLongitudeRef</span>
|
||||
@@ -1461,6 +1516,9 @@
|
||||
<span class="sd"> QuickTime:ModifyDate (UTC)</span>
|
||||
<span class="sd"> QuickTime: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"> """</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">" "</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">""</span>
|
||||
<span class="n">exif</span><span class="p">[</span><span class="s2">"EXIF:ImageDescription"</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">"XMP:Description"</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">"IPTC:Caption-Abstract"</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="n">exif</span><span class="p">[</span><span class="s2">"EXIF:ImageDescription"</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">"XMP:Description"</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">"IPTC:Caption-Abstract"</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="n">exif</span><span class="p">[</span><span class="s2">"XMP:Title"</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">"IPTC:ObjectName"</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="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="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">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="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">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="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="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="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="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_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="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>
|
||||
@@ -1547,25 +1609,26 @@
|
||||
<span class="n">exif</span><span class="p">[</span><span class="s2">"XMP:Subject"</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">"XMP:TagsList"</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">exif</span><span class="p">[</span><span class="s2">"XMP:PersonInImage"</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"># exif["Rating"] = 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">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="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">"EXIF:GPSLatitude"</span><span class="p">]</span> <span class="o">=</span> <span class="n">lat</span>
|
||||
<span class="n">exif</span><span class="p">[</span><span class="s2">"EXIF:GPSLongitude"</span><span class="p">]</span> <span class="o">=</span> <span class="n">lon</span>
|
||||
<span class="n">lat_ref</span> <span class="o">=</span> <span class="s2">"N"</span> <span class="k">if</span> <span class="n">lat</span> <span class="o">>=</span> <span class="mi">0</span> <span class="k">else</span> <span class="s2">"S"</span>
|
||||
<span class="n">lon_ref</span> <span class="o">=</span> <span class="s2">"E"</span> <span class="k">if</span> <span class="n">lon</span> <span class="o">>=</span> <span class="mi">0</span> <span class="k">else</span> <span class="s2">"W"</span>
|
||||
<span class="n">exif</span><span class="p">[</span><span class="s2">"EXIF:GPSLatitudeRef"</span><span class="p">]</span> <span class="o">=</span> <span class="n">lat_ref</span>
|
||||
<span class="n">exif</span><span class="p">[</span><span class="s2">"EXIF:GPSLongitudeRef"</span><span class="p">]</span> <span class="o">=</span> <span class="n">lon_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">"Keys:GPSCoordinates"</span><span class="p">]</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"</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">"</span>
|
||||
<span class="n">exif</span><span class="p">[</span><span class="s2">"UserData:GPSCoordinates"</span><span class="p">]</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"</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">"</span>
|
||||
<span class="k">if</span> <span class="n">location</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="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="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">"EXIF:GPSLatitude"</span><span class="p">]</span> <span class="o">=</span> <span class="n">lat</span>
|
||||
<span class="n">exif</span><span class="p">[</span><span class="s2">"EXIF:GPSLongitude"</span><span class="p">]</span> <span class="o">=</span> <span class="n">lon</span>
|
||||
<span class="n">lat_ref</span> <span class="o">=</span> <span class="s2">"N"</span> <span class="k">if</span> <span class="n">lat</span> <span class="o">>=</span> <span class="mi">0</span> <span class="k">else</span> <span class="s2">"S"</span>
|
||||
<span class="n">lon_ref</span> <span class="o">=</span> <span class="s2">"E"</span> <span class="k">if</span> <span class="n">lon</span> <span class="o">>=</span> <span class="mi">0</span> <span class="k">else</span> <span class="s2">"W"</span>
|
||||
<span class="n">exif</span><span class="p">[</span><span class="s2">"EXIF:GPSLatitudeRef"</span><span class="p">]</span> <span class="o">=</span> <span class="n">lat_ref</span>
|
||||
<span class="n">exif</span><span class="p">[</span><span class="s2">"EXIF:GPSLongitudeRef"</span><span class="p">]</span> <span class="o">=</span> <span class="n">lon_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">"Keys:GPSCoordinates"</span><span class="p">]</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"</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">"</span>
|
||||
<span class="n">exif</span><span class="p">[</span><span class="s2">"UserData:GPSCoordinates"</span><span class="p">]</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"</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">"</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>
|
||||
@@ -1624,6 +1687,13 @@
|
||||
<span class="n">exif</span><span class="p">[</span><span class="s2">"QuickTime:ModifyDate"</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="p">)</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">"%Y:%m:</span><span class="si">%d</span><span class="s2"> %H:%M:%S"</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">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span> <span class="s2">" "</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">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span> <span class="s2">" "</span><span class="p">)</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">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>
|
||||
|
||||
|
||||
@@ -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_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">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="sd">"""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>
|
||||
@@ -1687,13 +1760,18 @@
|
||||
<span class="sd"> merge_exif_keywords: boolean; if True, merged keywords found in file's exif data (requires exiftool)</span>
|
||||
<span class="sd"> merge_exif_persons: boolean; if True, merged persons found in file'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"> 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's additive</span>
|
||||
|
||||
<span class="sd"> Returns: dict with exiftool tags / values</span>
|
||||
|
||||
<span class="sd"> Exports the following:</span>
|
||||
<span class="sd"> EXIF:ImageDescription</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"> IPTC:ObjectName</span>
|
||||
<span class="sd"> XMP:TagsList</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>
|
||||
@@ -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_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">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="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">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">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="sd">"""returns string for XMP sidecar</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"> merge_exif_keywords: boolean; if True, merged keywords found in file's exif data (requires exiftool)</span>
|
||||
<span class="sd"> merge_exif_persons: boolean; if True, merged persons found in file'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's additive</span>
|
||||
<span class="sd"> """</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="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="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="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="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">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="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="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="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="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_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="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>
|
||||
@@ -1820,11 +1908,14 @@
|
||||
<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="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">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">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>
|
||||
@@ -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">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">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="p">)</span>
|
||||
|
||||
@@ -1912,7 +2004,7 @@
|
||||
©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>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>osxphotos.photoinfo._photoinfo_scoreinfo — osxphotos 0.41.0 documentation</title>
|
||||
<title>osxphotos.photoinfo._photoinfo_scoreinfo — osxphotos 0.41.4 documentation</title>
|
||||
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
|
||||
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
|
||||
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
|
||||
@@ -208,7 +208,7 @@
|
||||
©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>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>osxphotos.photoinfo._photoinfo_searchinfo — osxphotos 0.41.0 documentation</title>
|
||||
<title>osxphotos.photoinfo._photoinfo_searchinfo — osxphotos 0.41.4 documentation</title>
|
||||
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
|
||||
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
|
||||
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
|
||||
@@ -366,7 +366,7 @@
|
||||
©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>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>osxphotos.photoinfo.photoinfo — osxphotos 0.41.0 documentation</title>
|
||||
<title>osxphotos.photoinfo.photoinfo — osxphotos 0.41.6 documentation</title>
|
||||
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
|
||||
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
|
||||
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
|
||||
@@ -60,6 +60,7 @@
|
||||
<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="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">..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>
|
||||
@@ -485,9 +486,24 @@
|
||||
<span class="p">)</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_albums</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">burst_albums</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""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"""</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst_selected</span> <span class="ow">or</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">albums</span>
|
||||
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_burst_albums</span>
|
||||
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
|
||||
<span class="n">burst_albums</span> <span class="o">=</span> <span class="p">[]</span>
|
||||
<span class="k">for</span> <span class="n">photo</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst_photos</span><span class="p">:</span>
|
||||
<span class="n">burst_albums</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">photo</span><span class="o">.</span><span class="n">albums</span><span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_burst_albums</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">burst_albums</span><span class="p">))</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_burst_albums</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">album_info</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" list of AlbumInfo objects representing albums the photos is contained in """</span>
|
||||
<span class="sd">""" list of AlbumInfo objects representing albums the photo is contained in """</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_album_info</span>
|
||||
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
|
||||
@@ -497,6 +513,21 @@
|
||||
<span class="p">]</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_album_info</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">burst_album_info</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" 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 """</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst_selected</span> <span class="ow">or</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">album_info</span>
|
||||
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_burst_album_info</span>
|
||||
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
|
||||
<span class="n">burst_album_info</span> <span class="o">=</span> <span class="p">[]</span>
|
||||
<span class="k">for</span> <span class="n">photo</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">burst_photos</span><span class="p">:</span>
|
||||
<span class="n">burst_album_info</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">photo</span><span class="o">.</span><span class="n">album_info</span><span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_burst_album_info</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">burst_album_info</span><span class="p">))</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_burst_album_info</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">import_info</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">""" ImportInfo object representing import session for the photo or None if no import session """</span>
|
||||
@@ -543,6 +574,30 @@
|
||||
<span class="sd">""" True if picture has adjustments / edits """</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">"hasAdjustments"</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">""" Returns AdjustmentsInfo class for adjustment data or None if no adjustments; Photos 5+ only """</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"><=</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">"resources"</span>
|
||||
<span class="o">/</span> <span class="s2">"renders"</span>
|
||||
<span class="o">/</span> <span class="n">directory</span>
|
||||
<span class="o">/</span> <span class="sa">f</span><span class="s2">"</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"</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="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">""" Returns True if picture was edited outside of Photos using external editor """</span>
|
||||
@@ -688,6 +743,11 @@
|
||||
<span class="sd">""" Returns True if photo is part of a Burst photo set, otherwise False """</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">"burst"</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">""" Returns True if photo is a burst photo and has been selected from the burst set by the user, otherwise False """</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">"burst_key"</span><span class="p">]</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">burst_photos</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""If photo is a burst photo, returns list of PhotoInfo objects</span>
|
||||
@@ -856,8 +916,19 @@
|
||||
|
||||
<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="sd">""" returns EXIF orientation of the current photo version as int """</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">"orientation"</span><span class="p">]</span>
|
||||
<span class="sd">""" returns EXIF orientation of the current photo version as int or 0 if current orientation cannot be determined """</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"><=</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">"orientation"</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'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">"orientation"</span><span class="p">]</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>
|
||||
@@ -1185,7 +1256,7 @@
|
||||
©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>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>osxphotos.photosdb.photosdb — osxphotos 0.41.0 documentation</title>
|
||||
<title>osxphotos.photosdb.photosdb — osxphotos 0.41.10 documentation</title>
|
||||
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
|
||||
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
|
||||
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
|
||||
@@ -1861,7 +1861,6 @@
|
||||
|
||||
<span class="c1"># get details about photos</span>
|
||||
<span class="n">verbose</span><span class="p">(</span><span class="s2">"Processing photo details."</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">"Getting information about photos"</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">"""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>
|
||||
@@ -2074,6 +2073,7 @@
|
||||
<span class="c1"># > 6 = portrait (sometimes, see ZDEPTHSTATE/ZDEPTHTYPE)</span>
|
||||
<span class="n">info</span><span class="p">[</span><span class="s2">"customRenderedValue"</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">"hdr"</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">"depth_state"</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">"portrait"</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>
|
||||
@@ -2768,8 +2768,6 @@
|
||||
<span class="c1"># an empty album will be in _dbalbum_titles but not _dbalbums_album</span>
|
||||
<span class="k">pass</span>
|
||||
<span class="n">album_set</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="n">title_set</span><span class="p">)</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Could not find album '</span><span class="si">{</span><span class="n">album</span><span class="si">}</span><span class="s2">' in database"</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>
|
||||
@@ -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">if</span> <span class="n">u</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">:</span>
|
||||
<span class="n">uuid_set</span><span class="o">.</span><span class="n">update</span><span class="p">([</span><span class="n">u</span><span class="p">])</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Could not find uuid '</span><span class="si">{</span><span class="n">u</span><span class="si">}</span><span class="s2">' in database"</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>
|
||||
@@ -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">if</span> <span class="n">keyword</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_keyword</span><span class="p">:</span>
|
||||
<span class="n">keyword_set</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_keyword</span><span class="p">[</span><span class="n">keyword</span><span class="p">])</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Could not find keyword '</span><span class="si">{</span><span class="n">keyword</span><span class="si">}</span><span class="s2">' in database"</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>
|
||||
@@ -2800,8 +2794,6 @@
|
||||
<span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
|
||||
<span class="c1"># some persons have zero photos so they won't be in _dbfaces_pk</span>
|
||||
<span class="k">pass</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Could not find person '</span><span class="si">{</span><span class="n">person</span><span class="si">}</span><span class="s2">' in database"</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>
|
||||
@@ -2812,14 +2804,10 @@
|
||||
<span class="n">dsel</span> <span class="o">=</span> <span class="p">{</span>
|
||||
<span class="n">k</span><span class="p">:</span> <span class="n">v</span> <span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">dsel</span><span class="o">.</span><span class="n">items</span><span class="p">()</span> <span class="k">if</span> <span class="n">v</span><span class="p">[</span><span class="s2">"imageDate"</span><span class="p">]</span> <span class="o">>=</span> <span class="n">from_date</span>
|
||||
<span class="p">}</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"Found %i items with from_date </span><span class="si">{</span><span class="n">from_date</span><span class="si">}</span><span class="s2">"</span> <span class="o">%</span> <span class="nb">len</span><span class="p">(</span><span class="n">dsel</span><span class="p">)</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">if</span> <span class="n">to_date</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">datetime_has_tz</span><span class="p">(</span><span class="n">to_date</span><span class="p">):</span>
|
||||
<span class="n">to_date</span> <span class="o">=</span> <span class="n">datetime_naive_to_local</span><span class="p">(</span><span class="n">to_date</span><span class="p">)</span>
|
||||
<span class="n">dsel</span> <span class="o">=</span> <span class="p">{</span><span class="n">k</span><span class="p">:</span> <span class="n">v</span> <span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">dsel</span><span class="o">.</span><span class="n">items</span><span class="p">()</span> <span class="k">if</span> <span class="n">v</span><span class="p">[</span><span class="s2">"imageDate"</span><span class="p">]</span> <span class="o"><=</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">"Found %i items with to_date </span><span class="si">{</span><span class="n">to_date</span><span class="si">}</span><span class="s2">"</span> <span class="o">%</span> <span class="nb">len</span><span class="p">(</span><span class="n">dsel</span><span class="p">))</span>
|
||||
<span class="n">photos_sets</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">dsel</span><span class="o">.</span><span class="n">keys</span><span class="p">()))</span>
|
||||
|
||||
<span class="n">photoinfo</span> <span class="o">=</span> <span class="p">[]</span>
|
||||
@@ -2951,7 +2939,7 @@
|
||||
©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>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
</div>
|
||||
|
||||
7
docs/_static/doctools.js
vendored
7
docs/_static/doctools.js
vendored
@@ -29,9 +29,14 @@ if (!window.console || !console.firebug) {
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return decodeURIComponent(x).replace(/\+/g, ' ');
|
||||
if (!x) {
|
||||
return x
|
||||
}
|
||||
return decodeURIComponent(x.replace(/\+/g, ' '));
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
2
docs/_static/documentation_options.js
vendored
2
docs/_static/documentation_options.js
vendored
@@ -1,6 +1,6 @@
|
||||
var DOCUMENTATION_OPTIONS = {
|
||||
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
|
||||
VERSION: '0.41.0',
|
||||
VERSION: '0.42.2',
|
||||
LANGUAGE: 'None',
|
||||
COLLAPSE_INDEX: false,
|
||||
BUILDER: 'html',
|
||||
|
||||
4
docs/_static/language_data.js
vendored
4
docs/_static/language_data.js
vendored
@@ -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"];
|
||||
|
||||
|
||||
/* 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
|
||||
*/
|
||||
@@ -199,7 +200,6 @@ var Stemmer = function() {
|
||||
|
||||
|
||||
|
||||
|
||||
var splitChars = (function() {
|
||||
var result = {};
|
||||
var singles = [96, 180, 187, 191, 215, 247, 749, 885, 903, 907, 909, 930, 1014, 1648,
|
||||
|
||||
6
docs/_static/pygments.css
vendored
6
docs/_static/pygments.css
vendored
@@ -1,7 +1,7 @@
|
||||
pre { line-height: 125%; }
|
||||
td.linenos pre { color: #000000; background-color: #f0f0f0; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos { color: #000000; background-color: #f0f0f0; padding-left: 5px; padding-right: 5px; }
|
||||
td.linenos pre.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos { color: inherit; background-color: transparent; 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; }
|
||||
.highlight .hll { background-color: #ffffcc }
|
||||
.highlight { background: #f8f8f8; }
|
||||
|
||||
26
docs/_static/searchtools.js
vendored
26
docs/_static/searchtools.js
vendored
@@ -248,7 +248,7 @@ var Search = {
|
||||
// results left, load the summary and display it
|
||||
if (results.length) {
|
||||
var item = results.pop();
|
||||
var listItem = $('<li style="display:none"></li>');
|
||||
var listItem = $('<li></li>');
|
||||
var requestUrl = "";
|
||||
var linkUrl = "";
|
||||
if (DOCUMENTATION_OPTIONS.BUILDER === 'dirhtml') {
|
||||
@@ -273,9 +273,9 @@ var Search = {
|
||||
if (item[3]) {
|
||||
listItem.append($('<span> (' + item[3] + ')</span>'));
|
||||
Search.output.append(listItem);
|
||||
listItem.slideDown(5, function() {
|
||||
setTimeout(function() {
|
||||
displayNextItem();
|
||||
});
|
||||
}, 5);
|
||||
} else if (DOCUMENTATION_OPTIONS.HAS_SOURCE) {
|
||||
$.ajax({url: requestUrl,
|
||||
dataType: "text",
|
||||
@@ -285,16 +285,16 @@ var Search = {
|
||||
listItem.append(Search.makeSearchSummary(data, searchterms, hlterms));
|
||||
}
|
||||
Search.output.append(listItem);
|
||||
listItem.slideDown(5, function() {
|
||||
setTimeout(function() {
|
||||
displayNextItem();
|
||||
});
|
||||
}, 5);
|
||||
}});
|
||||
} else {
|
||||
// no source available, just display title
|
||||
Search.output.append(listItem);
|
||||
listItem.slideDown(5, function() {
|
||||
setTimeout(function() {
|
||||
displayNextItem();
|
||||
});
|
||||
}, 5);
|
||||
}
|
||||
}
|
||||
// search finished, update title and status message
|
||||
@@ -379,6 +379,13 @@ var Search = {
|
||||
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
|
||||
*/
|
||||
@@ -402,13 +409,14 @@ var Search = {
|
||||
];
|
||||
// add support for partial matches
|
||||
if (word.length > 2) {
|
||||
var word_regex = this.escapeRegExp(word);
|
||||
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})
|
||||
}
|
||||
}
|
||||
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})
|
||||
}
|
||||
}
|
||||
|
||||
2027
docs/_static/underscore-1.12.0.js
vendored
Normal file
2027
docs/_static/underscore-1.12.0.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
37
docs/_static/underscore.js
vendored
37
docs/_static/underscore.js
vendored
File diff suppressed because one or more lines are too long
482
docs/cli.html
482
docs/cli.html
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Index — osxphotos 0.41.0 documentation</title>
|
||||
<title>Index — osxphotos 0.42.2 documentation</title>
|
||||
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
||||
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
||||
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
|
||||
@@ -327,6 +327,15 @@
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-from-date">osxphotos-export command line option</a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-from-date">osxphotos-query command line option</a>
|
||||
</li>
|
||||
</ul></li>
|
||||
<li>
|
||||
--from-time <from_time>
|
||||
|
||||
<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>
|
||||
</ul></li>
|
||||
<li>
|
||||
@@ -509,6 +518,15 @@
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-missing">osxphotos-export command line option</a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-missing">osxphotos-query command line option</a>
|
||||
</li>
|
||||
</ul></li>
|
||||
<li>
|
||||
--name <FILENAME>
|
||||
|
||||
<ul>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-name">osxphotos-export command line option</a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-name">osxphotos-query command line option</a>
|
||||
</li>
|
||||
</ul></li>
|
||||
<li>
|
||||
@@ -529,8 +547,6 @@
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-no-description">osxphotos-query command line option</a>
|
||||
</li>
|
||||
</ul></li>
|
||||
</ul></td>
|
||||
<td style="width: 33%; vertical-align: top;"><ul>
|
||||
<li>
|
||||
--no-likes
|
||||
|
||||
@@ -540,6 +556,8 @@
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-no-likes">osxphotos-query command line option</a>
|
||||
</li>
|
||||
</ul></li>
|
||||
</ul></td>
|
||||
<td style="width: 33%; vertical-align: top;"><ul>
|
||||
<li>
|
||||
--no-place
|
||||
|
||||
@@ -776,6 +794,22 @@
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-portrait">osxphotos-export command line option</a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-portrait">osxphotos-query command line option</a>
|
||||
</li>
|
||||
</ul></li>
|
||||
<li>
|
||||
--query-eval <CRITERIA>
|
||||
|
||||
<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>
|
||||
</ul></li>
|
||||
<li>
|
||||
@@ -783,6 +817,13 @@
|
||||
|
||||
<ul>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-report">osxphotos-export command line option</a>
|
||||
</li>
|
||||
</ul></li>
|
||||
<li>
|
||||
--retry <RETRY>
|
||||
|
||||
<ul>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-retry">osxphotos-export command line option</a>
|
||||
</li>
|
||||
</ul></li>
|
||||
<li>
|
||||
@@ -909,6 +950,15 @@
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-to-date">osxphotos-export command line option</a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-to-date">osxphotos-query command line option</a>
|
||||
</li>
|
||||
</ul></li>
|
||||
<li>
|
||||
--to-time <to_time>
|
||||
|
||||
<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>
|
||||
</ul></li>
|
||||
<li>
|
||||
@@ -1017,6 +1067,8 @@
|
||||
<table style="width: 100%" class="indextable genindextable"><tr>
|
||||
<td style="width: 33%; vertical-align: top;"><ul>
|
||||
<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><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>
|
||||
</ul></li>
|
||||
<li><a href="reference.html#osxphotos.PhotosDB.albums_as_dict">albums_as_dict() (osxphotos.PhotosDB property)</a>
|
||||
</li>
|
||||
</ul></td>
|
||||
<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>
|
||||
<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><a href="reference.html#osxphotos.PhotoInfo.ExifInfo.bit_rate">bit_rate (osxphotos.PhotoInfo.ExifInfo attribute)</a>
|
||||
</li>
|
||||
</ul></td>
|
||||
<td style="width: 33%; vertical-align: top;"><ul>
|
||||
<li><a href="reference.html#osxphotos.PhotoInfo.SearchInfo.bodies_of_water">bodies_of_water() (osxphotos.PhotoInfo.SearchInfo property)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#osxphotos.PhotoInfo.burst">burst() (osxphotos.PhotoInfo property)</a>
|
||||
</li>
|
||||
</ul></td>
|
||||
<td style="width: 33%; vertical-align: top;"><ul>
|
||||
<li><a href="reference.html#osxphotos.PhotoInfo.burst_album_info">burst_album_info() (osxphotos.PhotoInfo property)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#osxphotos.PhotoInfo.burst_albums">burst_albums() (osxphotos.PhotoInfo property)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#osxphotos.PhotoInfo.burst_photos">burst_photos() (osxphotos.PhotoInfo property)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#osxphotos.PhotoInfo.burst_selected">burst_selected() (osxphotos.PhotoInfo property)</a>
|
||||
</li>
|
||||
</ul></td>
|
||||
</tr></table>
|
||||
@@ -1461,6 +1519,8 @@
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-folder">--folder <FOLDER></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-from-date">--from-date <from_date></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-from-time">--from-time <from_time></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-has-comment">--has-comment</a>
|
||||
</li>
|
||||
@@ -1497,6 +1557,8 @@
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-load-config">--load-config <config file path></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-missing">--missing</a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-name">--name <FILENAME></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-no-comment">--no-comment</a>
|
||||
</li>
|
||||
@@ -1553,8 +1615,14 @@
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-place">--place <PLACE></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-portrait">--portrait</a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-query-eval">--query-eval <CRITERIA></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-replace-keywords">--replace-keywords</a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-report">--report <path to export report></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-retry">--retry <RETRY></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-save-config">--save-config <config file path></a>
|
||||
</li>
|
||||
@@ -1587,6 +1655,8 @@
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-title">--title <TITLE></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-to-date">--to-date <to_date></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-to-time">--to-time <to_time></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-export-touch-file">--touch-file</a>
|
||||
</li>
|
||||
@@ -1713,6 +1783,8 @@
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-folder">--folder <FOLDER></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-from-date">--from-date <from_date></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-from-time">--from-time <from_time></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-has-comment">--has-comment</a>
|
||||
</li>
|
||||
@@ -1741,6 +1813,8 @@
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-live">--live</a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-missing">--missing</a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-name">--name <FILENAME></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-no-comment">--no-comment</a>
|
||||
</li>
|
||||
@@ -1795,6 +1869,8 @@
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-place">--place <PLACE></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-portrait">--portrait</a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-query-eval">--query-eval <CRITERIA></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-screenshot">--screenshot</a>
|
||||
</li>
|
||||
@@ -1809,6 +1885,8 @@
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-title">--title <TITLE></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-to-date">--to-date <to_date></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-to-time">--to-time <to_time></a>
|
||||
</li>
|
||||
<li><a href="cli.html#cmdoption-osxphotos-query-uti">--uti <UTI></a>
|
||||
</li>
|
||||
@@ -2108,7 +2186,7 @@
|
||||
©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>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Welcome to osxphotos’s documentation! — osxphotos 0.41.0 documentation</title>
|
||||
<title>Welcome to osxphotos’s documentation! — osxphotos 0.42.2 documentation</title>
|
||||
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
||||
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
||||
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
|
||||
@@ -365,7 +365,7 @@ Alternatively, you can also run the command line utility like this: <code class=
|
||||
©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>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
|
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>osxphotos — osxphotos 0.41.0 documentation</title>
|
||||
<title>osxphotos — osxphotos 0.42.2 documentation</title>
|
||||
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
||||
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
||||
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
|
||||
@@ -91,7 +91,7 @@
|
||||
©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>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
|
|
||||
|
||||
BIN
docs/objects.inv
BIN
docs/objects.inv
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Search — osxphotos 0.41.0 documentation</title>
|
||||
<title>Search — osxphotos 0.42.2 documentation</title>
|
||||
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
||||
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
©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>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
</div>
|
||||
|
||||
File diff suppressed because one or more lines are too long
30
examples/template_function.py
Normal file
30
examples/template_function.py
Normal 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
|
||||
@@ -1,3 +1,3 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.41.2"
|
||||
__version__ = "0.42.3"
|
||||
|
||||
496
osxphotos/cli.py
496
osxphotos/cli.py
@@ -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
|
||||
class CLI_Obj:
|
||||
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. '
|
||||
"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(
|
||||
"--uuid",
|
||||
metavar="UUID",
|
||||
@@ -386,14 +410,24 @@ def query_options(f):
|
||||
),
|
||||
o(
|
||||
"--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(),
|
||||
),
|
||||
o(
|
||||
"--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(),
|
||||
),
|
||||
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("--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."),
|
||||
@@ -413,6 +447,20 @@ def query_options(f):
|
||||
is_flag=True,
|
||||
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]:
|
||||
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. "
|
||||
"(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(
|
||||
"--export-by-date",
|
||||
is_flag=True,
|
||||
@@ -782,7 +837,10 @@ def cli(ctx, db, json_, debug):
|
||||
"--cleanup",
|
||||
is_flag=True,
|
||||
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(
|
||||
"--exportdb",
|
||||
@@ -836,6 +894,7 @@ def export(
|
||||
album,
|
||||
folder,
|
||||
uuid,
|
||||
name,
|
||||
uuid_from_file,
|
||||
title,
|
||||
no_title,
|
||||
@@ -853,6 +912,8 @@ def export(
|
||||
not_shared,
|
||||
from_date,
|
||||
to_date,
|
||||
from_time,
|
||||
to_time,
|
||||
verbose,
|
||||
missing,
|
||||
update,
|
||||
@@ -862,6 +923,7 @@ def export(
|
||||
export_as_hardlink,
|
||||
touch_file,
|
||||
overwrite,
|
||||
retry,
|
||||
export_by_date,
|
||||
skip_edited,
|
||||
skip_original_if_edited,
|
||||
@@ -936,6 +998,7 @@ def export(
|
||||
beta,
|
||||
in_album,
|
||||
not_in_album,
|
||||
query_eval,
|
||||
):
|
||||
"""Export photos from the Photos database.
|
||||
Export path DEST is required.
|
||||
@@ -982,6 +1045,7 @@ def export(
|
||||
person = cfg.person
|
||||
album = cfg.album
|
||||
folder = cfg.folder
|
||||
name = cfg.name
|
||||
uuid = cfg.uuid
|
||||
uuid_from_file = cfg.uuid_from_file
|
||||
title = cfg.title
|
||||
@@ -1000,6 +1064,8 @@ def export(
|
||||
not_shared = cfg.not_shared
|
||||
from_date = cfg.from_date
|
||||
to_date = cfg.to_date
|
||||
from_time = cfg.from_time
|
||||
to_time = cfg.to_time
|
||||
verbose = cfg.verbose
|
||||
missing = cfg.missing
|
||||
update = cfg.update
|
||||
@@ -1008,6 +1074,7 @@ def export(
|
||||
export_as_hardlink = cfg.export_as_hardlink
|
||||
touch_file = cfg.touch_file
|
||||
overwrite = cfg.overwrite
|
||||
retry = cfg.retry
|
||||
export_by_date = cfg.export_by_date
|
||||
skip_edited = cfg.skip_edited
|
||||
skip_original_if_edited = cfg.skip_original_if_edited
|
||||
@@ -1079,6 +1146,7 @@ def export(
|
||||
only_new = cfg.only_new
|
||||
in_album = cfg.in_album
|
||||
not_in_album = cfg.not_in_album
|
||||
query_eval = cfg.query_eval
|
||||
|
||||
# config file might have changed verbose
|
||||
VERBOSE = bool(verbose)
|
||||
@@ -1167,6 +1235,7 @@ def export(
|
||||
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):
|
||||
click.echo(
|
||||
@@ -1347,6 +1416,8 @@ def export(
|
||||
not_incloud=False,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
from_time=from_time,
|
||||
to_time=to_time,
|
||||
portrait=portrait,
|
||||
not_portrait=not_portrait,
|
||||
screenshot=screenshot,
|
||||
@@ -1374,6 +1445,11 @@ def export(
|
||||
is_reference=is_reference,
|
||||
in_album=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:
|
||||
@@ -1382,13 +1458,6 @@ def export(
|
||||
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]
|
||||
|
||||
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)
|
||||
# TODO: photos or photo appears several times, pull into a separate function
|
||||
photo_str = "photos" if num_photos > 1 else "photo"
|
||||
@@ -1445,6 +1514,7 @@ def export(
|
||||
strip=strip,
|
||||
jpeg_ext=jpeg_ext,
|
||||
replace_keywords=replace_keywords,
|
||||
retry=retry,
|
||||
)
|
||||
results += export_results
|
||||
|
||||
@@ -1506,10 +1576,14 @@ def export(
|
||||
+ [str(pathlib.Path(export_db_path).resolve())]
|
||||
)
|
||||
click.echo(f"Cleaning up {dest}")
|
||||
(cleaned_files, cleaned_dirs) = cleanup_files(dest, all_files, fileutil)
|
||||
file_str = "files" if cleaned_files != 1 else "file"
|
||||
dir_str = "directories" if cleaned_dirs != 1 else "directory"
|
||||
click.echo(f"Deleted: {cleaned_files} {file_str}, {cleaned_dirs} {dir_str}")
|
||||
cleaned_files, cleaned_dirs = cleanup_files(dest, all_files, fileutil)
|
||||
file_str = "files" if len(cleaned_files) != 1 else "file"
|
||||
dir_str = "directories" if len(cleaned_dirs) != 1 else "directory"
|
||||
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:
|
||||
verbose_(f"Writing export report to {report}")
|
||||
@@ -1600,6 +1674,7 @@ def query(
|
||||
person,
|
||||
album,
|
||||
folder,
|
||||
name,
|
||||
uuid,
|
||||
uuid_from_file,
|
||||
title,
|
||||
@@ -1631,6 +1706,8 @@ def query(
|
||||
not_incloud,
|
||||
from_date,
|
||||
to_date,
|
||||
from_time,
|
||||
to_time,
|
||||
portrait,
|
||||
not_portrait,
|
||||
screenshot,
|
||||
@@ -1658,6 +1735,7 @@ def query(
|
||||
is_reference,
|
||||
in_album,
|
||||
not_in_album,
|
||||
query_eval,
|
||||
):
|
||||
"""Query the Photos database using 1 or more search options;
|
||||
if more than one option is provided, they are treated as "AND"
|
||||
@@ -1671,6 +1749,7 @@ def query(
|
||||
person,
|
||||
album,
|
||||
folder,
|
||||
name,
|
||||
uuid,
|
||||
uuid_from_file,
|
||||
edited,
|
||||
@@ -1679,8 +1758,11 @@ def query(
|
||||
has_raw,
|
||||
from_date,
|
||||
to_date,
|
||||
from_time,
|
||||
to_time,
|
||||
label,
|
||||
is_reference,
|
||||
query_eval,
|
||||
]
|
||||
exclusive = [
|
||||
(favorite, not_favorite),
|
||||
@@ -1773,6 +1855,8 @@ def query(
|
||||
not_incloud=not_incloud,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
from_time=from_time,
|
||||
to_time=to_time,
|
||||
portrait=portrait,
|
||||
not_portrait=not_portrait,
|
||||
screenshot=screenshot,
|
||||
@@ -1800,6 +1884,8 @@ def query(
|
||||
is_reference=is_reference,
|
||||
in_album=in_album,
|
||||
not_in_album=not_in_album,
|
||||
name=name,
|
||||
query_eval=query_eval,
|
||||
)
|
||||
|
||||
# below needed for to make CliRunner work for testing
|
||||
@@ -1946,6 +2032,8 @@ def _query(
|
||||
not_incloud=None,
|
||||
from_date=None,
|
||||
to_date=None,
|
||||
from_time=None,
|
||||
to_time=None,
|
||||
portrait=None,
|
||||
not_portrait=None,
|
||||
screenshot=None,
|
||||
@@ -1973,6 +2061,10 @@ def _query(
|
||||
is_reference=False,
|
||||
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
|
||||
|
||||
@@ -2039,30 +2131,38 @@ def _query(
|
||||
if title:
|
||||
# search title field for text
|
||||
# if more than one, find photos with all title values in title
|
||||
photo_list = []
|
||||
if ignore_case:
|
||||
# case-insensitive
|
||||
for t in title:
|
||||
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:
|
||||
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:
|
||||
photos = [p for p in photos if not p.title]
|
||||
|
||||
if description:
|
||||
# search description field for text
|
||||
# if more than one, find photos with all description values in description
|
||||
photo_list = []
|
||||
if ignore_case:
|
||||
# case-insensitive
|
||||
for d in description:
|
||||
d = d.lower()
|
||||
photos = [
|
||||
p for p in photos if p.description and d in p.description.lower()
|
||||
]
|
||||
photo_list.extend(
|
||||
[p for p in photos if p.description and d in p.description.lower()]
|
||||
)
|
||||
else:
|
||||
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:
|
||||
photos = [p for p in photos if not p.description]
|
||||
|
||||
@@ -2210,6 +2310,65 @@ def _query(
|
||||
elif not_in_album:
|
||||
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
|
||||
|
||||
|
||||
@@ -2282,6 +2441,7 @@ def export_photo(
|
||||
strip=False,
|
||||
jpeg_ext=None,
|
||||
replace_keywords=False,
|
||||
retry=0,
|
||||
):
|
||||
"""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)
|
||||
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
|
||||
retry: retry up to retry # of times if there's an error
|
||||
|
||||
Returns:
|
||||
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)
|
||||
)
|
||||
else:
|
||||
try:
|
||||
export_results = photo.export2(
|
||||
dest_path,
|
||||
original_filename,
|
||||
sidecar=sidecar_flags,
|
||||
sidecar_drop_ext=sidecar_drop_ext,
|
||||
live_photo=export_live,
|
||||
raw_photo=export_raw,
|
||||
export_as_hardlink=export_as_hardlink,
|
||||
overwrite=overwrite,
|
||||
use_photos_export=use_photos_export,
|
||||
exiftool=exiftool,
|
||||
merge_exif_keywords=exiftool_merge_keywords,
|
||||
merge_exif_persons=exiftool_merge_persons,
|
||||
use_albums_as_keywords=album_keyword,
|
||||
use_persons_as_keywords=person_keyword,
|
||||
keyword_template=keyword_template,
|
||||
description_template=description_template,
|
||||
update=update,
|
||||
ignore_signature=ignore_signature,
|
||||
export_db=export_db,
|
||||
fileutil=fileutil,
|
||||
dry_run=dry_run,
|
||||
touch_file=touch_file,
|
||||
convert_to_jpeg=convert_to_jpeg,
|
||||
jpeg_quality=jpeg_quality,
|
||||
ignore_date_modified=ignore_date_modified,
|
||||
use_photokit=use_photokit,
|
||||
verbose=verbose_,
|
||||
exiftool_flags=exiftool_option,
|
||||
jpeg_ext=jpeg_ext,
|
||||
replace_keywords=replace_keywords,
|
||||
)
|
||||
results += export_results
|
||||
for warning_ in export_results.exiftool_warning:
|
||||
verbose_(
|
||||
f"exiftool warning for file {warning_[0]}: {warning_[1]}"
|
||||
tries = 0
|
||||
while tries <= retry:
|
||||
tries += 1
|
||||
error = 0
|
||||
try:
|
||||
export_results = photo.export2(
|
||||
dest_path,
|
||||
original_filename,
|
||||
sidecar=sidecar_flags,
|
||||
sidecar_drop_ext=sidecar_drop_ext,
|
||||
live_photo=export_live,
|
||||
raw_photo=export_raw,
|
||||
export_as_hardlink=export_as_hardlink,
|
||||
overwrite=overwrite,
|
||||
use_photos_export=use_photos_export,
|
||||
exiftool=exiftool,
|
||||
merge_exif_keywords=exiftool_merge_keywords,
|
||||
merge_exif_persons=exiftool_merge_persons,
|
||||
use_albums_as_keywords=album_keyword,
|
||||
use_persons_as_keywords=person_keyword,
|
||||
keyword_template=keyword_template,
|
||||
description_template=description_template,
|
||||
update=update,
|
||||
ignore_signature=ignore_signature,
|
||||
export_db=export_db,
|
||||
fileutil=fileutil,
|
||||
dry_run=dry_run,
|
||||
touch_file=touch_file,
|
||||
convert_to_jpeg=convert_to_jpeg,
|
||||
jpeg_quality=jpeg_quality,
|
||||
ignore_date_modified=ignore_date_modified,
|
||||
use_photokit=use_photokit,
|
||||
verbose=verbose_,
|
||||
exiftool_flags=exiftool_option,
|
||||
jpeg_ext=jpeg_ext,
|
||||
replace_keywords=replace_keywords,
|
||||
)
|
||||
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.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,
|
||||
),
|
||||
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,
|
||||
)
|
||||
|
||||
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)
|
||||
)
|
||||
if tries > retry:
|
||||
results.error.append(
|
||||
(str(pathlib.Path(dest) / original_filename), e)
|
||||
)
|
||||
break
|
||||
else:
|
||||
click.echo(
|
||||
f"Retrying export for photo ({photo.uuid}: {photo.original_filename})"
|
||||
)
|
||||
else:
|
||||
verbose_(f"Skipping original version of {photo.original_filename}")
|
||||
|
||||
@@ -2619,70 +2796,87 @@ def export_photo(
|
||||
)
|
||||
|
||||
else:
|
||||
try:
|
||||
export_results_edited = photo.export2(
|
||||
dest_path,
|
||||
edited_filename,
|
||||
sidecar=sidecar_flags,
|
||||
sidecar_drop_ext=sidecar_drop_ext,
|
||||
export_as_hardlink=export_as_hardlink,
|
||||
overwrite=overwrite,
|
||||
edited=True,
|
||||
use_photos_export=use_photos_export,
|
||||
exiftool=exiftool,
|
||||
merge_exif_keywords=exiftool_merge_keywords,
|
||||
merge_exif_persons=exiftool_merge_persons,
|
||||
use_albums_as_keywords=album_keyword,
|
||||
use_persons_as_keywords=person_keyword,
|
||||
keyword_template=keyword_template,
|
||||
description_template=description_template,
|
||||
update=update,
|
||||
ignore_signature=ignore_signature,
|
||||
export_db=export_db,
|
||||
fileutil=fileutil,
|
||||
dry_run=dry_run,
|
||||
touch_file=touch_file,
|
||||
convert_to_jpeg=convert_to_jpeg,
|
||||
jpeg_quality=jpeg_quality,
|
||||
ignore_date_modified=ignore_date_modified,
|
||||
use_photokit=use_photokit,
|
||||
verbose=verbose_,
|
||||
exiftool_flags=exiftool_option,
|
||||
jpeg_ext=jpeg_ext,
|
||||
replace_keywords=replace_keywords,
|
||||
)
|
||||
results += export_results_edited
|
||||
for warning_ in export_results_edited.exiftool_warning:
|
||||
verbose_(
|
||||
f"exiftool warning for file {warning_[0]}: {warning_[1]}"
|
||||
tries = 0
|
||||
while tries <= retry:
|
||||
tries += 1
|
||||
error = 0
|
||||
try:
|
||||
export_results_edited = photo.export2(
|
||||
dest_path,
|
||||
edited_filename,
|
||||
sidecar=sidecar_flags,
|
||||
sidecar_drop_ext=sidecar_drop_ext,
|
||||
export_as_hardlink=export_as_hardlink,
|
||||
overwrite=overwrite,
|
||||
edited=True,
|
||||
use_photos_export=use_photos_export,
|
||||
exiftool=exiftool,
|
||||
merge_exif_keywords=exiftool_merge_keywords,
|
||||
merge_exif_persons=exiftool_merge_persons,
|
||||
use_albums_as_keywords=album_keyword,
|
||||
use_persons_as_keywords=person_keyword,
|
||||
keyword_template=keyword_template,
|
||||
description_template=description_template,
|
||||
update=update,
|
||||
ignore_signature=ignore_signature,
|
||||
export_db=export_db,
|
||||
fileutil=fileutil,
|
||||
dry_run=dry_run,
|
||||
touch_file=touch_file,
|
||||
convert_to_jpeg=convert_to_jpeg,
|
||||
jpeg_quality=jpeg_quality,
|
||||
ignore_date_modified=ignore_date_modified,
|
||||
use_photokit=use_photokit,
|
||||
verbose=verbose_,
|
||||
exiftool_flags=exiftool_option,
|
||||
jpeg_ext=jpeg_ext,
|
||||
replace_keywords=replace_keywords,
|
||||
)
|
||||
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.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,
|
||||
),
|
||||
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,
|
||||
)
|
||||
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 tries > retry:
|
||||
results.error.append(
|
||||
(str(pathlib.Path(dest) / edited_filename), e)
|
||||
)
|
||||
break
|
||||
else:
|
||||
click.echo(
|
||||
f"Retrying export for photo ({photo.uuid}: {photo.original_filename})"
|
||||
)
|
||||
|
||||
if verbose:
|
||||
if update:
|
||||
@@ -2893,8 +3087,12 @@ def write_export_report(report_file, results):
|
||||
"exiftool_error": "",
|
||||
"extended_attributes_written": 0,
|
||||
"extended_attributes_skipped": 0,
|
||||
"cleanup_deleted_file": 0,
|
||||
"cleanup_deleted_directory": 0,
|
||||
}
|
||||
for result in results.all_files()
|
||||
+ results.deleted_files
|
||||
+ results.deleted_directories
|
||||
}
|
||||
|
||||
for result in results.exported:
|
||||
@@ -2960,6 +3158,12 @@ def write_export_report(report_file, results):
|
||||
for result in results.xattr_skipped:
|
||||
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 = [
|
||||
"filename",
|
||||
"exported",
|
||||
@@ -2978,6 +3182,8 @@ def write_export_report(report_file, results):
|
||||
"exiftool_error",
|
||||
"extended_attributes_written",
|
||||
"extended_attributes_skipped",
|
||||
"cleanup_deleted_file",
|
||||
"cleanup_deleted_directory",
|
||||
]
|
||||
|
||||
try:
|
||||
@@ -3004,27 +3210,27 @@ def cleanup_files(dest_path, files_to_keep, fileutil):
|
||||
fileutile: FileUtil object
|
||||
|
||||
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}
|
||||
|
||||
deleted_files = 0
|
||||
deleted_files = []
|
||||
for p in pathlib.Path(dest_path).rglob("*"):
|
||||
path = str(p).lower()
|
||||
if p.is_file() and path not in keepers:
|
||||
verbose_(f"Deleting {p}")
|
||||
fileutil.unlink(p)
|
||||
deleted_files += 1
|
||||
deleted_files.append(str(p))
|
||||
|
||||
# delete empty directories
|
||||
deleted_dirs = 0
|
||||
deleted_dirs = []
|
||||
for p in pathlib.Path(dest_path).rglob("*"):
|
||||
path = str(p).lower()
|
||||
# if directory and directory is empty
|
||||
if p.is_dir() and not next(p.iterdir(), False):
|
||||
verbose_(f"Deleting empty directory {p}")
|
||||
fileutil.rmdir(p)
|
||||
deleted_dirs += 1
|
||||
deleted_dirs.append(str(p))
|
||||
|
||||
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
|
||||
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)
|
||||
|
||||
@@ -3152,10 +3358,10 @@ def write_extended_attributes(photo, files, xattr_template, strip=False):
|
||||
),
|
||||
err=True,
|
||||
)
|
||||
|
||||
# filter out any template values that didn't match by looking for sentinel
|
||||
rendered = [
|
||||
value for value in rendered if _OSXPHOTOS_NONE_SENTINEL not in value
|
||||
]
|
||||
rendered = [value.replace(_OSXPHOTOS_NONE_SENTINEL, "") for value in rendered]
|
||||
|
||||
try:
|
||||
attributes[xattr].extend(rendered)
|
||||
except KeyError:
|
||||
|
||||
@@ -87,6 +87,8 @@ class ExportResults:
|
||||
exiftool_error=None,
|
||||
xattr_written=None,
|
||||
xattr_skipped=None,
|
||||
deleted_files=None,
|
||||
deleted_directories=None,
|
||||
):
|
||||
self.exported = exported or []
|
||||
self.new = new or []
|
||||
@@ -107,6 +109,8 @@ class ExportResults:
|
||||
self.exiftool_error = exiftool_error or []
|
||||
self.xattr_written = xattr_written or []
|
||||
self.xattr_skipped = xattr_skipped or []
|
||||
self.deleted_files = deleted_files or []
|
||||
self.deleted_directories = deleted_directories or []
|
||||
|
||||
def all_files(self):
|
||||
""" return all filenames contained in results """
|
||||
@@ -151,6 +155,8 @@ class ExportResults:
|
||||
self.error += other.error
|
||||
self.exiftool_warning += other.exiftool_warning
|
||||
self.exiftool_error += other.exiftool_error
|
||||
self.deleted_files += other.deleted_files
|
||||
self.deleted_directories += other.deleted_directories
|
||||
return self
|
||||
|
||||
def __str__(self):
|
||||
@@ -173,6 +179,8 @@ class ExportResults:
|
||||
+ f",error={self.error}"
|
||||
+ f",exiftool_warning={self.exiftool_warning}"
|
||||
+ 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_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:
|
||||
fname = self.filename
|
||||
fname = self.original_filename
|
||||
|
||||
uti = self.uti if edited else self.uti_original
|
||||
if convert_to_jpeg and self.isphoto and uti != "public.jpeg":
|
||||
@@ -1652,7 +1660,7 @@ def _exiftool_dict(
|
||||
if type(val) == str:
|
||||
exif[field] = val.replace("\n", " ")
|
||||
elif type(val) == list:
|
||||
exif[field] = [v.replace("\n", " ") for v in val]
|
||||
exif[field] = [str(v).replace("\n", " ") for v in val if v is not None]
|
||||
return exif
|
||||
|
||||
|
||||
|
||||
@@ -453,9 +453,24 @@ class PhotoInfo:
|
||||
)
|
||||
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
|
||||
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:
|
||||
return self._album_info
|
||||
except AttributeError:
|
||||
@@ -465,6 +480,21 @@ class PhotoInfo:
|
||||
]
|
||||
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
|
||||
def import_info(self):
|
||||
""" 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 """
|
||||
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
|
||||
def burst_photos(self):
|
||||
"""If photo is a burst photo, returns list of PhotoInfo objects
|
||||
|
||||
@@ -1828,7 +1828,6 @@ class PhotosDB:
|
||||
|
||||
# get details about photos
|
||||
verbose("Processing photo details.")
|
||||
logging.debug(f"Getting information about photos")
|
||||
c.execute(
|
||||
f"""SELECT {asset_table}.ZUUID,
|
||||
ZADDITIONALASSETATTRIBUTES.ZMASTERFINGERPRINT,
|
||||
@@ -2736,8 +2735,6 @@ class PhotosDB:
|
||||
# an empty album will be in _dbalbum_titles but not _dbalbums_album
|
||||
pass
|
||||
album_set.update(title_set)
|
||||
else:
|
||||
logging.debug(f"Could not find album '{album}' in database")
|
||||
photos_sets.append(album_set)
|
||||
|
||||
if uuid:
|
||||
@@ -2745,8 +2742,6 @@ class PhotosDB:
|
||||
for u in uuid:
|
||||
if u in self._dbphotos:
|
||||
uuid_set.update([u])
|
||||
else:
|
||||
logging.debug(f"Could not find uuid '{u}' in database")
|
||||
photos_sets.append(uuid_set)
|
||||
|
||||
if keywords:
|
||||
@@ -2754,8 +2749,6 @@ class PhotosDB:
|
||||
for keyword in keywords:
|
||||
if keyword in self._dbkeywords_keyword:
|
||||
keyword_set.update(self._dbkeywords_keyword[keyword])
|
||||
else:
|
||||
logging.debug(f"Could not find keyword '{keyword}' in database")
|
||||
photos_sets.append(keyword_set)
|
||||
|
||||
if persons:
|
||||
@@ -2768,8 +2761,6 @@ class PhotosDB:
|
||||
except KeyError:
|
||||
# some persons have zero photos so they won't be in _dbfaces_pk
|
||||
pass
|
||||
else:
|
||||
logging.debug(f"Could not find person '{person}' in database")
|
||||
photos_sets.append(person_set)
|
||||
|
||||
if from_date or to_date: # sourcery off
|
||||
@@ -2780,14 +2771,10 @@ class PhotosDB:
|
||||
dsel = {
|
||||
k: v for k, v in dsel.items() if v["imageDate"] >= from_date
|
||||
}
|
||||
logging.debug(
|
||||
f"Found %i items with from_date {from_date}" % len(dsel)
|
||||
)
|
||||
if to_date:
|
||||
if not datetime_has_tz(to_date):
|
||||
to_date = datetime_naive_to_local(to_date)
|
||||
dsel = {k: v for k, v in dsel.items() if v["imageDate"] <= to_date}
|
||||
logging.debug(f"Found %i items with to_date {to_date}" % len(dsel))
|
||||
photos_sets.append(set(dsel.keys()))
|
||||
|
||||
photoinfo = []
|
||||
|
||||
@@ -4,7 +4,7 @@ In its simplest form, a template statement has the form: `"{template_field}"`, f
|
||||
|
||||
Template statements may contain one or more modifiers. The full syntax is:
|
||||
|
||||
`"pretext{delim+template_field:subfield|filter(path_sep)[find,replace]?bool_value,default}posttext"`
|
||||
`"pretext{delim+template_field:subfield|filter(path_sep)[find,replace] conditional?bool_value,default}posttext"`
|
||||
|
||||
Template statements are white-space sensitive meaning that white space (spaces, tabs) changes the meaning of the template statement.
|
||||
|
||||
@@ -60,9 +60,43 @@ e.g. If Photo is in `Album1` in `Folder1`:
|
||||
- `"{folder_album(>)}"` renders to `["Folder1>Album1"]`
|
||||
- `"{folder_album()}"` renders to `["Folder1Album1"]`
|
||||
|
||||
`[find|replace]`: optional text replacement to perform on rendered template value. For example, to replace "/" in an album name, you could use the template `"{album[/,-]}"`. Multiple replacements can be made by appending "|" and adding another find|replace pair. e.g. to replace both "/" and ":" in album name: `"{album[/,-|:,-]}"`. find/replace pairs are not limited to single characters. The "|" character cannot be used in a find/replace pair.
|
||||
`[find,replace]`: optional text replacement to perform on rendered template value. For example, to replace "/" in an album name, you could use the template `"{album[/,-]}"`. Multiple replacements can be made by appending "|" and adding another find|replace pair. e.g. to replace both "/" and ":" in album name: `"{album[/,-|:,-]}"`. find/replace pairs are not limited to single characters. The "|" character cannot be used in a find/replace pair.
|
||||
|
||||
`?bool_value`: Template fields may be evaluated as boolean by appending "?" after the field name (and following "(path_sep)" or "[find/replace]". If a field is True (e.g. photo is HDR and field is `"{hdr}"`) or has any value, the value following the "?" will be used to render the template instead of the actual field value. If the template field evaluates to False (e.g. in above example, photo is not HDR) or has no value (e.g. photo has no title and field is `"{title}"`) then the default value following a "," will be used.
|
||||
`conditional`: optional conditional expression that is evaluated as boolean (True/False) for use with the `?bool_value` modifier. Conditional expressions take the form '` not operator value`' where `not` is an optional modifier that negates the `operator`. Note: the space before the conditional expression is required if you use a conditional expression. Valid comparison operators are:
|
||||
|
||||
- `contains`: template field contains value, similar to python's `in`
|
||||
- `matches`: template field contains exactly value, unlike `contains`: does not match partial matches
|
||||
- `startswith`: template field starts with value
|
||||
- `endswith`: template field ends with value
|
||||
- `<=`: template field is less than or equal to value
|
||||
- `>=`: template field is greater than or equal to value
|
||||
- `<`: template field is less than value
|
||||
- `>`: template field is greater than value
|
||||
- `==`: template field equals value
|
||||
- `!=`: template field does not equal value
|
||||
|
||||
The `value` part of the conditional expression is treated as a bare (unquoted) word/phrase. Multiple values may be separated by '|' (the pipe symbol). `value` is itself a template statement so you can use one or more template fields in `value` which will be resolved before the comparison occurs.
|
||||
|
||||
For example:
|
||||
|
||||
- `{keyword matches Beach}` resolves to True if 'Beach' is a keyword. It would not match keyword 'BeachDay'.
|
||||
- `{keyword contains Beach}` resolves to True if any keyword contains the word 'Beach' so it would match both 'Beach' and 'BeachDay'.
|
||||
- `{photo.score.overall > 0.7}` resolves to True if the photo's overall aesthetic score is greater than 0.7.
|
||||
- `{keyword|lower contains beach}` uses the lower case filter to do case-insensitive matching to match any keyword that contains the word 'beach'.
|
||||
- `{keyword|lower not contains beach}` uses the `not` modifier to negate the comparison so this resolves to True if there is no keyword that matches 'beach'.
|
||||
|
||||
Examples: to export photos that contain certain keywords with the `osxphotos export` command's `--directory` option:
|
||||
|
||||
`--directory "{keyword|lower matches travel|vacation?Travel-Photos,Not-Travel-Photos}"`
|
||||
|
||||
This exports any photo that has keywords 'travel' or 'vacation' into a directory 'Travel-Photos' and all other photos into directory 'Not-Travel-Photos'.
|
||||
|
||||
This can be used to rename files as well, for example:
|
||||
`--filename "{favorite?Favorite-{original_name},{original_name}}"`
|
||||
|
||||
This renames any photo that is a favorite as 'Favorite-ImageName.jpg' (where 'ImageName.jpg' is the original name of the photo) and all other photos with the unmodified original name.
|
||||
|
||||
`?bool_value`: Template fields may be evaluated as boolean (True/False) by appending "?" after the field name (and following "(path_sep)" or "[find/replace]". If a field is True (e.g. photo is HDR and field is `"{hdr}"`) or has any value, the value following the "?" will be used to render the template instead of the actual field value. If the template field evaluates to False (e.g. in above example, photo is not HDR) or has no value (e.g. photo has no title and field is `"{title}"`) then the default value following a "," will be used.
|
||||
|
||||
e.g. if photo is an HDR image,
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from ._constants import _UNKNOWN_PERSON
|
||||
from .datetime_formatter import DateTimeFormatter
|
||||
from .exiftool import ExifTool
|
||||
from .path_utils import sanitize_dirname, sanitize_filename, sanitize_pathpart
|
||||
from .utils import load_function
|
||||
|
||||
# ensure locale set to user's locale
|
||||
locale.setlocale(locale.LC_ALL, "")
|
||||
@@ -48,6 +49,7 @@ TEMPLATE_SUBSTITUTIONS = {
|
||||
"{photo_or_video}": "'photo' or 'video' depending on what type the image is. To customize, use default value as in '{photo_or_video,photo=fotos;video=videos}'",
|
||||
"{hdr}": "Photo is HDR?; True/False value, use in format '{hdr?VALUE_IF_TRUE,VALUE_IF_FALSE}'",
|
||||
"{edited}": "Photo has been edited (has adjustments)?; True/False value, use in format '{edited?VALUE_IF_TRUE,VALUE_IF_FALSE}'",
|
||||
"{favorite}": "Photo has been marked as favorite?; True/False value, use in format '{favorite?VALUE_IF_TRUE,VALUE_IF_FALSE}'",
|
||||
"{created.date}": "Photo's creation date in ISO format, e.g. '2020-03-22'",
|
||||
"{created.year}": "4-digit year of photo creation time",
|
||||
"{created.yy}": "2-digit year of photo creation time",
|
||||
@@ -119,6 +121,7 @@ TEMPLATE_SUBSTITUTIONS = {
|
||||
"{uuid}": "Photo's internal universally unique identifier (UUID) for the photo, a 36-character string unique to the photo, e.g. '128FB4C6-0B16-4E7D-9108-FB2E90DA1546'",
|
||||
"{comma}": "A comma: ','",
|
||||
"{semicolon}": "A semicolon: ';'",
|
||||
"{questionmark}": "A question mark: '?'",
|
||||
"{pipe}": "A vertical pipe: '|'",
|
||||
"{openbrace}": "An open brace: '{'",
|
||||
"{closebrace}": "A close brace: '}'",
|
||||
@@ -126,6 +129,10 @@ TEMPLATE_SUBSTITUTIONS = {
|
||||
"{closeparens}": "A close parentheses: ')'",
|
||||
"{openbracket}": "An open bracket: '['",
|
||||
"{closebracket}": "A close bracket: ']'",
|
||||
"{newline}": r"A newline: '\n'",
|
||||
"{lf}": r"A line feed: '\n', alias for {newline}",
|
||||
"{cr}": r"A carriage return: '\r'",
|
||||
"{crlf}": r"a carriage return + line feed: '\r\n'",
|
||||
}
|
||||
|
||||
# Permitted multi-value substitutions (each of these returns None or 1 or more values)
|
||||
@@ -134,7 +141,9 @@ TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
|
||||
"{folder_album}": "Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder",
|
||||
"{keyword}": "Keyword(s) assigned to photo",
|
||||
"{person}": "Person(s) / face(s) in a photo",
|
||||
"{label}": "Image categorization label associated with a photo (Photos 5+ only)",
|
||||
"{label}": "Image categorization label associated with a photo (Photos 5+ only). "
|
||||
"Labels are added automatically by Photos using machine learning algorithms to categorize images. "
|
||||
"These are not the same as {keyword} which refers to the user-defined keywords/tags applied in Photos.",
|
||||
"{label_normalized}": "All lower case version of 'label' (Photos 5+ only)",
|
||||
"{comment}": "Comment(s) on shared Photos; format is 'Person name: comment text' (Photos 5+ only)",
|
||||
"{exiftool}": "Format: '{exiftool:GROUP:TAGNAME}'; use exiftool (https://exiftool.org) to extract metadata, in form GROUP:TAGNAME, from image. "
|
||||
@@ -145,6 +154,15 @@ TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
|
||||
"{searchinfo.activity}": "Activities associated with a photo, e.g. 'Sporting Event'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).",
|
||||
"{searchinfo.venue}": "Venues associated with a photo, e.g. name of restaurant; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).",
|
||||
"{searchinfo.venue_type}": "Venue types associated with a photo, e.g. 'Restaurant'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).",
|
||||
"{photo}": "Provides direct access to the PhotoInfo object for the photo. "
|
||||
+ "Must be used in format '{photo.property}' where 'property' represents a PhotoInfo property. "
|
||||
+ "For example: '{photo.favorite}' is the same as '{favorite}' and '{photo.place.name}' is the same as '{place.name}'. "
|
||||
+ "'{photo}' provides access to properties that are not available as separate template fields but it assumes some knowledge of "
|
||||
+ "the underlying PhotoInfo class. See https://rhettbull.github.io/osxphotos/ for additional documentation on the PhotoInfo class.",
|
||||
"{function}": "Execute a python function from an external file and use return value as template substitution. "
|
||||
+ "Use in format: {function:file.py::function_name} where 'file.py' is the name of the python file and 'function_name' is the name of the function to call. "
|
||||
+ "The function will be passed the PhotoInfo object for the photo. "
|
||||
+ "See https://github.com/RhetTbull/osxphotos/blob/master/examples/template_function.py for an example of how to implement a template function.",
|
||||
}
|
||||
|
||||
FILTER_VALUES = {
|
||||
@@ -185,6 +203,11 @@ PUNCTUATION = {
|
||||
"closeparens": ")",
|
||||
"openbracket": "[",
|
||||
"closebracket": "]",
|
||||
"questionmark": "?",
|
||||
"newline": "\n",
|
||||
"lf": "\n",
|
||||
"cr": "\r",
|
||||
"crlf": "\r\n",
|
||||
}
|
||||
|
||||
|
||||
@@ -317,17 +340,6 @@ class PhotoTemplate:
|
||||
unmatched=unmatched,
|
||||
)
|
||||
|
||||
# process find/replace
|
||||
if ts.template and ts.template.findreplace:
|
||||
new_results = []
|
||||
for result in results:
|
||||
for pair in ts.template.findreplace.pairs:
|
||||
find = pair.find or ""
|
||||
repl = pair.replace or ""
|
||||
result = result.replace(find, repl)
|
||||
new_results.append(result)
|
||||
results = new_results
|
||||
|
||||
rendered_strings = results
|
||||
|
||||
if filename:
|
||||
@@ -362,7 +374,7 @@ class PhotoTemplate:
|
||||
if ts.template:
|
||||
# have a template field to process
|
||||
field = ts.template.field
|
||||
if field not in FIELD_NAMES:
|
||||
if field not in FIELD_NAMES and not field.startswith("photo"):
|
||||
unmatched.append(field)
|
||||
return [], unmatched
|
||||
|
||||
@@ -424,6 +436,30 @@ class PhotoTemplate:
|
||||
else:
|
||||
default = []
|
||||
|
||||
# process conditional
|
||||
if ts.template.conditional is not None:
|
||||
operator = ts.template.conditional.operator
|
||||
negation = ts.template.conditional.negation
|
||||
if ts.template.conditional.value is not None:
|
||||
# conditional value is also a TemplateString
|
||||
conditional_value, u = self._render_statement(
|
||||
ts.template.conditional.value,
|
||||
none_str=none_str,
|
||||
path_sep=path_sep,
|
||||
expand_inplace=expand_inplace,
|
||||
inplace_sep=inplace_sep,
|
||||
filename=filename,
|
||||
dirname=dirname,
|
||||
)
|
||||
unmatched.extend(u)
|
||||
else:
|
||||
# this shouldn't happen
|
||||
conditional_value = [""]
|
||||
else:
|
||||
operator = None
|
||||
negation = None
|
||||
conditional_value = []
|
||||
|
||||
vals = []
|
||||
if field in SINGLE_VALUE_SUBSTITUTIONS:
|
||||
vals = self.get_template_value(
|
||||
@@ -442,7 +478,15 @@ class PhotoTemplate:
|
||||
vals = self.get_template_value_exiftool(
|
||||
subfield, filename=filename, dirname=dirname
|
||||
)
|
||||
elif field in MULTI_VALUE_SUBSTITUTIONS:
|
||||
elif field == "function":
|
||||
if subfield is None:
|
||||
raise ValueError(
|
||||
"SyntaxError: filename and function must not be null with {function::filename.py:function_name}"
|
||||
)
|
||||
vals = self.get_template_value_function(
|
||||
subfield, filename=filename, dirname=dirname
|
||||
)
|
||||
elif field in MULTI_VALUE_SUBSTITUTIONS or field.startswith("photo"):
|
||||
vals = self.get_template_value_multi(
|
||||
field, path_sep=path_sep, filename=filename, dirname=dirname
|
||||
)
|
||||
@@ -452,14 +496,6 @@ class PhotoTemplate:
|
||||
|
||||
vals = [val for val in vals if val is not None]
|
||||
|
||||
if is_bool:
|
||||
if not vals:
|
||||
vals = default
|
||||
else:
|
||||
vals = bool_val
|
||||
elif not vals:
|
||||
vals = default or [none_str]
|
||||
|
||||
if expand_inplace or delim is not None:
|
||||
sep = delim if delim is not None else inplace_sep
|
||||
vals = [sep.join(sorted(vals))]
|
||||
@@ -467,6 +503,99 @@ class PhotoTemplate:
|
||||
for filter_ in filters:
|
||||
vals = self.get_template_value_filter(filter_, vals)
|
||||
|
||||
# process find/replace
|
||||
if ts.template.findreplace:
|
||||
new_vals = []
|
||||
for val in vals:
|
||||
for pair in ts.template.findreplace.pairs:
|
||||
find = pair.find or ""
|
||||
repl = pair.replace or ""
|
||||
val = val.replace(find, repl)
|
||||
new_vals.append(val)
|
||||
vals = new_vals
|
||||
|
||||
if operator:
|
||||
# have a conditional operator
|
||||
|
||||
def string_test(test_function):
|
||||
""" Perform string comparison using test_function; closure to capture conditional_value, vals, negation """
|
||||
match = False
|
||||
for c in conditional_value:
|
||||
for v in vals:
|
||||
if test_function(v, c):
|
||||
match = True
|
||||
break
|
||||
if match:
|
||||
break
|
||||
if (match and not negation) or (negation and not match):
|
||||
return ["True"]
|
||||
else:
|
||||
return []
|
||||
|
||||
def comparison_test(test_function):
|
||||
""" Perform numerical comparisons using test_function; closure to capture conditional_val, vals, negation """
|
||||
if len(vals) != 1 or len(conditional_value) != 1:
|
||||
raise ValueError(
|
||||
f"comparison operators may only be used with a single value: {vals} {conditional_value}"
|
||||
)
|
||||
try:
|
||||
match = (
|
||||
True
|
||||
if test_function(
|
||||
float(vals[0]), float(conditional_value[0])
|
||||
)
|
||||
else False
|
||||
)
|
||||
if (match and not negation) or (negation and not match):
|
||||
return ["True"]
|
||||
else:
|
||||
return []
|
||||
except ValueError as e:
|
||||
raise ValueError(
|
||||
f"comparison operators may only be used with values that can be converted to numbers: {vals} {conditional_value}"
|
||||
)
|
||||
|
||||
if operator in ["contains", "matches", "startswith", "endswith"]:
|
||||
# process any "or" values separated by "|"
|
||||
temp_values = []
|
||||
for c in conditional_value:
|
||||
temp_values.extend(c.split("|"))
|
||||
conditional_value = temp_values
|
||||
|
||||
if operator == "contains":
|
||||
vals = string_test(lambda v, c: c in v)
|
||||
elif operator == "matches":
|
||||
vals = string_test(lambda v, c: v == c)
|
||||
elif operator == "startswith":
|
||||
vals = string_test(lambda v, c: v.startswith(c))
|
||||
elif operator == "endswith":
|
||||
vals = string_test(lambda v, c: v.endswith(c))
|
||||
elif operator == "==":
|
||||
match = sorted(vals) == sorted(conditional_value)
|
||||
if (match and not negation) or (negation and not match):
|
||||
vals = ["True"]
|
||||
else:
|
||||
vals = []
|
||||
elif operator == "!=":
|
||||
match = sorted(vals) != sorted(conditional_value)
|
||||
if (match and not negation) or (negation and not match):
|
||||
vals = ["True"]
|
||||
else:
|
||||
vals = []
|
||||
elif operator == "<":
|
||||
vals = comparison_test(lambda v, c: v < c)
|
||||
elif operator == "<=":
|
||||
vals = comparison_test(lambda v, c: v <= c)
|
||||
elif operator == ">":
|
||||
vals = comparison_test(lambda v, c: v > c)
|
||||
elif operator == ">=":
|
||||
vals = comparison_test(lambda v, c: v >= c)
|
||||
|
||||
if is_bool:
|
||||
vals = default if not vals else bool_val
|
||||
elif not vals:
|
||||
vals = default or [none_str]
|
||||
|
||||
pre = ts.pre or ""
|
||||
post = ts.post or ""
|
||||
|
||||
@@ -539,6 +668,8 @@ class PhotoTemplate:
|
||||
value = "hdr" if self.photo.hdr else None
|
||||
elif field == "edited":
|
||||
value = "edited" if self.photo.hasadjustments else None
|
||||
elif field == "favorite":
|
||||
value = "favorite" if self.photo.favorite else None
|
||||
elif field == "created.date":
|
||||
value = DateTimeFormatter(self.photo.date).date
|
||||
elif field == "created.year":
|
||||
@@ -840,7 +971,7 @@ class PhotoTemplate:
|
||||
""" return list of values for a multi-valued template field """
|
||||
values = []
|
||||
if field == "album":
|
||||
values = self.photo.albums
|
||||
values = self.photo.burst_albums if self.photo.burst else self.photo.albums
|
||||
elif field == "keyword":
|
||||
values = self.photo.keywords
|
||||
elif field == "person":
|
||||
@@ -854,7 +985,11 @@ class PhotoTemplate:
|
||||
elif field == "folder_album":
|
||||
values = []
|
||||
# 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:
|
||||
# album in folder
|
||||
if dirname:
|
||||
@@ -887,6 +1022,32 @@ class PhotoTemplate:
|
||||
values = (
|
||||
self.photo.search_info.venue_types if self.photo.search_info else []
|
||||
)
|
||||
elif field.startswith("photo"):
|
||||
# provide access to PhotoInfo object
|
||||
properties = field.split(".")
|
||||
if len(properties) <= 1:
|
||||
raise ValueError(
|
||||
"Missing property in {photo} template. Use in form {photo.property}."
|
||||
)
|
||||
obj = self.photo
|
||||
for i in range(1, len(properties)):
|
||||
property_ = properties[i]
|
||||
try:
|
||||
obj = getattr(obj, property_)
|
||||
if obj is None:
|
||||
break
|
||||
except AttributeError:
|
||||
raise ValueError(
|
||||
"Invalid property for {photo} template: " + f"'{property_}'"
|
||||
)
|
||||
if obj is None:
|
||||
values = []
|
||||
elif isinstance(obj, bool):
|
||||
values = [property_] if obj else []
|
||||
elif isinstance(obj, (str, int, float)):
|
||||
values = [str(obj)]
|
||||
else:
|
||||
values = [val for val in obj]
|
||||
else:
|
||||
raise ValueError(f"Unhandled template value: {field}")
|
||||
|
||||
@@ -914,6 +1075,7 @@ class PhotoTemplate:
|
||||
if subfield in exifdict:
|
||||
values = exifdict[subfield]
|
||||
values = [values] if not isinstance(values, list) else values
|
||||
values = [str(v) for v in values]
|
||||
|
||||
# sanitize directory names if needed
|
||||
if filename:
|
||||
@@ -925,6 +1087,39 @@ class PhotoTemplate:
|
||||
|
||||
return values
|
||||
|
||||
def get_template_value_function(self, subfield, filename=None, dirname=None):
|
||||
"""Get template value from external function """
|
||||
|
||||
if "::" not in subfield:
|
||||
raise ValueError(
|
||||
f"SyntaxError: could not parse function name from '{subfield}'"
|
||||
)
|
||||
|
||||
filename, funcname = subfield.split("::")
|
||||
|
||||
print(filename, funcname)
|
||||
|
||||
if not pathlib.Path(filename).is_file():
|
||||
raise ValueError(f"'{filename}' does not appear to be a file")
|
||||
|
||||
template_func = load_function(filename, funcname)
|
||||
values = template_func(self.photo)
|
||||
|
||||
if not isinstance(values, (str, list)):
|
||||
raise TypeError(
|
||||
f"Invalid return type for function {funcname}: expected str or list"
|
||||
)
|
||||
if type(values) == str:
|
||||
values = [values]
|
||||
|
||||
# sanitize directory names if needed
|
||||
if filename:
|
||||
values = [sanitize_pathpart(value) for value in values]
|
||||
elif dirname:
|
||||
values = [sanitize_dirname(value) for value in values]
|
||||
|
||||
return values
|
||||
|
||||
def get_photo_video_type(self, default):
|
||||
""" return media type, e.g. photo or video """
|
||||
default_dict = parse_default_kv(default, PHOTO_VIDEO_TYPE_DEFAULTS)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// OSXPhotos Template Language (OTL)
|
||||
// a TemplateString has format:
|
||||
// pre{delim+template_field:subfield|filter(path_sep)[find,replace]?bool_value,default}post
|
||||
// pre{delim+template_field:subfield|filter(path_sep)[find,replace] conditional?bool_value,default}post
|
||||
// a TemplateStatement may contain zero or more TemplateStrings
|
||||
// The pre and post are optional strings
|
||||
// The template itself (inside the {}) is also optional but if present
|
||||
@@ -25,6 +25,7 @@ Template:
|
||||
filter=Filter
|
||||
pathsep=PathSep
|
||||
findreplace=FindReplace
|
||||
conditional=Conditional
|
||||
bool=Boolean
|
||||
default=Default
|
||||
"}"
|
||||
@@ -32,7 +33,7 @@ Template:
|
||||
;
|
||||
|
||||
NON_TEMPLATE_STRING:
|
||||
/[^\{\},]*/
|
||||
/[^\{\},\?]*/
|
||||
;
|
||||
|
||||
Delim:
|
||||
@@ -50,6 +51,10 @@ Field:
|
||||
FIELD_WORD+
|
||||
;
|
||||
|
||||
FIELD_WORD:
|
||||
/[\.\w]+/
|
||||
;
|
||||
|
||||
SubField:
|
||||
(
|
||||
":"-
|
||||
@@ -57,12 +62,8 @@ SubField:
|
||||
)?
|
||||
;
|
||||
|
||||
FIELD_WORD:
|
||||
/[\.\w]+/
|
||||
;
|
||||
|
||||
SUBFIELD_WORD:
|
||||
/[\.\w:]+/
|
||||
/[\.\w:\/]+/
|
||||
;
|
||||
|
||||
Filter:
|
||||
@@ -76,6 +77,24 @@ FILTER_WORD:
|
||||
/[\.\w]+/
|
||||
;
|
||||
|
||||
Conditional:
|
||||
(
|
||||
(" "+)-
|
||||
(negation=NEGATION)?
|
||||
(operator=OPERATOR)
|
||||
(" "+)-
|
||||
(value=Statement)
|
||||
)?
|
||||
;
|
||||
|
||||
NEGATION:
|
||||
"not "
|
||||
;
|
||||
|
||||
OPERATOR:
|
||||
"contains" | "matches" | "startswith" | "endswith" | "<=" | ">=" | "<" | ">" | "==" | "!="
|
||||
;
|
||||
|
||||
PathSep:
|
||||
(
|
||||
"("
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
""" Utility functions used in osxphotos """
|
||||
|
||||
import fnmatch
|
||||
import glob
|
||||
import importlib
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
@@ -13,6 +16,7 @@ import sys
|
||||
import unicodedata
|
||||
import urllib.parse
|
||||
from plistlib import load as plistload
|
||||
from typing import Callable
|
||||
|
||||
import CoreFoundation
|
||||
import CoreServices
|
||||
@@ -401,3 +405,28 @@ def increment_filename(filepath):
|
||||
count += 1
|
||||
dest = dest.parent / f"{dest_new}{dest.suffix}"
|
||||
return str(dest)
|
||||
|
||||
|
||||
def load_function(pyfile: str, function_name: str) -> Callable:
|
||||
""" Load function_name from python file pyfile """
|
||||
module_file = pathlib.Path(pyfile)
|
||||
if not module_file.is_file():
|
||||
raise FileNotFoundError(f"module {pyfile} does not appear to exist")
|
||||
|
||||
module_dir = module_file.parent or pathlib.Path(os.getcwd())
|
||||
module_name = module_file.stem
|
||||
|
||||
# store old sys.path and ensure module_dir at beginning of path
|
||||
syspath = sys.path
|
||||
sys.path = [str(module_dir)] + syspath
|
||||
module = importlib.import_module(module_name)
|
||||
|
||||
try:
|
||||
func = getattr(module, function_name)
|
||||
except AttributeError:
|
||||
raise ValueError(f"'{function_name}' not found in module '{module_name}'")
|
||||
finally:
|
||||
# restore sys.path
|
||||
sys.path = syspath
|
||||
|
||||
return func
|
||||
|
||||
@@ -49,7 +49,7 @@ pathvalidate==2.2.1
|
||||
pexpect==4.8.0
|
||||
photoscript==0.1.0
|
||||
pickleshare==0.7.5
|
||||
Pillow==7.2.0
|
||||
Pillow==8.1.1
|
||||
pkginfo==1.5.0.1
|
||||
pluggy==0.12.0
|
||||
prompt-toolkit==3.0.4
|
||||
@@ -59,7 +59,7 @@ py==1.8.0
|
||||
py2app==0.21
|
||||
pycparser==2.20
|
||||
pyfiglet==0.8.post1
|
||||
Pygments==2.6.1
|
||||
Pygments==2.7.4
|
||||
PyInstaller==3.6
|
||||
pyinstaller-setuptools==2019.3
|
||||
pylint==2.3.1
|
||||
@@ -181,7 +181,7 @@ pyobjc-framework-Vision==6.2.2
|
||||
pyobjc-framework-WebKit==6.2.2
|
||||
pyparsing==2.4.1.1
|
||||
python-dateutil==2.8.1
|
||||
PyYAML==5.1.2
|
||||
PyYAML==5.4
|
||||
pyzmq==18.1.1
|
||||
readme-renderer==25.0
|
||||
regex==2020.2.20
|
||||
|
||||
@@ -23,6 +23,14 @@ class PhotoInfoMock(PhotoInfo):
|
||||
else self._photo.hdr
|
||||
)
|
||||
|
||||
@property
|
||||
def favorite(self):
|
||||
return (
|
||||
self._mock_favorite
|
||||
if getattr(self, "_mock_favorite", None) is not None
|
||||
else self._photo.favorite
|
||||
)
|
||||
|
||||
@property
|
||||
def hasadjustments(self):
|
||||
return (
|
||||
|
||||
File diff suppressed because one or more lines are too long
20
tests/template_function.py
Normal file
20
tests/template_function.py
Normal 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"
|
||||
@@ -731,7 +731,7 @@ def test_export_1():
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
filename = photos[0].original_filename
|
||||
expected_dest = os.path.join(dest, filename)
|
||||
got_dest = photos[0].export(dest)[0]
|
||||
|
||||
@@ -776,7 +776,7 @@ def test_export_3():
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
filename = photos[0].original_filename
|
||||
filename2 = pathlib.Path(filename)
|
||||
filename2 = f"{filename2.stem} (1){filename2.suffix}"
|
||||
expected_dest_2 = os.path.join(dest, filename2)
|
||||
@@ -829,7 +829,7 @@ def test_export_5():
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
filename = photos[0].original_filename
|
||||
expected_dest = os.path.join(dest, filename)
|
||||
|
||||
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_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)
|
||||
|
||||
got_dest = photos[0].export(dest, edited=True)[0]
|
||||
|
||||
@@ -618,7 +618,7 @@ def test_export_1(photosdb):
|
||||
dest = tempdir.name
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
filename = photos[0].original_filename
|
||||
expected_dest = os.path.join(dest, filename)
|
||||
got_dest = photos[0].export(dest)[0]
|
||||
|
||||
@@ -657,7 +657,7 @@ def test_export_3(photosdb):
|
||||
dest = tempdir.name
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
filename = photos[0].original_filename
|
||||
filename2 = pathlib.Path(filename)
|
||||
filename2 = f"{filename2.stem} (1){filename2.suffix}"
|
||||
expected_dest_2 = os.path.join(dest, filename2)
|
||||
@@ -704,7 +704,7 @@ def test_export_5(photosdb):
|
||||
dest = tempdir.name
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
filename = photos[0].original_filename
|
||||
expected_dest = os.path.join(dest, filename)
|
||||
|
||||
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_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)
|
||||
|
||||
got_dest = photos[0].export(dest, edited=True)[0]
|
||||
|
||||
@@ -418,7 +418,7 @@ def test_export_1():
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
filename = photos[0].original_filename
|
||||
expected_dest = os.path.join(dest, filename)
|
||||
got_dest = photos[0].export(dest)[0]
|
||||
|
||||
@@ -463,7 +463,7 @@ def test_export_3():
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
filename = photos[0].original_filename
|
||||
filename2 = pathlib.Path(filename)
|
||||
filename2 = f"{filename2.stem} (1){filename2.suffix}"
|
||||
expected_dest_2 = os.path.join(dest, filename2)
|
||||
@@ -516,7 +516,7 @@ def test_export_5():
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
filename = photos[0].original_filename
|
||||
expected_dest = os.path.join(dest, filename)
|
||||
|
||||
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_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)
|
||||
|
||||
got_dest = photos[0].export(dest, edited=True)[0]
|
||||
|
||||
@@ -426,7 +426,7 @@ def test_export_1():
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
filename = photos[0].original_filename
|
||||
expected_dest = os.path.join(dest, filename)
|
||||
got_dest = photos[0].export(dest)[0]
|
||||
|
||||
@@ -471,7 +471,7 @@ def test_export_3():
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
filename = photos[0].original_filename
|
||||
filename2 = pathlib.Path(filename)
|
||||
filename2 = f"{filename2.stem} (1){filename2.suffix}"
|
||||
expected_dest_2 = os.path.join(dest, filename2)
|
||||
@@ -524,7 +524,7 @@ def test_export_5():
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
filename = photos[0].original_filename
|
||||
expected_dest = os.path.join(dest, filename)
|
||||
|
||||
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_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)
|
||||
|
||||
got_dest = photos[0].export(dest, edited=True)[0]
|
||||
|
||||
@@ -640,7 +640,7 @@ def test_export_1():
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
filename = photos[0].original_filename
|
||||
expected_dest = os.path.join(dest, filename)
|
||||
got_dest = photos[0].export(dest)[0]
|
||||
|
||||
@@ -685,7 +685,7 @@ def test_export_3():
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
filename = photos[0].original_filename
|
||||
filename2 = pathlib.Path(filename)
|
||||
filename2 = f"{filename2.stem} (1){filename2.suffix}"
|
||||
expected_dest_2 = os.path.join(dest, filename2)
|
||||
@@ -738,7 +738,7 @@ def test_export_5():
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
filename = photos[0].original_filename
|
||||
expected_dest = os.path.join(dest, filename)
|
||||
|
||||
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_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)
|
||||
|
||||
got_dest = photos[0].export(dest, edited=True)[0]
|
||||
|
||||
@@ -635,7 +635,7 @@ def test_export_1(photosdb):
|
||||
dest = tempdir.name
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
filename = photos[0].original_filename
|
||||
expected_dest = os.path.join(dest, filename)
|
||||
got_dest = photos[0].export(dest)[0]
|
||||
|
||||
@@ -666,7 +666,7 @@ def test_export_3(photosdb):
|
||||
dest = tempdir.name
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
filename = photos[0].original_filename
|
||||
filename2 = pathlib.Path(filename)
|
||||
filename2 = f"{filename2.stem} (1){filename2.suffix}"
|
||||
expected_dest_2 = os.path.join(dest, filename2)
|
||||
@@ -705,7 +705,7 @@ def test_export_5(photosdb):
|
||||
dest = tempdir.name
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
filename = photos[0].original_filename
|
||||
expected_dest = os.path.join(dest, filename)
|
||||
|
||||
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_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)
|
||||
|
||||
got_dest = photos[0].export(dest, edited=True)[0]
|
||||
|
||||
@@ -702,7 +702,7 @@ def test_export_1(photosdb):
|
||||
dest = tempdir.name
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
filename = photos[0].original_filename
|
||||
expected_dest = os.path.join(dest, filename)
|
||||
got_dest = photos[0].export(dest)[0]
|
||||
|
||||
@@ -733,7 +733,7 @@ def test_export_3(photosdb):
|
||||
dest = tempdir.name
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
filename = photos[0].original_filename
|
||||
filename2 = pathlib.Path(filename)
|
||||
filename2 = f"{filename2.stem} (1){filename2.suffix}"
|
||||
expected_dest_2 = os.path.join(dest, filename2)
|
||||
@@ -772,7 +772,7 @@ def test_export_5(photosdb):
|
||||
dest = tempdir.name
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
filename = photos[0].original_filename
|
||||
expected_dest = os.path.join(dest, filename)
|
||||
|
||||
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_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)
|
||||
|
||||
got_dest = photos[0].export(dest, edited=True)[0]
|
||||
|
||||
@@ -22,6 +22,17 @@ PHOTOS_DB_TOUCH = PHOTOS_DB_15_6
|
||||
PHOTOS_DB_14_6 = "tests/Test-10.14.6.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"
|
||||
|
||||
CLI_OUTPUT_NO_SUBCOMMAND = [
|
||||
@@ -451,7 +462,17 @@ CLI_FINDER_TAGS = {
|
||||
"XMP:Description": "Girl holding pumpkin",
|
||||
"XMP:PersonInImage": "Katie",
|
||||
"XMP:Subject": "Kids",
|
||||
}
|
||||
},
|
||||
"E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51": {
|
||||
"File:FileName": "wedding.jpg",
|
||||
"IPTC:Keywords": ["Maria", "wedding"],
|
||||
"XMP:TagsList": ["Maria", "wedding"],
|
||||
"XMP:Title": None,
|
||||
"EXIF:ImageDescription": "Bride Wedding day",
|
||||
"XMP:Description": "Bride Wedding day",
|
||||
"XMP:PersonInImage": "Maria",
|
||||
"XMP:Subject": ["Maria", "wedding"],
|
||||
},
|
||||
}
|
||||
|
||||
LABELS_JSON = {
|
||||
@@ -1846,6 +1867,36 @@ def test_query_date_timezone():
|
||||
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():
|
||||
"""Test query --keyword """
|
||||
import json
|
||||
@@ -5289,7 +5340,7 @@ def test_export_finder_tag_template_multiple():
|
||||
keywords = [keywords] if type(keywords) != list else keywords
|
||||
persons = CLI_FINDER_TAGS[uuid]["XMP:PersonInImage"]
|
||||
persons = [persons] if type(persons) != list else persons
|
||||
expected = [Tag(x) for x in keywords + persons]
|
||||
expected = [Tag(x) for x in set(keywords + persons)]
|
||||
assert sorted(md.tags) == sorted(expected)
|
||||
|
||||
|
||||
@@ -5327,7 +5378,42 @@ def test_export_finder_tag_template_keywords():
|
||||
keywords = [keywords] if type(keywords) != list else keywords
|
||||
persons = CLI_FINDER_TAGS[uuid]["XMP:PersonInImage"]
|
||||
persons = [persons] if type(persons) != list else persons
|
||||
expected = [Tag(x) for x in keywords + persons]
|
||||
expected = [Tag(x) for x in set(keywords + persons)]
|
||||
assert sorted(md.tags) == sorted(expected)
|
||||
|
||||
|
||||
def test_export_finder_tag_template_multi_field():
|
||||
""" test --finder-tag-template with multiple fields (issue #422) """
|
||||
import glob
|
||||
import os
|
||||
import os.path
|
||||
|
||||
from osxmetadata import OSXMetaData, Tag
|
||||
from osxphotos.cli import export
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
for uuid in CLI_FINDER_TAGS:
|
||||
result = runner.invoke(
|
||||
export,
|
||||
[
|
||||
os.path.join(cwd, PHOTOS_DB_15_7),
|
||||
".",
|
||||
"-V",
|
||||
"--finder-tag-template",
|
||||
"{title};{descr}",
|
||||
"--uuid",
|
||||
f"{uuid}",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
|
||||
md = OSXMetaData(CLI_FINDER_TAGS[uuid]["File:FileName"])
|
||||
title = CLI_FINDER_TAGS[uuid]["XMP:Title"] or ""
|
||||
descr = CLI_FINDER_TAGS[uuid]["XMP:Description"] or ""
|
||||
expected = [Tag(f"{title};{descr}")]
|
||||
assert sorted(md.tags) == sorted(expected)
|
||||
|
||||
|
||||
@@ -5356,7 +5442,7 @@ def test_export_xattr_template():
|
||||
"{person}",
|
||||
"--xattr-template",
|
||||
"comment",
|
||||
"{title}",
|
||||
"{title};{descr}",
|
||||
"--uuid",
|
||||
f"{uuid}",
|
||||
],
|
||||
@@ -5367,7 +5453,9 @@ def test_export_xattr_template():
|
||||
expected = CLI_FINDER_TAGS[uuid]["XMP:PersonInImage"]
|
||||
expected = [expected] if type(expected) != list else expected
|
||||
assert sorted(md.keywords) == sorted(expected)
|
||||
assert md.comment == CLI_FINDER_TAGS[uuid]["XMP:Title"]
|
||||
title = CLI_FINDER_TAGS[uuid]["XMP:Title"] or ""
|
||||
descr = CLI_FINDER_TAGS[uuid]["XMP:Description"] or ""
|
||||
assert md.comment == f"{title};{descr}"
|
||||
|
||||
# run again with --update, should skip writing extended attributes
|
||||
result = runner.invoke(
|
||||
@@ -5381,7 +5469,7 @@ def test_export_xattr_template():
|
||||
"{person}",
|
||||
"--xattr-template",
|
||||
"comment",
|
||||
"{title}",
|
||||
"{title};{descr}",
|
||||
"--uuid",
|
||||
f"{uuid}",
|
||||
"--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}.{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
|
||||
|
||||
@@ -95,7 +95,7 @@ def test_export_1(photosdb):
|
||||
dest = tempdir.name
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
filename = photos[0].original_filename
|
||||
expected_dest = os.path.join(dest, filename)
|
||||
got_dest = photos[0].export(dest)[0]
|
||||
|
||||
@@ -134,7 +134,7 @@ def test_export_3(photosdb):
|
||||
dest = tempdir.name
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
filename = photos[0].original_filename
|
||||
filename2 = pathlib.Path(filename)
|
||||
filename2 = f"{filename2.stem} (1){filename2.suffix}"
|
||||
expected_dest_2 = os.path.join(dest, filename2)
|
||||
@@ -181,7 +181,7 @@ def test_export_5(photosdb):
|
||||
dest = tempdir.name
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
filename = photos[0].original_filename
|
||||
expected_dest = os.path.join(dest, filename)
|
||||
|
||||
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_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)
|
||||
|
||||
got_dest = photos[0].export(dest, edited=True)[0]
|
||||
|
||||
@@ -17,6 +17,34 @@ UUID_DICT = {
|
||||
"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")
|
||||
def photosdb():
|
||||
@@ -29,7 +57,7 @@ def test_export_default_name(photosdb):
|
||||
# test basic export
|
||||
# get an unedited image and export it using default filename
|
||||
import os
|
||||
import os.path
|
||||
import pathlib
|
||||
import tempfile
|
||||
|
||||
import osxphotos
|
||||
@@ -38,11 +66,12 @@ def test_export_default_name(photosdb):
|
||||
dest = tempdir.name
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["no_adjustments"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
expected_dest = os.path.join(dest, filename)
|
||||
filename = photos[0].original_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]
|
||||
|
||||
assert got_dest == expected_dest
|
||||
assert got_dest == str(expected_dest)
|
||||
assert os.path.isfile(got_dest)
|
||||
|
||||
|
||||
@@ -82,7 +111,7 @@ def test_export_edited(photosdb):
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]])
|
||||
|
||||
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)
|
||||
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:
|
||||
assert photos[0].export(dest, use_photos_export=True, edited=True)
|
||||
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"])
|
||||
|
||||
@@ -16,14 +16,14 @@ UUID_DICT = {
|
||||
}
|
||||
|
||||
NAMES_DICT = {
|
||||
"raw": "D05A5FE3-15FB-49A1-A15D-AB3DA6F8B068.jpeg",
|
||||
"heic": "7783E8E6-9CAC-40F3-BE22-81FB7051C266.jpeg",
|
||||
"raw": "DSC03584.jpeg",
|
||||
"heic": "IMG_3092.jpeg"
|
||||
}
|
||||
|
||||
UUID_LIVE_HEIC = "612CE30B-3D8F-417A-9B14-EC42CBA10ACC"
|
||||
NAMES_LIVE_HEIC = [
|
||||
"612CE30B-3D8F-417A-9B14-EC42CBA10ACC.jpeg",
|
||||
"612CE30B-3D8F-417A-9B14-EC42CBA10ACC.mov",
|
||||
"IMG_3259.jpeg",
|
||||
"IMG_3259.mov"
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -29,13 +29,13 @@ def test_export_1():
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
filename = photos[0].original_filename
|
||||
expected_dest = os.path.join(dest, filename)
|
||||
got_dest = photos[0].export(dest)[0]
|
||||
|
||||
assert got_dest == expected_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():
|
||||
@@ -101,7 +101,7 @@ def test_export_edited_default():
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]])
|
||||
|
||||
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():
|
||||
|
||||
@@ -21,6 +21,8 @@ EXPORT_RESULT_ATTRIBUTES = [
|
||||
"error",
|
||||
"exiftool_warning",
|
||||
"exiftool_error",
|
||||
"deleted_files",
|
||||
"deleted_directories",
|
||||
]
|
||||
|
||||
|
||||
@@ -43,6 +45,8 @@ def test_exportresults_init():
|
||||
assert results.error == []
|
||||
assert results.exiftool_warning == []
|
||||
assert results.exiftool_error == []
|
||||
assert results.deleted_files == []
|
||||
assert results.deleted_directories == []
|
||||
|
||||
|
||||
def test_exportresults_iadd():
|
||||
@@ -64,6 +68,12 @@ def test_exportresults_iadd():
|
||||
results1.exiftool_error = [("exiftool_error1", "foo")]
|
||||
results2.exiftool_error = [("exiftool_error2", "bar")]
|
||||
|
||||
results1.deleted_files = [("foo1")]
|
||||
results2.deleted_files = [("foo2")]
|
||||
|
||||
results1.deleted_directories = [("bar1")]
|
||||
results2.deleted_directories = [("bar2")]
|
||||
|
||||
results1 += results2
|
||||
|
||||
assert results1.exiftool_warning == [
|
||||
@@ -75,6 +85,9 @@ def test_exportresults_iadd():
|
||||
("exiftool_error2", "bar"),
|
||||
]
|
||||
|
||||
assert results1.deleted_files == ["foo1", "foo2"]
|
||||
assert results1.deleted_directories == ["bar1", "bar2"]
|
||||
|
||||
|
||||
def test_all_files():
|
||||
""" test ExportResults.all_files() """
|
||||
@@ -84,10 +97,12 @@ def test_all_files():
|
||||
results.exiftool_warning = [("exiftool_warning1", "foo")]
|
||||
results.exiftool_error = [("exiftool_error1", "foo")]
|
||||
results.error = [("error1", "foo")]
|
||||
results.deleted_files = ["deleted_files1"]
|
||||
results.deleted_directories = ["deleted_directories1"]
|
||||
|
||||
assert sorted(results.all_files()) == sorted(
|
||||
[f"{x}1" for x in EXPORT_RESULT_ATTRIBUTES]
|
||||
)
|
||||
assert sorted(
|
||||
results.all_files() + results.deleted_files + results.deleted_directories
|
||||
) == sorted([f"{x}1" for x in EXPORT_RESULT_ATTRIBUTES])
|
||||
|
||||
|
||||
def test_str():
|
||||
@@ -95,6 +110,6 @@ def test_str():
|
||||
results = ExportResults()
|
||||
assert (
|
||||
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=[])"
|
||||
)
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ def test_export_live_1():
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["live"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
filename = photos[0].original_filename
|
||||
expected_dest = os.path.join(dest.name, filename)
|
||||
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"
|
||||
@@ -70,7 +70,7 @@ def test_export_live_2():
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["live"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
filename = photos[0].original_filename
|
||||
expected_dest = os.path.join(dest.name, filename)
|
||||
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"
|
||||
@@ -97,7 +97,7 @@ def test_export_live_3():
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
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_mov = f"{dest.name}/{pathlib.Path(expected_dest).stem}.mov"
|
||||
got_files = photos[0].export(dest.name, live_photo=True)
|
||||
|
||||
@@ -28,6 +28,7 @@ UUID_DICT = {
|
||||
"mojave_album_1": "15uNd7%8RguTEgNPKHfTWw",
|
||||
"date_modified": "A9B73E13-A6F2-4915-8D67-7213B39BAE9F",
|
||||
"date_not_modified": "128FB4C6-0B16-4E7D-9108-FB2E90DA1546",
|
||||
"favorite": "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51",
|
||||
}
|
||||
|
||||
UUID_MEDIA_TYPE = {
|
||||
@@ -133,6 +134,9 @@ UUID_EXIFTOOL = {
|
||||
"England,London,London 2018,St. James's Park,UK,United Kingdom"
|
||||
],
|
||||
},
|
||||
"1EB2B765-0765-43BA-A90C-0D0580E6172C": {
|
||||
"{exiftool:EXIF:SubSecTimeOriginal}": ["22"]
|
||||
},
|
||||
}
|
||||
|
||||
TEMPLATE_VALUES = {
|
||||
@@ -173,6 +177,8 @@ TEMPLATE_VALUES = {
|
||||
"{exif.lens_model}": "iPhone 6s back camera 4.15mm f/2.2",
|
||||
"{album?{folder_album},{created.year}/{created.mm}}": "2020/02",
|
||||
"{title?Title is '{title} - {descr}',No Title}": "Title is 'Glen Ord - Jack Rose Dining Saloon'",
|
||||
"{favorite}": "_",
|
||||
"{favorite?FAV,NOTFAV}": "NOTFAV",
|
||||
}
|
||||
|
||||
|
||||
@@ -250,6 +256,77 @@ COMMENT_UUID_DICT = {
|
||||
"4E4944A0-3E5C-4028-9600-A8709F2FA1DB": ["None: Nice trophy"],
|
||||
}
|
||||
|
||||
UUID_PHOTO = {
|
||||
"DC99FBDD-7A52-4100-A5BB-344131646C30": {
|
||||
"{photo.title}": ["St. James's Park"],
|
||||
"{photo.favorite?FAVORITE,NOTFAVORITE}": ["NOTFAVORITE"],
|
||||
"{photo.hdr}": ["_"],
|
||||
"{photo.keywords}": [
|
||||
"England",
|
||||
"London",
|
||||
"London 2018",
|
||||
"St. James's Park",
|
||||
"UK",
|
||||
"United Kingdom",
|
||||
],
|
||||
"{photo.keywords|lower}": [
|
||||
"england",
|
||||
"london",
|
||||
"london 2018",
|
||||
"st. james's park",
|
||||
"uk",
|
||||
"united kingdom",
|
||||
],
|
||||
},
|
||||
"3DD2C897-F19E-4CA6-8C22-B027D5A71907": {"{photo.place.country_code}": ["AU"]},
|
||||
"F12384F6-CD17-4151-ACBA-AE0E3688539E": {"{photo.place.name}": ["_"]},
|
||||
"E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51": {"{photo.favorite}": ["favorite"]},
|
||||
}
|
||||
|
||||
UUID_CONDITIONAL = {
|
||||
"3DD2C897-F19E-4CA6-8C22-B027D5A71907": {
|
||||
"{title matches Elder Park?YES,NO}": ["YES"],
|
||||
"{title matches not Elder Park?YES,NO}": ["NO"],
|
||||
"{title contains Park?YES,NO}": ["YES"],
|
||||
"{title not contains Park?YES,NO}": ["NO"],
|
||||
"{title matches Park?YES,NO}": ["NO"],
|
||||
"{title matches Elder Park?YES,NO}": ["YES"],
|
||||
"{title == Elder Park?YES,NO}": ["YES"],
|
||||
"{title != Elder Park?YES,NO}": ["NO"],
|
||||
"{title[ ,] == ElderPark?YES,NO}": ["YES"],
|
||||
"{title not != Elder Park?YES,NO}": ["YES"],
|
||||
"{title not == Elder Park?YES,NO}": ["NO"],
|
||||
"{title endswith Park?YES,NO}": ["YES"],
|
||||
"{title endswith Elder?YES,NO}": ["NO"],
|
||||
"{title startswith Elder?YES,NO}": ["YES"],
|
||||
"{title endswith Elder?YES,NO}": ["NO"],
|
||||
"{photo.place.name contains Adelaide?YES,NO}": ["YES"],
|
||||
"{photo.place.name|lower contains adelaide?YES,NO}": ["YES"],
|
||||
"{photo.place.name|lower not contains adelaide?YES,NO}": ["NO"],
|
||||
"{photo.score.overall < 0.7?YES,NO}": ["YES"],
|
||||
"{photo.score.overall <= 0.7?YES,NO}": ["YES"],
|
||||
"{photo.score.overall > 0.7?YES,NO}": ["NO"],
|
||||
"{photo.score.overall >= 0.7?YES,NO}": ["NO"],
|
||||
"{photo.score.overall not < 0.7?YES,NO}": ["NO"],
|
||||
"{folder_album(-) contains Folder1-SubFolder2-AlbumInFolder?YES,NO}": ["YES"],
|
||||
"{folder_album(-)[In,] contains Folder1-SubFolder2-AlbumFolder?YES,NO}": [
|
||||
"YES"
|
||||
],
|
||||
},
|
||||
"DC99FBDD-7A52-4100-A5BB-344131646C30": {
|
||||
"{keyword == {keyword}?YES,NO}": ["YES"],
|
||||
"{keyword contains England?YES,NO}": ["YES"],
|
||||
"{keyword contains Eng?YES,NO}": ["YES"],
|
||||
"{keyword contains Foo?YES,NO}": ["NO"],
|
||||
"{keyword matches England?YES,NO}": ["YES"],
|
||||
"{keyword matches Eng?YES,NO}": ["NO"],
|
||||
"{keyword contains Foo|Bar|England?YES,NO}": ["YES"],
|
||||
"{keyword contains Foo|Bar?YES,NO}": ["NO"],
|
||||
"{keyword matches Foo|Bar|England?YES,NO}": ["YES"],
|
||||
"{keyword matches Foo|Bar?YES,NO}": ["NO"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def photosdb_places():
|
||||
@@ -304,7 +381,7 @@ def test_lookup_multi(photosdb_places):
|
||||
|
||||
for subst in TEMPLATE_SUBSTITUTIONS_MULTI_VALUED:
|
||||
lookup_str = re.match(r"\{([^\\,}]+)\}", subst).group(1)
|
||||
if subst == "{exiftool}":
|
||||
if subst in ["{exiftool}", "{photo}", "{function}"]:
|
||||
continue
|
||||
lookup = template.get_template_value_multi(lookup_str, path_sep=os.path.sep)
|
||||
assert isinstance(lookup, list)
|
||||
@@ -828,6 +905,14 @@ def test_edited(photosdb):
|
||||
assert rendered == ["edited"]
|
||||
|
||||
|
||||
def test_favorite(photosdb):
|
||||
""" Test favorite"""
|
||||
photo = photosdb.get_photo(UUID_MULTI_KEYWORDS)
|
||||
photomock = PhotoInfoMock(photo, favorite=True)
|
||||
rendered, _ = photomock.render_template("{favorite}")
|
||||
assert rendered == ["favorite"]
|
||||
|
||||
|
||||
def test_nested_template_bool(photosdb):
|
||||
photo = photosdb.get_photo(UUID_MULTI_KEYWORDS)
|
||||
template = "{hdr?{edited?HDR_EDITED,HDR_NOT_EDITED},{edited?NOT_HDR_EDITED,NOT_HDR_NOT_EDITED}}"
|
||||
@@ -865,3 +950,35 @@ def test_punctuation(photosdb):
|
||||
rendered, _ = photo.render_template("{" + punc + "}")
|
||||
assert rendered[0] == PUNCTUATION[punc]
|
||||
|
||||
|
||||
def test_photo_template(photosdb):
|
||||
for uuid in UUID_PHOTO:
|
||||
photo = photosdb.get_photo(uuid)
|
||||
for template in UUID_PHOTO[uuid]:
|
||||
rendered, _ = photo.render_template(template)
|
||||
assert sorted(rendered) == sorted(UUID_PHOTO[uuid][template])
|
||||
|
||||
|
||||
def test_conditional(photosdb):
|
||||
for uuid in UUID_CONDITIONAL:
|
||||
photo = photosdb.get_photo(uuid)
|
||||
for template in UUID_CONDITIONAL[uuid]:
|
||||
rendered, _ = photo.render_template(template)
|
||||
assert sorted(rendered) == sorted(UUID_CONDITIONAL[uuid][template])
|
||||
|
||||
|
||||
def test_function(photosdb):
|
||||
""" Test {function} """
|
||||
photo = photosdb.get_photo(UUID_MULTI_KEYWORDS)
|
||||
rendered, _ = photo.render_template("{function:tests/template_function.py::foo}")
|
||||
assert rendered == [f"{photo.original_filename}-FOO"]
|
||||
|
||||
|
||||
def test_function_bad(photosdb):
|
||||
""" Test invalid {function} """
|
||||
photo = photosdb.get_photo(UUID_MULTI_KEYWORDS)
|
||||
with pytest.raises(ValueError):
|
||||
rendered, _ = photo.render_template(
|
||||
"{function:tests/template_function.py::foobar}"
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user