Files
bitvid/js/magnet.js
T

225 lines
5.3 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}`;
}
export function extractMagnetHints(rawValue) {
const hints = { ws: "", xs: "" };
if (typeof rawValue !== "string") {
return hints;
}
const trimmed = rawValue.trim();
if (!trimmed || !trimmed.toLowerCase().startsWith("magnet:")) {
return hints;
}
const queryIndex = trimmed.indexOf("?");
if (queryIndex === -1 || queryIndex === trimmed.length - 1) {
return hints;
}
const query = trimmed.slice(queryIndex + 1);
if (!query) {
return hints;
}
const parts = query.split("&");
for (const part of parts) {
if (!part) {
continue;
}
const [rawKey, rawValue = ""] = part.split("=", 2);
if (!rawKey) {
continue;
}
const lowerKey = rawKey.trim().toLowerCase();
if (lowerKey !== "ws" && lowerKey !== "xs") {
continue;
}
if (hints[lowerKey]) {
continue;
}
let decoded = rawValue;
try {
decoded = decodeURIComponent(rawValue);
} catch (err) {
// Ignore decode errors and use the raw value.
}
const clean = typeof decoded === "string" ? decoded.trim() : "";
if (!clean) {
continue;
}
hints[lowerKey] = clean;
}
return hints;
}