Skip to content

Commit

Permalink
Implements #1373, --favorite-rating
Browse files Browse the repository at this point in the history
  • Loading branch information
RhetTbull committed Apr 28, 2024
1 parent 5fc2e17 commit 33e49f6
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 26 deletions.
62 changes: 60 additions & 2 deletions osxphotos/cli/import_cli.py
Expand Up @@ -61,7 +61,11 @@
metadata_from_sidecar,
)
from osxphotos.photoinfo import PhotoInfoNone
from osxphotos.photoinfo_file import render_photo_template, strip_edited_suffix
from osxphotos.photoinfo_file import (
PhotoInfoFromFile,
render_photo_template,
strip_edited_suffix,
)
from osxphotos.photosalbum import PhotosAlbumPhotoScript
from osxphotos.phototemplate import PhotoTemplate, RenderOptions
from osxphotos.sqlite_utils import sqlite_columns
Expand Down Expand Up @@ -502,6 +506,35 @@ def set_photo_location(
return location


def set_photo_favorite(
photo: Photo | None,
filepath: pathlib.Path,
sidecar_filepath: pathlib.Path | None,
exiftool_path: str,
favorite_rating: int | None,
verbose: Callable[..., None],
dry_run: bool,
):
"""Set favorite status of photo based on XMP:Rating value"""
rating = get_photo_rating(filepath, sidecar_filepath, exiftool_path)
if rating is not None and rating >= favorite_rating:
verbose(
f"Setting favorite status of photo [filename]{filepath.name}[/] (XMP:Rating=[num]{rating}[/])"
)
if photo and not dry_run:
photo.favorite = True


def get_photo_rating(
filepath: pathlib.Path, sidecar: pathlib.Path | None, exiftool_path: str
) -> int | None:
"""Get XMP:Rating from file"""
photoinfo = PhotoInfoFromFile(
filepath, exiftool=exiftool_path, sidecar=str(sidecar) if sidecar else None
)
return photoinfo.rating


def combine_date_time(
photo: Photo | None,
filepath: str | pathlib.Path,
Expand Down Expand Up @@ -1070,7 +1103,7 @@ def collect_files_to_import(
else:
continue

files_to_import = [pathlib.Path(f) for f in files_to_import]
files_to_import = [pathlib.Path(f).absolute() for f in files_to_import]

# strip any sidecar files
files_to_import = [
Expand Down Expand Up @@ -1198,6 +1231,7 @@ def import_files(
edited_suffix: str | None,
exiftool: bool,
exiftool_path: str,
favorite_rating: int | None,
sidecar: bool,
sidecar_ignore_date: bool,
sidecar_filename_template: str,
Expand Down Expand Up @@ -1402,6 +1436,17 @@ def import_files(
if location:
set_photo_location(photo, filepath, location, verbose, dry_run)

if favorite_rating:
set_photo_favorite(
photo,
filepath,
sidecar_file,
exiftool_path,
favorite_rating,
verbose,
dry_run,
)

if parse_date:
set_photo_date_from_filename(
photo, filepath.name, filepath.name, parse_date, verbose, dry_run
Expand Down Expand Up @@ -1856,6 +1901,16 @@ def get_help(self, ctx):
"Longitude is a number in the range -180.0 to 180.0; "
"positive longitudes are east of the Prime Meridian; negative longitudes are west of the Prime Meridian.",
)
@click.option(
"--favorite-rating",
"-G",
metavar="RATING",
type=click.IntRange(1, 5),
help="If XMP:Rating is set to RATING or higher, mark imported photo as a favorite. "
"RATING must be in range 1 to 5. "
"XMP:Rating will be read from asset's metadata or from sidecar if --sidecar, --sidecare-filename is used. "
"Requires that exiftool be installed to read the rating from the asset's XMP data.",
)
@click.option(
"--parse-date",
"-P",
Expand Down Expand Up @@ -2153,6 +2208,7 @@ def import_main(
edited_suffix: str | None,
exiftool: bool,
exiftool_path: str | None,
favorite_rating: int | None,
files: tuple[str, ...],
glob: tuple[str, ...],
keyword: tuple[str, ...],
Expand Down Expand Up @@ -2211,6 +2267,7 @@ def import_cli(
edited_suffix: str | None = None,
exiftool: bool = False,
exiftool_path: str | None = None,
favorite_rating: int | None = None,
files: tuple[str, ...] = (),
glob: tuple[str, ...] = (),
keyword: tuple[str, ...] = (),
Expand Down Expand Up @@ -2344,6 +2401,7 @@ def import_cli(
edited_suffix=edited_suffix,
exiftool=exiftool,
exiftool_path=exiftool_path,
favorite_rating=favorite_rating,
sidecar=sidecar,
sidecar_ignore_date=sidecar_ignore_date,
sidecar_filename_template=sidecar_filename_template,
Expand Down
6 changes: 6 additions & 0 deletions osxphotos/metadata_reader.py
Expand Up @@ -34,6 +34,7 @@ class MetaData:
keywords: list of keywords for photo
location: tuple of lat, long or None, None if not set
favorite: bool, True if photo marked favorite
rating: int, rating of photo 0-5
persons: list of persons in photo
date: datetime for photo as naive datetime.datetime in local timezone or None if not set
timezone: timezone or None of original date (before conversion to local naive datetime)
Expand All @@ -45,6 +46,7 @@ class MetaData:
keywords: list[str]
location: tuple[Optional[float], Optional[float]]
favorite: bool = False
rating: int = 0
persons: list[str] = field(default_factory=list)
date: datetime.datetime | None = None
timezone: datetime.tzinfo | None = None
Expand Down Expand Up @@ -212,6 +214,7 @@ def metadata_from_sidecar(
description: str, XMP:Description, IPTC:Caption-Abstract, EXIF:ImageDescription, QuickTime:Description
keywords: str, XMP:Subject, XMP:TagsList, IPTC:Keywords (QuickTime:Keywords not supported)
location: Tuple[lat, lon], EXIF:GPSLatitudeRef, EXIF:GPSLongitudeRef, EXIF:GPSLongitude, QuickTime:GPSCoordinates, UserData:GPSCoordinates
rating: int, XMP:Rating
Raises:
ValueError if error reading sidecar file
Expand Down Expand Up @@ -318,6 +321,8 @@ def metadata_from_metadata_dict(metadata: dict) -> MetaData:
or metadata.get("Keywords")
)

rating = metadata.get("XMP:Rating") or metadata.get("Rating")

persons = metadata.get("XMP:PersonInImage", []) or metadata.get("PersonInImage", [])
if persons and not isinstance(persons, (tuple, list)):
persons = [persons]
Expand All @@ -344,6 +349,7 @@ def metadata_from_metadata_dict(metadata: dict) -> MetaData:
description=description,
keywords=keywords,
location=location,
rating = rating or 0,
favorite=False,
persons=persons,
date=date,
Expand Down
5 changes: 5 additions & 0 deletions osxphotos/photoinfo_file.py
Expand Up @@ -108,6 +108,11 @@ def description(self) -> str | None:
"""description of picture"""
return self._metadata.description

@property
def rating(self) -> int:
"""rating of picture; reads XMP:Rating from the photo or sidecar file if available, else returns 0"""
return self._metadata.rating

@property
def fingerprint(self) -> str | None:
"""Returns fingerprint of original photo as a string or None if not on macOS"""
Expand Down
49 changes: 25 additions & 24 deletions tests/test-images/IMG_4179.jpeg.json
@@ -1,25 +1,26 @@
[
{
"SourceFile": "IMG_4179.jpeg",
"ExifToolVersion": "12.00",
"FileName": "IMG_4179.jpeg",
"ImageDescription": "Image Description",
"Description": "Image Description",
"Caption-Abstract": "Image Description",
"Title": "Image Title",
"ObjectName": "Image Title",
"Keywords": ["nature"],
"Subject": ["nature"],
"TagsList": ["nature"],
"GPSLatitude": 33.71506,
"GPSLongitude": -118.31967,
"GPSLatitudeRef": "N",
"GPSLongitudeRef": "W",
"DateTimeOriginal": "2021:04:08 16:04:55",
"CreateDate": "2021:04:08 16:04:55",
"OffsetTimeOriginal": "-07:00",
"DateCreated": "2021:04:08",
"TimeCreated": "16:04:55-07:00",
"ModifyDate": "2021:04:08 16:04:55"
}
]
{
"SourceFile": "IMG_4179.jpeg",
"ExifToolVersion": "12.00",
"FileName": "IMG_4179.jpeg",
"ImageDescription": "Image Description",
"Description": "Image Description",
"Caption-Abstract": "Image Description",
"Title": "Image Title",
"ObjectName": "Image Title",
"Keywords": ["nature"],
"Subject": ["nature"],
"TagsList": ["nature"],
"GPSLatitude": 33.71506,
"GPSLongitude": -118.31967,
"GPSLatitudeRef": "N",
"GPSLongitudeRef": "W",
"DateTimeOriginal": "2021:04:08 16:04:55",
"CreateDate": "2021:04:08 16:04:55",
"OffsetTimeOriginal": "-07:00",
"DateCreated": "2021:04:08",
"TimeCreated": "16:04:55-07:00",
"ModifyDate": "2021:04:08 16:04:55",
"Rating": 5
}
]
32 changes: 32 additions & 0 deletions tests/test_cli_import.py
Expand Up @@ -846,11 +846,43 @@ def test_import_sidecar_filename():
assert photo_1.title == TEST_DATA[TEST_IMAGE_1]["sidecar"]["title"]
assert photo_1.description == TEST_DATA[TEST_IMAGE_1]["sidecar"]["description"]
assert photo_1.keywords == TEST_DATA[TEST_IMAGE_1]["sidecar"]["keywords"]
assert not photo_1.favorite
lat, lon = photo_1.location
assert lat == approx(TEST_DATA[TEST_IMAGE_1]["sidecar"]["lat"])
assert lon == approx(TEST_DATA[TEST_IMAGE_1]["sidecar"]["lon"])


@pytest.mark.test_import
def test_import_favorite_rating():
"""Test import file with --favorite-rating"""
cwd = os.getcwd()
test_image_1 = os.path.join(cwd, TEST_IMAGE_1)
runner = CliRunner()
result = runner.invoke(
import_main,
[
"--verbose",
"--clear-metadata",
"--sidecar",
"--favorite-rating",
5,
test_image_1,
],
terminal_width=TERMINAL_WIDTH,
)

assert result.exit_code == 0
assert "Setting favorite status" in result.output

import_data = parse_import_output(result.output)
file_1 = pathlib.Path(test_image_1).name
uuid_1 = import_data[file_1]
photo_1 = Photo(uuid_1)

assert photo_1.filename == file_1
assert photo_1.favorite


@pytest.mark.test_import
def test_import_glob():
"""Test import with --glob"""
Expand Down

0 comments on commit 33e49f6

Please sign in to comment.