From 7e2d09bf123428c09a669d8d581e1a35e374273d Mon Sep 17 00:00:00 2001 From: Rhet Turnbull Date: Sun, 4 Jul 2021 08:39:06 -0700 Subject: [PATCH] Added --preview, #470 --- osxphotos/_constants.py | 3 + osxphotos/_version.py | 2 +- osxphotos/cli.py | 130 +++++++++++++---------- osxphotos/photoinfo/_photoinfo_export.py | 5 +- tests/test_cli.py | 63 +++++++++++ 5 files changed, 142 insertions(+), 61 deletions(-) diff --git a/osxphotos/_constants.py b/osxphotos/_constants.py index 49f4cda9..91cb2125 100644 --- a/osxphotos/_constants.py +++ b/osxphotos/_constants.py @@ -210,6 +210,9 @@ DEFAULT_EDITED_SUFFIX = "_edited" # Default suffix to add to original images DEFAULT_ORIGINAL_SUFFIX = "" +# Default suffix to add to preview images +DEFAULT_PREVIEW_SUFFIX = "_preview" + # Colors for print CLI messages CLI_COLOR_ERROR = "red" CLI_COLOR_WARNING = "yellow" diff --git a/osxphotos/_version.py b/osxphotos/_version.py index f975f9fd..dfa5e9e5 100644 --- a/osxphotos/_version.py +++ b/osxphotos/_version.py @@ -1,3 +1,3 @@ """ version info """ -__version__ = "0.42.56" +__version__ = "0.42.57" diff --git a/osxphotos/cli.py b/osxphotos/cli.py index 83c2354b..57850870 100644 --- a/osxphotos/cli.py +++ b/osxphotos/cli.py @@ -17,9 +17,9 @@ import bitmath import click import osxmetadata import photoscript +import rich.traceback import yaml from rich import pretty -import rich.traceback import osxphotos @@ -33,6 +33,7 @@ from ._constants import ( DEFAULT_EDITED_SUFFIX, DEFAULT_JPEG_QUALITY, DEFAULT_ORIGINAL_SUFFIX, + DEFAULT_PREVIEW_SUFFIX, EXTENDED_ATTRIBUTE_NAMES, EXTENDED_ATTRIBUTE_NAMES_QUOTED, OSXPHOTOS_EXPORT_DB, @@ -704,12 +705,20 @@ def cli(ctx, db, json_, debug): "a value of 0.0 specifies maximum compression. " f"Defaults to {DEFAULT_JPEG_QUALITY}", ) -# @click.option( -# "--preview", -# is_flag=True, -# help="Export preview image generated by Photos. " -# "This is a lower-resolution image used by Photos to quickly preview the image.", -# ) +@click.option( + "--preview", + is_flag=True, + help="Export preview image generated by Photos. " + "This is a lower-resolution image used by Photos to quickly preview the image.", +) +@click.option( + "--preview-suffix", + metavar="SUFFIX", + help="Optional suffix template for naming preview photos. Default name for preview photos is in form " + f"'photoname{DEFAULT_PREVIEW_SUFFIX}.ext'. For example, with '--preview-suffix _low_res', the preview photo " + f"would be named 'photoname_low_res.ext'. The default suffix is '{DEFAULT_PREVIEW_SUFFIX}'. " + "Multi-value templates (see Templating System) are not permitted with --preview-suffix.", +) @click.option( "--download-missing", is_flag=True, @@ -1169,7 +1178,8 @@ def export( duplicate, post_command, post_function, - preview=False, + preview, + preview_suffix, ): """Export photos from the Photos database. Export path DEST is required. @@ -1330,6 +1340,8 @@ def export( duplicate = cfg.duplicate post_command = cfg.post_command post_function = cfg.post_function + preview = cfg.preview + preview_suffix = cfg.preview_suffix # config file might have changed verbose VERBOSE = bool(verbose) @@ -1419,6 +1431,9 @@ def export( original_suffix = ( DEFAULT_ORIGINAL_SUFFIX if original_suffix is None else original_suffix ) + preview_suffix = ( + DEFAULT_PREVIEW_SUFFIX if preview_suffix is None else preview_suffix + ) retry = 0 if not retry else retry if not os.path.isdir(dest): @@ -1737,6 +1752,7 @@ def export( retry=retry, export_dir=dest, export_preview=preview, + preview_suffix=preview_suffix, ) if post_function: @@ -2392,6 +2408,7 @@ def export_photo( retry=0, export_dir=None, export_preview=False, + preview_suffix=None, ): """Helper function for export that does the actual export @@ -2434,6 +2451,7 @@ def export_photo( retry: retry up to retry # of times if there's an error export_dir: top-level export directory for {export_dir} template export_preview: export the preview image generated by Photos + preview_suffix: str, template to use as suffix for preview images Returns: list of path(s) of exported photo or None if photo was missing @@ -2494,27 +2512,12 @@ def export_photo( if "exiftool" in sidecar: sidecar_flags |= SIDECAR_EXIFTOOL - rendered_suffix = "" - if original_suffix: - try: - options = RenderOptions(filename=True, strip=strip, export_dir=dest) - rendered_suffix, unmatched = photo.render_template(original_suffix, options) - except ValueError as e: - raise click.BadOptionUsage( - "original_suffix", - f"Invalid template for --original-suffix '{original_suffix}': {e}", - ) - if not rendered_suffix or unmatched: - raise click.BadOptionUsage( - "original_suffix", - f"Invalid template for --original-suffix '{original_suffix}': results={rendered_suffix} unknown field={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] + rendered_suffix = _render_suffix_template( + original_suffix, "original_suffix", "--original-suffix", strip, dest, photo + ) + rendered_preview_suffix = _render_suffix_template( + preview_suffix, "preview_suffix", "--preview-suffix", strip, dest, photo + ) # if download_missing and the photo is missing or path doesn't exist, # try to download with Photos @@ -2594,6 +2597,7 @@ def export_photo( retry=retry, export_dir=export_dir, export_preview=export_preview, + preview_suffix=rendered_preview_suffix, ) if export_edited and photo.hasadjustments: @@ -2625,35 +2629,12 @@ def export_photo( ): edited_ext = "." + jpeg_ext if jpeg_ext else ".jpeg" - if edited_suffix: - try: - options = RenderOptions( - filename=True, - strip=strip, - export_dir=dest, - ) - rendered_suffix, unmatched = photo.render_template( - edited_suffix, options - ) - except ValueError as e: - raise click.BadOptionUsage( - "edited_suffix", - f"Invalid template for --edited-suffix '{edited_suffix}': {e}", - ) - if not rendered_suffix or unmatched: - raise click.BadOptionUsage( - "edited_suffix", - f"Invalid template for --edited-suffix '{edited_suffix}': unknown field={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}" + rendered_edited_suffix = _render_suffix_template( + edited_suffix, "edited_suffix", "--edited-suffix", strip, dest, photo + ) + edited_filename = ( + f"{edited_filename.stem}{rendered_edited_suffix}{edited_ext}" + ) verbose_( f"Exporting edited version of {photo.original_filename} ({photo.filename}) as {edited_filename}" @@ -2700,11 +2681,42 @@ def export_photo( retry=retry, export_dir=export_dir, export_preview=not export_original and export_preview, + preview_suffix=rendered_preview_suffix, ) return results +def _render_suffix_template(suffix_template, var_name, option_name, strip, dest, photo): + """render suffix template + + Returns: + rendered template + """ + if not suffix_template: + return "" + + try: + options = RenderOptions(filename=True, strip=strip, export_dir=dest) + rendered_suffix, unmatched = photo.render_template(suffix_template, options) + except ValueError as e: + raise click.BadOptionUsage( + var_name, + f"Invalid template for {option_name} '{suffix_template}': {e}", + ) + if not rendered_suffix or unmatched: + raise click.BadOptionUsage( + var_name, + f"Invalid template for {option_name} '{suffix_template}': results={rendered_suffix} unknown field={unmatched}", + ) + if len(rendered_suffix) > 1: + raise click.BadOptionUsage( + var_name, + f"Invalid template for {option_name}: may not use multi-valued templates: '{suffix_template}': results={rendered_suffix}", + ) + return rendered_suffix[0] + + def export_photo_with_template( photo, filename, @@ -2746,6 +2758,7 @@ def export_photo_with_template( retry, export_dir, export_preview, + preview_suffix, ): """Evaluate directory template then export photo to each directory""" @@ -2839,6 +2852,7 @@ def export_photo_with_template( replace_keywords=replace_keywords, render_options=render_options, preview=export_preview, + preview_suffix=preview_suffix, ) for warning_ in export_results.exiftool_warning: verbose_(f"exiftool warning for file {warning_[0]}: {warning_[1]}") diff --git a/osxphotos/photoinfo/_photoinfo_export.py b/osxphotos/photoinfo/_photoinfo_export.py index 62373dbe..63d431fb 100644 --- a/osxphotos/photoinfo/_photoinfo_export.py +++ b/osxphotos/photoinfo/_photoinfo_export.py @@ -37,6 +37,7 @@ from .._constants import ( _UNKNOWN_PERSON, _XMP_TEMPLATE_NAME, _XMP_TEMPLATE_NAME_BETA, + DEFAULT_PREVIEW_SUFFIX, LIVE_VIDEO_EXTENSIONS, SIDECAR_EXIFTOOL, SIDECAR_JSON, @@ -527,7 +528,7 @@ def export2( location=True, replace_keywords=False, preview=False, - preview_suffix="_preview", + preview_suffix=DEFAULT_PREVIEW_SUFFIX, render_options: Optional[RenderOptions] = None, ): """export photo, like export but with update and dry_run options @@ -585,7 +586,7 @@ def export2( location: if True, include location in exported metadata replace_keywords: if True, keyword_template replaces any keywords, otherwise it's additive preview: if True, also exports preview image - preview_suffix: optional string to append to end of filename for preview images, if not provided, uses "_preview" + preview_suffix: optional string to append to end of filename for preview images render_options: optional osxphotos.phototemplate.RenderOptions instance to specify options for rendering templates Returns: ExportResults class diff --git a/tests/test_cli.py b/tests/test_cli.py index 3b125a04..83fd0e75 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -134,6 +134,7 @@ CLI_EXPORT_EDITED_SUFFIX = "_bearbeiten" CLI_EXPORT_EDITED_SUFFIX_TEMPLATE = "{edited?_edited,}" CLI_EXPORT_ORIGINAL_SUFFIX = "_original" CLI_EXPORT_ORIGINAL_SUFFIX_TEMPLATE = "{edited?_original,}" +CLI_EXPORT_PREVIEW_SUFFIX = "_lowres" CLI_EXPORT_FILENAMES_EDITED_SUFFIX = [ "Pumkins1.jpg", @@ -430,6 +431,8 @@ CLI_EXPORT_UUID_KEYWORD_PATHSEP = "7783E8E6-9CAC-40F3-BE22-81FB7051C266" CLI_EXPORT_UUID_LONG_DESCRIPTION = "8846E3E6-8AC8-4857-8448-E3D025784410" CLI_EXPORT_UUID_FILENAME = "Pumkins2.jpg" +CLI_EXPORT_UUID_FILENAME_PREVIEW = "Pumkins2_preview.jpeg" +CLI_EXPORT_UUID_FILENAME_PREVIEW_TEMPLATE = "Pumkins2_lowres.jpeg" CLI_EXPORT_BY_DATE_TOUCH_UUID = [ "1EB2B765-0765-43BA-A90C-0D0580E6172C", # Pumpkins3.jpg @@ -1242,6 +1245,66 @@ def test_export_uuid_from_file(): assert sorted(files) == sorted(CLI_EXPORT_UUID_FROM_FILE_FILENAMES) +def test_export_preview(): + """test export with --preview""" + import glob + import os + import os.path + + import osxphotos + from osxphotos.cli 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), + ".", + "-V", + "--preview", + "--uuid", + CLI_EXPORT_UUID, + ], + ) + assert result.exit_code == 0 + files = glob.glob("*") + assert CLI_EXPORT_UUID_FILENAME_PREVIEW in files + + +def test_export_preview_suffix(): + """test export with --preview and --preview-suffix""" + import glob + import os + import os.path + + import osxphotos + from osxphotos.cli 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), + ".", + "-V", + "--preview", + "--preview-suffix", + CLI_EXPORT_PREVIEW_SUFFIX, + "--uuid", + CLI_EXPORT_UUID, + ], + ) + assert result.exit_code == 0 + files = glob.glob("*") + assert CLI_EXPORT_UUID_FILENAME_PREVIEW_TEMPLATE in files + + def test_export_as_hardlink(): import glob import os