Architecture¶
Component Overview¶
Main flow:
- A browser connects to the companion service to configure settings and profiles.
- The companion service stores settings, builds playlists, and exposes the setup API used by the Roku.
- The Roku fetches the active profile and playlist from the companion.
- The Roku then loads media directly from immich.
- immich remains the source of truth for photos and metadata.
Weather path:
| Source | Destination | Purpose |
|---|---|---|
| Open-Meteo API | Roku device | Weather data polling for the optional on-screen weather display |
Key principle: The companion handles configuration and playlist building. The Roku handles media delivery and weather directly — the companion is never in those paths.
Component 1: Companion Service¶
Tech stack: ASP.NET Core (.NET 10), Blazor Server, JSON file storage, Docker
The companion is a Docker container that runs on a machine on your LAN. It provides:
- Web UI — browser-based configuration via Blazor Server
- REST API — used by the Roku to fetch profiles and playlists
- Playlist builder — queries immich, deduplicates, shuffles, and caches the asset list
Data Storage¶
/data/
settings.json # Global: immich URL, API key, friendly name, companion UUID
profiles/
living-room.json # One file per profile (no API key — injected at serve time)
cache/
living-room.json # Cached shuffled playlist (optional persistence)
Enriched Profile Response¶
When the Roku fetches a profile, the companion injects the immich connection details into the response. This means:
- Profile files on disk do not contain the API key
- The Roku receives a single JSON object with everything it needs (including the API key for direct immich calls)
- The Roku caches this enriched profile locally for offline fallback
Playlist Cache¶
The companion maintains one shuffled asset-ID list per profile. The cache lifecycle:
- Cold cache — returns
{ building: true }immediately; builds in background - Warm cache — returned immediately from memory (or disk if companion restarted)
- Proactive rebuild — triggered before expiry (20% of interval, clamped to 1–10 minutes early)
- Invalidation — immediately on profile update, profile delete, or global settings change
Component 2: Roku Channel + Screensaver¶
Tech stack: BrightScript, SceneGraph, Roku OS 15.0+
The Roku project builds separate channel and screensaver variants. Both run the same display logic, but the channel mode has a richer setup flow.
Startup Flow (Channel)¶
1. Read registry: companionUrl + selected profile ID
2. Missing? → Setup screen
3. Fetch enriched profile from companion
4. Success → save to the mode-specific cached profile registry key
5. Failure → load the mode-specific cached profile (or show error)
6. Fetch playlist (up to 500 entries)
7. Playlist building? → poll every 5s (up to 60s)
8. Success → hold in memory; save 100-entry truncated copy to registry
9. Failure → use the mode-specific cached playlist (or show error)
10. Start slideshow
11. Schedule periodic refresh (per refreshIntervalMinutes)
Registry Keys (Roku)¶
| Key | Purpose | Max size |
|---|---|---|
companionUrl |
Shared companion base URL | ~50 bytes |
channelProfileId |
Selected profile for the channel | ~70 bytes |
screensaverProfileId |
Selected profile for the screensaver | ~70 bytes |
cachedChannelProfile |
Cached enriched channel profile | ~4 KB |
cachedScreensaverProfile |
Cached enriched screensaver profile | ~4 KB |
cachedChannelPlaylist |
Cached channel playlist fallback | ~9.5 KB |
cachedScreensaverPlaylist |
Cached screensaver playlist fallback | ~9.5 KB |
The app keeps only one cached playlist per mode and truncates it before writing to the registry to stay within Roku's storage limits.
Media URL Construction¶
The Roku builds media URLs directly from the immich server URL stored in the cached enriched profile:
| Asset type | URL |
|---|---|
| Photo (preview) | <immichUrl>/api/assets/<id>/thumbnail?size=preview |
| Photo (original) | <immichUrl>/api/assets/<id>/original |
| Video | <immichUrl>/api/assets/<id>/video/playback |
| Live Photo | <immichUrl>/api/assets/<livePhotoVideoId>/video/playback |
All immich API calls include: x-api-key: <apiKey> header.
Fallback Chain¶
| Condition | Behaviour |
|---|---|
| Companion offline | Use the cached profile for the current mode; toast "Using cached config (companion offline)" |
| immich offline | Use the cached playlist for the current mode; toast "Using cached playlist (immich offline)" |
| Per-slide metadata fail | Omit that overlay field for the slide |
| Individual asset load fail | Skip silently; increment failure counter |
| >20 consecutive failures | Pause slideshow; show 60-second countdown |
| No cache + companion offline | Full-screen error with setup hint |
| Profile deleted (404) | Toast; navigate to setup |
Component 3: immich Server¶
The Roku interacts with immich directly using these endpoints:
| Call | Purpose |
|---|---|
GET /api/assets/<id> |
Per-slide metadata (date, location, people, EXIF) |
GET /api/assets/<id>/thumbnail?size=preview |
Preview image |
GET /api/assets/<id>/original |
Original image |
GET /api/assets/<id>/video/playback |
Video/Live Photo playback |
The companion uses these endpoints for playlist building:
| Call | Purpose |
|---|---|
POST /api/search/assets |
Search by album, person, or tag |
GET /api/memories?date=<date> |
Fetch "On This Day" memories |
Security Model¶
Immich Lounge is designed for trusted home networks only:
- The companion web UI and API are unauthenticated
- The immich API key is stored in plaintext in the companion's data volume
- The enriched profile (including the API key) is cached in the Roku's registry
- Do not expose the companion port (4383) to the internet
Future: optional Basic Auth on the companion (out of scope for v1).