radio/config/station.liq
profit 3d635b742c Initial commit: self-hosted personal radio station
Flask + React web UI with audio player, podcast queue, feed management,
episode browser, music library, schedule viewer, and log tail.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 19:01:33 -07:00

157 lines
4.5 KiB
Plaintext

#!/usr/bin/liquidsoap
# =============================================================================
# Local Radio Station — Liquidsoap Configuration
# =============================================================================
# Source priority (highest to lowest):
# 1. Podcast queue (from queue/ directory)
# 2. Scheduled music (time/day-based playlists)
# 3. Emergency fallback (static safe playlist)
# =============================================================================
settings.log.file.path := "/opt/localradio/logs/liquidsoap.log"
settings.log.level := 3
# --- Paths -------------------------------------------------------------------
music_base = "/opt/localradio/media/music"
queue_dir = "/opt/localradio/state/queue"
fallback_dir = "/opt/localradio/media/fallback"
# --- Icecast output settings -------------------------------------------------
icecast_host = "localhost"
icecast_port = 8000
icecast_password = "localradio_source"
icecast_mount = "/stream"
# --- Emergency Fallback ------------------------------------------------------
# A safe playlist that always has content. Put at least one audio file in
# /opt/localradio/media/fallback/ to guarantee the stream never dies.
fallback_source = playlist(
mode = "randomize",
reload_mode = "watch",
fallback_dir
)
# --- Scheduled Music Sources --------------------------------------------------
# Each playlist watches its directory for changes. If files are added/removed,
# the playlist updates automatically.
morning_music = playlist(
mode = "randomize",
reload_mode = "watch",
"#{music_base}/morning"
)
day_music = playlist(
mode = "randomize",
reload_mode = "watch",
"#{music_base}/day"
)
night_music = playlist(
mode = "randomize",
reload_mode = "watch",
"#{music_base}/night"
)
weekend_music = playlist(
mode = "randomize",
reload_mode = "watch",
"#{music_base}/weekend"
)
# --- Time-based music schedule ------------------------------------------------
# switch() picks the first matching source based on time predicates.
# Predicates use: { <time range> } and weekday functions.
#
# Weekday numbers: 1w=Monday ... 5w=Friday, 6w=Saturday, 7w=Sunday
scheduled_music = switch(
track_sensitive = true,
[
# Weekend: all day Saturday and Sunday
({ 6w or 7w }, weekend_music),
# Weekday morning: 06:00 - 11:59
({ 1w-5w and 6h-11h }, morning_music),
# Weekday afternoon: 12:00 - 17:59
({ 1w-5w and 12h-17h }, day_music),
# Weekday evening/night: 18:00 - 05:59
({ 1w-5w and (18h-23h or 0h-5h) }, night_music),
]
)
# Add fallback so scheduled_music never fails
music_safe = fallback(
track_sensitive = true,
[scheduled_music, fallback_source]
)
# --- Podcast Queue Source -----------------------------------------------------
# Watches the queue directory. When files appear, they play with priority.
# After a file finishes, it is removed from the queue directory.
#
# request.queue() is not used here because we want filesystem-based control
# from the Python poller. Instead, we use playlist() in "normal" mode on the
# queue directory, which plays files in alphabetical order (we prefix filenames
# with timestamps to ensure FIFO ordering).
podcast_queue = playlist(
mode = "normal",
reload_mode = "watch",
queue_dir
)
# --- Source Priority Merge ----------------------------------------------------
# fallback() plays the first available source.
# track_sensitive = true means it waits for the current music track to finish
# before switching to a podcast (and vice versa).
radio = fallback(
track_sensitive = true,
[podcast_queue, music_safe]
)
# --- Metadata Logging ---------------------------------------------------------
radio = on_metadata(
fun(m) -> begin
title = m["title"]
artist = m["artist"]
log("Now playing: #{artist} - #{title}")
end,
radio
)
# --- Make the source infallible -----------------------------------------------
# mksafe() ensures the source never fails. If all sources are unavailable,
# it outputs silence rather than crashing.
radio = mksafe(radio)
# --- Output to Icecast --------------------------------------------------------
# MP3 320kbps stereo output.
# To switch to Opus, replace %mp3 with %opus and adjust bitrate.
output.icecast(
%mp3(
bitrate = 320,
samplerate = 44100,
stereo = true
),
host = icecast_host,
port = icecast_port,
password = icecast_password,
mount = icecast_mount,
name = "Local Radio",
description = "Personal automated radio station",
genre = "Various",
url = "http://localhost:8000",
radio
)