diff --git a/public/scripts/player.js b/public/scripts/player.js index fe58ecf..4bb5f38 100644 --- a/public/scripts/player.js +++ b/public/scripts/player.js @@ -23,9 +23,13 @@ initialize = () => { var sound = new Howl({ src: ["https://stream.codey.lol/sfm.ogg"], html5: true, - // onend: () => { sound.unload(); } + onloaderror: (id) => { console.log(`Fail: ${id}`) }, + onplayerror: (id) => { console.log(`Fail b: ${id}`)}, + onend: (e) => { sound.play(); } }); + console.log(sound); + let currentTime = 0; let currentDuration = 0; let currentUUID = null; diff --git a/src/assets/styles/global.css b/src/assets/styles/global.css index a82c18d..9c77892 100644 --- a/src/assets/styles/global.css +++ b/src/assets/styles/global.css @@ -182,9 +182,9 @@ Custom transform: rotate(90deg); } -#alert { - margin-top: 5px; - margin-bottom: 5px; +.alert { + margin-top: 10%; + margin-bottom: 1%; } #spinner { diff --git a/src/assets/styles/player.css b/src/assets/styles/player.css index c088cc4..49b900c 100644 --- a/src/assets/styles/player.css +++ b/src/assets/styles/player.css @@ -53,6 +53,10 @@ section { padding: 1em; } +.station-tabs { + top: 4rem; +} + .music-container { display: flex; justify-content: center; @@ -64,6 +68,7 @@ section { max-width: 700px; box-shadow: 1px 1px 5px 0 rgba(0, 0, 0, 0.3); max-height: 290px; + margin-bottom: 50px; } .album-cover { diff --git a/src/components/AppLayout.jsx b/src/components/AppLayout.jsx index 2339f89..f9b8d36 100644 --- a/src/components/AppLayout.jsx +++ b/src/components/AppLayout.jsx @@ -8,7 +8,7 @@ import Alert from '@mui/joy/Alert'; import WarningIcon from '@mui/icons-material/Warning'; import CustomToastContainer from '../components/ToastProvider.jsx'; import LyricSearch from './LyricSearch.jsx'; -import 'primereact/resources/themes/bootstrap4-light-blue/theme.css'; // TEMP +import 'primereact/resources/themes/bootstrap4-light-blue/theme.css'; import 'primereact/resources/primereact.min.css'; export default function Root({child}) { @@ -24,6 +24,7 @@ export default function Root({child}) { closeOnClick={true}/> } variant="soft" color="danger"> diff --git a/src/components/AudioPlayer.jsx b/src/components/AudioPlayer.jsx index b247cc0..b4f4e49 100644 --- a/src/components/AudioPlayer.jsx +++ b/src/components/AudioPlayer.jsx @@ -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'; - +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 ( - - ); -} +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 ( - - ) +// 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 ( -
-
-
-
- - Cover Art - -
-
-

serious.FM

-

-

-

-
-

-

-
-
-
-
-
-
- {isPlaying == false && ( { setPlaying(!isPlaying); togglePlayback(); } } />)} - {isPlaying && ( { setPlaying(!isPlaying); togglePlayback(); }} />)} -
-
-
+ <> +
+ {Object.entries(STATIONS).map(([key, { label }]) => ( + + ))} +
+ +
+
+
+ Cover Art +
+
+

serious.FM

+

{trackTitle}

+

{trackArtist}

+ {trackGenre &&

{trackGenre}

} +
+

{formatTime(elapsed)}

+

{formatTime(duration - elapsed)}

+
+
+
+
+
+
+ {!isPlaying ? ( + + ) : ( + + )} +
+
+
-
-
- )}; \ No newline at end of file +
+ + ); +} diff --git a/src/components/BaseHead.astro b/src/components/BaseHead.astro index 8e83600..abf8062 100644 --- a/src/components/BaseHead.astro +++ b/src/components/BaseHead.astro @@ -9,7 +9,7 @@ import { metaData } from "../config"; import { SEO } from "astro-seo"; import { getImagePath } from "astro-opengraph-images"; import { JoyUIRootIsland } from "./Components" -import { useHtmlThemeAttr } from "../hooks/useHtmlThemeAttr"; // your existing theme hook +import { useHtmlThemeAttr } from "../hooks/useHtmlThemeAttr"; import { usePrimeReactThemeSwitcher } from "../hooks/usePrimeReactThemeSwitcher"; const { title, description = metaData.description, image } = Astro.props; diff --git a/src/components/Footer.astro b/src/components/Footer.astro index 89a34d3..b06d124 100644 --- a/src/components/Footer.astro +++ b/src/components/Footer.astro @@ -4,6 +4,9 @@ import RandomMsg from "../components/RandomMsg"; import { buildTime } from '../utils/buildTime.js'; const YEAR = new Date().getFullYear(); +var ENVIRONMENT = (Astro.url.hostname === "localhost") ? "Dev": "Prod"; + + ---