personal_website/src/contexts/MusicContext.tsx
gpt-engineer-app[bot] a9235fdb3f Changes
2025-12-21 13:55:16 +00:00

219 lines
6.3 KiB
TypeScript

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<void>;
stopAudio: () => void;
}
const MusicContext = createContext<MusicContextType | undefined>(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<Station[]>([]);
const [currentIndex, setCurrentIndex] = useState(0);
const [selectedStation, setSelectedStation] = useState<Station | null>(null);
const [hasFetched, setHasFetched] = useState(false);
const [failedStations, setFailedStations] = useState<Set<string>>(new Set());
const audioRef = useRef<HTMLAudioElement | null>(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;
try {
const response = await fetch(
'https://de1.api.radio-browser.info/json/stations/topclick/100'
);
if (!response.ok) throw new Error('Failed to fetch stations');
const data: Station[] = await response.json();
const validStations = data.filter(s => !failedStations.has(s.stationuuid));
setStations(validStations);
setHasFetched(true);
} catch (err) {
console.error('Error fetching stations:', err);
}
}, [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 (
<MusicContext.Provider
value={{
isPlaying,
isBuffering,
volume,
stations,
currentIndex,
selectedStation,
hasFetched,
setVolume,
playStation,
togglePlay,
playNext,
playPrevious,
fetchStations,
stopAudio: stopCurrentAudio,
}}
>
{children}
</MusicContext.Provider>
);
};
export const useMusic = () => {
const context = useContext(MusicContext);
if (context === undefined) {
throw new Error('useMusic must be used within a MusicProvider');
}
return context;
};