Files
espresso_frame/server/app/widgets/photos.py
T
tfaour 37bd657299
Build and push server image / test (push) Successful in 49s
Build and push server image / build-and-push (push) Successful in 1m56s
Widget system Phase 2: full cutover to widget-based rendering
device.py's mode-keyed dispatch is replaced by a real compositor:
load a frame's widgets, compute pixel rects via app/grid.py, render
each through its widget module, and composite with render_panel.
Physical NEXT/BACK buttons now execute each frame's assigned
FrameButtonAction rows instead of one hardcoded per-mode action.

api_frames.py, manage.py, and common.py's build_manage_content are
repointed to read/write the frame's widget config rows instead of
the old Frame columns, and every settings page (Photos/Calendar/
Whiteboard tabs) now pre-fills its form from the same widget config
the write endpoints actually save to -- previously the read and
write sides would have silently diverged. The old mode selector and
photo-inlay checkbox are removed along with their now-inert wiring;
arbitrary widget placement subsumes what the fixed inlay split did.

Ships together with Phase 1 (per-type render/action modules) since
splitting the read/write cutover across deploys would have left
settings changes with no visible effect.
2026-07-24 09:26:28 -04:00

83 lines
3.5 KiB
Python

"""Photos widget: composes one photo from Immich into the widget's own
region -- the widget-system analogue of routers/device.py's old
_render_photos_mode/_advance_photos_mode/_back_photos_mode, and
app/photo_queue.py's real client.
render() never raises -- an Immich hiccup for this one widget shouldn't
take down the whole panel's render just because one region out of
several couldn't be composed this cycle; it falls back to a small
placeholder instead, the same resilience calendar mode's old photo-inlay
already had (see routers/device.py's `except HTTPException: pass` around
its own inlay fetch)."""
from __future__ import annotations
from fastapi import HTTPException
from PIL import Image
from sqlalchemy.orm import Session
from .. import photo_queue, quiet_hours
from ..db import widget_locked
from ..image_pipeline import compose_into
from ..models import Frame, PhotoWidgetConfig, Widget
from ..routers.common import fetch_source_and_faces, immich_client_for, list_assets
from ._shared import placeholder_image
ACTION_LABELS = {"advance": "Next photo", "back": "Previous photo"}
def render(db: Session, frame: Frame, widget: Widget, target_w: int, target_h: int,
is_normal_wake: bool = True) -> Image.Image:
"""is_normal_wake is unused here -- photos mode's advance timing is
already fully idempotent via get_current()'s own elapsed-time check,
unlike calendar mode's browse_offset (see app/widgets/calendar.py's
render()). Accepted anyway so every widget type's render() shares one
call signature regardless of which ones actually care."""
cfg = db.get(PhotoWidgetConfig, widget.id)
if not cfg.album_id:
return placeholder_image(target_w, target_h, ["Photos widget", "not configured yet"])
try:
client = immich_client_for(frame)
assets = list_assets(client, cfg.album_id)
with widget_locked(db, frame.id, widget.id) as (locked_frame, _, locked_cfg):
photo_queue.get_current(locked_cfg, assets, locked_frame,
in_quiet_hours=quiet_hours.in_quiet_hours(locked_frame))
asset_id = locked_cfg.current_asset_id
if not asset_id:
return placeholder_image(target_w, target_h, ["No photos available"])
source, faces = fetch_source_and_faces(client, cfg.display_mode, asset_id)
except HTTPException as e:
return placeholder_image(target_w, target_h, ["Photos widget", str(e.detail)[:40]])
return compose_into(source, faces, target_w, target_h, cfg.display_mode)
def _advance(db: Session, frame: Frame, widget: Widget) -> None:
cfg = db.get(PhotoWidgetConfig, widget.id)
if not cfg.album_id:
return
try:
client = immich_client_for(frame)
assets = list_assets(client, cfg.album_id)
except HTTPException:
return # nothing to advance to this cycle -- next button press tries again
with widget_locked(db, frame.id, widget.id) as (locked_frame, _, locked_cfg):
photo_queue.advance_forced(locked_cfg, assets, locked_frame)
def _back(db: Session, frame: Frame, widget: Widget) -> None:
cfg = db.get(PhotoWidgetConfig, widget.id)
if not cfg.album_id:
return
try:
client = immich_client_for(frame)
assets = list_assets(client, cfg.album_id)
except HTTPException:
return
with widget_locked(db, frame.id, widget.id) as (locked_frame, _, locked_cfg):
photo_queue.back_forced(locked_cfg, assets, locked_frame)
ACTIONS = {"advance": _advance, "back": _back}