Added --preview-if-missing, #446

This commit is contained in:
Rhet Turnbull
2021-07-04 10:03:36 -07:00
parent 675371f0d7
commit 632169f277
11 changed files with 169 additions and 133 deletions

View File

@@ -891,7 +891,15 @@ Options:
--preview Export preview image generated by Photos. This --preview Export preview image generated by Photos. This
is a lower-resolution image used by Photos to is a lower-resolution image used by Photos to
quickly preview the image. quickly preview the image. See also --preview-
suffix and --preview-if-missing.
--preview-if-missing Export preview image generated by Photos if
the actual photo file is missing from the
library. This may be helpful if photos were
not copied to the Photos library and the
original photo is missing. See also --preview-
suffix and --preview.
--preview-suffix SUFFIX Optional suffix template for naming preview --preview-suffix SUFFIX Optional suffix template for naming preview
photos. Default name for preview photos is in photos. Default name for preview photos is in
@@ -900,7 +908,8 @@ Options:
photo would be named 'photoname_low_res.ext'. photo would be named 'photoname_low_res.ext'.
The default suffix is '_preview'. Multi-value The default suffix is '_preview'. Multi-value
templates (see Templating System) are not templates (see Templating System) are not
permitted with --preview-suffix. permitted with --preview-suffix. See also
--preview and --preview-if-missing.
--download-missing Attempt to download missing photos from --download-missing Attempt to download missing photos from
iCloud. The current implementation uses iCloud. The current implementation uses

View File

@@ -1,3 +1,3 @@
""" version info """ """ version info """
__version__ = "0.42.57" __version__ = "0.42.58"

View File

@@ -709,7 +709,15 @@ def cli(ctx, db, json_, debug):
"--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. "
"See also --preview-suffix and --preview-if-missing.",
)
@click.option(
"--preview-if-missing",
is_flag=True,
help="Export preview image generated by Photos if the actual photo file is missing from the library. "
"This may be helpful if photos were not copied to the Photos library and the original photo is missing. "
"See also --preview-suffix and --preview.",
) )
@click.option( @click.option(
"--preview-suffix", "--preview-suffix",
@@ -717,7 +725,8 @@ def cli(ctx, db, json_, debug):
help="Optional suffix template for naming preview photos. Default name for preview photos is in form " 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"'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}'. " 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.", "Multi-value templates (see Templating System) are not permitted with --preview-suffix. "
"See also --preview and --preview-if-missing.",
) )
@click.option( @click.option(
"--download-missing", "--download-missing",
@@ -1180,6 +1189,7 @@ def export(
post_function, post_function,
preview, preview,
preview_suffix, preview_suffix,
preview_if_missing,
): ):
"""Export photos from the Photos database. """Export photos from the Photos database.
Export path DEST is required. Export path DEST is required.
@@ -1342,6 +1352,7 @@ def export(
post_function = cfg.post_function post_function = cfg.post_function
preview = cfg.preview preview = cfg.preview
preview_suffix = cfg.preview_suffix preview_suffix = cfg.preview_suffix
preview_if_missing = cfg.preview_if_missing
# config file might have changed verbose # config file might have changed verbose
VERBOSE = bool(verbose) VERBOSE = bool(verbose)
@@ -1753,6 +1764,7 @@ def export(
export_dir=dest, export_dir=dest,
export_preview=preview, export_preview=preview,
preview_suffix=preview_suffix, preview_suffix=preview_suffix,
preview_if_missing=preview_if_missing,
) )
if post_function: if post_function:
@@ -2409,6 +2421,7 @@ def export_photo(
export_dir=None, export_dir=None,
export_preview=False, export_preview=False,
preview_suffix=None, preview_suffix=None,
preview_if_missing=False,
): ):
"""Helper function for export that does the actual export """Helper function for export that does the actual export
@@ -2452,6 +2465,7 @@ def export_photo(
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 preview_suffix: str, template to use as suffix for preview images
preview_if_missing: bool, export preview if original is missing
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
@@ -2598,6 +2612,7 @@ def export_photo(
export_dir=export_dir, export_dir=export_dir,
export_preview=export_preview, export_preview=export_preview,
preview_suffix=rendered_preview_suffix, preview_suffix=rendered_preview_suffix,
preview_if_missing=preview_if_missing,
) )
if export_edited and photo.hasadjustments: if export_edited and photo.hasadjustments:
@@ -2682,6 +2697,7 @@ def export_photo(
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, preview_suffix=rendered_preview_suffix,
preview_if_missing=preview_if_missing,
) )
return results return results
@@ -2759,9 +2775,9 @@ def export_photo_with_template(
export_dir, export_dir,
export_preview, export_preview,
preview_suffix, preview_suffix,
preview_if_missing,
): ):
"""Evaluate directory template then export photo to each directory""" """Evaluate directory template then export photo to each directory"""
results = ExportResults() results = ExportResults()
dest_paths = get_dirnames_from_template( dest_paths = get_dirnames_from_template(
@@ -2770,17 +2786,18 @@ def export_photo_with_template(
# export the photo to each path in dest_paths # export the photo to each path in dest_paths
for dest_path in dest_paths: for dest_path in dest_paths:
# TODO: if --skip-original-if-edited, it's possible edited version is on disk but
# original is missing, in which case we should download the edited version
if export_original: if export_original:
if missing: if missing and not preview_if_missing:
space = " " if not verbose else "" space = " " if not verbose else ""
verbose_( verbose_(
f"{space}Skipping missing photo {photo.original_filename} ({photo.uuid})" f"{space}Skipping missing photo {photo.original_filename} ({photo.uuid})"
) )
results.missing.append(str(pathlib.Path(dest_path) / filename)) results.missing.append(str(pathlib.Path(dest_path) / filename))
continue elif (
elif photo.intrash and (not photo.path or use_photos_export): photo.intrash
and (not photo.path or use_photos_export)
and not preview_if_missing
):
# skip deleted files if they're missing or using use_photos_export # skip deleted files if they're missing or using use_photos_export
# as AppleScript/PhotoKit cannot export deleted photos # as AppleScript/PhotoKit cannot export deleted photos
space = " " if not verbose else "" space = " " if not verbose else ""
@@ -2794,12 +2811,16 @@ def export_photo_with_template(
continue continue
else: else:
# exporting the edited version # exporting the edited version
if missing: if missing and not preview_if_missing:
space = " " if not verbose else "" space = " " if not verbose else ""
verbose_(f"{space}Skipping missing edited photo for {filename}") verbose_(f"{space}Skipping missing edited photo for {filename}")
results.missing.append(str(pathlib.Path(dest_path) / filename)) results.missing.append(str(pathlib.Path(dest_path) / filename))
continue continue
elif photo.intrash and (not photo.path_edited or use_photos_export): elif (
photo.intrash
and (not photo.path_edited or use_photos_export)
and not preview_if_missing
):
# skip deleted files if they're missing or using use_photos_export # skip deleted files if they're missing or using use_photos_export
# as AppleScript/PhotoKit cannot export deleted photos # as AppleScript/PhotoKit cannot export deleted photos
space = " " if not verbose else "" space = " " if not verbose else ""
@@ -2815,86 +2836,86 @@ def export_photo_with_template(
while tries <= retry: while tries <= retry:
tries += 1 tries += 1
error = 0 error = 0
try: # try:
export_results = photo.export2( export_results = photo.export2(
dest_path, dest_path,
original_filename=filename, original_filename=filename,
edited=edited, edited=edited,
original=export_original, original=export_original,
edited_filename=filename, edited_filename=filename,
sidecar=sidecar_flags, sidecar=sidecar_flags,
sidecar_drop_ext=sidecar_drop_ext, sidecar_drop_ext=sidecar_drop_ext,
live_photo=export_live, live_photo=export_live,
raw_photo=export_raw, raw_photo=export_raw,
export_as_hardlink=export_as_hardlink, export_as_hardlink=export_as_hardlink,
overwrite=overwrite, overwrite=overwrite,
use_photos_export=use_photos_export, use_photos_export=use_photos_export,
exiftool=exiftool, exiftool=exiftool,
merge_exif_keywords=exiftool_merge_keywords, merge_exif_keywords=exiftool_merge_keywords,
merge_exif_persons=exiftool_merge_persons, merge_exif_persons=exiftool_merge_persons,
use_albums_as_keywords=album_keyword, use_albums_as_keywords=album_keyword,
use_persons_as_keywords=person_keyword, use_persons_as_keywords=person_keyword,
keyword_template=keyword_template, keyword_template=keyword_template,
description_template=description_template, description_template=description_template,
update=update, update=update,
ignore_signature=ignore_signature, ignore_signature=ignore_signature,
export_db=export_db, export_db=export_db,
fileutil=fileutil, fileutil=fileutil,
dry_run=dry_run, dry_run=dry_run,
touch_file=touch_file, touch_file=touch_file,
convert_to_jpeg=convert_to_jpeg, convert_to_jpeg=convert_to_jpeg,
jpeg_quality=jpeg_quality, jpeg_quality=jpeg_quality,
ignore_date_modified=ignore_date_modified, ignore_date_modified=ignore_date_modified,
use_photokit=use_photokit, use_photokit=use_photokit,
verbose=verbose_, verbose=verbose_,
exiftool_flags=exiftool_option, exiftool_flags=exiftool_option,
jpeg_ext=jpeg_ext, jpeg_ext=jpeg_ext,
replace_keywords=replace_keywords, replace_keywords=replace_keywords,
render_options=render_options, render_options=render_options,
preview=export_preview, preview=export_preview or (missing and preview_if_missing),
preview_suffix=preview_suffix, 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]}")
for error_ in export_results.exiftool_error: for error_ in export_results.exiftool_error:
click.echo(
click.style(
f"exiftool error for file {error_[0]}: {error_[1]}",
fg=CLI_COLOR_ERROR,
),
err=True,
)
for error_ in export_results.error:
click.echo(
click.style(
f"Error exporting photo ({photo.uuid}: {photo.original_filename}) as {error_[0]}: {error_[1]}",
fg=CLI_COLOR_ERROR,
),
err=True,
)
error += 1
if not error or tries > retry:
results += export_results
break
else:
click.echo(
"Retrying export for photo ({photo.uuid}: {photo.original_filename})"
)
except Exception as e:
click.echo( click.echo(
click.style( click.style(
f"Error exporting photo ({photo.uuid}: {photo.original_filename}) as {filename}: {e}", f"exiftool error for file {error_[0]}: {error_[1]}",
fg=CLI_COLOR_ERROR, fg=CLI_COLOR_ERROR,
), ),
err=True, err=True,
) )
if tries > retry: for error_ in export_results.error:
results.error.append((str(pathlib.Path(dest) / filename), e)) click.echo(
break click.style(
else: f"Error exporting photo ({photo.uuid}: {photo.original_filename}) as {error_[0]}: {error_[1]}",
click.echo( fg=CLI_COLOR_ERROR,
f"Retrying export for photo ({photo.uuid}: {photo.original_filename})" ),
) err=True,
)
error += 1
if not error or tries > retry:
results += export_results
break
else:
click.echo(
"Retrying export for photo ({photo.uuid}: {photo.original_filename})"
)
# except Exception as e:
# click.echo(
# click.style(
# f"Error exporting photo ({photo.uuid}: {photo.original_filename}) as {filename}: {e}",
# fg=CLI_COLOR_ERROR,
# ),
# err=True,
# )
# if tries > retry:
# results.error.append((str(pathlib.Path(dest) / filename), e))
# break
# else:
# click.echo(
# f"Retrying export for photo ({photo.uuid}: {photo.original_filename})"
# )
if verbose: if verbose:
if update: if update:

View File

@@ -770,18 +770,10 @@ def export2(
# get path to source file and verify it's not None and is valid file # get path to source file and verify it's not None and is valid file
# TODO: how to handle ismissing or not hasadjustments and edited=True cases? # TODO: how to handle ismissing or not hasadjustments and edited=True cases?
export_src_dest = [] export_src_dest = []
if edited: if edited and self.path_edited is not None:
if self.path_edited is not None: export_src_dest.append((self.path_edited, dest_edited))
export_src_dest.append((self.path_edited, dest_edited)) elif not edited and self.path is not None:
else: export_src_dest.append((self.path, dest_original))
raise FileNotFoundError(
f"Cannot export edited photo if path_edited is None"
)
else:
if self.path is not None:
export_src_dest.append((self.path, dest_original))
else:
raise FileNotFoundError("Cannot export photo if path is None")
for src, dest in export_src_dest: for src, dest in export_src_dest:
if not pathlib.Path(src).is_file(): if not pathlib.Path(src).is_file():
@@ -907,7 +899,7 @@ def export2(
all_results += results all_results += results
# copy associated RAW image if requested # copy associated RAW image if requested
if raw_photo and self.has_raw: if raw_photo and self.has_raw and self.path_raw:
raw_path = pathlib.Path(self.path_raw) raw_path = pathlib.Path(self.path_raw)
raw_ext = raw_path.suffix raw_ext = raw_path.suffix
raw_name = dest.parent / f"{dest.stem}{raw_ext}" raw_name = dest.parent / f"{dest.stem}{raw_ext}"

View File

@@ -787,7 +787,7 @@ def test_export_7(photosdb):
def test_export_8(photosdb): def test_export_8(photosdb):
# try to export missing file # try to export missing file
# should raise exception # should return empty list
import os import os
import os.path import os.path
import tempfile import tempfile
@@ -796,11 +796,7 @@ def test_export_8(photosdb):
dest = tempdir.name dest = tempdir.name
photos = photosdb.photos(uuid=[UUID_DICT["missing"]]) photos = photosdb.photos(uuid=[UUID_DICT["missing"]])
filename = photos[0].filename assert photos[0].export(dest) == []
with pytest.raises(Exception) as e:
assert photos[0].export(dest)[0]
assert e.type == type(FileNotFoundError())
def test_export_9(photosdb): def test_export_9(photosdb):

View File

@@ -866,17 +866,12 @@ def test_export_7(photosdb):
def test_export_8(photosdb): def test_export_8(photosdb):
# try to export missing file # try to export missing file
# should raise exception
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_") tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
dest = tempdir.name dest = tempdir.name
photos = photosdb.photos(uuid=[UUID_DICT["missing"]]) photos = photosdb.photos(uuid=[UUID_DICT["missing"]])
filename = photos[0].filename assert photos[0].export(dest) == []
with pytest.raises(Exception) as e:
assert photos[0].export(dest)[0]
assert e.type == type(FileNotFoundError())
def test_export_9(photosdb): def test_export_9(photosdb):

View File

@@ -777,6 +777,12 @@ UUID_DUPLICATES = [
UUID_LOCATION = "D79B8D77-BFFC-460B-9312-034F2877D35B" # Pumkins2.jpg UUID_LOCATION = "D79B8D77-BFFC-460B-9312-034F2877D35B" # Pumkins2.jpg
UUID_NO_LOCATION = "6191423D-8DB8-4D4C-92BE-9BBBA308AAC4" # Tulips.jpg" UUID_NO_LOCATION = "6191423D-8DB8-4D4C-92BE-9BBBA308AAC4" # Tulips.jpg"
UUID_DICT_MISSING = {
"8E1D7BC9-9321-44F9-8CFB-4083F6B9232A": "IMG_2000.jpeg", # missing
"A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C": "Pumpkins4.jpeg", # missing
"D79B8D77-BFFC-460B-9312-034F2877D35B": "Pumkins2.jpg", # not missing
}
def modify_file(filename): def modify_file(filename):
"""appends data to a file to modify it""" """appends data to a file to modify it"""
@@ -1305,6 +1311,40 @@ def test_export_preview_suffix():
assert CLI_EXPORT_UUID_FILENAME_PREVIEW_TEMPLATE in files assert CLI_EXPORT_UUID_FILENAME_PREVIEW_TEMPLATE in files
def test_export_preview_if_missing():
"""test export with --preview_if_missing"""
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():
uuid_options = []
for uuid in UUID_DICT_MISSING:
uuid_options.extend(["--uuid", uuid])
result = runner.invoke(
export,
[
os.path.join(cwd, CLI_PHOTOS_DB),
".",
"-V",
"--preview-if-missing",
"--preview-suffix",
"",
*uuid_options,
],
)
assert result.exit_code == 0
files = glob.glob("*")
expected_files = list(UUID_DICT_MISSING.values())
assert sorted(files) == sorted(expected_files)
def test_export_as_hardlink(): def test_export_as_hardlink():
import glob import glob
import os import os

View File

@@ -240,7 +240,6 @@ def test_export_7(photosdb):
def test_export_8(photosdb): def test_export_8(photosdb):
# try to export missing file # try to export missing file
# should raise exception
import os import os
import os.path import os.path
import tempfile import tempfile
@@ -249,12 +248,7 @@ def test_export_8(photosdb):
dest = tempdir.name dest = tempdir.name
photos = photosdb.photos(uuid=[UUID_DICT["missing"]]) photos = photosdb.photos(uuid=[UUID_DICT["missing"]])
filename = photos[0].filename assert photos[0].export(dest) == []
expected_dest = os.path.join(dest, filename)
with pytest.raises(Exception) as e:
assert photos[0].export(dest)
assert e.type == type(FileNotFoundError())
def test_export_9(photosdb): def test_export_9(photosdb):

View File

@@ -211,7 +211,6 @@ def test_export_7(photosdb):
def test_export_8(photosdb): def test_export_8(photosdb):
# try to export missing file # try to export missing file
# should raise exception
import os import os
import os.path import os.path
import tempfile import tempfile
@@ -220,12 +219,7 @@ def test_export_8(photosdb):
dest = tempdir.name dest = tempdir.name
photos = photosdb.photos(uuid=[UUID_DICT["missing"]]) photos = photosdb.photos(uuid=[UUID_DICT["missing"]])
filename = photos[0].filename assert photos[0].export(dest) == []
expected_dest = os.path.join(dest, filename)
with pytest.raises(Exception) as e:
assert photos[0].export(dest)[0]
assert e.type == type(FileNotFoundError())
def test_export_9(photosdb): def test_export_9(photosdb):

View File

@@ -863,17 +863,12 @@ def test_export_7(photosdb):
def test_export_8(photosdb): def test_export_8(photosdb):
# try to export missing file # try to export missing file
# should raise exception
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_") tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
dest = tempdir.name dest = tempdir.name
photos = photosdb.photos(uuid=[UUID_DICT["missing"]]) photos = photosdb.photos(uuid=[UUID_DICT["missing"]])
filename = photos[0].filename assert photos[0].export(dest) == []
with pytest.raises(Exception) as e:
assert photos[0].export(dest)[0]
assert e.type == type(FileNotFoundError())
def test_export_9(photosdb): def test_export_9(photosdb):

View File

@@ -31,7 +31,7 @@ def test_dd_to_dms():
assert _dd_to_dms(-0.001) == (0, 0, -3.6) assert _dd_to_dms(-0.001) == (0, 0, -3.6)
@pytest.mark.skip(reason="Fails on some machines")
def test_get_system_library_path(): def test_get_system_library_path():
import osxphotos import osxphotos