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:
1 parent
04c4a53594
commit
a53c090882
9 files changed
+479
-13
No files matched your search
+10
-2
@@ -1263,7 +1263,13 @@ function clearMediaAudio() {
|
||||
|
||||
function updateMediaNowCard(track) {
|
||||
mediaTitleEl.textContent = track.title || track.id;
|
||||
mediaChannelEl.textContent = track.channel || (track.kind === "stream" ? "Live stream" : "");
|
||||
if (track.status === "failed") {
|
||||
mediaChannelEl.textContent = "Download failed — pick another result or try again.";
|
||||
} else if (track.status === "downloading") {
|
||||
mediaChannelEl.textContent = "Preparing…";
|
||||
} else {
|
||||
mediaChannelEl.textContent = track.channel || (track.kind === "stream" ? "Live stream" : "");
|
||||
}
|
||||
if (track.art_url) {
|
||||
mediaArt.src = track.art_url;
|
||||
mediaArt.hidden = false;
|
||||
@@ -1403,7 +1409,9 @@ mediaAudio.addEventListener("timeupdate", () => {
|
||||
});
|
||||
mediaAudio.addEventListener("durationchange", updateSeekUI);
|
||||
mediaAudio.addEventListener("error", () => {
|
||||
if (mediaCurrentTrackId) {
|
||||
// An empty src (track still downloading) also fires this — only surface
|
||||
// real playback failures.
|
||||
if (mediaCurrentTrackId && mediaAudio.getAttribute("src")) {
|
||||
mediaChannelEl.textContent = "Playback error — the track may still be preparing.";
|
||||
}
|
||||
});
|
||||
|
||||
+17
-6
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user