diff --git a/server/README.md b/server/README.md
index 43d75b0..b44afcf 100644
--- a/server/README.md
+++ b/server/README.md
@@ -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.
diff --git a/server/app/config.py b/server/app/config.py
index e109c1c..56d0bbc 100644
--- a/server/app/config.py
+++ b/server/app/config.py
@@ -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:
diff --git a/server/app/main.py b/server/app/main.py
index 35e7058..fc098a5 100644
--- a/server/app/main.py
+++ b/server/app/main.py
@@ -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:
diff --git a/server/app/photo_queue.py b/server/app/photo_queue.py
index 5144b31..60359fb 100644
--- a/server/app/photo_queue.py
+++ b/server/app/photo_queue.py
@@ -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
diff --git a/server/app/templates/index.html b/server/app/templates/index.html
index 35498f7..386fb82 100644
--- a/server/app/templates/index.html
+++ b/server/app/templates/index.html
@@ -76,6 +76,13 @@
+
@@ -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',