--- /dev/null
+#!/usr/bin/env python3
+
+import datetime
+import os
+import random
+import re
+from typing import Dict, List, Set
+
+from scottutilz import immich
+
+import file_writer
+import renderer
+import kiosk_secrets
+
+
+def format_coords(lat: float, lon: float) -> str:
+ """
+ Renders lat/lon floats into a human-readable string:
+ '47.6101° N, 122.2015° W'
+ """
+ lat_dir: str = 'N' if lat >= 0 else 'S'
+ lon_dir: str = 'E' if lon >= 0 else 'W'
+
+ # Use 4 decimal places for ~11m precision
+ return f"{abs(lat):.4f}° {lat_dir}, {abs(lon):.4f}° {lon_dir}"
+
+def natural_join(items: List[str], use_oxford_comma: bool = True) -> str:
+ """
+ Joins a list of strings into a natural language sentence fragment.
+
+ Examples:
+ ['A'] -> "A"
+ ['A', 'B'] -> "A and B"
+ ['A', 'B', 'C'] -> "A, B, and C"
+ """
+ count: int = len(items)
+
+ if count == 0:
+ return ""
+ if count == 1:
+ return items[0]
+ if count == 2:
+ return f"{items[0]} and {items[1]}"
+
+ # Handle 3 or more items
+ prefix: str = ", ".join(items[:-1])
+ 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"""
+
+ album_whitelist = frozenset(
+ [
+ 'Family, ALL',
+ 'Street Art, ALL',
+ 'Artsy',
+ 'Louisville, 2025: Google Folks',
+ 'France / Spain ALL, 2025',
+ 'Friends, ALL',
+ 'Spain and Portugal ALL, 2024',
+ ]
+ )
+
+ def __init__(self, name_to_timeout_dict: Dict[str, int]) -> None:
+ super().__init__(name_to_timeout_dict)
+ self.immich_cli = immich.ImmichSession(
+ api_key=kiosk_secrets.scott_immich_key
+ )
+ self.candidate_photo_uuids: Set[str] = set()
+
+ def debug_prefix(self) -> str:
+ return "immich_photos"
+
+ def periodic_render(self, key: str) -> bool:
+ if key == "Fetch Photos":
+ return self.index_photos()
+ elif key == "Rotate Current Photo":
+ return self.choose_photo()
+ else:
+ 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)
+ return True
+
+ def choose_photo(self):
+ """Pick one of the cached URLs and build a page."""
+ if len(self.candidate_photo_uuids) == 0:
+ print("No photos!")
+ return False
+ selected_uuid = random.sample(list(self.candidate_photo_uuids), 1)[0]
+ asset = self.immich_cli.get_full_asset(selected_uuid)
+ 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?!")
+ return False
+
+ exif = asset.get('exifInfo', {})
+ location = "Unknown location"
+ if exif:
+ date = datetime.datetime.fromisoformat(exif.get('dateTimeOriginal', None))
+ city = exif.get("city", "")
+ state = exif.get("state", "")
+ country = exif.get("country", "")
+ lat = exif.get("latitude", "")
+ lon = exif.get("longitude", "")
+ if city:
+ location = city
+ if state:
+ if location:
+ location += f", {state}"
+ else:
+ location = state
+ if country:
+ if location:
+ location += f", {country}"
+ else:
+ location = country
+ if lat and lon:
+ if location:
+ location += f" @ {format_coords(lat, lon)}"
+ 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:
+ f.write(
+ f"""
+<style>
+ body{{background-color:#303030;}}
+ div#time{{color:#dddddd;}}
+ div#date{{color:#dddddd;}}
+</style>
+<table>
+<tr>
+<td width=80%>
+ <center>
+ <img src="/kiosk/mfs/current.jpeg"
+ style="display:block;max-width=800;max-height:600;width:auto;height:auto">
+ </center>
+</td>
+<td valign="middle">
+ <font size=+1 color="#cccccc">
+ {descr}<br>
+ <HR>
+ {location}
+ <HR>
+ {date.strftime("%B %-d, %Y")}<br>
+ </font>
+</td>
+</tr>
+</table>
+""")
+ return True
+
+
+# Test code
+#x = immich_photo_renderer({"Fetch Photos": (60 * 60 * 12),
+# "Rotate Current Photo": (1)})
+#x.index_photos()
+#x.choose_photo()