From ddd8a393f42688aceea945531155ab13da8aa334 Mon Sep 17 00:00:00 2001
From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com>
Date: Thu, 25 Sep 2025 18:50:12 -0400
Subject: [PATCH] Add tracker health badges for magnets
---
css/style.css | 38 +++++++
js/app.js | 21 ++++
js/bufferPolyfill.js | 17 ++++
js/gridHealth.js | 186 ++++++++++++++++++++++++++++++++++
js/healthService.js | 98 ++++++++++++++++++
js/index.js | 1 +
js/magnets.js | 66 +++++++++++++
js/subscriptions.js | 30 +++++-
js/trackerConfig.js | 39 ++++++++
js/trackerPing.js | 230 +++++++++++++++++++++++++++++++++++++++++++
10 files changed, 724 insertions(+), 2 deletions(-)
create mode 100644 js/bufferPolyfill.js
create mode 100644 js/gridHealth.js
create mode 100644 js/healthService.js
create mode 100644 js/magnets.js
create mode 100644 js/trackerConfig.js
create mode 100644 js/trackerPing.js
diff --git a/css/style.css b/css/style.css
index 208e5552..bd9cdd74 100644
--- a/css/style.css
+++ b/css/style.css
@@ -74,6 +74,44 @@ header img {
box-shadow: var(--shadow-md);
}
+.video-card .stream-health {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 2rem;
+ font-size: 1.1rem;
+ line-height: 1;
+ transition: transform 150ms ease, filter 150ms ease, opacity 150ms ease;
+}
+
+.video-card .stream-health[data-stream-health-state="good"] {
+ filter: drop-shadow(0 0 6px rgba(34, 197, 94, 0.45));
+}
+
+.video-card .stream-health[data-stream-health-state="none"] {
+ opacity: 0.85;
+}
+
+.video-card .stream-health[data-stream-health-state="noresp"],
+.video-card .stream-health[data-stream-health-state="unknown"] {
+ opacity: 0.6;
+}
+
+.video-card .stream-health[data-stream-health-state="checking"] {
+ animation: stream-health-pulse 1.2s ease-in-out infinite alternate;
+}
+
+@keyframes stream-health-pulse {
+ from {
+ transform: scale(0.95);
+ opacity: 0.65;
+ }
+ to {
+ transform: scale(1.05);
+ opacity: 1;
+ }
+}
+
.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 1a58ee62..2be08ceb 100644
--- a/js/app.js
+++ b/js/app.js
@@ -10,6 +10,7 @@ import { normalizeAndAugmentMagnet } from "./magnet.js";
import { deriveTorrentPlaybackConfig } from "./playbackUtils.js";
import { URL_FIRST_ENABLED } from "./constants.js";
import { trackVideoView } from "./analytics.js";
+import { attachHealthBadges } from "./gridHealth.js";
import {
initialWhitelist,
initialBlacklist,
@@ -2545,6 +2546,19 @@ class bitvidApp {
+
+
+ Streamable?
+
+
+ 🟦
+
+
{
+ entries.forEach((entry) => {
+ const card = entry.target;
+ if (!(card instanceof HTMLElement)) {
+ return;
+ }
+ if (!entry.isIntersecting) {
+ return;
+ }
+ handleCardVisible({ card, pendingByCard });
+ });
+ },
+ { root: null, rootMargin: ROOT_MARGIN, threshold: 0.01 }
+ );
+
+ state = { observer, pendingByCard, observedCards };
+ containerState.set(container, state);
+ return state;
+}
+
+function toVisual(health) {
+ if (!health) {
+ return "unknown";
+ }
+ if (health.ok && health.seeders > 0) {
+ return "good";
+ }
+ if (health.responded) {
+ if (health.seeders > 0) {
+ return "good";
+ }
+ return "none";
+ }
+ return "noresp";
+}
+
+function formatCount(health) {
+ if (!health || !Number.isFinite(health.seeders) || health.seeders <= 0) {
+ return "";
+ }
+ return ` (${health.seeders})`;
+}
+
+function setBadge(card, visual, health) {
+ const el = card.querySelector(".stream-health");
+ if (!el) {
+ return;
+ }
+ const map = {
+ good: {
+ text: "🟢",
+ aria: "Streamable: seeders available",
+ },
+ none: {
+ text: "🟡",
+ aria: "No seeders reported by trackers",
+ },
+ noresp: {
+ text: "âš«",
+ aria: "No tracker response",
+ },
+ checking: {
+ text: "🟦",
+ aria: "Checking stream availability",
+ },
+ unknown: {
+ text: "⚪",
+ aria: "Unknown stream availability",
+ },
+ };
+ const entry = map[visual] || map.unknown;
+ const countText = formatCount(health);
+ el.textContent = `${entry.text}${countText}`;
+ el.setAttribute("aria-label", countText ? `${entry.aria}${countText}` : entry.aria);
+ el.setAttribute("title", countText ? `${entry.aria}${countText}` : entry.aria);
+ el.dataset.streamHealthState = visual;
+}
+
+function applyHealth(card, health) {
+ if (!health) {
+ setBadge(card, "unknown");
+ return;
+ }
+ const visual = toVisual(health);
+ setBadge(card, visual, health);
+}
+
+function handleCardVisible({ card, pendingByCard }) {
+ if (!(card instanceof HTMLElement)) {
+ return;
+ }
+ const magnet = card.dataset.magnet || "";
+ if (!magnet) {
+ setBadge(card, "unknown");
+ return;
+ }
+
+ const infoHash = infoHashFromMagnet(magnet);
+ if (!infoHash) {
+ setBadge(card, "unknown");
+ return;
+ }
+
+ const cached = getHealthCached(infoHash);
+ if (cached) {
+ applyHealth(card, cached);
+ return;
+ }
+
+ if (pendingByCard.has(card)) {
+ return;
+ }
+
+ setBadge(card, "checking", getDefaultHealth());
+ const pending = queueHealthCheck(magnet).then((health) => {
+ pendingByCard.delete(card);
+ if (!card.isConnected) {
+ return;
+ }
+ applyHealth(card, health);
+ });
+ pending.catch(() => {
+ pendingByCard.delete(card);
+ if (!card.isConnected) {
+ return;
+ }
+ setBadge(card, "noresp");
+ });
+ pendingByCard.set(card, pending);
+}
+
+export function attachHealthBadges(container) {
+ if (!(container instanceof HTMLElement)) {
+ return;
+ }
+ const state = ensureState(container);
+ const cards = container.querySelectorAll(".video-card");
+ cards.forEach((card) => {
+ if (!(card instanceof HTMLElement)) {
+ return;
+ }
+ if (state.observedCards.has(card)) {
+ return;
+ }
+ state.observedCards.add(card);
+ state.observer.observe(card);
+ if (!card.dataset.magnet) {
+ setBadge(card, "unknown");
+ }
+ });
+}
+
+export function refreshHealthBadges(container) {
+ if (!(container instanceof HTMLElement)) {
+ return;
+ }
+ const state = containerState.get(container);
+ if (!state) {
+ return;
+ }
+ state.observer.takeRecords().forEach((entry) => {
+ if (entry.isIntersecting) {
+ handleCardVisible({ card: entry.target, pendingByCard: state.pendingByCard });
+ }
+ });
+}
diff --git a/js/healthService.js b/js/healthService.js
new file mode 100644
index 00000000..de265bab
--- /dev/null
+++ b/js/healthService.js
@@ -0,0 +1,98 @@
+import PQueue from "https://esm.sh/p-queue@7.4.1";
+import { trackerPing } from "./trackerPing.js";
+import { infoHashFromMagnet } from "./magnets.js";
+import { HEALTH_TTL_MS, CONCURRENCY } from "./trackerConfig.js";
+
+const queue = new PQueue({ concurrency: CONCURRENCY });
+const cache = new Map();
+const inflight = new Map();
+
+export function getDefaultHealth() {
+ return {
+ ok: false,
+ seeders: 0,
+ leechers: 0,
+ responded: false,
+ from: [],
+ };
+}
+
+export function getHealthCached(infoHash) {
+ if (!infoHash) {
+ return null;
+ }
+ const entry = cache.get(infoHash);
+ if (!entry) {
+ return null;
+ }
+ if (Date.now() - entry.ts > HEALTH_TTL_MS) {
+ cache.delete(infoHash);
+ return null;
+ }
+ return entry.value;
+}
+
+export function setHealthCache(infoHash, value) {
+ if (!infoHash) {
+ return;
+ }
+ cache.set(infoHash, { ts: Date.now(), value });
+}
+
+export function queueHealthCheck(magnet, onResult) {
+ const infoHash = infoHashFromMagnet(magnet);
+ if (!infoHash) {
+ const fallback = getDefaultHealth();
+ if (typeof onResult === "function") {
+ onResult(fallback);
+ }
+ return Promise.resolve(fallback);
+ }
+
+ const cached = getHealthCached(infoHash);
+ if (cached) {
+ if (typeof onResult === "function") {
+ onResult(cached);
+ }
+ return Promise.resolve(cached);
+ }
+
+ if (inflight.has(infoHash)) {
+ const pending = inflight.get(infoHash);
+ if (typeof onResult === "function") {
+ pending.then(onResult).catch(() => {});
+ }
+ return pending;
+ }
+
+ const jobPromise = queue
+ .add(async () => {
+ try {
+ const health = await trackerPing(magnet);
+ setHealthCache(infoHash, health);
+ return health;
+ } catch (err) {
+ console.warn("trackerPing failed", err);
+ return getDefaultHealth();
+ }
+ })
+ .finally(() => {
+ inflight.delete(infoHash);
+ });
+
+ inflight.set(infoHash, jobPromise);
+ if (typeof onResult === "function") {
+ jobPromise.then(onResult).catch(() => {});
+ }
+ return jobPromise;
+}
+
+export function purgeHealthCache() {
+ const now = Date.now();
+ Array.from(cache.keys()).forEach((infoHash) => {
+ const entry = cache.get(infoHash);
+ if (!entry || now - entry.ts > HEALTH_TTL_MS) {
+ cache.delete(infoHash);
+ }
+ });
+}
diff --git a/js/index.js b/js/index.js
index d3c062d0..6ad7302f 100644
--- a/js/index.js
+++ b/js/index.js
@@ -1,5 +1,6 @@
// js/index.js
+import "./bufferPolyfill.js";
import { trackPageView } from "./analytics.js";
const INTERFACE_FADE_IN_ANIMATION = "interface-fade-in";
diff --git a/js/magnets.js b/js/magnets.js
new file mode 100644
index 00000000..b5ab9fd3
--- /dev/null
+++ b/js/magnets.js
@@ -0,0 +1,66 @@
+import parseMagnet from "https://esm.sh/magnet-uri@9.1.2";
+
+function normalizeInfoHash(candidate) {
+ const trimmed = typeof candidate === "string" ? candidate.trim() : "";
+ if (!trimmed) {
+ return null;
+ }
+ if (/^[0-9a-f]{40}$/i.test(trimmed)) {
+ return trimmed.toLowerCase();
+ }
+ if (/^[a-z2-7]{32}$/i.test(trimmed)) {
+ try {
+ const parsed = parseMagnet(`magnet:?xt=urn:btih:${trimmed}`);
+ const hash = typeof parsed.infoHash === "string" ? parsed.infoHash : "";
+ return hash ? hash.toLowerCase() : null;
+ } catch (err) {
+ console.warn("Failed to normalize base32 info hash", err);
+ }
+ }
+ return null;
+}
+
+export function infoHashFromMagnet(magnet) {
+ if (typeof magnet !== "string") {
+ return null;
+ }
+ const direct = normalizeInfoHash(magnet);
+ if (direct) {
+ return direct;
+ }
+ try {
+ const parsed = parseMagnet(magnet);
+ const hash = typeof parsed.infoHash === "string" ? parsed.infoHash : "";
+ return hash ? hash.toLowerCase() : null;
+ } catch (err) {
+ console.warn("Failed to parse magnet for info hash", err);
+ return null;
+ }
+}
+
+export function trackersFromMagnet(magnet) {
+ if (typeof magnet !== "string") {
+ return [];
+ }
+ try {
+ const parsed = parseMagnet(magnet);
+ if (!parsed || !Array.isArray(parsed.announce)) {
+ return [];
+ }
+ const deduped = new Set();
+ parsed.announce.forEach((url) => {
+ if (typeof url !== "string") {
+ return;
+ }
+ const trimmed = url.trim();
+ if (!trimmed) {
+ return;
+ }
+ deduped.add(trimmed);
+ });
+ return Array.from(deduped);
+ } catch (err) {
+ console.warn("Failed to parse magnet trackers", err);
+ return [];
+ }
+}
diff --git a/js/subscriptions.js b/js/subscriptions.js
index 2ef66b04..50ed39cb 100644
--- a/js/subscriptions.js
+++ b/js/subscriptions.js
@@ -3,6 +3,7 @@ import {
nostrClient,
convertEventToVideo as sharedConvertEventToVideo,
} from "./nostr.js";
+import { attachHealthBadges } from "./gridHealth.js";
function getAbsoluteShareUrl(nevent) {
if (!nevent) {
@@ -379,9 +380,14 @@ class SubscriptionsManager {
const safeThumb = window.app?.escapeHTML(video.thumbnail) || "";
const playbackUrl =
typeof video.url === "string" ? video.url : "";
- const playbackMagnet =
- typeof video.magnet === "string" ? video.magnet : "";
const trimmedUrl = playbackUrl ? playbackUrl.trim() : "";
+ const trimmedMagnet =
+ typeof video.magnet === "string" ? video.magnet.trim() : "";
+ const legacyInfoHash =
+ typeof video.infoHash === "string" ? video.infoHash.trim() : "";
+ const magnetCandidate = trimmedMagnet || legacyInfoHash;
+ const playbackMagnet = magnetCandidate;
+ const magnetProvided = magnetCandidate.length > 0;
const urlStatusHtml = trimmedUrl
? window.app?.getUrlHealthPlaceholderMarkup?.() ?? ""
: "";
@@ -403,6 +409,19 @@ class SubscriptionsManager {
+
+
+ Streamable?
+
+
+ 🟦
+
+
{
+ if (typeof url !== "string") {
+ return;
+ }
+ const trimmed = url.trim();
+ if (!trimmed) {
+ return;
+ }
+ if (!/^wss:\/\//i.test(trimmed)) {
+ return;
+ }
+ const normalized = trimmed.toLowerCase();
+ if (seen.has(normalized)) {
+ return;
+ }
+ seen.add(normalized);
+ combined.push(trimmed);
+ };
+
+ if (Array.isArray(magnetTrackers)) {
+ magnetTrackers.forEach(pushUnique);
+ }
+
+ DEFAULT_WSS_TRACKERS.forEach(pushUnique);
+
+ return combined.slice(0, TRACKER_PER_MAGNET);
+}
diff --git a/js/trackerPing.js b/js/trackerPing.js
new file mode 100644
index 00000000..eb85c6ba
--- /dev/null
+++ b/js/trackerPing.js
@@ -0,0 +1,230 @@
+import "./bufferPolyfill.js";
+import Client from "https://esm.sh/bittorrent-tracker@11.0.0/client?bundle";
+import { infoHashFromMagnet, trackersFromMagnet } from "./magnets.js";
+import {
+ resolveTrackerList,
+ TRACKER_TIMEOUT_MS,
+ TRACKER_ERROR_COOLDOWN_MS,
+} from "./trackerConfig.js";
+
+const trackerState = new Map();
+
+function now() {
+ return Date.now();
+}
+
+function randomPeerId() {
+ const bytes = new Uint8Array(20);
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
+ crypto.getRandomValues(bytes);
+ } else {
+ for (let i = 0; i < bytes.length; i += 1) {
+ bytes[i] = Math.floor(Math.random() * 256);
+ }
+ }
+ return bytes;
+}
+
+function getDefaultHealth() {
+ return {
+ ok: false,
+ seeders: 0,
+ leechers: 0,
+ responded: false,
+ from: [],
+ };
+}
+
+function getTrackerEntry(url) {
+ const existing = trackerState.get(url);
+ if (existing) {
+ return existing;
+ }
+ const entry = {
+ consecutiveErrors: 0,
+ lastErrorAt: 0,
+ cooldownUntil: 0,
+ };
+ trackerState.set(url, entry);
+ return entry;
+}
+
+function markTrackerSuccess(url) {
+ const entry = getTrackerEntry(url);
+ entry.consecutiveErrors = 0;
+ entry.cooldownUntil = 0;
+}
+
+function markTrackerError(url) {
+ const entry = getTrackerEntry(url);
+ const nowTs = now();
+ if (entry.lastErrorAt && nowTs - entry.lastErrorAt < TRACKER_ERROR_COOLDOWN_MS) {
+ entry.consecutiveErrors += 1;
+ } else {
+ entry.consecutiveErrors = 1;
+ }
+ entry.lastErrorAt = nowTs;
+ if (entry.consecutiveErrors >= 2) {
+ entry.cooldownUntil = nowTs + TRACKER_ERROR_COOLDOWN_MS;
+ }
+}
+
+function isTrackerUsable(url) {
+ const entry = getTrackerEntry(url);
+ return entry.cooldownUntil === 0 || entry.cooldownUntil <= now();
+}
+
+export async function trackerPing(magnet, trackers) {
+ const infoHash = infoHashFromMagnet(magnet);
+ if (!infoHash) {
+ return getDefaultHealth();
+ }
+
+ const magnetTrackers = trackers || trackersFromMagnet(magnet);
+ const announceList = resolveTrackerList({ magnetTrackers });
+ const usable = announceList.filter(isTrackerUsable);
+ const announces = usable.length ? usable : announceList;
+
+ if (!announces.length) {
+ return getDefaultHealth();
+ }
+
+ const peerId = randomPeerId();
+ const result = getDefaultHealth();
+ const clients = new Set();
+ let settled = false;
+ let timeoutId = null;
+
+ const finalize = () => {
+ if (settled) {
+ return;
+ }
+ settled = true;
+ if (timeoutId) {
+ clearTimeout(timeoutId);
+ }
+ clients.forEach((client) => {
+ try {
+ client.destroy();
+ } catch (err) {
+ // ignore
+ }
+ });
+ clients.clear();
+ };
+
+ return new Promise((resolve) => {
+ if (Number.isFinite(TRACKER_TIMEOUT_MS) && TRACKER_TIMEOUT_MS > 0) {
+ timeoutId = setTimeout(() => {
+ finalize();
+ resolve(result);
+ }, TRACKER_TIMEOUT_MS);
+ }
+
+ let remaining = announces.length;
+
+ const handleComplete = (client, url) => {
+ if (clients.has(client)) {
+ clients.delete(client);
+ }
+ if (url && result.from.includes(url) && result.ok) {
+ markTrackerSuccess(url);
+ }
+ remaining -= 1;
+ if (remaining <= 0 && !settled) {
+ finalize();
+ resolve(result);
+ }
+ };
+
+ const handleResult = (url, data) => {
+ result.responded = true;
+ if (url && !result.from.includes(url)) {
+ result.from.push(url);
+ }
+ const seeders = Number.isFinite(data?.complete)
+ ? Number(data.complete)
+ : 0;
+ const leechers = Number.isFinite(data?.incomplete)
+ ? Number(data.incomplete)
+ : 0;
+ if (seeders > result.seeders) {
+ result.seeders = seeders;
+ }
+ if (leechers > result.leechers) {
+ result.leechers = leechers;
+ }
+ if (seeders > 0) {
+ result.ok = true;
+ }
+ };
+
+ announces.forEach((url) => {
+ let client;
+ try {
+ client = new Client({
+ infoHash,
+ peerId,
+ announce: [url],
+ });
+ } catch (err) {
+ markTrackerError(url);
+ remaining -= 1;
+ if (remaining <= 0 && !settled) {
+ finalize();
+ resolve(result);
+ }
+ return;
+ }
+
+ clients.add(client);
+
+ const cleanupAndResolve = () => {
+ if (settled) {
+ return;
+ }
+ finalize();
+ resolve(result);
+ };
+
+ client.once("update", (data) => {
+ handleResult(url, data);
+ markTrackerSuccess(url);
+ if (result.ok) {
+ cleanupAndResolve();
+ return;
+ }
+ handleComplete(client, url);
+ });
+
+ client.once("error", () => {
+ markTrackerError(url);
+ handleComplete(client, url);
+ });
+
+ client.once("warning", () => {
+ handleComplete(client, url);
+ });
+
+ try {
+ client.start();
+ } catch (err) {
+ markTrackerError(url);
+ handleComplete(client, url);
+ }
+ });
+
+ if (announces.length === 0) {
+ finalize();
+ resolve(result);
+ }
+ });
+}
+
+export function getTrackerStateSnapshot() {
+ const snapshot = {};
+ trackerState.forEach((value, key) => {
+ snapshot[key] = { ...value };
+ });
+ return snapshot;
+}