Post

Automating Icecast Song Metadata with a systemd Service

Automating Icecast Song Metadata with a systemd Service

When we listen to KAAD’s internet stream in the car, it is a lot nicer when the stream shows the song title and artist instead of a blank field. It is a small thing, but it makes the station feel more complete and easier to use in players like VLC and CarPlay.

KAAD’s audio setup sends the music fine, but it does not send track names on its own. To fix that, I use ACRCloud, which is a music recognition service (basically Shazam-style matching for streams). It listens to the audio feed, identifies what song is playing, and gives that result back through an API.

From there, I run a small tool called slogger that takes that song info and updates Icecast automatically.

This post shows how I run slogger on kaad-one so now-playing metadata stays current without anyone needing to type it in by hand.

What we’re talking about

  • slogger: The local metadata updater tooling and scripts.
  • ACRCloud: Provides recent track recognition results from the monitored stream.
  • Icecast admin metadata: Endpoint used to update the current song text.
  • Bash updater: Polls ACRCloud, parses JSON, and updates Icecast on change.
  • systemd service: Keeps the updater running across reboots and crashes.
  • Env file: Stores API tokens and credentials outside the script.

Why this setup

  • It avoids manual metadata entry during live operation.
  • The script is easy to audit and troubleshoot.
  • systemd is reliable and already present on modern Linux hosts.
  • Secrets stay in a local env file instead of hardcoded script text.
  • Metadata updates are deduplicated, so Icecast is not spammed with repeats.

Files

  • Script (slogger): /home/user/slogger/icecast-meta.sh
  • Env file (slogger): /home/user/slogger/icecast-meta.env
  • Service on kaad-one: /etc/systemd/system/icecast-meta.service

1) Keep runtime values in an env file

Create an env file with API and Icecast settings:

/home/user/slogger/icecast-meta.env

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ACRCloud API endpoint + token
ACR_URL="https://api-v2.acrcloud.com/api/bm-cs-projects/PROJECT_ID/streams/STREAM_ID/results?type=last"
ACR_TOKEN="REDACTED"

# Icecast server/admin settings
ICECAST_SCHEME="https"
ICECAST_HOST="your-icecast-host"
ICECAST_PORT="11555"
ICECAST_USER="admin"
ICECAST_PASS="REDACTED"
ICECAST_MOUNT="stream"

# Polling + TLS behavior
POLL_SECONDS="20"
CURL_INSECURE="0"

Minimum permission baseline:

1
2
sudo chown user:user /home/user/slogger/icecast-meta.env
sudo chmod 600 /home/user/slogger/icecast-meta.env

2) Metadata update script

The updater script:

  • loads env values,
  • polls ACRCloud JSON with bearer auth,
  • extracts artist and title with jq fallbacks,
  • builds a normalized Artist - Title value,
  • deduplicates by track ID (or normalized song fallback),
  • updates Icecast metadata only on change.

/home/user/slogger/icecast-meta.sh

Show icecast-meta.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="${ENV_FILE:-${SCRIPT_DIR}/icecast-meta.env}"
if [[ -f "$ENV_FILE" ]]; then
  # shellcheck disable=SC1090
  source "$ENV_FILE"
fi

ACR_URL="${ACR_URL:-https://api-v2.acrcloud.com/api/bm-cs-projects/PROJECT_ID/streams/STREAM_ID/results?type=last}"
ACR_TOKEN="${ACR_TOKEN:-}"

ICECAST_HOST="${ICECAST_HOST:-your-icecast-host}"
ICECAST_PORT="${ICECAST_PORT:-11555}"
ICECAST_SCHEME="${ICECAST_SCHEME:-https}"
ICECAST_USER="${ICECAST_USER:-admin}"
ICECAST_PASS="${ICECAST_PASS:-}"
ICECAST_MOUNT="${ICECAST_MOUNT:-stream}"

POLL_SECONDS="${POLL_SECONDS:-20}"
CURL_INSECURE="${CURL_INSECURE:-0}"

: "${ACR_TOKEN:?ACR_TOKEN is required}"
: "${ICECAST_PASS:?ICECAST_PASS is required}"

auth_header_file="$(mktemp)"
chmod 600 "$auth_header_file"
printf 'Authorization: Bearer %s\n' "$ACR_TOKEN" > "$auth_header_file"
trap 'rm -f "$auth_header_file"' EXIT

last_song_key=""
curl_tls_opts=()
if [[ "$CURL_INSECURE" == "1" ]]; then
  curl_tls_opts+=(-k)
fi

while true; do
  json="$({
    curl -fsS "${curl_tls_opts[@]}" \
      -H "Accept: application/json" \
      -H "@${auth_header_file}" \
      "$ACR_URL"
  } || true)"

  if [[ -n "$json" ]]; then
    title="$(jq -r '
      [
        (.. | objects | .metadata?.music?[0]?.title?),
        (.. | objects | .music?[0]?.title?),
        (.. | objects | .title?)
      ]
      | map(select(type == "string" and length > 0))
      | .[0] // empty
    ' <<<"$json")"

    artist="$(jq -r '
      [
        (.. | objects | .metadata?.music?[0]?.artists?[0]?.name?),
        (.. | objects | .music?[0]?.artists?[0]?.name?),
        (.. | objects | .artist?.name?),
        (.. | objects | .artist?)
      ]
      | map(select(type == "string" and length > 0))
      | .[0] // empty
    ' <<<"$json")"

    if [[ -n "$artist" && -n "$title" ]]; then
      song="${artist} - ${title}"
    elif [[ -n "$title" ]]; then
      song="$title"
    else
      song=""
    fi

    song="$(jq -rn --arg s "$song" '$s | gsub("\\s+";" ") | gsub("^\\s+|\\s+$";"")')"

    track_id="$(jq -r '
      [
        (.. | objects | .metadata?.music?[0]?.acrid?),
        (.. | objects | .music?[0]?.acrid?),
        (.. | objects | .result_id?),
        (.. | objects | .id?)
      ]
      | map(select(type == "string" and length > 0))
      | .[0] // empty
    ' <<<"$json")"

    if [[ -n "$track_id" ]]; then
      song_key="$track_id"
    else
      song_key="$(jq -rn --arg s "$song" '$s | ascii_downcase')"
    fi

    if [[ -n "$song" && "$song_key" != "$last_song_key" ]]; then
      curl -fsS "${curl_tls_opts[@]}" \
        -u "${ICECAST_USER}:${ICECAST_PASS}" \
        --get "${ICECAST_SCHEME}://${ICECAST_HOST}:${ICECAST_PORT}/admin/metadata" \
        --data-urlencode "mount=/${ICECAST_MOUNT#/}" \
        --data-urlencode "mode=updinfo" \
        --data-urlencode "song=${song}" \
        >/dev/null

      echo "Updated metadata: ${song}"
      last_song_key="$song_key"
    fi
  fi

  sleep "$POLL_SECONDS"
done

Make it executable:

1
chmod 750 /home/user/slogger/icecast-meta.sh

3) systemd service

Create a dedicated service so metadata updates survive reboots and process exits.

/etc/systemd/system/icecast-meta.service

Show icecast-meta.service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[Unit]
Description=Icecast metadata updater from ACRCloud
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=user
Group=user
WorkingDirectory=/home/user/slogger
Environment=ENV_FILE=/home/user/slogger/icecast-meta.env
ExecStart=/home/user/slogger/icecast-meta.sh
Restart=always
RestartSec=2
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/home/user/slogger

[Install]
WantedBy=multi-user.target

Enable it:

1
2
sudo systemctl daemon-reload
sudo systemctl enable --now icecast-meta.service

4) Useful checks

For day-to-day operation:

1
2
3
4
sudo systemctl status icecast-meta.service
sudo journalctl -u icecast-meta.service -f
sudo journalctl -u icecast-meta.service -n 50 --no-pager
sudo systemctl restart icecast-meta.service

Expected healthy log lines include:

1
Updated metadata: Artist - Track Title

If you see repeated HTTP 403 errors, validate token scope/expiry and confirm the ACR_URL still points to the correct project stream.

Security notes

  • Never commit real .env files or raw tokens/passwords.
  • Keep credentials file permissions tight (chmod 600).
  • Use least-privilege API credentials where possible.
  • Rotate secrets immediately if there is any chance they were exposed.

This setup gives a straightforward, auditable metadata path with native Linux components and very little operational overhead.

This post is licensed under CC BY 4.0 by the author.