Add Api for song upload

This commit is contained in:
Esa Kataja
2024-10-12 16:21:06 +03:00
parent 6bf57ff672
commit d47631abe9
6 changed files with 315 additions and 8 deletions
+4 -1
View File
@@ -27,5 +27,8 @@
"tailwindcss": "^3.4.13",
"vite": "^5.0.3"
},
"type": "module"
"type": "module",
"dependencies": {
"music-metadata": "^10.5.0"
}
}
+113
View File
@@ -7,6 +7,10 @@ settings:
importers:
.:
dependencies:
music-metadata:
specifier: ^10.5.0
version: 10.5.0
devDependencies:
'@sveltejs/adapter-auto':
specifier: ^3.0.0
@@ -493,6 +497,9 @@ packages:
cpu: [x64]
os: [win32]
'@sec-ant/[email protected]':
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
'@sveltejs/[email protected]':
resolution: {integrity: sha512-27LR+uKccZ62lgq4N/hvyU2G+hTP9fxWEAfnZcl70HnyfAjMSsGk1z/SjAPXNCD1mVJIE7IFu3TQ8cQ/UH3c0A==}
peerDependencies:
@@ -528,6 +535,9 @@ packages:
svelte: ^4.0.0 || ^5.0.0-next.0
vite: ^5.0.0
'@tokenizer/[email protected]':
resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
'@types/[email protected]':
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
@@ -662,6 +672,10 @@ packages:
[email protected]:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
[email protected]:
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
engines: {node: '>= 0.6'}
[email protected]:
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
engines: {node: '>= 0.6'}
@@ -834,6 +848,10 @@ packages:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
[email protected]:
resolution: {integrity: sha512-dMuq6WWnP6BpQY0zYJNpTtQWgeCImSMG0BTIzUBXvxbwc1HWP/E7AE4UWU9XSCOPGJuOHda0HpDnwM2FW+d90A==}
engines: {node: '>=18'}
[email protected]:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
@@ -864,6 +882,10 @@ packages:
[email protected]:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
[email protected]:
resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==}
engines: {node: '>=18'}
[email protected]:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@@ -898,6 +920,9 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
[email protected]:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
[email protected]:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
@@ -947,6 +972,10 @@ packages:
[email protected]:
resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==}
[email protected]:
resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==}
engines: {node: '>=18'}
[email protected]:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
@@ -1014,6 +1043,10 @@ packages:
[email protected]:
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
[email protected]:
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
engines: {node: '>= 0.8'}
[email protected]:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
@@ -1044,6 +1077,10 @@ packages:
[email protected]:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
[email protected]:
resolution: {integrity: sha512-j9KDoFzud12gKtl6+SEX4nVMzJyASZ9XtcH04kHhi5Q7sWBnH6GtzgzRFFkOVEojSctBecj9JqTDqyrTAs5kOA==}
engines: {node: '>=16.0.0'}
[email protected]:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
@@ -1108,6 +1145,10 @@ packages:
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
engines: {node: '>=16 || 14 >=14.18'}
[email protected]:
resolution: {integrity: sha512-U94a+eXHzct7vAd19GH3UQ2dH4Satbng0MyYTMaQatL0pvYYL5CTPR25HBhKtecl+4bfu1/i3vC6k0hydO5Vcw==}
engines: {node: '>=14.16'}
[email protected]:
resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==}
@@ -1299,6 +1340,14 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
[email protected]:
resolution: {integrity: sha512-ExzDvHYPj6F6QkSNe/JxSlBxTh3OrI6wrAIz53ulxo1c4hBJ1bT9C/JrAthEKHWG9riVH3Xzg7B03Oxty6S2Lw==}
engines: {node: '>=16'}
[email protected]:
resolution: {integrity: sha512-fuapOUOgBhPh474Xx2pDSN/Kd5ACsLWF0nPKk9F9QfB+5iKxov1pNS9CqPv+t09cw6WRV+iRkH1gkCTDlM0kvw==}
engines: {node: '>=16'}
[email protected]:
resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
engines: {node: '>=16 || 14 >=14.17'}
@@ -1358,6 +1407,10 @@ packages:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
[email protected]:
resolution: {integrity: sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==}
engines: {node: '>=14.16'}
[email protected]:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'}
@@ -1372,6 +1425,10 @@ packages:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
[email protected]:
resolution: {integrity: sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==}
engines: {node: '>=18'}
[email protected]:
resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==}
hasBin: true
@@ -1764,6 +1821,8 @@ snapshots:
'@rollup/[email protected]':
optional: true
'@sec-ant/[email protected]': {}
'@sveltejs/[email protected](@sveltejs/[email protected](@sveltejs/[email protected]([email protected])([email protected]))([email protected])([email protected]))':
dependencies:
'@sveltejs/kit': 2.7.0(@sveltejs/[email protected]([email protected])([email protected]))([email protected])([email protected])
@@ -1820,6 +1879,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@tokenizer/[email protected]': {}
'@types/[email protected]': {}
'@types/[email protected]':
@@ -1954,6 +2015,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]:
@@ -2161,6 +2224,13 @@ snapshots:
dependencies:
flat-cache: 4.0.1
[email protected]:
dependencies:
get-stream: 9.0.1
strtok3: 8.1.0
token-types: 6.0.0
uint8array-extras: 1.4.0
[email protected]:
dependencies:
to-regex-range: 5.0.1
@@ -2189,6 +2259,11 @@ snapshots:
[email protected]: {}
[email protected]:
dependencies:
'@sec-ant/readable-stream': 0.4.1
is-stream: 4.0.1
[email protected]:
dependencies:
is-glob: 4.0.3
@@ -2220,6 +2295,8 @@ snapshots:
dependencies:
function-bind: 1.1.2
[email protected]: {}
[email protected]: {}
[email protected]: {}
@@ -2257,6 +2334,8 @@ snapshots:
dependencies:
'@types/estree': 1.0.6
[email protected]: {}
[email protected]: {}
[email protected]:
@@ -2312,6 +2391,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]:
@@ -2335,6 +2416,19 @@ snapshots:
[email protected]: {}
[email protected]:
dependencies:
'@tokenizer/token': 0.3.0
content-type: 1.0.5
debug: 4.3.7
file-type: 19.5.0
media-typer: 1.1.0
strtok3: 9.0.0
token-types: 6.0.0
uint8array-extras: 1.4.0
transitivePeerDependencies:
- supports-color
[email protected]:
dependencies:
any-promise: 1.3.0
@@ -2389,6 +2483,8 @@ snapshots:
lru-cache: 10.4.3
minipass: 7.1.2
[email protected]: {}
[email protected]:
dependencies:
'@types/estree': 1.0.6
@@ -2588,6 +2684,16 @@ snapshots:
[email protected]: {}
[email protected]:
dependencies:
'@tokenizer/token': 0.3.0
peek-readable: 5.2.0
[email protected]:
dependencies:
'@tokenizer/token': 0.3.0
peek-readable: 5.2.0
[email protected]:
dependencies:
'@jridgewell/gen-mapping': 0.3.5
@@ -2685,6 +2791,11 @@ snapshots:
dependencies:
is-number: 7.0.0
[email protected]:
dependencies:
'@tokenizer/token': 0.3.0
ieee754: 1.2.1
[email protected]: {}
[email protected]: {}
@@ -2696,6 +2807,8 @@ snapshots:
dependencies:
prelude-ls: 1.2.1
[email protected]: {}
[email protected]([email protected]):
dependencies:
browserslist: 4.24.0
+86 -6
View File
@@ -1,27 +1,107 @@
<script>
import NewEntryDetailForm from './newEntry/NewEntryDetailForm.svelte';
let artist = '';
let songName = '';
let isEntered = false;
async function handleUploadSubmit(event) {
event.preventDefault();
const formData = new FormData(event.target);
const response = await fetch('/api/fileUpload', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.error) {
console.error(data.error);
} else {
console.log(data.metadata);
artist = data.metadata.common.artist || '';
songName = data.metadata.common.title || '';
isEntered = true;
}
}
async function handleYTLinkSubmit(event) {
event.preventDefault();
const formData = new FormData(event.target);
const ytLink = formData.get('ytlink');
if (!ytLink) {
console.error('No YouTube link provided');
return;
}
try {
const id = extractYouTubeId(ytLink);
if (!id) {
console.error('Invalid YouTube link');
return;
}
const response = await fetch('/api/ytUpload', {
method: 'POST',
body: JSON.stringify({ id })
});
if (!response.ok) {
console.error('Error uploading YouTube link:', response.status, response.statusText);
return;
}
const data = await response.json();
if (data.error) {
console.error('Error uploading YouTube link:', data.error);
} else {
artist = data.body.metadata.author_name || 'Artisti';
songName = data.body.metadata.title || 'Kappale';
isEntered = true;
}
} catch (error) {
console.error('Error handling YouTube link submission:', error);
}
}
function extractYouTubeId(link) {
const regex = /^(?:https?:\/\/)?(?:www\.)?youtu(?:be\.com|\.be)\/(?:watch\?v=)?([^&\s]+)/;
const match = link.match(regex);
return match && match[1];
}
</script>
<div class="my-20">
<h2 class="text-center text-2xl font-semibold">🎉🎉🎉 Uusi kierros on alkanut! 🎉🎉🎉</h2>
<h2 class="flex justify-center gap-4 items-center text-center text-2xl font-semibold">
<span class="hidden sm:inline">🎉🎉🎉</span><span>Uusi kierros on alkanut!</span><span
class="hidden sm:inline">🎉🎉🎉</span
>
</h2>
<p class="my-10">
Uusi kierros on alkanut. Et ole kuitenkaan vielä laittanut omaa ehdokasta sisään. Voit tehdä sen
alla olevan lomakkeen avulla.
</p>
<div id="upload-status" class={isEntered ? '' : 'hidden'}>
<NewEntryDetailForm {artist} {songName} />
</div>
<div
class="flex flex-col items-center border-2 border-zinc-500 rounded-xl p-10 text-center bg-zinc-700"
class={`flex flex-col md:flex-row md:justify-between items-center border-2 border-zinc-500 rounded-xl p-10 text-center bg-zinc-700 ${isEntered ? 'hidden' : ''}`}
>
<form action="/api/newentry" method="post">
<form on:submit|preventDefault={handleYTLinkSubmit} method="post" class="w-2/5">
<label for="ytlink" class="my-2">Youtube-linkki:</label>
<input
type="text"
name="ytlink"
class="input-text-field block w-full my-6"
placeholder="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
placeholder="https://www.youtube.com/watch?v=nxMMElT61A8"
/>
<button type="submit" class="button-primary mt-4">Lataa</button>
</form>
<p class="my-6 text-2xl font-bold">--- TAI ---</p>
<form on:submit|preventDefault={handleUploadSubmit} method="post" class="w-2/5 text-center">
<label for="upload" class="mb-2">Tiedosto:</label>
<input type="file" name="upload" accept="audio/*" class="block w-full my-6" />
<button type="submit" class="button-primary mt-4">Liity mukaan</button>
<input type="file" name="upload" id="upload" accept="audio/*" class="block w-full my-6" />
<button type="submit" class="button-primary mt-4">Lataa</button>
</form>
</div>
</div>
@@ -0,0 +1,35 @@
<script>
export let artist = '';
export let songName = '';
export let isEntered;
function cancel() {
artist = '';
songName = '';
isEntered = false;
}
</script>
<div class="flex flex-col max-w-md mx-auto my-6 gap-4">
<label for="artist">Artisti:</label>
<input
type="text"
bind:value={artist}
name="artist"
id="artist"
class="input-text-field"
required
/>
<label for="songName">Kappale:</label>
<input
type="text"
bind:value={songName}
name="songName"
id="songName"
class="input-text-field"
required
/>
<button class="button-cancel mt-4" on:click={cancel}>Tyhjennä</button>
<button type="submit" class="button-primary">Läheta</button>
</div>
+39
View File
@@ -0,0 +1,39 @@
import { json } from '@sveltejs/kit';
import { parseBuffer } from 'music-metadata';
import { writeFile } from 'fs/promises';
import { join } from 'path';
import { unlink } from 'fs/promises';
export const config = {
api: {
bodyParser: false
}
};
export async function POST({ request }) {
try {
const formData = await request.formData();
const file = formData.get('upload');
if (!file) {
return json({ error: 'No file uploaded' }, { status: 400 });
}
// Save the file temporarily
const buffer = await file.arrayBuffer();
const tempPath = join('/tmp', `upload_${Date.now()}_${file.name}`);
await writeFile(tempPath, Buffer.from(buffer));
// Parse metadata
const metadata = await parseBuffer(Buffer.from(buffer));
// Clean up the temporary file
// You might want to handle this asynchronously or use a try-finally block
await unlink(tempPath);
return json({ metadata });
} catch (error) {
console.error('Error processing file:', error);
return json({ error: 'Failed to process file' }, { status: 500 });
}
}
+37
View File
@@ -0,0 +1,37 @@
export async function POST({ request }) {
console.log('ytUpload API endpoint hit');
const body = await request.json();
const ytId = body.id;
if (!ytId) {
return {
status: 400,
body: { error: 'No id provided' }
};
}
try {
console.log('Fetching YouTube metadata...');
const response = await fetch(
`https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${ytId}&format=json`
);
const data = await response.json();
console.log('Response from YouTube:', data);
if (data.error) {
return Response.json({
status: 400,
body: { error: data.error.message }
});
}
return Response.json({
status: 200,
body: { metadata: data }
});
} catch (error) {
console.error('Error fetching YouTube metadata:', error);
return Response.json({
status: 500,
body: { error: 'Internal Server Error' }
});
}
}