mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
231 lines
5.1 KiB
JavaScript
231 lines
5.1 KiB
JavaScript
import "./bufferPolyfill.js";
|
|
import Client from "https://esm.sh/[email protected]/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;
|
|
}
|