diff --git a/src/libpoketube/libpoketube-core.js b/src/libpoketube/libpoketube-core.js index a58f03e0..c969ea22 100644 --- a/src/libpoketube/libpoketube-core.js +++ b/src/libpoketube/libpoketube-core.js @@ -50,7 +50,6 @@ class InnerTubePokeVidious { return { error: true, message: "No video ID provided" }; } - // If cached result exists and is less than 1 hour old, return it if (this.cache[v] && Date.now() - this.cache[v].timestamp < 3600000) { return this.cache[v].result; } @@ -59,30 +58,17 @@ class InnerTubePokeVidious { "User-Agent": this.useragent, }; - // Retries only within a 5s window that starts AFTER the first 500/502. - // Fast path: one plain fetch with no extra timers/signals unless 500/502 occurs. - const fetchWithRetry = async (url, options = {}, maxRetryTime = 5000) => { + const fetchWithRetry = async (url, options = {}, maxRetryTime = 8000) => { let lastError; - // Trigger statuses that arm the retry window - const TRIGGER = 500 | 502; // bitwise trick for branch hints; DO NOT rely on value const isTrigger = (s) => (s === 500 || s === 502); - - // Once armed, these are retryable (plus network errors) const RETRYABLE = new Set([429, 500, 502, 503, 504]); - - // Backoff (decorrelated jitter) — gentle defaults const MIN_DELAY_MS = 150; const BASE_DELAY_MS = 250; const MAX_DELAY_MS = 2000; const JITTER_FACTOR = 3; - - // Per-attempt timeout (only used after window is armed) const PER_TRY_TIMEOUT_MS = 2000; - const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); - - // Parse Retry-After (delta-seconds or HTTP-date) const parseRetryAfter = (hdr) => { if (!hdr) return null; const s = String(hdr).trim(); @@ -93,7 +79,6 @@ class InnerTubePokeVidious { return null; }; - // FAST PATH: single plain fetch (no AbortController, no timeout, no extra work) let res; try { res = await fetch(url, { @@ -104,33 +89,26 @@ class InnerTubePokeVidious { }, }); } catch (err) { - // Network error BEFORE any 500/502 trigger → surface immediately (no retries) this?.initError?.(`Fetch error for ${url}`, err); throw err; } if (res.ok) return res; - - // Not a trigger? return immediately (no retry window, no delays) if (!isTrigger(res.status)) return res; - // SLOW PATH (only after a 500/502): arm the retry window const retryStart = Date.now(); - let delayMs = BASE_DELAY_MS; // backoff seed + let delayMs = BASE_DELAY_MS; let attempt = 1; const callerSignal = options?.signal || null; - // Helper: one attempt with internal timeout that respects caller aborts const attemptWithTimeout = async (timeoutMs) => { const controller = new AbortController(); const timer = setTimeout( () => controller.abort(new Error("Fetch attempt timed out")), timeoutMs > 0 ? timeoutMs : 1 ); - const onCallerAbort = () => controller.abort(callerSignal?.reason || new Error("Aborted by caller")); - if (callerSignal) { if (callerSignal.aborted) { controller.abort(callerSignal.reason || new Error("Aborted by caller")); @@ -138,7 +116,6 @@ class InnerTubePokeVidious { callerSignal.addEventListener("abort", onCallerAbort, { once: true }); } } - try { return await fetch(url, { ...options, @@ -154,17 +131,12 @@ class InnerTubePokeVidious { } }; - // Optional short stagger before the first retry to reduce herd effects - // await sleep(50 + ((Math.random() * 150) | 0)); - - // Retry loop within the 5s window while (true) { const elapsed = Date.now() - retryStart; const remaining = maxRetryTime - elapsed; if (remaining <= 0) { throw new Error(`Fetch failed for ${url} after ${maxRetryTime}ms`); } - const perTryTimeout = Math.min(PER_TRY_TIMEOUT_MS, Math.max(100, remaining - 50)); try { @@ -172,17 +144,14 @@ class InnerTubePokeVidious { if (r.ok) return r; if (!RETRYABLE.has(r.status)) { - // Non-retryable after window armed → return immediately return r; } - // Respect server cooldown if provided const retryAfterMs = parseRetryAfter(r.headers.get("Retry-After")); let waitMs; if (retryAfterMs != null) { waitMs = Math.max(MIN_DELAY_MS, Math.min(retryAfterMs, Math.max(0, remaining - 10))); } else { - // Decorrelated jitter: min(MAX, random(MIN, prev*factor)) const next = Math.min(MAX_DELAY_MS, Math.random() * delayMs * JITTER_FACTOR); delayMs = next < MIN_DELAY_MS ? MIN_DELAY_MS : next; waitMs = Math.min(delayMs, Math.max(0, remaining - 10)); @@ -197,15 +166,11 @@ class InnerTubePokeVidious { await sleep(waitMs); continue; } catch (err) { - // Caller aborted → surface immediately if (callerSignal && callerSignal.aborted) throw err; - lastError = err; - const remaining2 = maxRetryTime - (Date.now() - retryStart); if (remaining2 <= 0) throw lastError; - // Backoff after network/timeout errors, too const next = Math.min(MAX_DELAY_MS, Math.random() * delayMs * JITTER_FACTOR); delayMs = next < MIN_DELAY_MS ? MIN_DELAY_MS : next; const waitMs = Math.min(delayMs, Math.max(0, remaining2 - 10)); @@ -219,64 +184,36 @@ class InnerTubePokeVidious { } }; - // Helper: fetch the videos endpoint but fall back to inv_fallback if primary fails or times out - const fetchVideoTextWithFallback = async (videoId, lang, region) => { - const primaryUrl = `${this.config.invapi}/videos/${videoId}?hl=${lang}®ion=${region}&h=${btoa( - Date.now() - )}`; - - // Build fallback URL carefully — allow inv_fallback to end with or without trailing slash. - // inv_fallback is expected to be something like: https://poketube.duti.dev/api/v1/videos/ - const fallbackBase = (this.config.inv_fallback || "").replace(/\/+$/, ""); - const fallbackUrl = - fallbackBase && fallbackBase.includes("/videos") - ? `${fallbackBase.replace(/\/+$/, "")}/${videoId}` - : `${fallbackBase}/${videoId}?hl=${lang}®ion=${region}&h=${btoa(Date.now())}`; - - const PRIMARY_TIMEOUT_MS = 5000; - - // Try primary within timeout window - try { - // Race the fetchWithRetry against a timeout promise - const primaryPromise = fetchWithRetry(primaryUrl, {}, 8000); - const timeoutPromise = new Promise((_, rej) => - setTimeout(() => rej(new Error("Primary videos API timed out")), PRIMARY_TIMEOUT_MS) - ); - - const r = await Promise.race([primaryPromise, timeoutPromise]); - // If we got a Response-like object, return its text (works even if r is a Response) - return await r.text(); - } catch (errPrimary) { - // Primary failed or timed out — try fallback if available - this.initError("Primary videos API failed or timed out, trying fallback", errPrimary?.stack || errPrimary); - - if (!fallbackBase) { - // No fallback configured; rethrow primary error - throw errPrimary; - } - - try { - const r2 = await fetchWithRetry(fallbackUrl, {}, 8000); - // If fallback returns a Response-like object, return its text - return await r2.text(); - } catch (errFallback) { - // Both failed — log and rethrow - this.initError("Fallback videos API failed", errFallback?.stack || errFallback); - throw errFallback; - } - } - }; - try { const [invComments, videoInfo] = await Promise.all([ - // Comments: only from primary invapi, same as before fetchWithRetry( `${this.config.invapi}/comments/${v}?hl=${contentlang}®ion=${contentregion}&h=${btoa( Date.now() )}` ).then((res) => res?.text()), - // Videos: use the wrapper that falls back after ~5s to inv_fallback - fetchVideoTextWithFallback(v, contentlang, contentregion), + (async () => { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + try { + const res = await fetchWithRetry( + `${this.config.invapi}/videos/${v}?hl=${contentlang}®ion=${contentregion}&h=${btoa( + Date.now() + )}`, + { signal: controller.signal } + ); + return await res.text(); + } catch (err) { + this.initError("Primary video fetch failed, trying fallback", err); + const fallbackRes = await fetchWithRetry( + `${this.config.inv_fallback}${v}?hl=${contentlang}®ion=${contentregion}&h=${btoa( + Date.now() + )}` + ); + return await fallbackRes.text(); + } finally { + clearTimeout(timeout); + } + })(), ]); const comments = this.getJson(invComments); @@ -302,9 +239,6 @@ class InnerTubePokeVidious { let color = "#0ea5e9"; let color2 = "#111827"; try { - // `sqp` is a URL parameter used by YouTube thumbnail/image servers - // to request a specific scale, crop or quality profile (base64-encoded), - // controlling how the thumbnail is sized or compressed. const palette = await getColors( `https://i.ytimg.com/vi/${v}/hqdefault.jpg?sqp=${this.sqp}` ); @@ -321,7 +255,7 @@ class InnerTubePokeVidious { vid, comments, channel_uploads: " ", - engagement: returnyoutubedisapi.engagement, + engagement: returnyoutubedislikesapi.engagement, wiki: "", desc: "", color, @@ -353,7 +287,7 @@ class InnerTubePokeVidious { const pokeTubeApiCore = new InnerTubePokeVidious({ invapi: "https://invid-api.poketube.fun/bHj665PpYhUdPWuKPfZuQGoX/api/v1", - inv_fallback:"https://poketube.duti.dev/api/v1/videos/", + inv_fallback: "https://poketube.duti.dev/api/v1/videos/", useragent: config.useragent, });