Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6347d94dfb | ||
|
|
a32c102d62 |
@@ -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
|
||||
|
||||
27
README.md
27
README.md
@@ -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|
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.39.4"
|
||||
__version__ = "0.39.5"
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user