Fix race condition in choose_photo.
authorScott Gasch <[email protected]>
Mon, 11 May 2026 20:05:11 +0000 (13:05 -0700)
committerScott Gasch <[email protected]>
Mon, 11 May 2026 20:05:11 +0000 (13:05 -0700)
immich_photo_renderer.py

index 55901545fdbb6f01c29e86cb13e3a7cb8b8ac4c7..1f2fea4773ba321538e2fa38b48dedb4b7b903a7 100644 (file)
@@ -141,95 +141,101 @@ class immich_photo_renderer(renderer.abstaining_renderer):
             text
         ).strip()
 
-    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
+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
+
+    # --- Fetch ALL network data first, before touching disk ---
+    try:
+        jpeg = self.immich_cli.get_asset_thumbnail(selected_uuid, size="fullsize")
+    except Exception:
+        print("Failed to get image!")
+        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)
-                f.flush()
-                os.fsync(f.fileno())
-        except Exception:
-            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 = parse_exif_date(exif)
-            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 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)
+    try:
+        albums = self.immich_cli.get_asset_albums(selected_uuid)
+    except Exception:
+        print("Failed to get albums!")
+        albums = []
+
+    album_covers = {}
+    for album in albums:
+        album_cover_uuid = album.get('albumThumbnailAssetId')
+        if album_cover_uuid:
+            try:
+                album_covers[album_cover_uuid] = self.immich_cli.get_asset_thumbnail(
+                    album_cover_uuid, size="thumbnail"
+                )
+            except Exception:
+                print(f"Failed to get album cover for {album_cover_uuid}, skipping.")
+
+    # --- All network calls done. Now compute derived values. ---
+    layout_class = get_layout_class(asset)
+    exif = asset.get('exifInfo', {})
+    date = None
+    location = "Unknown location"
+    descr = ''
+    if exif:
+        date = parse_exif_date(exif)
+        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:
+            location = f"{location}, {state}" if location else state
+        if country:
+            location = f"{location}, {country}" if location else country
+        if lat and lon:
+            location = f"{location} @ {format_coords(lat, lon)}" if location else format_coords(lat, lon)
+        descr = exif.get("description", "")
+
+    if not descr:
+        people = asset.get('people', [])
+        if people:
+            descr = "A photo of " + natural_join([p.get("name", "") for p in people])
+        else:
+            descr = "No description data available.  #needs description"
+
+    descr = self._highlight_hashtags(descr)
+    date_str = date.strftime("%B %-d, %Y") if date else "Unknown date"
+    album_html = ''
+    for album in albums:
+        album_name = album.get('albumName')
+        album_cover_uuid = album.get('albumThumbnailAssetId')
+        if album_cover_uuid and album_cover_uuid in album_covers:
             album_html += f"""
 <div class="album-card">
    <img src="/kiosk/mfs/{album_cover_uuid}.jpeg" class="album-thumb" alt="Album Art">
    <span class="album-name">{album_name}</span>
 </div>
 """
-        cache_buster = int(time.time())
-        with file_writer.file_writer("photo_33_3600.html") as f:
-            f.write(
-                f"""
+
+    # --- All data ready. Now write to disk atomically. ---
+    with open("/var/www/kiosk/pages/mfs/current.jpeg", "wb") as f:
+        f.write(jpeg)
+        f.flush()
+        os.fsync(f.fileno())
+
+    for cover_uuid, cover_data in album_covers.items():
+        with open(f"/var/www/kiosk/pages/mfs/{cover_uuid}.jpeg", "wb") as f:
+            f.write(cover_data)
+
+    cache_buster = int(time.time())
+    with file_writer.file_writer("photo_33_3600.html") as f:
+        f.write(
+            f"""
 <style>
   div#time{{color:#dddddd;}}
   div#date{{color:#dddddd;}}
@@ -256,7 +262,6 @@ class immich_photo_renderer(renderer.abstaining_renderer):
         height: 100vh;
         width: 100vw;
   }}
-
   /* --- Photo Pane Logic --- */
   .photo-pane {{
         display: flex;
@@ -282,7 +287,6 @@ class immich_photo_renderer(renderer.abstaining_renderer):
         object-fit: contain;
         box-shadow: 0 0 50px rgba(0,0,0,0.5);
   }}
-
   /* --- Info Pane Logic --- */
   .info-pane {{
         display: flex;
@@ -308,11 +312,9 @@ class immich_photo_renderer(renderer.abstaining_renderer):
         flex: 0 1 auto;
         min-height: 20vh;
         display: flex;
-        /* flex-direction: row; */
         align-items: center;
         padding: 2vh 3vw;
   }}
-
   /* --- Typography & Elements --- */
   .description {{
         font-weight: 600;
@@ -330,9 +332,8 @@ class immich_photo_renderer(renderer.abstaining_renderer):
         overflow: visible;
         margin: 0;
         padding-right: 2vw;
-        min-width: 0; /* Allows text to truncate/wrap properly */
+        min-width: 0;
   }}
-
   .divider {{
         background: var(--accent);
   }}
@@ -346,17 +347,14 @@ class immich_photo_renderer(renderer.abstaining_renderer):
          height: 50px;
          margin: 0;
   }}
-
-  /* Metadata section: Constrained to 20% width */
   .panorama_mode .meta-group {{
         flex: 0 0 20%;
         display: flex;
         flex-direction: column;
         justify-content: center;
         padding: 0 1.5vw;
-        min-width: 0; /* Crucial for internal text wrapping */
+        min-width: 0;
   }}
-
   .location {{
         font-size: 0.9rem;
         color: var(--text-dim);
@@ -371,7 +369,6 @@ class immich_photo_renderer(renderer.abstaining_renderer):
         font-size: 1rem;
         margin-top: 4px;
   }}
-
   /* --- Album List --- */
   .album-section-title {{
         font-size: 0.7rem;
@@ -389,7 +386,6 @@ class immich_photo_renderer(renderer.abstaining_renderer):
         transform: rotate(180deg);
         margin: 10px;
   }}
-
   .album-grid {{
         display: flex;
         gap: 15px;
@@ -423,17 +419,10 @@ class immich_photo_renderer(renderer.abstaining_renderer):
         margin-top: 5px;
         text-align: center;
         width: 100%;
-/*        white-space: nowrap;
-        overflow: hidden;
-        text-overflow: ellipsis; */
-
-        /* Fix #1: Force long words like 'AdobeLightroom' to break */
         word-break: break-word;
         overflow-wrap: break-word;
-
-        /* Fix #2: Multi-line truncation (The "Best of Both Worlds") */
         display: -webkit-box;
-        -webkit-line-clamp: 2; /* Show exactly 3 lines, then dots */
+        -webkit-line-clamp: 2;
         -webkit-box-orient: vertical;
         overflow: hidden;
   }}
@@ -443,7 +432,6 @@ class immich_photo_renderer(renderer.abstaining_renderer):
     <div class="photo-pane">
         <img src="/kiosk/mfs/current.jpeg?v={cache_buster}" alt="Current kiosk photo">
     </div>
-
     <!-- Metadata Section -->
     <div class="info-pane">
         <div class="description">
@@ -455,19 +443,17 @@ class immich_photo_renderer(renderer.abstaining_renderer):
                 {location}
             </div>
             <div class="date">
-                {date.strftime("%B %-d, %Y")}
+                {date_str}
             </div>
         </div>
-     <!--   <div class="divider"></div> -->
         <div class="album-section-title">Appears In</div>
         <div class="album-grid">
             {album_html}
         </div>
     </div>
 </div>
-                """)
-        return True
-
+            """)
+    return True
 
 # Test code
 x = immich_photo_renderer({"Fetch Photos": (60 * 60 * 12),