radio: /srv/betoniarka paths, in-container Icecast, drop ffmpeg.pref

Per review of the first radio-container cut:

- radio_conjurer.liq: replace the cargo-culted /home/pi/Conjurer and
  /home/pi/MediaFolder paths with the container layout -
  /srv/betoniarka/data (playlists, script.params, persistence/radio logs),
  /srv/betoniarka/music (library + emergency track) and
  /srv/betoniarka/secrets/icecast_credentials.json;
  interactive.persistent now uses an absolute path;
  output.icecast targets host="localhost" (icecast lives in the container)
- Icecast2 now runs INSIDE the radio container: entrypoint renders
  /etc/icecast2/icecast.xml from docker/icecast.xml.tpl, filling
  source/admin/relay passwords from the secret provisioned at install time
  (same pattern as the bot's netrc); optional admin_password/relay_password
  fields default to password; icecast starts as its unprivileged user;
  port 8000 exposed for listeners; extra_hosts hack removed
- compose.radio.yaml: same-path mounts for /srv/betoniarka/{data,music,secrets}
- ffmpeg.pref removed from the repo (the pin only mattered against the
  RPi OS repo; bookworm ships the right FFmpeg 5.x natively) - verdict
  preserved in the docs and the legacy installer note updated
- docs: updated radio section (volumes, secret provisioning, wire-up)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Michal Tuszowski
2026-07-08 11:32:48 +02:00
committed by Michał Tuszowski
parent ebc73c16d4
commit fcb03e304c
8 changed files with 162 additions and 64 deletions
+14 -14
View File
@@ -1,16 +1,16 @@
# FILEPATH: /home/mtuszowski/conjurer/conjurer_musician/radio_conjurer.liq # Radio Conjurer - Liquidsoap script (containerised paths: /srv/betoniarka/*)
# This script sets up a Liquidsoap radio stream with various features and configurations. # This script sets up a Liquidsoap radio stream with various features and configurations.
# Load icecast credentials from a JSON file # Load icecast credentials from a JSON file
let json.parse credentials = file.contents("/home/pi/Conjurer/icecast_credentials.json") let json.parse credentials = file.contents("/srv/betoniarka/secrets/icecast_credentials.json")
# Enable replaygain metadata processing # Enable replaygain metadata processing
enable_replaygain_metadata() enable_replaygain_metadata()
# Set up a playlog for tracking played tracks # Set up a playlog for tracking played tracks
l = playlog(duration = 72000.0, persistency="/home/pi/Conjurer/persistence.log") l = playlog(duration = 72000.0, persistency="/srv/betoniarka/data/persistence.log")
# Function to check if a track can be played based on its metadata # Function to check if a track can be played based on its metadata
def check(r) def check(r)
@@ -25,20 +25,20 @@ def check(r)
end end
# Define playlists to be used in the stream # Define playlists to be used in the stream
s1 = replaygain(playlist(reload_mode="watch", check_next=check, "/home/pi/Conjurer/all_playlist.playlist")) s1 = replaygain(playlist(reload_mode="watch", check_next=check, "/srv/betoniarka/data/all_playlist.playlist"))
s2 = replaygain(playlist(reload_mode="watch", check_next=check, "/home/pi/Conjurer/priority_queue.playlist")) s2 = replaygain(playlist(reload_mode="watch", check_next=check, "/srv/betoniarka/data/priority_queue.playlist"))
s3 = replaygain(playlist(reload_mode="watch", check_next=check, "/home/pi/Conjurer/hit.playlist")) s3 = replaygain(playlist(reload_mode="watch", check_next=check, "/srv/betoniarka/data/hit.playlist"))
# Create a request queue for user-generated requests # Create a request queue for user-generated requests
requests_queue = request.queue() requests_queue = request.queue()
# Function to process the request queue and add new requests # Function to process the request queue and add new requests
def queue_processing() def queue_processing()
text=file.lines("/home/pi/Conjurer/request.playlist") text=file.lines("/srv/betoniarka/data/request.playlist")
if text != [] then if text != [] then
list.iter(fun(item) -> requests_queue.push.uri(item), text) list.iter(fun(item) -> requests_queue.push.uri(item), text)
file.remove("/home/pi/Conjurer/request.playlist") file.remove("/srv/betoniarka/data/request.playlist")
f = file.open("/home/pi/Conjurer/request.playlist", create=true) f = file.open("/srv/betoniarka/data/request.playlist", create=true)
f.close() f.close()
end end
end end
@@ -63,7 +63,7 @@ end
s4 = random(id="randomizer", weights=[2, 3, 5], [s1, s2, s3]) s4 = random(id="randomizer", weights=[2, 3, 5], [s1, s2, s3])
# Load jingles playlist # Load jingles playlist
jingles = (playlist(reload_mode="watch", "/home/pi/Conjurer/jingles.playlist")) jingles = (playlist(reload_mode="watch", "/srv/betoniarka/data/jingles.playlist"))
# Create the main stream with random playlist and jingles # Create the main stream with random playlist and jingles
s = rotate(id="randomizer", weights=[10, 1], [s4, jingles]) s = rotate(id="randomizer", weights=[10, 1], [s4, jingles])
@@ -111,7 +111,7 @@ s=switch(track_sensitive=true,
# Configure logging settings # Configure logging settings
log_to_stdout = true log_to_stdout = true
log_to_file = true log_to_file = true
logpath = "/home/pi/Conjurer/radio_log.log" logpath = "/srv/betoniarka/data/radio_log.log"
loglevel = 3 loglevel = 3
set("log.stdout", log_to_stdout) set("log.stdout", log_to_stdout)
set("log.level", loglevel) set("log.level", loglevel)
@@ -120,7 +120,7 @@ set("log.level", loglevel)
set("log.file", log_to_file) set("log.file", log_to_file)
set("log.file.path", logpath) set("log.file.path", logpath)
# Set up emergency fallback track # Set up emergency fallback track
emergency = single("/home/pi/MediaFolder/mp3/Youtube/Dr. Peacock - Trip to Ireland [GvrvQTUbUcA].mp3") emergency = single("/srv/betoniarka/music/Youtube/Dr. Peacock - Trip to Ireland [GvrvQTUbUcA].mp3")
radio = fallback(id="switcher2", track_sensitive=false, [s, emergency]) radio = fallback(id="switcher2", track_sensitive=false, [s, emergency])
# Set up an interactive control for skipping tracks # Set up an interactive control for skipping tracks
@@ -150,11 +150,11 @@ thread.run(every=15., check_skip)
# Enable persistent script parameters # Enable persistent script parameters
interactive.persistent("script.params") interactive.persistent("/srv/betoniarka/data/script.params")
# Configure output formats and destinations # Configure output formats and destinations
output.icecast(%mp3, host="radio", port=8000, password=credentials.password, icy_metadata="true", mount="mp3-stream", radio) output.icecast(%mp3, host="localhost", port=8000, password=credentials.password, icy_metadata="true", mount="mp3-stream", radio)
output.pulseaudio(radio) output.pulseaudio(radio)
#output.file.hls("/tmp/hls", [("mp3-low", %mp3(bitrate=96)), ("mp3-hi", %mp3(bitrate=160))], radio) #output.file.hls("/tmp/hls", [("mp3-low", %mp3(bitrate=96)), ("mp3-hi", %mp3(bitrate=160))], radio)
# Uncomment the following lines to enable additional output formats # Uncomment the following lines to enable additional output formats
+14 -8
View File
@@ -43,6 +43,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libavdevice-dev libswresample-dev libswscale-dev libpostproc-dev \ libavdevice-dev libswresample-dev libswscale-dev libpostproc-dev \
ffmpeg \ ffmpeg \
pulseaudio pulseaudio-utils \ pulseaudio pulseaudio-utils \
icecast2 jq \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Group membership for PulseAudio/ALSA access. On the Pi this was done by # Group membership for PulseAudio/ALSA access. On the Pi this was done by
@@ -75,20 +76,25 @@ RUN liquidsoap --version
COPY docker/pulse-system.pa /etc/pulse/system.pa COPY docker/pulse-system.pa /etc/pulse/system.pa
# The script and its persistent params are seeded into the data volume on # The script and its persistent params are seeded into the data volume on
# first run (never overwritten), so live edits survive image rebuilds. # first run (never overwritten), so live edits survive image rebuilds. The
# icecast config is rendered from the template at startup with passwords
# taken from the secrets volume (never baked into the image).
WORKDIR /app WORKDIR /app
COPY conjurer_musician/radio_conjurer.liq ./radio_conjurer.liq COPY conjurer_musician/radio_conjurer.liq ./radio_conjurer.liq
COPY conjurer_musician/script.params ./script.params COPY conjurer_musician/script.params ./script.params
COPY docker/icecast.xml.tpl ./icecast.xml.tpl
COPY docker/entrypoint.radio.sh /usr/local/bin/entrypoint.sh COPY docker/entrypoint.radio.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh RUN chmod +x /usr/local/bin/entrypoint.sh
# Mount points mirror the paths hardcoded in radio_conjurer.liq, so the # Volume layout matches the paths inside radio_conjurer.liq:
# script runs unmodified: data at /home/pi/Conjurer, music at # /srv/betoniarka/data - playlists, script.params, persistence/radio logs
# /home/pi/MediaFolder/mp3. # /srv/betoniarka/music - the mp3 library
VOLUME ["/home/pi/Conjurer", "/home/pi/MediaFolder/mp3"] # /srv/betoniarka/secrets - icecast_credentials.json (provisioned at install)
VOLUME ["/srv/betoniarka/data", "/srv/betoniarka/music", "/srv/betoniarka/secrets"]
# 54321 = harbor /skip (the bot's RADIO_HARBOR), 9999 = interactive harbor. # 8000 = icecast (listeners), 54321 = harbor /skip (the bot's RADIO_HARBOR),
EXPOSE 54321 9999 # 9999 = interactive harbor.
EXPOSE 8000 54321 9999
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["liquidsoap", "/home/pi/Conjurer/radio_conjurer.liq"] CMD ["liquidsoap", "/srv/betoniarka/data/radio_conjurer.liq"]
+11 -9
View File
@@ -1,4 +1,4 @@
# Radio (Liquidsoap) VM. Run from the repository root: # Radio (Liquidsoap + Icecast) VM. Run from the repository root:
# docker compose -f docker/compose.radio.yaml up -d --build # docker compose -f docker/compose.radio.yaml up -d --build
# #
# Version pins are build args - override via environment, e.g.: # Version pins are build args - override via environment, e.g.:
@@ -21,19 +21,21 @@ services:
# host = mount the host pulse socket below and set PULSE_SERVER # host = mount the host pulse socket below and set PULSE_SERVER
PULSE_MODE: ${PULSE_MODE:-internal} PULSE_MODE: ${PULSE_MODE:-internal}
# PULSE_SERVER: unix:/tmp/pulseaudio.socket # PULSE_SERVER: unix:/tmp/pulseaudio.socket
extra_hosts: # Hostname icecast reports in its status/YP pages:
# radio_conjurer.liq streams to host="radio" - point it at icecast. ICECAST_HOSTNAME: ${ICECAST_HOSTNAME:-localhost}
- "radio:${ICECAST_HOST_IP:-192.168.1.12}"
ports: ports:
- "8000:8000" # icecast - listeners tune in here
- "54321:54321" # harbor /skip - the bot's CONJURER_RADIO_HARBOR - "54321:54321" # harbor /skip - the bot's CONJURER_RADIO_HARBOR
- "9999:9999" # interactive harbor (keep LAN-only!) - "9999:9999" # interactive harbor (keep LAN-only!)
volumes: volumes:
# Playlists, logs, script.params, icecast_credentials.json, the script # Playlists, logs, script.params and the script itself (seeded on first
# itself (seeded on first run) - paths mirror the ones hardcoded in # run) - same paths inside and outside the container.
# radio_conjurer.liq so it runs unmodified. - /srv/betoniarka/data:/srv/betoniarka/data
- /srv/radio/data:/home/pi/Conjurer
# The mp3 library. Writable because the entrypoint seeds a silent # The mp3 library. Writable because the entrypoint seeds a silent
# emergency-fallback mp3 when the hardcoded single() file is missing. # emergency-fallback mp3 when the hardcoded single() file is missing.
- /srv/radio/music:/home/pi/MediaFolder/mp3 - /srv/betoniarka/music:/srv/betoniarka/music
# icecast_credentials.json - provision at install like the other
# services' secrets (a CHANGE_ME placeholder is seeded if absent).
- /srv/betoniarka/secrets:/srv/betoniarka/secrets
# PULSE_MODE=host: uncomment and adjust # PULSE_MODE=host: uncomment and adjust
# - /tmp/pulseaudio.socket:/tmp/pulseaudio.socket # - /tmp/pulseaudio.socket:/tmp/pulseaudio.socket
+34 -12
View File
@@ -1,12 +1,14 @@
#!/bin/sh #!/bin/sh
# Prepare the radio volumes so radio_conjurer.liq boots on a fresh VM, then # Prepare the radio volumes, start the in-container Icecast server, wire up
# start (or point at) PulseAudio and exec liquidsoap. Existing files are never # PulseAudio and exec liquidsoap. Existing files are never overwritten -
# overwritten - live-edited script/params/playlists always win. # live-edited script/params/playlists always win.
set -e set -e
DATA="${RADIO_DATA_DIR:-/home/pi/Conjurer}" DATA="${RADIO_DATA_DIR:-/srv/betoniarka/data}"
MUSIC="${RADIO_MUSIC_DIR:-/home/pi/MediaFolder/mp3}" MUSIC="${RADIO_MUSIC_DIR:-/srv/betoniarka/music}"
mkdir -p "$DATA" "$MUSIC" SECRETS="${RADIO_SECRETS_DIR:-/srv/betoniarka/secrets}"
CREDS="$SECRETS/icecast_credentials.json"
mkdir -p "$DATA" "$MUSIC" "$SECRETS"
# Seed the script + persistent interactive params from the image on first run. # Seed the script + persistent interactive params from the image on first run.
[ -e "$DATA/radio_conjurer.liq" ] || cp /app/radio_conjurer.liq "$DATA/" [ -e "$DATA/radio_conjurer.liq" ] || cp /app/radio_conjurer.liq "$DATA/"
@@ -21,14 +23,34 @@ for f in all_playlist.playlist priority_queue.playlist hit.playlist \
[ -e "$DATA/$f" ] || : > "$DATA/$f" [ -e "$DATA/$f" ] || : > "$DATA/$f"
done done
# Icecast credentials are a secret - never baked into the image. Seed a # The icecast secret is provisioned at install time, like the other services'
# placeholder so the script can parse the JSON, but shout about it. # secrets (bot: /srv/conjurer/secrets/.netrc). A placeholder keeps the stack
if [ ! -e "$DATA/icecast_credentials.json" ]; then # bootable, but both icecast and the stream stay locked until you fix it.
printf '{\n"password" : "CHANGE_ME"\n}\n' > "$DATA/icecast_credentials.json" if [ ! -e "$CREDS" ]; then
echo "WARNING: seeded placeholder icecast_credentials.json - the icecast" >&2 printf '{\n"password" : "CHANGE_ME"\n}\n' > "$CREDS"
echo " output will be rejected until you set the real password!" >&2 echo "WARNING: $CREDS was missing - seeded a CHANGE_ME placeholder." >&2
echo " Put the real password there (see docs) and restart." >&2
fi fi
# Render /etc/icecast2/icecast.xml from the template with passwords from the
# secret. Optional fields admin_password/relay_password default to password.
SOURCE_PW=$(jq -r '.password' "$CREDS")
ADMIN_PW=$(jq -r '.admin_password // .password' "$CREDS")
RELAY_PW=$(jq -r '.relay_password // .password' "$CREDS")
ICECAST_HOSTNAME="${ICECAST_HOSTNAME:-localhost}"
sed -e "s|__SOURCE_PASSWORD__|$SOURCE_PW|" \
-e "s|__ADMIN_PASSWORD__|$ADMIN_PW|" \
-e "s|__RELAY_PASSWORD__|$RELAY_PW|" \
-e "s|__HOSTNAME__|$ICECAST_HOSTNAME|" \
/app/icecast.xml.tpl > /etc/icecast2/icecast.xml
chown icecast2:icecast /etc/icecast2/icecast.xml 2>/dev/null || true
chmod 640 /etc/icecast2/icecast.xml
# Start Icecast in the background as its unprivileged user.
mkdir -p /var/log/icecast2 && chown -R icecast2:icecast /var/log/icecast2
su -s /bin/sh icecast2 -c "icecast2 -b -c /etc/icecast2/icecast.xml" \
|| echo "WARNING: icecast2 failed to start - the stream output will retry" >&2
# single() aborts the whole script when its file is missing; guarantee the # single() aborts the whole script when its file is missing; guarantee the
# emergency fallback exists (5s of silence beats a dead radio). # emergency fallback exists (5s of silence beats a dead radio).
EMERGENCY="$MUSIC/Youtube/Dr. Peacock - Trip to Ireland [GvrvQTUbUcA].mp3" EMERGENCY="$MUSIC/Youtube/Dr. Peacock - Trip to Ireland [GvrvQTUbUcA].mp3"
+55
View File
@@ -0,0 +1,55 @@
<!-- Icecast2 config template for the radio container.
The entrypoint substitutes the __*_PASSWORD__ placeholders from
/srv/betoniarka/secrets/icecast_credentials.json and writes the result
to /etc/icecast2/icecast.xml. Never commit real passwords here. -->
<icecast>
<location>Wolne Ksiestwo Baluty</location>
<admin>admin@localhost</admin>
<limits>
<clients>64</clients>
<sources>4</sources>
<queue-size>524288</queue-size>
<client-timeout>30</client-timeout>
<header-timeout>15</header-timeout>
<source-timeout>10</source-timeout>
<burst-on-connect>1</burst-on-connect>
<burst-size>65535</burst-size>
</limits>
<authentication>
<source-password>__SOURCE_PASSWORD__</source-password>
<relay-password>__RELAY_PASSWORD__</relay-password>
<admin-user>admin</admin-user>
<admin-password>__ADMIN_PASSWORD__</admin-password>
</authentication>
<hostname>__HOSTNAME__</hostname>
<listen-socket>
<port>8000</port>
<bind-address>0.0.0.0</bind-address>
</listen-socket>
<http-headers>
<header name="Access-Control-Allow-Origin" value="*" />
</http-headers>
<fileserve>1</fileserve>
<paths>
<basedir>/usr/share/icecast2</basedir>
<logdir>/var/log/icecast2</logdir>
<webroot>/usr/share/icecast2/web</webroot>
<adminroot>/usr/share/icecast2/admin</adminroot>
<alias source="/" destination="/status.xsl"/>
</paths>
<logging>
<accesslog>access.log</accesslog>
<errorlog>error.log</errorlog>
<loglevel>3</loglevel>
<logsize>10000</logsize>
<logarchive>0</logarchive>
</logging>
</icecast>
+33 -17
View File
@@ -235,26 +235,42 @@ those two files into the same `/srv/musician/data` directory (or set
| `OPAM_VERSION` | `2.1.5` | static binary from GitHub releases | | `OPAM_VERSION` | `2.1.5` | static binary from GitHub releases |
| `LIQ_OPAM_PACKAGES` | `mad lame cry taglib pulseaudio samplerate inotify ffmpeg` | exactly the features the script uses (mp3 in/out, icecast, tags/replaygain, pulse mic/out, watch-reload) | | `LIQ_OPAM_PACKAGES` | `mad lame cry taglib pulseaudio samplerate inotify ffmpeg` | exactly the features the script uses (mp3 in/out, icecast, tags/replaygain, pulse mic/out, watch-reload) |
```bash The container runs **both Liquidsoap and Icecast** — the stream is served
sudo mkdir -p /srv/radio/data /srv/radio/music from this one container (`output.icecast(host="localhost", …)` in the
# put the real password in place (otherwise a CHANGE_ME placeholder is seeded): script). Volume layout (same paths inside and outside):
echo '{ "password" : "..." }' | sudo tee /srv/radio/data/icecast_credentials.json
ICECAST_HOST_IP=192.168.1.12 docker compose -f docker/compose.radio.yaml up -d --build | Host & container path | Holds |
|---|---|
| `/srv/betoniarka/data` | playlists, `script.params`, `persistence.log`, `radio_log.log`, the seeded `radio_conjurer.liq` |
| `/srv/betoniarka/music` | the mp3 library |
| `/srv/betoniarka/secrets` | `icecast_credentials.json`**provision at install**, like the other services' secrets |
```bash
sudo mkdir -p /srv/betoniarka/data /srv/betoniarka/music /srv/betoniarka/secrets
# provision the icecast secret (same pattern as the bot's netrc):
echo '{ "password" : "SOURCE_PW", "admin_password" : "ADMIN_PW" }' \
| sudo tee /srv/betoniarka/secrets/icecast_credentials.json
sudo chmod 600 /srv/betoniarka/secrets/icecast_credentials.json
docker compose -f docker/compose.radio.yaml up -d --build
docker logs -f conjurer-radio docker logs -f conjurer-radio
``` ```
The container mounts data at **`/home/pi/Conjurer`** and music at At startup the entrypoint renders `/etc/icecast2/icecast.xml` from
**`/home/pi/MediaFolder/mp3`** — the exact paths hardcoded in the script — so `docker/icecast.xml.tpl`, filling the source/admin/relay passwords from the
`radio_conjurer.liq` runs **unmodified**. The script + `script.params` are secret (`admin_password`/`relay_password` are optional and default to
seeded into the volume on first run and never overwritten (live edits `password`), and starts icecast as its unprivileged user. If the secret is
survive rebuilds). Missing playlists are created empty and a silent missing, a `CHANGE_ME` placeholder is seeded with a loud warning — the stack
emergency-fallback mp3 is generated if the `single()` file is absent, so the boots but the stream stays locked until you fix it.
script always boots. `host="radio"` (the icecast output) resolves via the
compose `extra_hosts` entry — set `ICECAST_HOST_IP`.
Point the bot at this VM: `CONJURER_RADIO_HARBOR=http://RADIO_VM_IP:54321` The script + `script.params` are seeded into the data volume on first run
(the harbor `/skip` endpoint lives in the script itself). and never overwritten (live edits survive rebuilds). Missing playlists are
created empty and a silent emergency-fallback mp3 is generated if the
`single()` file is absent, so the script always boots.
Wire-up: listeners tune to `http://RADIO_VM_IP:8000/mp3-stream`; the bot
points at `CONJURER_RADIO_HARBOR=http://RADIO_VM_IP:54321` (harbor `/skip`
lives in the script itself).
**PulseAudio** (`PULSE_MODE` env): **PulseAudio** (`PULSE_MODE` env):
- `internal` (default) — a system-wide pulse daemon runs inside the container - `internal` (default) — a system-wide pulse daemon runs inside the container
@@ -272,8 +288,8 @@ Point the bot at this VM: `CONJURER_RADIO_HARBOR=http://RADIO_VM_IP:54321`
for 2.1.x are compiled against Debian bookworm's FFmpeg 5.x sonames, and for 2.1.x are compiled against Debian bookworm's FFmpeg 5.x sonames, and
the pin forced those over conflicting Raspberry Pi OS repo builds (even as the pin forced those over conflicting Raspberry Pi OS repo builds (even as
a downgrade). In this single-repo container the same versions come a downgrade). In this single-repo container the same versions come
naturally, so **no pin is needed** — documented here so it doesn't get naturally, so the pin is redundant — **the file has been removed from the
cargo-culted forward. repo** (this note preserves the knowledge).
- adding root to `pulse-access`/`audio` groups: needed on the Pi for the - adding root to `pulse-access`/`audio` groups: needed on the Pi for the
system-wide pulse socket and ALSA device access. The image bakes the same system-wide pulse socket and ALSA device access. The image bakes the same
memberships in (`usermod -aG audio,pulse-access root`) — harmless with the memberships in (`usermod -aG audio,pulse-access root`) — harmless with the
-3
View File
@@ -1,3 +0,0 @@
Package: ffmpeg libavcodec-dev libavcodec59 libavdevice59 libavfilter8 libavformat-dev libavformat59 libavutil-dev libavutil57 libpostproc56 libswresample-dev libswresample4 libswscale-dev libswscale6 libavdevice-dev libavfilter-dev libpostproc-dev
Pin: origin deb.debian.org
Pin-Priority: 1001
+1 -1
View File
@@ -27,7 +27,7 @@ sudo usermod -aG audio root
echo "Add exception suppresion to signal handler in spotify __ini__.py" echo "Add exception suppresion to signal handler in spotify __ini__.py"
echo "Add password to youtube opts in spotify_dl youtube.py" echo "Add password to youtube opts in spotify_dl youtube.py"
echo "Downgrade ffmpeg by copying ffmpeg.pref from repository to /etc/apt/preferences.d" echo "NOTE: the old ffmpeg.pref pin is obsolete (containerised radio uses Debian bookworm ffmpeg 5.x natively; file removed from the repo)"
echo "Install opam from installation link" echo "Install opam from installation link"
echo "Initialize opam" echo "Initialize opam"
echo "Install liquidsoap and its dependencies" echo "Install liquidsoap and its dependencies"