diff --git a/src/components/AudioBlobList/AudioBlobList.tsx b/src/components/AudioBlobList/AudioBlobList.tsx index 48e8344..0687aad 100644 --- a/src/components/AudioBlobList/AudioBlobList.tsx +++ b/src/components/AudioBlobList/AudioBlobList.tsx @@ -71,7 +71,7 @@ const AudioBlobList = ({ audioFiles, handleSelectBlob, selectedBlobs }: AudioBlo {blob.data.id3 && (
- {blob.data.id3.title && {blob.data.id3.title}} + {blob.data.id3.title && {blob.data.id3.title}} {blob.data.id3.artist && {blob.data.id3.artist}} {blob.data.id3.album && ( diff --git a/src/components/AudioPlayer.tsx b/src/components/AudioPlayer.tsx index 952da9d..882ba22 100644 --- a/src/components/AudioPlayer.tsx +++ b/src/components/AudioPlayer.tsx @@ -140,7 +140,7 @@ const AudioPlayer: React.FC = () => { {currentSong.id3.title}
-
{currentSong.id3.title}
+
{currentSong.id3.title}
{currentSong.id3.artist}
diff --git a/src/components/FileEventEditor/FileEventEditor.tsx b/src/components/FileEventEditor/FileEventEditor.tsx index 0124d83..2563b66 100644 --- a/src/components/FileEventEditor/FileEventEditor.tsx +++ b/src/components/FileEventEditor/FileEventEditor.tsx @@ -8,6 +8,7 @@ import { transferBlob } from '../../utils/transfer'; import { useNDK } from '../../utils/ndk'; import TagInput from '../TagInput'; import { allGenres } from '../../utils/genres'; +import { useServerInfo } from '../../utils/useServerInfo'; export type FileEventData = { originalFile: File; @@ -35,6 +36,7 @@ export type FileEventData = { const FileEventEditor = ({ data }: { data: FileEventData }) => { const { signEventTemplate } = useNDK(); + const { serverInfo } = useServerInfo(); const [fileEventData, setFileEventData] = useState(data); const [selectedThumbnail, setSelectedThumbnail] = useState(); @@ -78,15 +80,15 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => { } }, [fileEventData]); - function extractProtocolAndDomain(url: string): string | null { - const regex = /^(https?:\/\/[^/]+)/; + function extractDomain(url: string): string | null { + const regex = /^(https?:\/\/)([^/]+)/; const match = url.match(regex); - return match ? match[0] : null; + return match ? match[2]?.toLocaleLowerCase() : null; } const publishSelectedThumbnailToAllOwnServers = async (): Promise => { // TODO investigate why mimetype is not set for reuploaded thumbnail (on mediaserver) - const servers = fileEventData.url.map(extractProtocolAndDomain); + const servers = fileEventData.url.map(extractDomain); // upload selected thumbnail to the same blossom servers as the video let uploadedThumbnails: BlobDescriptor[] = []; @@ -94,7 +96,11 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => { uploadedThumbnails = ( await Promise.all( servers.map(s => { - if (s && selectedThumbnail) return transferBlob(selectedThumbnail, s, signEventTemplate); + if (s && selectedThumbnail) { + console.log(s); + console.log(serverInfo); + return transferBlob(selectedThumbnail, serverInfo[s], signEventTemplate); + } }) ) ).filter(t => t !== undefined) as BlobDescriptor[]; @@ -211,45 +217,47 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => { className="textarea textarea-primary" placeholder="Caption" > - - Genre -
- - setFileEventData(ed => ({ ...ed, genre: e.target.value, subgenre: '' }))} + > + + {Object.keys(allGenres).map(g => ( + + ))} + + -
- + {fileEventData.genre && + allGenres[fileEventData.genre] && + allGenres[fileEventData.genre].length > 0 && + allGenres[fileEventData.genre].map(g => ( + + ))} + + + + )} Tags = ({ value={imageResize} > {ResizeOptions.map((ro, i) => ( - ))} diff --git a/src/pages/Upload.tsx b/src/pages/Upload.tsx index 63e6a41..e25752c 100644 --- a/src/pages/Upload.tsx +++ b/src/pages/Upload.tsx @@ -118,20 +118,22 @@ function Upload() { try { let newBlob: BlobDescriptor; + const progressHandler = (progressEvent: AxiosProgressEvent) => { + setTransfers(ut => ({ + ...ut, + [server.name]: { + ...ut[server.name], + transferred: serverTransferred + progressEvent.loaded, + rate: progressEvent.rate || 0, + }, + })); + }; if (server.type == 'blossom') { - newBlob = await uploadBlob(serverUrl, file, uploadAuth, progressEvent => { - setTransfers(ut => ({ - ...ut, - [server.name]: { - ...ut[server.name], - transferred: serverTransferred + progressEvent.loaded, - rate: progressEvent.rate || 0, - }, - })); - }); + newBlob = await uploadBlob(serverUrl, file, uploadAuth, progressHandler); } else { - newBlob = await uploadNip96File(server, file, '', signEventTemplate); + newBlob = await uploadNip96File(server, file, '', signEventTemplate, progressHandler); } + console.log('newBlob', newBlob); serverTransferred += file.size; setTransfers(ut => ({ ...ut, diff --git a/src/utils/blossom.ts b/src/utils/blossom.ts index ae5b9f1..7c4194e 100644 --- a/src/utils/blossom.ts +++ b/src/utils/blossom.ts @@ -53,10 +53,7 @@ export const uploadBlossomBlob = async ( return res.data; }; -export const downloadBlossomBlob = async ( - url: string, - onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void -) => { +export const downloadBlossomBlob = async (url: string, onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void) => { const response = await axios.get(url, { responseType: 'blob', onDownloadProgress, @@ -92,4 +89,4 @@ export const mirrordBlossomBlob = async ( } ); return res.data; -}; +}; \ No newline at end of file diff --git a/src/utils/nip96.ts b/src/utils/nip96.ts index 9cde626..6690aa4 100644 --- a/src/utils/nip96.ts +++ b/src/utils/nip96.ts @@ -1,6 +1,7 @@ import { BlobDescriptor, EventTemplate, SignedEvent } from 'blossom-client-sdk'; import { Server } from './useUserServers'; import dayjs from 'dayjs'; +import axios, { AxiosProgressEvent } from 'axios'; type MediaTransformation = 'resizing' | 'format_conversion' | 'compression' | 'metadata_stripping'; @@ -74,7 +75,6 @@ async function createNip98UploadAuthToken( ], }; const signedEvent = await signEventTemplate(authEvent); - console.log(JSON.stringify(signedEvent)); return btoa(JSON.stringify(signedEvent)); } @@ -82,18 +82,20 @@ const getValueByTag = (tags: string[][] | undefined, t: string) => tags && tags. export async function fetchNip96List( server: Server, - signEventTemplate: (template: EventTemplate) => Promise + signEventTemplate: (template: EventTemplate) => Promise, + onProgress?: (progressEvent: AxiosProgressEvent) => void ) { const page = 0; const count = 100; const baseUrl = server.nip96?.api_url || server.url; const listUrl = `${baseUrl}?page=${page}&count=${count}`; - const response = await fetch(listUrl, { + const response = await axios.get(listUrl, { headers: { Authorization: `Nostr ${await createNip98UploadAuthToken(listUrl, 'GET', signEventTemplate)}` }, + onDownloadProgress: onProgress, }); - const list = (await response.json()) as Nip96ListResponse; + const list = response.data as Nip96ListResponse; return list.files.map( file => @@ -132,35 +134,36 @@ The server MUST link the user's pubkey string as the owner of the file so to lat no_transform can be used to replicate a file to multiple servers for redundancy, clients can use the server list to find alternative servers which might contain the same file. When uploading a file and requesting no_transform clients should check that the hash matches in the response in order to detect if the file was modified. */ + export async function uploadNip96File( server: Server, file: File, caption: string, - signEventTemplate: (template: EventTemplate) => Promise + signEventTemplate: (template: EventTemplate) => Promise, + onProgress?: (progressEvent: AxiosProgressEvent) => void ): Promise { const formData = new FormData(); formData.append('file', file); formData.append('caption', caption || ''); // RECOMMENDED TODO ADD //formData.append('expiration', server.expiration || ''); formData.append('size', file.size.toString()); - //formData.append('alt', server.alt || ''); // RECOMMENDED + formData.append('alt', caption || ''); // RECOMMENDED //formData.append('media_type', // avatar / banner formData.append('content_type', file.type || ''); - formData.append('no_transform', 'true'); + formData.append('no_transform', 'true'); // we don't use any transform for blossom compatibility const baseUrl = server.nip96?.api_url || server.url; - const response = await fetch(baseUrl, { - method: 'POST', + const response = await axios.post(baseUrl, formData, { headers: { Authorization: `Nostr ${await createNip98UploadAuthToken(baseUrl, 'POST', signEventTemplate)}` }, - body: formData, + onUploadProgress: onProgress, }); - if (!response.ok) { + if (response.status >= 400) { throw new Error(`Failed to upload file: ${response.statusText}`); } - const result = (await response.json()) as Nip96UploadResult; + const result = response.data as Nip96UploadResult; console.log(result); const x = getValueByTag(result.nip94_event?.tags, 'x') || getValueByTag(result.nip94_event?.tags, 'ox'); @@ -217,19 +220,18 @@ export async function deleteNip96File( const auth = await createNip98UploadAuthToken(url, 'DELETE', signEventTemplate); - const response = await fetch(url, { - method: 'DELETE', + const response = await axios.delete(url, { headers: { ...headers, authorization: `Nostr ${auth}`, }, }); - if (!response.ok) { + if (response.status >= 400) { throw new Error(`Failed to delete file: ${response.statusText}`); } - const result = await response.json(); + const result = response.data; if (result.status !== 'success') { throw new Error(`Failed to delete file: ${result.message}`); } diff --git a/src/utils/transfer.ts b/src/utils/transfer.ts index 247ae7c..12fd6dd 100644 --- a/src/utils/transfer.ts +++ b/src/utils/transfer.ts @@ -1,6 +1,8 @@ import { AxiosProgressEvent } from 'axios'; import { BlobDescriptor, EventTemplate, SignedEvent } from 'blossom-client-sdk'; import { downloadBlossomBlob, mirrordBlossomBlob, uploadBlossomBlob } from './blossom'; +import { Server } from './useUserServers'; +import { uploadNip96File } from './nip96'; async function blobUrlToFile(blobUrl: string, fileName: string): Promise { const response = await fetch(blobUrl); @@ -12,26 +14,36 @@ async function blobUrlToFile(blobUrl: string, fileName: string): Promise { // TODO support nip96 export const transferBlob = async ( sourceUrl: string, - targetServer: string, + targetServer: Server, signEventTemplate: (template: EventTemplate) => Promise, - onUploadProgress?: (progressEvent: AxiosProgressEvent) => void + onProgress?: (progressEvent: AxiosProgressEvent) => void ): Promise => { console.log({ sourceUrl, targetServer }); if (sourceUrl.startsWith('blob:')) { const file = await blobUrlToFile(sourceUrl, 'cover.jpg'); - return await uploadBlossomBlob(targetServer, file, signEventTemplate, onUploadProgress); + if (targetServer.type == 'blossom') { + return await uploadBlossomBlob(targetServer.url, file, signEventTemplate, onProgress); + } else { + return await uploadNip96File(targetServer, file, 'cover.jpg', signEventTemplate, onProgress); + } } else { - const blob = await mirrordBlossomBlob(targetServer, sourceUrl, signEventTemplate); - if (blob) return blob; - console.log('Mirror failed. Using download + upload instead.'); + if (targetServer.type == 'blossom') { + const blob = await mirrordBlossomBlob(targetServer.url, sourceUrl, signEventTemplate); + if (blob) return blob; + console.log('Mirror failed. Using download + upload instead.'); + } - const result = await downloadBlossomBlob(sourceUrl, onUploadProgress); + const result = await downloadBlossomBlob(sourceUrl, onProgress); const fileName = sourceUrl.replace(/.*\//, ''); const file = new File([result.data], fileName, { type: result.type, lastModified: new Date().getTime() }); - return await uploadBlossomBlob(targetServer, file, signEventTemplate, onUploadProgress); + if (targetServer.type == 'blossom') { + return await uploadBlossomBlob(targetServer.url, file, signEventTemplate, onProgress); + } else { + return await uploadNip96File(targetServer, file, fileName, signEventTemplate, onProgress); // TODO add caption + } } }; diff --git a/tailwind.config.ts b/tailwind.config.ts index b93af0b..8103a64 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -5,7 +5,6 @@ export default { extend: {}, }, plugins: [require('daisyui')], - daisyui: { themes: [ { @@ -13,7 +12,7 @@ export default { ...require('daisyui/src/theming/themes')['dark'], primary: '#be185d', secondary: '#2563eb', - accent: '#fb923c', + accent: '#ffffff', info: '#a5b4fc', success: '#6ee7b7', warning: '#facc15', @@ -25,7 +24,7 @@ export default { ...require('daisyui/src/theming/themes')['cupcake'], primary: '#be185d', secondary: '#2563eb', - accent: '#fb923c', + accent: '#000000', neutral: '#e0e0e0', info: '#a5b4fc', success: '#6ee7b7',