mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Merge pull request #394 from PR0M3TH3AN/codex/update-medialoader.js-and-confirm-load-handler
Fix lazy thumbnail load completion
This commit is contained in:
6 files changed
+536
-21
No files matched your search
@@ -162,6 +162,15 @@ class Application {
|
||||
? helpers.mediaLoader
|
||||
: mediaLoaderFactory();
|
||||
this.loadedThumbnails = new Map();
|
||||
this.urlHealthSnapshots = new Map();
|
||||
this.streamHealthSnapshots = new Map();
|
||||
this.boundStreamHealthBadgeHandler = (detail) =>
|
||||
this.handleStreamHealthBadgeUpdate(detail);
|
||||
this.attachHealthBadgesWithCache = (container) => {
|
||||
attachHealthBadges(container, {
|
||||
onUpdate: this.boundStreamHealthBadgeHandler,
|
||||
});
|
||||
};
|
||||
this.relayManager = relayManager;
|
||||
this.activeIntervals = [];
|
||||
this.watchHistoryTelemetry = null;
|
||||
@@ -787,7 +796,11 @@ class Application {
|
||||
const videoListViewConfig = {
|
||||
document,
|
||||
mediaLoader: this.mediaLoader,
|
||||
badgeHelpers: { attachHealthBadges, attachUrlHealthBadges },
|
||||
badgeHelpers: {
|
||||
attachHealthBadges: this.attachHealthBadgesWithCache,
|
||||
attachUrlHealthBadges: (container, onCheck) =>
|
||||
attachUrlHealthBadges(container, onCheck),
|
||||
},
|
||||
formatters: {
|
||||
formatTimeAgo: (timestamp) => this.formatTimeAgo(timestamp),
|
||||
formatViewCountLabel: (total) => this.formatViewCountLabel(total),
|
||||
@@ -805,6 +818,8 @@ class Application {
|
||||
state: {
|
||||
loadedThumbnails: this.loadedThumbnails,
|
||||
videosMap: this.videosMap,
|
||||
urlHealthByVideoId: this.urlHealthSnapshots,
|
||||
streamHealthByVideoId: this.streamHealthSnapshots,
|
||||
},
|
||||
utils: {
|
||||
dedupeVideos: (videos) => this.dedupeVideosByRoot(videos),
|
||||
@@ -4104,6 +4119,20 @@ class Application {
|
||||
"text-gray-300"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
videoId &&
|
||||
this.videoListView &&
|
||||
typeof this.videoListView.cacheUrlHealth === "function"
|
||||
) {
|
||||
this.videoListView.cacheUrlHealth(videoId, {
|
||||
status,
|
||||
message,
|
||||
lastCheckedAt: Number.isFinite(state?.lastCheckedAt)
|
||||
? state.lastCheckedAt
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleUrlHealthBadge({ video, url, badgeEl }) {
|
||||
@@ -4201,6 +4230,46 @@ class Application {
|
||||
});
|
||||
}
|
||||
|
||||
handleStreamHealthBadgeUpdate(detail) {
|
||||
if (!detail || typeof detail !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
const card = detail.card;
|
||||
if (!(card instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let videoId =
|
||||
(card.dataset && card.dataset.videoId) ||
|
||||
(typeof card.getAttribute === "function" ? card.getAttribute("data-video-id") : "") ||
|
||||
"";
|
||||
|
||||
if (!videoId && typeof card.querySelector === "function") {
|
||||
const fallback = card.querySelector("[data-video-id]");
|
||||
if (fallback instanceof HTMLElement && fallback.dataset.videoId) {
|
||||
videoId = fallback.dataset.videoId;
|
||||
}
|
||||
}
|
||||
|
||||
if (!videoId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.videoListView && typeof this.videoListView.cacheStreamHealth === "function") {
|
||||
this.videoListView.cacheStreamHealth(videoId, {
|
||||
state: detail.state,
|
||||
peers: detail.peers,
|
||||
reason: detail.reason,
|
||||
checkedAt: detail.checkedAt,
|
||||
text: detail.text,
|
||||
tooltip: detail.tooltip,
|
||||
role: detail.role,
|
||||
ariaLive: detail.ariaLive,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async renderVideoList(payload) {
|
||||
if (!this.videoListView) {
|
||||
return;
|
||||
|
||||
+39
-2
@@ -2,6 +2,25 @@ import { createCardObserver } from "./dom/cardObserver.js";
|
||||
import { infoHashFromMagnet } from "./magnets.js";
|
||||
import { TorrentClient, torrentClient } from "./webtorrent.js";
|
||||
|
||||
const badgeUpdateListeners = new Set();
|
||||
|
||||
function notifyBadgeUpdate(payload) {
|
||||
if (!payload) {
|
||||
return;
|
||||
}
|
||||
|
||||
badgeUpdateListeners.forEach((listener) => {
|
||||
if (typeof listener !== "function") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
listener(payload);
|
||||
} catch (err) {
|
||||
console.warn("[gridHealth] badge listener failed", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const ROOT_MARGIN = "200px 0px";
|
||||
|
||||
function now() {
|
||||
@@ -479,7 +498,8 @@ function setBadge(card, state, details) {
|
||||
});
|
||||
badge.setAttribute("aria-label", tooltip);
|
||||
badge.setAttribute("title", tooltip);
|
||||
badge.setAttribute("aria-live", entry.role === "alert" ? "assertive" : "polite");
|
||||
const ariaLive = entry.role === "alert" ? "assertive" : "polite";
|
||||
badge.setAttribute("aria-live", ariaLive);
|
||||
badge.setAttribute("role", entry.role);
|
||||
badge.dataset.streamHealthState = normalizedState;
|
||||
if (hasPeerCount) {
|
||||
@@ -487,6 +507,20 @@ function setBadge(card, state, details) {
|
||||
} else if (badge.dataset.streamHealthPeers) {
|
||||
delete badge.dataset.streamHealthPeers;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
card,
|
||||
state: normalizedState,
|
||||
peers: hasPeerCount ? peersValue : null,
|
||||
reason: details && typeof details.reason === "string" ? details.reason : null,
|
||||
checkedAt:
|
||||
details && Number.isFinite(details.checkedAt) ? Number(details.checkedAt) : null,
|
||||
text: badge.textContent,
|
||||
tooltip,
|
||||
role: entry.role,
|
||||
ariaLive,
|
||||
};
|
||||
notifyBadgeUpdate(payload);
|
||||
}
|
||||
|
||||
function handleCardVisible({ card, pendingByCard, priority = 0 }) {
|
||||
@@ -597,10 +631,13 @@ const gridCardObserver = createCardObserver({
|
||||
},
|
||||
});
|
||||
|
||||
export function attachHealthBadges(container) {
|
||||
export function attachHealthBadges(container, options = {}) {
|
||||
if (!(container instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
if (options && typeof options.onUpdate === "function") {
|
||||
badgeUpdateListeners.add(options.onUpdate);
|
||||
}
|
||||
gridCardObserver.observe(container);
|
||||
}
|
||||
|
||||
|
||||
@@ -518,6 +518,20 @@ class SubscriptionsManager {
|
||||
? baseView.state.videosMap
|
||||
: new Map();
|
||||
|
||||
const urlHealthCache =
|
||||
app?.urlHealthSnapshots instanceof Map
|
||||
? app.urlHealthSnapshots
|
||||
: baseView?.state?.urlHealthByVideoId instanceof Map
|
||||
? baseView.state.urlHealthByVideoId
|
||||
: new Map();
|
||||
|
||||
const streamHealthCache =
|
||||
app?.streamHealthSnapshots instanceof Map
|
||||
? app.streamHealthSnapshots
|
||||
: baseView?.state?.streamHealthByVideoId instanceof Map
|
||||
? baseView.state.streamHealthByVideoId
|
||||
: new Map();
|
||||
|
||||
const listViewConfig = {
|
||||
document: doc,
|
||||
container,
|
||||
@@ -538,6 +552,8 @@ class SubscriptionsManager {
|
||||
state: {
|
||||
loadedThumbnails,
|
||||
videosMap,
|
||||
urlHealthByVideoId: urlHealthCache,
|
||||
streamHealthByVideoId: streamHealthCache,
|
||||
},
|
||||
utils: {
|
||||
dedupeVideos: (videos) => (Array.isArray(videos) ? [...videos] : []),
|
||||
|
||||
+303
-16
@@ -56,6 +56,12 @@ export class VideoCard {
|
||||
this.state = {
|
||||
loadedThumbnails:
|
||||
state.loadedThumbnails instanceof Map ? state.loadedThumbnails : null,
|
||||
urlHealthByVideoId:
|
||||
state.urlHealthByVideoId instanceof Map ? state.urlHealthByVideoId : null,
|
||||
streamHealthByVideoId:
|
||||
state.streamHealthByVideoId instanceof Map
|
||||
? state.streamHealthByVideoId
|
||||
: null,
|
||||
};
|
||||
this.ensureGlobalMoreMenuHandlers =
|
||||
typeof ensureGlobalMoreMenuHandlers === "function"
|
||||
@@ -210,6 +216,10 @@ export class VideoCard {
|
||||
|
||||
this.root = root;
|
||||
|
||||
if (this.video?.id) {
|
||||
root.dataset.videoId = this.video.id;
|
||||
}
|
||||
|
||||
this.applyPointerDataset();
|
||||
this.applyOwnerDataset();
|
||||
this.applySourceDatasets();
|
||||
@@ -887,17 +897,13 @@ export class VideoCard {
|
||||
"px-2",
|
||||
"py-1",
|
||||
"rounded",
|
||||
"inline-flex",
|
||||
"items-center",
|
||||
"gap-1",
|
||||
"bg-gray-800",
|
||||
"text-gray-300",
|
||||
"transition-colors",
|
||||
"duration-200",
|
||||
],
|
||||
textContent: "Checking hosted URL…",
|
||||
});
|
||||
badge.dataset.urlHealthState = "checking";
|
||||
badge.setAttribute("aria-live", "polite");
|
||||
badge.setAttribute("role", "status");
|
||||
this.applyUrlBadgeVisualState(badge, this.getCachedUrlHealthEntry());
|
||||
this.urlHealthBadgeEl = badge;
|
||||
pieces.push(badge);
|
||||
}
|
||||
@@ -911,19 +917,13 @@ export class VideoCard {
|
||||
"px-2",
|
||||
"py-1",
|
||||
"rounded",
|
||||
"inline-flex",
|
||||
"items-center",
|
||||
"gap-1",
|
||||
"bg-gray-800",
|
||||
"text-gray-300",
|
||||
"transition-colors",
|
||||
"duration-200",
|
||||
],
|
||||
textContent: "⏳ Torrent",
|
||||
});
|
||||
badge.dataset.streamHealthState = "checking";
|
||||
badge.setAttribute("aria-live", "polite");
|
||||
badge.setAttribute("role", "status");
|
||||
this.applyStreamBadgeVisualState(badge, this.getCachedStreamHealthEntry());
|
||||
this.torrentHealthBadgeEl = badge;
|
||||
pieces.push(badge);
|
||||
}
|
||||
@@ -939,6 +939,269 @@ export class VideoCard {
|
||||
return container;
|
||||
}
|
||||
|
||||
getCachedUrlHealthEntry() {
|
||||
if (!this.video?.id || !(this.state.urlHealthByVideoId instanceof Map)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const entry = this.state.urlHealthByVideoId.get(this.video.id);
|
||||
if (!entry || typeof entry !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalized = {};
|
||||
if (typeof entry.status === "string" && entry.status) {
|
||||
normalized.status = entry.status;
|
||||
}
|
||||
if (typeof entry.message === "string" && entry.message) {
|
||||
normalized.message = entry.message;
|
||||
}
|
||||
if (Number.isFinite(entry.lastCheckedAt)) {
|
||||
normalized.lastCheckedAt = Math.floor(entry.lastCheckedAt);
|
||||
}
|
||||
|
||||
return Object.keys(normalized).length ? normalized : null;
|
||||
}
|
||||
|
||||
getCachedStreamHealthEntry() {
|
||||
if (!this.video?.id || !(this.state.streamHealthByVideoId instanceof Map)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const entry = this.state.streamHealthByVideoId.get(this.video.id);
|
||||
if (!entry || typeof entry !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalized = {};
|
||||
if (typeof entry.state === "string" && entry.state) {
|
||||
normalized.state = entry.state;
|
||||
}
|
||||
if (Number.isFinite(entry.peers)) {
|
||||
normalized.peers = Math.max(0, Number(entry.peers));
|
||||
}
|
||||
if (typeof entry.reason === "string" && entry.reason) {
|
||||
normalized.reason = entry.reason;
|
||||
}
|
||||
if (Number.isFinite(entry.checkedAt)) {
|
||||
normalized.checkedAt = Math.floor(entry.checkedAt);
|
||||
}
|
||||
if (typeof entry.text === "string" && entry.text) {
|
||||
normalized.text = entry.text;
|
||||
}
|
||||
if (typeof entry.tooltip === "string" && entry.tooltip) {
|
||||
normalized.tooltip = entry.tooltip;
|
||||
}
|
||||
if (entry.role === "alert" || entry.role === "status") {
|
||||
normalized.role = entry.role;
|
||||
}
|
||||
if (entry.ariaLive === "assertive" || entry.ariaLive === "polite") {
|
||||
normalized.ariaLive = entry.ariaLive;
|
||||
}
|
||||
|
||||
return Object.keys(normalized).length ? normalized : null;
|
||||
}
|
||||
|
||||
formatTorrentCheckedTime(timestamp) {
|
||||
if (!Number.isFinite(timestamp)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
return new Date(timestamp).toLocaleTimeString([], { hour12: false });
|
||||
} catch (error) {
|
||||
try {
|
||||
return new Date(timestamp).toLocaleTimeString();
|
||||
} catch (err) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTorrentTooltip({ peers = null, checkedAt = null, reason = null } = {}) {
|
||||
const parts = [];
|
||||
if (Number.isFinite(peers)) {
|
||||
parts.push(`Peers: ${Math.max(0, Number(peers))}`);
|
||||
}
|
||||
if (Number.isFinite(checkedAt)) {
|
||||
const formatted = this.formatTorrentCheckedTime(checkedAt);
|
||||
if (formatted) {
|
||||
parts.push(`Checked ${formatted}`);
|
||||
}
|
||||
}
|
||||
if (typeof reason === "string" && reason && reason !== "peer") {
|
||||
let normalizedReason;
|
||||
if (reason === "timeout") {
|
||||
normalizedReason = "Timed out";
|
||||
} else if (reason === "no-trackers") {
|
||||
normalizedReason = "No WSS trackers";
|
||||
} else if (reason === "invalid") {
|
||||
normalizedReason = "Invalid magnet";
|
||||
} else {
|
||||
normalizedReason = reason.charAt(0).toUpperCase() + reason.slice(1);
|
||||
}
|
||||
parts.push(normalizedReason);
|
||||
}
|
||||
if (!parts.length) {
|
||||
return "WebTorrent status unknown";
|
||||
}
|
||||
return `WebTorrent • ${parts.join(" • ")}`;
|
||||
}
|
||||
|
||||
applyUrlBadgeVisualState(badge, entry) {
|
||||
if (!(badge instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const status =
|
||||
typeof entry?.status === "string" && entry.status ? entry.status : "checking";
|
||||
const message =
|
||||
typeof entry?.message === "string" && entry.message
|
||||
? entry.message
|
||||
: "Checking hosted URL…";
|
||||
|
||||
const hadMargin = badge.classList.contains("mt-3");
|
||||
const baseClasses = [
|
||||
"url-health-badge",
|
||||
"text-xs",
|
||||
"font-semibold",
|
||||
"px-2",
|
||||
"py-1",
|
||||
"rounded",
|
||||
"transition-colors",
|
||||
"duration-200",
|
||||
];
|
||||
if (hadMargin) {
|
||||
baseClasses.unshift("mt-3");
|
||||
}
|
||||
badge.className = baseClasses.join(" ");
|
||||
|
||||
const common = ["inline-flex", "items-center", "gap-1"];
|
||||
const addClasses = (classes) => {
|
||||
classes.forEach((cls) => badge.classList.add(cls));
|
||||
};
|
||||
|
||||
if (status === "healthy") {
|
||||
addClasses([...common, "bg-green-900", "text-green-200"]);
|
||||
} else if (status === "offline") {
|
||||
addClasses([...common, "bg-red-900", "text-red-200"]);
|
||||
} else if (status === "unknown" || status === "timeout") {
|
||||
addClasses([...common, "bg-amber-900", "text-amber-200"]);
|
||||
} else {
|
||||
addClasses([...common, "bg-gray-800", "text-gray-300"]);
|
||||
}
|
||||
|
||||
badge.dataset.urlHealthState = status;
|
||||
badge.textContent = message;
|
||||
badge.setAttribute("aria-live", "polite");
|
||||
badge.setAttribute("role", status === "offline" ? "alert" : "status");
|
||||
}
|
||||
|
||||
applyStreamBadgeVisualState(badge, entry) {
|
||||
if (!(badge instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const state =
|
||||
typeof entry?.state === "string" && entry.state ? entry.state : "checking";
|
||||
const peersValue = Number.isFinite(entry?.peers)
|
||||
? Math.max(0, Number(entry.peers))
|
||||
: null;
|
||||
const reason = typeof entry?.reason === "string" && entry.reason ? entry.reason : null;
|
||||
const text = typeof entry?.text === "string" && entry.text ? entry.text : null;
|
||||
const tooltip =
|
||||
typeof entry?.tooltip === "string" && entry.tooltip ? entry.tooltip : null;
|
||||
const role = entry?.role === "alert" || entry?.role === "status" ? entry.role : null;
|
||||
const ariaLive =
|
||||
entry?.ariaLive === "assertive" || entry?.ariaLive === "polite"
|
||||
? entry.ariaLive
|
||||
: role === "alert"
|
||||
? "assertive"
|
||||
: "polite";
|
||||
|
||||
const hadMargin = badge.classList.contains("mt-3");
|
||||
const baseClasses = [
|
||||
"torrent-health-badge",
|
||||
"text-xs",
|
||||
"font-semibold",
|
||||
"px-2",
|
||||
"py-1",
|
||||
"rounded",
|
||||
"transition-colors",
|
||||
"duration-200",
|
||||
];
|
||||
if (hadMargin) {
|
||||
baseClasses.unshift("mt-3");
|
||||
}
|
||||
badge.className = baseClasses.join(" ");
|
||||
|
||||
const common = ["inline-flex", "items-center", "gap-1"];
|
||||
const addClasses = (classes) => {
|
||||
classes.forEach((cls) => badge.classList.add(cls));
|
||||
};
|
||||
|
||||
if (state === "healthy") {
|
||||
addClasses([...common, "bg-green-900", "text-green-200"]);
|
||||
} else if (state === "unhealthy") {
|
||||
addClasses([...common, "bg-red-900", "text-red-200"]);
|
||||
} else {
|
||||
addClasses([...common, "bg-gray-800", "text-gray-300"]);
|
||||
}
|
||||
|
||||
const map = {
|
||||
healthy: {
|
||||
icon: "🟢",
|
||||
aria: "WebTorrent peers available",
|
||||
},
|
||||
unhealthy: {
|
||||
icon: "🔴",
|
||||
aria: "WebTorrent peers unavailable",
|
||||
},
|
||||
checking: {
|
||||
icon: "⏳",
|
||||
aria: "Checking WebTorrent peers",
|
||||
},
|
||||
unknown: {
|
||||
icon: "⚪",
|
||||
aria: "WebTorrent status unknown",
|
||||
},
|
||||
};
|
||||
|
||||
const descriptor = map[state] || map.unknown;
|
||||
const peersText = state === "healthy" && peersValue > 0 ? ` (${peersValue})` : "";
|
||||
const iconPrefix = descriptor.icon ? `${descriptor.icon} ` : "";
|
||||
const computedText = `${iconPrefix}WebTorrent${peersText}`;
|
||||
badge.textContent = text || computedText;
|
||||
|
||||
const tooltipValue =
|
||||
tooltip ||
|
||||
(state === "checking" || state === "unknown"
|
||||
? descriptor.aria
|
||||
: this.buildTorrentTooltip({
|
||||
peers: peersValue,
|
||||
checkedAt: Number.isFinite(entry?.checkedAt) ? entry.checkedAt : null,
|
||||
reason,
|
||||
}));
|
||||
|
||||
badge.setAttribute("aria-label", tooltipValue);
|
||||
badge.setAttribute("title", tooltipValue);
|
||||
badge.setAttribute("aria-live", ariaLive);
|
||||
badge.setAttribute("role", role || (state === "unhealthy" ? "alert" : "status"));
|
||||
|
||||
badge.dataset.streamHealthState = state;
|
||||
if (reason) {
|
||||
badge.dataset.streamHealthReason = reason;
|
||||
} else if (badge.dataset.streamHealthReason) {
|
||||
delete badge.dataset.streamHealthReason;
|
||||
}
|
||||
|
||||
if (Number.isFinite(peersValue) && peersValue !== null) {
|
||||
badge.dataset.streamHealthPeers = String(peersValue);
|
||||
} else if (badge.dataset.streamHealthPeers) {
|
||||
delete badge.dataset.streamHealthPeers;
|
||||
}
|
||||
}
|
||||
|
||||
buildDiscussionCount() {
|
||||
if (this.video.enableComments === false) {
|
||||
return null;
|
||||
@@ -1033,8 +1296,13 @@ export class VideoCard {
|
||||
return;
|
||||
}
|
||||
|
||||
const cachedUrlHealth = this.getCachedUrlHealthEntry();
|
||||
if (this.playbackUrl) {
|
||||
this.root.dataset.urlHealthState = "checking";
|
||||
const status =
|
||||
typeof cachedUrlHealth?.status === "string" && cachedUrlHealth.status
|
||||
? cachedUrlHealth.status
|
||||
: "checking";
|
||||
this.root.dataset.urlHealthState = status;
|
||||
delete this.root.dataset.urlHealthReason;
|
||||
this.root.dataset.urlHealthEventId = this.video.id || "";
|
||||
this.root.dataset.urlHealthUrl = encodeURIComponent(this.playbackUrl);
|
||||
@@ -1045,14 +1313,33 @@ export class VideoCard {
|
||||
delete this.root.dataset.urlHealthUrl;
|
||||
}
|
||||
|
||||
const cachedStreamHealth = this.getCachedStreamHealthEntry();
|
||||
if (this.magnetProvided && this.magnetSupported) {
|
||||
this.root.dataset.streamHealthState = "checking";
|
||||
const state =
|
||||
typeof cachedStreamHealth?.state === "string" && cachedStreamHealth.state
|
||||
? cachedStreamHealth.state
|
||||
: "checking";
|
||||
this.root.dataset.streamHealthState = state;
|
||||
if (cachedStreamHealth?.reason) {
|
||||
this.root.dataset.streamHealthReason = cachedStreamHealth.reason;
|
||||
} else if (this.root.dataset.streamHealthReason) {
|
||||
delete this.root.dataset.streamHealthReason;
|
||||
}
|
||||
if (Number.isFinite(cachedStreamHealth?.peers)) {
|
||||
this.root.dataset.streamHealthPeers = String(
|
||||
Math.max(0, Number(cachedStreamHealth.peers))
|
||||
);
|
||||
} else if (this.root.dataset.streamHealthPeers) {
|
||||
delete this.root.dataset.streamHealthPeers;
|
||||
}
|
||||
} else {
|
||||
this.root.dataset.streamHealthState = "unhealthy";
|
||||
this.root.dataset.streamHealthReason = this.magnetProvided
|
||||
? "unsupported"
|
||||
: "missing-source";
|
||||
if (this.root.dataset.streamHealthPeers) {
|
||||
delete this.root.dataset.streamHealthPeers;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.magnetProvided) {
|
||||
|
||||
@@ -54,6 +54,14 @@ export class VideoListView {
|
||||
loadedThumbnails:
|
||||
state.loadedThumbnails instanceof Map ? state.loadedThumbnails : new Map(),
|
||||
videosMap: state.videosMap instanceof Map ? state.videosMap : new Map(),
|
||||
urlHealthByVideoId:
|
||||
state.urlHealthByVideoId instanceof Map
|
||||
? state.urlHealthByVideoId
|
||||
: new Map(),
|
||||
streamHealthByVideoId:
|
||||
state.streamHealthByVideoId instanceof Map
|
||||
? state.streamHealthByVideoId
|
||||
: new Map(),
|
||||
feedMetadata:
|
||||
state.feedMetadata && typeof state.feedMetadata === "object"
|
||||
? { ...state.feedMetadata }
|
||||
@@ -268,6 +276,7 @@ export class VideoListView {
|
||||
|
||||
this.syncViewCountSubscriptions(displayVideos);
|
||||
this.cleanupThumbnailCache(displayVideos);
|
||||
this.cleanupHealthCache(displayVideos);
|
||||
|
||||
if (!displayVideos.length) {
|
||||
this.renderedVideoIds.clear();
|
||||
@@ -383,7 +392,11 @@ export class VideoListView {
|
||||
fallbackThumbnailSrc: this.assets.fallbackThumbnailSrc,
|
||||
unsupportedBtihMessage: this.assets.unsupportedBtihMessage,
|
||||
},
|
||||
state: { loadedThumbnails: this.state.loadedThumbnails },
|
||||
state: {
|
||||
loadedThumbnails: this.state.loadedThumbnails,
|
||||
urlHealthByVideoId: this.state.urlHealthByVideoId,
|
||||
streamHealthByVideoId: this.state.streamHealthByVideoId,
|
||||
},
|
||||
ensureGlobalMoreMenuHandlers: () =>
|
||||
this.utils.ensureGlobalMoreMenuHandlers(),
|
||||
onRequestCloseAllMenus: () => this.closeAllMenus(),
|
||||
@@ -517,6 +530,99 @@ export class VideoListView {
|
||||
return displayVideos;
|
||||
}
|
||||
|
||||
cacheUrlHealth(videoId, entry) {
|
||||
if (!videoId || !(this.state.urlHealthByVideoId instanceof Map)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!entry || typeof entry !== "object") {
|
||||
this.state.urlHealthByVideoId.delete(videoId);
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = {
|
||||
status:
|
||||
typeof entry.status === "string" && entry.status ? entry.status : "checking",
|
||||
message:
|
||||
typeof entry.message === "string" && entry.message
|
||||
? entry.message
|
||||
: "Checking hosted URL…",
|
||||
};
|
||||
|
||||
if (Number.isFinite(entry.lastCheckedAt)) {
|
||||
normalized.lastCheckedAt = Math.floor(entry.lastCheckedAt);
|
||||
}
|
||||
|
||||
this.state.urlHealthByVideoId.set(videoId, normalized);
|
||||
}
|
||||
|
||||
cacheStreamHealth(videoId, entry) {
|
||||
if (!videoId || !(this.state.streamHealthByVideoId instanceof Map)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!entry || typeof entry !== "object") {
|
||||
this.state.streamHealthByVideoId.delete(videoId);
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = {
|
||||
state:
|
||||
typeof entry.state === "string" && entry.state ? entry.state : "unknown",
|
||||
};
|
||||
|
||||
if (Number.isFinite(entry.peers)) {
|
||||
normalized.peers = Math.max(0, Number(entry.peers));
|
||||
}
|
||||
if (typeof entry.reason === "string" && entry.reason) {
|
||||
normalized.reason = entry.reason;
|
||||
}
|
||||
if (Number.isFinite(entry.checkedAt)) {
|
||||
normalized.checkedAt = Math.floor(entry.checkedAt);
|
||||
}
|
||||
if (typeof entry.text === "string" && entry.text) {
|
||||
normalized.text = entry.text;
|
||||
}
|
||||
if (typeof entry.tooltip === "string" && entry.tooltip) {
|
||||
normalized.tooltip = entry.tooltip;
|
||||
}
|
||||
if (entry.role === "alert" || entry.role === "status") {
|
||||
normalized.role = entry.role;
|
||||
}
|
||||
if (entry.ariaLive === "assertive" || entry.ariaLive === "polite") {
|
||||
normalized.ariaLive = entry.ariaLive;
|
||||
}
|
||||
|
||||
this.state.streamHealthByVideoId.set(videoId, normalized);
|
||||
}
|
||||
|
||||
cleanupHealthCache(videos) {
|
||||
const activeIds = new Set();
|
||||
if (Array.isArray(videos)) {
|
||||
videos.forEach((video) => {
|
||||
if (video && typeof video.id === "string" && video.id) {
|
||||
activeIds.add(video.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this.state.urlHealthByVideoId instanceof Map) {
|
||||
Array.from(this.state.urlHealthByVideoId.keys()).forEach((videoId) => {
|
||||
if (!activeIds.has(videoId)) {
|
||||
this.state.urlHealthByVideoId.delete(videoId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this.state.streamHealthByVideoId instanceof Map) {
|
||||
Array.from(this.state.streamHealthByVideoId.keys()).forEach((videoId) => {
|
||||
if (!activeIds.has(videoId)) {
|
||||
this.state.streamHealthByVideoId.delete(videoId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateVideo(videoId, patch) {
|
||||
if (!videoId) {
|
||||
return null;
|
||||
|
||||
@@ -49,7 +49,7 @@ export class MediaLoader {
|
||||
el.style.backgroundImage = `url('${lazySrc}')`;
|
||||
}
|
||||
|
||||
el.dataset.thumbnailLoaded = "true";
|
||||
delete el.dataset.lazy;
|
||||
this.observer.unobserve(el);
|
||||
}
|
||||
}, {
|
||||
|
||||
Reference in new issue
Block a user