From 5b8170ece0530de788faeec73babf9489f93a16b Mon Sep 17 00:00:00 2001 From: Josue Zamudio Date: Mon, 7 Sep 2026 02:46:28 +0000 Subject: [PATCH] Subtitles: wire hianime's external .vtt into the player MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - hianime.py: capture the first .vtt from the deobfuscated embed JSON (SUBS), expose subtitles(ep_id) - upstream.py: subtitles() dispatcher (None for anidb, which bakes subs in) - app.py: /api/episode/{id}/stream also returns a 'sub' stream token - app.js: add a native after hls.attachMedia (hls.js _cleanTracks disables tracks present at attach time — that silently killed earlier attempts) and set mode=showing so Chrome fetches the cues --- server/app.py | 6 +++++- server/providers/hianime.py | 9 +++++++++ server/upstream.py | 11 +++++++++++ static/app.js | 12 ++++++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/server/app.py b/server/app.py index 3523936..cd300ee 100644 --- a/server/app.py +++ b/server/app.py @@ -533,7 +533,11 @@ def api_stream(ep_id: int, lang: str = "jpn", request: Request = None): master = _upstream(upstream.master_m3u8, ep_id, lang) if not master: raise HTTPException(404, f"no {lang} stream for episode {ep_id}") - return {"token": _b64(master)} + out = {"token": _b64(master)} + sub = upstream.subtitles(ep_id) # external .vtt (hianime); anidb bakes subs in + if sub: + out["sub"] = _b64(sub) + return out try: out = go() diff --git a/server/providers/hianime.py b/server/providers/hianime.py index 6f11fec..741879b 100644 --- a/server/providers/hianime.py +++ b/server/providers/hianime.py @@ -20,6 +20,7 @@ KEY = b"otaku-embed-v1" REFERER = None # embed origin, set when a master playlist is resolved STREAM_HOSTS = set() # playlist/segment hosts the /stream proxy may fetch +SUBS = {} # ep_id -> external .vtt url (the player bakes no subs) def _search(q): @@ -107,6 +108,9 @@ def _master_m3u8(ep_id, lang="eng"): if line and not line.startswith("#"): hosts.add(urlparse(urljoin(master, line)).netloc) STREAM_HOSTS |= hosts + subs = j.get("subtitles") or [] + if subs and (subs[0] or {}).get("src"): + SUBS[ep_id] = subs[0]["src"] # first track is the english one return master return None @@ -124,3 +128,8 @@ def stream_meta(url): if REFERER and urlparse(url).netloc in STREAM_HOSTS: return {"Referer": REFERER} return None + + +def subtitles(ep_id): + """External .vtt for this episode, or None (anidb bakes subs in).""" + return SUBS.get(ep_id) diff --git a/server/upstream.py b/server/upstream.py index bbe6624..5282c86 100644 --- a/server/upstream.py +++ b/server/upstream.py @@ -205,6 +205,17 @@ def stream_meta(url): return None +def subtitles(ep_id): + """External .vtt url for an episode, or None (anidb bakes subs in).""" + for p in _providers(): + fn = getattr(_prov(p), "subtitles", None) + if fn: + v = fn(ep_id) + if v: + return v + return None + + if __name__ == "__main__": # ponytail: assert self-check instead of a test framework. # Needs a live source: ANI_SOURCE= python -m server.upstream diff --git a/static/app.js b/static/app.js index ee8b951..e4c30ed 100644 --- a/static/app.js +++ b/static/app.js @@ -927,6 +927,17 @@ async function play(ep_id, ep_no) { const s = await api(`/api/episode/${ep_id}/stream?lang=${lang}`); stopHls(); const v = videoEl(); + let subTrack = null; + if (s.sub) { + // external subtitle track (hianime serves a .vtt; anidb bakes subs in). + // native works on MSE video (same as dash.js); served via /stream + // so the referer-hungry CDN is behind our proxy. Appended AFTER the + // branch below: hls.js's attachMedia runs _cleanTracks, which disables + // every track present at attach time + subTrack = document.createElement("track"); + subTrack.kind = "subtitles"; subTrack.srclang = "en"; subTrack.label = "English"; subTrack.default = true; + subTrack.src = "/stream/" + s.sub; + } v.playbackRate = +(localStorage.getItem("aniweb_spd") || 1); document.getElementById("player").replaceChildren(v); playerOverlay("Loading\u2026", null, null, true); @@ -1007,6 +1018,7 @@ async function play(ep_id, ep_no) { ov: (document.querySelector(".pl-overlay")?.textContent || "").slice(0, 60)}); }, 15000); } + if (subTrack) { v.append(subTrack); subTrack.mode = "showing"; } currentEp = {ep_id, ep_no, v}; lastSavedPos = 0; lastSeenPos = 0;