Refactored phototemplate.py to add PATH_SEP option
This commit is contained in:
138
README.md
138
README.md
@@ -388,30 +388,72 @@ option to re-export the entire library thus rebuilding the
|
|||||||
|
|
||||||
** Templating System **
|
** Templating System **
|
||||||
|
|
||||||
Several options, such as --directory, allow you to specify a template which
|
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
|
will be rendered to substitute template fields with values from the photo.
|
||||||
example, '{created.month}' would be replaced with the month name of the photo
|
For example, '{created.month}' would be replaced with the month name of the
|
||||||
creation date. e.g. 'November'. The general format for a template is
|
photo creation date. e.g. 'November'.
|
||||||
'{TEMPLATE_FIELD[,[DEFAULT]]}'. The ',' and DEFAULT value are optional. If
|
|
||||||
TEMPLATE_FIELD results in a null (empty) value, the default is '_'. You may
|
The general format for a template is '{TEMPLATE_FIELD[,[DEFAULT]]}'. Some
|
||||||
specify an alternate default value by appending ',DEFAULT' after
|
templates have optional modifiers in form
|
||||||
template_field. e.g. '{title,no_title}' would result in 'no_title' if the
|
'{[[DELIM]+]TEMPLATE_FIELD[(PATH_SEP)][?VALUE_IF_TRUE][,[DEFAULT]]}'
|
||||||
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} -
|
The ',' and DEFAULT value are optional. If TEMPLATE_FIELD results in a null
|
||||||
{created.month}' (e.g. '2020 - November'). Some template fields such as 'hdr'
|
(empty) value, the default is '_'. You may specify an alternate default
|
||||||
are boolean and resolve to True or False. These take the form:
|
value by appending ',DEFAULT' after template_field. e.g. '{title,no_title}'
|
||||||
'{TEMPLATE_FIELD?VALUE_IF_TRUE,VALUE_IF_FALSE}', e.g. '{hdr?is_hdr,not_hdr}'.
|
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').
|
||||||
|
|
||||||
|
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':
|
||||||
|
|
||||||
|
'{keyword}' renders to 'foo' and 'bar'
|
||||||
|
|
||||||
|
'{,+keyword}' renders to: 'foo,bar'
|
||||||
|
|
||||||
|
'{; +keyword}' renders to: 'foo; bar'
|
||||||
|
|
||||||
|
'{+keyword}' renders to 'foobar'
|
||||||
|
|
||||||
|
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
|
With the --directory and --filename options you may specify a template for the
|
||||||
export directory or filename, respectively. The directory will be appended to
|
export directory or filename, respectively. The directory will be appended to
|
||||||
the export path specified in the export DEST argument to export. For example,
|
the export path specified in the export DEST argument to export. For example,
|
||||||
if template is '{created.year}/{created.month}', and export desitnation DEST
|
if template is '{created.year}/{created.month}', and export destination DEST
|
||||||
is '/Users/maria/Pictures/export', the actual export directory for a photo
|
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
|
would be '/Users/maria/Pictures/export/2020/March' if the photo was created in
|
||||||
March 2020. Some template substitutions may result in more than one value, for
|
March 2020.
|
||||||
example '{album}' if photo is in more than one album or '{keyword}' if photo
|
|
||||||
has more than one keyword. In this case, more than one copy of the photo will
|
|
||||||
be exported, each in a separate directory or with a different filename.
|
|
||||||
|
|
||||||
The templating system may also be used with the --keyword-template option to
|
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
|
set keywords on export (with --exiftool or --sidecar), for example, to set a
|
||||||
@@ -1424,11 +1466,13 @@ If overwrite=False and increment=False, export will fail if destination file alr
|
|||||||
|
|
||||||
#### <a name="rendertemplate">`render_template()`</a>
|
#### <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, replacement=":",)`
|
||||||
Render template string for photo. none_str is used if template substitution results in None value and no default specified.
|
|
||||||
- `template_str`: str in form "{name,DEFAULT}" where name is one of the values in table below. The "," and default value that follows are optional. If specified, "DEFAULT" will be used if "name" is None. This is useful for values which are not always present, for example reverse geolocation data.
|
Render template string for photo. none_str is used if template substitution results in None value and no default specified.
|
||||||
|
|
||||||
|
- `template_str`: str in format "{[[DELIM]+]name[(PATH_SEP)][?TRUE_VALUE][,[DEFAULT]]}" where name is one of the values in the [Template Substitutions](#template-substitutions) table. See notes below regarding specific details of the syntax.
|
||||||
- `none_str`: optional str to use as substitution when template value is None and no default specified in the template string. default is "_".
|
- `none_str`: optional str to use as substitution when template value is None and no default specified in the template string. default is "_".
|
||||||
- `path_sep`: optional character to use as path separator, default is os.path.sep
|
- `path_sep`: optional character to use as path separator, default is `os.path.sep`
|
||||||
- `expand_inplace`: expand multi-valued substitutions in-place as a single string instead of returning individual strings
|
- `expand_inplace`: expand multi-valued substitutions in-place as a single string instead of returning individual strings
|
||||||
- `inplace_sep`: optional string to use as separator between multi-valued keywords with expand_inplace; default is ','
|
- `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
|
- `filename`: if True, template output will be sanitized to produce valid file name
|
||||||
@@ -1445,6 +1489,56 @@ e.g. `render_template("{created.year}/{{foo}}", photo)` would return `(["2020/{f
|
|||||||
|
|
||||||
Some substitutions, notably `album`, `keyword`, and `person` could return multiple values, hence a new string will be return for each possible substitution (hence why a list of rendered strings is returned). For example, a photo in 2 albums: 'Vacation' and 'Family' would result in the following rendered values if template was "{created.year}/{album}" and created.year == 2020: `["2020/Vacation","2020/Family"]`
|
Some substitutions, notably `album`, `keyword`, and `person` could return multiple values, hence a new string will be return for each possible substitution (hence why a list of rendered strings is returned). For example, a photo in 2 albums: 'Vacation' and 'Family' would result in the following rendered values if template was "{created.year}/{album}" and created.year == 2020: `["2020/Vacation","2020/Family"]`
|
||||||
|
|
||||||
|
The template field format contains optional modifiers:
|
||||||
|
|
||||||
|
`"{[[DELIM]+]name[(PATH_SEP)][?TRUE_VALUE][,[DEFAULT]]}"`
|
||||||
|
|
||||||
|
`DELIM`: optional delimiter string to use when expanding multi-valued template values in-place
|
||||||
|
|
||||||
|
`+`: If present before template `name`, expands the template in place. If `DELIM` not provided, values are joined with no delimiter.
|
||||||
|
|
||||||
|
e.g. if Photo keywords are `["foo","bar"]`:
|
||||||
|
|
||||||
|
- `"{keyword}"` renders to `["foo", "bar"]`
|
||||||
|
- `"{,+keyword}"` renders to: `["foo,bar"]`
|
||||||
|
- `"{; +keyword}"` renders to: `["foo; bar"]`
|
||||||
|
- `"{+keyword}"` renders to `["foobar"]`
|
||||||
|
|
||||||
|
`PATH_SEP`: optional path separator to use when joining path like fields, for example `{folder_album}`. May also be provided as `path_sep` argument in `render_template()`. If provided both in the call to `render_template()` and in the template itself, the value in the template string takes precedence. If not provided in either the template string or in `path_sep` argument, defaults to `os.path.sep`.
|
||||||
|
|
||||||
|
e.g. If Photo is in `Album1` in `Folder1`:
|
||||||
|
|
||||||
|
- `"{folder_album}"` renders to `["Folder1/Album1"]`
|
||||||
|
- `"{folder_album(:)}"` renders to `["Folder1:Album1"]`
|
||||||
|
- `"{folder_album()}"` renders to `["Folder1Album1"]`
|
||||||
|
|
||||||
|
`?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,
|
||||||
|
|
||||||
|
- `"{hdr?ISHDR,NOTHDR}"` renders to `["ISHDR"]`
|
||||||
|
|
||||||
|
and if it is not an HDR image,
|
||||||
|
|
||||||
|
- `"{hdr?ISHDR,NOTHDR}"` renders to `["NOTHDR"]`
|
||||||
|
|
||||||
|
Either or both `TRUE_VALUE` or `DEFAULT` (False value) may be empty which would result in empty string `[""]` when rendered.
|
||||||
|
|
||||||
|
`,DEFAULT`: optional default value to use if the template name has no value. This modifier is also used for the value if False for boolean-type fields (see above) as well as to hold a sub-template for values like `{created.strftime}`. If no default value provided, "_" is used. May also be provided in the `none_str` argument to `render_template()`. If provided both in the template string and in `none_str`, the value in the template string takes precedence.
|
||||||
|
|
||||||
|
e.g., if photo has no title set,
|
||||||
|
|
||||||
|
- `"{title}"` renders to ["_"]
|
||||||
|
- `"{title,I have no title}"` renders to `["I have no title"]`
|
||||||
|
|
||||||
|
Template fields such as `created.strftime` use the DEFAULT value to pass the template to use for `strftime`.
|
||||||
|
|
||||||
|
e.g., if photo date is 4 February 2020, 19:07:38,
|
||||||
|
|
||||||
|
- `"{created.strftime,%Y-%m-%d-%H%M%S}"` renders to `["2020-02-04-190738"]`
|
||||||
|
|
||||||
|
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 was a time_lapse photo, `media_type` would resolve to `vidéo_accélérée` instead of `time_lapse`.
|
||||||
|
|
||||||
See [Template Substitutions](#template-substitutions) for additional details.
|
See [Template Substitutions](#template-substitutions) for additional details.
|
||||||
|
|
||||||
### ExifInfo
|
### ExifInfo
|
||||||
|
|||||||
@@ -159,20 +159,64 @@ class ExportCommand(click.Command):
|
|||||||
formatter.write_text("** Templating System **")
|
formatter.write_text("** Templating System **")
|
||||||
formatter.write("\n")
|
formatter.write("\n")
|
||||||
formatter.write_text(
|
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. "
|
Several options, such as --directory, allow you to specify a template
|
||||||
+ "For example, '{created.month}' would be replaced with the month name of the photo creation date. "
|
which will be rendered to substitute template fields with values from the photo.
|
||||||
+ "e.g. 'November'. "
|
For example, '{created.month}' would be replaced with the month name of the photo creation date.
|
||||||
+ "The general format for a template is '{TEMPLATE_FIELD[,[DEFAULT]]}'. "
|
e.g. 'November'.
|
||||||
+ "The ',' and DEFAULT value are optional. "
|
\n
|
||||||
+ "If TEMPLATE_FIELD results in a null (empty) value, the default is '_'. "
|
The general format for a template is '{TEMPLATE_FIELD[,[DEFAULT]]}'.
|
||||||
+ "You may specify an alternate default value by appending ',DEFAULT' after template_field. "
|
Some templates have optional modifiers in form
|
||||||
+ "e.g. '{title,no_title}' would result in 'no_title' if the photo had no title. "
|
'{[[DELIM]+]TEMPLATE_FIELD[(PATH_SEP)][?VALUE_IF_TRUE][,[DEFAULT]]}'
|
||||||
+ "You may include other text in the template string outside the {} and use more than "
|
\n
|
||||||
+ "one template field, e.g. '{created.year} - {created.month}' (e.g. '2020 - November'). "
|
The ',' and DEFAULT value are optional.
|
||||||
+ "Some template fields such as 'hdr' are boolean and resolve to True or False. "
|
If TEMPLATE_FIELD results in a null (empty) value, the default is '_'.
|
||||||
+ "These take the form: '{TEMPLATE_FIELD?VALUE_IF_TRUE,VALUE_IF_FALSE}', e.g. "
|
You may specify an alternate default value by appending ',DEFAULT' after template_field.
|
||||||
+ "'{hdr?is_hdr,not_hdr}'."
|
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
|
||||||
|
'{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.
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
formatter.write("\n")
|
formatter.write("\n")
|
||||||
formatter.write_text(
|
formatter.write_text(
|
||||||
@@ -180,14 +224,10 @@ class ExportCommand(click.Command):
|
|||||||
+ "export directory or filename, respectively. "
|
+ "export directory or filename, respectively. "
|
||||||
+ "The directory will be appended to the export path specified "
|
+ "The directory will be appended to the export path specified "
|
||||||
+ "in the export DEST argument to export. For example, if template is "
|
+ "in the export DEST argument to export. For example, if template is "
|
||||||
+ "'{created.year}/{created.month}', and export desitnation DEST is "
|
+ "'{created.year}/{created.month}', and export destination DEST is "
|
||||||
+ "'/Users/maria/Pictures/export', "
|
+ "'/Users/maria/Pictures/export', "
|
||||||
+ "the actual export directory for a photo would be '/Users/maria/Pictures/export/2020/March' "
|
+ "the actual export directory for a photo would be '/Users/maria/Pictures/export/2020/March' "
|
||||||
+ "if the photo was created in March 2020. "
|
+ "if the photo was created in March 2020. "
|
||||||
+ "Some template substitutions may result in more than one value, for example '{album}' if "
|
|
||||||
+ "photo is in more than one album or '{keyword}' if photo has more than one keyword. "
|
|
||||||
+ "In this case, more than one copy of the photo will be exported, each in a separate directory "
|
|
||||||
+ "or with a different filename."
|
|
||||||
)
|
)
|
||||||
formatter.write("\n")
|
formatter.write("\n")
|
||||||
formatter.write_text(
|
formatter.write_text(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
""" version info """
|
""" version info """
|
||||||
|
|
||||||
__version__ = "0.36.12"
|
__version__ = "0.36.13"
|
||||||
|
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ class PhotoTemplate:
|
|||||||
Args:
|
Args:
|
||||||
template: str template
|
template: str template
|
||||||
none_str: str to use default for None values, default is '_'
|
none_str: str to use default for None values, default is '_'
|
||||||
path_sep: optional character to use as path separator, default is os.path.sep
|
path_sep: optional string to use as path separator, default is os.path.sep
|
||||||
expand_inplace: expand multi-valued substitutions in-place as a single string
|
expand_inplace: expand multi-valued substitutions in-place as a single string
|
||||||
instead of returning individual strings
|
instead of returning individual strings
|
||||||
inplace_sep: optional string to use as separator between multi-valued keywords
|
inplace_sep: optional string to use as separator between multi-valued keywords
|
||||||
@@ -181,8 +181,6 @@ class PhotoTemplate:
|
|||||||
|
|
||||||
if path_sep is None:
|
if path_sep is None:
|
||||||
path_sep = os.path.sep
|
path_sep = os.path.sep
|
||||||
elif path_sep is not None and len(path_sep) != 1:
|
|
||||||
raise ValueError(f"path_sep must be single character: {path_sep}")
|
|
||||||
|
|
||||||
if inplace_sep is None:
|
if inplace_sep is None:
|
||||||
inplace_sep = ","
|
inplace_sep = ","
|
||||||
@@ -196,9 +194,17 @@ class PhotoTemplate:
|
|||||||
# there would be 6 possible renderings (2 albums x 3 persons)
|
# there would be 6 possible renderings (2 albums x 3 persons)
|
||||||
|
|
||||||
# regex to find {template_field,optional_default} in strings
|
# regex to find {template_field,optional_default} in strings
|
||||||
# for explanation of regex see https://regex101.com/r/YFpWsn/1
|
|
||||||
# pylint: disable=anomalous-backslash-in-string
|
# pylint: disable=anomalous-backslash-in-string
|
||||||
regex = r"(?<!\{)\{([^}]*\+)?([^\\,}+\?]+)(\?[^\\,}]*)?(,[\w\=\;\-\%. ]*)?(?=\}(?!\}))\}"
|
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 }}
|
||||||
|
)
|
||||||
|
|
||||||
if type(template) is not str:
|
if type(template) is not str:
|
||||||
raise TypeError(f"template must be type str, not {type(template)}")
|
raise TypeError(f"template must be type str, not {type(template)}")
|
||||||
|
|
||||||
@@ -219,21 +225,24 @@ class PhotoTemplate:
|
|||||||
# closure to capture photo, none_str, filename, dirname in subst
|
# closure to capture photo, none_str, filename, dirname in subst
|
||||||
def subst(matchobj):
|
def subst(matchobj):
|
||||||
groups = len(matchobj.groups())
|
groups = len(matchobj.groups())
|
||||||
if groups == 4:
|
if groups == 5:
|
||||||
delim = matchobj.group(1)
|
delim = matchobj.group(1)
|
||||||
field = matchobj.group(2)
|
field = matchobj.group(2)
|
||||||
bool_val = matchobj.group(3)
|
path_sep = matchobj.group(3)
|
||||||
default = matchobj.group(4)
|
bool_val = matchobj.group(4)
|
||||||
|
default = matchobj.group(5)
|
||||||
|
|
||||||
# drop the comma on default
|
|
||||||
default_val = default[1:] if default is not None else None
|
|
||||||
# drop the '+' on delim
|
# drop the '+' on delim
|
||||||
delim = delim[:-1] if delim is not None else None
|
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 the ? on bool_val
|
# drop the ? on bool_val
|
||||||
bool_val = bool_val[1:] if bool_val is not None else None
|
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:
|
try:
|
||||||
val = get_func(field, default_val, bool_val, delim)
|
val = get_func(field, default_val, bool_val, delim, path_sep)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return matchobj.group(0)
|
return matchobj.group(0)
|
||||||
|
|
||||||
@@ -284,12 +293,15 @@ class PhotoTemplate:
|
|||||||
for field in MULTI_VALUE_SUBSTITUTIONS:
|
for field in MULTI_VALUE_SUBSTITUTIONS:
|
||||||
# Build a regex that matches only the field being processed
|
# Build a regex that matches only the field being processed
|
||||||
re_str = (
|
re_str = (
|
||||||
r"(?<!\{)\{"
|
r"(?<!\{)\{" # match { but not {{
|
||||||
+ r"([^}]*\+)?" # group 1, optional delim/expand in place
|
+ r"([^}]*\+)?" # group 1: optional DELIM+
|
||||||
+ r"("
|
+ r"("
|
||||||
+ field # group 2 (field name)
|
+ field # group 2: field name
|
||||||
+ r")"
|
+ r")"
|
||||||
+ r"(\?[^\\,}]*)?(,[\w\=\;\-\%. ]*)?(?=\}(?!\}))\}"
|
+ 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 }}
|
||||||
)
|
)
|
||||||
regex_multi = re.compile(re_str)
|
regex_multi = re.compile(re_str)
|
||||||
|
|
||||||
@@ -299,6 +311,11 @@ class PhotoTemplate:
|
|||||||
for str_template in rendered_strings:
|
for str_template in rendered_strings:
|
||||||
matches = regex_multi.search(str_template)
|
matches = regex_multi.search(str_template)
|
||||||
if matches:
|
if matches:
|
||||||
|
path_sep = (
|
||||||
|
matches.group(3).strip("()")
|
||||||
|
if matches.group(3) is not None
|
||||||
|
else path_sep
|
||||||
|
)
|
||||||
values = self.get_template_value_multi(
|
values = self.get_template_value_multi(
|
||||||
field,
|
field,
|
||||||
path_sep,
|
path_sep,
|
||||||
@@ -308,14 +325,10 @@ class PhotoTemplate:
|
|||||||
)
|
)
|
||||||
if expand_inplace or matches.group(1) is not None:
|
if expand_inplace or matches.group(1) is not None:
|
||||||
delim = (
|
delim = (
|
||||||
matches.group(1)[:-1]
|
matches.group(1)[:-1] if matches.group(1) is not None else inplace_sep
|
||||||
if matches.group(1) is not None
|
|
||||||
else inplace_sep
|
|
||||||
)
|
)
|
||||||
# instead of returning multiple strings, join values into a single string
|
# instead of returning multiple strings, join values into a single string
|
||||||
val = (
|
val = delim.join(sorted(values)) if values and values[0] else None
|
||||||
delim.join(sorted(values)) if values and values[0] else None
|
|
||||||
)
|
|
||||||
|
|
||||||
def lookup_template_value_multi(lookup_value, *_):
|
def lookup_template_value_multi(lookup_value, *_):
|
||||||
""" Closure passed to make_subst_function get_func
|
""" Closure passed to make_subst_function get_func
|
||||||
@@ -389,6 +402,7 @@ class PhotoTemplate:
|
|||||||
default,
|
default,
|
||||||
bool_val=None,
|
bool_val=None,
|
||||||
delim=None,
|
delim=None,
|
||||||
|
path_sep=None,
|
||||||
filename=False,
|
filename=False,
|
||||||
dirname=False,
|
dirname=False,
|
||||||
replacement=":",
|
replacement=":",
|
||||||
@@ -398,6 +412,9 @@ class PhotoTemplate:
|
|||||||
Args:
|
Args:
|
||||||
field: template field to find value for.
|
field: template field to find value for.
|
||||||
default: the default value provided by the user
|
default: the default value provided by the user
|
||||||
|
bool_val: True value if expression is boolean
|
||||||
|
delim: delimiter for expand in place
|
||||||
|
path_sep: path separator for fields that are path-like
|
||||||
filename: if True, template output will be sanitized to produce valid file name
|
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
|
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 = ":"
|
replacement: str, value to replace any illegal file path characters with; default = ":"
|
||||||
|
|||||||
@@ -496,6 +496,25 @@ def test_subst_multi_folder_albums_1():
|
|||||||
assert unknown == []
|
assert unknown == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_subst_multi_folder_albums_1_path_sep():
|
||||||
|
""" Test substitutions for folder_album are correct with custom PATH_SEP """
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_7)
|
||||||
|
|
||||||
|
# photo in an album in a folder
|
||||||
|
photo = photosdb.photos(uuid=[UUID_DICT["folder_album_1"]])[0]
|
||||||
|
template = "{folder_album(:)}"
|
||||||
|
expected = [
|
||||||
|
"2018-10 - Sponsion, Museum, Frühstück, Römermuseum",
|
||||||
|
"2019-10/11 Paris Clermont",
|
||||||
|
"Folder1:SubFolder2:AlbumInFolder",
|
||||||
|
]
|
||||||
|
rendered, unknown = photo.render_template(template)
|
||||||
|
assert sorted(rendered) == sorted(expected)
|
||||||
|
assert unknown == []
|
||||||
|
|
||||||
|
|
||||||
def test_subst_multi_folder_albums_2():
|
def test_subst_multi_folder_albums_2():
|
||||||
""" Test substitutions for folder_album are correct """
|
""" Test substitutions for folder_album are correct """
|
||||||
import osxphotos
|
import osxphotos
|
||||||
@@ -511,6 +530,21 @@ def test_subst_multi_folder_albums_2():
|
|||||||
assert unknown == []
|
assert unknown == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_subst_multi_folder_albums_2_path_sep():
|
||||||
|
""" Test substitutions for folder_album are correct with custom PATH_SEP """
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_7)
|
||||||
|
|
||||||
|
# photo in an album in a folder
|
||||||
|
photo = photosdb.photos(uuid=[UUID_DICT["folder_album_no_folder"]])[0]
|
||||||
|
template = "{folder_album(:)}"
|
||||||
|
expected = ["Pumpkin Farm", "Test Album"]
|
||||||
|
rendered, unknown = photo.render_template(template)
|
||||||
|
assert sorted(rendered) == sorted(expected)
|
||||||
|
assert unknown == []
|
||||||
|
|
||||||
|
|
||||||
def test_subst_multi_folder_albums_3():
|
def test_subst_multi_folder_albums_3():
|
||||||
""" Test substitutions for folder_album on < Photos 5 """
|
""" Test substitutions for folder_album on < Photos 5 """
|
||||||
import osxphotos
|
import osxphotos
|
||||||
@@ -526,6 +560,21 @@ def test_subst_multi_folder_albums_3():
|
|||||||
assert unknown == []
|
assert unknown == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_subst_multi_folder_albums_3_path_sep():
|
||||||
|
""" Test substitutions for folder_album on < Photos 5 with custom PATH_SEP """
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_14_6)
|
||||||
|
|
||||||
|
# photo in an album in a folder
|
||||||
|
photo = photosdb.photos(uuid=[UUID_DICT["mojave_album_1"]])[0]
|
||||||
|
template = "{folder_album(:)}"
|
||||||
|
expected = ["Folder1:SubFolder2:AlbumInFolder", "Pumpkin Farm", "Test Album (1)"]
|
||||||
|
rendered, unknown = photo.render_template(template)
|
||||||
|
assert sorted(rendered) == sorted(expected)
|
||||||
|
assert unknown == []
|
||||||
|
|
||||||
|
|
||||||
def test_subst_strftime():
|
def test_subst_strftime():
|
||||||
""" Test that strftime substitutions are correct """
|
""" Test that strftime substitutions are correct """
|
||||||
import locale
|
import locale
|
||||||
@@ -687,4 +736,4 @@ def test_expand_in_place_with_delim_single_value():
|
|||||||
|
|
||||||
for template in TEMPLATE_VALUES_TITLE:
|
for template in TEMPLATE_VALUES_TITLE:
|
||||||
rendered, _ = photo.render_template(template)
|
rendered, _ = photo.render_template(template)
|
||||||
assert sorted(rendered) == sorted(TEMPLATE_VALUES_TITLE[template])
|
assert sorted(rendered) == sorted(TEMPLATE_VALUES_TITLE[template])
|
||||||
|
|||||||
Reference in New Issue
Block a user