Provider layer + hianime source (primary)

- providers/{anidb,anikoto,hianime}.py: per-source modules behind the
  ANI_SOURCE fallback dispatch (upstream.py _call/_providers)
- hianime.py: pystardust/ani-cli#1897 flow — search/episode-list/
  servers API, HD-1 hash -> base64 embed url, window.__P blob
  (base64 + XOR 'otaku-embed-v1') -> master m3u8; registers CDN hosts
  and the required Referer via stream_meta()
- upstream.py: get_stream/fetch_parallel accept extra headers;
  default source order hianime,anidb,anikoto (anidb.app is 503)
- app.py: /stream allowlist accepts provider-registered hosts and
  sends their Referer; status probe follows the primary provider
  (hianime variant URIs are relative to the variant path)
- app.js: error-time position save floors at furthest point reached
  (hls.js resets currentTime to 0 before playerFail on fatal errors)
- tests: default-order + hianime import checks; live browser suite green
This commit is contained in:
jozamudi committed 2026-09-07 01:38:53 +00:00
1 parent ff8311c1a8
commit 834ca80a8c
8 files changed
+553 -123

No files matched your search

+18 -8
View File
@@ -762,7 +762,7 @@ function videoEl() {
function stopHls() { if (hls) { hls.destroy(); hls = null; } if (bufTimer) { clearInterval(bufTimer); bufTimer = null; } }
let currentEp = null, lastSavedPos = 0, playerBroken = false, nextCta = null;
let currentEp = null, lastSavedPos = 0, lastSeenPos = 0, playerBroken = false, nextCta = null;
function playerOverlay(text, onAction, actionLabel = "Retry", spin = false) {
const p = document.getElementById("player");
@@ -786,8 +786,9 @@ function playerFail(msg) {
playerBroken = true;
// a fatal error stops the periodic saves (timeupdate/pause never come
// again); persist the position now or the user loses everything since
// the last 10s save when they leave
savePos();
// the last 10s save when they leave. hls.js may have already reset
// currentTime to 0, so floor at the furthest point reached this episode
savePos(lastSeenPos);
const { ep_id, ep_no } = currentEp;
postEvent({type: "error", detail: msg, ep_id, ep: ep_no, lang, ua: navigator.userAgent});
playerOverlay("Playback failed: " + msg, () => play(ep_id, ep_no));
@@ -1008,6 +1009,7 @@ async function play(ep_id, ep_no) {
}
currentEp = {ep_id, ep_no, v};
lastSavedPos = 0;
lastSeenPos = 0;
if (nextCta) { nextCta.remove(); nextCta = null; }
const saved = history.find(x => x.slug_id === detail.slug_id && x.ep_id === ep_id);
if (saved?.pos > 10) {
@@ -1090,16 +1092,24 @@ const meta = v => ({...(Number.isFinite(v.duration) && v.duration > 0 ? {dur: v.
// one savePos covers all video elements: it reads currentEp, so stale
// closures from earlier plays can't write the wrong episode's position
function savePos() {
if (!currentEp || currentEp.v.currentTime <= 3) return;
lastSavedPos = currentEp.v.currentTime;
function savePos(floor = 0) {
if (!currentEp) return;
// a fatal frag error can reset video.currentTime to 0 before this runs
// (hls.js recovery); playerFail passes the furthest point reached as floor
const pos = Math.max(currentEp.v.currentTime, floor);
if (pos <= 3) return;
lastSavedPos = pos;
postHistory({...meta(currentEp.v), title: detail.name, slug_id: detail.slug_id, season_id: seasonId,
ep_id: currentEp.ep_id, lang, ep: currentEp.ep_no, pos: currentEp.v.currentTime,
ep_id: currentEp.ep_id, lang, ep: currentEp.ep_no, pos,
...(epsList.length ? {total: epsList.length} : {})});
}
function watchPos(v) {
v.addEventListener("timeupdate", () => { if (v.currentTime - lastSavedPos >= 10) savePos(); });
// 'seeking' fires when currentTime is set, even before the target segment
// buffers — so a seek made just before a stream death is still captured
const seen = () => { lastSeenPos = Math.max(lastSeenPos, v.currentTime); };
v.addEventListener("timeupdate", () => { seen(); if (v.currentTime - lastSavedPos >= 10) savePos(); });
v.addEventListener("seeking", seen);
v.addEventListener("pause", savePos);
// pagehide is bound once at boot (per-play listeners accumulated and fired
// with stale video elements)