From 854216f33115bfe30562fd9490234a06ddadc604 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 27 Sep 2025 13:33:13 -0400 Subject: [PATCH 1/3] Prioritize visible videos for health checks --- js/gridHealth.js | 71 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 64 insertions(+), 7 deletions(-) diff --git a/js/gridHealth.js b/js/gridHealth.js index 11815a6d..aba14ed8 100644 --- a/js/gridHealth.js +++ b/js/gridHealth.js @@ -236,14 +236,26 @@ function ensureState(container) { const observer = new IntersectionObserver( (entries) => { - entries.forEach((entry) => { + const viewportCenter = getViewportCenter(); + const prioritized = entries + .filter((entry) => entry.isIntersecting && entry.target instanceof HTMLElement) + .map((entry) => ({ + entry, + ratio: + typeof entry.intersectionRatio === "number" + ? entry.intersectionRatio + : 0, + distance: calculateDistanceSquared(entry, viewportCenter), + })) + .sort((a, b) => { + if (b.ratio !== a.ratio) { + return b.ratio - a.ratio; + } + return a.distance - b.distance; + }); + + prioritized.forEach(({ entry }) => { const card = entry.target; - if (!(card instanceof HTMLElement)) { - return; - } - if (!entry.isIntersecting) { - return; - } handleCardVisible({ card, pendingByCard }); }); }, @@ -255,6 +267,51 @@ function ensureState(container) { return state; } +function getViewportCenter() { + if (typeof window === "undefined") { + return null; + } + const width = Number(window.innerWidth) || 0; + const height = Number(window.innerHeight) || 0; + if (width <= 0 && height <= 0) { + return null; + } + return { + x: width > 0 ? width / 2 : 0, + y: height > 0 ? height / 2 : 0, + }; +} + +function calculateDistanceSquared(entry, viewportCenter) { + if (!viewportCenter) { + return Number.POSITIVE_INFINITY; + } + const rect = getIntersectionRect(entry); + if (!rect) { + return Number.POSITIVE_INFINITY; + } + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + const dx = centerX - viewportCenter.x; + const dy = centerY - viewportCenter.y; + return dx * dx + dy * dy; +} + +function getIntersectionRect(entry) { + if (!entry) { + return null; + } + const rect = entry.intersectionRect; + if (rect && rect.width > 0 && rect.height > 0) { + return rect; + } + const fallback = entry.boundingClientRect; + if (fallback && fallback.width > 0 && fallback.height > 0) { + return fallback; + } + return rect || fallback || null; +} + function setBadge(card, state, details) { const badge = card.querySelector(".torrent-health-badge"); if (!badge) { From 1985b160644ea3ba88151199b208624d04b44d64 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 27 Sep 2025 13:49:45 -0400 Subject: [PATCH 2/3] Improve health probe prioritization --- js/gridHealth.js | 139 +++++++++++++++++++++++++++++++---------------- 1 file changed, 91 insertions(+), 48 deletions(-) diff --git a/js/gridHealth.js b/js/gridHealth.js index aba14ed8..b2cf13ca 100644 --- a/js/gridHealth.js +++ b/js/gridHealth.js @@ -110,7 +110,7 @@ function normalizeResult(result) { }; } -function queueProbe(magnet, cacheKey) { +function queueProbe(magnet, cacheKey, priority = 0) { if (!magnet) { return Promise.resolve(null); } @@ -125,21 +125,23 @@ function queueProbe(magnet, cacheKey) { } const job = probeQueue - .run(() => - torrentClient - .probePeers(magnet, { - timeoutMs: PROBE_TIMEOUT_MS, - maxWebConns: 2, - polls: PROBE_POLL_COUNT, - }) - .catch((err) => ({ - healthy: false, - peers: 0, - reason: "error", - error: err, - appendedTrackers: false, - hasProbeTrackers: false, - })) + .run( + () => + torrentClient + .probePeers(magnet, { + timeoutMs: PROBE_TIMEOUT_MS, + maxWebConns: 2, + polls: PROBE_POLL_COUNT, + }) + .catch((err) => ({ + healthy: false, + peers: 0, + reason: "error", + error: err, + appendedTrackers: false, + hasProbeTrackers: false, + })), + priority ) .then((result) => { const normalized = normalizeResult(result); @@ -173,13 +175,35 @@ class ProbeQueue { this.queue = []; } - run(task) { + run(task, priority = 0) { return new Promise((resolve, reject) => { - this.queue.push({ task, resolve, reject }); + const normalizedPriority = Number.isFinite(priority) ? priority : 0; + const job = { + task, + resolve, + reject, + priority: normalizedPriority, + }; + this.enqueue(job); this.drain(); }); } + enqueue(job) { + if (!this.queue.length) { + this.queue.push(job); + return; + } + const index = this.queue.findIndex( + (existing) => existing.priority < job.priority + ); + if (index === -1) { + this.queue.push(job); + } else { + this.queue.splice(index, 0, job); + } + } + drain() { if (this.running >= this.max) { return; @@ -233,36 +257,16 @@ function ensureState(container) { const pendingByCard = new WeakMap(); const observedCards = new WeakSet(); + state = { observer: null, pendingByCard, observedCards }; const observer = new IntersectionObserver( (entries) => { - const viewportCenter = getViewportCenter(); - const prioritized = entries - .filter((entry) => entry.isIntersecting && entry.target instanceof HTMLElement) - .map((entry) => ({ - entry, - ratio: - typeof entry.intersectionRatio === "number" - ? entry.intersectionRatio - : 0, - distance: calculateDistanceSquared(entry, viewportCenter), - })) - .sort((a, b) => { - if (b.ratio !== a.ratio) { - return b.ratio - a.ratio; - } - return a.distance - b.distance; - }); - - prioritized.forEach(({ entry }) => { - const card = entry.target; - handleCardVisible({ card, pendingByCard }); - }); + processObserverEntries(entries, state); }, { root: null, rootMargin: ROOT_MARGIN, threshold: 0.01 } ); - state = { observer, pendingByCard, observedCards }; + state.observer = observer; containerState.set(container, state); return state; } @@ -297,6 +301,47 @@ function calculateDistanceSquared(entry, viewportCenter) { return dx * dx + dy * dy; } +function computeVisibilityPriority({ ratio, distance }) { + const normalizedRatio = Number.isFinite(ratio) + ? Math.min(Math.max(ratio, 0), 1) + : 0; + const distanceScore = Number.isFinite(distance) ? 1 / (1 + distance) : 0; + return normalizedRatio * 1000 + distanceScore; +} + +function prioritizeEntries(entries, viewportCenter) { + if (!Array.isArray(entries) || entries.length === 0) { + return []; + } + return entries + .filter((entry) => entry.isIntersecting && entry.target instanceof HTMLElement) + .map((entry) => { + const ratio = + typeof entry.intersectionRatio === "number" ? entry.intersectionRatio : 0; + const distance = calculateDistanceSquared(entry, viewportCenter); + const priority = computeVisibilityPriority({ ratio, distance }); + return { entry, ratio, distance, priority }; + }) + .sort((a, b) => { + if (b.priority !== a.priority) { + return b.priority - a.priority; + } + return a.distance - b.distance; + }); +} + +function processObserverEntries(entries, state) { + if (!state || !entries || entries.length === 0) { + return; + } + const viewportCenter = getViewportCenter(); + const prioritized = prioritizeEntries(entries, viewportCenter); + prioritized.forEach(({ entry, priority }) => { + const card = entry.target; + handleCardVisible({ card, pendingByCard: state.pendingByCard, priority }); + }); +} + function getIntersectionRect(entry) { if (!entry) { return null; @@ -395,7 +440,7 @@ function setBadge(card, state, details) { } } -function handleCardVisible({ card, pendingByCard }) { +function handleCardVisible({ card, pendingByCard, priority = 0 }) { if (!(card instanceof HTMLElement)) { return; } @@ -428,7 +473,7 @@ function handleCardVisible({ card, pendingByCard }) { setBadge(card, "checking"); - const probePromise = queueProbe(magnet, infoHash); + const probePromise = queueProbe(magnet, infoHash, priority); pendingByCard.set(card, probePromise); @@ -481,6 +526,7 @@ export function attachHealthBadges(container) { setBadge(card, "unknown"); } }); + processObserverEntries(state.observer.takeRecords(), state); } export function refreshHealthBadges(container) { @@ -491,9 +537,6 @@ export function refreshHealthBadges(container) { if (!state) { return; } - state.observer.takeRecords().forEach((entry) => { - if (entry.isIntersecting) { - handleCardVisible({ card: entry.target, pendingByCard: state.pendingByCard }); - } - }); + const records = state.observer.takeRecords(); + processObserverEntries(records, state); } From 02170097522e2a97d3524e615c2c8986040cbed6 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 27 Sep 2025 14:23:05 -0400 Subject: [PATCH 3/3] Retry CDN probes after timeouts --- js/app.js | 52 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/js/app.js b/js/app.js index 3d87f522..7d693751 100644 --- a/js/app.js +++ b/js/app.js @@ -78,7 +78,9 @@ const PROFILE_CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes // recovers. Results are stored both in-memory and in localStorage so other // views can reuse them without re-probing. const URL_HEALTH_TTL_MS = 45 * 60 * 1000; // 45 minutes +const URL_HEALTH_TIMEOUT_RETRY_MS = 5 * 60 * 1000; // 5 minutes const URL_PROBE_TIMEOUT_MS = 8 * 1000; // 8 seconds +const URL_PROBE_TIMEOUT_RETRY_MS = 15 * 1000; // 15 seconds const URL_HEALTH_STORAGE_PREFIX = "bitvid:urlHealth:"; const urlHealthCache = new Map(); const urlHealthInFlight = new Map(); @@ -3358,7 +3360,12 @@ class bitvidApp { }; } - return this.storeUrlHealth(eventId, trimmedUrl, entry); + const ttlOverride = + entry.status === "timeout" || entry.status === "unknown" + ? URL_HEALTH_TIMEOUT_RETRY_MS + : undefined; + + return this.storeUrlHealth(eventId, trimmedUrl, entry, ttlOverride); }) .catch((err) => { console.warn(`[urlHealth] probe failed for ${trimmedUrl}:`, err); @@ -4507,19 +4514,42 @@ class bitvidApp { return null; } - try { - const result = await this.probeUrlWithVideoElement(trimmed); - if (result && result.outcome) { - return result; + const initialTimeout = + Number.isFinite(options?.videoProbeTimeoutMs) && + options.videoProbeTimeoutMs > 0 + ? options.videoProbeTimeoutMs + : URL_PROBE_TIMEOUT_MS; + + const attemptWithTimeout = async (timeoutMs) => { + try { + const result = await this.probeUrlWithVideoElement(trimmed, timeoutMs); + if (result && result.outcome) { + return result; + } + } catch (err) { + console.warn( + `[probeUrl] Video element probe threw for ${trimmed}:`, + err + ); + } + return null; + }; + + let result = await attemptWithTimeout(initialTimeout); + + if ( + result && + result.outcome === "timeout" && + Number.isFinite(URL_PROBE_TIMEOUT_RETRY_MS) && + URL_PROBE_TIMEOUT_RETRY_MS > initialTimeout + ) { + const retryResult = await attemptWithTimeout(URL_PROBE_TIMEOUT_RETRY_MS); + if (retryResult) { + result = { ...retryResult, retriedAfterTimeout: true }; } - } catch (err) { - console.warn( - `[probeUrl] Video element probe threw for ${trimmed}:`, - err - ); } - return null; + return result; }; const supportsAbort = typeof AbortController !== "undefined";