From a064cbda2086b79b6cc04111cb308a6b2d770aa6 Mon Sep 17 00:00:00 2001 From: heqnx Date: Sun, 6 Jul 2025 14:57:39 +0300 Subject: replaced ices2 which only supports vorbis, with liquidsoap + creation of title metadata; reorganized templates --- templates/etc/icecast2/icecast.xml.j2 | 66 +++++++++ templates/etc/icecast2/ices-playlist.xml.j2 | 44 ++++++ templates/etc/icecast2/mp3-to-ogg.sh.j2 | 36 +++++ templates/etc/nginx/nginx.conf.j2 | 147 +++++++++++++++++++++ templates/etc/systemd/system/ices2.service | 17 +++ .../etc/systemd/system/liquidsoap-radio.service | 17 +++ templates/icecast2/icecast.xml.j2 | 66 --------- templates/icecast2/ices-playlist.xml.j2 | 44 ------ templates/icecast2/mp3-to-ogg.sh.j2 | 36 ----- templates/nginx/nginx.conf.j2 | 147 --------------------- templates/srv/radio/create-playlist.sh.j2 | 22 +++ templates/srv/radio/radio.liq.j2 | 23 ++++ templates/systemd/system/ices2.service | 17 --- templates/var/www/html/audio-controls.js | 5 +- 14 files changed, 374 insertions(+), 313 deletions(-) create mode 100644 templates/etc/icecast2/icecast.xml.j2 create mode 100644 templates/etc/icecast2/ices-playlist.xml.j2 create mode 100644 templates/etc/icecast2/mp3-to-ogg.sh.j2 create mode 100644 templates/etc/nginx/nginx.conf.j2 create mode 100644 templates/etc/systemd/system/ices2.service create mode 100644 templates/etc/systemd/system/liquidsoap-radio.service delete mode 100644 templates/icecast2/icecast.xml.j2 delete mode 100644 templates/icecast2/ices-playlist.xml.j2 delete mode 100644 templates/icecast2/mp3-to-ogg.sh.j2 delete mode 100644 templates/nginx/nginx.conf.j2 create mode 100644 templates/srv/radio/create-playlist.sh.j2 create mode 100644 templates/srv/radio/radio.liq.j2 delete mode 100644 templates/systemd/system/ices2.service (limited to 'templates') diff --git a/templates/etc/icecast2/icecast.xml.j2 b/templates/etc/icecast2/icecast.xml.j2 new file mode 100644 index 0000000..ef6e539 --- /dev/null +++ b/templates/etc/icecast2/icecast.xml.j2 @@ -0,0 +1,66 @@ + + Earth + {{ email }} + + + 1000 + 2 + 524288 + 30 + 15 + 10 + 1 + 65535 + + + + {{ random_password }} + disabled + admin + {{ random_password }} + + + localhost + "" + + + 8000 + 127.0.0.1 + + + +
+ + + + /stream + 1000 + 1 + 1 + + + 1 + + + /usr/share/icecast2 + /var/log/icecast2 + /usr/share/icecast2/web + /usr/share/icecast2/admin + + + + + access.log + error.log + 2 + 10000 + + + + 1 + + icecast2 + icecast + + + diff --git a/templates/etc/icecast2/ices-playlist.xml.j2 b/templates/etc/icecast2/ices-playlist.xml.j2 new file mode 100644 index 0000000..89fc4c9 --- /dev/null +++ b/templates/etc/icecast2/ices-playlist.xml.j2 @@ -0,0 +1,44 @@ + + + 1 + /var/log/ices + ices.log + 4 + 1 + + + + + + Example stream name + Example genre + A short description of your stream + + + + playlist + basic + playlist.txt + 1 + 0 + 0 + + + + localhost + 8000 + {{ random_password }} + /stream + 0 + 60 + 10 + 80 + + + + + diff --git a/templates/etc/icecast2/mp3-to-ogg.sh.j2 b/templates/etc/icecast2/mp3-to-ogg.sh.j2 new file mode 100644 index 0000000..f475d36 --- /dev/null +++ b/templates/etc/icecast2/mp3-to-ogg.sh.j2 @@ -0,0 +1,36 @@ +#!/bin/bash + +if ! command -v ffmpeg &>/dev/null; then + printf "%s\n" "[err] ffmpeg not found" + exit 1 +fi + +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}" + + 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 + printf "%s\n" "[err] conversion failed for ${mp3file}" + 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" + printf "%s\n" "[inf] chowned ${DIR} with icecast2:icecast" +else + printf "%s\n" "[err] user or group icecast2:icecast does not exist, skipping chown" +fi diff --git a/templates/etc/nginx/nginx.conf.j2 b/templates/etc/nginx/nginx.conf.j2 new file mode 100644 index 0000000..bb0930e --- /dev/null +++ b/templates/etc/nginx/nginx.conf.j2 @@ -0,0 +1,147 @@ +user www-data; +worker_processes auto; +pid /run/nginx.pid; +include /etc/nginx/modules-enabled/*.conf; + +events { + worker_connections 1024; + multi_accept on; +} + +http { + sendfile on; + tcp_nopush on; + tcp_nodelay on; + sendfile_max_chunk 512k; + keepalive_timeout 300s; + keepalive_requests 1000; + types_hash_max_size 2048; + server_tokens off; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log warn; + + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_http_version 1.1; + gzip_min_length 256; + gzip_types + text/plain + text/css + application/json + application/javascript + text/xml + application/xml + application/xml+rss + text/javascript + image/svg+xml; + + upstream icecast_backend { + server 127.0.0.1:8000; + keepalive 32; + } + + server { + listen 80; + server_name {{ domain }}; + return 301 https://{{ domain }}$request_uri; + } + + server { + listen 443 ssl http2; + server_name {{ domain }}; + + ssl_certificate /etc/letsencrypt/live/{{ domain }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ domain }}/privkey.pem; + #ssl_trusted_certificate /etc/letsencrypt/live/{{ domain }}/chain.pem; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers off; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; + ssl_session_tickets off; + + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Frame-Options "DENY" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Content-Security-Policy "default-src 'self'; connect-src *; media-src * blob: data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline';" always; + add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; + + root /var/www/html; + index index.html index.htm; + + location / { + try_files $uri $uri/ =404; + } + + location /stream { + proxy_pass http://icecast_backend/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; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_http_version 1.1; + proxy_buffering off; + proxy_request_buffering off; + + gzip off; + proxy_read_timeout 3600s; + proxy_connect_timeout 300s; + proxy_send_timeout 3600s; + chunked_transfer_encoding on; + + add_header Access-Control-Allow-Origin "{{ radio_url }}" always; + add_header Access-Control-Allow-Methods "GET, OPTIONS" always; + add_header Access-Control-Allow-Headers "Range" always; + add_header Access-Control-Expose-Headers "Content-Length,Content-Range" always; + + access_log off; + error_log /var/log/nginx/icecast-error.log warn; + } + + location /info { + 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 "{{ radio_url }}" 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; + } + + location /admin/ { + deny all; + return 403; + } + + location ~ ^/(status|statistics|server|webadmin) { + deny all; + return 403; + } + + access_log /var/log/nginx/icecast-access.log; + error_log /var/log/nginx/icecast-error.log warn; + } +} diff --git a/templates/etc/systemd/system/ices2.service b/templates/etc/systemd/system/ices2.service new file mode 100644 index 0000000..d3a4c2f --- /dev/null +++ b/templates/etc/systemd/system/ices2.service @@ -0,0 +1,17 @@ +[Unit] +Description=ICES2 Icecast source client +After=network.target icecast2.service +Requires=icecast2.service + +[Service] +Type=simple +User=icecast2 +Group=icecast +WorkingDirectory={{ radio_music_dir }} +ExecStart=/usr/bin/ices2 /etc/icecast2/ices-playlist.xml +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target + diff --git a/templates/etc/systemd/system/liquidsoap-radio.service b/templates/etc/systemd/system/liquidsoap-radio.service new file mode 100644 index 0000000..c10983d --- /dev/null +++ b/templates/etc/systemd/system/liquidsoap-radio.service @@ -0,0 +1,17 @@ +[Unit] +Description=Liquidsoap Radio Streaming Service +After=network.target + +[Service] +Type=simple +User=icecast2 +#Group=icecast +ExecStart=/usr/bin/liquidsoap /srv/radio/radio.liq +Restart=on-failure +RestartSec=5 +StandardOutput=syslog +StandardError=syslog +SyslogIdentifier=liquidsoap-radio + +[Install] +WantedBy=multi-user.target diff --git a/templates/icecast2/icecast.xml.j2 b/templates/icecast2/icecast.xml.j2 deleted file mode 100644 index ef6e539..0000000 --- a/templates/icecast2/icecast.xml.j2 +++ /dev/null @@ -1,66 +0,0 @@ - - Earth - {{ email }} - - - 1000 - 2 - 524288 - 30 - 15 - 10 - 1 - 65535 - - - - {{ random_password }} - disabled - admin - {{ random_password }} - - - localhost - "" - - - 8000 - 127.0.0.1 - - - -
- - - - /stream - 1000 - 1 - 1 - - - 1 - - - /usr/share/icecast2 - /var/log/icecast2 - /usr/share/icecast2/web - /usr/share/icecast2/admin - - - - - access.log - error.log - 2 - 10000 - - - - 1 - - icecast2 - icecast - - - diff --git a/templates/icecast2/ices-playlist.xml.j2 b/templates/icecast2/ices-playlist.xml.j2 deleted file mode 100644 index 89fc4c9..0000000 --- a/templates/icecast2/ices-playlist.xml.j2 +++ /dev/null @@ -1,44 +0,0 @@ - - - 1 - /var/log/ices - ices.log - 4 - 1 - - - - - - Example stream name - Example genre - A short description of your stream - - - - playlist - basic - playlist.txt - 1 - 0 - 0 - - - - localhost - 8000 - {{ random_password }} - /stream - 0 - 60 - 10 - 80 - - - - - diff --git a/templates/icecast2/mp3-to-ogg.sh.j2 b/templates/icecast2/mp3-to-ogg.sh.j2 deleted file mode 100644 index f475d36..0000000 --- a/templates/icecast2/mp3-to-ogg.sh.j2 +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -if ! command -v ffmpeg &>/dev/null; then - printf "%s\n" "[err] ffmpeg not found" - exit 1 -fi - -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}" - - 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 - printf "%s\n" "[err] conversion failed for ${mp3file}" - 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" - printf "%s\n" "[inf] chowned ${DIR} with icecast2:icecast" -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 deleted file mode 100644 index bb0930e..0000000 --- a/templates/nginx/nginx.conf.j2 +++ /dev/null @@ -1,147 +0,0 @@ -user www-data; -worker_processes auto; -pid /run/nginx.pid; -include /etc/nginx/modules-enabled/*.conf; - -events { - worker_connections 1024; - multi_accept on; -} - -http { - sendfile on; - tcp_nopush on; - tcp_nodelay on; - sendfile_max_chunk 512k; - keepalive_timeout 300s; - keepalive_requests 1000; - types_hash_max_size 2048; - server_tokens off; - - include /etc/nginx/mime.types; - default_type application/octet-stream; - - access_log /var/log/nginx/access.log; - error_log /var/log/nginx/error.log warn; - - gzip on; - gzip_vary on; - gzip_proxied any; - gzip_comp_level 6; - gzip_buffers 16 8k; - gzip_http_version 1.1; - gzip_min_length 256; - gzip_types - text/plain - text/css - application/json - application/javascript - text/xml - application/xml - application/xml+rss - text/javascript - image/svg+xml; - - upstream icecast_backend { - server 127.0.0.1:8000; - keepalive 32; - } - - server { - listen 80; - server_name {{ domain }}; - return 301 https://{{ domain }}$request_uri; - } - - server { - listen 443 ssl http2; - server_name {{ domain }}; - - ssl_certificate /etc/letsencrypt/live/{{ domain }}/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/{{ domain }}/privkey.pem; - #ssl_trusted_certificate /etc/letsencrypt/live/{{ domain }}/chain.pem; - - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers HIGH:!aNULL:!MD5; - ssl_prefer_server_ciphers off; - ssl_session_cache shared:SSL:10m; - ssl_session_timeout 1d; - ssl_session_tickets off; - - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; - add_header X-Frame-Options "DENY" always; - add_header X-Content-Type-Options "nosniff" always; - add_header X-XSS-Protection "1; mode=block" always; - add_header Referrer-Policy "strict-origin-when-cross-origin" always; - add_header Content-Security-Policy "default-src 'self'; connect-src *; media-src * blob: data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline';" always; - add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; - - root /var/www/html; - index index.html index.htm; - - location / { - try_files $uri $uri/ =404; - } - - location /stream { - proxy_pass http://icecast_backend/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; - proxy_set_header X-Forwarded-Proto $scheme; - - proxy_http_version 1.1; - proxy_buffering off; - proxy_request_buffering off; - - gzip off; - proxy_read_timeout 3600s; - proxy_connect_timeout 300s; - proxy_send_timeout 3600s; - chunked_transfer_encoding on; - - add_header Access-Control-Allow-Origin "{{ radio_url }}" always; - add_header Access-Control-Allow-Methods "GET, OPTIONS" always; - add_header Access-Control-Allow-Headers "Range" always; - add_header Access-Control-Expose-Headers "Content-Length,Content-Range" always; - - access_log off; - error_log /var/log/nginx/icecast-error.log warn; - } - - location /info { - 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 "{{ radio_url }}" 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; - } - - location /admin/ { - deny all; - return 403; - } - - location ~ ^/(status|statistics|server|webadmin) { - deny all; - return 403; - } - - access_log /var/log/nginx/icecast-access.log; - error_log /var/log/nginx/icecast-error.log warn; - } -} diff --git a/templates/srv/radio/create-playlist.sh.j2 b/templates/srv/radio/create-playlist.sh.j2 new file mode 100644 index 0000000..d596e8a --- /dev/null +++ b/templates/srv/radio/create-playlist.sh.j2 @@ -0,0 +1,22 @@ +#!/bin/bash + +DIR="{{ radio_music_dir }}" + +> "${DIR}/playlist.txt" +for i in "${DIR}"/tracks/*.mp3; do + title="$(basename "${i}" .mp3)" + printf "%s\n" "annotate:title=\"${title}\":${i}" >> "${DIR}/playlist.txt" +done + +if id -u icecast2 >/dev/null 2>&1 && getent group icecast >/dev/null 2>&1; then + chown -R icecast2:icecast "${DIR}" + printf "%s\n" "[inf] chowned ${DIR} with icecast2:icecast" +else + printf "%s\n" "[err] user or group icecast2:icecast does not exist, skipping chown" +fi + +if systemctl is-active liquidsoap-radio.service; then + systemctl restart liquidsoap-radio.service +else + printf "%s\n" "[err] liquidsoap-radio.service is not active, skipping restart" +fi diff --git a/templates/srv/radio/radio.liq.j2 b/templates/srv/radio/radio.liq.j2 new file mode 100644 index 0000000..0a070fe --- /dev/null +++ b/templates/srv/radio/radio.liq.j2 @@ -0,0 +1,23 @@ +#!/usr/bin/liquidsoap + +#settings.init.allow_root := true +set("log.file.path", "{{ radio_music_dir }}/logs/radio.log") + +music = playlist("{{ radio_music_dir }}/playlist.txt") +music = mksafe(music) + +def append_branding(m) = + title = m["title"] + [("title", "#{title}")] +end + +music = map_metadata(append_branding, music) + +output.icecast( + %mp3(bitrate=128), + music, + host="localhost", + port=8000, + password="{{ random_password }}", + mount="/stream", +) diff --git a/templates/systemd/system/ices2.service b/templates/systemd/system/ices2.service deleted file mode 100644 index d3a4c2f..0000000 --- a/templates/systemd/system/ices2.service +++ /dev/null @@ -1,17 +0,0 @@ -[Unit] -Description=ICES2 Icecast source client -After=network.target icecast2.service -Requires=icecast2.service - -[Service] -Type=simple -User=icecast2 -Group=icecast -WorkingDirectory={{ radio_music_dir }} -ExecStart=/usr/bin/ices2 /etc/icecast2/ices-playlist.xml -Restart=on-failure -RestartSec=5 - -[Install] -WantedBy=multi-user.target - diff --git a/templates/var/www/html/audio-controls.js b/templates/var/www/html/audio-controls.js index 3b0a0c1..7f488f8 100644 --- a/templates/var/www/html/audio-controls.js +++ b/templates/var/www/html/audio-controls.js @@ -42,13 +42,12 @@ async function fetchCurrentTrack() { const data = await response.json(); const source = data.icestats.source; - const title = source.title || 'Unknown'; - const bitrate = source['ice-bitrate'] || 0; + const title = source.title || 'unknown'; const listeners = source.listeners || 0; const listenerLabel = listeners === 1 ? 'listener' : 'listeners'; stopSpinner(); - infoEl.textContent = `${title} | ${bitrate} kbps | ${listeners} ${listenerLabel}`; + infoEl.textContent = `${title} | ${listeners} ${listenerLabel}`; spinnerHasRun = true; -- cgit v1.2.3