From 2d48f89f454a84a686cb24d032760b2dbeeac04e Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 25 Sep 2025 12:53:29 -0400 Subject: [PATCH] Improve thumbnail loading stability and skip tracking in modals --- js/app.js | 91 ++++++++++++++++++++++++++++++++++++++++++++--- js/viewManager.js | 6 ++++ 2 files changed, 93 insertions(+), 4 deletions(-) diff --git a/js/app.js b/js/app.js index 040c2c38..83cb4666 100644 --- a/js/app.js +++ b/js/app.js @@ -27,6 +27,7 @@ const UNSUPPORTED_BTITH_MESSAGE = "This magnet link is missing a compatible BitTorrent v1 info hash."; const FALLBACK_THUMBNAIL_SRC = "assets/jpg/video-thumbnail-fallback.jpg"; +const TRACKING_SCRIPT_PATTERN = /(?:^|\/)tracking\.js(?:$|\?)/; const EMPTY_VIDEO_LIST_SIGNATURE = "__EMPTY__"; // We probe hosted URLs often enough that a naive implementation would spam // remote CDNs. A medium-lived cache (roughly 45 minutes) keeps us from @@ -38,6 +39,19 @@ const URL_HEALTH_STORAGE_PREFIX = "bitvid:urlHealth:"; const urlHealthCache = new Map(); const urlHealthInFlight = new Map(); +function removeTrackingScripts(root) { + if (!root || typeof root.querySelectorAll !== "function") { + return; + } + + root.querySelectorAll("script[src]").forEach((script) => { + const src = script.getAttribute("src") || ""; + if (TRACKING_SCRIPT_PATTERN.test(src)) { + script.remove(); + } + }); +} + function getUrlHealthStorageKey(eventId) { return `${URL_HEALTH_STORAGE_PREFIX}${eventId}`; } @@ -415,6 +429,7 @@ class bitvidApp { // Lazy-loading helper for images this.mediaLoader = new MediaLoader(); + this.loadedThumbnails = new Map(); this.activeIntervals = []; this.urlPlaybackWatchdogCleanup = null; @@ -618,6 +633,7 @@ class bitvidApp { // Instead of overwriting, we append a new DIV with the fetched HTML const wrapper = document.createElement("div"); wrapper.innerHTML = html; // set the markup + removeTrackingScripts(wrapper); modalContainer.appendChild(wrapper); // append the markup // Now we can safely find elements inside: @@ -859,6 +875,7 @@ class bitvidApp { // Append the upload modal markup const wrapper = document.createElement("div"); wrapper.innerHTML = html; + removeTrackingScripts(wrapper); modalContainer.appendChild(wrapper); // Grab references @@ -911,6 +928,7 @@ class bitvidApp { } const wrapper = document.createElement("div"); wrapper.innerHTML = html; + removeTrackingScripts(wrapper); modalContainer.appendChild(wrapper); // Now references @@ -2088,6 +2106,20 @@ class bitvidApp { const dedupedVideos = this.dedupeVideosByRoot(videos); + if (this.loadedThumbnails && this.loadedThumbnails.size) { + const activeIds = new Set(); + dedupedVideos.forEach((video) => { + if (video && typeof video.id === "string" && video.id) { + activeIds.add(video.id); + } + }); + Array.from(this.loadedThumbnails.keys()).forEach((videoId) => { + if (!activeIds.has(videoId)) { + this.loadedThumbnails.delete(videoId); + } + }); + } + // 1) If no videos if (!dedupedVideos.length) { if (this.lastRenderedVideoSignature === EMPTY_VIDEO_LIST_SIGNATURE) { @@ -2233,6 +2265,36 @@ class bitvidApp { ? this.getUrlHealthPlaceholderMarkup() : ""; + const rawThumbnail = + typeof video.thumbnail === "string" ? video.thumbnail.trim() : ""; + const escapedThumbnail = rawThumbnail + ? this.escapeHTML(rawThumbnail) + : ""; + const previouslyLoadedThumbnail = + this.loadedThumbnails.get(video.id) || ""; + const shouldLazyLoadThumbnail = + escapedThumbnail && previouslyLoadedThumbnail !== escapedThumbnail; + const thumbnailAttrLines = ["data-video-thumbnail=\"true\""]; + if (shouldLazyLoadThumbnail) { + thumbnailAttrLines.push( + `src=\"${FALLBACK_THUMBNAIL_SRC}\"`, + `data-fallback-src=\"${FALLBACK_THUMBNAIL_SRC}\"`, + `data-lazy=\"${escapedThumbnail}\"`, + 'loading="lazy"', + 'decoding="async"' + ); + } else { + thumbnailAttrLines.push( + `src=\"${escapedThumbnail || FALLBACK_THUMBNAIL_SRC}\"`, + `data-fallback-src=\"${FALLBACK_THUMBNAIL_SRC}\"`, + 'loading="lazy"', + 'decoding="async"' + ); + } + thumbnailAttrLines.push( + `alt=\"${this.escapeHTML(video.title)}\"` + ); + const cardHtml = `
@@ -2246,10 +2308,7 @@ class bitvidApp { >
${this.escapeHTML(video.title)}
@@ -2299,6 +2358,30 @@ class bitvidApp { template.innerHTML = cardHtml.trim(); const cardEl = template.content.firstElementChild; if (cardEl) { + const thumbnailEl = cardEl.querySelector("[data-video-thumbnail]"); + if (thumbnailEl) { + const handleThumbnailLoad = () => { + if (!thumbnailEl.dataset.thumbnailFailed && escapedThumbnail) { + this.loadedThumbnails.set(video.id, escapedThumbnail); + } + }; + const handleThumbnailError = () => { + if ( + escapedThumbnail && + this.loadedThumbnails.get(video.id) === escapedThumbnail + ) { + this.loadedThumbnails.delete(video.id); + } + }; + + thumbnailEl.addEventListener("load", handleThumbnailLoad, { + once: true, + }); + thumbnailEl.addEventListener("error", handleThumbnailError, { + once: true, + }); + } + if (showUnsupportedTorrentBadge) { cardEl.dataset.torrentSupported = "false"; } else if (magnetProvided && magnetSupported) { diff --git a/js/viewManager.js b/js/viewManager.js index 3f21aa75..f22746c7 100644 --- a/js/viewManager.js +++ b/js/viewManager.js @@ -2,6 +2,8 @@ import { initChannelProfileView } from "./channelProfile.js"; import { subscriptions } from "./subscriptions.js"; +const TRACKING_SCRIPT_PATTERN = /(?:^|\/)tracking\.js(?:$|\?)/; + /** * Load a partial view by URL into the #viewContainer. */ @@ -23,6 +25,10 @@ export async function loadView(viewUrl) { // Copy and execute any inline scripts const scriptTags = doc.querySelectorAll("script"); scriptTags.forEach((oldScript) => { + const src = oldScript.getAttribute("src") || ""; + if (src && TRACKING_SCRIPT_PATTERN.test(src)) { + return; + } const newScript = document.createElement("script"); Array.from(oldScript.attributes).forEach((attr) => { newScript.setAttribute(attr.name, attr.value);