Files
codey.lol/src/components/AudioPlayer.jsx
2025-07-24 10:06:36 -04:00

251 lines
7.5 KiB
JavaScript

import { useState, useEffect, useRef } from "react";
import { Howl, Howler } from "howler";
import { metaData } from "../config";
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 setPageTitle = (artist, song) => {
document.title = metaData.title + ` - Radio - ${artist} - ${song} [${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);
setPageTitle(data.artist, data.song);
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 (
<>
<div className="station-tabs flex gap-2 justify-center mb-4 flex-wrap z-10 relative">
{Object.entries(STATIONS).map(([key, { label }]) => (
<button
key={key}
className={`px-3 py-1 rounded-full text-sm font-semibold transition-colors ${
activeStation === key
? "bg-neutral-800 text-white dark:bg-white dark:text-black"
: "bg-neutral-200 text-black dark:bg-neutral-700 dark:text-white"
}`}
onClick={() => setActiveStation(key)}
aria-pressed={activeStation === key}
>
{label}
</button>
))}
</div>
<div className="c-containter">
<div className="music-container mt-8">
<section className="album-cover">
<img src={coverArt}
className="cover"
title={`"${trackAlbum}" Cover Art`}
alt={`"${trackAlbum}" Cover Art`} />
</section>
<section className="music-player">
<h1 className="music-player__header">serious.FM</h1>
<h1 className="music-player__title">{trackTitle}</h1>
<h2 className="music-player__author">{trackArtist}</h2>
{trackGenre && <h2 className="music-player__genre">{trackGenre}</h2>}
<div className="music-time">
<p className="music-time__current">{formatTime(elapsed)}</p>
<p className="music-time__last">{formatTime(duration - elapsed)}</p>
</div>
<div className="w-full h-2 rounded bg-neutral-300 dark:bg-neutral-700 overflow-hidden">
<div
className={`h-full transition-all duration-200 ${progressColorClass}`}
style={{ width: `${progress}%` }}
></div>
</div>
<div className="music-control">
<div
className="music-control__play"
id="play"
onClick={togglePlayback}
role="button"
tabIndex={0}
aria-pressed={isPlaying}
>
{!isPlaying ? <Play /> : <Pause />}
</div>
</div>
</section>
</div>
</div>
</>
);
}