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.
This commit is contained in:
1 parent
c0192fca7a
commit
15c7c38d86
4 files changed
+49
-25
No files matched your search
@@ -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:
|
||||
|
||||
+16
-18
@@ -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()
|
||||
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
<span class="badge {{ 'on' if rotation.enabled else 'off' }}">{{ "on" if rotation.enabled else "off" }}</span>
|
||||
{{ rotation.screens | join(" → ") }} (every {{ rotation.interval_minutes }} min)
|
||||
</dd>
|
||||
{% if status.manual_screen %}
|
||||
{% if status.pinned_screen %}
|
||||
<dt>Manual pin</dt>
|
||||
<dd><span class="badge on">{{ screens[status.manual_screen].display_name }}</span> until {{ status.manual_until }}</dd>
|
||||
<dd><span class="badge on">{{ screens[status.pinned_screen].display_name }}</span> -- fixed until resumed</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
</div>
|
||||
@@ -40,7 +40,7 @@
|
||||
<button class="secondary" type="submit">Show {{ screen.display_name }}</button>
|
||||
</form>
|
||||
{% endfor %}
|
||||
{% if status.manual_screen %}
|
||||
{% if status.pinned_screen %}
|
||||
<form class="inline" method="post" action="{{ url_for('resume_rotation') }}">
|
||||
<button class="secondary" type="submit">Resume automatic</button>
|
||||
</form>
|
||||
|
||||
@@ -18,10 +18,11 @@
|
||||
Enabled -- cycle through the screens below on a timer
|
||||
</label>
|
||||
<p class="hint" style="margin: 0.6rem 0 1.1rem;">
|
||||
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.
|
||||
</p>
|
||||
<div class="field">
|
||||
<label for="screens">Order</label>
|
||||
|
||||
Reference in new issue
Block a user