Replaced template renderer with regex-based renderer
This commit is contained in:
@@ -21,7 +21,7 @@ import osxphotos
|
|||||||
from ._constants import _EXIF_TOOL_URL, _PHOTOS_5_VERSION
|
from ._constants import _EXIF_TOOL_URL, _PHOTOS_5_VERSION
|
||||||
from ._version import __version__
|
from ._version import __version__
|
||||||
from .exiftool import get_exiftool_path
|
from .exiftool import get_exiftool_path
|
||||||
from .template import render_filename_template, TEMPLATE_SUBSTITUTIONS
|
from .template import render_filepath_template, TEMPLATE_SUBSTITUTIONS
|
||||||
from .utils import _copy_file, create_path_by_date
|
from .utils import _copy_file, create_path_by_date
|
||||||
|
|
||||||
|
|
||||||
@@ -1403,7 +1403,7 @@ def export_photo(
|
|||||||
date_created = photo.date.timetuple()
|
date_created = photo.date.timetuple()
|
||||||
dest = create_path_by_date(dest, date_created)
|
dest = create_path_by_date(dest, date_created)
|
||||||
elif directory:
|
elif directory:
|
||||||
dirname, unmatched = render_filename_template(directory, photo)
|
dirname, unmatched = render_filepath_template(directory, photo)
|
||||||
if unmatched:
|
if unmatched:
|
||||||
click.echo(
|
click.echo(
|
||||||
f"Possible unmatched substitution in template: {unmatched}", err=True
|
f"Possible unmatched substitution in template: {unmatched}", err=True
|
||||||
|
|||||||
@@ -39,125 +39,185 @@ TEMPLATE_SUBSTITUTIONS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def render_filename_template(
|
def get_template_value(lookup, photo):
|
||||||
|
""" lookup: value to find a match for
|
||||||
|
photo: PhotoInfo object whose data will be used for value substitutions
|
||||||
|
returns: either the matching template value (which may be None)
|
||||||
|
raises: KeyError if no rule exists for lookup """
|
||||||
|
|
||||||
|
# must be a valid keyword
|
||||||
|
if lookup == "name":
|
||||||
|
return pathlib.Path(photo.filename).stem
|
||||||
|
|
||||||
|
if lookup == "original_name":
|
||||||
|
return pathlib.Path(photo.original_filename).stem
|
||||||
|
|
||||||
|
if lookup == "title":
|
||||||
|
return photo.title
|
||||||
|
|
||||||
|
if lookup == "descr":
|
||||||
|
return photo.description
|
||||||
|
|
||||||
|
if lookup == "created.date":
|
||||||
|
return DateTimeFormatter(photo.date).date
|
||||||
|
|
||||||
|
if lookup == "created.year":
|
||||||
|
return DateTimeFormatter(photo.date).year
|
||||||
|
|
||||||
|
if lookup == "created.yy":
|
||||||
|
return DateTimeFormatter(photo.date).yy
|
||||||
|
|
||||||
|
if lookup == "created.mm":
|
||||||
|
return DateTimeFormatter(photo.date).mm
|
||||||
|
|
||||||
|
if lookup == "created.month":
|
||||||
|
return DateTimeFormatter(photo.date).month
|
||||||
|
|
||||||
|
if lookup == "created.mon":
|
||||||
|
return DateTimeFormatter(photo.date).mon
|
||||||
|
|
||||||
|
if lookup == "created.doy":
|
||||||
|
return DateTimeFormatter(photo.date).doy
|
||||||
|
|
||||||
|
if lookup == "modified.date":
|
||||||
|
return (
|
||||||
|
DateTimeFormatter(photo.date_modified).date if photo.date_modified else None
|
||||||
|
)
|
||||||
|
|
||||||
|
if lookup == "modified.year":
|
||||||
|
return (
|
||||||
|
DateTimeFormatter(photo.date_modified).year if photo.date_modified else None
|
||||||
|
)
|
||||||
|
|
||||||
|
if lookup == "modified.yy":
|
||||||
|
return (
|
||||||
|
DateTimeFormatter(photo.date_modified).yy if photo.date_modified else None
|
||||||
|
)
|
||||||
|
|
||||||
|
if lookup == "modified.mm":
|
||||||
|
return (
|
||||||
|
DateTimeFormatter(photo.date_modified).mm if photo.date_modified else None
|
||||||
|
)
|
||||||
|
|
||||||
|
if lookup == "modified.month":
|
||||||
|
return (
|
||||||
|
DateTimeFormatter(photo.date_modified).month
|
||||||
|
if photo.date_modified
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
if lookup == "modified.mon":
|
||||||
|
return (
|
||||||
|
DateTimeFormatter(photo.date_modified).mon if photo.date_modified else None
|
||||||
|
)
|
||||||
|
|
||||||
|
if lookup == "modified.doy":
|
||||||
|
return (
|
||||||
|
DateTimeFormatter(photo.date_modified).doy if photo.date_modified else None
|
||||||
|
)
|
||||||
|
|
||||||
|
if lookup == "place.name":
|
||||||
|
return photo.place.name if photo.place else None
|
||||||
|
|
||||||
|
if lookup == "place.names.country":
|
||||||
|
return (
|
||||||
|
photo.place.names.country[0]
|
||||||
|
if photo.place and photo.place.names.country
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
if lookup == "place.names.state_province":
|
||||||
|
return (
|
||||||
|
photo.place.names.state_province[0]
|
||||||
|
if photo.place and photo.place.names.state_province
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
if lookup == "place.names.city":
|
||||||
|
return (
|
||||||
|
photo.place.names.city[0]
|
||||||
|
if photo.place and photo.place.names.city
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
if lookup == "place.names.area_of_interest":
|
||||||
|
return (
|
||||||
|
photo.place.names.area_of_interest[0]
|
||||||
|
if photo.place and photo.place.names.area_of_interest
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
# if here, didn't get a match
|
||||||
|
raise KeyError(f"No rule for processing {lookup}")
|
||||||
|
|
||||||
|
|
||||||
|
def render_filepath_template(
|
||||||
template: str, photo: PhotoInfo, none_str: str = "_"
|
template: str, photo: PhotoInfo, none_str: str = "_"
|
||||||
) -> Tuple[str, list]:
|
) -> Tuple[str, list]:
|
||||||
""" render a filename or directory template """
|
""" render a filename or directory template """
|
||||||
|
|
||||||
|
# pylint: disable=anomalous-backslash-in-string
|
||||||
|
regex = r"""(?<!\\)\{([^\\,}]+)(,{0,1}(([\w\-. ]+))?)\}"""
|
||||||
|
|
||||||
|
# pylint: disable=anomalous-backslash-in-string
|
||||||
|
unmatched_regex = r"(?<!\\)(\{[^\\,}]+\})"
|
||||||
|
|
||||||
|
# Explanation for regex:
|
||||||
|
# (?<!\\) Negative Lookbehind to skip escaped braces
|
||||||
|
# assert regex following does not match "\" preceeding "{"
|
||||||
|
# \{ Match the opening brace
|
||||||
|
# 1st Capturing Group ([^\\,}]+) Don't match "\", ",", or "}"
|
||||||
|
# 2nd Capturing Group (,?(([\w\-. ]+))?)
|
||||||
|
# ,{0,1} optional ","
|
||||||
|
# 3rd Capturing Group (([\w\-. ]+))?
|
||||||
|
# Matches the comma and any word characters after
|
||||||
|
# 4th Capturing Group ([\w\-. ]+)
|
||||||
|
# Matches just the characters after the comma
|
||||||
|
# \} Matches the closing brace
|
||||||
|
|
||||||
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)}")
|
||||||
|
|
||||||
if type(photo) is not PhotoInfo:
|
if type(photo) is not PhotoInfo:
|
||||||
raise TypeError(f"photo must be type osxphotos.PhotoInfo, not {type(photo)}")
|
raise TypeError(f"photo must be type osxphotos.PhotoInfo, not {type(photo)}")
|
||||||
|
|
||||||
rendered = template
|
def make_subst_function(photo, none_str):
|
||||||
original_name = pathlib.Path(photo.original_filename).stem
|
""" returns: substitution function for use in re.sub """
|
||||||
current_name = pathlib.Path(photo.filename).stem
|
# closure to capture photo, none_str in subst
|
||||||
created = DateTimeFormatter(photo.date)
|
def subst(matchobj):
|
||||||
if photo.date_modified:
|
groups = len(matchobj.groups())
|
||||||
modified = DateTimeFormatter(photo.date_modified)
|
if groups == 4:
|
||||||
|
try:
|
||||||
|
val = get_template_value(matchobj.group(1), photo)
|
||||||
|
except KeyError:
|
||||||
|
return matchobj.group(0)
|
||||||
|
|
||||||
|
if val is None:
|
||||||
|
return (
|
||||||
|
matchobj.group(3) if matchobj.group(3) is not None else none_str
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
modified = None
|
return val
|
||||||
|
|
||||||
# make substitutions
|
|
||||||
rendered = rendered.replace("{name}", current_name)
|
|
||||||
rendered = rendered.replace("{original_name}", original_name)
|
|
||||||
|
|
||||||
title = photo.title if photo.title is not None else none_str
|
|
||||||
rendered = rendered.replace("{title}", f"{title}")
|
|
||||||
|
|
||||||
descr = photo.description if photo.description is not None else none_str
|
|
||||||
rendered = rendered.replace("{descr}", f"{descr}")
|
|
||||||
|
|
||||||
rendered = rendered.replace("{created.date}", photo.date.date().isoformat())
|
|
||||||
rendered = rendered.replace("{created.year}", created.year)
|
|
||||||
rendered = rendered.replace("{created.yy}", created.yy)
|
|
||||||
rendered = rendered.replace("{created.mm}", created.mm)
|
|
||||||
rendered = rendered.replace("{created.month}", created.month)
|
|
||||||
rendered = rendered.replace("{created.mon}", created.mon)
|
|
||||||
rendered = rendered.replace("{created.doy}", created.doy)
|
|
||||||
|
|
||||||
if modified is not None:
|
|
||||||
rendered = rendered.replace(
|
|
||||||
"{modified.date}", photo.date_modified.date().isoformat()
|
|
||||||
)
|
|
||||||
rendered = rendered.replace("{modified.year}", modified.year)
|
|
||||||
rendered = rendered.replace("{modified.yy}", modified.yy)
|
|
||||||
rendered = rendered.replace("{modified.mm}", modified.mm)
|
|
||||||
rendered = rendered.replace("{modified.month}", modified.month)
|
|
||||||
rendered = rendered.replace("{modified.mon}", modified.mon)
|
|
||||||
rendered = rendered.replace("{modified.doy}", modified.doy)
|
|
||||||
else:
|
else:
|
||||||
rendered = rendered.replace("{modified.year}", none_str)
|
raise ValueError(
|
||||||
rendered = rendered.replace("{modified.yy}", none_str)
|
f"Unexpected number of groups: expected 4, got {groups}"
|
||||||
rendered = rendered.replace("{modified.mm}", none_str)
|
|
||||||
rendered = rendered.replace("{modified.month}", none_str)
|
|
||||||
rendered = rendered.replace("{modified.mon}", none_str)
|
|
||||||
rendered = rendered.replace("{modified.doy}", none_str)
|
|
||||||
|
|
||||||
place_name = photo.place.name if photo.place and photo.place.name else none_str
|
|
||||||
rendered = rendered.replace("{place.name}", place_name)
|
|
||||||
|
|
||||||
# place_names = (
|
|
||||||
# "_".join(photo.place.names) if photo.place and photo.place.names else none_str
|
|
||||||
# )
|
|
||||||
# rendered = rendered.replace("{place.names}", place_names)
|
|
||||||
|
|
||||||
address = (
|
|
||||||
photo.place.address_str if photo.place and photo.place.address_str else none_str
|
|
||||||
)
|
)
|
||||||
rendered = rendered.replace("{place.address}", address)
|
|
||||||
|
|
||||||
street = (
|
return subst
|
||||||
photo.place.address.street
|
|
||||||
if photo.place and photo.place.address.street
|
|
||||||
else none_str
|
|
||||||
)
|
|
||||||
rendered = rendered.replace("{place.street}", street)
|
|
||||||
|
|
||||||
city = (
|
subst_func = make_subst_function(photo, none_str)
|
||||||
photo.place.address.city
|
|
||||||
if photo.place and photo.place.address.city
|
|
||||||
else none_str
|
|
||||||
)
|
|
||||||
rendered = rendered.replace("{place.city}", city)
|
|
||||||
|
|
||||||
state_province = (
|
# do the replacements
|
||||||
photo.place.address.state_province
|
rendered = re.sub(regex, subst_func, template)
|
||||||
if photo.place and photo.place.address.state_province
|
|
||||||
else none_str
|
|
||||||
)
|
|
||||||
rendered = rendered.replace("{place.state_province}", state_province)
|
|
||||||
|
|
||||||
postal_code = (
|
# find any {words} that weren't replaced
|
||||||
photo.place.address.postal_code
|
unmatched = re.findall(unmatched_regex, rendered)
|
||||||
if photo.place and photo.place.address.postal_code
|
|
||||||
else none_str
|
|
||||||
)
|
|
||||||
rendered = rendered.replace("{place.postal_code}", postal_code)
|
|
||||||
|
|
||||||
country = (
|
|
||||||
photo.place.address.country
|
|
||||||
if photo.place and photo.place.address.country
|
|
||||||
else none_str
|
|
||||||
)
|
|
||||||
rendered = rendered.replace("{place.country}", country)
|
|
||||||
|
|
||||||
country_code = (
|
|
||||||
photo.place.country_code
|
|
||||||
if photo.place and photo.place.country_code
|
|
||||||
else none_str
|
|
||||||
)
|
|
||||||
rendered = rendered.replace("{place.country_code}", country_code)
|
|
||||||
|
|
||||||
# fix any escaped curly braces
|
# fix any escaped curly braces
|
||||||
rendered = re.sub(r"\\{", "{", rendered)
|
rendered = re.sub(r"\\{", "{", rendered)
|
||||||
rendered = re.sub(r"\\}", "}", rendered)
|
rendered = re.sub(r"\\}", "}", rendered)
|
||||||
|
|
||||||
# find any {words} that weren't replaced
|
return rendered, unmatched
|
||||||
unmatched = re.findall(r"{\w+}", rendered)
|
|
||||||
|
|
||||||
return (rendered, unmatched)
|
|
||||||
|
|
||||||
|
|
||||||
class DateTimeFormatter:
|
class DateTimeFormatter:
|
||||||
@@ -166,6 +226,12 @@ class DateTimeFormatter:
|
|||||||
def __init__(self, dt: datetime.datetime):
|
def __init__(self, dt: datetime.datetime):
|
||||||
self.dt = dt
|
self.dt = dt
|
||||||
|
|
||||||
|
@property
|
||||||
|
def date(self):
|
||||||
|
""" ISO date in form 2020-03-22 """
|
||||||
|
date = self.dt.date().isoformat()
|
||||||
|
return date
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def year(self):
|
def year(self):
|
||||||
""" 4 digit year """
|
""" 4 digit year """
|
||||||
|
|||||||
Reference in New Issue
Block a user