Added {exiftool} template, implements issue #259
This commit is contained in:
16
README.md
16
README.md
@@ -639,15 +639,23 @@ albums 'Vacation' and 'Family': 2019/Vacation, 2019/Family
|
|||||||
Substitution Description
|
Substitution Description
|
||||||
{album} Album(s) photo is contained in
|
{album} Album(s) photo is contained in
|
||||||
{folder_album} Folder path + album photo is contained in. e.g.
|
{folder_album} Folder path + album photo is contained in. e.g.
|
||||||
'Folder/Subfolder/Album' or just 'Album' if no enclosing
|
'Folder/Subfolder/Album' or just 'Album' if no
|
||||||
folder
|
enclosing folder
|
||||||
{keyword} Keyword(s) assigned to photo
|
{keyword} Keyword(s) assigned to photo
|
||||||
{person} Person(s) / face(s) in a photo
|
{person} Person(s) / face(s) in a photo
|
||||||
{label} Image categorization label associated with a photo
|
{label} Image categorization label associated with a photo
|
||||||
(Photos 5 only)
|
(Photos 5 only)
|
||||||
{label_normalized} All lower case version of 'label' (Photos 5 only)
|
{label_normalized} All lower case version of 'label' (Photos 5 only)
|
||||||
{comment} Comment(s) on shared Photos; format is 'Person name:
|
{comment} Comment(s) on shared Photos; format is 'Person
|
||||||
comment text' (Photos 5 only)
|
name: comment text' (Photos 5 only)
|
||||||
|
{exiftool:GROUP:TAGNAME} Use exiftool (https://exiftool.org) to extract
|
||||||
|
metadata, in form GROUP:TAGNAME, from image. E.g.
|
||||||
|
'{exiftool:EXIF:Make}' to get camera make, or
|
||||||
|
{exiftool:IPTC:Keywords} to extract keywords. See
|
||||||
|
https://exiftool.org/TagNames/ for list of valid
|
||||||
|
tag names. You must specify group (e.g. EXIF,
|
||||||
|
IPTC, etc) as used in `exiftool -G`. exiftool must
|
||||||
|
be installed in the path to use this template.
|
||||||
```
|
```
|
||||||
|
|
||||||
Example: export all photos to ~/Desktop/export group in folders by date created
|
Example: export all photos to ~/Desktop/export group in folders by date created
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
""" version info """
|
""" version info """
|
||||||
|
|
||||||
__version__ = "0.36.25"
|
__version__ = "0.37.0"
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from functools import partial
|
|||||||
|
|
||||||
from ._constants import _UNKNOWN_PERSON
|
from ._constants import _UNKNOWN_PERSON
|
||||||
from .datetime_formatter import DateTimeFormatter
|
from .datetime_formatter import DateTimeFormatter
|
||||||
|
from .exiftool import ExifTool
|
||||||
from .path_utils import sanitize_dirname, sanitize_filename, sanitize_pathpart
|
from .path_utils import sanitize_dirname, sanitize_filename, sanitize_pathpart
|
||||||
|
|
||||||
# ensure locale set to user's locale
|
# ensure locale set to user's locale
|
||||||
@@ -126,6 +127,10 @@ TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
|
|||||||
"{label}": "Image categorization label associated with a photo (Photos 5 only)",
|
"{label}": "Image categorization label associated with a photo (Photos 5 only)",
|
||||||
"{label_normalized}": "All lower case version of 'label' (Photos 5 only)",
|
"{label_normalized}": "All lower case version of 'label' (Photos 5 only)",
|
||||||
"{comment}": "Comment(s) on shared Photos; format is 'Person name: comment text' (Photos 5 only)",
|
"{comment}": "Comment(s) on shared Photos; format is 'Person name: comment text' (Photos 5 only)",
|
||||||
|
"{exiftool:GROUP:TAGNAME}": "Use exiftool (https://exiftool.org) to extract metadata, in form GROUP:TAGNAME, from image. "
|
||||||
|
"E.g. '{exiftool:EXIF:Make}' to get camera make, or {exiftool:IPTC:Keywords} to extract keywords. "
|
||||||
|
"See https://exiftool.org/TagNames/ for list of valid tag names. You must specify group (e.g. EXIF, IPTC, etc) "
|
||||||
|
"as used in `exiftool -G`. exiftool must be installed in the path to use this template.",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Just the multi-valued substitution names without the braces
|
# Just the multi-valued substitution names without the braces
|
||||||
@@ -150,6 +155,62 @@ class PhotoTemplate:
|
|||||||
# gets initialized in get_template_value
|
# gets initialized in get_template_value
|
||||||
self.today = None
|
self.today = None
|
||||||
|
|
||||||
|
def make_subst_function(
|
||||||
|
self, none_str, filename, dirname, replacement, 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
|
||||||
|
default is get_template_value which handles the single-value fields """
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
# closure to capture photo, none_str, filename, dirname in subst
|
||||||
|
def subst(matchobj):
|
||||||
|
groups = len(matchobj.groups())
|
||||||
|
if groups != 5:
|
||||||
|
raise ValueError(
|
||||||
|
f"Unexpected number of groups: expected 4, got {groups}"
|
||||||
|
)
|
||||||
|
|
||||||
|
delim = matchobj.group(1)
|
||||||
|
field = matchobj.group(2)
|
||||||
|
path_sep = matchobj.group(3)
|
||||||
|
bool_val = matchobj.group(4)
|
||||||
|
default = matchobj.group(5)
|
||||||
|
|
||||||
|
# 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 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)
|
||||||
|
except ValueError:
|
||||||
|
return matchobj.group(0)
|
||||||
|
|
||||||
|
if val is None:
|
||||||
|
# field valid but didn't match a value
|
||||||
|
if default == ",":
|
||||||
|
val = ""
|
||||||
|
else:
|
||||||
|
val = default_val if default_val is not None else none_str
|
||||||
|
|
||||||
|
return val
|
||||||
|
|
||||||
|
return subst
|
||||||
|
|
||||||
def render(
|
def render(
|
||||||
self,
|
self,
|
||||||
template,
|
template,
|
||||||
@@ -208,60 +269,7 @@ class PhotoTemplate:
|
|||||||
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)}")
|
||||||
|
|
||||||
# used by make_subst_function to get the value for a template substitution
|
subst_func = self.make_subst_function(none_str, filename, dirname, replacement)
|
||||||
get_func = partial(
|
|
||||||
self.get_template_value,
|
|
||||||
filename=filename,
|
|
||||||
dirname=dirname,
|
|
||||||
replacement=replacement,
|
|
||||||
)
|
|
||||||
|
|
||||||
def make_subst_function(self, none_str, get_func=get_func):
|
|
||||||
""" 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
|
|
||||||
default is get_template_value which handles the single-value fields """
|
|
||||||
|
|
||||||
# closure to capture photo, none_str, filename, dirname in subst
|
|
||||||
def subst(matchobj):
|
|
||||||
groups = len(matchobj.groups())
|
|
||||||
if groups == 5:
|
|
||||||
delim = matchobj.group(1)
|
|
||||||
field = matchobj.group(2)
|
|
||||||
path_sep = matchobj.group(3)
|
|
||||||
bool_val = matchobj.group(4)
|
|
||||||
default = matchobj.group(5)
|
|
||||||
|
|
||||||
# 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 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)
|
|
||||||
except ValueError:
|
|
||||||
return matchobj.group(0)
|
|
||||||
|
|
||||||
if val is None:
|
|
||||||
# field valid but didn't match a value
|
|
||||||
if default == ",":
|
|
||||||
val = ""
|
|
||||||
else:
|
|
||||||
val = default_val if default_val is not None else none_str
|
|
||||||
|
|
||||||
return val
|
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
f"Unexpected number of groups: expected 4, got {groups}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return subst
|
|
||||||
|
|
||||||
subst_func = make_subst_function(self, none_str)
|
|
||||||
|
|
||||||
# do the replacements
|
# do the replacements
|
||||||
rendered = re.sub(regex, subst_func, template)
|
rendered = re.sub(regex, subst_func, template)
|
||||||
@@ -289,7 +297,68 @@ class PhotoTemplate:
|
|||||||
# '2011/Album2/keyword1/person1',
|
# '2011/Album2/keyword1/person1',
|
||||||
# '2011/Album2/keyword2/person1',]
|
# '2011/Album2/keyword2/person1',]
|
||||||
|
|
||||||
|
rendered_strings = self._render_multi_valued_templates(
|
||||||
|
rendered,
|
||||||
|
none_str,
|
||||||
|
path_sep,
|
||||||
|
expand_inplace,
|
||||||
|
inplace_sep,
|
||||||
|
filename,
|
||||||
|
dirname,
|
||||||
|
replacement,
|
||||||
|
)
|
||||||
|
|
||||||
|
# process exiftool: templates
|
||||||
|
rendered_strings = self._render_exiftool_template(
|
||||||
|
rendered_strings,
|
||||||
|
none_str,
|
||||||
|
path_sep,
|
||||||
|
expand_inplace,
|
||||||
|
inplace_sep,
|
||||||
|
filename,
|
||||||
|
dirname,
|
||||||
|
replacement,
|
||||||
|
)
|
||||||
|
|
||||||
|
# find any {fields} that weren't replaced
|
||||||
|
unmatched = []
|
||||||
|
for rendered_str in rendered_strings:
|
||||||
|
unmatched.extend(
|
||||||
|
[
|
||||||
|
no_match[1]
|
||||||
|
for no_match in re.findall(regex, rendered_str)
|
||||||
|
if no_match[1] not in unmatched
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# fix any escaped curly braces
|
||||||
|
rendered_strings = [
|
||||||
|
rendered_str.replace("{{", "{").replace("}}", "}")
|
||||||
|
for rendered_str in rendered_strings
|
||||||
|
]
|
||||||
|
|
||||||
|
if filename:
|
||||||
|
rendered_strings = [
|
||||||
|
sanitize_filename(rendered_str) for rendered_str in rendered_strings
|
||||||
|
]
|
||||||
|
|
||||||
|
return rendered_strings, unmatched
|
||||||
|
|
||||||
|
def _render_multi_valued_templates(
|
||||||
|
self,
|
||||||
|
rendered,
|
||||||
|
none_str,
|
||||||
|
path_sep,
|
||||||
|
expand_inplace,
|
||||||
|
inplace_sep,
|
||||||
|
filename,
|
||||||
|
dirname,
|
||||||
|
replacement,
|
||||||
|
):
|
||||||
rendered_strings = [rendered]
|
rendered_strings = [rendered]
|
||||||
|
new_rendered_strings = []
|
||||||
|
while new_rendered_strings != rendered_strings:
|
||||||
|
new_rendered_strings = rendered_strings
|
||||||
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 = (
|
||||||
@@ -325,10 +394,16 @@ 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] if matches.group(1) is not None else inplace_sep
|
matches.group(1)[:-1]
|
||||||
|
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 = delim.join(sorted(values)) if values and values[0] else None
|
val = (
|
||||||
|
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
|
||||||
@@ -338,15 +413,21 @@ class PhotoTemplate:
|
|||||||
if lookup_value == field:
|
if lookup_value == field:
|
||||||
return val
|
return val
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unexpected value: {lookup_value}")
|
raise ValueError(
|
||||||
|
f"Unexpected value: {lookup_value}"
|
||||||
|
)
|
||||||
|
|
||||||
subst = make_subst_function(
|
subst = self.make_subst_function(
|
||||||
self, none_str, get_func=lookup_template_value_multi
|
none_str,
|
||||||
|
filename,
|
||||||
|
dirname,
|
||||||
|
replacement,
|
||||||
|
get_func=lookup_template_value_multi,
|
||||||
)
|
)
|
||||||
new_string = regex_multi.sub(subst, str_template)
|
new_string = regex_multi.sub(subst, str_template)
|
||||||
|
|
||||||
# update rendered_strings for the next field to process
|
# update rendered_strings for the next field to process
|
||||||
rendered_strings = {new_string}
|
rendered_strings = list({new_string})
|
||||||
else:
|
else:
|
||||||
# create a new template string for each value
|
# create a new template string for each value
|
||||||
for val in values:
|
for val in values:
|
||||||
@@ -363,38 +444,143 @@ class PhotoTemplate:
|
|||||||
f"Unexpected value: {lookup_value}"
|
f"Unexpected value: {lookup_value}"
|
||||||
)
|
)
|
||||||
|
|
||||||
subst = make_subst_function(
|
subst = self.make_subst_function(
|
||||||
self, none_str, get_func=lookup_template_value_multi
|
none_str,
|
||||||
|
filename,
|
||||||
|
dirname,
|
||||||
|
replacement,
|
||||||
|
get_func=lookup_template_value_multi,
|
||||||
)
|
)
|
||||||
new_string = regex_multi.sub(subst, str_template)
|
new_string = regex_multi.sub(subst, str_template)
|
||||||
new_strings[new_string] = 1
|
new_strings[new_string] = 1
|
||||||
|
|
||||||
# update rendered_strings for the next field to process
|
# update rendered_strings for the next field to process
|
||||||
rendered_strings = list(new_strings.keys())
|
rendered_strings = sorted(list(new_strings.keys()))
|
||||||
|
return rendered_strings
|
||||||
|
|
||||||
# find any {fields} that weren't replaced
|
def _render_exiftool_template(
|
||||||
unmatched = []
|
self,
|
||||||
for rendered_str in rendered_strings:
|
rendered_strings,
|
||||||
unmatched.extend(
|
none_str,
|
||||||
[
|
path_sep,
|
||||||
no_match[1]
|
expand_inplace,
|
||||||
for no_match in re.findall(regex, rendered_str)
|
inplace_sep,
|
||||||
if no_match[1] not in unmatched
|
filename,
|
||||||
]
|
dirname,
|
||||||
|
replacement,
|
||||||
|
):
|
||||||
|
# TODO: lots of code commonality with render_multi_valued_templates -- combine or pull out
|
||||||
|
# TODO: put these in globals
|
||||||
|
if path_sep is None:
|
||||||
|
path_sep = os.path.sep
|
||||||
|
|
||||||
|
if inplace_sep is None:
|
||||||
|
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 }}
|
||||||
|
)
|
||||||
|
regex_multi = re.compile(re_str)
|
||||||
|
|
||||||
|
# holds each of the new rendered_strings, dict to avoid repeats (dict.keys())
|
||||||
|
new_rendered_strings = []
|
||||||
|
while new_rendered_strings != rendered_strings:
|
||||||
|
new_rendered_strings = rendered_strings
|
||||||
|
new_strings = {}
|
||||||
|
for str_template in rendered_strings:
|
||||||
|
matches = regex_multi.search(str_template)
|
||||||
|
if matches:
|
||||||
|
# allmatches = regex_multi.finditer(str_template)
|
||||||
|
# for matches in allmatches:
|
||||||
|
path_sep = (
|
||||||
|
matches.group(3).strip("()")
|
||||||
|
if matches.group(3) is not None
|
||||||
|
else path_sep
|
||||||
|
)
|
||||||
|
field = matches.group(2)
|
||||||
|
subfield = field[9:]
|
||||||
|
|
||||||
|
if not self.photo.path:
|
||||||
|
values = []
|
||||||
|
else:
|
||||||
|
exif = ExifTool(self.photo.path)
|
||||||
|
exifdict = exif.asdict()
|
||||||
|
exifdict = {k.lower(): v for (k, v) in exifdict.items()}
|
||||||
|
subfield = subfield.lower()
|
||||||
|
if subfield in exifdict:
|
||||||
|
values = exifdict[subfield]
|
||||||
|
values = (
|
||||||
|
[values] if not isinstance(values, list) else values
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
values = [None]
|
||||||
|
if expand_inplace or matches.group(1) is not None:
|
||||||
|
delim = (
|
||||||
|
matches.group(1)[:-1]
|
||||||
|
if matches.group(1) is not None
|
||||||
|
else inplace_sep
|
||||||
|
)
|
||||||
|
# instead of returning multiple strings, join values into a single string
|
||||||
|
val = (
|
||||||
|
delim.join(sorted(values)) if values and values[0] else None
|
||||||
)
|
)
|
||||||
|
|
||||||
# fix any escaped curly braces
|
def lookup_template_value_exif(lookup_value, *_):
|
||||||
rendered_strings = [
|
""" Closure passed to make_subst_function get_func
|
||||||
rendered_str.replace("{{", "{").replace("}}", "}")
|
Capture val and field in the closure
|
||||||
for rendered_str in rendered_strings
|
Allows make_subst_function to be re-used w/o modification
|
||||||
]
|
_ is not used but required so signature matches get_template_value """
|
||||||
|
if lookup_value == field:
|
||||||
|
return val
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unexpected value: {lookup_value}")
|
||||||
|
|
||||||
if filename:
|
subst = self.make_subst_function(
|
||||||
rendered_strings = [
|
none_str,
|
||||||
sanitize_filename(rendered_str) for rendered_str in rendered_strings
|
filename,
|
||||||
]
|
dirname,
|
||||||
|
replacement,
|
||||||
|
get_func=lookup_template_value_exif,
|
||||||
|
)
|
||||||
|
new_string = regex_multi.sub(subst, str_template)
|
||||||
|
# update rendered_strings for the next field to process
|
||||||
|
rendered_strings = list({new_string})
|
||||||
|
else:
|
||||||
|
# create a new template string for each value
|
||||||
|
for val in values:
|
||||||
|
|
||||||
return rendered_strings, unmatched
|
def lookup_template_value_exif(lookup_value, *_):
|
||||||
|
""" 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
|
||||||
|
_ is not used but required so signature matches get_template_value """
|
||||||
|
if lookup_value == field:
|
||||||
|
return val
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Unexpected value: {lookup_value}"
|
||||||
|
)
|
||||||
|
|
||||||
|
subst = self.make_subst_function(
|
||||||
|
none_str,
|
||||||
|
filename,
|
||||||
|
dirname,
|
||||||
|
replacement,
|
||||||
|
get_func=lookup_template_value_exif,
|
||||||
|
)
|
||||||
|
new_string = regex_multi.sub(subst, str_template)
|
||||||
|
new_strings[new_string] = 1
|
||||||
|
# update rendered_strings for the next field to process
|
||||||
|
rendered_strings = sorted(list(new_strings.keys()))
|
||||||
|
return rendered_strings
|
||||||
|
|
||||||
def get_template_value(
|
def get_template_value(
|
||||||
self,
|
self,
|
||||||
@@ -681,6 +867,7 @@ class PhotoTemplate:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
""" return list of values for a multi-valued template field """
|
""" return list of values for a multi-valued template field """
|
||||||
|
values = []
|
||||||
if field == "album":
|
if field == "album":
|
||||||
values = self.photo.albums
|
values = self.photo.albums
|
||||||
elif field == "keyword":
|
elif field == "keyword":
|
||||||
@@ -724,7 +911,7 @@ class PhotoTemplate:
|
|||||||
values = [
|
values = [
|
||||||
f"{comment.user}: {comment.text}" for comment in self.photo.comments
|
f"{comment.user}: {comment.text}" for comment in self.photo.comments
|
||||||
]
|
]
|
||||||
else:
|
elif not field.startswith("exiftool:"):
|
||||||
raise ValueError(f"Unhandled template value: {field}")
|
raise ValueError(f"Unhandled template value: {field}")
|
||||||
|
|
||||||
# sanitize directory names if needed, folder_album handled differently above
|
# sanitize directory names if needed, folder_album handled differently above
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
""" Test template.py """
|
""" Test template.py """
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from osxphotos.exiftool import get_exiftool_path
|
||||||
|
|
||||||
|
try:
|
||||||
|
exiftool = get_exiftool_path()
|
||||||
|
except:
|
||||||
|
exiftool = None
|
||||||
|
|
||||||
PHOTOS_DB_PLACES = (
|
PHOTOS_DB_PLACES = (
|
||||||
"./tests/Test-Places-Catalina-10_15_7.photoslibrary/database/photos.db"
|
"./tests/Test-Places-Catalina-10_15_7.photoslibrary/database/photos.db"
|
||||||
)
|
)
|
||||||
@@ -57,6 +64,29 @@ UUID_BOOL_VALUES = {"hdr": "D11D25FF-5F31-47D2-ABA9-58418878DC15"}
|
|||||||
# Boolean type values that render to False
|
# Boolean type values that render to False
|
||||||
UUID_BOOL_VALUES_NOT = {"hdr": "51F2BEF7-431A-4D31-8AC1-3284A57826AE"}
|
UUID_BOOL_VALUES_NOT = {"hdr": "51F2BEF7-431A-4D31-8AC1-3284A57826AE"}
|
||||||
|
|
||||||
|
# for exiftool template
|
||||||
|
UUID_EXIFTOOL = {
|
||||||
|
"A92D9C26-3A50-4197-9388-CB5F7DB9FA91": {
|
||||||
|
"{exiftool:EXIF:Make}": ["Canon"],
|
||||||
|
"{exiftool:EXIF:Model}": ["Canon PowerShot G10"],
|
||||||
|
"{exiftool:EXIF:Make}/{exiftool:EXIF:Model}": ["Canon/Canon PowerShot G10"],
|
||||||
|
"{exiftool:IPTC:Keywords,foo}": ["foo"],
|
||||||
|
},
|
||||||
|
"DC99FBDD-7A52-4100-A5BB-344131646C30": {
|
||||||
|
"{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"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
TEMPLATE_VALUES = {
|
TEMPLATE_VALUES = {
|
||||||
"{name}": "128FB4C6-0B16-4E7D-9108-FB2E90DA1546",
|
"{name}": "128FB4C6-0B16-4E7D-9108-FB2E90DA1546",
|
||||||
"{original_name}": "IMG_1064",
|
"{original_name}": "IMG_1064",
|
||||||
@@ -737,3 +767,15 @@ 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])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(exiftool is None, reason="exiftool not installed")
|
||||||
|
def test_exiftool_template():
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_7)
|
||||||
|
for uuid in UUID_EXIFTOOL:
|
||||||
|
photo = photosdb.get_photo(uuid)
|
||||||
|
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