Restore legacy magnet playback in converter

This commit is contained in:
thePR0M3TH3AN committed 2025-09-24 19:57:29 -04:00
1 parent 82bd833594
commit 940d9e66f7
5 files changed
+120 -207

No files matched your search

+5 -85
View File
@@ -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.
*/
+92 -36
View File
@@ -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 = {
+4 -85
View File
@@ -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) {
+1 -1
View File
@@ -255,7 +255,7 @@ export function parseVideoEventPayload(event = {}) {
};
}
function magnetFromText(value) {
export function magnetFromText(value) {
if (typeof value !== "string") {
return "";
}
+18
View File
@@ -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: "",