Added --preview, #470
This commit is contained in:
@@ -210,6 +210,9 @@ DEFAULT_EDITED_SUFFIX = "_edited"
|
|||||||
# Default suffix to add to original images
|
# Default suffix to add to original images
|
||||||
DEFAULT_ORIGINAL_SUFFIX = ""
|
DEFAULT_ORIGINAL_SUFFIX = ""
|
||||||
|
|
||||||
|
# Default suffix to add to preview images
|
||||||
|
DEFAULT_PREVIEW_SUFFIX = "_preview"
|
||||||
|
|
||||||
# Colors for print CLI messages
|
# Colors for print CLI messages
|
||||||
CLI_COLOR_ERROR = "red"
|
CLI_COLOR_ERROR = "red"
|
||||||
CLI_COLOR_WARNING = "yellow"
|
CLI_COLOR_WARNING = "yellow"
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
""" version info """
|
""" version info """
|
||||||
|
|
||||||
__version__ = "0.42.56"
|
__version__ = "0.42.57"
|
||||||
|
|||||||
130
osxphotos/cli.py
130
osxphotos/cli.py
@@ -17,9 +17,9 @@ import bitmath
|
|||||||
import click
|
import click
|
||||||
import osxmetadata
|
import osxmetadata
|
||||||
import photoscript
|
import photoscript
|
||||||
|
import rich.traceback
|
||||||
import yaml
|
import yaml
|
||||||
from rich import pretty
|
from rich import pretty
|
||||||
import rich.traceback
|
|
||||||
|
|
||||||
import osxphotos
|
import osxphotos
|
||||||
|
|
||||||
@@ -33,6 +33,7 @@ from ._constants import (
|
|||||||
DEFAULT_EDITED_SUFFIX,
|
DEFAULT_EDITED_SUFFIX,
|
||||||
DEFAULT_JPEG_QUALITY,
|
DEFAULT_JPEG_QUALITY,
|
||||||
DEFAULT_ORIGINAL_SUFFIX,
|
DEFAULT_ORIGINAL_SUFFIX,
|
||||||
|
DEFAULT_PREVIEW_SUFFIX,
|
||||||
EXTENDED_ATTRIBUTE_NAMES,
|
EXTENDED_ATTRIBUTE_NAMES,
|
||||||
EXTENDED_ATTRIBUTE_NAMES_QUOTED,
|
EXTENDED_ATTRIBUTE_NAMES_QUOTED,
|
||||||
OSXPHOTOS_EXPORT_DB,
|
OSXPHOTOS_EXPORT_DB,
|
||||||
@@ -704,12 +705,20 @@ def cli(ctx, db, json_, debug):
|
|||||||
"a value of 0.0 specifies maximum compression. "
|
"a value of 0.0 specifies maximum compression. "
|
||||||
f"Defaults to {DEFAULT_JPEG_QUALITY}",
|
f"Defaults to {DEFAULT_JPEG_QUALITY}",
|
||||||
)
|
)
|
||||||
# @click.option(
|
@click.option(
|
||||||
# "--preview",
|
"--preview",
|
||||||
# is_flag=True,
|
is_flag=True,
|
||||||
# help="Export preview image generated by Photos. "
|
help="Export preview image generated by Photos. "
|
||||||
# "This is a lower-resolution image used by Photos to quickly preview the image.",
|
"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(
|
@click.option(
|
||||||
"--download-missing",
|
"--download-missing",
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
@@ -1169,7 +1178,8 @@ def export(
|
|||||||
duplicate,
|
duplicate,
|
||||||
post_command,
|
post_command,
|
||||||
post_function,
|
post_function,
|
||||||
preview=False,
|
preview,
|
||||||
|
preview_suffix,
|
||||||
):
|
):
|
||||||
"""Export photos from the Photos database.
|
"""Export photos from the Photos database.
|
||||||
Export path DEST is required.
|
Export path DEST is required.
|
||||||
@@ -1330,6 +1340,8 @@ def export(
|
|||||||
duplicate = cfg.duplicate
|
duplicate = cfg.duplicate
|
||||||
post_command = cfg.post_command
|
post_command = cfg.post_command
|
||||||
post_function = cfg.post_function
|
post_function = cfg.post_function
|
||||||
|
preview = cfg.preview
|
||||||
|
preview_suffix = cfg.preview_suffix
|
||||||
|
|
||||||
# config file might have changed verbose
|
# config file might have changed verbose
|
||||||
VERBOSE = bool(verbose)
|
VERBOSE = bool(verbose)
|
||||||
@@ -1419,6 +1431,9 @@ def export(
|
|||||||
original_suffix = (
|
original_suffix = (
|
||||||
DEFAULT_ORIGINAL_SUFFIX if original_suffix is None else 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
|
retry = 0 if not retry else retry
|
||||||
|
|
||||||
if not os.path.isdir(dest):
|
if not os.path.isdir(dest):
|
||||||
@@ -1737,6 +1752,7 @@ def export(
|
|||||||
retry=retry,
|
retry=retry,
|
||||||
export_dir=dest,
|
export_dir=dest,
|
||||||
export_preview=preview,
|
export_preview=preview,
|
||||||
|
preview_suffix=preview_suffix,
|
||||||
)
|
)
|
||||||
|
|
||||||
if post_function:
|
if post_function:
|
||||||
@@ -2392,6 +2408,7 @@ def export_photo(
|
|||||||
retry=0,
|
retry=0,
|
||||||
export_dir=None,
|
export_dir=None,
|
||||||
export_preview=False,
|
export_preview=False,
|
||||||
|
preview_suffix=None,
|
||||||
):
|
):
|
||||||
"""Helper function for export that does the actual export
|
"""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
|
retry: retry up to retry # of times if there's an error
|
||||||
export_dir: top-level export directory for {export_dir} template
|
export_dir: top-level export directory for {export_dir} template
|
||||||
export_preview: export the preview image generated by Photos
|
export_preview: export the preview image generated by Photos
|
||||||
|
preview_suffix: str, template to use as suffix for preview images
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list of path(s) of exported photo or None if photo was missing
|
list of path(s) of exported photo or None if photo was missing
|
||||||
@@ -2494,27 +2512,12 @@ def export_photo(
|
|||||||
if "exiftool" in sidecar:
|
if "exiftool" in sidecar:
|
||||||
sidecar_flags |= SIDECAR_EXIFTOOL
|
sidecar_flags |= SIDECAR_EXIFTOOL
|
||||||
|
|
||||||
rendered_suffix = ""
|
rendered_suffix = _render_suffix_template(
|
||||||
if original_suffix:
|
original_suffix, "original_suffix", "--original-suffix", strip, dest, photo
|
||||||
try:
|
)
|
||||||
options = RenderOptions(filename=True, strip=strip, export_dir=dest)
|
rendered_preview_suffix = _render_suffix_template(
|
||||||
rendered_suffix, unmatched = photo.render_template(original_suffix, options)
|
preview_suffix, "preview_suffix", "--preview-suffix", strip, dest, photo
|
||||||
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]
|
|
||||||
|
|
||||||
# if download_missing and the photo is missing or path doesn't exist,
|
# if download_missing and the photo is missing or path doesn't exist,
|
||||||
# try to download with Photos
|
# try to download with Photos
|
||||||
@@ -2594,6 +2597,7 @@ def export_photo(
|
|||||||
retry=retry,
|
retry=retry,
|
||||||
export_dir=export_dir,
|
export_dir=export_dir,
|
||||||
export_preview=export_preview,
|
export_preview=export_preview,
|
||||||
|
preview_suffix=rendered_preview_suffix,
|
||||||
)
|
)
|
||||||
|
|
||||||
if export_edited and photo.hasadjustments:
|
if export_edited and photo.hasadjustments:
|
||||||
@@ -2625,35 +2629,12 @@ def export_photo(
|
|||||||
):
|
):
|
||||||
edited_ext = "." + jpeg_ext if jpeg_ext else ".jpeg"
|
edited_ext = "." + jpeg_ext if jpeg_ext else ".jpeg"
|
||||||
|
|
||||||
if edited_suffix:
|
rendered_edited_suffix = _render_suffix_template(
|
||||||
try:
|
edited_suffix, "edited_suffix", "--edited-suffix", strip, dest, photo
|
||||||
options = RenderOptions(
|
)
|
||||||
filename=True,
|
edited_filename = (
|
||||||
strip=strip,
|
f"{edited_filename.stem}{rendered_edited_suffix}{edited_ext}"
|
||||||
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}"
|
|
||||||
|
|
||||||
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}"
|
||||||
@@ -2700,11 +2681,42 @@ def export_photo(
|
|||||||
retry=retry,
|
retry=retry,
|
||||||
export_dir=export_dir,
|
export_dir=export_dir,
|
||||||
export_preview=not export_original and export_preview,
|
export_preview=not export_original and export_preview,
|
||||||
|
preview_suffix=rendered_preview_suffix,
|
||||||
)
|
)
|
||||||
|
|
||||||
return results
|
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(
|
def export_photo_with_template(
|
||||||
photo,
|
photo,
|
||||||
filename,
|
filename,
|
||||||
@@ -2746,6 +2758,7 @@ def export_photo_with_template(
|
|||||||
retry,
|
retry,
|
||||||
export_dir,
|
export_dir,
|
||||||
export_preview,
|
export_preview,
|
||||||
|
preview_suffix,
|
||||||
):
|
):
|
||||||
"""Evaluate directory template then export photo to each directory"""
|
"""Evaluate directory template then export photo to each directory"""
|
||||||
|
|
||||||
@@ -2839,6 +2852,7 @@ def export_photo_with_template(
|
|||||||
replace_keywords=replace_keywords,
|
replace_keywords=replace_keywords,
|
||||||
render_options=render_options,
|
render_options=render_options,
|
||||||
preview=export_preview,
|
preview=export_preview,
|
||||||
|
preview_suffix=preview_suffix,
|
||||||
)
|
)
|
||||||
for warning_ in export_results.exiftool_warning:
|
for warning_ in export_results.exiftool_warning:
|
||||||
verbose_(f"exiftool warning for file {warning_[0]}: {warning_[1]}")
|
verbose_(f"exiftool warning for file {warning_[0]}: {warning_[1]}")
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ from .._constants import (
|
|||||||
_UNKNOWN_PERSON,
|
_UNKNOWN_PERSON,
|
||||||
_XMP_TEMPLATE_NAME,
|
_XMP_TEMPLATE_NAME,
|
||||||
_XMP_TEMPLATE_NAME_BETA,
|
_XMP_TEMPLATE_NAME_BETA,
|
||||||
|
DEFAULT_PREVIEW_SUFFIX,
|
||||||
LIVE_VIDEO_EXTENSIONS,
|
LIVE_VIDEO_EXTENSIONS,
|
||||||
SIDECAR_EXIFTOOL,
|
SIDECAR_EXIFTOOL,
|
||||||
SIDECAR_JSON,
|
SIDECAR_JSON,
|
||||||
@@ -527,7 +528,7 @@ def export2(
|
|||||||
location=True,
|
location=True,
|
||||||
replace_keywords=False,
|
replace_keywords=False,
|
||||||
preview=False,
|
preview=False,
|
||||||
preview_suffix="_preview",
|
preview_suffix=DEFAULT_PREVIEW_SUFFIX,
|
||||||
render_options: Optional[RenderOptions] = None,
|
render_options: Optional[RenderOptions] = None,
|
||||||
):
|
):
|
||||||
"""export photo, like export but with update and dry_run options
|
"""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
|
location: if True, include location in exported metadata
|
||||||
replace_keywords: if True, keyword_template replaces any keywords, otherwise it's additive
|
replace_keywords: if True, keyword_template replaces any keywords, otherwise it's additive
|
||||||
preview: if True, also exports preview image
|
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
|
render_options: optional osxphotos.phototemplate.RenderOptions instance to specify options for rendering templates
|
||||||
|
|
||||||
Returns: ExportResults class
|
Returns: ExportResults class
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ CLI_EXPORT_EDITED_SUFFIX = "_bearbeiten"
|
|||||||
CLI_EXPORT_EDITED_SUFFIX_TEMPLATE = "{edited?_edited,}"
|
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_ORIGINAL_SUFFIX_TEMPLATE = "{edited?_original,}"
|
||||||
|
CLI_EXPORT_PREVIEW_SUFFIX = "_lowres"
|
||||||
|
|
||||||
CLI_EXPORT_FILENAMES_EDITED_SUFFIX = [
|
CLI_EXPORT_FILENAMES_EDITED_SUFFIX = [
|
||||||
"Pumkins1.jpg",
|
"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_LONG_DESCRIPTION = "8846E3E6-8AC8-4857-8448-E3D025784410"
|
||||||
|
|
||||||
CLI_EXPORT_UUID_FILENAME = "Pumkins2.jpg"
|
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 = [
|
CLI_EXPORT_BY_DATE_TOUCH_UUID = [
|
||||||
"1EB2B765-0765-43BA-A90C-0D0580E6172C", # Pumpkins3.jpg
|
"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)
|
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():
|
def test_export_as_hardlink():
|
||||||
import glob
|
import glob
|
||||||
import os
|
import os
|
||||||
|
|||||||
Reference in New Issue
Block a user