Files
bitvid/js/magnets.js
T

148 lines
3.3 KiB
JavaScript

const BASE32_ALPHABET = "abcdefghijklmnopqrstuvwxyz234567";
function decodeBase32ToHex(value) {
if (typeof value !== "string" || !value) {
return "";
}
const normalized = value.toLowerCase();
let bits = 0;
let buffer = 0;
const bytes = [];
for (const char of normalized) {
const index = BASE32_ALPHABET.indexOf(char);
if (index === -1) {
return "";
}
buffer = (buffer << 5) | index;
bits += 5;
if (bits >= 8) {
bits -= 8;
const byte = (buffer >> bits) & 0xff;
bytes.push(byte);
}
}
if (bytes.length === 0) {
return "";
}
return bytes.map((byte) => byte.toString(16).padStart(2, "0")).join("");
}
function extractInfoHashFromXt(value) {
if (typeof value !== "string") {
return "";
}
const trimmed = value.trim();
if (!trimmed) {
return "";
}
const lower = trimmed.toLowerCase();
if (!lower.startsWith("urn:btih:")) {
return "";
}
const payload = trimmed.slice(9);
return normalizeInfoHash(payload);
}
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)) {
const hex = decodeBase32ToHex(trimmed);
return hex ? hex.toLowerCase() : null;
}
return null;
}
export function infoHashFromMagnet(magnet) {
if (typeof magnet !== "string") {
return null;
}
const direct = normalizeInfoHash(magnet);
if (direct) {
return direct;
}
const parsed = parseMagnetLite(magnet);
const hash = typeof parsed.infoHash === "string" ? parsed.infoHash : "";
return hash ? hash.toLowerCase() : null;
}
export function trackersFromMagnet(magnet) {
if (typeof magnet !== "string") {
return [];
}
const parsed = parseMagnetLite(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);
}
function parseMagnetLite(magnet) {
if (typeof magnet !== "string") {
return { infoHash: "", announce: [] };
}
const trimmed = magnet.trim();
if (!trimmed) {
return { infoHash: "", announce: [] };
}
const lower = trimmed.toLowerCase();
const queryIndex = lower.indexOf("?");
const paramsPart = queryIndex >= 0 ? trimmed.slice(queryIndex + 1) : "";
const params = paramsPart.split("&");
const announce = [];
let infoHash = "";
for (const param of params) {
if (!param) {
continue;
}
const [rawKey, ...rawValueParts] = param.split("=");
const rawValue = rawValueParts.join("=");
const key = decodeURIComponent((rawKey || "").replace(/\+/g, " ")).toLowerCase();
const value = decodeURIComponent((rawValue || "").replace(/\+/g, " "));
if (!key) {
continue;
}
if (key === "xt") {
const derived = extractInfoHashFromXt(value);
if (derived && !infoHash) {
infoHash = derived;
}
} else if (key === "tr") {
if (value) {
announce.push(value);
}
}
}
return { infoHash, announce };
}