From 866e4f11fbe06d18a879687d6357b5f284da8958 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 27 Sep 2025 14:45:48 -0400 Subject: [PATCH 1/4] Hide videos with dead CDN and torrent peers --- css/style.css | 4 ++++ js/app.js | 20 ++++++++++++++++++++ js/channelProfile.js | 16 ++++++++++++++++ js/gridHealth.js | 16 ++++++++++++++-- js/subscriptions.js | 16 ++++++++++++++++ 5 files changed, 70 insertions(+), 2 deletions(-) diff --git a/css/style.css b/css/style.css index 208e5552..07aa6a01 100644 --- a/css/style.css +++ b/css/style.css @@ -74,6 +74,10 @@ header img { box-shadow: var(--shadow-md); } +.video-card[data-owner-is-viewer="false"][data-url-health-state="offline"][data-stream-health-state="unhealthy"] { + display: none; +} + .video-card--enter { opacity: 0; animation: video-card-fade-in 220ms ease-out forwards; diff --git a/js/app.js b/js/app.js index 7d693751..98b97656 100644 --- a/js/app.js +++ b/js/app.js @@ -3243,6 +3243,10 @@ class bitvidApp { const hadMargin = badgeEl.classList.contains("mt-3"); badgeEl.dataset.urlHealthState = status; + const cardEl = badgeEl.closest(".video-card"); + if (cardEl) { + cardEl.dataset.urlHealthState = status; + } badgeEl.setAttribute("aria-live", "polite"); badgeEl.setAttribute("role", status === "offline" ? "alert" : "status"); badgeEl.textContent = message; @@ -3677,6 +3681,22 @@ class bitvidApp { template.innerHTML = cardHtml.trim(); const cardEl = template.content.firstElementChild; if (cardEl) { + cardEl.dataset.ownerIsViewer = canEdit ? "true" : "false"; + if (typeof video.pubkey === "string" && video.pubkey) { + cardEl.dataset.ownerPubkey = video.pubkey; + } else if (cardEl.dataset.ownerPubkey) { + delete cardEl.dataset.ownerPubkey; + } + + cardEl.dataset.urlHealthState = trimmedUrl ? "checking" : "absent"; + if (magnetProvided && magnetSupported) { + cardEl.dataset.streamHealthState = "checking"; + } else if (magnetProvided) { + cardEl.dataset.streamHealthState = "unsupported"; + } else { + cardEl.dataset.streamHealthState = "absent"; + } + const thumbnailEl = cardEl.querySelector("[data-video-thumbnail]"); if (thumbnailEl) { const markThumbnailAsLoaded = () => { diff --git a/js/channelProfile.js b/js/channelProfile.js index 531d5ad4..abeb30bd 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -360,6 +360,13 @@ async function loadUserVideos(pubkey) { "duration-300" ); + cardEl.dataset.ownerIsViewer = canEdit ? "true" : "false"; + if (typeof video.pubkey === "string" && video.pubkey) { + cardEl.dataset.ownerPubkey = video.pubkey; + } else if (cardEl.dataset.ownerPubkey) { + delete cardEl.dataset.ownerPubkey; + } + const rawMagnet = typeof video.magnet === "string" ? video.magnet : ""; const trimmedMagnet = rawMagnet ? rawMagnet.trim() : ""; @@ -433,6 +440,15 @@ async function loadUserVideos(pubkey) { delete cardEl.dataset.torrentSupported; } + cardEl.dataset.urlHealthState = trimmedUrl ? "checking" : "absent"; + if (magnetProvided && magnetSupported) { + cardEl.dataset.streamHealthState = "checking"; + } else if (magnetProvided) { + cardEl.dataset.streamHealthState = "unsupported"; + } else { + cardEl.dataset.streamHealthState = "absent"; + } + if (magnetProvided) { cardEl.dataset.magnet = playbackMagnet; } else if (cardEl.dataset.magnet) { diff --git a/js/gridHealth.js b/js/gridHealth.js index b2cf13ca..a0394eed 100644 --- a/js/gridHealth.js +++ b/js/gridHealth.js @@ -433,11 +433,23 @@ function setBadge(card, state, details) { badge.setAttribute("aria-live", entry.role === "alert" ? "assertive" : "polite"); badge.setAttribute("role", entry.role); badge.dataset.streamHealthState = state; - if (Number.isFinite(peers)) { - badge.dataset.streamHealthPeers = String(peers); + const hasPeerCount = Number.isFinite(peers); + const peersTextValue = hasPeerCount ? String(peers) : ""; + if (hasPeerCount) { + badge.dataset.streamHealthPeers = peersTextValue; } else if (badge.dataset.streamHealthPeers) { delete badge.dataset.streamHealthPeers; } + + const card = badge.closest(".video-card"); + if (card) { + card.dataset.streamHealthState = state; + if (hasPeerCount) { + card.dataset.streamHealthPeers = peersTextValue; + } else if (card.dataset.streamHealthPeers) { + delete card.dataset.streamHealthPeers; + } + } } function handleCardVisible({ card, pendingByCard, priority = 0 }) { diff --git a/js/subscriptions.js b/js/subscriptions.js index 68b42102..216d1ab3 100644 --- a/js/subscriptions.js +++ b/js/subscriptions.js @@ -467,6 +467,22 @@ class SubscriptionsManager { t.innerHTML = cardHtml.trim(); const cardEl = t.content.firstElementChild; if (cardEl) { + cardEl.dataset.ownerIsViewer = canEdit ? "true" : "false"; + if (typeof video.pubkey === "string" && video.pubkey) { + cardEl.dataset.ownerPubkey = video.pubkey; + } else if (cardEl.dataset.ownerPubkey) { + delete cardEl.dataset.ownerPubkey; + } + + cardEl.dataset.urlHealthState = trimmedUrl ? "checking" : "absent"; + if (magnetProvided && magnetSupported) { + cardEl.dataset.streamHealthState = "checking"; + } else if (magnetProvided) { + cardEl.dataset.streamHealthState = "unsupported"; + } else { + cardEl.dataset.streamHealthState = "absent"; + } + // Leave the data-play-* attributes empty in the literal markup so we can // assign the raw URL/magnet strings post-parsing without HTML entity // escaping, mirroring the approach in app.js. The URL is encoded so that From 0b4523b0be8f3e29b5bd1d3027ecd25b82b7f6a0 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 27 Sep 2025 14:50:56 -0400 Subject: [PATCH 2/4] Fix duplicate card variable in health badge update --- js/gridHealth.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/js/gridHealth.js b/js/gridHealth.js index a0394eed..c1185a73 100644 --- a/js/gridHealth.js +++ b/js/gridHealth.js @@ -441,13 +441,13 @@ function setBadge(card, state, details) { delete badge.dataset.streamHealthPeers; } - const card = badge.closest(".video-card"); - if (card) { - card.dataset.streamHealthState = state; + const cardEl = badge.closest(".video-card"); + if (cardEl) { + cardEl.dataset.streamHealthState = state; if (hasPeerCount) { - card.dataset.streamHealthPeers = peersTextValue; - } else if (card.dataset.streamHealthPeers) { - delete card.dataset.streamHealthPeers; + cardEl.dataset.streamHealthPeers = peersTextValue; + } else if (cardEl.dataset.streamHealthPeers) { + delete cardEl.dataset.streamHealthPeers; } } } From 2dfe20907ba66867557ffc4492cd7f9a67801ba3 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 27 Sep 2025 14:59:45 -0400 Subject: [PATCH 3/4] Treat missing CDN/WebTorrent sources as offline --- js/app.js | 20 ++++++++++++--- js/channelProfile.js | 20 ++++++++++++--- js/gridHealth.js | 60 ++++++++++++++++++++++++++------------------ js/subscriptions.js | 20 ++++++++++++--- 4 files changed, 84 insertions(+), 36 deletions(-) diff --git a/js/app.js b/js/app.js index 98b97656..e379a8d6 100644 --- a/js/app.js +++ b/js/app.js @@ -3688,13 +3688,25 @@ class bitvidApp { delete cardEl.dataset.ownerPubkey; } - cardEl.dataset.urlHealthState = trimmedUrl ? "checking" : "absent"; + if (trimmedUrl) { + cardEl.dataset.urlHealthState = "checking"; + if (cardEl.dataset.urlHealthReason) { + delete cardEl.dataset.urlHealthReason; + } + } else { + cardEl.dataset.urlHealthState = "offline"; + cardEl.dataset.urlHealthReason = "missing-source"; + } if (magnetProvided && magnetSupported) { cardEl.dataset.streamHealthState = "checking"; - } else if (magnetProvided) { - cardEl.dataset.streamHealthState = "unsupported"; + if (cardEl.dataset.streamHealthReason) { + delete cardEl.dataset.streamHealthReason; + } } else { - cardEl.dataset.streamHealthState = "absent"; + cardEl.dataset.streamHealthState = "unhealthy"; + cardEl.dataset.streamHealthReason = magnetProvided + ? "unsupported" + : "missing-source"; } const thumbnailEl = cardEl.querySelector("[data-video-thumbnail]"); diff --git a/js/channelProfile.js b/js/channelProfile.js index abeb30bd..c9a08660 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -440,13 +440,25 @@ async function loadUserVideos(pubkey) { delete cardEl.dataset.torrentSupported; } - cardEl.dataset.urlHealthState = trimmedUrl ? "checking" : "absent"; + if (trimmedUrl) { + cardEl.dataset.urlHealthState = "checking"; + if (cardEl.dataset.urlHealthReason) { + delete cardEl.dataset.urlHealthReason; + } + } else { + cardEl.dataset.urlHealthState = "offline"; + cardEl.dataset.urlHealthReason = "missing-source"; + } if (magnetProvided && magnetSupported) { cardEl.dataset.streamHealthState = "checking"; - } else if (magnetProvided) { - cardEl.dataset.streamHealthState = "unsupported"; + if (cardEl.dataset.streamHealthReason) { + delete cardEl.dataset.streamHealthReason; + } } else { - cardEl.dataset.streamHealthState = "absent"; + cardEl.dataset.streamHealthState = "unhealthy"; + cardEl.dataset.streamHealthReason = magnetProvided + ? "unsupported" + : "missing-source"; } if (magnetProvided) { diff --git a/js/gridHealth.js b/js/gridHealth.js index c1185a73..997ce041 100644 --- a/js/gridHealth.js +++ b/js/gridHealth.js @@ -358,10 +358,37 @@ function getIntersectionRect(entry) { } function setBadge(card, state, details) { + if (!(card instanceof HTMLElement)) { + return; + } + + const normalizedState = + typeof state === "string" && state ? state : "unknown"; + const peersValue = + details && Number.isFinite(details.peers) + ? Math.max(0, Number(details.peers)) + : 0; + const hasPeerCount = details ? Number.isFinite(details.peers) : false; + const peersTextValue = hasPeerCount ? String(peersValue) : ""; + + card.dataset.streamHealthState = normalizedState; + if (hasPeerCount) { + card.dataset.streamHealthPeers = peersTextValue; + } else if (card.dataset.streamHealthPeers) { + delete card.dataset.streamHealthPeers; + } + + if (details && typeof details.reason === "string" && details.reason) { + card.dataset.streamHealthReason = details.reason; + } else if (card.dataset.streamHealthReason) { + delete card.dataset.streamHealthReason; + } + const badge = card.querySelector(".torrent-health-badge"); if (!badge) { return; } + const hadMargin = badge.classList.contains("mt-3"); const baseClasses = [ @@ -409,22 +436,19 @@ function setBadge(card, state, details) { }, }; - const entry = map[state] || map.unknown; + const entry = map[normalizedState] || map.unknown; entry.classes.forEach((cls) => badge.classList.add(cls)); - const peers = - details && Number.isFinite(details.peers) - ? Math.max(0, Number(details.peers)) - : 0; - const peersText = state === "healthy" && peers > 0 ? ` (${peers})` : ""; + const peersText = + normalizedState === "healthy" && peersValue > 0 ? ` (${peersValue})` : ""; const iconPrefix = entry.icon ? `${entry.icon} ` : ""; badge.textContent = `${iconPrefix}WebTorrent${peersText}`; const tooltip = - state === "checking" || state === "unknown" + normalizedState === "checking" || normalizedState === "unknown" ? entry.aria : buildTooltip({ - peers, + peers: peersValue, checkedAt: details?.checkedAt, reason: details?.reason, }); @@ -432,24 +456,12 @@ function setBadge(card, state, details) { badge.setAttribute("title", tooltip); badge.setAttribute("aria-live", entry.role === "alert" ? "assertive" : "polite"); badge.setAttribute("role", entry.role); - badge.dataset.streamHealthState = state; - const hasPeerCount = Number.isFinite(peers); - const peersTextValue = hasPeerCount ? String(peers) : ""; + badge.dataset.streamHealthState = normalizedState; if (hasPeerCount) { badge.dataset.streamHealthPeers = peersTextValue; } else if (badge.dataset.streamHealthPeers) { delete badge.dataset.streamHealthPeers; } - - const cardEl = badge.closest(".video-card"); - if (cardEl) { - cardEl.dataset.streamHealthState = state; - if (hasPeerCount) { - cardEl.dataset.streamHealthPeers = peersTextValue; - } else if (cardEl.dataset.streamHealthPeers) { - delete cardEl.dataset.streamHealthPeers; - } - } } function handleCardVisible({ card, pendingByCard, priority = 0 }) { @@ -458,13 +470,13 @@ function handleCardVisible({ card, pendingByCard, priority = 0 }) { } const magnet = card.dataset.magnet || ""; if (!magnet) { - setBadge(card, "unknown"); + setBadge(card, "unhealthy", { reason: "missing-source" }); return; } const infoHash = infoHashFromMagnet(magnet); if (!infoHash) { - setBadge(card, "unknown"); + setBadge(card, "unhealthy", { reason: "invalid" }); return; } @@ -535,7 +547,7 @@ export function attachHealthBadges(container) { state.observedCards.add(card); state.observer.observe(card); if (!card.dataset.magnet) { - setBadge(card, "unknown"); + setBadge(card, "unhealthy", { reason: "missing-source" }); } }); processObserverEntries(state.observer.takeRecords(), state); diff --git a/js/subscriptions.js b/js/subscriptions.js index 216d1ab3..9a55fef9 100644 --- a/js/subscriptions.js +++ b/js/subscriptions.js @@ -474,13 +474,25 @@ class SubscriptionsManager { delete cardEl.dataset.ownerPubkey; } - cardEl.dataset.urlHealthState = trimmedUrl ? "checking" : "absent"; + if (trimmedUrl) { + cardEl.dataset.urlHealthState = "checking"; + if (cardEl.dataset.urlHealthReason) { + delete cardEl.dataset.urlHealthReason; + } + } else { + cardEl.dataset.urlHealthState = "offline"; + cardEl.dataset.urlHealthReason = "missing-source"; + } if (magnetProvided && magnetSupported) { cardEl.dataset.streamHealthState = "checking"; - } else if (magnetProvided) { - cardEl.dataset.streamHealthState = "unsupported"; + if (cardEl.dataset.streamHealthReason) { + delete cardEl.dataset.streamHealthReason; + } } else { - cardEl.dataset.streamHealthState = "absent"; + cardEl.dataset.streamHealthState = "unhealthy"; + cardEl.dataset.streamHealthReason = magnetProvided + ? "unsupported" + : "missing-source"; } // Leave the data-play-* attributes empty in the literal markup so we can From 2b0f3547cb2022d86914f1d174a4d01c8282efc4 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 27 Sep 2025 15:11:02 -0400 Subject: [PATCH 4/4] Highlight owner offline videos with critical styling --- css/style.css | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/css/style.css b/css/style.css index 07aa6a01..b523fd42 100644 --- a/css/style.css +++ b/css/style.css @@ -3,6 +3,7 @@ --color-card: #1e293b; --color-primary: #fe0032; --color-secondary: #ff93a5; + --color-card-critical: #3f0b13; --color-text: #f8fafc; --color-muted: #94a3b8; --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); @@ -78,6 +79,15 @@ header img { display: none; } +.video-card[data-owner-is-viewer="true"][data-url-health-state="offline"][data-stream-health-state="unhealthy"] { + background-color: var(--color-card-critical); + box-shadow: 0 0 0 1px rgba(239, 68, 68, 0.35), var(--shadow-md); +} + +.video-card[data-owner-is-viewer="true"][data-url-health-state="offline"][data-stream-health-state="unhealthy"]:hover { + box-shadow: 0 0 0 1px rgba(239, 68, 68, 0.45), var(--shadow-lg); +} + .video-card--enter { opacity: 0; animation: video-card-fade-in 220ms ease-out forwards;