feat: Improved audio publishing (genres, thumbnail)

This commit is contained in:
florian 2024-07-16 23:11:24 +02:00
parent 679cca119d
commit 40c8b3db15
7 changed files with 643 additions and 158 deletions

4
TODO.md Normal file
View File

@ -0,0 +1,4 @@
- When 403 from nogood, error should be displayed.
- Delete does not work in the ALL SERVERS view

View File

@ -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>
</>
);

View File

@ -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);

View File

@ -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);

View File

@ -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
View 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'],
['Childrens Music','Lullabies'],
['Childrens Music','Sing-Along'],
['Childrens 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/Drumn\'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: Childrens'],
['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) );

View File

@ -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);
}
};