Support legacy Nostr video payloads

This commit is contained in:
thePR0M3TH3AN committed 2025-09-23 10:30:21 -04:00
1 parent a84f635539
commit 0569755901
4 files changed
+190 -97

No files matched your search

+28 -32
View File
@@ -5,6 +5,7 @@ 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";
/**
* Initialize the channel profile view.
@@ -467,40 +468,35 @@ function dedupeToNewestByRoot(videos) {
* Convert raw event => "video" object.
*/
function localConvertEventToVideo(event) {
try {
const content = JSON.parse(event.content || "{}");
const isSupportedVersion = content.version >= 2;
const hasRequiredFields = !!(
content.title && (content.url || content.magnet)
);
const { parsedContent, parseError, title, url, magnet, version } =
parseVideoEventPayload(event);
if (!isSupportedVersion) {
return { id: event.id, invalid: true, reason: "version <2" };
}
if (!hasRequiredFields) {
return { id: event.id, invalid: true, reason: "missing title/url+magnet" };
}
return {
id: event.id,
videoRootId: content.videoRootId || event.id,
version: content.version,
isPrivate: content.isPrivate ?? false,
title: content.title ?? "",
url: content.url ?? "",
magnet: content.magnet ?? "",
thumbnail: content.thumbnail ?? "",
description: content.description ?? "",
mode: content.mode ?? "live",
deleted: content.deleted === true,
pubkey: event.pubkey,
created_at: event.created_at,
tags: event.tags,
invalid: false,
};
} catch (err) {
return { id: event.id, invalid: true, reason: "json parse error" };
if (!title) {
const reason = parseError ? "missing title (json parse error)" : "missing title";
return { id: event.id, invalid: true, reason };
}
if (!url && !magnet) {
return { id: event.id, invalid: true, reason: "missing playable source" };
}
return {
id: event.id,
videoRootId: parsedContent.videoRootId || event.id,
version,
isPrivate: parsedContent.isPrivate ?? false,
title,
url,
magnet,
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,
};
}
/**
+30 -41
View File
@@ -2,6 +2,7 @@
import { isDevMode } from "./config.js";
import { accessControl } from "./accessControl.js";
import { parseVideoEventPayload } from "./videoEventUtils.js";
/**
* The usual relays
@@ -90,51 +91,39 @@ function inferMimeTypeFromUrl(url) {
* CHANGED: skip if version <2
*/
function convertEventToVideo(event) {
try {
const content = JSON.parse(event.content || "{}");
const { parsedContent, parseError, title, url, magnet, version } =
parseVideoEventPayload(event);
// Example checks:
const isSupportedVersion = content.version >= 2;
const hasRequiredFields = !!(
content.title && (content.url || content.magnet)
);
if (!isSupportedVersion) {
return {
id: event.id,
invalid: true,
reason: "version <2",
};
}
if (!hasRequiredFields) {
return {
id: event.id,
invalid: true,
reason: "missing title/url+magnet",
};
}
if (!title) {
const reason = parseError ? "missing title (json parse error)" : "missing title";
return { id: event.id, invalid: true, reason };
}
if (!url && !magnet) {
return {
id: event.id,
videoRootId: content.videoRootId || event.id,
version: content.version,
isPrivate: content.isPrivate ?? false,
title: content.title ?? "",
url: content.url ?? "",
magnet: content.magnet ?? "",
thumbnail: content.thumbnail ?? "",
description: content.description ?? "",
mode: content.mode ?? "live",
deleted: content.deleted === true,
pubkey: event.pubkey,
created_at: event.created_at,
tags: event.tags,
invalid: false,
invalid: true,
reason: "missing playable source",
};
} catch (err) {
// JSON parse error
return { id: event.id, invalid: true, reason: "json parse error" };
}
return {
id: event.id,
videoRootId: parsedContent.videoRootId || event.id,
version,
isPrivate: parsedContent.isPrivate ?? false,
title,
url,
magnet,
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,
};
}
/**
@@ -787,7 +776,7 @@ class NostrClient {
sub.on("eose", () => {
if (isDevMode && invalidDuringSub.length > 0) {
console.warn(
`[subscribeVideos] found ${invalidDuringSub.length} invalid v2 notes:`,
`[subscribeVideos] found ${invalidDuringSub.length} invalid video notes (with reasons):`,
invalidDuringSub
);
}
@@ -854,7 +843,7 @@ class NostrClient {
// OPTIONAL: Log invalid stats
if (invalidNotes.length > 0 && isDevMode) {
console.warn(
`Skipped ${invalidNotes.length} invalid v2 notes:\n`,
`Skipped ${invalidNotes.length} invalid video notes:\n`,
invalidNotes.map((n) => `${n.id.slice(0, 8)}.. => ${n.reason}`)
);
}
+33 -24
View File
@@ -1,5 +1,6 @@
// js/subscriptions.js
import { nostrClient } from "./nostr.js";
import { parseVideoEventPayload } from "./videoEventUtils.js";
/**
* Manages the user's subscription list (kind=30002) *privately*,
@@ -500,34 +501,42 @@ class SubscriptionsManager {
}
convertEventToVideo(evt) {
try {
const content = JSON.parse(evt.content || "{}");
const hasFields = !!(
content.title && (content.url || content.magnet)
);
const versionOk = content.version >= 2;
if (!versionOk || !hasFields) {
return { id: evt.id, invalid: true };
}
const { parsedContent, parseError, title, url, magnet, version } =
parseVideoEventPayload(evt);
if (!title) {
return {
id: evt.id,
pubkey: evt.pubkey,
created_at: evt.created_at,
videoRootId: content.videoRootId || evt.id,
version: content.version,
deleted: content.deleted === true,
isPrivate: content.isPrivate === true,
title: content.title || "",
url: content.url || "",
magnet: content.magnet || "",
thumbnail: content.thumbnail || "",
description: content.description || "",
tags: evt.tags || [],
invalid: false,
invalid: true,
reason: parseError ? "missing title (json parse error)" : "missing title",
};
} catch (err) {
return { id: evt.id, invalid: true };
}
if (!url && !magnet) {
return {
id: evt.id,
invalid: true,
reason: "missing playable source",
};
}
return {
id: evt.id,
pubkey: evt.pubkey,
created_at: evt.created_at,
videoRootId: parsedContent.videoRootId || evt.id,
version,
deleted: parsedContent.deleted === true,
isPrivate: parsedContent.isPrivate ?? false,
title,
url,
magnet,
thumbnail: parsedContent.thumbnail ?? "",
description: parsedContent.description ?? "",
mode: parsedContent.mode ?? "live",
tags: evt.tags || [],
invalid: false,
};
}
dedupeToNewestByRoot(videos) {
+99
View File
@@ -0,0 +1,99 @@
// js/videoEventUtils.js
/**
* Extracts normalized fields from a Bitvid video event while
* tolerating legacy payloads that may omit version >= 2 metadata.
*/
export function parseVideoEventPayload(event = {}) {
const rawContent = typeof event.content === "string" ? event.content : "";
let parsedContent = {};
let parseError = null;
if (rawContent) {
try {
const parsed = JSON.parse(rawContent);
if (parsed && typeof parsed === "object") {
parsedContent = parsed;
}
} catch (err) {
parseError = err;
parsedContent = {};
}
}
const title = typeof parsedContent.title === "string"
? parsedContent.title.trim()
: "";
const thumbnail = typeof parsedContent.thumbnail === "string"
? parsedContent.thumbnail.trim()
: "";
const magnetCandidates = [];
const urlCandidates = [];
const pushUnique = (arr, value) => {
if (!value || typeof value !== "string") return;
const trimmed = value.trim();
if (!trimmed || arr.includes(trimmed)) return;
arr.push(trimmed);
};
if (typeof parsedContent.magnet === "string") {
pushUnique(magnetCandidates, parsedContent.magnet);
}
if (typeof parsedContent.url === "string") {
const parsedUrl = parsedContent.url.trim();
if (parsedUrl && parsedUrl !== thumbnail) {
pushUnique(urlCandidates, parsedUrl);
}
}
const tags = Array.isArray(event.tags) ? event.tags : [];
const urlTagKeys = new Set(["r", "url", "u"]);
for (const tag of tags) {
if (!Array.isArray(tag) || tag.length < 2) continue;
const key = typeof tag[0] === "string" ? tag[0] : "";
const value = typeof tag[1] === "string" ? tag[1] : "";
if (!value) continue;
if (value.toLowerCase().startsWith("magnet:")) {
pushUnique(magnetCandidates, value);
continue;
}
if (urlTagKeys.has(key) && /^https?:\/\//i.test(value)) {
pushUnique(urlCandidates, value);
}
}
const magnetMatch = rawContent.match(/magnet:\?xt=urn:[^"'\s<>]+/i);
if (magnetMatch) {
pushUnique(magnetCandidates, magnetMatch[0]);
}
const magnet = magnetCandidates.find(Boolean) || "";
const url =
urlCandidates.find(
(candidate) => candidate && !candidate.toLowerCase().startsWith("magnet:")
) || "";
const rawVersion = parsedContent.version;
let version = 0;
if (typeof rawVersion === "number" && Number.isFinite(rawVersion)) {
version = rawVersion;
} else if (typeof rawVersion === "string") {
const parsedVersion = Number(rawVersion);
if (!Number.isNaN(parsedVersion)) {
version = parsedVersion;
}
}
return {
parsedContent,
parseError,
title,
url,
magnet,
version,
};
}