mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-01 18:32:27 +00:00
Support legacy Nostr video payloads
This commit is contained in:
1 parent
a84f635539
commit
0569755901
4 files changed
+190
-97
No files matched your search
+28
-32
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user