summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--files/MorePerfectDOSVGA.ttfbin0 -> 78252 bytes
-rw-r--r--templates/icecast2/mp3-to-ogg.sh.j212
-rw-r--r--templates/nginx/nginx.conf.j219
-rw-r--r--templates/var/www/html/index.html88
-rw-r--r--templates/var/www/html/style.css105
-rw-r--r--vars/main.yaml1
6 files changed, 220 insertions, 5 deletions
diff --git a/files/MorePerfectDOSVGA.ttf b/files/MorePerfectDOSVGA.ttf
new file mode 100644
index 0000000..bf70112
--- /dev/null
+++ b/files/MorePerfectDOSVGA.ttf
Binary files differ
diff --git a/templates/icecast2/mp3-to-ogg.sh.j2 b/templates/icecast2/mp3-to-ogg.sh.j2
index b37f5af..f475d36 100644
--- a/templates/icecast2/mp3-to-ogg.sh.j2
+++ b/templates/icecast2/mp3-to-ogg.sh.j2
@@ -10,10 +10,9 @@ DIR="{{ radio_music_dir }}"
shopt -s nullglob
for mp3file in "${DIR}"/*.mp3; do
oggfile="${mp3file%.mp3}.ogg"
+ printf "%s\n" "[inf] converting ${mp3file} to ${oggfile} with ${title}"
- printf "%s\n" "[inf] converting ${mp3file} to ${oggfile}"
-
- if ffmpeg -loglevel error -y -i "${mp3file}" -acodec libvorbis -q:a 5 "${oggfile}"; then
+ if ffmpeg -loglevel error -y -i "${mp3file}" -acodec libvorbis -q:a 5 -metadata title="${title}" "${oggfile}"; then
printf "%s\n" "[inf] conversion successful, removing ${mp3file}"
rm -f "${mp3file}"
else
@@ -21,7 +20,13 @@ for mp3file in "${DIR}"/*.mp3; do
fi
done
+for oggfile in "${DIR}"/*.ogg; do
+ title="$(basename "${oggfile}" .ogg)"
+ vorbiscomment -w -t "TITLE=${title}" "${oggfile}"
+done
+
ls "${DIR}"/*.ogg > "${DIR}/playlist.txt"
+printf "%s\n" "[inf] playlist generated at ${DIR}/playlist.txt"
if id -u icecast2 >/dev/null 2>&1 && getent group icecast >/dev/null 2>&1; then
chown -R icecast2:icecast "$DIR"
@@ -29,4 +34,3 @@ if id -u icecast2 >/dev/null 2>&1 && getent group icecast >/dev/null 2>&1; then
else
printf "%s\n" "[err] user or group icecast2:icecast does not exist, skipping chown"
fi
-
diff --git a/templates/nginx/nginx.conf.j2 b/templates/nginx/nginx.conf.j2
index 7f1ac2b..2e257e0 100644
--- a/templates/nginx/nginx.conf.j2
+++ b/templates/nginx/nginx.conf.j2
@@ -77,7 +77,7 @@ http {
}
location /stream {
- proxy_pass http://localhost:8000/stream;
+ proxy_pass http://127.0.0.1:8000/stream;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@@ -96,6 +96,23 @@ http {
add_header Access-Control-Expose-Headers "Content-Length,Content-Range" always;
}
+ location /nowplaying {
+ proxy_pass http://127.0.0.1:8000/status-json.xsl;
+
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ proxy_http_version 1.1;
+ proxy_buffering off;
+ proxy_request_buffering off;
+
+ add_header Access-Control-Allow-Origin "*" always;
+ add_header Access-Control-Allow-Methods "GET, OPTIONS" always;
+ add_header Access-Control-Allow-Headers "Content-Type" always;
+ }
+
location /admin {
deny all;
return 403;
diff --git a/templates/var/www/html/index.html b/templates/var/www/html/index.html
new file mode 100644
index 0000000..14ed148
--- /dev/null
+++ b/templates/var/www/html/index.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
+ <link rel="icon" href="data:," />
+ <link rel="stylesheet" href="style.css" />
+ <title>{{ radio_name }}</title>
+</head>
+<body>
+ <div class="index-container">
+ <div class="player-wrapper">
+ <a id="radioButton" class="btn" href="#">Play</a>
+ <div id="info"></div>
+ <audio id="radioPlayer" hidden>
+ <source src="/stream" type="audio/ogg" />
+ Your browser does not support the audio element.
+ </audio>
+ </div>
+ </div>
+
+ <script>
+ const player = document.getElementById('radioPlayer');
+ const button = document.getElementById('radioButton');
+ const infoEl = document.getElementById('info');
+
+ button.addEventListener('click', () => {
+ if (player.paused) {
+ player.play();
+ button.textContent = 'Stop';
+ } else {
+ player.pause();
+ player.currentTime = 0;
+ button.textContent = 'Play';
+ }
+ });
+
+ let spinnerInterval;
+
+ function startSpinner() {
+ const spinnerChars = ['|', '/', '-', '\\'];
+ let i = 0;
+ spinnerInterval = setInterval(() => {
+ infoEl.textContent = `${spinnerChars[i++ % spinnerChars.length]}`;
+ }, 100);
+ }
+
+ function stopSpinner() {
+ clearInterval(spinnerInterval);
+ }
+
+ let spinnerHasRun = false;
+
+ async function fetchCurrentTrack() {
+ if (!spinnerHasRun) {
+ startSpinner();
+ await new Promise(resolve => setTimeout(resolve, 3000));
+ }
+
+ try {
+ const response = await fetch('/info');
+ if (!response.ok) throw new Error('Network response was not ok');
+
+ const data = await response.json();
+ const source = data.icestats.source;
+
+ const title = source.title || 'Unknown';
+ const bitrate = source['ice-bitrate'] || 0;
+ const listeners = source.listeners || 0;
+ const listenerLabel = listeners === 1 ? 'listener' : 'listeners';
+
+ stopSpinner();
+ infoEl.textContent = `${title} | ${bitrate} kbps | ${listeners} ${listenerLabel}`;
+
+ spinnerHasRun = true;
+
+ } catch (error) {
+ stopSpinner();
+ infoEl.textContent = 'Error loading track info';
+ console.error(error);
+ }
+ }
+
+ fetchCurrentTrack();
+ setInterval(fetchCurrentTrack, 10000);
+ </script>
+</body>
+</html>
diff --git a/templates/var/www/html/style.css b/templates/var/www/html/style.css
new file mode 100644
index 0000000..542b59c
--- /dev/null
+++ b/templates/var/www/html/style.css
@@ -0,0 +1,105 @@
+:root {
+ --color-black: #000000;
+ --color-dark-cyan: #008080;
+ --color-light-gray: #C0C0C0;
+ --color-dark-gray: #808080;
+}
+
+@font-face {
+ font-family: 'MorePerfectDOSVGA';
+ src: url('MorePerfectDOSVGA.ttf') format('truetype');
+}
+
+::-webkit-scrollbar {
+ display: none;
+}
+
+* {
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+}
+
+body {
+ background-color: var(--color-black);
+ color: var(--color-light-gray);
+ font-family: 'MorePerfectDOSVGA', monospace;
+ font-size: 14px;
+ line-height: 1.6;
+ margin: 0 auto;
+ padding: 0;
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100vh;
+ text-align: center;
+}
+
+.index-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100vh;
+ width: 100%;
+ padding: 0 10px;
+ box-sizing: border-box;
+}
+
+.player-wrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
+ max-width: 100%;
+ width: 320px;
+}
+
+.btn {
+ color: var(--color-black);
+ background-color: var(--color-light-gray);
+ padding: 8px 16px;
+ text-decoration: none;
+ font-size: 16px;
+ font-weight: bold;
+ position: relative;
+ display: inline-block;
+ cursor: pointer;
+ user-select: none;
+}
+
+.btn::before {
+ content: '';
+ background-color: var(--color-dark-gray);
+ position: absolute;
+ top: 4px;
+ left: 4px;
+ width: 100%;
+ height: 100%;
+ z-index: -1;
+}
+
+.btn:hover::before {
+ background-color: var(--color-dark-cyan);
+}
+
+#info {
+ font-size: 14px;
+ color: var(--color-light-gray);
+ min-height: 18px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+@media (max-width: 400px) {
+ .btn {
+ font-size: 14px;
+ padding: 6px 12px;
+ }
+ #info {
+ font-size: 12px;
+ }
+ .player-wrapper {
+ width: 90vw;
+ }
+}
diff --git a/vars/main.yaml b/vars/main.yaml
index 9f8f32c..56da4e3 100644
--- a/vars/main.yaml
+++ b/vars/main.yaml
@@ -9,6 +9,7 @@ apt_packages:
- nginx
- ufw
- unattended-upgrades
+ - vorbis-tools
- vim
fail2ban_jail_dir: /etc/fail2ban/jail.d