From d271bd335cb05ff0475f1c536cc41fd3bdbcc346 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 23 Sep 2025 13:52:33 -0400 Subject: [PATCH] Clarify legacy handling comment --- js/channelProfile.js | 58 +++++++++++++++++++++++------ js/magnetUtils.js | 11 +++++- js/nostr.js | 44 ++++++++++++++++------ js/subscriptions.js | 64 ++++++++++++++++++++++++-------- js/videoEventUtils.js | 67 ++++++++++++++++++++++++++++++++++ tests/legacy-infohash.test.mjs | 17 +++++++++ 6 files changed, 222 insertions(+), 39 deletions(-) diff --git a/js/channelProfile.js b/js/channelProfile.js index c17a65fa..02d61019 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -5,7 +5,10 @@ import { app } from "./app.js"; import { subscriptions } from "./subscriptions.js"; // <-- NEW import import { initialBlacklist, initialWhitelist } from "./lists.js"; import { isWhitelistEnabled } from "./config.js"; -import { parseVideoEventPayload } from "./videoEventUtils.js"; +import { + deriveTitleFromEvent, + parseVideoEventPayload, +} from "./videoEventUtils.js"; /** * Initialize the channel profile view. @@ -473,26 +476,57 @@ function dedupeToNewestByRoot(videos) { * Convert raw event => "video" object. */ function localConvertEventToVideo(event) { - const { parsedContent, parseError, title, url, magnet, version } = - parseVideoEventPayload(event); + const { + parsedContent, + parseError, + title, + url, + magnet, + infoHash, + version, + } = parseVideoEventPayload(event); - if (!title) { - const reason = parseError ? "missing title (json parse error)" : "missing title"; - return { id: event.id, invalid: true, reason }; + const trimmedUrl = typeof url === "string" ? url.trim() : ""; + const trimmedMagnet = typeof magnet === "string" ? magnet.trim() : ""; + const trimmedInfoHash = typeof infoHash === "string" ? infoHash.trim() : ""; + const playbackMagnet = trimmedMagnet || trimmedInfoHash; + const numericVersion = Number.isFinite(version) ? version : 0; + + const hasPlayableSource = Boolean(trimmedUrl) || Boolean(playbackMagnet); + if (!hasPlayableSource) { + return { id: event.id, invalid: true, reason: "missing playable source" }; } - if (!url && !magnet) { - 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, + version: numericVersion, isPrivate: parsedContent.isPrivate ?? false, - title, - url, - magnet, + title: resolvedTitle, + url: trimmedUrl, + magnet: playbackMagnet, + rawMagnet: trimmedMagnet, + infoHash: trimmedInfoHash, thumbnail: parsedContent.thumbnail ?? "", description: parsedContent.description ?? "", mode: parsedContent.mode ?? "live", diff --git a/js/magnetUtils.js b/js/magnetUtils.js index 25908f49..4cf206a3 100644 --- a/js/magnetUtils.js +++ b/js/magnetUtils.js @@ -205,7 +205,16 @@ export function normalizeAndAugmentMagnet( } } - const finalMagnet = parsed.toString(); + let finalMagnet = parsed.toString(); + const decodedFinalMagnet = finalMagnet.replace( + ENCODED_BTih_PATTERN, + (_, hash) => `xt=${BTIH_PREFIX}${hash}` + ); + if (decodedFinalMagnet !== finalMagnet) { + finalMagnet = decodedFinalMagnet; + didMutate = true; + } + return { magnet: finalMagnet, didChange: didMutate || finalMagnet !== initial, diff --git a/js/nostr.js b/js/nostr.js index c40c6007..06c65763 100644 --- a/js/nostr.js +++ b/js/nostr.js @@ -2,7 +2,10 @@ import { isDevMode } from "./config.js"; import { accessControl } from "./accessControl.js"; -import { parseVideoEventPayload } from "./videoEventUtils.js"; +import { + deriveTitleFromEvent, + parseVideoEventPayload, +} from "./videoEventUtils.js"; /** * The usual relays @@ -88,7 +91,7 @@ function inferMimeTypeFromUrl(url) { /** * Convert a raw Nostr event => your "video" object. - * CHANGED: skip if version <2 + * Accepts legacy (<2) payloads when they expose a usable magnet/info hash. */ function convertEventToVideo(event) { const { @@ -101,16 +104,15 @@ function convertEventToVideo(event) { version, } = parseVideoEventPayload(event); - if (!title) { - const reason = parseError ? "missing title (json parse error)" : "missing title"; - return { id: event.id, invalid: true, reason }; - } - + const trimmedUrl = typeof url === "string" ? url.trim() : ""; const trimmedMagnet = typeof magnet === "string" ? magnet.trim() : ""; const trimmedInfoHash = typeof infoHash === "string" ? infoHash.trim() : ""; const playbackMagnet = trimmedMagnet || trimmedInfoHash; - if (!url && !playbackMagnet) { + const numericVersion = Number.isFinite(version) ? version : 0; + const hasPlayableSource = Boolean(trimmedUrl) || Boolean(playbackMagnet); + + if (!hasPlayableSource) { return { id: event.id, invalid: true, @@ -118,13 +120,33 @@ function convertEventToVideo(event) { }; } + 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, + version: numericVersion, isPrivate: parsedContent.isPrivate ?? false, - title, - url, + title: resolvedTitle, + url: trimmedUrl, magnet: playbackMagnet, rawMagnet: trimmedMagnet, infoHash: trimmedInfoHash, diff --git a/js/subscriptions.js b/js/subscriptions.js index 29386149..b49e7bf8 100644 --- a/js/subscriptions.js +++ b/js/subscriptions.js @@ -1,6 +1,9 @@ // js/subscriptions.js import { nostrClient } from "./nostr.js"; -import { parseVideoEventPayload } from "./videoEventUtils.js"; +import { + deriveTitleFromEvent, + parseVideoEventPayload, +} from "./videoEventUtils.js"; /** * Manages the user's subscription list (kind=30002) *privately*, @@ -501,18 +504,24 @@ class SubscriptionsManager { } convertEventToVideo(evt) { - const { parsedContent, parseError, title, url, magnet, version } = - parseVideoEventPayload(evt); + const { + parsedContent, + parseError, + title, + url, + magnet, + infoHash, + version, + } = parseVideoEventPayload(evt); - if (!title) { - return { - id: evt.id, - invalid: true, - reason: parseError ? "missing title (json parse error)" : "missing title", - }; - } + const trimmedUrl = typeof url === "string" ? url.trim() : ""; + const trimmedMagnet = typeof magnet === "string" ? magnet.trim() : ""; + const trimmedInfoHash = typeof infoHash === "string" ? infoHash.trim() : ""; + const playbackMagnet = trimmedMagnet || trimmedInfoHash; + const numericVersion = Number.isFinite(version) ? version : 0; - if (!url && !magnet) { + const hasPlayableSource = Boolean(trimmedUrl) || Boolean(playbackMagnet); + if (!hasPlayableSource) { return { id: evt.id, invalid: true, @@ -520,17 +529,42 @@ class SubscriptionsManager { }; } + 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, + version: numericVersion, deleted: parsedContent.deleted === true, isPrivate: parsedContent.isPrivate ?? false, - title, - url, - magnet, + title: resolvedTitle, + url: trimmedUrl, + magnet: playbackMagnet, + rawMagnet: trimmedMagnet, + infoHash: trimmedInfoHash, thumbnail: parsedContent.thumbnail ?? "", description: parsedContent.description ?? "", mode: parsedContent.mode ?? "live", diff --git a/js/videoEventUtils.js b/js/videoEventUtils.js index 71fcd74f..4f835f7d 100644 --- a/js/videoEventUtils.js +++ b/js/videoEventUtils.js @@ -7,6 +7,73 @@ const MAGNET_URI_PATTERN = /^magnet:\?/i; const HEX_INFO_HASH_PATTERN = /\b[0-9a-f]{40}\b/gi; +function safeTrim(value) { + return typeof value === "string" ? value.trim() : ""; +} + +function pickFirstString(values = []) { + if (!Array.isArray(values)) { + return ""; + } + + for (const value of values) { + const trimmed = safeTrim(value); + if (trimmed) { + return trimmed; + } + } + + return ""; +} + +export function deriveTitleFromEvent({ + parsedContent = {}, + tags = [], + primaryTitle = "", +} = {}) { + const initial = safeTrim(primaryTitle); + if (initial) { + return initial; + } + + const directCandidates = pickFirstString([ + parsedContent?.title, + parsedContent?.name, + parsedContent?.filename, + parsedContent?.fileName, + parsedContent?.subject, + parsedContent?.caption, + ]); + if (directCandidates) { + return directCandidates; + } + + const metaTitle = safeTrim(parsedContent?.meta?.title); + if (metaTitle) { + return metaTitle; + } + + if (Array.isArray(tags)) { + for (const tag of tags) { + if (!Array.isArray(tag) || tag.length < 2) { + continue; + } + const key = safeTrim(tag[0]).toLowerCase(); + if (!key) { + continue; + } + if (["title", "subject", "caption", "name"].includes(key)) { + const candidate = safeTrim(tag[1]); + if (candidate) { + return candidate; + } + } + } + } + + return ""; +} + function extractInfoHashesFromString(value, pushInfoHash) { if (typeof value !== "string") { return; diff --git a/tests/legacy-infohash.test.mjs b/tests/legacy-infohash.test.mjs index 0aa76ac6..add5a474 100644 --- a/tests/legacy-infohash.test.mjs +++ b/tests/legacy-infohash.test.mjs @@ -64,6 +64,23 @@ const LEGACY_INFO_HASH = "0123456789abcdef0123456789abcdef01234567"; assert.equal(video.rawMagnet, ""); })(); +(function testLegacyEventWithoutTitleStillLoads() { + const event = { + id: "evt-no-title", + pubkey: "pk2", + created_at: 2, + tags: [], + content: JSON.stringify({ + version: 1, + magnet: `magnet:?xt=urn:btih:${LEGACY_INFO_HASH}`, + }), + }; + + const video = convertEventToVideo(event); + assert.equal(video.invalid, false, "Legacy events without title should fallback"); + assert.ok(video.title && video.title.length > 0, "Fallback title should be provided"); +})(); + (function testPlaybackConfigNormalizesInfoHash() { const result = deriveTorrentPlaybackConfig({ magnet: "",