// Radio Emulator // Kaotisk Hund 2024 // // A simple co-mechanism to pretend playing a live radio as it would happen for // a radio station that mostly plays prerecoded shows. // // Client side implementation // Let's remind here the structures we are waiting for: // 1. Hash // 2. 0 // 3. audio/ogg file // 4. application/json file // // 1. Hash // Can be an SHA512sum or SHA256sum, we don't do checks, we only ask the hash // accompanied by what do we think it is. // // 2. 0 // When nothing exists on the radio station we are visiting. // // 3. audio/ogg file // An audio file to play. // // 4. application/json file // Could be one of the following: // - list // - show_info // var debugMode = true; const apiURL = "http://z.kaotisk-hund.com:8010/"; const version = "v0"; const listRequest = `${apiURL}${version}/list` const jsonRequest = `${apiURL}${version}/application/json/` const audioRequest = `${apiURL}${version}/audio/ogg/` function debugLog(message) { if ( debugMode ) console.log(message); } var audioElement = document.querySelector('audio'); var placeholders = { currentTime: document.querySelector('.current-time'), listStarted: document.querySelector('.started-on'), currentShowHash : document.querySelector('.current-show-hash'), listHash : document.querySelector('.list-hash'), artist: document.querySelector('.artist'), title: document.querySelector('.title'), radioPlayer: document.querySelector('.radio-player'), youAreHere : document.querySelector('.you-are-here'), playProgress: document.querySelector('.play-progress'), startsOn: document.querySelector('.starts-on'), showDuration: document.querySelectorAll('.show-duration'), relativeTime: document.querySelector('.relative-time'), listeningAt: document.querySelector('.listening-at'), dlProgress: document.querySelector('.dl-progress'), minTimesPlayed: document.querySelector('.min-played'), maxTimesPlayed: document.querySelector('.max-played'), listDuration: document.querySelector('.list-duration'), previouslyPlayed: document.querySelector('.previously-played'), showInfo: document.querySelector('.show-info'), listInfo: document.querySelector('.list-info'), dt: document.querySelector('.d-t'), deltaTime: document.querySelector('.delta-time'), } const documentTitle = "Radio Station Emulator"; const separator = " :: "; function setTitleSyncing(){ document.title = "🔃Syncing" + separator + documentTitle; } function setTitleMessage(message){ document.title = message + separator + documentTitle; } var seconds_here = 0; function increaseSeconds() { seconds_here = seconds_here+1; placeholders.youAreHere.innerText = seconds_here; // debugLog(`${typeof(progressbarValue)} progressbarValue = ${progressbarValue}`) placeholders.playProgress.value = isNaN(parseInt(placeholders.currentTime.innerText)) ? 0 : parseInt(placeholders.currentTime.innerText) + parseInt(seconds_here); placeholders.relativeTime.innerText = parseInt(placeholders.currentTime.innerText) + seconds_here; placeholders.listeningAt.innerText = Math.floor(audioElement.currentTime); return seconds_here; } function getSecondsHere() { return seconds_here; } setInterval(increaseSeconds, 1000); function FetchJSON( url, callback, params ) { setTitleSyncing(); const request = new XMLHttpRequest(); request.addEventListener("load", ()=>{ var json = JSON.parse(request.response); if(request.status !== 404){ callback(json, params); } else { debugLog(`ERROR ${request.status} while loading ${url}`); } }); request.addEventListener("error", ()=>{ debugLog("An error occured. While loading: "+url+" for "+callback+"."); }); request.addEventListener("abort", ()=>{ debugLog("Request aborted: "+url+" for "+callback+"."); }); request.open("GET", url); request.send(); } function FetchAudio(url) { const request = new XMLHttpRequest(); request.responseType = 'blob'; request.addEventListener("load", ()=>{ if(request.status === 200){ debugLog("Got it... trying!") placeholders.dlProgress.value = 100; audioElement.src = URL.createObjectURL(request.response); audioElement.play(); debugLog("Tried... did it work?"); } else { debugLog(`ERROR ${request.status} while loading ${url}`); } }); request.addEventListener("progress", (event)=>{ if (event.lengthComputable) { dlProgressBar.value = (event.loaded/event.total)*100; } }); request.addEventListener("error", ()=>{ debugLog("An error occured. While loading: "+url); }); request.addEventListener("abort", ()=>{ debugLog("Request aborted: "+url); }); request.open("GET", url); request.send(); } function genericCallback(json, params) { debugLog('genericCallback'); debugLog(json); debugLog(params); } var calledLoadShowCallback = 0; function stopHereAndReflect() { return 0; } function loadShowCallback(json, params) { const [ list, now_on_sequence, element, hash_of_list ] = params; debugLog('loadShowCallback'); debugLog(json); debugLog(element); //debugLog(params); placeholders.currentShowHash.innerText = element.hash; placeholders.artist.innerText = json.artist; placeholders.title.innerText = json.title; setTitleMessage ( "▶️ " + json.artist + " - " + json.title ); debugLog('----------'); placeholders.showDuration.forEach((elem)=>{elem.innerText = Math.floor(element.duration/1000)}); placeholders.startsOn.innerText = element.starts_on; placeholders.playProgress.max = Math.floor(element.duration/1000); placeholders.showInfo.innerText = JSON.stringify(json, null, 2); audioElement.load(); FetchAudio(`${audioRequest}${json.hash}`); audioElement.type = json.mimetype; placeholders.currentTime.innerText = Math.floor((now_on_sequence - element.starts_on)/1000); audioElement.addEventListener('ended', function(){ FetchJSON(`${listRequest}`, hashCallback, [ new Date().getTime() ]); setTimeout(sync_radio, 10000); }); setTimeout(sync_radio, 10000); } function slowIncreaseVolume() { if (audioElement.volume < 0.91) { audioElement.volume += 0.05; } else { audioElement.volume = 1; } } function fadeInAudio() { setTimeout( slowIncreaseVolume, 250); setTimeout( slowIncreaseVolume, 500); setTimeout( slowIncreaseVolume, 750); setTimeout( slowIncreaseVolume, 1000); setTimeout( slowIncreaseVolume, 1250); setTimeout( slowIncreaseVolume, 1500); setTimeout( slowIncreaseVolume, 1750); setTimeout( slowIncreaseVolume, 2050); setTimeout( slowIncreaseVolume, 2350); setTimeout( slowIncreaseVolume, 2700); } function sync_radio() { var value = placeholders.currentTime.innerText; if ( value !== "" ) { var new_now = parseFloat(value) + getSecondsHere(); debugLog("Trying to sync @ "+ value + " + " + getSecondsHere() + " = " + new_now); audioElement.currentTime = new_now; audioElement.play(); audioElement.muted = false; audioElement.volume = 0; fadeInAudio(); return new_now; } return 0; } function listCallback(json, params) { debugLog('listCallback'); debugLog(json); debugLog(params); var [ now, hash_of_list ] = params; var delta_time = now - json.started_on; var min_times_played = Math.floor( delta_time / json.duration ); var max_times_to_be_played = delta_time / json.duration; var Dt = max_times_to_be_played - min_times_played; var now_on_sequence = Dt * json.duration; placeholders.listStarted.innerText = json.started_on; placeholders.listHash.innerText = hash_of_list; placeholders.deltaTime.innerText = delta_time; placeholders.dt.innerText = Dt; placeholders.currentTime.innerText = now_on_sequence; placeholders.listDuration.innerText = json.duration; placeholders.minTimesPlayed.innerText = min_times_played; placeholders.maxTimesPlayed.innerText = max_times_to_be_played; placeholders.listInfo.innerText = JSON.stringify(json, null, 2); debugLog(`now_on_sequence: ${now_on_sequence}, Dt: ${Dt}`) previous = { starts_on: -1 }; if ( json.list.length === 1 ) { FetchJSON(`${jsonRequest}${json.list[0].hash}`, loadShowCallback, [json, now_on_sequence, json.list[0], hash_of_list]); } for ( var i = 1; i < json.list.length; i++) { debugLog("getting there") if ( now_on_sequence < json.list[i].starts_on && now_on_sequence > json.list[i-1].starts_on ) { var tmp = document.createElement('pre'); tmp.innerText = JSON.stringify(json.list[i-1], null, 2); placeholders.previouslyPlayed.append(tmp); now_on_sequence = now_on_sequence - previous.starts_on; debugLog(`now_on_sequence (1updated): ${now_on_sequence}`); previous = json.list[i-1]; FetchJSON(`${jsonRequest}${previous.hash}`, loadShowCallback, [json, now_on_sequence, previous, hash_of_list]); debugLog('First!'); break; } else if ( now_on_sequence > json.list[i].starts_on && now_on_sequence > json.list[i-1].starts_on ) { var tmp = document.createElement('pre'); tmp.innerText = JSON.stringify(json.list[i-1], null, 2); placeholders.previouslyPlayed.append(tmp); FetchJSON(`${jsonRequest}${json.list[i].hash}`, loadShowCallback, [json, now_on_sequence, json.list[i], hash_of_list]); debugLog('Second!'); break; } else { debugLog(now_on_sequence); debugLog(json.list[i].starts_on); debugLog(json.list[i-1].starts_on); debugLog('Nothing!'); continue; } } } function hashCallback(json, params) { var [ now ] = params; debugLog('hashCallback'); FetchJSON(`${jsonRequest}${json.latest_list}`, listCallback, [now, json.latest_list]); } FetchJSON(`${listRequest}`, hashCallback, [ new Date().getTime() ]);