- {fileEventData.m?.startsWith('video/') && (
- <>
- {thumbnailRequestEventId &&
- (fileEventData.thumbnails && fileEventData.thumbnails.length > 0 ? (
-
- ))}
- >
- )}
-
- {fileEventData.m?.startsWith('image/') && (
-
-
Type
-
{fileEventData.m}
-
- {fileEventData.dim && (
+ <>
+
+ {fileEventData.m?.startsWith('video/') && (
<>
-
Dimensions
-
{fileEventData.dim}
+ {thumbnailRequestEventId &&
+ (fileEventData.thumbnails && fileEventData.thumbnails.length > 0 ? (
+
+
+ {fileEventData.thumbnails.map((t, i) => (
+
+
+
+ ))}
+
+
+
+ ) : (
+
+ Creating previews
+
+ ))}
>
)}
+ {fileEventData.m?.startsWith('audio/') && fileEventData.thumbnail && (
+
+
+
+ )}
-
File size
-
{fileEventData.size ? formatFileSize(fileEventData.size) : 'unknown'}
-
Content / Description
-
-
URL
-
- {fileEventData.url.map((text, i) => (
-
- {text}
-
- ))}
+ {fileEventData.m?.startsWith('image/') && (
+
+
+
+ )}
+
+ {fileEventData.title && (
+ <>
+
Title
+
{fileEventData.title}
+ >
+ )}
+ {fileEventData.artist && (
+ <>
+
Artist
+
{fileEventData.artist}
+ >
+ )}
+ {fileEventData.album && (
+ <>
+
Album
+
{fileEventData.album}
+ >
+ )}
+ {fileEventData.year && (
+ <>
+
Year
+
{fileEventData.year}
+ >
+ )}
+
+
Type
+
{fileEventData.m}
+
+ {fileEventData.dim && (
+ <>
+
Dimensions
+
{fileEventData.dim}
+ >
+ )}
+
+
File size
+
{fileEventData.size ? formatFileSize(fileEventData.size) : 'unknown'}
+
Content / Description
+
+
URL
+
+ {fileEventData.url.map((text, i) => (
+
+ {text}
+
+ ))}
+
+
+
{' '}
+
+
+ DEVELOPMENT ZONE! These publish buttons do not work yet. Events are only shown in the browser console.
+
+
+
+
+
-
-
+ >
);
};
diff --git a/src/components/FileEventEditor/dvm.ts b/src/components/FileEventEditor/dvm.ts
new file mode 100644
index 0000000..065b0fe
--- /dev/null
+++ b/src/components/FileEventEditor/dvm.ts
@@ -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
>) => {
+ const [thumbnailRequestEventId, setThumbnailRequestEventId] = useState();
+ 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;
diff --git a/src/components/FileEventEditor/usePublishing.ts b/src/components/FileEventEditor/usePublishing.ts
new file mode 100644
index 0000000..5fba647
--- /dev/null
+++ b/src/components/FileEventEditor/usePublishing.ts
@@ -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,
+ };
+};
diff --git a/src/pages/Upload.tsx b/src/pages/Upload.tsx
index a7526cc..df6d9bf 100644
--- a/src/pages/Upload.tsx
+++ b/src/pages/Upload.tsx
@@ -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) => {
diff --git a/src/utils/id3.ts b/src/utils/id3.ts
index cfe6581..63fe2cc 100644
--- a/src/utils/id3.ts
+++ b/src/utils/id3.ts
@@ -67,11 +67,8 @@ function saveID3TagToDB(db: IDBDatabase, key: string, id3Tag: ID3Tag): Promise {
+function resizeImage(imageBlobUrl: string, maxWidth: number, maxHeight: number): Promise {
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 => {
+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 => {
// 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 => {
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);
};