fix stuff + add stuff
This commit is contained in:
parent
0bad881acb
commit
71409df15f
@ -65,57 +65,37 @@ class InnerTubePokeVidious {
|
||||
};
|
||||
|
||||
const fetchWithRetry = async (url, options = {}, maxRetryTime = 5000) => {
|
||||
let lastError;
|
||||
|
||||
const isTrigger = (s) => (s === 500 || s === 502);
|
||||
const RETRYABLE = new Set([429, 500, 502, 503, 504]);
|
||||
const MIN_DELAY_MS = 150;
|
||||
const BASE_DELAY_MS = 250;
|
||||
const MAX_DELAY_MS = 2000;
|
||||
const JITTER_FACTOR = 3;
|
||||
const PER_TRY_TIMEOUT_MS = 2000;
|
||||
const MIN_DELAY_MS = 100;
|
||||
const BASE_DELAY_MS = 120;
|
||||
const MAX_DELAY_MS = 800;
|
||||
const JITTER_FACTOR = 2;
|
||||
const PER_TRY_TIMEOUT_MS = 1500;
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
const parseRetryAfter = (hdr) => {
|
||||
if (!hdr) return null;
|
||||
const s = String(hdr).trim();
|
||||
const delta = Number(s);
|
||||
if (Number.isFinite(delta)) return Math.max(0, (delta * 1000) | 0);
|
||||
const secs = Number(s);
|
||||
if (Number.isFinite(secs)) return Math.max(0, (secs * 1000) | 0);
|
||||
const when = Date.parse(s);
|
||||
if (!Number.isNaN(when)) return Math.max(0, when - Date.now());
|
||||
return null;
|
||||
};
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...options?.headers,
|
||||
...headers,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
this?.initError?.(`Fetch error for ${url}`, err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (res.ok) return res;
|
||||
if (!isTrigger(res.status)) return res;
|
||||
|
||||
const retryStart = Date.now();
|
||||
let delayMs = BASE_DELAY_MS;
|
||||
let attempt = 1;
|
||||
const callerSignal = options?.signal || null;
|
||||
|
||||
const attemptWithTimeout = async (timeoutMs) => {
|
||||
const attemptWithTimeout = async (timeoutMs, attemptSignal) => {
|
||||
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) {
|
||||
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 (attemptSignal) {
|
||||
if (attemptSignal.aborted) {
|
||||
controller.abort(attemptSignal.reason || new Error("Aborted by attemptSignal"));
|
||||
} else {
|
||||
attemptSignal.addEventListener("abort", onCallerAbort, { once: true });
|
||||
}
|
||||
} else if (callerSignal) {
|
||||
if (callerSignal.aborted) {
|
||||
controller.abort(callerSignal.reason || new Error("Aborted by caller"));
|
||||
} else {
|
||||
@ -133,30 +113,38 @@ class InnerTubePokeVidious {
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
if (callerSignal) callerSignal.removeEventListener("abort", onCallerAbort);
|
||||
if (attemptSignal) attemptSignal.removeEventListener("abort", onCallerAbort);
|
||||
if (callerSignal && !attemptSignal) callerSignal.removeEventListener("abort", onCallerAbort);
|
||||
}
|
||||
};
|
||||
|
||||
const start = Date.now();
|
||||
let delayMs = BASE_DELAY_MS;
|
||||
let attempt = 1;
|
||||
let lastErr = null;
|
||||
|
||||
while (true) {
|
||||
const elapsed = Date.now() - retryStart;
|
||||
const elapsed = Date.now() - start;
|
||||
const remaining = maxRetryTime - elapsed;
|
||||
if (remaining <= 0) {
|
||||
throw new Error(`Fetch failed for ${url} after ${maxRetryTime}ms`);
|
||||
const err = new Error(`Fetch failed for ${url} after ${maxRetryTime}ms`);
|
||||
err.cause = lastErr;
|
||||
throw err;
|
||||
}
|
||||
|
||||
const perTryTimeout = Math.min(PER_TRY_TIMEOUT_MS, Math.max(100, remaining - 50));
|
||||
|
||||
try {
|
||||
const r = await attemptWithTimeout(perTryTimeout);
|
||||
if (r.ok) return r;
|
||||
const res = await attemptWithTimeout(perTryTimeout, callerSignal);
|
||||
// if success or non-retryable status, return immediately
|
||||
if (res.ok) return res;
|
||||
if (!RETRYABLE.has(res.status)) return res;
|
||||
|
||||
if (!RETRYABLE.has(r.status)) {
|
||||
return r;
|
||||
}
|
||||
|
||||
const retryAfterMs = parseRetryAfter(r.headers.get("Retry-After"));
|
||||
// retryable status -> compute wait
|
||||
const ra = parseRetryAfter(res.headers.get("Retry-After"));
|
||||
let waitMs;
|
||||
if (retryAfterMs != null) {
|
||||
waitMs = Math.max(MIN_DELAY_MS, Math.min(retryAfterMs, Math.max(0, remaining - 10)));
|
||||
if (ra != null) {
|
||||
waitMs = Math.max(MIN_DELAY_MS, Math.min(ra, Math.max(0, remaining - 10)));
|
||||
} else {
|
||||
const next = Math.min(MAX_DELAY_MS, Math.random() * delayMs * JITTER_FACTOR);
|
||||
delayMs = next < MIN_DELAY_MS ? MIN_DELAY_MS : next;
|
||||
@ -167,20 +155,21 @@ class InnerTubePokeVidious {
|
||||
throw new Error(`Fetch failed for ${url} after ${maxRetryTime}ms (window depleted)`);
|
||||
}
|
||||
|
||||
this?.initError?.(`Retrying fetch for ${url}`, r.status);
|
||||
this?.initError?.(`Retrying fetch for ${url}`, res.status);
|
||||
attempt++;
|
||||
await sleep(waitMs);
|
||||
continue;
|
||||
} catch (err) {
|
||||
// aborted by caller -> bubble up
|
||||
if (callerSignal && callerSignal.aborted) throw err;
|
||||
lastError = err;
|
||||
const remaining2 = maxRetryTime - (Date.now() - retryStart);
|
||||
if (remaining2 <= 0) throw lastError;
|
||||
lastErr = err;
|
||||
const remaining2 = maxRetryTime - (Date.now() - start);
|
||||
if (remaining2 <= 0) throw lastErr;
|
||||
|
||||
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));
|
||||
if (waitMs <= 0) throw lastError;
|
||||
if (waitMs <= 0) throw lastErr;
|
||||
|
||||
this?.initError?.(`Fetch error for ${url}`, err);
|
||||
attempt++;
|
||||
@ -190,60 +179,82 @@ class InnerTubePokeVidious {
|
||||
}
|
||||
};
|
||||
|
||||
// --- scheduling logic ---
|
||||
// Rotate which window
|
||||
// prefers fallback/primary every 2 hours using a custom sequence.
|
||||
//
|
||||
// Sequence: fallback, normal, normal, normal, normal, fallback
|
||||
// each element = which API is preferred *for that 2-hour block*.
|
||||
//
|
||||
// Within the chosen preference, the 10-minute switching (minute % 20 >= 10)
|
||||
// still applies, but which side corresponds to the inFallbackWindow is flipped
|
||||
// depending on the 2-hour preference.
|
||||
|
||||
const minute = new Date().getMinutes();
|
||||
const hour = new Date().getHours();
|
||||
|
||||
// pattern for each 2-hour block (6 blocks to cover 12 hours; repeats every 12 hours)
|
||||
const pattern = ["fallback", "normal", "normal", "normal", "normal", "fallback"];
|
||||
|
||||
// determine which 2-hour slot we're in (0..11 hours cover repeating pattern every 12 hours)
|
||||
const twoHourIndex = Math.floor(hour / 2) % pattern.length;
|
||||
const currentPreference = pattern[twoHourIndex]; // 'fallback' or 'normal'
|
||||
const currentPreference = pattern[twoHourIndex];
|
||||
const inFallbackWindow = minute % 20 >= 10;
|
||||
|
||||
// 10-minute toggle windows
|
||||
const inFallbackWindow = minute % 20 >= 10;
|
||||
|
||||
// build the URLs
|
||||
const primaryUrl = `${this.config.invapi}/videos/${v}?hl=${contentlang}®ion=${contentregion}&h=${this.toBase64(Date.now())}`;
|
||||
const fallbackUrl = `${this.config.inv_fallback}${v}?hl=${contentlang}®ion=${contentregion}&h=${this.toBase64(Date.now())}`;
|
||||
|
||||
// decide which URL is chosen first based on 2-hour preference and 10-minute window
|
||||
// If currentPreference === 'fallback', we bias the schedule so the inFallbackWindow
|
||||
// maps to fallback being primary. If it's 'normal', we map inFallbackWindow to primary.
|
||||
const preferFallbackPrimary = currentPreference === "fallback";
|
||||
const chooseFirst = preferFallbackPrimary ? inFallbackWindow ? fallbackUrl : primaryUrl : inFallbackWindow ? primaryUrl : fallbackUrl;
|
||||
const chooseFirst = preferFallbackPrimary ? (inFallbackWindow ? fallbackUrl : primaryUrl) : (inFallbackWindow ? primaryUrl : fallbackUrl);
|
||||
const chooseSecond = chooseFirst === primaryUrl ? fallbackUrl : primaryUrl;
|
||||
|
||||
// Helper: race two fetch attempts but prefer whichever returns ok first.
|
||||
const fetchPrefer = async (urlA, urlB, maxRetryTime = 5000) => {
|
||||
const acA = new AbortController();
|
||||
const acB = new AbortController();
|
||||
const wrapped = (url, ac) =>
|
||||
fetchWithRetry(url, { signal: ac.signal }, maxRetryTime)
|
||||
.then((res) => ({ url, res }))
|
||||
.catch((err) => ({ url, err }));
|
||||
|
||||
// start both in parallel immediately to reduce latency
|
||||
const pA = wrapped(urlA, acA);
|
||||
const pB = wrapped(urlB, acB);
|
||||
|
||||
// wait for any to return an ok response quickly
|
||||
const results = await Promise.allSettled([pA, pB]);
|
||||
|
||||
// Prefer an ok response from either promise
|
||||
for (const r of results) {
|
||||
if (r.status === "fulfilled" && r.value && r.value.res && r.value.res.ok) {
|
||||
// abort the other one
|
||||
if (r.value.url === urlA) acB.abort();
|
||||
else acA.abort();
|
||||
return r.value.res;
|
||||
}
|
||||
}
|
||||
|
||||
// If none were ok, prefer the first fulfilled non-error response
|
||||
for (const r of results) {
|
||||
if (r.status === "fulfilled" && r.value && r.value.res) {
|
||||
if (r.value.url === urlA) acB.abort();
|
||||
else acA.abort();
|
||||
return r.value.res;
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise throw the first error we have
|
||||
for (const r of results) {
|
||||
if (r.status === "fulfilled" && r.value && r.value.err) throw r.value.err;
|
||||
}
|
||||
// fallback: throw aggregated
|
||||
throw new Error("Both fetches failed");
|
||||
};
|
||||
|
||||
try {
|
||||
const [invComments, videoInfo] = await Promise.all([
|
||||
fetchWithRetry(
|
||||
`${this.config.invapi}/comments/${v}?hl=${contentlang}®ion=${contentregion}&h=${this.toBase64(
|
||||
Date.now()
|
||||
)}`
|
||||
).then((res) => res?.text()),
|
||||
(async () => {
|
||||
try {
|
||||
const r = await fetchWithRetry(chooseFirst);
|
||||
if (r.ok) return await r.text();
|
||||
throw new Error(`First API ${chooseFirst} failed with ${r.status}`);
|
||||
} catch (err) {
|
||||
this.initError("Primary choice failed, trying secondary", err);
|
||||
const r2 = await fetchWithRetry(chooseSecond);
|
||||
return await r2.text();
|
||||
}
|
||||
})(),
|
||||
]);
|
||||
const invCommentsPromise = fetchWithRetry(
|
||||
`${this.config.invapi}/comments/${v}?hl=${contentlang}®ion=${contentregion}&h=${this.toBase64(
|
||||
Date.now()
|
||||
)}`,
|
||||
{},
|
||||
2500
|
||||
).then((r) => r?.text()).catch((err) => {
|
||||
this.initError("Comments fetch error", err);
|
||||
return null;
|
||||
});
|
||||
|
||||
const videoInfoPromise = (async () => {
|
||||
const r = await fetchPrefer(chooseFirst, chooseSecond, 5000);
|
||||
return await r.text();
|
||||
})();
|
||||
|
||||
const [invComments, videoInfo] = await Promise.all([invCommentsPromise, videoInfoPromise]);
|
||||
|
||||
const comments = this.getJson(invComments);
|
||||
const vid = this.getJson(videoInfo);
|
||||
@ -258,28 +269,45 @@ class InnerTubePokeVidious {
|
||||
}
|
||||
|
||||
if (this.checkUnexistingObject(vid)) {
|
||||
let returnyoutubedislikesapi = { engagement: null };
|
||||
try {
|
||||
returnyoutubedislikesapi = await getdislikes(v);
|
||||
} catch (err) {
|
||||
this.initError("Dislike API error", err);
|
||||
}
|
||||
// Run dislikes and thumbnail color extraction in parallel (don't block)
|
||||
const dislikePromise = (async () => {
|
||||
try {
|
||||
return await getdislikes(v);
|
||||
} catch (err) {
|
||||
this.initError("Dislike API error", err);
|
||||
return { engagement: null };
|
||||
}
|
||||
})();
|
||||
|
||||
const colorPromise = (async () => {
|
||||
try {
|
||||
// Fetch thumbnail colors but give it a short timeout by wrapping in Promise.race
|
||||
// `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 imgUrl = `https://i.ytimg.com/vi/${v}/hqdefault.jpg?sqp=${this.sqp}`;
|
||||
// get-image-colors may fetch internally; keep it but don't block main window too long
|
||||
const p = getColors(imgUrl);
|
||||
const timeout = new Promise((_, rej) => setTimeout(() => rej(new Error("Color extraction timeout")), 1200));
|
||||
const palette = await Promise.race([p, timeout]);
|
||||
if (Array.isArray(palette) && palette[0] && palette[1]) {
|
||||
return [palette[0].hex(), palette[1].hex()];
|
||||
}
|
||||
return null;
|
||||
} catch (err) {
|
||||
this.initError("Thumbnail color extraction error", err);
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
const [returnyoutubedislikesapi, paletteResult] = await Promise.all([dislikePromise, colorPromise]);
|
||||
|
||||
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}`
|
||||
);
|
||||
if (Array.isArray(palette) && palette[0] && palette[1]) {
|
||||
color = palette[0].hex();
|
||||
color2 = palette[1].hex();
|
||||
}
|
||||
} catch (err) {
|
||||
this.initError("Thumbnail color extraction error", err);
|
||||
if (Array.isArray(paletteResult) && paletteResult[0]) {
|
||||
color = paletteResult[0] || color;
|
||||
color2 = paletteResult[1] || color2;
|
||||
}
|
||||
|
||||
this.cache[v] = {
|
||||
@ -287,7 +315,7 @@ class InnerTubePokeVidious {
|
||||
vid,
|
||||
comments,
|
||||
channel_uploads: " ",
|
||||
engagement: returnyoutubedislikesapi.engagement,
|
||||
engagement: returnyoutubedislikesapi?.engagement ?? null,
|
||||
wiki: "",
|
||||
desc: "",
|
||||
color,
|
||||
@ -302,6 +330,7 @@ class InnerTubePokeVidious {
|
||||
}
|
||||
} catch (error) {
|
||||
this.initError(`Error getting video ${v}`, error);
|
||||
return { error: true, message: "Fetch error", detail: String(error) };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user