mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
- Replaced unbounded `Promise.all` with `pMap` and `RELAY_BACKGROUND_CONCURRENCY || 3` in `js/nostr/dmSignalEvents.js` and `js/services/dmNostrService.js` to prevent network saturation. - Updated `scripts/check-hex.js` to exclude agent artifact directories (`perf/`, `test_logs/`, `context/`, etc.) to prevent lint failures when agents log code snippets or hex codes. - Increased `maxBuffer` in `scripts/check-hex.js` to 10MB to be robust against large matches. - Updated `js/services/exploreDataService.js` to trigger immediate data refresh (`refreshWatchHistoryTagCounts` and `refreshTagIdf`) when visibility state changes to visible, fixing a unit test failure. - Includes daily performance report and context logs. Co-authored-by: PR0M3TH3AN <[email protected]>
476 lines
14 KiB
JavaScript
476 lines
14 KiB
JavaScript
import { extractMagnetHints } from "../magnetShared.js";
|
|
import { normalizeAndAugmentMagnet } from "../magnetUtils.js";
|
|
import { infoHashFromMagnet } from "../magnets.js";
|
|
import {
|
|
deriveStoragePointerFromUrl,
|
|
normalizeStoragePointer,
|
|
} from "../utils/storagePointer.js";
|
|
|
|
export const VIDEO_NOTE_ERROR_CODES = {
|
|
MISSING_TITLE: "missing_title",
|
|
MISSING_SOURCE: "missing_source",
|
|
INVALID_URL_PROTOCOL: "invalid_url_protocol",
|
|
};
|
|
|
|
export const VIDEO_NOTE_ERROR_MESSAGES = {
|
|
[VIDEO_NOTE_ERROR_CODES.MISSING_TITLE]: "Title is required.",
|
|
[VIDEO_NOTE_ERROR_CODES.MISSING_SOURCE]:
|
|
"Provide a hosted URL, magnet link, or an imeta variant before publishing.",
|
|
[VIDEO_NOTE_ERROR_CODES.INVALID_URL_PROTOCOL]:
|
|
"Hosted video URLs must use HTTPS.",
|
|
};
|
|
|
|
export function getVideoNoteErrorMessage(code) {
|
|
return VIDEO_NOTE_ERROR_MESSAGES[code] || "Unable to submit video. Please try again.";
|
|
}
|
|
|
|
function normalizeString(value) {
|
|
if (typeof value === "string") {
|
|
return value.trim();
|
|
}
|
|
if (typeof value === "number") {
|
|
return Number.isFinite(value) ? String(value).trim() : "";
|
|
}
|
|
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;
|
|
}
|
|
const parsed = Number(value);
|
|
return Number.isFinite(parsed) ? parsed : null;
|
|
}
|
|
|
|
function parseUnixSeconds(value) {
|
|
if (value === null || value === undefined || value === "") {
|
|
return null;
|
|
}
|
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
const normalized = value > 1e12 ? value / 1000 : value;
|
|
return Math.floor(normalized);
|
|
}
|
|
const trimmed = normalizeString(value);
|
|
if (!trimmed) {
|
|
return null;
|
|
}
|
|
const numeric = Number(trimmed);
|
|
if (Number.isFinite(numeric)) {
|
|
const normalized = numeric > 1e12 ? numeric / 1000 : numeric;
|
|
return Math.floor(normalized);
|
|
}
|
|
const timestamp = Date.parse(trimmed);
|
|
if (!Number.isNaN(timestamp)) {
|
|
return Math.floor(timestamp / 1000);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function normalizeStringArray(input) {
|
|
if (!Array.isArray(input)) {
|
|
return [];
|
|
}
|
|
return input
|
|
.map((entry) => normalizeString(entry))
|
|
.filter((entry) => Boolean(entry));
|
|
}
|
|
|
|
function normalizeBooleanFlag(value, defaultValue = false) {
|
|
if (value === true) {
|
|
return true;
|
|
}
|
|
if (value === false) {
|
|
return false;
|
|
}
|
|
return Boolean(defaultValue);
|
|
}
|
|
|
|
function normalizeImetaVariant(variant) {
|
|
if (!variant || typeof variant !== "object") {
|
|
return null;
|
|
}
|
|
const normalized = {};
|
|
const m = normalizeString(variant.m);
|
|
if (m) normalized.m = m.toLowerCase();
|
|
const dim = normalizeString(variant.dim);
|
|
if (dim) normalized.dim = dim;
|
|
const url = normalizeString(variant.url);
|
|
if (url) normalized.url = url;
|
|
const x = normalizeString(variant.x);
|
|
if (x) normalized.x = x;
|
|
const durationValue = parseNumberOrNull(variant.duration);
|
|
if (durationValue !== null && durationValue >= 0) {
|
|
normalized.duration = durationValue;
|
|
} else {
|
|
const durationString = normalizeString(variant.duration);
|
|
if (durationString) {
|
|
normalized.duration = durationString;
|
|
}
|
|
}
|
|
const bitrateValue = parseNumberOrNull(variant.bitrate);
|
|
if (bitrateValue !== null && bitrateValue >= 0) {
|
|
normalized.bitrate = bitrateValue;
|
|
} else {
|
|
const bitrateString = normalizeString(variant.bitrate);
|
|
if (bitrateString) {
|
|
normalized.bitrate = bitrateString;
|
|
}
|
|
}
|
|
const image = normalizeStringArray(variant.image);
|
|
if (image.length) normalized.image = image;
|
|
const fallback = normalizeStringArray(variant.fallback);
|
|
if (fallback.length) normalized.fallback = fallback;
|
|
const service = normalizeStringArray(variant.service);
|
|
if (service.length) normalized.service = service;
|
|
if (variant.autoGenerated === true) {
|
|
normalized.autoGenerated = true;
|
|
}
|
|
|
|
return Object.keys(normalized).length ? normalized : null;
|
|
}
|
|
|
|
function normalizeSegments(segments) {
|
|
if (!Array.isArray(segments)) {
|
|
return [];
|
|
}
|
|
return segments
|
|
.map((segment) => {
|
|
if (!segment || typeof segment !== "object") {
|
|
return null;
|
|
}
|
|
const startRaw = segment.start;
|
|
const endRaw = segment.end;
|
|
const start = parseNumberOrNull(startRaw);
|
|
const end = parseNumberOrNull(endRaw);
|
|
const normalizedSegment = {};
|
|
if (start !== null) {
|
|
normalizedSegment.start = start;
|
|
} else {
|
|
const startString = normalizeString(startRaw);
|
|
if (startString) {
|
|
normalizedSegment.start = startString;
|
|
}
|
|
}
|
|
if (end !== null) {
|
|
normalizedSegment.end = end;
|
|
} else {
|
|
const endString = normalizeString(endRaw);
|
|
if (endString) {
|
|
normalizedSegment.end = endString;
|
|
}
|
|
}
|
|
const title = normalizeString(segment.title);
|
|
if (title) {
|
|
normalizedSegment.title = title;
|
|
}
|
|
const thumbnail = normalizeString(segment.thumbnail);
|
|
if (thumbnail) {
|
|
normalizedSegment.thumbnail = thumbnail;
|
|
}
|
|
return Object.keys(normalizedSegment).length ? normalizedSegment : null;
|
|
})
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function normalizeTextTracks(tracks) {
|
|
if (!Array.isArray(tracks)) {
|
|
return [];
|
|
}
|
|
return tracks
|
|
.map((track) => {
|
|
if (!track || typeof track !== "object") {
|
|
return null;
|
|
}
|
|
const normalizedTrack = {};
|
|
const url = normalizeString(track.url);
|
|
if (url) normalizedTrack.url = url;
|
|
const type = normalizeString(track.type);
|
|
if (type) normalizedTrack.type = type;
|
|
const language = normalizeString(track.language);
|
|
if (language) normalizedTrack.language = language;
|
|
return Object.keys(normalizedTrack).length ? normalizedTrack : null;
|
|
})
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function normalizeParticipants(participants) {
|
|
if (!Array.isArray(participants)) {
|
|
return [];
|
|
}
|
|
return participants
|
|
.map((participant) => {
|
|
if (!participant || typeof participant !== "object") {
|
|
return null;
|
|
}
|
|
const normalizedParticipant = {};
|
|
const pubkey = normalizeString(participant.pubkey);
|
|
if (pubkey) normalizedParticipant.pubkey = pubkey;
|
|
const relay = normalizeString(participant.relay);
|
|
if (relay) normalizedParticipant.relay = relay;
|
|
return Object.keys(normalizedParticipant).length
|
|
? normalizedParticipant
|
|
: null;
|
|
})
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function normalizeNip71Metadata(rawMetadata) {
|
|
if (!rawMetadata || typeof rawMetadata !== "object") {
|
|
return null;
|
|
}
|
|
|
|
const normalized = {};
|
|
|
|
if (rawMetadata.kind !== undefined) {
|
|
const kindNumber = parseNumberOrNull(rawMetadata.kind);
|
|
if (kindNumber !== null) {
|
|
normalized.kind = kindNumber;
|
|
} else {
|
|
const kindString = normalizeString(rawMetadata.kind);
|
|
if (kindString) {
|
|
normalized.kind = kindString;
|
|
}
|
|
}
|
|
}
|
|
|
|
const summary = normalizeString(rawMetadata.summary);
|
|
if (summary) {
|
|
normalized.summary = summary;
|
|
}
|
|
|
|
const publishedAt = parseUnixSeconds(rawMetadata.publishedAt);
|
|
if (publishedAt !== null) {
|
|
normalized.publishedAt = publishedAt;
|
|
}
|
|
|
|
const alt = normalizeString(rawMetadata.alt);
|
|
if (alt) {
|
|
normalized.alt = alt;
|
|
}
|
|
|
|
const duration = parseNumberOrNull(rawMetadata.duration);
|
|
if (duration !== null) {
|
|
normalized.duration = duration;
|
|
}
|
|
|
|
const contentWarning = normalizeString(rawMetadata.contentWarning);
|
|
if (contentWarning) {
|
|
normalized.contentWarning = contentWarning;
|
|
}
|
|
|
|
const imeta = Array.isArray(rawMetadata.imeta)
|
|
? rawMetadata.imeta
|
|
.map((variant) => normalizeImetaVariant(variant))
|
|
.filter(Boolean)
|
|
: [];
|
|
if (imeta.length) {
|
|
normalized.imeta = imeta;
|
|
if (normalized.duration === undefined) {
|
|
const variantDurations = imeta
|
|
.map((variant) => parseNumberOrNull(variant.duration))
|
|
.filter((value) => value !== null && value >= 0);
|
|
if (variantDurations.length) {
|
|
normalized.duration = Math.max(...variantDurations);
|
|
}
|
|
}
|
|
}
|
|
|
|
const textTracks = normalizeTextTracks(rawMetadata.textTracks);
|
|
if (textTracks.length) {
|
|
normalized.textTracks = textTracks;
|
|
}
|
|
|
|
const segments = normalizeSegments(rawMetadata.segments);
|
|
if (segments.length) {
|
|
normalized.segments = segments;
|
|
}
|
|
|
|
const hashtags = normalizeStringArray(rawMetadata.hashtags);
|
|
if (hashtags.length) {
|
|
normalized.hashtags = hashtags;
|
|
}
|
|
|
|
const participants = normalizeParticipants(rawMetadata.participants);
|
|
if (participants.length) {
|
|
normalized.participants = participants;
|
|
}
|
|
|
|
const references = normalizeStringArray(rawMetadata.references);
|
|
if (references.length) {
|
|
normalized.references = references;
|
|
}
|
|
|
|
return Object.keys(normalized).length ? normalized : null;
|
|
}
|
|
|
|
export function normalizeVideoNotePayload(input) {
|
|
const rawPayload = input && typeof input === "object" ? input : {};
|
|
const legacyPayload =
|
|
rawPayload.legacyFormData && typeof rawPayload.legacyFormData === "object"
|
|
? rawPayload.legacyFormData
|
|
: rawPayload;
|
|
|
|
const rawNip71 =
|
|
(rawPayload.nip71 && typeof rawPayload.nip71 === "object"
|
|
? rawPayload.nip71
|
|
: null) ||
|
|
(legacyPayload.nip71 && typeof legacyPayload.nip71 === "object"
|
|
? legacyPayload.nip71
|
|
: null);
|
|
|
|
const title = normalizeString(legacyPayload?.title || "");
|
|
const url = normalizeString(legacyPayload?.url || "");
|
|
const magnet = normalizeString(legacyPayload?.magnet || "");
|
|
const thumbnail = normalizeString(legacyPayload?.thumbnail || "");
|
|
const description = normalizeString(legacyPayload?.description || "");
|
|
const ws = normalizeString(legacyPayload?.ws || "");
|
|
const xs = normalizeString(legacyPayload?.xs || "");
|
|
const storagePointer = normalizeStoragePointer(
|
|
legacyPayload?.storagePointer || legacyPayload?.storage || ""
|
|
);
|
|
const storageProvider = normalizeString(
|
|
legacyPayload?.storageProvider || ""
|
|
);
|
|
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);
|
|
const rawIsNsfw = normalizeBooleanFlag(legacyPayload?.isNsfw, false);
|
|
const rawIsForKids = normalizeBooleanFlag(legacyPayload?.isForKids, false);
|
|
const isNsfw = rawIsNsfw;
|
|
const isForKids = rawIsNsfw ? false : rawIsForKids;
|
|
const isPrivate = normalizeBooleanFlag(legacyPayload?.isPrivate, false);
|
|
|
|
const legacyFormData = {
|
|
version: 3,
|
|
title,
|
|
url,
|
|
magnet,
|
|
thumbnail,
|
|
description,
|
|
mode: normalizedMode,
|
|
enableComments,
|
|
isNsfw,
|
|
isForKids,
|
|
};
|
|
|
|
if (isPrivate) {
|
|
legacyFormData.isPrivate = true;
|
|
}
|
|
|
|
const normalizedNip71 = normalizeNip71Metadata(rawNip71);
|
|
|
|
const hasLegacySource = Boolean(legacyFormData.url || legacyFormData.magnet);
|
|
const hasImetaVariant = Boolean(
|
|
normalizedNip71?.imeta?.some((variant) =>
|
|
Boolean(
|
|
(variant.url && variant.url.length) ||
|
|
(variant.m && variant.m.length) ||
|
|
(variant.x && variant.x.length) ||
|
|
(variant.dim && variant.dim.length) ||
|
|
(Array.isArray(variant.image) && variant.image.length > 0) ||
|
|
(Array.isArray(variant.fallback) && variant.fallback.length > 0) ||
|
|
(Array.isArray(variant.service) && variant.service.length > 0) ||
|
|
(variant.duration !== undefined && variant.duration !== null && variant.duration !== "") ||
|
|
(variant.bitrate !== undefined && variant.bitrate !== null && variant.bitrate !== ""),
|
|
),
|
|
),
|
|
);
|
|
|
|
const errors = [];
|
|
|
|
if (!legacyFormData.title) {
|
|
errors.push(VIDEO_NOTE_ERROR_CODES.MISSING_TITLE);
|
|
}
|
|
|
|
if (!hasLegacySource && !hasImetaVariant) {
|
|
errors.push(VIDEO_NOTE_ERROR_CODES.MISSING_SOURCE);
|
|
}
|
|
|
|
if (legacyFormData.url && !/^https:\/\//i.test(legacyFormData.url)) {
|
|
errors.push(VIDEO_NOTE_ERROR_CODES.INVALID_URL_PROTOCOL);
|
|
}
|
|
|
|
if (legacyFormData.magnet) {
|
|
const wsList = [];
|
|
if (ws) {
|
|
wsList.push(ws);
|
|
}
|
|
if (legacyFormData.url && /^https?:\/\//i.test(legacyFormData.url)) {
|
|
wsList.push(legacyFormData.url);
|
|
}
|
|
|
|
const result = normalizeAndAugmentMagnet(legacyFormData.magnet, {
|
|
webSeed: wsList,
|
|
xs,
|
|
});
|
|
legacyFormData.magnet = result.magnet;
|
|
const hints = extractMagnetHints(result.magnet);
|
|
legacyFormData.ws = hints.ws || "";
|
|
legacyFormData.xs = hints.xs || "";
|
|
} else {
|
|
legacyFormData.ws = "";
|
|
legacyFormData.xs = "";
|
|
}
|
|
|
|
const derivedStoragePointer =
|
|
storagePointer ||
|
|
deriveStoragePointerFromUrl(
|
|
legacyFormData.url,
|
|
storageProvider || "url"
|
|
);
|
|
if (derivedStoragePointer) {
|
|
legacyFormData.storagePointer = derivedStoragePointer;
|
|
}
|
|
|
|
const derivedInfoHash =
|
|
infoHash || normalizeInfoHash(legacyFormData.magnet) || "";
|
|
if (derivedInfoHash) {
|
|
legacyFormData.infoHash = derivedInfoHash;
|
|
}
|
|
|
|
if (fileSha256) {
|
|
legacyFormData.fileSha256 = fileSha256;
|
|
}
|
|
|
|
if (originalFileSha256) {
|
|
legacyFormData.originalFileSha256 = originalFileSha256;
|
|
}
|
|
|
|
const publishPayload = {
|
|
legacyFormData,
|
|
};
|
|
|
|
if (normalizedNip71) {
|
|
publishPayload.nip71 = normalizedNip71;
|
|
}
|
|
|
|
return {
|
|
payload: publishPayload,
|
|
errors,
|
|
};
|
|
}
|