Files

107 lines
3.8 KiB
Python

"""Live push (SSE).
One watcher thread polls the classifier indexes (.cls.json, rewritten atomically) of the
two newest day dirs every second and appends newly classified recordings to a ring buffer.
SSE handlers block on a Condition and stream new entries. Clients are capped.
"""
import json
import threading
import os
import time
from radio.config import MAX_SSE, REC_DIR
from radio.recordings import cls_index, list_days, make_item
from web.status import status
_ev = {"seq": 0, "buf": [], "known": {}, "mt": {}, "clients": 0}
_ev_cond = threading.Condition()
def watch():
first = True
while True:
try:
new = []
for day in list_days()[:2]:
p = os.path.join(REC_DIR, day, ".cls.json")
try:
mt = os.stat(p).st_mtime
except OSError:
continue
if _ev["mt"].get(day) == mt:
continue
_ev["mt"][day] = mt
idx = cls_index(day)
known = _ev["known"].setdefault(day, set())
for n in sorted(set(idx) - known):
known.add(n)
if first:
continue
ci = idx[n]
sub = "" if ci.get("c") == "voice" else "noise/"
it = make_item(day, n, sub, ci.get("c"), ci)
if it:
new.append(it)
for d in list(_ev["known"]):
if d not in list_days()[:2]:
_ev["known"].pop(d, None)
_ev["mt"].pop(d, None)
first = False
if new:
with _ev_cond:
for it in new:
_ev["seq"] += 1
_ev["buf"].append((_ev["seq"], it))
del _ev["buf"][:-300]
_ev_cond.notify_all()
except Exception: # never let the watcher die
pass
time.sleep(1.0)
def stream(h):
"""Server-Sent Events on handler H: `event: rec` per newly classified recording, `event: status` every 15 s."""
with _ev_cond:
if _ev["clients"] >= MAX_SSE:
return h.send(503, "too many listeners", "text/plain", {"Retry-After": "30"})
_ev["clients"] += 1
try:
try:
last = int(h.headers.get("Last-Event-ID") or -1)
except ValueError:
last = -1
with _ev_cond:
if last < 0 or last > _ev["seq"]:
last = _ev["seq"]
h.close_connection = True
h.send_response(200)
h.send_header("Content-Type", "text/event-stream; charset=utf-8")
h.send_header("Cache-Control", "no-cache")
h.send_header("X-Accel-Buffering", "no")
h.send_header("Connection", "close")
h.end_headers()
h.wfile.write(b"retry: 3000\n\n")
h.wfile.flush()
next_status = 0
while True:
with _ev_cond:
pend = [(q, it) for q, it in _ev["buf"] if q > last]
if not pend:
_ev_cond.wait(timeout=max(0.5, next_status - time.time()))
pend = [(q, it) for q, it in _ev["buf"] if q > last]
out = []
for q, it in pend:
last = q
out.append(f"id: {q}\nevent: rec\ndata: {json.dumps(it, ensure_ascii=False)}\n\n")
if time.time() >= next_status:
next_status = time.time() + 15
out.append(f"event: status\ndata: {json.dumps(status(), ensure_ascii=False)}\n\n")
if out:
h.wfile.write("".join(out).encode("utf-8"))
h.wfile.flush()
except (BrokenPipeError, ConnectionResetError, TimeoutError, OSError):
pass
finally:
with _ev_cond:
_ev["clients"] -= 1