diff --git a/app/api/music/tracks/route.ts b/app/api/music/tracks/route.ts new file mode 100644 index 0000000..48027c9 --- /dev/null +++ b/app/api/music/tracks/route.ts @@ -0,0 +1,73 @@ +// app/api/music/tracks/route.ts +import { NextResponse } from 'next/server'; +import { readdir } from 'fs/promises'; +import { join } from 'path'; + +export const dynamic = 'force-dynamic'; + +interface Track { + title: string; + fileName: string; + artist: string; + theme: string; +} + +function parseFileName(fileName: string): Track { + // Remove file extension + const nameWithoutExt = fileName.replace(/\.(mp4|mp3|wav|ogg)$/i, ''); + + // Parse format: [music_name]_[theme/genre].[format] + const parts = nameWithoutExt.split('_'); + + if (parts.length >= 2) { + const theme = parts[parts.length - 1].toLowerCase(); + const title = parts.slice(0, -1).join(' '); + + return { + title: title.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()), + fileName: fileName, + artist: theme.charAt(0).toUpperCase() + theme.slice(1) + ' Collection', + theme: theme + }; + } + + // Fallback for files that don't follow the naming convention + return { + title: nameWithoutExt.replace(/-|_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()), + fileName: fileName, + artist: 'Unknown Artist', + theme: 'general' + }; +} + +export async function GET() { + try { + // Path to your music directory + const musicDir = join(process.cwd(), 'public', 'files', 'music'); + + // Read all files in the directory + const files = await readdir(musicDir); + + // Filter for audio/video files and parse them + const tracks = files + .filter(file => /\.(mp4|mp3|wav|ogg)$/i.test(file)) + .map(file => parseFileName(file)) + .sort((a, b) => { + // Sort by theme first, then by title + if (a.theme !== b.theme) { + return a.theme.localeCompare(b.theme); + } + return a.title.localeCompare(b.title); + }); + + return NextResponse.json({ tracks }); + } catch (error) { + console.error('Error reading music directory:', error); + + // Return fallback tracks if directory reading fails + return NextResponse.json({ + tracks: [ + ] + }); + } +} diff --git a/app/layout.tsx b/app/layout.tsx index 5126a2d..159066b 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -7,64 +7,69 @@ import InteractiveTopo from "@/components/interactiveTopo"; import "./globals.css"; export const metadata: Metadata = { - title: "OpenWheels Racing", - description: "Free racing community - Live dashboard and rankings", + title: "OpenWheels Racing", + description: "Free racing community - Live dashboard and rankings", }; export default function RootLayout({ - children, + children, }: { - children: React.ReactNode; + children: React.ReactNode; }) { - return ( - -
-This is still a work in progress. Stay tuned for updates!
- + + ); +} + +function PauseIcon({ className = "w-6 h-6" }) { + return ( + + ); +} + +function SkipNextIcon({ className = "w-6 h-6" }) { + return ( + + ); +} + +function SkipPrevIcon({ className = "w-6 h-6" }) { + return ( + + ); +} + +function VideoIcon({ className = "w-5 h-5" }) { + return ( + + ); +} + +function MusicIcon({ className = "w-5 h-5" }) { + return ( + + ); +} + +function RefreshIcon({ className = "w-5 h-5" }) { + return ( + + ); +} + +export default function MusicPage() { + const [tracks, setTracks] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [currentTrackIndex, setCurrentTrackIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [currentTime, setCurrentTime] = useState(0); + const [duration, setDuration] = useState(0); + const [selectedTheme, setSelectedTheme] = useState('all'); + const mediaRef = useRef(null); + + // Fetch available tracks from the server + const fetchTracks = async () => { + setLoading(true); + setError(null); + try { + const response = await fetch('/api/music/tracks'); + if (!response.ok) { + throw new Error('Failed to fetch tracks'); + } + const data = await response.json(); + setTracks(data.tracks || []); + } catch (err) { + console.error('Error fetching tracks:', err); + setError(err.message); + // Fallback to hardcoded tracks if API fails + setTracks([ + { title: "Alone Again", fileName: "AloneAgain.mp4", artist: "Unknown Artist", theme: "general" }, + { title: "House Music VOL1", fileName: "deephousetherapy.mp4", artist: "Deep House Therapy", theme: "house" }, + { title: "House Music VOL2", fileName: "videoplayback.mp4", artist: "Various Artists", theme: "house" }, + ]); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchTracks(); + }, []); + + // Group tracks by theme + const tracksByTheme = tracks.reduce((acc, track) => { + const theme = track.theme || 'general'; + if (!acc[theme]) { + acc[theme] = []; + } + acc[theme].push(track); + return acc; + }, {}); + + const themes = Object.keys(tracksByTheme).sort(); + + // Get filtered tracks based on selected theme + const filteredTracks = selectedTheme === 'all' + ? tracks + : tracksByTheme[selectedTheme] || []; + + const currentTrack = filteredTracks[currentTrackIndex]; + const isVideo = currentTrack?.fileName.toLowerCase().endsWith('.mp4'); + + useEffect(() => { + if (mediaRef.current && currentTrack) { + mediaRef.current.load(); + if (isPlaying) { + mediaRef.current.play().catch(err => { + console.error('Playback error:', err); + setIsPlaying(false); + }); + } + } + }, [currentTrackIndex, currentTrack]); + + const togglePlayPause = () => { + if (mediaRef.current) { + if (isPlaying) { + mediaRef.current.pause(); + } else { + mediaRef.current.play().catch(err => { + console.error('Playback error:', err); + }); + } + setIsPlaying(!isPlaying); + } + }; + + const nextTrack = () => { + if (filteredTracks.length > 0) { + setCurrentTrackIndex((prev) => (prev + 1) % filteredTracks.length); + setIsPlaying(true); + } + }; + + const prevTrack = () => { + if (filteredTracks.length > 0) { + setCurrentTrackIndex((prev) => (prev - 1 + filteredTracks.length) % filteredTracks.length); + setIsPlaying(true); + } + }; + + const handleTimeUpdate = () => { + if (mediaRef.current) { + setCurrentTime(mediaRef.current.currentTime); + setDuration(mediaRef.current.duration || 0); + } + }; + + const handleSeek = (e) => { + const rect = e.currentTarget.getBoundingClientRect(); + const x = e.clientX - rect.left; + const percentage = x / rect.width; + if (mediaRef.current) { + mediaRef.current.currentTime = percentage * duration; + } + }; + + const formatTime = (time) => { + if (isNaN(time)) return "0:00"; + const minutes = Math.floor(time / 60); + const seconds = Math.floor(time % 60); + return `${minutes}:${seconds.toString().padStart(2, '0')}`; + }; + + const handleThemeChange = (theme) => { + setSelectedTheme(theme); + setCurrentTrackIndex(0); + setIsPlaying(false); + }; + + if (loading) { + return ( ++ High-fidelity streaming for the racing community +
+LOADING TRACKS...
++ High-fidelity streaming for the racing community +
+NO TRACKS AVAILABLE
+ {error &&Error: {error}
} + ++ High-fidelity streaming for the racing community +
+ + {/* Theme Filter */} ++ {currentTrack.artist} +
+FORMAT
+MP4 / AUDIO
+QUALITY
+HIGH FIDELITY
+TRACKS
+{tracks.length} AVAILABLE
+STREAMING
+PROGRESSIVE LOAD
+