navbar changes, radio/webplayer rewrite + additional stations
This commit is contained in:
@@ -1,57 +1,243 @@
|
||||
import {useState, React} from "react";
|
||||
import jQuery from "jquery";
|
||||
import Play from '@mui/icons-material/PlayArrow';
|
||||
import Pause from '@mui/icons-material/Pause';
|
||||
<style>
|
||||
@import "@styles/player.css";
|
||||
</style>
|
||||
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";
|
||||
|
||||
export const render = false;
|
||||
const API_URL = "https://api.codey.lol";
|
||||
Howler.html5PoolSize = 32;
|
||||
|
||||
const PlayIcon = () => {
|
||||
return (
|
||||
<Play />
|
||||
);
|
||||
}
|
||||
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" },
|
||||
};
|
||||
|
||||
const PauseIcon = () => {
|
||||
return (
|
||||
<Pause />
|
||||
)
|
||||
// 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 [isPlaying, setPlaying] = useState(false);
|
||||
window.isPlaying = isPlaying;
|
||||
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 (
|
||||
<div>
|
||||
<div className="c-containter">
|
||||
<div className="music-container">
|
||||
<section className="album-cover">
|
||||
|
||||
<img src="https://api.codey.lol/radio/album_art" className="cover" alt="Cover Art" />
|
||||
|
||||
</section>
|
||||
<section className="music-player">
|
||||
<h1 className='music-player__header'>serious.FM</h1>
|
||||
<h1 className="music-player__title"></h1>
|
||||
<h2 className="music-player__author"></h2>
|
||||
<h2 className="music-player__genre"></h2>
|
||||
<div className="music-time">
|
||||
<p className="music-time__current"></p>
|
||||
<p className="music-time__last"></p>
|
||||
</div>
|
||||
<div className="music-bar" id="progress">
|
||||
<div id="length"></div>
|
||||
</div>
|
||||
<div className="music-control">
|
||||
<div className="music-control__play" id="play">
|
||||
{isPlaying == false && (<Play onClick={(e) => { setPlaying(!isPlaying); togglePlayback(); } } />)}
|
||||
{isPlaying && (<Pause onClick={(e) => { setPlaying(!isPlaying); togglePlayback(); }} />)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<>
|
||||
<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" alt="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="music-bar" id="progress">
|
||||
<div id="length" style={{ width: `${progress}%` }}></div>
|
||||
</div>
|
||||
<div className="music-control">
|
||||
<div className="music-control__play" id="play">
|
||||
{!isPlaying ? (
|
||||
<Play onClick={togglePlayback} />
|
||||
) : (
|
||||
<Pause onClick={togglePlayback} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)};
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user