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>
|
</div>
|
||||||
{blob.data.id3 && (
|
{blob.data.id3 && (
|
||||||
<div className="flex flex-col pb-1 md:pb-4 flex-grow">
|
<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.artist && <span className=" text-sm"> {blob.data.id3.artist}</span>}
|
||||||
{blob.data.id3.album && (
|
{blob.data.id3.album && (
|
||||||
<span className="text-sm">
|
<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} />
|
<img className="w-12 h-12" src={currentSong.id3.cover} alt={currentSong.id3.title} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col text-sm">
|
<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>{currentSong.id3.artist}</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -8,6 +8,7 @@ import { transferBlob } from '../../utils/transfer';
|
|||||||
import { useNDK } from '../../utils/ndk';
|
import { useNDK } from '../../utils/ndk';
|
||||||
import TagInput from '../TagInput';
|
import TagInput from '../TagInput';
|
||||||
import { allGenres } from '../../utils/genres';
|
import { allGenres } from '../../utils/genres';
|
||||||
|
import { useServerInfo } from '../../utils/useServerInfo';
|
||||||
|
|
||||||
export type FileEventData = {
|
export type FileEventData = {
|
||||||
originalFile: File;
|
originalFile: File;
|
||||||
@ -35,6 +36,7 @@ export type FileEventData = {
|
|||||||
|
|
||||||
const FileEventEditor = ({ data }: { data: FileEventData }) => {
|
const FileEventEditor = ({ data }: { data: FileEventData }) => {
|
||||||
const { signEventTemplate } = useNDK();
|
const { signEventTemplate } = useNDK();
|
||||||
|
const { serverInfo } = useServerInfo();
|
||||||
const [fileEventData, setFileEventData] = useState(data);
|
const [fileEventData, setFileEventData] = useState(data);
|
||||||
const [selectedThumbnail, setSelectedThumbnail] = useState<string | undefined>();
|
const [selectedThumbnail, setSelectedThumbnail] = useState<string | undefined>();
|
||||||
|
|
||||||
@ -78,15 +80,15 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => {
|
|||||||
}
|
}
|
||||||
}, [fileEventData]);
|
}, [fileEventData]);
|
||||||
|
|
||||||
function extractProtocolAndDomain(url: string): string | null {
|
function extractDomain(url: string): string | null {
|
||||||
const regex = /^(https?:\/\/[^/]+)/;
|
const regex = /^(https?:\/\/)([^/]+)/;
|
||||||
const match = url.match(regex);
|
const match = url.match(regex);
|
||||||
return match ? match[0] : null;
|
return match ? match[2]?.toLocaleLowerCase() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const publishSelectedThumbnailToAllOwnServers = async (): Promise<BlobDescriptor | undefined> => {
|
const publishSelectedThumbnailToAllOwnServers = async (): Promise<BlobDescriptor | undefined> => {
|
||||||
// TODO investigate why mimetype is not set for reuploaded thumbnail (on mediaserver)
|
// 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
|
// upload selected thumbnail to the same blossom servers as the video
|
||||||
let uploadedThumbnails: BlobDescriptor[] = [];
|
let uploadedThumbnails: BlobDescriptor[] = [];
|
||||||
@ -94,7 +96,11 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => {
|
|||||||
uploadedThumbnails = (
|
uploadedThumbnails = (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
servers.map(s => {
|
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[];
|
).filter(t => t !== undefined) as BlobDescriptor[];
|
||||||
@ -211,7 +217,8 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => {
|
|||||||
className="textarea textarea-primary"
|
className="textarea textarea-primary"
|
||||||
placeholder="Caption"
|
placeholder="Caption"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
{isAudio && (
|
||||||
|
<>
|
||||||
<span className="font-bold">Genre</span>
|
<span className="font-bold">Genre</span>
|
||||||
<div>
|
<div>
|
||||||
<select
|
<select
|
||||||
@ -249,7 +256,8 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<span className="font-bold">Tags</span>
|
<span className="font-bold">Tags</span>
|
||||||
<TagInput
|
<TagInput
|
||||||
tags={fileEventData.tags}
|
tags={fileEventData.tags}
|
||||||
|
@ -18,16 +18,16 @@ export const ResizeOptions: ResizeOptionType[] = [
|
|||||||
width: undefined,
|
width: undefined,
|
||||||
height: undefined,
|
height: undefined,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'max. 2048x2048 pixels',
|
|
||||||
width: 2048,
|
|
||||||
height: 2048,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'max. 1080x1080 pixels',
|
name: 'max. 1080x1080 pixels',
|
||||||
width: 1080,
|
width: 1080,
|
||||||
height: 1080,
|
height: 1080,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'max. 2048x2048 pixels',
|
||||||
|
width: 2048,
|
||||||
|
height: 2048,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export type TransferStats = {
|
export type TransferStats = {
|
||||||
@ -170,7 +170,7 @@ const UploadFileSelection: React.FC<UploadFileSelectionProps> = ({
|
|||||||
value={imageResize}
|
value={imageResize}
|
||||||
>
|
>
|
||||||
{ResizeOptions.map((ro, i) => (
|
{ResizeOptions.map((ro, i) => (
|
||||||
<option key={ro.name} disabled={i == 0}>
|
<option key={ro.name} value={i} disabled={i == 0}>
|
||||||
{ro.name}
|
{ro.name}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
|
@ -118,8 +118,7 @@ function Upload() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
let newBlob: BlobDescriptor;
|
let newBlob: BlobDescriptor;
|
||||||
if (server.type == 'blossom') {
|
const progressHandler = (progressEvent: AxiosProgressEvent) => {
|
||||||
newBlob = await uploadBlob(serverUrl, file, uploadAuth, progressEvent => {
|
|
||||||
setTransfers(ut => ({
|
setTransfers(ut => ({
|
||||||
...ut,
|
...ut,
|
||||||
[server.name]: {
|
[server.name]: {
|
||||||
@ -128,10 +127,13 @@ function Upload() {
|
|||||||
rate: progressEvent.rate || 0,
|
rate: progressEvent.rate || 0,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
});
|
};
|
||||||
|
if (server.type == 'blossom') {
|
||||||
|
newBlob = await uploadBlob(serverUrl, file, uploadAuth, progressHandler);
|
||||||
} else {
|
} else {
|
||||||
newBlob = await uploadNip96File(server, file, '', signEventTemplate);
|
newBlob = await uploadNip96File(server, file, '', signEventTemplate, progressHandler);
|
||||||
}
|
}
|
||||||
|
console.log('newBlob', newBlob);
|
||||||
serverTransferred += file.size;
|
serverTransferred += file.size;
|
||||||
setTransfers(ut => ({
|
setTransfers(ut => ({
|
||||||
...ut,
|
...ut,
|
||||||
|
@ -53,10 +53,7 @@ export const uploadBlossomBlob = async (
|
|||||||
return res.data;
|
return res.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const downloadBlossomBlob = async (
|
export const downloadBlossomBlob = async (url: string, onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void) => {
|
||||||
url: string,
|
|
||||||
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void
|
|
||||||
) => {
|
|
||||||
const response = await axios.get(url, {
|
const response = await axios.get(url, {
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
onDownloadProgress,
|
onDownloadProgress,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { BlobDescriptor, EventTemplate, SignedEvent } from 'blossom-client-sdk';
|
import { BlobDescriptor, EventTemplate, SignedEvent } from 'blossom-client-sdk';
|
||||||
import { Server } from './useUserServers';
|
import { Server } from './useUserServers';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import axios, { AxiosProgressEvent } from 'axios';
|
||||||
|
|
||||||
type MediaTransformation = 'resizing' | 'format_conversion' | 'compression' | 'metadata_stripping';
|
type MediaTransformation = 'resizing' | 'format_conversion' | 'compression' | 'metadata_stripping';
|
||||||
|
|
||||||
@ -74,7 +75,6 @@ async function createNip98UploadAuthToken(
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
const signedEvent = await signEventTemplate(authEvent);
|
const signedEvent = await signEventTemplate(authEvent);
|
||||||
console.log(JSON.stringify(signedEvent));
|
|
||||||
return btoa(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(
|
export async function fetchNip96List(
|
||||||
server: Server,
|
server: Server,
|
||||||
signEventTemplate: (template: EventTemplate) => Promise<SignedEvent>
|
signEventTemplate: (template: EventTemplate) => Promise<SignedEvent>,
|
||||||
|
onProgress?: (progressEvent: AxiosProgressEvent) => void
|
||||||
) {
|
) {
|
||||||
const page = 0;
|
const page = 0;
|
||||||
const count = 100;
|
const count = 100;
|
||||||
const baseUrl = server.nip96?.api_url || server.url;
|
const baseUrl = server.nip96?.api_url || server.url;
|
||||||
const listUrl = `${baseUrl}?page=${page}&count=${count}`;
|
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)}` },
|
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(
|
return list.files.map(
|
||||||
file =>
|
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.
|
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(
|
export async function uploadNip96File(
|
||||||
server: Server,
|
server: Server,
|
||||||
file: File,
|
file: File,
|
||||||
caption: string,
|
caption: string,
|
||||||
signEventTemplate: (template: EventTemplate) => Promise<SignedEvent>
|
signEventTemplate: (template: EventTemplate) => Promise<SignedEvent>,
|
||||||
|
onProgress?: (progressEvent: AxiosProgressEvent) => void
|
||||||
): Promise<BlobDescriptor> {
|
): Promise<BlobDescriptor> {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
formData.append('caption', caption || ''); // RECOMMENDED TODO ADD
|
formData.append('caption', caption || ''); // RECOMMENDED TODO ADD
|
||||||
//formData.append('expiration', server.expiration || '');
|
//formData.append('expiration', server.expiration || '');
|
||||||
formData.append('size', file.size.toString());
|
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('media_type', // avatar / banner
|
||||||
formData.append('content_type', file.type || '');
|
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 baseUrl = server.nip96?.api_url || server.url;
|
||||||
|
|
||||||
const response = await fetch(baseUrl, {
|
const response = await axios.post(baseUrl, formData, {
|
||||||
method: 'POST',
|
|
||||||
headers: { Authorization: `Nostr ${await createNip98UploadAuthToken(baseUrl, 'POST', signEventTemplate)}` },
|
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}`);
|
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);
|
console.log(result);
|
||||||
|
|
||||||
const x = getValueByTag(result.nip94_event?.tags, 'x') || getValueByTag(result.nip94_event?.tags, 'ox');
|
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 auth = await createNip98UploadAuthToken(url, 'DELETE', signEventTemplate);
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await axios.delete(url, {
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
headers: {
|
||||||
...headers,
|
...headers,
|
||||||
authorization: `Nostr ${auth}`,
|
authorization: `Nostr ${auth}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (response.status >= 400) {
|
||||||
throw new Error(`Failed to delete file: ${response.statusText}`);
|
throw new Error(`Failed to delete file: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await response.json();
|
const result = response.data;
|
||||||
if (result.status !== 'success') {
|
if (result.status !== 'success') {
|
||||||
throw new Error(`Failed to delete file: ${result.message}`);
|
throw new Error(`Failed to delete file: ${result.message}`);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { AxiosProgressEvent } from 'axios';
|
import { AxiosProgressEvent } from 'axios';
|
||||||
import { BlobDescriptor, EventTemplate, SignedEvent } from 'blossom-client-sdk';
|
import { BlobDescriptor, EventTemplate, SignedEvent } from 'blossom-client-sdk';
|
||||||
import { downloadBlossomBlob, mirrordBlossomBlob, uploadBlossomBlob } from './blossom';
|
import { downloadBlossomBlob, mirrordBlossomBlob, uploadBlossomBlob } from './blossom';
|
||||||
|
import { Server } from './useUserServers';
|
||||||
|
import { uploadNip96File } from './nip96';
|
||||||
|
|
||||||
async function blobUrlToFile(blobUrl: string, fileName: string): Promise<File> {
|
async function blobUrlToFile(blobUrl: string, fileName: string): Promise<File> {
|
||||||
const response = await fetch(blobUrl);
|
const response = await fetch(blobUrl);
|
||||||
@ -12,26 +14,36 @@ async function blobUrlToFile(blobUrl: string, fileName: string): Promise<File> {
|
|||||||
// TODO support nip96
|
// TODO support nip96
|
||||||
export const transferBlob = async (
|
export const transferBlob = async (
|
||||||
sourceUrl: string,
|
sourceUrl: string,
|
||||||
targetServer: string,
|
targetServer: Server,
|
||||||
signEventTemplate: (template: EventTemplate) => Promise<SignedEvent>,
|
signEventTemplate: (template: EventTemplate) => Promise<SignedEvent>,
|
||||||
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void
|
onProgress?: (progressEvent: AxiosProgressEvent) => void
|
||||||
): Promise<BlobDescriptor> => {
|
): Promise<BlobDescriptor> => {
|
||||||
console.log({ sourceUrl, targetServer });
|
console.log({ sourceUrl, targetServer });
|
||||||
|
|
||||||
if (sourceUrl.startsWith('blob:')) {
|
if (sourceUrl.startsWith('blob:')) {
|
||||||
const file = await blobUrlToFile(sourceUrl, 'cover.jpg');
|
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 {
|
} else {
|
||||||
const blob = await mirrordBlossomBlob(targetServer, sourceUrl, signEventTemplate);
|
return await uploadNip96File(targetServer, file, 'cover.jpg', signEventTemplate, onProgress);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (targetServer.type == 'blossom') {
|
||||||
|
const blob = await mirrordBlossomBlob(targetServer.url, sourceUrl, signEventTemplate);
|
||||||
if (blob) return blob;
|
if (blob) return blob;
|
||||||
console.log('Mirror failed. Using download + upload instead.');
|
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 fileName = sourceUrl.replace(/.*\//, '');
|
||||||
|
|
||||||
const file = new File([result.data], fileName, { type: result.type, lastModified: new Date().getTime() });
|
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: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
plugins: [require('daisyui')],
|
plugins: [require('daisyui')],
|
||||||
|
|
||||||
daisyui: {
|
daisyui: {
|
||||||
themes: [
|
themes: [
|
||||||
{
|
{
|
||||||
@ -13,7 +12,7 @@ export default {
|
|||||||
...require('daisyui/src/theming/themes')['dark'],
|
...require('daisyui/src/theming/themes')['dark'],
|
||||||
primary: '#be185d',
|
primary: '#be185d',
|
||||||
secondary: '#2563eb',
|
secondary: '#2563eb',
|
||||||
accent: '#fb923c',
|
accent: '#ffffff',
|
||||||
info: '#a5b4fc',
|
info: '#a5b4fc',
|
||||||
success: '#6ee7b7',
|
success: '#6ee7b7',
|
||||||
warning: '#facc15',
|
warning: '#facc15',
|
||||||
@ -25,7 +24,7 @@ export default {
|
|||||||
...require('daisyui/src/theming/themes')['cupcake'],
|
...require('daisyui/src/theming/themes')['cupcake'],
|
||||||
primary: '#be185d',
|
primary: '#be185d',
|
||||||
secondary: '#2563eb',
|
secondary: '#2563eb',
|
||||||
accent: '#fb923c',
|
accent: '#000000',
|
||||||
neutral: '#e0e0e0',
|
neutral: '#e0e0e0',
|
||||||
info: '#a5b4fc',
|
info: '#a5b4fc',
|
||||||
success: '#6ee7b7',
|
success: '#6ee7b7',
|
||||||
|
Loading…
Reference in New Issue
Block a user