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>
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
#!/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
|
||||
)
|
||||
Reference in New Issue
Block a user