fix: video upload
feat: use new video/short kinds
This commit is contained in:
parent
4bade72dd6
commit
de88158b3a
@ -8,11 +8,11 @@
|
||||
"@noble/hashes": "^1.4.0",
|
||||
"@scure/base": "^1.1.6",
|
||||
"@snort/shared": "^1.0.17",
|
||||
"@snort/system": "^1.5.6",
|
||||
"@snort/system-react": "^1.5.6",
|
||||
"@snort/system": "^1.6.1",
|
||||
"@snort/system-react": "^1.6.1",
|
||||
"@snort/system-wasm": "^1.0.5",
|
||||
"@snort/wallet": "^0.2.1",
|
||||
"@snort/worker-relay": "^1.3.0",
|
||||
"@snort/wallet": "^0.2.4",
|
||||
"@snort/worker-relay": "^1.3.1",
|
||||
"@szhsin/react-menu": "^4.1.0",
|
||||
"@types/webscopeio__react-textarea-autocomplete": "^4.7.5",
|
||||
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
|
||||
|
@ -8,8 +8,10 @@ export const GOAL = 9041 as EventKind;
|
||||
export const USER_CARDS = 17_777 as EventKind;
|
||||
export const CARD = 37_777 as EventKind;
|
||||
|
||||
export const VIDEO_KIND = 34_235 as EventKind;
|
||||
export const SHORTS_KIND = 34_236 as EventKind;
|
||||
export const VIDEO_KIND = 21 as EventKind;
|
||||
export const SHORTS_KIND = 22 as EventKind;
|
||||
export const OLD_VIDEO_KIND = 34_235 as EventKind;
|
||||
export const OLD_SHORTS_KIND = 34_236 as EventKind;
|
||||
|
||||
export const MINUTE = 60;
|
||||
export const HOUR = 60 * MINUTE;
|
||||
|
@ -5,13 +5,15 @@ import { Goal } from "./goal";
|
||||
import { Note } from "./note";
|
||||
import { EmojiPack } from "./emoji-pack";
|
||||
import { BadgeInfo } from "./badge";
|
||||
import { GOAL, LIVE_STREAM_CLIP, StreamState } from "@/const";
|
||||
import { GOAL, LIVE_STREAM_CLIP, OLD_SHORTS_KIND, OLD_VIDEO_KIND, SHORTS_KIND, StreamState, VIDEO_KIND } from "@/const";
|
||||
import { useEventFeed } from "@snort/system-react";
|
||||
import LiveStreamClip from "./stream/clip";
|
||||
import { ExternalLink } from "./external-link";
|
||||
import { extractStreamInfo } from "@/utils";
|
||||
import LiveVideoPlayer from "./stream/live-video-player";
|
||||
import { HTMLProps } from "react";
|
||||
import { HTMLProps, ReactNode } from "react";
|
||||
import { ShortPage } from "@/pages/short";
|
||||
import { VideoPage } from "@/pages/video";
|
||||
|
||||
interface EventProps {
|
||||
link: NostrLink;
|
||||
@ -31,36 +33,49 @@ export function EventIcon({ kind }: { kind?: EventKind }) {
|
||||
}
|
||||
|
||||
export function NostrEvent({ ev }: { ev: TaggedNostrEvent }) {
|
||||
const link = NostrLink.fromEvent(ev);
|
||||
function modalPage(inner: ReactNode) {
|
||||
return <div className="rounded-2xl px-4 py-3 md:w-[700px] mx-auto w-full">{inner}</div>;
|
||||
}
|
||||
|
||||
switch (ev.kind) {
|
||||
case GOAL: {
|
||||
return <Goal ev={ev} />;
|
||||
return modalPage(<Goal ev={ev} />);
|
||||
}
|
||||
case EventKind.EmojiSet: {
|
||||
return <EmojiPack ev={ev} />;
|
||||
return modalPage(<EmojiPack ev={ev} />);
|
||||
}
|
||||
case EventKind.Badge: {
|
||||
return <BadgeInfo ev={ev} />;
|
||||
return modalPage(<BadgeInfo ev={ev} />);
|
||||
}
|
||||
case EventKind.TextNote: {
|
||||
return <Note ev={ev} />;
|
||||
return modalPage(<Note ev={ev} />);
|
||||
}
|
||||
case LIVE_STREAM_CLIP: {
|
||||
return <LiveStreamClip ev={ev} />;
|
||||
return modalPage(<LiveStreamClip ev={ev} />);
|
||||
}
|
||||
case OLD_SHORTS_KIND:
|
||||
case SHORTS_KIND: {
|
||||
return <ShortPage link={link} evPreload={ev} />;
|
||||
}
|
||||
case OLD_VIDEO_KIND:
|
||||
case VIDEO_KIND: {
|
||||
return <VideoPage link={link} evPreload={ev} />;
|
||||
}
|
||||
case EventKind.LiveEvent: {
|
||||
const info = extractStreamInfo(ev);
|
||||
return (
|
||||
return modalPage(
|
||||
<LiveVideoPlayer
|
||||
link={link}
|
||||
title={info.title}
|
||||
status={info.status}
|
||||
stream={info.status === StreamState.Live ? info.stream : info.recording}
|
||||
poster={info.image}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
}
|
||||
default: {
|
||||
const link = NostrLink.fromEvent(ev);
|
||||
return <ExternalLink href={`https://snort.social/${link.encode()}`}>{link.encode()}</ExternalLink>;
|
||||
return modalPage(<ExternalLink href={`https://snort.social/${link.encode()}`}>{link.encode()}</ExternalLink>);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -145,7 +145,7 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const startsTimestamp = parseInt(start ?? (new Date() / 1000));
|
||||
const startsTimestamp = Number(start ?? (new Date().getTime() / 1000));
|
||||
const startsDate = new Date(startsTimestamp * 1000);
|
||||
|
||||
return (
|
||||
|
@ -1,13 +1,10 @@
|
||||
import { unwrap } from "@snort/shared";
|
||||
import { NostrLink, NostrPrefix, RequestBuilder, TaggedNostrEvent } from "@snort/system";
|
||||
import { useRequestBuilder } from "@snort/system-react";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { LIVE_STREAM } from "@/const";
|
||||
import { getHost } from "@/utils";
|
||||
|
||||
export function useCurrentStreamFeed(link: NostrLink, leaveOpen = false, evPreload?: TaggedNostrEvent) {
|
||||
const author = link.type === NostrPrefix.Address ? unwrap(link.author) : link.id;
|
||||
const sub = useMemo(() => {
|
||||
const b = new RequestBuilder(`current-event:${link.id}`);
|
||||
b.withOptions({
|
||||
@ -16,14 +13,8 @@ export function useCurrentStreamFeed(link: NostrLink, leaveOpen = false, evPrelo
|
||||
if (link.type === NostrPrefix.PublicKey || link.type === NostrPrefix.Profile) {
|
||||
b.withFilter().authors([link.id]).kinds([LIVE_STREAM]);
|
||||
b.withFilter().tag("p", [link.id]).kinds([LIVE_STREAM]);
|
||||
} else if (link.type === NostrPrefix.Address) {
|
||||
const f = b.withFilter().tag("d", [link.id]);
|
||||
if (link.author) {
|
||||
f.authors([link.author]);
|
||||
}
|
||||
if (link.kind) {
|
||||
f.kinds([link.kind]);
|
||||
}
|
||||
} else {
|
||||
b.withFilter().link(link);
|
||||
}
|
||||
return b;
|
||||
}, [link.id, leaveOpen]);
|
||||
@ -31,9 +22,7 @@ export function useCurrentStreamFeed(link: NostrLink, leaveOpen = false, evPrelo
|
||||
const q = useRequestBuilder(sub);
|
||||
|
||||
return useMemo(() => {
|
||||
const hosting = [...q, ...(evPreload ? [evPreload] : [])]
|
||||
.filter(a => getHost(a) === author || a.pubkey === author)
|
||||
.sort((a, b) => (b.created_at > a.created_at ? 1 : -1));
|
||||
const hosting = [...q, ...(evPreload ? [evPreload] : [])].sort((a, b) => (b.created_at > a.created_at ? 1 : -1));
|
||||
return hosting.at(0);
|
||||
}, [q]);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { SHORTS_KIND, VIDEO_KIND } from "@/const";
|
||||
import { OLD_SHORTS_KIND, OLD_VIDEO_KIND, SHORTS_KIND, VIDEO_KIND } from "@/const";
|
||||
import { MediaPayload, VideoInfo } from "@/service/video/info";
|
||||
import { findTag } from "@/utils";
|
||||
import { NostrEvent, TaggedNostrEvent } from "@snort/system";
|
||||
@ -58,7 +58,7 @@ export function useDeadLink(ev: TaggedNostrEvent | NostrEvent) {
|
||||
|
||||
useEffect(() => {
|
||||
const links =
|
||||
ev.kind === VIDEO_KIND || ev.kind === SHORTS_KIND
|
||||
ev.kind === VIDEO_KIND || ev.kind === SHORTS_KIND || ev.kind == OLD_SHORTS_KIND || ev.kind == OLD_VIDEO_KIND
|
||||
? VideoInfo.parse(ev)?.sources()
|
||||
: [
|
||||
{
|
||||
|
@ -4,12 +4,7 @@ import { removeUndefined, sanitizeRelayUrl } from "@snort/shared";
|
||||
import { Nip96Server } from "@/service/upload/nip96";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export const DefaultMediaServers = [
|
||||
//"https://media.zap.stream",
|
||||
new UnknownTag(["server", "https://nostr.build/"]),
|
||||
new UnknownTag(["server", "https://nostrcheck.me/"]),
|
||||
new UnknownTag(["server", "https://files.v0l.io/"]),
|
||||
];
|
||||
export const DefaultMediaServers = [new UnknownTag(["server", "https://nostr.download/"])];
|
||||
|
||||
export function useMediaServerList() {
|
||||
const login = useLogin();
|
||||
|
@ -221,6 +221,9 @@
|
||||
"BD0vyn": {
|
||||
"defaultMessage": "{name} created a clip"
|
||||
},
|
||||
"BHNL+v": {
|
||||
"defaultMessage": "Raw Data:"
|
||||
},
|
||||
"Bd1yEX": {
|
||||
"defaultMessage": "New Stream Goal"
|
||||
},
|
||||
@ -641,6 +644,9 @@
|
||||
"ZcgtZo": {
|
||||
"defaultMessage": "We've put together a list of easy-to-use exchanges that will allow you to buy a small amount of bitcoin (sats) and to transfer them to your wallet of choice."
|
||||
},
|
||||
"Zfr//4": {
|
||||
"defaultMessage": "No duration provided, please try another upload server."
|
||||
},
|
||||
"ZmqxZs": {
|
||||
"defaultMessage": "You can change this later"
|
||||
},
|
||||
@ -705,6 +711,9 @@
|
||||
"dkUMIH": {
|
||||
"defaultMessage": "Clip by {name}"
|
||||
},
|
||||
"dqGkI+": {
|
||||
"defaultMessage": "Video durations vary too much, are you sure each variant is the same video?"
|
||||
},
|
||||
"e011kf": {
|
||||
"defaultMessage": "FAQ",
|
||||
"description": "Title: FAQ page"
|
||||
@ -945,6 +954,9 @@
|
||||
"vP4dFa": {
|
||||
"defaultMessage": "Visit {link} to get some sweet zap.stream merch!"
|
||||
},
|
||||
"vaZKTn": {
|
||||
"defaultMessage": "Add more content"
|
||||
},
|
||||
"vrTOHJ": {
|
||||
"defaultMessage": "{amount} sats"
|
||||
},
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { LIVE_STREAM, SHORTS_KIND, VIDEO_KIND } from "@/const";
|
||||
import { LIVE_STREAM, OLD_SHORTS_KIND, OLD_VIDEO_KIND, SHORTS_KIND, VIDEO_KIND } from "@/const";
|
||||
import { useStreamLink } from "@/hooks/stream-link";
|
||||
import { getEventFromLocationState } from "@/utils";
|
||||
import { NostrPrefix } from "@snort/system";
|
||||
@ -20,20 +20,16 @@ export function LinkHandler() {
|
||||
if (!link) return;
|
||||
|
||||
if (link.type === NostrPrefix.Event) {
|
||||
return (
|
||||
<div className="rounded-2xl px-4 py-3 md:w-[700px] mx-auto w-full">
|
||||
<NostrEventElement link={link} />
|
||||
</div>
|
||||
);
|
||||
return <NostrEventElement link={link} />;
|
||||
} else if (link.kind === LIVE_STREAM || link.type === NostrPrefix.PublicKey) {
|
||||
return (
|
||||
<div className={classNames(layoutContext.showHeader ? "h-[calc(100dvh-44px)]" : "h-[calc(100dvh)]", "w-full")}>
|
||||
<StreamPage link={link} evPreload={evPreload} />
|
||||
</div>
|
||||
);
|
||||
} else if (link.kind === VIDEO_KIND) {
|
||||
} else if (link.kind === VIDEO_KIND || link.kind === OLD_VIDEO_KIND) {
|
||||
return <VideoPage link={link} evPreload={evPreload} />;
|
||||
} else if (link.kind === SHORTS_KIND) {
|
||||
} else if (link.kind === SHORTS_KIND || link.kind === OLD_SHORTS_KIND) {
|
||||
return <ShortPage link={link} evPreload={evPreload} />;
|
||||
} else {
|
||||
return (
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { SHORTS_KIND } from "@/const";
|
||||
import { OLD_SHORTS_KIND, SHORTS_KIND } from "@/const";
|
||||
import VideoGrid from "@/element/video-grid";
|
||||
import { VideoTile } from "@/element/video/video-tile";
|
||||
import { findTag } from "@/utils";
|
||||
@ -8,13 +8,12 @@ import { FormattedMessage } from "react-intl";
|
||||
|
||||
export function ShortsPage() {
|
||||
const rb = new RequestBuilder("shorts");
|
||||
rb.withFilter().kinds([SHORTS_KIND]);
|
||||
rb.withFilter().kinds([SHORTS_KIND, OLD_SHORTS_KIND]);
|
||||
|
||||
const videos = useRequestBuilder(rb);
|
||||
|
||||
const sorted = videos.sort((a, b) => {
|
||||
const pubA = findTag(a, "published_at");
|
||||
const pubB = findTag(b, "published_at");
|
||||
const pubA = findTag(a, "published_at") ?? a.created_at;
|
||||
const pubB = findTag(b, "published_at") ?? b.created_at;
|
||||
return Number(pubA) > Number(pubB) ? -1 : 1;
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { VIDEO_KIND } from "@/const";
|
||||
import { SHORTS_KIND, VIDEO_KIND } from "@/const";
|
||||
import { DefaultButton, IconButton, Layer3Button, PrimaryButton, WarningButton } from "@/element/buttons";
|
||||
import { Icon } from "@/element/icon";
|
||||
import Modal from "@/element/modal";
|
||||
@ -9,11 +9,11 @@ import { ServerList } from "@/element/upload/server-list";
|
||||
import useImgProxy from "@/hooks/img-proxy";
|
||||
import { useLogin } from "@/hooks/login";
|
||||
import { useMediaServerList } from "@/hooks/media-servers";
|
||||
import { Nip94Tags, UploadResult, nip94TagsToIMeta } from "@/service/upload";
|
||||
import { Nip94Tags, UploadResult, nip94TagsToIMeta, readNip94Tags } from "@/service/upload";
|
||||
import { Nip96Server } from "@/service/upload/nip96";
|
||||
import { openFile } from "@/utils";
|
||||
import { ExternalStore, removeUndefined, unixNow, unwrap } from "@snort/shared";
|
||||
import { EventPublisher, NostrLink } from "@snort/system";
|
||||
import { ExternalStore, removeUndefined, unwrap } from "@snort/shared";
|
||||
import { EventBuilder, EventPublisher, NostrEvent, NostrLink } from "@snort/system";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
import { useContext, useEffect, useState, useSyncExternalStore } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
@ -69,6 +69,23 @@ class UploadManager extends ExternalStore<Array<UploadStatus>> {
|
||||
}
|
||||
}
|
||||
|
||||
addUpload(server: string, file: NostrEvent, meta: Nip94Tags, type: UploadStatus["type"]) {
|
||||
const name = file.content ?? meta.summary ?? meta.alt ?? "";
|
||||
const uploadKey = `${name}:${server}:${type}`;
|
||||
this.#uploads.set(uploadKey, {
|
||||
type,
|
||||
name,
|
||||
size: meta.size ?? 0,
|
||||
server,
|
||||
result: {
|
||||
url: meta.url,
|
||||
header: file,
|
||||
metadata: meta,
|
||||
},
|
||||
});
|
||||
this.notifyChange();
|
||||
}
|
||||
|
||||
async uploadTo(server: string, file: File, pub: EventPublisher, type: UploadStatus["type"]) {
|
||||
let uploader = this.#uploaders.get(server);
|
||||
if (!uploader) {
|
||||
@ -130,6 +147,27 @@ class UploadManager extends ExternalStore<Array<UploadStatus>> {
|
||||
return resGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the [min, max] duration from all variants
|
||||
*/
|
||||
duration() {
|
||||
const uploads = this.snapshot();
|
||||
return uploads.reduce(
|
||||
(acc, v) => {
|
||||
if (v.result?.metadata?.duration) {
|
||||
if (acc[1] < v.result.metadata.duration) {
|
||||
acc[1] = v.result.metadata.duration;
|
||||
}
|
||||
if (acc[0] > v.result.metadata.duration) {
|
||||
acc[0] = v.result.metadata.duration;
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[1_000_000, 0],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the `imeta` tag for this upload
|
||||
*/
|
||||
@ -187,21 +225,24 @@ export function UploadPage() {
|
||||
return error.length == 0 && uploads.length > 0 && uploads.every(a => a.result !== undefined);
|
||||
}
|
||||
|
||||
async function publish() {
|
||||
const pub = login?.publisher();
|
||||
if (!pub) return;
|
||||
const ev = await pub.generic(eb => {
|
||||
eb.kind(VIDEO_KIND);
|
||||
eb.tag(["d", manager.id]);
|
||||
eb.tag(["title", title]);
|
||||
eb.tag(["published_at", unixNow().toString()]);
|
||||
eb.content(summary);
|
||||
function makeEvent() {
|
||||
const duration = manager.duration();
|
||||
const eb = new EventBuilder()
|
||||
.pubKey(login?.pubkey ?? "00".repeat(31))
|
||||
.kind(duration[1] <= 60 ? SHORTS_KIND : VIDEO_KIND)
|
||||
.tag(["title", title])
|
||||
.content(summary);
|
||||
|
||||
const imeta = manager.makeIMeta();
|
||||
imeta.forEach(a => eb.tag(a));
|
||||
|
||||
return eb;
|
||||
});
|
||||
}
|
||||
|
||||
async function publish() {
|
||||
const pub = login?.publisher();
|
||||
if (!pub) return;
|
||||
const ev = await makeEvent().buildAndSign(pub.signer);
|
||||
console.debug(ev);
|
||||
await system.BroadcastEvent(ev);
|
||||
navigate(`/${NostrLink.fromEvent(ev).encode()}`);
|
||||
@ -229,7 +270,16 @@ export function UploadPage() {
|
||||
const data = await rsp.blob();
|
||||
const pub = login?.publisher();
|
||||
if (pub) {
|
||||
servers.servers.forEach(b => manager.uploadTo(b, new File([data], "thumb.jpg"), pub, "thumb"));
|
||||
servers.servers.forEach(b =>
|
||||
manager.uploadTo(
|
||||
b,
|
||||
new File([data], "thumb.jpg", {
|
||||
type: "image/jpeg",
|
||||
}),
|
||||
pub,
|
||||
"thumb",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -291,6 +341,17 @@ export function UploadPage() {
|
||||
}),
|
||||
);
|
||||
}
|
||||
const d = manager.duration();
|
||||
if (d[0] === 0 || d[1] === 0) {
|
||||
err.push(formatMessage({ defaultMessage: "No duration provided, please try another upload server." }));
|
||||
}
|
||||
if (Math.abs(d[0] - d[1]) >= 0.5) {
|
||||
err.push(
|
||||
formatMessage({
|
||||
defaultMessage: "Video durations vary too much, are you sure each variant is the same video?",
|
||||
}),
|
||||
);
|
||||
}
|
||||
setError(err);
|
||||
}
|
||||
|
||||
@ -312,6 +373,7 @@ export function UploadPage() {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-xl:w-full xl:w-[1200px] xl:mx-auto grid gap-6 xl:grid-cols-[auto_350px] max-xl:px-4">
|
||||
<div className="flex flex-col gap-6">
|
||||
@ -359,8 +421,11 @@ export function UploadPage() {
|
||||
))}
|
||||
</div>
|
||||
{videos > 0 && (
|
||||
<div onClick={() => uploadFile()} className="cursor-pointer">
|
||||
{uploadButton()}
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-xl">
|
||||
<FormattedMessage defaultMessage="Add more content" />
|
||||
</div>
|
||||
<div className="flex gap-4 items-center">{uploadButton()}</div>
|
||||
</div>
|
||||
)}
|
||||
{uploads.length > 0 && (
|
||||
@ -377,7 +442,7 @@ export function UploadPage() {
|
||||
<FormattedMessage defaultMessage="Thumbnail" />
|
||||
</div>
|
||||
<div className="border border-layer-3 border-dashed border-2 rounded-xl aspect-video overflow-hidden">
|
||||
{thumb && <img src={proxy(thumb)} className="w-full h-full" />}
|
||||
{thumb && <img src={proxy(thumb)} className="w-full h-full object-contain" />}
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<DefaultButton onClick={() => uploadThumb()}>
|
||||
@ -405,9 +470,13 @@ export function UploadPage() {
|
||||
</WarningButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FormattedMessage defaultMessage="Raw Data:" />
|
||||
<pre className="text-xs font-mono overflow-wrap text-pretty">
|
||||
{JSON.stringify(manager.makeIMeta(), undefined, 2)}
|
||||
{JSON.stringify(makeEvent().build(), undefined, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
{editServers && (
|
||||
<Modal id="server-list" onClose={() => setEditServers(false)}>
|
||||
<ServerList />
|
||||
@ -416,7 +485,19 @@ export function UploadPage() {
|
||||
{mediaPicker && (
|
||||
<Modal id="media-picker" onClose={() => setMediaPicker(false)} largeModal={true}>
|
||||
<MediaServerFileList
|
||||
onPicked={() => {
|
||||
onPicked={files => {
|
||||
files.forEach(f => {
|
||||
const meta = readNip94Tags(f.tags);
|
||||
if (meta.url) {
|
||||
const url = new URL(meta.url);
|
||||
manager.addUpload(
|
||||
`${url.protocol}//${url.host}/`,
|
||||
f,
|
||||
meta,
|
||||
meta.mimeType?.startsWith("image/") ?? false ? "thumb" : "video",
|
||||
);
|
||||
}
|
||||
});
|
||||
setMediaPicker(false);
|
||||
}}
|
||||
/>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { VIDEO_KIND } from "@/const";
|
||||
import { OLD_VIDEO_KIND, VIDEO_KIND } from "@/const";
|
||||
import VideoGrid from "@/element/video-grid";
|
||||
import { findTag, getHost } from "@/utils";
|
||||
import { NostrLink, RequestBuilder } from "@snort/system";
|
||||
@ -11,7 +11,7 @@ export function VideosPage() {
|
||||
const login = useLogin();
|
||||
|
||||
const rb = new RequestBuilder("videos");
|
||||
rb.withFilter().kinds([VIDEO_KIND]);
|
||||
rb.withFilter().kinds([VIDEO_KIND, OLD_VIDEO_KIND]);
|
||||
|
||||
const videos = useRequestBuilder(rb);
|
||||
|
||||
@ -22,8 +22,8 @@ export function VideosPage() {
|
||||
return (login?.state?.muted.length ?? 0) === 0 || !login?.state?.muted.some(a => a.equals(link));
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const pubA = findTag(a, "published_at");
|
||||
const pubB = findTag(b, "published_at");
|
||||
const pubA = findTag(a, "published_at") ?? a.created_at;
|
||||
const pubB = findTag(b, "published_at") ?? b.created_at;
|
||||
return Number(pubA) > Number(pubB) ? -1 : 1;
|
||||
});
|
||||
|
||||
|
@ -16,6 +16,8 @@ export interface Nip94Tags {
|
||||
summary?: string;
|
||||
alt?: string;
|
||||
fallback?: Array<string>;
|
||||
duration?: number;
|
||||
bitrate?: number;
|
||||
}
|
||||
|
||||
export interface UploadResult {
|
||||
@ -103,6 +105,14 @@ export function readNip94Tags(tags: Array<Array<string>>) {
|
||||
res.fallback.push(v);
|
||||
break;
|
||||
}
|
||||
case "duration": {
|
||||
res.duration = Number(v);
|
||||
break;
|
||||
}
|
||||
case "bitrate": {
|
||||
res.bitrate = Number(v);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
@ -126,6 +136,8 @@ export function nip94TagsToIMeta(meta: Nip94Tags) {
|
||||
ifPush("thumb", meta.thumb);
|
||||
ifPush("summary", meta.summary);
|
||||
ifPush("alt", meta.alt);
|
||||
ifPush("duration", meta.duration);
|
||||
ifPush("bitrate", meta.bitrate);
|
||||
if (meta.image) {
|
||||
meta.image.forEach(a => ifPush("image", a));
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ export class Nip96Server {
|
||||
const fd = new FormData();
|
||||
fd.append("size", file.size.toString());
|
||||
fd.append("caption", filename);
|
||||
fd.append("media_type", file.type);
|
||||
fd.append("content_type", file.type);
|
||||
fd.append("file", file);
|
||||
|
||||
const rsp = await this.#req("", "POST", fd);
|
||||
|
@ -2,7 +2,6 @@ import { NostrEvent } from "@snort/system";
|
||||
import { GameInfo } from "../game-database";
|
||||
import { Nip94Tags, readNip94Tags, readNip94TagsFromIMeta } from "../upload";
|
||||
import { getHost, sortStreamTags, extractGameTag, findTag } from "@/utils";
|
||||
import { unwrap } from "@snort/shared";
|
||||
|
||||
export interface MediaPayload {
|
||||
url: string;
|
||||
@ -19,9 +18,12 @@ export class VideoInfo {
|
||||
goal?: string;
|
||||
gameId?: string;
|
||||
gameInfo?: GameInfo;
|
||||
duration?: number;
|
||||
publishedAt?: number;
|
||||
|
||||
get duration() {
|
||||
return this.media.find(m => m.duration)?.duration;
|
||||
}
|
||||
|
||||
constructor(
|
||||
readonly host: string,
|
||||
readonly id: string,
|
||||
@ -31,28 +33,13 @@ export class VideoInfo {
|
||||
|
||||
static parse(ev: NostrEvent) {
|
||||
const { regularTags, prefixedTags } = sortStreamTags(ev.tags);
|
||||
const ret = new VideoInfo(getHost(ev), unwrap(findTag(ev, "d")), regularTags, VideoInfo.#parseMediaTags(ev.tags));
|
||||
const ret = new VideoInfo(getHost(ev), findTag(ev, "d") ?? ev.id, regularTags, VideoInfo.#parseMediaTags(ev.tags));
|
||||
|
||||
const matchInto = <K extends keyof VideoInfo>(
|
||||
tag: Array<string>,
|
||||
key: string,
|
||||
into: K,
|
||||
fn?: (v: string) => never,
|
||||
) => {
|
||||
if (tag[0] === key) {
|
||||
ret[into] = fn ? fn(tag[1]) : (tag[1] as never);
|
||||
}
|
||||
};
|
||||
|
||||
for (const t of ev.tags) {
|
||||
matchInto(t, "d", "id");
|
||||
matchInto(t, "title", "title");
|
||||
matchInto(t, "summary", "summary");
|
||||
matchInto(t, "content-warning", "contentWarning");
|
||||
matchInto(t, "goal", "goal");
|
||||
matchInto(t, "duration", "duration");
|
||||
matchInto(t, "published_at", "publishedAt");
|
||||
}
|
||||
ret.title = findTag(ev, "title");
|
||||
ret.summary = findTag(ev, "summary") ?? ev.content;
|
||||
ret.contentWarning = findTag(ev, "content-warning");
|
||||
ret.goal = findTag(ev, "goal");
|
||||
ret.publishedAt = Number(findTag(ev, "published_at") ?? ev.created_at);
|
||||
|
||||
const { gameInfo, gameId } = extractGameTag(prefixedTags);
|
||||
ret.gameId = gameId;
|
||||
|
@ -73,6 +73,7 @@
|
||||
"Axo/o5": "Science & Technology",
|
||||
"AyGauy": "Login",
|
||||
"BD0vyn": "{name} created a clip",
|
||||
"BHNL+v": "Raw Data:",
|
||||
"Bd1yEX": "New Stream Goal",
|
||||
"Bep/gA": "Private key",
|
||||
"BzQPM+": "Destination",
|
||||
@ -211,6 +212,7 @@
|
||||
"ZXp0z1": "Features",
|
||||
"ZaNcK4": "No goals yet",
|
||||
"ZcgtZo": "We've put together a list of easy-to-use exchanges that will allow you to buy a small amount of bitcoin (sats) and to transfer them to your wallet of choice.",
|
||||
"Zfr//4": "No duration provided, please try another upload server.",
|
||||
"ZmqxZs": "You can change this later",
|
||||
"ZsYhvh": "Zaps are lightning payments, which are published on nostr as receipts.",
|
||||
"Zse7yG": "Raid target",
|
||||
@ -232,6 +234,7 @@
|
||||
"dOQCL8": "Display name",
|
||||
"dVD/AR": "Top Zappers",
|
||||
"dkUMIH": "Clip by {name}",
|
||||
"dqGkI+": "Video durations vary too much, are you sure each variant is the same video?",
|
||||
"e011kf": "FAQ",
|
||||
"ebmhes": "Nostr Extension",
|
||||
"f6biFA": "Oh, and you have {n} sats of free streaming on us! 💜",
|
||||
@ -311,6 +314,7 @@
|
||||
"ug01Mk": "Time",
|
||||
"uksRSi": "Latest Videos",
|
||||
"vP4dFa": "Visit {link} to get some sweet zap.stream merch!",
|
||||
"vaZKTn": "Add more content",
|
||||
"vrTOHJ": "{amount} sats",
|
||||
"w+2Vw7": "Shorts",
|
||||
"w0Xm2F": "Start typing",
|
||||
|
44
yarn.lock
44
yarn.lock
@ -2573,14 +2573,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@snort/system-react@npm:^1.5.6":
|
||||
version: 1.5.6
|
||||
resolution: "@snort/system-react@npm:1.5.6"
|
||||
"@snort/system-react@npm:^1.6.1":
|
||||
version: 1.6.1
|
||||
resolution: "@snort/system-react@npm:1.6.1"
|
||||
dependencies:
|
||||
"@snort/shared": "npm:^1.0.17"
|
||||
"@snort/system": "npm:^1.5.6"
|
||||
"@snort/system": "npm:^1.6.1"
|
||||
react: "npm:^18.2.0"
|
||||
checksum: 10c0/b7fb8dbc87328603b202dad9ce57dbff2cb8231829128b48346ba10925ee9fa5103652941b6a0456d6221fce8379a7148dc6507ebe8e1d0d35f7efd92738d08a
|
||||
checksum: 10c0/fe3180c1f4341df4fd585c4031ef632a7d59c7637c6aed16ea254a1c22a66e288516d96939c0080efd0fb73940531855a59a24fae5f82c326f6544362eb0394c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -2591,9 +2591,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@snort/system@npm:^1.5.6":
|
||||
version: 1.5.6
|
||||
resolution: "@snort/system@npm:1.5.6"
|
||||
"@snort/system@npm:^1.6.1":
|
||||
version: 1.6.1
|
||||
resolution: "@snort/system@npm:1.6.1"
|
||||
dependencies:
|
||||
"@noble/ciphers": "npm:^0.6.0"
|
||||
"@noble/curves": "npm:^1.4.0"
|
||||
@ -2608,33 +2608,33 @@ __metadata:
|
||||
nostr-social-graph: "npm:^1.0.3"
|
||||
uuid: "npm:^9.0.0"
|
||||
ws: "npm:^8.14.0"
|
||||
checksum: 10c0/38fee2d55240f91a5e6ea0684a4bd94e6f4c56fab9b6a20d9ebef26dcdd17ed6c9a42bf1804c7bcf135a89c6882659baecede6d885dcead4a167d5e3337c9764
|
||||
checksum: 10c0/5da01450970f4a51df98369c4cdd2b8886add3480e7962d11ed17498cf7db421bcd5e585abe532ff754efc0bf26b5734b9b24005f958802357ec390ccb74d281
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@snort/wallet@npm:^0.2.1":
|
||||
version: 0.2.1
|
||||
resolution: "@snort/wallet@npm:0.2.1"
|
||||
"@snort/wallet@npm:^0.2.4":
|
||||
version: 0.2.4
|
||||
resolution: "@snort/wallet@npm:0.2.4"
|
||||
dependencies:
|
||||
"@cashu/cashu-ts": "npm:^1.0.0-rc.3"
|
||||
"@lightninglabs/lnc-web": "npm:^0.3.1-alpha"
|
||||
"@scure/base": "npm:^1.1.6"
|
||||
"@snort/shared": "npm:^1.0.17"
|
||||
"@snort/system": "npm:^1.5.6"
|
||||
"@snort/system": "npm:^1.6.1"
|
||||
debug: "npm:^4.3.4"
|
||||
eventemitter3: "npm:^5.0.1"
|
||||
checksum: 10c0/0dcf4b0336029e336bd6abcd7b79cf60d6fed08b2ab2847a8e791bb2399646e86c95aefc4dcfea08365c3dea417c362cdfa93939d7c36f037762de601936b331
|
||||
checksum: 10c0/7d3e23d1d79595ee99e041b816cec5e7bfc6f34bfce6f0864a556de82cfcf29200f6978744b0717b5f6873c5c498e2dd9005571171f1f533f235cfbadf205a44
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@snort/worker-relay@npm:^1.3.0":
|
||||
version: 1.3.0
|
||||
resolution: "@snort/worker-relay@npm:1.3.0"
|
||||
"@snort/worker-relay@npm:^1.3.1":
|
||||
version: 1.3.1
|
||||
resolution: "@snort/worker-relay@npm:1.3.1"
|
||||
dependencies:
|
||||
"@sqlite.org/sqlite-wasm": "npm:^3.46.1-build3"
|
||||
eventemitter3: "npm:^5.0.1"
|
||||
uuid: "npm:^9.0.1"
|
||||
checksum: 10c0/1a0eb175f50787bbcaa585641bf710347b59f3d3426cbf0f83182a5574bf7a63beb3e5d66bb41506e2d50c3ee904d55670c85c7f1542018936dd5a4ce06726e8
|
||||
checksum: 10c0/19b89e4f96df425d2d73e87fda1f82844bf7f3a1ba114073d0bf4052c9d5fe3eac9e6ca6d88ad8e36b65bae6dfcf69db5cb47828ef1c195419a94bd87ae2ff53
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -7354,11 +7354,11 @@ __metadata:
|
||||
"@noble/hashes": "npm:^1.4.0"
|
||||
"@scure/base": "npm:^1.1.6"
|
||||
"@snort/shared": "npm:^1.0.17"
|
||||
"@snort/system": "npm:^1.5.6"
|
||||
"@snort/system-react": "npm:^1.5.6"
|
||||
"@snort/system": "npm:^1.6.1"
|
||||
"@snort/system-react": "npm:^1.6.1"
|
||||
"@snort/system-wasm": "npm:^1.0.5"
|
||||
"@snort/wallet": "npm:^0.2.1"
|
||||
"@snort/worker-relay": "npm:^1.3.0"
|
||||
"@snort/wallet": "npm:^0.2.4"
|
||||
"@snort/worker-relay": "npm:^1.3.1"
|
||||
"@szhsin/react-menu": "npm:^4.1.0"
|
||||
"@testing-library/dom": "npm:^9.3.1"
|
||||
"@types/node": "npm:^20.12.12"
|
||||
|
Loading…
x
Reference in New Issue
Block a user