Refactored sidecar code
This commit is contained in:
54
README.md
54
README.md
@@ -301,23 +301,33 @@ Options:
|
|||||||
the primary photo will be exported--
|
the primary photo will be exported--
|
||||||
associated burst images will be skipped.
|
associated burst images will be skipped.
|
||||||
--sidecar FORMAT Create sidecar for each photo exported;
|
--sidecar FORMAT Create sidecar for each photo exported;
|
||||||
valid FORMAT values: xmp, json; --sidecar
|
valid FORMAT values: xmp, json, exiftool;
|
||||||
json: create JSON sidecar useable by
|
--sidecar xmp: create XMP sidecar used by
|
||||||
exiftool (https://exiftool.org/) The sidecar
|
Adobe Lightroom, etc. The sidecar file is
|
||||||
file can be used to apply metadata to the
|
named in format photoname.ext.xmp The XMP
|
||||||
file with exiftool, for example: "exiftool
|
sidecar exports the following tags:
|
||||||
|
Description, Title, Keywords/Tags, Subject
|
||||||
|
(set to Keywords + PersonInImage),
|
||||||
|
PersonInImage, CreateDate, ModifyDate,
|
||||||
|
GPSLongitude.
|
||||||
|
--sidecar json: create JSON
|
||||||
|
sidecar useable by exiftool
|
||||||
|
(https://exiftool.org/) The sidecar file can
|
||||||
|
be used to apply metadata to the file with
|
||||||
|
exiftool, for example: "exiftool
|
||||||
-j=photoname.jpg.json photoname.jpg" The
|
-j=photoname.jpg.json photoname.jpg" The
|
||||||
sidecar file is named in format
|
sidecar file is named in format
|
||||||
photoname.ext.json --sidecar xmp: create
|
photoname.ext.json; format includes tag
|
||||||
XMP sidecar used by Adobe Lightroom, etc.The
|
groups (equivalent to running 'exiftool -G
|
||||||
sidecar file is named in format
|
-j').
|
||||||
photoname.ext.xmpThe XMP sidecar exports the
|
--sidecar exiftool: create JSON
|
||||||
following tags: Description, Title,
|
sidecar compatible with output of 'exiftool
|
||||||
Keywords/Tags, Subject (set to Keywords +
|
-j'. Unlike '--sidecar json', '--sidecar
|
||||||
PersonInImage), PersonInImage, CreateDate,
|
exiftool' does not export tag groups.
|
||||||
ModifyDate, GPSLongitude. For a list of tags
|
Sidecar filename is in format
|
||||||
exported in the JSON sidecar, see
|
photoname.ext.json; For a list of tags
|
||||||
--exiftool.
|
exported in the JSON and exiftool sidecar,
|
||||||
|
see '--exiftool'.
|
||||||
--exiftool Use exiftool to write metadata directly to
|
--exiftool Use exiftool to write metadata directly to
|
||||||
exported photos. To use this option,
|
exported photos. To use this option,
|
||||||
exiftool must be installed and in the path.
|
exiftool must be installed and in the path.
|
||||||
@@ -327,14 +337,12 @@ Options:
|
|||||||
metadata: EXIF:ImageDescription,
|
metadata: EXIF:ImageDescription,
|
||||||
XMP:Description (see also --description-
|
XMP:Description (see also --description-
|
||||||
template); XMP:Title; XMP:TagsList,
|
template); XMP:Title; XMP:TagsList,
|
||||||
IPTC:Keywords (see also --keyword-template,
|
IPTC:Keywords, XMP:Subject (see also
|
||||||
--person-keyword, --album-keyword);
|
--keyword-template, --person-keyword,
|
||||||
XMP:Subject (set to keywords + person in
|
--album-keyword); XMP:PersonInImage;
|
||||||
image to mirror Photos' behavior);
|
EXIF:GPSLatitudeRef; EXIF:GPSLongitudeRef;
|
||||||
XMP:PersonInImage; EXIF:GPSLatitudeRef;
|
EXIF:GPSLatitude; EXIF:GPSLongitude;
|
||||||
EXIF:GPSLongitudeRef; EXIF:GPSLatitude;
|
EXIF:GPSPosition; EXIF:DateTimeOriginal;
|
||||||
EXIF:GPSLongitude; EXIF:GPSPosition;
|
|
||||||
EXIF:DateTimeOriginal;
|
|
||||||
EXIF:OffsetTimeOriginal; EXIF:ModifyDate
|
EXIF:OffsetTimeOriginal; EXIF:ModifyDate
|
||||||
(see --ignore-date-modified);
|
(see --ignore-date-modified);
|
||||||
IPTC:DateCreated; IPTC:TimeCreated; (video
|
IPTC:DateCreated; IPTC:TimeCreated; (video
|
||||||
|
|||||||
@@ -1342,8 +1342,8 @@ def query(
|
|||||||
metavar="FORMAT",
|
metavar="FORMAT",
|
||||||
type=click.Choice(["xmp", "json", "exiftool"], case_sensitive=False),
|
type=click.Choice(["xmp", "json", "exiftool"], case_sensitive=False),
|
||||||
help="Create sidecar for each photo exported; valid FORMAT values: xmp, json, exiftool; "
|
help="Create sidecar for each photo exported; valid FORMAT values: xmp, json, exiftool; "
|
||||||
"--sidecar xmp: create XMP sidecar used by Adobe Lightroom, etc."
|
"--sidecar xmp: create XMP sidecar used by Adobe Lightroom, etc. "
|
||||||
"The sidecar file is named in format photoname.ext.xmp"
|
"The sidecar file is named in format photoname.ext.xmp "
|
||||||
"The XMP sidecar exports the following tags: Description, Title, Keywords/Tags, "
|
"The XMP sidecar exports the following tags: Description, Title, Keywords/Tags, "
|
||||||
"Subject (set to Keywords + PersonInImage), PersonInImage, CreateDate, ModifyDate, "
|
"Subject (set to Keywords + PersonInImage), PersonInImage, CreateDate, ModifyDate, "
|
||||||
"GPSLongitude. "
|
"GPSLongitude. "
|
||||||
@@ -1353,9 +1353,9 @@ def query(
|
|||||||
"The sidecar file is named in format photoname.ext.json; "
|
"The sidecar file is named in format photoname.ext.json; "
|
||||||
"format includes tag groups (equivalent to running 'exiftool -G -j'). "
|
"format includes tag groups (equivalent to running 'exiftool -G -j'). "
|
||||||
"\n--sidecar exiftool: create JSON sidecar compatible with output of 'exiftool -j'. "
|
"\n--sidecar exiftool: create JSON sidecar compatible with output of 'exiftool -j'. "
|
||||||
"Unlike --sidecar json, --sidecar exiftool does not export tag groups. "
|
"Unlike '--sidecar json', '--sidecar exiftool' does not export tag groups. "
|
||||||
"Sidecar filename is in format photoname.ext.json;"
|
"Sidecar filename is in format photoname.ext.json; "
|
||||||
"For a list of tags exported in the JSON sidecar, see --exiftool.",
|
"For a list of tags exported in the JSON and exiftool sidecar, see '--exiftool'.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--exiftool",
|
"--exiftool",
|
||||||
@@ -1365,8 +1365,8 @@ def query(
|
|||||||
"exiftool may be installed from https://exiftool.org/. "
|
"exiftool may be installed from https://exiftool.org/. "
|
||||||
"Cannot be used with --export-as-hardlink. Writes the following metadata: "
|
"Cannot be used with --export-as-hardlink. Writes the following metadata: "
|
||||||
"EXIF:ImageDescription, XMP:Description (see also --description-template); "
|
"EXIF:ImageDescription, XMP:Description (see also --description-template); "
|
||||||
"XMP:Title; XMP:TagsList, IPTC:Keywords (see also --keyword-template, --person-keyword, --album-keyword); "
|
"XMP:Title; XMP:TagsList, IPTC:Keywords, XMP:Subject "
|
||||||
"XMP:Subject (set to keywords + person in image to mirror Photos' behavior); "
|
"(see also --keyword-template, --person-keyword, --album-keyword); "
|
||||||
"XMP:PersonInImage; EXIF:GPSLatitudeRef; EXIF:GPSLongitudeRef; EXIF:GPSLatitude; EXIF:GPSLongitude; "
|
"XMP:PersonInImage; EXIF:GPSLatitudeRef; EXIF:GPSLongitudeRef; EXIF:GPSLatitude; EXIF:GPSLongitude; "
|
||||||
"EXIF:GPSPosition; EXIF:DateTimeOriginal; EXIF:OffsetTimeOriginal; "
|
"EXIF:GPSPosition; EXIF:DateTimeOriginal; EXIF:OffsetTimeOriginal; "
|
||||||
"EXIF:ModifyDate (see --ignore-date-modified); IPTC:DateCreated; IPTC:TimeCreated; "
|
"EXIF:ModifyDate (see --ignore-date-modified); IPTC:DateCreated; IPTC:TimeCreated; "
|
||||||
@@ -1752,6 +1752,7 @@ def export(
|
|||||||
|
|
||||||
verbose_(f"osxphotos version {__version__}")
|
verbose_(f"osxphotos version {__version__}")
|
||||||
|
|
||||||
|
# validate options
|
||||||
exclusive_options = [
|
exclusive_options = [
|
||||||
("favorite", "not_favorite"),
|
("favorite", "not_favorite"),
|
||||||
("hidden", "not_hidden"),
|
("hidden", "not_hidden"),
|
||||||
@@ -1795,6 +1796,16 @@ def export(
|
|||||||
)
|
)
|
||||||
raise click.Abort()
|
raise click.Abort()
|
||||||
|
|
||||||
|
if all(x in [s.lower() for s in sidecar] for x in ["json", "exiftool"]):
|
||||||
|
click.echo(
|
||||||
|
click.style(
|
||||||
|
"Cannot use --sidecar json with --sidecar exiftool due to name collisions",
|
||||||
|
fg=CLI_COLOR_ERROR,
|
||||||
|
),
|
||||||
|
err=True,
|
||||||
|
)
|
||||||
|
raise click.Abort()
|
||||||
|
|
||||||
if save_config:
|
if save_config:
|
||||||
verbose_(f"Saving options to file {save_config}")
|
verbose_(f"Saving options to file {save_config}")
|
||||||
cfg.write_to_file(save_config)
|
cfg.write_to_file(save_config)
|
||||||
@@ -3130,6 +3141,7 @@ def write_export_report(report_file, results):
|
|||||||
"converted_to_jpeg": 0,
|
"converted_to_jpeg": 0,
|
||||||
"sidecar_xmp": 0,
|
"sidecar_xmp": 0,
|
||||||
"sidecar_json": 0,
|
"sidecar_json": 0,
|
||||||
|
"sidecar_exiftool": 0,
|
||||||
"missing": 0,
|
"missing": 0,
|
||||||
"error": 0,
|
"error": 0,
|
||||||
"exiftool_warning": "",
|
"exiftool_warning": "",
|
||||||
@@ -3175,6 +3187,14 @@ def write_export_report(report_file, results):
|
|||||||
all_results[result]["sidecar_json"] = 1
|
all_results[result]["sidecar_json"] = 1
|
||||||
all_results[result]["skipped"] = 1
|
all_results[result]["skipped"] = 1
|
||||||
|
|
||||||
|
for result in results.sidecar_exiftool_written:
|
||||||
|
all_results[result]["sidecar_exiftool"] = 1
|
||||||
|
all_results[result]["exported"] = 1
|
||||||
|
|
||||||
|
for result in results.sidecar_exiftool_skipped:
|
||||||
|
all_results[result]["sidecar_exiftool"] = 1
|
||||||
|
all_results[result]["skipped"] = 1
|
||||||
|
|
||||||
for result in results.missing:
|
for result in results.missing:
|
||||||
all_results[result]["missing"] = 1
|
all_results[result]["missing"] = 1
|
||||||
|
|
||||||
@@ -3198,6 +3218,7 @@ def write_export_report(report_file, results):
|
|||||||
"converted_to_jpeg",
|
"converted_to_jpeg",
|
||||||
"sidecar_xmp",
|
"sidecar_xmp",
|
||||||
"sidecar_json",
|
"sidecar_json",
|
||||||
|
"sidecar_exiftool",
|
||||||
"missing",
|
"missing",
|
||||||
"error",
|
"error",
|
||||||
"exiftool_warning",
|
"exiftool_warning",
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ from .._constants import (
|
|||||||
_TEMPLATE_DIR,
|
_TEMPLATE_DIR,
|
||||||
_UNKNOWN_PERSON,
|
_UNKNOWN_PERSON,
|
||||||
_XMP_TEMPLATE_NAME,
|
_XMP_TEMPLATE_NAME,
|
||||||
SIDECAR_JSON,
|
|
||||||
SIDECAR_EXIFTOOL,
|
SIDECAR_EXIFTOOL,
|
||||||
|
SIDECAR_JSON,
|
||||||
SIDECAR_XMP,
|
SIDECAR_XMP,
|
||||||
)
|
)
|
||||||
from ..datetime_utils import datetime_tz_to_utc
|
from ..datetime_utils import datetime_tz_to_utc
|
||||||
@@ -44,8 +44,8 @@ from ..fileutil import FileUtil
|
|||||||
from ..photokit import (
|
from ..photokit import (
|
||||||
PHOTOS_VERSION_CURRENT,
|
PHOTOS_VERSION_CURRENT,
|
||||||
PHOTOS_VERSION_ORIGINAL,
|
PHOTOS_VERSION_ORIGINAL,
|
||||||
PhotoLibrary,
|
|
||||||
PhotoKitFetchFailed,
|
PhotoKitFetchFailed,
|
||||||
|
PhotoLibrary,
|
||||||
)
|
)
|
||||||
from ..utils import dd_to_dms_str, findfiles, noop
|
from ..utils import dd_to_dms_str, findfiles, noop
|
||||||
|
|
||||||
@@ -885,9 +885,14 @@ def export2(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# export metadata
|
# export metadata
|
||||||
# TODO: repetitive code here is prime for refactoring
|
sidecars = []
|
||||||
sidecar_json_files_skipped = []
|
sidecar_json_files_skipped = []
|
||||||
sidecar_json_files_written = []
|
sidecar_json_files_written = []
|
||||||
|
sidecar_exiftool_files_skipped = []
|
||||||
|
sidecar_exiftool_files_written = []
|
||||||
|
sidecar_xmp_files_skipped = []
|
||||||
|
sidecar_xmp_files_written = []
|
||||||
|
|
||||||
if sidecar & SIDECAR_JSON:
|
if sidecar & SIDECAR_JSON:
|
||||||
sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}{dest.suffix}.json")
|
sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}{dest.suffix}.json")
|
||||||
sidecar_str = self._exiftool_json_sidecar(
|
sidecar_str = self._exiftool_json_sidecar(
|
||||||
@@ -897,35 +902,16 @@ def export2(
|
|||||||
description_template=description_template,
|
description_template=description_template,
|
||||||
ignore_date_modified=ignore_date_modified,
|
ignore_date_modified=ignore_date_modified,
|
||||||
)
|
)
|
||||||
sidecar_digest = hexdigest(sidecar_str)
|
sidecars.append(
|
||||||
old_sidecar_digest, sidecar_sig = export_db.get_sidecar_for_file(
|
(
|
||||||
sidecar_filename
|
|
||||||
)
|
|
||||||
write_sidecar = (
|
|
||||||
not update
|
|
||||||
or (update and not sidecar_filename.exists())
|
|
||||||
or (
|
|
||||||
update
|
|
||||||
and (sidecar_digest != old_sidecar_digest)
|
|
||||||
or not fileutil.cmp_file_sig(sidecar_filename, sidecar_sig)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if write_sidecar:
|
|
||||||
verbose(f"Writing JSON sidecar {sidecar_filename}")
|
|
||||||
sidecar_json_files_written.append(str(sidecar_filename))
|
|
||||||
if not dry_run:
|
|
||||||
self._write_sidecar(sidecar_filename, sidecar_str)
|
|
||||||
export_db.set_sidecar_for_file(
|
|
||||||
sidecar_filename,
|
sidecar_filename,
|
||||||
sidecar_digest,
|
sidecar_str,
|
||||||
fileutil.file_sig(sidecar_filename),
|
sidecar_json_files_written,
|
||||||
|
sidecar_json_files_skipped,
|
||||||
|
"JSON",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
verbose(f"Skipped up to date JSON sidecar {sidecar_filename}")
|
|
||||||
sidecar_json_files_skipped.append(str(sidecar_filename))
|
|
||||||
|
|
||||||
sidecar_exiftool_files_skipped = []
|
|
||||||
sidecar_exiftool_files_written = []
|
|
||||||
if sidecar & SIDECAR_EXIFTOOL:
|
if sidecar & SIDECAR_EXIFTOOL:
|
||||||
sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}{dest.suffix}.json")
|
sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}{dest.suffix}.json")
|
||||||
sidecar_str = self._exiftool_json_sidecar(
|
sidecar_str = self._exiftool_json_sidecar(
|
||||||
@@ -936,35 +922,16 @@ def export2(
|
|||||||
ignore_date_modified=ignore_date_modified,
|
ignore_date_modified=ignore_date_modified,
|
||||||
tag_groups=False,
|
tag_groups=False,
|
||||||
)
|
)
|
||||||
sidecar_digest = hexdigest(sidecar_str)
|
sidecars.append(
|
||||||
old_sidecar_digest, sidecar_sig = export_db.get_sidecar_for_file(
|
(
|
||||||
sidecar_filename
|
|
||||||
)
|
|
||||||
write_sidecar = (
|
|
||||||
not update
|
|
||||||
or (update and not sidecar_filename.exists())
|
|
||||||
or (
|
|
||||||
update
|
|
||||||
and (sidecar_digest != old_sidecar_digest)
|
|
||||||
or not fileutil.cmp_file_sig(sidecar_filename, sidecar_sig)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if write_sidecar:
|
|
||||||
verbose(f"Writing exiftool sidecar {sidecar_filename}")
|
|
||||||
sidecar_exiftool_files_written.append(str(sidecar_filename))
|
|
||||||
if not dry_run:
|
|
||||||
self._write_sidecar(sidecar_filename, sidecar_str)
|
|
||||||
export_db.set_sidecar_for_file(
|
|
||||||
sidecar_filename,
|
sidecar_filename,
|
||||||
sidecar_digest,
|
sidecar_str,
|
||||||
fileutil.file_sig(sidecar_filename),
|
sidecar_exiftool_files_written,
|
||||||
|
sidecar_exiftool_files_skipped,
|
||||||
|
"exiftool",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
verbose(f"Skipped up to date exiftool sidecar {sidecar_filename}")
|
|
||||||
sidecar_exiftool_files_skipped.append(str(sidecar_filename))
|
|
||||||
|
|
||||||
sidecar_xmp_files_skipped = []
|
|
||||||
sidecar_xmp_files_written = []
|
|
||||||
if sidecar & SIDECAR_XMP:
|
if sidecar & SIDECAR_XMP:
|
||||||
sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}{dest.suffix}.xmp")
|
sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}{dest.suffix}.xmp")
|
||||||
sidecar_str = self._xmp_sidecar(
|
sidecar_str = self._xmp_sidecar(
|
||||||
@@ -974,6 +941,23 @@ def export2(
|
|||||||
description_template=description_template,
|
description_template=description_template,
|
||||||
extension=dest.suffix[1:] if dest.suffix else None,
|
extension=dest.suffix[1:] if dest.suffix else None,
|
||||||
)
|
)
|
||||||
|
sidecars.append(
|
||||||
|
(
|
||||||
|
sidecar_filename,
|
||||||
|
sidecar_str,
|
||||||
|
sidecar_xmp_files_written,
|
||||||
|
sidecar_xmp_files_skipped,
|
||||||
|
"XMP",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for data in sidecars:
|
||||||
|
sidecar_filename = data[0]
|
||||||
|
sidecar_str = data[1]
|
||||||
|
files_written = data[2]
|
||||||
|
files_skipped = data[3]
|
||||||
|
sidecar_type = data[4]
|
||||||
|
|
||||||
sidecar_digest = hexdigest(sidecar_str)
|
sidecar_digest = hexdigest(sidecar_str)
|
||||||
old_sidecar_digest, sidecar_sig = export_db.get_sidecar_for_file(
|
old_sidecar_digest, sidecar_sig = export_db.get_sidecar_for_file(
|
||||||
sidecar_filename
|
sidecar_filename
|
||||||
@@ -988,8 +972,8 @@ def export2(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
if write_sidecar:
|
if write_sidecar:
|
||||||
verbose(f"Writing XMP sidecar {sidecar_filename}")
|
verbose(f"Writing {sidecar_type} sidecar {sidecar_filename}")
|
||||||
sidecar_xmp_files_written.append(str(sidecar_filename))
|
files_written.append(str(sidecar_filename))
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
self._write_sidecar(sidecar_filename, sidecar_str)
|
self._write_sidecar(sidecar_filename, sidecar_str)
|
||||||
export_db.set_sidecar_for_file(
|
export_db.set_sidecar_for_file(
|
||||||
@@ -998,8 +982,8 @@ def export2(
|
|||||||
fileutil.file_sig(sidecar_filename),
|
fileutil.file_sig(sidecar_filename),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
verbose(f"Skipped up to date XMP sidecar {sidecar_filename}")
|
verbose(f"Skipped up to date {sidecar_type} sidecar {sidecar_filename}")
|
||||||
sidecar_xmp_files_skipped.append(str(sidecar_filename))
|
files_skipped.append(str(sidecar_filename))
|
||||||
|
|
||||||
# if exiftool, write the metadata
|
# if exiftool, write the metadata
|
||||||
if update:
|
if update:
|
||||||
|
|||||||
@@ -2256,6 +2256,33 @@ def test_export_sidecar_update():
|
|||||||
assert "Writing JSON sidecar" in result.output
|
assert "Writing JSON sidecar" in result.output
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_sidecar_invalid():
|
||||||
|
""" test invalid combination of sidecars """
|
||||||
|
import os
|
||||||
|
|
||||||
|
from osxphotos.__main__ import cli
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
# pylint: disable=not-context-manager
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
result = runner.invoke(
|
||||||
|
cli,
|
||||||
|
[
|
||||||
|
"export",
|
||||||
|
"--db",
|
||||||
|
os.path.join(cwd, CLI_PHOTOS_DB),
|
||||||
|
".",
|
||||||
|
"--sidecar=json",
|
||||||
|
"--sidecar=exiftool",
|
||||||
|
f"--uuid={CLI_EXPORT_UUID}",
|
||||||
|
"-V",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code != 0
|
||||||
|
assert "Cannot use --sidecar json with --sidecar exiftool" in result.output
|
||||||
|
|
||||||
|
|
||||||
def test_export_live():
|
def test_export_live():
|
||||||
import glob
|
import glob
|
||||||
import os
|
import os
|
||||||
|
|||||||
Reference in New Issue
Block a user