From: Scott Gasch Date: Tue, 7 Apr 2026 02:28:57 +0000 (-0700) Subject: Several improvements to the photo renderer. X-Git-Url: https://git.acknak.org/gitweb/?a=commitdiff_plain;h=bd7e07b5977362bdc269c05a104f5967919944f1;p=kiosk.git Several improvements to the photo renderer. --- diff --git a/immich_photo_renderer.py b/immich_photo_renderer.py index 5f6fb7b..be6e5db 100644 --- a/immich_photo_renderer.py +++ b/immich_photo_renderer.py @@ -4,7 +4,7 @@ import datetime import os import random import re -from typing import Dict, List, Set +from typing import Any, Dict, List, Optional, Set from scottutilz import immich @@ -47,21 +47,51 @@ def natural_join(items: List[str], use_oxford_comma: bool = True) -> str: suffix: str = f", and {items[-1]}" if use_oxford_comma else f" and {items[-1]}" return f"{prefix}{suffix}" -class immich_photo_renderer(renderer.abstaining_renderer): - """A renderer that uses immich photos""" +def parse_exif_date(exif_data: dict[str, Any]) -> Optional[datetime.datetime]: + date_str: Optional[str] = exif_data.get('dateTimeOriginal') + + if not date_str: + return None + + try: + # Attempt 1: Standard ISO format + return datetime.datetime.fromisoformat(date_str) + except ValueError: + try: + # Attempt 2: Handle common EXIF format 'YYYY:MM:DD HH:MM:SS' + clean_date = date_str.split('+')[0].split('-')[0] + return datetime.datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S.%f%z") + except ValueError: + # Final Fallback: Return current time or a 'null' date to prevent crash + print(f"Warning: Could not parse date string: {date_str}") + return None + +def get_layout_class(asset: dict[str, Any]) -> str: + exif = asset.get('exifInfo', {}) + width: Optional[int] = exif.get('exifImageWidth') + height: Optional[int] = exif.get('exifImageHeight') + + if not width or not height: + return 'standard_mode' + + # Use the actual orientation if the EXIF swap is happening + # Panorama strictly means "Much wider than it is tall" + aspect_ratio = float(width) / float(height) - album_whitelist = frozenset( - [ - 'Family, ALL', - 'Street Art, ALL', - 'Artsy', - 'Louisville, 2025: Google Folks', - 'France / Spain ALL, 2025', - 'Friends, ALL', - 'Spain and Portugal ALL, 2024', - ] - ) + # Figure out 90 degree rotations... + orientation = exif.get('orientation') + if orientation: + orientation = int(orientation) + if orientation in [6, 8]: + aspect_ratio = float(1) / aspect_ratio + if aspect_ratio > 1.5: + return 'panorama_mode' + return 'standard_mode' + + +class immich_photo_renderer(renderer.abstaining_renderer): + """A renderer that uses immich photos""" def __init__(self, name_to_timeout_dict: Dict[str, int]) -> None: super().__init__(name_to_timeout_dict) self.immich_cli = immich.ImmichSession( @@ -81,25 +111,34 @@ class immich_photo_renderer(renderer.abstaining_renderer): raise Exception("Unexpected operation") def index_photos(self) -> bool: - for album_name in self.album_whitelist: - album = self.immich_cli.search_album(album_name)[0] - if not album: - print(f"Album name '{album_name}' was not found, skipped...") - continue - album_uuid = album.get('id', None) - if not album_uuid: - print(f"Album {album} has no id?!?!") - continue - for photo in self.immich_cli.search_all_assets(album_id=[album_uuid], tag_id=["0ded3e16-2a34-4785-9681-f5f49ed6b4d8"]): - photo_uuid = photo.get('id', None) - if photo_uuid: - self.candidate_photo_uuids.add(photo_uuid) - for photo in self.immich_cli.search_all_assets(album_id=[album_uuid], tag_id=["a28a3f61-773d-4d5c-8569-9f255d40a7f8"]): - photo_uuid = photo.get('id', None) - if photo_uuid: - self.candidate_photo_uuids.add(photo_uuid) + all_assets = ( + self.immich_cli.search_all_assets( + tag_id=["0ded3e16-2a34-4785-9681-f5f49ed6b4d8"] + ) + + self.immich_cli.search_all_assets( + tag_id=["a28a3f61-773d-4d5c-8569-9f255d40a7f8"] + ) + ) + print(f"{len(all_assets)} kiosk assets.") + for lite_asset in all_assets: + asset_uuid = lite_asset.get('id') + if asset_uuid: + self.candidate_photo_uuids.add(asset_uuid) return True + @staticmethod + def _highlight_hashtags(text: Any, color: str = "#888888") -> str: + # Regex breakdown: + # (# -> Start a capture group beginning with # + # [^#]+ -> Match one or more characters that are NOT a # + # ) -> End capture group + # (?=#|$) -> Lookahead: ensure the next char is a # OR the end of string + return re.sub( + r"(#[^#]+)(?=#|$)", + lambda m: f'{m.group(1).strip()} ', + text + ).strip() + def choose_photo(self): """Pick one of the cached URLs and build a page.""" if len(self.candidate_photo_uuids) == 0: @@ -110,18 +149,31 @@ class immich_photo_renderer(renderer.abstaining_renderer): if not asset: print(f"Asset {selected_uuid} is unknown?!") return False + try: jpeg = self.immich_cli.get_asset_thumbnail(selected_uuid, size="fullsize") with open("/var/www/kiosk/pages/mfs/current.jpeg", "wb") as f: f.write(jpeg) except Exception: - logger.exception("Can't fetch/write the photo?!") + print("Failed to get image!") return False + # Simple aspect ratio check for layout decision. + layout_class = get_layout_class(asset) + + # Albums list... + try: + albums = self.immich_cli.get_asset_albums(selected_uuid) + except Exception: + print("Failed to get albums!") + albums = [] + + # Location, description, etc... exif = asset.get('exifInfo', {}) location = "Unknown location" + descr = '' if exif: - date = datetime.datetime.fromisoformat(exif.get('dateTimeOriginal', None)) + date = parse_exif_date(exif) city = exif.get("city", "") state = exif.get("state", "") country = exif.get("country", "") @@ -145,46 +197,264 @@ class immich_photo_renderer(renderer.abstaining_renderer): else: location = format_coords(lat, lon) descr = exif.get("description", "") + if not descr: people = asset.get('people', []) if people: descr = "A photo of " descr += natural_join([p.get("name", "") for p in asset.get("people", [])]) else: - descr = "No description data" - with file_writer.file_writer("photo_23_3600.html") as f: + descr = "No description data available. #needs description" + descr = self._highlight_hashtags(descr) + + album_html = '' + for album in albums: + album_name = album.get('albumName') + album_cover_uuid = album.get('albumThumbnailAssetId') + album_cover = self.immich_cli.get_asset_thumbnail( + album_cover_uuid, size="thumbnail" + ) + with open(f"/var/www/kiosk/pages/mfs/{album_cover_uuid}.jpeg", "wb") as f: + f.write(album_cover) + album_html += f""" +
+ Album Art + {album_name} +
+""" + with file_writer.file_writer("photo_33_3600.html") as f: f.write( f""" - - - - - -
-
- -
-
- - {descr}
-
- {location} -
- {date.strftime("%B %-d, %Y")}
-
-
-""") +
+ +
+ Current kiosk photo +
+ + +
+
+ {descr} +
+
+
+
+ {location} +
+
+ {date.strftime("%B %-d, %Y")} +
+
+
Appears In
+
+ {album_html} +
+
+
+ """) return True # Test code -#x = immich_photo_renderer({"Fetch Photos": (60 * 60 * 12), -# "Rotate Current Photo": (1)}) -#x.index_photos() -#x.choose_photo() +x = immich_photo_renderer({"Fetch Photos": (60 * 60 * 12), + "Rotate Current Photo": (1)}) +x.index_photos() +x.choose_photo()