Subtitles: wire hianime's external .vtt into the player

- 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 <track> 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
This commit is contained in:
jozamudi committed 2026-09-07 02:46:28 +00:00
1 parent 834ca80a8c
commit 5b8170ece0
4 files changed
+37 -1

No files matched your search

+5 -1
View File
@@ -533,7 +533,11 @@ def api_stream(ep_id: int, lang: str = "jpn", request: Request = None):
master = _upstream(upstream.master_m3u8, ep_id, lang) master = _upstream(upstream.master_m3u8, ep_id, lang)
if not master: if not master:
raise HTTPException(404, f"no {lang} stream for episode {ep_id}") 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: try:
out = go() out = go()
+9
View File
@@ -20,6 +20,7 @@ KEY = b"otaku-embed-v1"
REFERER = None # embed origin, set when a master playlist is resolved REFERER = None # embed origin, set when a master playlist is resolved
STREAM_HOSTS = set() # playlist/segment hosts the /stream proxy may fetch 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): def _search(q):
@@ -107,6 +108,9 @@ def _master_m3u8(ep_id, lang="eng"):
if line and not line.startswith("#"): if line and not line.startswith("#"):
hosts.add(urlparse(urljoin(master, line)).netloc) hosts.add(urlparse(urljoin(master, line)).netloc)
STREAM_HOSTS |= hosts 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 master
return None return None
@@ -124,3 +128,8 @@ def stream_meta(url):
if REFERER and urlparse(url).netloc in STREAM_HOSTS: if REFERER and urlparse(url).netloc in STREAM_HOSTS:
return {"Referer": REFERER} return {"Referer": REFERER}
return None return None
def subtitles(ep_id):
"""External .vtt for this episode, or None (anidb bakes subs in)."""
return SUBS.get(ep_id)
+11
View File
@@ -205,6 +205,17 @@ def stream_meta(url):
return None 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__": if __name__ == "__main__":
# ponytail: assert self-check instead of a test framework. # ponytail: assert self-check instead of a test framework.
# Needs a live source: ANI_SOURCE=<provider> python -m server.upstream # Needs a live source: ANI_SOURCE=<provider> python -m server.upstream
+12
View File
@@ -927,6 +927,17 @@ async function play(ep_id, ep_no) {
const s = await api(`/api/episode/${ep_id}/stream?lang=${lang}`); const s = await api(`/api/episode/${ep_id}/stream?lang=${lang}`);
stopHls(); stopHls();
const v = videoEl(); const v = videoEl();
let subTrack = null;
if (s.sub) {
// external subtitle track (hianime serves a .vtt; anidb bakes subs in).
// native <track> 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); v.playbackRate = +(localStorage.getItem("aniweb_spd") || 1);
document.getElementById("player").replaceChildren(v); document.getElementById("player").replaceChildren(v);
playerOverlay("Loading\u2026", null, null, true); 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)}); ov: (document.querySelector(".pl-overlay")?.textContent || "").slice(0, 60)});
}, 15000); }, 15000);
} }
if (subTrack) { v.append(subTrack); subTrack.mode = "showing"; }
currentEp = {ep_id, ep_no, v}; currentEp = {ep_id, ep_no, v};
lastSavedPos = 0; lastSavedPos = 0;
lastSeenPos = 0; lastSeenPos = 0;