Make the upcoming-photos queue length user-configurable
Build and push server image / build-and-push (push) Successful in 42s
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:
+11
-9
@@ -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.
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user