Make the upcoming-photos queue length user-configurable
Build and push server image / build-and-push (push) Successful in 42s

Adds "Upcoming photos to show" to the config UI (queue_target_len, 5-50,
default 20, replacing the hardcoded QUEUE_TARGET_LEN constant). Lowering
it trims the queue immediately on next page load rather than waiting for
enough advances to consume the excess naturally; raising it tops back up
the same way, via a new photo_queue.sync_queue_length() called from
GET /api/queue.
This commit is contained in:
2026-07-19 01:03:05 -04:00
parent de41b00210
commit 42d7c09f97
5 changed files with 50 additions and 17 deletions
+11 -9
View File
@@ -31,10 +31,10 @@ algorithm itself -- it just streams the response straight to the panel.
## Endpoints
- `GET /` -- config UI (album, order, refresh interval, face-aware crop
toggle, now-displaying + reorderable upcoming photos -- not Immich
URL/API key, see Setup above)
toggle, upcoming-photos count, now-displaying + drag-to-reorder
upcoming grid -- not Immich URL/API key, see Setup above)
- `GET /api/albums` -- lists Immich albums (used by the config UI)
- `POST /api/config` -- saves album/order/refresh_interval_s/smart_crop_faces
- `POST /api/config` -- saves album/order/refresh_interval_s/smart_crop_faces/queue_target_len
- `GET /frame/image` -- returns the current photo pre-processed into the
panel's raw 800x480, 4-bit-per-pixel, 2-pixels-per-byte format
(`application/octet-stream`, exactly 192,000 bytes). **Side-effect-free**
@@ -64,12 +64,14 @@ algorithm itself -- it just streams the response straight to the panel.
mount. Immich URL/API key are too if set via the web UI, but
`IMMICH_URL`/`IMMICH_API_KEY` env vars (see Setup above) always take
precedence when present.
- The upcoming queue is a bounded lookahead (`QUEUE_TARGET_LEN` in
`app/photo_queue.py`, currently 24 photos), not the whole album -- it's
topped up automatically as photos are consumed, in sequential or
shuffle order per the Order setting. Dragging photos in the web UI (or
using "Show next") only rearranges what's already in that lookahead;
it doesn't add or remove photos from the album.
- The upcoming queue is a bounded lookahead, not the whole album --
"Upcoming photos to show" in the config UI (`queue_target_len`, 5-50,
default 20) controls its size and takes effect immediately (the queue
is topped up or trimmed the next time the page loads, not lazily over
future advances). It's topped up automatically as photos are consumed,
in sequential or shuffle order per the Order setting. Dragging photos
in the web UI (or using "Show next") only rearranges what's already in
that lookahead; it doesn't add or remove photos from the album.
- `/frame/image` and `/frame/advance` aren't authenticated yet. That's
fine on a trusted home LAN for now, but worth revisiting once the ESP32
side is wired up to send a shared device token.
+1
View File
@@ -30,6 +30,7 @@ class FrameConfig(BaseModel):
current_asset_set_at: float = 0.0
queue: list[str] = []
queue_cursor: int = 0 # internal bookkeeping for sequential queue top-up; not user-facing
queue_target_len: int = 20 # how many upcoming photos to keep queued/shown in the web UI
def load() -> FrameConfig:
+8 -1
View File
@@ -24,6 +24,8 @@ templates = Jinja2Templates(directory="app/templates")
MIN_REFRESH_INTERVAL_S = 60
MAX_REFRESH_INTERVAL_S = 86400
MIN_QUEUE_TARGET_LEN = 5
MAX_QUEUE_TARGET_LEN = 50
@app.get("/health")
@@ -65,6 +67,7 @@ def api_config_save(
order: str = Form("sequential"),
refresh_interval_s: int = Form(3600),
smart_crop_faces: bool = Form(True),
queue_target_len: int = Form(20),
):
# Immich URL/API key are env-var only (IMMICH_URL/IMMICH_API_KEY, see
# docker-compose.yml.example) -- config.load() already applies them,
@@ -82,6 +85,7 @@ def api_config_save(
cfg.order = order if order in ("sequential", "shuffle") else "sequential"
cfg.refresh_interval_s = max(MIN_REFRESH_INTERVAL_S, min(MAX_REFRESH_INTERVAL_S, refresh_interval_s))
cfg.smart_crop_faces = smart_crop_faces
cfg.queue_target_len = max(MIN_QUEUE_TARGET_LEN, min(MAX_QUEUE_TARGET_LEN, queue_target_len))
config.save(cfg)
return {"status": "saved"}
@@ -166,7 +170,10 @@ def api_queue():
client = ImmichClient(cfg.immich_url, cfg.immich_api_key)
assets = _list_assets(client, cfg)
if photo_queue.get_current(cfg, assets):
current_changed = photo_queue.get_current(cfg, assets)
queue_before = list(cfg.queue)
photo_queue.sync_queue_length(cfg, assets)
if current_changed or cfg.queue != queue_before:
config.save(cfg)
def entry(asset_id: str) -> dict:
+22 -7
View File
@@ -8,9 +8,10 @@ photo instead of silently skipping ahead) or an explicit forced advance
button -- ignoring elapsed time).
`queue` is a small reorderable lookahead the web UI can preview and
rearrange, topped up automatically from the album as it's consumed.
`queue_cursor` is separate, internal-only bookkeeping for where sequential
top-up resumes in the album -- not shown or reordered in the UI.
rearrange, topped up (or trimmed) automatically to match
`cfg.queue_target_len` (user-configurable from the web UI) as it's
consumed. `queue_cursor` is separate, internal-only bookkeeping for where
sequential top-up resumes in the album -- not shown or reordered in the UI.
"""
from __future__ import annotations
@@ -20,14 +21,20 @@ import time
from .config import FrameConfig
QUEUE_TARGET_LEN = 24
def _top_up(cfg: FrameConfig, assets: list[dict]) -> None:
valid_ids = {a["id"] for a in assets}
cfg.queue = [asset_id for asset_id in cfg.queue if asset_id in valid_ids]
needed = QUEUE_TARGET_LEN - len(cfg.queue)
target = cfg.queue_target_len
if len(cfg.queue) > target:
# Target was lowered since this queue was built -- shrink it
# immediately rather than waiting for enough advances to consume
# the excess naturally.
cfg.queue = cfg.queue[:target]
return
needed = target - len(cfg.queue)
if needed <= 0 or not assets:
return
@@ -72,13 +79,21 @@ def advance_forced(cfg: FrameConfig, assets: list[dict]) -> None:
# only asset is already current) -- keep showing what we have.
cfg.current_asset_id = assets[0]["id"]
cfg.current_asset_set_at = time.time()
# Refill back up to QUEUE_TARGET_LEN now that current_asset_id has
# Refill back up to queue_target_len now that current_asset_id has
# changed -- otherwise the queue is left one short until the *next*
# advance, since the pop above consumes one of the items _top_up just
# added.
_top_up(cfg, assets)
def sync_queue_length(cfg: FrameConfig, assets: list[dict]) -> None:
"""Tops up or trims cfg.queue to match cfg.queue_target_len without
otherwise touching current_asset_id. Used by GET /api/queue so a
change to the "upcoming photos to show" setting takes effect on page
load rather than waiting for the next natural advance."""
_top_up(cfg, assets)
def get_current(cfg: FrameConfig, assets: list[dict]) -> bool:
"""Time-based, idempotent path used by GET /frame/image. Advances only
if the current photo is unset/invalid or refresh_interval_s has
+8
View File
@@ -76,6 +76,13 @@
<input type="checkbox" id="smart_crop_faces" {% if cfg.smart_crop_faces %}checked{% endif %}>
<label for="smart_crop_faces">Center faces in crop</label>
</div>
<label>Upcoming photos to show
<select id="queue_target_len">
{% for n in [5, 10, 15, 20, 25, 30, 40, 50] %}
<option value="{{ n }}" {% if cfg.queue_target_len == n %}selected{% endif %}>{{ n }}</option>
{% endfor %}
</select>
</label>
<button type="button" class="secondary" id="load-albums">Load Albums</button>
<button type="submit">Save</button>
</form>
@@ -103,6 +110,7 @@
order: document.getElementById('order').value,
refresh_interval_s: String(minutes * 60),
smart_crop_faces: String(document.getElementById('smart_crop_faces').checked),
queue_target_len: document.getElementById('queue_target_len').value,
});
const resp = await fetch('/api/config', {
method: 'POST',