Implemented --favorite-rating, #732

This commit is contained in:
Rhet Turnbull 2022-07-23 18:16:06 -07:00
parent b4caea15fa
commit 5d33dcdcc3
5 changed files with 119 additions and 2 deletions

View File

@ -364,6 +364,13 @@ from .verbose import get_verbose_console, time_stamp, verbose_print
is_flag=True,
help="Merge any persons found in the original file with persons used for '--exiftool' and '--sidecar'.",
)
@click.option(
"--favorite-rating",
is_flag=True,
help="When used with --exiftool or --sidecar, "
"set XMP:Rating=5 for photos marked as Favorite and XMP:Rating=0 for non-Favorites. "
"If not specified, XMP:Rating is not set.",
)
@click.option(
"--ignore-date-modified",
is_flag=True,
@ -730,6 +737,7 @@ def export(
exportdb,
external_edit,
favorite,
favorite_rating,
filename_template,
finder_tag_keywords,
finder_tag_template,
@ -949,6 +957,7 @@ def export(
exportdb = cfg.exportdb
external_edit = cfg.external_edit
favorite = cfg.favorite
favorite_rating = cfg.favorite_rating
filename_template = cfg.filename_template
finder_tag_keywords = cfg.finder_tag_keywords
finder_tag_template = cfg.finder_tag_template
@ -1102,6 +1111,7 @@ def export(
dependent_options = [
("exiftool_merge_keywords", ("exiftool", "sidecar")),
("exiftool_merge_persons", ("exiftool", "sidecar")),
("favorite_rating", ("exiftool", "sidecar")),
("exiftool_option", ("exiftool")),
("ignore_signature", ("update", "force_update")),
("jpeg_quality", ("convert_to_jpeg")),
@ -1470,6 +1480,7 @@ def export(
export_live=export_live,
export_preview=preview,
export_raw=export_raw,
favorite_rating=favorite_rating,
filename_template=filename_template,
fileutil=fileutil,
force_update=force_update,
@ -1725,6 +1736,7 @@ def export_photo(
exiftool_merge_keywords=False,
exiftool_merge_persons=False,
directory=None,
favorite_rating=False,
filename_template=None,
export_raw=None,
album_keyword=None,
@ -1778,6 +1790,7 @@ def export_photo(
export_live: bool; also export live video component if photo is a live photo; live video will have same name as photo but with .mov extension
export_preview: export the preview image generated by Photos
export_raw: bool; if True exports raw image associate with the photo
favorite_rating: bool; if True, set XMP:Rating=5 for favorite images and XMP:Rating=0 for non-favorites
filename_template: template use to determine output file
fileutil: file util class compatible with FileUtilABC
force_update: bool, only export updated photos but trigger export even if only metadata has changed
@ -1943,6 +1956,7 @@ def export_photo(
export_original=export_original,
export_preview=export_preview,
export_raw=export_raw,
favorite_rating=favorite_rating,
filename=original_filename,
fileutil=fileutil,
force_update=force_update,
@ -2057,6 +2071,7 @@ def export_photo(
export_original=False,
export_preview=not export_original and export_preview,
export_raw=not export_original and export_raw,
favorite_rating=favorite_rating,
filename=edited_filename,
fileutil=fileutil,
force_update=force_update,
@ -2142,6 +2157,7 @@ def export_photo_to_directory(
export_original,
export_preview,
export_raw,
favorite_rating,
filename,
fileutil,
force_update,
@ -2203,6 +2219,7 @@ def export_photo_to_directory(
exiftool=exiftool,
export_as_hardlink=export_as_hardlink,
export_db=export_db,
favorite_rating=favorite_rating,
fileutil=fileutil,
force_update=force_update,
ignore_date_modified=ignore_date_modified,

View File

@ -136,6 +136,7 @@ class ExportOptions:
use_photokit (bool, default=False): if True, will use photokit to export photos when use_photos_export is True
verbose (callable): optional callable function to use for printing verbose text during processing; if None (default), does not print output.
tmpdir: (str, default=None): Optional directory to use for temporary files, if None (default) uses system tmp directory
favorite_rating (bool): if True, set XMP:Rating=5 for favorite images and XMP:Rating=0 for non-favorites
"""
@ -181,6 +182,7 @@ class ExportOptions:
use_photos_export: bool = False
verbose: t.Optional[t.Callable] = None
tmpdir: t.Optional[str] = None
favorite_rating: bool = False
def asdict(self):
return asdict(self)
@ -1586,6 +1588,7 @@ class PhotoExporter:
QuickTime:ModifyDate (UTC)
QuickTime:GPSCoordinates
UserData:GPSCoordinates
XMP:Rating
Reference:
https://iptc.org/std/photometadata/specification/IPTC-PhotoMetadata-201610_1.pdf
@ -1701,8 +1704,8 @@ class PhotoExporter:
if options.face_regions and self.photo.face_info:
exif.update(self._get_mwg_face_regions_exiftool())
# if self.favorite():
# exif["Rating"] = 5
if options.favorite_rating:
exif["XMP:Rating"] = 5 if self.photo.favorite else 0
if options.location:
(lat, lon) = self.photo.location
@ -2009,6 +2012,11 @@ class PhotoExporter:
latlon = self.photo.location if options.location else (None, None)
if options.favorite_rating:
rating = 5 if self.photo.favorite else 0
else:
rating = None
xmp_str = xmp_template.render(
photo=self.photo,
description=description,
@ -2018,6 +2026,7 @@ class PhotoExporter:
extension=extension,
location=latlon,
version=__version__,
rating=rating,
)
# remove extra lines that mako inserts from template

View File

@ -92,6 +92,12 @@
% endif
</%def>
<%def name="xmp_rating(rating)">
% if rating is not None:
<xmp:Rating>${rating}</xmp:Rating>
% endif
</%def>
<%def name="gps_info(latitude, longitude)">
% if latitude is not None and longitude is not None:
<exif:GPSLongitude>${int(abs(longitude))},${(abs(longitude) % 1) * 60}${"E" if longitude >= 0 else "W"}</exif:GPSLongitude>
@ -174,6 +180,7 @@
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
${adobe_createdate(photo.date)}
${adobe_modifydate(photo.date)}
${xmp_rating(rating)}
</rdf:Description>
<rdf:Description rdf:about=""

View File

@ -92,6 +92,12 @@
% endif
</%def>
<%def name="xmp_rating(rating)">
% if rating is not None:
<xmp:Rating>${rating}</xmp:Rating>
% endif
</%def>
<%def name="gps_info(latitude, longitude)">
% if latitude is not None and longitude is not None:
<exif:GPSLongitude>${int(abs(longitude))},${(abs(longitude) % 1) * 60}${"E" if longitude >= 0 else "W"}</exif:GPSLongitude>
@ -174,6 +180,7 @@
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
${adobe_createdate(photo.date)}
${adobe_modifydate(photo.date)}
${xmp_rating(rating)}
</rdf:Description>
<rdf:Description rdf:about=""

View File

@ -9,6 +9,7 @@ import os.path
import pathlib
import re
import shutil
import subprocess
import sqlite3
import tempfile
import time
@ -987,6 +988,11 @@ CLI_EXPORT_LIVE_EDITED = [
"IMG_4813_edited.mov",
]
UUID_FAVORITE = "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51"
FILE_FAVORITE = "wedding.jpg"
UUID_NOT_FAVORITE = "1EB2B765-0765-43BA-A90C-0D0580E6172C"
FILE_NOT_FAVORITE = "Pumpkins3.jpg"
def modify_file(filename):
"""appends data to a file to modify it"""
@ -2264,6 +2270,34 @@ def test_export_exiftool_merge_sidecar():
assert exif[key] == CLI_EXIFTOOL_MERGE[uuid][key]
@pytest.mark.skipif(exiftool is None, reason="exiftool not installed")
def test_export_exiftool_favorite_rating():
"""Test --exiftol --favorite-rating"""
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
for uuid in CLI_EXIFTOOL:
result = runner.invoke(
export,
[
os.path.join(cwd, PHOTOS_DB_15_7),
".",
"-V",
"--exiftool",
"--uuid",
UUID_FAVORITE,
"--uuid",
UUID_NOT_FAVORITE,
"--favorite-rating",
],
)
assert result.exit_code == 0
assert ExifTool(FILE_FAVORITE).asdict()["XMP:Rating"] == 5
assert ExifTool(FILE_NOT_FAVORITE).asdict()["XMP:Rating"] == 0
def test_export_edited_suffix():
"""test export with --edited-suffix"""
@ -3067,6 +3101,49 @@ def test_export_sidecar():
assert sorted(files) == sorted(CLI_EXPORT_SIDECAR_FILENAMES)
def test_export_sidecar_favorite_rating():
"""test --sidecar --favorite-rating"""
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
result = runner.invoke(
cli_main,
[
"export",
"--db",
os.path.join(cwd, CLI_PHOTOS_DB),
".",
"--sidecar=json",
"--sidecar=xmp",
f"--uuid={UUID_FAVORITE}",
f"--uuid={UUID_NOT_FAVORITE}",
"--favorite-rating",
"-V",
],
)
assert result.exit_code == 0
with open(f"{FILE_FAVORITE}.json") as fp:
json_sidecar = json.load(fp)
assert json_sidecar[0]["XMP:Rating"] == 5
with open(f"{FILE_NOT_FAVORITE}.json") as fp:
json_sidecar = json.load(fp)
assert json_sidecar[0]["XMP:Rating"] == 0
results = subprocess.run(
["grep", "xmp:Rating", f"{FILE_FAVORITE}.xmp"], capture_output=True
)
results_stdout = results.stdout.decode("utf-8")
assert "<xmp:Rating>5</xmp:Rating>" in results_stdout
results = subprocess.run(
["grep", "xmp:Rating", f"{FILE_NOT_FAVORITE}.xmp"], capture_output=True
)
results_stdout = results.stdout.decode("utf-8")
assert "<xmp:Rating>0</xmp:Rating>" in results_stdout
def test_export_sidecar_drop_ext():
"""test --sidecar with --sidecar-drop-ext option"""