homelab-server/website/script.js

205 lines
5.7 KiB
JavaScript

// Rapture Radio Local Playlist Configuration (Only containing your 6 downloaded files)
const playlist = [
{
title: "Midnight, The Stars And You (1934) - Al Bowlly",
src: "audio/Al Bowlly - Midnight,Stars And You.mp3"
},
{
title: "Heartaches (1931) - Al Bowlly",
src: "audio/Al Bowlly - Heartaches.mp3"
},
{
title: "Belleville (1942) - Django Reinhardt",
src: "audio/Django Reinhardt - Belleville.mp3"
},
{
title: "Love Is The Sweetest Thing (1932) - Al Bowlly",
src: "audio/Al Bowlly - Love Is the Sweetest Thing.mp3"
},
{
title: "Nuages (1940) - Django Reinhardt",
src: "audio/Django Reinhardt - Nuages.mp3"
},
{
title: "Close Your Eyes (1933) - Al Bowlly",
src: "audio/Al Bowlly - Close Your Eyes.mp3"
}
];
let currentTrackIndex = 0;
let audioCtx;
let filterNode;
let sourceNode;
let isPlaying = false; // Explicit playback tracking flag
const audioElement = document.getElementById('radio-audio');
const playButton = document.getElementById('radio-toggle');
const volumeControl = document.getElementById('volume');
const nowPlayingText = document.querySelector('.now-playing');
// Load the initial track data
loadTrack(currentTrackIndex);
// Function to load a track by index
function loadTrack(index) {
const track = playlist[index];
audioElement.src = track.src;
nowPlayingText.textContent = `TUNED: ${track.title}`;
}
// Function to initialize Audio Context & Filter
function initAudio() {
if (audioCtx) return; // Initialize only once
const AudioContext = window.AudioContext || window.webkitAudioContext;
audioCtx = new AudioContext();
sourceNode = audioCtx.createMediaElementSource(audioElement);
// Apply AM Radio Bandpass Filter
filterNode = audioCtx.createBiquadFilter();
filterNode.type = 'bandpass';
filterNode.frequency.value = 1100;
filterNode.Q.value = 1.8;
// Connection chain
sourceNode.connect(filterNode);
filterNode.connect(audioCtx.destination);
}
// Resilient Play/Pause toggle
playButton.addEventListener('click', () => {
initAudio();
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
// Play synthetic mechanical switch SFX
playSwitchSound();
if (!isPlaying) {
isPlaying = true;
playButton.textContent = 'ON';
playButton.classList.add('active');
audioElement.play().catch(err => {
console.error("Playback failed. Make sure the local file exists on disk!", err);
});
} else {
audioElement.pause();
isPlaying = false;
playButton.textContent = 'OFF';
playButton.classList.remove('active');
}
});
// Auto-play next song when current one ends
audioElement.addEventListener('ended', () => {
currentTrackIndex = (currentTrackIndex + 1) % playlist.length;
loadTrack(currentTrackIndex);
if (isPlaying) {
audioElement.play().catch(err => {
console.error("Autoplay failed:", err);
});
}
});
// Adjust volume
volumeControl.addEventListener('input', (e) => {
audioElement.volume = e.target.value;
});
// Dynamic Node Status Monitoring
function updateNodeStatuses() {
fetch('status.json')
.then(res => {
if (!res.ok) throw new Error("status.json not ready yet");
return res.json();
})
.then(data => {
updateIndicator('status-control', data.control);
updateIndicator('status-web', data.web);
updateIndicator('status-media', data.media);
})
.catch(err => {
console.warn("Status check failed (waiting for script to run on VM):", err);
});
}
function updateIndicator(id, isOnline) {
const el = document.getElementById(id);
if (!el) return;
if (isOnline) {
el.className = "status-indicator online";
} else {
el.className = "status-indicator offline";
}
}
// Poll status.json every 10 seconds
setInterval(updateNodeStatuses, 10000);
updateNodeStatuses(); // Run immediately on page load
// Sound synthesis utilities (Pure Web Audio API, zero downloads required)
function playClickSound() {
if (!audioCtx) return; // Wait for initial user click to enable audio
if (audioCtx.state === 'suspended') return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(1400, audioCtx.currentTime); // High pitch click
osc.frequency.exponentialRampToValueAtTime(400, audioCtx.currentTime + 0.03);
gain.gain.setValueAtTime(0.04, audioCtx.currentTime); // Soft volume
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.03); // Rapid decay
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 0.03);
}
function playSwitchSound() {
if (!audioCtx) return;
if (audioCtx.state === 'suspended') return;
// Dual oscillator thick mechanical click sound
const osc1 = audioCtx.createOscillator();
const osc2 = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc1.type = 'triangle';
osc1.frequency.setValueAtTime(180, audioCtx.currentTime);
osc1.frequency.exponentialRampToValueAtTime(60, audioCtx.currentTime + 0.08);
osc2.type = 'sawtooth';
osc2.frequency.setValueAtTime(260, audioCtx.currentTime);
osc2.frequency.exponentialRampToValueAtTime(90, audioCtx.currentTime + 0.06);
gain.gain.setValueAtTime(0.12, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.1);
osc1.connect(gain);
osc2.connect(gain);
gain.connect(audioCtx.destination);
osc1.start();
osc2.start();
osc1.stop(audioCtx.currentTime + 0.1);
osc2.stop(audioCtx.currentTime + 0.1);
}
// Bind mechanical hover sound clicks to all cards
document.querySelectorAll('.card').forEach(card => {
card.addEventListener('mouseenter', () => {
playClickSound();
});
});