zoitestream/app/state.py
2026-09-14 13:45:57 -04:00

76 lines
2.5 KiB
Python

"""SQLite-backed tracking of active + historical streams.
Backed by a file on the shared `appdata` PV (mounted at STATE_DB_PATH), so
state survives pod restarts/redeploys. "Active" streams are just history
rows with end IS NULL — no separate table needed, which also gives free
crash recovery (if the pod dies mid-stream, the row is simply still open).
"""
import os
import sqlite3
import threading
from datetime import datetime, timezone
def _now_iso():
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
SCHEMA = """
CREATE TABLE IF NOT EXISTS streams (
id INTEGER PRIMARY KEY AUTOINCREMENT,
path TEXT NOT NULL,
source_type TEXT,
start TEXT NOT NULL,
end TEXT
);
CREATE INDEX IF NOT EXISTS idx_streams_active ON streams (path) WHERE end IS NULL;
"""
class StreamState:
def __init__(self, db_path, history_limit=50):
self._db_path = db_path
self._history_limit = history_limit
self._lock = threading.Lock()
os.makedirs(os.path.dirname(db_path) or ".", exist_ok=True)
with self._connect() as conn:
conn.executescript(SCHEMA)
def _connect(self):
conn = sqlite3.connect(self._db_path)
conn.row_factory = sqlite3.Row
return conn
def on_ready(self, path, source_type=None):
with self._lock, self._connect() as conn:
conn.execute(
"INSERT INTO streams (path, source_type, start, end) VALUES (?, ?, ?, NULL)",
(path, source_type, _now_iso()),
)
def on_not_ready(self, path):
with self._lock, self._connect() as conn:
row = conn.execute(
"SELECT id FROM streams WHERE path = ? AND end IS NULL ORDER BY id DESC LIMIT 1",
(path,),
).fetchone()
if row:
conn.execute(
"UPDATE streams SET end = ? WHERE id = ?",
(_now_iso(), row["id"]),
)
def active(self):
with self._connect() as conn:
rows = conn.execute(
"SELECT path, source_type, start FROM streams WHERE end IS NULL ORDER BY id DESC"
).fetchall()
return [dict(r) for r in rows]
def history(self):
with self._connect() as conn:
rows = conn.execute(
"SELECT path, source_type, start, end FROM streams ORDER BY id DESC LIMIT ?",
(self._history_limit,),
).fetchall()
return [dict(r) for r in rows]