From 940d9e66f717675443e4fba6903c1ab9a2e8de5a Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 24 Sep 2025 19:57:29 -0400 Subject: [PATCH 1/3] Restore legacy magnet playback in converter --- js/channelProfile.js | 90 ++--------------------- js/nostr.js | 128 +++++++++++++++++++++++---------- js/subscriptions.js | 89 ++--------------------- js/videoEventUtils.js | 2 +- tests/legacy-infohash.test.mjs | 18 +++++ 5 files changed, 120 insertions(+), 207 deletions(-) diff --git a/js/channelProfile.js b/js/channelProfile.js index 21309171..60d5df9b 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -1,15 +1,13 @@ // js/channelProfile.js -import { nostrClient } from "./nostr.js"; +import { + nostrClient, + convertEventToVideo as sharedConvertEventToVideo, +} from "./nostr.js"; import { app } from "./app.js"; import { subscriptions } from "./subscriptions.js"; // <-- NEW import import { initialBlacklist, initialWhitelist } from "./lists.js"; import { isWhitelistEnabled } from "./config.js"; -import { - deriveTitleFromEvent, - parseVideoEventPayload, - findLegacyMagnetInEvent, -} from "./videoEventUtils.js"; /** * Initialize the channel profile view. @@ -204,7 +202,7 @@ async function loadUserVideos(pubkey) { // 3) Convert to "video" objects let videos = []; for (const evt of events) { - const vid = localConvertEventToVideo(evt); + const vid = sharedConvertEventToVideo(evt); if (!vid.invalid && !vid.deleted) { videos.push(vid); } @@ -479,84 +477,6 @@ function dedupeToNewestByRoot(videos) { return Array.from(map.values()); } -/** - * Convert raw event => "video" object. - */ -function localConvertEventToVideo(event) { - const { - parsedContent, - parseError, - title, - url, - magnet, - infoHash, - version, - } = parseVideoEventPayload(event); - - const trimmedUrl = typeof url === "string" ? url.trim() : ""; - const trimmedMagnet = typeof magnet === "string" ? magnet.trim() : ""; - const legacyMagnet = findLegacyMagnetInEvent(event); - const trimmedLegacyMagnet = - typeof legacyMagnet === "string" ? legacyMagnet.trim() : ""; - const trimmedInfoHash = typeof infoHash === "string" ? infoHash.trim() : ""; - const playbackMagnet = - trimmedMagnet || trimmedLegacyMagnet || trimmedInfoHash; - const parsedVersion = - typeof version === "number" - ? version - : typeof version === "string" - ? Number.parseInt(version, 10) - : Number.NaN; - const numericVersion = Number.isFinite(parsedVersion) - ? parsedVersion - : 0; - - const hasPlayableSource = Boolean(trimmedUrl) || Boolean(playbackMagnet); - if (!hasPlayableSource) { - return { id: event.id, invalid: true, reason: "missing playable source" }; - } - - const derivedTitle = deriveTitleFromEvent({ - parsedContent, - tags: event.tags, - primaryTitle: title, - }); - - let resolvedTitle = derivedTitle; - if (!resolvedTitle && numericVersion < 2 && playbackMagnet) { - resolvedTitle = trimmedInfoHash - ? `Legacy Video ${trimmedInfoHash.slice(0, 8)}` - : "Legacy BitTorrent Video"; - } - - if (!resolvedTitle) { - const reason = parseError - ? "missing title (json parse error)" - : "missing title"; - return { id: event.id, invalid: true, reason }; - } - - return { - id: event.id, - videoRootId: parsedContent.videoRootId || event.id, - version: numericVersion, - isPrivate: parsedContent.isPrivate ?? false, - title: resolvedTitle, - url: trimmedUrl, - magnet: playbackMagnet, - rawMagnet: trimmedMagnet || trimmedLegacyMagnet, - infoHash: trimmedInfoHash, - thumbnail: parsedContent.thumbnail ?? "", - description: parsedContent.description ?? "", - mode: parsedContent.mode ?? "live", - deleted: parsedContent.deleted === true, - pubkey: event.pubkey, - created_at: event.created_at, - tags: event.tags, - invalid: false, - }; -} - /** * Basic escaping to avoid XSS. */ diff --git a/js/nostr.js b/js/nostr.js index e043cff3..75d557f3 100644 --- a/js/nostr.js +++ b/js/nostr.js @@ -1,11 +1,12 @@ // js/nostr.js import { isDevMode } from "./config.js"; +import { ACCEPT_LEGACY_V1 } from "./constants.js"; import { accessControl } from "./accessControl.js"; import { deriveTitleFromEvent, + magnetFromText, parseVideoEventPayload, - findLegacyMagnetInEvent, } from "./videoEventUtils.js"; /** @@ -91,32 +92,82 @@ function inferMimeTypeFromUrl(url) { } /** - * Convert a raw Nostr event => your "video" object. - * Accepts legacy (<2) payloads when they expose a usable magnet/info hash. + * Convert a raw Nostr event into Bitvid's canonical "video" object. + * + * The converter intentionally centralises all of the quirky legacy handling so + * that feed rendering, subscriptions, and deep links rely on the exact same + * rules. Any future regression around magnet-only posts or malformed JSON + * should be solved by updating this function (and its tests) instead of + * sprinkling ad-hoc checks elsewhere in the UI. */ -function convertEventToVideo(event) { +function convertEventToVideo(event = {}) { + const tags = Array.isArray(event.tags) ? event.tags : []; + const { parsedContent, parseError, - title, - url, - magnet, + title: declaredTitle, + url: parsedUrl, + magnet: parsedMagnet, infoHash, version, } = parseVideoEventPayload(event); - const trimmedUrl = typeof url === "string" ? url.trim() : ""; - const trimmedMagnet = typeof magnet === "string" ? magnet.trim() : ""; - const legacyMagnet = findLegacyMagnetInEvent(event); - const trimmedLegacyMagnet = - typeof legacyMagnet === "string" ? legacyMagnet.trim() : ""; - const trimmedInfoHash = typeof infoHash === "string" ? infoHash.trim() : ""; - const playbackMagnet = - trimmedMagnet || trimmedLegacyMagnet || trimmedInfoHash; + const safeTrim = (value) => (typeof value === "string" ? value.trim() : ""); + const normalizeOptional = (value) => safeTrim(value) || ""; const numericVersion = Number.isFinite(version) ? version : 0; - const hasPlayableSource = Boolean(trimmedUrl) || Boolean(playbackMagnet); + const normalizedUrl = safeTrim(parsedUrl); + const directMagnet = safeTrim(parsedMagnet); + const normalizedInfoHash = safeTrim(infoHash).toLowerCase(); + // Legacy Bitvid posts (version < 2) sometimes stored the magnet in free-form + // JSON strings or tags. We keep that scavenging behind ACCEPT_LEGACY_V1 so it + // can be disabled globally without touching each call-site. + let legacyMagnet = ""; + if (!directMagnet && ACCEPT_LEGACY_V1) { + const fromContent = magnetFromText(event.content); + if (fromContent) { + legacyMagnet = safeTrim(fromContent); + } + + if (!legacyMagnet) { + for (const tag of tags) { + if (!Array.isArray(tag)) { + continue; + } + + // Specified magnet tags always win. + if (safeTrim(tag[0]).toLowerCase() === "magnet" && tag[1]) { + const tagValue = magnetFromText(tag[1]) || tag[1]; + const trimmed = safeTrim(tagValue); + if (trimmed) { + legacyMagnet = trimmed; + break; + } + } + + // Fall back to scanning the remaining tag values for inline magnets. + for (let i = 1; i < tag.length && !legacyMagnet; i += 1) { + const candidate = magnetFromText(tag[i]); + if (candidate) { + legacyMagnet = safeTrim(candidate); + } + } + + if (legacyMagnet) { + break; + } + } + } + } + + const magnetCandidate = directMagnet || legacyMagnet; + const magnetForPlayback = magnetCandidate + || (ACCEPT_LEGACY_V1 ? normalizedInfoHash : ""); + + const hasPlayableSource = Boolean(normalizedUrl) + || Boolean(magnetForPlayback); if (!hasPlayableSource) { return { id: event.id, @@ -125,20 +176,25 @@ function convertEventToVideo(event) { }; } - const derivedTitle = deriveTitleFromEvent({ + const resolvedTitle = deriveTitleFromEvent({ parsedContent, - tags: event.tags, - primaryTitle: title, + tags, + primaryTitle: declaredTitle, }); - let resolvedTitle = derivedTitle; - if (!resolvedTitle && numericVersion < 2 && playbackMagnet) { - resolvedTitle = trimmedInfoHash - ? `Legacy Video ${trimmedInfoHash.slice(0, 8)}` + let finalTitle = resolvedTitle; + if ( + !finalTitle && + ACCEPT_LEGACY_V1 && + numericVersion < 2 && + magnetForPlayback + ) { + finalTitle = normalizedInfoHash + ? `Legacy Video ${normalizedInfoHash.slice(0, 8)}` : "Legacy BitTorrent Video"; } - if (!resolvedTitle) { + if (!finalTitle) { const reason = parseError ? "missing title (json parse error)" : "missing title"; @@ -147,21 +203,21 @@ function convertEventToVideo(event) { return { id: event.id, - videoRootId: parsedContent.videoRootId || event.id, + videoRootId: normalizeOptional(parsedContent.videoRootId) || event.id, version: numericVersion, isPrivate: parsedContent.isPrivate ?? false, - title: resolvedTitle, - url: trimmedUrl, - magnet: playbackMagnet, - rawMagnet: trimmedMagnet || trimmedLegacyMagnet, - infoHash: trimmedInfoHash, - thumbnail: parsedContent.thumbnail ?? "", - description: parsedContent.description ?? "", - mode: parsedContent.mode ?? "live", + title: finalTitle, + url: normalizedUrl, + magnet: magnetForPlayback, + rawMagnet: directMagnet || (ACCEPT_LEGACY_V1 ? legacyMagnet : ""), + infoHash: normalizedInfoHash, + thumbnail: normalizeOptional(parsedContent.thumbnail), + description: normalizeOptional(parsedContent.description), + mode: normalizeOptional(parsedContent.mode) || "live", deleted: parsedContent.deleted === true, pubkey: event.pubkey, created_at: event.created_at, - tags: event.tags, + tags, invalid: false, }; } @@ -332,14 +388,14 @@ class NostrClient { const contentObject = { videoRootId, version: 2, // forcibly set version=2 - deleted: false, - isPrivate: videoData.isPrivate ?? false, title: finalTitle, url: finalUrl, magnet: finalMagnet, thumbnail: finalThumbnail, description: finalDescription, mode: videoData.mode || "live", + deleted: false, + isPrivate: videoData.isPrivate ?? false, }; const event = { diff --git a/js/subscriptions.js b/js/subscriptions.js index 4b80ebe6..fff42045 100644 --- a/js/subscriptions.js +++ b/js/subscriptions.js @@ -1,10 +1,8 @@ // js/subscriptions.js -import { nostrClient } from "./nostr.js"; import { - deriveTitleFromEvent, - parseVideoEventPayload, - findLegacyMagnetInEvent, -} from "./videoEventUtils.js"; + nostrClient, + convertEventToVideo as sharedConvertEventToVideo, +} from "./nostr.js"; /** * Manages the user's subscription list (kind=30002) *privately*, @@ -521,86 +519,7 @@ class SubscriptionsManager { } convertEventToVideo(evt) { - const { - parsedContent, - parseError, - title, - url, - magnet, - infoHash, - version, - } = parseVideoEventPayload(evt); - - const trimmedUrl = typeof url === "string" ? url.trim() : ""; - const trimmedMagnet = typeof magnet === "string" ? magnet.trim() : ""; - const legacyMagnet = findLegacyMagnetInEvent(evt); - const trimmedLegacyMagnet = - typeof legacyMagnet === "string" ? legacyMagnet.trim() : ""; - const trimmedInfoHash = - typeof infoHash === "string" ? infoHash.trim() : ""; - const playbackMagnet = - trimmedMagnet || trimmedLegacyMagnet || trimmedInfoHash; - const parsedVersion = - typeof version === "number" - ? version - : typeof version === "string" - ? Number.parseInt(version, 10) - : Number.NaN; - const numericVersion = Number.isFinite(parsedVersion) - ? parsedVersion - : 0; - - const hasPlayableSource = Boolean(trimmedUrl) || Boolean(playbackMagnet); - if (!hasPlayableSource) { - return { - id: evt.id, - invalid: true, - reason: "missing playable source", - }; - } - - const derivedTitle = deriveTitleFromEvent({ - parsedContent, - tags: evt.tags, - primaryTitle: title, - }); - - let resolvedTitle = derivedTitle; - if (!resolvedTitle && numericVersion < 2 && playbackMagnet) { - resolvedTitle = trimmedInfoHash - ? `Legacy Video ${trimmedInfoHash.slice(0, 8)}` - : "Legacy BitTorrent Video"; - } - - if (!resolvedTitle) { - return { - id: evt.id, - invalid: true, - reason: parseError - ? "missing title (json parse error)" - : "missing title", - }; - } - - return { - id: evt.id, - pubkey: evt.pubkey, - created_at: evt.created_at, - videoRootId: parsedContent.videoRootId || evt.id, - version: numericVersion, - deleted: parsedContent.deleted === true, - isPrivate: parsedContent.isPrivate ?? false, - title: resolvedTitle, - url: trimmedUrl, - magnet: playbackMagnet, - rawMagnet: trimmedMagnet || trimmedLegacyMagnet, - infoHash: trimmedInfoHash, - thumbnail: parsedContent.thumbnail ?? "", - description: parsedContent.description ?? "", - mode: parsedContent.mode ?? "live", - tags: evt.tags || [], - invalid: false, - }; + return sharedConvertEventToVideo(evt); } dedupeToNewestByRoot(videos) { diff --git a/js/videoEventUtils.js b/js/videoEventUtils.js index bfca5107..acf48f63 100644 --- a/js/videoEventUtils.js +++ b/js/videoEventUtils.js @@ -255,7 +255,7 @@ export function parseVideoEventPayload(event = {}) { }; } -function magnetFromText(value) { +export function magnetFromText(value) { if (typeof value !== "string") { return ""; } diff --git a/tests/legacy-infohash.test.mjs b/tests/legacy-infohash.test.mjs index d2ac0ff8..1a1f7dce 100644 --- a/tests/legacy-infohash.test.mjs +++ b/tests/legacy-infohash.test.mjs @@ -81,6 +81,24 @@ const LEGACY_INFO_HASH = "0123456789abcdef0123456789abcdef01234567"; assert.ok(video.title && video.title.length > 0, "Fallback title should be provided"); })(); +(function testConvertRecoversMagnetFromMagnetTag() { + const magnetUri = + `magnet:?xt=urn:btih:${LEGACY_INFO_HASH}&dn=legacy-tag&tr=wss://tracker.openwebtorrent.com`; + const event = { + id: "evt-magnet-tag", + pubkey: "pk3", + created_at: 3, + // Deliberately invalid JSON so the converter must rely on scavenging. + content: "{not valid json}", + tags: [["magnet", magnetUri]], + }; + + const video = convertEventToVideo(event); + assert.equal(video.invalid, false, "Magnet in tag should still be playable"); + assert.equal(video.magnet, magnetUri); + assert.equal(video.rawMagnet, magnetUri); +})(); + (function testPlaybackConfigNormalizesInfoHash() { const result = deriveTorrentPlaybackConfig({ magnet: "", From b52076cc26f03f3874dffc253e5b973afb99c086 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 24 Sep 2025 20:01:49 -0400 Subject: [PATCH 2/3] Refine legacy magnet scavenging after merge --- js/nostr.js | 50 ++++++++++---------------------------------------- 1 file changed, 10 insertions(+), 40 deletions(-) diff --git a/js/nostr.js b/js/nostr.js index 75d557f3..44eaee77 100644 --- a/js/nostr.js +++ b/js/nostr.js @@ -5,7 +5,7 @@ import { ACCEPT_LEGACY_V1 } from "./constants.js"; import { accessControl } from "./accessControl.js"; import { deriveTitleFromEvent, - magnetFromText, + findLegacyMagnetInEvent, parseVideoEventPayload, } from "./videoEventUtils.js"; @@ -124,47 +124,17 @@ function convertEventToVideo(event = {}) { // Legacy Bitvid posts (version < 2) sometimes stored the magnet in free-form // JSON strings or tags. We keep that scavenging behind ACCEPT_LEGACY_V1 so it // can be disabled globally without touching each call-site. - let legacyMagnet = ""; - if (!directMagnet && ACCEPT_LEGACY_V1) { - const fromContent = magnetFromText(event.content); - if (fromContent) { - legacyMagnet = safeTrim(fromContent); - } - - if (!legacyMagnet) { - for (const tag of tags) { - if (!Array.isArray(tag)) { - continue; - } - - // Specified magnet tags always win. - if (safeTrim(tag[0]).toLowerCase() === "magnet" && tag[1]) { - const tagValue = magnetFromText(tag[1]) || tag[1]; - const trimmed = safeTrim(tagValue); - if (trimmed) { - legacyMagnet = trimmed; - break; - } - } - - // Fall back to scanning the remaining tag values for inline magnets. - for (let i = 1; i < tag.length && !legacyMagnet; i += 1) { - const candidate = magnetFromText(tag[i]); - if (candidate) { - legacyMagnet = safeTrim(candidate); - } - } - - if (legacyMagnet) { - break; - } - } - } - } + const legacyMagnet = + !directMagnet && ACCEPT_LEGACY_V1 + ? safeTrim(findLegacyMagnetInEvent(event)) + : ""; + // Prefer the structured magnet from the payload; otherwise accept the + // scavenged legacy value. As a last resort we promote the recovered info hash + // so downstream callers can rebuild a usable magnet URI during playback. const magnetCandidate = directMagnet || legacyMagnet; - const magnetForPlayback = magnetCandidate - || (ACCEPT_LEGACY_V1 ? normalizedInfoHash : ""); + const magnetForPlayback = + magnetCandidate || (ACCEPT_LEGACY_V1 ? normalizedInfoHash : ""); const hasPlayableSource = Boolean(normalizedUrl) || Boolean(magnetForPlayback); From 996647f23d1981bd4cc764f98461b1f93fee4327 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 24 Sep 2025 20:11:40 -0400 Subject: [PATCH 3/3] Restore legacy magnet playback in converter --- js/nostr.js | 127 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 93 insertions(+), 34 deletions(-) diff --git a/js/nostr.js b/js/nostr.js index 44eaee77..7b8312f8 100644 --- a/js/nostr.js +++ b/js/nostr.js @@ -5,7 +5,7 @@ import { ACCEPT_LEGACY_V1 } from "./constants.js"; import { accessControl } from "./accessControl.js"; import { deriveTitleFromEvent, - findLegacyMagnetInEvent, + magnetFromText, parseVideoEventPayload, } from "./videoEventUtils.js"; @@ -101,6 +101,10 @@ function inferMimeTypeFromUrl(url) { * sprinkling ad-hoc checks elsewhere in the UI. */ function convertEventToVideo(event = {}) { + const safeTrim = (value) => (typeof value === "string" ? value.trim() : ""); + const normalizeOptional = (value) => safeTrim(value) || ""; + + const rawContent = typeof event.content === "string" ? event.content : ""; const tags = Array.isArray(event.tags) ? event.tags : []; const { @@ -113,32 +117,60 @@ function convertEventToVideo(event = {}) { version, } = parseVideoEventPayload(event); - const safeTrim = (value) => (typeof value === "string" ? value.trim() : ""); - const normalizeOptional = (value) => safeTrim(value) || ""; - - const numericVersion = Number.isFinite(version) ? version : 0; const normalizedUrl = safeTrim(parsedUrl); const directMagnet = safeTrim(parsedMagnet); - const normalizedInfoHash = safeTrim(infoHash).toLowerCase(); + const parsedInfoHash = safeTrim(infoHash).toLowerCase(); - // Legacy Bitvid posts (version < 2) sometimes stored the magnet in free-form - // JSON strings or tags. We keep that scavenging behind ACCEPT_LEGACY_V1 so it - // can be disabled globally without touching each call-site. - const legacyMagnet = - !directMagnet && ACCEPT_LEGACY_V1 - ? safeTrim(findLegacyMagnetInEvent(event)) - : ""; + let recoveredMagnet = ""; + if (!directMagnet && ACCEPT_LEGACY_V1) { + // Legacy v1 payloads frequently stuffed the magnet into arbitrary JSON + // strings. Inspect the raw content before JSON parsing so malformed + // payloads (or intentionally non-JSON legacy notes) still surface a magnet. + const inlineMagnet = magnetFromText(rawContent); + if (inlineMagnet) { + recoveredMagnet = safeTrim(inlineMagnet); + } - // Prefer the structured magnet from the payload; otherwise accept the - // scavenged legacy value. As a last resort we promote the recovered info hash - // so downstream callers can rebuild a usable magnet URI during playback. - const magnetCandidate = directMagnet || legacyMagnet; - const magnetForPlayback = - magnetCandidate || (ACCEPT_LEGACY_V1 ? normalizedInfoHash : ""); + if (!recoveredMagnet) { + // Older clients also placed magnets into tags, sometimes under a literal + // ["magnet", ...] tuple and other times hidden in auxiliary values. The + // scavenger walks each tag to retain those posts while keeping the logic + // behind ACCEPT_LEGACY_V1 for easy rollback. + for (const tag of tags) { + if (!Array.isArray(tag) || tag.length === 0) { + continue; + } - const hasPlayableSource = Boolean(normalizedUrl) - || Boolean(magnetForPlayback); - if (!hasPlayableSource) { + const key = typeof tag[0] === "string" ? tag[0].trim().toLowerCase() : ""; + if (key === "magnet" && typeof tag[1] === "string") { + const tagMagnet = magnetFromText(tag[1]) || tag[1]; + const trimmed = safeTrim(tagMagnet); + if (trimmed) { + recoveredMagnet = trimmed; + break; + } + } + + for (let i = 1; i < tag.length && !recoveredMagnet; i += 1) { + const candidate = magnetFromText(tag[i]); + if (candidate) { + recoveredMagnet = safeTrim(candidate); + } + } + + if (recoveredMagnet) { + break; + } + } + } + } + + const magnetCandidate = directMagnet || recoveredMagnet; + const normalizedMagnet = safeTrim(magnetCandidate); + const fallbackInfoHash = ACCEPT_LEGACY_V1 ? parsedInfoHash : ""; + const magnetForPlayback = normalizedMagnet || fallbackInfoHash; + + if (!normalizedUrl && !magnetForPlayback) { return { id: event.id, invalid: true, @@ -146,21 +178,38 @@ function convertEventToVideo(event = {}) { }; } - const resolvedTitle = deriveTitleFromEvent({ + const deriveInfoHash = () => { + // Prefer the structured info hash found during payload parsing so the UI + // can surface it directly and playbackUtils can promote it to a magnet. + if (parsedInfoHash) { + return parsedInfoHash; + } + const source = normalizedMagnet || ""; + if (!source) { + return ""; + } + // When only a magnet string exists we still try to recover the info hash + // because downstream helpers display a friendlier fallback title and can + // rebuild a magnet URI if the original string gets mangled later on. + const match = source.match(/xt=urn:btih:([0-9a-z]+)/i); + if (match && match[1]) { + return match[1].toLowerCase(); + } + return ""; + }; + + const resolvedInfoHash = deriveInfoHash(); + + const derivedTitle = deriveTitleFromEvent({ parsedContent, tags, primaryTitle: declaredTitle, }); - let finalTitle = resolvedTitle; - if ( - !finalTitle && - ACCEPT_LEGACY_V1 && - numericVersion < 2 && - magnetForPlayback - ) { - finalTitle = normalizedInfoHash - ? `Legacy Video ${normalizedInfoHash.slice(0, 8)}` + let finalTitle = derivedTitle; + if (!finalTitle && ACCEPT_LEGACY_V1 && magnetForPlayback) { + finalTitle = resolvedInfoHash + ? `Legacy Video ${resolvedInfoHash.slice(0, 8)}` : "Legacy BitTorrent Video"; } @@ -171,6 +220,16 @@ function convertEventToVideo(event = {}) { return { id: event.id, invalid: true, reason }; } + let numericVersion = 0; + if (typeof version === "number" && Number.isFinite(version)) { + numericVersion = version; + } else if (typeof version === "string") { + const parsedVersion = Number(version); + if (Number.isFinite(parsedVersion)) { + numericVersion = parsedVersion; + } + } + return { id: event.id, videoRootId: normalizeOptional(parsedContent.videoRootId) || event.id, @@ -179,8 +238,8 @@ function convertEventToVideo(event = {}) { title: finalTitle, url: normalizedUrl, magnet: magnetForPlayback, - rawMagnet: directMagnet || (ACCEPT_LEGACY_V1 ? legacyMagnet : ""), - infoHash: normalizedInfoHash, + rawMagnet: directMagnet || (ACCEPT_LEGACY_V1 ? recoveredMagnet : ""), + infoHash: resolvedInfoHash, thumbnail: normalizeOptional(parsedContent.thumbnail), description: normalizeOptional(parsedContent.description), mode: normalizeOptional(parsedContent.mode) || "live",