import { useState, useEffect, useRef } from "react"; import { Howl, Howler } from "howler"; import Play from "@mui/icons-material/PlayArrow"; import Pause from "@mui/icons-material/Pause"; import "@styles/player.css"; const API_URL = "https://api.codey.lol"; Howler.html5PoolSize = 32; const STATIONS = { main: { label: "Main", streamPath: "/sfm.ogg" }, rock: { label: "Rock", streamPath: "/rock.ogg" }, rap: { label: "Rap", streamPath: "/rap.ogg" }, electronic: { label: "Electronic", streamPath: "/electronic.ogg" }, classical: { label: "Classical", streamPath: "/classical.ogg" }, pop: { label: "Pop", streamPath: "/pop.ogg" }, }; // Global interval tracking (required for Astro + client:only) let activeInterval = null; let currentStationForInterval = null; function clearGlobalMetadataInterval() { if (activeInterval) { clearInterval(activeInterval); activeInterval = null; currentStationForInterval = null; } } export function Player() { const [activeStation, setActiveStation] = useState("main"); const [isPlaying, setIsPlaying] = useState(false); const [trackTitle, setTrackTitle] = useState(""); const [trackArtist, setTrackArtist] = useState(""); const [trackGenre, setTrackGenre] = useState(""); const [coverArt, setCoverArt] = useState("/images/radio_art_default.jpg"); const [elapsed, setElapsed] = useState(0); const [duration, setDuration] = useState(0); const soundRef = useRef(null); const uuidRef = useRef(null); const lastStationData = useRef(null); const formatTime = (seconds) => { const m = String(Math.floor(seconds / 60)).padStart(2, "0"); const s = String(Math.floor(seconds % 60)).padStart(2, "0"); return `${m}:${s}`; }; const progress = duration > 0 ? (elapsed / duration) * 100 : 0; // Create Howl instance on activeStation change useEffect(() => { if (soundRef.current) { soundRef.current.unload(); soundRef.current = null; } const streamUrl = "https://stream.codey.lol" + STATIONS[activeStation].streamPath; const howl = new Howl({ src: [streamUrl], html5: true, onend: () => howl.play(), onplay: () => setIsPlaying(true), onpause: () => setIsPlaying(false), onstop: () => setIsPlaying(false), onloaderror: (id, err) => console.error("Load error", err), onplayerror: (id, err) => { console.error("Play error", err); setTimeout(() => howl.play(), 1000); }, }); soundRef.current = howl; uuidRef.current = null; lastStationData.current = null; // Autoplay if previously playing if (isPlaying) { setTimeout(() => { if (soundRef.current) { soundRef.current.play(); } }, 100); } return () => { howl.stop(); howl.unload(); }; }, [activeStation]); const togglePlayback = () => { if (isPlaying) { if (soundRef.current) { soundRef.current.pause(); } setIsPlaying(false); } else { // If a previous Howl exists, unload it if (soundRef.current) { soundRef.current.stop(); soundRef.current.unload(); } const streamUrl = "https://stream.codey.lol" + STATIONS[activeStation].streamPath + `?t=${Date.now()}`; // Cache-busting param const newHowl = new Howl({ src: [streamUrl], html5: true, onend: () => newHowl.play(), onplay: () => { setIsPlaying(true); }, onpause: () => setIsPlaying(false), onstop: () => setIsPlaying(false), onloaderror: (_, err) => console.error("Load error", err), onplayerror: (_, err) => { console.error("Play error", err); setTimeout(() => newHowl.play(), 1000); }, }); soundRef.current = newHowl; newHowl.play(); } }; // Metadata fetcher: global-safe useEffect(() => { clearGlobalMetadataInterval(); currentStationForInterval = activeStation; const fetchTrackData = async () => { try { const response = await fetch(`${API_URL}/radio/np?station=${activeStation}`, { method: "POST", }); const data = await response.json(); // Ignore stale interval calls if (currentStationForInterval !== activeStation) return; if (data.artist === "N/A" && data.song === "N/A") { if (lastStationData.current !== "offline") { setTrackTitle("Offline"); setTrackArtist(""); setTrackGenre(""); setCoverArt("/images/radio_art_default.jpg"); setElapsed(0); setDuration(0); lastStationData.current = "offline"; } return; } if (data.uuid === uuidRef.current) { if (lastStationData.current === data.uuid) { setElapsed(data.elapsed); setDuration(data.duration); } return; } uuidRef.current = data.uuid; lastStationData.current = data.uuid; setTrackTitle(data.song); setTrackArtist(data.artist); setTrackGenre(data.genre !== "N/A" ? data.genre : ""); setCoverArt(`${API_URL}/radio/album_art?station=${activeStation}&_=${Date.now()}`); setElapsed(data.elapsed); setDuration(data.duration); } catch (error) { console.error("Failed to fetch track data:", error); } }; fetchTrackData(); activeInterval = setInterval(fetchTrackData, 1000); return () => { clearGlobalMetadataInterval(); }; }, [activeStation]); return ( <>
{formatTime(elapsed)}
{formatTime(duration - elapsed)}