import { createContext, useContext, useState, useRef, useCallback, useEffect, ReactNode } from 'react'; import { useAudioAnalyzer } from './AudioAnalyzerContext'; export interface Station { stationuuid: string; name: string; url: string; favicon: string; country: string; tags: string; bitrate: number; } interface MusicContextType { isPlaying: boolean; isBuffering: boolean; volume: number; stations: Station[]; currentIndex: number; selectedStation: Station | null; hasFetched: boolean; setVolume: (volume: number) => void; playStation: (station: Station, index: number) => void; togglePlay: () => void; playNext: () => void; playPrevious: () => void; fetchStations: () => Promise; stopAudio: () => void; } const MusicContext = createContext(undefined); export const MusicProvider = ({ children }: { children: ReactNode }) => { const [isPlaying, setIsPlaying] = useState(false); const [isBuffering, setIsBuffering] = useState(false); const [volume, setVolumeState] = useState(50); const [stations, setStations] = useState([]); const [currentIndex, setCurrentIndex] = useState(0); const [selectedStation, setSelectedStation] = useState(null); const [hasFetched, setHasFetched] = useState(false); const [failedStations, setFailedStations] = useState>(new Set()); const audioRef = useRef(null); const { connectAudioElement, disconnectAudioElement } = useAudioAnalyzer(); // Update volume on audio element when volume state changes useEffect(() => { if (audioRef.current) { audioRef.current.volume = volume / 100; } }, [volume]); const setVolume = useCallback((newVolume: number) => { setVolumeState(newVolume); }, []); const fetchStations = useCallback(async () => { if (hasFetched) return; // Multiple radio-browser API servers for fallback const apiServers = [ 'https://de1.api.radio-browser.info', 'https://de2.api.radio-browser.info', 'https://nl1.api.radio-browser.info', 'https://nl2.api.radio-browser.info', 'https://at1.api.radio-browser.info', 'https://fr1.api.radio-browser.info', 'https://all.api.radio-browser.info', ]; for (const server of apiServers) { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); const response = await fetch( `${server}/json/stations/topclick/100`, { signal: controller.signal } ); clearTimeout(timeoutId); if (!response.ok) continue; const data: Station[] = await response.json(); const validStations = data.filter(s => !failedStations.has(s.stationuuid)); if (validStations.length > 0) { setStations(validStations); setHasFetched(true); console.log(`Fetched stations from ${server}`); return; } } catch (err) { console.warn(`Failed to fetch from ${server}:`, err); continue; } } console.error('All radio API servers failed'); }, [hasFetched, failedStations]); const stopCurrentAudio = useCallback(() => { if (audioRef.current) { disconnectAudioElement(audioRef.current); audioRef.current.pause(); audioRef.current.src = ''; audioRef.current.onplay = null; audioRef.current.onpause = null; audioRef.current.onerror = null; audioRef.current.onwaiting = null; audioRef.current.onplaying = null; audioRef.current = null; } setIsBuffering(false); }, [disconnectAudioElement]); const playStation = useCallback((station: Station, index: number) => { stopCurrentAudio(); setIsBuffering(true); const audio = new Audio(station.url); audio.crossOrigin = 'anonymous'; audio.volume = volume / 100; audioRef.current = audio; // Connect to analyzer for visualization connectAudioElement(audio); audio.onerror = () => { console.error('Failed to play station:', station.name); setFailedStations(prev => new Set(prev).add(station.stationuuid)); setIsPlaying(false); setIsBuffering(false); setSelectedStation(null); }; audio.onwaiting = () => { setIsBuffering(true); }; audio.onplaying = () => { setIsBuffering(false); setIsPlaying(true); // Set flag for music listener achievement (checked in AchievementsContext) localStorage.setItem('has-listened-to-music', 'true'); }; audio.onplay = () => { setIsPlaying(true); }; audio.onpause = () => { if (audioRef.current === audio) { setIsPlaying(false); } }; audio.play().catch((err) => { console.error('Playback error:', err); setFailedStations(prev => new Set(prev).add(station.stationuuid)); setIsPlaying(false); setIsBuffering(false); }); setSelectedStation(station); setCurrentIndex(index); }, [volume, stopCurrentAudio, connectAudioElement]); const togglePlay = useCallback(() => { if (!audioRef.current || !selectedStation) { if (stations.length > 0) { playStation(stations[0], 0); } return; } if (isPlaying) { audioRef.current.pause(); } else { setIsBuffering(true); audioRef.current.play().catch(() => { setIsBuffering(false); }); } }, [selectedStation, stations, playStation, isPlaying]); const playNext = useCallback(() => { if (stations.length === 0) return; const nextIndex = (currentIndex + 1) % stations.length; playStation(stations[nextIndex], nextIndex); }, [currentIndex, stations, playStation]); const playPrevious = useCallback(() => { if (stations.length === 0) return; const prevIndex = currentIndex === 0 ? stations.length - 1 : currentIndex - 1; playStation(stations[prevIndex], prevIndex); }, [currentIndex, stations, playStation]); // Handle media keys useEffect(() => { const handleMediaKey = (e: KeyboardEvent) => { if (e.key === 'MediaPlayPause') { e.preventDefault(); togglePlay(); } else if (e.key === 'MediaTrackNext') { e.preventDefault(); playNext(); } else if (e.key === 'MediaTrackPrevious') { e.preventDefault(); playPrevious(); } }; window.addEventListener('keydown', handleMediaKey); return () => window.removeEventListener('keydown', handleMediaKey); }, [togglePlay, playNext, playPrevious]); return ( {children} ); }; export const useMusic = () => { const context = useContext(MusicContext); if (context === undefined) { throw new Error('useMusic must be used within a MusicProvider'); } return context; };