127 lines
4.0 KiB
Python
127 lines
4.0 KiB
Python
#!/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 |