Added --sidecar exiftool, issue #303

This commit is contained in:
Rhet Turnbull
2020-12-27 22:17:56 -08:00
parent 34841f86c0
commit d833c14ef4
6 changed files with 225 additions and 37 deletions

View File

@@ -1337,17 +1337,21 @@ def query(
default=None, default=None,
multiple=True, multiple=True,
metavar="FORMAT", metavar="FORMAT",
type=click.Choice(["xmp", "json"], case_sensitive=False), type=click.Choice(["xmp", "json", "exiftool"], case_sensitive=False),
help="Create sidecar for each photo exported; valid FORMAT values: xmp, json; " help="Create sidecar for each photo exported; valid FORMAT values: xmp, json, exiftool; "
f"--sidecar json: create JSON sidecar useable by exiftool ({_EXIF_TOOL_URL}) "
"The sidecar file can be used to apply metadata to the file with exiftool, for example: "
'"exiftool -j=photoname.jpg.json photoname.jpg" '
"The sidecar file is named in format photoname.ext.json "
"--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. "
f"\n--sidecar json: create JSON sidecar useable by exiftool ({_EXIF_TOOL_URL}) "
"The sidecar file can be used to apply metadata to the file with exiftool, for example: "
'"exiftool -j=photoname.jpg.json photoname.jpg" '
"The sidecar file is named in format photoname.ext.json; "
"format includes tag groups (equivalent to running 'exiftool -G -j'). "
"\n--sidecar exiftool: create JSON sidecar compatible with output of 'exiftool -j'. "
"Unlike --sidecar json, --sidecar exiftool does not export tag groups. "
"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 sidecar, see --exiftool.",
) )
@click.option( @click.option(
@@ -2098,21 +2102,6 @@ def export(
) )
results += export_results results += export_results
# print summary results
# print(f"results_exported: {results_exported}")
# print(f"results_new: {results_new}")
# print(f"results_updated: {results_updated}")
# print(f"results_skipped: {results_skipped}")
# print(f"results_exif_updated: {results_exif_updated}")
# print(f"results_touched: {results_touched}")
# print(f"results_converted: {results_converted}")
# print(f"results_sidecar_json_written: {results_sidecar_json_written}")
# print(f"results_sidecar_json_skipped: {results_sidecar_json_skipped}")
# print(f"results_sidecar_xmp_written: {results_sidecar_xmp_written}")
# print(f"results_sidecar_xmp_skipped: {results_sidecar_xmp_skipped}")
# print(f"results_missing: {results_missing}")
# print(f"results_error: {results_error}")
if cleanup: if cleanup:
all_files = ( all_files = (
results.exported results.exported
@@ -2122,6 +2111,8 @@ def export(
+ results.converted_to_jpeg + results.converted_to_jpeg
+ results.sidecar_json_written + results.sidecar_json_written
+ results.sidecar_json_skipped + results.sidecar_json_skipped
+ results.sidecar_exiftool_written
+ results.sidecar_exiftool_skipped
+ results.sidecar_xmp_written + results.sidecar_xmp_written
+ results.sidecar_xmp_skipped + results.sidecar_xmp_skipped
# include missing so a file that was already in export directory # include missing so a file that was already in export directory
@@ -2763,11 +2754,13 @@ def export_photo(
) )
sidecar = [s.lower() for s in sidecar] sidecar = [s.lower() for s in sidecar]
sidecar_json = sidecar_xmp = False sidecar_json, sidecar_xmp, sidecar_exiftool = False, False, False
if "json" in sidecar: if "json" in sidecar:
sidecar_json = True sidecar_json = True
if "xmp" in sidecar: if "xmp" in sidecar:
sidecar_xmp = True sidecar_xmp = True
if "exiftool" in sidecar:
sidecar_exiftool = True
# 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
@@ -2797,6 +2790,7 @@ def export_photo(
dest_path, dest_path,
original_filename, original_filename,
sidecar_json=sidecar_json, sidecar_json=sidecar_json,
sidecar_exiftool=sidecar_exiftool,
sidecar_xmp=sidecar_xmp, sidecar_xmp=sidecar_xmp,
live_photo=export_live, live_photo=export_live,
raw_photo=export_raw, raw_photo=export_raw,
@@ -2902,6 +2896,7 @@ def export_photo(
dest_path, dest_path,
edited_filename, edited_filename,
sidecar_json=sidecar_json, sidecar_json=sidecar_json,
sidecar_exiftool=sidecar_exiftool,
sidecar_xmp=sidecar_xmp, sidecar_xmp=sidecar_xmp,
export_as_hardlink=export_as_hardlink, export_as_hardlink=export_as_hardlink,
overwrite=overwrite, overwrite=overwrite,

View File

@@ -1,5 +1,5 @@
""" version info """ """ version info """
__version__ = "0.38.14" __version__ = "0.38.15"

View File

@@ -11,6 +11,7 @@
""" """
# TODO: should this be its own PhotoExporter class? # TODO: should this be its own PhotoExporter class?
# TODO: the various sidecar_json, sidecar_xmp, etc args should all be collapsed to a sidecar param using a bit mask
import glob import glob
import hashlib import hashlib
@@ -60,6 +61,8 @@ class ExportResults:
converted_to_jpeg=None, converted_to_jpeg=None,
sidecar_json_written=None, sidecar_json_written=None,
sidecar_json_skipped=None, sidecar_json_skipped=None,
sidecar_exiftool_written=None,
sidecar_exiftool_skipped=None,
sidecar_xmp_written=None, sidecar_xmp_written=None,
sidecar_xmp_skipped=None, sidecar_xmp_skipped=None,
missing=None, missing=None,
@@ -76,6 +79,8 @@ class ExportResults:
self.converted_to_jpeg = converted_to_jpeg or [] self.converted_to_jpeg = converted_to_jpeg or []
self.sidecar_json_written = sidecar_json_written or [] self.sidecar_json_written = sidecar_json_written or []
self.sidecar_json_skipped = sidecar_json_skipped or [] self.sidecar_json_skipped = sidecar_json_skipped or []
self.sidecar_exiftool_written = sidecar_exiftool_written or []
self.sidecar_exiftool_skipped = sidecar_exiftool_skipped or []
self.sidecar_xmp_written = sidecar_xmp_written or [] self.sidecar_xmp_written = sidecar_xmp_written or []
self.sidecar_xmp_skipped = sidecar_xmp_skipped or [] self.sidecar_xmp_skipped = sidecar_xmp_skipped or []
self.missing = missing or [] self.missing = missing or []
@@ -95,6 +100,8 @@ class ExportResults:
+ self.converted_to_jpeg + self.converted_to_jpeg
+ self.sidecar_json_written + self.sidecar_json_written
+ self.sidecar_json_skipped + self.sidecar_json_skipped
+ self.sidecar_exiftool_written
+ self.sidecar_exiftool_skipped
+ self.sidecar_xmp_written + self.sidecar_xmp_written
+ self.sidecar_xmp_skipped + self.sidecar_xmp_skipped
+ self.missing + self.missing
@@ -116,6 +123,8 @@ class ExportResults:
self.converted_to_jpeg += other.converted_to_jpeg self.converted_to_jpeg += other.converted_to_jpeg
self.sidecar_json_written += other.sidecar_json_written self.sidecar_json_written += other.sidecar_json_written
self.sidecar_json_skipped += other.sidecar_json_skipped self.sidecar_json_skipped += other.sidecar_json_skipped
self.sidecar_exiftool_written += other.sidecar_exiftool_written
self.sidecar_exiftool_skipped += other.sidecar_exiftool_skipped
self.sidecar_xmp_written += other.sidecar_xmp_written self.sidecar_xmp_written += other.sidecar_xmp_written
self.sidecar_xmp_skipped += other.sidecar_xmp_skipped self.sidecar_xmp_skipped += other.sidecar_xmp_skipped
self.missing += other.missing self.missing += other.missing
@@ -136,6 +145,8 @@ class ExportResults:
+ f",converted_to_jpeg={self.converted_to_jpeg}" + f",converted_to_jpeg={self.converted_to_jpeg}"
+ f",sidecar_json_written={self.sidecar_json_written}" + f",sidecar_json_written={self.sidecar_json_written}"
+ f",sidecar_json_skipped={self.sidecar_json_skipped}" + f",sidecar_json_skipped={self.sidecar_json_skipped}"
+ f",sidecar_exiftool_written={self.sidecar_exiftool_written}"
+ f",sidecar_exiftool_skipped={self.sidecar_exiftool_skipped}"
+ f",sidecar_xmp_written={self.sidecar_xmp_written}" + f",sidecar_xmp_written={self.sidecar_xmp_written}"
+ f",sidecar_xmp_skipped={self.sidecar_xmp_skipped}" + f",sidecar_xmp_skipped={self.sidecar_xmp_skipped}"
+ f",missing={self.missing}" + f",missing={self.missing}"
@@ -323,6 +334,7 @@ def export(
overwrite=False, overwrite=False,
increment=True, increment=True,
sidecar_json=False, sidecar_json=False,
sidecar_exiftool=False,
sidecar_xmp=False, sidecar_xmp=False,
use_photos_export=False, use_photos_export=False,
timeout=120, timeout=120,
@@ -352,8 +364,10 @@ def export(
overwrite: (boolean, default=False); if True will overwrite files if they alreay exist overwrite: (boolean, default=False); if True will overwrite files if they alreay exist
increment: (boolean, default=True); if True, will increment file name until a non-existant name is found increment: (boolean, default=True); if True, will increment file name until a non-existant name is found
if overwrite=False and increment=False, export will fail if destination file already exists if overwrite=False and increment=False, export will fail if destination file already exists
sidecar_json: (boolean, default = False); if True will also write a json sidecar with IPTC data in format readable by exiftool sidecar_json: (boolean, default = False); if True will also write a json sidecar with data in format readable by exiftool
sidecar filename will be dest/filename.json sidecar filename will be dest/filename.json, includes exiftool tag group names (e.g. `exiftool -G -j`)
sidecar_exiftool: (boolean, default = False); if True will also write a json sidecar with data in format readable by exiftool
sidecar filename will be dest/filename.json; does not include exiftool tag group names (e.g. `exiftool -j`)
sidecar_xmp: (boolean, default = False); if True will also write a XMP sidecar with IPTC data sidecar_xmp: (boolean, default = False); if True will also write a XMP sidecar with IPTC data
sidecar filename will be dest/filename.xmp sidecar filename will be dest/filename.xmp
use_photos_export: (boolean, default=False); if True will attempt to export photo via applescript interaction with Photos use_photos_export: (boolean, default=False); if True will attempt to export photo via applescript interaction with Photos
@@ -382,6 +396,7 @@ def export(
overwrite=overwrite, overwrite=overwrite,
increment=increment, increment=increment,
sidecar_json=sidecar_json, sidecar_json=sidecar_json,
sidecar_exiftool=sidecar_exiftool,
sidecar_xmp=sidecar_xmp, sidecar_xmp=sidecar_xmp,
use_photos_export=use_photos_export, use_photos_export=use_photos_export,
timeout=timeout, timeout=timeout,
@@ -406,6 +421,7 @@ def export2(
overwrite=False, overwrite=False,
increment=True, increment=True,
sidecar_json=False, sidecar_json=False,
sidecar_exiftool=False,
sidecar_xmp=False, sidecar_xmp=False,
use_photos_export=False, use_photos_export=False,
timeout=120, timeout=120,
@@ -445,8 +461,10 @@ def export2(
overwrite: (boolean, default=False); if True will overwrite files if they alreay exist overwrite: (boolean, default=False); if True will overwrite files if they alreay exist
increment: (boolean, default=True); if True, will increment file name until a non-existant name is found increment: (boolean, default=True); if True, will increment file name until a non-existant name is found
if overwrite=False and increment=False, export will fail if destination file already exists if overwrite=False and increment=False, export will fail if destination file already exists
sidecar_json: (boolean, default = False); if True will also write a json sidecar with IPTC data in format readable by exiftool sidecar_json: (boolean, default = False); if True will also write a json sidecar with data in format readable by exiftool
sidecar filename will be dest/filename.json sidecar filename will be dest/filename.json; includes exiftool tag group names (e.g. `exiftool -G -j`)
sidecar_exiftool: (boolean, default = False); if True will also write a json sidecar with data in format readable by exiftool
sidecar filename will be dest/filename.json; does not include exiftool tag group names (e.g. `exiftool -j`)
sidecar_xmp: (boolean, default = False); if True will also write a XMP sidecar with IPTC data sidecar_xmp: (boolean, default = False); if True will also write a XMP sidecar with IPTC data
sidecar filename will be dest/filename.xmp sidecar filename will be dest/filename.xmp
use_photos_export: (boolean, default=False); if True will attempt to export photo via applescript interaction with Photos use_photos_export: (boolean, default=False); if True will attempt to export photo via applescript interaction with Photos
@@ -483,6 +501,8 @@ def export2(
"converted_to_jpeg", "converted_to_jpeg",
"sidecar_json_written", "sidecar_json_written",
"sidecar_json_skipped", "sidecar_json_skipped",
"sidecar_exiftool_written",
"sidecar_exiftool_skipped",
"sidecar_xmp_written", "sidecar_xmp_written",
"sidecar_xmp_skipped", "sidecar_xmp_skipped",
"missing", "missing",
@@ -857,6 +877,7 @@ def export2(
) )
# export metadata # export metadata
# TODO: repetitive code here is prime for refactoring
sidecar_json_files_skipped = [] sidecar_json_files_skipped = []
sidecar_json_files_written = [] sidecar_json_files_written = []
if sidecar_json: if sidecar_json:
@@ -882,7 +903,7 @@ def export2(
) )
) )
if write_sidecar: if write_sidecar:
verbose(f"Writing exiftool JSON sidecar {sidecar_filename}") verbose(f"Writing JSON sidecar {sidecar_filename}")
sidecar_json_files_written.append(str(sidecar_filename)) sidecar_json_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)
@@ -892,9 +913,48 @@ def export2(
fileutil.file_sig(sidecar_filename), fileutil.file_sig(sidecar_filename),
) )
else: else:
verbose(f"Skipped up to date exiftool JSON sidecar {sidecar_filename}") verbose(f"Skipped up to date JSON sidecar {sidecar_filename}")
sidecar_json_files_skipped.append(str(sidecar_filename)) sidecar_json_files_skipped.append(str(sidecar_filename))
sidecar_exiftool_files_skipped = []
sidecar_exiftool_files_written = []
if sidecar_exiftool:
sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}{dest.suffix}.json")
sidecar_str = self._exiftool_json_sidecar(
use_albums_as_keywords=use_albums_as_keywords,
use_persons_as_keywords=use_persons_as_keywords,
keyword_template=keyword_template,
description_template=description_template,
ignore_date_modified=ignore_date_modified,
tag_groups=False,
)
sidecar_digest = hexdigest(sidecar_str)
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_digest,
fileutil.file_sig(sidecar_filename),
)
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_skipped = []
sidecar_xmp_files_written = [] sidecar_xmp_files_written = []
if sidecar_xmp: if sidecar_xmp:
@@ -1051,6 +1111,8 @@ def export2(
converted_to_jpeg=converted_to_jpeg_files, converted_to_jpeg=converted_to_jpeg_files,
sidecar_json_written=sidecar_json_files_written, sidecar_json_written=sidecar_json_files_written,
sidecar_json_skipped=sidecar_json_files_skipped, sidecar_json_skipped=sidecar_json_files_skipped,
sidecar_exiftool_written=sidecar_exiftool_files_written,
sidecar_exiftool_skipped=sidecar_exiftool_files_skipped,
sidecar_xmp_written=sidecar_xmp_files_written, sidecar_xmp_written=sidecar_xmp_files_written,
sidecar_xmp_skipped=sidecar_xmp_files_skipped, sidecar_xmp_skipped=sidecar_xmp_files_skipped,
error=errors, error=errors,
@@ -1233,6 +1295,8 @@ def _export_photo(
converted_to_jpeg=converted_to_jpeg_files, converted_to_jpeg=converted_to_jpeg_files,
sidecar_json_written=[], sidecar_json_written=[],
sidecar_json_skipped=[], sidecar_json_skipped=[],
sidecar_exiftool_written=[],
sidecar_exiftool_skipped=[],
sidecar_xmp_written=[], sidecar_xmp_written=[],
sidecar_xmp_skipped=[], sidecar_xmp_skipped=[],
missing=[], missing=[],
@@ -1482,6 +1546,7 @@ def _exiftool_json_sidecar(
keyword_template=None, keyword_template=None,
description_template=None, description_template=None,
ignore_date_modified=False, ignore_date_modified=False,
tag_groups=True,
): ):
"""Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool. """Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool.
Does not include all the EXIF fields as those are likely already in the image. Does not include all the EXIF fields as those are likely already in the image.
@@ -1492,6 +1557,7 @@ def _exiftool_json_sidecar(
keyword_template: (list of strings); list of template strings to render as keywords keyword_template: (list of strings); list of template strings to render as keywords
description_template: (list of strings); list of template strings to render for the description description_template: (list of strings); list of template strings to render for the description
ignore_date_modified: if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set ignore_date_modified: if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set
tag_groups: if True, tags are in form Group:TagName, e.g. IPTC:Keywords, otherwise group name is omitted, e.g. Keywords
Returns: dict with exiftool tags / values Returns: dict with exiftool tags / values
@@ -1524,6 +1590,15 @@ def _exiftool_json_sidecar(
description_template=description_template, description_template=description_template,
ignore_date_modified=ignore_date_modified, ignore_date_modified=ignore_date_modified,
) )
if not tag_groups:
# strip tag groups
exif_new = {}
for k, v in exif.items():
k = re.sub(r".*:", "", k)
exif_new[k] = v
exif = exif_new
return json.dumps([exif]) return json.dumps([exif])

View File

@@ -2003,6 +2003,38 @@ def test_export_sidecar():
assert sorted(files) == sorted(CLI_EXPORT_SIDECAR_FILENAMES) assert sorted(files) == sorted(CLI_EXPORT_SIDECAR_FILENAMES)
def test_export_sidecar_exiftool():
""" test --sidecar exiftool """
import glob
import os
import os.path
import osxphotos
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=exiftool",
"--sidecar=xmp",
f"--uuid={CLI_EXPORT_UUID}",
"-V",
],
)
assert result.exit_code == 0
assert "Writing exiftool sidecar" in result.output
files = glob.glob("*.*")
assert sorted(files) == sorted(CLI_EXPORT_SIDECAR_FILENAMES)
def test_export_sidecar_templates(): def test_export_sidecar_templates():
import json import json
import os import os
@@ -2045,6 +2077,49 @@ def test_export_sidecar_templates():
) )
def test_export_sidecar_templates_exiftool():
""" test --sidecar exiftool with templates """
import json
import os
import os.path
import osxphotos
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, PHOTOS_DB_15_5),
".",
"--sidecar=exiftool",
f"--uuid={CLI_UUID_DICT_15_5['template']}",
"-V",
"--keyword-template",
"{person}",
"--description-template",
"{descr} {person} {keyword} {album}",
],
)
assert result.exit_code == 0
assert os.path.isfile(CLI_TEMPLATE_SIDECAR_FILENAME)
with open(CLI_TEMPLATE_SIDECAR_FILENAME, "r") as jsonfile:
exifdata = json.load(jsonfile)
assert (
exifdata[0]["Description"]
== "Girls with pumpkins Katie, Suzy Kids Pumpkin Farm, Test Album"
)
assert (
exifdata[0]["ImageDescription"]
== "Girls with pumpkins Katie, Suzy Kids Pumpkin Farm, Test Album"
)
def test_export_sidecar_update(): def test_export_sidecar_update():
""" test sidecar don't update if not changed and do update if changed """ """ test sidecar don't update if not changed and do update if changed """
import datetime import datetime
@@ -2075,7 +2150,7 @@ def test_export_sidecar_update():
) )
assert result.exit_code == 0 assert result.exit_code == 0
assert "Writing XMP sidecar" in result.output assert "Writing XMP sidecar" in result.output
assert "Writing exiftool JSON sidecar" in result.output assert "Writing JSON sidecar" in result.output
# delete a sidecar file and run update # delete a sidecar file and run update
fileutil = FileUtil() fileutil = FileUtil()
@@ -2097,7 +2172,7 @@ def test_export_sidecar_update():
) )
assert result.exit_code == 0 assert result.exit_code == 0
assert "Skipped up to date XMP sidecar" in result.output assert "Skipped up to date XMP sidecar" in result.output
assert "Writing exiftool JSON sidecar" in result.output assert "Writing JSON sidecar" in result.output
# run update again, no sidecar files should update # run update again, no sidecar files should update
result = runner.invoke( result = runner.invoke(
@@ -2116,7 +2191,7 @@ def test_export_sidecar_update():
) )
assert result.exit_code == 0 assert result.exit_code == 0
assert "Skipped up to date XMP sidecar" in result.output assert "Skipped up to date XMP sidecar" in result.output
assert "Skipped up to date exiftool JSON sidecar" in result.output assert "Skipped up to date JSON sidecar" in result.output
# touch a file and export again # touch a file and export again
ts = datetime.datetime.now().timestamp() + 1000 ts = datetime.datetime.now().timestamp() + 1000
@@ -2138,7 +2213,7 @@ def test_export_sidecar_update():
) )
assert result.exit_code == 0 assert result.exit_code == 0
assert "Writing XMP sidecar" in result.output assert "Writing XMP sidecar" in result.output
assert "Skipped up to date exiftool JSON sidecar" in result.output assert "Skipped up to date JSON sidecar" in result.output
# run update again, no sidecar files should update # run update again, no sidecar files should update
result = runner.invoke( result = runner.invoke(
@@ -2157,7 +2232,7 @@ def test_export_sidecar_update():
) )
assert result.exit_code == 0 assert result.exit_code == 0
assert "Skipped up to date XMP sidecar" in result.output assert "Skipped up to date XMP sidecar" in result.output
assert "Skipped up to date exiftool JSON sidecar" in result.output assert "Skipped up to date JSON sidecar" in result.output
# run update again with updated metadata, forcing update # run update again with updated metadata, forcing update
result = runner.invoke( result = runner.invoke(
@@ -2178,7 +2253,7 @@ def test_export_sidecar_update():
) )
assert result.exit_code == 0 assert result.exit_code == 0
assert "Writing XMP sidecar" in result.output assert "Writing XMP sidecar" in result.output
assert "Writing exiftool JSON sidecar" in result.output assert "Writing JSON sidecar" in result.output
def test_export_live(): def test_export_live():
@@ -4442,7 +4517,7 @@ def test_save_load_config():
], ],
) )
assert result.exit_code == 0 assert result.exit_code == 0
assert "Writing exiftool JSON sidecar" in result.output assert "Writing JSON sidecar" in result.output
assert "Writing XMP sidecar" not in result.output assert "Writing XMP sidecar" not in result.output

View File

@@ -91,6 +91,21 @@ EXIF_JSON_EXPECTED = """
"EXIF:ModifyDate": "2019:07:27 17:33:28"}] "EXIF:ModifyDate": "2019:07:27 17:33:28"}]
""" """
EXIFTOOL_SIDECAR_EXPECTED = """
[{"ImageDescription": "Bride Wedding day",
"Description": "Bride Wedding day",
"TagsList": ["Maria", "wedding"],
"Keywords": ["Maria", "wedding"],
"PersonInImage": ["Maria"],
"Subject": ["wedding", "Maria"],
"DateTimeOriginal": "2019:04:15 14:40:24",
"CreateDate": "2019:04:15 14:40:24",
"OffsetTimeOriginal": "-04:00",
"DateCreated": "2019:04:15",
"TimeCreated": "14:40:24-04:00",
"ModifyDate": "2019:07:27 17:33:28"}]
"""
EXIF_JSON_EXPECTED_IGNORE_DATE_MODIFIED = """ EXIF_JSON_EXPECTED_IGNORE_DATE_MODIFIED = """
[{"EXIF:ImageDescription": "Bride Wedding day", [{"EXIF:ImageDescription": "Bride Wedding day",
"XMP:Description": "Bride Wedding day", "XMP:Description": "Bride Wedding day",
@@ -658,6 +673,30 @@ def test_exiftool_json_sidecar_use_albums_keyword(photosdb):
assert json_got[k] == v assert json_got[k] == v
def test_exiftool_sidecar(photosdb):
import json
photos = photosdb.photos(uuid=[EXIF_JSON_UUID])
json_expected = json.loads(EXIFTOOL_SIDECAR_EXPECTED)[0]
json_got = photos[0]._exiftool_json_sidecar(tag_groups=False)
json_got = json.loads(json_got)[0]
# some gymnastics to account for different sort order in different pythons
for k, v in json_got.items():
if type(v) in (list, tuple):
assert sorted(json_expected[k]) == sorted(v)
else:
assert json_expected[k] == v
for k, v in json_expected.items():
if type(v) in (list, tuple):
assert sorted(json_got[k]) == sorted(v)
else:
assert json_got[k] == v
@pytest.mark.skipif(exiftool is None, reason="exiftool not installed") @pytest.mark.skipif(exiftool is None, reason="exiftool not installed")
def test_xmp_sidecar_is_valid(tmp_path, photosdb): def test_xmp_sidecar_is_valid(tmp_path, photosdb):
""" validate XMP sidecar file with exiftool """ """ validate XMP sidecar file with exiftool """

View File

@@ -13,6 +13,8 @@ EXPORT_RESULT_ATTRIBUTES = [
"converted_to_jpeg", "converted_to_jpeg",
"sidecar_json_written", "sidecar_json_written",
"sidecar_json_skipped", "sidecar_json_skipped",
"sidecar_exiftool_written",
"sidecar_exiftool_skipped",
"sidecar_xmp_written", "sidecar_xmp_written",
"sidecar_xmp_skipped", "sidecar_xmp_skipped",
"missing", "missing",
@@ -33,6 +35,8 @@ def test_exportresults_init():
assert results.converted_to_jpeg == [] assert results.converted_to_jpeg == []
assert results.sidecar_json_written == [] assert results.sidecar_json_written == []
assert results.sidecar_json_skipped == [] assert results.sidecar_json_skipped == []
assert results.sidecar_exiftool_written == []
assert results.sidecar_exiftool_skipped == []
assert results.sidecar_xmp_written == [] assert results.sidecar_xmp_written == []
assert results.sidecar_xmp_skipped == [] assert results.sidecar_xmp_skipped == []
assert results.missing == [] assert results.missing == []
@@ -90,6 +94,6 @@ def test_str():
results = ExportResults() results = ExportResults()
assert ( assert (
str(results) str(results)
== "ExportResults(exported=[],new=[],updated=[],skipped=[],exif_updated=[],touched=[],converted_to_jpeg=[],sidecar_json_written=[],sidecar_json_skipped=[],sidecar_xmp_written=[],sidecar_xmp_skipped=[],missing=[],error=[],exiftool_warning=[],exiftool_error=[])" == "ExportResults(exported=[],new=[],updated=[],skipped=[],exif_updated=[],touched=[],converted_to_jpeg=[],sidecar_json_written=[],sidecar_json_skipped=[],sidecar_exiftool_written=[],sidecar_exiftool_skipped=[],sidecar_xmp_written=[],sidecar_xmp_skipped=[],missing=[],error=[],exiftool_warning=[],exiftool_error=[])"
) )