You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

297 lines
11 KiB
JavaScript

document.addEventListener('DOMContentLoaded', function() {
const songTitle = document.getElementById('songTitle');
const artist = document.getElementById('artist');
const statusElement = document.getElementById('status');
const listenersElement = document.getElementById('listeners');
const bitrateElement = document.getElementById('bitrate');
const uptimeElement = document.getElementById('uptime');
const audio = new Audio('https://broadcast.cif.su/main');
audio.crossOrigin = 'anonymous';
let isPlaying = false;
let metadataInterval;
let statsInterval;
let currentShow = '';
// Volume Knob Variables
var knobPositionX;
var knobPositionY;
var mouseX;
var mouseY;
var knobCenterX;
var knobCenterY;
var adjacentSide;
var oppositeSide;
var currentRadiansAngle;
var getRadiansInDegrees;
var finalAngleInDegrees;
var volumeSetting = 0; // Set initial volume to 0%
var tickHighlightPosition;
var startingTickAngle = -135;
var tickContainer = document.getElementById("tickContainer");
var volumeKnob = document.getElementById("knob");
var boundingRectangle = volumeKnob.getBoundingClientRect();
// Set initial volume
audio.volume = volumeSetting / 100;
// Format time (seconds to HH:MM:SS)
function formatTime(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
return [
hours.toString().padStart(2, '0'),
minutes.toString().padStart(2, '0'),
secs.toString().padStart(2, '0')
].join(':');
}
// Fetch Icecast stats
async function fetchStats() {
try {
const response = await fetch('https://broadcast.cif.su/status-json.xsl');
if (!response.ok) throw new Error('Network response was not ok');
const data = await response.json();
const source = data.icestats.source;
if (source) {
// Update stats
listenersElement.textContent = source.listeners || '0';
if (source.server_start_iso8601) {
const startTime = new Date(source.server_start_iso8601);
const uptimeSeconds = Math.floor((new Date() - startTime) / 1000);
uptimeElement.textContent = formatTime(uptimeSeconds);
}
}
} catch (error) {
console.error('Error fetching stats:', error);
}
}
// Fetch Libretime current track info
async function fetchCurrentTrack() {
try {
const response = await fetch('https://fm.cif.su/api/live-info');
if (!response.ok) throw new Error('Network response was not ok');
const data = await response.json();
if (data.current && data.current.metadata) {
const track = data.current.metadata;
// Update song info
songTitle.textContent = track.track_title || 'Unknown Track';
artist.textContent = track.artist_name || 'Unknown Artist';
// Update show info if available
if (data.current.show && data.current.show.name !== currentShow) {
currentShow = data.current.show.name;
statusElement.textContent = isPlaying ? 'Playing' : `Current Show: ${currentShow}`;
}
}
} catch (error) {
console.error('Error fetching current track:', error);
// Fallback to Icecast metadata if Libretime API fails
fetchIcecastMetadata();
}
}
// Fallback to Icecast metadata if needed
async function fetchIcecastMetadata() {
try {
const response = await fetch('https://broadcast.cif.su/status-json.xsl');
if (!response.ok) throw new Error('Network response was not ok');
const data = await response.json();
const source = data.icestats.source;
if (source && source.title) {
const titleParts = source.title.split(' - ');
if (titleParts.length > 1) {
artist.textContent = titleParts[0];
songTitle.textContent = titleParts.slice(1).join(' - ');
} else {
songTitle.textContent = source.title;
}
}
} catch (error) {
console.error('Error fetching Icecast metadata:', error);
}
}
// Volume Knob Functions
function initVolumeKnob() {
// Calculate initial rotation angle based on initial volume (0%)
const initialAngle = (volumeSetting / 100) * 270;
volumeKnob.style.transform = "rotate(" + initialAngle + "deg)";
// Calculate how many ticks to highlight (0% of 27 ticks is 0)
tickHighlightPosition = Math.round((volumeSetting * 2.7) / 10);
createTicks(27, tickHighlightPosition);
volumeKnob.addEventListener(getMouseDown(), onMouseDown);
document.addEventListener(getMouseUp(), onMouseUp);
}
//on mouse button down
function onMouseDown() {
document.addEventListener(getMouseMove(), onMouseMove); //start drag
}
//on mouse button release
function onMouseUp() {
document.removeEventListener(getMouseMove(), onMouseMove); //stop drag
}
//compute mouse angle relative to center of volume knob
function onMouseMove(event) {
knobPositionX = boundingRectangle.left;
knobPositionY = boundingRectangle.top;
if(detectMobile() == "desktop") {
mouseX = event.pageX;
mouseY = event.pageY;
} else {
mouseX = event.touches[0].pageX;
mouseY = event.touches[0].pageY;
}
knobCenterX = boundingRectangle.width / 2 + knobPositionX;
knobCenterY = boundingRectangle.height / 2 + knobPositionY;
adjacentSide = knobCenterX - mouseX;
oppositeSide = knobCenterY - mouseY;
currentRadiansAngle = Math.atan2(adjacentSide, oppositeSide);
getRadiansInDegrees = currentRadiansAngle * 180 / Math.PI;
finalAngleInDegrees = -(getRadiansInDegrees - 135);
if(finalAngleInDegrees >= 0 && finalAngleInDegrees <= 270) {
volumeKnob.style.transform = "rotate(" + finalAngleInDegrees + "deg)";
volumeSetting = Math.floor(finalAngleInDegrees / (270 / 100));
tickHighlightPosition = Math.round((volumeSetting * 2.7) / 10);
createTicks(27, tickHighlightPosition);
// Handle play/pause based on volume setting
if (volumeSetting > 0 && !isPlaying) {
audio.play()
.then(() => {
statusElement.textContent = 'Playing';
isPlaying = true;
// Don't clear the metadata interval when pausing
})
.catch(error => {
console.error('Error playing stream:', error);
statusElement.textContent = 'Error: ' + error.message;
});
} else if (volumeSetting === 0 && isPlaying) {
audio.pause();
statusElement.textContent = currentShow ? `Current Show: ${currentShow}` : 'Paused';
isPlaying = false;
// Don't clear the metadata interval when pausing
}
audio.volume = volumeSetting / 100;
document.getElementById("volumeValue").textContent = volumeSetting + "%";
}
}
//dynamically create volume knob "ticks"
function createTicks(numTicks, highlightNumTicks) {
while(tickContainer.firstChild) {
tickContainer.removeChild(tickContainer.firstChild);
}
for(var i=0;i<numTicks;i++) {
var tick = document.createElement("div");
tick.className = i < highlightNumTicks ? "tick activetick" : "tick";
tickContainer.appendChild(tick);
tick.style.transform = "rotate(" + startingTickAngle + "deg)";
startingTickAngle += 10;
}
startingTickAngle = -135;
}
//detect for mobile devices
function detectMobile() {
var result = (navigator.userAgent.match(/(iphone)|(ipod)|(ipad)|(android)|(blackberry)|(windows phone)|(symbian)/i));
return result !== null ? "mobile" : "desktop";
}
function getMouseDown() {
return detectMobile() == "desktop" ? "mousedown" : "touchstart";
}
function getMouseUp() {
return detectMobile() == "desktop" ? "mouseup" : "touchend";
}
function getMouseMove() {
return detectMobile() == "desktop" ? "mousemove" : "touchmove";
}
// Initial setup
fetchStats();
fetchCurrentTrack();
statsInterval = setInterval(fetchStats, 10000);
// Start metadata updates regardless of playback state
metadataInterval = setInterval(fetchCurrentTrack, 10000);
initVolumeKnob();
audio.addEventListener('error', function() {
statusElement.textContent = 'Error connecting to stream';
isPlaying = false;
});
// Cover art functionality
const PLACEHOLDER_IMAGE_URL = 'https://code.cif.su/CiF/Branding/raw/branch/main/Logos/Radio/CiF-Radio.svg';
let currentImageVisible = true;
document.getElementById('placeholder-image').src = PLACEHOLDER_IMAGE_URL;
async function fetchNowPlaying() {
try {
const response = await fetch('https://fm.cif.su/api/live-info');
const data = await response.json();
const trackId = data.current.metadata.id;
const artworkUrl = `https://fm.cif.su/api/track?id=${trackId}&return=artwork&t=${new Date().getTime()}`;
const visibleImage = currentImageVisible ? 'cover-image' : 'temp-image';
const hiddenImage = currentImageVisible ? 'temp-image' : 'cover-image';
const hiddenImg = document.getElementById(hiddenImage);
const placeholderImg = document.getElementById('placeholder-image');
hiddenImg.onload = function() {
placeholderImg.style.opacity = '0';
hiddenImg.style.opacity = '1';
document.getElementById(visibleImage).style.opacity = '0';
currentImageVisible = !currentImageVisible;
};
hiddenImg.onerror = function() {
placeholderImg.style.opacity = '1';
document.getElementById('cover-image').style.opacity = '0';
document.getElementById('temp-image').style.opacity = '0';
};
hiddenImg.src = artworkUrl;
} catch (error) {
console.error('Error fetching now playing data:', error);
document.getElementById('placeholder-image').style.opacity = '1';
}
}
document.getElementById('cover-image').style.opacity = '0';
document.getElementById('temp-image').style.opacity = '0';
document.getElementById('placeholder-image').style.opacity = '1';
fetchNowPlaying();
setInterval(fetchNowPlaying, 10000);
});