docs: media player subsystem architecture and conventions

Document the media server process, /media/ port map, library layout on
the ./media bind mount, Hope's media MCP toolset, the music/mic
mutual-exclusion rule, and yt-dlp download/proxy conventions.
This commit is contained in:
snowspeeder committed 2026-08-29 13:22:01 -04:00
1 parent 04c4a53594
commit a53c090882
9 files changed
+479 -13

No files matched your search

+17 -6
View File
@@ -63,6 +63,12 @@ _executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="media-dl")
for _d in (FILES_DIR, THUMBS_DIR, JOBS_DIR):
_d.mkdir(parents=True, exist_ok=True)
# The bind mount may be inspected/cleaned from the host by a non-root
# user (Unraid share convention) — keep the dirs world-traversable.
try:
os.chmod(_d, 0o777)
except OSError:
pass
# ── Library store ───────────────────────────────────────────────────────────
@@ -263,6 +269,7 @@ def _download_worker(job: dict, track: dict, video_id: str) -> 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)
@@ -385,7 +392,10 @@ async def search(request: Request) -> dict:
query = str(body.get("query", "")).strip()
if not query:
raise HTTPException(400, "query is required")
limit = max(1, min(int(body.get("limit", 6)), 15))
try:
limit = max(1, min(int(body.get("limit", 6)), 15))
except (TypeError, ValueError):
raise HTTPException(400, "limit must be an integer")
return {"query": query, "results": _run_search(query, limit)}
@@ -479,10 +489,11 @@ async def control(request: Request) -> dict:
elif action == "stop":
st["desired"] = "stopped"
elif action == "seek":
st["seek_hint"] = {
"position": max(0.0, float(body.get("position", 0))),
"ts": time.time(),
}
try:
position = max(0.0, float(body.get("position", 0)))
except (TypeError, ValueError):
raise HTTPException(400, "position must be a number")
st["seek_hint"] = {"position": position, "ts": time.time()}
elif action == "skip":
current = st["track_id"]
candidates = sorted(
@@ -574,7 +585,7 @@ async def stream_proxy(track_id: str, request: Request):
raise HTTPException(409, f"track is {track.get('status')}, not ready")
range_header = request.headers.get("range")
headers = {"Range": range_header} if range_header else {"Range": "bytes=0-"}
headers = {"Range": range_header} if range_header else {}
client = httpx.AsyncClient(timeout=httpx.Timeout(30, read=600), follow_redirects=True)
try:
upstream = await client.send(client.build_request("GET", track["url"], headers=headers), stream=True)