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