#!/usr/bin/env python3 """ Shared play/pause/stop/skip control for the sand table player. Replaces the old OS-signal-based approach (SIGUSR1/SIGUSR2/SIGTERM caught by a handler with module-global flags, process discovery via psutil scanning cmdlines for "PlayQueue.py"). That worked but had some real downsides: - views.py's "playing" flag was a plain Python global, so with more than one web worker process it could disagree with reality. - "is a player running" was answered by grepping every process's cmdline for a string, which is a little fragile. - Signals give no visibility into *what* state the player is actually in (playing/paused/idle) or which track - only views.py's own guess. This module stores that state in a single row of a small SQLite table instead - the same DB the rest of the project already uses. The running player (playTrack.py) polls `get_command()` between points; the web app (views.py) writes to it with `set_command()` and reads real status with `get_status()`. No signals, no process-name matching for control - psutil is only still used as a safety net to force-kill a stuck process by its recorded PID. """ import sqlite3 import psutil DB_PATH = "/home/pi/SandTable/db.sqlite3" TABLE = "sandtable_control" # status: what the player is actually doing right now STATUS_IDLE = "idle" STATUS_PLAYING = "playing" STATUS_PAUSED = "paused" # command: what the player has been asked to do - it's expected to notice # and react next time it checks COMMAND_PLAY = "play" COMMAND_PAUSE = "pause" COMMAND_STOP = "stop" COMMAND_NEXT = "next" def _connect(): conn = sqlite3.connect(DB_PATH) conn.execute( f""" CREATE TABLE IF NOT EXISTS {TABLE} ( id INTEGER PRIMARY KEY CHECK (id = 0), command TEXT NOT NULL DEFAULT '{COMMAND_PLAY}', status TEXT NOT NULL DEFAULT '{STATUS_IDLE}', current_track TEXT, pid INTEGER ) """ ) conn.execute( f"INSERT OR IGNORE INTO {TABLE} (id, command, status) VALUES (0, ?, ?)", (COMMAND_PLAY, STATUS_IDLE), ) return conn def init_control_table(): conn = _connect() conn.commit() conn.close() def set_command(command): conn = _connect() conn.execute(f"UPDATE {TABLE} SET command = ? WHERE id = 0", (command,)) conn.commit() conn.close() def get_command(): conn = _connect() row = conn.execute(f"SELECT command FROM {TABLE} WHERE id = 0").fetchone() conn.close() return row[0] if row else COMMAND_PLAY def set_status(status, current_track=None, pid=None): """current_track/pid are only updated when explicitly passed - pass current_track='' or pid=0 if you actually want to clear them.""" conn = _connect() if current_track is not None: conn.execute(f"UPDATE {TABLE} SET current_track = ? WHERE id = 0", (current_track,)) if pid is not None: conn.execute(f"UPDATE {TABLE} SET pid = ? WHERE id = 0", (pid,)) conn.execute(f"UPDATE {TABLE} SET status = ? WHERE id = 0", (status,)) conn.commit() conn.close() def get_status(): conn = _connect() row = conn.execute(f"SELECT status, current_track, pid FROM {TABLE} WHERE id = 0").fetchone() conn.close() if row is None: return {"status": STATUS_IDLE, "current_track": None, "pid": None} return {"status": row[0], "current_track": row[1], "pid": row[2]} def stop_player(hard=True): """Ask the running player to stop, and (by default) also force-kill its process by the PID it registered - useful right before a manual home/stop/shutdown, where you don't want to wait for the cooperative stop to be noticed. Returns True if a live player was found.""" status = get_status() pid = status.get("pid") found = False if pid and psutil.pid_exists(pid): found = True if hard: try: psutil.Process(pid).terminate() except psutil.NoSuchProcess: pass set_command(COMMAND_STOP) set_status(STATUS_IDLE, pid=0) return found