mirror of
https://github.com/migatu/conjurer.git
synced 2026-07-14 13:34:40 +00:00
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:
@@ -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
@@ -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"]
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user