feat: Added audio / video events
This commit is contained in:
parent
1fb2d54191
commit
2286c2c18d
@ -1,7 +1,7 @@
|
|||||||
import { formatFileSize, formatDate } from '../../utils/utils';
|
import { formatFileSize, formatDate } from '../../utils/utils';
|
||||||
import { ClipboardDocumentIcon, TrashIcon } from '@heroicons/react/24/outline';
|
import { ClipboardDocumentIcon, TrashIcon } from '@heroicons/react/24/outline';
|
||||||
import { BlobDescriptor } from 'blossom-client-sdk';
|
import { BlobDescriptor } from 'blossom-client-sdk';
|
||||||
import { fetchId3Tag } from '../../utils/id3';
|
import { AudioBlob, fetchId3Tag } from '../../utils/id3';
|
||||||
import { useQueries } from '@tanstack/react-query';
|
import { useQueries } from '@tanstack/react-query';
|
||||||
import { useGlobalContext } from '../../GlobalState';
|
import { useGlobalContext } from '../../GlobalState';
|
||||||
import { PauseIcon, PlayIcon } from '@heroicons/react/24/solid';
|
import { PauseIcon, PlayIcon } from '@heroicons/react/24/solid';
|
||||||
@ -17,7 +17,10 @@ const AudioBlobList = ({ audioFiles, onDelete }: AudioBlobListProps) => {
|
|||||||
const audioFilesWithId3 = useQueries({
|
const audioFilesWithId3 = useQueries({
|
||||||
queries: audioFiles.map(af => ({
|
queries: audioFiles.map(af => ({
|
||||||
queryKey: ['id3', af.sha256],
|
queryKey: ['id3', af.sha256],
|
||||||
queryFn: async () => await fetchId3Tag(af),
|
queryFn: async () => {
|
||||||
|
const id3Tag = await fetchId3Tag(af.sha256, af.url);
|
||||||
|
return { ...af, id3: id3Tag?.id3 } as AudioBlob;
|
||||||
|
},
|
||||||
staleTime: 1000 * 60 * 5,
|
staleTime: 1000 * 60 * 5,
|
||||||
cacheTime: 1000 * 60 * 5,
|
cacheTime: 1000 * 60 * 5,
|
||||||
})),
|
})),
|
||||||
|
@ -31,7 +31,7 @@ const BlobListTypeMenu = ({ mode, setMode, hasImages, hasAudio, hasDocs, hasVide
|
|||||||
}, [hasAudio, hasDocs, hasImages, hasVideo, mode, setMode]);
|
}, [hasAudio, hasDocs, hasImages, hasVideo, mode, setMode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className="menu menu-horizontal menu-active bg-base-200 rounded-box">
|
<ul className="menu menu-horizontal menu-active bg-base-200 rounded-box gap-1">
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
className={' tooltip ' + (mode == 'list' ? 'bg-primary text-primary-content hover:bg-primary ' : '')}
|
className={' tooltip ' + (mode == 'list' ? 'bg-primary text-primary-content hover:bg-primary ' : '')}
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import { NDKEvent, NDKKind, NDKUser, NostrEvent } from '@nostr-dev-kit/ndk';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNDK } from '../../utils/ndk';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
|
||||||
import uniq from 'lodash/uniq';
|
|
||||||
import { formatFileSize } from '../../utils/utils';
|
import { formatFileSize } from '../../utils/utils';
|
||||||
import useEvents from '../../utils/useEvents';
|
import { fetchId3Tag } from '../../utils/id3';
|
||||||
|
import useVideoThumbnailDvm from './dvm';
|
||||||
|
import { usePublishing } from './usePublishing';
|
||||||
|
|
||||||
export type FileEventData = {
|
export type FileEventData = {
|
||||||
|
originalFile: File;
|
||||||
content: string;
|
content: string;
|
||||||
url: string[];
|
url: string[];
|
||||||
dim?: string;
|
dim?: string;
|
||||||
@ -17,236 +16,171 @@ export type FileEventData = {
|
|||||||
thumbnail?: string;
|
thumbnail?: string;
|
||||||
//summary: string;
|
//summary: string;
|
||||||
//alt: string;
|
//alt: string;
|
||||||
|
|
||||||
|
artist?: string;
|
||||||
|
title?: string;
|
||||||
|
album?: string;
|
||||||
|
year?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ensureDecrypted = async (dvm: NDKUser, event: NDKEvent) => {
|
|
||||||
if (!event) return undefined;
|
|
||||||
|
|
||||||
const encrypted = event.tags.some(t => t[0] == 'encrypted');
|
|
||||||
|
|
||||||
if (encrypted) {
|
|
||||||
const decryptedContent = await event.ndk?.signer?.decrypt(dvm, event.content);
|
|
||||||
|
|
||||||
if (decryptedContent) {
|
|
||||||
return {
|
|
||||||
...event,
|
|
||||||
tags: event.tags.filter(t => t[0] !== 'encrypted').concat(JSON.parse(decryptedContent)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return event;
|
|
||||||
};
|
|
||||||
|
|
||||||
const NPUB_DVM_THUMBNAIL_CREATION = 'npub1q8cv87l47fql2xer2uyw509y5n5s9f53h76hvf9377efdptmsvusxf3n8s';
|
|
||||||
|
|
||||||
const FileEventEditor = ({ data }: { data: FileEventData }) => {
|
const FileEventEditor = ({ data }: { data: FileEventData }) => {
|
||||||
const [fileEventData, setFileEventData] = useState(data);
|
const [fileEventData, setFileEventData] = useState(data);
|
||||||
const [thumbnailRequestEventId, setThumbnailRequestEventId] = useState<string | undefined>();
|
const { createDvmThumbnailRequest, thumbnailRequestEventId } = useVideoThumbnailDvm(setFileEventData);
|
||||||
const { ndk, user } = useNDK();
|
const { publishAudioEvent, publishFileEvent, publishVideoEvent } = usePublishing();
|
||||||
const dvm = ndk.getUser({ npub: NPUB_DVM_THUMBNAIL_CREATION });
|
|
||||||
|
|
||||||
const thumbnailDvmFilter = useMemo(
|
|
||||||
() => ({ kinds: [6204 as NDKKind], '#e': [thumbnailRequestEventId || ''] }),
|
|
||||||
[thumbnailRequestEventId]
|
|
||||||
);
|
|
||||||
const thumbnailSubscription = useEvents(thumbnailDvmFilter, {
|
|
||||||
closeOnEose: false,
|
|
||||||
disable: thumbnailRequestEventId == undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const doASync = async () => {
|
|
||||||
const firstEvent = await ensureDecrypted(dvm, thumbnailSubscription.events[0]);
|
|
||||||
if (firstEvent) {
|
|
||||||
const urls = firstEvent.tags.filter(t => t[0] === 'thumb').map(t => t[1]);
|
|
||||||
const dim = firstEvent.tags.find(t => t[0] === 'dim')?.[1];
|
|
||||||
setFileEventData(ed => ({ ...ed, thumbnails: urls, dim, thumbnail: urls[0] }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
doASync();
|
|
||||||
}, [thumbnailSubscription.events]);
|
|
||||||
|
|
||||||
const publishFileEvent = async (data: FileEventData) => {
|
|
||||||
// TODO REupload selected video thumbnail from DVM
|
|
||||||
|
|
||||||
const e: NostrEvent = {
|
|
||||||
created_at: dayjs().unix(),
|
|
||||||
content: data.content,
|
|
||||||
tags: [
|
|
||||||
...uniq(data.url).map(du => ['url', du]),
|
|
||||||
['x', data.x],
|
|
||||||
//['summary', data.summary],
|
|
||||||
//['alt', data.alt],
|
|
||||||
],
|
|
||||||
kind: 1063,
|
|
||||||
pubkey: user?.pubkey || '',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (data.size) {
|
|
||||||
e.tags.push(['size', `${data.size}`]);
|
|
||||||
}
|
|
||||||
if (data.dim) {
|
|
||||||
e.tags.push(['dim', data.dim]);
|
|
||||||
}
|
|
||||||
if (data.m) {
|
|
||||||
e.tags.push(['m', data.m]);
|
|
||||||
}
|
|
||||||
if (data.thumbnail) {
|
|
||||||
e.tags.push(['thumb', data.thumbnail]);
|
|
||||||
e.tags.push(['image', data.thumbnail]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ev = new NDKEvent(ndk, e);
|
|
||||||
await ev.sign();
|
|
||||||
console.log(ev.rawEvent());
|
|
||||||
// await ev.publish();
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
async function createDvmBlossemAuthToken() {
|
|
||||||
const pubkey = ndk.activeUser?.pubkey;
|
|
||||||
if (!ndk.signer || !pubkey) return;
|
|
||||||
const tenMinutes = () => dayjs().unix() + 10 * 60;
|
|
||||||
const authEvent = ({
|
|
||||||
pubkey,
|
|
||||||
created_at: dayjs().unix(),
|
|
||||||
kind: 24242,
|
|
||||||
content: 'Upload thumbail',
|
|
||||||
tags: [
|
|
||||||
['t', 'upload'],
|
|
||||||
['name', `thumb_${Math.random().toString(36).substring(2)}`], // make sure the auth events are unique
|
|
||||||
['expiration', String(tenMinutes())],
|
|
||||||
],
|
|
||||||
});
|
|
||||||
const ev = new NDKEvent(ndk, authEvent);
|
|
||||||
await ev.sign();
|
|
||||||
console.log(JSON.stringify(ev.rawEvent()));
|
|
||||||
return btoa(JSON.stringify(ev.rawEvent()));
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
const getThumbnails = async (data: FileEventData) => {
|
|
||||||
if (!ndk.signer) return;
|
|
||||||
|
|
||||||
const thumbCount = 3;
|
|
||||||
|
|
||||||
/*s
|
|
||||||
const authTokens = [];
|
|
||||||
for (let i = 0; i < thumbCount; i++) {
|
|
||||||
const uploadAuth = await createDvmBlossemAuthToken();
|
|
||||||
if (uploadAuth) {
|
|
||||||
authTokens.push(['param', 'authToken', uploadAuth]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
const e: NostrEvent = {
|
|
||||||
created_at: dayjs().unix(),
|
|
||||||
content: await ndk.signer?.encrypt(
|
|
||||||
dvm,
|
|
||||||
JSON.stringify([
|
|
||||||
['i', data.url[0], 'url'],
|
|
||||||
['output', 'image/jpeg'],
|
|
||||||
['param', 'thumbnailCount', `${thumbCount}`],
|
|
||||||
['relays', user?.relayUrls.join(',') || ndk.explicitRelayUrls?.join(',') || ''],
|
|
||||||
])
|
|
||||||
),
|
|
||||||
tags: [
|
|
||||||
['p', dvm.pubkey],
|
|
||||||
['encrypted'],
|
|
||||||
// TODO set expiration
|
|
||||||
],
|
|
||||||
kind: 5204,
|
|
||||||
pubkey: user?.pubkey || '',
|
|
||||||
};
|
|
||||||
const ev = new NDKEvent(ndk, e);
|
|
||||||
await ev.sign();
|
|
||||||
console.log(ev.rawEvent());
|
|
||||||
setThumbnailRequestEventId(ev.id);
|
|
||||||
await ev.publish();
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (fileEventData.m?.startsWith('video/') && fileEventData.thumbnails == undefined) {
|
if (fileEventData.m?.startsWith('video/') && fileEventData.thumbnails == undefined) {
|
||||||
getThumbnails(fileEventData);
|
createDvmThumbnailRequest(fileEventData);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
fileEventData.m?.startsWith('audio/') &&
|
||||||
|
!(
|
||||||
|
fileEventData.title ||
|
||||||
|
fileEventData.artist ||
|
||||||
|
fileEventData.album ||
|
||||||
|
fileEventData.year ||
|
||||||
|
fileEventData.thumbnail
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
console.log('getting id3 cover image', fileEventData.x, fileEventData.url[0], fileEventData.originalFile);
|
||||||
|
fetchId3Tag(fileEventData.x, fileEventData.url[0], fileEventData.originalFile).then(res => {
|
||||||
|
if (!res) return;
|
||||||
|
|
||||||
|
const { id3 } = res;
|
||||||
|
console.log(res.coverFull);
|
||||||
|
setFileEventData({
|
||||||
|
...fileEventData,
|
||||||
|
artist: id3.artist,
|
||||||
|
album: id3.album,
|
||||||
|
title: id3.title,
|
||||||
|
year: id3.year,
|
||||||
|
thumbnail: res.coverFull,
|
||||||
|
thumbnails: res.coverFull ? [res.coverFull] : [],
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [fileEventData]);
|
}, [fileEventData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className=" bg-base-200 rounded-xl p-4 text-neutral-content gap-4 flex flex-row">
|
<>
|
||||||
{fileEventData.m?.startsWith('video/') && (
|
<div className=" bg-base-200 rounded-xl p-4 text-neutral-content gap-4 flex flex-row">
|
||||||
<>
|
{fileEventData.m?.startsWith('video/') && (
|
||||||
{thumbnailRequestEventId &&
|
|
||||||
(fileEventData.thumbnails && fileEventData.thumbnails.length > 0 ? (
|
|
||||||
<div className="w-2/6">
|
|
||||||
<div className="carousel w-full">
|
|
||||||
{fileEventData.thumbnails.map((t, i) => (
|
|
||||||
<div id={`item${i + 1}`} key={`item${i + 1}`} className="carousel-item w-full">
|
|
||||||
<img src={t} className="w-full" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-center w-full py-2 gap-2">
|
|
||||||
{fileEventData.thumbnails.map((t, i) => (
|
|
||||||
<a
|
|
||||||
key={`link${i + 1}`}
|
|
||||||
href={`#item${i + 1}`}
|
|
||||||
onClick={() => setFileEventData(ed => ({ ...ed, thumbnail: t }))}
|
|
||||||
className={'btn btn-xs ' + (t == fileEventData.thumbnail ? 'btn-primary' : '')}
|
|
||||||
>{`${i + 1}`}</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
Creating previews <span className="loading loading-spinner loading-md"></span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{fileEventData.m?.startsWith('image/') && (
|
|
||||||
<div className="p-4 bg-base-300 w-2/6">
|
|
||||||
<img
|
|
||||||
width={300}
|
|
||||||
height={300}
|
|
||||||
src={`https://images.slidestr.net/insecure/f:webp/rs:fill:300/plain/${fileEventData.url[0]}`}
|
|
||||||
></img>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="grid gap-4 w-4/6" style={{ gridTemplateColumns: '1fr 30em' }}>
|
|
||||||
<span className="font-bold">Type</span>
|
|
||||||
<span>{fileEventData.m}</span>
|
|
||||||
|
|
||||||
{fileEventData.dim && (
|
|
||||||
<>
|
<>
|
||||||
<span className="font-bold">Dimensions</span>
|
{thumbnailRequestEventId &&
|
||||||
<span>{fileEventData.dim}</span>
|
(fileEventData.thumbnails && fileEventData.thumbnails.length > 0 ? (
|
||||||
|
<div className="w-2/6">
|
||||||
|
<div className="carousel w-full">
|
||||||
|
{fileEventData.thumbnails.map((t, i) => (
|
||||||
|
<div id={`item${i + 1}`} key={`item${i + 1}`} className="carousel-item w-full">
|
||||||
|
<img src={t} className="w-full" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-center w-full py-2 gap-2">
|
||||||
|
{fileEventData.thumbnails.map((t, i) => (
|
||||||
|
<a
|
||||||
|
key={`link${i + 1}`}
|
||||||
|
href={`#item${i + 1}`}
|
||||||
|
onClick={() => setFileEventData(ed => ({ ...ed, thumbnail: t }))}
|
||||||
|
className={'btn btn-xs ' + (t == fileEventData.thumbnail ? 'btn-primary' : '')}
|
||||||
|
>{`${i + 1}`}</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
Creating previews <span className="loading loading-spinner loading-md"></span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{fileEventData.m?.startsWith('audio/') && fileEventData.thumbnail && (
|
||||||
|
<div className="w-2/6">
|
||||||
|
<img src={fileEventData.thumbnail} className="w-full" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<span className="font-bold">File size</span>
|
{fileEventData.m?.startsWith('image/') && (
|
||||||
<span>{fileEventData.size ? formatFileSize(fileEventData.size) : 'unknown'}</span>
|
<div className="p-4 bg-base-300 w-2/6">
|
||||||
<span className="font-bold">Content / Description</span>
|
<img
|
||||||
<textarea
|
width={300}
|
||||||
value={fileEventData.content}
|
height={300}
|
||||||
onChange={e => setFileEventData(ed => ({ ...ed, content: e.target.value }))}
|
src={`https://images.slidestr.net/insecure/f:webp/rs:fill:300/plain/${fileEventData.url[0]}`}
|
||||||
className="textarea"
|
></img>
|
||||||
placeholder="Caption"
|
</div>
|
||||||
></textarea>
|
)}
|
||||||
<span className="font-bold">URL</span>
|
<div className="grid gap-4 w-4/6" style={{ gridTemplateColumns: '1fr 30em' }}>
|
||||||
<div className="">
|
{fileEventData.title && (
|
||||||
{fileEventData.url.map((text, i) => (
|
<>
|
||||||
<div key={i} className="break-words mb-2">
|
<span className="font-bold">Title</span>
|
||||||
{text}
|
<span>{fileEventData.title}</span>
|
||||||
</div>
|
</>
|
||||||
))}
|
)}
|
||||||
|
{fileEventData.artist && (
|
||||||
|
<>
|
||||||
|
<span className="font-bold">Artist</span>
|
||||||
|
<span>{fileEventData.artist}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{fileEventData.album && (
|
||||||
|
<>
|
||||||
|
<span className="font-bold">Album</span>
|
||||||
|
<span>{fileEventData.album}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{fileEventData.year && (
|
||||||
|
<>
|
||||||
|
<span className="font-bold">Year</span>
|
||||||
|
<span>{fileEventData.year}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<span className="font-bold">Type</span>
|
||||||
|
<span>{fileEventData.m}</span>
|
||||||
|
|
||||||
|
{fileEventData.dim && (
|
||||||
|
<>
|
||||||
|
<span className="font-bold">Dimensions</span>
|
||||||
|
<span>{fileEventData.dim}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<span className="font-bold">File size</span>
|
||||||
|
<span>{fileEventData.size ? formatFileSize(fileEventData.size) : 'unknown'}</span>
|
||||||
|
<span className="font-bold">Content / Description</span>
|
||||||
|
<textarea
|
||||||
|
value={fileEventData.content}
|
||||||
|
onChange={e => setFileEventData(ed => ({ ...ed, content: e.target.value }))}
|
||||||
|
className="textarea"
|
||||||
|
placeholder="Caption"
|
||||||
|
></textarea>
|
||||||
|
<span className="font-bold">URL</span>
|
||||||
|
<div className="">
|
||||||
|
{fileEventData.url.map((text, i) => (
|
||||||
|
<div key={i} className="break-words mb-2">
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>{' '}
|
||||||
|
<div className="flex gap-2 flex-col">
|
||||||
|
<div className=" alert alert-warning ">
|
||||||
|
DEVELOPMENT ZONE! These publish buttons do not work yet. Events are only shown in the browser console.
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button className="btn btn-primary" onClick={() => publishFileEvent(fileEventData)}>
|
||||||
|
Create File Event
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-primary" onClick={() => publishAudioEvent(fileEventData)}>
|
||||||
|
Create Audio Event
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-primary" onClick={() => publishVideoEvent(fileEventData)}>
|
||||||
|
Create Video Event
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button className="btn btn-primary" onClick={() => publishFileEvent(fileEventData)}>
|
|
||||||
Publish
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
124
src/components/FileEventEditor/dvm.ts
Normal file
124
src/components/FileEventEditor/dvm.ts
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
/*
|
||||||
|
async function createDvmBlossemAuthToken() {
|
||||||
|
const pubkey = ndk.activeUser?.pubkey;
|
||||||
|
if (!ndk.signer || !pubkey) return;
|
||||||
|
const tenMinutes = () => dayjs().unix() + 10 * 60;
|
||||||
|
const authEvent = ({
|
||||||
|
pubkey,
|
||||||
|
created_at: dayjs().unix(),
|
||||||
|
kind: 24242,
|
||||||
|
content: 'Upload thumbail',
|
||||||
|
tags: [
|
||||||
|
['t', 'upload'],
|
||||||
|
['name', `thumb_${Math.random().toString(36).substring(2)}`], // make sure the auth events are unique
|
||||||
|
['expiration', String(tenMinutes())],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const ev = new NDKEvent(ndk, authEvent);
|
||||||
|
await ev.sign();
|
||||||
|
console.log(JSON.stringify(ev.rawEvent()));
|
||||||
|
return btoa(JSON.stringify(ev.rawEvent()));
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NDKEvent, NDKKind, NDKUser, NostrEvent } from '@nostr-dev-kit/ndk';
|
||||||
|
import { FileEventData } from './FileEventEditor';
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useNDK } from '../../utils/ndk';
|
||||||
|
import useEvents from '../../utils/useEvents';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
const NPUB_DVM_THUMBNAIL_CREATION = 'npub1q8cv87l47fql2xer2uyw509y5n5s9f53h76hvf9377efdptmsvusxf3n8s';
|
||||||
|
|
||||||
|
const ensureDecrypted = async (dvm: NDKUser, event: NDKEvent) => {
|
||||||
|
if (!event) return undefined;
|
||||||
|
|
||||||
|
const encrypted = event.tags.some(t => t[0] == 'encrypted');
|
||||||
|
|
||||||
|
if (encrypted) {
|
||||||
|
const decryptedContent = await event.ndk?.signer?.decrypt(dvm, event.content);
|
||||||
|
|
||||||
|
if (decryptedContent) {
|
||||||
|
return {
|
||||||
|
...event,
|
||||||
|
tags: event.tags.filter(t => t[0] !== 'encrypted').concat(JSON.parse(decryptedContent)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return event;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useVideoThumbnailDvm = (setFileEventData: React.Dispatch<React.SetStateAction<FileEventData>>) => {
|
||||||
|
const [thumbnailRequestEventId, setThumbnailRequestEventId] = useState<string | undefined>();
|
||||||
|
const { ndk, user } = useNDK();
|
||||||
|
const dvm = ndk.getUser({ npub: NPUB_DVM_THUMBNAIL_CREATION });
|
||||||
|
|
||||||
|
const thumbnailDvmFilter = useMemo(
|
||||||
|
() => ({ kinds: [6204 as NDKKind], '#e': [thumbnailRequestEventId || ''] }),
|
||||||
|
[thumbnailRequestEventId]
|
||||||
|
);
|
||||||
|
const thumbnailSubscription = useEvents(thumbnailDvmFilter, {
|
||||||
|
closeOnEose: false,
|
||||||
|
disable: thumbnailRequestEventId == undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const doASync = async () => {
|
||||||
|
const firstEvent = await ensureDecrypted(dvm, thumbnailSubscription.events[0]);
|
||||||
|
if (firstEvent) {
|
||||||
|
const urls = firstEvent.tags.filter(t => t[0] === 'thumb').map(t => t[1]);
|
||||||
|
const dim = firstEvent.tags.find(t => t[0] === 'dim')?.[1];
|
||||||
|
setFileEventData(ed => ({ ...ed, thumbnails: urls, dim, thumbnail: urls[0] }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
doASync();
|
||||||
|
}, [thumbnailSubscription.events]);
|
||||||
|
|
||||||
|
const createDvmThumbnailRequest = async (data: FileEventData) => {
|
||||||
|
if (!ndk.signer) return;
|
||||||
|
|
||||||
|
const thumbCount = 3;
|
||||||
|
|
||||||
|
/*s
|
||||||
|
const authTokens = [];
|
||||||
|
for (let i = 0; i < thumbCount; i++) {
|
||||||
|
const uploadAuth = await createDvmBlossemAuthToken();
|
||||||
|
if (uploadAuth) {
|
||||||
|
authTokens.push(['param', 'authToken', uploadAuth]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
const e: NostrEvent = {
|
||||||
|
created_at: dayjs().unix(),
|
||||||
|
content: await ndk.signer?.encrypt(
|
||||||
|
dvm,
|
||||||
|
JSON.stringify([
|
||||||
|
['i', data.url[0], 'url'],
|
||||||
|
['output', 'image/jpeg'],
|
||||||
|
['param', 'thumbnailCount', `${thumbCount}`],
|
||||||
|
['relays', user?.relayUrls.join(',') || ndk.explicitRelayUrls?.join(',') || ''],
|
||||||
|
])
|
||||||
|
),
|
||||||
|
tags: [
|
||||||
|
['p', dvm.pubkey],
|
||||||
|
['encrypted'],
|
||||||
|
// TODO set expiration
|
||||||
|
],
|
||||||
|
kind: 5204,
|
||||||
|
pubkey: user?.pubkey || '',
|
||||||
|
};
|
||||||
|
const ev = new NDKEvent(ndk, e);
|
||||||
|
await ev.sign();
|
||||||
|
console.log(ev.rawEvent());
|
||||||
|
setThumbnailRequestEventId(ev.id);
|
||||||
|
await ev.publish();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createDvmThumbnailRequest,
|
||||||
|
thumbnailRequestEventId,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useVideoThumbnailDvm;
|
125
src/components/FileEventEditor/usePublishing.ts
Normal file
125
src/components/FileEventEditor/usePublishing.ts
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import { NDKEvent, NostrEvent } from '@nostr-dev-kit/ndk';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { FileEventData } from './FileEventEditor';
|
||||||
|
import { uniq } from 'lodash';
|
||||||
|
import { useNDK } from '../../utils/ndk';
|
||||||
|
|
||||||
|
export const usePublishing = () => {
|
||||||
|
const { ndk, user } = useNDK();
|
||||||
|
|
||||||
|
const publishFileEvent = async (data: FileEventData) => {
|
||||||
|
// TODO REupload selected video thumbnail from DVM
|
||||||
|
|
||||||
|
const e: NostrEvent = {
|
||||||
|
created_at: dayjs().unix(),
|
||||||
|
content: data.content,
|
||||||
|
tags: [
|
||||||
|
...uniq(data.url).map(du => ['url', du]),
|
||||||
|
['x', data.x],
|
||||||
|
//['summary', data.summary],
|
||||||
|
//['alt', data.alt],
|
||||||
|
],
|
||||||
|
kind: 1063,
|
||||||
|
pubkey: user?.pubkey || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data.size) {
|
||||||
|
e.tags.push(['size', `${data.size}`]);
|
||||||
|
}
|
||||||
|
if (data.dim) {
|
||||||
|
e.tags.push(['dim', data.dim]);
|
||||||
|
}
|
||||||
|
if (data.m) {
|
||||||
|
e.tags.push(['m', data.m]);
|
||||||
|
}
|
||||||
|
if (data.thumbnail) {
|
||||||
|
// TODO upload thumbnail to own storage
|
||||||
|
e.tags.push(['thumb', data.thumbnail]);
|
||||||
|
e.tags.push(['image', data.thumbnail]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ev = new NDKEvent(ndk, e);
|
||||||
|
await ev.sign();
|
||||||
|
console.log(ev.rawEvent());
|
||||||
|
// await ev.publish();
|
||||||
|
};
|
||||||
|
|
||||||
|
const publishAudioEvent = async (data: FileEventData) => {
|
||||||
|
const e: NostrEvent = {
|
||||||
|
created_at: dayjs().unix(),
|
||||||
|
content: `${data.artist} - ${data.title}`,
|
||||||
|
tags: [
|
||||||
|
['d', data.x],
|
||||||
|
...uniq(data.url).map(du => ['media', du]),
|
||||||
|
['x', data.x],
|
||||||
|
...uniq(data.url).map(du => ['imeta', `url ${du}`, `m ${data.m}`]),
|
||||||
|
],
|
||||||
|
kind: 31337,
|
||||||
|
pubkey: user?.pubkey || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data.title) {
|
||||||
|
e.tags.push(['title', `${data.title}`]);
|
||||||
|
e.tags.push(['subject', `${data.title}`]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.artist) {
|
||||||
|
e.tags.push(['creator', `${data.artist}`]);
|
||||||
|
e.tags.push(['creator', `${data.artist}`, 'Artist']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.album) {
|
||||||
|
e.tags.push(['album', `${data.album}`]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ev = new NDKEvent(ndk, e);
|
||||||
|
await ev.sign();
|
||||||
|
console.log(ev.rawEvent());
|
||||||
|
// await ev.publish();
|
||||||
|
};
|
||||||
|
|
||||||
|
const publishVideoEvent = async (data: FileEventData) => {
|
||||||
|
const e: NostrEvent = {
|
||||||
|
created_at: dayjs().unix(),
|
||||||
|
content: data.content,
|
||||||
|
tags: [
|
||||||
|
['d', data.x],
|
||||||
|
['x', data.x],
|
||||||
|
['url', data.url[0]],
|
||||||
|
['title', data.content],
|
||||||
|
// ['summary', data.], TODO add summary
|
||||||
|
['published_at', `${dayjs().unix()}`],
|
||||||
|
['client', 'bouquet'],
|
||||||
|
],
|
||||||
|
kind: 31337,
|
||||||
|
pubkey: user?.pubkey || '',
|
||||||
|
};
|
||||||
|
if (data.size) {
|
||||||
|
e.tags.push(['size', `${data.size}`]);
|
||||||
|
}
|
||||||
|
if (data.dim) {
|
||||||
|
e.tags.push(['dim', data.dim]);
|
||||||
|
}
|
||||||
|
if (data.m) {
|
||||||
|
e.tags.push(['m', data.m]);
|
||||||
|
}
|
||||||
|
if (data.thumbnail) {
|
||||||
|
// TODO upload to own blossom instance
|
||||||
|
e.tags.push(['thumb', data.thumbnail]);
|
||||||
|
e.tags.push(['preview', data.thumbnail]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO add tags ("t")
|
||||||
|
|
||||||
|
const ev = new NDKEvent(ndk, e);
|
||||||
|
await ev.sign();
|
||||||
|
console.log(ev.rawEvent());
|
||||||
|
// await ev.publish();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
publishAudioEvent,
|
||||||
|
publishFileEvent,
|
||||||
|
publishVideoEvent,
|
||||||
|
};
|
||||||
|
};
|
@ -105,7 +105,11 @@ function Upload() {
|
|||||||
// for image resizing
|
// for image resizing
|
||||||
const fileDimensions: { [key: string]: FileEventData } = {};
|
const fileDimensions: { [key: string]: FileEventData } = {};
|
||||||
for (const file of filesToUpload) {
|
for (const file of filesToUpload) {
|
||||||
let data = { content: file.name.replace(/\.[a-zA-Z0-9]{3,4}$/, ''), url: [] as string[] } as FileEventData;
|
let data = {
|
||||||
|
content: file.name.replace(/\.[a-zA-Z0-9]{3,4}$/, ''),
|
||||||
|
url: [] as string[],
|
||||||
|
originalFile: file,
|
||||||
|
} as FileEventData;
|
||||||
if (file.type.startsWith('image/')) {
|
if (file.type.startsWith('image/')) {
|
||||||
const dimensions = await getImageSize(file);
|
const dimensions = await getImageSize(file);
|
||||||
data = { ...data, dim: `${dimensions.width}x${dimensions.height}` };
|
data = { ...data, dim: `${dimensions.width}x${dimensions.height}` };
|
||||||
@ -199,23 +203,6 @@ function Upload() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
clearTransfers();
|
clearTransfers();
|
||||||
/*
|
|
||||||
setFileEventsToPublish([
|
|
||||||
{
|
|
||||||
content: '_DSF3852.jpg',
|
|
||||||
dim: '1365x2048',
|
|
||||||
m: 'image/jpeg',
|
|
||||||
size: 599988,
|
|
||||||
url: [
|
|
||||||
'https://test-store.slidestr.net/d32b7eff53919bc38b59e05b2fe4bda3067c46589eeee743a46649ae71f4b659',
|
|
||||||
|
|
||||||
'https://media-server.slidestr.net/d32b7eff53919bc38b59e05b2fe4bda3067c46589eeee743a46649ae71f4b659',
|
|
||||||
|
|
||||||
'https://cdn.satellite.earth/d32b7eff53919bc38b59e05b2fe4bda3067c46589eeee743a46649ae71f4b659.jpg',
|
|
||||||
],
|
|
||||||
x: 'd32b7eff53919bc38b59e05b2fe4bda3067c46589eeee743a46649ae71f4b659',
|
|
||||||
},
|
|
||||||
]);*/
|
|
||||||
}, [servers]);
|
}, [servers]);
|
||||||
|
|
||||||
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
@ -67,11 +67,8 @@ function saveID3TagToDB(db: IDBDatabase, key: string, id3Tag: ID3Tag): Promise<v
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Function to resize image
|
// Function to resize image
|
||||||
function resizeImage(imageArray: ArrayBuffer, maxWidth: number, maxHeight: number): Promise<string> {
|
function resizeImage(imageBlobUrl: string, maxWidth: number, maxHeight: number): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const blob = new Blob([imageArray], { type: 'image/jpeg' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
let width = img.width;
|
let width = img.width;
|
||||||
@ -108,24 +105,29 @@ function resizeImage(imageArray: ArrayBuffer, maxWidth: number, maxHeight: numbe
|
|||||||
reject(new Error('Canvas context could not be retrieved'));
|
reject(new Error('Canvas context could not be retrieved'));
|
||||||
}
|
}
|
||||||
|
|
||||||
URL.revokeObjectURL(url); // Clean up
|
// URL.revokeObjectURL(url); // Clean up
|
||||||
};
|
};
|
||||||
|
|
||||||
img.onerror = () => {
|
img.onerror = () => {
|
||||||
reject(new Error('Image could not be loaded'));
|
reject(new Error('Image could not be loaded'));
|
||||||
URL.revokeObjectURL(url); // Clean up
|
// URL.revokeObjectURL(url); // Clean up
|
||||||
};
|
};
|
||||||
|
|
||||||
img.src = url;
|
img.src = imageBlobUrl;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchId3Tag = async (blob: BlobDescriptor): Promise<AudioBlob> => {
|
export const fetchId3Tag = async (
|
||||||
|
blobHash: string,
|
||||||
|
blobUrl?: string,
|
||||||
|
localFile?: File
|
||||||
|
): Promise<{ id3: ID3Tag; coverFull?: string } | undefined> => {
|
||||||
const db = await openIndexedDB();
|
const db = await openIndexedDB();
|
||||||
const cachedID3Tag = await getID3TagFromDB(db, blob.sha256);
|
const cachedID3Tag = await getID3TagFromDB(db, blobHash);
|
||||||
|
|
||||||
if (cachedID3Tag) {
|
// Don't cache the ID3 tag if we have a local file
|
||||||
return { ...blob, id3: cachedID3Tag } as AudioBlob;
|
if (!localFile && cachedID3Tag) {
|
||||||
|
return { id3: cachedID3Tag };
|
||||||
}
|
}
|
||||||
|
|
||||||
function arrayBufferToFile(arrayBuffer: ArrayBuffer, fileName: string, mimeType: string) {
|
function arrayBufferToFile(arrayBuffer: ArrayBuffer, fileName: string, mimeType: string) {
|
||||||
@ -141,10 +143,18 @@ export const fetchId3Tag = async (blob: BlobDescriptor): Promise<AudioBlob> => {
|
|||||||
// const id3Tag = await id3.fromUrl(blob.url).catch(e => console.warn(e));
|
// const id3Tag = await id3.fromUrl(blob.url).catch(e => console.warn(e));
|
||||||
|
|
||||||
// download the whole song, convert to blob and file to read mp3 tag
|
// download the whole song, convert to blob and file to read mp3 tag
|
||||||
const response = await fetch(blob.url);
|
let file = localFile;
|
||||||
const buffer = await response.arrayBuffer();
|
if (!file) {
|
||||||
const file = arrayBufferToFile(buffer, `${blob.sha256}.mp3`, blob.type || 'audio/mpeg');
|
if (!blobUrl) return undefined;
|
||||||
|
|
||||||
|
// if we don't have a local file, download from blob url
|
||||||
|
const response = await fetch(blobUrl);
|
||||||
|
const buffer = await response.arrayBuffer();
|
||||||
|
file = arrayBufferToFile(buffer, `${blobHash}.mp3`, 'audio/mpeg');
|
||||||
|
}
|
||||||
|
|
||||||
const id3Tag = await id3.fromFile(file).catch(e => console.warn(e));
|
const id3Tag = await id3.fromFile(file).catch(e => console.warn(e));
|
||||||
|
let imageBlobUrl: string | undefined;
|
||||||
|
|
||||||
if (id3Tag) {
|
if (id3Tag) {
|
||||||
const tagResult: ID3Tag = {
|
const tagResult: ID3Tag = {
|
||||||
@ -157,16 +167,16 @@ export const fetchId3Tag = async (blob: BlobDescriptor): Promise<AudioBlob> => {
|
|||||||
if (id3Tag.kind == 'v2') {
|
if (id3Tag.kind == 'v2') {
|
||||||
const id3v2 = id3Tag as ID3TagV2;
|
const id3v2 = id3Tag as ID3TagV2;
|
||||||
if (id3v2.images[0].data) {
|
if (id3v2.images[0].data) {
|
||||||
tagResult.cover = await resizeImage(id3v2.images[0].data, 128, 128);
|
const blob = new Blob([id3v2.images[0].data], { type: 'image/jpeg' });
|
||||||
|
imageBlobUrl = URL.createObjectURL(blob);
|
||||||
|
tagResult.cover = await resizeImage(imageBlobUrl, 128, 128);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log(blob.sha256, tagResult);
|
console.log(blobHash, blobUrl, tagResult);
|
||||||
|
|
||||||
await saveID3TagToDB(db, blob.sha256, tagResult);
|
await saveID3TagToDB(db, blobHash, tagResult);
|
||||||
return { ...blob, id3: tagResult };
|
return { id3: tagResult, coverFull: imageBlobUrl };
|
||||||
}
|
}
|
||||||
console.log('No ID3 tag found for ' + blob.sha256);
|
console.log('No ID3 tag found for ' + blobHash);
|
||||||
|
|
||||||
return blob; // only when ID3 fails completely
|
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user