fix: harden media server — temp-file yt-dlp stderr, parse guards, UI failure surfacing
Rebuild & Deploy / rebuild (push) Failing after 4m27s

- yt-dlp stderr now goes to a temp file so a chatty stderr can never
  deadlock the stdout progress loop.
- report/search/seek numeric parsing returns 400 instead of 500.
- now-playing card reflects failed/preparing download states.
- media runtime data (library.json, jobs/) is gitignored; only
  .gitkeep is tracked.
This commit is contained in:
snowspeeder committed 2026-08-29 13:27:58 -04:00
1 parent f534b8ccb5
commit 48b92585c6
1 file changed
+13 -4
+13 -4
View File
@@ -29,6 +29,7 @@ import os
import re import re
import subprocess import subprocess
import sys import sys
import tempfile
import threading import threading
import time import time
import uuid import uuid
@@ -259,13 +260,16 @@ def _download_worker(job: dict, track: dict, video_id: str) -> None:
] ]
job["stage"] = "downloading" job["stage"] = "downloading"
info: dict = {} info: dict = {}
# stderr goes to a temp file: reading it from a PIPE while the stdout
# loop blocks could deadlock if yt-dlp ever fills the stderr buffer.
with tempfile.TemporaryFile(mode="w+", encoding="utf-8", errors="replace") as errfile:
try: try:
proc = subprocess.Popen( proc = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cmd, stdout=subprocess.PIPE, stderr=errfile,
text=True, errors="replace", text=True, errors="replace",
) )
deadline = time.monotonic() + DOWNLOAD_TIMEOUT deadline = time.monotonic() + DOWNLOAD_TIMEOUT
assert proc.stdout is not None and proc.stderr is not None assert proc.stdout is not None
for line in proc.stdout: for line in proc.stdout:
if time.monotonic() > deadline: if time.monotonic() > deadline:
proc.kill() proc.kill()
@@ -287,7 +291,8 @@ def _download_worker(job: dict, track: dict, video_id: str) -> None:
pass pass
proc.wait(timeout=30) proc.wait(timeout=30)
if proc.returncode != 0: if proc.returncode != 0:
stderr_tail = (proc.stderr.read() or "").strip().splitlines() errfile.seek(0)
stderr_tail = (errfile.read() or "").strip().splitlines()
detail = stderr_tail[-1] if stderr_tail else f"exit code {proc.returncode}" detail = stderr_tail[-1] if stderr_tail else f"exit code {proc.returncode}"
raise RuntimeError(detail) raise RuntimeError(detail)
if not target.exists(): if not target.exists():
@@ -548,12 +553,16 @@ async def report(request: Request) -> dict:
"""The browser reports real playback state (source of truth for the player).""" """The browser reports real playback state (source of truth for the player)."""
body = await request.json() body = await request.json()
room = str(body.get("room") or "default") room = str(body.get("room") or "default")
try:
position = float(body.get("position") or 0.0)
except (TypeError, ValueError):
position = 0.0
with _lock: with _lock:
st = _room_state(room) st = _room_state(room)
st["reported"] = { st["reported"] = {
"track_id": body.get("track_id"), "track_id": body.get("track_id"),
"state": str(body.get("state") or "stopped"), "state": str(body.get("state") or "stopped"),
"position": float(body.get("position") or 0.0), "position": position,
"duration": body.get("duration"), "duration": body.get("duration"),
"ts": time.time(), "ts": time.time(),
} }