fix: harden media server — temp-file yt-dlp stderr, parse guards, UI failure surfacing
Rebuild & Deploy / rebuild (push) Failing after 4m27s
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:
1 parent
f534b8ccb5
commit
48b92585c6
1 file changed
+46
-37
+46
-37
@@ -29,6 +29,7 @@ import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
@@ -259,42 +260,46 @@ def _download_worker(job: dict, track: dict, video_id: str) -> None:
|
||||
]
|
||||
job["stage"] = "downloading"
|
||||
info: dict = {}
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
text=True, errors="replace",
|
||||
)
|
||||
deadline = time.monotonic() + DOWNLOAD_TIMEOUT
|
||||
assert proc.stdout is not None and proc.stderr is not None
|
||||
for line in proc.stdout:
|
||||
if time.monotonic() > deadline:
|
||||
proc.kill()
|
||||
proc.wait(timeout=30)
|
||||
raise RuntimeError(f"Download exceeded {DOWNLOAD_TIMEOUT}s")
|
||||
line = line.strip()
|
||||
m = _PROGRESS_RE.search(line)
|
||||
if m:
|
||||
job["progress"] = min(85.0, float(m.group(1)) * 0.85)
|
||||
_persist_job(job)
|
||||
elif line.startswith(("[ExtractAudio]", "[Metadata]", "[EmbedThumbnail]")):
|
||||
job["stage"] = "transcoding"
|
||||
job["progress"] = max(job["progress"], 86.0)
|
||||
_persist_job(job)
|
||||
elif line.startswith("{"):
|
||||
try:
|
||||
info = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
proc.wait(timeout=30)
|
||||
if proc.returncode != 0:
|
||||
stderr_tail = (proc.stderr.read() or "").strip().splitlines()
|
||||
detail = stderr_tail[-1] if stderr_tail else f"exit code {proc.returncode}"
|
||||
raise RuntimeError(detail)
|
||||
if not target.exists():
|
||||
raise RuntimeError("yt-dlp finished but no mp3 was produced")
|
||||
except Exception as exc: # noqa: BLE001 — any failure marks the track failed
|
||||
_fail_job(job, track, str(exc))
|
||||
return
|
||||
# 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:
|
||||
proc = subprocess.Popen(
|
||||
cmd, stdout=subprocess.PIPE, stderr=errfile,
|
||||
text=True, errors="replace",
|
||||
)
|
||||
deadline = time.monotonic() + DOWNLOAD_TIMEOUT
|
||||
assert proc.stdout is not None
|
||||
for line in proc.stdout:
|
||||
if time.monotonic() > deadline:
|
||||
proc.kill()
|
||||
proc.wait(timeout=30)
|
||||
raise RuntimeError(f"Download exceeded {DOWNLOAD_TIMEOUT}s")
|
||||
line = line.strip()
|
||||
m = _PROGRESS_RE.search(line)
|
||||
if m:
|
||||
job["progress"] = min(85.0, float(m.group(1)) * 0.85)
|
||||
_persist_job(job)
|
||||
elif line.startswith(("[ExtractAudio]", "[Metadata]", "[EmbedThumbnail]")):
|
||||
job["stage"] = "transcoding"
|
||||
job["progress"] = max(job["progress"], 86.0)
|
||||
_persist_job(job)
|
||||
elif line.startswith("{"):
|
||||
try:
|
||||
info = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
proc.wait(timeout=30)
|
||||
if proc.returncode != 0:
|
||||
errfile.seek(0)
|
||||
stderr_tail = (errfile.read() or "").strip().splitlines()
|
||||
detail = stderr_tail[-1] if stderr_tail else f"exit code {proc.returncode}"
|
||||
raise RuntimeError(detail)
|
||||
if not target.exists():
|
||||
raise RuntimeError("yt-dlp finished but no mp3 was produced")
|
||||
except Exception as exc: # noqa: BLE001 — any failure marks the track failed
|
||||
_fail_job(job, track, str(exc))
|
||||
return
|
||||
|
||||
thumb_src = FILES_DIR / f"{track['id']}.jpg"
|
||||
try:
|
||||
@@ -548,12 +553,16 @@ async def report(request: Request) -> dict:
|
||||
"""The browser reports real playback state (source of truth for the player)."""
|
||||
body = await request.json()
|
||||
room = str(body.get("room") or "default")
|
||||
try:
|
||||
position = float(body.get("position") or 0.0)
|
||||
except (TypeError, ValueError):
|
||||
position = 0.0
|
||||
with _lock:
|
||||
st = _room_state(room)
|
||||
st["reported"] = {
|
||||
"track_id": body.get("track_id"),
|
||||
"state": str(body.get("state") or "stopped"),
|
||||
"position": float(body.get("position") or 0.0),
|
||||
"position": position,
|
||||
"duration": body.get("duration"),
|
||||
"ts": time.time(),
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user