mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Add WebTorrent probe for grid health badges
This commit is contained in:
1 parent
a05e0c77fd
commit
bc085ad3be
2 files changed
+590
-81
No files matched your search
+301
-81
@@ -1,13 +1,230 @@
|
||||
import { infoHashFromMagnet } from "./magnets.js";
|
||||
import {
|
||||
getDefaultHealth,
|
||||
getHealthCached,
|
||||
queueHealthCheck,
|
||||
} from "./healthService.js";
|
||||
import { TorrentClient, torrentClient } from "./webtorrent.js";
|
||||
|
||||
const containerState = new WeakMap();
|
||||
const ROOT_MARGIN = "200px 0px";
|
||||
|
||||
function now() {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
function getCacheEntry(key) {
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
const entry = probeCache.get(key);
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
if (now() - entry.ts > PROBE_CACHE_TTL_MS) {
|
||||
probeCache.delete(key);
|
||||
return null;
|
||||
}
|
||||
return entry.value;
|
||||
}
|
||||
|
||||
function setCacheEntry(key, value) {
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
probeCache.set(key, { ts: now(), value });
|
||||
}
|
||||
|
||||
function formatTime(ts) {
|
||||
if (!Number.isFinite(ts)) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return new Date(ts).toLocaleTimeString([], { hour12: false });
|
||||
} catch (err) {
|
||||
return new Date(ts).toLocaleTimeString();
|
||||
}
|
||||
}
|
||||
|
||||
function buildTooltip({ peers = 0, checkedAt, reason } = {}) {
|
||||
const parts = [];
|
||||
if (Number.isFinite(peers)) {
|
||||
parts.push(`Peers: ${Math.max(0, peers)}`);
|
||||
}
|
||||
if (Number.isFinite(checkedAt)) {
|
||||
const formatted = formatTime(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(" • ")}`;
|
||||
}
|
||||
|
||||
function normalizeResult(result) {
|
||||
const fallback = {
|
||||
healthy: false,
|
||||
peers: 0,
|
||||
reason: "error",
|
||||
appendedTrackers: false,
|
||||
hasProbeTrackers: false,
|
||||
usedTrackers: Array.isArray(TorrentClient.PROBE_TRACKERS)
|
||||
? [...TorrentClient.PROBE_TRACKERS]
|
||||
: [],
|
||||
durationMs: 0,
|
||||
};
|
||||
if (!result || typeof result !== "object") {
|
||||
return { ...fallback, checkedAt: now() };
|
||||
}
|
||||
const peers = Number.isFinite(result.peers)
|
||||
? Math.max(0, Number(result.peers))
|
||||
: 0;
|
||||
const healthy = Boolean(result.healthy) && peers > 0;
|
||||
const reason = typeof result.reason === "string" ? result.reason : "error";
|
||||
return {
|
||||
healthy,
|
||||
peers: healthy ? Math.max(1, peers) : peers,
|
||||
reason,
|
||||
appendedTrackers: Boolean(result.appendedTrackers),
|
||||
hasProbeTrackers:
|
||||
typeof result.hasProbeTrackers === "boolean"
|
||||
? result.hasProbeTrackers
|
||||
: healthy || Boolean(result.appendedTrackers),
|
||||
usedTrackers: Array.isArray(result.usedTrackers)
|
||||
? result.usedTrackers.slice()
|
||||
: fallback.usedTrackers,
|
||||
durationMs: Number.isFinite(result.durationMs)
|
||||
? Math.max(0, Number(result.durationMs))
|
||||
: 0,
|
||||
checkedAt: now(),
|
||||
};
|
||||
}
|
||||
|
||||
function queueProbe(magnet, cacheKey) {
|
||||
if (!magnet) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
const cached = getCacheEntry(cacheKey);
|
||||
if (cached) {
|
||||
return Promise.resolve(cached);
|
||||
}
|
||||
|
||||
if (probeInflight.has(cacheKey)) {
|
||||
return probeInflight.get(cacheKey);
|
||||
}
|
||||
|
||||
const job = probeQueue
|
||||
.run(() =>
|
||||
torrentClient
|
||||
.probePeers(magnet, {
|
||||
timeoutMs: PROBE_TIMEOUT_MS,
|
||||
maxWebConns: 2,
|
||||
polls: PROBE_POLL_COUNT,
|
||||
})
|
||||
.catch((err) => ({
|
||||
healthy: false,
|
||||
peers: 0,
|
||||
reason: "error",
|
||||
error: err,
|
||||
appendedTrackers: false,
|
||||
hasProbeTrackers: false,
|
||||
}))
|
||||
)
|
||||
.then((result) => {
|
||||
const normalized = normalizeResult(result);
|
||||
if (cacheKey) {
|
||||
setCacheEntry(cacheKey, normalized);
|
||||
}
|
||||
return normalized;
|
||||
})
|
||||
.finally(() => {
|
||||
if (cacheKey) {
|
||||
probeInflight.delete(cacheKey);
|
||||
}
|
||||
});
|
||||
|
||||
if (cacheKey) {
|
||||
probeInflight.set(cacheKey, job);
|
||||
}
|
||||
|
||||
return job;
|
||||
}
|
||||
|
||||
const PROBE_CACHE_TTL_MS = 5 * 60 * 1000;
|
||||
const PROBE_TIMEOUT_MS = 8000;
|
||||
const PROBE_CONCURRENCY = 3;
|
||||
const PROBE_POLL_COUNT = 3;
|
||||
|
||||
class ProbeQueue {
|
||||
constructor(max = 2) {
|
||||
this.max = Math.max(1, Number(max) || 1);
|
||||
this.running = 0;
|
||||
this.queue = [];
|
||||
}
|
||||
|
||||
run(task) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.queue.push({ task, resolve, reject });
|
||||
this.drain();
|
||||
});
|
||||
}
|
||||
|
||||
drain() {
|
||||
if (this.running >= this.max) {
|
||||
return;
|
||||
}
|
||||
const job = this.queue.shift();
|
||||
if (!job) {
|
||||
return;
|
||||
}
|
||||
this.running += 1;
|
||||
let finished = false;
|
||||
const finalize = () => {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
this.running -= 1;
|
||||
this.drain();
|
||||
};
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = job.task();
|
||||
} catch (err) {
|
||||
job.reject(err);
|
||||
finalize();
|
||||
return;
|
||||
}
|
||||
|
||||
Promise.resolve(result)
|
||||
.then((value) => {
|
||||
job.resolve(value);
|
||||
})
|
||||
.catch((err) => {
|
||||
job.reject(err);
|
||||
})
|
||||
.finally(() => {
|
||||
finalize();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const probeQueue = new ProbeQueue(PROBE_CONCURRENCY);
|
||||
const probeCache = new Map();
|
||||
const probeInflight = new Map();
|
||||
|
||||
function ensureState(container) {
|
||||
let state = containerState.get(container);
|
||||
if (state) {
|
||||
@@ -38,23 +255,7 @@ function ensureState(container) {
|
||||
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) {
|
||||
function setBadge(card, state, details) {
|
||||
const badge = card.querySelector(".torrent-health-badge");
|
||||
if (!badge) {
|
||||
return;
|
||||
@@ -80,65 +281,61 @@ function setBadge(card, visual, health) {
|
||||
badge.className = baseClasses.join(" ");
|
||||
|
||||
const map = {
|
||||
good: {
|
||||
icon: "✅",
|
||||
aria: "WebTorrent fallback ready",
|
||||
healthy: {
|
||||
icon: "🟢",
|
||||
aria: "WebTorrent peers available",
|
||||
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",
|
||||
unhealthy: {
|
||||
icon: "🔴",
|
||||
aria: "WebTorrent peers unavailable",
|
||||
classes: ["bg-red-900", "text-red-200"],
|
||||
role: "alert",
|
||||
},
|
||||
checking: {
|
||||
icon: "⏳",
|
||||
aria: "Checking Torrent availability",
|
||||
aria: "Checking WebTorrent peers",
|
||||
classes: ["bg-gray-800", "text-gray-300"],
|
||||
role: "status",
|
||||
},
|
||||
unknown: {
|
||||
icon: "⚠️",
|
||||
aria: "Torrent availability unknown",
|
||||
classes: ["bg-amber-900", "text-amber-200"],
|
||||
icon: "⚪",
|
||||
aria: "WebTorrent status unknown",
|
||||
classes: ["bg-gray-800", "text-gray-300"],
|
||||
role: "status",
|
||||
},
|
||||
};
|
||||
|
||||
const entry = map[visual] || map.unknown;
|
||||
const entry = map[state] || 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 peers =
|
||||
details && Number.isFinite(details.peers)
|
||||
? Math.max(0, Number(details.peers))
|
||||
: 0;
|
||||
const peersText = state === "healthy" && peers > 0 ? ` (${peers})` : "";
|
||||
|
||||
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.textContent = `${iconPrefix}WebTorrent${peersText}`;
|
||||
const tooltip =
|
||||
state === "checking" || state === "unknown"
|
||||
? entry.aria
|
||||
: buildTooltip({
|
||||
peers,
|
||||
checkedAt: details?.checkedAt,
|
||||
reason: details?.reason,
|
||||
});
|
||||
badge.setAttribute("aria-label", tooltip);
|
||||
badge.setAttribute("title", tooltip);
|
||||
badge.setAttribute("aria-live", entry.role === "alert" ? "assertive" : "polite");
|
||||
badge.setAttribute("role", entry.role);
|
||||
badge.dataset.streamHealthState = visual;
|
||||
}
|
||||
|
||||
function applyHealth(card, health) {
|
||||
if (!health) {
|
||||
setBadge(card, "unknown");
|
||||
return;
|
||||
badge.dataset.streamHealthState = state;
|
||||
if (Number.isFinite(peers)) {
|
||||
badge.dataset.streamHealthPeers = String(peers);
|
||||
} else if (badge.dataset.streamHealthPeers) {
|
||||
delete badge.dataset.streamHealthPeers;
|
||||
}
|
||||
const visual = toVisual(health);
|
||||
setBadge(card, visual, health);
|
||||
}
|
||||
|
||||
function handleCardVisible({ card, pendingByCard }) {
|
||||
@@ -157,32 +354,55 @@ function handleCardVisible({ card, pendingByCard }) {
|
||||
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);
|
||||
const cached = getCacheEntry(infoHash);
|
||||
if (cached) {
|
||||
const cachedState = cached.healthy
|
||||
? "healthy"
|
||||
: cached.hasProbeTrackers
|
||||
? "unhealthy"
|
||||
: "unknown";
|
||||
setBadge(card, cachedState, cached);
|
||||
return;
|
||||
}
|
||||
|
||||
setBadge(card, "checking");
|
||||
|
||||
const probePromise = queueProbe(magnet, infoHash);
|
||||
|
||||
pendingByCard.set(card, probePromise);
|
||||
|
||||
probePromise
|
||||
.then((result) => {
|
||||
pendingByCard.delete(card);
|
||||
if (!card.isConnected) {
|
||||
return;
|
||||
}
|
||||
if (!result) {
|
||||
setBadge(card, "unknown");
|
||||
return;
|
||||
}
|
||||
if (!result.hasProbeTrackers) {
|
||||
setBadge(card, "unknown", result);
|
||||
return;
|
||||
}
|
||||
if (result.healthy) {
|
||||
setBadge(card, "healthy", result);
|
||||
return;
|
||||
}
|
||||
setBadge(card, "unhealthy", result);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.warn("[gridHealth] probe failed", err);
|
||||
pendingByCard.delete(card);
|
||||
if (!card.isConnected) {
|
||||
return;
|
||||
}
|
||||
setBadge(card, "unhealthy");
|
||||
});
|
||||
}
|
||||
|
||||
export function attachHealthBadges(container) {
|
||||
|
||||
@@ -2,11 +2,145 @@
|
||||
|
||||
import WebTorrent from "./webtorrent.min.js";
|
||||
|
||||
const DEFAULT_PROBE_TRACKERS = Object.freeze([
|
||||
"wss://tracker.btorrent.xyz",
|
||||
"wss://tracker.openwebtorrent.com",
|
||||
]);
|
||||
|
||||
function decodeComponentSafe(value) {
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return decodeURIComponent(value);
|
||||
} catch (err) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeTrackerList(trackers) {
|
||||
const normalized = [];
|
||||
const seen = new Set();
|
||||
if (!Array.isArray(trackers)) {
|
||||
return normalized;
|
||||
}
|
||||
trackers.forEach((tracker) => {
|
||||
if (typeof tracker !== "string") {
|
||||
return;
|
||||
}
|
||||
const trimmed = tracker.trim();
|
||||
if (!trimmed || !/^wss:\/\//i.test(trimmed)) {
|
||||
return;
|
||||
}
|
||||
const lower = trimmed.toLowerCase();
|
||||
if (seen.has(lower)) {
|
||||
return;
|
||||
}
|
||||
seen.add(lower);
|
||||
normalized.push(trimmed);
|
||||
});
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function appendProbeTrackers(magnetURI, trackers) {
|
||||
if (typeof magnetURI !== "string") {
|
||||
return { magnet: "", appended: false, hasProbeTrackers: false };
|
||||
}
|
||||
|
||||
const trimmedMagnet = magnetURI.trim();
|
||||
if (!trimmedMagnet) {
|
||||
return { magnet: "", appended: false, hasProbeTrackers: false };
|
||||
}
|
||||
|
||||
const probeTrackers = normalizeTrackerList(trackers);
|
||||
if (!probeTrackers.length) {
|
||||
return {
|
||||
magnet: trimmedMagnet,
|
||||
appended: false,
|
||||
hasProbeTrackers: false,
|
||||
};
|
||||
}
|
||||
|
||||
const trackerSet = new Set();
|
||||
const [withoutFragment, fragment = ""] = trimmedMagnet.split("#", 2);
|
||||
const [, queryPart = ""] = withoutFragment.split("?", 2);
|
||||
|
||||
if (queryPart) {
|
||||
queryPart
|
||||
.split("&")
|
||||
.map((segment) => segment.trim())
|
||||
.filter(Boolean)
|
||||
.forEach((segment) => {
|
||||
const [rawKey, rawValue = ""] = segment.split("=", 2);
|
||||
if (!rawKey || rawKey.trim().toLowerCase() !== "tr") {
|
||||
return;
|
||||
}
|
||||
const decoded = decodeComponentSafe(rawValue).trim().toLowerCase();
|
||||
if (decoded) {
|
||||
trackerSet.add(decoded);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const normalizedProbe = probeTrackers.map((url) => url.toLowerCase());
|
||||
const hadProbeTracker = normalizedProbe.some((url) => trackerSet.has(url));
|
||||
|
||||
const toAppend = [];
|
||||
probeTrackers.forEach((tracker, index) => {
|
||||
const normalizedTracker = normalizedProbe[index];
|
||||
if (trackerSet.has(normalizedTracker)) {
|
||||
return;
|
||||
}
|
||||
trackerSet.add(normalizedTracker);
|
||||
toAppend.push(`tr=${encodeURIComponent(tracker)}`);
|
||||
});
|
||||
|
||||
if (!toAppend.length) {
|
||||
return {
|
||||
magnet: trimmedMagnet,
|
||||
appended: false,
|
||||
hasProbeTrackers: hadProbeTracker,
|
||||
};
|
||||
}
|
||||
|
||||
const separator = queryPart ? "&" : "?";
|
||||
const augmented = `${withoutFragment}${separator}${toAppend.join("&")}`;
|
||||
const finalMagnet = fragment
|
||||
? `${augmented}#${fragment}`
|
||||
: augmented;
|
||||
|
||||
return {
|
||||
magnet: finalMagnet,
|
||||
appended: true,
|
||||
hasProbeTrackers: true,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeNumber(value, fallback = 0) {
|
||||
const coerced = Number(value);
|
||||
if (Number.isFinite(coerced)) {
|
||||
return coerced;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function toError(err) {
|
||||
if (err instanceof Error) {
|
||||
return err;
|
||||
}
|
||||
try {
|
||||
return new Error(String(err));
|
||||
} catch (stringifyError) {
|
||||
return new Error("Unknown error");
|
||||
}
|
||||
}
|
||||
|
||||
export class TorrentClient {
|
||||
constructor() {
|
||||
// Reusable objects and flags
|
||||
this.client = null;
|
||||
this.currentTorrent = null;
|
||||
this.probeClient = null;
|
||||
|
||||
// Service worker registration is cached
|
||||
this.swRegistration = null;
|
||||
@@ -16,6 +150,155 @@ export class TorrentClient {
|
||||
this.TIMEOUT_DURATION = 60000;
|
||||
}
|
||||
|
||||
ensureClientForProbe() {
|
||||
if (!this.probeClient) {
|
||||
this.probeClient = new WebTorrent();
|
||||
}
|
||||
return this.probeClient;
|
||||
}
|
||||
|
||||
async probePeers(
|
||||
magnetURI,
|
||||
{ timeoutMs = 8000, maxWebConns = 2, polls = 3 } = {}
|
||||
) {
|
||||
const magnet = typeof magnetURI === "string" ? magnetURI.trim() : "";
|
||||
if (!magnet) {
|
||||
return {
|
||||
healthy: false,
|
||||
peers: 0,
|
||||
reason: "invalid",
|
||||
appendedTrackers: false,
|
||||
hasProbeTrackers: false,
|
||||
usedTrackers: [...TorrentClient.PROBE_TRACKERS],
|
||||
durationMs: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const trackers = normalizeTrackerList(TorrentClient.PROBE_TRACKERS);
|
||||
const { magnet: augmentedMagnet, appended, hasProbeTrackers } =
|
||||
appendProbeTrackers(magnet, trackers);
|
||||
|
||||
if (!hasProbeTrackers) {
|
||||
return {
|
||||
healthy: false,
|
||||
peers: 0,
|
||||
reason: "no-trackers",
|
||||
appendedTrackers: false,
|
||||
hasProbeTrackers: false,
|
||||
usedTrackers: trackers,
|
||||
durationMs: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const client = this.ensureClientForProbe();
|
||||
const safeTimeout = Math.max(0, normalizeNumber(timeoutMs, 8000));
|
||||
const safePolls = Math.max(1, Math.floor(normalizeNumber(polls, 3)));
|
||||
const safeMaxWebConns = Math.max(1, Math.floor(normalizeNumber(maxWebConns, 2)));
|
||||
const pollInterval = Math.max(
|
||||
250,
|
||||
Math.floor(safeTimeout / Math.max(1, safePolls))
|
||||
);
|
||||
|
||||
const startedAt =
|
||||
typeof performance !== "undefined" && performance?.now
|
||||
? performance.now()
|
||||
: Date.now();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let settled = false;
|
||||
let torrent = null;
|
||||
let timeoutId = null;
|
||||
let pollId = null;
|
||||
|
||||
const finalize = (overrides = {}) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
if (pollId) {
|
||||
clearInterval(pollId);
|
||||
pollId = null;
|
||||
}
|
||||
if (torrent) {
|
||||
try {
|
||||
torrent.destroy({ destroyStore: true });
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const endedAt =
|
||||
typeof performance !== "undefined" && performance?.now
|
||||
? performance.now()
|
||||
: Date.now();
|
||||
|
||||
resolve({
|
||||
healthy: false,
|
||||
peers: 0,
|
||||
reason: "timeout",
|
||||
appendedTrackers: appended,
|
||||
hasProbeTrackers,
|
||||
usedTrackers: trackers,
|
||||
durationMs: Math.max(0, endedAt - startedAt),
|
||||
...overrides,
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
torrent = client.add(augmentedMagnet, {
|
||||
announce: trackers,
|
||||
maxWebConns: safeMaxWebConns,
|
||||
});
|
||||
} catch (err) {
|
||||
finalize({
|
||||
reason: "error",
|
||||
error: toError(err),
|
||||
peers: 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const settleHealthy = () => {
|
||||
const peers = Math.max(1, Math.floor(normalizeNumber(torrent?.numPeers, 1)));
|
||||
finalize({ healthy: true, peers, reason: "peer" });
|
||||
};
|
||||
|
||||
torrent.once("wire", settleHealthy);
|
||||
|
||||
torrent.once("error", (err) => {
|
||||
finalize({
|
||||
healthy: false,
|
||||
reason: "error",
|
||||
error: toError(err),
|
||||
peers: Math.max(0, Math.floor(normalizeNumber(torrent?.numPeers, 0))),
|
||||
});
|
||||
});
|
||||
|
||||
if (safeTimeout > 0) {
|
||||
timeoutId = setTimeout(() => {
|
||||
finalize({
|
||||
healthy: false,
|
||||
peers: Math.max(0, Math.floor(normalizeNumber(torrent?.numPeers, 0))),
|
||||
});
|
||||
}, safeTimeout);
|
||||
}
|
||||
|
||||
pollId = setInterval(() => {
|
||||
if (!torrent || settled) {
|
||||
return;
|
||||
}
|
||||
const peers = Math.max(0, Math.floor(normalizeNumber(torrent.numPeers, 0)));
|
||||
if (peers > 0) {
|
||||
finalize({ healthy: true, peers, reason: "peer" });
|
||||
}
|
||||
}, pollInterval);
|
||||
});
|
||||
}
|
||||
|
||||
log(msg) {
|
||||
console.log(msg);
|
||||
}
|
||||
@@ -502,6 +785,10 @@ export class TorrentClient {
|
||||
await this.client.destroy();
|
||||
this.client = null;
|
||||
}
|
||||
if (this.probeClient) {
|
||||
await this.probeClient.destroy();
|
||||
this.probeClient = null;
|
||||
}
|
||||
this.currentTorrent = null;
|
||||
this.serverCreated = false;
|
||||
} catch (error) {
|
||||
@@ -510,4 +797,6 @@ export class TorrentClient {
|
||||
}
|
||||
}
|
||||
|
||||
TorrentClient.PROBE_TRACKERS = DEFAULT_PROBE_TRACKERS;
|
||||
|
||||
export const torrentClient = new TorrentClient();
|
||||
Reference in new issue
Block a user