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',