Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
478715a363 | ||
|
|
74f1002b9a | ||
|
|
2f57abd23c | ||
|
|
f9a43b92c1 |
12
CHANGELOG.md
12
CHANGELOG.md
@@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file. Dates are d
|
||||
|
||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
|
||||
#### [v0.39.3](https://github.com/RhetTbull/osxphotos/compare/v0.39.2...v0.39.3)
|
||||
|
||||
> 31 December 2020
|
||||
|
||||
- Fixed modified template to use creation time if no modificationd date, issue #312 [`2f57abd`](https://github.com/RhetTbull/osxphotos/commit/2f57abd23cabe57bcf667a1713c37689b330a702)
|
||||
|
||||
#### [v0.39.2](https://github.com/RhetTbull/osxphotos/compare/v0.39.1...v0.39.2)
|
||||
|
||||
> 31 December 2020
|
||||
|
||||
- Added --xattr-template, closes #242 [`#242`](https://github.com/RhetTbull/osxphotos/issues/242)
|
||||
|
||||
#### [v0.39.1](https://github.com/RhetTbull/osxphotos/compare/v0.39.0...v0.39.1)
|
||||
|
||||
> 31 December 2020
|
||||
|
||||
193
README.md
193
README.md
@@ -588,50 +588,24 @@ _keys
|
||||
** Templating System **
|
||||
|
||||
Several options, such as --directory, allow you to specify a template which
|
||||
will be rendered to substitute template fields with values from the photo.
|
||||
For example, '{created.month}' would be replaced with the month name of the
|
||||
photo creation date. e.g. 'November'.
|
||||
will be rendered to substitute template fields with values from the photo. For
|
||||
example, '{created.month}' would be replaced with the month name of the photo
|
||||
creation date. e.g. 'November'.
|
||||
|
||||
Some options supporting templates may be repeated e.g., --keyword-template
|
||||
'{label}' --keyword-template '{media_type}' to add both labels and media
|
||||
types to the keywords.
|
||||
|
||||
The general format for a template is '{TEMPLATE_FIELD[,[DEFAULT]]}'. Some
|
||||
templates have optional modifiers in form
|
||||
'{[[DELIM]+]TEMPLATE_FIELD[(PATH_SEP)][?VALUE_IF_TRUE][,[DEFAULT]]}'
|
||||
The general format for a template is '{TEMPLATE_FIELD,DEFAULT}'. The full
|
||||
template format is:
|
||||
'{DELIM+TEMPLATE_FIELD(PATH_SEP)[OLD,NEW]?VALUE_IF_TRUE,DEFAULT}'
|
||||
|
||||
The ',' and DEFAULT value are optional. If TEMPLATE_FIELD results in a null
|
||||
(empty) value, the default is '_'. You may specify an alternate default
|
||||
value by appending ',DEFAULT' after template_field. e.g. '{title,no_title}'
|
||||
would result in 'no_title' if the photo had no title. You may include other
|
||||
text in the template string outside the {} and use more than one template
|
||||
field, e.g. '{created.year} - {created.month}' (e.g. '2020 - November').
|
||||
With a few exceptions (like '{created.strftime}') everything but the
|
||||
TEMPLATE_FIELD is optional.
|
||||
|
||||
Some template fields such as 'hdr' are boolean and resolve to True or False.
|
||||
These take the form: '{TEMPLATE_FIELD?VALUE_IF_TRUE,VALUE_IF_FALSE}', e.g.
|
||||
{hdr?is_hdr,not_hdr} which would result in 'is_hdr' if photo is an HDR image
|
||||
and 'not_hdr' otherwise.
|
||||
|
||||
Some template fields such as 'folder_template' are "path-like" in that they
|
||||
join multiple elements into a single path-like string. For example, if photo
|
||||
is in album Album1 in folder Folder1, '{folder_album}` results in
|
||||
'Folder1/Album1'. This is so these template fields may be used as paths in
|
||||
--directory. If you intend to use such a field as a string, e.g. in the
|
||||
filename, you may specify a different path separator using the form:
|
||||
'{TEMPLATE_FIELD(PATH_SEP)}'. For example, using the example above,
|
||||
'{folder_album(-)}' would result in 'Folder1-Album1' and '{folder_album()}'
|
||||
would result in 'Folder1Album1'.
|
||||
|
||||
Some templates may resolve to more than one value. For example, a photo can
|
||||
have multiple keywords so '{keyword}' can result in multiple values. If used
|
||||
in a filename or directory, these templates may result in more than one copy
|
||||
of the photo being exported. For example, if photo has keywords "foo" and
|
||||
"bar", --directory '{keyword}' will result in copies of the photo being
|
||||
exported to 'foo/image_name.jpeg' and 'bar/image_name.jpeg'.
|
||||
|
||||
Multi-value template fields such as '{keyword}' may be expanded 'in place'
|
||||
with an optional delimiter using the template form '{DELIM+TEMPLATE_FIELD}'.
|
||||
For example, a photo with keywords 'foo' and 'bar':
|
||||
- 'DELIM+' Multi-value template fields such as '{keyword}' may be expanded 'in
|
||||
place' with an optional delimiter using the template form
|
||||
'{DELIM+TEMPLATE_FIELD}'. For example, a photo with keywords 'foo' and 'bar':
|
||||
|
||||
'{keyword}' renders to 'foo' and 'bar'
|
||||
|
||||
@@ -641,6 +615,62 @@ For example, a photo with keywords 'foo' and 'bar':
|
||||
|
||||
'{+keyword}' renders to 'foobar'
|
||||
|
||||
- 'TEMPLATE_FIELD' The name of the template field, for example 'keyword'
|
||||
|
||||
- '(PATH_SEP)' Some template fields such as '{folder_album}' are "path-like"
|
||||
in that they join multiple elements into a single path-like string. For
|
||||
example, if photo is in album Album1 in folder Folder1, '{folder_album}'
|
||||
results in 'Folder1/Album1'. This is so these template fields may be used as
|
||||
paths in --directory. If you intend to use such a field as a string, e.g. in
|
||||
the filename, you may specify a different path separator using the form:
|
||||
'{TEMPLATE_FIELD(PATH_SEP)}'. For example, using the example above,
|
||||
'{folder_album(-)}' would result in 'Folder1-Album1' and '{folder_album()}'
|
||||
would result in 'Folder1Album1'.
|
||||
|
||||
- '[OLD,NEW]' Use the [OLD,NEW] option to replace text "OLD" in the template
|
||||
value with text "NEW". For example, if you have album names with '/' in the
|
||||
album name you could replace '/' with "-" using the template '{album[/,-]}'.
|
||||
This would replace any occurence of "/" in the album name with "-"; album
|
||||
"Vacation/2019" would thus become "Vacation-2019". You may specify more than
|
||||
one pair of OLD,NEW values by listing them delimited by '|'. For example:
|
||||
'{album[/,-|:,-]}' to replace both '/' and ':' by '-'. You can also use the
|
||||
[OLD,NEW] syntax to delete a character by omitting the NEW value as in
|
||||
'{album[/,]}'.
|
||||
|
||||
- '?' Some template fields such as 'hdr' are boolean and resolve to True or
|
||||
False. These take the form: '{TEMPLATE_FIELD?VALUE_IF_TRUE,VALUE_IF_FALSE}',
|
||||
e.g. {hdr?is_hdr,not_hdr} which would result in 'is_hdr' if photo is an HDR
|
||||
image and 'not_hdr' otherwise.
|
||||
|
||||
- ',DEFAULT' The ',' and DEFAULT value are optional. If TEMPLATE_FIELD
|
||||
results in a null (empty) value, the template will result in default value of
|
||||
'_'. You may specify an alternate default value by appending ',DEFAULT' after
|
||||
template_field. Example: '{title,no_title}' would result in 'no_title' if the
|
||||
photo had no title. Example: '{created.year}/{place.address,NO_ADDRESS}' but
|
||||
there was no address associated with the photo, the resulting output would
|
||||
be: '2020/NO_ADDRESS/photoname.jpg'. If specified, the default value may not
|
||||
contain a brace symbol ('{' or '}').
|
||||
|
||||
Again, if you do not specify a default value and the template substitution has
|
||||
no value, '_' (underscore) will be used as the default value. For example, in
|
||||
the above example, this would result in '2020/_/photoname.jpg' if address was
|
||||
null.
|
||||
|
||||
You may specify a null default (e.g. "" or empty string) by omitting the value
|
||||
after the comma, e.g. {title,} which would render to "" if title had no value
|
||||
thus effectively deleting the template from the resulting string.
|
||||
|
||||
You may include other text in the template string outside the {} and use more
|
||||
than one template field in a single string, e.g. '{created.year} -
|
||||
{created.month}' (e.g. '2020 - November').
|
||||
|
||||
Some templates may resolve to more than one value. For example, a photo can
|
||||
have multiple keywords so '{keyword}' can result in multiple values. If used
|
||||
in a filename or directory, these templates may result in more than one copy
|
||||
of the photo being exported. For example, if photo has keywords "foo" and
|
||||
"bar", --directory '{keyword}' will result in copies of the photo being
|
||||
exported to 'foo/image_name.jpeg' and 'bar/image_name.jpeg'.
|
||||
|
||||
Some template fields such as '{media_type}' use the 'DEFAULT' value to allow
|
||||
customization of the output. For example, '{media_type}' resolves to the
|
||||
special media type of the photo such as 'panorama' or 'selfie'. You may use
|
||||
@@ -650,6 +680,28 @@ photo is a time_lapse photo, 'media_type' would resolve to 'vidéo_accélérée
|
||||
instead of 'time_lapse' and video would resolve to 'vidéo' if photo is an
|
||||
ordinary video.
|
||||
|
||||
With the --directory and --filename options you may specify a template for the
|
||||
export directory or filename, respectively. The directory will be appended to
|
||||
the export path specified in the export DEST argument to export. For example,
|
||||
if template is '{created.year}/{created.month}', and export destination DEST
|
||||
is '/Users/maria/Pictures/export', the actual export directory for a photo
|
||||
would be '/Users/maria/Pictures/export/2020/March' if the photo was created in
|
||||
March 2020.
|
||||
|
||||
The templating system may also be used with the --keyword-template option to
|
||||
set keywords on export (with --exiftool or --sidecar), for example, to set a
|
||||
new keyword in format 'folder/subfolder/album' to preserve the folder/album
|
||||
structure, you can use --keyword-template "{folder_album}"
|
||||
|
||||
In the template, valid template substitutions will be replaced by the
|
||||
corresponding value from the table below. Invalid substitutions will result
|
||||
in an error.
|
||||
|
||||
If you want the actual text of the template substition to appear in the
|
||||
rendered name, use double braces, e.g. '{{' or '}}', thus using
|
||||
'{created.year}/{{name}}' for --directory would result in output of
|
||||
2020/{name}/photoname.jpg
|
||||
|
||||
With the --directory and --filename options you may specify a template for the
|
||||
export directory or filename, respectively. The directory will be appended to
|
||||
the export path specified in the export DEST argument to export. For example,
|
||||
@@ -740,27 +792,39 @@ Substitution Description
|
||||
https://strftime.org/ for help on strftime
|
||||
templates.
|
||||
{modified.date} Photo's modification date in ISO format,
|
||||
e.g. '2020-03-22'
|
||||
{modified.year} 4-digit year of photo modification time
|
||||
{modified.yy} 2-digit year of photo modification time
|
||||
e.g. '2020-03-22'; uses creation date if
|
||||
photo is not modified
|
||||
{modified.year} 4-digit year of photo modification time;
|
||||
uses creation date if photo is not modified
|
||||
{modified.yy} 2-digit year of photo modification time;
|
||||
uses creation date if photo is not modified
|
||||
{modified.mm} 2-digit month of the photo modification time
|
||||
(zero padded)
|
||||
(zero padded); uses creation date if photo
|
||||
is not modified
|
||||
{modified.month} Month name in user's locale of the photo
|
||||
modification time
|
||||
modification time; uses creation date if
|
||||
photo is not modified
|
||||
{modified.mon} Month abbreviation in the user's locale of
|
||||
the photo modification time
|
||||
the photo modification time; uses creation
|
||||
date if photo is not modified
|
||||
{modified.dd} 2-digit day of the month (zero padded) of
|
||||
the photo modification time
|
||||
the photo modification time; uses creation
|
||||
date if photo is not modified
|
||||
{modified.dow} Day of week in user's locale of the photo
|
||||
modification time
|
||||
modification time; uses creation date if
|
||||
photo is not modified
|
||||
{modified.doy} 3-digit day of year (e.g Julian day) of
|
||||
photo modification time, starting from 1
|
||||
(zero padded)
|
||||
{modified.hour} 2-digit hour of the photo modification time
|
||||
(zero padded); uses creation date if photo
|
||||
is not modified
|
||||
{modified.hour} 2-digit hour of the photo modification time;
|
||||
uses creation date if photo is not modified
|
||||
{modified.min} 2-digit minute of the photo modification
|
||||
time
|
||||
time; uses creation date if photo is not
|
||||
modified
|
||||
{modified.sec} 2-digit second of the photo modification
|
||||
time
|
||||
time; uses creation date if photo is not
|
||||
modified
|
||||
{today.date} Current date in iso format, e.g.
|
||||
'2020-03-22'
|
||||
{today.year} 4-digit year of current date
|
||||
@@ -1714,7 +1778,7 @@ If overwrite=False and increment=False, export will fail if destination file alr
|
||||
|
||||
#### <a name="rendertemplate">`render_template()`</a>
|
||||
|
||||
`render_template(template_str, none_str = "_", path_sep = None, expand_inplace = False, inplace_sep = None, filename=False, dirname=False, replacement=":",)`
|
||||
`render_template(template_str, none_str = "_", path_sep = None, expand_inplace = False, inplace_sep = None, filename=False, dirname=False)`
|
||||
|
||||
Render template string for photo. none_str is used if template substitution results in None value and no default specified.
|
||||
|
||||
@@ -1725,7 +1789,6 @@ Render template string for photo. none_str is used if template substitution res
|
||||
- `inplace_sep`: optional string to use as separator between multi-valued keywords with expand_inplace; default is ','
|
||||
- `filename`: if True, template output will be sanitized to produce valid file name
|
||||
- `dirname`: if True, template output will be sanitized to produce valid directory name
|
||||
- `replacement`: str, value to replace any illegal file path characters with; default = ":"
|
||||
|
||||
Returns a tuple of (rendered, unmatched) where rendered is a list of rendered strings with all substitutions made and unmatched is a list of any strings that resembled a template substitution but did not match a known substitution. E.g. if template contained "{foo}", unmatched would be ["foo"].
|
||||
|
||||
@@ -1739,7 +1802,7 @@ Some substitutions, notably `album`, `keyword`, and `person` could return multip
|
||||
|
||||
The template field format contains optional modifiers:
|
||||
|
||||
`"{[[DELIM]+]name[(PATH_SEP)][?TRUE_VALUE][,[DEFAULT]]}"`
|
||||
`"{DELIM+name(PATH_SEP)[OLD,NEW]?TRUE_VALUE,DEFAULT}"`
|
||||
|
||||
`DELIM`: optional delimiter string to use when expanding multi-valued template values in-place
|
||||
|
||||
@@ -1760,6 +1823,8 @@ e.g. If Photo is in `Album1` in `Folder1`:
|
||||
- `"{folder_album(:)}"` renders to `["Folder1:Album1"]`
|
||||
- `"{folder_album()}"` renders to `["Folder1Album1"]`
|
||||
|
||||
`[OLD,NEW]`: optional text replacement to perform on rendered template value. For example, to replace "/" in an album name, you could use the template `"{album[/,-]}"`.
|
||||
|
||||
`?TRUE_VALUE`: optional value to use if name is boolean-type field which evaluates to true. For example `"{hdr}"` evaluates to True if photo is an high dynamic range (HDR) image and False otherwise. In these types of fields, use `?TRUE_VALUE` to provide the value if True and `,DEFAULT` to provide the value of False.
|
||||
|
||||
e.g. if photo is an HDR image,
|
||||
@@ -2297,18 +2362,18 @@ The following template field substitutions are availabe for use with `PhotoInfo.
|
||||
|{created.min}|2-digit minute of the photo creation time|
|
||||
|{created.sec}|2-digit second of the photo creation time|
|
||||
|{created.strftime}|Apply strftime template to file creation date/time. Should be used in form {created.strftime,TEMPLATE} where TEMPLATE is a valid strftime template, e.g. {created.strftime,%Y-%U} would result in year-week number of year: '2020-23'. If used with no template will return null value. See https://strftime.org/ for help on strftime templates.|
|
||||
|{modified.date}|Photo's modification date in ISO format, e.g. '2020-03-22'|
|
||||
|{modified.year}|4-digit year of photo modification time|
|
||||
|{modified.yy}|2-digit year of photo modification time|
|
||||
|{modified.mm}|2-digit month of the photo modification time (zero padded)|
|
||||
|{modified.month}|Month name in user's locale of the photo modification time|
|
||||
|{modified.mon}|Month abbreviation in the user's locale of the photo modification time|
|
||||
|{modified.dd}|2-digit day of the month (zero padded) of the photo modification time|
|
||||
|{modified.dow}|Day of week in user's locale of the photo modification time|
|
||||
|{modified.doy}|3-digit day of year (e.g Julian day) of photo modification time, starting from 1 (zero padded)|
|
||||
|{modified.hour}|2-digit hour of the photo modification time|
|
||||
|{modified.min}|2-digit minute of the photo modification time|
|
||||
|{modified.sec}|2-digit second of the photo modification time|
|
||||
|{modified.date}|Photo's modification date in ISO format, e.g. '2020-03-22'; uses creation date if photo is not modified|
|
||||
|{modified.year}|4-digit year of photo modification time; uses creation date if photo is not modified|
|
||||
|{modified.yy}|2-digit year of photo modification time; uses creation date if photo is not modified|
|
||||
|{modified.mm}|2-digit month of the photo modification time (zero padded); uses creation date if photo is not modified|
|
||||
|{modified.month}|Month name in user's locale of the photo modification time; uses creation date if photo is not modified|
|
||||
|{modified.mon}|Month abbreviation in the user's locale of the photo modification time; uses creation date if photo is not modified|
|
||||
|{modified.dd}|2-digit day of the month (zero padded) of the photo modification time; uses creation date if photo is not modified|
|
||||
|{modified.dow}|Day of week in user's locale of the photo modification time; uses creation date if photo is not modified|
|
||||
|{modified.doy}|3-digit day of year (e.g Julian day) of photo modification time, starting from 1 (zero padded); uses creation date if photo is not modified|
|
||||
|{modified.hour}|2-digit hour of the photo modification time; uses creation date if photo is not modified|
|
||||
|{modified.min}|2-digit minute of the photo modification time; uses creation date if photo is not modified|
|
||||
|{modified.sec}|2-digit second of the photo modification time; uses creation date if photo is not modified|
|
||||
|{today.date}|Current date in iso format, e.g. '2020-03-22'|
|
||||
|{today.year}|4-digit year of current date|
|
||||
|{today.yy}|2-digit year of current date|
|
||||
|
||||
@@ -222,66 +222,118 @@ The following attributes may be used with '--xattr-template':
|
||||
formatter.write("\n")
|
||||
formatter.write_text(
|
||||
"""
|
||||
Several options, such as --directory, allow you to specify a template
|
||||
which will be rendered to substitute template fields with values from the photo.
|
||||
For example, '{created.month}' would be replaced with the month name of the photo creation date.
|
||||
e.g. 'November'.
|
||||
\n
|
||||
Some options supporting templates may be repeated e.g., --keyword-template '{label}'
|
||||
--keyword-template '{media_type}' to add both labels and media types to the
|
||||
keywords.
|
||||
\n
|
||||
The general format for a template is '{TEMPLATE_FIELD[,[DEFAULT]]}'.
|
||||
Some templates have optional modifiers in form
|
||||
'{[[DELIM]+]TEMPLATE_FIELD[(PATH_SEP)][?VALUE_IF_TRUE][,[DEFAULT]]}'
|
||||
\n
|
||||
The ',' and DEFAULT value are optional.
|
||||
If TEMPLATE_FIELD results in a null (empty) value, the default is '_'.
|
||||
You may specify an alternate default value by appending ',DEFAULT' after template_field.
|
||||
e.g. '{title,no_title}' would result in 'no_title' if the photo had no title.
|
||||
You may include other text in the template string outside the {} and use more than
|
||||
one template field, e.g. '{created.year} - {created.month}' (e.g. '2020 - November').
|
||||
\n
|
||||
Some template fields such as 'hdr' are boolean and resolve to True or False.
|
||||
These take the form: '{TEMPLATE_FIELD?VALUE_IF_TRUE,VALUE_IF_FALSE}', e.g.
|
||||
{hdr?is_hdr,not_hdr} which would result in 'is_hdr' if photo is an HDR
|
||||
image and 'not_hdr' otherwise.
|
||||
\n
|
||||
Some template fields such as 'folder_template' are "path-like" in that they join
|
||||
multiple elements into a single path-like string. For example, if photo is in
|
||||
album Album1 in folder Folder1, '{folder_album}` results in 'Folder1/Album1'.
|
||||
This is so these template fields may be used as paths in --directory.
|
||||
If you intend to use such a field as a string, e.g. in the filename, you may specify
|
||||
a different path separator using the form: '{TEMPLATE_FIELD(PATH_SEP)}'.
|
||||
For example, using the example above, '{folder_album(-)}' would result in
|
||||
'Folder1-Album1' and '{folder_album()}' would result in
|
||||
'Folder1Album1'.
|
||||
\n
|
||||
Some templates may resolve to more than one value. For example, a photo can have
|
||||
multiple keywords so '{keyword}' can result in multiple values. If used in a filename
|
||||
or directory, these templates may result in more than one copy of the photo being exported.
|
||||
For example, if photo has keywords "foo" and "bar", --directory '{keyword}' will result in
|
||||
copies of the photo being exported to 'foo/image_name.jpeg' and 'bar/image_name.jpeg'.
|
||||
\n
|
||||
Multi-value template fields such as '{keyword}' may be expanded 'in place' with an optional
|
||||
delimiter using the template form '{DELIM+TEMPLATE_FIELD}'. For example, a photo with
|
||||
keywords 'foo' and 'bar':
|
||||
\n
|
||||
Several options, such as --directory, allow you to specify a template which
|
||||
will be rendered to substitute template fields with values from the photo.
|
||||
For example, '{created.month}' would be replaced with the month name of the
|
||||
photo creation date. e.g. 'November'.
|
||||
|
||||
Some options supporting templates may be repeated e.g., --keyword-template
|
||||
'{label}' --keyword-template '{media_type}' to add both labels and media
|
||||
types to the keywords.
|
||||
|
||||
The general format for a template is '{TEMPLATE_FIELD,DEFAULT}'. The full template format is:
|
||||
'{DELIM+TEMPLATE_FIELD(PATH_SEP)[OLD,NEW]?VALUE_IF_TRUE,DEFAULT}'
|
||||
|
||||
With a few exceptions (like '{created.strftime}') everything but the TEMPLATE_FIELD
|
||||
is optional.
|
||||
|
||||
- 'DELIM+' Multi-value template fields such as '{keyword}' may be expanded 'in place'
|
||||
with an optional delimiter using the template form '{DELIM+TEMPLATE_FIELD}'.
|
||||
For example, a photo with keywords 'foo' and 'bar':
|
||||
|
||||
'{keyword}' renders to 'foo' and 'bar'
|
||||
\n
|
||||
|
||||
'{,+keyword}' renders to: 'foo,bar'
|
||||
\n
|
||||
|
||||
'{; +keyword}' renders to: 'foo; bar'
|
||||
\n
|
||||
|
||||
'{+keyword}' renders to 'foobar'
|
||||
\n
|
||||
Some template fields such as '{media_type}' use the 'DEFAULT' value to allow customization
|
||||
of the output. For example, '{media_type}' resolves to the special media type of the
|
||||
photo such as 'panorama' or 'selfie'. You may use the 'DEFAULT' value to override
|
||||
these in form: '{media_type,video=vidéo;time_lapse=vidéo_accélérée}'.
|
||||
In this example, if photo is a time_lapse photo, 'media_type' would resolve to
|
||||
'vidéo_accélérée' instead of 'time_lapse' and video would resolve to 'vidéo' if photo
|
||||
is an ordinary video.
|
||||
|
||||
- 'TEMPLATE_FIELD' The name of the template field, for example 'keyword'
|
||||
|
||||
- '(PATH_SEP)' Some template fields such as '{folder_album}' are "path-like" in
|
||||
that they join multiple elements into a single path-like string. For example,
|
||||
if photo is in album Album1 in folder Folder1, '{folder_album}' results in
|
||||
'Folder1/Album1'. This is so these template fields may be used as paths in
|
||||
--directory. If you intend to use such a field as a string, e.g. in the
|
||||
filename, you may specify a different path separator using the form:
|
||||
'{TEMPLATE_FIELD(PATH_SEP)}'. For example, using the example above,
|
||||
'{folder_album(-)}' would result in 'Folder1-Album1' and '{folder_album()}'
|
||||
would result in 'Folder1Album1'.
|
||||
|
||||
- '[OLD,NEW]' Use the [OLD,NEW] option to replace text "OLD" in the template value
|
||||
with text "NEW". For example, if you have album names with '/' in the album name you
|
||||
could replace '/' with "-" using the template '{album[/,-]}'. This would replace
|
||||
any occurence of "/" in the album name with "-"; album "Vacation/2019" would thus
|
||||
become "Vacation-2019". You may specify more than one pair of OLD,NEW values by
|
||||
listing them delimited by '|'. For example: '{album[/,-|:,-]}' to replace both
|
||||
'/' and ':' by '-'. You can also use the [OLD,NEW] syntax to delete a character by
|
||||
omitting the NEW value as in '{album[/,]}'.
|
||||
|
||||
- '?' Some template fields such as 'hdr' are boolean and resolve to True or False.
|
||||
These take the form: '{TEMPLATE_FIELD?VALUE_IF_TRUE,VALUE_IF_FALSE}', e.g.
|
||||
{hdr?is_hdr,not_hdr} which would result in 'is_hdr' if photo is an HDR image
|
||||
and 'not_hdr' otherwise.
|
||||
|
||||
- ',DEFAULT' The ',' and DEFAULT value are optional. If TEMPLATE_FIELD results
|
||||
in a null (empty) value, the template will result in default value of '_'.
|
||||
You may specify an alternate default value by appending ',DEFAULT' after
|
||||
template_field. Example: '{title,no_title}' would result in 'no_title' if the photo
|
||||
had no title. Example: '{created.year}/{place.address,NO_ADDRESS}' but there was
|
||||
no address associated with the photo, the resulting output would be:
|
||||
'2020/NO_ADDRESS/photoname.jpg'. If specified, the default value may not
|
||||
contain a brace symbol ('{' or '}').
|
||||
|
||||
Again, if you do not specify a default value and the template substitution has no
|
||||
value, '_' (underscore) will be used as the default value. For example, in the
|
||||
above example, this would result in '2020/_/photoname.jpg' if address was
|
||||
null.
|
||||
|
||||
You may specify a null default (e.g. "" or empty string) by omitting the value
|
||||
after the comma, e.g. {title,} which would render to "" if title had no value thus
|
||||
effectively deleting the template from the resulting string.
|
||||
|
||||
You may include other text in the template string outside the {}
|
||||
and use more than one template field in a single string,
|
||||
e.g. '{created.year} - {created.month}' (e.g. '2020 - November').
|
||||
|
||||
Some templates may resolve to more than one value. For example, a photo can
|
||||
have multiple keywords so '{keyword}' can result in multiple values. If used
|
||||
in a filename or directory, these templates may result in more than one copy
|
||||
of the photo being exported. For example, if photo has keywords "foo" and
|
||||
"bar", --directory '{keyword}' will result in copies of the photo being
|
||||
exported to 'foo/image_name.jpeg' and 'bar/image_name.jpeg'.
|
||||
|
||||
Some template fields such as '{media_type}' use the 'DEFAULT' value to allow
|
||||
customization of the output. For example, '{media_type}' resolves to the
|
||||
special media type of the photo such as 'panorama' or 'selfie'. You may use
|
||||
the 'DEFAULT' value to override these in form:
|
||||
'{media_type,video=vidéo;time_lapse=vidéo_accélérée}'. In this example, if
|
||||
photo is a time_lapse photo, 'media_type' would resolve to 'vidéo_accélérée'
|
||||
instead of 'time_lapse' and video would resolve to 'vidéo' if photo is an
|
||||
ordinary video.
|
||||
|
||||
With the --directory and --filename options you may specify a template for the
|
||||
export directory or filename, respectively. The directory will be appended to
|
||||
the export path specified in the export DEST argument to export. For example,
|
||||
if template is '{created.year}/{created.month}', and export destination DEST
|
||||
is '/Users/maria/Pictures/export', the actual export directory for a photo
|
||||
would be '/Users/maria/Pictures/export/2020/March' if the photo was created in
|
||||
March 2020.
|
||||
|
||||
The templating system may also be used with the --keyword-template option to
|
||||
set keywords on export (with --exiftool or --sidecar), for example, to set a
|
||||
new keyword in format 'folder/subfolder/album' to preserve the folder/album
|
||||
structure, you can use --keyword-template "{folder_album}"
|
||||
|
||||
In the template, valid template substitutions will be replaced by the
|
||||
corresponding value from the table below. Invalid substitutions will result
|
||||
in an error.
|
||||
|
||||
If you want the actual text of the template substition to appear in the
|
||||
rendered name, use double braces, e.g. '{{' or '}}', thus using
|
||||
'{created.year}/{{name}}' for --directory would result in output of
|
||||
2020/{name}/photoname.jpg
|
||||
"""
|
||||
)
|
||||
formatter.write("\n")
|
||||
@@ -1515,7 +1567,7 @@ def query(
|
||||
help="Set extended attribute ATTRIBUTE to TEMPLATE value. Valid attributes are: "
|
||||
f"{', '.join(EXTENDED_ATTRIBUTE_NAMES_QUOTED)}. "
|
||||
"For example, to set Finder comment to the photo's title and description: "
|
||||
"'--xattr-template findercomment \"{title}; {descr}\" "
|
||||
'\'--xattr-template findercomment "{title}; {descr}" '
|
||||
"See Extended Attributes below for additional details on this option.",
|
||||
)
|
||||
@click.option(
|
||||
@@ -2863,9 +2915,15 @@ def export_photo(
|
||||
filenames = get_filenames_from_template(photo, filename_template, original_name)
|
||||
for filename in filenames:
|
||||
if original_suffix:
|
||||
rendered_suffix, unmatched = photo.render_template(
|
||||
original_suffix, filename=True
|
||||
)
|
||||
try:
|
||||
rendered_suffix, unmatched = photo.render_template(
|
||||
original_suffix, filename=True
|
||||
)
|
||||
except ValueError:
|
||||
raise click.BadOptionUsage(
|
||||
"original_suffix",
|
||||
f"Invalid template for --original-suffix '{original_suffix}'",
|
||||
)
|
||||
if not rendered_suffix or unmatched:
|
||||
raise click.BadOptionUsage(
|
||||
"original_suffix",
|
||||
@@ -3002,10 +3060,15 @@ def export_photo(
|
||||
edited_ext = pathlib.Path(photo.filename).suffix
|
||||
|
||||
if edited_suffix:
|
||||
rendered_suffix, unmatched = photo.render_template(
|
||||
edited_suffix, filename=True
|
||||
)
|
||||
|
||||
try:
|
||||
rendered_suffix, unmatched = photo.render_template(
|
||||
edited_suffix, filename=True
|
||||
)
|
||||
except ValueError:
|
||||
raise click.BadOptionUsage(
|
||||
"edited_suffix",
|
||||
f"Invalid template for --edited-suffix '{edited_suffix}'",
|
||||
)
|
||||
if not rendered_suffix or unmatched:
|
||||
raise click.BadOptionUsage(
|
||||
"edited_suffix",
|
||||
@@ -3120,9 +3183,14 @@ def get_filenames_from_template(photo, filename_template, original_name):
|
||||
"""
|
||||
if filename_template:
|
||||
photo_ext = pathlib.Path(photo.original_filename).suffix
|
||||
filenames, unmatched = photo.render_template(
|
||||
filename_template, path_sep="_", filename=True
|
||||
)
|
||||
try:
|
||||
filenames, unmatched = photo.render_template(
|
||||
filename_template, path_sep="_", filename=True
|
||||
)
|
||||
except ValueError:
|
||||
raise click.BadOptionUsage(
|
||||
"filename_template", f"Invalid template '{filename_template}'"
|
||||
)
|
||||
if not filenames or unmatched:
|
||||
raise click.BadOptionUsage(
|
||||
"filename_template",
|
||||
@@ -3167,7 +3235,10 @@ def get_dirnames_from_template(photo, directory, export_by_date, dest, dry_run):
|
||||
dest_paths = [dest_path]
|
||||
elif directory:
|
||||
# got a directory template, render it and check results are valid
|
||||
dirnames, unmatched = photo.render_template(directory, dirname=True)
|
||||
try:
|
||||
dirnames, unmatched = photo.render_template(directory, dirname=True)
|
||||
except ValueError:
|
||||
raise click.BadOptionUsage("directory", f"Invalid template '{directory}'")
|
||||
if not dirnames or unmatched:
|
||||
raise click.BadOptionUsage(
|
||||
"directory",
|
||||
@@ -3464,9 +3535,16 @@ def write_finder_tags(
|
||||
if finder_tag_template:
|
||||
rendered_tags = []
|
||||
for template_str in finder_tag_template:
|
||||
rendered, unmatched = photo.render_template(
|
||||
template_str, none_str=_OSXPHOTOS_NONE_SENTINEL, path_sep="/"
|
||||
)
|
||||
try:
|
||||
rendered, unmatched = photo.render_template(
|
||||
template_str, none_str=_OSXPHOTOS_NONE_SENTINEL, path_sep="/"
|
||||
)
|
||||
except ValueError:
|
||||
raise click.BadOptionUsage(
|
||||
"finder_tag_template",
|
||||
f"Invalid template for --finder-tag-template': {template_str}",
|
||||
)
|
||||
|
||||
if unmatched:
|
||||
click.echo(
|
||||
click.style(
|
||||
@@ -3510,9 +3588,15 @@ def write_extended_attributes(photo, files, xattr_template):
|
||||
|
||||
attributes = {}
|
||||
for xattr, template_str in xattr_template:
|
||||
rendered, unmatched = photo.render_template(
|
||||
template_str, none_str=_OSXPHOTOS_NONE_SENTINEL, path_sep="/"
|
||||
)
|
||||
try:
|
||||
rendered, unmatched = photo.render_template(
|
||||
template_str, none_str=_OSXPHOTOS_NONE_SENTINEL, path_sep="/"
|
||||
)
|
||||
except ValueError:
|
||||
raise click.BadOptionUsage(
|
||||
"xattr_template",
|
||||
f"Invalid template for --xattr-template': {template_str}",
|
||||
)
|
||||
if unmatched:
|
||||
click.echo(
|
||||
click.style(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.39.2"
|
||||
__version__ = "0.39.4"
|
||||
|
||||
|
||||
|
||||
@@ -832,7 +832,6 @@ class PhotoInfo:
|
||||
inplace_sep=None,
|
||||
filename=False,
|
||||
dirname=False,
|
||||
replacement=":",
|
||||
):
|
||||
"""Renders a template string for PhotoInfo instance using PhotoTemplate
|
||||
|
||||
@@ -847,7 +846,6 @@ class PhotoInfo:
|
||||
with expand_inplace; default is ','
|
||||
filename: if True, template output will be sanitized to produce valid file name
|
||||
dirname: if True, template output will be sanitized to produce valid directory name
|
||||
replacement: str, value to replace any illegal file path characters with; default = ":"
|
||||
|
||||
Returns:
|
||||
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values
|
||||
@@ -861,7 +859,6 @@ class PhotoInfo:
|
||||
inplace_sep=inplace_sep,
|
||||
filename=filename,
|
||||
dirname=dirname,
|
||||
replacement=replacement,
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
# 2. Needed to handle default values if template not found
|
||||
# 3. Didn't want user to need to know python (e.g. by using Mako which is
|
||||
# already used elsewhere in this project)
|
||||
# 4. Couldn't figure out how to do #1 and #2 with str.format()
|
||||
#
|
||||
# This code isn't elegant but it seems to work well. PRs gladly accepted.
|
||||
# This code isn't elegant and is prime for refactoring but it seems to work well. PRs gladly accepted.
|
||||
|
||||
import datetime
|
||||
import locale
|
||||
import os
|
||||
@@ -70,18 +70,18 @@ TEMPLATE_SUBSTITUTIONS = {
|
||||
+ "{created.strftime,%Y-%U} would result in year-week number of year: '2020-23'. "
|
||||
+ "If used with no template will return null value. "
|
||||
+ "See https://strftime.org/ for help on strftime templates.",
|
||||
"{modified.date}": "Photo's modification date in ISO format, e.g. '2020-03-22'",
|
||||
"{modified.year}": "4-digit year of photo modification time",
|
||||
"{modified.yy}": "2-digit year of photo modification time",
|
||||
"{modified.mm}": "2-digit month of the photo modification time (zero padded)",
|
||||
"{modified.month}": "Month name in user's locale of the photo modification time",
|
||||
"{modified.mon}": "Month abbreviation in the user's locale of the photo modification time",
|
||||
"{modified.dd}": "2-digit day of the month (zero padded) of the photo modification time",
|
||||
"{modified.dow}": "Day of week in user's locale of the photo modification time",
|
||||
"{modified.doy}": "3-digit day of year (e.g Julian day) of photo modification time, starting from 1 (zero padded)",
|
||||
"{modified.hour}": "2-digit hour of the photo modification time",
|
||||
"{modified.min}": "2-digit minute of the photo modification time",
|
||||
"{modified.sec}": "2-digit second of the photo modification time",
|
||||
"{modified.date}": "Photo's modification date in ISO format, e.g. '2020-03-22'; uses creation date if photo is not modified",
|
||||
"{modified.year}": "4-digit year of photo modification time; uses creation date if photo is not modified",
|
||||
"{modified.yy}": "2-digit year of photo modification time; uses creation date if photo is not modified",
|
||||
"{modified.mm}": "2-digit month of the photo modification time (zero padded); uses creation date if photo is not modified",
|
||||
"{modified.month}": "Month name in user's locale of the photo modification time; uses creation date if photo is not modified",
|
||||
"{modified.mon}": "Month abbreviation in the user's locale of the photo modification time; uses creation date if photo is not modified",
|
||||
"{modified.dd}": "2-digit day of the month (zero padded) of the photo modification time; uses creation date if photo is not modified",
|
||||
"{modified.dow}": "Day of week in user's locale of the photo modification time; uses creation date if photo is not modified",
|
||||
"{modified.doy}": "3-digit day of year (e.g Julian day) of photo modification time, starting from 1 (zero padded); uses creation date if photo is not modified",
|
||||
"{modified.hour}": "2-digit hour of the photo modification time; uses creation date if photo is not modified",
|
||||
"{modified.min}": "2-digit minute of the photo modification time; uses creation date if photo is not modified",
|
||||
"{modified.sec}": "2-digit second of the photo modification time; uses creation date if photo is not modified",
|
||||
# "{modified.strftime}": "Apply strftime template to file modification date/time. Should be used in form "
|
||||
# + "{modified.strftime,TEMPLATE} where TEMPLATE is a valid strftime template, e.g. "
|
||||
# + "{modified.strftime,%Y-%U} would result in year-week number of year: '2020-23'. "
|
||||
@@ -145,6 +145,29 @@ MULTI_VALUE_SUBSTITUTIONS = [
|
||||
for field in TEMPLATE_SUBSTITUTIONS_MULTI_VALUED
|
||||
]
|
||||
|
||||
# regular expressions for matching template syntax
|
||||
RE_OPENING_BRACE = r"(?<!\{)\{" # match { but not {{
|
||||
RE_DELIM = r"([^}]*\+)?" # group 1: optional DELIM+
|
||||
RE_FIELD_NAME = r"([^\\,}+\?]+)" # group 2: field name
|
||||
RE_PATH_SEP = r"(\([^{}\)]*\))?" # group 3: optional (PATH_SEP)
|
||||
# + r"(\[[^{}\)]*\])?" # group 4: optional [REPLACE]
|
||||
RE_REPLACE = r"(\[[^{}]*\])?" # group 4: optional [REPLACE]
|
||||
RE_BOOL_VAL = r"(\?[^\\,}]*)?" # group 5: optional ?TRUE_VALUE for boolean fields
|
||||
RE_DEFAULT_VAL = r"(,[\w\=\;\-\%. ]*)?" # group 6: optional ,DEFAULT
|
||||
RE_CLOSING_BRACE = r"(?=\}(?!\}))\}" # match } but not }}
|
||||
|
||||
MATCH_GROUPS_TOTAL = 6
|
||||
MATCH_GROUPS_DELIM = 1
|
||||
MATCH_GROUPS_FIELD = 2
|
||||
MATCH_GROUPS_PATH_SEP = 3
|
||||
MATCH_GROUPS_REPLACE = 4
|
||||
MATCH_GROUPS_BOOL_VAL = 5
|
||||
MATCH_GROUPS_DEFAULT = 6
|
||||
|
||||
# default values for string manipulation template options
|
||||
INPLACE_DEFAULT = ","
|
||||
PATH_SEP_DEFAULT = os.path.sep
|
||||
|
||||
|
||||
class PhotoTemplate:
|
||||
""" PhotoTemplate class to render a template string from a PhotoInfo object """
|
||||
@@ -163,9 +186,7 @@ class PhotoTemplate:
|
||||
# gets initialized in get_template_value
|
||||
self.today = None
|
||||
|
||||
def make_subst_function(
|
||||
self, none_str, filename, dirname, replacement, get_func=None
|
||||
):
|
||||
def make_subst_function(self, none_str, filename, dirname, get_func=None):
|
||||
""" returns: substitution function for use in re.sub
|
||||
none_str: value to use if substitution lookup is None and no default provided
|
||||
get_func: function that gets the substitution value for a given template field
|
||||
@@ -174,37 +195,39 @@ class PhotoTemplate:
|
||||
if get_func is None:
|
||||
# used by make_subst_function to get the value for a template substitution
|
||||
get_func = partial(
|
||||
self.get_template_value,
|
||||
filename=filename,
|
||||
dirname=dirname,
|
||||
replacement=replacement,
|
||||
self.get_template_value, filename=filename, dirname=dirname
|
||||
)
|
||||
|
||||
# closure to capture photo, none_str, filename, dirname in subst
|
||||
def subst(matchobj):
|
||||
groups = len(matchobj.groups())
|
||||
if groups != 5:
|
||||
if groups != MATCH_GROUPS_TOTAL:
|
||||
raise ValueError(
|
||||
f"Unexpected number of groups: expected 4, got {groups}"
|
||||
f"Unexpected number of groups: expected {MATCH_GROUPS_TOTAL}, got {groups}"
|
||||
)
|
||||
|
||||
delim = matchobj.group(1)
|
||||
field = matchobj.group(2)
|
||||
path_sep = matchobj.group(3)
|
||||
bool_val = matchobj.group(4)
|
||||
default = matchobj.group(5)
|
||||
delim = matchobj.group(MATCH_GROUPS_DELIM)
|
||||
field = matchobj.group(MATCH_GROUPS_FIELD)
|
||||
path_sep = matchobj.group(MATCH_GROUPS_PATH_SEP)
|
||||
replace = matchobj.group(MATCH_GROUPS_REPLACE)
|
||||
bool_val = matchobj.group(MATCH_GROUPS_BOOL_VAL)
|
||||
default = matchobj.group(MATCH_GROUPS_DEFAULT)
|
||||
|
||||
# drop the '+' on delim
|
||||
delim = delim[:-1] if delim is not None else None
|
||||
# drop () from path_sep
|
||||
path_sep = path_sep.strip("()") if path_sep is not None else None
|
||||
# drop [] from replace
|
||||
replace = replace[1:-1] if replace is not None else None
|
||||
# drop the ? on bool_val
|
||||
bool_val = bool_val[1:] if bool_val is not None else None
|
||||
# drop the comma on default
|
||||
default_val = default[1:] if default is not None else None
|
||||
|
||||
try:
|
||||
val = get_func(field, default_val, bool_val, delim, path_sep)
|
||||
val = get_func(
|
||||
field, default_val, bool_val, delim, path_sep, replacement=replace
|
||||
)
|
||||
except ValueError:
|
||||
return matchobj.group(0)
|
||||
|
||||
@@ -228,7 +251,6 @@ class PhotoTemplate:
|
||||
inplace_sep=None,
|
||||
filename=False,
|
||||
dirname=False,
|
||||
replacement=":",
|
||||
):
|
||||
""" Render a filename or directory template
|
||||
|
||||
@@ -242,17 +264,16 @@ class PhotoTemplate:
|
||||
with expand_inplace; default is ','
|
||||
filename: if True, template output will be sanitized to produce valid file name
|
||||
dirname: if True, template output will be sanitized to produce valid directory name
|
||||
replacement: str, value to replace any illegal file path characters with; default = ":"
|
||||
|
||||
Returns:
|
||||
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values
|
||||
"""
|
||||
|
||||
if path_sep is None:
|
||||
path_sep = os.path.sep
|
||||
path_sep = PATH_SEP_DEFAULT
|
||||
|
||||
if inplace_sep is None:
|
||||
inplace_sep = ","
|
||||
inplace_sep = INPLACE_DEFAULT
|
||||
|
||||
# the rendering happens in two phases:
|
||||
# phase 1: handle all the single-value template substitutions
|
||||
@@ -265,19 +286,20 @@ class PhotoTemplate:
|
||||
# regex to find {template_field,optional_default} in strings
|
||||
# pylint: disable=anomalous-backslash-in-string
|
||||
regex = (
|
||||
r"(?<!\{)\{" # match { but not {{
|
||||
+ r"([^}]*\+)?" # group 1: optional DELIM+
|
||||
+ r"([^\\,}+\?]+)" # group 2: field name
|
||||
+ r"(\([^{}\)]*\))?" # group 3: optional (PATH_SEP)
|
||||
+ r"(\?[^\\,}]*)?" # group 4: optional ?TRUE_VALUE for boolean fields
|
||||
+ r"(,[\w\=\;\-\%. ]*)?" # group 5: optional ,DEFAULT
|
||||
+ r"(?=\}(?!\}))\}" # match } but not }}
|
||||
RE_OPENING_BRACE
|
||||
+ RE_DELIM
|
||||
+ RE_FIELD_NAME
|
||||
+ RE_PATH_SEP
|
||||
+ RE_REPLACE
|
||||
+ RE_BOOL_VAL
|
||||
+ RE_DEFAULT_VAL
|
||||
+ RE_CLOSING_BRACE
|
||||
)
|
||||
|
||||
if type(template) is not str:
|
||||
raise TypeError(f"template must be type str, not {type(template)}")
|
||||
|
||||
subst_func = self.make_subst_function(none_str, filename, dirname, replacement)
|
||||
subst_func = self.make_subst_function(none_str, filename, dirname)
|
||||
|
||||
# do the replacements
|
||||
rendered = re.sub(regex, subst_func, template)
|
||||
@@ -306,14 +328,7 @@ class PhotoTemplate:
|
||||
# '2011/Album2/keyword2/person1',]
|
||||
|
||||
rendered_strings = self._render_multi_valued_templates(
|
||||
rendered,
|
||||
none_str,
|
||||
path_sep,
|
||||
expand_inplace,
|
||||
inplace_sep,
|
||||
filename,
|
||||
dirname,
|
||||
replacement,
|
||||
rendered, none_str, path_sep, expand_inplace, inplace_sep, filename, dirname
|
||||
)
|
||||
|
||||
# process exiftool: templates
|
||||
@@ -325,7 +340,6 @@ class PhotoTemplate:
|
||||
inplace_sep,
|
||||
filename,
|
||||
dirname,
|
||||
replacement,
|
||||
)
|
||||
|
||||
# find any {fields} that weren't replaced
|
||||
@@ -361,7 +375,6 @@ class PhotoTemplate:
|
||||
inplace_sep,
|
||||
filename,
|
||||
dirname,
|
||||
replacement,
|
||||
):
|
||||
rendered_strings = [rendered]
|
||||
new_rendered_strings = []
|
||||
@@ -370,15 +383,16 @@ class PhotoTemplate:
|
||||
for field in MULTI_VALUE_SUBSTITUTIONS:
|
||||
# Build a regex that matches only the field being processed
|
||||
re_str = (
|
||||
r"(?<!\{)\{" # match { but not {{
|
||||
+ r"([^}]*\+)?" # group 1: optional DELIM+
|
||||
RE_OPENING_BRACE
|
||||
+ RE_DELIM
|
||||
+ r"("
|
||||
+ field # group 2: field name
|
||||
+ r")"
|
||||
+ r"(\([^{}\)]*\))?" # group 3: optional (PATH_SEP)
|
||||
+ r"(\?[^\\,}]*)?" # group 4: optional ?TRUE_VALUE for boolean fields
|
||||
+ r"(,[\w\=\;\-\%. ]*)?" # group 5: optional ,DEFAULT
|
||||
+ r"(?=\}(?!\}))\}" # match } but not }}
|
||||
+ RE_PATH_SEP
|
||||
+ RE_REPLACE
|
||||
+ RE_BOOL_VAL
|
||||
+ RE_DEFAULT_VAL
|
||||
+ RE_CLOSING_BRACE
|
||||
)
|
||||
regex_multi = re.compile(re_str)
|
||||
|
||||
@@ -389,21 +403,29 @@ class PhotoTemplate:
|
||||
matches = regex_multi.search(str_template)
|
||||
if matches:
|
||||
path_sep = (
|
||||
matches.group(3).strip("()")
|
||||
if matches.group(3) is not None
|
||||
matches.group(MATCH_GROUPS_PATH_SEP).strip("()")
|
||||
if matches.group(MATCH_GROUPS_PATH_SEP) is not None
|
||||
else path_sep
|
||||
)
|
||||
replace = (
|
||||
matches.group(MATCH_GROUPS_REPLACE)[1:-1]
|
||||
if matches.group(MATCH_GROUPS_REPLACE) is not None
|
||||
else None
|
||||
)
|
||||
values = self.get_template_value_multi(
|
||||
field,
|
||||
path_sep,
|
||||
filename=filename,
|
||||
dirname=dirname,
|
||||
replacement=replacement,
|
||||
replacement=replace,
|
||||
)
|
||||
if expand_inplace or matches.group(1) is not None:
|
||||
if (
|
||||
expand_inplace
|
||||
or matches.group(MATCH_GROUPS_DELIM) is not None
|
||||
):
|
||||
delim = (
|
||||
matches.group(1)[:-1]
|
||||
if matches.group(1) is not None
|
||||
matches.group(MATCH_GROUPS_DELIM)[:-1]
|
||||
if matches.group(MATCH_GROUPS_DELIM) is not None
|
||||
else inplace_sep
|
||||
)
|
||||
# instead of returning multiple strings, join values into a single string
|
||||
@@ -413,7 +435,9 @@ class PhotoTemplate:
|
||||
else None
|
||||
)
|
||||
|
||||
def lookup_template_value_multi(lookup_value, *_):
|
||||
def lookup_template_value_multi(
|
||||
lookup_value, *args, **kwargs
|
||||
):
|
||||
""" Closure passed to make_subst_function get_func
|
||||
Capture val and field in the closure
|
||||
Allows make_subst_function to be re-used w/o modification
|
||||
@@ -429,7 +453,6 @@ class PhotoTemplate:
|
||||
none_str,
|
||||
filename,
|
||||
dirname,
|
||||
replacement,
|
||||
get_func=lookup_template_value_multi,
|
||||
)
|
||||
new_string = regex_multi.sub(subst, str_template)
|
||||
@@ -440,7 +463,9 @@ class PhotoTemplate:
|
||||
# create a new template string for each value
|
||||
for val in values:
|
||||
|
||||
def lookup_template_value_multi(lookup_value, *_):
|
||||
def lookup_template_value_multi(
|
||||
lookup_value, *args, **kwargs
|
||||
):
|
||||
""" Closure passed to make_subst_function get_func
|
||||
Capture val and field in the closure
|
||||
Allows make_subst_function to be re-used w/o modification
|
||||
@@ -456,7 +481,6 @@ class PhotoTemplate:
|
||||
none_str,
|
||||
filename,
|
||||
dirname,
|
||||
replacement,
|
||||
get_func=lookup_template_value_multi,
|
||||
)
|
||||
new_string = regex_multi.sub(subst, str_template)
|
||||
@@ -475,7 +499,6 @@ class PhotoTemplate:
|
||||
inplace_sep,
|
||||
filename,
|
||||
dirname,
|
||||
replacement,
|
||||
):
|
||||
# TODO: lots of code commonality with render_multi_valued_templates -- combine or pull out
|
||||
# TODO: put these in globals
|
||||
@@ -486,15 +509,15 @@ class PhotoTemplate:
|
||||
inplace_sep = ","
|
||||
|
||||
# Build a regex that matches only the field being processed
|
||||
# todo: pull out regexes into globals?
|
||||
re_str = (
|
||||
r"(?<!\{)\{" # match { but not {{
|
||||
+ r"([^}]*\+)?" # group 1: optional DELIM+
|
||||
+ r"(exiftool:[^\\,}+\?]+)" # group 3 field name
|
||||
+ r"(\([^{}\)]*\))?" # group 3: optional (PATH_SEP)
|
||||
+ r"(\?[^\\,}]*)?" # group 4: optional ?TRUE_VALUE for boolean fields
|
||||
+ r"(,[\w\=\;\-\%. ]*)?" # group 5: optional ,DEFAULT
|
||||
+ r"(?=\}(?!\}))\}" # match } but not }}
|
||||
RE_OPENING_BRACE
|
||||
+ RE_DELIM
|
||||
+ r"(exiftool:[^\\,}+\?\[\]]+)" # group 3 field name
|
||||
+ RE_PATH_SEP
|
||||
+ RE_REPLACE
|
||||
+ RE_BOOL_VAL
|
||||
+ RE_DEFAULT_VAL
|
||||
+ RE_CLOSING_BRACE
|
||||
)
|
||||
regex_multi = re.compile(re_str)
|
||||
|
||||
@@ -509,13 +532,17 @@ class PhotoTemplate:
|
||||
# allmatches = regex_multi.finditer(str_template)
|
||||
# for matches in allmatches:
|
||||
path_sep = (
|
||||
matches.group(3).strip("()")
|
||||
if matches.group(3) is not None
|
||||
matches.group(MATCH_GROUPS_PATH_SEP).strip("()")
|
||||
if matches.group(MATCH_GROUPS_PATH_SEP) is not None
|
||||
else path_sep
|
||||
)
|
||||
field = matches.group(2)
|
||||
replace = (
|
||||
matches.group(MATCH_GROUPS_REPLACE)[1:-1]
|
||||
if matches.group(MATCH_GROUPS_REPLACE) is not None
|
||||
else None
|
||||
)
|
||||
field = matches.group(MATCH_GROUPS_FIELD)
|
||||
subfield = field[9:]
|
||||
|
||||
if not self.photo.path:
|
||||
values = [None]
|
||||
else:
|
||||
@@ -528,12 +555,24 @@ class PhotoTemplate:
|
||||
values = (
|
||||
[values] if not isinstance(values, list) else values
|
||||
)
|
||||
if replace and values:
|
||||
new_values = []
|
||||
for value in values:
|
||||
new_values.append(self.replace(value, replace))
|
||||
values = new_values
|
||||
|
||||
# sanitize directory names if needed
|
||||
if filename:
|
||||
values = [sanitize_pathpart(value) for value in values]
|
||||
elif dirname:
|
||||
values = [sanitize_dirname(value) for value in values]
|
||||
|
||||
else:
|
||||
values = [None]
|
||||
if expand_inplace or matches.group(1) is not None:
|
||||
if expand_inplace or matches.group(MATCH_GROUPS_DELIM) is not None:
|
||||
delim = (
|
||||
matches.group(1)[:-1]
|
||||
if matches.group(1) is not None
|
||||
matches.group(MATCH_GROUPS_DELIM)[:-1]
|
||||
if matches.group(MATCH_GROUPS_DELIM) is not None
|
||||
else inplace_sep
|
||||
)
|
||||
# instead of returning multiple strings, join values into a single string
|
||||
@@ -541,7 +580,7 @@ class PhotoTemplate:
|
||||
delim.join(sorted(values)) if values and values[0] else None
|
||||
)
|
||||
|
||||
def lookup_template_value_exif(lookup_value, *_):
|
||||
def lookup_template_value_exif(lookup_value, *args, **kwargs):
|
||||
""" Closure passed to make_subst_function get_func
|
||||
Capture val and field in the closure
|
||||
Allows make_subst_function to be re-used w/o modification
|
||||
@@ -555,7 +594,6 @@ class PhotoTemplate:
|
||||
none_str,
|
||||
filename,
|
||||
dirname,
|
||||
replacement,
|
||||
get_func=lookup_template_value_exif,
|
||||
)
|
||||
new_string = regex_multi.sub(subst, str_template)
|
||||
@@ -565,7 +603,9 @@ class PhotoTemplate:
|
||||
# create a new template string for each value
|
||||
for val in values:
|
||||
|
||||
def lookup_template_value_exif(lookup_value, *_):
|
||||
def lookup_template_value_exif(
|
||||
lookup_value, *args, **kwargs
|
||||
):
|
||||
""" Closure passed to make_subst_function get_func
|
||||
Capture val and field in the closure
|
||||
Allows make_subst_function to be re-used w/o modification
|
||||
@@ -581,7 +621,6 @@ class PhotoTemplate:
|
||||
none_str,
|
||||
filename,
|
||||
dirname,
|
||||
replacement,
|
||||
get_func=lookup_template_value_exif,
|
||||
)
|
||||
new_string = regex_multi.sub(subst, str_template)
|
||||
@@ -599,7 +638,7 @@ class PhotoTemplate:
|
||||
path_sep=None,
|
||||
filename=False,
|
||||
dirname=False,
|
||||
replacement=":",
|
||||
replacement=None,
|
||||
):
|
||||
"""lookup value for template field (single-value template substitutions)
|
||||
|
||||
@@ -679,73 +718,73 @@ class PhotoTemplate:
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).date
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).date
|
||||
)
|
||||
elif field == "modified.year":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).year
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).year
|
||||
)
|
||||
elif field == "modified.yy":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).yy
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).yy
|
||||
)
|
||||
elif field == "modified.mm":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).mm
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).mm
|
||||
)
|
||||
elif field == "modified.month":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).month
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).month
|
||||
)
|
||||
elif field == "modified.mon":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).mon
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).mon
|
||||
)
|
||||
elif field == "modified.dd":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).dd
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).dd
|
||||
)
|
||||
elif field == "modified.dow":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).dow
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).dow
|
||||
)
|
||||
elif field == "modified.doy":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).doy
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).doy
|
||||
)
|
||||
elif field == "modified.hour":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).hour
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).hour
|
||||
)
|
||||
elif field == "modified.min":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).min
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).min
|
||||
)
|
||||
elif field == "modified.sec":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).sec
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).sec
|
||||
)
|
||||
elif field == "today.date":
|
||||
value = DateTimeFormatter(self.today).date
|
||||
@@ -855,14 +894,44 @@ class PhotoTemplate:
|
||||
# if here, didn't get a match
|
||||
raise ValueError(f"Unhandled template value: {field}")
|
||||
|
||||
if value and replacement:
|
||||
value = self.replace(value, replacement)
|
||||
# process character replacements
|
||||
|
||||
if filename:
|
||||
value = sanitize_pathpart(value, replacement=replacement)
|
||||
value = sanitize_pathpart(value)
|
||||
elif dirname:
|
||||
value = sanitize_dirname(value, replacement=replacement)
|
||||
value = sanitize_dirname(value)
|
||||
|
||||
return value
|
||||
|
||||
def replace(self, value, replacement):
|
||||
""" process REPLACE template option
|
||||
|
||||
Args:
|
||||
value: str value to process
|
||||
replacement: str in form OLD,NEW|OLD,NEW... with old and new values for replacement
|
||||
|
||||
Returns:
|
||||
value with all replacements done
|
||||
|
||||
Raises:
|
||||
ValueError if replacement string is in wrong format
|
||||
"""
|
||||
if not value:
|
||||
return value
|
||||
|
||||
replacements = replacement.split("|")
|
||||
for r in replacements:
|
||||
try:
|
||||
old, new = r.split(",")
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid template REPLACE value: {replacement}")
|
||||
value = value.replace(old, new)
|
||||
return value
|
||||
|
||||
def get_template_value_multi(
|
||||
self, field, path_sep, filename=False, dirname=False, replacement=":"
|
||||
self, field, path_sep, filename=False, dirname=False, replacement=None
|
||||
):
|
||||
"""lookup value for template field (multi-value template substitutions)
|
||||
|
||||
@@ -901,12 +970,9 @@ class PhotoTemplate:
|
||||
if dirname:
|
||||
# being used as a filepath so sanitize each part
|
||||
folder = path_sep.join(
|
||||
sanitize_dirname(f, replacement=replacement)
|
||||
for f in album.folder_names
|
||||
)
|
||||
folder += path_sep + sanitize_dirname(
|
||||
album.title, replacement=replacement
|
||||
sanitize_dirname(f) for f in album.folder_names
|
||||
)
|
||||
folder += path_sep + sanitize_dirname(album.title)
|
||||
else:
|
||||
folder = path_sep.join(album.folder_names)
|
||||
folder += path_sep + album.title
|
||||
@@ -914,9 +980,7 @@ class PhotoTemplate:
|
||||
else:
|
||||
# album not in folder
|
||||
if dirname:
|
||||
values.append(
|
||||
sanitize_dirname(album.title, replacement=replacement)
|
||||
)
|
||||
values.append(sanitize_dirname(album.title))
|
||||
else:
|
||||
values.append(album.title)
|
||||
elif field == "comment":
|
||||
@@ -934,18 +998,23 @@ class PhotoTemplate:
|
||||
self.photo.search_info.venue_types if self.photo.search_info else []
|
||||
)
|
||||
elif not field.startswith("exiftool:"):
|
||||
# exiftool: templates handled by _render_exiftool_template
|
||||
raise ValueError(f"Unhandled template value: {field}")
|
||||
|
||||
# do any replacements needs
|
||||
if replacement:
|
||||
new_values = []
|
||||
for value in values:
|
||||
# process replacements
|
||||
new_values.append(self.replace(value, replacement))
|
||||
values = new_values
|
||||
|
||||
# sanitize directory names if needed, folder_album handled differently above
|
||||
if filename:
|
||||
values = [
|
||||
sanitize_pathpart(value, replacement=replacement) for value in values
|
||||
]
|
||||
values = [sanitize_pathpart(value) for value in values]
|
||||
elif dirname and field != "folder_album":
|
||||
# skip folder_album because it would have been handled above
|
||||
values = [
|
||||
sanitize_dirname(value, replacement=replacement) for value in values
|
||||
]
|
||||
values = [sanitize_dirname(value) for value in values]
|
||||
|
||||
# If no values, insert None so code below will substite none_str for None
|
||||
values = values or [None]
|
||||
|
||||
@@ -53,6 +53,7 @@ TEMPLATE_VALUES_MULTI_KEYWORDS = {
|
||||
UUID_TITLE = "6191423D-8DB8-4D4C-92BE-9BBBA308AAC4"
|
||||
TEMPLATE_VALUES_TITLE = {
|
||||
"{title}": ["Tulips tied together at a flower shop"],
|
||||
"{title[ ,_]}": ["Tulips_tied_together_at_a_flower_shop"],
|
||||
"{+title}": ["Tulips tied together at a flower shop"],
|
||||
"{,+title}": ["Tulips tied together at a flower shop"],
|
||||
"{, +title}": ["Tulips tied together at a flower shop"],
|
||||
@@ -74,7 +75,9 @@ UUID_BOOL_VALUES_NOT = {
|
||||
UUID_EXIFTOOL = {
|
||||
"A92D9C26-3A50-4197-9388-CB5F7DB9FA91": {
|
||||
"{exiftool:EXIF:Make}": ["Canon"],
|
||||
"{exiftool:EXIF:Make[Canon,CANON]}": ["CANON"],
|
||||
"{exiftool:EXIF:Model}": ["Canon PowerShot G10"],
|
||||
"{exiftool:EXIF:Model[ G10,]}": ["Canon PowerShot"],
|
||||
"{exiftool:EXIF:Make}/{exiftool:EXIF:Model}": ["Canon/Canon PowerShot G10"],
|
||||
"{exiftool:IPTC:Keywords,foo}": ["foo"],
|
||||
},
|
||||
@@ -87,6 +90,14 @@ UUID_EXIFTOOL = {
|
||||
"UK",
|
||||
"United Kingdom",
|
||||
],
|
||||
"{exiftool:IPTC:Keywords[ ,_|.,]}": [
|
||||
"England",
|
||||
"London",
|
||||
"London_2018",
|
||||
"St_James's_Park",
|
||||
"UK",
|
||||
"United_Kingdom",
|
||||
],
|
||||
"{,+exiftool:IPTC:Keywords}": [
|
||||
"England,London,London 2018,St. James's Park,UK,United Kingdom"
|
||||
],
|
||||
@@ -96,7 +107,9 @@ UUID_EXIFTOOL = {
|
||||
TEMPLATE_VALUES = {
|
||||
"{name}": "128FB4C6-0B16-4E7D-9108-FB2E90DA1546",
|
||||
"{original_name}": "IMG_1064",
|
||||
"{original_name[_,-]}": "IMG-1064",
|
||||
"{title}": "Glen Ord",
|
||||
"{title[ ,]}": "GlenOrd",
|
||||
"{descr}": "Jack Rose Dining Saloon",
|
||||
"{created.date}": "2020-02-04",
|
||||
"{created.year}": "2020",
|
||||
@@ -170,17 +183,21 @@ TEMPLATE_VALUES_DATE_MODIFIED = {
|
||||
}
|
||||
|
||||
TEMPLATE_VALUES_DATE_NOT_MODIFIED = {
|
||||
# uses creation date instead of modified date
|
||||
"{name}": "128FB4C6-0B16-4E7D-9108-FB2E90DA1546",
|
||||
"{original_name}": "IMG_1064",
|
||||
"{modified.date}": "_",
|
||||
"{modified.year}": "_",
|
||||
"{modified.yy}": "_",
|
||||
"{modified.mm}": "_",
|
||||
"{modified.month}": "_",
|
||||
"{modified.mon}": "_",
|
||||
"{modified.dd}": "_",
|
||||
"{modified.doy}": "_",
|
||||
"{modified.dow}": "_",
|
||||
"{modified.date}": "2020-02-04",
|
||||
"{modified.year}": "2020",
|
||||
"{modified.yy}": "20",
|
||||
"{modified.mm}": "02",
|
||||
"{modified.month}": "February",
|
||||
"{modified.mon}": "Feb",
|
||||
"{modified.dd}": "04",
|
||||
"{modified.dow}": "Tuesday",
|
||||
"{modified.doy}": "035",
|
||||
"{modified.hour}": "19",
|
||||
"{modified.min}": "07",
|
||||
"{modified.sec}": "38",
|
||||
}
|
||||
|
||||
|
||||
@@ -785,3 +802,4 @@ def test_exiftool_template():
|
||||
for template in UUID_EXIFTOOL[uuid]:
|
||||
rendered, _ = photo.render_template(template)
|
||||
assert sorted(rendered) == sorted(UUID_EXIFTOOL[uuid][template])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user