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/react": "^16.3.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"materialize-css": "^1.0.0",
|
"materialize-css": "^1.0.0",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-materialize": "^3.10.0",
|
"react-materialize": "^3.10.0",
|
||||||
|
256
src/App.js
256
src/App.js
@ -1,175 +1,111 @@
|
|||||||
import "./App.css";
|
import "./App.css";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import WaveForm from "./WaveForm";
|
import WaveForm from "./WaveForm";
|
||||||
import M from "materialize-css";
|
import M from "materialize-css";
|
||||||
import { Button, Icon, Range } from "react-materialize";
|
|
||||||
import text from "./list.txt";
|
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 App = () => {
|
||||||
const [audioUrl, setAudioUrl] = useState(null),
|
// Custom hook for managing background images
|
||||||
[analyzerData, setAnalyzerData] = useState(null),
|
const { loadImages } = useBackgroundImages(text);
|
||||||
[bounce, setBounce] = useState(''),
|
|
||||||
[json, setJson] = useState({}),
|
// Custom hook for streaming data
|
||||||
[currentVolume, setCurrentVolume] = useState(0.5),
|
const {
|
||||||
[muted, setMuted] = useState(false),
|
json,
|
||||||
[link, setLink] = useState(''),
|
currentListeners,
|
||||||
[title, setTitle] = useState('Stream Radio'),
|
maxListeners,
|
||||||
[images, setImages] = useState([]),
|
title,
|
||||||
[currentListeners, setCurrentListeners] = useState(0),
|
link,
|
||||||
[maxListeners, setMaxListeners] = useState(0),
|
trackInfo,
|
||||||
[paused, setPaused] = useState(true),
|
loadAllData
|
||||||
audioElmRef = useRef(null),
|
} = useStreamData();
|
||||||
loadedAnalyzer = useRef(false),
|
|
||||||
once = useRef(false),
|
// Custom hook for audio player functionality
|
||||||
audioAnalyzer = () => {
|
const {
|
||||||
const audioCtx = new (window.AudioContext || window.webkitAudioContext)(),
|
audioElmRef,
|
||||||
source = audioCtx.createMediaElementSource(audioElmRef.current),
|
analyzerData,
|
||||||
analyzer = audioCtx.createAnalyser()
|
muted,
|
||||||
analyzer.fftSize = 2048
|
paused,
|
||||||
const bufferLength = analyzer.frequencyBinCount,
|
play,
|
||||||
dataArray = new Uint8Array(bufferLength)
|
pause,
|
||||||
source.connect(analyzer)
|
toggleMute
|
||||||
source.connect(audioCtx.destination)
|
} = useAudioPlayer("/stream.mp3");
|
||||||
source.onended = () => {
|
|
||||||
source.disconnect()
|
// Initialization flag to prevent multiple initializations
|
||||||
}
|
const initialized = useRef(false);
|
||||||
setAnalyzerData({ analyzer, bufferLength, dataArray })
|
|
||||||
},
|
// Initialize app and setup periodic data refresh
|
||||||
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()
|
|
||||||
})
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (json?.media?.track[0] && bounce.search(json?.media?.track[0].Title) === -1) {
|
if (initialized.current) return;
|
||||||
setBounce('Now Playing: ')
|
initialized.current = true;
|
||||||
setLink(json?.media['@ref']?.replace('/musica', 'https://manalejandro.com'))
|
|
||||||
Object.keys(json.media.track[0]).map((key) => {
|
// Initialize Materialize components
|
||||||
if (key === 'Title' || key === 'Performer' || key === 'Album') {
|
M.AutoInit();
|
||||||
if (key === 'Title') {
|
|
||||||
setTitle(json.media.track[0][key])
|
// Load initial data
|
||||||
}
|
loadAllData();
|
||||||
setBounce(data => data + json.media.track[0][key] + ' - ')
|
loadImages();
|
||||||
} else if (key === 'Filesize') {
|
|
||||||
setBounce(data => data + Math.round(parseInt(json.media.track[0][key]) / 1024 / 1024) + 'Mb - ')
|
// Set up periodic data refresh
|
||||||
} else if (key === 'Bitrate') {
|
const dataRefreshInterval = setInterval(() => {
|
||||||
setBounce(data => data + Math.floor(parseInt(json.media.track[0][key]) / 1000) + 'kbps - ')
|
loadAllData();
|
||||||
} else if (key === 'Duration') {
|
}, (Math.floor(Math.random() * 20) + 10) * 1000);
|
||||||
setBounce(data => data + Math.floor(parseInt(json.media.track[0][key])) + 's - ')
|
|
||||||
} else if (key === 'Recorded_Date') {
|
// Set up periodic background image refresh
|
||||||
setBounce(data => data + json.media.track[0][key])
|
const imageRefreshInterval = setInterval(() => {
|
||||||
}
|
loadImages();
|
||||||
})
|
}, (Math.floor(Math.random() * 60) + 90) * 1000);
|
||||||
} else if (!json?.media && bounce.search('Now Playing: Title not available') === -1) {
|
|
||||||
setLink('')
|
// Cleanup function
|
||||||
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)
|
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(inter)
|
clearInterval(dataRefreshInterval);
|
||||||
clearInterval(interback)
|
clearInterval(imageRefreshInterval);
|
||||||
}
|
};
|
||||||
}, [])
|
}, [loadAllData, loadImages]);
|
||||||
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])
|
|
||||||
return (
|
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} />}
|
{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 />
|
<br /><br />
|
||||||
<div className="brown darken-3 player">
|
|
||||||
{paused ?
|
<AudioControls
|
||||||
<Button node="button" className="brown" onClick={() => play()} waves="light" floating><Icon>play_arrow</Icon></Button>
|
paused={paused}
|
||||||
:
|
muted={muted}
|
||||||
<Button node="button" className="brown" onClick={() => {
|
currentListeners={currentListeners}
|
||||||
audioElmRef.current?.pause()
|
maxListeners={maxListeners}
|
||||||
setPaused(true)
|
onPlay={play}
|
||||||
}} waves="light" floating><Icon>pause</Icon></Button>
|
onPause={pause}
|
||||||
}
|
onToggleMute={toggleMute}
|
||||||
{muted ?
|
/>
|
||||||
<Button node="button" className="brown" onClick={() => setMuted(false)} waves="light" floating><Icon>volume_off</Icon></Button>
|
|
||||||
:
|
<audio
|
||||||
<Button node="button" className="brown" onClick={() => setMuted(true)} waves="light" floating><Icon>volume_up</Icon></Button>
|
src="/stream.mp3"
|
||||||
}
|
ref={audioElmRef}
|
||||||
<Range className="brown" waves="light"
|
preload="none"
|
||||||
min={0}
|
muted={muted}
|
||||||
max={100}
|
controls={false}
|
||||||
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} />
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
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