From 15c7c38d86b4401037c02ed6bf89c5b7f63e19d6 Mon Sep 17 00:00:00 2001 From: Honey Date: Wed, 22 Jul 2026 00:07:25 +0200 Subject: [PATCH] Make manual pin permanent instead of a 5-minute timeout "Show X now" now persists indefinitely (survives restarts too, it's in config.json) instead of auto-expiring back to rotation after 5 minutes. Implemented by reusing the existing rotation.enabled flag rather than a separate scheduler-side timer: pinning a screen sets rotation.enabled=False and records which screen in a new pinned_screen config field. Re-enabling rotation -- either the "Resume automatic" button or just checking "Enabled" again on the Rotation settings page -- clears the pin and resumes normal cycling either way. Navidrome priority is unchanged: still checked first, still preempts a pin the instant something's playing, same as it already preempted rotation. Verified end-to-end: pin persists across many scheduler ticks (previously would have reverted after 5 min), both resume paths (button and settings checkbox) clear it correctly. --- config.py | 25 ++++++++++++++++++++ scheduler.py | 34 +++++++++++++--------------- web/templates/index.html | 6 ++--- web/templates/settings_rotation.html | 9 ++++---- 4 files changed, 49 insertions(+), 25 deletions(-) diff --git a/config.py b/config.py index 4e24c45..458eabd 100644 --- a/config.py +++ b/config.py @@ -42,6 +42,7 @@ class Config: data["rotation"].setdefault("interval_minutes", DEFAULT_ROTATION["interval_minutes"]) data["rotation"].setdefault("enabled", DEFAULT_ROTATION["enabled"]) data.setdefault("navidrome_priority", True) + data.setdefault("pinned_screen", None) data.setdefault("settings", {}) for screen_id, screen in screens.items(): data["settings"].setdefault(screen_id, {}) @@ -81,6 +82,30 @@ class Config: def set_rotation(self, screens: list[str], interval_minutes: int, enabled: bool) -> None: with self._lock: self._data["rotation"] = {"screens": screens, "interval_minutes": interval_minutes, "enabled": enabled} + if enabled: + # Re-enabling rotation (e.g. via the settings checkbox) + # supersedes any manual pin -- that's the intended way to + # get back to automatic switching. + self._data["pinned_screen"] = None + self.save() + + def get_pinned_screen(self) -> str | None: + with self._lock: + return self._data.get("pinned_screen") + + def pin_screen(self, screen_id: str) -> None: + """Pin a screen indefinitely -- turns rotation off so it doesn't + fight with the pin. Stays until the pin is explicitly cleared or + rotation is re-enabled (settings checkbox, or resume_rotation()).""" + with self._lock: + self._data["pinned_screen"] = screen_id + self._data["rotation"]["enabled"] = False + self.save() + + def resume_rotation(self) -> None: + with self._lock: + self._data["rotation"]["enabled"] = True + self._data["pinned_screen"] = None self.save() def get_navidrome_priority(self) -> bool: diff --git a/scheduler.py b/scheduler.py index 6f58748..00a5fda 100644 --- a/scheduler.py +++ b/scheduler.py @@ -22,7 +22,6 @@ from PIL import Image logger = logging.getLogger(__name__) TICK_SECONDS = 5 -MANUAL_OVERRIDE_SECONDS = 300 # how long "show this now" sticks before falling back to automatic PREVIEW_PATH = Path(__file__).resolve().parent / "web" / "static" / "preview.png" @@ -49,8 +48,6 @@ class Scheduler: self._rotation_switched_at = time.time() self._last_image_hash: str | None = None - self._manual_screen: str | None = None - self._manual_until: float = 0.0 self._force_once = False self._wake = threading.Event() @@ -80,23 +77,23 @@ class Scheduler: self._wake.set() def request_switch(self, screen_id: str): - """Pin a screen for MANUAL_OVERRIDE_SECONDS. Navidrome priority can - still preempt it if something starts playing -- that's the one thing - this app promises to always show reliably -- but rotation won't.""" - self._manual_screen = screen_id - self._manual_until = time.time() + MANUAL_OVERRIDE_SECONDS + """Pin a screen indefinitely (persisted in config, turns rotation + off). Navidrome priority can still preempt it if something starts + playing -- that's the one thing this app promises to always show + reliably -- but rotation won't, until it's explicitly re-enabled.""" + self.config.pin_screen(screen_id) self._wake.set() def request_resume_rotation(self): - self._manual_screen = None + self.config.resume_rotation() self._wake.set() def get_status(self) -> dict: with self._status_lock: status = dict(self._status) - manual_active = bool(self._manual_screen) and time.time() < self._manual_until - status["manual_screen"] = self._manual_screen if manual_active else None - status["manual_until"] = time.strftime("%H:%M:%S", time.localtime(self._manual_until)) if manual_active else None + rotation = self.config.get_rotation() + pinned = self.config.get_pinned_screen() + status["pinned_screen"] = pinned if (pinned and not rotation.get("enabled", True)) else None return status # ------------------------------------------------------------- internal @@ -144,18 +141,19 @@ class Scheduler: Priority order: Navidrome-when-playing (if enabled) always wins -- that's the one thing this app promises to show reliably. Then a - manual "show this now" pin, while it's still within its window. Then - normal rotation. + manual "show this now" pin, if rotation is off because of one (set + together in Config.pin_screen()). Then normal rotation. """ if self.config.get_navidrome_priority() and "navidrome" in self.screens: screen_id, state, checked = self._poll_screen("navidrome") if state is not None: return screen_id, state, checked - if self._manual_screen: - if time.time() < self._manual_until: - return self._poll_screen(self._manual_screen) - self._manual_screen = None # window expired + rotation = self.config.get_rotation() + if not rotation.get("enabled", True): + pinned = self.config.get_pinned_screen() + if pinned and pinned in self.screens: + return self._poll_screen(pinned) return self._rotation_screen() diff --git a/web/templates/index.html b/web/templates/index.html index b3b30bb..fbe3021 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -22,9 +22,9 @@ {{ "on" if rotation.enabled else "off" }} {{ rotation.screens | join(" → ") }} (every {{ rotation.interval_minutes }} min) - {% if status.manual_screen %} + {% if status.pinned_screen %}
Manual pin
-
{{ screens[status.manual_screen].display_name }} until {{ status.manual_until }}
+
{{ screens[status.pinned_screen].display_name }} -- fixed until resumed
{% endif %} @@ -40,7 +40,7 @@ {% endfor %} - {% if status.manual_screen %} + {% if status.pinned_screen %}
diff --git a/web/templates/settings_rotation.html b/web/templates/settings_rotation.html index da7a162..ffe4acf 100644 --- a/web/templates/settings_rotation.html +++ b/web/templates/settings_rotation.html @@ -18,10 +18,11 @@ Enabled -- cycle through the screens below on a timer

- Off just pins to the first screen in the order below (still shown when - Navidrome priority is on but nothing's playing). "Show X now" buttons on - the Status page still work either way, they just don't stick past the - next check. + Clicking "Show X now" on the Status page turns this off automatically + and fixes the display to that screen until you turn rotation back on + here (Navidrome priority above still applies on top of a pin, if + enabled). With no pin and rotation off, it just sits on the first + screen in the order below.