Compare commits

...

8 Commits

Author SHA1 Message Date
Rhet Turnbull
dce002cdfe Added --sidecar-drop-ext, issue #291 2020-12-28 11:54:02 -08:00
Rhet Turnbull
7bd189e9b2 Updated Template Substitution table 2020-12-28 09:26:38 -08:00
Rhet Turnbull
baa86c77f6 Updated CHANGELOG.md 2020-12-28 09:17:06 -08:00
Rhet Turnbull
0d086bf851 Added searchinfo templates, issue #302 2020-12-28 09:14:08 -08:00
Rhet Turnbull
ade98fc150 Refactored sidecar code 2020-12-28 08:23:23 -08:00
Rhet Turnbull
0d66759b1c Refactored export2 to use sidecar bit field 2020-12-27 22:45:47 -08:00
Rhet Turnbull
d833c14ef4 Added --sidecar exiftool, issue #303 2020-12-27 22:17:56 -08:00
Rhet Turnbull
34841f86c0 Updated CHANGELOG.md 2020-12-27 09:29:32 -08:00
10 changed files with 494 additions and 122 deletions

View File

@@ -4,6 +4,26 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [v0.38.16](https://github.com/RhetTbull/osxphotos/compare/v0.38.15...v0.38.16)
> 28 December 2020
- Added searchinfo templates, issue #302 [`0d086bf`](https://github.com/RhetTbull/osxphotos/commit/0d086bf85102ce78b3111c64bfa88673fbc19559)
#### [v0.38.15](https://github.com/RhetTbull/osxphotos/compare/v0.38.14...v0.38.15)
> 28 December 2020
- Added --sidecar exiftool, issue #303 [`d833c14`](https://github.com/RhetTbull/osxphotos/commit/d833c14ef4b3f9375a85034cf0fb0f85a68cabb4)
- Refactored sidecar code [`ade98fc`](https://github.com/RhetTbull/osxphotos/commit/ade98fc15051684bfb54d0199d9c370481b70dcc)
- Refactored export2 to use sidecar bit field [`0d66759`](https://github.com/RhetTbull/osxphotos/commit/0d66759b1c200f1ecda202e28c259f88fd3db599)
#### [v0.38.14](https://github.com/RhetTbull/osxphotos/compare/v0.38.13...v0.38.14)
> 27 December 2020
- Bug fix for --description-template, issue #304 [`4cc40d2`](https://github.com/RhetTbull/osxphotos/commit/4cc40d24cfb11ef8668c5d3c3bab40371fdd0436)
#### [v0.38.13](https://github.com/RhetTbull/osxphotos/compare/v0.38.12...v0.38.13)
> 27 December 2020

105
README.md
View File

@@ -301,23 +301,45 @@ Options:
the primary photo will be exported--
associated burst images will be skipped.
--sidecar FORMAT Create sidecar for each photo exported;
valid FORMAT values: xmp, json; --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
valid FORMAT values: xmp, json, exiftool;
--sidecar xmp: create XMP sidecar used by
Adobe Lightroom, etc. The sidecar file is
named in format photoname.ext.xmp The XMP
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
sidecar file is named in format
photoname.ext.json --sidecar xmp: create
XMP sidecar used by Adobe Lightroom, etc.The
sidecar file is named in format
photoname.ext.xmpThe XMP sidecar exports the
following tags: Description, Title,
Keywords/Tags, Subject (set to Keywords +
PersonInImage), PersonInImage, CreateDate,
ModifyDate, GPSLongitude. For a list of tags
exported in the JSON sidecar, see
--exiftool.
photoname.ext.json; format includes tag
groups (equivalent to running 'exiftool -G
-j').
--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 and exiftool sidecar,
see '--exiftool'.
--sidecar-drop-ext Drop the photo's extension when naming
sidecar files. By default, sidecar files are
named in format
'photo_filename.photo_ext.sidecar_ext', e.g.
'IMG_1234.JPG.json'. Use '--sidecar-drop-
ext' to ignore the photo extension.
Resulting sidecar files will have name in
format 'IMG_1234.json'. Warning: this may
result in sidecar filename collisions if
there are files of different types but the
same name in the output directory, e.g.
'IMG_1234.JPG' and 'IMG_1234.MOV'.
--exiftool Use exiftool to write metadata directly to
exported photos. To use this option,
exiftool must be installed and in the path.
@@ -327,14 +349,12 @@ Options:
metadata: EXIF:ImageDescription,
XMP:Description (see also --description-
template); XMP:Title; XMP:TagsList,
IPTC:Keywords (see also --keyword-template,
--person-keyword, --album-keyword);
XMP:Subject (set to keywords + person in
image to mirror Photos' behavior);
XMP:PersonInImage; EXIF:GPSLatitudeRef;
EXIF:GPSLongitudeRef; EXIF:GPSLatitude;
EXIF:GPSLongitude; EXIF:GPSPosition;
EXIF:DateTimeOriginal;
IPTC:Keywords, XMP:Subject (see also
--keyword-template, --person-keyword,
--album-keyword); XMP:PersonInImage;
EXIF:GPSLatitudeRef; EXIF:GPSLongitudeRef;
EXIF:GPSLatitude; EXIF:GPSLongitude;
EXIF:GPSPosition; EXIF:DateTimeOriginal;
EXIF:OffsetTimeOriginal; EXIF:ModifyDate
(see --ignore-date-modified);
IPTC:DateCreated; IPTC:TimeCreated; (video
@@ -718,6 +738,10 @@ Substitution Description
'United States'
{place.address.country_code} ISO country code of the postal address, e.g.
'US'
{searchinfo.season} Season of the year associated with a photo,
e.g. 'Summer'; (Photos 5+ only, applied
automatically by Photos' image
categorization algorithms).
The following substitutions may result in multiple values. Thus if specified
for --directory these could result in multiple copies of a photo being being
@@ -734,10 +758,10 @@ Substitution Description
{keyword} Keyword(s) assigned to photo
{person} Person(s) / face(s) in a photo
{label} Image categorization label associated with a photo
(Photos 5 only)
{label_normalized} All lower case version of 'label' (Photos 5 only)
(Photos 5+ only)
{label_normalized} All lower case version of 'label' (Photos 5+ only)
{comment} Comment(s) on shared Photos; format is 'Person
name: comment text' (Photos 5 only)
name: comment text' (Photos 5+ only)
{exiftool:GROUP:TAGNAME} Use exiftool (https://exiftool.org) to extract
metadata, in form GROUP:TAGNAME, from image. E.g.
'{exiftool:EXIF:Make}' to get camera make, or
@@ -746,6 +770,20 @@ Substitution Description
tag names. You must specify group (e.g. EXIF,
IPTC, etc) as used in `exiftool -G`. exiftool must
be installed in the path to use this template.
{searchinfo.holiday} Holiday names associated with a photo, e.g.
'Christmas Day'; (Photos 5+ only, applied
automatically by Photos' image categorization
algorithms).
{searchinfo.activity} Activities associated with a photo, e.g. 'Sporting
Event'; (Photos 5+ only, applied automatically by
Photos' image categorization algorithms).
{searchinfo.venue} Venues associated with a photo, e.g. name of
restaurant; (Photos 5+ only, applied automatically
by Photos' image categorization algorithms).
{searchinfo.venue_type} Venue types associated with a photo, e.g.
'Restaurant'; (Photos 5+ only, applied
automatically by Photos' image categorization
algorithms).
```
Example: export all photos to ~/Desktop/export group in folders by date created
@@ -1556,7 +1594,7 @@ Returns a JSON representation of all photo info.
Returns a dictionary representation of all photo info.
#### `export()`
`export(dest, *filename, edited=False, live_photo=False, export_as_hardlink=False, overwrite=False, increment=True, sidecar_json=False, sidecar_xmp=False, use_photos_export=False, timeout=120, exiftool=False, use_albums_as_keywords=False, use_persons_as_keywords=False)`
`export(dest, *filename, edited=False, live_photo=False, export_as_hardlink=False, overwrite=False, increment=True, sidecar_json=False, sidecar_exiftool=False, sidecar_xmp=False, use_photos_export=False, timeout=120, exiftool=False, use_albums_as_keywords=False, use_persons_as_keywords=False)`
Export photo from the Photos library to another destination on disk.
- dest: must be valid destination path as str (or exception raised).
@@ -1567,6 +1605,8 @@ Export photo from the Photos library to another destination on disk.
- live_photo: boolean; if True (default=False), will also export the associted .mov for live photos; exported live photo will be named filename.mov
- increment: boolean; if True (default=True), will increment file name until a non-existent name is found
- sidecar_json: (boolean, default = False); if True will also write a json sidecar with metadata in format readable by exiftool; sidecar filename will be dest/filename.json where filename is the stem of the photo name
- sidecar_json: (boolean, default = False); if True will also write a json sidecar with metadata in format readable by exiftool; sidecar filename will be dest/filename.json where filename is the stem of the photo name; resulting json file will include tag group names (e.g. `exiftool -G -j`)
- sidecar_exiftool: (boolean, default = False); if True will also write a json sidecar with metadata in format readable by exiftool; sidecar filename will be dest/filename.json where filename is the stem of the photo name; resulting json file will not include tag group names (e.g. `exiftool -j`)
- sidecar_xmp: (boolean, default = False); if True will also write a XMP sidecar with metadata; sidecar filename will be dest/filename.xmp where filename is the stem of the photo name
- use_photos_export: boolean; (default=False), if True will attempt to export photo via applescript interaction with Photos; useful for forcing download of missing photos. This only works if the Photos library being used is the default library (last opened by Photos) as applescript will directly interact with whichever library Photos is currently using.
- timeout: (int, default=120) timeout in seconds used with use_photos_export
@@ -2216,14 +2256,19 @@ The following template field substitutions are availabe for use with `PhotoInfo.
|{place.address.postal_code}|Postal code part of the postal address, e.g. '20009'|
|{place.address.country}|Country name of the postal address, e.g. 'United States'|
|{place.address.country_code}|ISO country code of the postal address, e.g. 'US'|
|{searchinfo.season}|Season of the year associated with a photo, e.g. 'Summer'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|{album}|Album(s) photo is contained in|
|{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|
|{keyword}|Keyword(s) assigned to photo|
|{person}|Person(s) / face(s) in a photo|
|{label}|Image categorization label associated with a photo (Photos 5 only)|
|{label_normalized}|All lower case version of 'label' (Photos 5 only)|
|{comment}|Comment(s) on shared Photos; format is 'Person name: comment text' (Photos 5 only)|
|{label}|Image categorization label associated with a photo (Photos 5+ only)|
|{label_normalized}|All lower case version of 'label' (Photos 5+ only)|
|{comment}|Comment(s) on shared Photos; format is 'Person name: comment text' (Photos 5+ only)|
|{exiftool:GROUP:TAGNAME}|Use exiftool (https://exiftool.org) to extract metadata, in form GROUP:TAGNAME, from image. E.g. '{exiftool:EXIF:Make}' to get camera make, or {exiftool:IPTC:Keywords} to extract keywords. See https://exiftool.org/TagNames/ for list of valid tag names. You must specify group (e.g. EXIF, IPTC, etc) as used in `exiftool -G`. exiftool must be installed in the path to use this template.|
|{searchinfo.holiday}|Holiday names associated with a photo, e.g. 'Christmas Day'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|{searchinfo.activity}|Activities associated with a photo, e.g. 'Sporting Event'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|{searchinfo.venue}|Venues associated with a photo, e.g. name of restaurant; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|{searchinfo.venue_type}|Venue types associated with a photo, e.g. 'Restaurant'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
### Utility Functions

View File

@@ -21,9 +21,12 @@ from ._constants import (
_UNKNOWN_PLACE,
CLI_COLOR_ERROR,
CLI_COLOR_WARNING,
DEFAULT_JPEG_QUALITY,
DEFAULT_EDITED_SUFFIX,
DEFAULT_JPEG_QUALITY,
DEFAULT_ORIGINAL_SUFFIX,
SIDECAR_EXIFTOOL,
SIDECAR_JSON,
SIDECAR_XMP,
UNICODE_FORMAT,
)
from ._version import __version__
@@ -1337,18 +1340,32 @@ def query(
default=None,
multiple=True,
metavar="FORMAT",
type=click.Choice(["xmp", "json"], case_sensitive=False),
help="Create sidecar for each photo exported; valid FORMAT values: xmp, json; "
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."
"The sidecar file is named in format photoname.ext.xmp"
type=click.Choice(["xmp", "json", "exiftool"], case_sensitive=False),
help="Create sidecar for each photo exported; valid FORMAT values: xmp, json, exiftool; "
"--sidecar xmp: create XMP sidecar used by Adobe Lightroom, etc. "
"The sidecar file is named in format photoname.ext.xmp "
"The XMP sidecar exports the following tags: Description, Title, Keywords/Tags, "
"Subject (set to Keywords + PersonInImage), PersonInImage, CreateDate, ModifyDate, "
"GPSLongitude. "
"For a list of tags exported in the JSON sidecar, see --exiftool.",
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 and exiftool sidecar, see '--exiftool'.",
)
@click.option(
"--sidecar-drop-ext",
is_flag=True,
help="Drop the photo's extension when naming sidecar files. "
"By default, sidecar files are named in format 'photo_filename.photo_ext.sidecar_ext', "
"e.g. 'IMG_1234.JPG.json'. Use '--sidecar-drop-ext' to ignore the photo extension. "
"Resulting sidecar files will have name in format 'IMG_1234.json'. "
"Warning: this may result in sidecar filename collisions if there are files of different "
"types but the same name in the output directory, e.g. 'IMG_1234.JPG' and 'IMG_1234.MOV'.",
)
@click.option(
"--exiftool",
@@ -1358,8 +1375,8 @@ def query(
"exiftool may be installed from https://exiftool.org/. "
"Cannot be used with --export-as-hardlink. Writes the following metadata: "
"EXIF:ImageDescription, XMP:Description (see also --description-template); "
"XMP:Title; XMP:TagsList, IPTC:Keywords (see also --keyword-template, --person-keyword, --album-keyword); "
"XMP:Subject (set to keywords + person in image to mirror Photos' behavior); "
"XMP:Title; XMP:TagsList, IPTC:Keywords, XMP:Subject "
"(see also --keyword-template, --person-keyword, --album-keyword); "
"XMP:PersonInImage; EXIF:GPSLatitudeRef; EXIF:GPSLongitudeRef; EXIF:GPSLatitude; EXIF:GPSLongitude; "
"EXIF:GPSPosition; EXIF:DateTimeOriginal; EXIF:OffsetTimeOriginal; "
"EXIF:ModifyDate (see --ignore-date-modified); IPTC:DateCreated; IPTC:TimeCreated; "
@@ -1564,6 +1581,7 @@ def export(
convert_to_jpeg,
jpeg_quality,
sidecar,
sidecar_drop_ext,
only_photos,
only_movies,
burst,
@@ -1745,6 +1763,7 @@ def export(
verbose_(f"osxphotos version {__version__}")
# validate options
exclusive_options = [
("favorite", "not_favorite"),
("hidden", "not_hidden"),
@@ -1788,6 +1807,16 @@ def export(
)
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:
verbose_(f"Saving options to file {save_config}")
cfg.write_to_file(save_config)
@@ -2018,6 +2047,7 @@ def export(
verbose=verbose,
export_by_date=export_by_date,
sidecar=sidecar,
sidecar_drop_ext=sidecar_drop_ext,
update=update,
ignore_signature=ignore_signature,
export_as_hardlink=export_as_hardlink,
@@ -2066,6 +2096,7 @@ def export(
verbose=verbose,
export_by_date=export_by_date,
sidecar=sidecar,
sidecar_drop_ext=sidecar_drop_ext,
update=update,
ignore_signature=ignore_signature,
export_as_hardlink=export_as_hardlink,
@@ -2098,21 +2129,6 @@ def export(
)
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:
all_files = (
results.exported
@@ -2122,6 +2138,8 @@ def export(
+ results.converted_to_jpeg
+ results.sidecar_json_written
+ results.sidecar_json_skipped
+ results.sidecar_exiftool_written
+ results.sidecar_exiftool_skipped
+ results.sidecar_xmp_written
+ results.sidecar_xmp_skipped
# include missing so a file that was already in export directory
@@ -2611,6 +2629,7 @@ def export_photo(
verbose=None,
export_by_date=None,
sidecar=None,
sidecar_drop_ext=False,
update=None,
ignore_signature=None,
export_as_hardlink=None,
@@ -2649,6 +2668,7 @@ def export_photo(
verbose: boolean; print verbose output
export_by_date: boolean; create export folder in form dest/YYYY/MM/DD
sidecar: list zero, 1 or 2 of ["json","xmp"] of sidecar variety to export
sidecar_drop_ext: boolean; if True, drops photo extension from sidecar name
export_as_hardlink: boolean; hardlink files instead of copying them
overwrite: boolean; overwrite dest file if it already exists
original_name: boolean; use original filename instead of current filename
@@ -2763,11 +2783,13 @@ def export_photo(
)
sidecar = [s.lower() for s in sidecar]
sidecar_json = sidecar_xmp = False
sidecar_flags = 0
if "json" in sidecar:
sidecar_json = True
sidecar_flags |= SIDECAR_JSON
if "xmp" in sidecar:
sidecar_xmp = True
sidecar_flags |= SIDECAR_XMP
if "exiftool" in sidecar:
sidecar_flags |= SIDECAR_EXIFTOOL
# if download_missing and the photo is missing or path doesn't exist,
# try to download with Photos
@@ -2796,8 +2818,8 @@ def export_photo(
export_results = photo.export2(
dest_path,
original_filename,
sidecar_json=sidecar_json,
sidecar_xmp=sidecar_xmp,
sidecar=sidecar_flags,
sidecar_drop_ext=sidecar_drop_ext,
live_photo=export_live,
raw_photo=export_raw,
export_as_hardlink=export_as_hardlink,
@@ -2901,8 +2923,8 @@ def export_photo(
export_results_edited = photo.export2(
dest_path,
edited_filename,
sidecar_json=sidecar_json,
sidecar_xmp=sidecar_xmp,
sidecar=sidecar_flags,
sidecar_drop_ext=sidecar_drop_ext,
export_as_hardlink=export_as_hardlink,
overwrite=overwrite,
edited=True,
@@ -3136,6 +3158,7 @@ def write_export_report(report_file, results):
"converted_to_jpeg": 0,
"sidecar_xmp": 0,
"sidecar_json": 0,
"sidecar_exiftool": 0,
"missing": 0,
"error": 0,
"exiftool_warning": "",
@@ -3181,6 +3204,14 @@ def write_export_report(report_file, results):
all_results[result]["sidecar_json"] = 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:
all_results[result]["missing"] = 1
@@ -3204,6 +3235,7 @@ def write_export_report(report_file, results):
"converted_to_jpeg",
"sidecar_xmp",
"sidecar_json",
"sidecar_exiftool",
"missing",
"error",
"exiftool_warning",

View File

@@ -178,3 +178,8 @@ DEFAULT_ORIGINAL_SUFFIX = ""
# Colors for print CLI messages
CLI_COLOR_ERROR = "red"
CLI_COLOR_WARNING = "yellow"
# Bit masks for --sidecar
SIDECAR_JSON = 0x1
SIDECAR_EXIFTOOL = 0x2
SIDECAR_XMP = 0x4

View File

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

View File

@@ -11,6 +11,7 @@
"""
# 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 hashlib
@@ -32,6 +33,9 @@ from .._constants import (
_TEMPLATE_DIR,
_UNKNOWN_PERSON,
_XMP_TEMPLATE_NAME,
SIDECAR_EXIFTOOL,
SIDECAR_JSON,
SIDECAR_XMP,
)
from ..datetime_utils import datetime_tz_to_utc
from ..exiftool import ExifTool
@@ -40,8 +44,8 @@ from ..fileutil import FileUtil
from ..photokit import (
PHOTOS_VERSION_CURRENT,
PHOTOS_VERSION_ORIGINAL,
PhotoLibrary,
PhotoKitFetchFailed,
PhotoLibrary,
)
from ..utils import dd_to_dms_str, findfiles, noop
@@ -60,6 +64,8 @@ class ExportResults:
converted_to_jpeg=None,
sidecar_json_written=None,
sidecar_json_skipped=None,
sidecar_exiftool_written=None,
sidecar_exiftool_skipped=None,
sidecar_xmp_written=None,
sidecar_xmp_skipped=None,
missing=None,
@@ -76,6 +82,8 @@ class ExportResults:
self.converted_to_jpeg = converted_to_jpeg or []
self.sidecar_json_written = sidecar_json_written 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_skipped = sidecar_xmp_skipped or []
self.missing = missing or []
@@ -95,6 +103,8 @@ class ExportResults:
+ self.converted_to_jpeg
+ self.sidecar_json_written
+ self.sidecar_json_skipped
+ self.sidecar_exiftool_written
+ self.sidecar_exiftool_skipped
+ self.sidecar_xmp_written
+ self.sidecar_xmp_skipped
+ self.missing
@@ -116,6 +126,8 @@ class ExportResults:
self.converted_to_jpeg += other.converted_to_jpeg
self.sidecar_json_written += other.sidecar_json_written
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_skipped += other.sidecar_xmp_skipped
self.missing += other.missing
@@ -136,6 +148,8 @@ class ExportResults:
+ f",converted_to_jpeg={self.converted_to_jpeg}"
+ f",sidecar_json_written={self.sidecar_json_written}"
+ 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_skipped={self.sidecar_xmp_skipped}"
+ f",missing={self.missing}"
@@ -323,6 +337,7 @@ def export(
overwrite=False,
increment=True,
sidecar_json=False,
sidecar_exiftool=False,
sidecar_xmp=False,
use_photos_export=False,
timeout=120,
@@ -352,10 +367,12 @@ def export(
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
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 filename will be dest/filename.json
sidecar_xmp: (boolean, default = False); if True will also write a XMP sidecar with IPTC data
sidecar filename will be dest/filename.xmp
sidecar_json: if set will write a json sidecar with data in format readable by exiftool
sidecar filename will be dest/filename.json; includes exiftool tag group names (e.g. `exiftool -G -j`)
sidecar_exiftool: if set will 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: if set will write an XMP sidecar with IPTC data
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
timeout: (int, default=120) timeout in seconds used with use_photos_export
exiftool: (boolean, default = False); if True, will use exiftool to write metadata to export file
@@ -372,6 +389,14 @@ def export(
# Implementation note: calls export2 to actually do the work
sidecar = 0
if sidecar_json:
sidecar |= SIDECAR_JSON
if sidecar_exiftool:
sidecar |= SIDECAR_EXIFTOOL
if sidecar_xmp:
sidecar |= SIDECAR_XMP
results = self.export2(
dest,
*filename,
@@ -381,8 +406,7 @@ def export(
export_as_hardlink=export_as_hardlink,
overwrite=overwrite,
increment=increment,
sidecar_json=sidecar_json,
sidecar_xmp=sidecar_xmp,
sidecar=sidecar,
use_photos_export=use_photos_export,
timeout=timeout,
exiftool=exiftool,
@@ -405,8 +429,8 @@ def export2(
export_as_hardlink=False,
overwrite=False,
increment=True,
sidecar_json=False,
sidecar_xmp=False,
sidecar=0,
sidecar_drop_ext=False,
use_photos_export=False,
timeout=120,
exiftool=False,
@@ -445,10 +469,14 @@ def export2(
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
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 filename will be dest/filename.json
sidecar_xmp: (boolean, default = False); if True will also write a XMP sidecar with IPTC data
sidecar: bit field: set to one or more of SIDECAR_XMP, SIDECAR_JSON, SIDECAR_EXIFTOOL
SIDECAR_JSON: if set will write a json sidecar with data in format readable by exiftool
sidecar filename will be dest/filename.json; includes exiftool tag group names (e.g. `exiftool -G -j`)
SIDECAR_EXIFTOOL: if set will 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: if set will write an XMP sidecar with IPTC data
sidecar filename will be dest/filename.xmp
sidecar_drop_ext: (boolean, default=False); if True, drops the photo's extension from sidecar filename (e.g. 'IMG_1234.json' instead of 'IMG_1234.JPG.json')
use_photos_export: (boolean, default=False); if True will attempt to export photo via applescript interaction with Photos
timeout: (int, default=120) timeout in seconds used with use_photos_export
exiftool: (boolean, default = False); if True, will use exiftool to write metadata to export file
@@ -483,6 +511,8 @@ def export2(
"converted_to_jpeg",
"sidecar_json_written",
"sidecar_json_skipped",
"sidecar_exiftool_written",
"sidecar_exiftool_skipped",
"sidecar_xmp_written",
"sidecar_xmp_skipped",
"missing",
@@ -857,10 +887,17 @@ def export2(
)
# export metadata
sidecars = []
sidecar_json_files_skipped = []
sidecar_json_files_written = []
if sidecar_json:
sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}{dest.suffix}.json")
sidecar_exiftool_files_skipped = []
sidecar_exiftool_files_written = []
sidecar_xmp_files_skipped = []
sidecar_xmp_files_written = []
dest_suffix = "" if sidecar_drop_ext else dest.suffix
if sidecar & SIDECAR_JSON:
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,
@@ -868,37 +905,38 @@ def export2(
description_template=description_template,
ignore_date_modified=ignore_date_modified,
)
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)
sidecars.append(
(
sidecar_filename,
sidecar_str,
sidecar_json_files_written,
sidecar_json_files_skipped,
"JSON",
)
)
if write_sidecar:
verbose(f"Writing exiftool 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_digest,
fileutil.file_sig(sidecar_filename),
)
else:
verbose(f"Skipped up to date exiftool JSON sidecar {sidecar_filename}")
sidecar_json_files_skipped.append(str(sidecar_filename))
sidecar_xmp_files_skipped = []
sidecar_xmp_files_written = []
if sidecar_xmp:
sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}{dest.suffix}.xmp")
if sidecar & 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,
)
sidecars.append(
(
sidecar_filename,
sidecar_str,
sidecar_exiftool_files_written,
sidecar_exiftool_files_skipped,
"exiftool",
)
)
if sidecar & SIDECAR_XMP:
sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}{dest_suffix}.xmp")
sidecar_str = self._xmp_sidecar(
use_albums_as_keywords=use_albums_as_keywords,
use_persons_as_keywords=use_persons_as_keywords,
@@ -906,6 +944,23 @@ def export2(
description_template=description_template,
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)
old_sidecar_digest, sidecar_sig = export_db.get_sidecar_for_file(
sidecar_filename
@@ -920,8 +975,8 @@ def export2(
)
)
if write_sidecar:
verbose(f"Writing XMP sidecar {sidecar_filename}")
sidecar_xmp_files_written.append(str(sidecar_filename))
verbose(f"Writing {sidecar_type} sidecar {sidecar_filename}")
files_written.append(str(sidecar_filename))
if not dry_run:
self._write_sidecar(sidecar_filename, sidecar_str)
export_db.set_sidecar_for_file(
@@ -930,8 +985,8 @@ def export2(
fileutil.file_sig(sidecar_filename),
)
else:
verbose(f"Skipped up to date XMP sidecar {sidecar_filename}")
sidecar_xmp_files_skipped.append(str(sidecar_filename))
verbose(f"Skipped up to date {sidecar_type} sidecar {sidecar_filename}")
files_skipped.append(str(sidecar_filename))
# if exiftool, write the metadata
if update:
@@ -1051,6 +1106,8 @@ def export2(
converted_to_jpeg=converted_to_jpeg_files,
sidecar_json_written=sidecar_json_files_written,
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_skipped=sidecar_xmp_files_skipped,
error=errors,
@@ -1233,6 +1290,8 @@ def _export_photo(
converted_to_jpeg=converted_to_jpeg_files,
sidecar_json_written=[],
sidecar_json_skipped=[],
sidecar_exiftool_written=[],
sidecar_exiftool_skipped=[],
sidecar_xmp_written=[],
sidecar_xmp_skipped=[],
missing=[],
@@ -1482,6 +1541,7 @@ def _exiftool_json_sidecar(
keyword_template=None,
description_template=None,
ignore_date_modified=False,
tag_groups=True,
):
"""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.
@@ -1492,6 +1552,7 @@ def _exiftool_json_sidecar(
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
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
@@ -1524,6 +1585,15 @@ def _exiftool_json_sidecar(
description_template=description_template,
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])

View File

@@ -117,6 +117,7 @@ TEMPLATE_SUBSTITUTIONS = {
"{place.address.postal_code}": "Postal code part of the postal address, e.g. '20009'",
"{place.address.country}": "Country name of the postal address, e.g. 'United States'",
"{place.address.country_code}": "ISO country code of the postal address, e.g. 'US'",
"{searchinfo.season}": "Season of the year associated with a photo, e.g. 'Summer'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).",
}
# Permitted multi-value substitutions (each of these returns None or 1 or more values)
@@ -125,13 +126,17 @@ TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
"{folder_album}": "Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder",
"{keyword}": "Keyword(s) assigned to photo",
"{person}": "Person(s) / face(s) in a photo",
"{label}": "Image categorization label associated with a photo (Photos 5 only)",
"{label_normalized}": "All lower case version of 'label' (Photos 5 only)",
"{comment}": "Comment(s) on shared Photos; format is 'Person name: comment text' (Photos 5 only)",
"{label}": "Image categorization label associated with a photo (Photos 5+ only)",
"{label_normalized}": "All lower case version of 'label' (Photos 5+ only)",
"{comment}": "Comment(s) on shared Photos; format is 'Person name: comment text' (Photos 5+ only)",
"{exiftool:GROUP:TAGNAME}": "Use exiftool (https://exiftool.org) to extract metadata, in form GROUP:TAGNAME, from image. "
"E.g. '{exiftool:EXIF:Make}' to get camera make, or {exiftool:IPTC:Keywords} to extract keywords. "
"See https://exiftool.org/TagNames/ for list of valid tag names. You must specify group (e.g. EXIF, IPTC, etc) "
"as used in `exiftool -G`. exiftool must be installed in the path to use this template.",
"{searchinfo.holiday}": "Holiday names associated with a photo, e.g. 'Christmas Day'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).",
"{searchinfo.activity}": "Activities associated with a photo, e.g. 'Sporting Event'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).",
"{searchinfo.venue}": "Venues associated with a photo, e.g. name of restaurant; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).",
"{searchinfo.venue_type}": "Venue types associated with a photo, e.g. 'Restaurant'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).",
}
# Just the multi-valued substitution names without the braces
@@ -842,6 +847,8 @@ class PhotoTemplate:
if self.photo.place and self.photo.place.address.iso_country_code
else None
)
elif field == "searchinfo.season":
value = self.photo.search_info.season if self.photo.search_info else None
else:
# if here, didn't get a match
raise ValueError(f"Unhandled template value: {field}")
@@ -914,6 +921,16 @@ class PhotoTemplate:
values = [
f"{comment.user}: {comment.text}" for comment in self.photo.comments
]
elif field == "searchinfo.holiday":
values = self.photo.search_info.holidays if self.photo.search_info else []
elif field == "searchinfo.activity":
values = self.photo.search_info.activities if self.photo.search_info else []
elif field == "searchinfo.venue":
values = self.photo.search_info.venues if self.photo.search_info else []
elif field == "searchinfo.venue_type":
values = (
self.photo.search_info.venue_types if self.photo.search_info else []
)
elif not field.startswith("exiftool:"):
raise ValueError(f"Unhandled template value: {field}")

View File

@@ -306,6 +306,11 @@ CLI_EXPORT_BY_DATE_NEED_TOUCH_TIMES = [1538165227, 1539436692]
CLI_EXPORT_BY_DATE = ["2018/09/28/Pumpkins3.jpg", "2018/09/28/Pumkins1.jpg"]
CLI_EXPORT_SIDECAR_FILENAMES = ["Pumkins2.jpg", "Pumkins2.jpg.json", "Pumkins2.jpg.xmp"]
CLI_EXPORT_SIDECAR_DROP_EXT_FILENAMES = [
"Pumkins2.jpg",
"Pumkins2.json",
"Pumkins2.xmp",
]
CLI_EXPORT_LIVE = [
"51F2BEF7-431A-4D31-8AC1-3284A57826AE.jpeg",
@@ -1974,6 +1979,7 @@ def test_query_deleted_4():
def test_export_sidecar():
""" test --sidecar """
import glob
import os
import os.path
@@ -2003,6 +2009,70 @@ def test_export_sidecar():
assert sorted(files) == sorted(CLI_EXPORT_SIDECAR_FILENAMES)
def test_export_sidecar_drop_ext():
""" test --sidecar with --sidecar-drop-ext option """
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=json",
"--sidecar=xmp",
"--sidecar-drop-ext",
f"--uuid={CLI_EXPORT_UUID}",
"-V",
],
)
assert result.exit_code == 0
files = glob.glob("*.*")
assert sorted(files) == sorted(CLI_EXPORT_SIDECAR_DROP_EXT_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():
import json
import os
@@ -2045,6 +2115,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():
""" test sidecar don't update if not changed and do update if changed """
import datetime
@@ -2075,7 +2188,7 @@ def test_export_sidecar_update():
)
assert result.exit_code == 0
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
fileutil = FileUtil()
@@ -2097,7 +2210,7 @@ def test_export_sidecar_update():
)
assert result.exit_code == 0
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
result = runner.invoke(
@@ -2116,7 +2229,7 @@ def test_export_sidecar_update():
)
assert result.exit_code == 0
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
ts = datetime.datetime.now().timestamp() + 1000
@@ -2138,7 +2251,7 @@ def test_export_sidecar_update():
)
assert result.exit_code == 0
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
result = runner.invoke(
@@ -2157,7 +2270,7 @@ def test_export_sidecar_update():
)
assert result.exit_code == 0
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
result = runner.invoke(
@@ -2178,7 +2291,34 @@ def test_export_sidecar_update():
)
assert result.exit_code == 0
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_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():
@@ -4442,7 +4582,7 @@ def test_save_load_config():
],
)
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

View File

@@ -91,6 +91,21 @@ EXIF_JSON_EXPECTED = """
"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:ImageDescription": "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
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")
def test_xmp_sidecar_is_valid(tmp_path, photosdb):
""" validate XMP sidecar file with exiftool """

View File

@@ -13,6 +13,8 @@ EXPORT_RESULT_ATTRIBUTES = [
"converted_to_jpeg",
"sidecar_json_written",
"sidecar_json_skipped",
"sidecar_exiftool_written",
"sidecar_exiftool_skipped",
"sidecar_xmp_written",
"sidecar_xmp_skipped",
"missing",
@@ -33,6 +35,8 @@ def test_exportresults_init():
assert results.converted_to_jpeg == []
assert results.sidecar_json_written == []
assert results.sidecar_json_skipped == []
assert results.sidecar_exiftool_written == []
assert results.sidecar_exiftool_skipped == []
assert results.sidecar_xmp_written == []
assert results.sidecar_xmp_skipped == []
assert results.missing == []
@@ -90,6 +94,6 @@ def test_str():
results = ExportResults()
assert (
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=[])"
)