From 50793430a8569bdeac2d74d9733e109580509c49 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Mon, 26 Jan 2026 21:49:18 -0500 Subject: [PATCH] Add btih extraction helper --- js/app.js | 8 ++-- js/magnetShared.js | 93 +++++++++++++++++++++++++++++++++++++++++++++- js/magnetUtils.js | 4 ++ js/magnets.js | 57 ++-------------------------- js/nostr/nip71.js | 8 ++-- 5 files changed, 107 insertions(+), 63 deletions(-) diff --git a/js/app.js b/js/app.js index bd65bacc..ca73f976 100644 --- a/js/app.js +++ b/js/app.js @@ -13,7 +13,7 @@ import { ALLOW_NSFW_CONTENT, } from "./config.js"; import { accessControl } from "./accessControl.js"; -import { safeDecodeMagnet } from "./magnetUtils.js"; +import { extractBtihFromMagnet, safeDecodeMagnet } from "./magnetUtils.js"; import { deriveTorrentPlaybackConfig } from "./playbackUtils.js"; import { URL_FIRST_ENABLED, @@ -8571,9 +8571,9 @@ class Application { typeof video.infoHash === "string" ? video.infoHash.trim().toLowerCase() : ""; const fallbackMagnetForCandidate = fallbackMagnetCandidate || ""; if (!legacyInfoHash && fallbackMagnetForCandidate) { - const match = fallbackMagnetForCandidate.match(/xt=urn:btih:([0-9a-z]+)/i); - if (match && match[1]) { - legacyInfoHash = match[1].toLowerCase(); + const extracted = extractBtihFromMagnet(fallbackMagnetForCandidate); + if (extracted) { + legacyInfoHash = extracted; } } diff --git a/js/magnetShared.js b/js/magnetShared.js index 2d347cf6..0d4be7ab 100644 --- a/js/magnetShared.js +++ b/js/magnetShared.js @@ -5,9 +5,11 @@ import { } from "./utils/safeDecode.js"; const HEX_INFO_HASH = /^[0-9a-f]{40}$/i; +const BASE32_INFO_HASH = /^[a-z2-7]{32}$/i; const BTIH_PREFIX = "urn:btih:"; const MAGNET_SCHEME = "magnet:"; const ENCODED_BTih_PATTERN = /xt=urn%3Abtih%3A([0-9a-z]+)/gi; +const BASE32_ALPHABET = "abcdefghijklmnopqrstuvwxyz234567"; export function safeDecodeMagnet(value) { if (typeof value !== "string") { @@ -56,6 +58,96 @@ export function normalizeForComparison(value) { } } +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) { + return ""; + } + + return bytes.map((byte) => byte.toString(16).padStart(2, "0")).join(""); +} + +export function normalizeInfoHash(value) { + const trimmed = typeof value === "string" ? value.trim() : ""; + if (!trimmed) { + return ""; + } + if (HEX_INFO_HASH.test(trimmed)) { + return trimmed.toLowerCase(); + } + if (BASE32_INFO_HASH.test(trimmed)) { + const hex = decodeBase32ToHex(trimmed); + return hex ? hex.toLowerCase() : ""; + } + return ""; +} + +function extractInfoHashFromXt(value) { + if (typeof value !== "string") { + return ""; + } + const trimmed = value.trim(); + if (!trimmed) { + return ""; + } + const lower = trimmed.toLowerCase(); + if (!lower.startsWith(BTIH_PREFIX)) { + return ""; + } + return normalizeInfoHash(trimmed.slice(BTIH_PREFIX.length)); +} + +export function extractBtihFromMagnet(rawValue) { + const decoded = safeDecodeMagnet(rawValue); + const { initial, isMagnet, params } = normalizeMagnetInput(decoded); + + if (!initial) { + return ""; + } + + if (!isMagnet) { + if (initial.toLowerCase().startsWith(BTIH_PREFIX)) { + return normalizeInfoHash(initial.slice(BTIH_PREFIX.length)); + } + return normalizeInfoHash(initial); + } + + for (const param of params) { + if (param.lowerKey !== "xt") { + continue; + } + const candidate = param.decoded || param.value; + const normalized = extractInfoHashFromXt(candidate); + if (normalized) { + return normalized; + } + } + + return ""; +} + function createParam(key, value) { const trimmedValue = typeof value === "string" ? value.trim() : ""; const decoded = safeDecodeURIComponentLoose(trimmedValue); @@ -342,4 +434,3 @@ export function extractMagnetHints(rawValue) { } return hints; } - diff --git a/js/magnetUtils.js b/js/magnetUtils.js index 6e547fe4..939b6fd6 100644 --- a/js/magnetUtils.js +++ b/js/magnetUtils.js @@ -6,13 +6,17 @@ import { ensureTrackers, ensureTorrentHint, ensureWebSeeds, + extractBtihFromMagnet as sharedExtractBtihFromMagnet, normalizeMagnetInput, + normalizeInfoHash as sharedNormalizeInfoHash, resolveAppProtocol, safeDecodeMagnet as sharedSafeDecodeMagnet, } from "./magnetShared.js"; export { WSS_TRACKERS }; export const safeDecodeMagnet = sharedSafeDecodeMagnet; +export const extractBtihFromMagnet = sharedExtractBtihFromMagnet; +export const normalizeInfoHash = sharedNormalizeInfoHash; export function normalizeAndAugmentMagnet( rawValue, diff --git a/js/magnets.js b/js/magnets.js index 7527750a..c280014d 100644 --- a/js/magnets.js +++ b/js/magnets.js @@ -1,35 +1,4 @@ -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(""); -} +import { extractBtihFromMagnet, normalizeInfoHash } from "./magnetShared.js"; function extractInfoHashFromXt(value) { if (typeof value !== "string") { @@ -50,32 +19,12 @@ function extractInfoHashFromXt(value) { 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; + const extracted = extractBtihFromMagnet(magnet); + return extracted || null; } export function trackersFromMagnet(magnet) { diff --git a/js/nostr/nip71.js b/js/nostr/nip71.js index 2d02c751..a20ef173 100644 --- a/js/nostr/nip71.js +++ b/js/nostr/nip71.js @@ -1,5 +1,5 @@ import { deriveTitleFromEvent } from "../videoEventUtils.js"; -import { extractMagnetHints } from "../magnetShared.js"; +import { extractBtihFromMagnet, extractMagnetHints } from "../magnetShared.js"; import { devLogger } from "../utils/logger.js"; import { getCachedNostrTools } from "./toolkit.js"; @@ -1432,9 +1432,9 @@ export function convertEventToVideo(event = {}) { const originalFileSha256 = normalizeSha256(parsedContent.originalFileSha256); if (!infoHash && magnet) { - const match = magnet.match(/xt=urn:btih:([0-9a-z]+)/i); - if (match && match[1]) { - pushInfoHash(match[1]); + const extracted = extractBtihFromMagnet(magnet); + if (extracted) { + infoHash = extracted; } }