This commit is contained in:
G. Gibson 2026-09-01 09:57:29 -07:00
commit 11e1292a75
3 changed files with 55 additions and 52 deletions

2
.gitignore vendored
View file

@ -1,6 +1,6 @@
# Secrets and local configuration # Secrets and local configuration
config.json config.json
dirs.txt
# Runtime data (should live under the storage path, not here) # Runtime data (should live under the storage path, not here)
.venv/ .venv/
state/ state/

12
check_dirs.sh Executable file
View file

@ -0,0 +1,12 @@
#!/bin/bash
STORAGE="/mnt/storage/radio"
JINGLES="no"
ANNOUNCEMENTS="no"
if find "$STORAGE/jingles" -type f \( -name "*.mp3" -o -name "*.m4a" \) 2>/dev/null | grep -q .; then
JINGLES="yes"
fi
if find "$STORAGE/announcements" -type f \( -name "*.mp3" -o -name "*.m4a" \) 2>/dev/null | grep -q .; then
ANNOUNCEMENTS="yes"
fi
echo "jingles=$JINGLES" > /srv/radio/dirs.txt
echo "announcements=$ANNOUNCEMENTS" >> /srv/radio/dirs.txt

View file

@ -2,18 +2,20 @@
# --------------------------------------------------------------- # ---------------------------------------------------------------
# station.liq - Liquidsoap configuration for radio automation # station.liq - Liquidsoap configuration for radio automation
# Auto-detects its own location so all paths are relative. # Written for Liquidsoap 2.2.x. Run from /srv/radio so that the
# Music/jingles/announcements live under the storage path from # relative config.json path resolves correctly; media lives under
# config.json; code stays in the install dir. # the storage path read from that file. Directory playlists scan
# recursively for audio files automatically.
# --------------------------------------------------------------- # ---------------------------------------------------------------
configure.bindir()
set("log.stdout", true) set("log.stdout", true)
set("log.level", 3) set("log.level", 3)
# --- Load Icecast credentials + storage path from config.json --- # --- Step 1: path to config.json (relative to working directory) ---
let cfg : { let config_path = "config.json"
# --- Step 2: read and parse the whole file with explicit types --
let json.parse (cfg : {
storage: string, storage: string,
icecast: { icecast: {
host: string, host: string,
@ -21,82 +23,71 @@ let cfg : {
mount: string, mount: string,
username: string, username: string,
password: string password: string
},
gpodder: {
enable: bool,
host: string,
username: string,
password: string
} }
} = json.parse(file.contents("#{bindir()}/config.json")) }) = file.contents(config_path)
# --- Step 3: pull out the values we need ------------------------
let storage = cfg.storage
let ic_host = cfg.icecast.host let ic_host = cfg.icecast.host
let ic_port = cfg.icecast.port let ic_port = cfg.icecast.port
let ic_mount = cfg.icecast.mount let ic_mount = cfg.icecast.mount
let ic_username = cfg.icecast.username let ic_username = cfg.icecast.username
let ic_password = cfg.icecast.password let ic_password = cfg.icecast.password
let storage = cfg.storage
set("log.file.path", "#{storage}/logs/liquidsoap.log") set("log.file.path", "#{storage}/logs/liquidsoap.log")
# --- Background music library (recursive scan for mp3/m4a) --- # --- Background music library (directory scanned recursively) --
music_playlist = music_playlist =
request.cue( playlist("#{storage}/music")
playlist(
recurse=true,
pattern="\\.(mp3|m4a)$",
"#{storage}/music"
)
)
# --- Scheduled content: request queue fed by schedule.txt --- # --- Scheduled content: request queue fed by schedule.txt ------
sched_queue = request.queue(id="scheduler") sched_queue = request.queue(id="scheduler")
# --- Jingles / announcements (optional) --- # --- Helper: check if a directory contains any audio files -----
jingle_dir = "#{storage}/jingles"
announce_dir = "#{storage}/announcements"
def has_audio(dir) = def has_audio(dir) =
try try
let l = list.filter(fun(f) -> regexp("\\.(mp3|m4a)$").test(f), ls.dir(dir)) let entries = file.ls(dir)
list.length(l) > 0 let audio_count = list.length(
list.filter(fun(f) -> regexp("\\.(mp3|m4a)$").test(f), entries)
)
audio_count > 0
catch _ do catch _ do
false false
end end
end
jingles_src = # --- Jingles / announcements (only active if they have content) ---
if has_audio(jingle_dir) then jingles_active = has_audio("#{storage}/jingles")
mksafe(request.cue(playlist(recurse=true, pattern="\\.(mp3|m4a)$", jingle_dir))) announcements_active = has_audio("#{storage}/announcements")
else
null()
end
announcements_src = log("#[info] Jingles directory has audio: #{string(jingles_active)}")
if has_audio(announce_dir) then log("#[info] Announcements directory has audio: #{string(announcements_active)}")
mksafe(request.cue(playlist(recurse=true, pattern="\\.(mp3|m4a)$", announce_dir)))
else
null()
end
# --- Assemble the stream --- # --- Assemble the stream ---------------------------------------
primary = fallback(track_sensitive=false, [sched_queue, music_playlist]) primary = fallback(track_sensitive=false, [sched_queue, music_playlist])
content = content =
match jingles_src with if jingles_active then
| null() => primary random(weights=[1, 8], [
| src => random(weights=[1, 8], [src, primary]) mksafe(playlist("#{storage}/jingles")),
primary
])
else
primary
end end
final_content = final_content =
match announcements_src with if announcements_active then
| null() => content fallback(track_sensitive=false, [
| src => fallback(track_sensitive=false, [src, content]) mksafe(playlist("#{storage}/announcements")),
content
])
else
content
end end
radio = normalize(final_content) radio = normalize(final_content)
# --- Output to Icecast --- # --- Output to Icecast -----------------------------------------
output.icecast( output.icecast(
%mp3(bitrate=128, samplerate=44100, stereo=true), %mp3(bitrate=128, samplerate=44100, stereo=true),
host=ic_host, host=ic_host,