mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Add storage pointer tags to video posts
This commit is contained in:
1 parent
da5b1107d7
commit
419a3f419c
11 files changed
+328
-4
No files matched your search
@@ -150,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`, `infoHash`, `fileSha256`, `originalFileSha256`, `ws`, `xs`) |
|
||||
| Video post (`NOTE_TYPES.VIDEO_POST`) | `30078` | `['t','video']`, `['d', <stable video identifier>]`, `['s', <storage pointer>]` 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. |
|
||||
@@ -163,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 now include a required storage pointer tag (`['s', '<provider>:<prefix>']`). The `<prefix>` should resolve to the public storage base for the video assets (for example, a public bucket URL plus the object key without its extension). Clients derive `info.json` by appending `.info.json` to the prefix (or by combining a path-style prefix with the hosted URL origin if needed).
|
||||
|
||||
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. |
|
||||
|
||||
+73
-2
@@ -36,6 +36,12 @@ import {
|
||||
buildVideoPointerValue,
|
||||
stringFromInput,
|
||||
} from "./nip71.js";
|
||||
import {
|
||||
buildStoragePointerValue,
|
||||
deriveStoragePointerFromUrl,
|
||||
getStoragePointerFromTags,
|
||||
normalizeStoragePointer,
|
||||
} from "../utils/storagePointer.js";
|
||||
import {
|
||||
createWatchHistoryManager,
|
||||
normalizePointerInput,
|
||||
@@ -818,6 +824,40 @@ function normalizeHexHash(candidate) {
|
||||
return HEX64_REGEX.test(trimmed) ? trimmed : "";
|
||||
}
|
||||
|
||||
function resolveStoragePointerValue({
|
||||
storagePointer,
|
||||
url,
|
||||
infoHash,
|
||||
fallbackId,
|
||||
provider,
|
||||
} = {}) {
|
||||
const normalized = normalizeStoragePointer(storagePointer);
|
||||
if (normalized) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
const derivedFromUrl = deriveStoragePointerFromUrl(url, provider || "url");
|
||||
if (derivedFromUrl) {
|
||||
return derivedFromUrl;
|
||||
}
|
||||
|
||||
if (typeof infoHash === "string" && infoHash.trim()) {
|
||||
return buildStoragePointerValue({
|
||||
provider: "btih",
|
||||
prefix: infoHash.trim().toLowerCase(),
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof fallbackId === "string" && fallbackId.trim()) {
|
||||
return buildStoragePointerValue({
|
||||
provider: "nostr",
|
||||
prefix: fallbackId.trim(),
|
||||
});
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
const BlobConstructor = typeof Blob !== "undefined" ? Blob : null;
|
||||
let sharedTextEncoder = null;
|
||||
|
||||
@@ -5365,6 +5405,13 @@ export class NostrClient {
|
||||
}
|
||||
const fileSha256 = normalizeHexHash(videoData.fileSha256);
|
||||
const originalFileSha256 = normalizeHexHash(videoData.originalFileSha256);
|
||||
const storagePointer = resolveStoragePointerValue({
|
||||
storagePointer: videoData.storagePointer,
|
||||
url: finalUrl,
|
||||
infoHash,
|
||||
fallbackId: videoRootId,
|
||||
provider: videoData.storageProvider,
|
||||
});
|
||||
|
||||
const contentObject = {
|
||||
version: 3,
|
||||
@@ -5406,12 +5453,15 @@ export class NostrClient {
|
||||
nip71Metadata && typeof nip71Metadata === "object" ? nip71Metadata : null
|
||||
);
|
||||
|
||||
const additionalTags = storagePointer
|
||||
? [["s", storagePointer], ...nip71Tags]
|
||||
: nip71Tags;
|
||||
const event = buildVideoPostEvent({
|
||||
pubkey: normalizedPubkey,
|
||||
created_at: createdAt,
|
||||
dTagValue,
|
||||
content: contentObject,
|
||||
additionalTags: nip71Tags,
|
||||
additionalTags,
|
||||
});
|
||||
|
||||
devLogger.log("Publish event with series identifier:", videoRootId);
|
||||
@@ -5782,6 +5832,16 @@ export class NostrClient {
|
||||
|
||||
// Use the existing videoRootId (or fall back to the base event's ID)
|
||||
const oldRootId = baseEvent.videoRootId || baseEvent.id;
|
||||
const storagePointer = resolveStoragePointerValue({
|
||||
storagePointer:
|
||||
updatedData?.storagePointer ||
|
||||
baseEvent.storagePointer ||
|
||||
getStoragePointerFromTags(baseEvent.tags),
|
||||
url: finalUrl,
|
||||
infoHash,
|
||||
fallbackId: oldRootId,
|
||||
provider: updatedData?.storageProvider || baseEvent.storageProvider,
|
||||
});
|
||||
|
||||
const preservedDTag = this.resolveEventDTag(baseEvent, originalEventStub);
|
||||
const fallbackDTag =
|
||||
@@ -5847,12 +5907,15 @@ export class NostrClient {
|
||||
|
||||
const nip71Tags = buildNip71MetadataTags(metadataForTags);
|
||||
|
||||
const additionalTags = storagePointer
|
||||
? [["s", storagePointer], ...nip71Tags]
|
||||
: nip71Tags;
|
||||
const event = buildVideoPostEvent({
|
||||
pubkey: userPubkeyLower,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
dTagValue: finalDTagValue,
|
||||
content: contentObject,
|
||||
additionalTags: nip71Tags,
|
||||
additionalTags,
|
||||
});
|
||||
|
||||
devLogger.log("Creating edited event with root ID:", oldRootId);
|
||||
@@ -6022,11 +6085,19 @@ export class NostrClient {
|
||||
mode: oldContent.mode || "live",
|
||||
};
|
||||
|
||||
const storagePointer = resolveStoragePointerValue({
|
||||
storagePointer: getStoragePointerFromTags(baseEvent.tags),
|
||||
url: oldContent.url,
|
||||
infoHash: oldContent.infoHash,
|
||||
fallbackId: finalRootId,
|
||||
});
|
||||
const additionalTags = storagePointer ? [["s", storagePointer]] : [];
|
||||
const event = buildVideoPostEvent({
|
||||
pubkey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
dTagValue: stableDTag,
|
||||
content: contentObject,
|
||||
additionalTags,
|
||||
});
|
||||
|
||||
await this.ensureActiveSignerForPubkey(pubkey);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { deriveTitleFromEvent } from "../videoEventUtils.js";
|
||||
import { extractBtihFromMagnet, extractMagnetHints } from "../magnetShared.js";
|
||||
import { devLogger } from "../utils/logger.js";
|
||||
import {
|
||||
getStoragePointerFromTags,
|
||||
resolveInfoJsonUrl,
|
||||
} from "../utils/storagePointer.js";
|
||||
import { getCachedNostrTools } from "./toolkit.js";
|
||||
|
||||
function stringFromInput(value) {
|
||||
@@ -1476,6 +1480,11 @@ export function convertEventToVideo(event = {}) {
|
||||
: { ws: "", xs: "" };
|
||||
const ws = wsField || magnetHints.ws || "";
|
||||
const xs = xsField || magnetHints.xs || "";
|
||||
const storagePointer = getStoragePointerFromTags(tags);
|
||||
const infoJsonUrl = resolveInfoJsonUrl({
|
||||
storagePointer,
|
||||
url,
|
||||
});
|
||||
|
||||
return {
|
||||
id: event.id,
|
||||
@@ -1497,6 +1506,8 @@ export function convertEventToVideo(event = {}) {
|
||||
deleted,
|
||||
ws,
|
||||
xs,
|
||||
storagePointer,
|
||||
infoJsonUrl,
|
||||
enableComments,
|
||||
pubkey: event.pubkey,
|
||||
created_at: event.created_at,
|
||||
|
||||
@@ -265,6 +265,7 @@ const BASE_SCHEMAS = {
|
||||
kind: 30078,
|
||||
topicTag: { name: "t", value: "video" },
|
||||
identifierTag: { name: "d" },
|
||||
requiredTagNames: ["s"],
|
||||
appendTags: DEFAULT_APPEND_TAGS,
|
||||
content: {
|
||||
format: "json",
|
||||
@@ -2521,6 +2522,19 @@ function hasTag(tags, tagName, tagValue = null) {
|
||||
);
|
||||
}
|
||||
|
||||
function hasTagName(tags, tagName) {
|
||||
if (!Array.isArray(tags)) return false;
|
||||
return tags.some((tag) => {
|
||||
if (!Array.isArray(tag) || tag[0] !== tagName) {
|
||||
return false;
|
||||
}
|
||||
if (tag.length < 2 || typeof tag[1] !== "string") {
|
||||
return false;
|
||||
}
|
||||
return tag[1].trim().length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
export function validateEventStructure(type, event) {
|
||||
const schema = getNostrEventSchema(type);
|
||||
const errors = [];
|
||||
@@ -2558,6 +2572,14 @@ export function validateEventStructure(type, event) {
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(schema.requiredTagNames)) {
|
||||
schema.requiredTagNames.forEach((tagName) => {
|
||||
if (!hasTagName(event.tags, tagName)) {
|
||||
errors.push(`Missing required tag for ${type}: ${tagName}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (schema.appendTags) {
|
||||
schema.appendTags.forEach((appendTag) => {
|
||||
if (Array.isArray(appendTag) && appendTag.length >= 2) {
|
||||
|
||||
@@ -38,6 +38,10 @@ import {
|
||||
import { ensureS3SdkLoaded, makeS3Client } from "../storage/s3-client.js";
|
||||
import { truncateMiddle } from "../utils/formatters.js";
|
||||
import { userLogger, devLogger } from "../utils/logger.js";
|
||||
import {
|
||||
buildStoragePointerValue,
|
||||
buildStoragePrefixFromKey,
|
||||
} from "../utils/storagePointer.js";
|
||||
import {
|
||||
getVideoNoteErrorMessage,
|
||||
normalizeVideoNotePayload,
|
||||
@@ -1033,12 +1037,22 @@ class R2Service {
|
||||
);
|
||||
}
|
||||
|
||||
const storagePrefix = buildStoragePrefixFromKey({
|
||||
publicBaseUrl: bucketEntry.publicBaseUrl,
|
||||
key,
|
||||
});
|
||||
const storagePointer = buildStoragePointerValue({
|
||||
provider: "r2",
|
||||
prefix: storagePrefix,
|
||||
});
|
||||
|
||||
const rawVideoPayload = {
|
||||
title,
|
||||
url: publicUrl, // Primary URL
|
||||
magnet: generatedMagnet || (metadata?.magnet ?? ""),
|
||||
thumbnail: metadata?.thumbnail ?? "",
|
||||
description: metadata?.description ?? "",
|
||||
storagePointer,
|
||||
ws: generatedWs || (metadata?.ws ?? ""),
|
||||
xs: torrentUrl || (metadata?.xs ?? ""),
|
||||
infoHash: hasValidInfoHash ? normalizedInfoHash : "",
|
||||
|
||||
@@ -8,6 +8,10 @@ import {
|
||||
validateS3Connection,
|
||||
} from "./s3Service.js";
|
||||
import { userLogger } from "../utils/logger.js";
|
||||
import {
|
||||
buildStoragePointerValue,
|
||||
buildStoragePrefixFromKey,
|
||||
} from "../utils/storagePointer.js";
|
||||
import {
|
||||
getVideoNoteErrorMessage,
|
||||
normalizeVideoNotePayload,
|
||||
@@ -370,12 +374,22 @@ class S3UploadService {
|
||||
);
|
||||
}
|
||||
|
||||
const storagePrefix = buildStoragePrefixFromKey({
|
||||
publicBaseUrl: normalized.publicBaseUrl,
|
||||
key,
|
||||
});
|
||||
const storagePointer = buildStoragePointerValue({
|
||||
provider: "s3",
|
||||
prefix: storagePrefix,
|
||||
});
|
||||
|
||||
const rawVideoPayload = {
|
||||
title,
|
||||
url: publicUrl,
|
||||
magnet: generatedMagnet || (metadata?.magnet ?? ""),
|
||||
thumbnail: metadata?.thumbnail ?? "",
|
||||
description: metadata?.description ?? "",
|
||||
storagePointer,
|
||||
ws: generatedWs || (metadata?.ws ?? ""),
|
||||
xs: torrentUrl || (metadata?.xs ?? ""),
|
||||
infoHash: hasValidInfoHash ? normalizedInfoHash : "",
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
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",
|
||||
@@ -340,6 +344,12 @@ export function normalizeVideoNotePayload(input) {
|
||||
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(
|
||||
@@ -426,6 +436,16 @@ export function normalizeVideoNotePayload(input) {
|
||||
legacyFormData.xs = "";
|
||||
}
|
||||
|
||||
const derivedStoragePointer =
|
||||
storagePointer ||
|
||||
deriveStoragePointerFromUrl(
|
||||
legacyFormData.url,
|
||||
storageProvider || "url"
|
||||
);
|
||||
if (derivedStoragePointer) {
|
||||
legacyFormData.storagePointer = derivedStoragePointer;
|
||||
}
|
||||
|
||||
const derivedInfoHash =
|
||||
infoHash || normalizeInfoHash(legacyFormData.magnet) || "";
|
||||
if (derivedInfoHash) {
|
||||
|
||||
@@ -14,6 +14,11 @@ import { sanitizeBucketName } from "../../storage/r2-mgmt.js";
|
||||
import { buildR2Key, buildPublicUrl } from "../../r2.js";
|
||||
import { buildS3ObjectUrl } from "../../services/s3Service.js";
|
||||
import { PROVIDERS } from "../../services/storageService.js";
|
||||
import {
|
||||
buildStoragePointerValue,
|
||||
buildStoragePrefixFromKey,
|
||||
deriveStoragePointerFromUrl,
|
||||
} from "../../utils/storagePointer.js";
|
||||
import {
|
||||
getActiveSigner,
|
||||
requestDefaultExtensionPermissions,
|
||||
@@ -594,6 +599,18 @@ export class UploadModal {
|
||||
this.videoUploadState.status = 'complete';
|
||||
this.videoUploadState.url = videoPublicUrl;
|
||||
this.videoUploadState.key = videoKey;
|
||||
const storagePrefix = buildStoragePrefixFromKey({
|
||||
publicBaseUrl: baseDomain,
|
||||
key: videoKey,
|
||||
});
|
||||
const providerLabel =
|
||||
this.activeProvider === PROVIDERS.R2 || this.activeProvider === "cloudflare_r2"
|
||||
? "r2"
|
||||
: "s3";
|
||||
this.videoUploadState.storagePointer = buildStoragePointerValue({
|
||||
provider: providerLabel,
|
||||
prefix: storagePrefix,
|
||||
});
|
||||
this.results.videoUrl.value = videoPublicUrl;
|
||||
this.updateVideoProgress(1, "Video uploaded.");
|
||||
|
||||
@@ -1023,6 +1040,7 @@ export class UploadModal {
|
||||
// These might be empty depending on mode, filled below
|
||||
url: "",
|
||||
magnet: "",
|
||||
storagePointer: "",
|
||||
ws: this.inputs.ws?.value?.trim() || "",
|
||||
xs: this.inputs.xs?.value?.trim() || "",
|
||||
infoHash: this.torrentState.infoHash || "",
|
||||
@@ -1039,6 +1057,13 @@ export class UploadModal {
|
||||
// We already have the URLs in state
|
||||
metadata.url = this.videoUploadState.url;
|
||||
metadata.magnet = this.torrentState.magnet || "";
|
||||
const providerLabel =
|
||||
this.activeProvider === PROVIDERS.R2 || this.activeProvider === "cloudflare_r2"
|
||||
? "r2"
|
||||
: "s3";
|
||||
metadata.storagePointer =
|
||||
this.videoUploadState.storagePointer ||
|
||||
deriveStoragePointerFromUrl(metadata.url, providerLabel);
|
||||
metadata.ws = this.videoUploadState.url; // WebSeed is same as R2 URL
|
||||
metadata.xs = this.torrentState.url || "";
|
||||
|
||||
@@ -1112,6 +1137,8 @@ export class UploadModal {
|
||||
async handleExternalFlow(metadata) {
|
||||
metadata.url = this.inputs.url?.value?.trim() || "";
|
||||
metadata.magnet = this.inputs.magnet?.value?.trim() || "";
|
||||
metadata.storagePointer =
|
||||
metadata.storagePointer || deriveStoragePointerFromUrl(metadata.url, "url");
|
||||
|
||||
const hasUrl = metadata.url.length > 0;
|
||||
const hasMagnet = metadata.magnet.length > 0;
|
||||
@@ -1162,6 +1189,7 @@ export class UploadModal {
|
||||
progress: 0,
|
||||
url: '',
|
||||
key: '',
|
||||
storagePointer: '',
|
||||
file: null,
|
||||
};
|
||||
this.thumbnailUploadState = {
|
||||
|
||||
@@ -2667,6 +2667,13 @@ export class VideoCard {
|
||||
if (!el) return;
|
||||
el.dataset.playUrl = encodeURIComponent(this.playbackUrl || "");
|
||||
el.dataset.playMagnet = this.playbackMagnet || "";
|
||||
const infoJsonUrl =
|
||||
typeof this.video?.infoJsonUrl === "string" ? this.video.infoJsonUrl : "";
|
||||
if (infoJsonUrl) {
|
||||
el.dataset.infoJson = encodeURIComponent(infoJsonUrl);
|
||||
} else if (el.dataset.infoJson) {
|
||||
delete el.dataset.infoJson;
|
||||
}
|
||||
if (this.magnetProvided) {
|
||||
el.dataset.torrentSupported = this.magnetSupported ? "true" : "false";
|
||||
} else if (el.dataset.torrentSupported) {
|
||||
|
||||
@@ -1041,6 +1041,10 @@ export class VideoListView {
|
||||
(target?.dataset && typeof target.dataset.playMagnet === "string"
|
||||
? target.dataset.playMagnet
|
||||
: null) ?? target?.getAttribute?.("data-play-magnet") ?? "";
|
||||
const rawInfoJsonValue =
|
||||
(target?.dataset && typeof target.dataset.infoJson === "string"
|
||||
? target.dataset.infoJson
|
||||
: null) ?? target?.getAttribute?.("data-info-json") ?? "";
|
||||
|
||||
let url = "";
|
||||
if (rawUrlValue) {
|
||||
@@ -1052,10 +1056,18 @@ export class VideoListView {
|
||||
}
|
||||
|
||||
const magnet = typeof rawMagnetValue === "string" ? rawMagnetValue : "";
|
||||
let infoJsonUrl = "";
|
||||
if (rawInfoJsonValue) {
|
||||
try {
|
||||
infoJsonUrl = decodeURIComponent(rawInfoJsonValue);
|
||||
} catch (error) {
|
||||
infoJsonUrl = rawInfoJsonValue;
|
||||
}
|
||||
}
|
||||
const videoId =
|
||||
target?.dataset?.videoId || target?.getAttribute?.("data-video-id") || video?.id || "";
|
||||
|
||||
return { videoId, url, magnet, video, trigger: element };
|
||||
return { videoId, url, magnet, infoJsonUrl, video, trigger: element };
|
||||
}
|
||||
|
||||
emitSelected(detail) {
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
const INFO_JSON_SUFFIX = ".info.json";
|
||||
|
||||
function safeTrim(value) {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
export function normalizeStoragePointer(value) {
|
||||
return safeTrim(value);
|
||||
}
|
||||
|
||||
export function parseStoragePointer(value) {
|
||||
const normalized = normalizeStoragePointer(value);
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const separatorIndex = normalized.indexOf(":");
|
||||
if (separatorIndex <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const provider = normalized.slice(0, separatorIndex).trim().toLowerCase();
|
||||
const prefix = normalized.slice(separatorIndex + 1).trim();
|
||||
if (!provider || !prefix) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { provider, prefix };
|
||||
}
|
||||
|
||||
export function buildStoragePointerValue({ provider, prefix } = {}) {
|
||||
const normalizedProvider = safeTrim(provider).toLowerCase();
|
||||
const normalizedPrefix = safeTrim(prefix);
|
||||
if (!normalizedProvider || !normalizedPrefix) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return `${normalizedProvider}:${normalizedPrefix}`;
|
||||
}
|
||||
|
||||
export function deriveStoragePrefixFromUrl(url) {
|
||||
const trimmed = safeTrim(url);
|
||||
if (!trimmed || !/^https?:\/\//i.test(trimmed)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = new URL(trimmed);
|
||||
const path = parsed.pathname || "";
|
||||
if (!path || path === "/") {
|
||||
return "";
|
||||
}
|
||||
const normalizedPath = path.replace(/\/+$/, "");
|
||||
const withoutExtension = normalizedPath.replace(/\.[^/.]+$/, "");
|
||||
const cleanedPath = withoutExtension.startsWith("/")
|
||||
? withoutExtension
|
||||
: `/${withoutExtension}`;
|
||||
return `${parsed.origin}${cleanedPath}`;
|
||||
} catch (error) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export function deriveStoragePointerFromUrl(url, provider = "url") {
|
||||
const prefix = deriveStoragePrefixFromUrl(url);
|
||||
return buildStoragePointerValue({ provider, prefix });
|
||||
}
|
||||
|
||||
export function buildStoragePrefixFromKey({ publicBaseUrl, key } = {}) {
|
||||
const base = safeTrim(publicBaseUrl).replace(/\/+$/, "");
|
||||
const normalizedKey = safeTrim(key).replace(/^\//, "");
|
||||
if (!base || !normalizedKey) {
|
||||
return "";
|
||||
}
|
||||
const baseKey = normalizedKey.replace(/\.[^/.]+$/, "");
|
||||
if (!baseKey) {
|
||||
return "";
|
||||
}
|
||||
return `${base}/${baseKey}`;
|
||||
}
|
||||
|
||||
export function getStoragePointerFromTags(tags = []) {
|
||||
if (!Array.isArray(tags)) {
|
||||
return "";
|
||||
}
|
||||
const tag = tags.find(
|
||||
(entry) =>
|
||||
Array.isArray(entry) &&
|
||||
entry[0] === "s" &&
|
||||
typeof entry[1] === "string"
|
||||
);
|
||||
return normalizeStoragePointer(tag?.[1]);
|
||||
}
|
||||
|
||||
function normalizeInfoJsonBase(base) {
|
||||
const trimmed = safeTrim(base);
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
if (trimmed.toLowerCase().endsWith(INFO_JSON_SUFFIX)) {
|
||||
return trimmed;
|
||||
}
|
||||
return `${trimmed.replace(/\/+$/, "")}${INFO_JSON_SUFFIX}`;
|
||||
}
|
||||
|
||||
export function resolveInfoJsonUrl({ storagePointer, url } = {}) {
|
||||
const parsed = parseStoragePointer(storagePointer);
|
||||
if (!parsed) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const prefix = parsed.prefix;
|
||||
if (/^https?:\/\//i.test(prefix)) {
|
||||
return normalizeInfoJsonBase(prefix);
|
||||
}
|
||||
|
||||
const fallbackPrefix = deriveStoragePrefixFromUrl(url);
|
||||
if (fallbackPrefix) {
|
||||
return normalizeInfoJsonBase(fallbackPrefix);
|
||||
}
|
||||
|
||||
return normalizeInfoJsonBase(prefix);
|
||||
}
|
||||
Reference in new issue
Block a user