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>
157 lines
4.5 KiB
Plaintext
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
|
|
)
|