mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-29 00:42:30 +00:00
223 lines
5.1 KiB
JavaScript
223 lines
5.1 KiB
JavaScript
import { infoHashFromMagnet } from "./magnets.js";
|
|
import {
|
|
getDefaultHealth,
|
|
getHealthCached,
|
|
queueHealthCheck,
|
|
} from "./healthService.js";
|
|
|
|
const containerState = new WeakMap();
|
|
const ROOT_MARGIN = "200px 0px";
|
|
|
|
function ensureState(container) {
|
|
let state = containerState.get(container);
|
|
if (state) {
|
|
return state;
|
|
}
|
|
|
|
const pendingByCard = new WeakMap();
|
|
const observedCards = new WeakSet();
|
|
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
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 setBadge(card, visual, health) {
|
|
const badge = card.querySelector(".torrent-health-badge");
|
|
if (!badge) {
|
|
return;
|
|
}
|
|
const hadMargin = badge.classList.contains("mt-3");
|
|
|
|
const baseClasses = [
|
|
"torrent-health-badge",
|
|
"text-xs",
|
|
"font-semibold",
|
|
"px-2",
|
|
"py-1",
|
|
"rounded",
|
|
"inline-flex",
|
|
"items-center",
|
|
"gap-1",
|
|
"transition-colors",
|
|
"duration-200",
|
|
];
|
|
if (hadMargin) {
|
|
baseClasses.unshift("mt-3");
|
|
}
|
|
badge.className = baseClasses.join(" ");
|
|
|
|
const map = {
|
|
good: {
|
|
icon: "✅",
|
|
aria: "WebTorrent fallback ready",
|
|
classes: ["bg-green-900", "text-green-200"],
|
|
role: "status",
|
|
},
|
|
none: {
|
|
icon: "⚠️",
|
|
aria: "No seeders reported by trackers",
|
|
classes: ["bg-amber-900", "text-amber-200"],
|
|
role: "status",
|
|
},
|
|
noresp: {
|
|
icon: "❌",
|
|
aria: "No tracker response",
|
|
classes: ["bg-red-900", "text-red-200"],
|
|
role: "alert",
|
|
},
|
|
checking: {
|
|
icon: "⏳",
|
|
aria: "Checking Torrent availability",
|
|
classes: ["bg-gray-800", "text-gray-300"],
|
|
role: "status",
|
|
},
|
|
unknown: {
|
|
icon: "⚠️",
|
|
aria: "Torrent availability unknown",
|
|
classes: ["bg-amber-900", "text-amber-200"],
|
|
role: "status",
|
|
},
|
|
};
|
|
|
|
const entry = map[visual] || map.unknown;
|
|
entry.classes.forEach((cls) => badge.classList.add(cls));
|
|
|
|
const seederCount =
|
|
health && Number.isFinite(health.seeders) && health.seeders > 0
|
|
? health.seeders
|
|
: null;
|
|
const countText = seederCount ? ` (${seederCount})` : "";
|
|
const ariaCount = seederCount ? ` with ${seederCount} seeders` : "";
|
|
|
|
const iconPrefix = entry.icon ? `${entry.icon} ` : "";
|
|
badge.textContent = `${iconPrefix}Torrent${countText}`;
|
|
const ariaLabel = `${entry.aria}${ariaCount}`;
|
|
badge.setAttribute("aria-label", ariaLabel);
|
|
badge.setAttribute("title", ariaLabel);
|
|
badge.setAttribute("aria-live", "polite");
|
|
badge.setAttribute("role", entry.role);
|
|
badge.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 });
|
|
}
|
|
});
|
|
}
|