Added hour, min, sec, strftime templates, closes #158
This commit is contained in:
18
README.md
18
README.md
@@ -367,7 +367,8 @@ contain a brace symbol ('{' or '}').
|
|||||||
|
|
||||||
If you do not specify a default value and the template substitution has no
|
If you do not specify a default value and the template substitution has no
|
||||||
value, '_' (underscore) will be used as the default value. For example, in the
|
value, '_' (underscore) will be used as the default value. For example, in the
|
||||||
above example, this would result in '2020/_/photoname.jpg' if address was null.
|
above example, this would result in '2020/_/photoname.jpg' if address was
|
||||||
|
null.
|
||||||
|
|
||||||
Substitution Description
|
Substitution Description
|
||||||
{name} Current filename of the photo
|
{name} Current filename of the photo
|
||||||
@@ -391,6 +392,18 @@ Substitution Description
|
|||||||
creation time
|
creation time
|
||||||
{created.doy} 3-digit day of year (e.g Julian day) of file
|
{created.doy} 3-digit day of year (e.g Julian day) of file
|
||||||
creation time, starting from 1 (zero padded)
|
creation time, starting from 1 (zero padded)
|
||||||
|
{created.hour} 2-digit hour of the file creation time
|
||||||
|
{created.min} 2-digit minute of the file creation time
|
||||||
|
{created.sec} 2-digit second of the file creation time
|
||||||
|
{created.strftime} Apply strftime template to file creation
|
||||||
|
date/time. Should be used in form
|
||||||
|
{created.strftime,TEMPLATE} where TEMPLATE
|
||||||
|
is a valid strftime template, e.g.
|
||||||
|
{created.strftime,%Y-%U} would result in
|
||||||
|
year-week number of year: '2020-23'. If used
|
||||||
|
with no template will return null value. See
|
||||||
|
https://strftime.org/ for help on strftime
|
||||||
|
templates.
|
||||||
{modified.date} Photo's modification date in ISO format,
|
{modified.date} Photo's modification date in ISO format,
|
||||||
e.g. '2020-03-22'
|
e.g. '2020-03-22'
|
||||||
{modified.year} 4-digit year of file modification time
|
{modified.year} 4-digit year of file modification time
|
||||||
@@ -406,6 +419,9 @@ Substitution Description
|
|||||||
{modified.doy} 3-digit day of year (e.g Julian day) of file
|
{modified.doy} 3-digit day of year (e.g Julian day) of file
|
||||||
modification time, starting from 1 (zero
|
modification time, starting from 1 (zero
|
||||||
padded)
|
padded)
|
||||||
|
{modified.hour} 2-digit hour of the file modification time
|
||||||
|
{modified.min} 2-digit minute of the file modification time
|
||||||
|
{modified.sec} 2-digit second of the file modification time
|
||||||
{place.name} Place name from the photo's reverse
|
{place.name} Place name from the photo's reverse
|
||||||
geolocation data, as displayed in Photos
|
geolocation data, as displayed in Photos
|
||||||
{place.country_code} The ISO country code from the photo's
|
{place.country_code} The ISO country code from the photo's
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
""" version info """
|
""" version info """
|
||||||
|
|
||||||
__version__ = "0.29.15"
|
__version__ = "0.29.16"
|
||||||
|
|||||||
@@ -637,6 +637,9 @@ class PhotoInfo:
|
|||||||
none_str: a str to use if template field renders to None, default is "_".
|
none_str: a str to use if template field renders to None, default is "_".
|
||||||
path_sep: a single character str to use as path separator when joining
|
path_sep: a single character str to use as path separator when joining
|
||||||
fields like folder_album; if not provided, defaults to os.path.sep
|
fields like folder_album; if not provided, defaults to os.path.sep
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values
|
||||||
"""
|
"""
|
||||||
template = PhotoTemplate(self)
|
template = PhotoTemplate(self)
|
||||||
return template.render(template_str, none_str, path_sep)
|
return template.render(template_str, none_str, path_sep)
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ TEMPLATE_SUBSTITUTIONS = {
|
|||||||
"{created.hour}": "2-digit hour of the file creation time",
|
"{created.hour}": "2-digit hour of the file creation time",
|
||||||
"{created.min}": "2-digit minute of the file creation time",
|
"{created.min}": "2-digit minute of the file creation time",
|
||||||
"{created.sec}": "2-digit second of the file creation time",
|
"{created.sec}": "2-digit second of the file creation time",
|
||||||
|
"{created.strftime}": "Apply strftime template to file creation date/time. Should be used in form "
|
||||||
|
+ "{created.strftime,TEMPLATE} where TEMPLATE is a valid strftime template, e.g. "
|
||||||
|
+ "{created.strftime,%Y-%U} would result in year-week number of year: '2020-23'. "
|
||||||
|
+ "If used with no template will return null value. "
|
||||||
|
+ "See https://strftime.org/ for help on strftime templates.",
|
||||||
"{modified.date}": "Photo's modification date in ISO format, e.g. '2020-03-22'",
|
"{modified.date}": "Photo's modification date in ISO format, e.g. '2020-03-22'",
|
||||||
"{modified.year}": "4-digit year of file modification time",
|
"{modified.year}": "4-digit year of file modification time",
|
||||||
"{modified.yy}": "2-digit year of file modification time",
|
"{modified.yy}": "2-digit year of file modification time",
|
||||||
@@ -49,6 +54,11 @@ TEMPLATE_SUBSTITUTIONS = {
|
|||||||
"{modified.hour}": "2-digit hour of the file modification time",
|
"{modified.hour}": "2-digit hour of the file modification time",
|
||||||
"{modified.min}": "2-digit minute of the file modification time",
|
"{modified.min}": "2-digit minute of the file modification time",
|
||||||
"{modified.sec}": "2-digit second of the file modification time",
|
"{modified.sec}": "2-digit second of the file modification time",
|
||||||
|
# "{modified.strftime}": "Apply strftime template to file modification date/time. Should be used in form "
|
||||||
|
# + "{modified.strftime,TEMPLATE} where TEMPLATE is a valid strftime template, e.g. "
|
||||||
|
# + "{modified.strftime,%Y-%U} would result in year-week number of year: '2020-23'. "
|
||||||
|
# + "If used with no template will return null value. "
|
||||||
|
# + "See https://strftime.org/ for help on strftime templates.",
|
||||||
"{place.name}": "Place name from the photo's reverse geolocation data, as displayed in Photos",
|
"{place.name}": "Place name from the photo's reverse geolocation data, as displayed in Photos",
|
||||||
"{place.country_code}": "The ISO country code from the photo's reverse geolocation data",
|
"{place.country_code}": "The ISO country code from the photo's reverse geolocation data",
|
||||||
"{place.name.country}": "Country name from the photo's reverse geolocation data",
|
"{place.name.country}": "Country name from the photo's reverse geolocation data",
|
||||||
@@ -93,10 +103,17 @@ class PhotoTemplate:
|
|||||||
self.photo = photo
|
self.photo = photo
|
||||||
|
|
||||||
def render(self, template, none_str="_", path_sep=None):
|
def render(self, template, none_str="_", path_sep=None):
|
||||||
""" render a filename or directory template
|
""" Render a filename or directory template
|
||||||
|
|
||||||
|
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 character to use as path separator, default is os.path.sep
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values
|
||||||
|
"""
|
||||||
|
|
||||||
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:
|
elif path_sep is not None and len(path_sep) != 1:
|
||||||
@@ -113,7 +130,7 @@ class PhotoTemplate:
|
|||||||
# 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/4JJg42/1
|
# for explanation of regex see https://regex101.com/r/4JJg42/1
|
||||||
# pylint: disable=anomalous-backslash-in-string
|
# pylint: disable=anomalous-backslash-in-string
|
||||||
regex = r"(?<!\{)\{([^\\,}]+)(,{0,1}(([\w\-. ]+))?)(?=\}(?!\}))\}"
|
regex = r"(?<!\{)\{([^\\,}]+)(,{0,1}(([\w\-\%. ]+))?)(?=\}(?!\}))\}"
|
||||||
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)}")
|
||||||
|
|
||||||
@@ -128,7 +145,7 @@ class PhotoTemplate:
|
|||||||
groups = len(matchobj.groups())
|
groups = len(matchobj.groups())
|
||||||
if groups == 4:
|
if groups == 4:
|
||||||
try:
|
try:
|
||||||
val = get_func(matchobj.group(1))
|
val = get_func(matchobj.group(1), matchobj.group(3))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return matchobj.group(0)
|
return matchobj.group(0)
|
||||||
|
|
||||||
@@ -178,7 +195,7 @@ class PhotoTemplate:
|
|||||||
rendered_strings = set([rendered])
|
rendered_strings = set([rendered])
|
||||||
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 = r"(?<!\\)\{(" + field + r")(,(([\w\-. ]{0,})))?\}"
|
re_str = r"(?<!\\)\{(" + field + r")(,(([\w\-\%. ]{0,})))?\}"
|
||||||
regex_multi = re.compile(re_str)
|
regex_multi = re.compile(re_str)
|
||||||
|
|
||||||
# holds each of the new rendered_strings, set() to avoid duplicates
|
# holds each of the new rendered_strings, set() to avoid duplicates
|
||||||
@@ -189,10 +206,11 @@ class PhotoTemplate:
|
|||||||
values = self.get_template_value_multi(field, path_sep)
|
values = self.get_template_value_multi(field, path_sep)
|
||||||
for val in values:
|
for val in values:
|
||||||
|
|
||||||
def lookup_template_value_multi(lookup_value):
|
def lookup_template_value_multi(lookup_value, default):
|
||||||
""" Closure passed to make_subst_function get_func
|
""" Closure passed to make_subst_function get_func
|
||||||
Capture val and field in the closure
|
Capture val and field in the closure
|
||||||
Allows make_subst_function to be re-used w/o modification """
|
Allows make_subst_function to be re-used w/o modification
|
||||||
|
default is not used but required so signature matches get_template_value """
|
||||||
if lookup_value == field:
|
if lookup_value == field:
|
||||||
return val
|
return val
|
||||||
else:
|
else:
|
||||||
@@ -226,11 +244,12 @@ class PhotoTemplate:
|
|||||||
|
|
||||||
return rendered_strings, unmatched
|
return rendered_strings, unmatched
|
||||||
|
|
||||||
def get_template_value(self, field):
|
def get_template_value(self, field, default):
|
||||||
"""lookup value for template field (single-value template substitutions)
|
"""lookup value for template field (single-value template substitutions)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
field: template field to find value for.
|
field: template field to find value for.
|
||||||
|
default: the default value provided by the user
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The matching template value (which may be None).
|
The matching template value (which may be None).
|
||||||
@@ -288,6 +307,15 @@ class PhotoTemplate:
|
|||||||
if field == "created.sec":
|
if field == "created.sec":
|
||||||
return DateTimeFormatter(self.photo.date).sec
|
return DateTimeFormatter(self.photo.date).sec
|
||||||
|
|
||||||
|
if field == "created.strftime":
|
||||||
|
if default:
|
||||||
|
try:
|
||||||
|
return self.photo.date.strftime(default)
|
||||||
|
except:
|
||||||
|
raise ValueError(f"Invalid strftime template: '{default}'")
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
if field == "modified.date":
|
if field == "modified.date":
|
||||||
return (
|
return (
|
||||||
DateTimeFormatter(self.photo.date_modified).date
|
DateTimeFormatter(self.photo.date_modified).date
|
||||||
@@ -365,6 +393,17 @@ class PhotoTemplate:
|
|||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# TODO: disabling modified.strftime for now because now clean way to pass
|
||||||
|
# a default value if modified time is None
|
||||||
|
# if field == "modified.strftime":
|
||||||
|
# if default and self.photo.date_modified:
|
||||||
|
# try:
|
||||||
|
# return self.photo.date_modified.strftime(default)
|
||||||
|
# except:
|
||||||
|
# raise ValueError(f"Invalid strftime template: '{default}'")
|
||||||
|
# else:
|
||||||
|
# return None
|
||||||
|
|
||||||
if field == "place.name":
|
if field == "place.name":
|
||||||
return self.photo.place.name if self.photo.place else None
|
return self.photo.place.name if self.photo.place else None
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ def test_lookup():
|
|||||||
|
|
||||||
for subst in TEMPLATE_SUBSTITUTIONS:
|
for subst in TEMPLATE_SUBSTITUTIONS:
|
||||||
lookup_str = re.match(r"\{([^\\,}]+)\}", subst).group(1)
|
lookup_str = re.match(r"\{([^\\,}]+)\}", subst).group(1)
|
||||||
lookup = template.get_template_value(lookup_str)
|
lookup = template.get_template_value(lookup_str, None)
|
||||||
assert lookup or lookup is None
|
assert lookup or lookup is None
|
||||||
|
|
||||||
|
|
||||||
@@ -442,3 +442,19 @@ def test_subst_multi_folder_albums_3():
|
|||||||
rendered, unknown = photo.render_template(template)
|
rendered, unknown = photo.render_template(template)
|
||||||
assert sorted(rendered) == sorted(expected)
|
assert sorted(rendered) == sorted(expected)
|
||||||
assert unknown == []
|
assert unknown == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_subst_strftime():
|
||||||
|
""" Test that strftime substitutions are correct """
|
||||||
|
import locale
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
locale.setlocale(locale.LC_ALL, "en_US")
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_PLACES)
|
||||||
|
photo = photosdb.photos(uuid=[UUID_DICT["place_dc"]])[0]
|
||||||
|
|
||||||
|
rendered, unmatched = photo.render_template("{created.strftime,%Y-%m-%d-%H%M%S}")
|
||||||
|
assert rendered[0] == "2020-02-04-190738"
|
||||||
|
|
||||||
|
rendered, unmatched = photo.render_template("{created.strftime}")
|
||||||
|
assert rendered[0] == "_"
|
||||||
|
|||||||
Reference in New Issue
Block a user