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 { ClipboardDocumentIcon, TrashIcon } from '@heroicons/react/24/outline';
|
||||
import { BlobDescriptor } from 'blossom-client-sdk';
|
||||
import { fetchId3Tag } from '../../utils/id3';
|
||||
import { AudioBlob, fetchId3Tag } from '../../utils/id3';
|
||||
import { useQueries } from '@tanstack/react-query';
|
||||
import { useGlobalContext } from '../../GlobalState';
|
||||
import { PauseIcon, PlayIcon } from '@heroicons/react/24/solid';
|
||||
@ -17,7 +17,10 @@ const AudioBlobList = ({ audioFiles, onDelete }: AudioBlobListProps) => {
|
||||
const audioFilesWithId3 = useQueries({
|
||||
queries: audioFiles.map(af => ({
|
||||
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,
|
||||
cacheTime: 1000 * 60 * 5,
|
||||
})),
|
||||
|
@ -31,7 +31,7 @@ const BlobListTypeMenu = ({ mode, setMode, hasImages, hasAudio, hasDocs, hasVide
|
||||
}, [hasAudio, hasDocs, hasImages, hasVideo, mode, setMode]);
|
||||
|
||||
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>
|
||||
<a
|
||||
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 { useNDK } from '../../utils/ndk';
|
||||
import dayjs from 'dayjs';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import uniq from 'lodash/uniq';
|
||||
import { useEffect, useState } from 'react';
|
||||
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 = {
|
||||
originalFile: File;
|
||||
content: string;
|
||||
url: string[];
|
||||
dim?: string;
|
||||
@ -17,236 +16,171 @@ export type FileEventData = {
|
||||
thumbnail?: string;
|
||||
//summary: 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 [fileEventData, setFileEventData] = useState(data);
|
||||
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 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();
|
||||
};
|
||||
const { createDvmThumbnailRequest, thumbnailRequestEventId } = useVideoThumbnailDvm(setFileEventData);
|
||||
const { publishAudioEvent, publishFileEvent, publishVideoEvent } = usePublishing();
|
||||
|
||||
useEffect(() => {
|
||||
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]);
|
||||
|
||||
return (
|
||||
<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 && (
|
||||
<>
|
||||
<div className=" bg-base-200 rounded-xl p-4 text-neutral-content gap-4 flex flex-row">
|
||||
{fileEventData.m?.startsWith('video/') && (
|
||||
<>
|
||||
<span className="font-bold">Dimensions</span>
|
||||
<span>{fileEventData.dim}</span>
|
||||
{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('audio/') && fileEventData.thumbnail && (
|
||||
<div className="w-2/6">
|
||||
<img src={fileEventData.thumbnail} className="w-full" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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>
|
||||
))}
|
||||
{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' }}>
|
||||
{fileEventData.title && (
|
||||
<>
|
||||
<span className="font-bold">Title</span>
|
||||
<span>{fileEventData.title}</span>
|
||||
</>
|
||||
)}
|
||||
{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>
|
||||
<button className="btn btn-primary" onClick={() => publishFileEvent(fileEventData)}>
|
||||
Publish
|
||||
</button>
|
||||
</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
|
||||
const fileDimensions: { [key: string]: FileEventData } = {};
|
||||
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/')) {
|
||||
const dimensions = await getImageSize(file);
|
||||
data = { ...data, dim: `${dimensions.width}x${dimensions.height}` };
|
||||
@ -199,23 +203,6 @@ function Upload() {
|
||||
|
||||
useEffect(() => {
|
||||
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]);
|
||||
|
||||
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
|
@ -67,11 +67,8 @@ function saveID3TagToDB(db: IDBDatabase, key: string, id3Tag: ID3Tag): Promise<v
|
||||
}
|
||||
|
||||
// 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) => {
|
||||
const blob = new Blob([imageArray], { type: 'image/jpeg' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
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'));
|
||||
}
|
||||
|
||||
URL.revokeObjectURL(url); // Clean up
|
||||
// URL.revokeObjectURL(url); // Clean up
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
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 cachedID3Tag = await getID3TagFromDB(db, blob.sha256);
|
||||
const cachedID3Tag = await getID3TagFromDB(db, blobHash);
|
||||
|
||||
if (cachedID3Tag) {
|
||||
return { ...blob, id3: cachedID3Tag } as AudioBlob;
|
||||
// Don't cache the ID3 tag if we have a local file
|
||||
if (!localFile && cachedID3Tag) {
|
||||
return { id3: cachedID3Tag };
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
// download the whole song, convert to blob and file to read mp3 tag
|
||||
const response = await fetch(blob.url);
|
||||
const buffer = await response.arrayBuffer();
|
||||
const file = arrayBufferToFile(buffer, `${blob.sha256}.mp3`, blob.type || 'audio/mpeg');
|
||||
let file = localFile;
|
||||
if (!file) {
|
||||
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));
|
||||
let imageBlobUrl: string | undefined;
|
||||
|
||||
if (id3Tag) {
|
||||
const tagResult: ID3Tag = {
|
||||
@ -157,16 +167,16 @@ export const fetchId3Tag = async (blob: BlobDescriptor): Promise<AudioBlob> => {
|
||||
if (id3Tag.kind == 'v2') {
|
||||
const id3v2 = id3Tag as ID3TagV2;
|
||||
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);
|
||||
return { ...blob, id3: tagResult };
|
||||
await saveID3TagToDB(db, blobHash, tagResult);
|
||||
return { id3: tagResult, coverFull: imageBlobUrl };
|
||||
}
|
||||
console.log('No ID3 tag found for ' + blob.sha256);
|
||||
|
||||
return blob; // only when ID3 fails completely
|
||||
console.log('No ID3 tag found for ' + blobHash);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user