Forum teuk.org

Radio stack on Debian 13: Icecast2, Liquidsoap, systemd, and bot integration

in Mediabot · started by TeuK · 6d ago

TeuK · 6d ago

Radio stack on Debian 13: Icecast2, Liquidsoap, systemd, and bot integration

This write-up documents the radio stack we just set up for Mediabot v3 on Debian 13.

The goal was not to touch the existing production stream on port 15000, but to build a clean test instance on port 8000:

  • a dedicated Icecast2 instance
  • a dedicated Liquidsoap instance
  • dedicated systemd units
  • separate logs and runtime files
  • a clean Mediabot integration through Icecast JSON status

That separation matters. It lets you test and iterate without risking the production radio.


1. Install Icecast2 on Debian 13

icecast2 is available in Debian 13, and Icecast itself is controlled primarily through an XML configuration file passed with -c. Debian’s own manpage explicitly says the XML config is the source of truth for runtime behavior.

Install the package:

apt update
apt install icecast2

For convenience during testing, also install a couple of tools:

apt install curl jq

2. Minimal dedicated Icecast2 config for port 8000

Do not reuse the production config file directly. Create a dedicated one for the test instance.

Create /etc/icecast2/icecast-8000.xml:

<icecast>
    <location>Earth</location>
    <admin>icemaster@localhost</admin>

    <limits>
        <clients>50</clients>
        <sources>3</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>w3bradiale</source-password>
        <relay-password>ic3cast4dmin#</relay-password>
        <admin-user>admin</admin-user>
        <admin-password>**********</admin-password>
    </authentication>

    <hostname>teuk.org</hostname>

    <listen-socket>
        <port>8000</port>
    </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-8000</logdir>
        <webroot>/usr/share/icecast2/web</webroot>
        <adminroot>/usr/share/icecast2/admin</adminroot>
        <pidfile>/run/icecast2-8000/icecast.pid</pidfile>
        <alias source="/" destination="/status.xsl"/>
    </paths>

    <logging>
        <accesslog>access.log</accesslog>
        <errorlog>error.log</errorlog>
        <loglevel>3</loglevel>
        <logsize>10000</logsize>
    </logging>

    <security>
        <chroot>0</chroot>
    </security>

    <mount type="default">
        <charset>UTF-8</charset>
    </mount>
</icecast>

Create the runtime and log directories:

mkdir -p /var/log/icecast2-8000
mkdir -p /run/icecast2-8000
chown -R icecast2:icecast2 /var/log/icecast2-8000 /run/icecast2-8000
chmod 755 /var/log/icecast2-8000 /run/icecast2-8000

Validate the config before starting the service:

icecast2 -t -c /etc/icecast2/icecast-8000.xml

3. Dedicated systemd unit for Icecast2

Create /etc/systemd/system/icecast2-8000.service:

[Unit]
Description=Icecast2 test instance on port 8000
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=icecast2
Group=icecast2
RuntimeDirectory=icecast2-8000
RuntimeDirectoryMode=0755
ExecStart=/usr/bin/icecast2 -c /etc/icecast2/icecast-8000.xml
Restart=on-failure
RestartSec=3
NoNewPrivileges=true
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Then enable and start it:

systemctl daemon-reload
systemctl enable --now icecast2-8000.service

Check that it is listening:

ss -lntp | egrep ':8000\b'
systemctl status icecast2-8000.service --no-pager -l

4. Install Liquidsoap on Debian 13

liquidsoap is also available in Debian 13. Debian describes it as a scripting language for complex audio streaming systems, especially Internet radio/icecast setups. citeturn938244search1turn938244search3

Install it with:

apt update
apt install liquidsoap

5. Dedicated Liquidsoap config using a playlist M3U

The working model here is deliberately simple:

  • one playlist file (.m3u)
  • one live harbor input for future live shows
  • one fallback
  • three MP3 outputs to the dedicated Icecast2 instance on 8000

Create /etc/liquidsoap/radio-8000.liq:

#!/usr/bin/liquidsoap --debug -U

settings.init.allow_root.set(false)

set("log.file.path","/var/log/liquidsoap/liquidsoap-8000.log")
set("log.level",3)
set("scheduler.log",true)
set("decoder.debug",false)

# systemd manages the process, not Liquidsoap itself
set("init.daemon",false)

set("server.telnet",true)
set("server.telnet.bind_addr","127.0.0.1")
set("server.telnet.port",1235)

set("harbor.reverse_dns",true)

set("audio.converter.samplerate.libsamplerate.quality","fast")

global_playlist = playlist("/home/airtime/m3u/playlist_23052014.m3u",reload=0,reload_mode="rounds")
global_playlist = audio_to_stereo(global_playlist)

live = input.harbor("/",port=18005,password="w3bradiale",icy=true)

radio = global_playlist
full = fallback(track_sensitive=false,[live,radio])

output.icecast(%mp3(stereo=true, samplerate=44100, bitrate=160),
  host="localhost",
  port=8000,
  password="**********",
  mount="/radio160.mp3",
  description="TeuKRadio TEST",
  url="http://teuk.org",
  public=false,
  fallible=true,
  full)

output.icecast(%mp3(stereo=true, samplerate=44100, bitrate=320),
  host="localhost",
  port=8000,
  password="**********",
  mount="/radio320.mp3",
  description="TeuKRadio TEST",
  url="http://teuk.org",
  public=false,
  fallible=true,
  full)

output.icecast(%mp3(stereo=true, samplerate=44100, bitrate=64),
  host="localhost",
  port=8000,
  password="**********",
  mount="/radio64.mp3",
  description="TeuKRadio TEST",
  url="http://teuk.org",
  public=false,
  fallible=true,
  full)

Important notes

  • This instance is not started as root.
  • set("init.daemon",false) is important because systemd is supervising the process. Letting Liquidsoap daemonize itself under systemd is asking for trouble.
  • The playlist is read as-is from the M3U path:
playlist("/home/airtime/m3u/playlist_23052014.m3u",reload=0,reload_mode="rounds")

That means Liquidsoap follows the file exactly in the order defined there, round after round.

Create the log directory if needed:

mkdir -p /var/log/liquidsoap
chown -R liquidsoap:liquidsoap /var/log/liquidsoap
chmod 755 /var/log/liquidsoap

Validate the script before starting the service:

liquidsoap -c /etc/liquidsoap/radio-8000.liq

6. Dedicated systemd unit for Liquidsoap

Create /etc/systemd/system/liquidsoap-radio8000.service:

[Unit]
Description=Liquidsoap test instance for Icecast2 port 8000
After=network-online.target icecast2-8000.service
Wants=network-online.target
Requires=icecast2-8000.service

[Service]
Type=simple
User=liquidsoap
Group=liquidsoap
RuntimeDirectory=liquidsoap
RuntimeDirectoryMode=0755
ExecStart=/usr/bin/liquidsoap /etc/liquidsoap/radio-8000.liq
Restart=on-failure
RestartSec=3
NoNewPrivileges=true
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Enable and start it:

systemctl daemon-reload
systemctl enable --now liquidsoap-radio8000.service

Check the service and listening sockets:

systemctl status liquidsoap-radio8000.service --no-pager -l
ss -lntp | egrep ':1235\b|:18005\b|:8000\b'
tail -n 100 /var/log/liquidsoap/liquidsoap-8000.log

7. Verify that Icecast2 really sees the mounts

Do not guess. Query the local instance directly.

Check JSON status:

curl -s http://127.0.0.1:8000/status-json.xsl | jq .

Check admin stats:

curl -s -u admin:'**********' http://127.0.0.1:8000/admin/stats

A working instance should expose mounts such as:

  • /radio64.mp3
  • /radio160.mp3
  • /radio320.mp3

This is exactly the kind of data Mediabot now reads from Icecast JSON. The project roadmap explicitly aimed for a radio service on 8000 that Mediabot can observe cleanly before moving on to the web UI. citeturn25file4turn25file5


8. Mediabot configuration

The current sample config still shows the older [radio] keys like RADIO_PORT, RADIO_JSON, RADIO_HOSTNAME, RADIO_URL, and Liquidsoap telnet settings. fileciteturn25file0

For the new Icecast-based status flow, add these keys to the [radio] section of mediabot.conf:

[radio]
RADIO_PUB=900
RADIO_PORT=8000
RADIO_SOURCE=0
YOUTUBEDL_INCOMING=/home/mediabot/mediabot_v3/mp3
LIQUIDSOAP_PLAYLIST=plglob(dot)m3u
RADIO_ADMINPASS=**********
LIQUIDSOAP_TELNET_PORT=1234
RADIO_HOSTNAME=myradio.com
YTDLP_PATH=/usr/local/bin/yt-dlp
LIQUIDSOAP_TELNET_HOST=localhost
RADIO_URL=radio.mp3
RADIO_JSON=status-json.xsl

RADIO_ICECAST_STATUS_BASE_URL=http://127.0.0.1:8000
RADIO_ICECAST_PUBLIC_BASE_URL=http://teuk.org:8000
RADIO_ICECAST_TIMEOUT=5
RADIO_ICECAST_PRIMARY_MOUNT=/radio160.mp3

These new keys are used by the new Icecast reader and by the local radio JSON API exposed by Mediabot.

The live mediabot.conf you shared still only had the older radio keys before this update, so adding the RADIO_ICECAST_* values is required for the new path to work cleanly. fileciteturn25file3


9. Sample config files to commit

If this setup is going to live in the repository, commit sample files too. At minimum:

  • contrib/icecast2/icecast-8000.sample.xml
  • contrib/liquidsoap/radio-8000.sample.liq
  • updated mediabot.sample.conf

That way the repository documents not just the bot side, but the service layout around it.

The existing mediabot.sample.conf still reflects the older radio section and should be updated with the new RADIO_ICECAST_* keys. fileciteturn25file0


10. Sanity checks after deployment

A quick checklist:

# Icecast
systemctl status icecast2-8000.service --no-pager -l
curl -s http://127.0.0.1:8000/status-json.xsl | jq .

# Liquidsoap
systemctl status liquidsoap-radio8000.service --no-pager -l
tail -n 100 /var/log/liquidsoap/liquidsoap-8000.log

# Mediabot local radio API
curl -s http://127.0.0.1:9108/api/radio/status | jq .

If all of that works, you have:

  • a dedicated Icecast2 instance on 8000
  • a dedicated Liquidsoap instance feeding it
  • a clean separation from the production stream on 15000
  • a usable radio status source for Mediabot
  • the right foundation for the future Node.js interface

11. Why this approach is the right one

The roadmap was very clear: first stabilize the bot, then build a clean radio brick on 8000, then expose that state to the future web UI. fileciteturn25file1turn25file4turn25file5

That is exactly what this setup achieves.

Instead of coupling everything too early, the stack is now split into sane layers:

  1. Icecast2 / Liquidsoap test stack
  2. Mediabot radio reader via Icecast JSON
  3. local API for future Node.js consumption

That is a much better base than trying to bolt new features directly onto the old production radio flow.

You must be logged in to reply.