Build and push server image / build-and-push (push) Successful in 32s
Replaces the up/down-button vertical list with a responsive photo grid (native HTML5 drag-and-drop between cards, reusing the existing POST /api/queue/reorder endpoint -- no new server route needed). Each card also gets a "Show next" button that jumps it straight to the front of the queue. Also bumps the queue lookahead from 10 to 24 photos (QUEUE_TARGET_LEN in photo_queue.py) now that the grid has room to show more at once.
101 lines
3.9 KiB
Python
101 lines
3.9 KiB
Python
"""Tracks which photo is currently displayed and what's queued up next.
|
|
|
|
`current_asset_id` only ever changes two ways: the configured refresh
|
|
interval elapsing (`get_current`, called on every `GET /frame/image` --
|
|
a no-op otherwise, so an unplanned device reboot just redisplays the same
|
|
photo instead of silently skipping ahead) or an explicit forced advance
|
|
(`advance_forced`, called from `POST /frame/advance` -- the next-photo
|
|
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.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import random
|
|
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)
|
|
if needed <= 0 or not assets:
|
|
return
|
|
|
|
excluded = set(cfg.queue)
|
|
if cfg.current_asset_id:
|
|
excluded.add(cfg.current_asset_id)
|
|
|
|
if cfg.order == "shuffle":
|
|
candidates = [a["id"] for a in assets if a["id"] not in excluded]
|
|
cfg.queue.extend(random.sample(candidates, min(needed, len(candidates))))
|
|
return
|
|
|
|
# Sequential: walk the album starting at queue_cursor, at most one full
|
|
# pass, wrapping around. queue_cursor resumes right after wherever this
|
|
# pass stopped, whether or not it filled the queue (e.g. a small album
|
|
# where everything's already queued/current -- next call is then a
|
|
# cheap no-op scan until something's consumed).
|
|
n = len(assets)
|
|
cfg.queue_cursor %= n
|
|
added = 0
|
|
i = 0
|
|
for i in range(n):
|
|
if added >= needed:
|
|
break
|
|
asset_id = assets[(cfg.queue_cursor + i) % n]["id"]
|
|
if asset_id not in excluded:
|
|
cfg.queue.append(asset_id)
|
|
excluded.add(asset_id)
|
|
added += 1
|
|
cfg.queue_cursor = (cfg.queue_cursor + i + 1) % n
|
|
|
|
|
|
def advance_forced(cfg: FrameConfig, assets: list[dict]) -> None:
|
|
"""Unconditionally moves to the next photo, ignoring elapsed time, and
|
|
resets the interval clock from now. Used only by the explicit
|
|
next-photo action (POST /frame/advance) -- always mutates cfg."""
|
|
_top_up(cfg, assets)
|
|
if cfg.queue:
|
|
cfg.current_asset_id = cfg.queue.pop(0)
|
|
elif assets:
|
|
# Queue still empty after top-up (e.g. a single-photo album whose
|
|
# 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
|
|
# 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 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
|
|
elapsed since it was set. Returns whether it changed anything, so the
|
|
caller knows whether to persist. Calling this repeatedly well within
|
|
the interval is a no-op both times -- what makes an unplanned device
|
|
reboot safe: it just re-reads the current photo instead of skipping
|
|
ahead, while a wake that lands after the interval has elapsed still
|
|
advances exactly once, even after a long time offline."""
|
|
valid_ids = {a["id"] for a in assets}
|
|
stale = (
|
|
not cfg.current_asset_id
|
|
or cfg.current_asset_id not in valid_ids
|
|
or (time.time() - cfg.current_asset_set_at) >= cfg.refresh_interval_s
|
|
)
|
|
if not stale:
|
|
return False
|
|
advance_forced(cfg, assets)
|
|
return True
|