refactored template code to fix #213
This commit is contained in:
@@ -1,9 +1,7 @@
|
||||
""" command line interface for osxphotos """
|
||||
import csv
|
||||
import datetime
|
||||
import functools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import os.path
|
||||
import pathlib
|
||||
@@ -14,12 +12,6 @@ import unicodedata
|
||||
|
||||
import click
|
||||
import yaml
|
||||
from pathvalidate import (
|
||||
is_valid_filename,
|
||||
is_valid_filepath,
|
||||
sanitize_filename,
|
||||
sanitize_filepath,
|
||||
)
|
||||
|
||||
import osxphotos
|
||||
|
||||
@@ -29,11 +21,12 @@ from ._constants import (
|
||||
_UNKNOWN_PLACE,
|
||||
UNICODE_FORMAT,
|
||||
)
|
||||
from .export_db import ExportDB, ExportDBInMemory
|
||||
from ._version import __version__
|
||||
from .datetime_formatter import DateTimeFormatter
|
||||
from .exiftool import get_exiftool_path
|
||||
from .export_db import ExportDB, ExportDBInMemory
|
||||
from .fileutil import FileUtil, FileUtilNoOp
|
||||
from .path_utils import is_valid_filepath, sanitize_filename, sanitize_filepath
|
||||
from .photoinfo import ExportResults
|
||||
from .phototemplate import TEMPLATE_SUBSTITUTIONS, TEMPLATE_SUBSTITUTIONS_MULTI_VALUED
|
||||
|
||||
@@ -1240,10 +1233,10 @@ def query(
|
||||
"--jpeg-quality",
|
||||
type=click.FloatRange(0.0, 1.0),
|
||||
default=1.0,
|
||||
help="Value in range 0.0 to 1.0 to use with --convert-to-jpeg. "
|
||||
help="Value in range 0.0 to 1.0 to use with --convert-to-jpeg. "
|
||||
"A value of 1.0 specifies best quality, "
|
||||
"a value of 0.0 specifies maximum compression. "
|
||||
"Defaults to 1.0."
|
||||
"Defaults to 1.0.",
|
||||
)
|
||||
@click.option(
|
||||
"--sidecar",
|
||||
@@ -2409,7 +2402,9 @@ def get_filenames_from_template(photo, filename_template, original_name):
|
||||
"""
|
||||
if filename_template:
|
||||
photo_ext = pathlib.Path(photo.original_filename).suffix
|
||||
filenames, unmatched = photo.render_template(filename_template, path_sep="_")
|
||||
filenames, unmatched = photo.render_template(
|
||||
filename_template, path_sep="_", filename=True
|
||||
)
|
||||
if not filenames or unmatched:
|
||||
raise click.BadOptionUsage(
|
||||
"filename_template",
|
||||
@@ -2418,6 +2413,8 @@ def get_filenames_from_template(photo, filename_template, original_name):
|
||||
filenames = [f"{file_}{photo_ext}" for file_ in filenames]
|
||||
else:
|
||||
filenames = [photo.original_filename] if original_name else [photo.filename]
|
||||
|
||||
filenames = [sanitize_filename(filename) for filename in filenames]
|
||||
return filenames
|
||||
|
||||
|
||||
@@ -2448,22 +2445,18 @@ def get_dirnames_from_template(photo, directory, export_by_date, dest, dry_run):
|
||||
dest_paths = [dest_path]
|
||||
elif directory:
|
||||
# got a directory template, render it and check results are valid
|
||||
dirnames, unmatched = photo.render_template(directory)
|
||||
if not dirnames:
|
||||
raise click.BadOptionUsage(
|
||||
"directory",
|
||||
f"Invalid template '{directory}': results={dirnames} unmatched={unmatched}",
|
||||
)
|
||||
elif unmatched:
|
||||
dirnames, unmatched = photo.render_template(directory, dirname=True)
|
||||
if not dirnames or unmatched:
|
||||
raise click.BadOptionUsage(
|
||||
"directory",
|
||||
f"Invalid template '{directory}': results={dirnames} unmatched={unmatched}",
|
||||
)
|
||||
|
||||
dest_paths = []
|
||||
for dirname in dirnames:
|
||||
dirname = sanitize_filepath(dirname, platform="auto")
|
||||
dirname = sanitize_filepath(dirname)
|
||||
dest_path = os.path.join(dest, dirname)
|
||||
if not is_valid_filepath(dest_path, platform="auto"):
|
||||
if not is_valid_filepath(dest_path):
|
||||
raise ValueError(f"Invalid file path: '{dest_path}'")
|
||||
if not dry_run and not os.path.isdir(dest_path):
|
||||
os.makedirs(dest_path)
|
||||
@@ -2491,7 +2484,7 @@ def find_files_in_branch(pathname, filename):
|
||||
files = []
|
||||
|
||||
# walk down the tree
|
||||
for root, directories, filenames in os.walk(pathname):
|
||||
for root, _, filenames in os.walk(pathname):
|
||||
# for directory in directories:
|
||||
# print(os.path.join(root, directory))
|
||||
for fname in filenames:
|
||||
|
||||
@@ -102,3 +102,10 @@ _OSXPHOTOS_NONE_SENTINEL = "OSXPhotosXYZZY42_Sentinel$"
|
||||
|
||||
# SearchInfo categories for Photos 5, corresponds to categories in database/search/psi.sqlite
|
||||
SEARCH_CATEGORY_LABEL = 2024
|
||||
|
||||
# Max filename length on MacOS
|
||||
MAX_FILENAME_LEN = 255
|
||||
|
||||
# Max directory name length on MacOS
|
||||
MAX_DIRNAME_LEN = 255
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.35.3"
|
||||
__version__ = "0.35.4"
|
||||
|
||||
|
||||
78
osxphotos/path_utils.py
Normal file
78
osxphotos/path_utils.py
Normal file
@@ -0,0 +1,78 @@
|
||||
""" utility functions for validating/sanitizing path components """
|
||||
|
||||
from ._constants import MAX_DIRNAME_LEN, MAX_FILENAME_LEN
|
||||
import pathvalidate
|
||||
|
||||
|
||||
def sanitize_filepath(filepath):
|
||||
""" sanitize a filepath """
|
||||
return pathvalidate.sanitize_filepath(filepath, platform="macos")
|
||||
|
||||
|
||||
def is_valid_filepath(filepath):
|
||||
""" returns True if a filepath is valid otherwise False """
|
||||
return pathvalidate.is_valid_filepath(filepath, platform="macos")
|
||||
|
||||
|
||||
def sanitize_filename(filename, replacement=":"):
|
||||
""" replace any illegal characters in a filename and truncate filename if needed
|
||||
|
||||
Args:
|
||||
filename: str, filename to sanitze
|
||||
replacement: str, value to replace any illegal characters with; default = ":"
|
||||
|
||||
Returns:
|
||||
filename with any illegal characters replaced by replacement and truncated if necessary
|
||||
"""
|
||||
|
||||
if filename:
|
||||
filename = filename.replace("/", replacement)
|
||||
if len(filename) > MAX_FILENAME_LEN:
|
||||
parts = filename.split(".")
|
||||
drop = len(filename) - MAX_FILENAME_LEN
|
||||
if len(parts) > 1:
|
||||
# has an extension
|
||||
ext = parts.pop(-1)
|
||||
stem = ".".join(parts)
|
||||
if drop > len(stem):
|
||||
ext = ext[:-drop]
|
||||
else:
|
||||
stem = stem[:-drop]
|
||||
filename = f"{stem}.{ext}"
|
||||
else:
|
||||
filename = filename[:-drop]
|
||||
return filename
|
||||
|
||||
|
||||
def sanitize_dirname(dirname, replacement=":"):
|
||||
""" replace any illegal characters in a directory name and truncate directory name if needed
|
||||
|
||||
Args:
|
||||
dirname: str, directory name to sanitze
|
||||
replacement: str, value to replace any illegal characters with; default = ":"
|
||||
|
||||
Returns:
|
||||
dirname with any illegal characters replaced by replacement and truncated if necessary
|
||||
"""
|
||||
if dirname:
|
||||
dirname = sanitize_pathpart(dirname, replacement=replacement)
|
||||
return dirname
|
||||
|
||||
|
||||
def sanitize_pathpart(pathpart, replacement=":"):
|
||||
""" replace any illegal characters in a path part (either directory or filename without extension) and truncate name if needed
|
||||
|
||||
Args:
|
||||
pathpart: str, path part to sanitze
|
||||
replacement: str, value to replace any illegal characters with; default = ":"
|
||||
|
||||
Returns:
|
||||
pathpart with any illegal characters replaced by replacement and truncated if necessary
|
||||
"""
|
||||
if pathpart:
|
||||
pathpart = pathpart.replace("/", replacement)
|
||||
if len(pathpart) > MAX_DIRNAME_LEN:
|
||||
drop = len(pathpart) - MAX_DIRNAME_LEN
|
||||
pathpart = pathpart[:-drop]
|
||||
return pathpart
|
||||
|
||||
@@ -808,6 +808,9 @@ class PhotoInfo:
|
||||
path_sep=None,
|
||||
expand_inplace=False,
|
||||
inplace_sep=None,
|
||||
filename=False,
|
||||
dirname=False,
|
||||
replacement=":",
|
||||
):
|
||||
"""Renders a template string for PhotoInfo instance using PhotoTemplate
|
||||
|
||||
@@ -820,6 +823,9 @@ class PhotoInfo:
|
||||
instead of returning individual strings
|
||||
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
|
||||
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 = ":"
|
||||
|
||||
Returns:
|
||||
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values
|
||||
@@ -831,6 +837,9 @@ class PhotoInfo:
|
||||
path_sep=path_sep,
|
||||
expand_inplace=expand_inplace,
|
||||
inplace_sep=inplace_sep,
|
||||
filename=filename,
|
||||
dirname=dirname,
|
||||
replacement=replacement
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -12,11 +12,13 @@
|
||||
import datetime
|
||||
import locale
|
||||
import os
|
||||
import re
|
||||
import pathlib
|
||||
import re
|
||||
from functools import partial
|
||||
|
||||
from ._constants import _UNKNOWN_PERSON
|
||||
from .datetime_formatter import DateTimeFormatter
|
||||
from .path_utils import sanitize_dirname, sanitize_filename, sanitize_pathpart
|
||||
|
||||
# ensure locale set to user's locale
|
||||
locale.setlocale(locale.LC_ALL, "")
|
||||
@@ -131,6 +133,9 @@ class PhotoTemplate:
|
||||
path_sep=None,
|
||||
expand_inplace=False,
|
||||
inplace_sep=None,
|
||||
filename=False,
|
||||
dirname=False,
|
||||
replacement=":",
|
||||
):
|
||||
""" Render a filename or directory template
|
||||
|
||||
@@ -142,6 +147,9 @@ class PhotoTemplate:
|
||||
instead of returning individual strings
|
||||
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
|
||||
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 = ":"
|
||||
|
||||
Returns:
|
||||
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values
|
||||
@@ -170,13 +178,21 @@ class PhotoTemplate:
|
||||
if type(template) is not str:
|
||||
raise TypeError(f"template must be type str, not {type(template)}")
|
||||
|
||||
def make_subst_function(self, none_str, get_func=self.get_template_value):
|
||||
# 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 in subst
|
||||
# closure to capture photo, none_str, filename, dirname in subst
|
||||
def subst(matchobj):
|
||||
groups = len(matchobj.groups())
|
||||
if groups == 4:
|
||||
@@ -186,13 +202,13 @@ class PhotoTemplate:
|
||||
return matchobj.group(0)
|
||||
|
||||
if val is None:
|
||||
return (
|
||||
val = (
|
||||
matchobj.group(3)
|
||||
if matchobj.group(3) is not None
|
||||
else none_str
|
||||
)
|
||||
else:
|
||||
return val
|
||||
|
||||
return val
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unexpected number of groups: expected 4, got {groups}"
|
||||
@@ -239,7 +255,13 @@ class PhotoTemplate:
|
||||
|
||||
for str_template in rendered_strings:
|
||||
if regex_multi.search(str_template):
|
||||
values = self.get_template_value_multi(field, path_sep)
|
||||
values = self.get_template_value_multi(
|
||||
field,
|
||||
path_sep,
|
||||
filename=filename,
|
||||
dirname=dirname,
|
||||
replacement=replacement,
|
||||
)
|
||||
if expand_inplace:
|
||||
# instead of returning multiple strings, join values into a single string
|
||||
val = (
|
||||
@@ -248,11 +270,11 @@ class PhotoTemplate:
|
||||
else None
|
||||
)
|
||||
|
||||
def lookup_template_value_multi(lookup_value, default):
|
||||
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
|
||||
default is not used but required so signature matches get_template_value """
|
||||
_ is not used but required so signature matches get_template_value """
|
||||
if lookup_value == field:
|
||||
return val
|
||||
else:
|
||||
@@ -269,11 +291,11 @@ class PhotoTemplate:
|
||||
# create a new template string for each value
|
||||
for val in values:
|
||||
|
||||
def lookup_template_value_multi(lookup_value, default):
|
||||
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
|
||||
default is not used but required so signature matches get_template_value """
|
||||
_ is not used but required so signature matches get_template_value """
|
||||
if lookup_value == field:
|
||||
return val
|
||||
else:
|
||||
@@ -307,14 +329,24 @@ class PhotoTemplate:
|
||||
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 get_template_value(self, field, default):
|
||||
def get_template_value(
|
||||
self, field, default, filename=False, dirname=False, replacement=":"
|
||||
):
|
||||
"""lookup value for template field (single-value template substitutions)
|
||||
|
||||
Args:
|
||||
field: template field to find value for.
|
||||
default: the default value provided by the user
|
||||
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
|
||||
replacement: str, value to replace any illegal file path characters with; default = ":"
|
||||
|
||||
Returns:
|
||||
The matching template value (which may be None).
|
||||
@@ -327,289 +359,236 @@ class PhotoTemplate:
|
||||
if self.today is None:
|
||||
self.today = datetime.datetime.now()
|
||||
|
||||
# must be a valid keyword
|
||||
value = None
|
||||
|
||||
# wouldn't a switch/case statement be nice...
|
||||
if field == "name":
|
||||
return pathlib.Path(self.photo.filename).stem
|
||||
|
||||
if field == "original_name":
|
||||
return pathlib.Path(self.photo.original_filename).stem
|
||||
|
||||
if field == "title":
|
||||
return self.photo.title
|
||||
|
||||
if field == "descr":
|
||||
return self.photo.description
|
||||
|
||||
if field == "created.date":
|
||||
return DateTimeFormatter(self.photo.date).date
|
||||
|
||||
if field == "created.year":
|
||||
return DateTimeFormatter(self.photo.date).year
|
||||
|
||||
if field == "created.yy":
|
||||
return DateTimeFormatter(self.photo.date).yy
|
||||
|
||||
if field == "created.mm":
|
||||
return DateTimeFormatter(self.photo.date).mm
|
||||
|
||||
if field == "created.month":
|
||||
return DateTimeFormatter(self.photo.date).month
|
||||
|
||||
if field == "created.mon":
|
||||
return DateTimeFormatter(self.photo.date).mon
|
||||
|
||||
if field == "created.dd":
|
||||
return DateTimeFormatter(self.photo.date).dd
|
||||
|
||||
if field == "created.dow":
|
||||
return DateTimeFormatter(self.photo.date).dow
|
||||
|
||||
if field == "created.doy":
|
||||
return DateTimeFormatter(self.photo.date).doy
|
||||
|
||||
if field == "created.hour":
|
||||
return DateTimeFormatter(self.photo.date).hour
|
||||
|
||||
if field == "created.min":
|
||||
return DateTimeFormatter(self.photo.date).min
|
||||
|
||||
if field == "created.sec":
|
||||
return DateTimeFormatter(self.photo.date).sec
|
||||
|
||||
if field == "created.strftime":
|
||||
value = pathlib.Path(self.photo.filename).stem
|
||||
elif field == "original_name":
|
||||
value = pathlib.Path(self.photo.original_filename).stem
|
||||
elif field == "title":
|
||||
value = self.photo.title
|
||||
elif field == "descr":
|
||||
value = self.photo.description
|
||||
elif field == "created.date":
|
||||
value = DateTimeFormatter(self.photo.date).date
|
||||
elif field == "created.year":
|
||||
value = DateTimeFormatter(self.photo.date).year
|
||||
elif field == "created.yy":
|
||||
value = DateTimeFormatter(self.photo.date).yy
|
||||
elif field == "created.mm":
|
||||
value = DateTimeFormatter(self.photo.date).mm
|
||||
elif field == "created.month":
|
||||
value = DateTimeFormatter(self.photo.date).month
|
||||
elif field == "created.mon":
|
||||
value = DateTimeFormatter(self.photo.date).mon
|
||||
elif field == "created.dd":
|
||||
value = DateTimeFormatter(self.photo.date).dd
|
||||
elif field == "created.dow":
|
||||
value = DateTimeFormatter(self.photo.date).dow
|
||||
elif field == "created.doy":
|
||||
value = DateTimeFormatter(self.photo.date).doy
|
||||
elif field == "created.hour":
|
||||
value = DateTimeFormatter(self.photo.date).hour
|
||||
elif field == "created.min":
|
||||
value = DateTimeFormatter(self.photo.date).min
|
||||
elif field == "created.sec":
|
||||
value = DateTimeFormatter(self.photo.date).sec
|
||||
elif field == "created.strftime":
|
||||
if default:
|
||||
try:
|
||||
return self.photo.date.strftime(default)
|
||||
value = self.photo.date.strftime(default)
|
||||
except:
|
||||
raise ValueError(f"Invalid strftime template: '{default}'")
|
||||
else:
|
||||
return None
|
||||
|
||||
if field == "modified.date":
|
||||
return (
|
||||
value = None
|
||||
elif field == "modified.date":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).date
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
)
|
||||
|
||||
if field == "modified.year":
|
||||
return (
|
||||
elif field == "modified.year":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).year
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
)
|
||||
|
||||
if field == "modified.yy":
|
||||
return (
|
||||
elif field == "modified.yy":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).yy
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
)
|
||||
|
||||
if field == "modified.mm":
|
||||
return (
|
||||
elif field == "modified.mm":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).mm
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
)
|
||||
|
||||
if field == "modified.month":
|
||||
return (
|
||||
elif field == "modified.month":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).month
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
)
|
||||
|
||||
if field == "modified.mon":
|
||||
return (
|
||||
elif field == "modified.mon":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).mon
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
)
|
||||
|
||||
if field == "modified.dd":
|
||||
return (
|
||||
elif field == "modified.dd":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).dd
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
)
|
||||
|
||||
if field == "modified.doy":
|
||||
return (
|
||||
elif field == "modified.doy":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).doy
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
)
|
||||
|
||||
if field == "modified.hour":
|
||||
return (
|
||||
elif field == "modified.hour":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).hour
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
)
|
||||
|
||||
if field == "modified.min":
|
||||
return (
|
||||
elif field == "modified.min":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).min
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
)
|
||||
|
||||
if field == "modified.sec":
|
||||
return (
|
||||
elif field == "modified.sec":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).sec
|
||||
if self.photo.date_modified
|
||||
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 == "today.date":
|
||||
return DateTimeFormatter(self.today).date
|
||||
|
||||
if field == "today.year":
|
||||
return DateTimeFormatter(self.today).year
|
||||
|
||||
if field == "today.yy":
|
||||
return DateTimeFormatter(self.today).yy
|
||||
|
||||
if field == "today.mm":
|
||||
return DateTimeFormatter(self.today).mm
|
||||
|
||||
if field == "today.month":
|
||||
return DateTimeFormatter(self.today).month
|
||||
|
||||
if field == "today.mon":
|
||||
return DateTimeFormatter(self.today).mon
|
||||
|
||||
if field == "today.dd":
|
||||
return DateTimeFormatter(self.today).dd
|
||||
|
||||
if field == "today.dow":
|
||||
return DateTimeFormatter(self.today).dow
|
||||
|
||||
if field == "today.doy":
|
||||
return DateTimeFormatter(self.today).doy
|
||||
|
||||
if field == "today.hour":
|
||||
return DateTimeFormatter(self.today).hour
|
||||
|
||||
if field == "today.min":
|
||||
return DateTimeFormatter(self.today).min
|
||||
|
||||
if field == "today.sec":
|
||||
return DateTimeFormatter(self.today).sec
|
||||
|
||||
if field == "today.strftime":
|
||||
elif field == "today.date":
|
||||
value = DateTimeFormatter(self.today).date
|
||||
elif field == "today.year":
|
||||
value = DateTimeFormatter(self.today).year
|
||||
elif field == "today.yy":
|
||||
value = DateTimeFormatter(self.today).yy
|
||||
elif field == "today.mm":
|
||||
value = DateTimeFormatter(self.today).mm
|
||||
elif field == "today.month":
|
||||
value = DateTimeFormatter(self.today).month
|
||||
elif field == "today.mon":
|
||||
value = DateTimeFormatter(self.today).mon
|
||||
elif field == "today.dd":
|
||||
value = DateTimeFormatter(self.today).dd
|
||||
elif field == "today.dow":
|
||||
value = DateTimeFormatter(self.today).dow
|
||||
elif field == "today.doy":
|
||||
value = DateTimeFormatter(self.today).doy
|
||||
elif field == "today.hour":
|
||||
value = DateTimeFormatter(self.today).hour
|
||||
elif field == "today.min":
|
||||
value = DateTimeFormatter(self.today).min
|
||||
elif field == "today.sec":
|
||||
value = DateTimeFormatter(self.today).sec
|
||||
elif field == "today.strftime":
|
||||
if default:
|
||||
try:
|
||||
return self.today.strftime(default)
|
||||
value = self.today.strftime(default)
|
||||
except:
|
||||
raise ValueError(f"Invalid strftime template: '{default}'")
|
||||
else:
|
||||
return None
|
||||
|
||||
if field == "place.name":
|
||||
return self.photo.place.name if self.photo.place else None
|
||||
|
||||
if field == "place.country_code":
|
||||
return self.photo.place.country_code if self.photo.place else None
|
||||
|
||||
if field == "place.name.country":
|
||||
return (
|
||||
value = None
|
||||
elif field == "place.name":
|
||||
value = self.photo.place.name if self.photo.place else None
|
||||
elif field == "place.country_code":
|
||||
value = self.photo.place.country_code if self.photo.place else None
|
||||
elif field == "place.name.country":
|
||||
value = (
|
||||
self.photo.place.names.country[0]
|
||||
if self.photo.place and self.photo.place.names.country
|
||||
else None
|
||||
)
|
||||
|
||||
if field == "place.name.state_province":
|
||||
return (
|
||||
elif field == "place.name.state_province":
|
||||
value = (
|
||||
self.photo.place.names.state_province[0]
|
||||
if self.photo.place and self.photo.place.names.state_province
|
||||
else None
|
||||
)
|
||||
|
||||
if field == "place.name.city":
|
||||
return (
|
||||
elif field == "place.name.city":
|
||||
value = (
|
||||
self.photo.place.names.city[0]
|
||||
if self.photo.place and self.photo.place.names.city
|
||||
else None
|
||||
)
|
||||
|
||||
if field == "place.name.area_of_interest":
|
||||
return (
|
||||
elif field == "place.name.area_of_interest":
|
||||
value = (
|
||||
self.photo.place.names.area_of_interest[0]
|
||||
if self.photo.place and self.photo.place.names.area_of_interest
|
||||
else None
|
||||
)
|
||||
|
||||
if field == "place.address":
|
||||
return (
|
||||
elif field == "place.address":
|
||||
value = (
|
||||
self.photo.place.address_str
|
||||
if self.photo.place and self.photo.place.address_str
|
||||
else None
|
||||
)
|
||||
|
||||
if field == "place.address.street":
|
||||
return (
|
||||
elif field == "place.address.street":
|
||||
value = (
|
||||
self.photo.place.address.street
|
||||
if self.photo.place and self.photo.place.address.street
|
||||
else None
|
||||
)
|
||||
|
||||
if field == "place.address.city":
|
||||
return (
|
||||
elif field == "place.address.city":
|
||||
value = (
|
||||
self.photo.place.address.city
|
||||
if self.photo.place and self.photo.place.address.city
|
||||
else None
|
||||
)
|
||||
|
||||
if field == "place.address.state_province":
|
||||
return (
|
||||
elif field == "place.address.state_province":
|
||||
value = (
|
||||
self.photo.place.address.state_province
|
||||
if self.photo.place and self.photo.place.address.state_province
|
||||
else None
|
||||
)
|
||||
|
||||
if field == "place.address.postal_code":
|
||||
return (
|
||||
elif field == "place.address.postal_code":
|
||||
value = (
|
||||
self.photo.place.address.postal_code
|
||||
if self.photo.place and self.photo.place.address.postal_code
|
||||
else None
|
||||
)
|
||||
|
||||
if field == "place.address.country":
|
||||
return (
|
||||
elif field == "place.address.country":
|
||||
value = (
|
||||
self.photo.place.address.country
|
||||
if self.photo.place and self.photo.place.address.country
|
||||
else None
|
||||
)
|
||||
|
||||
if field == "place.address.country_code":
|
||||
return (
|
||||
elif field == "place.address.country_code":
|
||||
value = (
|
||||
self.photo.place.address.iso_country_code
|
||||
if self.photo.place and self.photo.place.address.iso_country_code
|
||||
else None
|
||||
)
|
||||
else:
|
||||
# if here, didn't get a match
|
||||
raise ValueError(f"Unhandled template value: {field}")
|
||||
|
||||
# if here, didn't get a match
|
||||
raise ValueError(f"Unhandled template value: {field}")
|
||||
if filename:
|
||||
value = sanitize_pathpart(value, replacement=replacement)
|
||||
elif dirname:
|
||||
value = sanitize_dirname(value, replacement=replacement)
|
||||
return value
|
||||
|
||||
def get_template_value_multi(self, field, path_sep):
|
||||
def get_template_value_multi(
|
||||
self, field, path_sep, filename=False, dirname=False, replacement=":"
|
||||
):
|
||||
"""lookup value for template field (multi-value template substitutions)
|
||||
|
||||
Args:
|
||||
field: template field to find value for.
|
||||
path_sep: path separator to use for folder_album field
|
||||
dirname: if True, values will be sanitized to be valid directory names; default = False
|
||||
|
||||
Returns:
|
||||
List of the matching template values or [None].
|
||||
@@ -621,9 +600,6 @@ class PhotoTemplate:
|
||||
""" return list of values for a multi-valued template field """
|
||||
if field == "album":
|
||||
values = self.photo.albums
|
||||
values = [
|
||||
value.replace("/", ":") for value in values
|
||||
] # TODO: temp fix for issue #213
|
||||
elif field == "keyword":
|
||||
values = self.photo.keywords
|
||||
elif field == "person":
|
||||
@@ -640,17 +616,42 @@ class PhotoTemplate:
|
||||
for album in self.photo.album_info:
|
||||
if album.folder_names:
|
||||
# album in folder
|
||||
folder = path_sep.join(album.folder_names)
|
||||
folder += path_sep + album.title.replace(
|
||||
"/", ":"
|
||||
) # TODO: temp fix for issue #213
|
||||
if dirname:
|
||||
# being used as a filepath so sanitize each part
|
||||
folder = path_sep.join(
|
||||
sanitize_dirname(f, replacement=replacement)
|
||||
for f in album.folder_names
|
||||
)
|
||||
folder += path_sep + sanitize_dirname(
|
||||
album.title, replacement=replacement
|
||||
)
|
||||
else:
|
||||
folder = path_sep.join(album.folder_names)
|
||||
folder += path_sep + album.title
|
||||
values.append(folder)
|
||||
else:
|
||||
# album not in folder
|
||||
values.append(album.title.replace("/", ":"))
|
||||
if dirname:
|
||||
values.append(
|
||||
sanitize_dirname(album.title, replacement=replacement)
|
||||
)
|
||||
else:
|
||||
values.append(album.title)
|
||||
else:
|
||||
raise ValueError(f"Unhandleded template value: {field}")
|
||||
raise ValueError(f"Unhandled template value: {field}")
|
||||
|
||||
# sanitize directory names if needed, folder_album handled differently above
|
||||
if filename:
|
||||
values = [
|
||||
sanitize_pathpart(value, replacement=replacement) for value in values
|
||||
]
|
||||
elif dirname and field != "folder_album":
|
||||
# skip folder_album because it would have been handled above
|
||||
values = [
|
||||
sanitize_dirname(value, replacement=replacement) for value in values
|
||||
]
|
||||
|
||||
# If no values, insert None so code below will substite none_str for None
|
||||
values = values or [None]
|
||||
return values
|
||||
|
||||
|
||||
Reference in New Issue
Block a user