diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c30cfb1 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "icecast-metadata-js"] + path = icecast-metadata-js + url = https://github.com/eshaz/icecast-metadata-js.git diff --git a/embed.html b/embed.html new file mode 100644 index 0000000..6ff8657 --- /dev/null +++ b/embed.html @@ -0,0 +1,188 @@ + + + wormTuner Embed + + + + + + + + + + + + +
wormTuner
+ + + + +
+ +
+ + \ No newline at end of file diff --git a/favicon.png b/favicon.png new file mode 100644 index 0000000..c93f85b Binary files /dev/null and b/favicon.png differ diff --git a/heart.png b/heart.png new file mode 100644 index 0000000..ad1d536 Binary files /dev/null and b/heart.png differ diff --git a/icecast-metadata-js b/icecast-metadata-js new file mode 160000 index 0000000..818cf3f --- /dev/null +++ b/icecast-metadata-js @@ -0,0 +1 @@ +Subproject commit 818cf3fe5cafeb46c4ec91dc15ea9105c5838bc8 diff --git a/index.html b/index.html index ecea275..ecdc46b 100644 --- a/index.html +++ b/index.html @@ -1,40 +1,75 @@ + - - wormTuner - - - - - - - - - - - -
- -
-
-
-
-
-
- - Test - -
- -
- -
-
-
-
-

Click on a station to tune in! If you are having trouble, try the Alternate player.

-
- -
-
- - + + wormTuner + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ + +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ + +
+ +
+
+ + + \ No newline at end of file diff --git a/play.png b/play.png new file mode 100644 index 0000000..ee71f0e Binary files /dev/null and b/play.png differ diff --git a/radio.css b/radio.css new file mode 100644 index 0000000..a2639d0 --- /dev/null +++ b/radio.css @@ -0,0 +1,304 @@ +:root { + --background: #b3a99e; + + --primary: #0079b5; + --primary-border: #009ae6; + + --secondary: #f3dca9; + --secondary-border: #f2e5c8; + + --button: #DFDFDF; + --button-active: gray; + + --monitor: #0F0F0F; + --monitor-text: #F0F0F0; + --monitor-border: gray; + + --link-color: unset; +} + +*:link, *:visited { + color: var(--link-color); +} + +html { + height: 100%; +} + +body { + background: var(--background); + background-repeat: repeat; + height: 100%; + margin: 0; +} + +.container, .player-container { + background: var(--primary); + border: 2px outset var(--primary-border); + padding: 4px; +} +.container { + min-width: 320px; + min-height: 280px; + display: flex; + gap: 4px; + margin: auto; + max-width: 976px; +} +.player-container { + position: relative; + border-bottom: none; + width: 320px; + height: 78px; + margin-bottom: -2px; + z-index: 1; +} +.footer { + min-width: 320px; + max-width: 976px; + text-align: right; + margin: auto; +} + +player, visualizer { + display: flex; + flex-direction: column; + background: var(--secondary); + border: 2px outset var(--secondary-border); + flex-shrink: 0; + padding: 4px; +} + +.banner { + display: flex; +} + +player { + width: 308px; height: 68px; +} + +visualizer { + margin-bottom: 4px; +} + +.vis-container { + width: 320px; + max-width: 320px; +} + +button, fakebutton, slider { + border: 2px outset var(--button); + background: var(--button); +} + +slider input { + background: lightgray; + -webkit-appearance: none; + appearance: none; + height: 4px; + margin: 6 2; + width: 96px; +} + +slider input::-moz-range-thumb, +slider input::-webkit-slider-thumb { + background: gray; + -webkit-appearance: none; + appearance: none; + width: 8px; + height: 16px; + border: none; + border-radius: 0; +} + +select { + border: none; + background: none; +} + +button:active, +button.active, +fakebutton:active, +fakebutton.active { + border: 2px inset var(--button-active); + background: var(--button-active); +} + +fakebutton { display: flex; } +fakebutton img { margin: 2px; } + +controls { + display: flex; +} + +controls input { flex-grow: 1; } + +tablist { + display: flex; + flex-shrink: 0; +} + +metadata, canvas { + color: var(--monitor-text); + background: black; + border: 2px inset var(--secondary-border); +} + +metadata { + padding: 4px; + display: flex; + flex-direction: column; +} +metadata station { font-weight: bold; } + +panel { + display: flex; + flex-direction: column; + flex: 1; +} + +stations { + display: flex; + flex-direction: column; + overflow-y: auto; + border: 2px inset var(--primary-border); + margin: 2px; + flex-grow: 1; +} + +favorites, history, stats { + background: var(--monitor); + color: var(--monitor-text); + border: 2px inset var(--monitor-border); + flex-grow: 1; + display: flex; + flex-direction: column; + padding: 4px; + overflow-y: scroll; + min-height: 0; +} + +tabbox { + display: none; + flex-direction: column; + flex-grow: 1; +} +tabbox.active { display: flex; } + +station { + display: flex; + flex-direction: column; + cursor: pointer; + user-select: none; + padding: 4px; + margin: 2px; + border: 2px inset var(--button-active); + background: var(--button-active); +} + +station.active { + background: var(--button); + border: 2px outset var(--button); + color: black; +} + +station station-name { font-weight: bold; } +station track-meta { + font-size: 12px; +} + +history track-container { + display: flex; + flex-direction: column; + padding: 2px; +} + +history track-container track-station { font-weight: bold; } + +history track-container:nth-of-type(1) { + background-color: var(--monitor-text); + color: var(--monitor); +} + +canvas.vis, canvas.metadata { border-bottom: none; } + +.button-row { display: flex; } + +.fav-button img { + filter: contrast(0%); +} + +.fav-button.fav img { + filter: none; +} + +favorites .station-header, +history .station-header { + text-align: center; + margin: 4px 0; + text-decoration: underline; + margin-top: 12px; +} + +favorites .track, +history .track { + display: flex; + border-bottom: 1px dashed var(--monitor-text); + margin: 2px 0; +} + +favorites .track button, +history .track button { + background: none; + border: none; + cursor: pointer; + flex-shrink: 0; + flex-grow: 0; + margin: 2px; +} + +favorites .track a, +history .track a { + flex-grow: 1; +} + +@media only screen and (orientation: portrait) { + .container, .footer { + flex-direction: column; + width: 320px; + } + + .banner { flex-direction: column-reverse; } + .banner img, .player-container { + margin: auto; + } + .banner > img { flex-shrink: 0; } + .player-container { margin-bottom: -2px; } + + panel { + max-height: 480px; + } + + favorites, history { + height: 240px; + } +} + +@media only screen and (orientation: landscape) { + .container, .footer { + width: calc(100% - 64px); + height: calc(100% - 212px); + } + + .banner { + margin: auto; + min-width: 320px; + width: calc(100% - 52px); + max-width: 988px; + } + .banner > img { margin-top: 2px; } + + .player-container { + margin-top: 48px; + } +} \ No newline at end of file diff --git a/radio.js b/radio.js index d89002f..7bf3d94 100644 --- a/radio.js +++ b/radio.js @@ -1,310 +1,871 @@ -/* == wormTuner == */ -/* The Icecast Radio JS Tuner */ - -const nameJs = "wormTuner"; -const version = "0.3.1"; - -const statusJson = "/status-json.xsl"; - -// 'mimetype': 'icon-path' -const mimeIcons = { -// 'audio/aac':'/mime/aac.png', +const tuner_settings = { + tuner_name: "wormTuner", + icecast_status: "/stream/status-json.xsl", + update_interval: 0 // in milliseconds (ms). 0 means no update }; -const options = { - 'json-timer': 2500, // Time (in ms) for each stream update - 'visualizers': true, // whether to support visualizers - 'video-support': true, // whether to support video streams - 'character-overflow': 38, // character count needed to put in marquee. - 'replace-url': false, // fellow lazies please stand up - 'url-replacement': ['',''], - 'crossorigin': true +var tuner_mem = { + xhr: null }; -var ajax = new XMLHttpRequest(); -var sources = []; +// VISUALIZER VARS START +var visualizer = { + canvas: null, + mode: 1, + drawRequest: 0, + renderInterval: null, + startTime: new Date() +}; +const vis_modes = [ + "None", + "Bars", + "Scope", + "Multi-Scope", + "Spectrogram", + "Hyperdrive", + "Nixie Bars", + "Wire Meters", + "VU Bars", + "Stereo Diff" +]; +// VISUALIZER VARS END -var isFancy = true; -var castContainer; -var currentStation = ""; +var tuner = { + player: null, + playing: true, + volume: 100, + canvas: { + element: null, + drawRequest: 0 + }, + html_elements: { + title: null, + station: null + }, + analysers: null, + context: null, + splitter: null, + merger: null, + history_container: null, + station_container: null, + favorites_container: null +}; -var audioPlayer = document.createElement('video'); -var audioContext; -var audioAnalyser; -var visualizer; -var visMode = 0; +var station = null; -var videoMode = false; +var track_favorites = { +}; -// Timers -var volTimer; -var visTimer; +var track_history = [ +]; -function updateJSON() { - ajax.open('GET', statusJson, true); - ajax.send(); +// Updates tuner_mem.xhr +function updateXHR() { + tuner_mem.xhr.open('GET', tuner_settings.icecast_status, true); + tuner_mem.xhr.send(); } -function processListenURL(url) { - if (!options['replace-url'] || !url) return url; - // Add Date.now() to prevent browser caching. Cache-Control isn't reliable IME - return url.replace(options['url-replacement'][0], options['url-replacement'][1])+"?"+Date.now(); +// Fixes listen_url to return a proper proxied address. +function fixURL(url) { + return url.replace('http://example.com:8000/', '/stream/'); } -function setSource(source) { - if (!source) return; - currentStation = source.listenurl.substr(23); - window.location.hash = "#"+currentStation; - - if (options['video-support'] && source.server_type.substr(0, source.server_type.indexOf('/')) == 'video') { - visMode = -1; - } else if (visMode < 0) visMode = 0; - - audioPlayer.src = processListenURL(source.listenurl); - audioPlayer.type = source.server_type; - - if (isFancy) { - if (!audioContext || !audioAnalyser) { - audioContext = new AudioContext(); - audioAnalyser = createVisualizer(audioPlayer, audioContext); - } - setVisualizer(visualizer, audioAnalyser, visMode); - } else audioPlayer = document.querySelector("video"); - - audioPlayer.play(); - document.body.scrollTop = 0; - updateJSON(); +function startStation(new_station) { + station = new_station; + updateMetadata(null); // Avoid HUGE ROCK STATION playing Seasame Street theme text. + if (tuner.player != null) tuner.player.stop(); + // Start player using IcecastMetadataPlayer for the metadata. + // Use ?+date for avoiding problems with cache. + tuner.player = new IcecastMetadataPlayer(new_station.listen_url+"?"+Date.now(), { + onMetadata: (metadata) => {updateMetadata(metadata.StreamTitle);}, + metadataTypes: ["icy"] + }); + + // Start new context with analysers + // Check if channels is only 1. If not, we assume stereo. + tuner.context = new AudioContext(); + tuner.analysers = createVisualizer(tuner.player.audioElement, tuner.context, new_station.channels != 1); + setVisualizer(tuner.analysers, visualizer.mode); + + // Properly set volume value. + tuner.player.audioElement.volume = tuner.volume/100; + play(); } -function handleVolume(event) { - event.preventDefault(); - var volMeter = document.querySelector('.vol'); - volMeter.classList.remove('hidden'); - var volInc = 0.05; - - if (event.deltaY < 0) { - if (audioPlayer.volume+volInc > 1) audioPlayer.volume=1; - else audioPlayer.volume+=volInc; - } - else if (event.deltaY > 0) { - if (audioPlayer.volume-volInc < 0) audioPlayer.volume=0; - else audioPlayer.volume-=volInc; - } - - volMeter.innerText = "VOL ["+'='.repeat(((audioPlayer.volume / 1) * 20)).padEnd(20, ' ') + "] "+(Math.round(audioPlayer.volume * 100)+'').padStart(3, ' ')+"%"; - - window.clearTimeout(volTimer) - volTimer = setTimeout(function(){ - volMeter.classList.add('hidden'); - }, 5000); +function searchStations(query) { + var stations = document.querySelectorAll("stations > station"); + for(let f = 0; f < stations.length; f++) { + var station = stations[f]; + var name = station.querySelector(':scope > station-name').innerText; + var description = station.querySelector(':scope > description').innerText; + var meta = station.querySelector(':scope > track-meta').innerText; + + if (name.toLowerCase().includes(query.toLowerCase()) || description.toLowerCase().includes(query.toLowerCase()) || meta.toLowerCase().includes(query.toLowerCase())) + station.style.display = ''; + else + station.style.display = 'none'; + } } -function switchMode() { - if (videoMode) return; - if (!isFancy) return; - if (visMode == modes.length - 1) // - 1 if we count empty. - visMode = 0; - else visMode++; - - var visCounter = document.querySelector('.vismode'); - visCounter.classList.remove('hidden'); - - visCounter.innerText = modes[visMode]+' '+visMode+'/'+(modes.length - 1); - - setVisualizer(visualizer, audioAnalyser, visMode); - - window.clearTimeout(visTimer) - visTimer = setTimeout(function(){ - visCounter.classList.add('hidden'); - }, 5000); - - audioPlayer.play(); +function play() { + tuner.player.play(); + document.querySelector('.play').classList.add('active'); } -function newEntry(source, i) { - var entryContainer = document.createElement('div'); - entryContainer.classList.add('station'); - - entryContainer.name = source.listenurl.substr(23); - - var titlebar = document.createElement('div'), - infoBar = document.createElement('div'), - descBox = document.createElement('div'); - - titlebar.classList.add('titlebar'); - infoBar.classList.add('infobar'); - descBox.classList.add('descbox'); - - var titleTxt = document.createElement('b'), - listenerBox = document.createElement('div'); - - titleTxt.innerText = source.server_name; - titlebar.appendChild(titleTxt); - - var stationLineHeight = 24; - var grower = document.createElement('div'); - grower.style.flexGrow = "1"; - titlebar.appendChild(grower); - - var listenerImg = document.createElement('img'), - listenerTxt = document.createElement('a'); - - - listenerImg.src = "users.png"; - listenerImg.width = listenerImg.height = stationLineHeight; - listenerTxt.innerText = source.listeners; - - listenerBox.title = source.listeners+" listener"+(source.listeners != 1 ? 's' : '')+" (peak "+source.listener_peak+")"; - listenerBox.appendChild(listenerImg); - listenerBox.appendChild(listenerTxt); - titlebar.appendChild(listenerBox); - - entryContainer.appendChild(titlebar); - - entryContainer.appendChild(document.createElement('hr')); - - var bitrateTxt = document.createElement('a'), - urlTxt = document.createElement('a'), - urlImg = document.createElement('img'), - dirTxt = document.createElement('a'), - dirImg = document.createElement('img'); - - bitrateTxt.classList.add('bitrate'); - listenerBox.classList.add('listeners'); - - if (mimeIcons[source.server_type]) { - var formatImg = document.createElement('img'); - formatImg.classList.add('format'); - formatImg.height = stationLineHeight; - formatImg.alt = formatImg.title = source.server_type.substr(source.server_type.indexOf('/') + 1); - formatImg.src = mimeIcons[source.server_type]; - infoBar.appendChild(formatImg); - } else { - var formatTxt = document.createElement('a'); - formatTxt.classList.add('format'); - formatTxt.innerText = formatTxt.title = source.server_type.substr(source.server_type.indexOf('/') + 1).toUpperCase(); - infoBar.appendChild(formatTxt); - } - - var btr = source.bitrate; - if (!source.bitrate && source.audio_bitrate) btr = source.audio_bitrate / 1000; - else if (!source.audio_bitrate && !source.bitrate) btr = "???"; - bitrateTxt.innerHTML = btr + "kbps"; - infoBar.appendChild(bitrateTxt); - - if (source.server_url) { - urlTxt.href = source.server_url; - urlTxt.target = "_blank"; - urlImg.src = "url.png"; - urlImg.height = stationLineHeight; - urlTxt.appendChild(urlImg); - infoBar.appendChild(urlTxt); - } - - dirTxt.href = processListenURL(source.listenurl); - dirTxt.target = "_blank"; - dirImg.src = "dir.png"; - dirImg.height = stationLineHeight; - dirTxt.appendChild(dirImg); - infoBar.appendChild(dirTxt); - - entryContainer.appendChild(infoBar); - - var descTxt = document.createElement('p'); - descTxt.innerHTML = source.server_description + " (Genre: "+source.genre+")"; - descBox.appendChild(descTxt); - - entryContainer.appendChild(descBox); - - if (source.listenurl.substr(23) == currentStation) entryContainer.classList.add("selected"); - entryContainer.onclick = function() { - setSource(sources[i]); - this.classList.add("selected"); - } - - return entryContainer; +function stop() { + tuner.player.stop(); + document.querySelector('.play').classList.remove('active'); + station = null; + updateMetadata(''); } -function setMaybeOverflow(elem, txt) { - if (!txt) return; - var limit = options['character-overflow']; - if (elem.innerText == txt) return; - if (txt.length > limit) - elem.innerHTML = ""+txt+""; - else if (txt.length <= limit) - elem.innerHTML = txt; - +function updateMetadata(track) { + var line_data = [ + { + 'index': 0, + 'timer': new Date() + } + ]; + var load_str = [ // Fuck off. + '==-'.padStart(0, ' '), + '--==-'.padStart(4, ' '), + '--==-'.padStart(8, ' '), + '--==-'.padStart(12, ' '), + '--==-'.padStart(16, ' '), + '--==-'.padStart(20, ' '), + '--==-'.padStart(24, ' '), + '--==-'.padStart(28, ' '), + '--==-'.padStart(32, ' '), + '-=='.padStart(36, ' '), + '-==--'.padStart(32, ' '), + '-==--'.padStart(28, ' '), + '-==--'.padStart(24, ' '), + '-==--'.padStart(20, ' '), + '-==--'.padStart(16, ' '), + '-==--'.padStart(12, ' '), + '-==--'.padStart(8, ' '), + '-==--'.padStart(4, ' ') + ]; + var max_chars = 36; + function marqueeify(line, x, y, e) { + if (!line_data[e]) line_data[e] = { 'index': 0,'timer': new Date() }; + if (line != null && line.length >= max_chars) { + var txt = line.concat(' ').concat(line); + if (line_data[e].index - 4 > line.length) {line_data[e].index = 0;} + ctx.fillText(txt.substr(line_data[e].index,max_chars), x,y); + var now = new Date(); + if (now >= new Date(line_data[e].timer.getTime() + 250)) { + line_data[e].timer = now; + line_data[e].index++; + } + } else if (line == null || line.length == 0) { + if (line_data[e].index >= load_str.length) line_data[e].index = 0; + ctx.fillText(load_str[line_data[e].index], x, y); + var now = new Date(); + if (now >= new Date(line_data[e].timer.getTime() + 50)) { + line_data[e].timer = now; + line_data[e].index++; + } + } else ctx.fillText(line,x,y); + } + + if (track != null) { + addHistory(station, track); + updateFavStatus(isFavorite(station.listen_url, track)); + } + + station.title = track; + if (tuner.canvas.element) { + var ctx = tuner.canvas.element.getContext("2d",{antialias: false,alpha: false}); + + var WIDTH = tuner.canvas.element.width; + var HEIGHT = tuner.canvas.element.height; + + window.cancelAnimationFrame(tuner.canvas.drawRequest); + function drawMeta() { + tuner.canvas.drawRequest = window.requestAnimationFrame(drawMeta); + + ctx.clearRect(0,0,WIDTH,HEIGHT); + if (station != null) { + let quality_text = station.bitrate ? station.bitrate+'kbps' : station.quality; + + ctx.font = "16px hack,monospace"; + ctx.fillStyle = "#FFF"; + ctx.textAlign = "justify"; + + ctx.font = "bold 16px hack,monospace"; + marqueeify(station.name,6,20,0); + ctx.font = "16px hack,monospace"; + marqueeify(track,8,36,1); + } + } + drawMeta(); + } } -function updateStreamInfo(source) { - var stationName = document.querySelector(".textainer .station"), - title = document.querySelector(".textainer .track"), - genre = document.querySelector(".textainer .genre"); - if (source) { - setMaybeOverflow(stationName,source.server_name); - if (source.artist) - setMaybeOverflow(title,source.artist+" - "+source.title); - else - setMaybeOverflow(title,source.title); - setMaybeOverflow(genre,source.genre); - } else stationName.innerText = title.innerText = genre.innerText = ""; +function setTab(i) { + var tabbuttons = document.querySelectorAll('tablist button'); + var tabboxes = document.querySelectorAll('tabbox'); + for (let t = 0; t < tabboxes.length; t++) { + var tabbox = tabboxes[t]; + var tabbutton = tabbuttons[t]; + if (t != i) { + tabbox.classList.remove('active'); + tabbutton.classList.remove('active'); + } else { + tabbox.classList.add('active'); + tabbutton.classList.add('active'); + } + } } -ajax.onload = function() { - // Clear Container for new stuff. - while(castContainer.childElementCount > 0) { - castContainer.removeChild(castContainer.lastChild); - } - - if (ajax.status == 200) { // OK - if (ajax.response.icestats.source) { - if (ajax.response.icestats.source.length > 0) sources = ajax.response.icestats.source; - else sources = [ ajax.response.icestats.source ]; - } else sources = []; - var index = -1; - for (let i = 0; i < sources.length; i++) { - castContainer.appendChild(newEntry(sources[i], i)); - if (sources[i].listenurl.substr(23) == currentStation) index = i; - } - if (index != -1) updateStreamInfo(sources[index]); - if (audioPlayer.paused && currentStation.length > 0) - setSource(sources[index]); - } +function addHistory(station, track = null) { + if (station == null) return; + var track = track??station.title; + + if (track_history.length > 0 && (track_history[0].station_id == station.listen_url && track_history[0].title == track)) return; + + console.log(station.name.concat(' > ').concat(track)); + var trackContainer = document.createElement('div'), + favButton = document.createElement('button'), + favImg = document.createElement('img'), + trackTitle = document.createElement('a'); + + let station_url = station.listen_url; + let station_track = track; + + favButton.classList.add('fav-button'); + + favButton.onclick = function() { + let status = favoriteTrack(station_url, station_track); + updateFavStatus(status) + favButton.classList.toggle('fav', status); + }; + favImg.src = 'heart.png'; + if (isFavorite(station_url, station_track)) favButton.classList.add('fav'); + + trackTitle.innerText = track; + + favButton.appendChild(favImg); + + trackContainer.appendChild(trackTitle); + trackContainer.appendChild(favButton); + trackContainer.classList.add('track'); + + if (track_history[0] == null || track_history[0].station_id != station.listen_url) { + tuner.history_container.insertBefore(trackContainer, tuner.history_container.childNodes[0]); + + var statCont = document.createElement('div'), + statText = document.createElement('b'); + + statText.innerText = station.name; + statCont.appendChild(statText); + statCont.classList.add('station-header'); + + tuner.history_container.insertBefore(statCont, tuner.history_container.childNodes[0]); + } else { + tuner.history_container.insertBefore(trackContainer, tuner.history_container.childNodes[1]); + } + + var history_entry = { + station_id: station.listen_url, + title: track + }; + + track_history.unshift(history_entry); +} + +function isFavorite(track_station, title) { + return track_favorites[track_station] != null && track_favorites[track_station].includes(title); +} + +function favoriteCurrentTrack() { + updateFavStatus(favoriteTrack(station.listen_url, station.title)); +} + +function favoriteTrack(track_station, track) { + if (track_favorites[track_station] == null) + track_favorites[track_station] = []; + + var result = false; + if (track_favorites[track_station].includes(track)) { + track_favorites[track_station].splice(track_favorites[track_station].indexOf(track), 1); + } else { + track_favorites[track_station].unshift(track); + result = true; + } + const d = new Date(); + d.setTime(d.getTime() + (99983090*24*60*60*1000)); + let expires = "expires=" + d.toUTCString(); + document.cookie = "favorites="+JSON.stringify(track_favorites)+";"+expires+";SameSite=None;secure=1;"; + + populateFavorites(); + return result; +} + +function updateFavStatus(isFav = false) { + var favButton = document.querySelector('button.fav-button'); + if (isFav) { + favButton.classList.add('fav'); + } else { + favButton.classList.remove('fav'); + } +} + +function exportFavorites() { + var dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(track_favorites, null, 2)); + var downloadAnchorNode = document.createElement('a'); + downloadAnchorNode.setAttribute("href", dataStr); + downloadAnchorNode.setAttribute("download", "worlio_station_favorites.json"); + document.body.appendChild(downloadAnchorNode); + downloadAnchorNode.click(); + downloadAnchorNode.remove(); +} + +function populateFavorites() { + while (tuner.favorites_container.hasChildNodes()) { + tuner.favorites_container.lastElementChild.remove(); + } + for (var key in track_favorites) { + var statCont = document.createElement('div'), + statText = document.createElement('b'); + + statText.innerText = key; + statCont.appendChild(statText); + statCont.classList.add('station-header'); + + tuner.favorites_container.append(statCont); + + var value = track_favorites[key]; + for (let t = 0; t < value.length; t++) { + var track = value[t]; + + var trackContainer = document.createElement('div'), + favButton = document.createElement('button'), + favImg = document.createElement('img'), + trackTitle = document.createElement('a'); + + let station_url = key; + let station_track = track; + + favButton.classList.add('fav-button'); + + favButton.onclick = function() { + let status = favoriteTrack(station_url, station_track); + updateFavStatus(status) + favButton.classList.toggle('fav', status); + }; + favImg.src = 'heart.png'; + if (isFavorite(station_url, station_track)) favButton.classList.add('fav'); + + trackTitle.innerText = track; + + favButton.appendChild(favImg); + + trackContainer.appendChild(trackTitle); + trackContainer.appendChild(favButton); + trackContainer.classList.add('track'); + + tuner.favorites_container.append(trackContainer); + } + } +} + +function searchFavorites(query) { + var tracks = document.querySelectorAll("favorites > .track"); + for(let f = 0; f < tracks.length; f++) { + var track = tracks[f]; + var title = track.querySelector(':scope > a'); + + if (!title.innerText.toLowerCase().includes(query.toLowerCase())) + track.style.display = 'none'; + else + track.style.display = ''; + } +} + +function volChange(e) { + tuner.player.audioElement.volume=e.value/100; + e.parentNode.title = e.value+'%'; + tuner.volume = e.value; +} + +function setVisMode(i) { + if (i > vis_modes.length) visualizer.mode = 1; + else visualizer.mode = i; + setVisualizer(tuner.analysers, visualizer.mode); +} + +function createVisualizer(player, context, stereo = true) { + var audioSrc = context.createMediaElementSource(player); + if (stereo) { + tuner.splitter = context.createChannelSplitter(2); + audioSrc.connect(tuner.splitter); + tuner.merger = context.createChannelMerger(2); + + var analysers = { + left: tuner.context.createAnalyser(), + right: tuner.context.createAnalyser() + }; + + tuner.splitter.connect(analysers.left, 0); + tuner.splitter.connect(analysers.right, 1); + + analysers.left.connect(tuner.merger, 0, 0); + analysers.right.connect(tuner.merger, 0, 1); + + tuner.merger.connect(context.destination); + } else { + var analy = context.createAnalyser(); + audioSrc.connect(analy); + analy.connect(context.destination); + var analysers = { + left: analy, + right: analy + }; + } + setVisualizer(analysers, 0); + + return analysers; +}; + +function setVisualizer(analysers, mode = 0) { + var canvas = visualizer.canvas; + if (!analysers) return; + clearInterval(visualizer.renderInverval); + var ctx = canvas.getContext("2d",{antialias: false,alpha: false}); + + var HEIGHT; + var WIDTH = canvas.width; + + // Set the "defaults" + analysers.left.smoothingTimeConstant = 0.8; + analysers.left.fftSize = 2048; + + analysers.right.smoothingTimeConstant = 0.8; + analysers.right.fftSize = 2048; + + var analyser = analysers.right; + + ctx.clearRect(0, 0, WIDTH, HEIGHT); + window.cancelAnimationFrame(visualizer.drawRequest); + if (mode == 0 || mode > vis_modes.length) { + HEIGHT = canvas.height = 96; + function drawVis() { + ctx.clearRect(0, 0, WIDTH, HEIGHT); + visualizer.drawRequest = window.requestAnimationFrame(drawVis); + } + drawVis(); + } else if (mode == 1) { // BAR + HEIGHT = canvas.height = 96; + analysers.right.fftSize = analysers.left.fftSize = 256; + var bufferLength = analyser.frequencyBinCount; + var dataArrayL = new Uint8Array(bufferLength) + var dataArrayR = new Uint8Array(bufferLength) + + function drawVis() { + ctx.clearRect(0, 0, WIDTH, HEIGHT); + visualizer.drawRequest = window.requestAnimationFrame(drawVis); + + analysers.left.getByteFrequencyData(dataArrayL); + analysers.right.getByteFrequencyData(dataArrayR); + ctx.fillStyle = "#FFF"; + + var x = 0; + + ctx.beginPath(); + for (var i = 0; i < bufferLength; i++) { + var value = (dataArrayL[i] + dataArrayR[i]) / 2; + barHeight = (value / 256) * HEIGHT; + ctx.fillRect(x, HEIGHT - barHeight, WIDTH / bufferLength, barHeight); + x += (WIDTH / bufferLength) + 1; + } + ctx.closePath(); + } + drawVis(); + } else if (mode == 2) { // SCOPE + HEIGHT = canvas.height = 96; + analysers.left.smoothingTimeConstant = analysers.right.smoothingTimeConstant = 0; + var dataArrayL = new Uint8Array(analysers.left.fftSize) + var dataArrayR = new Uint8Array(analysers.right.fftSize) + + function drawVis() { + ctx.clearRect(0, 0, WIDTH, HEIGHT); + visualizer.drawRequest = window.requestAnimationFrame(drawVis); + + analysers.left.getByteTimeDomainData(dataArrayL); + analysers.right.getByteTimeDomainData(dataArrayR); + + const step = WIDTH / dataArrayL.length; + + ctx.strokeStyle = "#0F0"; + + ctx.beginPath(); + for (let i = 0; i < dataArrayL.length; i += 2) { + var percent = (dataArrayL[i] + dataArrayR[i]) / 512; + ctx.lineTo(i * step, HEIGHT * percent); + } + ctx.stroke(); + } + drawVis(); + } else if (mode == 3) { // MULTI SCOPE + HEIGHT = canvas.height = 96; + analysers.left.smoothingTimeConstant = analysers.right.smoothingTimeConstant = 0; + var dataArrayL = new Uint8Array(analysers.left.fftSize) + var dataArrayR = new Uint8Array(analysers.right.fftSize) + + function drawVis() { + ctx.clearRect(0, 0, WIDTH, HEIGHT); + visualizer.drawRequest = window.requestAnimationFrame(drawVis); + + analysers.left.getByteTimeDomainData(dataArrayL); + analysers.right.getByteTimeDomainData(dataArrayR); + + const step = WIDTH / dataArrayL.length; + + ctx.strokeStyle = "#0F0"; + + ctx.beginPath(); + for (let i = 0; i < dataArrayL.length; i += 2) { + var percentL = dataArrayL[i] / 512; + ctx.lineTo(i * step, HEIGHT * percentL); + } + ctx.stroke(); + ctx.beginPath(); + for (let i = 0; i < dataArrayR.length; i += 2) { + var percentR = dataArrayR[i] / 512; + ctx.lineTo(i * step, (HEIGHT * percentR) + HEIGHT/2); + } + ctx.stroke(); + } + drawVis(); + } else if (mode == 4) { // SPECTROGRAM + HEIGHT = canvas.height = 128; + analysers.left.smoothingTimeConstant = analysers.right.smoothingTimeConstant = 0; + + var tempCanvas = document.createElement("canvas"), + tempCtx = tempCanvas.getContext("2d"); + tempCanvas.width = WIDTH; + tempCanvas.height = HEIGHT; + + analysers.left.fftSize = analysers.right.fftSize = Math.pow(2, Math.ceil(Math.log(WIDTH)/Math.log(2))); + + var bufferLength = analyser.frequencyBinCount; + var dataArrayL = new Uint8Array(bufferLength); + var dataArrayR = new Uint8Array(bufferLength); + + var startTime = new Date(); + function drawVis() { + visualizer.drawRequest = window.requestAnimationFrame(drawVis); + + analysers.left.getByteFrequencyData(dataArrayL); + analysers.right.getByteFrequencyData(dataArrayR); + var barHeight = WIDTH/analysers.left.fftSize; + + var now = new Date(); + if (now < new Date(startTime.getTime() + 20)) { return; } + startTime = now; + + tempCtx.drawImage(ctx.canvas, 0, 0, WIDTH, HEIGHT); + + for (var i = 0; i < bufferLength; i++) { + var value = (dataArrayL[i]/2)+(dataArrayR[i]/2); + ctx.fillStyle = 'rgb('+((value > 190) ? 255 : value)+', '+ ((value > 220) ? 255 : value-100) +', 0)'; + ctx.fillRect(WIDTH - 1, HEIGHT - i*barHeight, 1, barHeight); + } + ctx.translate(-1, 0); + ctx.drawImage(tempCanvas, 0, 0, WIDTH, HEIGHT, 0, 0, WIDTH, HEIGHT); + ctx.setTransform(1, 0, 0, 1, 0, 0); + } + drawVis(); + } else if (mode == 5) { // LASER RAIN + HEIGHT = canvas.height = 64; + analysers.left.smoothingTimeConstant = analysers.right.smoothingTimeConstant = 0; + var bufferLength = analysers.left.frequencyBinCount; + var dataArrayL = new Uint8Array(bufferLength); + var dataArrayR = new Uint8Array(bufferLength); + + //ctx.clearRect(0,0,WIDTH,HEIGHT); + function drawVis() { + visualizer.drawRequest = window.requestAnimationFrame(drawVis); + + analysers.left.getByteFrequencyData(dataArrayL); + analysers.right.getByteFrequencyData(dataArrayR); + + ctx.clearRect(0,0,WIDTH,HEIGHT); + for (var i = 0; i < bufferLength; i++) { + if (dataArrayL[i] != 0) { + ctx.fillStyle = 'rgb('+dataArrayL[i]/4+','+dataArrayL[i]/2+','+dataArrayL[i]+')'; + ctx.fillRect((WIDTH / 2)-(i+1), 0, 1, HEIGHT); + } + + if (dataArrayR[i] != 0) { + ctx.fillStyle = 'rgb('+dataArrayR[i]/4+','+dataArrayR[i]/2+','+dataArrayR[i]+')'; + ctx.fillRect((WIDTH / 2)+i, 0, 1, HEIGHT); + } + } + } + drawVis(); + } else if (mode == 6) { // HEATBARS + HEIGHT = canvas.height = 96; + analysers.left.fftSize = 256; + analysers.right.fftSize = 256; + var bufferLength = analyser.frequencyBinCount; + var dataArray = new Uint8Array(bufferLength); + var dataArrayL = new Uint8Array(bufferLength); + var dataArrayR = new Uint8Array(bufferLength); + + function drawVis() { + ctx.clearRect(0, 0, WIDTH, HEIGHT); + visualizer.drawRequest = window.requestAnimationFrame(drawVis); + + analyser.getByteFrequencyData(dataArray); + analysers.left.getByteFrequencyData(dataArrayL); + analysers.right.getByteFrequencyData(dataArrayR); + + for (let i = 0; i < bufferLength; i++) { + ctx.beginPath(); + ctx.fillStyle = 'rgb(255,'+dataArray[i]+',0)'; + ctx.ellipse((i*3), ((HEIGHT)-(dataArrayL[i]-dataArrayR[i])/4)/2, 1, dataArray[i] / 5, 0, 0, 2 * Math.PI); + ctx.fill(); + ctx.closePath(); + } + } + drawVis(); + } else if (mode == 7) { // VU METERS + HEIGHT = canvas.height = 96; + analysers.left.fftSize = 256; + analysers.right.fftSize = 256; + var bufferLength = analyser.frequencyBinCount; + var dataArrayL = new Uint8Array(bufferLength); + var dataArrayR = new Uint8Array(bufferLength); + + function drawVis() { + ctx.clearRect(0, 0, WIDTH, HEIGHT); + visualizer.drawRequest = window.requestAnimationFrame(drawVis); + + analysers.left.getByteFrequencyData(dataArrayL); + analysers.right.getByteFrequencyData(dataArrayR); + + var dataL = dataArrayL.reduce(function(a,b){ return a+b; }); + var dataR = dataArrayR.reduce(function(a,b){ return a+b; }); + + function drawChannel(data, x) { + ctx.beginPath(); + ctx.arc(x, HEIGHT, HEIGHT-24, 1*Math.PI, 0, false); + ctx.strokeStyle = "white"; + ctx.stroke(); + ctx.closePath(); + + ctx.beginPath(); + + ctx.arc(x, HEIGHT, HEIGHT-32, 1.3*Math.PI, (1.3*Math.PI)+(data/12000), false); + ctx.strokeStyle = "transparent"; + ctx.stroke(); + + ctx.lineTo(x, HEIGHT); + ctx.strokeStyle = "red"; + ctx.stroke(); + ctx.closePath(); + + } + + drawChannel(dataL, (WIDTH/4)); + drawChannel(dataR, (WIDTH/4)*3); + } + drawVis(); + } else if (mode == 8) { // VU BARS + tuner.analysers.left.smoothingTimeConstant = tuner.analysers.right.smoothingTimeConstant = 0.2; + HEIGHT = canvas.height = 32; + analysers.left.fftSize = 256; + analysers.right.fftSize = 256; + var dataArrayL = new Uint8Array(analysers.left.frequencyBinCount); + var dataArrayR = new Uint8Array(analysers.right.frequencyBinCount); + + function drawVis() { + ctx.clearRect(0, 0, WIDTH, HEIGHT); + visualizer.drawRequest = window.requestAnimationFrame(drawVis); + + analysers.left.getByteFrequencyData(dataArrayL); + analysers.right.getByteFrequencyData(dataArrayR); + + var dataL = dataArrayL.reduce(function(a,b){ return a+b; }); + var dataR = dataArrayR.reduce(function(a,b){ return a+b; }); + + ctx.beginPath(); + for (let l = 0; l < Math.min(Math.max(Math.round(dataL / 1270), 0), 31); l++) { + if (l > 16) ctx.fillStyle = "rgb(255,0,0)"; + else if (l > 10) ctx.fillStyle = "rgb(255,255,0)"; + else ctx.fillStyle = "rgb(0,255,0)"; + ctx.fillRect(2+(l*14), (HEIGHT/2)-6, 12, 4); + } + ctx.closePath(); + + ctx.beginPath(); + for (let r = 0; r < Math.min(Math.max(Math.round(dataR / 1270), 0), 31); r++) { + if (r > 16) ctx.fillStyle = "rgb(255,0,0)"; + else if (r > 10) ctx.fillStyle = "rgb(255,255,0)"; + else ctx.fillStyle = "rgb(0,255,0)"; + ctx.fillRect(2+(r*14), (HEIGHT/2)+2, 12, 4); + } + ctx.closePath(); + } + drawVis(); + } else if (mode == 9) { // STEREO DIFFERENCE + HEIGHT = canvas.height = 128; + analysers.left.smoothingTimeConstant = analysers.right.smoothingTimeConstant = 0; + + var tempCanvas = document.createElement("canvas"), + tempCtx = tempCanvas.getContext("2d"); + tempCanvas.width = WIDTH; + tempCanvas.height = HEIGHT; + + analysers.left.fftSize = analysers.right.fftSize = Math.pow(2, Math.ceil(Math.log(WIDTH)/Math.log(2))); + + var bufferLength = analyser.frequencyBinCount; + var dataArrayL = new Uint8Array(bufferLength); + var dataArrayR = new Uint8Array(bufferLength); + + var startTime = new Date(); + function drawVis() { + visualizer.drawRequest = window.requestAnimationFrame(drawVis); + + analysers.left.getByteFrequencyData(dataArrayL); + analysers.right.getByteFrequencyData(dataArrayR); + var barHeight = WIDTH/analysers.left.fftSize; + + var now = new Date(); + if (now < new Date(startTime.getTime() + 20)) { return; } + startTime = now; + + tempCtx.drawImage(ctx.canvas, 0, 0, WIDTH, HEIGHT); + + for (var i = 0; i < bufferLength; i++) { + var value = dataArrayL[i] - dataArrayR[i]; + // LEFT + if (value > 0) ctx.fillStyle = 'rgb('+value+', 0, 0)'; + // RIGHT + else if (value < 0) ctx.fillStyle = 'rgb(0, 0, '+((value*-1)*4)+')'; + else ctx.fillStyle = 'rgb(0, 0, 0)'; + ctx.fillRect(WIDTH - 1, HEIGHT - i*barHeight, 1, barHeight); + } + ctx.translate(-1, 0); + ctx.drawImage(tempCanvas, 0, 0, WIDTH, HEIGHT, 0, 0, WIDTH, HEIGHT); + ctx.setTransform(1, 0, 0, 1, 0, 0); + } + drawVis(); + } } window.addEventListener('DOMContentLoaded', (event) => { - castContainer = document.querySelector(".broadcasts"); - ajax.responseType = 'json'; - updateJSON(); - - visualizer = document.querySelector('canvas.vis'); - isFancy = isFancy && (visualizer != null); - - // Detect Visualizer and initialize events - if (isFancy) { - visualizer.addEventListener('click', (event) => { - switchMode(); - }); - visualizer.addEventListener('wheel', handleVolume); - } - - // #my-stream.ogg -> listenurl: /my-stream.ogg - if (window.location.hash.length > 2) currentStation = window.location.hash.substr(1); - - // Handle name and versioning - var _nameElem = document.querySelector('.about .name'); - if (_nameElem) _nameElem.innerText = nameJs; - var _versElem = document.querySelector('.about .vers'); - if (_versElem) _versElem.innerText = 'v'+version; - - setInterval(function() { - updateJSON(); - }, options['json-timer']); - - if (options['crossorigin']) audioPlayer.setAttribute('crossorigin','anonymous'); - audioPlayer.onended = function () { - currentStation = ""; - console.log("Stream over"); - updateStreamInfo(null); - }; + visualizer.canvas = document.querySelector('canvas.vis'); + + tuner.canvas.element = document.querySelector('canvas.metadata'); + tuner.history_container = document.querySelector('history'); + tuner.station_container = document.querySelector('stations'); + tuner.favorites_container = document.querySelector('favorites'); + + var fav_cookie = getCookie('favorites'); + if (fav_cookie != null && fav_cookie.length > 0) track_favorites = JSON.parse(getCookie('favorites')); + populateFavorites(); + + var visModeChanger = document.querySelector('select[name="visModes"]'); + for (let v = 0; v < vis_modes.length; v++) { + var option = document.createElement('option'); + option.innerText = vis_modes[v]; + option.value = v; + visModeChanger.appendChild(option); + } + visModeChanger.selectedIndex = visualizer.mode; + + tuner_mem.xhr = new XMLHttpRequest(); + tuner_mem.xhr.responseType = 'json'; + tuner_mem.xhr.onload = function() { + var xhr = tuner_mem.xhr; + + while (tuner.station_container.hasChildNodes()) { + tuner.station_container.lastElementChild.remove(); + } + + if (xhr.response) { + var stations = xhr.response.icestats.source; + for(let s = 0; s < stations.length; s++) { + let st = stations[s]; + + var stationContainer = document.createElement('station'), + stationName = document.createElement('station-name'), + stationURLs = document.createElement('station-urls'), + stationDesc = document.createElement('description'), + stationMeta = document.createElement('track-meta'); + + stationName.innerText = st.server_name; + stationDesc.innerText = st.server_description; + + let urls = []; + urls.push('Direct'); + if (st.server_url) urls.push('Site'); + stationURLs.innerHTML = urls.join(' - '); + + let meta = []; + meta.push(st.server_type.substr(st.server_type.indexOf('/')+1).toUpperCase()); + if (st.bitrate) meta.push(st.bitrate+"kbps"); + else if (st.quality) meta.push(st.quality); + else meta.push('???kbps'); + if (st.channels) meta.push(st.channels+'ch'); + else meta.push('?ch'); + if (st.samplerate) meta.push((st.samplerate / 1000)+'hz'); + else meta.push('????hz'); + meta.push(st.genre); + meta.push(st.listeners+" Listening"); + stationMeta.innerText = meta.join(' - '); + + stationContainer.appendChild(stationName); + stationContainer.appendChild(stationURLs); + stationContainer.appendChild(stationDesc); + stationContainer.appendChild(stationMeta); + + let nstat = { + name: st.server_name, + listen_url: fixURL(st.listenurl), + channels: st.channels, + bitrate: st.bitrate, + quality: st.quality, + listeners: st.listeners, + genre: st.genre, + samplerate: st.samplerate, + description: st.server_description, + type: st.server_type, + site_url: st.server_url + }; + + if (station != null && fixURL(st.listenurl) == station.listen_url) { + stationContainer.classList.add('active'); + } + + stationContainer.addEventListener('click', (e) => { + startStation(nstat); + var stationboxes = document.querySelectorAll('stations station'); + for (let s = 0; s < stationboxes.length; s++) { + stationboxes[s].classList.remove('active'); + } + e.currentTarget.classList.add('active'); + }); + + tuner.station_container.appendChild(stationContainer); + } + } + } + + updateXHR(); }); + +function getCookie(cName) { + const name = cName + "="; + const cDecoded = decodeURIComponent(document.cookie); //to be careful + const cArr = cDecoded .split('; '); + let res; + cArr.forEach(val => { + if (val.indexOf(name) === 0) res = val.substring(name.length); + }) + return res; +} \ No newline at end of file diff --git a/stop.png b/stop.png new file mode 100644 index 0000000..848cf6d Binary files /dev/null and b/stop.png differ diff --git a/vis.png b/vis.png new file mode 100644 index 0000000..043f760 Binary files /dev/null and b/vis.png differ diff --git a/vol.png b/vol.png new file mode 100644 index 0000000..92edd89 Binary files /dev/null and b/vol.png differ