mirror of
https://git.v0l.io/florian/bouquet.git
synced 2025-03-17 18:13:00 +01:00
fix: Fixed upload bugs, an improved nip96 support
This commit is contained in:
parent
74b0f086f9
commit
37bc592a6d
@ -71,7 +71,7 @@ const AudioBlobList = ({ audioFiles, handleSelectBlob, selectedBlobs }: AudioBlo
|
||||
</div>
|
||||
{blob.data.id3 && (
|
||||
<div className="flex flex-col pb-1 md:pb-4 flex-grow">
|
||||
{blob.data.id3.title && <span className="font-bold">{blob.data.id3.title}</span>}
|
||||
{blob.data.id3.title && <span className=" text-accent">{blob.data.id3.title}</span>}
|
||||
{blob.data.id3.artist && <span className=" text-sm"> {blob.data.id3.artist}</span>}
|
||||
{blob.data.id3.album && (
|
||||
<span className="text-sm">
|
||||
|
@ -140,7 +140,7 @@ const AudioPlayer: React.FC = () => {
|
||||
<img className="w-12 h-12" src={currentSong.id3.cover} alt={currentSong.id3.title} />
|
||||
</div>
|
||||
<div className="flex flex-col text-sm">
|
||||
<div className="text-white">{currentSong.id3.title}</div>
|
||||
<div className="text-accent">{currentSong.id3.title}</div>
|
||||
<div>{currentSong.id3.artist}</div>
|
||||
</div>
|
||||
</>
|
||||
|
@ -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<string | undefined>();
|
||||
|
||||
@ -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<BlobDescriptor | undefined> => {
|
||||
// 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"
|
||||
></textarea>
|
||||
|
||||
<span className="font-bold">Genre</span>
|
||||
<div>
|
||||
<select
|
||||
className="select select-bordered select-primary w-full max-w-xs"
|
||||
value={fileEventData.genre}
|
||||
onChange={e => setFileEventData(ed => ({ ...ed, genre: e.target.value, subgenre: '' }))}
|
||||
>
|
||||
<option disabled>Select a genre</option>
|
||||
{Object.keys(allGenres).map(g => (
|
||||
<option key={g} value={g}>
|
||||
{g}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
className="select select-bordered select-primary w-full max-w-xs mt-2"
|
||||
value={fileEventData.subgenre}
|
||||
disabled={
|
||||
fileEventData.genre == undefined ||
|
||||
allGenres[fileEventData.genre] == undefined ||
|
||||
allGenres[fileEventData.genre].length == 0
|
||||
}
|
||||
onChange={e => setFileEventData(ed => ({ ...ed, subgenre: e.target.value }))}
|
||||
>
|
||||
<option disabled value="">
|
||||
Select a sub genre
|
||||
</option>
|
||||
{fileEventData.genre &&
|
||||
allGenres[fileEventData.genre] &&
|
||||
allGenres[fileEventData.genre].length > 0 &&
|
||||
allGenres[fileEventData.genre].map(g => (
|
||||
<option key={g} value={g}>
|
||||
{g}
|
||||
{isAudio && (
|
||||
<>
|
||||
<span className="font-bold">Genre</span>
|
||||
<div>
|
||||
<select
|
||||
className="select select-bordered select-primary w-full max-w-xs"
|
||||
value={fileEventData.genre}
|
||||
onChange={e => setFileEventData(ed => ({ ...ed, genre: e.target.value, subgenre: '' }))}
|
||||
>
|
||||
<option disabled>Select a genre</option>
|
||||
{Object.keys(allGenres).map(g => (
|
||||
<option key={g} value={g}>
|
||||
{g}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
className="select select-bordered select-primary w-full max-w-xs mt-2"
|
||||
value={fileEventData.subgenre}
|
||||
disabled={
|
||||
fileEventData.genre == undefined ||
|
||||
allGenres[fileEventData.genre] == undefined ||
|
||||
allGenres[fileEventData.genre].length == 0
|
||||
}
|
||||
onChange={e => setFileEventData(ed => ({ ...ed, subgenre: e.target.value }))}
|
||||
>
|
||||
<option disabled value="">
|
||||
Select a sub genre
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{fileEventData.genre &&
|
||||
allGenres[fileEventData.genre] &&
|
||||
allGenres[fileEventData.genre].length > 0 &&
|
||||
allGenres[fileEventData.genre].map(g => (
|
||||
<option key={g} value={g}>
|
||||
{g}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<span className="font-bold">Tags</span>
|
||||
<TagInput
|
||||
tags={fileEventData.tags}
|
||||
|
@ -18,16 +18,16 @@ export const ResizeOptions: ResizeOptionType[] = [
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
},
|
||||
{
|
||||
name: 'max. 2048x2048 pixels',
|
||||
width: 2048,
|
||||
height: 2048,
|
||||
},
|
||||
{
|
||||
name: 'max. 1080x1080 pixels',
|
||||
width: 1080,
|
||||
height: 1080,
|
||||
},
|
||||
{
|
||||
name: 'max. 2048x2048 pixels',
|
||||
width: 2048,
|
||||
height: 2048,
|
||||
},
|
||||
];
|
||||
|
||||
export type TransferStats = {
|
||||
@ -170,7 +170,7 @@ const UploadFileSelection: React.FC<UploadFileSelectionProps> = ({
|
||||
value={imageResize}
|
||||
>
|
||||
{ResizeOptions.map((ro, i) => (
|
||||
<option key={ro.name} disabled={i == 0}>
|
||||
<option key={ro.name} value={i} disabled={i == 0}>
|
||||
{ro.name}
|
||||
</option>
|
||||
))}
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
};
|
||||
};
|
@ -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<SignedEvent>
|
||||
signEventTemplate: (template: EventTemplate) => Promise<SignedEvent>,
|
||||
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<SignedEvent>
|
||||
signEventTemplate: (template: EventTemplate) => Promise<SignedEvent>,
|
||||
onProgress?: (progressEvent: AxiosProgressEvent) => void
|
||||
): Promise<BlobDescriptor> {
|
||||
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}`);
|
||||
}
|
||||
|
@ -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<File> {
|
||||
const response = await fetch(blobUrl);
|
||||
@ -12,26 +14,36 @@ async function blobUrlToFile(blobUrl: string, fileName: string): Promise<File> {
|
||||
// TODO support nip96
|
||||
export const transferBlob = async (
|
||||
sourceUrl: string,
|
||||
targetServer: string,
|
||||
targetServer: Server,
|
||||
signEventTemplate: (template: EventTemplate) => Promise<SignedEvent>,
|
||||
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void
|
||||
onProgress?: (progressEvent: AxiosProgressEvent) => void
|
||||
): Promise<BlobDescriptor> => {
|
||||
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
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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',
|
||||
|
Loading…
x
Reference in New Issue
Block a user