Fix for issue #263

This commit is contained in:
Rhet Turnbull
2020-12-13 22:18:39 -08:00
parent 5c1c0c5c5a
commit d5730dd8ae
6 changed files with 161 additions and 32 deletions

View File

@@ -374,20 +374,24 @@ 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.
--edited-suffix SUFFIX Optional suffix for naming edited photos. --edited-suffix SUFFIX Optional suffix template for naming edited
Default name for edited photos is in form photos. Default name for edited photos is
'photoname_edited.ext'. For example, with ' in form 'photoname_edited.ext'. For example,
--edited-suffix _bearbeiten', the edited with '--edited-suffix _bearbeiten', the
photo would be named edited photo would be named
'photoname_bearbeiten.ext'. The default 'photoname_bearbeiten.ext'. The default
suffix is '_edited'. suffix is '_edited'. Multi-value templates
--original-suffix SUFFIX Optional suffix for naming original photos. (see Templating System) are not permitted
Default name for original photos is in form with --edited-suffix.
'filename.ext'. For example, with '-- --original-suffix SUFFIX Optional suffix template for naming original
original-suffix _original', the original photos. Default name for original photos is
in form 'filename.ext'. For example, with '
--original-suffix _original', the original
photo would be named photo would be named
'filename_original.ext'. The default suffix 'filename_original.ext'. The default suffix
is '' (no suffix). is '' (no suffix). Multi-value templates
(see Templating System) are not permitted
with --original-suffix.
--use-photos-export Force the use of AppleScript or PhotoKit to --use-photos-export Force the use of AppleScript or PhotoKit to
export even if not missing (see also '-- export even if not missing (see also '--
download-missing' and '--use-photokit'). download-missing' and '--use-photokit').
@@ -569,6 +573,9 @@ Substitution Description
'{photo_or_video,photo=fotos;video=videos}' '{photo_or_video,photo=fotos;video=videos}'
{hdr} Photo is HDR?; True/False value, use in {hdr} Photo is HDR?; True/False value, use in
format '{hdr?VALUE_IF_TRUE,VALUE_IF_FALSE}' format '{hdr?VALUE_IF_TRUE,VALUE_IF_FALSE}'
{edited} Photo has been edited (has adjustments)?;
True/False value, use in format
'{edited?VALUE_IF_TRUE,VALUE_IF_FALSE}'
{created.date} Photo's creation date in ISO format, e.g. {created.date} Photo's creation date in ISO format, e.g.
'2020-03-22' '2020-03-22'
{created.year} 4-digit year of photo creation time {created.year} 4-digit year of photo creation time
@@ -2041,6 +2048,7 @@ The following template field substitutions are availabe for use with `PhotoInfo.
|{media_type}|Special media type resolved in this precedence: selfie, time_lapse, panorama, slow_mo, screenshot, portrait, live_photo, burst, photo, video. Defaults to 'photo' or 'video' if no special type. Customize one or more media types using format: '{media_type,video=vidéo;time_lapse=vidéo_accélérée}'| |{media_type}|Special media type resolved in this precedence: selfie, time_lapse, panorama, slow_mo, screenshot, portrait, live_photo, burst, photo, video. Defaults to 'photo' or 'video' if no special type. Customize one or more media types using format: '{media_type,video=vidéo;time_lapse=vidéo_accélérée}'|
|{photo_or_video}|'photo' or 'video' depending on what type the image is. To customize, use default value as in '{photo_or_video,photo=fotos;video=videos}'| |{photo_or_video}|'photo' or 'video' depending on what type the image is. To customize, use default value as in '{photo_or_video,photo=fotos;video=videos}'|
|{hdr}|Photo is HDR?; True/False value, use in format '{hdr?VALUE_IF_TRUE,VALUE_IF_FALSE}'| |{hdr}|Photo is HDR?; True/False value, use in format '{hdr?VALUE_IF_TRUE,VALUE_IF_FALSE}'|
|{edited}|Photo has been edited (has adjustments)?; True/False value, use in format '{edited?VALUE_IF_TRUE,VALUE_IF_FALSE}'|
|{created.date}|Photo's creation date in ISO format, e.g. '2020-03-22'| |{created.date}|Photo's creation date in ISO format, e.g. '2020-03-22'|
|{created.year}|4-digit year of photo creation time| |{created.year}|4-digit year of photo creation time|
|{created.yy}|2-digit year of photo creation time| |{created.yy}|2-digit year of photo creation time|

View File

@@ -1402,16 +1402,18 @@ def query(
@click.option( @click.option(
"--edited-suffix", "--edited-suffix",
metavar="SUFFIX", metavar="SUFFIX",
help="Optional suffix for naming edited photos. Default name for edited photos is in form " help="Optional suffix template for naming edited photos. Default name for edited photos is in form "
"'photoname_edited.ext'. For example, with '--edited-suffix _bearbeiten', the edited photo " "'photoname_edited.ext'. For example, with '--edited-suffix _bearbeiten', the edited photo "
f"would be named 'photoname_bearbeiten.ext'. The default suffix is '{DEFAULT_EDITED_SUFFIX}'.", f"would be named 'photoname_bearbeiten.ext'. The default suffix is '{DEFAULT_EDITED_SUFFIX}'. "
"Multi-value templates (see Templating System) are not permitted with --edited-suffix.",
) )
@click.option( @click.option(
"--original-suffix", "--original-suffix",
metavar="SUFFIX", metavar="SUFFIX",
help="Optional suffix for naming original photos. Default name for original photos is in form " help="Optional suffix template for naming original photos. Default name for original photos is in form "
"'filename.ext'. For example, with '--original-suffix _original', the original photo " "'filename.ext'. For example, with '--original-suffix _original', the original photo "
"would be named 'filename_original.ext'. The default suffix is '' (no suffix).", "would be named 'filename_original.ext'. The default suffix is '' (no suffix). "
"Multi-value templates (see Templating System) are not permitted with --original-suffix.",
) )
@click.option( @click.option(
"--use-photos-export", "--use-photos-export",
@@ -1578,8 +1580,6 @@ def export(
ignore=["ctx", "cli_obj", "dest", "load_config", "save_config"], ignore=["ctx", "cli_obj", "dest", "load_config", "save_config"],
) )
# print(jpeg_quality, edited_suffix, original_suffix)
global VERBOSE global VERBOSE
VERBOSE = bool(verbose) VERBOSE = bool(verbose)
@@ -1716,11 +1716,7 @@ def export(
("jpeg_quality", ("convert_to_jpeg")), ("jpeg_quality", ("convert_to_jpeg")),
] ]
try: try:
cfg.validate( cfg.validate(exclusive=exclusive_options, dependent=dependent_options, cli=True)
exclusive=exclusive_options,
dependent=dependent_options,
cli=True,
)
except ConfigOptionsInvalidError as e: except ConfigOptionsInvalidError as e:
click.echo(f"Incompatible export options: {e.message}", err=True) click.echo(f"Incompatible export options: {e.message}", err=True)
raise click.Abort() raise click.Abort()
@@ -1736,8 +1732,6 @@ def export(
DEFAULT_ORIGINAL_SUFFIX if original_suffix is None else original_suffix DEFAULT_ORIGINAL_SUFFIX if original_suffix is None else original_suffix
) )
# print(jpeg_quality, edited_suffix, original_suffix)
if not os.path.isdir(dest): if not os.path.isdir(dest):
click.echo(f"DEST {dest} must be valid path", err=True) click.echo(f"DEST {dest} must be valid path", err=True)
raise click.Abort() raise click.Abort()
@@ -2693,10 +2687,25 @@ def export_photo(
filenames = get_filenames_from_template(photo, filename_template, original_name) filenames = get_filenames_from_template(photo, filename_template, original_name)
for filename in filenames: for filename in filenames:
if original_suffix: if original_suffix:
rendered_suffix, unmatched = photo.render_template(
original_suffix, filename=True
)
if not rendered_suffix or unmatched:
raise click.BadOptionUsage(
"original_suffix",
f"Invalid template for --original-suffix '{original_suffix}': results={rendered_suffix} unmatched={unmatched}",
)
if len(rendered_suffix) > 1:
raise click.BadOptionUsage(
"original_suffix",
f"Invalid template for --original-suffix: may not use multi-valued templates: '{original_suffix}': results={rendered_suffix}",
)
rendered_suffix = rendered_suffix[0]
original_filename = pathlib.Path(filename) original_filename = pathlib.Path(filename)
original_filename = ( original_filename = (
original_filename.parent original_filename.parent
/ f"{original_filename.stem}{original_suffix}{original_filename.suffix}" / f"{original_filename.stem}{rendered_suffix}{original_filename.suffix}"
) )
original_filename = str(original_filename) original_filename = str(original_filename)
else: else:
@@ -2813,7 +2822,30 @@ def export_photo(
# use filename suffix which might be wrong, # use filename suffix which might be wrong,
# will be corrected by use_photos_export # will be corrected by use_photos_export
edited_ext = pathlib.Path(photo.filename).suffix edited_ext = pathlib.Path(photo.filename).suffix
edited_filename = f"{edited_filename.stem}{edited_suffix}{edited_ext}"
if edited_suffix:
rendered_suffix, unmatched = photo.render_template(
edited_suffix, filename=True
)
if not rendered_suffix or unmatched:
raise click.BadOptionUsage(
"edited_suffix",
f"Invalid template for --edited-suffix '{edited_suffix}': results={rendered_suffix} unmatched={unmatched}",
)
if len(rendered_suffix) > 1:
raise click.BadOptionUsage(
"edited_suffix",
f"Invalid template for --edited-suffix: may not use multi-valued templates: '{edited_suffix}': results={rendered_suffix}",
)
rendered_suffix = rendered_suffix[0]
edited_filename = (
f"{edited_filename.stem}{rendered_suffix}{edited_ext}"
)
else:
edited_filename = f"{edited_filename.stem}{edited_ext}"
verbose_( verbose_(
f"Exporting edited version of {photo.original_filename} ({photo.filename}) as {edited_filename}" f"Exporting edited version of {photo.original_filename} ({photo.filename}) as {edited_filename}"
) )

View File

@@ -1,5 +1,5 @@
""" version info """ """ version info """
__version__ = "0.38.3" __version__ = "0.38.4"

View File

@@ -52,6 +52,7 @@ TEMPLATE_SUBSTITUTIONS = {
), ),
"{photo_or_video}": "'photo' or 'video' depending on what type the image is. To customize, use default value as in '{photo_or_video,photo=fotos;video=videos}'", "{photo_or_video}": "'photo' or 'video' depending on what type the image is. To customize, use default value as in '{photo_or_video,photo=fotos;video=videos}'",
"{hdr}": "Photo is HDR?; True/False value, use in format '{hdr?VALUE_IF_TRUE,VALUE_IF_FALSE}'", "{hdr}": "Photo is HDR?; True/False value, use in format '{hdr?VALUE_IF_TRUE,VALUE_IF_FALSE}'",
"{edited}": "Photo has been edited (has adjustments)?; True/False value, use in format '{edited?VALUE_IF_TRUE,VALUE_IF_FALSE}'",
"{created.date}": "Photo's creation date in ISO format, e.g. '2020-03-22'", "{created.date}": "Photo's creation date in ISO format, e.g. '2020-03-22'",
"{created.year}": "4-digit year of photo creation time", "{created.year}": "4-digit year of photo creation time",
"{created.yy}": "2-digit year of photo creation time", "{created.yy}": "2-digit year of photo creation time",
@@ -632,7 +633,9 @@ class PhotoTemplate:
elif field == "photo_or_video": elif field == "photo_or_video":
value = self.get_photo_video_type(default) value = self.get_photo_video_type(default)
elif field == "hdr": elif field == "hdr":
value = self.get_photo_hdr(default, bool_val) value = self.get_photo_bool_attribute("hdr", default, bool_val)
elif field == "edited":
value = self.get_photo_bool_attribute("hasadjustments", default, bool_val)
elif field == "created.date": elif field == "created.date":
value = DateTimeFormatter(self.photo.date).date value = DateTimeFormatter(self.photo.date).date
elif field == "created.year": elif field == "created.year":
@@ -962,8 +965,10 @@ class PhotoTemplate:
else: else:
return default_dict["photo"] return default_dict["photo"]
def get_photo_hdr(self, default, bool_val): def get_photo_bool_attribute(self, attr, default, bool_val):
if self.photo.hdr: # get value for a PhotoInfo bool attribute
val = getattr(self.photo, attr)
if val:
return bool_val return bool_val
else: else:
return default return default

View File

@@ -66,7 +66,9 @@ CLI_EXPORT_FILENAMES_ALBUM_UNICODE = ["IMG_4547.jpg"]
CLI_EXPORT_FILENAMES_DELETED_TWIN = ["wedding.jpg", "wedding_edited.jpeg"] CLI_EXPORT_FILENAMES_DELETED_TWIN = ["wedding.jpg", "wedding_edited.jpeg"]
CLI_EXPORT_EDITED_SUFFIX = "_bearbeiten" CLI_EXPORT_EDITED_SUFFIX = "_bearbeiten"
CLI_EXPORT_EDITED_SUFFIX_TEMPLATE = "{edited?_edited,}"
CLI_EXPORT_ORIGINAL_SUFFIX = "_original" CLI_EXPORT_ORIGINAL_SUFFIX = "_original"
CLI_EXPORT_ORIGINAL_SUFFIX_TEMPLATE = "{edited?_original,}"
CLI_EXPORT_FILENAMES_EDITED_SUFFIX = [ CLI_EXPORT_FILENAMES_EDITED_SUFFIX = [
"Pumkins1.jpg", "Pumkins1.jpg",
@@ -79,6 +81,17 @@ CLI_EXPORT_FILENAMES_EDITED_SUFFIX = [
"wedding_bearbeiten.jpeg", "wedding_bearbeiten.jpeg",
] ]
CLI_EXPORT_FILENAMES_EDITED_SUFFIX_TEMPLATE = [
"Pumkins1.jpg",
"Pumkins2.jpg",
"Pumpkins3.jpg",
"St James Park.jpg",
"St James Park_edited.jpeg",
"Tulips.jpg",
"wedding.jpg",
"wedding_edited.jpeg",
]
CLI_EXPORT_FILENAMES_ORIGINAL_SUFFIX = [ CLI_EXPORT_FILENAMES_ORIGINAL_SUFFIX = [
"Pumkins1_original.jpg", "Pumkins1_original.jpg",
"Pumkins2_original.jpg", "Pumkins2_original.jpg",
@@ -90,6 +103,17 @@ CLI_EXPORT_FILENAMES_ORIGINAL_SUFFIX = [
"wedding_edited.jpeg", "wedding_edited.jpeg",
] ]
CLI_EXPORT_FILENAMES_ORIGINAL_SUFFIX_TEMPLATE = [
"Pumkins1.jpg",
"Pumkins2.jpg",
"Pumpkins3.jpg",
"St James Park_original.jpg",
"St James Park_edited.jpeg",
"Tulips.jpg",
"wedding_original.jpg",
"wedding_edited.jpeg",
]
CLI_EXPORT_FILENAMES_CURRENT = [ CLI_EXPORT_FILENAMES_CURRENT = [
"1EB2B765-0765-43BA-A90C-0D0580E6172C.jpeg", "1EB2B765-0765-43BA-A90C-0D0580E6172C.jpeg",
"3DD2C897-F19E-4CA6-8C22-B027D5A71907.jpeg", "3DD2C897-F19E-4CA6-8C22-B027D5A71907.jpeg",
@@ -1087,6 +1111,33 @@ def test_export_edited_suffix():
assert sorted(files) == sorted(CLI_EXPORT_FILENAMES_EDITED_SUFFIX) assert sorted(files) == sorted(CLI_EXPORT_FILENAMES_EDITED_SUFFIX)
def test_export_edited_suffix_template():
""" test export with --edited-suffix template """
import glob
import os
import os.path
import osxphotos
from osxphotos.__main__ import export
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),
".",
"--edited-suffix",
CLI_EXPORT_EDITED_SUFFIX_TEMPLATE,
"-V",
],
)
assert result.exit_code == 0
files = glob.glob("*")
assert sorted(files) == sorted(CLI_EXPORT_FILENAMES_EDITED_SUFFIX_TEMPLATE)
def test_export_original_suffix(): def test_export_original_suffix():
""" test export with --original-suffix """ """ test export with --original-suffix """
import glob import glob
@@ -1114,6 +1165,33 @@ def test_export_original_suffix():
assert sorted(files) == sorted(CLI_EXPORT_FILENAMES_ORIGINAL_SUFFIX) assert sorted(files) == sorted(CLI_EXPORT_FILENAMES_ORIGINAL_SUFFIX)
def test_export_original_suffix_template():
""" test export with --original-suffix template """
import glob
import os
import os.path
import osxphotos
from osxphotos.__main__ import export
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),
".",
"--original-suffix",
CLI_EXPORT_ORIGINAL_SUFFIX_TEMPLATE,
"-V",
],
)
assert result.exit_code == 0
files = glob.glob("*")
assert sorted(files) == sorted(CLI_EXPORT_FILENAMES_ORIGINAL_SUFFIX_TEMPLATE)
@pytest.mark.skipif( @pytest.mark.skipif(
"OSXPHOTOS_TEST_CONVERT" not in os.environ, "OSXPHOTOS_TEST_CONVERT" not in os.environ,
reason="Skip if running in Github actions, no GPU.", reason="Skip if running in Github actions, no GPU.",

View File

@@ -59,10 +59,16 @@ TEMPLATE_VALUES_TITLE = {
} }
# Boolean type values that render to True # Boolean type values that render to True
UUID_BOOL_VALUES = {"hdr": "D11D25FF-5F31-47D2-ABA9-58418878DC15"} UUID_BOOL_VALUES = {
"hdr": "D11D25FF-5F31-47D2-ABA9-58418878DC15",
"edited": "51F2BEF7-431A-4D31-8AC1-3284A57826AE",
}
# Boolean type values that render to False # Boolean type values that render to False
UUID_BOOL_VALUES_NOT = {"hdr": "51F2BEF7-431A-4D31-8AC1-3284A57826AE"} UUID_BOOL_VALUES_NOT = {
"hdr": "51F2BEF7-431A-4D31-8AC1-3284A57826AE",
"edited": "CCBE0EB9-AE9F-4479-BFFD-107042C75227",
}
# for exiftool template # for exiftool template
UUID_EXIFTOOL = { UUID_EXIFTOOL = {