From a76da7efde0e6ea3386f6b052a39da0951c3d726 Mon Sep 17 00:00:00 2001 From: apatil Date: Tue, 6 May 2025 19:59:01 +0100 Subject: [PATCH] Adding music player --- package-lock.json | 57 +++++++++++++++++++++++- package.json | 1 + src/app/api/music/[track]/route.ts | 21 +++++++++ src/app/api/tracks/route.ts | 8 ++++ src/app/api/upload/route.ts | 18 ++++++++ src/app/music/page.tsx | 70 ++++++++++++++++++++++++++++++ src/components/Header/Header.tsx | 1 + 7 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 src/app/api/music/[track]/route.ts create mode 100644 src/app/api/tracks/route.ts create mode 100644 src/app/api/upload/route.ts create mode 100644 src/app/music/page.tsx diff --git a/package-lock.json b/package-lock.json index a63ec69..154d411 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/package.json b/package.json index cfb45a1..9bd42c5 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/api/music/[track]/route.ts b/src/app/api/music/[track]/route.ts new file mode 100644 index 0000000..82102bb --- /dev/null +++ b/src/app/api/music/[track]/route.ts @@ -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(), + }, + }); +} diff --git a/src/app/api/tracks/route.ts b/src/app/api/tracks/route.ts new file mode 100644 index 0000000..c57bec7 --- /dev/null +++ b/src/app/api/tracks/route.ts @@ -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); +} diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts new file mode 100644 index 0000000..e9c5045 --- /dev/null +++ b/src/app/api/upload/route.ts @@ -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 }); +} diff --git a/src/app/music/page.tsx b/src/app/music/page.tsx new file mode 100644 index 0000000..5603cc7 --- /dev/null +++ b/src/app/music/page.tsx @@ -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([]); + const [currentTrack, setCurrentTrack] = useState(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) => { + 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 ( + + + Music Player + + + + + + {tracks.map((track, idx) => ( + setCurrentTrack(track)}> + + + ))} + + {currentTrack && ( + + {currentTrack} + + )} + + ); +} \ No newline at end of file diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 6cd5924..1176bd5 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -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)