Added {detected_text} template

This commit is contained in:
Rhet Turnbull
2021-07-27 06:08:49 -07:00
parent 123340eada
commit c2335236be
6 changed files with 305 additions and 294 deletions

224
README.md
View File

@@ -573,13 +573,13 @@ osxphotos is very flexible. If you merely want to backup your Photos library, t
Usage: osxphotos export [OPTIONS] [PHOTOS_LIBRARY]... DEST Usage: osxphotos export [OPTIONS] [PHOTOS_LIBRARY]... DEST
Export photos from the Photos database. Export path DEST is required. Export photos from the Photos database. Export path DEST is required.
Optionally, query the Photos database using 1 or more search options; if Optionally, query the Photos database using 1 or more search options; if more
more than one option is provided, they are treated as "AND" (e.g. search for than one option is provided, they are treated as "AND" (e.g. search for photos
photos matching all options). If no query options are provided, all photos matching all options). If no query options are provided, all photos will be
will be exported. By default, all versions of all photos will be exported exported. By default, all versions of all photos will be exported including
including edited versions, live photo movies, burst photos, and associated edited versions, live photo movies, burst photos, and associated raw images.
raw images. See --skip-edited, --skip-live, --skip-bursts, and --skip-raw See --skip-edited, --skip-live, --skip-bursts, and --skip-raw options to
options to modify this behavior. modify this behavior.
Options: Options:
--db <Photos database path> Specify Photos database path. Path to Photos --db <Photos database path> Specify Photos database path. Path to Photos
@@ -590,63 +590,49 @@ Options:
use in the following order: 1. last opened use in the following order: 1. last opened
library, 2. system library, 3. library, 2. system library, 3.
~/Pictures/Photos Library.photoslibrary ~/Pictures/Photos Library.photoslibrary
-V, --verbose Print verbose output. -V, --verbose Print verbose output.
--keyword KEYWORD Search for photos with keyword KEYWORD. If --keyword KEYWORD Search for photos with keyword KEYWORD. If
more than one keyword, treated as "OR", e.g. more than one keyword, treated as "OR", e.g.
find photos matching any keyword find photos matching any keyword
--person PERSON Search for photos with person PERSON. If more --person PERSON Search for photos with person PERSON. If more
than one person, treated as "OR", e.g. find than one person, treated as "OR", e.g. find
photos matching any person photos matching any person
--album ALBUM Search for photos in album ALBUM. If more than --album ALBUM Search for photos in album ALBUM. If more than
one album, treated as "OR", e.g. find photos one album, treated as "OR", e.g. find photos
matching any album matching any album
--folder FOLDER Search for photos in an album in folder --folder FOLDER Search for photos in an album in folder
FOLDER. If more than one folder, treated as FOLDER. If more than one folder, treated as
"OR", e.g. find photos in any FOLDER. Only "OR", e.g. find photos in any FOLDER. Only
searches top level folders (e.g. does not look searches top level folders (e.g. does not look
at subfolders) at subfolders)
--name FILENAME Search for photos with filename matching --name FILENAME Search for photos with filename matching
FILENAME. If more than one --name options is FILENAME. If more than one --name options is
specified, they are treated as "OR", e.g. find specified, they are treated as "OR", e.g. find
photos matching any FILENAME. photos matching any FILENAME.
--uuid UUID Search for photos with UUID(s). --uuid UUID Search for photos with UUID(s).
--uuid-from-file FILE Search for photos with UUID(s) loaded from --uuid-from-file FILE Search for photos with UUID(s) loaded from
FILE. Format is a single UUID per line. Lines FILE. Format is a single UUID per line. Lines
preceded with # are ignored. preceded with # are ignored.
--title TITLE Search for TITLE in title of photo. --title TITLE Search for TITLE in title of photo.
--no-title Search for photos with no title. --no-title Search for photos with no title.
--description DESC Search for DESC in description of photo. --description DESC Search for DESC in description of photo.
--no-description Search for photos with no description. --no-description Search for photos with no description.
--place PLACE Search for PLACE in photo's reverse --place PLACE Search for PLACE in photo's reverse
geolocation info geolocation info
--no-place Search for photos with no associated place --no-place Search for photos with no associated place
name info (no reverse geolocation info) name info (no reverse geolocation info)
--location Search for photos with associated location --location Search for photos with associated location
info (e.g. GPS coordinates) info (e.g. GPS coordinates)
--no-location Search for photos with no associated location --no-location Search for photos with no associated location
info (e.g. no GPS coordinates) info (e.g. no GPS coordinates)
--label LABEL Search for photos with image classification --label LABEL Search for photos with image classification
label LABEL (Photos 5 only). If more than one label LABEL (Photos 5 only). If more than one
label, treated as "OR", e.g. find photos label, treated as "OR", e.g. find photos
matching any label matching any label
--uti UTI Search for photos whose uniform type --uti UTI Search for photos whose uniform type
identifier (UTI) matches UTI identifier (UTI) matches UTI
-i, --ignore-case Case insensitive search for title, -i, --ignore-case Case insensitive search for title,
description, place, keyword, person, or album. description, place, keyword, person, or album.
--edited Search for photos that have been edited. --edited Search for photos that have been edited.
--external-edit Search for photos edited in external editor. --external-edit Search for photos edited in external editor.
--favorite Search for photos marked favorite. --favorite Search for photos marked favorite.
@@ -655,67 +641,51 @@ Options:
--not-hidden Search for photos not marked hidden. --not-hidden Search for photos not marked hidden.
--shared Search for photos in shared iCloud album --shared Search for photos in shared iCloud album
(Photos 5 only). (Photos 5 only).
--not-shared Search for photos not in shared iCloud album --not-shared Search for photos not in shared iCloud album
(Photos 5 only). (Photos 5 only).
--burst Search for photos that were taken in a burst. --burst Search for photos that were taken in a burst.
--not-burst Search for photos that are not part of a --not-burst Search for photos that are not part of a
burst. burst.
--live Search for Apple live photos --live Search for Apple live photos
--not-live Search for photos that are not Apple live --not-live Search for photos that are not Apple live
photos. photos.
--portrait Search for Apple portrait mode photos. --portrait Search for Apple portrait mode photos.
--not-portrait Search for photos that are not Apple portrait --not-portrait Search for photos that are not Apple portrait
mode photos. mode photos.
--screenshot Search for screenshot photos. --screenshot Search for screenshot photos.
--not-screenshot Search for photos that are not screenshot --not-screenshot Search for photos that are not screenshot
photos. photos.
--slow-mo Search for slow motion videos. --slow-mo Search for slow motion videos.
--not-slow-mo Search for photos that are not slow motion --not-slow-mo Search for photos that are not slow motion
videos. videos.
--time-lapse Search for time lapse videos. --time-lapse Search for time lapse videos.
--not-time-lapse Search for photos that are not time lapse --not-time-lapse Search for photos that are not time lapse
videos. videos.
--hdr Search for high dynamic range (HDR) photos. --hdr Search for high dynamic range (HDR) photos.
--not-hdr Search for photos that are not HDR photos. --not-hdr Search for photos that are not HDR photos.
--selfie Search for selfies (photos taken with front- --selfie Search for selfies (photos taken with front-
facing cameras). facing cameras).
--not-selfie Search for photos that are not selfies. --not-selfie Search for photos that are not selfies.
--panorama Search for panorama photos. --panorama Search for panorama photos.
--not-panorama Search for photos that are not panoramas. --not-panorama Search for photos that are not panoramas.
--has-raw Search for photos with both a jpeg and raw --has-raw Search for photos with both a jpeg and raw
version version
--only-movies Search only for movies (default searches both --only-movies Search only for movies (default searches both
images and movies). images and movies).
--only-photos Search only for photos/images (default --only-photos Search only for photos/images (default
searches both images and movies). searches both images and movies).
--from-date DATETIME Search by item start date, e.g. --from-date DATETIME Search by item start date, e.g.
2000-01-12T12:00:00, 2000-01-12T12:00:00,
2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO
8601 with/without timezone). 8601 with/without timezone).
--to-date DATETIME Search by item end date, e.g. --to-date DATETIME Search by item end date, e.g.
2000-01-12T12:00:00, 2000-01-12T12:00:00,
2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO
8601 with/without timezone). 8601 with/without timezone).
--from-time TIME Search by item start time of day, e.g. 12:00, --from-time TIME Search by item start time of day, e.g. 12:00,
or 12:00:00. or 12:00:00.
--to-time TIME Search by item end time of day, e.g. 12:00 or --to-time TIME Search by item end time of day, e.g. 12:00 or
12:00:00. 12:00:00.
--has-comment Search for photos that have comments. --has-comment Search for photos that have comments.
--no-comment Search for photos with no comments. --no-comment Search for photos with no comments.
--has-likes Search for photos that have likes. --has-likes Search for photos that have likes.
@@ -723,10 +693,8 @@ Options:
--is-reference Search for photos that were imported as --is-reference Search for photos that were imported as
referenced files (not copied into Photos referenced files (not copied into Photos
library). library).
--in-album Search for photos that are in one or more --in-album Search for photos that are in one or more
albums. albums.
--not-in-album Search for photos that are not in any albums. --not-in-album Search for photos that are not in any albums.
--duplicate Search for photos with possible duplicates. --duplicate Search for photos with possible duplicates.
osxphotos will compare signatures of photos, osxphotos will compare signatures of photos,
@@ -736,7 +704,6 @@ Options:
for-byte nor compare hashes but should find for-byte nor compare hashes but should find
photos imported multiple times or duplicated photos imported multiple times or duplicated
within Photos. within Photos.
--min-size SIZE Search for photos with size >= SIZE bytes. The --min-size SIZE Search for photos with size >= SIZE bytes. The
size evaluated is the photo's original size size evaluated is the photo's original size
(when imported to Photos). Size may be (when imported to Photos). Size may be
@@ -744,7 +711,6 @@ Options:
units. For example, the following are all units. For example, the following are all
valid and equivalent sizes: '1048576' valid and equivalent sizes: '1048576'
'1.048576MB', '1 MiB'. '1.048576MB', '1 MiB'.
--max-size SIZE Search for photos with size <= SIZE bytes. The --max-size SIZE Search for photos with size <= SIZE bytes. The
size evaluated is the photo's original size size evaluated is the photo's original size
(when imported to Photos). Size may be (when imported to Photos). Size may be
@@ -752,17 +718,14 @@ Options:
units. For example, the following are all units. For example, the following are all
valid and equivalent sizes: '1048576' valid and equivalent sizes: '1048576'
'1.048576MB', '1 MiB'. '1.048576MB', '1 MiB'.
--regex REGEX TEMPLATE Search for photos where TEMPLATE matches --regex REGEX TEMPLATE Search for photos where TEMPLATE matches
regular expression REGEX. For example, to find regular expression REGEX. For example, to find
photos in an album that begins with 'Beach': ' photos in an album that begins with 'Beach': '
--regex "^Beach" "{album}"'. You may specify --regex "^Beach" "{album}"'. You may specify
more than one regular expression match by more than one regular expression match by
repeating '--regex' with different arguments. repeating '--regex' with different arguments.
--selected Filter for photos that are currently selected --selected Filter for photos that are currently selected
in Photos. in Photos.
--query-eval CRITERIA Evaluate CRITERIA to filter photos. CRITERIA --query-eval CRITERIA Evaluate CRITERIA to filter photos. CRITERIA
will be evaluated in context of the following will be evaluated in context of the following
python list comprehension: `photos = [photo python list comprehension: `photos = [photo
@@ -777,7 +740,6 @@ Options:
https://rhettbull.github.io/osxphotos/ for https://rhettbull.github.io/osxphotos/ for
additional documentation on the PhotoInfo additional documentation on the PhotoInfo
class. class.
--query-function filename.py::function --query-function filename.py::function
Run function to filter photos. Use this in Run function to filter photos. Use this in
format: --query-function filename.py::function format: --query-function filename.py::function
@@ -794,19 +756,14 @@ Options:
evaluated. See https://github.com/RhetTbull/os evaluated. See https://github.com/RhetTbull/os
xphotos/blob/master/examples/query_function.py xphotos/blob/master/examples/query_function.py
for example of how to use this option. for example of how to use this option.
--missing Export only photos missing from the Photos --missing Export only photos missing from the Photos
library; must be used with --download-missing. library; must be used with --download-missing.
--deleted Include photos from the 'Recently Deleted' --deleted Include photos from the 'Recently Deleted'
folder. folder.
--deleted-only Include only photos from the 'Recently --deleted-only Include only photos from the 'Recently
Deleted' folder. Deleted' folder.
--update Only export new or updated files. See notes --update Only export new or updated files. See notes
below on export and --update. below on export and --update.
--ignore-signature When used with '--update', ignores file --ignore-signature When used with '--update', ignores file
signature when updating files. This is useful signature when updating files. This is useful
if you have processed or edited exported if you have processed or edited exported
@@ -825,15 +782,12 @@ Options:
not; 3) if a sidecar does not exist for the not; 3) if a sidecar does not exist for the
photo, a sidecar will be written whether or photo, a sidecar will be written whether or
not the photo file was written or updated. not the photo file was written or updated.
--only-new If used with --update, ignores any previously --only-new If used with --update, ignores any previously
exported files, even if missing from the exported files, even if missing from the
export folder and only exports new files that export folder and only exports new files that
haven't previously been exported. haven't previously been exported.
--dry-run Dry run (test) the export but don't actually --dry-run Dry run (test) the export but don't actually
export any files; most useful with --verbose. export any files; most useful with --verbose.
--export-as-hardlink Hardlink files instead of copying them. Cannot --export-as-hardlink Hardlink files instead of copying them. Cannot
be used with --exiftool which creates copies be used with --exiftool which creates copies
of the files with embedded EXIF data. Note: on of the files with embedded EXIF data. Note: on
@@ -841,49 +795,38 @@ Options:
giving many of the same advantages as giving many of the same advantages as
hardlinks without having to use --export-as- hardlinks without having to use --export-as-
hardlink. hardlink.
--touch-file Sets the file's modification time to match --touch-file Sets the file's modification time to match
photo date. photo date.
--overwrite Overwrite existing files. Default behavior is --overwrite Overwrite existing files. Default behavior is
to add (1), (2), etc to filename if file to add (1), (2), etc to filename if file
already exists. Use this with caution as it already exists. Use this with caution as it
may create name collisions on export. (e.g. if may create name collisions on export. (e.g. if
two files happen to have the same name) two files happen to have the same name)
--retry RETRY Automatically retry export up to RETRY times --retry RETRY Automatically retry export up to RETRY times
if an error occurs during export. This may be if an error occurs during export. This may be
useful with network drives that experience useful with network drives that experience
intermittent errors. intermittent errors.
--export-by-date Automatically create output folders to --export-by-date Automatically create output folders to
organize photos by date created (e.g. organize photos by date created (e.g.
DEST/2019/12/20/photoname.jpg). DEST/2019/12/20/photoname.jpg).
--skip-edited Do not export edited version of photo if an --skip-edited Do not export edited version of photo if an
edited version exists. edited version exists.
--skip-original-if-edited Do not export original if there is an edited --skip-original-if-edited Do not export original if there is an edited
version (exports only the edited version). version (exports only the edited version).
--skip-bursts Do not export all associated burst images in --skip-bursts Do not export all associated burst images in
the library if a photo is a burst photo. the library if a photo is a burst photo.
--skip-live Do not export the associated live video --skip-live Do not export the associated live video
component of a live photo. component of a live photo.
--skip-raw Do not export associated RAW image of a --skip-raw Do not export associated RAW image of a
RAW+JPEG pair. Note: this does not skip RAW RAW+JPEG pair. Note: this does not skip RAW
photos if the RAW photo does not have an photos if the RAW photo does not have an
associated JPEG image (e.g. the RAW file was associated JPEG image (e.g. the RAW file was
imported to Photos without a JPEG preview). imported to Photos without a JPEG preview).
--current-name Use photo's current filename instead of --current-name Use photo's current filename instead of
original filename for export. Note: Starting original filename for export. Note: Starting
with Photos 5, all photos are renamed upon with Photos 5, all photos are renamed upon
import. By default, photos are exported with import. By default, photos are exported with
the the original name they had before import. the the original name they had before import.
--convert-to-jpeg Convert all non-JPEG images (e.g. RAW, HEIC, --convert-to-jpeg Convert all non-JPEG images (e.g. RAW, HEIC,
PNG, etc) to JPEG upon export. Note: does not PNG, etc) to JPEG upon export. Note: does not
convert the RAW component of a RAW+JPEG pair convert the RAW component of a RAW+JPEG pair
@@ -893,24 +836,20 @@ Options:
also --jpeg-quality and --jpeg-ext. Only works also --jpeg-quality and --jpeg-ext. Only works
if your Mac has a GPU (thus may not work on if your Mac has a GPU (thus may not work on
virtual machines). virtual machines).
--jpeg-quality FLOAT RANGE Value in range 0.0 to 1.0 to use with --jpeg-quality FLOAT RANGE Value in range 0.0 to 1.0 to use with
--convert-to-jpeg. A value of 1.0 specifies --convert-to-jpeg. A value of 1.0 specifies
best quality, a value of 0.0 specifies maximum best quality, a value of 0.0 specifies maximum
compression. Defaults to 1.0 compression. Defaults to 1.0 [0.0<=x<=1.0]
--preview Export preview image generated by Photos. This --preview Export preview image generated by Photos. This
is a lower-resolution image used by Photos to is a lower-resolution image used by Photos to
quickly preview the image. See also --preview- quickly preview the image. See also --preview-
suffix and --preview-if-missing. suffix and --preview-if-missing.
--preview-if-missing Export preview image generated by Photos if --preview-if-missing Export preview image generated by Photos if
the actual photo file is missing from the the actual photo file is missing from the
library. This may be helpful if photos were library. This may be helpful if photos were
not copied to the Photos library and the not copied to the Photos library and the
original photo is missing. See also --preview- original photo is missing. See also --preview-
suffix and --preview. suffix and --preview.
--preview-suffix SUFFIX Optional suffix template for naming preview --preview-suffix SUFFIX Optional suffix template for naming preview
photos. Default name for preview photos is in photos. Default name for preview photos is in
form 'photoname_preview.ext'. For example, form 'photoname_preview.ext'. For example,
@@ -920,7 +859,6 @@ Options:
templates (see Templating System) are not templates (see Templating System) are not
permitted with --preview-suffix. See also permitted with --preview-suffix. See also
--preview and --preview-if-missing. --preview and --preview-if-missing.
--download-missing Attempt to download missing photos from --download-missing Attempt to download missing photos from
iCloud. The current implementation uses iCloud. The current implementation uses
Applescript to interact with Photos to export Applescript to interact with Photos to export
@@ -933,7 +871,6 @@ Options:
export all burst images; only the primary export all burst images; only the primary
photo will be exported--associated burst photo will be exported--associated burst
images will be skipped. images will be skipped.
--sidecar FORMAT Create sidecar for each photo exported; valid --sidecar FORMAT Create sidecar for each photo exported; valid
FORMAT values: xmp, json, exiftool; --sidecar FORMAT values: xmp, json, exiftool; --sidecar
xmp: create XMP sidecar used by Digikam, Adobe xmp: create XMP sidecar used by Digikam, Adobe
@@ -960,7 +897,6 @@ Options:
tags exported in the JSON and exiftool tags exported in the JSON and exiftool
sidecar, see '--exiftool'. See also '--ignore- sidecar, see '--exiftool'. See also '--ignore-
signature'. signature'.
--sidecar-drop-ext Drop the photo's extension when naming sidecar --sidecar-drop-ext Drop the photo's extension when naming sidecar
files. By default, sidecar files are named in files. By default, sidecar files are named in
format 'photo_filename.photo_ext.sidecar_ext', format 'photo_filename.photo_ext.sidecar_ext',
@@ -972,7 +908,6 @@ Options:
of different types but the same name in the of different types but the same name in the
output directory, e.g. 'IMG_1234.JPG' and output directory, e.g. 'IMG_1234.JPG' and
'IMG_1234.MOV'. 'IMG_1234.MOV'.
--exiftool Use exiftool to write metadata directly to --exiftool Use exiftool to write metadata directly to
exported photos. To use this option, exiftool exported photos. To use this option, exiftool
must be installed and in the path. exiftool must be installed and in the path. exiftool
@@ -994,10 +929,8 @@ Options:
QuickTime:ModifyDate (see also --ignore-date- QuickTime:ModifyDate (see also --ignore-date-
modified); QuickTime:GPSCoordinates; modified); QuickTime:GPSCoordinates;
UserData:GPSCoordinates. UserData:GPSCoordinates.
--exiftool-path EXIFTOOL_PATH Optionally specify path to exiftool; if not --exiftool-path EXIFTOOL_PATH Optionally specify path to exiftool; if not
provided, will look for exiftool in $PATH. provided, will look for exiftool in $PATH.
--exiftool-option OPTION Optional flag/option to pass to exiftool when --exiftool-option OPTION Optional flag/option to pass to exiftool when
using --exiftool. For example, --exiftool- using --exiftool. For example, --exiftool-
option '-m' to ignore minor warnings. Specify option '-m' to ignore minor warnings. Specify
@@ -1007,27 +940,21 @@ Options:
full list of options. More than one option may full list of options. More than one option may
be specified by repeating the option, e.g. be specified by repeating the option, e.g.
--exiftool-option '-m' --exiftool-option '-F'. --exiftool-option '-m' --exiftool-option '-F'.
--exiftool-merge-keywords Merge any keywords found in the original file --exiftool-merge-keywords Merge any keywords found in the original file
with keywords used for '--exiftool' and '-- with keywords used for '--exiftool' and '--
sidecar'. sidecar'.
--exiftool-merge-persons Merge any persons found in the original file --exiftool-merge-persons Merge any persons found in the original file
with persons used for '--exiftool' and '-- with persons used for '--exiftool' and '--
sidecar'. sidecar'.
--ignore-date-modified If used with --exiftool or --sidecar, will --ignore-date-modified If used with --exiftool or --sidecar, will
ignore the photo modification date and set ignore the photo modification date and set
EXIF:ModifyDate to EXIF:DateTimeOriginal; this EXIF:ModifyDate to EXIF:DateTimeOriginal; this
is consistent with how Photos handles the is consistent with how Photos handles the
EXIF:ModifyDate tag. EXIF:ModifyDate tag.
--person-keyword Use person in image as keyword/tag when --person-keyword Use person in image as keyword/tag when
exporting metadata. exporting metadata.
--album-keyword Use album name as keyword/tag when exporting --album-keyword Use album name as keyword/tag when exporting
metadata. metadata.
--keyword-template TEMPLATE For use with --exiftool, --sidecar; specify a --keyword-template TEMPLATE For use with --exiftool, --sidecar; specify a
template string to use as keyword in the form template string to use as keyword in the form
'{name,DEFAULT}' This is the same format as '{name,DEFAULT}' This is the same format as
@@ -1040,7 +967,6 @@ Options:
"{folder_album}" --keyword-template "{folder_album}" --keyword-template
"{created.year}". See '--replace-keywords' and "{created.year}". See '--replace-keywords' and
Templating System below. Templating System below.
--replace-keywords Replace keywords with any values specified --replace-keywords Replace keywords with any values specified
with --keyword-template. By default, with --keyword-template. By default,
--keyword-template will add keywords to any --keyword-template will add keywords to any
@@ -1049,7 +975,6 @@ Options:
from --keyword-template will replace any from --keyword-template will replace any
existing keywords instead of adding additional existing keywords instead of adding additional
keywords. keywords.
--description-template TEMPLATE --description-template TEMPLATE
For use with --exiftool, --sidecar; specify a For use with --exiftool, --sidecar; specify a
template string to use as description in the template string to use as description in the
@@ -1060,7 +985,6 @@ Options:
--description-template "{descr} exported with --description-template "{descr} exported with
osxphotos on {today.date}" See Templating osxphotos on {today.date}" See Templating
System below. System below.
--finder-tag-template TEMPLATE Set MacOS Finder tags to TEMPLATE. These tags --finder-tag-template TEMPLATE Set MacOS Finder tags to TEMPLATE. These tags
can be searched in the Finder or Spotlight can be searched in the Finder or Spotlight
with 'tag:tagname' format. For example, '-- with 'tag:tagname' format. For example, '--
@@ -1069,13 +993,11 @@ Options:
TEMPLATE values by using '--finder-tag- TEMPLATE values by using '--finder-tag-
template' multiple times. See also '--finder- template' multiple times. See also '--finder-
tag-keywords and Extended Attributes below.'. tag-keywords and Extended Attributes below.'.
--finder-tag-keywords Set MacOS Finder tags to keywords; any --finder-tag-keywords Set MacOS Finder tags to keywords; any
keywords specified via '--keyword-template', ' keywords specified via '--keyword-template', '
--person-keyword', etc. will also be used as --person-keyword', etc. will also be used as
Finder tags. See also '--finder-tag-template Finder tags. See also '--finder-tag-template
and Extended Attributes below.'. and Extended Attributes below.'.
--xattr-template ATTRIBUTE TEMPLATE --xattr-template ATTRIBUTE TEMPLATE
Set extended attribute ATTRIBUTE to TEMPLATE Set extended attribute ATTRIBUTE to TEMPLATE
value. Valid attributes are: 'authors', value. Valid attributes are: 'authors',
@@ -1088,19 +1010,16 @@ Options:
findercomment "{title}; {descr}" See Extended findercomment "{title}; {descr}" See Extended
Attributes below for additional details on Attributes below for additional details on
this option. this option.
--directory DIRECTORY Optional template for specifying name of --directory DIRECTORY Optional template for specifying name of
output directory in the form '{name,DEFAULT}'. output directory in the form '{name,DEFAULT}'.
See below for additional details on templating See below for additional details on templating
system. system.
--filename FILENAME Optional template for specifying name of --filename FILENAME Optional template for specifying name of
output file in the form '{name,DEFAULT}'. File output file in the form '{name,DEFAULT}'. File
extension will be added automatically--do not extension will be added automatically--do not
include an extension in the FILENAME template. include an extension in the FILENAME template.
See below for additional details on templating See below for additional details on templating
system. system.
--jpeg-ext EXTENSION Specify file extension for JPEG files. Photos --jpeg-ext EXTENSION Specify file extension for JPEG files. Photos
uses .jpeg for edited images but many images uses .jpeg for edited images but many images
are imported with .jpg or .JPG which can are imported with .jpg or .JPG which can
@@ -1110,14 +1029,12 @@ Options:
exported JPEG images. Valid values are jpeg, exported JPEG images. Valid values are jpeg,
jpg, JPEG, JPG; e.g. '--jpeg-ext jpg' to use jpg, JPEG, JPG; e.g. '--jpeg-ext jpg' to use
'.jpg' for all JPEGs. '.jpg' for all JPEGs.
--strip Optionally strip leading and trailing --strip Optionally strip leading and trailing
whitespace from any rendered templates. For whitespace from any rendered templates. For
example, if --filename template is "{title,} example, if --filename template is "{title,}
{original_name}" and image has no title, {original_name}" and image has no title,
resulting file would have a leading space but resulting file would have a leading space but
if used with --strip, this will be removed. if used with --strip, this will be removed.
--edited-suffix SUFFIX Optional suffix template for naming edited --edited-suffix SUFFIX Optional suffix template for naming edited
photos. Default name for edited photos is in photos. Default name for edited photos is in
form 'photoname_edited.ext'. For example, with form 'photoname_edited.ext'. For example, with
@@ -1127,7 +1044,6 @@ Options:
suffix is '_edited'. Multi-value templates suffix is '_edited'. Multi-value templates
(see Templating System) are not permitted with (see Templating System) are not permitted with
--edited-suffix. --edited-suffix.
--original-suffix SUFFIX Optional suffix template for naming original --original-suffix SUFFIX Optional suffix template for naming original
photos. Default name for original photos is photos. Default name for original photos is
in form 'filename.ext'. For example, with '-- in form 'filename.ext'. For example, with '--
@@ -1136,11 +1052,9 @@ Options:
default suffix is '' (no suffix). Multi-value default suffix is '' (no suffix). Multi-value
templates (see Templating System) are not templates (see Templating System) are not
permitted with --original-suffix. permitted with --original-suffix.
--use-photos-export Force the use of AppleScript or PhotoKit to --use-photos-export Force the use of AppleScript or PhotoKit to
export even if not missing (see also '-- export even if not missing (see also '--
download-missing' and '--use-photokit'). download-missing' and '--use-photokit').
--use-photokit Use with '--download-missing' or '--use- --use-photokit Use with '--download-missing' or '--use-
photos-export' to use direct Photos interface photos-export' to use direct Photos interface
instead of AppleScript to export. Highly instead of AppleScript to export. Highly
@@ -1148,11 +1062,9 @@ Options:
iTerm2 (use with Terminal.app). This is faster iTerm2 (use with Terminal.app). This is faster
and more reliable than the default AppleScript and more reliable than the default AppleScript
interface. interface.
--report <path to export report> --report <path to export report>
Write a CSV formatted report of all files that Write a CSV formatted report of all files that
were exported. were exported.
--cleanup Cleanup export directory by deleting any files --cleanup Cleanup export directory by deleting any files
which were not included in this export set. which were not included in this export set.
For example, photos which had previously been For example, photos which had previously been
@@ -1164,7 +1076,6 @@ Options:
you intend before using --cleanup. Use --dry- you intend before using --cleanup. Use --dry-
run with --cleanup first if you're not run with --cleanup first if you're not
certain. certain.
--add-exported-to-album ALBUM Add all exported photos to album ALBUM in --add-exported-to-album ALBUM Add all exported photos to album ALBUM in
Photos. Album ALBUM will be created if it Photos. Album ALBUM will be created if it
doesn't exist. All exported photos will be doesn't exist. All exported photos will be
@@ -1174,7 +1085,6 @@ Options:
feature is currently experimental. I don't feature is currently experimental. I don't
know how well it will work on large export know how well it will work on large export
sets. sets.
--add-skipped-to-album ALBUM Add all skipped photos to album ALBUM in --add-skipped-to-album ALBUM Add all skipped photos to album ALBUM in
Photos. Album ALBUM will be created if it Photos. Album ALBUM will be created if it
doesn't exist. All skipped photos will be doesn't exist. All skipped photos will be
@@ -1184,7 +1094,6 @@ Options:
feature is currently experimental. I don't feature is currently experimental. I don't
know how well it will work on large export know how well it will work on large export
sets. sets.
--add-missing-to-album ALBUM Add all missing photos to album ALBUM in --add-missing-to-album ALBUM Add all missing photos to album ALBUM in
Photos. Album ALBUM will be created if it Photos. Album ALBUM will be created if it
doesn't exist. All missing photos will be doesn't exist. All missing photos will be
@@ -1194,7 +1103,6 @@ Options:
feature is currently experimental. I don't feature is currently experimental. I don't
know how well it will work on large export know how well it will work on large export
sets. sets.
--post-command CATEGORY COMMAND --post-command CATEGORY COMMAND
Run COMMAND on exported files of category Run COMMAND on exported files of category
CATEGORY. CATEGORY can be one of: exported, CATEGORY. CATEGORY can be one of: exported,
@@ -1213,7 +1121,6 @@ Options:
command by repeating the '--post-command' command by repeating the '--post-command'
option with different arguments. See Post option with different arguments. See Post
Command below. Command below.
--post-function filename.py::function --post-function filename.py::function
Run function on exported files. Use this in Run function on exported files. Use this in
format: --post-function filename.py::function format: --post-function filename.py::function
@@ -1226,7 +1133,6 @@ Options:
You can run more than one function by You can run more than one function by
repeating the '--post-function' option with repeating the '--post-function' option with
different arguments. See Post Function below. different arguments. See Post Function below.
--exportdb EXPORTDB_FILE Specify alternate name for database file which --exportdb EXPORTDB_FILE Specify alternate name for database file which
stores state information for export and stores state information for export and
--update. If --exportdb is not specified, --update. If --exportdb is not specified,
@@ -1235,7 +1141,6 @@ Options:
directory. Must be specified as filename directory. Must be specified as filename
only, not a path, as export database will be only, not a path, as export database will be
saved in export directory. saved in export directory.
--load-config <config file path> --load-config <config file path>
Load options from file as written with --save- Load options from file as written with --save-
config. This allows you to save a complex config. This allows you to save a complex
@@ -1247,11 +1152,9 @@ Options:
line options are used in conjunction with line options are used in conjunction with
--load-config, they will override the --load-config, they will override the
corresponding values in the config file. corresponding values in the config file.
--save-config <config file path> --save-config <config file path>
Save options to file for use with --load- Save options to file for use with --load-
config. File format is TOML. config. File format is TOML.
--help Show this message and exit. --help Show this message and exit.
** Export ** ** Export **
@@ -1306,59 +1209,45 @@ The following attributes may be used with '--xattr-template':
authors The author, or authors, of the contents of the file. A list of authors The author, or authors, of the contents of the file. A list of
strings. (com.apple.metadata:kMDItemAuthors) strings. (com.apple.metadata:kMDItemAuthors)
comment A comment related to the file. This differs from the Finder comment A comment related to the file. This differs from the Finder
comment, kMDItemFinderComment. A string. comment, kMDItemFinderComment. A string.
(com.apple.metadata:kMDItemComment) (com.apple.metadata:kMDItemComment)
copyright The copyright owner of the file contents. A string. copyright The copyright owner of the file contents. A string.
(com.apple.metadata:kMDItemCopyright) (com.apple.metadata:kMDItemCopyright)
creator Application used to create the document content (for example creator Application used to create the document content (for example
“Word”, “Pages”, and so on). A string. “Word”, “Pages”, and so on). A string.
(com.apple.metadata:kMDItemCreator) (com.apple.metadata:kMDItemCreator)
description A description of the content of the resource. The description description A description of the content of the resource. The description
may include an abstract, table of contents, reference to a may include an abstract, table of contents, reference to a
graphical representation of content or a free-text account of graphical representation of content or a free-text account of
the content. A string. (com.apple.metadata:kMDItemDescription) the content. A string. (com.apple.metadata:kMDItemDescription)
findercomment Finder comments for this file. A string. findercomment Finder comments for this file. A string.
(com.apple.metadata:kMDItemFinderComment) (com.apple.metadata:kMDItemFinderComment)
headline A publishable entry providing a synopsis of the contents of the headline A publishable entry providing a synopsis of the contents of the
file. A string. (com.apple.metadata:kMDItemHeadline) file. A string. (com.apple.metadata:kMDItemHeadline)
keywords Keywords associated with this file. For example, “Birthday”, keywords Keywords associated with this file. For example, “Birthday”,
“Important”, etc. This differs from Finder tags “Important”, etc. This differs from Finder tags
(_kMDItemUserTags) which are keywords/tags shown in the Finder (_kMDItemUserTags) which are keywords/tags shown in the Finder
and searchable in Spotlight using "tag:tag_name". A list of and searchable in Spotlight using "tag:tag_name". A list of
strings. (com.apple.metadata:kMDItemKeywords) strings. (com.apple.metadata:kMDItemKeywords)
participants The list of people who are visible in an image or movie or participants The list of people who are visible in an image or movie or
written about in a document. A list of strings. written about in a document. A list of strings.
(com.apple.metadata:kMDItemParticipants) (com.apple.metadata:kMDItemParticipants)
projects The list of projects that this file is part of. For example, if projects The list of projects that this file is part of. For example, if
you were working on a movie all of the files could be marked as you were working on a movie all of the files could be marked as
belonging to the project “My Movie”. A list of strings. belonging to the project “My Movie”. A list of strings.
(com.apple.metadata:kMDItemProjects) (com.apple.metadata:kMDItemProjects)
rating User rating of this item. For example, the stars rating of an rating User rating of this item. For example, the stars rating of an
iTunes track. An integer. iTunes track. An integer.
(com.apple.metadata:kMDItemStarRating) (com.apple.metadata:kMDItemStarRating)
subject Subject of the this item. A string. subject Subject of the this item. A string.
(com.apple.metadata:kMDItemSubject) (com.apple.metadata:kMDItemSubject)
title The title of the file. For example, this could be the title of title The title of the file. For example, this could be the title of
a document, the name of a song, or the subject of an email a document, the name of a song, or the subject of an email
message. A string. (com.apple.metadata:kMDItemTitle) message. A string. (com.apple.metadata:kMDItemTitle)
version The version number of this file. A string. version The version number of this file. A string.
(com.apple.metadata:kMDItemVersion) (com.apple.metadata:kMDItemVersion)
For additional information on extended attributes see: https://developer.apple.c For additional information on extended attributes see: https://developer.apple.c
om/documentation/coreservices/file_metadata/mditem/common_metadata_attribute_key om/documentation/coreservices/file_metadata/mditem/common_metadata_attribute_key
s s
@@ -1584,7 +1473,6 @@ Substitution Description
{name} Current filename of the photo {name} Current filename of the photo
{original_name} Photo's original filename when imported to {original_name} Photo's original filename when imported to
Photos Photos
{title} Title of the photo {title} Title of the photo
{descr} Description of the photo {descr} Description of the photo
{media_type} Special media type resolved in this {media_type} Special media type resolved in this
@@ -1594,48 +1482,35 @@ Substitution Description
'video' if no special type. Customize one or 'video' if no special type. Customize one or
more media types using format: '{media_type,vi more media types using format: '{media_type,vi
deo=vidéo;time_lapse=vidéo_accélérée}' deo=vidéo;time_lapse=vidéo_accélérée}'
{photo_or_video} 'photo' or 'video' depending on what type the {photo_or_video} 'photo' or 'video' depending on what type the
image is. To customize, use default value as image is. To customize, use default value as
in '{photo_or_video,photo=fotos;video=videos}' in '{photo_or_video,photo=fotos;video=videos}'
{hdr} Photo is HDR?; True/False value, use in format {hdr} Photo is HDR?; True/False value, use in format
'{hdr?VALUE_IF_TRUE,VALUE_IF_FALSE}' '{hdr?VALUE_IF_TRUE,VALUE_IF_FALSE}'
{edited} True if photo has been edited (has {edited} True if photo has been edited (has
adjustments), otherwise False; use in format adjustments), otherwise False; use in format
'{edited?VALUE_IF_TRUE,VALUE_IF_FALSE}' '{edited?VALUE_IF_TRUE,VALUE_IF_FALSE}'
{edited_version} True if template is being rendered for the {edited_version} True if template is being rendered for the
edited version of a photo, otherwise False. edited version of a photo, otherwise False.
{favorite} Photo has been marked as favorite?; True/False {favorite} Photo has been marked as favorite?; True/False
value, use in format value, use in format
'{favorite?VALUE_IF_TRUE,VALUE_IF_FALSE}' '{favorite?VALUE_IF_TRUE,VALUE_IF_FALSE}'
{created.date} Photo's creation date in ISO format, e.g. {created.date} Photo's creation date in ISO format, e.g.
'2020-03-22' '2020-03-22'
{created.year} 4-digit year of photo creation time {created.year} 4-digit year of photo creation time
{created.yy} 2-digit year of photo creation time {created.yy} 2-digit year of photo creation time
{created.mm} 2-digit month of the photo creation time (zero {created.mm} 2-digit month of the photo creation time (zero
padded) padded)
{created.month} Month name in user's locale of the photo {created.month} Month name in user's locale of the photo
creation time creation time
{created.mon} Month abbreviation in the user's locale of the {created.mon} Month abbreviation in the user's locale of the
photo creation time photo creation time
{created.dd} 2-digit day of the month (zero padded) of {created.dd} 2-digit day of the month (zero padded) of
photo creation time photo creation time
{created.dow} Day of week in user's locale of the photo {created.dow} Day of week in user's locale of the photo
creation time creation time
{created.doy} 3-digit day of year (e.g Julian day) of photo {created.doy} 3-digit day of year (e.g Julian day) of photo
creation time, starting from 1 (zero padded) creation time, starting from 1 (zero padded)
{created.hour} 2-digit hour of the photo creation time {created.hour} 2-digit hour of the photo creation time
{created.min} 2-digit minute of the photo creation time {created.min} 2-digit minute of the photo creation time
{created.sec} 2-digit second of the photo creation time {created.sec} 2-digit second of the photo creation time
@@ -1648,51 +1523,38 @@ Substitution Description
no template will return null value. See no template will return null value. See
https://strftime.org/ for help on strftime https://strftime.org/ for help on strftime
templates. templates.
{modified.date} Photo's modification date in ISO format, e.g. {modified.date} Photo's modification date in ISO format, e.g.
'2020-03-22'; uses creation date if photo is '2020-03-22'; uses creation date if photo is
not modified not modified
{modified.year} 4-digit year of photo modification time; uses {modified.year} 4-digit year of photo modification time; uses
creation date if photo is not modified creation date if photo is not modified
{modified.yy} 2-digit year of photo modification time; uses {modified.yy} 2-digit year of photo modification time; uses
creation date if photo is not modified creation date if photo is not modified
{modified.mm} 2-digit month of the photo modification time {modified.mm} 2-digit month of the photo modification time
(zero padded); uses creation date if photo is (zero padded); uses creation date if photo is
not modified not modified
{modified.month} Month name in user's locale of the photo {modified.month} Month name in user's locale of the photo
modification time; uses creation date if photo modification time; uses creation date if photo
is not modified is not modified
{modified.mon} Month abbreviation in the user's locale of the {modified.mon} Month abbreviation in the user's locale of the
photo modification time; uses creation date if photo modification time; uses creation date if
photo is not modified photo is not modified
{modified.dd} 2-digit day of the month (zero padded) of the {modified.dd} 2-digit day of the month (zero padded) of the
photo modification time; uses creation date if photo modification time; uses creation date if
photo is not modified photo is not modified
{modified.dow} Day of week in user's locale of the photo {modified.dow} Day of week in user's locale of the photo
modification time; uses creation date if photo modification time; uses creation date if photo
is not modified is not modified
{modified.doy} 3-digit day of year (e.g Julian day) of photo {modified.doy} 3-digit day of year (e.g Julian day) of photo
modification time, starting from 1 (zero modification time, starting from 1 (zero
padded); uses creation date if photo is not padded); uses creation date if photo is not
modified modified
{modified.hour} 2-digit hour of the photo modification time; {modified.hour} 2-digit hour of the photo modification time;
uses creation date if photo is not modified uses creation date if photo is not modified
{modified.min} 2-digit minute of the photo modification time; {modified.min} 2-digit minute of the photo modification time;
uses creation date if photo is not modified uses creation date if photo is not modified
{modified.sec} 2-digit second of the photo modification time; {modified.sec} 2-digit second of the photo modification time;
uses creation date if photo is not modified uses creation date if photo is not modified
{modified.strftime} Apply strftime template to file modification {modified.strftime} Apply strftime template to file modification
date/time. Should be used in form date/time. Should be used in form
{modified.strftime,TEMPLATE} where TEMPLATE is {modified.strftime,TEMPLATE} where TEMPLATE is
@@ -1703,28 +1565,21 @@ Substitution Description
creation date if photo is not modified. See creation date if photo is not modified. See
https://strftime.org/ for help on strftime https://strftime.org/ for help on strftime
templates. templates.
{today.date} Current date in iso format, e.g. '2020-03-22' {today.date} Current date in iso format, e.g. '2020-03-22'
{today.year} 4-digit year of current date {today.year} 4-digit year of current date
{today.yy} 2-digit year of current date {today.yy} 2-digit year of current date
{today.mm} 2-digit month of the current date (zero {today.mm} 2-digit month of the current date (zero
padded) padded)
{today.month} Month name in user's locale of the current {today.month} Month name in user's locale of the current
date date
{today.mon} Month abbreviation in the user's locale of the {today.mon} Month abbreviation in the user's locale of the
current date current date
{today.dd} 2-digit day of the month (zero padded) of {today.dd} 2-digit day of the month (zero padded) of
current date current date
{today.dow} Day of week in user's locale of the current {today.dow} Day of week in user's locale of the current
date date
{today.doy} 3-digit day of year (e.g Julian day) of {today.doy} 3-digit day of year (e.g Julian day) of
current date, starting from 1 (zero padded) current date, starting from 1 (zero padded)
{today.hour} 2-digit hour of the current date {today.hour} 2-digit hour of the current date
{today.min} 2-digit minute of the current date {today.min} 2-digit minute of the current date
{today.sec} 2-digit second of the current date {today.sec} 2-digit second of the current date
@@ -1737,70 +1592,51 @@ Substitution Description
no template will return null value. See no template will return null value. See
https://strftime.org/ for help on strftime https://strftime.org/ for help on strftime
templates. templates.
{place.name} Place name from the photo's reverse {place.name} Place name from the photo's reverse
geolocation data, as displayed in Photos geolocation data, as displayed in Photos
{place.country_code} The ISO country code from the photo's reverse {place.country_code} The ISO country code from the photo's reverse
geolocation data geolocation data
{place.name.country} Country name from the photo's reverse {place.name.country} Country name from the photo's reverse
geolocation data geolocation data
{place.name.state_province} State or province name from the photo's {place.name.state_province} State or province name from the photo's
reverse geolocation data reverse geolocation data
{place.name.city} City or locality name from the photo's reverse {place.name.city} City or locality name from the photo's reverse
geolocation data geolocation data
{place.name.area_of_interest} Area of interest name (e.g. landmark or public {place.name.area_of_interest} Area of interest name (e.g. landmark or public
place) from the photo's reverse geolocation place) from the photo's reverse geolocation
data data
{place.address} Postal address from the photo's reverse {place.address} Postal address from the photo's reverse
geolocation data, e.g. '2007 18th St NW, geolocation data, e.g. '2007 18th St NW,
Washington, DC 20009, United States' Washington, DC 20009, United States'
{place.address.street} Street part of the postal address, e.g. '2007 {place.address.street} Street part of the postal address, e.g. '2007
18th St NW' 18th St NW'
{place.address.city} City part of the postal address, e.g. {place.address.city} City part of the postal address, e.g.
'Washington' 'Washington'
{place.address.state_province} State/province part of the postal address, {place.address.state_province} State/province part of the postal address,
e.g. 'DC' e.g. 'DC'
{place.address.postal_code} Postal code part of the postal address, e.g. {place.address.postal_code} Postal code part of the postal address, e.g.
'20009' '20009'
{place.address.country} Country name of the postal address, e.g. {place.address.country} Country name of the postal address, e.g.
'United States' 'United States'
{place.address.country_code} ISO country code of the postal address, e.g. {place.address.country_code} ISO country code of the postal address, e.g.
'US' 'US'
{searchinfo.season} Season of the year associated with a photo, {searchinfo.season} Season of the year associated with a photo,
e.g. 'Summer'; (Photos 5+ only, applied e.g. 'Summer'; (Photos 5+ only, applied
automatically by Photos' image categorization automatically by Photos' image categorization
algorithms). algorithms).
{exif.camera_make} Camera make from original photo's EXIF {exif.camera_make} Camera make from original photo's EXIF
information as imported by Photos, e.g. information as imported by Photos, e.g.
'Apple' 'Apple'
{exif.camera_model} Camera model from original photo's EXIF {exif.camera_model} Camera model from original photo's EXIF
information as imported by Photos, e.g. information as imported by Photos, e.g.
'iPhone 6s' 'iPhone 6s'
{exif.lens_model} Lens model from original photo's EXIF {exif.lens_model} Lens model from original photo's EXIF
information as imported by Photos, e.g. information as imported by Photos, e.g.
'iPhone 6s back camera 4.15mm f/2.2' 'iPhone 6s back camera 4.15mm f/2.2'
{uuid} Photo's internal universally unique identifier {uuid} Photo's internal universally unique identifier
(UUID) for the photo, a 36-character string (UUID) for the photo, a 36-character string
unique to the photo, e.g. unique to the photo, e.g.
'128FB4C6-0B16-4E7D-9108-FB2E90DA1546' '128FB4C6-0B16-4E7D-9108-FB2E90DA1546'
{id} A unique number for the photo based on its {id} A unique number for the photo based on its
primary key in the Photos database. A primary key in the Photos database. A
sequential integer, e.g. 1, 2, 3...etc. Each sequential integer, e.g. 1, 2, 3...etc. Each
@@ -1811,7 +1647,6 @@ Substitution Description
5-digit integer and pad with zeros, use 5-digit integer and pad with zeros, use
'{id:05d}' which results in 00001, 00002, '{id:05d}' which results in 00001, 00002,
00003...etc. 00003...etc.
{album_seq} An integer, starting at 0, indicating the {album_seq} An integer, starting at 0, indicating the
photo's index (sequence) in the containing photo's index (sequence) in the containing
album. Only valid when used in a '--filename' album. Only valid when used in a '--filename'
@@ -1831,7 +1666,6 @@ Substitution Description
This may result in incorrect sequences if you This may result in incorrect sequences if you
have duplicate albums with the same name; see have duplicate albums with the same name; see
also '{folder_album_seq}'. also '{folder_album_seq}'.
{folder_album_seq} An integer, starting at 0, indicating the {folder_album_seq} An integer, starting at 0, indicating the
photo's index (sequence) in the containing photo's index (sequence) in the containing
album and folder path. Only valid when used in album and folder path. Only valid when used in
@@ -1852,7 +1686,6 @@ Substitution Description
incorrect sequences if you have duplicate incorrect sequences if you have duplicate
albums with the same name in the same folder; albums with the same name in the same folder;
see also '{album_seq}'. see also '{album_seq}'.
{comma} A comma: ',' {comma} A comma: ','
{semicolon} A semicolon: ';' {semicolon} A semicolon: ';'
{questionmark} A question mark: '?' {questionmark} A question mark: '?'
@@ -1867,7 +1700,7 @@ Substitution Description
{lf} A line feed: '\n', alias for {newline} {lf} A line feed: '\n', alias for {newline}
{cr} A carriage return: '\r' {cr} A carriage return: '\r'
{crlf} a carriage return + line feed: '\r\n' {crlf} a carriage return + line feed: '\r\n'
{osxphotos_version} The osxphotos version, e.g. '0.42.67' {osxphotos_version} The osxphotos version, e.g. '0.42.68'
{osxphotos_cmd_line} The full command line used to run osxphotos {osxphotos_cmd_line} The full command line used to run osxphotos
The following substitutions may result in multiple values. Thus if specified for The following substitutions may result in multiple values. Thus if specified for
@@ -1882,7 +1715,6 @@ Substitution Description
{folder_album} Folder path + album photo is contained in. e.g. {folder_album} Folder path + album photo is contained in. e.g.
'Folder/Subfolder/Album' or just 'Album' if no 'Folder/Subfolder/Album' or just 'Album' if no
enclosing folder enclosing folder
{keyword} Keyword(s) assigned to photo {keyword} Keyword(s) assigned to photo
{person} Person(s) / face(s) in a photo {person} Person(s) / face(s) in a photo
{label} Image categorization label associated with a photo {label} Image categorization label associated with a photo
@@ -1891,11 +1723,9 @@ Substitution Description
categorize images. These are not the same as categorize images. These are not the same as
{keyword} which refers to the user-defined {keyword} which refers to the user-defined
keywords/tags applied in Photos. keywords/tags applied in Photos.
{label_normalized} All lower case version of 'label' (Photos 5+ only) {label_normalized} All lower case version of 'label' (Photos 5+ only)
{comment} Comment(s) on shared Photos; format is 'Person name: {comment} Comment(s) on shared Photos; format is 'Person name:
comment text' (Photos 5+ only) comment text' (Photos 5+ only)
{exiftool} Format: '{exiftool:GROUP:TAGNAME}'; use exiftool {exiftool} Format: '{exiftool:GROUP:TAGNAME}'; use exiftool
(https://exiftool.org) to extract metadata, in form (https://exiftool.org) to extract metadata, in form
GROUP:TAGNAME, from image. E.g. GROUP:TAGNAME, from image. E.g.
@@ -1905,24 +1735,19 @@ Substitution Description
names. You must specify group (e.g. EXIF, IPTC, etc) names. You must specify group (e.g. EXIF, IPTC, etc)
as used in `exiftool -G`. exiftool must be installed as used in `exiftool -G`. exiftool must be installed
in the path to use this template. in the path to use this template.
{searchinfo.holiday} Holiday names associated with a photo, e.g. {searchinfo.holiday} Holiday names associated with a photo, e.g.
'Christmas Day'; (Photos 5+ only, applied 'Christmas Day'; (Photos 5+ only, applied
automatically by Photos' image categorization automatically by Photos' image categorization
algorithms). algorithms).
{searchinfo.activity} Activities associated with a photo, e.g. 'Sporting {searchinfo.activity} Activities associated with a photo, e.g. 'Sporting
Event'; (Photos 5+ only, applied automatically by Event'; (Photos 5+ only, applied automatically by
Photos' image categorization algorithms). Photos' image categorization algorithms).
{searchinfo.venue} Venues associated with a photo, e.g. name of {searchinfo.venue} Venues associated with a photo, e.g. name of
restaurant; (Photos 5+ only, applied automatically by restaurant; (Photos 5+ only, applied automatically by
Photos' image categorization algorithms). Photos' image categorization algorithms).
{searchinfo.venue_type} Venue types associated with a photo, e.g. {searchinfo.venue_type} Venue types associated with a photo, e.g.
'Restaurant'; (Photos 5+ only, applied automatically 'Restaurant'; (Photos 5+ only, applied automatically
by Photos' image categorization algorithms). by Photos' image categorization algorithms).
{photo} Provides direct access to the PhotoInfo object for {photo} Provides direct access to the PhotoInfo object for
the photo. Must be used in format '{photo.property}' the photo. Must be used in format '{photo.property}'
where 'property' represents a PhotoInfo property. For where 'property' represents a PhotoInfo property. For
@@ -1934,12 +1759,23 @@ Substitution Description
underlying PhotoInfo class. See underlying PhotoInfo class. See
https://rhettbull.github.io/osxphotos/ for additional https://rhettbull.github.io/osxphotos/ for additional
documentation on the PhotoInfo class. documentation on the PhotoInfo class.
{detected_text} List of text strings found in the image after
performing text detection. Using '{detected_text}'
will cause osxphotos to perform text detection using
the built-in macOS text detection algorithms which
will slow down your export. The results for each
photo will be cached in the export database so that
future exports with '--update' do not need to
reprocess each photo. You may pass a confidence
threshold value between 0.0 and 1.0 after a colon as
in '{detected_text:0.5}'; The default confidence
threshold is 0.75. Note: this feature is not the same
thing as Live Text in macOS Monterey, which osxphotos
does not yet support.
{shell_quote} Use in form '{shell_quote,TEMPLATE}'; quotes the {shell_quote} Use in form '{shell_quote,TEMPLATE}'; quotes the
rendered TEMPLATE value(s) for safe usage in the rendered TEMPLATE value(s) for safe usage in the
shell, e.g. My file.jpeg => 'My file.jpeg'; only adds shell, e.g. My file.jpeg => 'My file.jpeg'; only adds
quotes if needed. quotes if needed.
{function} Execute a python function from an external file and {function} Execute a python function from an external file and
use return value as template substitution. Use in use return value as template substitution. Use in
format: {function:file.py::function_name} where format: {function:file.py::function_name} where
@@ -1950,7 +1786,6 @@ Substitution Description
/blob/master/examples/template_function.py for an /blob/master/examples/template_function.py for an
example of how to implement a template function. example of how to implement a template function.
The following substitutions are file or directory paths. You can access various The following substitutions are file or directory paths. You can access various
parts of the path using the following modifiers: parts of the path using the following modifiers:
@@ -1985,41 +1820,29 @@ exported All exported files
new When used with '--update', all newly exported files new When used with '--update', all newly exported files
updated When used with '--update', all files which were updated When used with '--update', all files which were
previously exported but updated this time previously exported but updated this time
skipped When used with '--update', all files which were skipped When used with '--update', all files which were
skipped (because they were previously exported and skipped (because they were previously exported and
didn't change) didn't change)
missing All files which were not exported because they were missing All files which were not exported because they were
missing from the Photos library missing from the Photos library
exif_updated When used with '--exiftool', all files on which exif_updated When used with '--exiftool', all files on which
exiftool updated the metadata exiftool updated the metadata
touched When used with '--touch-file', all files where the touched When used with '--touch-file', all files where the
date was touched date was touched
converted_to_jpeg When used with '--convert-to-jpeg', all files which converted_to_jpeg When used with '--convert-to-jpeg', all files which
were converted to jpeg were converted to jpeg
sidecar_json_written When used with '--sidecar json', all JSON sidecar sidecar_json_written When used with '--sidecar json', all JSON sidecar
files which were written files which were written
sidecar_json_skipped When used with '--sidecar json' and '--update', all sidecar_json_skipped When used with '--sidecar json' and '--update', all
JSON sidecar files which were skipped JSON sidecar files which were skipped
sidecar_exiftool_written When used with '--sidecar exiftool', all exiftool sidecar_exiftool_written When used with '--sidecar exiftool', all exiftool
sidecar files which were written sidecar files which were written
sidecar_exiftool_skipped When used with '--sidecar exiftool' and '--update, sidecar_exiftool_skipped When used with '--sidecar exiftool' and '--update,
all exiftool sidecar files which were skipped all exiftool sidecar files which were skipped
sidecar_xmp_written When used with '--sidecar xmp', all XMP sidecar sidecar_xmp_written When used with '--sidecar xmp', all XMP sidecar
files which were written files which were written
sidecar_xmp_skipped When used with '--sidecar xmp' and '--update', all sidecar_xmp_skipped When used with '--sidecar xmp' and '--update', all
XMP sidecar files which were skipped XMP sidecar files which were skipped
error All files which produced an error during export error All files which produced an error during export
In addition to all normal template fields, the template fields '{filepath}' and In addition to all normal template fields, the template fields '{filepath}' and
@@ -3730,7 +3553,7 @@ The following template field substitutions are availabe for use the templating s
|{lf}|A line feed: '\n', alias for {newline}| |{lf}|A line feed: '\n', alias for {newline}|
|{cr}|A carriage return: '\r'| |{cr}|A carriage return: '\r'|
|{crlf}|a carriage return + line feed: '\r\n'| |{crlf}|a carriage return + line feed: '\r\n'|
|{osxphotos_version}|The osxphotos version, e.g. '0.42.67'| |{osxphotos_version}|The osxphotos version, e.g. '0.42.68'|
|{osxphotos_cmd_line}|The full command line used to run osxphotos| |{osxphotos_cmd_line}|The full command line used to run osxphotos|
|{album}|Album(s) photo is contained in| |{album}|Album(s) photo is contained in|
|{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder| |{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|
@@ -3745,6 +3568,7 @@ The following template field substitutions are availabe for use the templating s
|{searchinfo.venue}|Venues associated with a photo, e.g. name of restaurant; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).| |{searchinfo.venue}|Venues associated with a photo, e.g. name of restaurant; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|{searchinfo.venue_type}|Venue types associated with a photo, e.g. 'Restaurant'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).| |{searchinfo.venue_type}|Venue types associated with a photo, e.g. 'Restaurant'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|{photo}|Provides direct access to the PhotoInfo object for the photo. Must be used in format '{photo.property}' where 'property' represents a PhotoInfo property. For example: '{photo.favorite}' is the same as '{favorite}' and '{photo.place.name}' is the same as '{place.name}'. '{photo}' provides access to properties that are not available as separate template fields but it assumes some knowledge of the underlying PhotoInfo class. See https://rhettbull.github.io/osxphotos/ for additional documentation on the PhotoInfo class.| |{photo}|Provides direct access to the PhotoInfo object for the photo. Must be used in format '{photo.property}' where 'property' represents a PhotoInfo property. For example: '{photo.favorite}' is the same as '{favorite}' and '{photo.place.name}' is the same as '{place.name}'. '{photo}' provides access to properties that are not available as separate template fields but it assumes some knowledge of the underlying PhotoInfo class. See https://rhettbull.github.io/osxphotos/ for additional documentation on the PhotoInfo class.|
|{detected_text}|List of text strings found in the image after performing text detection. Using '{detected_text}' will cause osxphotos to perform text detection using the built-in macOS text detection algorithms which will slow down your export. The results for each photo will be cached in the export database so that future exports with '--update' do not need to reprocess each photo. You may pass a confidence threshold value between 0.0 and 1.0 after a colon as in '{detected_text:0.5}'; The default confidence threshold is 0.75. Note: this feature is not the same thing as Live Text in macOS Monterey, which osxphotos does not yet support.|
|{shell_quote}|Use in form '{shell_quote,TEMPLATE}'; quotes the rendered TEMPLATE value(s) for safe usage in the shell, e.g. My file.jpeg => 'My file.jpeg'; only adds quotes if needed.| |{shell_quote}|Use in form '{shell_quote,TEMPLATE}'; quotes the rendered TEMPLATE value(s) for safe usage in the shell, e.g. My file.jpeg => 'My file.jpeg'; only adds quotes if needed.|
|{function}|Execute a python function from an external file and use return value as template substitution. Use in format: {function:file.py::function_name} where 'file.py' is the name of the python file and 'function_name' is the name of the function to call. The function will be passed the PhotoInfo object for the photo. See https://github.com/RhetTbull/osxphotos/blob/master/examples/template_function.py for an example of how to implement a template function.| |{function}|Execute a python function from an external file and use return value as template substitution. Use in format: {function:file.py::function_name} where 'file.py' is the name of the python file and 'function_name' is the name of the function to call. The function will be passed the PhotoInfo object for the photo. See https://github.com/RhetTbull/osxphotos/blob/master/examples/template_function.py for an example of how to implement a template function.|
<!-- OSXPHOTOS-TEMPLATE-TABLE:END --> <!-- OSXPHOTOS-TEMPLATE-TABLE:END -->

View File

@@ -1,3 +1,3 @@
""" version info """ """ version info """
__version__ = "0.42.68" __version__ = "0.42.69"

View File

@@ -1796,6 +1796,7 @@ def export(
export_dir=dest, export_dir=dest,
dry_run=dry_run, dry_run=dry_run,
exiftool_path=exiftool_path, exiftool_path=exiftool_path,
export_db=export_db,
) )
if album_export and export_results.exported: if album_export and export_results.exported:
@@ -1865,6 +1866,7 @@ def export(
finder_tag_template=finder_tag_template, finder_tag_template=finder_tag_template,
strip=strip, strip=strip,
export_dir=dest, export_dir=dest,
export_db=export_db,
) )
results.xattr_written.extend(tags_written) results.xattr_written.extend(tags_written)
results.xattr_skipped.extend(tags_skipped) results.xattr_skipped.extend(tags_skipped)
@@ -1876,6 +1878,7 @@ def export(
xattr_template, xattr_template,
strip=strip, strip=strip,
export_dir=dest, export_dir=dest,
export_db=export_db,
) )
results.xattr_written.extend(xattr_written) results.xattr_written.extend(xattr_written)
results.xattr_skipped.extend(xattr_skipped) results.xattr_skipped.extend(xattr_skipped)
@@ -2538,10 +2541,22 @@ def export_photo(
sidecar_flags |= SIDECAR_EXIFTOOL sidecar_flags |= SIDECAR_EXIFTOOL
rendered_suffix = _render_suffix_template( rendered_suffix = _render_suffix_template(
original_suffix, "original_suffix", "--original-suffix", strip, dest, photo original_suffix,
"original_suffix",
"--original-suffix",
strip,
dest,
photo,
export_db,
) )
rendered_preview_suffix = _render_suffix_template( rendered_preview_suffix = _render_suffix_template(
preview_suffix, "preview_suffix", "--preview-suffix", strip, dest, photo preview_suffix,
"preview_suffix",
"--preview-suffix",
strip,
dest,
photo,
export_db,
) )
# if download_missing and the photo is missing or path doesn't exist, # if download_missing and the photo is missing or path doesn't exist,
@@ -2557,11 +2572,24 @@ def export_photo(
results = ExportResults() results = ExportResults()
dest_paths = get_dirnames_from_template( dest_paths = get_dirnames_from_template(
photo, directory, export_by_date, dest, dry_run, strip=strip, edited=False photo,
directory,
export_by_date,
dest,
dry_run,
strip=strip,
edited=False,
export_db=export_db,
) )
for dest_path in dest_paths: for dest_path in dest_paths:
filenames = get_filenames_from_template( filenames = get_filenames_from_template(
photo, filename_template, dest, dest_path, original_name, strip=strip photo,
filename_template,
dest,
dest_path,
original_name,
strip=strip,
export_db=export_db,
) )
for filename in filenames: for filename in filenames:
@@ -2632,12 +2660,26 @@ def export_photo(
if export_edited and photo.hasadjustments: if export_edited and photo.hasadjustments:
dest_paths = get_dirnames_from_template( dest_paths = get_dirnames_from_template(
photo, directory, export_by_date, dest, dry_run, strip=strip, edited=True photo,
directory,
export_by_date,
dest,
dry_run,
strip=strip,
edited=True,
export_db=export_db,
) )
for dest_path in dest_paths: for dest_path in dest_paths:
# if export-edited, also export the edited version # if export-edited, also export the edited version
edited_filenames = get_filenames_from_template( edited_filenames = get_filenames_from_template(
photo, filename_template, dest, dest_path, original_name, strip=strip, edited=True photo,
filename_template,
dest,
dest_path,
original_name,
strip=strip,
edited=True,
export_db=export_db,
) )
for edited_filename in edited_filenames: for edited_filename in edited_filenames:
edited_filename = pathlib.Path(edited_filename) edited_filename = pathlib.Path(edited_filename)
@@ -2674,6 +2716,7 @@ def export_photo(
strip, strip,
dest, dest,
photo, photo,
export_db,
) )
edited_filename = ( edited_filename = (
f"{edited_filename.stem}{rendered_edited_suffix}{edited_ext}" f"{edited_filename.stem}{rendered_edited_suffix}{edited_ext}"
@@ -2729,7 +2772,9 @@ def export_photo(
return results return results
def _render_suffix_template(suffix_template, var_name, option_name, strip, dest, photo): def _render_suffix_template(
suffix_template, var_name, option_name, strip, dest, photo, export_db
):
"""render suffix template """render suffix template
Returns: Returns:
@@ -2739,7 +2784,9 @@ def _render_suffix_template(suffix_template, var_name, option_name, strip, dest,
return "" return ""
try: try:
options = RenderOptions(filename=True, strip=strip, export_dir=dest) options = RenderOptions(
filename=True, strip=strip, export_dir=dest, exportdb=export_db
)
rendered_suffix, unmatched = photo.render_template(suffix_template, options) rendered_suffix, unmatched = photo.render_template(suffix_template, options)
except ValueError as e: except ValueError as e:
raise click.BadOptionUsage( raise click.BadOptionUsage(
@@ -2848,7 +2895,9 @@ def export_photo_to_directory(
results.missing.append(str(pathlib.Path(dest_path) / filename)) results.missing.append(str(pathlib.Path(dest_path) / filename))
return results return results
render_options = RenderOptions(export_dir=export_dir, dest_path=dest_path) render_options = RenderOptions(
export_dir=export_dir, dest_path=dest_path, exportdb=export_db
)
tries = 0 tries = 0
while tries <= retry: while tries <= retry:
@@ -2960,6 +3009,7 @@ def get_filenames_from_template(
original_name, original_name,
strip=False, strip=False,
edited=False, edited=False,
export_db=None,
): ):
"""get list of export filenames for a photo """get list of export filenames for a photo
@@ -2987,6 +3037,7 @@ def get_filenames_from_template(
edited_version=edited, edited_version=edited,
export_dir=export_dir, export_dir=export_dir,
dest_path=dest_path, dest_path=dest_path,
exportdb=export_db,
) )
filenames, unmatched = photo.render_template(filename_template, options) filenames, unmatched = photo.render_template(filename_template, options)
except ValueError as e: except ValueError as e:
@@ -3011,7 +3062,14 @@ def get_filenames_from_template(
def get_dirnames_from_template( def get_dirnames_from_template(
photo, directory, export_by_date, dest, dry_run, strip=False, edited=False photo,
directory,
export_by_date,
dest,
dry_run,
strip=False,
edited=False,
export_db=None,
): ):
"""get list of directories to export a photo into, creates directories if they don't exist """get list of directories to export a photo into, creates directories if they don't exist
@@ -3042,7 +3100,9 @@ def get_dirnames_from_template(
elif directory: elif directory:
# got a directory template, render it and check results are valid # got a directory template, render it and check results are valid
try: try:
options = RenderOptions(dirname=True, strip=strip, edited_version=edited) options = RenderOptions(
dirname=True, strip=strip, edited_version=edited, exportdb=export_db
)
dirnames, unmatched = photo.render_template(directory, options) dirnames, unmatched = photo.render_template(directory, options)
except ValueError as e: except ValueError as e:
raise click.BadOptionUsage( raise click.BadOptionUsage(
@@ -3325,6 +3385,7 @@ def write_finder_tags(
finder_tag_template=None, finder_tag_template=None,
strip=False, strip=False,
export_dir=None, export_dir=None,
export_db=None,
): ):
"""Write Finder tags (extended attributes) to files; only writes attributes if attributes on file differ from what would be written """Write Finder tags (extended attributes) to files; only writes attributes if attributes on file differ from what would be written
@@ -3338,6 +3399,7 @@ def write_finder_tags(
exiftool_merge_keywords: if True, include any keywords in the exif data of the source image as keywords exiftool_merge_keywords: if True, include any keywords in the exif data of the source image as keywords
finder_tag_template: list of templates to evaluate for determining Finder tags finder_tag_template: list of templates to evaluate for determining Finder tags
export_dir: value to use for {export_dir} template export_dir: value to use for {export_dir} template
export_db: an ExportDB object
Returns: Returns:
(list of file paths that were updated with new Finder tags, list of file paths skipped because Finder tags didn't need updating) (list of file paths that were updated with new Finder tags, list of file paths skipped because Finder tags didn't need updating)
@@ -3369,6 +3431,7 @@ def write_finder_tags(
path_sep="/", path_sep="/",
strip=strip, strip=strip,
export_dir=export_dir, export_dir=export_dir,
exportdb=export_db,
) )
rendered, unmatched = photo.render_template(template_str, options) rendered, unmatched = photo.render_template(template_str, options)
except ValueError as e: except ValueError as e:
@@ -3408,7 +3471,12 @@ def write_finder_tags(
def write_extended_attributes( def write_extended_attributes(
photo, files, xattr_template, strip=False, export_dir=None photo,
files,
xattr_template,
strip=False,
export_dir=None,
export_db=None,
): ):
"""Writes extended attributes to exported files """Writes extended attributes to exported files
@@ -3416,6 +3484,7 @@ def write_extended_attributes(
photo: a PhotoInfo object photo: a PhotoInfo object
strip: xattr_template: list of tuples: (attribute name, attribute template) strip: xattr_template: list of tuples: (attribute name, attribute template)
export_dir: value to use for {export_dir} template export_dir: value to use for {export_dir} template
exportdb: an ExportDB object
Returns: Returns:
tuple(list of file paths that were updated with new attributes, list of file paths skipped because attributes didn't need updating) tuple(list of file paths that were updated with new attributes, list of file paths skipped because attributes didn't need updating)
@@ -3429,6 +3498,7 @@ def write_extended_attributes(
path_sep="/", path_sep="/",
strip=strip, strip=strip,
export_dir=export_dir, export_dir=export_dir,
exportdb=export_db,
) )
rendered, unmatched = photo.render_template(template_str, options) rendered, unmatched = photo.render_template(template_str, options)
except ValueError as e: except ValueError as e:
@@ -3480,7 +3550,7 @@ def write_extended_attributes(
def run_post_command( def run_post_command(
photo, post_command, export_results, export_dir, dry_run, exiftool_path photo, post_command, export_results, export_dir, dry_run, exiftool_path, export_db
): ):
# todo: pass in RenderOptions from export? (e.g. so it contains strip, etc?) # todo: pass in RenderOptions from export? (e.g. so it contains strip, etc?)
# todo: need a shell_quote template type: # todo: need a shell_quote template type:
@@ -3492,7 +3562,9 @@ def run_post_command(
# some categories, like error, return a tuple of (file, error str) # some categories, like error, return a tuple of (file, error str)
if isinstance(f, tuple): if isinstance(f, tuple):
f = f[0] f = f[0]
render_options = RenderOptions(export_dir=export_dir, filepath=f) render_options = RenderOptions(
export_dir=export_dir, filepath=f, exportdb=export_db
)
template = PhotoTemplate(photo, exiftool_path=exiftool_path) template = PhotoTemplate(photo, exiftool_path=exiftool_path)
command, _ = template.render(command_template, options=render_options) command, _ = template.render(command_template, options=render_options)
command = command[0] if command else None command = command[0] if command else None

View File

@@ -1,6 +1,4 @@
""" Helper class for managing a database used by """ Helper class for managing a database used by PhotoInfo.export for tracking state of exports and updates """
PhotoInfo.export for tracking state of exports and updates
"""
import datetime import datetime
import logging import logging
@@ -12,13 +10,15 @@ from abc import ABC, abstractmethod
from io import StringIO from io import StringIO
from sqlite3 import Error from sqlite3 import Error
from ._constants import OSXPHOTOS_EXPORT_DB
from ._version import __version__ from ._version import __version__
OSXPHOTOS_EXPORTDB_VERSION = "3.2" OSXPHOTOS_EXPORTDB_VERSION = "4.0"
OSXPHOTOS_ABOUT_STRING = f"Created by osxphotos version {__version__} (https://github.com/RhetTbull/osxphotos) on {str(datetime.datetime.now())}"
class ExportDB_ABC(ABC): class ExportDB_ABC(ABC):
""" abstract base class for ExportDB """ """abstract base class for ExportDB"""
@abstractmethod @abstractmethod
def get_uuid_for_file(self, filename): def get_uuid_for_file(self, filename):
@@ -88,6 +88,14 @@ class ExportDB_ABC(ABC):
def get_previous_uuids(self): def get_previous_uuids(self):
pass pass
@abstractmethod
def get_detected_text_for_uuid(self, uuid, confidence):
pass
@abstractmethod
def set_detected_text_for_uuid(self, uuid, text, confidence):
pass
@abstractmethod @abstractmethod
def set_data( def set_data(
self, self,
@@ -104,7 +112,7 @@ class ExportDB_ABC(ABC):
class ExportDBNoOp(ExportDB_ABC): class ExportDBNoOp(ExportDB_ABC):
""" An ExportDB with NoOp methods """ """An ExportDB with NoOp methods"""
def __init__(self): def __init__(self):
self.was_created = True self.was_created = True
@@ -162,6 +170,12 @@ class ExportDBNoOp(ExportDB_ABC):
def get_previous_uuids(self): def get_previous_uuids(self):
return [] return []
def get_detected_text_for_uuid(self, uuid, confidence):
return []
def set_detected_text_for_uuid(self, uuid, text, confidence):
pass
def set_data( def set_data(
self, self,
filename, filename,
@@ -177,23 +191,23 @@ class ExportDBNoOp(ExportDB_ABC):
class ExportDB(ExportDB_ABC): class ExportDB(ExportDB_ABC):
""" Interface to sqlite3 database used to store state information for osxphotos export command """ """Interface to sqlite3 database used to store state information for osxphotos export command"""
def __init__(self, dbfile): def __init__(self, dbfile):
""" dbfile: path to osxphotos export database file """ """dbfile: path to osxphotos export database file"""
self._dbfile = dbfile self._dbfile = dbfile
# _path is parent of the database # _path is parent of the database
# all files referenced by get_/set_uuid_for_file will be converted to # all files referenced by get_/set_uuid_for_file will be converted to
# relative paths to this parent _path # relative paths to this parent _path
# this allows the entire export tree to be moved to a new disk/location # this allows the entire export tree to be moved to a new disk/location
# whilst preserving the UUID to filename mappping # whilst preserving the UUID to filename mapping
self._path = pathlib.Path(dbfile).parent self._path = pathlib.Path(dbfile).parent
self._conn = self._open_export_db(dbfile) self._conn = self._open_export_db(dbfile)
self._insert_run_info() self._insert_run_info()
def get_uuid_for_file(self, filename): def get_uuid_for_file(self, filename):
""" query database for filename and return UUID """query database for filename and return UUID
returns None if filename not found in database returns None if filename not found in database
""" """
filename = str(pathlib.Path(filename).relative_to(self._path)).lower() filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
conn = self._conn conn = self._conn
@@ -211,7 +225,7 @@ class ExportDB(ExportDB_ABC):
return uuid return uuid
def set_uuid_for_file(self, filename, uuid): def set_uuid_for_file(self, filename, uuid):
""" set UUID of filename to uuid in the database """ """set UUID of filename to uuid in the database"""
filename = str(pathlib.Path(filename).relative_to(self._path)) filename = str(pathlib.Path(filename).relative_to(self._path))
filename_normalized = filename.lower() filename_normalized = filename.lower()
conn = self._conn conn = self._conn
@@ -226,9 +240,9 @@ class ExportDB(ExportDB_ABC):
logging.warning(e) logging.warning(e)
def set_stat_orig_for_file(self, filename, stats): def set_stat_orig_for_file(self, filename, stats):
""" set stat info for filename """set stat info for filename
filename: filename to set the stat info for filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime """ stat: a tuple of length 3: mode, size, mtime"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower() filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
if len(stats) != 3: if len(stats) != 3:
raise ValueError(f"expected 3 elements for stat, got {len(stats)}") raise ValueError(f"expected 3 elements for stat, got {len(stats)}")
@@ -247,8 +261,8 @@ class ExportDB(ExportDB_ABC):
logging.warning(e) logging.warning(e)
def get_stat_orig_for_file(self, filename): def get_stat_orig_for_file(self, filename):
""" get stat info for filename """get stat info for filename
returns: tuple of (mode, size, mtime) returns: tuple of (mode, size, mtime)
""" """
filename = str(pathlib.Path(filename).relative_to(self._path)).lower() filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
conn = self._conn conn = self._conn
@@ -272,21 +286,21 @@ class ExportDB(ExportDB_ABC):
return stats return stats
def set_stat_edited_for_file(self, filename, stats): def set_stat_edited_for_file(self, filename, stats):
""" set stat info for edited version of image (in Photos' library) """set stat info for edited version of image (in Photos' library)
filename: filename to set the stat info for filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime """ stat: a tuple of length 3: mode, size, mtime"""
return self._set_stat_for_file("edited", filename, stats) return self._set_stat_for_file("edited", filename, stats)
def get_stat_edited_for_file(self, filename): def get_stat_edited_for_file(self, filename):
""" get stat info for edited version of image (in Photos' library) """get stat info for edited version of image (in Photos' library)
filename: filename to set the stat info for filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime """ stat: a tuple of length 3: mode, size, mtime"""
return self._get_stat_for_file("edited", filename) return self._get_stat_for_file("edited", filename)
def set_stat_exif_for_file(self, filename, stats): def set_stat_exif_for_file(self, filename, stats):
""" set stat info for filename (after exiftool has updated it) """set stat info for filename (after exiftool has updated it)
filename: filename to set the stat info for filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime """ stat: a tuple of length 3: mode, size, mtime"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower() filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
if len(stats) != 3: if len(stats) != 3:
raise ValueError(f"expected 3 elements for stat, got {len(stats)}") raise ValueError(f"expected 3 elements for stat, got {len(stats)}")
@@ -305,8 +319,8 @@ class ExportDB(ExportDB_ABC):
logging.warning(e) logging.warning(e)
def get_stat_exif_for_file(self, filename): def get_stat_exif_for_file(self, filename):
""" get stat info for filename (after exiftool has updated it) """get stat info for filename (after exiftool has updated it)
returns: tuple of (mode, size, mtime) returns: tuple of (mode, size, mtime)
""" """
filename = str(pathlib.Path(filename).relative_to(self._path)).lower() filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
conn = self._conn conn = self._conn
@@ -330,19 +344,19 @@ class ExportDB(ExportDB_ABC):
return stats return stats
def set_stat_converted_for_file(self, filename, stats): def set_stat_converted_for_file(self, filename, stats):
""" set stat info for filename (after image converted to jpeg) """set stat info for filename (after image converted to jpeg)
filename: filename to set the stat info for filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime """ stat: a tuple of length 3: mode, size, mtime"""
return self._set_stat_for_file("converted", filename, stats) return self._set_stat_for_file("converted", filename, stats)
def get_stat_converted_for_file(self, filename): def get_stat_converted_for_file(self, filename):
""" get stat info for filename (after jpeg conversion) """get stat info for filename (after jpeg conversion)
returns: tuple of (mode, size, mtime) returns: tuple of (mode, size, mtime)
""" """
return self._get_stat_for_file("converted", filename) return self._get_stat_for_file("converted", filename)
def get_info_for_uuid(self, uuid): def get_info_for_uuid(self, uuid):
""" returns the info JSON struct for a UUID """ """returns the info JSON struct for a UUID"""
conn = self._conn conn = self._conn
try: try:
c = conn.cursor() c = conn.cursor()
@@ -356,7 +370,7 @@ class ExportDB(ExportDB_ABC):
return info return info
def set_info_for_uuid(self, uuid, info): def set_info_for_uuid(self, uuid, info):
""" sets the info JSON struct for a UUID """ """sets the info JSON struct for a UUID"""
conn = self._conn conn = self._conn
try: try:
c = conn.cursor() c = conn.cursor()
@@ -369,7 +383,7 @@ class ExportDB(ExportDB_ABC):
logging.warning(e) logging.warning(e)
def get_exifdata_for_file(self, filename): def get_exifdata_for_file(self, filename):
""" returns the exifdata JSON struct for a file """ """returns the exifdata JSON struct for a file"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower() filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
conn = self._conn conn = self._conn
try: try:
@@ -387,7 +401,7 @@ class ExportDB(ExportDB_ABC):
return exifdata return exifdata
def set_exifdata_for_file(self, filename, exifdata): def set_exifdata_for_file(self, filename, exifdata):
""" sets the exifdata JSON struct for a file """ """sets the exifdata JSON struct for a file"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower() filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
conn = self._conn conn = self._conn
try: try:
@@ -401,7 +415,7 @@ class ExportDB(ExportDB_ABC):
logging.warning(e) logging.warning(e)
def get_sidecar_for_file(self, filename): def get_sidecar_for_file(self, filename):
""" returns the sidecar data and signature for a file """ """returns the sidecar data and signature for a file"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower() filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
conn = self._conn conn = self._conn
try: try:
@@ -429,7 +443,7 @@ class ExportDB(ExportDB_ABC):
return sidecar_data, sidecar_sig return sidecar_data, sidecar_sig
def set_sidecar_for_file(self, filename, sidecar_data, sidecar_sig): def set_sidecar_for_file(self, filename, sidecar_data, sidecar_sig):
""" sets the sidecar data and signature for a file """ """sets the sidecar data and signature for a file"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower() filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
conn = self._conn conn = self._conn
try: try:
@@ -443,7 +457,7 @@ class ExportDB(ExportDB_ABC):
logging.warning(e) logging.warning(e)
def get_previous_uuids(self): def get_previous_uuids(self):
"""returns list of UUIDs of previously exported photos found in export database """ """returns list of UUIDs of previously exported photos found in export database"""
conn = self._conn conn = self._conn
previous_uuids = [] previous_uuids = []
try: try:
@@ -455,6 +469,36 @@ class ExportDB(ExportDB_ABC):
logging.warning(e) logging.warning(e)
return previous_uuids return previous_uuids
def get_detected_text_for_uuid(self, uuid):
"""Get the detected_text for a uuid"""
conn = self._conn
try:
c = conn.cursor()
c.execute(
"SELECT text_data FROM detected_text WHERE uuid = ?",
(uuid,),
)
results = c.fetchone()
detected_text = results[0] if results else None
except Error as e:
logging.warning(e)
detected_text = None
return detected_text
def set_detected_text_for_uuid(self, uuid, text_json):
"""Set the detected text for uuid"""
conn = self._conn
try:
c = conn.cursor()
c.execute(
"INSERT OR REPLACE INTO detected_text(uuid, text_data) VALUES (?, ?);",
(uuid, text_json),
)
conn.commit()
except Error as e:
logging.warning(e)
def set_data( def set_data(
self, self,
filename, filename,
@@ -466,8 +510,7 @@ class ExportDB(ExportDB_ABC):
info_json, info_json,
exif_json, exif_json,
): ):
""" sets all the data for file and uuid at once """sets all the data for file and uuid at once"""
"""
filename = str(pathlib.Path(filename).relative_to(self._path)) filename = str(pathlib.Path(filename).relative_to(self._path))
filename_normalized = filename.lower() filename_normalized = filename.lower()
conn = self._conn conn = self._conn
@@ -510,7 +553,7 @@ class ExportDB(ExportDB_ABC):
logging.warning(e) logging.warning(e)
def close(self): def close(self):
""" close the database connection """ """close the database connection"""
try: try:
self._conn.close() self._conn.close()
except Error as e: except Error as e:
@@ -548,9 +591,9 @@ class ExportDB(ExportDB_ABC):
return stats return stats
def _open_export_db(self, dbfile): def _open_export_db(self, dbfile):
""" open export database and return a db connection """open export database and return a db connection
if dbfile does not exist, will create and initialize the database if dbfile does not exist, will create and initialize the database
returns: connection to the database returns: connection to the database
""" """
if not os.path.isfile(dbfile): if not os.path.isfile(dbfile):
@@ -573,7 +616,7 @@ class ExportDB(ExportDB_ABC):
return conn return conn
def _get_db_connection(self, dbfile): def _get_db_connection(self, dbfile):
""" return db connection to dbname """ """return db connection to dbname"""
try: try:
conn = sqlite3.connect(dbfile) conn = sqlite3.connect(dbfile)
except Error as e: except Error as e:
@@ -583,15 +626,15 @@ class ExportDB(ExportDB_ABC):
return conn return conn
def _get_database_version(self, conn): def _get_database_version(self, conn):
""" return tuple of (osxphotos, exportdb) versions for database connection conn """ """return tuple of (osxphotos, exportdb) versions for database connection conn"""
version_info = conn.execute( version_info = conn.execute(
"SELECT osxphotos, exportdb, max(id) FROM version" "SELECT osxphotos, exportdb, max(id) FROM version"
).fetchone() ).fetchone()
return (version_info[0], version_info[1]) return (version_info[0], version_info[1])
def _create_db_tables(self, conn): def _create_db_tables(self, conn):
""" create (if not already created) the necessary db tables for the export database """create (if not already created) the necessary db tables for the export database
conn: sqlite3 db connection conn: sqlite3 db connection
""" """
sql_commands = { sql_commands = {
"sql_version_table": """ CREATE TABLE IF NOT EXISTS version ( "sql_version_table": """ CREATE TABLE IF NOT EXISTS version (
@@ -599,6 +642,10 @@ class ExportDB(ExportDB_ABC):
osxphotos TEXT, osxphotos TEXT,
exportdb TEXT exportdb TEXT
); """, ); """,
"sql_about_table": """ CREATE TABLE IF NOT EXISTS about (
id INTEGER PRIMARY KEY,
about TEXT
);""",
"sql_files_table": """ CREATE TABLE IF NOT EXISTS files ( "sql_files_table": """ CREATE TABLE IF NOT EXISTS files (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
filepath TEXT NOT NULL, filepath TEXT NOT NULL,
@@ -651,12 +698,18 @@ class ExportDB(ExportDB_ABC):
size INTEGER, size INTEGER,
mtime REAL mtime REAL
); """, ); """,
"sql_detected_text_table": """ CREATE TABLE IF NOT EXISTS detected_text (
id INTEGER PRIMARY KEY,
uuid TEXT NOT NULL,
text_data JSON
); """,
"sql_files_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_files_filepath_normalized on files (filepath_normalized); """, "sql_files_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_files_filepath_normalized on files (filepath_normalized); """,
"sql_info_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_info_uuid on info (uuid); """, "sql_info_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_info_uuid on info (uuid); """,
"sql_exifdata_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_exifdata_filename on exifdata (filepath_normalized); """, "sql_exifdata_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_exifdata_filename on exifdata (filepath_normalized); """,
"sql_edited_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_edited_filename on edited (filepath_normalized);""", "sql_edited_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_edited_filename on edited (filepath_normalized);""",
"sql_converted_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_converted_filename on converted (filepath_normalized);""", "sql_converted_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_converted_filename on converted (filepath_normalized);""",
"sql_sidecar_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_sidecar_filename on sidecar (filepath_normalized);""", "sql_sidecar_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_sidecar_filename on sidecar (filepath_normalized);""",
"sql_detected_text_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_detected_text on detected_text (uuid);""",
} }
try: try:
c = conn.cursor() c = conn.cursor()
@@ -666,12 +719,13 @@ class ExportDB(ExportDB_ABC):
"INSERT INTO version(osxphotos, exportdb) VALUES (?, ?);", "INSERT INTO version(osxphotos, exportdb) VALUES (?, ?);",
(__version__, OSXPHOTOS_EXPORTDB_VERSION), (__version__, OSXPHOTOS_EXPORTDB_VERSION),
) )
c.execute("INSERT INTO about(about) VALUES (?);", (OSXPHOTOS_ABOUT_STRING,))
conn.commit() conn.commit()
except Error as e: except Error as e:
logging.warning(e) logging.warning(e)
def __del__(self): def __del__(self):
""" ensure the database connection is closed """ """ensure the database connection is closed"""
try: try:
self._conn.close() self._conn.close()
except: except:
@@ -696,35 +750,33 @@ class ExportDB(ExportDB_ABC):
class ExportDBInMemory(ExportDB): class ExportDBInMemory(ExportDB):
""" In memory version of ExportDB """In memory version of ExportDB
Copies the on-disk database into memory so it may be operated on without Copies the on-disk database into memory so it may be operated on without
modifying the on-disk verison modifying the on-disk version
""" """
def init(self, dbfile): def __init__(self, dbfile):
self._dbfile = dbfile self._dbfile = dbfile or f"./{OSXPHOTOS_EXPORT_DB}"
# _path is parent of the database # _path is parent of the database
# all files referenced by get_/set_uuid_for_file will be converted to # all files referenced by get_/set_uuid_for_file will be converted to
# relative paths to this parent _path # relative paths to this parent _path
# this allows the entire export tree to be moved to a new disk/location # this allows the entire export tree to be moved to a new disk/location
# whilst preserving the UUID to filename mappping # whilst preserving the UUID to filename mapping
self._path = pathlib.Path(dbfile).parent self._path = pathlib.Path(self._dbfile).parent
self._conn = self._open_export_db(dbfile) self._conn = self._open_export_db(self._dbfile)
self._insert_run_info() self._insert_run_info()
def _open_export_db(self, dbfile): def _open_export_db(self, dbfile):
""" open export database and return a db connection """open export database and return a db connection
returns: connection to the database returns: connection to the database
""" """
if not os.path.isfile(dbfile): if not os.path.isfile(dbfile):
conn = self._get_db_connection() conn = self._get_db_connection()
if conn: if not conn:
self._create_db_tables(conn)
self.was_created = True
self.was_upgraded = ()
self.version = OSXPHOTOS_EXPORTDB_VERSION
else:
raise Exception("Error getting connection to in-memory database") raise Exception("Error getting connection to in-memory database")
self._create_db_tables(conn)
self.was_created = True
self.was_upgraded = ()
else: else:
try: try:
conn = sqlite3.connect(dbfile) conn = sqlite3.connect(dbfile)
@@ -749,12 +801,11 @@ class ExportDBInMemory(ExportDB):
self.was_upgraded = (exportdb_ver, OSXPHOTOS_EXPORTDB_VERSION) self.was_upgraded = (exportdb_ver, OSXPHOTOS_EXPORTDB_VERSION)
else: else:
self.was_upgraded = () self.was_upgraded = ()
self.version = OSXPHOTOS_EXPORTDB_VERSION self.version = OSXPHOTOS_EXPORTDB_VERSION
return conn return conn
def _get_db_connection(self): def _get_db_connection(self):
""" return db connection to in memory database """ """return db connection to in memory database"""
try: try:
conn = sqlite3.connect(":memory:") conn = sqlite3.connect(":memory:")
except Error as e: except Error as e:

View File

@@ -1,6 +1,7 @@
""" Custom template system for osxphotos, implements osxphotos template language (OTL) """ """ Custom template system for osxphotos, implements osxphotos template language (OTL) """
import datetime import datetime
import json
import locale import locale
import os import os
import pathlib import pathlib
@@ -11,11 +12,13 @@ from typing import Optional
from textx import TextXSyntaxError, metamodel_from_file from textx import TextXSyntaxError, metamodel_from_file
from ._constants import _UNKNOWN_PERSON from ._constants import _UNKNOWN_PERSON, TEXT_DETECTION_CONFIDENCE_THRESHOLD
from ._version import __version__ from ._version import __version__
from .datetime_formatter import DateTimeFormatter from .datetime_formatter import DateTimeFormatter
from .exiftool import ExifToolCaching from .exiftool import ExifToolCaching
from .export_db import ExportDB_ABC, ExportDBInMemory
from .path_utils import sanitize_dirname, sanitize_filename, sanitize_pathpart from .path_utils import sanitize_dirname, sanitize_filename, sanitize_pathpart
from .text_detection import detect_text
from .utils import expand_and_validate_filepath, load_function from .utils import expand_and_validate_filepath, load_function
# TODO: a lot of values are passed from function to function like path_sep--make these all class properties # TODO: a lot of values are passed from function to function like path_sep--make these all class properties
@@ -197,6 +200,12 @@ TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
+ "For example: '{photo.favorite}' is the same as '{favorite}' and '{photo.place.name}' is the same as '{place.name}'. " + "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 " + "'{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.", + "the underlying PhotoInfo class. See https://rhettbull.github.io/osxphotos/ for additional documentation on the PhotoInfo class.",
"{detected_text}": "List of text strings found in the image after performing text detection. "
+ "Using '{detected_text}' will cause osxphotos to perform text detection using the built-in macOS text detection algorithms which will slow down your export. "
+ "The results for each photo will be cached in the export database so that future exports with '--update' do not need to reprocess each photo. "
+ "You may pass a confidence threshold value between 0.0 and 1.0 after a colon as in '{detected_text:0.5}'; "
+ f"The default confidence threshold is {TEXT_DETECTION_CONFIDENCE_THRESHOLD}. "
+ "Note: this feature is not the same thing as Live Text in macOS Monterey, which osxphotos does not yet support.",
"{shell_quote}": "Use in form '{shell_quote,TEMPLATE}'; quotes the rendered TEMPLATE value(s) for safe usage in the shell, e.g. My file.jpeg => 'My file.jpeg'; only adds quotes if needed.", "{shell_quote}": "Use in form '{shell_quote,TEMPLATE}'; quotes the rendered TEMPLATE value(s) for safe usage in the shell, e.g. My file.jpeg => 'My file.jpeg'; only adds quotes if needed.",
"{function}": "Execute a python function from an external file and use return value as template substitution. " "{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. " + "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. "
@@ -276,6 +285,7 @@ class RenderOptions:
dest_path: set to the destination path of the photo (for use by {function} template), only valid with --filename dest_path: set to the destination path of the photo (for use by {function} template), only valid with --filename
filepath: set to value for filepath of the exported photo if you want to evaluate {filepath} template filepath: set to value for filepath of the exported photo if you want to evaluate {filepath} template
quote: quote path templates for execution in the shell quote: quote path templates for execution in the shell
exportdb: ExportDB object
""" """
none_str: str = "_" none_str: str = "_"
@@ -290,6 +300,7 @@ class RenderOptions:
dest_path: Optional[str] = None dest_path: Optional[str] = None
filepath: Optional[str] = None filepath: Optional[str] = None
quote: bool = False quote: bool = False
exportdb: Optional[ExportDB_ABC] = None
class PhotoTemplateParser: class PhotoTemplateParser:
@@ -358,6 +369,7 @@ class PhotoTemplate:
self.filepath = options.filepath self.filepath = options.filepath
self.quote = options.quote self.quote = options.quote
self.dest_path = options.dest_path self.dest_path = options.dest_path
self.exportdb = options.exportdb or ExportDBInMemory(None)
def render( def render(
self, self,
@@ -391,6 +403,7 @@ class PhotoTemplate:
self.filepath = options.filepath self.filepath = options.filepath
self.quote = options.quote self.quote = options.quote
self.dest_path = options.dest_path self.dest_path = options.dest_path
self.exportdb = options.exportdb or self.exportdb
try: try:
model = self.parser.parse(template) model = self.parser.parse(template)
@@ -547,7 +560,7 @@ class PhotoTemplate:
) )
elif field in MULTI_VALUE_SUBSTITUTIONS or field.startswith("photo"): elif field in MULTI_VALUE_SUBSTITUTIONS or field.startswith("photo"):
vals = self.get_template_value_multi( vals = self.get_template_value_multi(
field, path_sep=path_sep, default=default field, subfield, path_sep=path_sep, default=default
) )
elif field.split(".")[0] in PATHLIB_SUBSTITUTIONS: elif field.split(".")[0] in PATHLIB_SUBSTITUTIONS:
vals = self.get_template_value_pathlib(field) vals = self.get_template_value_pathlib(field)
@@ -1073,11 +1086,12 @@ class PhotoTemplate:
value = [] value = []
return value return value
def get_template_value_multi(self, field, path_sep, default): def get_template_value_multi(self, field, subfield, path_sep, default):
"""lookup value for template field (multi-value template substitutions) """lookup value for template field (multi-value template substitutions)
Args: Args:
field: template field to find value for. field: template field to find value for.
subfield: the template subfield value
path_sep: path separator to use for folder_album field path_sep: path separator to use for folder_album field
default: value of default field default: value of default field
@@ -1126,12 +1140,10 @@ class PhotoTemplate:
folder = path_sep.join(album.folder_names) folder = path_sep.join(album.folder_names)
folder += path_sep + album.title folder += path_sep + album.title
values.append(folder) values.append(folder)
elif self.dirname:
values.append(sanitize_dirname(album.title))
else: else:
# album not in folder values.append(album.title)
if self.dirname:
values.append(sanitize_dirname(album.title))
else:
values.append(album.title)
elif field == "comment": elif field == "comment":
values = [ values = [
f"{comment.user}: {comment.text}" for comment in self.photo.comments f"{comment.user}: {comment.text}" for comment in self.photo.comments
@@ -1174,6 +1186,8 @@ class PhotoTemplate:
values = [str(obj)] values = [str(obj)]
else: else:
values = [val for val in obj] values = [val for val in obj]
elif field == "detected_text":
values = _get_detected_text(self.photo, self.exportdb, confidence=subfield)
else: else:
raise ValueError(f"Unhandled template value: {field}") raise ValueError(f"Unhandled template value: {field}")
@@ -1414,3 +1428,37 @@ def _get_album_by_path(photo, folder_album_path):
if folder_album_path.endswith(folder): if folder_album_path.endswith(folder):
return album_info return album_info
return None return None
def _get_detected_text(photo, exportdb, confidence=TEXT_DETECTION_CONFIDENCE_THRESHOLD):
"""Returns the detected text for a photo
{detected_text} uses this instead of PhotoInfo.detected_text() to cache the text for all confidence values
"""
if not photo.isphoto:
return []
confidence = (
float(confidence)
if confidence is not None
else TEXT_DETECTION_CONFIDENCE_THRESHOLD
)
detected_text = exportdb.get_detected_text_for_uuid(photo.uuid)
if detected_text:
detected_text = json.loads(detected_text)
else:
path = (
photo.path_edited
if photo.hasadjustments and photo.path_edited
else photo.path
)
path = path or photo.path_derivatives[0] if photo.path_derivatives else None
if not path:
detected_text = []
else:
try:
detected_text = detect_text(path)
except Exception as e:
detected_text = []
exportdb.set_detected_text_for_uuid(photo.uuid, json.dumps(detected_text))
return [text for text, conf in detected_text if conf >= confidence]

View File

@@ -262,6 +262,11 @@ TEMPLATE_VALUES_DATE_NOT_MODIFIED = {
"{modified.strftime,%Y-%m-%d-%H%M%S}": "2020-02-04-190738", "{modified.strftime,%Y-%m-%d-%H%M%S}": "2020-02-04-190738",
} }
UUID_DETECTED_TEXT = "E2078879-A29C-4D6F-BACB-E3BBE6C3EB91"
TEMPLATE_VALUES_DETECTED_TEXT = {
"{detected_text}": "osxphotos",
"{;+detected_text:0.5}": "osxphotos;",
}
COMMENT_UUID_DICT = { COMMENT_UUID_DICT = {
"4AD7C8EF-2991-4519-9D3A-7F44A6F031BE": [ "4AD7C8EF-2991-4519-9D3A-7F44A6F031BE": [
@@ -434,7 +439,10 @@ def test_lookup_multi(photosdb_places):
if subst in ["{exiftool}", "{photo}", "{function}"]: if subst in ["{exiftool}", "{photo}", "{function}"]:
continue continue
lookup = template.get_template_value_multi( lookup = template.get_template_value_multi(
lookup_str, path_sep=os.path.sep, default=[] lookup_str,
path_sep=os.path.sep,
default=[],
subfield=None,
) )
assert isinstance(lookup, list) assert isinstance(lookup, list)
@@ -1161,3 +1169,11 @@ def test_album_seq(photosdb):
for template, value in UUID_ALBUM_SEQ[uuid]["templates"].items(): for template, value in UUID_ALBUM_SEQ[uuid]["templates"].items():
rendered, _ = photo.render_template(template, options=options) rendered, _ = photo.render_template(template, options=options)
assert rendered[0] == value assert rendered[0] == value
def test_detected_text(photosdb):
"""Test {detected_text} template"""
photo = photosdb.get_photo(UUID_DETECTED_TEXT)
for template, value in TEMPLATE_VALUES_DETECTED_TEXT.items():
rendered, _ = photo.render_template(template)
assert value in "".join(rendered)