mirror of
https://git.v0l.io/florian/bouquet.git
synced 2025-03-17 18:13:00 +01:00
feat: Improved audio publishing (genres, thumbnail)
This commit is contained in:
parent
679cca119d
commit
40c8b3db15
4
TODO.md
Normal file
4
TODO.md
Normal file
@ -0,0 +1,4 @@
|
||||
|
||||
|
||||
- When 403 from nogood, error should be displayed.
|
||||
- Delete does not work in the ALL SERVERS view
|
@ -1,14 +1,16 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
const CheckBox = ({
|
||||
name,
|
||||
checked,
|
||||
setChecked,
|
||||
label,
|
||||
children,
|
||||
disabled = false,
|
||||
}: {
|
||||
name: string;
|
||||
checked: boolean;
|
||||
setChecked: (checked: boolean) => void;
|
||||
label: string;
|
||||
children: ReactNode;
|
||||
disabled: boolean;
|
||||
}) => (
|
||||
<>
|
||||
@ -20,8 +22,8 @@ const CheckBox = ({
|
||||
checked={checked}
|
||||
onChange={e => setChecked(e.currentTarget.checked)}
|
||||
/>
|
||||
<label htmlFor={name} className="cursor-pointer select-none">
|
||||
{label}
|
||||
<label htmlFor={name} className="cursor-pointer select-none flex flex-row gap-2">
|
||||
{children}
|
||||
</label>
|
||||
</>
|
||||
);
|
||||
|
@ -7,6 +7,7 @@ import { BlobDescriptor } from 'blossom-client-sdk';
|
||||
import { transferBlob } from '../../utils/transfer';
|
||||
import { useNDK } from '../../utils/ndk';
|
||||
import TagInput from '../TagInput';
|
||||
import { allGenres } from '../../utils/genres';
|
||||
|
||||
export type FileEventData = {
|
||||
originalFile: File;
|
||||
@ -19,7 +20,7 @@ export type FileEventData = {
|
||||
m?: string;
|
||||
size: number;
|
||||
thumbnails?: string[];
|
||||
thumbnail?: string;
|
||||
publishedThumbnail?: string;
|
||||
blurHash?: string;
|
||||
tags: string[];
|
||||
duration?: string;
|
||||
@ -28,6 +29,8 @@ export type FileEventData = {
|
||||
title?: string;
|
||||
album?: string;
|
||||
year?: string;
|
||||
genre?: string;
|
||||
subgenre?: string;
|
||||
};
|
||||
|
||||
const FileEventEditor = ({ data }: { data: FileEventData }) => {
|
||||
@ -53,7 +56,7 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => {
|
||||
fileEventData.artist ||
|
||||
fileEventData.album ||
|
||||
fileEventData.year ||
|
||||
fileEventData.thumbnail
|
||||
fileEventData.publishedThumbnail
|
||||
)
|
||||
) {
|
||||
console.log('getting id3 cover image', fileEventData.x, fileEventData.url[0], fileEventData.originalFile);
|
||||
@ -68,9 +71,9 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => {
|
||||
album: id3.album,
|
||||
title: id3.title,
|
||||
year: id3.year,
|
||||
thumbnail: res.coverFull,
|
||||
thumbnails: res.coverFull ? [res.coverFull] : [],
|
||||
});
|
||||
setSelectedThumbnail(res.coverFull);
|
||||
});
|
||||
}
|
||||
}, [fileEventData]);
|
||||
@ -81,9 +84,9 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => {
|
||||
return match ? match[0] : null;
|
||||
}
|
||||
|
||||
const publishSelectedThumbnailToOwnServer = async (): Promise<BlobDescriptor | undefined> => {
|
||||
const publishSelectedThumbnailToAllOwnServers = async (): Promise<BlobDescriptor | undefined> => {
|
||||
// TODO investigate why mimetype is not set for reuploaded thumbnail (on mediaserver)
|
||||
const servers = data.url.map(extractProtocolAndDomain);
|
||||
const servers = fileEventData.url.map(extractProtocolAndDomain);
|
||||
|
||||
// upload selected thumbnail to the same blossom servers as the video
|
||||
let uploadedThumbnails: BlobDescriptor[] = [];
|
||||
@ -107,7 +110,6 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => {
|
||||
}
|
||||
}
|
||||
}, [fileEventData.thumbnails, selectedThumbnail]);
|
||||
|
||||
// TODO add tags editor
|
||||
return (
|
||||
<>
|
||||
@ -147,12 +149,16 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => {
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{isAudio && fileEventData.thumbnail && (
|
||||
{isAudio && (fileEventData.publishedThumbnail || selectedThumbnail) && (
|
||||
<div className="w-2/6">
|
||||
<img
|
||||
width={300}
|
||||
height={300}
|
||||
src={`https://images.slidestr.net/insecure/f:webp/rs:fill:600/plain/${fileEventData.thumbnail || selectedThumbnail}`}
|
||||
src={
|
||||
fileEventData.publishedThumbnail
|
||||
? `https://images.slidestr.net/insecure/f:webp/rs:fill:600/plain/${fileEventData.publishedThumbnail}`
|
||||
: selectedThumbnail
|
||||
}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
@ -206,6 +212,44 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => {
|
||||
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}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<span className="font-bold">Tags</span>
|
||||
<TagInput
|
||||
tags={fileEventData.tags}
|
||||
@ -242,8 +286,8 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => {
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={async () => {
|
||||
if (!data.thumbnail) {
|
||||
await publishSelectedThumbnailToOwnServer();
|
||||
if (!fileEventData.publishedThumbnail) {
|
||||
await publishSelectedThumbnailToAllOwnServers();
|
||||
}
|
||||
setJsonOutput(await publishFileEvent(fileEventData));
|
||||
}}
|
||||
@ -252,19 +296,40 @@ const FileEventEditor = ({ data }: { data: FileEventData }) => {
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={async () => setJsonOutput(await publishAudioEvent(fileEventData))}
|
||||
onClick={async () => {
|
||||
if (!fileEventData.publishedThumbnail) {
|
||||
const selfHostedThumbnail = await publishSelectedThumbnailToAllOwnServers();
|
||||
if (selfHostedThumbnail) {
|
||||
const newData: FileEventData = {
|
||||
...fileEventData,
|
||||
publishedThumbnail: selfHostedThumbnail.url,
|
||||
thumbnails: [selfHostedThumbnail.url],
|
||||
};
|
||||
setFileEventData(newData);
|
||||
setJsonOutput(await publishAudioEvent(newData));
|
||||
} else {
|
||||
// self hosting failed
|
||||
console.log('self hosting failed');
|
||||
setJsonOutput(await publishAudioEvent(fileEventData));
|
||||
}
|
||||
} else {
|
||||
// data thumbnail already defined
|
||||
console.log('data thumbnail already defined');
|
||||
setJsonOutput(await publishAudioEvent(fileEventData));
|
||||
}
|
||||
}}
|
||||
>
|
||||
Create Audio Event
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={async () => {
|
||||
if (!data.thumbnail) {
|
||||
const selfHostedThumbnail = await publishSelectedThumbnailToOwnServer();
|
||||
if (!fileEventData.publishedThumbnail) {
|
||||
const selfHostedThumbnail = await publishSelectedThumbnailToAllOwnServers();
|
||||
if (selfHostedThumbnail) {
|
||||
const newData = {
|
||||
const newData: FileEventData = {
|
||||
...fileEventData,
|
||||
thumbnail: selfHostedThumbnail.url,
|
||||
publishedThumbnail: selfHostedThumbnail.url,
|
||||
thumbnails: [selfHostedThumbnail.url],
|
||||
};
|
||||
setFileEventData(newData);
|
||||
|
@ -37,9 +37,9 @@ export const usePublishing = () => {
|
||||
if (data.blurHash) {
|
||||
e.tags.push(['blurhash', data.blurHash]);
|
||||
}
|
||||
if (data.thumbnail) {
|
||||
e.tags.push(['thumb', data.thumbnail]);
|
||||
e.tags.push(['image', data.thumbnail]);
|
||||
if (data.publishedThumbnail) {
|
||||
e.tags.push(['thumb', data.publishedThumbnail]);
|
||||
e.tags.push(['image', data.publishedThumbnail]);
|
||||
}
|
||||
|
||||
const ev = new NDKEvent(ndk, e);
|
||||
@ -56,8 +56,7 @@ export const usePublishing = () => {
|
||||
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}`]),
|
||||
...uniq(data.url).map(du => ['imeta', `url ${du}`, `m ${data.m}`, `x ${data.x}`]),
|
||||
...data.tags.map(t => ['t', t]),
|
||||
],
|
||||
kind: KIND_AUDIO,
|
||||
@ -70,14 +69,27 @@ export const usePublishing = () => {
|
||||
}
|
||||
|
||||
if (data.artist) {
|
||||
e.tags.push(['creator', `${data.artist}`]);
|
||||
e.tags.push(['creator', `${data.artist}`, 'Artist']);
|
||||
e.tags.push(['c', `${data.artist}`, 'artist']);
|
||||
}
|
||||
|
||||
if (data.album) {
|
||||
e.tags.push(['album', `${data.album}`]);
|
||||
e.tags.push(['c', `${data.album}`, 'album']);
|
||||
}
|
||||
|
||||
if (data.publishedThumbnail) {
|
||||
e.tags.push(['cover', `${data.publishedThumbnail}`]);
|
||||
}
|
||||
|
||||
if (data.genre) {
|
||||
e.tags.push(['c', `${data.genre}`, 'genre']);
|
||||
|
||||
if (data.subgenre) {
|
||||
e.tags.push(['c', `${data.subgenre}`, 'subgenre']);
|
||||
}
|
||||
}
|
||||
|
||||
// published_at
|
||||
|
||||
const ev = new NDKEvent(ndk, e);
|
||||
await ev.sign();
|
||||
console.log(ev.rawEvent());
|
||||
@ -118,9 +130,9 @@ export const usePublishing = () => {
|
||||
if (data.m) {
|
||||
e.tags.push(['m', data.m]);
|
||||
}
|
||||
if (data.thumbnail) {
|
||||
e.tags.push(['thumb', data.thumbnail]);
|
||||
e.tags.push(['image', data.thumbnail]);
|
||||
if (data.publishedThumbnail) {
|
||||
e.tags.push(['thumb', data.publishedThumbnail]);
|
||||
e.tags.push(['image', data.publishedThumbnail]);
|
||||
}
|
||||
|
||||
const ev = new NDKEvent(ndk, e);
|
||||
|
@ -4,8 +4,8 @@ import { useNDK } from '../utils/ndk';
|
||||
import { useServerInfo } from '../utils/useServerInfo';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { removeExifData } from '../utils/exif';
|
||||
import axios, { AxiosProgressEvent } from 'axios';
|
||||
import { ArrowUpOnSquareIcon, TrashIcon } from '@heroicons/react/24/outline';
|
||||
import axios, { AxiosError, AxiosProgressEvent } from 'axios';
|
||||
import { ArrowUpOnSquareIcon, ServerIcon, TrashIcon } from '@heroicons/react/24/outline';
|
||||
import CheckBox from '../components/CheckBox/CheckBox';
|
||||
import ProgressBar from '../components/ProgressBar/ProgressBar';
|
||||
import { formatFileSize } from '../utils/utils';
|
||||
@ -21,6 +21,7 @@ type TransferStats = {
|
||||
size: number;
|
||||
transferred: number;
|
||||
rate: number;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
/*
|
||||
@ -118,6 +119,7 @@ function Upload() {
|
||||
}
|
||||
|
||||
const fileDimensions: { [key: string]: FileEventData } = {};
|
||||
|
||||
for (const file of filesToUpload) {
|
||||
let data = {
|
||||
content: file.name.replace(/\.[a-zA-Z0-9]{3,4}$/, ''),
|
||||
@ -158,32 +160,44 @@ function Upload() {
|
||||
const uploadAuth = await BlossomClient.getUploadAuth(file, signEventTemplate, 'Upload Blob');
|
||||
console.log(`Created auth event in ${Date.now() - authStartTime} ms`, uploadAuth);
|
||||
|
||||
const newBlob = await uploadBlob(serverUrl, file, uploadAuth, progressEvent => {
|
||||
try {
|
||||
const newBlob = await uploadBlob(serverUrl, file, uploadAuth, progressEvent => {
|
||||
setTransfers(ut => ({
|
||||
...ut,
|
||||
[server.name]: {
|
||||
...ut[server.name],
|
||||
transferred: serverTransferred + progressEvent.loaded,
|
||||
rate: progressEvent.rate || 0,
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
serverTransferred += file.size;
|
||||
setTransfers(ut => ({
|
||||
...ut,
|
||||
[server.name]: {
|
||||
...ut[server.name],
|
||||
transferred: serverTransferred + progressEvent.loaded,
|
||||
rate: progressEvent.rate || 0,
|
||||
},
|
||||
[server.name]: { ...ut[server.name], transferred: serverTransferred, rate: 0 },
|
||||
}));
|
||||
});
|
||||
|
||||
serverTransferred += file.size;
|
||||
setTransfers(ut => ({
|
||||
...ut,
|
||||
[server.name]: { ...ut[server.name], transferred: serverTransferred, rate: 0 },
|
||||
}));
|
||||
|
||||
fileDimensions[file.name] = {
|
||||
...fileDimensions[file.name],
|
||||
x: newBlob.sha256,
|
||||
url: primary
|
||||
? [newBlob.url, ...fileDimensions[file.name].url]
|
||||
: [...fileDimensions[file.name].url, newBlob.url],
|
||||
size: newBlob.size,
|
||||
m: newBlob.type,
|
||||
};
|
||||
fileDimensions[file.name] = {
|
||||
...fileDimensions[file.name],
|
||||
x: newBlob.sha256,
|
||||
url: primary
|
||||
? [newBlob.url, ...fileDimensions[file.name].url]
|
||||
: [...fileDimensions[file.name].url, newBlob.url],
|
||||
size: newBlob.size,
|
||||
m: newBlob.type,
|
||||
};
|
||||
} catch (e) {
|
||||
const axiosError = e as AxiosError;
|
||||
const response = axiosError.response?.data as {message?: string}
|
||||
console.error(e);
|
||||
console.warn();
|
||||
// Record error in transfer log
|
||||
setTransfers(ut => ({
|
||||
...ut,
|
||||
[server.name]: { ...ut[server.name], error: `${axiosError.message} / ${response.message}` },
|
||||
}));
|
||||
}
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: ['blobs', server.name] });
|
||||
};
|
||||
@ -198,6 +212,7 @@ function Upload() {
|
||||
for (const server of servers) {
|
||||
if (newTransfers[server.name].enabled) {
|
||||
newTransfers[server.name].size = totalSize;
|
||||
newTransfers[server.name].error = undefined;
|
||||
}
|
||||
}
|
||||
return newTransfers;
|
||||
@ -214,7 +229,14 @@ function Upload() {
|
||||
}
|
||||
|
||||
setUploadBusy(false);
|
||||
setUploadStep(2);
|
||||
|
||||
//console.log(transfers);
|
||||
// TODO transfer can not be accessed yet, errors are not visible here. TODO pout errors somewhere else
|
||||
const errorsTransfers = Object.keys(transfers).filter(ts => transfers[ts].enabled && !!transfers[ts].error);
|
||||
if (errorsTransfers.length == 0) {
|
||||
// Only go to the next step if no errors have occured
|
||||
setUploadStep(2);
|
||||
}
|
||||
};
|
||||
|
||||
const clearTransfers = () => {
|
||||
@ -264,6 +286,8 @@ function Upload() {
|
||||
};
|
||||
|
||||
const sizeOfFilesToUpload = useMemo(() => files.reduce((acc, file) => (acc += file.size), 0), [files]);
|
||||
const imagesAreUploaded = useMemo(() => files.some(file => file.type.startsWith('image/')), [files]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ul className="steps p-8">
|
||||
@ -273,103 +297,140 @@ function Upload() {
|
||||
<li className={`step ${uploadStep >= 3 ? 'step-primary' : ''}`}>Publish to NOSTR</li>
|
||||
</ul>
|
||||
{uploadStep <= 1 && (
|
||||
<div className=" bg-base-200 rounded-xl p-4 text-neutral-content gap-4 flex flex-col">
|
||||
<input
|
||||
id="browse"
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
disabled={uploadBusy}
|
||||
hidden
|
||||
multiple
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<label
|
||||
htmlFor="browse"
|
||||
className="p-8 bg-base-100 rounded-lg hover:text-primary text-neutral-content border-dashed border-neutral-content border-opacity-50 border-2 block cursor-pointer text-center"
|
||||
onDrop={handleDrop}
|
||||
onDragOver={event => event.preventDefault()}
|
||||
>
|
||||
<ArrowUpOnSquareIcon className="w-8 inline" /> Browse or drag & drop
|
||||
</label>
|
||||
<h3 className="text-lg">Servers</h3>
|
||||
<div className="cursor-pointer grid gap-2" style={{ gridTemplateColumns: '1.5em 20em auto' }}>
|
||||
{servers.map(s => (
|
||||
<>
|
||||
<CheckBox
|
||||
name={s.name}
|
||||
disabled={uploadBusy}
|
||||
checked={transfers[s.name]?.enabled || false}
|
||||
setChecked={c =>
|
||||
setTransfers(ut => ({ ...ut, [s.name]: { enabled: c, transferred: 0, size: 0, rate: 0 } }))
|
||||
}
|
||||
label={s.name}
|
||||
></CheckBox>
|
||||
{transfers[s.name]?.enabled ? (
|
||||
<ProgressBar
|
||||
value={transfers[s.name].transferred}
|
||||
max={transfers[s.name].size}
|
||||
description={transfers[s.name].rate > 0 ? '' + formatFileSize(transfers[s.name].rate) + '/s' : ''}
|
||||
/>
|
||||
) : (
|
||||
<div></div>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
<h3 className="text-lg text-neutral-content">Image Options</h3>
|
||||
<div className="cursor-pointer grid gap-2 items-center" style={{ gridTemplateColumns: '1.5em auto' }}>
|
||||
<CheckBox
|
||||
name="cleanData"
|
||||
disabled={uploadBusy}
|
||||
checked={cleanPrivateData}
|
||||
setChecked={c => setCleanPrivateData(c)}
|
||||
label="Clean private data in images (EXIF)"
|
||||
></CheckBox>
|
||||
<input
|
||||
className="checkbox checkbox-primary "
|
||||
id="resizeOption"
|
||||
disabled={uploadBusy}
|
||||
type="checkbox"
|
||||
checked={imageResize > 0}
|
||||
onChange={() => setImageResize(irs => (irs > 0 ? 0 : 1))}
|
||||
/>
|
||||
<div>
|
||||
<label htmlFor="resizeOption" className="cursor-pointer select-none">
|
||||
Resize Image
|
||||
</label>
|
||||
<select
|
||||
disabled={uploadBusy || imageResize == 0}
|
||||
className="select select-bordered select-sm ml-4 w-full max-w-xs"
|
||||
onChange={e => setImageResize(e.target.selectedIndex)}
|
||||
value={imageResize}
|
||||
<div className="bg-base-200 rounded-xl p-4 text-neutral-content gap-4 flex flex-col">
|
||||
{uploadStep == 0 && (
|
||||
<>
|
||||
<input
|
||||
id="browse"
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
disabled={uploadBusy}
|
||||
hidden
|
||||
multiple
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<label
|
||||
htmlFor="browse"
|
||||
className="p-8 bg-base-100 rounded-lg hover:text-primary text-neutral-content border-dashed border-neutral-content border-opacity-50 border-2 block cursor-pointer text-center"
|
||||
onDrop={handleDrop}
|
||||
onDragOver={event => event.preventDefault()}
|
||||
>
|
||||
{ResizeOptions.map((ro, i) => (
|
||||
<option key={ro.name} disabled={i == 0}>
|
||||
{ro.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row gap-2">
|
||||
<button className="btn btn-primary" onClick={() => upload()} disabled={uploadBusy || files.length == 0}>
|
||||
Upload{files.length > 0 ? (files.length == 1 ? ` 1 file` : ` ${files.length} files`) : ''} /{' '}
|
||||
{formatFileSize(sizeOfFilesToUpload)}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary "
|
||||
disabled={uploadBusy || files.length == 0}
|
||||
onClick={() => {
|
||||
clearTransfers();
|
||||
setFiles([]);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="w-6" />
|
||||
</button>
|
||||
</div>
|
||||
<ArrowUpOnSquareIcon className="w-8 inline" /> Browse or drag & drop
|
||||
</label>
|
||||
|
||||
<div className="cursor-pointer gap-2 flex flex-row">
|
||||
<div className="flex flex-col gap-4 w-1/2">
|
||||
<h3 className="text-lg text-neutral-content">Servers</h3>
|
||||
<div className="grid gap-2" style={{ gridTemplateColumns: '2em auto' }}>
|
||||
{servers.map(s => (
|
||||
<CheckBox
|
||||
name={s.name}
|
||||
disabled={uploadBusy}
|
||||
checked={transfers[s.name]?.enabled || false}
|
||||
setChecked={c =>
|
||||
setTransfers(ut => ({ ...ut, [s.name]: { enabled: c, transferred: 0, size: 0, rate: 0 } }))
|
||||
}
|
||||
>
|
||||
<ServerIcon className="w-6" />
|
||||
{s.name} <div className="badge badge-neutral">{serverInfo[s.name].type}</div>
|
||||
</CheckBox>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{imagesAreUploaded && (
|
||||
<div className="flex flex-col gap-4 w-1/2">
|
||||
<h3 className="text-lg text-neutral-content">Image Options</h3>
|
||||
<div
|
||||
className="cursor-pointer grid gap-2 items-center"
|
||||
style={{ gridTemplateColumns: '1.5em auto' }}
|
||||
>
|
||||
<CheckBox
|
||||
name="cleanData"
|
||||
disabled={uploadBusy}
|
||||
checked={cleanPrivateData}
|
||||
setChecked={c => setCleanPrivateData(c)}
|
||||
>
|
||||
Clean private data in images (EXIF)
|
||||
</CheckBox>
|
||||
<input
|
||||
className="checkbox checkbox-primary "
|
||||
id="resizeOption"
|
||||
disabled={uploadBusy}
|
||||
type="checkbox"
|
||||
checked={imageResize > 0}
|
||||
onChange={() => setImageResize(irs => (irs > 0 ? 0 : 1))}
|
||||
/>
|
||||
<div>
|
||||
<label htmlFor="resizeOption" className="cursor-pointer select-none">
|
||||
Resize Image
|
||||
</label>
|
||||
<select
|
||||
disabled={uploadBusy || imageResize == 0}
|
||||
className="select select-bordered select-sm ml-4 w-full max-w-xs"
|
||||
onChange={e => setImageResize(e.target.selectedIndex)}
|
||||
value={imageResize}
|
||||
>
|
||||
{ResizeOptions.map((ro, i) => (
|
||||
<option key={ro.name} disabled={i == 0}>
|
||||
{ro.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
<button className="btn btn-primary" onClick={() => upload()} disabled={uploadBusy || files.length == 0}>
|
||||
Upload{files.length > 0 ? (files.length == 1 ? ` 1 file` : ` ${files.length} files`) : ''} /{' '}
|
||||
{formatFileSize(sizeOfFilesToUpload)}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary "
|
||||
disabled={uploadBusy || files.length == 0}
|
||||
onClick={() => {
|
||||
clearTransfers();
|
||||
setFiles([]);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="w-6" />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{uploadStep == 1 && (
|
||||
<>
|
||||
<h3 className="text-lg">Servers</h3>
|
||||
<div className="cursor-pointer grid gap-2" style={{ gridTemplateColumns: '1.5em 20em auto' }}>
|
||||
{servers.map(
|
||||
s =>
|
||||
transfers[s.name]?.enabled && (
|
||||
<>
|
||||
<ServerIcon></ServerIcon> {s.name}
|
||||
<div className="flex flex-col gap-2">
|
||||
<ProgressBar
|
||||
value={transfers[s.name].transferred}
|
||||
max={transfers[s.name].size}
|
||||
description={
|
||||
transfers[s.name].rate > 0 ? '' + formatFileSize(transfers[s.name].rate) + '/s' : ''
|
||||
}
|
||||
/>
|
||||
{transfers[s.name].error && (
|
||||
<div className="alert alert-error">{transfers[s.name].error}</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{fileEventsToPublish.length > 0 && (
|
||||
|
328
src/utils/genres.ts
Normal file
328
src/utils/genres.ts
Normal file
@ -0,0 +1,328 @@
|
||||
import { groupBy, mapValues } from "lodash";
|
||||
|
||||
// https://raw.githubusercontent.com/wavlake/genre-list/main/list.csv
|
||||
const wavlakeGenres = [
|
||||
['Alternative','Alternative Rock'],
|
||||
['Alternative','College Rock'],
|
||||
['Alternative','Experimental Rock'],
|
||||
['Alternative','Goth Rock'],
|
||||
['Alternative','Grunge'],
|
||||
['Alternative','Hardcore Punk'],
|
||||
['Alternative','Hard Rock'],
|
||||
['Alternative','Indie Rock'],
|
||||
['Alternative','New Wave'],
|
||||
['Alternative','Progressive Rock'],
|
||||
['Alternative','Punk'],
|
||||
['Alternative','Shoegaze'],
|
||||
['Alternative','Steampunk'],
|
||||
['Blues','Acoustic Blues'],
|
||||
['Blues','Chicago Blues'],
|
||||
['Blues','Classic Blues'],
|
||||
['Blues','Contemporary Blues'],
|
||||
['Blues','Country Blues'],
|
||||
['Blues','Delta Blues'],
|
||||
['Blues','Electric Blues'],
|
||||
['Children’s Music','Lullabies'],
|
||||
['Children’s Music','Sing-Along'],
|
||||
['Children’s Music','Stories'],
|
||||
['Classical','Avant-Garde'],
|
||||
['Classical','Baroque'],
|
||||
['Classical','Chamber Music'],
|
||||
['Classical','Chant'],
|
||||
['Classical','Choral'],
|
||||
['Classical','Classical Crossover'],
|
||||
['Classical','Early Music'],
|
||||
['Classical','High Classical'],
|
||||
['Classical','Impressionist'],
|
||||
['Classical','Medieval'],
|
||||
['Classical','Minimalism'],
|
||||
['Classical','Modern Composition'],
|
||||
['Classical','Opera'],
|
||||
['Classical','Orchestral'],
|
||||
['Classical','Renaissance'],
|
||||
['Classical','Romantic'],
|
||||
['Classical','Wedding Music'],
|
||||
['Comedy','Novelty'],
|
||||
['Comedy','Standup Comedy'],
|
||||
['Country','Alternative Country'],
|
||||
['Country','Americana'],
|
||||
['Country','Bluegrass'],
|
||||
['Country','Contemporary Bluegrass'],
|
||||
['Country','Contemporary Country'],
|
||||
['Country','Country Gospel'],
|
||||
['Country','Honky Tonk'],
|
||||
['Country','Outlaw Country'],
|
||||
['Country','Traditional Bluegrass'],
|
||||
['Country','Traditional Country'],
|
||||
['Country','Urban Cowboy'],
|
||||
['Dance/EDM','Breakbeat'],
|
||||
['Dance/EDM','Dubstep'],
|
||||
['Dance/EDM','Exercise'],
|
||||
['Dance/EDM','Garage'],
|
||||
['Dance/EDM','Hardcore'],
|
||||
['Dance/EDM','Hard Dance'],
|
||||
['Dance/EDM','Hi-NRG / Eurodance'],
|
||||
['Dance/EDM','House'],
|
||||
['Dance/EDM','Jackin House'],
|
||||
['Dance/EDM','Jungle/Drum’n\'bass'],
|
||||
['Dance/EDM','Techno'],
|
||||
['Dance/EDM','Trance'],
|
||||
['Easy Listening','Bop'],
|
||||
['Easy Listening','Lounge'],
|
||||
['Easy Listening','Swing'],
|
||||
['Electronic','Ambient'],
|
||||
['Electronic','Crunk'],
|
||||
['Electronic','Downtempo'],
|
||||
['Electronic','Electro'],
|
||||
['Electronic','Electronica'],
|
||||
['Electronic','Electronic Rock'],
|
||||
['Electronic','IDM/Experimental'],
|
||||
['Electronic','Industrial'],
|
||||
['Hip-Hop/Rap','Alternative Rap'],
|
||||
['Hip-Hop/Rap','Bounce'],
|
||||
['Hip-Hop/Rap','Dirty South'],
|
||||
['Hip-Hop/Rap','East Coast Rap'],
|
||||
['Hip-Hop/Rap','Gangsta Rap'],
|
||||
['Hip-Hop/Rap','Hardcore Rap'],
|
||||
['Hip-Hop/Rap','Hip-Hop'],
|
||||
['Hip-Hop/Rap','Latin Rap'],
|
||||
['Hip-Hop/Rap','Old School Rap'],
|
||||
['Hip-Hop/Rap','Rap'],
|
||||
['Hip-Hop/Rap','Underground Rap'],
|
||||
['Hip-Hop/Rap','West Coast Rap'],
|
||||
['Holiday','Chanukah'],
|
||||
['Holiday','Christmas'],
|
||||
['Holiday','Christmas: Children’s'],
|
||||
['Holiday','Christmas: Classic'],
|
||||
['Holiday','Christmas: Classical'],
|
||||
['Holiday','Christmas: Jazz'],
|
||||
['Holiday','Christmas: Modern'],
|
||||
['Holiday','Christmas: Pop'],
|
||||
['Holiday','Christmas: R&B'],
|
||||
['Holiday','Christmas: Religious'],
|
||||
['Holiday','Christmas: Rock'],
|
||||
['Holiday','Easter'],
|
||||
['Holiday','Halloween'],
|
||||
['Holiday','Holiday: Other'],
|
||||
['Holiday','Thanksgiving'],
|
||||
['Inspirational – Christian & Gospel','CCM'],
|
||||
['Inspirational – Christian & Gospel','Christian Metal'],
|
||||
['Inspirational – Christian & Gospel','Christian Pop'],
|
||||
['Inspirational – Christian & Gospel','Christian Rap'],
|
||||
['Inspirational – Christian & Gospel','Christian Rock'],
|
||||
['Inspirational – Christian & Gospel','Classic Christian'],
|
||||
['Inspirational – Christian & Gospel','Contemporary Gospel'],
|
||||
['Inspirational – Christian & Gospel','Gospel'],
|
||||
['Inspirational – Christian & Gospel','Christian & Gospel'],
|
||||
['Inspirational – Christian & Gospel','Praise & Worship'],
|
||||
['Inspirational – Christian & Gospel','Qawwali'],
|
||||
['Inspirational – Christian & Gospel','Southern Gospel'],
|
||||
['Inspirational – Christian & Gospel','Traditional Gospel'],
|
||||
['Instrumental','March (Marching Band)'],
|
||||
['Instrumental','Karaoke'],
|
||||
['Jazz','Acid Jazz'],
|
||||
['Jazz','Avant-Garde Jazz'],
|
||||
['Jazz','Big Band'],
|
||||
['Jazz','Blue Note'],
|
||||
['Jazz','Contemporary Jazz'],
|
||||
['Jazz','Cool'],
|
||||
['Jazz','Crossover Jazz'],
|
||||
['Jazz','Dixieland'],
|
||||
['Jazz','Ethio-jazz'],
|
||||
['Jazz','Fusion'],
|
||||
['Jazz','Hard Bop'],
|
||||
['Jazz','Latin Jazz'],
|
||||
['Jazz','Mainstream Jazz'],
|
||||
['Jazz','Ragtime'],
|
||||
['Jazz','Smooth Jazz'],
|
||||
['Jazz','Trad Jazz'],
|
||||
['Latino','Alternativo & Rock Latino'],
|
||||
['Latino','Baladas y Boleros'],
|
||||
['Latino','Brazilian'],
|
||||
['Latino','Contemporary Latin'],
|
||||
['Latino','Latin Jazz'],
|
||||
['Latino','Pop Latino'],
|
||||
['Latino','Raíces'],
|
||||
['Latino','Reggaeton y Hip-Hop'],
|
||||
['Latino','Regional Mexicano'],
|
||||
['Latino','Salsa y Tropical'],
|
||||
['New Age','Environmental'],
|
||||
['New Age','Healing'],
|
||||
['New Age','Meditation'],
|
||||
['New Age','Nature'],
|
||||
['New Age','Relaxation'],
|
||||
['New Age','Travel'],
|
||||
['Pop','Adult Contemporary'],
|
||||
['Pop','Britpop'],
|
||||
['Pop','Pop/Rock'],
|
||||
['Pop','Soft Rock'],
|
||||
['Pop','Teen Pop'],
|
||||
['Pop','Indie Pop'],
|
||||
['Pop','Anime'],
|
||||
['Pop','K-Pop'],
|
||||
['Pop','J-Pop'],
|
||||
['Pop','French Pop'],
|
||||
['Pop','German Pop'],
|
||||
['R&B/Soul','Contemporary R&B'],
|
||||
['R&B/Soul','Disco'],
|
||||
['R&B/Soul','Doo Wop'],
|
||||
['R&B/Soul','Funk'],
|
||||
['R&B/Soul','Motown'],
|
||||
['R&B/Soul','Neo-Soul'],
|
||||
['R&B/Soul','Quiet Storm'],
|
||||
['R&B/Soul','Soul'],
|
||||
['Reggae','Dancehall'],
|
||||
['Reggae','Dub'],
|
||||
['Reggae','Roots Reggae'],
|
||||
['Reggae','Ska'],
|
||||
['Rock','Adult Alternative'],
|
||||
['Rock','American Trad Rock'],
|
||||
['Rock','Arena Rock'],
|
||||
['Rock','Blues-Rock'],
|
||||
['Rock','British Invasion'],
|
||||
['Rock','Death Metal/Black Metal'],
|
||||
['Rock','Glam Rock'],
|
||||
['Rock','Hair Metal'],
|
||||
['Rock','Hard Rock'],
|
||||
['Rock','Metal'],
|
||||
['Rock','Jam Bands'],
|
||||
['Rock','Prog-Rock/Art Rock'],
|
||||
['Rock','Psychedelic'],
|
||||
['Rock','Rock & Roll'],
|
||||
['Rock','Rockabilly'],
|
||||
['Rock','Roots Rock'],
|
||||
['Rock','Singer/Songwriter'],
|
||||
['Rock','Southern Rock'],
|
||||
['Rock','Surf'],
|
||||
['Rock','Tex-Mex'],
|
||||
['Singer/Songwriter','Alternative Folk'],
|
||||
['Singer/Songwriter','Contemporary Folk'],
|
||||
['Singer/Songwriter','Contemporary Singer/Songwriter'],
|
||||
['Singer/Songwriter','Folk-Rock'],
|
||||
['Singer/Songwriter','New Acoustic'],
|
||||
['Singer/Songwriter','Traditional Folk'],
|
||||
['Singer/Songwriter','German Folk'],
|
||||
['Soundtrack','Foreign Cinema'],
|
||||
['Soundtrack','Musicals'],
|
||||
['Soundtrack','Original Score'],
|
||||
['Soundtrack','Soundtrack'],
|
||||
['Soundtrack','TV Soundtrack'],
|
||||
['Spoken Word'],
|
||||
['Tex-Mex/Tejano','Chicano'],
|
||||
['Tex-Mex/Tejano','Classic'],
|
||||
['Tex-Mex/Tejano','Conjunto'],
|
||||
['Tex-Mex/Tejano','Conjunto Progressive'],
|
||||
['Tex-Mex/Tejano','New Mex'],
|
||||
['Tex-Mex/Tejano','Tex-Mex'],
|
||||
['Vocal','Barbershop'],
|
||||
['Vocal','Doo-wop'],
|
||||
['Vocal','Standards'],
|
||||
['Vocal','Traditional Pop'],
|
||||
['Vocal','Vocal Jazz'],
|
||||
['Vocal','Vocal Pop'],
|
||||
['Vocal','Enka'],
|
||||
['Vocal','Sea Shanty'],
|
||||
['Africa','African Heavy Metal'],
|
||||
['Africa','African Hip Hop'],
|
||||
['Africa','Afro-Beat'],
|
||||
['Africa','Afro-House'],
|
||||
['Africa','Afro-Pop'],
|
||||
['Africa','Apala/Akpala'],
|
||||
['Africa','Benga'],
|
||||
['Africa','Bikutsi'],
|
||||
['Africa','Bongo Flava'],
|
||||
['Africa','Cape Jazz'],
|
||||
['Africa','Chimurenga'],
|
||||
['Africa','Coupé-Décalé'],
|
||||
['Africa','Fuji Music'],
|
||||
['Africa','Genge'],
|
||||
['Africa','Gnawa'],
|
||||
['Africa','Highlife'],
|
||||
['Africa','Hiplife'],
|
||||
['Africa','Isicathamiya'],
|
||||
['Africa','Jit'],
|
||||
['Africa','Jùjú'],
|
||||
['Africa','Kapuka'],
|
||||
['Africa','Kizomba'],
|
||||
['Africa','Kuduro'],
|
||||
['Africa','Kwaito'],
|
||||
['Africa','Kwela'],
|
||||
['Africa','Lingala'],
|
||||
['Africa','Makossa'],
|
||||
['Africa','Maloya'],
|
||||
['Africa','Marrabenta'],
|
||||
['Africa','Mbalax'],
|
||||
['Africa','Mbaqanga'],
|
||||
['Africa','Mbube'],
|
||||
['Africa','Morna'],
|
||||
['Africa','Museve'],
|
||||
['Africa','Negro Spiritual'],
|
||||
['Africa','Palm-Wine'],
|
||||
['Africa','Raï'],
|
||||
['Africa','Sakara'],
|
||||
['Africa','Sega'],
|
||||
['Africa','Seggae'],
|
||||
['Africa','Semba'],
|
||||
['Africa','Soukous'],
|
||||
['Africa','Taarab'],
|
||||
['Africa','Zouglou'],
|
||||
['Asia','Anison'],
|
||||
['Asia','Baithak Gana'],
|
||||
['Asia','C-Pop'],
|
||||
['Asia','CityPop'],
|
||||
['Asia','Cantopop'],
|
||||
['Asia','Enka'],
|
||||
['Asia','Hong Kong English Pop'],
|
||||
['Asia','Fann At-Tanbura'],
|
||||
['Asia','Fijiri'],
|
||||
['Asia','Khaliji'],
|
||||
['Asia','Kayōkyoku'],
|
||||
['Asia','Liwa'],
|
||||
['Asia','Mandopop'],
|
||||
['Asia','Onkyokei'],
|
||||
['Asia','Taiwanese Pop'],
|
||||
['Asia','Thai Pop'],
|
||||
['Asia','Sawt'],
|
||||
['World','Cajun'],
|
||||
['World','Calypso'],
|
||||
['Caribbean','Chutney'],
|
||||
['Caribbean','Chutney Soca'],
|
||||
['Caribbean','Compas'],
|
||||
['Caribbean','Mambo'],
|
||||
['Caribbean','Merengue'],
|
||||
['Caribbean','Méringue'],
|
||||
['World','Carnatic (Karnataka Sanghetha)'],
|
||||
['World','Celtic'],
|
||||
['World','Celtic Folk'],
|
||||
['World','Contemporary Celtic'],
|
||||
['World','Coupé-décalé – Congo'],
|
||||
['World','Dangdut'],
|
||||
['World','Drinking Songs'],
|
||||
['World','Drone'],
|
||||
['World','Klezmer'],
|
||||
['World','Mbalax – Senegal'],
|
||||
['World','Polka'],
|
||||
['World','Soca'],
|
||||
['World','Baila'],
|
||||
['World','Bhangra'],
|
||||
['World','Bhojpuri'],
|
||||
['World','Dangdut'],
|
||||
['World','Filmi'],
|
||||
['World','Indian Pop'],
|
||||
['World','Hindustani'],
|
||||
['World','Indian Ghazal'],
|
||||
['World','Lavani'],
|
||||
['World','Luk Thung'],
|
||||
['World','Luk Krung'],
|
||||
['World','Manila Sound'],
|
||||
['World','Morlam'],
|
||||
['World','Pinoy Pop'],
|
||||
['World','Pop Sunda'],
|
||||
['World','Ragini'],
|
||||
['World','Thai Pop'],
|
||||
['World','Traditional Celtic'],
|
||||
['World','Worldbeat'],
|
||||
['World','Zydeco']
|
||||
];
|
||||
|
||||
export const allGenres = mapValues(groupBy(wavlakeGenres, g => g[0]), v => v.flatMap(x => x[1]).filter(x => !!x) );
|
@ -61,6 +61,13 @@ export const mirrordBlob = async (
|
||||
return res.data;
|
||||
};
|
||||
|
||||
async function blobUrlToFile(blobUrl: string, fileName: string): Promise<File> {
|
||||
const response = await fetch(blobUrl);
|
||||
const blob = await response.blob();
|
||||
const fileOptions = { type: blob.type, lastModified: Date.now() };
|
||||
return new File([blob], fileName, fileOptions);
|
||||
}
|
||||
|
||||
export const transferBlob = async (
|
||||
sourceUrl: string,
|
||||
targetServer: string,
|
||||
@ -69,15 +76,21 @@ export const transferBlob = async (
|
||||
): Promise<BlobDescriptor> => {
|
||||
console.log({ sourceUrl, targetServer });
|
||||
|
||||
const blob = await mirrordBlob(targetServer, sourceUrl, signEventTemplate);
|
||||
if (blob) return blob;
|
||||
console.log('Mirror failed. Using download + upload instead.');
|
||||
if (sourceUrl.startsWith('blob:')) {
|
||||
const file = await blobUrlToFile(sourceUrl, 'cover.jpg');
|
||||
return await uploadBlob(targetServer, file, signEventTemplate, onUploadProgress);
|
||||
|
||||
const result = await downloadBlob(sourceUrl, onUploadProgress);
|
||||
} else {
|
||||
const blob = await mirrordBlob(targetServer, sourceUrl, signEventTemplate);
|
||||
if (blob) return blob;
|
||||
console.log('Mirror failed. Using download + upload instead.');
|
||||
|
||||
const fileName = sourceUrl.replace(/.*\//, '');
|
||||
const result = await downloadBlob(sourceUrl, onUploadProgress);
|
||||
|
||||
const file = new File([result.data], fileName, { type: result.type, lastModified: new Date().getTime() });
|
||||
const fileName = sourceUrl.replace(/.*\//, '');
|
||||
|
||||
return await uploadBlob(targetServer, file, signEventTemplate, onUploadProgress);
|
||||
const file = new File([result.data], fileName, { type: result.type, lastModified: new Date().getTime() });
|
||||
|
||||
return await uploadBlob(targetServer, file, signEventTemplate, onUploadProgress);
|
||||
}
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user