Compare commits

...

2 Commits

Author SHA1 Message Date
Rhet Turnbull
6347d94dfb Updated CHANGELOG.md 2021-01-03 08:47:16 -08:00
Rhet Turnbull
a32c102d62 Updated CHANGELOG.md 2021-01-03 08:46:36 -08:00
9 changed files with 129 additions and 16 deletions

View File

@@ -4,6 +4,12 @@ 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.39.5](https://github.com/RhetTbull/osxphotos/compare/v0.39.3...v0.39.5)
> 3 January 2021
- Implemented text replacement for templates, issue #316 [`478715a`](https://github.com/RhetTbull/osxphotos/commit/478715a363f5009e4a38148e832bf0ad3c4cc4f8)
#### [v0.39.3](https://github.com/RhetTbull/osxphotos/compare/v0.39.2...v0.39.3)
> 31 December 2020

View File

@@ -446,6 +446,13 @@ Options:
do not include an extension in the FILENAME
template. See below for additional details
on templating system.
--strip Optionally strip leading and trailing
whitespace from any rendered templates. For
example, if --filename template is "{title,}
{original_name}" and image has no title,
resulting file would have a leading space
but if used with --strip, this will be
removed.
--edited-suffix SUFFIX Optional suffix template for naming edited
photos. Default name for edited photos is
in form 'photoname_edited.ext'. For example,
@@ -885,6 +892,19 @@ Substitution Description
e.g. 'Summer'; (Photos 5+ only, applied
automatically by Photos' image
categorization algorithms).
{exif.camera_make} Camera make from original photo's EXIF
inormation as imported by Photos, e.g.
'Apple'
{exif.camera_model} Camera model from original photo's EXIF
inormation as imported by Photos, e.g.
'iPhone 6s'
{exif.lens_model} Lens model from original photo's EXIF
inormation as imported by Photos, e.g.
'iPhone 6s back camera 4.15mm f/2.2'
{uuid} Photo's internal universally unique
identifier (UUID) for the photo, a
36-character string unique to the photo,
e.g. '128FB4C6-0B16-4E7D-9108-FB2E90DA1546'
The following substitutions may result in multiple values. Thus if specified
for --directory these could result in multiple copies of a photo being being
@@ -1778,7 +1798,7 @@ If overwrite=False and increment=False, export will fail if destination file alr
#### <a name="rendertemplate">`render_template()`</a>
`render_template(template_str, none_str = "_", path_sep = None, expand_inplace = False, inplace_sep = None, filename=False, dirname=False)`
`render_template(template_str, none_str = "_", path_sep = None, expand_inplace = False, inplace_sep = None, filename=False, dirname=False, strip=False)`
Render template string for photo. none_str is used if template substitution results in None value and no default specified.
@@ -1789,6 +1809,7 @@ Render template string for photo. none_str is used if template substitution res
- `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
- `strip`: if True, leading/trailign whitespace will be stripped from rendered template strings
Returns a tuple of (rendered, unmatched) where rendered is a list of rendered strings with all substitutions made and unmatched is a list of any strings that resembled a template substitution but did not match a known substitution. E.g. if template contained "{foo}", unmatched would be ["foo"].
@@ -2401,6 +2422,10 @@ The following template field substitutions are availabe for use with `PhotoInfo.
|{place.address.country}|Country name of the postal address, e.g. 'United States'|
|{place.address.country_code}|ISO country code of the postal address, e.g. 'US'|
|{searchinfo.season}|Season of the year associated with a photo, e.g. 'Summer'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|{exif.camera_make}|Camera make from original photo's EXIF inormation as imported by Photos, e.g. 'Apple'|
|{exif.camera_model}|Camera model from original photo's EXIF inormation as imported by Photos, e.g. 'iPhone 6s'|
|{exif.lens_model}|Lens model from original photo's EXIF inormation as imported by Photos, e.g. 'iPhone 6s back camera 4.15mm f/2.2'|
|{uuid}|Photo's internal universally unique identifier (UUID) for the photo, a 36-character string unique to the photo, e.g. '128FB4C6-0B16-4E7D-9108-FB2E90DA1546'|
|{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|

View File

@@ -1586,6 +1586,14 @@ def query(
"File extension will be added automatically--do not include an extension in the FILENAME template. "
"See below for additional details on templating system.",
)
@click.option(
"--strip",
is_flag=True,
help="Optionally strip leading and trailing whitespace from any rendered templates. "
'For example, if --filename template is "{title,} {original_name}" and image has no '
"title, resulting file would have a leading space but if used with --strip, this will "
"be removed.",
)
@click.option(
"--edited-suffix",
metavar="SUFFIX",
@@ -1749,6 +1757,7 @@ def export(
has_raw,
directory,
filename_template,
strip,
edited_suffix,
original_suffix,
place,
@@ -1887,6 +1896,7 @@ def export(
has_raw = cfg.has_raw
directory = cfg.directory
filename_template = cfg.filename_template
strip = cfg.strip
edited_suffix = cfg.edited_suffix
original_suffix = cfg.original_suffix
place = cfg.place
@@ -2252,6 +2262,7 @@ def export(
ignore_date_modified=ignore_date_modified,
use_photokit=use_photokit,
exiftool_option=exiftool_option,
strip=strip,
)
results += export_results
@@ -2276,13 +2287,14 @@ def export(
person_keyword=person_keyword,
exiftool_merge_keywords=exiftool_merge_keywords,
finder_tag_template=finder_tag_template,
strip=strip,
)
results.xattr_written.extend(tags_written)
results.xattr_skipped.extend(tags_skipped)
if xattr_template:
xattr_written, xattr_skipped = write_extended_attributes(
p, photo_files, xattr_template
p, photo_files, xattr_template, strip=strip
)
results.xattr_written.extend(xattr_written)
results.xattr_skipped.extend(xattr_skipped)
@@ -2822,6 +2834,7 @@ def export_photo(
ignore_date_modified=False,
use_photokit=False,
exiftool_option=None,
strip=False,
):
"""Helper function for export that does the actual export
@@ -2912,12 +2925,14 @@ def export_photo(
if photo.hasadjustments and photo.path_edited is None:
missing_edited = True
filenames = get_filenames_from_template(photo, filename_template, original_name)
filenames = get_filenames_from_template(
photo, filename_template, original_name, strip=strip
)
for filename in filenames:
if original_suffix:
try:
rendered_suffix, unmatched = photo.render_template(
original_suffix, filename=True
original_suffix, filename=True, strip=strip
)
except ValueError:
raise click.BadOptionUsage(
@@ -2950,7 +2965,7 @@ def export_photo(
)
dest_paths = get_dirnames_from_template(
photo, directory, export_by_date, dest, dry_run
photo, directory, export_by_date, dest, dry_run, strip=strip
)
sidecar = [s.lower() for s in sidecar]
@@ -3062,7 +3077,7 @@ def export_photo(
if edited_suffix:
try:
rendered_suffix, unmatched = photo.render_template(
edited_suffix, filename=True
edited_suffix, filename=True, strip=strip
)
except ValueError:
raise click.BadOptionUsage(
@@ -3167,7 +3182,7 @@ def export_photo(
return results
def get_filenames_from_template(photo, filename_template, original_name):
def get_filenames_from_template(photo, filename_template, original_name, strip=False):
"""get list of export filenames for a photo
Args:
@@ -3185,7 +3200,7 @@ def get_filenames_from_template(photo, filename_template, original_name):
photo_ext = pathlib.Path(photo.original_filename).suffix
try:
filenames, unmatched = photo.render_template(
filename_template, path_sep="_", filename=True
filename_template, path_sep="_", filename=True, strip=strip
)
except ValueError:
raise click.BadOptionUsage(
@@ -3208,7 +3223,9 @@ def get_filenames_from_template(photo, filename_template, original_name):
return filenames
def get_dirnames_from_template(photo, directory, export_by_date, dest, dry_run):
def get_dirnames_from_template(
photo, directory, export_by_date, dest, dry_run, strip=False
):
"""get list of directories to export a photo into, creates directories if they don't exist
Args:
@@ -3236,7 +3253,9 @@ def get_dirnames_from_template(photo, directory, export_by_date, dest, dry_run):
elif directory:
# got a directory template, render it and check results are valid
try:
dirnames, unmatched = photo.render_template(directory, dirname=True)
dirnames, unmatched = photo.render_template(
directory, dirname=True, strip=strip
)
except ValueError:
raise click.BadOptionUsage("directory", f"Invalid template '{directory}'")
if not dirnames or unmatched:
@@ -3498,6 +3517,7 @@ def write_finder_tags(
person_keyword=None,
exiftool_merge_keywords=None,
finder_tag_template=None,
strip=False,
):
"""Write Finder tags (extended attributes) to files; only writes attributes if attributes on file differ from what would be written
@@ -3537,7 +3557,10 @@ def write_finder_tags(
for template_str in finder_tag_template:
try:
rendered, unmatched = photo.render_template(
template_str, none_str=_OSXPHOTOS_NONE_SENTINEL, path_sep="/"
template_str,
none_str=_OSXPHOTOS_NONE_SENTINEL,
path_sep="/",
strip=strip,
)
except ValueError:
raise click.BadOptionUsage(
@@ -3575,7 +3598,7 @@ def write_finder_tags(
return (written, skipped)
def write_extended_attributes(photo, files, xattr_template):
def write_extended_attributes(photo, files, xattr_template, strip=False):
""" Writes extended attributes to exported files
Args:
@@ -3590,7 +3613,10 @@ def write_extended_attributes(photo, files, xattr_template):
for xattr, template_str in xattr_template:
try:
rendered, unmatched = photo.render_template(
template_str, none_str=_OSXPHOTOS_NONE_SENTINEL, path_sep="/"
template_str,
none_str=_OSXPHOTOS_NONE_SENTINEL,
path_sep="/",
strip=strip,
)
except ValueError:
raise click.BadOptionUsage(

View File

@@ -1,5 +1,5 @@
""" version info """
__version__ = "0.39.4"
__version__ = "0.39.5"

View File

@@ -832,6 +832,7 @@ class PhotoInfo:
inplace_sep=None,
filename=False,
dirname=False,
strip=False,
):
"""Renders a template string for PhotoInfo instance using PhotoTemplate
@@ -846,6 +847,7 @@ class PhotoInfo:
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
strip: if True, strips leading/trailing white space from resulting template
Returns:
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values
@@ -859,6 +861,7 @@ class PhotoInfo:
inplace_sep=inplace_sep,
filename=filename,
dirname=dirname,
strip=strip,
)
@property

View File

@@ -118,6 +118,10 @@ TEMPLATE_SUBSTITUTIONS = {
"{place.address.country}": "Country name of the postal address, e.g. 'United States'",
"{place.address.country_code}": "ISO country code of the postal address, e.g. 'US'",
"{searchinfo.season}": "Season of the year associated with a photo, e.g. 'Summer'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).",
"{exif.camera_make}": "Camera make from original photo's EXIF inormation as imported by Photos, e.g. 'Apple'",
"{exif.camera_model}": "Camera model from original photo's EXIF inormation as imported by Photos, e.g. 'iPhone 6s'",
"{exif.lens_model}": "Lens model from original photo's EXIF inormation as imported by Photos, e.g. 'iPhone 6s back camera 4.15mm f/2.2'",
"{uuid}": "Photo's internal universally unique identifier (UUID) for the photo, a 36-character string unique to the photo, e.g. '128FB4C6-0B16-4E7D-9108-FB2E90DA1546'",
}
# Permitted multi-value substitutions (each of these returns None or 1 or more values)
@@ -251,6 +255,7 @@ class PhotoTemplate:
inplace_sep=None,
filename=False,
dirname=False,
strip=False,
):
""" Render a filename or directory template
@@ -264,6 +269,7 @@ class PhotoTemplate:
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
strip: if True, strips leading/trailing whitespace from rendered templates
Returns:
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values
@@ -364,6 +370,11 @@ class PhotoTemplate:
sanitize_filename(rendered_str) for rendered_str in rendered_strings
]
if strip:
rendered_strings = [
rendered_str.strip() for rendered_str in rendered_strings
]
return rendered_strings, unmatched
def _render_multi_valued_templates(
@@ -890,6 +901,14 @@ class PhotoTemplate:
)
elif field == "searchinfo.season":
value = self.photo.search_info.season if self.photo.search_info else None
elif field == "exif.camera_make":
value = self.photo.exif_info.camera_make if self.photo.exif_info else None
elif field == "exif.camera_model":
value = self.photo.exif_info.camera_model if self.photo.exif_info else None
elif field == "exif.lens_model":
value = self.photo.exif_info.lens_model if self.photo.exif_info else None
elif field == "uuid":
value = self.photo.uuid
else:
# if here, didn't get a match
raise ValueError(f"Unhandled template value: {field}")

File diff suppressed because one or more lines are too long

View File

@@ -2880,7 +2880,6 @@ def test_export_filename_template_1():
],
)
assert result.exit_code == 0
workdir = os.getcwd()
files = glob.glob("*.*")
assert sorted(files) == sorted(CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES1)
@@ -2915,6 +2914,37 @@ def test_export_filename_template_2():
assert sorted(files) == sorted(CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES2)
def test_export_filename_template_strip():
""" export photos using filename template with --strip """
import glob
import locale
import os
import os.path
import osxphotos
from osxphotos.__main__ import export
locale.setlocale(locale.LC_ALL, "en_US")
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),
".",
"-V",
"--filename",
"{searchinfo.venue,} {created.year}-{original_name}",
"--strip",
],
)
assert result.exit_code == 0
files = glob.glob("*.*")
assert sorted(files) == sorted(CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES1)
def test_export_filename_template_pathsep_in_name_1():
""" export photos using filename template with folder_album and "/" in album name """
import locale

View File

@@ -136,6 +136,10 @@ TEMPLATE_VALUES = {
"{place.address.postal_code}": "20009",
"{place.address.country}": "United States",
"{place.address.country_code}": "US",
"{uuid}": "128FB4C6-0B16-4E7D-9108-FB2E90DA1546",
"{exif.camera_make}": "Apple",
"{exif.camera_model}": "iPhone 6s",
"{exif.lens_model}": "iPhone 6s back camera 4.15mm f/2.2",
}