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 isIOS = /iP(ad|hone|od)/.test(navigator.userAgent); 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" }, }; 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 [trackAlbum, setTrackAlbum] = 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; const playStream = () => { let streamPath = STATIONS[activeStation].streamPath; if (isIOS) { let lcStation = activeStation.toLowerCase(); streamPath = `/hls/${lcStation}/${lcStation}.m3u8`; console.log(`Replaced streamPath: ${streamPath}`); } const streamUrl = "https://stream.codey.lol" + streamPath + `?t=${Date.now()}`; if (soundRef.current) { soundRef.current.stop(); soundRef.current.unload(); } 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(); }; const togglePlayback = () => { if (isPlaying) { soundRef.current?.pause(); setIsPlaying(false); } else { if (!soundRef.current) { playStream(); } else { soundRef.current.play(); } setIsPlaying(true); } }; useEffect(() => { if (soundRef.current) { soundRef.current.stop(); soundRef.current.unload(); soundRef.current = null; } if (isPlaying) { playStream(); } }, [activeStation]); 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(); if (currentStationForInterval !== activeStation) return; if (data.artist === "N/A" && data.song === "N/A") { if (lastStationData.current !== "offline") { setTrackTitle("Offline"); setTrackArtist(""); setTrackAlbum(""); 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 : ""); setTrackAlbum(data.album); 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]); const remaining = duration - elapsed; const progressColorClass = progress >= 90 ? "bg-red-500 dark:bg-red-400" : progress >= 75 || remaining <= 20 ? "bg-yellow-400 dark:bg-yellow-300" : "bg-blue-500 dark:bg-blue-400"; return ( <>
{formatTime(elapsed)}
{formatTime(duration - elapsed)}