Store video file fingerprints alongside series IDs

This commit is contained in:
thePR0M3TH3AN committed 2026-01-26 12:25:54 -05:00
1 parent f907f96e1b
commit 82986650ce
8 files changed
+121 -2

No files matched your search

+9 -2
View File
@@ -33,7 +33,12 @@ const event = buildVideoPostEvent({
pubkey,
created_at: Math.floor(Date.now() / 1000),
dTagValue: "debug-video",
content: { version: 3, title: "Test", videoRootId: "debug" },
content: {
version: 3,
title: "Test",
videoRootId: "debug",
infoHash: "0123456789abcdef0123456789abcdef01234567",
},
});
```
@@ -145,7 +150,7 @@ import { updateWatchHistoryListWithDefaultClient } from "./nostrWatchHistoryFaca
| Note | Kind (default) | Tags | Content format |
| --- | --- | --- | --- |
| Video post (`NOTE_TYPES.VIDEO_POST`) | `30078` | `['t','video']`, `['d', <stable video identifier>]` plus optional schema append tags | JSON payload using Content Schema v3 (`version`, `title`, optional `url`, `magnet`, `thumbnail`, `description`, `mode`, `videoRootId`, `deleted`, `isPrivate`, `isNsfw`, `isForKids`, `enableComments`, `ws`, `xs`) |
| Video post (`NOTE_TYPES.VIDEO_POST`) | `30078` | `['t','video']`, `['d', <stable video identifier>]` plus optional schema append tags | JSON payload using Content Schema v3 (`version`, `title`, optional `url`, `magnet`, `thumbnail`, `description`, `mode`, `videoRootId`, `deleted`, `isPrivate`, `isNsfw`, `isForKids`, `enableComments`, `infoHash`, `fileSha256`, `originalFileSha256`, `ws`, `xs`) |
| NIP-94 mirror (`NOTE_TYPES.VIDEO_MIRROR`) | `1063` | Tags forwarded from `publishVideo` (URL, mime type, thumbnail, alt text, magnet) | Plain text alt description |
| Repost (`NOTE_TYPES.REPOST`) | `6` | `['e', <event id>, <relay?>]` with optional address pointer `['a', <kind:pubkey:identifier>, <relay?>]`, and `['p', <pubkey>]` when the origin author is known; inherits schema append tags | JSON-serialized event being reposted (or empty if unavailable) |
| Generic Repost (`NOTE_TYPES.GENERIC_REPOST`) | `16` | Same as Repost; specific for non-text kinds (e.g. videos). | Same as Repost. |
@@ -158,6 +163,8 @@ import { updateWatchHistoryListWithDefaultClient } from "./nostrWatchHistoryFaca
> **Publishing note:** Session actors only emit passive telemetry (for example, view counters) and must **not** sign video comments. Require a logged-in Nostr signer for comment publishing via [`commentEvents`](../js/nostr/commentEvents.js).
Video posts should treat `videoRootId` as the stable series identifier that remains unchanged across edits and deletes. File fingerprints such as `infoHash` or `fileSha256` live alongside the content payload so media swaps do not break the version chain.
| NIP-71 video (`NOTE_TYPES.NIP71_VIDEO`) | `21` | `['title', <title>]`, optional `['published_at', <unix seconds>]`, optional `['alt', <text>]`, repeated `['imeta', ...]` entries describing NIP-92 media variants, optional `['duration', <seconds>]`, repeated `['text-track', <url>, <kind>, <language>]`, optional `['content-warning', <reason>]`, repeated `['segment', <start>, <end>, <title>, <thumbnail>]`, repeated hashtags `['t', <tag>]`, repeated participants `['p', <pubkey>, <relay?>]`, repeated references `['r', <url>]` | Plain text summary carried in the content field. Publishing is gated by the `FEATURE_PUBLISH_NIP71` runtime flag while the rollout stabilizes. |
| NIP-71 short video (`NOTE_TYPES.NIP71_SHORT_VIDEO`) | `22` | Same as `NOTE_TYPES.NIP71_VIDEO`; the kind differentiates short-form presentations. | Plain text summary; gated by `FEATURE_PUBLISH_NIP71`. |
| Relay list (`NOTE_TYPES.RELAY_LIST`) | `10002` | Repeating `['r', <relay url>]` tags, optionally with a marker of `'read'` or `'write'` to scope the relay; marker omitted for read/write relays | Empty content |
+52
View File
@@ -21,6 +21,7 @@ import { isDevMode } from "../config.js";
import { FEATURE_PUBLISH_NIP71 } from "../constants.js";
import { accessControl } from "../accessControl.js";
import { bytesToHex, sha256 } from "../../vendor/crypto-helpers.bundle.min.js";
import { infoHashFromMagnet } from "../magnets.js";
// 🔧 merged conflicting changes from codex/update-video-publishing-and-parsing-logic vs unstable
import {
buildNip71MetadataTags,
@@ -5415,6 +5416,20 @@ export class NostrClient {
typeof videoData.ws === "string" ? videoData.ws.trim() : "";
const finalXs =
typeof videoData.xs === "string" ? videoData.xs.trim() : "";
const infoHashCandidates = [videoData.infoHash];
let infoHash = "";
for (const candidate of infoHashCandidates) {
const normalized = infoHashFromMagnet(candidate);
if (normalized) {
infoHash = normalized;
break;
}
}
if (!infoHash && finalMagnet) {
infoHash = infoHashFromMagnet(finalMagnet) || "";
}
const fileSha256 = normalizeHexHash(videoData.fileSha256);
const originalFileSha256 = normalizeHexHash(videoData.originalFileSha256);
const contentObject = {
version: 3,
@@ -5432,6 +5447,18 @@ export class NostrClient {
enableComments: finalEnableComments,
};
if (infoHash) {
contentObject.infoHash = infoHash;
}
if (fileSha256) {
contentObject.fileSha256 = fileSha256;
}
if (originalFileSha256) {
contentObject.originalFileSha256 = originalFileSha256;
}
if (finalWs) {
contentObject.ws = finalWs;
}
@@ -5805,6 +5832,19 @@ export class NostrClient {
? false
: true;
const updatedInfoHash = infoHashFromMagnet(updatedData?.infoHash || "");
let infoHash =
updatedInfoHash ||
infoHashFromMagnet(finalMagnet) ||
infoHashFromMagnet(baseEvent.infoHash) ||
"";
const fileSha256 =
normalizeHexHash(updatedData?.fileSha256) ||
normalizeHexHash(baseEvent.fileSha256);
const originalFileSha256 =
normalizeHexHash(updatedData?.originalFileSha256) ||
normalizeHexHash(baseEvent.originalFileSha256);
// Use the existing videoRootId (or fall back to the base event's ID)
const oldRootId = baseEvent.videoRootId || baseEvent.id;
@@ -5837,6 +5877,18 @@ export class NostrClient {
enableComments: finalEnableComments,
};
if (infoHash) {
contentObject.infoHash = infoHash;
}
if (fileSha256) {
contentObject.fileSha256 = fileSha256;
}
if (originalFileSha256) {
contentObject.originalFileSha256 = originalFileSha256;
}
if (finalWs) {
contentObject.ws = finalWs;
}
+15
View File
@@ -1403,6 +1403,17 @@ export function convertEventToVideo(event = {}) {
const enableComments =
parsedContent.enableComments === false ? false : true;
const normalizeSha256 = (candidate) => {
if (typeof candidate !== "string") {
return "";
}
const normalized = candidate.trim().toLowerCase();
if (!normalized) {
return "";
}
return /^[0-9a-f]{64}$/.test(normalized) ? normalized : "";
};
let infoHash = "";
const pushInfoHash = (candidate) => {
if (typeof candidate !== "string") {
@@ -1417,6 +1428,8 @@ export function convertEventToVideo(event = {}) {
};
pushInfoHash(parsedContent.infoHash);
const fileSha256 = normalizeSha256(parsedContent.fileSha256);
const originalFileSha256 = normalizeSha256(parsedContent.originalFileSha256);
if (!infoHash && magnet) {
const match = magnet.match(/xt=urn:btih:([0-9a-z]+)/i);
@@ -1476,6 +1489,8 @@ export function convertEventToVideo(event = {}) {
magnet,
rawMagnet,
infoHash,
fileSha256,
originalFileSha256,
thumbnail,
description,
mode,
+3
View File
@@ -283,6 +283,9 @@ const BASE_SCHEMAS = {
{ key: "isNsfw", type: "boolean", required: false },
{ key: "isForKids", type: "boolean", required: false },
{ key: "enableComments", type: "boolean", required: false },
{ key: "infoHash", type: "string", required: false },
{ key: "fileSha256", type: "string", required: false },
{ key: "originalFileSha256", type: "string", required: false },
{ key: "ws", type: "string", required: false },
{ key: "xs", type: "string", required: false },
],
+1
View File
@@ -1018,6 +1018,7 @@ class R2Service {
description: metadata?.description ?? "",
ws: generatedWs || (metadata?.ws ?? ""),
xs: torrentUrl || (metadata?.xs ?? ""),
infoHash: hasValidInfoHash ? normalizedInfoHash : "",
enableComments: metadata?.enableComments,
isNsfw: metadata?.isNsfw,
isForKids: metadata?.isForKids,
+1
View File
@@ -355,6 +355,7 @@ class S3UploadService {
description: metadata?.description ?? "",
ws: generatedWs || (metadata?.ws ?? ""),
xs: torrentUrl || (metadata?.xs ?? ""),
infoHash: hasValidInfoHash ? normalizedInfoHash : "",
enableComments: metadata?.enableComments,
isNsfw: metadata?.isNsfw,
isForKids: metadata?.isForKids,
+39
View File
@@ -1,5 +1,6 @@
import { extractMagnetHints } from "../magnetShared.js";
import { normalizeAndAugmentMagnet } from "../magnetUtils.js";
import { infoHashFromMagnet } from "../magnets.js";
export const VIDEO_NOTE_ERROR_CODES = {
MISSING_TITLE: "missing_title",
@@ -29,6 +30,25 @@ function normalizeString(value) {
return String(value ?? "").trim();
}
function normalizeSha256Hex(value) {
if (value === undefined || value === null) {
return "";
}
const normalized = normalizeString(value).toLowerCase();
if (!normalized) {
return "";
}
return /^[0-9a-f]{64}$/.test(normalized) ? normalized : "";
}
function normalizeInfoHash(value) {
if (value === undefined || value === null) {
return "";
}
const normalized = infoHashFromMagnet(normalizeString(value));
return typeof normalized === "string" && normalized ? normalized : "";
}
function parseNumberOrNull(value) {
if (value === null || value === undefined || value === "") {
return null;
@@ -320,6 +340,11 @@ export function normalizeVideoNotePayload(input) {
const description = normalizeString(legacyPayload?.description || "");
const ws = normalizeString(legacyPayload?.ws || "");
const xs = normalizeString(legacyPayload?.xs || "");
const infoHash = normalizeInfoHash(legacyPayload?.infoHash || "");
const fileSha256 = normalizeSha256Hex(legacyPayload?.fileSha256 || "");
const originalFileSha256 = normalizeSha256Hex(
legacyPayload?.originalFileSha256 || ""
);
const rawMode = normalizeString(legacyPayload?.mode || "");
const normalizedMode = rawMode && rawMode.toLowerCase() === "dev" ? "dev" : "live";
const enableComments = normalizeBooleanFlag(legacyPayload?.enableComments, true);
@@ -401,6 +426,20 @@ export function normalizeVideoNotePayload(input) {
legacyFormData.xs = "";
}
const derivedInfoHash =
infoHash || normalizeInfoHash(legacyFormData.magnet) || "";
if (derivedInfoHash) {
legacyFormData.infoHash = derivedInfoHash;
}
if (fileSha256) {
legacyFormData.fileSha256 = fileSha256;
}
if (originalFileSha256) {
legacyFormData.originalFileSha256 = originalFileSha256;
}
const publishPayload = {
legacyFormData,
};
+1
View File
@@ -1017,6 +1017,7 @@ export class UploadModal {
magnet: "",
ws: this.inputs.ws?.value?.trim() || "",
xs: this.inputs.xs?.value?.trim() || "",
infoHash: this.torrentState.infoHash || "",
};
// NIP-71 Advanced Data