From 08e6a17aa1b70703460eb2cae0326ebc3ec97794 Mon Sep 17 00:00:00 2001 From: "Alexander \"Arav\" Andreev" Date: Mon, 19 Sep 2022 22:06:06 +0400 Subject: [PATCH] Radio and Files services are deleted. --- files/config.example.js | 3 - files/index.js | 117 ------------- files/static/assets/css/main.css | 170 ------------------- files/static/assets/js/main.js | 56 ------ files/static/robots.txt | 2 - files/views/index.pug | 44 ----- radio/config.example.js | 2 - radio/index.js | 79 --------- radio/static/assets/css/main.css | 154 ----------------- radio/static/assets/files/radio.arav.top.m3u | 9 - radio/static/assets/js/main.js | 48 ------ radio/static/robots.txt | 3 - radio/views/index.pug | 60 ------- 13 files changed, 747 deletions(-) delete mode 100644 files/config.example.js delete mode 100644 files/index.js delete mode 100644 files/static/assets/css/main.css delete mode 100644 files/static/assets/js/main.js delete mode 100644 files/static/robots.txt delete mode 100644 files/views/index.pug delete mode 100644 radio/config.example.js delete mode 100644 radio/index.js delete mode 100644 radio/static/assets/css/main.css delete mode 100644 radio/static/assets/files/radio.arav.top.m3u delete mode 100644 radio/static/assets/js/main.js delete mode 100644 radio/static/robots.txt delete mode 100644 radio/views/index.pug diff --git a/files/config.example.js b/files/config.example.js deleted file mode 100644 index 1a61c58..0000000 --- a/files/config.example.js +++ /dev/null @@ -1,3 +0,0 @@ -exports.host = "0.0.0.0"; -exports.port = 32202; -exports.share_path = "/srv/ftp"; \ No newline at end of file diff --git a/files/index.js b/files/index.js deleted file mode 100644 index 1c65332..0000000 --- a/files/index.js +++ /dev/null @@ -1,117 +0,0 @@ -const fs = require("fs"); -const path = require("path"); -const { ENOENT, ECONNRESET } = require("constants"); - -const Koa = require("koa"); -const koaPug = require("koa-pug"); -const koaRouter = require("koa-router"); - -const config = require("./config"); -const util = require("../shared/util"); - - - -const SIZE_UNITS = [ "B", "KiB", "MiB", "GiB" ]; - -function addFileSizeUnit(size) { - let i = 0; - for (; size > 1024; size /= 1024) ++i; - return [i > 0 ? size.toFixed(2) : size, SIZE_UNITS[i]]; -} - - -function buildCurrentPathLink(path) { - let currentPath = `root`; - - if (path.endsWith("/")) - path = path.slice(1, path.length-1); - - const path_parts = path.split("/"); - - for (let i = 0; i < path_parts.length; ++i) { - let lnk = ""; - - for (let j = 0; j < i+1; ++j) - lnk += `/${path_parts[j]}`; - - currentPath += `/${path_parts[i]}`; - } - - return currentPath; -} - -function sortFuncByNameField(a, b) { - return (a.name > b.name) ? 1 : (a.name < b.name) ? -1 : 0; -} - -async function getDirectoryList(dir_path, orig_url) { - let directories = []; - let files = []; - let total_files_size = 0; - let directory = await fs.promises.opendir(dir_path); - - for await (const dirent of directory) { - const stat = await fs.promises.stat(path.join(dir_path, dirent.name)); - const [s, u] = addFileSizeUnit(stat.size); - - if (stat.isDirectory()) - directories.push({ - name: dirent.name, - link: `${encodeURIComponent(dirent.name)}/`, - datetime: stat.mtime, - size: "DIR" }); - else { - total_files_size += stat.size; - files.push({ - name: dirent.name, - link: `/files${orig_url}${encodeURIComponent(dirent.name)}`, - datetime: stat.mtime, - size: `${s} ${u}` }); - } - } - - directories.sort(sortFuncByNameField); - files.sort(sortFuncByNameField); - - return [directories.concat(files), directories.length, files.length, - addFileSizeUnit(total_files_size).join(' ')]; -} - -function setRoutes() { - return koaRouter().get('/(.*)?', async ctx => { - const file_path = path.join(config.share_path, decodeURI(ctx.originalUrl)); - - let stat = await fs.promises.stat(file_path); - - if (stat.isDirectory()) { - const [items, total_directories, total_files, total_files_size] - = await getDirectoryList(file_path, decodeURI(ctx.originalUrl)); - - await ctx.render('index', { - clientTZ: util.getClientTimezone(ctx), - main_site: util.getServiceByHost(ctx.header.host), - current_path: buildCurrentPathLink(decodeURI(ctx.originalUrl)), - total_files: total_files, - total_files_size: total_files_size, - total_directories: total_directories, - items: items }); - } - }); -} - - -const app = new Koa(); -const pug = new koaPug({ - viewPath: path.join(__dirname, "views"), - locals: { - moment: (date, tz) => util.datetime(date, util.date_formats.file_date, tz), }, - app: app -}); - -app.proxy = true; -app - .use(setRoutes().routes()) - // .on("error", err => { if (!err.code == ECONNRESET || !err.code == ENOENT) console.log(err.code); }) - .listen(config.port, config.host); - -console.log("Arav's dwelling / Files is up."); \ No newline at end of file diff --git a/files/static/assets/css/main.css b/files/static/assets/css/main.css deleted file mode 100644 index 555cf67..0000000 --- a/files/static/assets/css/main.css +++ /dev/null @@ -1,170 +0,0 @@ -@font-face { - font-family: 'Roboto Condensed'; - font-style: normal; - font-weight: 400; - src: local('RobotoCondensed'), local('RobotoCondensed-Regular'), - url(/shared/fonts/RobotoCondensed-Regular.ttf); } - -:root { - --background-color: #0a0a0a; - --primary-color: #cd2682; - --secondary-color: #9f2b68; - --text-color: #f5f5f5; - --text-indent: 1.6rem; - --overlay-background-color: #f5f5f574; - scrollbar-color: var(--primary-color) var(--background-color); } - -@media (prefers-color-scheme: light) { - :root { - --background-color: #f5f5f5; - --primary-color: #9f2b68; - --secondary-color: #cd2682; - --text-color: #0a0a0a; - --overlay-background-color: #0a0a0a74; } } - -* { margin: 0; } - -::selection { - background-color: var(--secondary-color); - color: var(--background-color); } - -a { - color: var(--primary-color); - text-decoration: none; } - -a:hover { - color: var(--secondary-color); - text-decoration: underline; - text-decoration-style: dotted; - transition: .5s; } - -p { - text-align: justify; - line-height: var(--text-indent); - text-indent: var(--text-indent); } - -p:not(:last-child) { margin-bottom: .1rem; } - -h1, -h2 { - font-size: 1.8rem; - font-variant: small-caps; - text-align: center; - margin-bottom: 1rem; } - -h2 { - font-size: 1.4rem; - margin: 1rem 0; } - -html { margin-left: calc(100vw - 100%); } - -body { - background-color: var(--background-color); - color: var(--text-color); - font-family: 'Roboto Condensed', Roboto, sans-serif; - font-size: 1.1rem; - margin: 0 auto; - max-width: 960px; - width: 98%; } - -header { - display: flex; - flex-wrap: wrap; - justify-content: space-between; } - -#logo { - display: block; - width: 360px; } - -#logo text { fill: var(--text-color); } - -#logo .logo { - font-size: 2rem; - font-variant-caps: small-caps; - font-weight: bold; } - -@media screen and (-webkit-min-device-pixel-ratio:0) { - #logo .logo { font-size: 2.082rem; } } - -@-moz-document url-prefix() { - #logo .logo { font-size: 2rem; } } - -#logo .under { font-size: .88rem; } - -nav { margin-top: .5rem; } - -nav a { font-variant: small-caps; } - -nav h1 { - color: var(--secondary-color); - margin: 0; } - -section { margin-top: 1rem; } - -#overlay { - align-items: center; - background-color: var(--overlay-background-color); - display: flex; - flex-direction: column; - height: 100%; - left: 0; - max-height: 100%; - position: fixed; - top: 0; - visibility: hidden; - width: 100%; } - -#overlay video, -#overlay audio, -#overlay img { - margin: auto; - max-height: 100%; - max-width: 86%; -} - -#overlay span { - color: var(--background-color); - text-shadow: 0 0 .3rem var(--secondary-color); -} - -table { overflow-y: scroll; width: 100%; } - -tr { vertical-align: top; } - -tr:hover { background-color: var(--primary-color); color: white; } - -tr:hover a { color: white; } - -th { text-align: left; } - -th:nth-child(2), -th:last-child { width: 1%; white-space: nowrap; } - -th, -td { line-break: strict; } - -th:nth-child(2), -td:nth-child(2) { padding: 0 1rem; } - -td a { display: block; width: 100%; } - -td a:hover { transition: none; } - -td:nth-child(2), -td:last-child { white-space: nowrap; } - -footer { - font-size: .8rem; - text-align: center; - padding: 1rem 0; } - -@media screen and (max-width: 640px) { - header { display: block; } - - #logo { - margin: 0 auto; - width: 100%; } - - nav { - width: 100%; - text-align: center; } } \ No newline at end of file diff --git a/files/static/assets/js/main.js b/files/static/assets/js/main.js deleted file mode 100644 index 2779545..0000000 --- a/files/static/assets/js/main.js +++ /dev/null @@ -1,56 +0,0 @@ -const video_formats = ["webm", "mp4"]; -const audio_formats = ["mp3", "flac", "opus", "ogg", "m4a"]; -const image_formats = ["jpg", "jpeg", "gif", "png", "bmp", "webp"]; - -const overlay = document.getElementById("overlay"); -let g_scale = 1; -let g_volume = 1.0; - -function mousescroll(e) { - e.preventDefault(); - g_scale = Math.min(Math.max(0.25, g_scale + (e.deltaY * -0.001)), 4); - e.target.style.transform = `scale(${g_scale})`; -} - -function onvolumechange(e) { - g_volume = e.target.volume; -} - -const ext_filter = (ext, pathname) => pathname.toLowerCase().endsWith(ext); - -function to_overlay(eltyp, pathname) { - const el = document.createElement(eltyp); - const el_label = document.createElement("span"); - el_label.textContent = decodeURI(pathname.substr(pathname.lastIndexOf("/") + 1)); - if (eltyp !== "audio") el.addEventListener('wheel', mousescroll); - if (eltyp !== "img") { - el.autoplay = el.controls = true; - el.addEventListener("volumechange", onvolumechange); - el.volume = g_volume; - } - el.src = pathname; - overlay.appendChild(el); - overlay.appendChild(el_label); - overlay.style.visibility = "visible"; -} - -document.getElementById("overlay").addEventListener("click", e => { - e.target.firstChild.remove(); - e.target.firstChild.remove(); - e.target.style.visibility = "hidden"; - g_scale = 1; -}); - -const file_links = Array.from(document.getElementsByTagName('tr')).slice(2).filter(e => e.lastChild.innerHTML != "DIR").map(l => l.firstChild.firstChild); - -file_links.forEach(f => f.addEventListener('click', e => { - const pathname = e.target.pathname; - if (video_formats.some(ext => ext_filter(ext, pathname))) - to_overlay("video", pathname); - else if (audio_formats.some(ext => ext_filter(ext, pathname))) - to_overlay("audio", pathname); - else if (image_formats.some(ext => ext_filter(ext, pathname))) - to_overlay("img", pathname); - if (overlay.firstChild != null) - e.preventDefault(); -})); diff --git a/files/static/robots.txt b/files/static/robots.txt deleted file mode 100644 index 8811370..0000000 --- a/files/static/robots.txt +++ /dev/null @@ -1,2 +0,0 @@ -User-agent: * -Disallow: /assets/ \ No newline at end of file diff --git a/files/views/index.pug b/files/views/index.pug deleted file mode 100644 index 36af7ba..0000000 --- a/files/views/index.pug +++ /dev/null @@ -1,44 +0,0 @@ -doctype html -html(lang='en') - head - title Arav's dwelling / Files - meta(charset='utf-8') - meta(http-equiv='X-UA-Compatible' content='IE=edge') - meta(name='viewport' content='width=device-width, initial-scale=1.0') - meta(name='theme-color' content='#cd2682') - meta(name='description' content='My file share.') - link(rel='icon' type='image/svg+xml' href='/shared/img/favicon.svg' sizes='any') - link(href='/assets/css/main.css' rel='stylesheet') - script(src='/assets/js/main.js' defer) - body - header - svg#logo(viewBox='0 -25 216 40') - text.logo Arav's dwelling - text.under(y='11') Welcome to my sacred place, wanderer - nav - a(href=main_site) Back to main website - h1 Files - section#files - span#current-path!= current_path - p Files: #{total_files} (#{total_files_size}); Directories: #{total_directories}. - table - thead - tr - th Name - th Date - th Size - tbody - tr - td #[a(href="../") ../] - each item in items - tr - td #[a(href=item.link)= item.name] - td!= moment(item.datetime, clientTZ) - td= item.size - section#privacy - h2 Privacy statements - p I collect access logs that include access date and time, IP-address, User-Agent, referer URL that tells me where have you came from, request that you sent to me. In addition there are GeoIP information added based on your IP-address that includes country, region, and city for my convenience. - p This site makes use of JavaScript purely for convenient functionality, like being able to watch video, listen to music, and look images in an overlay without the need to open a file in a new tab or return back. - footer - | 2017—2022 Arav <#[a(href='mailto:me@arav.top') me@arav.top]> - div#overlay \ No newline at end of file diff --git a/radio/config.example.js b/radio/config.example.js deleted file mode 100644 index 692db11..0000000 --- a/radio/config.example.js +++ /dev/null @@ -1,2 +0,0 @@ -exports.port = 32251; -exports.host = "127.0.0.1"; diff --git a/radio/index.js b/radio/index.js deleted file mode 100644 index 4d00d8e..0000000 --- a/radio/index.js +++ /dev/null @@ -1,79 +0,0 @@ -const fs = require("fs"); -const path = require("path"); -const uti = require("util"); -const exec = uti.promisify(require("child_process").exec); - -const Koa = require("koa"); -const koaPug = require("koa-pug"); -const koaRouter = require("koa-router"); -const fetch = require("node-fetch"); -const moment = require("moment-timezone"); - -const config = require("./config"); -const util = require("../shared/util"); - -async function getRadioStatus() { - try { - let status = await fetch('http://radio.arav.home.arpa/status-json.xsl').then(r => r.json()); - return { - server_start_iso8601: status.icestats.source.stream_start_iso8601, - server_start_date: util.datetime(status.icestats.source.stream_start_iso8601, util.date_formats.post_date), - song: `${status.icestats.source.artist} - ${status.icestats.source.title}`, - listener_peak: status.icestats.source.listener_peak, - listeners: status.icestats.source.listeners - } - } catch { - return { - server_start_iso8601: "n/a", - server_start_date: "n/a", - song: "n/a", - listener_peak: "n/a", - listeners: "n/a" - } - } -} - -async function getLastPlayedSongs(count, tz) { - try { - const { stdout, _ } = await exec(`tail -n${count} /var/log/icecast/playlist.log | head -n-1 | cut -d"|" -f1,4`); - let songs = stdout.trim().split("\n"); - for (let i = 0; i < songs.length; ++i) { - let [t, s] = songs[i].split('|'); - t = moment(t, "DD/MMM/YYYY:HH:mm:ss ZZ").tz(tz).format("HH:mm"); - let song = s.split(' - '); - songs[i] = { "start_time_local": t, "artist": song[0], "title": song[1] }; - } - return songs; - } catch { - return []; - } -} - -function setRoutes() { - return koaRouter().get('/', async ctx => { - await ctx.render('index', { - main_site: util.getServiceByHost(ctx.header.host), - radio_status: await getRadioStatus(), - last_songs: await getLastPlayedSongs(11, util.getClientTimezone(ctx)) - }); - }) - .get('/stats', async ctx => { - ctx.body = await getRadioStatus(); - }) - .get('/lastsong', async ctx => { - ctx.body = (await getLastPlayedSongs(2, util.getClientTimezone(ctx)))[0]; - }); -} - -const app = new Koa(); -const pug = new koaPug({ - viewPath: path.join(__dirname, "views"), - locals: { - moment: util.datetime }, - app: app -}); - -app.proxy = true; -app - .use(setRoutes().routes()) - .listen(config.port, config.host); diff --git a/radio/static/assets/css/main.css b/radio/static/assets/css/main.css deleted file mode 100644 index 97dc940..0000000 --- a/radio/static/assets/css/main.css +++ /dev/null @@ -1,154 +0,0 @@ -@font-face { - font-family: 'Roboto Condensed'; - font-style: normal; - font-weight: 400; - src: local('RobotoCondensed'), local('RobotoCondensed-Regular'), - url(/shared/fonts/RobotoCondensed-Regular.ttf); } - -:root { - --background-color: #0a0a0a; - --primary-color: #cd2682; - --secondary-color: #9f2b68; - --text-color: #f5f5f5; - --text-indent: 1.6rem; - scrollbar-color: var(--primary-color) var(--background-color); } - -@media (prefers-color-scheme: light) { - :root { - --background-color: #f5f5f5; - --primary-color: #9f2b68; - --secondary-color: #cd2682; - --text-color: #0a0a0a; } } - -* { margin: 0; } - -::selection { - background-color: var(--secondary-color); - color: var(--background-color); } - -a, -button { - color: var(--primary-color); - text-decoration: none; } - -a:hover, -button:hover { - color: var(--secondary-color); - cursor: pointer; - text-decoration: underline dotted; - transition: .5s; } - -button { - background: none; - border: none; - font: inherit; - padding: 0; } - -p { - text-align: justify; - line-height: var(--text-indent); - text-indent: var(--text-indent); } - -p:not(:last-child) { margin-bottom: .1rem; } - -h1, -h2 { - font-size: 1.8rem; - font-variant: small-caps; - text-align: center; - margin-bottom: 1rem; } - -h2 { - font-size: 1.4rem; - margin: 1rem 0; } - -small { font-size: .8rem; } - -small.player-links a { margin: 0 .2rem; } - -audio { - background-color: var(--primary-color); - box-shadow: 5px 5px var(--primary-color); - width: 100%; } - -@media screen and (-webkit-min-device-pixel-ratio:0) { - audio::-webkit-media-controls-panel { - background-color: var(--secondary-color); } - - audio { border-radius: 1.6rem; } } - -@-moz-document url-prefix() { - audio { border-radius: 0; } } - -html { margin-left: calc(100vw - 100%); } - -body { - background-color: var(--background-color); - color: var(--text-color); - font-family: 'Roboto Condensed', Roboto, sans-serif; - font-size: 1.1rem; - margin: 0 auto; - max-width: 960px; - width: 98%; } - -header { - display: flex; - flex-wrap: wrap; - justify-content: space-between; } - -#logo { - display: block; - width: 360px; } - -#logo text { fill: var(--text-color); } - -#logo .logo { - font-size: 2rem; - font-variant-caps: small-caps; - font-weight: bold; } - -@media screen and (-webkit-min-device-pixel-ratio:0) { - #logo .logo { font-size: 2.082rem; } } - -@-moz-document url-prefix() { - #logo .logo { font-size: 2rem; } } - -#logo .under { font-size: .88rem; } - -nav { margin-top: .5rem; } - -nav a { font-variant: small-caps; } - -nav h1 { - color: var(--secondary-color); - margin: 0; } - -section { margin-top: 1rem; } - -#last-played { - margin: 0 auto; - min-width: 80%; - width: 80%; } - -#last-played tbody tr { - display: grid; - gap: .5rem; - grid-template-columns: 3rem 1fr 1fr; } - -#last-played tbody tr td:nth-child(2) { text-align: right; } - -footer { - font-size: .8rem; - text-align: center; - padding: 1rem 0; } - -@media screen and (max-width: 640px) { - header { display: block; } - - #logo { - margin: 0 auto; - width: 100%; } - - nav { - width: 100%; - text-align: center; } } \ No newline at end of file diff --git a/radio/static/assets/files/radio.arav.top.m3u b/radio/static/assets/files/radio.arav.top.m3u deleted file mode 100644 index 8730df1..0000000 --- a/radio/static/assets/files/radio.arav.top.m3u +++ /dev/null @@ -1,9 +0,0 @@ -#EXTM3U -#EXTINF:-1,Arav's dwelling / Radio -http://radio.arav.top:8000/stream.ogg -#EXTINF:-1,Arav's dwelling / Radio (HTTPS) -https://radio.arav.top/live/stream.ogg -#EXTINF:-1,Arav's dwelling / Radio on Tor -http://wsmkgnmhmzqm7kyzv7jnzzafvgm7xlmlfvzhgorpapd5or2arnhuktqd.onion/live/stream.ogg -#EXTINF:-1,Arav's dwelling / Radio on I2P -http://radio.arav.i2p/live/stream.ogg \ No newline at end of file diff --git a/radio/static/assets/js/main.js b/radio/static/assets/js/main.js deleted file mode 100644 index f9b23f0..0000000 --- a/radio/static/assets/js/main.js +++ /dev/null @@ -1,48 +0,0 @@ -function $(id) { return document.getElementById(id); } - -function updateRadioStatus() { - fetch("/stats") - .then(r => r.json()) - .then(r => { - $("radio-status").innerHTML = - `On-air since `; - $("radio-song").textContent = r.song; - $("radio-listeners").textContent = r.listeners; - $("radio-listener-peak").textContent = r.listener_peak; - }).catch(() => { - $("radio-status").textContent = "Radio is offline."; - $("radio-song").textContent = - $("radio-listeners").textContent = - $("radio-listener-peak").textContent = "n/a"; - }); -} - -function updateLastPlayedSong() { - fetch('/lastsong') - .then(r => r.json()) - .then(last_played => { - let cur_artist = $('last-played').firstChild.lastChild.children[1].innerText; - let cur_title = $('last-played').firstChild.lastChild.lastChild.innerText; - - if (last_played.artist == cur_artist && last_played.title == cur_title) - return; - - $('last-played').firstChild.firstChild.remove(); - - let row = $('last-played').insertRow(); - let start_time = row.insertCell(); - start_time.appendChild(document.createTextNode(last_played.start_time_local)); - let artist_cell = row.insertCell(); - artist_cell.appendChild(document.createTextNode(last_played.artist)); - let title_cell = row.insertCell(); - title_cell.appendChild(document.createTextNode(last_played.title)); - }); -} - -document.getElementById("btn-update").addEventListener("click", () => { - updateLastPlayedSong(); - updateRadioStatus(); -}) - -setInterval(updateRadioStatus, 45000); -setInterval(updateLastPlayedSong, 45000); \ No newline at end of file diff --git a/radio/static/robots.txt b/radio/static/robots.txt deleted file mode 100644 index 912a23f..0000000 --- a/radio/static/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -User-agent: * -Disallow: /assets/ -Disallow: /live/ \ No newline at end of file diff --git a/radio/views/index.pug b/radio/views/index.pug deleted file mode 100644 index 9de5bf2..0000000 --- a/radio/views/index.pug +++ /dev/null @@ -1,60 +0,0 @@ -mixin radioStatus(date, iso) - if (date != "n/a") - p #[span#radio-status On-air since #[time(datetime=iso)= date]] - else - p #[span#radio-status Radio is offline.] - -doctype html -html(lang='en') - head - title Arav's dwelling / Radio - meta(charset='utf-8') - meta(http-equiv='X-UA-Compatible' content='IE=edge') - meta(name='viewport' content='width=device-width, initial-scale=1.0') - meta(name='theme-color' content='#cd2682') - meta(name='description' content='Internet-radio broadcasting from under my desk.') - link(rel='icon' href='/shared/img/favicon.svg' sizes='any' type='image/svg+xml') - link(href='/assets/css/main.css' rel='stylesheet') - script(src='/assets/js/main.js' defer) - body - header - svg#logo(viewBox='0 -25 216 40') - text.logo Arav's dwelling - text.under(y='11') Welcome to my sacred place, wanderer - nav - a(href=main_site) Back to main website - h1 Radio - section - small.player-links - a(href='/filelist') filelist - a(href='/playlist') playlist (.m3u) - a(href='/live/stream.ogg') direct link - a(href='http://radio.arav.top:8000/stream.ogg') direct link (http) - a(href='http://wsmkgnmhmzqm7kyzv7jnzzafvgm7xlmlfvzhgorpapd5or2arnhuktqd.onion/live/stream.ogg') direct link (Tor) - a(href='http://radio.arav.i2p/live/stream.ogg') direct link (I2P) - a(href='https://dir.xiph.org/search?q=arav\'s+dwelling') Xiph - | OGG 128 Kb/s - audio(preload='none' controls) - source(src='/live/stream.ogg' type='audio/ogg') - | Your browser doesn't support an audio element, it's sad... But you always can take the #[a(href='/playlist') playlist]! - +radioStatus(radio_status.server_start_date, radio_status.server_start_iso8601) - p Now playing: #[span#radio-song= radio_status.song] - p Current/peak listeners: #[span#radio-listeners= radio_status.listeners] / #[span#radio-listener-peak= radio_status.listener_peak] - p - small Notice: information updates every 45 seconds. But you can #[button(id='btn-update') update] it forcibly. - if (last_songs) - section - h2 Last 10 songs - table#last-played - each song in last_songs - tr - td= song.start_time_local - td= song.artist - td= song.title - section - p The largest number of simultaneous listeners was #[b 7] at #[time(datetime='2022-02-19') 19 February 2022], and the song was "Röyksopp - 49 Percent". - section - h2 Privacy statements - p Logs are collected and include access date and time, IP-address, User-Agent, referer URL, request. This website makes use of JavaScript to update a radio status and last 10 songs list. - footer - | 2017—2022 Arav <#[a(href='mailto:me@arav.top') me@arav.top]>