Files
bitvid/js/magnet.js
2025-09-24 20:53:54 -04:00

171 lines
4.1 KiB
JavaScript

import { WSS_TRACKERS } from "./constants.js";
const HEX_INFO_HASH = /^[0-9a-f]{40}$/i;
const ENCODED_BTih_PATTERN = /xt=urn%3Abtih%3A([0-9a-z]+)/gi;
function getOriginProtocol() {
if (typeof window !== "undefined" && window.location?.protocol) {
return window.location.protocol.toLowerCase();
}
return "https:";
}
function decodeLoose(value) {
if (typeof value !== "string") {
return "";
}
const trimmed = value.trim();
if (!trimmed) {
return "";
}
try {
return decodeURIComponent(trimmed);
} catch (err) {
return trimmed;
}
}
function normalizeForComparison(value) {
if (typeof value !== "string") {
return "";
}
return value.trim().replace(/\/+$/, "").toLowerCase();
}
function tryFormatAbsoluteUrl(candidate) {
if (typeof candidate !== "string") {
return "";
}
const trimmed = candidate.trim();
if (!trimmed) {
return "";
}
try {
const parsed = new URL(trimmed);
return parsed.toString();
} catch (err) {
return trimmed;
}
}
/**
* Normalize and enrich a magnet string entered in the upload form so that it
* always produces a WebTorrent-friendly URI. The logic mirrors the behaviour of
* the legacy magnetUtils helper but returns a plain string for ease of use.
*/
export function normalizeAndAugmentMagnet(rawValue, { ws = "", xs = "" } = {}) {
const initial = typeof rawValue === "string" ? rawValue.trim() : "";
if (!initial) {
return "";
}
let working = initial;
if (HEX_INFO_HASH.test(working)) {
working = `magnet:?xt=urn:btih:${working.toLowerCase()}`;
}
working = working.replace(ENCODED_BTih_PATTERN, (_, hash) => `xt=urn:btih:${hash}`);
if (!/^magnet:/i.test(working)) {
return working;
}
let fragment = "";
const hashIndex = working.indexOf("#");
if (hashIndex !== -1) {
fragment = working.slice(hashIndex);
working = working.slice(0, hashIndex);
}
const [, queryPart = ""] = working.split("?", 2);
const normalizedScheme = "magnet:";
const rawParams = queryPart
.split("&")
.map((part) => part.trim())
.filter(Boolean);
const params = [];
for (const rawParam of rawParams) {
const [rawKey, rawVal = ""] = rawParam.split("=", 2);
const key = rawKey.trim();
if (!key) {
continue;
}
let value = rawVal.trim();
if (key.toLowerCase() === "xt" && value) {
const decoded = decodeLoose(value);
if (decoded) {
value = decoded;
}
}
params.push({
key,
value,
lowerKey: key.toLowerCase(),
compareValue: normalizeForComparison(value),
});
}
const appendUniqueParam = (key, value) => {
if (!value) {
return;
}
const trimmedValue = value.trim();
if (!trimmedValue) {
return;
}
const lowerKey = key.toLowerCase();
const compareValue = normalizeForComparison(trimmedValue);
if (compareValue) {
const exists = params.some(
(param) => param.lowerKey === lowerKey && param.compareValue === compareValue
);
if (exists) {
return;
}
}
params.push({
key,
value: trimmedValue,
lowerKey,
compareValue,
});
};
for (const tracker of WSS_TRACKERS) {
if (typeof tracker !== "string") {
continue;
}
const trimmedTracker = tracker.trim();
if (!/^wss:\/\//i.test(trimmedTracker)) {
continue;
}
appendUniqueParam("tr", trimmedTracker);
}
const normalizedXs = typeof xs === "string" ? xs.trim() : "";
if (normalizedXs) {
appendUniqueParam("xs", normalizedXs);
}
const originProtocol = getOriginProtocol();
const rawWs = typeof ws === "string" ? ws.trim() : "";
if (rawWs) {
const formattedWs = tryFormatAbsoluteUrl(rawWs);
const lowerWs = formattedWs.toLowerCase();
const isHttpSeed = lowerWs.startsWith("http://");
const allowHttpSeed = originProtocol === "http:";
if (!isHttpSeed || allowHttpSeed) {
appendUniqueParam("ws", formattedWs);
}
}
const queryString = params
.map(({ key, value }) => (value ? `${key}=${value}` : key))
.join("&");
return `${normalizedScheme}${queryString ? `?${queryString}` : ""}${fragment}`;
}