refactor AI
Signed-off-by: ale <ale@manalejandro.com>
This commit is contained in:
parent
1cb4b8a023
commit
2ec5c3df5e
@ -8,6 +8,7 @@
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"materialize-css": "^1.0.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-materialize": "^3.10.0",
|
||||
|
256
src/App.js
256
src/App.js
@ -1,175 +1,111 @@
|
||||
import "./App.css";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import WaveForm from "./WaveForm";
|
||||
import M from "materialize-css";
|
||||
import { Button, Icon, Range } from "react-materialize";
|
||||
import text from "./list.txt";
|
||||
|
||||
// Custom hooks
|
||||
import useAudioPlayer from "./hooks/useAudioPlayer";
|
||||
import useStreamData from "./hooks/useStreamData";
|
||||
import useBackgroundImages from "./hooks/useBackgroundImages";
|
||||
|
||||
// Components
|
||||
import Header from "./components/Header";
|
||||
import TrackInfo from "./components/TrackInfo";
|
||||
import AudioControls from "./components/AudioControls";
|
||||
|
||||
/**
|
||||
* Main App component
|
||||
*/
|
||||
const App = () => {
|
||||
const [audioUrl, setAudioUrl] = useState(null),
|
||||
[analyzerData, setAnalyzerData] = useState(null),
|
||||
[bounce, setBounce] = useState(''),
|
||||
[json, setJson] = useState({}),
|
||||
[currentVolume, setCurrentVolume] = useState(0.5),
|
||||
[muted, setMuted] = useState(false),
|
||||
[link, setLink] = useState(''),
|
||||
[title, setTitle] = useState('Stream Radio'),
|
||||
[images, setImages] = useState([]),
|
||||
[currentListeners, setCurrentListeners] = useState(0),
|
||||
[maxListeners, setMaxListeners] = useState(0),
|
||||
[paused, setPaused] = useState(true),
|
||||
audioElmRef = useRef(null),
|
||||
loadedAnalyzer = useRef(false),
|
||||
once = useRef(false),
|
||||
audioAnalyzer = () => {
|
||||
const audioCtx = new (window.AudioContext || window.webkitAudioContext)(),
|
||||
source = audioCtx.createMediaElementSource(audioElmRef.current),
|
||||
analyzer = audioCtx.createAnalyser()
|
||||
analyzer.fftSize = 2048
|
||||
const bufferLength = analyzer.frequencyBinCount,
|
||||
dataArray = new Uint8Array(bufferLength)
|
||||
source.connect(analyzer)
|
||||
source.connect(audioCtx.destination)
|
||||
source.onended = () => {
|
||||
source.disconnect()
|
||||
}
|
||||
setAnalyzerData({ analyzer, bufferLength, dataArray })
|
||||
},
|
||||
loadData = async () => {
|
||||
try {
|
||||
const response = await fetch('/stream.json'),
|
||||
json = await response.json()
|
||||
setJson(json)
|
||||
} catch (err) {
|
||||
console.error('Error fetching data: ' + err.message)
|
||||
}
|
||||
},
|
||||
loadListeners = async () => {
|
||||
try {
|
||||
const response = await fetch('/status.xsl'),
|
||||
data = await response.text(),
|
||||
parser = new DOMParser(),
|
||||
xmlDoc = parser.parseFromString(data, 'text/html'),
|
||||
listeners = xmlDoc.getElementsByTagName('td')
|
||||
for (let i = 0; i < listeners.length; i++) {
|
||||
if (i === 9) {
|
||||
setCurrentListeners(listeners[i].textContent)
|
||||
} else if (i === 11) {
|
||||
setMaxListeners(listeners[i].textContent)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching data: ' + err.message)
|
||||
}
|
||||
},
|
||||
loadImages = useCallback(async () => {
|
||||
const response = await fetch(text),
|
||||
data = await response.text()
|
||||
setImages(data.split('\n').filter(line => line.length > 0))
|
||||
}),
|
||||
load = useCallback(async () => {
|
||||
await loadData()
|
||||
await loadListeners()
|
||||
}),
|
||||
play = useCallback(async () => {
|
||||
if (!loadedAnalyzer.current) {
|
||||
audioAnalyzer()
|
||||
loadedAnalyzer.current = true
|
||||
}
|
||||
setPaused(false)
|
||||
setMuted(false)
|
||||
await audioElmRef.current?.play()
|
||||
})
|
||||
// Custom hook for managing background images
|
||||
const { loadImages } = useBackgroundImages(text);
|
||||
|
||||
// Custom hook for streaming data
|
||||
const {
|
||||
json,
|
||||
currentListeners,
|
||||
maxListeners,
|
||||
title,
|
||||
link,
|
||||
trackInfo,
|
||||
loadAllData
|
||||
} = useStreamData();
|
||||
|
||||
// Custom hook for audio player functionality
|
||||
const {
|
||||
audioElmRef,
|
||||
analyzerData,
|
||||
muted,
|
||||
paused,
|
||||
play,
|
||||
pause,
|
||||
toggleMute
|
||||
} = useAudioPlayer("/stream.mp3");
|
||||
|
||||
// Initialization flag to prevent multiple initializations
|
||||
const initialized = useRef(false);
|
||||
|
||||
// Initialize app and setup periodic data refresh
|
||||
useEffect(() => {
|
||||
if (json?.media?.track[0] && bounce.search(json?.media?.track[0].Title) === -1) {
|
||||
setBounce('Now Playing: ')
|
||||
setLink(json?.media['@ref']?.replace('/musica', 'https://manalejandro.com'))
|
||||
Object.keys(json.media.track[0]).map((key) => {
|
||||
if (key === 'Title' || key === 'Performer' || key === 'Album') {
|
||||
if (key === 'Title') {
|
||||
setTitle(json.media.track[0][key])
|
||||
}
|
||||
setBounce(data => data + json.media.track[0][key] + ' - ')
|
||||
} else if (key === 'Filesize') {
|
||||
setBounce(data => data + Math.round(parseInt(json.media.track[0][key]) / 1024 / 1024) + 'Mb - ')
|
||||
} else if (key === 'Bitrate') {
|
||||
setBounce(data => data + Math.floor(parseInt(json.media.track[0][key]) / 1000) + 'kbps - ')
|
||||
} else if (key === 'Duration') {
|
||||
setBounce(data => data + Math.floor(parseInt(json.media.track[0][key])) + 's - ')
|
||||
} else if (key === 'Recorded_Date') {
|
||||
setBounce(data => data + json.media.track[0][key])
|
||||
}
|
||||
})
|
||||
} else if (!json?.media && bounce.search('Now Playing: Title not available') === -1) {
|
||||
setLink('')
|
||||
setTitle('Stream Radio')
|
||||
setBounce('Now Playing: Title not available')
|
||||
}
|
||||
}, [json])
|
||||
useEffect(() => {
|
||||
document.body.style.backgroundImage = `url('/wallpapers/${images[Math.floor(Math.random() * images.length)]}')`
|
||||
}, [images])
|
||||
useEffect(() => {
|
||||
if (once.current) return
|
||||
once.current = true
|
||||
M.AutoInit()
|
||||
load()
|
||||
loadImages()
|
||||
setAudioUrl('/stream.mp3')
|
||||
document.querySelector('input[type="range"]').addEventListener('change', (e) => {
|
||||
setCurrentVolume(e.target.value / 100)
|
||||
})
|
||||
const inter = setInterval(() => {
|
||||
load()
|
||||
}, (Math.floor(Math.random() * 20) + 10) * 1000),
|
||||
interback = setInterval(() => {
|
||||
loadImages()
|
||||
}, (Math.floor(Math.random() * 60) + 90) * 1000)
|
||||
if (initialized.current) return;
|
||||
initialized.current = true;
|
||||
|
||||
// Initialize Materialize components
|
||||
M.AutoInit();
|
||||
|
||||
// Load initial data
|
||||
loadAllData();
|
||||
loadImages();
|
||||
|
||||
// Set up periodic data refresh
|
||||
const dataRefreshInterval = setInterval(() => {
|
||||
loadAllData();
|
||||
}, (Math.floor(Math.random() * 20) + 10) * 1000);
|
||||
|
||||
// Set up periodic background image refresh
|
||||
const imageRefreshInterval = setInterval(() => {
|
||||
loadImages();
|
||||
}, (Math.floor(Math.random() * 60) + 90) * 1000);
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
clearInterval(inter)
|
||||
clearInterval(interback)
|
||||
}
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
if (audioElmRef.current && audioElmRef.current.volume !== currentVolume) {
|
||||
if (currentVolume > 0 && currentVolume <= 1) {
|
||||
setMuted(false)
|
||||
} else if (currentVolume === 0) {
|
||||
setMuted(true)
|
||||
}
|
||||
audioElmRef.current.volume = currentVolume
|
||||
}
|
||||
}, [currentVolume])
|
||||
clearInterval(dataRefreshInterval);
|
||||
clearInterval(imageRefreshInterval);
|
||||
};
|
||||
}, [loadAllData, loadImages]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1><a href="https://git.manalejandro.com/ale/stream-radio" title="Stream Radio Git Repository" alt="Stream Radio Git Repository" target="_blank"><Icon className="medium">hearing</Icon> Stream Radio</a></h1>
|
||||
<Header />
|
||||
|
||||
{analyzerData && <WaveForm analyzerData={analyzerData} />}
|
||||
<h4><p className="bounce"><a href={link} target="_blank" title={title} alt={title}>{bounce}</a></p></h4>
|
||||
|
||||
<TrackInfo
|
||||
title={title}
|
||||
link={link}
|
||||
trackInfo={trackInfo}
|
||||
/>
|
||||
|
||||
<br /><br />
|
||||
<div className="brown darken-3 player">
|
||||
{paused ?
|
||||
<Button node="button" className="brown" onClick={() => play()} waves="light" floating><Icon>play_arrow</Icon></Button>
|
||||
:
|
||||
<Button node="button" className="brown" onClick={() => {
|
||||
audioElmRef.current?.pause()
|
||||
setPaused(true)
|
||||
}} waves="light" floating><Icon>pause</Icon></Button>
|
||||
}
|
||||
{muted ?
|
||||
<Button node="button" className="brown" onClick={() => setMuted(false)} waves="light" floating><Icon>volume_off</Icon></Button>
|
||||
:
|
||||
<Button node="button" className="brown" onClick={() => setMuted(true)} waves="light" floating><Icon>volume_up</Icon></Button>
|
||||
}
|
||||
<Range className="brown" waves="light"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
/>
|
||||
<span>
|
||||
<Icon tiny>people</Icon> {currentListeners} / {maxListeners}
|
||||
</span>
|
||||
</div >
|
||||
<audio src={audioUrl} ref={audioElmRef} volume={Math.log10(currentVolume * 10)} preload={"none"} muted={muted} controls={false} />
|
||||
|
||||
<AudioControls
|
||||
paused={paused}
|
||||
muted={muted}
|
||||
currentListeners={currentListeners}
|
||||
maxListeners={maxListeners}
|
||||
onPlay={play}
|
||||
onPause={pause}
|
||||
onToggleMute={toggleMute}
|
||||
/>
|
||||
|
||||
<audio
|
||||
src="/stream.mp3"
|
||||
ref={audioElmRef}
|
||||
preload="none"
|
||||
muted={muted}
|
||||
controls={false}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
93
src/components/AudioControls.js
Normal file
93
src/components/AudioControls.js
Normal file
@ -0,0 +1,93 @@
|
||||
import { Button, Icon } from "react-materialize";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
/**
|
||||
* Audio player controls component
|
||||
*/
|
||||
const AudioControls = ({
|
||||
paused,
|
||||
muted,
|
||||
currentListeners,
|
||||
maxListeners,
|
||||
onPlay,
|
||||
onPause,
|
||||
onToggleMute
|
||||
}) => {
|
||||
return (
|
||||
<div className="brown darken-3 player">
|
||||
{paused ? (
|
||||
<Button
|
||||
node="button"
|
||||
className="brown"
|
||||
onClick={onPlay}
|
||||
waves="light"
|
||||
floating
|
||||
>
|
||||
<Icon>play_arrow</Icon>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
node="button"
|
||||
className="brown"
|
||||
onClick={onPause}
|
||||
waves="light"
|
||||
floating
|
||||
>
|
||||
<Icon>pause</Icon>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{muted ? (
|
||||
<Button
|
||||
node="button"
|
||||
className="brown"
|
||||
onClick={onToggleMute}
|
||||
waves="light"
|
||||
floating
|
||||
>
|
||||
<Icon>volume_off</Icon>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
node="button"
|
||||
className="brown"
|
||||
onClick={onToggleMute}
|
||||
waves="light"
|
||||
floating
|
||||
>
|
||||
<Icon>volume_up</Icon>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<input
|
||||
type="range"
|
||||
className="brown waves-light range-field"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
defaultValue={50}
|
||||
/>
|
||||
|
||||
<span>
|
||||
<Icon tiny>people</Icon> {currentListeners} / {maxListeners}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AudioControls.propTypes = {
|
||||
paused: PropTypes.bool.isRequired,
|
||||
muted: PropTypes.bool.isRequired,
|
||||
currentListeners: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
maxListeners: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
onPlay: PropTypes.func.isRequired,
|
||||
onPause: PropTypes.func.isRequired,
|
||||
onToggleMute: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
AudioControls.defaultProps = {
|
||||
currentListeners: 0,
|
||||
maxListeners: 0
|
||||
};
|
||||
|
||||
export default AudioControls;
|
32
src/components/Header.js
Normal file
32
src/components/Header.js
Normal file
@ -0,0 +1,32 @@
|
||||
import { useEffect, useRef, useCallback } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Icon } from "react-materialize";
|
||||
|
||||
/**
|
||||
* Header component with title and link
|
||||
*/
|
||||
const Header = ({ repoUrl }) => {
|
||||
return (
|
||||
<h1>
|
||||
<a
|
||||
href={repoUrl}
|
||||
title="Stream Radio Git Repository"
|
||||
alt="Stream Radio Git Repository"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon className="medium">hearing</Icon> Stream Radio
|
||||
</a>
|
||||
</h1>
|
||||
);
|
||||
};
|
||||
|
||||
Header.propTypes = {
|
||||
repoUrl: PropTypes.string
|
||||
};
|
||||
|
||||
Header.defaultProps = {
|
||||
repoUrl: "https://git.manalejandro.com/ale/stream-radio"
|
||||
};
|
||||
|
||||
export default Header;
|
35
src/components/TrackInfo.js
Normal file
35
src/components/TrackInfo.js
Normal file
@ -0,0 +1,35 @@
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
/**
|
||||
* Track info component with marquee effect
|
||||
*/
|
||||
const TrackInfo = ({ title, link, trackInfo }) => {
|
||||
return (
|
||||
<h4>
|
||||
<p className="bounce">
|
||||
<a
|
||||
href={link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title={title}
|
||||
alt={title}
|
||||
>
|
||||
{trackInfo}
|
||||
</a>
|
||||
</p>
|
||||
</h4>
|
||||
);
|
||||
};
|
||||
|
||||
TrackInfo.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
link: PropTypes.string,
|
||||
trackInfo: PropTypes.string
|
||||
};
|
||||
|
||||
TrackInfo.defaultProps = {
|
||||
link: '',
|
||||
trackInfo: 'Now Playing: Title not available'
|
||||
};
|
||||
|
||||
export default TrackInfo;
|
107
src/hooks/useAudioPlayer.js
Normal file
107
src/hooks/useAudioPlayer.js
Normal file
@ -0,0 +1,107 @@
|
||||
import { useCallback, useRef, useState, useEffect } from "react";
|
||||
|
||||
/**
|
||||
* Custom hook for managing audio playback functionality
|
||||
* @param {string} audioUrl - URL of the audio stream
|
||||
* @returns {Object} - Audio player state and controls
|
||||
*/
|
||||
const useAudioPlayer = (audioUrl) => {
|
||||
const [analyzerData, setAnalyzerData] = useState(null);
|
||||
const [currentVolume, setCurrentVolume] = useState(0.5);
|
||||
const [muted, setMuted] = useState(false);
|
||||
const [paused, setPaused] = useState(true);
|
||||
|
||||
const audioElmRef = useRef(null);
|
||||
const loadedAnalyzer = useRef(false);
|
||||
|
||||
// Initialize audio analyzer for visualization
|
||||
const initAudioAnalyzer = useCallback(() => {
|
||||
if (!audioElmRef.current) return;
|
||||
|
||||
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
const source = audioCtx.createMediaElementSource(audioElmRef.current);
|
||||
const analyzer = audioCtx.createAnalyser();
|
||||
|
||||
analyzer.fftSize = 2048;
|
||||
const bufferLength = analyzer.frequencyBinCount;
|
||||
const dataArray = new Uint8Array(bufferLength);
|
||||
|
||||
source.connect(analyzer);
|
||||
source.connect(audioCtx.destination);
|
||||
source.onended = () => source.disconnect();
|
||||
|
||||
setAnalyzerData({ analyzer, bufferLength, dataArray });
|
||||
loadedAnalyzer.current = true;
|
||||
}, []);
|
||||
|
||||
// Play audio function
|
||||
const play = useCallback(async () => {
|
||||
if (!loadedAnalyzer.current) {
|
||||
initAudioAnalyzer();
|
||||
}
|
||||
|
||||
setPaused(false);
|
||||
setMuted(false);
|
||||
try {
|
||||
await audioElmRef.current?.play();
|
||||
} catch (error) {
|
||||
console.error('Failed to play audio:', error);
|
||||
}
|
||||
}, [initAudioAnalyzer]);
|
||||
|
||||
// Pause audio function
|
||||
const pause = useCallback(() => {
|
||||
audioElmRef.current?.pause();
|
||||
setPaused(true);
|
||||
}, []);
|
||||
|
||||
// Toggle mute function
|
||||
const toggleMute = useCallback(() => {
|
||||
setMuted(prevMuted => !prevMuted);
|
||||
}, []);
|
||||
|
||||
// Update volume when currentVolume changes
|
||||
useEffect(() => {
|
||||
if (!audioElmRef.current || audioElmRef.current.volume === currentVolume) return;
|
||||
|
||||
// Update muted state based on volume level
|
||||
if (currentVolume > 0) {
|
||||
setMuted(false);
|
||||
} else if (currentVolume === 0) {
|
||||
setMuted(true);
|
||||
}
|
||||
|
||||
audioElmRef.current.volume = currentVolume;
|
||||
}, [currentVolume]);
|
||||
|
||||
// Setup volume change listener
|
||||
useEffect(() => {
|
||||
const volumeSlider = document.querySelector('input[type="range"]');
|
||||
|
||||
if (!volumeSlider) return;
|
||||
|
||||
const handleVolumeChange = (e) => {
|
||||
setCurrentVolume(e.target.value / 100);
|
||||
};
|
||||
|
||||
volumeSlider.addEventListener('change', handleVolumeChange);
|
||||
|
||||
return () => {
|
||||
volumeSlider.removeEventListener('change', handleVolumeChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
audioElmRef,
|
||||
analyzerData,
|
||||
currentVolume,
|
||||
muted,
|
||||
paused,
|
||||
setCurrentVolume,
|
||||
play,
|
||||
pause,
|
||||
toggleMute
|
||||
};
|
||||
};
|
||||
|
||||
export default useAudioPlayer;
|
39
src/hooks/useBackgroundImages.js
Normal file
39
src/hooks/useBackgroundImages.js
Normal file
@ -0,0 +1,39 @@
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
|
||||
/**
|
||||
* Custom hook for managing background images
|
||||
* @param {string} imagePath - Path to the text file containing image filenames
|
||||
* @returns {Object} - Background image state and loading function
|
||||
*/
|
||||
const useBackgroundImages = (imagePath) => {
|
||||
const [images, setImages] = useState([]);
|
||||
|
||||
// Load images from text file
|
||||
const loadImages = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch(imagePath);
|
||||
const data = await response.text();
|
||||
const imageList = data.split('\n').filter(line => line.length > 0);
|
||||
setImages(imageList);
|
||||
return imageList;
|
||||
} catch (err) {
|
||||
console.error('Error loading background images:', err.message);
|
||||
return [];
|
||||
}
|
||||
}, [imagePath]);
|
||||
|
||||
// Set random background image when images list changes
|
||||
useEffect(() => {
|
||||
if (images.length > 0) {
|
||||
const randomImage = images[Math.floor(Math.random() * images.length)];
|
||||
document.body.style.backgroundImage = `url('/wallpapers/${randomImage}')`;
|
||||
}
|
||||
}, [images]);
|
||||
|
||||
return {
|
||||
images,
|
||||
loadImages
|
||||
};
|
||||
};
|
||||
|
||||
export default useBackgroundImages;
|
122
src/hooks/useStreamData.js
Normal file
122
src/hooks/useStreamData.js
Normal file
@ -0,0 +1,122 @@
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
|
||||
/**
|
||||
* Custom hook for fetching streaming data
|
||||
* @returns {Object} - Streaming data and loading functions
|
||||
*/
|
||||
const useStreamData = () => {
|
||||
const [streamInfo, setStreamInfo] = useState({
|
||||
json: {},
|
||||
currentListeners: 0,
|
||||
maxListeners: 0,
|
||||
title: 'Stream Radio',
|
||||
link: '',
|
||||
trackInfo: '',
|
||||
});
|
||||
|
||||
// Load stream metadata
|
||||
const loadStreamMetadata = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch('/stream.json');
|
||||
const json = await response.json();
|
||||
return json;
|
||||
} catch (err) {
|
||||
console.error('Error fetching stream metadata:', err.message);
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Load listener statistics
|
||||
const loadListenerStats = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch('/status.xsl');
|
||||
const data = await response.text();
|
||||
const parser = new DOMParser();
|
||||
const xmlDoc = parser.parseFromString(data, 'text/html');
|
||||
const listeners = xmlDoc.getElementsByTagName('td');
|
||||
|
||||
let current = 0;
|
||||
let max = 0;
|
||||
|
||||
for (let i = 0; i < listeners.length; i++) {
|
||||
if (i === 9) {
|
||||
current = listeners[i].textContent;
|
||||
} else if (i === 11) {
|
||||
max = listeners[i].textContent;
|
||||
}
|
||||
}
|
||||
|
||||
return { current, max };
|
||||
} catch (err) {
|
||||
console.error('Error fetching listener stats:', err.message);
|
||||
return { current: 0, max: 0 };
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Load all stream data
|
||||
const loadAllData = useCallback(async () => {
|
||||
const [jsonData, listenerStats] = await Promise.all([
|
||||
loadStreamMetadata(),
|
||||
loadListenerStats()
|
||||
]);
|
||||
|
||||
setStreamInfo(prev => ({
|
||||
...prev,
|
||||
json: jsonData || {},
|
||||
currentListeners: listenerStats.current,
|
||||
maxListeners: listenerStats.max
|
||||
}));
|
||||
|
||||
return jsonData;
|
||||
}, [loadStreamMetadata, loadListenerStats]);
|
||||
|
||||
// Update track info when json changes
|
||||
useEffect(() => {
|
||||
const { json } = streamInfo;
|
||||
|
||||
if (json?.media?.track && json.media.track[0]) {
|
||||
const track = json.media.track[0];
|
||||
|
||||
// Build track info string
|
||||
let trackInfo = 'Now Playing: ';
|
||||
let trackTitle = '';
|
||||
|
||||
Object.entries(track).forEach(([key, value]) => {
|
||||
if (['Title', 'Performer', 'Album'].includes(key)) {
|
||||
trackInfo += value + ' - ';
|
||||
if (key === 'Title') trackTitle = value;
|
||||
} else if (key === 'Filesize') {
|
||||
trackInfo += Math.round(parseInt(value) / 1024 / 1024) + 'Mb - ';
|
||||
} else if (key === 'Bitrate') {
|
||||
trackInfo += Math.floor(parseInt(value) / 1000) + 'kbps - ';
|
||||
} else if (key === 'Duration') {
|
||||
trackInfo += Math.floor(parseInt(value)) + 's - ';
|
||||
} else if (key === 'Recorded_Date') {
|
||||
trackInfo += value;
|
||||
}
|
||||
});
|
||||
|
||||
// Update stream info
|
||||
setStreamInfo(prev => ({
|
||||
...prev,
|
||||
title: trackTitle || 'Stream Radio',
|
||||
link: json?.media['@ref']?.replace('/musica', 'https://manalejandro.com') || '',
|
||||
trackInfo
|
||||
}));
|
||||
} else if (!json?.media) {
|
||||
setStreamInfo(prev => ({
|
||||
...prev,
|
||||
title: 'Stream Radio',
|
||||
link: '',
|
||||
trackInfo: 'Now Playing: Title not available'
|
||||
}));
|
||||
}
|
||||
}, [streamInfo.json]);
|
||||
|
||||
return {
|
||||
...streamInfo,
|
||||
loadAllData
|
||||
};
|
||||
};
|
||||
|
||||
export default useStreamData;
|
Loading…
x
Reference in New Issue
Block a user