Compare commits

..

4 Commits

Author SHA1 Message Date
Rhet Turnbull
48acb42631 Added {exiftool} template, implements issue #259 2020-11-27 16:43:48 -08:00
Rhet Turnbull
eba661acf7 Updated CHANGELOG.md 2020-11-26 19:53:35 -08:00
Rhet Turnbull
399d432a66 Added --original-suffix for issue #263 2020-11-26 18:36:17 -08:00
Rhet Turnbull
4cebc57d60 Updated CHANGELOG.md 2020-11-26 15:26:54 -08:00
7 changed files with 483 additions and 158 deletions

View File

@@ -4,6 +4,20 @@ 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.36.25](https://github.com/RhetTbull/osxphotos/compare/v0.36.24...v0.36.25)
> 27 November 2020
- Added --original-suffix for issue #263 [`399d432`](https://github.com/RhetTbull/osxphotos/commit/399d432a66354b9c235f30d10c6985fbde1b7e4f)
#### [v0.36.24](https://github.com/RhetTbull/osxphotos/compare/v0.36.23...v0.36.24)
> 26 November 2020
- Initial implementation for issue #265 [`382fca3`](https://github.com/RhetTbull/osxphotos/commit/382fca3f92a3c251c12426dd0dc6d7dc21b691cf)
- More work on issue #265 [`d5a9f76`](https://github.com/RhetTbull/osxphotos/commit/d5a9f767199d25ebd9d5925d05ee39ea7e51ac26)
- Simplified sidecar table in export_db [`0632a97`](https://github.com/RhetTbull/osxphotos/commit/0632a97f55af67c7e5265b0d3283155c7c087e89)
#### [v0.36.23](https://github.com/RhetTbull/osxphotos/compare/v0.36.22...v0.36.23)
> 26 November 2020

View File

@@ -351,6 +351,13 @@ Options:
photo would be named
'photoname_bearbeiten.ext'. The default
suffix is '_edited'.
--original-suffix SUFFIX Optional suffix for naming original photos.
Default name for original photos is in form
'filename.ext'. For example, with '--
original-suffix _original', the original
photo would be named
'filename_original.ext'. The default suffix
is '' (no suffix).
--no-extended-attributes Don't copy extended attributes when
exporting. You only need this if exporting
to a filesystem that doesn't support Mac OS
@@ -629,18 +636,26 @@ exported, one to each directory. For example: --directory
of the following directories if the photos were created in 2019 and were in
albums 'Vacation' and 'Family': 2019/Vacation, 2019/Family
Substitution Description
{album} Album(s) photo is contained in
{folder_album} Folder path + album photo is contained in. e.g.
'Folder/Subfolder/Album' or just 'Album' if no enclosing
folder
{keyword} Keyword(s) assigned to photo
{person} Person(s) / face(s) in a photo
{label} Image categorization label associated with a photo
(Photos 5 only)
{label_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)
Substitution Description
{album} Album(s) photo is contained in
{folder_album} Folder path + album photo is contained in. e.g.
'Folder/Subfolder/Album' or just 'Album' if no
enclosing folder
{keyword} Keyword(s) assigned to photo
{person} Person(s) / face(s) in a photo
{label} Image categorization label associated with a photo
(Photos 5 only)
{label_normalized} All lower case version of 'label' (Photos 5 only)
{comment} Comment(s) on shared Photos; format is 'Person
name: comment text' (Photos 5 only)
{exiftool: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

View File

@@ -1383,6 +1383,14 @@ def query(
"'photoname_edited.ext'. For example, with '--edited-suffix _bearbeiten', the edited photo "
"would be named 'photoname_bearbeiten.ext'. The default suffix is '_edited'.",
)
@click.option(
"--original-suffix",
metavar="SUFFIX",
default="",
help="Optional suffix for naming original photos. Default name for original photos is in form "
"'filename.ext'. For example, with '--original-suffix _original', the original photo "
"would be named 'filename_original.ext'. The default suffix is '' (no suffix).",
)
@click.option(
"--no-extended-attributes",
is_flag=True,
@@ -1484,6 +1492,7 @@ def export(
directory,
filename_template,
edited_suffix,
original_suffix,
place,
no_place,
has_comment,
@@ -1752,6 +1761,7 @@ def export(
dry_run=dry_run,
touch_file=touch_file,
edited_suffix=edited_suffix,
original_suffix=original_suffix,
use_photos_export=use_photos_export,
convert_to_jpeg=convert_to_jpeg,
jpeg_quality=jpeg_quality,
@@ -1805,6 +1815,7 @@ def export(
dry_run=dry_run,
touch_file=touch_file,
edited_suffix=edited_suffix,
original_suffix=original_suffix,
use_photos_export=use_photos_export,
convert_to_jpeg=convert_to_jpeg,
jpeg_quality=jpeg_quality,
@@ -2324,6 +2335,7 @@ def export_photo(
dry_run=None,
touch_file=None,
edited_suffix="_edited",
original_suffix="",
use_photos_export=False,
convert_to_jpeg=False,
jpeg_quality=1.0,
@@ -2422,7 +2434,19 @@ def export_photo(
filenames = get_filenames_from_template(photo, filename_template, original_name)
for filename in filenames:
verbose(f"Exporting {photo.original_filename} ({photo.filename}) as {filename}")
if original_suffix:
original_filename = pathlib.Path(filename)
original_filename = (
original_filename.parent
/ f"{original_filename.stem}{original_suffix}{original_filename.suffix}"
)
original_filename = str(original_filename)
else:
original_filename = filename
verbose(
f"Exporting {photo.original_filename} ({photo.filename}) as {original_filename}"
)
dest_paths = get_dirnames_from_template(
photo, directory, export_by_date, dest, dry_run
@@ -2448,12 +2472,10 @@ def export_photo(
# export the photo to each path in dest_paths
for dest_path in dest_paths:
if not export_original:
verbose(f"Skipping original version of {photo.original_filename}")
else:
if export_original:
export_results = photo.export2(
dest_path,
filename,
original_filename,
sidecar_json=sidecar_json,
sidecar_xmp=sidecar_xmp,
live_photo=export_live,
@@ -2500,6 +2522,9 @@ def export_photo(
for touched in export_results.touched:
verbose(f"Touched date on file {touched}")
else:
verbose(f"Skipping original version of {photo.original_filename}")
# if export-edited, also export the edited version
# verify the photo has adjustments and valid path to avoid raising an exception
if export_edited and photo.hasadjustments:
@@ -2508,7 +2533,7 @@ def export_photo(
if not download_missing and photo.path_edited is None:
verbose(f"Skipping missing edited photo for {filename}")
else:
edited_name = pathlib.Path(filename)
edited_filename = pathlib.Path(filename)
# check for correct edited suffix
if photo.path_edited is not None:
edited_ext = pathlib.Path(photo.path_edited).suffix
@@ -2516,11 +2541,15 @@ def export_photo(
# use filename suffix which might be wrong,
# will be corrected by use_photos_export
edited_ext = pathlib.Path(photo.filename).suffix
edited_name = f"{edited_name.stem}{edited_suffix}{edited_ext}"
verbose(f"Exporting edited version of {filename} as {edited_name}")
edited_filename = (
f"{edited_filename.stem}{edited_suffix}{edited_ext}"
)
verbose(
f"Exporting edited version of {filename} as {edited_filename}"
)
export_results_edited = photo.export2(
dest_path,
edited_name,
edited_filename,
sidecar_json=sidecar_json,
sidecar_xmp=sidecar_xmp,
export_as_hardlink=export_as_hardlink,

View File

@@ -1,4 +1,4 @@
""" version info """
__version__ = "0.36.24"
__version__ = "0.37.0"

View File

@@ -18,6 +18,7 @@ from functools import partial
from ._constants import _UNKNOWN_PERSON
from .datetime_formatter import DateTimeFormatter
from .exiftool import ExifTool
from .path_utils import sanitize_dirname, sanitize_filename, sanitize_pathpart
# 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_normalized}": "All lower case version of 'label' (Photos 5 only)",
"{comment}": "Comment(s) on shared Photos; format is 'Person name: comment text' (Photos 5 only)",
"{exiftool: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
@@ -150,6 +155,62 @@ class PhotoTemplate:
# gets initialized in get_template_value
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(
self,
template,
@@ -208,60 +269,7 @@ class PhotoTemplate:
if type(template) is not str:
raise TypeError(f"template must be type str, not {type(template)}")
# 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,
)
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)
subst_func = self.make_subst_function(none_str, filename, dirname, replacement)
# do the replacements
rendered = re.sub(regex, subst_func, template)
@@ -289,88 +297,28 @@ class PhotoTemplate:
# '2011/Album2/keyword1/person1',
# '2011/Album2/keyword2/person1',]
rendered_strings = [rendered]
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+
+ 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 }}
)
regex_multi = re.compile(re_str)
rendered_strings = self._render_multi_valued_templates(
rendered,
none_str,
path_sep,
expand_inplace,
inplace_sep,
filename,
dirname,
replacement,
)
# holds each of the new rendered_strings, dict to avoid repeats (dict.keys())
new_strings = {}
for str_template in rendered_strings:
matches = regex_multi.search(str_template)
if matches:
path_sep = (
matches.group(3).strip("()")
if matches.group(3) is not None
else path_sep
)
values = self.get_template_value_multi(
field,
path_sep,
filename=filename,
dirname=dirname,
replacement=replacement,
)
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
def lookup_template_value_multi(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 = make_subst_function(
self, none_str, get_func=lookup_template_value_multi
)
new_string = regex_multi.sub(subst, str_template)
# update rendered_strings for the next field to process
rendered_strings = {new_string}
else:
# create a new template string for each value
for val in values:
def lookup_template_value_multi(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 = make_subst_function(
self, none_str, get_func=lookup_template_value_multi
)
new_string = regex_multi.sub(subst, str_template)
new_strings[new_string] = 1
# update rendered_strings for the next field to process
rendered_strings = list(new_strings.keys())
# 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 = []
@@ -396,6 +344,244 @@ class PhotoTemplate:
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]
new_rendered_strings = []
while new_rendered_strings != rendered_strings:
new_rendered_strings = rendered_strings
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+
+ 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 }}
)
regex_multi = re.compile(re_str)
# holds each of the new rendered_strings, dict to avoid repeats (dict.keys())
new_strings = {}
for str_template in rendered_strings:
matches = regex_multi.search(str_template)
if matches:
path_sep = (
matches.group(3).strip("()")
if matches.group(3) is not None
else path_sep
)
values = self.get_template_value_multi(
field,
path_sep,
filename=filename,
dirname=dirname,
replacement=replacement,
)
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
)
def lookup_template_value_multi(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_multi,
)
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:
def lookup_template_value_multi(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_multi,
)
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 _render_exiftool_template(
self,
rendered_strings,
none_str,
path_sep,
expand_inplace,
inplace_sep,
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
)
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)
# 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:
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(
self,
field,
@@ -681,6 +867,7 @@ class PhotoTemplate:
"""
""" return list of values for a multi-valued template field """
values = []
if field == "album":
values = self.photo.albums
elif field == "keyword":
@@ -724,7 +911,7 @@ class PhotoTemplate:
values = [
f"{comment.user}: {comment.text}" for comment in self.photo.comments
]
else:
elif not field.startswith("exiftool:"):
raise ValueError(f"Unhandled template value: {field}")
# sanitize directory names if needed, folder_album handled differently above

View File

@@ -65,6 +65,7 @@ CLI_EXPORT_FILENAMES_ALBUM_UNICODE = ["IMG_4547.jpg"]
CLI_EXPORT_FILENAMES_DELETED_TWIN = ["wedding.jpg", "wedding_edited.jpeg"]
CLI_EXPORT_EDITED_SUFFIX = "_bearbeiten"
CLI_EXPORT_ORIGINAL_SUFFIX = "_original"
CLI_EXPORT_FILENAMES_EDITED_SUFFIX = [
"Pumkins1.jpg",
@@ -77,6 +78,16 @@ CLI_EXPORT_FILENAMES_EDITED_SUFFIX = [
"wedding_bearbeiten.jpeg",
]
CLI_EXPORT_FILENAMES_ORIGINAL_SUFFIX = [
"Pumkins1_original.jpg",
"Pumkins2_original.jpg",
"Pumpkins3_original.jpg",
"St James Park_original.jpg",
"St James Park_edited.jpeg",
"Tulips_original.jpg",
"wedding_original.jpg",
"wedding_edited.jpeg",
]
CLI_EXPORT_FILENAMES_CURRENT = [
"1EB2B765-0765-43BA-A90C-0D0580E6172C.jpeg",
@@ -1010,6 +1021,33 @@ def test_export_edited_suffix():
assert sorted(files) == sorted(CLI_EXPORT_FILENAMES_EDITED_SUFFIX)
def test_export_original_suffix():
""" test export with --original-suffix """
import glob
import os
import os.path
import osxphotos
from osxphotos.__main__ import export
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
result = runner.invoke(
export,
[
os.path.join(cwd, CLI_PHOTOS_DB),
".",
"--original-suffix",
CLI_EXPORT_ORIGINAL_SUFFIX,
"-V",
],
)
assert result.exit_code == 0
files = glob.glob("*")
assert sorted(files) == sorted(CLI_EXPORT_FILENAMES_ORIGINAL_SUFFIX)
@pytest.mark.skipif(
"OSXPHOTOS_TEST_CONVERT" not in os.environ,
reason="Skip if running in Github actions, no GPU.",

View File

@@ -1,6 +1,13 @@
""" Test template.py """
import pytest
from osxphotos.exiftool import get_exiftool_path
try:
exiftool = get_exiftool_path()
except:
exiftool = None
PHOTOS_DB_PLACES = (
"./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
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 = {
"{name}": "128FB4C6-0B16-4E7D-9108-FB2E90DA1546",
"{original_name}": "IMG_1064",
@@ -737,3 +767,15 @@ def test_expand_in_place_with_delim_single_value():
for template in TEMPLATE_VALUES_TITLE:
rendered, _ = photo.render_template(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])