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
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);
|
|
}); |