This commit is contained in:
parent
ec7da2c831
commit
a76da7efde
57
package-lock.json
generated
57
package-lock.json
generated
@ -14,6 +14,7 @@
|
|||||||
"@mui/icons-material": "^7.0.2",
|
"@mui/icons-material": "^7.0.2",
|
||||||
"@mui/material": "^7.0.2",
|
"@mui/material": "^7.0.2",
|
||||||
"@mui/material-nextjs": "^7.0.2",
|
"@mui/material-nextjs": "^7.0.2",
|
||||||
|
"formidable": "^3.5.4",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"next": "15.3.1",
|
"next": "15.3.1",
|
||||||
@ -1442,6 +1443,18 @@
|
|||||||
"node": ">= 10"
|
"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": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@ -1490,6 +1503,15 @@
|
|||||||
"node": ">=12.4.0"
|
"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": {
|
"node_modules/@popperjs/core": {
|
||||||
"version": "2.11.8",
|
"version": "2.11.8",
|
||||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||||
@ -2378,6 +2400,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/ast-types-flow": {
|
||||||
"version": "0.0.8",
|
"version": "0.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
|
"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"
|
"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": {
|
"node_modules/doctrine": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
||||||
@ -3998,6 +4036,23 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@ -6076,7 +6131,6 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
@ -7933,7 +7987,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/yaml": {
|
"node_modules/yaml": {
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
"@mui/icons-material": "^7.0.2",
|
"@mui/icons-material": "^7.0.2",
|
||||||
"@mui/material": "^7.0.2",
|
"@mui/material": "^7.0.2",
|
||||||
"@mui/material-nextjs": "^7.0.2",
|
"@mui/material-nextjs": "^7.0.2",
|
||||||
|
"formidable": "^3.5.4",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"next": "15.3.1",
|
"next": "15.3.1",
|
||||||
|
|||||||
21
src/app/api/music/[track]/route.ts
Normal file
21
src/app/api/music/[track]/route.ts
Normal 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(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
8
src/app/api/tracks/route.ts
Normal file
8
src/app/api/tracks/route.ts
Normal 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);
|
||||||
|
}
|
||||||
18
src/app/api/upload/route.ts
Normal file
18
src/app/api/upload/route.ts
Normal 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
70
src/app/music/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -19,6 +19,7 @@ import ModeSwitch from '../ModeSwitch/ModeSwitch'
|
|||||||
const navItems = [
|
const navItems = [
|
||||||
{ label: 'Home', href: '/' },
|
{ label: 'Home', href: '/' },
|
||||||
{ label: 'Blog', href: '/blog' },
|
{ label: 'Blog', href: '/blog' },
|
||||||
|
{ label: 'Music', href: '/music' },
|
||||||
]
|
]
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const [mobileOpen, setMobileOpen] = React.useState(false)
|
const [mobileOpen, setMobileOpen] = React.useState(false)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user