Updated CHANGELOG.md

This commit is contained in:
Rhet Turnbull
2021-01-03 08:46:36 -08:00
parent 478715a363
commit a32c102d62
8 changed files with 123 additions and 16 deletions

View File

@@ -446,6 +446,13 @@ Options:
do not include an extension in the FILENAME do not include an extension in the FILENAME
template. See below for additional details template. See below for additional details
on templating system. 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 --edited-suffix SUFFIX Optional suffix template for naming edited
photos. Default name for edited photos is photos. Default name for edited photos is
in form 'photoname_edited.ext'. For example, in form 'photoname_edited.ext'. For example,
@@ -885,6 +892,19 @@ Substitution Description
e.g. 'Summer'; (Photos 5+ only, applied e.g. 'Summer'; (Photos 5+ only, applied
automatically by Photos' image automatically by Photos' image
categorization algorithms). 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 The following substitutions may result in multiple values. Thus if specified
for --directory these could result in multiple copies of a photo being being 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> #### <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. 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 ',' - `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 - `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 - `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"]. 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}|Country name of the postal address, e.g. 'United States'|
|{place.address.country_code}|ISO country code of the postal address, e.g. 'US'| |{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).| |{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| |{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| |{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| |{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. " "File extension will be added automatically--do not include an extension in the FILENAME template. "
"See below for additional details on templating system.", "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( @click.option(
"--edited-suffix", "--edited-suffix",
metavar="SUFFIX", metavar="SUFFIX",
@@ -1749,6 +1757,7 @@ def export(
has_raw, has_raw,
directory, directory,
filename_template, filename_template,
strip,
edited_suffix, edited_suffix,
original_suffix, original_suffix,
place, place,
@@ -1887,6 +1896,7 @@ def export(
has_raw = cfg.has_raw has_raw = cfg.has_raw
directory = cfg.directory directory = cfg.directory
filename_template = cfg.filename_template filename_template = cfg.filename_template
strip = cfg.strip
edited_suffix = cfg.edited_suffix edited_suffix = cfg.edited_suffix
original_suffix = cfg.original_suffix original_suffix = cfg.original_suffix
place = cfg.place place = cfg.place
@@ -2252,6 +2262,7 @@ def export(
ignore_date_modified=ignore_date_modified, ignore_date_modified=ignore_date_modified,
use_photokit=use_photokit, use_photokit=use_photokit,
exiftool_option=exiftool_option, exiftool_option=exiftool_option,
strip=strip,
) )
results += export_results results += export_results
@@ -2276,13 +2287,14 @@ def export(
person_keyword=person_keyword, person_keyword=person_keyword,
exiftool_merge_keywords=exiftool_merge_keywords, exiftool_merge_keywords=exiftool_merge_keywords,
finder_tag_template=finder_tag_template, finder_tag_template=finder_tag_template,
strip=strip,
) )
results.xattr_written.extend(tags_written) results.xattr_written.extend(tags_written)
results.xattr_skipped.extend(tags_skipped) results.xattr_skipped.extend(tags_skipped)
if xattr_template: if xattr_template:
xattr_written, xattr_skipped = write_extended_attributes( 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_written.extend(xattr_written)
results.xattr_skipped.extend(xattr_skipped) results.xattr_skipped.extend(xattr_skipped)
@@ -2822,6 +2834,7 @@ def export_photo(
ignore_date_modified=False, ignore_date_modified=False,
use_photokit=False, use_photokit=False,
exiftool_option=None, exiftool_option=None,
strip=False,
): ):
"""Helper function for export that does the actual export """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: if photo.hasadjustments and photo.path_edited is None:
missing_edited = True 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: for filename in filenames:
if original_suffix: if original_suffix:
try: try:
rendered_suffix, unmatched = photo.render_template( rendered_suffix, unmatched = photo.render_template(
original_suffix, filename=True original_suffix, filename=True, strip=strip
) )
except ValueError: except ValueError:
raise click.BadOptionUsage( raise click.BadOptionUsage(
@@ -2950,7 +2965,7 @@ def export_photo(
) )
dest_paths = get_dirnames_from_template( 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] sidecar = [s.lower() for s in sidecar]
@@ -3062,7 +3077,7 @@ def export_photo(
if edited_suffix: if edited_suffix:
try: try:
rendered_suffix, unmatched = photo.render_template( rendered_suffix, unmatched = photo.render_template(
edited_suffix, filename=True edited_suffix, filename=True, strip=strip
) )
except ValueError: except ValueError:
raise click.BadOptionUsage( raise click.BadOptionUsage(
@@ -3167,7 +3182,7 @@ def export_photo(
return results 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 """get list of export filenames for a photo
Args: Args:
@@ -3185,7 +3200,7 @@ def get_filenames_from_template(photo, filename_template, original_name):
photo_ext = pathlib.Path(photo.original_filename).suffix photo_ext = pathlib.Path(photo.original_filename).suffix
try: try:
filenames, unmatched = photo.render_template( filenames, unmatched = photo.render_template(
filename_template, path_sep="_", filename=True filename_template, path_sep="_", filename=True, strip=strip
) )
except ValueError: except ValueError:
raise click.BadOptionUsage( raise click.BadOptionUsage(
@@ -3208,7 +3223,9 @@ def get_filenames_from_template(photo, filename_template, original_name):
return filenames 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 """get list of directories to export a photo into, creates directories if they don't exist
Args: Args:
@@ -3236,7 +3253,9 @@ def get_dirnames_from_template(photo, directory, export_by_date, dest, dry_run):
elif directory: elif directory:
# got a directory template, render it and check results are valid # got a directory template, render it and check results are valid
try: try:
dirnames, unmatched = photo.render_template(directory, dirname=True) dirnames, unmatched = photo.render_template(
directory, dirname=True, strip=strip
)
except ValueError: except ValueError:
raise click.BadOptionUsage("directory", f"Invalid template '{directory}'") raise click.BadOptionUsage("directory", f"Invalid template '{directory}'")
if not dirnames or unmatched: if not dirnames or unmatched:
@@ -3498,6 +3517,7 @@ def write_finder_tags(
person_keyword=None, person_keyword=None,
exiftool_merge_keywords=None, exiftool_merge_keywords=None,
finder_tag_template=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 """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: for template_str in finder_tag_template:
try: try:
rendered, unmatched = photo.render_template( 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: except ValueError:
raise click.BadOptionUsage( raise click.BadOptionUsage(
@@ -3575,7 +3598,7 @@ def write_finder_tags(
return (written, skipped) 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 """ Writes extended attributes to exported files
Args: Args:
@@ -3590,7 +3613,10 @@ def write_extended_attributes(photo, files, xattr_template):
for xattr, template_str in xattr_template: for xattr, template_str in xattr_template:
try: try:
rendered, unmatched = photo.render_template( 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: except ValueError:
raise click.BadOptionUsage( raise click.BadOptionUsage(

View File

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

View File

@@ -832,6 +832,7 @@ class PhotoInfo:
inplace_sep=None, inplace_sep=None,
filename=False, filename=False,
dirname=False, dirname=False,
strip=False,
): ):
"""Renders a template string for PhotoInfo instance using PhotoTemplate """Renders a template string for PhotoInfo instance using PhotoTemplate
@@ -846,6 +847,7 @@ class PhotoInfo:
with expand_inplace; default is ',' with expand_inplace; default is ','
filename: if True, template output will be sanitized to produce valid file name 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 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: Returns:
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values ([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, inplace_sep=inplace_sep,
filename=filename, filename=filename,
dirname=dirname, dirname=dirname,
strip=strip,
) )
@property @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}": "Country name of the postal address, e.g. 'United States'",
"{place.address.country_code}": "ISO country code of the postal address, e.g. 'US'", "{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).", "{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) # Permitted multi-value substitutions (each of these returns None or 1 or more values)
@@ -251,6 +255,7 @@ class PhotoTemplate:
inplace_sep=None, inplace_sep=None,
filename=False, filename=False,
dirname=False, dirname=False,
strip=False,
): ):
""" Render a filename or directory template """ Render a filename or directory template
@@ -264,6 +269,7 @@ class PhotoTemplate:
with expand_inplace; default is ',' with expand_inplace; default is ','
filename: if True, template output will be sanitized to produce valid file name 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 dirname: if True, template output will be sanitized to produce valid directory name
strip: if True, strips leading/trailing whitespace from rendered templates
Returns: Returns:
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values ([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 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 return rendered_strings, unmatched
def _render_multi_valued_templates( def _render_multi_valued_templates(
@@ -890,6 +901,14 @@ class PhotoTemplate:
) )
elif field == "searchinfo.season": elif field == "searchinfo.season":
value = self.photo.search_info.season if self.photo.search_info else None 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: else:
# if here, didn't get a match # if here, didn't get a match
raise ValueError(f"Unhandled template value: {field}") 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 assert result.exit_code == 0
workdir = os.getcwd()
files = glob.glob("*.*") files = glob.glob("*.*")
assert sorted(files) == sorted(CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES1) 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) 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(): def test_export_filename_template_pathsep_in_name_1():
""" export photos using filename template with folder_album and "/" in album name """ """ export photos using filename template with folder_album and "/" in album name """
import locale import locale

View File

@@ -136,6 +136,10 @@ TEMPLATE_VALUES = {
"{place.address.postal_code}": "20009", "{place.address.postal_code}": "20009",
"{place.address.country}": "United States", "{place.address.country}": "United States",
"{place.address.country_code}": "US", "{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",
} }