Adding music player
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

This commit is contained in:
apatil 2025-05-06 19:59:01 +01:00
parent ec7da2c831
commit a76da7efde
7 changed files with 174 additions and 2 deletions

57
package-lock.json generated
View File

@ -14,6 +14,7 @@
"@mui/icons-material": "^7.0.2",
"@mui/material": "^7.0.2",
"@mui/material-nextjs": "^7.0.2",
"formidable": "^3.5.4",
"gray-matter": "^4.0.3",
"highlight.js": "^11.11.1",
"next": "15.3.1",
@ -1442,6 +1443,18 @@
"node": ">= 10"
}
},
"node_modules/@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -1490,6 +1503,15 @@
"node": ">=12.4.0"
}
},
"node_modules/@paralleldrive/cuid2": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz",
"integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "^1.1.5"
}
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
@ -2378,6 +2400,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/asap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"license": "MIT"
},
"node_modules/ast-types-flow": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
@ -3018,6 +3046,16 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/dezalgo": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
"integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
"license": "ISC",
"dependencies": {
"asap": "^2.0.0",
"wrappy": "1"
}
},
"node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@ -3998,6 +4036,23 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/formidable": {
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz",
"integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==",
"license": "MIT",
"dependencies": {
"@paralleldrive/cuid2": "^2.2.2",
"dezalgo": "^1.0.4",
"once": "^1.4.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"url": "https://ko-fi.com/tunnckoCore/commissions"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@ -6076,7 +6131,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"license": "ISC",
"dependencies": {
"wrappy": "1"
@ -7933,7 +7987,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true,
"license": "ISC"
},
"node_modules/yaml": {

View File

@ -15,6 +15,7 @@
"@mui/icons-material": "^7.0.2",
"@mui/material": "^7.0.2",
"@mui/material-nextjs": "^7.0.2",
"formidable": "^3.5.4",
"gray-matter": "^4.0.3",
"highlight.js": "^11.11.1",
"next": "15.3.1",

View File

@ -0,0 +1,21 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import fs from 'fs';
import path from 'path';
import { NextRequest } from 'next/server';
const MUSIC_DIR = '/app/music';
export async function GET(req: NextRequest, { params }: { params: { track: string } }) {
const track = decodeURIComponent(params.track);
const filePath = path.join(MUSIC_DIR, track);
if (!fs.existsSync(filePath)) {
return new Response('Not Found', { status: 404 });
}
const stat = fs.statSync(filePath);
const file = fs.createReadStream(filePath);
return new Response(file as any, {
headers: {
'Content-Type': 'audio/mpeg',
'Content-Length': stat.size.toString(),
},
});
}

View File

@ -0,0 +1,8 @@
import fs from 'fs';
import { NextResponse } from 'next/server';
const MUSIC_DIR = '/app/music';
export async function GET() {
const files = fs.readdirSync(MUSIC_DIR).filter((f) => f.endsWith('.mp3'));
return NextResponse.json(files);
}

View File

@ -0,0 +1,18 @@
import fs from 'fs';
import path from 'path';
import { NextRequest, NextResponse } from 'next/server';
const MUSIC_DIR = '/app/music';
export async function POST(req: NextRequest) {
const formData = await req.formData();
const file = formData.get('file') as File;
if (!file || !file.name.endsWith('.mp3')) {
return NextResponse.json({ error: 'Only .mp3 files are allowed.' }, { status: 400 });
}
const arrayBuffer = await file.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
const filePath = path.join(MUSIC_DIR, file.name);
fs.writeFileSync(filePath, buffer);
return NextResponse.json({ success: true });
}

70
src/app/music/page.tsx Normal file
View File

@ -0,0 +1,70 @@
'use client';
import { useEffect, useState } from 'react';
import {
Container, Typography, List, ListItem, ListItemText, Box, Button, Input, useMediaQuery
} from '@mui/material';
import UploadIcon from '@mui/icons-material/Upload';
import { useTheme } from '@mui/material/styles';
export default function Home() {
const [tracks, setTracks] = useState<string[]>([]);
const [currentTrack, setCurrentTrack] = useState<string | null>(null);
const [uploading, setUploading] = useState(false);
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
useEffect(() => {
fetchTracks();
}, []);
const fetchTracks = async () => {
const res = await fetch('/api/tracks');
const data = await res.json();
setTracks(data);
};
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
if (!e.target.files?.length) return;
const formData = new FormData();
formData.append('file', e.target.files[0]);
setUploading(true);
const res = await fetch('/api/upload', {
method: 'POST',
body: formData,
});
setUploading(false);
if (res.ok) {
fetchTracks();
}
};
return (
<Container maxWidth="md" sx={{ paddingTop: 4 }}>
<Typography variant="h4" gutterBottom textAlign="center">
Music Player
</Typography>
<Box textAlign="center" mb={3}>
<label htmlFor="upload-mp3">
<Input
id="upload-mp3"
type="file"
sx={{ display: 'none' }}
inputProps={{ accept: '.mp3' }}
onChange={handleUpload}
/>
<Button variant="contained" component="span" startIcon={<UploadIcon />} disabled={uploading}>
{uploading ? 'Uploading...' : 'Upload MP3'}
</Button>
</label>
</Box>
<List>
{tracks.map((track, idx) => (
<ListItem component={'button'} key={idx} onClick={() => setCurrentTrack(track)}>
<ListItemText primary={track} />
</ListItem>
))}
</List>
{currentTrack && (
<Box textAlign="center" mt={3}>
<Typography variant="subtitle1" gutterBottom>{currentTrack}</Typography>
<audio controls autoPlay src={`/api/music/${encodeURIComponent(currentTrack)}`} style={{ width: isMobile ? '100%' : '60%' }} />
</Box>
)}
</Container>
);
}

View File

@ -19,6 +19,7 @@ import ModeSwitch from '../ModeSwitch/ModeSwitch'
const navItems = [
{ label: 'Home', href: '/' },
{ label: 'Blog', href: '/blog' },
{ label: 'Music', href: '/music' },
]
export default function Header() {
const [mobileOpen, setMobileOpen] = React.useState(false)