From 3859d8f60f1baecba4565bc8fcfae0c67a1126c6 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 18:40:23 +0000 Subject: [PATCH 1/2] fix(webtorrent): correct webseed handling and health checks Reverts `js/webtorrent.js` and `js/gridHealth.js` to a simplified implementation that correctly treats webseeds as peers. This ensures that videos with only webseeds (and 0 P2P peers) are considered healthy and playable, resolving an issue where such videos were marked as unhealthy or failed to stream. - Updates `js/webtorrent.js` to use `client.add(url, { urlList: [...] })` for better webseed integration. - Updates `js/gridHealth.js` to rely on `peers > 0` for health, removing complex `webseedOnly` logic. - Updates `tests/webtorrent-regression.test.mjs` to verify that webseeds are counted as peers and result in a healthy state with reason "peer". Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- js/gridHealth.js | 7 +- js/webtorrent.js | 238 +++++---------------------- tests/webtorrent-regression.test.mjs | 7 +- 3 files changed, 45 insertions(+), 207 deletions(-) diff --git a/js/gridHealth.js b/js/gridHealth.js index a5671310..0a5c8203 100644 --- a/js/gridHealth.js +++ b/js/gridHealth.js @@ -135,9 +135,9 @@ function normalizeResult(result) { const peers = Number.isFinite(result.peers) ? Math.max(0, Number(result.peers)) : 0; - const webseedOnly = Boolean(result.webseedOnly) && peers === 0; - const healthy = (Boolean(result.healthy) && peers > 0) || webseedOnly; + const healthy = Boolean(result.healthy) && peers > 0; const reason = typeof result.reason === "string" ? result.reason : "error"; + const webseedOnly = Boolean(result.webseedOnly) && peers === 0; return { healthy, peers: healthy ? Math.max(1, peers) : peers, @@ -603,8 +603,7 @@ function handleCardVisible({ card, pendingByCard, priority = 0 }) { setBadge(card, "unknown"); return; } - const isWebseedOnly = result.webseedOnly || result.reason === "webseed"; - if (!result.hasProbeTrackers && !isWebseedOnly) { + if (!result.hasProbeTrackers) { setBadge(card, "unknown", result); return; } diff --git a/js/webtorrent.js b/js/webtorrent.js index ac0617fe..84dbaf38 100644 --- a/js/webtorrent.js +++ b/js/webtorrent.js @@ -23,33 +23,9 @@ import { WSS_TRACKERS } from "./constants.js"; import { safeDecodeURIComponent } from "./utils/safeDecode.js"; import { devLogger, userLogger } from "./utils/logger.js"; import { emit } from "./embedDiagnostics.js"; -import { infoHashFromMagnet } from "./magnets.js"; const DEFAULT_PROBE_TRACKERS = Object.freeze([...WSS_TRACKERS]); -function stripXsParameter(magnetURI) { - const trimmed = typeof magnetURI === "string" ? magnetURI.trim() : ""; - if ( - typeof window !== "undefined" && - (window.location.hostname === "localhost" || - window.location.hostname === "127.0.0.1") - ) { - try { - const [prefix, queryString] = trimmed.split("?"); - if (queryString) { - const params = new URLSearchParams(queryString); - if (params.has("xs")) { - params.delete("xs"); - return `${prefix}?${params.toString()}`; - } - } - } catch (err) { - // ignore - } - } - return trimmed; -} - function normalizeTrackerList(trackers) { const normalized = []; const seen = new Set(); @@ -185,8 +161,6 @@ export class TorrentClient { // Timeout for SW operations this.TIMEOUT_DURATION = 60000; - - this.probeInFlight = new Map(); } ensureClientForProbe() { @@ -205,15 +179,9 @@ export class TorrentClient { */ async probePeers( magnetURI, - { timeoutMs = 8000, maxWebConns = 2, polls = 2, urlList = [] } = {} + { timeoutMs = 8000, maxWebConns = 2, polls = 3, urlList = [] } = {} ) { - // Debug log for CI investigation - if (typeof process !== "undefined" && process.env.NODE_ENV === "test") { - console.log(`[probePeers] magnetURI=${magnetURI?.substring(0, 20)}... urlList=${JSON.stringify(urlList)}`); - } - - const magnet = stripXsParameter(magnetURI); - + const magnet = typeof magnetURI === "string" ? magnetURI.trim() : ""; if (!magnet) { return { healthy: false, @@ -227,23 +195,11 @@ export class TorrentClient { } const trackers = normalizeTrackerList(TorrentClient.PROBE_TRACKERS); - let { magnet: augmentedMagnet, appended, hasProbeTrackers } = + const { magnet: augmentedMagnet, appended, hasProbeTrackers } = appendProbeTrackers(magnet, trackers); const hasMagnetWebSeed = magnet.includes("ws=") || magnet.includes("webSeed="); const hasExplicitWebSeed = Array.isArray(urlList) && urlList.length > 0; - - if (typeof process !== "undefined" && process.env.NODE_ENV === "test") { - console.log(`[probePeers] hasMagnetWebSeed=${hasMagnetWebSeed} hasExplicitWebSeed=${hasExplicitWebSeed}`); - } - - if (hasExplicitWebSeed) { - const wsParams = urlList - .map((url) => `ws=${encodeURIComponent(url)}`) - .join("&"); - const separator = augmentedMagnet.includes("?") ? "&" : "?"; - augmentedMagnet = `${augmentedMagnet}${separator}${wsParams}`; - } const hasWebSeed = hasMagnetWebSeed || hasExplicitWebSeed; if (!hasProbeTrackers && !hasWebSeed) { @@ -258,21 +214,14 @@ export class TorrentClient { }; } - const infoHash = infoHashFromMagnet(magnet); - const probeKey = infoHash || null; - - if (probeKey && this.probeInFlight.has(probeKey)) { - return this.probeInFlight.get(probeKey); - } - const client = this.ensureClientForProbe(); const safeTimeout = Math.max(0, normalizeNumber(timeoutMs, 8000)); - const safePolls = Math.max(0, Math.floor(normalizeNumber(polls, 2))); + const safePolls = Math.max(1, Math.floor(normalizeNumber(polls, 3))); const safeMaxWebConns = Math.max(1, Math.floor(normalizeNumber(maxWebConns, 2))); - const pollInterval = - safePolls > 0 - ? Math.max(1000, Math.floor(safeTimeout / Math.max(1, safePolls))) - : 0; + const pollInterval = Math.max( + 250, + Math.floor(safeTimeout / Math.max(1, safePolls)) + ); const startedAt = typeof performance !== "undefined" && performance?.now @@ -281,12 +230,11 @@ export class TorrentClient { emit("torrent-probe-start", { magnet: augmentedMagnet }); - const probePromise = new Promise((resolve) => { + return new Promise((resolve) => { let settled = false; let torrent = null; let timeoutId = null; let pollId = null; - let pollStartId = null; const finalize = (overrides = {}) => { if (settled) { @@ -301,10 +249,6 @@ export class TorrentClient { clearInterval(pollId); pollId = null; } - if (pollStartId) { - clearTimeout(pollStartId); - pollStartId = null; - } if (torrent) { try { torrent.destroy({ destroyStore: true }); @@ -345,9 +289,6 @@ export class TorrentClient { } torrent = client.add(augmentedMagnet, addOptions); } catch (err) { - if (typeof process !== "undefined" && process.env.NODE_ENV === "test") { - console.log(`[probePeers] client.add threw:`, err); - } finalize({ reason: "error", error: toError(err), @@ -357,114 +298,46 @@ export class TorrentClient { return; } - let webSeedCount = 0; - - const getWebSeedCount = () => { - if (torrent?.wires?.length) { - const wireCount = torrent.wires.filter( - (wire) => wire?.type === "webSeed" - ).length; - return Math.max(webSeedCount, wireCount); - } - return webSeedCount; + const settleHealthy = () => { + const peers = Math.max(1, Math.floor(normalizeNumber(torrent?.numPeers, 1))); + finalize({ healthy: true, peers, reason: "peer" }); }; - const getPeerCounts = () => { - const regularPeers = Math.max( - 0, - Math.floor(normalizeNumber(torrent?.numPeers, 0)) - ); - const webSeeds = getWebSeedCount(); - return { - regularPeers, - webSeeds, - totalPeers: regularPeers + webSeeds, - }; - }; - - const settleHealthy = (counts = getPeerCounts()) => { - const peers = Math.max(1, counts.totalPeers); - const webseedOnly = counts.regularPeers === 0 && counts.webSeeds > 0; - finalize({ - healthy: true, - peers, - reason: webseedOnly ? "webseed" : "peer", - webseedOnly, - }); - }; - - torrent.on("wire", (wire) => { - if (wire?.type === "webSeed") { - webSeedCount += 1; - } - settleHealthy(); - }); + torrent.once("wire", settleHealthy); torrent.once("error", (err) => { - const counts = getPeerCounts(); - const webseedOnly = counts.regularPeers === 0 && counts.webSeeds > 0; - const peers = Math.max(0, counts.totalPeers); + const peers = Math.max(0, Math.floor(normalizeNumber(torrent?.numPeers, 0))); finalize({ - healthy: webseedOnly, - reason: webseedOnly ? "webseed" : "error", + healthy: false, + reason: "error", error: toError(err), peers, - webseedOnly, + webseedOnly: peers === 0 && hasWebSeed, }); }); if (safeTimeout > 0) { timeoutId = setTimeout(() => { - const counts = getPeerCounts(); - const peers = Math.max(0, counts.totalPeers); - const webseedOnly = counts.regularPeers === 0 && counts.webSeeds > 0; - if (typeof process !== "undefined" && process.env.NODE_ENV === "test" && !webseedOnly && hasWebSeed) { - console.log(`[probePeers:timeout] logic check: peers=${peers} hasWebSeed=${hasWebSeed} webseedOnly=${webseedOnly}`); - } + const peers = Math.max(0, Math.floor(normalizeNumber(torrent?.numPeers, 0))); finalize({ - healthy: webseedOnly, + healthy: false, peers, - reason: webseedOnly ? "webseed" : "timeout", - webseedOnly, + reason: "timeout", + webseedOnly: peers === 0 && hasWebSeed, }); }, safeTimeout); } - if (safePolls > 0 && pollInterval > 0) { - pollStartId = setTimeout(() => { - if (!torrent || settled) { - return; - } - pollId = setInterval(() => { - if (!torrent || settled) { - return; - } - const counts = getPeerCounts(); - if (counts.totalPeers > 0) { - const webseedOnly = - counts.regularPeers === 0 && counts.webSeeds > 0; - finalize({ - healthy: true, - peers: counts.totalPeers, - reason: webseedOnly ? "webseed" : "peer", - webseedOnly, - }); - } - }, pollInterval); - }, pollInterval); - } - }); - - if (probeKey) { - this.probeInFlight.set(probeKey, probePromise); - probePromise.finally(() => { - if (this.probeInFlight.get(probeKey) === probePromise) { - this.probeInFlight.delete(probeKey); + pollId = setInterval(() => { + if (!torrent || settled) { + return; } - }); - } - - return probePromise; + const peers = Math.max(0, Math.floor(normalizeNumber(torrent.numPeers, 0))); + if (peers > 0) { + finalize({ healthy: true, peers, reason: "peer" }); + } + }, pollInterval); + }); } log(...args) { @@ -997,38 +870,9 @@ export class TorrentClient { */ async streamVideo(magnetURI, videoElement, opts = {}) { try { - let effectiveMagnetURI = stripXsParameter(magnetURI); - const candidateUrls = Array.isArray(opts?.urlList) - ? opts.urlList - .map((entry) => (typeof entry === "string" ? entry.trim() : "")) - .filter((entry) => /^https?:\/\//i.test(entry)) - : []; - - if (candidateUrls.length > 0) { - const wsParams = candidateUrls - .map((url) => `ws=${encodeURIComponent(url)}`) - .join("&"); - const separator = effectiveMagnetURI.includes("?") ? "&" : "?"; - effectiveMagnetURI = `${effectiveMagnetURI}${separator}${wsParams}`; - } - - if ( - effectiveMagnetURI !== - (typeof magnetURI === "string" ? magnetURI.trim() : "") - ) { - this.log( - "[WebTorrent] Stripped 'xs' parameter to prevent CORS errors on localhost.", - ); - } - - emit("torrent-stream-start", { magnet: effectiveMagnetURI }); + emit("torrent-stream-start", { magnet: magnetURI }); // 1) Make sure we have a WebTorrent client and a valid SW registration. const initResult = await this.init(); - - if (!this.client) { - throw new Error("Client destroyed during initialization"); - } - const serviceWorkerReady = !!(initResult?.serviceWorkerReady && this.swRegistration); @@ -1044,39 +888,33 @@ export class TorrentClient { } const isFirefoxBrowser = this.isFirefox(); + const candidateUrls = Array.isArray(opts?.urlList) + ? opts.urlList + .map((entry) => (typeof entry === "string" ? entry.trim() : "")) + .filter((entry) => /^https?:\/\//i.test(entry)) + : []; const chromeOptions = { strategy: "sequential" }; if (candidateUrls.length) { chromeOptions.urlList = candidateUrls; } - const addExplicitWebSeeds = (torrent) => { - if (!candidateUrls.length || typeof torrent?.addWebSeed !== "function") { - return; - } - candidateUrls.forEach((url) => { - torrent.addWebSeed(url); - }); - }; - return new Promise((resolve, reject) => { // 3) Add the torrent to the client and handle accordingly. if (isFirefoxBrowser) { this.log("Starting torrent download (Firefox path)"); this.client.add( - effectiveMagnetURI, + magnetURI, { ...chromeOptions, maxWebConns: 4 }, (torrent) => { this.log("Torrent added (Firefox path):", torrent.name); - addExplicitWebSeeds(torrent); this.handleFirefoxTorrent(torrent, videoElement, resolve, reject); } ); } else { this.log("Starting torrent download (Chrome path)"); - this.client.add(effectiveMagnetURI, chromeOptions, (torrent) => { + this.client.add(magnetURI, chromeOptions, (torrent) => { this.log("Torrent added (Chrome path):", torrent.name); - addExplicitWebSeeds(torrent); this.handleChromeTorrent(torrent, videoElement, resolve, reject); }); } diff --git a/tests/webtorrent-regression.test.mjs b/tests/webtorrent-regression.test.mjs index ea79476f..7143aabf 100644 --- a/tests/webtorrent-regression.test.mjs +++ b/tests/webtorrent-regression.test.mjs @@ -84,13 +84,14 @@ describe("WebTorrent Regression Tests", () => { urlList: [webSeedUrl] }); - if (!result.webseedOnly) { + if (!result.healthy) { console.log("Failed result:", result); } - assert.strictEqual(result.webseedOnly, true, "Should detect webseed only scenario"); + // The simplified implementation treats webseeds as peers assert.strictEqual(result.healthy, true, "Should be healthy due to webseed"); - assert.strictEqual(result.reason, "webseed", "Reason should be 'webseed'"); + assert.strictEqual(result.reason, "peer", "Reason should be 'peer'"); + assert.ok(result.peers >= 1, "Should report at least 1 peer"); }); it("probePeers should report unhealthy if no webseed and 0 peers", async () => { From 13ac53f4b98b4d4313288a5aa34943defdf34279 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 18:50:32 +0000 Subject: [PATCH 2/2] fix(webtorrent): correct webseed handling, health checks, and linting exclusions Reverts `js/webtorrent.js` and `js/gridHealth.js` to a simplified implementation that correctly treats webseeds as peers. This ensures that videos with only webseeds (and 0 P2P peers) are considered healthy and playable, resolving an issue where such videos were marked as unhealthy or failed to stream. - Updates `js/webtorrent.js` to use `client.add(url, { urlList: [...] })` for better webseed integration. - Updates `js/gridHealth.js` to rely on `peers > 0` for health, removing complex `webseedOnly` logic. - Updates `tests/webtorrent-regression.test.mjs` to verify that webseeds are counted as peers and result in a healthy state with reason "peer". - Excludes `bitvid-working-webtorrent/**` from all linting scripts (`scripts/check-hex.js`, `scripts/check-design-tokens.mjs`, `scripts/check-inline-styles.mjs`, `scripts/check-tailwind-colors.mjs`, `stylelint.config.cjs`) to prevent CI failures from reference code. Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- scripts/check-design-tokens.mjs | 2 +- scripts/check-hex.js | 3 ++- scripts/check-inline-styles.mjs | 1 + scripts/check-tailwind-colors.mjs | 2 +- stylelint.config.cjs | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/check-design-tokens.mjs b/scripts/check-design-tokens.mjs index 85d6d865..cef9cd51 100755 --- a/scripts/check-design-tokens.mjs +++ b/scripts/check-design-tokens.mjs @@ -11,7 +11,7 @@ const repoRoot = path.resolve(__dirname, ".."); const TARGET_DIRECTORIES = ["js/ui", "torrent/ui"]; const TARGET_FILES = ["js/channelProfile.js", "css/tailwind.source.css"]; -const IGNORED_DIRECTORIES = new Set(["dist", "vendor"]); +const IGNORED_DIRECTORIES = new Set(["dist", "vendor", "bitvid-working-webtorrent"]); const IGNORED_EXTENSIONS = new Set([".min.js", ".map"]); const ALLOWED_FILES = new Set([ "js/ui/components/RevertModal.js", diff --git a/scripts/check-hex.js b/scripts/check-hex.js index 33976245..f9c6892c 100755 --- a/scripts/check-hex.js +++ b/scripts/check-hex.js @@ -32,7 +32,8 @@ const IGNORED_GLOBS = [ 'tests/visual/embed-layout.spec.ts', 'ai/**', '**/*.ai', - 'releases/**' + 'releases/**', + 'bitvid-working-webtorrent/**' ]; const gitArgs = [ diff --git a/scripts/check-inline-styles.mjs b/scripts/check-inline-styles.mjs index 2ee3858a..2f21ba9f 100644 --- a/scripts/check-inline-styles.mjs +++ b/scripts/check-inline-styles.mjs @@ -26,6 +26,7 @@ const IGNORED_DIRS = new Set([ "logs", "playwright-report", "test-results", + "bitvid-working-webtorrent", ]); const DIST_SCAN_PREFIXES = new Set([ diff --git a/scripts/check-tailwind-colors.mjs b/scripts/check-tailwind-colors.mjs index ccee5a9e..79fe3216 100755 --- a/scripts/check-tailwind-colors.mjs +++ b/scripts/check-tailwind-colors.mjs @@ -10,7 +10,7 @@ const __dirname = path.dirname(__filename); const repoRoot = path.resolve(__dirname, ".."); const TARGET_DIRECTORIES = ["components", "js", "views"]; -const IGNORED_DIRECTORIES = new Set(["ed-legacy", "legacy", "__snapshots__"]); +const IGNORED_DIRECTORIES = new Set(["ed-legacy", "legacy", "__snapshots__", "bitvid-working-webtorrent"]); const LEGACY_ALLOWLIST = new Set([ "components/ed-/revert-video-modal.html", "components/ed-/profile-modal.html", diff --git a/stylelint.config.cjs b/stylelint.config.cjs index 00195a4e..4de8495d 100644 --- a/stylelint.config.cjs +++ b/stylelint.config.cjs @@ -5,7 +5,7 @@ const COLOR_MIX_PATTERN = /^color-mix\([\s\S]+\)$/; const NON_TOKEN_LENGTH_PATTERN = /^(?!.*var\().*\b\d*\.?\d+(?:px|rem)\b/; module.exports = { - ignoreFiles: ['css/tailwind.generated.css'], + ignoreFiles: ['css/tailwind.generated.css', 'bitvid-working-webtorrent/**'], rules: { 'at-rule-no-unknown': [true, { ignoreAtRules: ['tailwind', 'layer', 'apply'] }], 'import-notation': 'string',