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,11 @@
|
||||
feeds:
|
||||
- name: Example Podcast
|
||||
url: https://feeds.example.com/podcast.xml
|
||||
enabled: false
|
||||
priority: 10
|
||||
max_episodes: 20
|
||||
- name: c
|
||||
url: https://rss.libsyn.com/shows/467049/destinations/3949218.xml
|
||||
enabled: true
|
||||
priority: 10
|
||||
max_episodes: 20
|
||||
@@ -0,0 +1,58 @@
|
||||
<icecast>
|
||||
<location>Local</location>
|
||||
<admin>admin@localhost</admin>
|
||||
|
||||
<limits>
|
||||
<clients>5</clients>
|
||||
<sources>2</sources>
|
||||
<queue-size>524288</queue-size>
|
||||
<client-timeout>30</client-timeout>
|
||||
<header-timeout>15</header-timeout>
|
||||
<source-timeout>10</source-timeout>
|
||||
<burst-on-connect>1</burst-on-connect>
|
||||
<burst-size>65535</burst-size>
|
||||
</limits>
|
||||
|
||||
<authentication>
|
||||
<source-password>localradio_source</source-password>
|
||||
<relay-password>localradio_relay</relay-password>
|
||||
<admin-user>admin</admin-user>
|
||||
<admin-password>localradio_admin</admin-password>
|
||||
</authentication>
|
||||
|
||||
<hostname>localhost</hostname>
|
||||
|
||||
<listen-socket>
|
||||
<port>8000</port>
|
||||
<bind-address>127.0.0.1</bind-address>
|
||||
</listen-socket>
|
||||
|
||||
<mount>
|
||||
<mount-name>/stream</mount-name>
|
||||
<fallback-mount>/fallback</fallback-mount>
|
||||
<fallback-override>1</fallback-override>
|
||||
<hidden>0</hidden>
|
||||
<public>0</public>
|
||||
</mount>
|
||||
|
||||
<fileserve>1</fileserve>
|
||||
|
||||
<paths>
|
||||
<basedir>/usr/share/icecast2</basedir>
|
||||
<logdir>/opt/localradio/logs</logdir>
|
||||
<webroot>/usr/share/icecast2/web</webroot>
|
||||
<adminroot>/usr/share/icecast2/admin</adminroot>
|
||||
<alias source="/" destination="/status.xsl"/>
|
||||
</paths>
|
||||
|
||||
<logging>
|
||||
<accesslog>icecast_access.log</accesslog>
|
||||
<errorlog>icecast_error.log</errorlog>
|
||||
<loglevel>3</loglevel>
|
||||
<logsize>10000</logsize>
|
||||
</logging>
|
||||
|
||||
<security>
|
||||
<chroot>0</chroot>
|
||||
</security>
|
||||
</icecast>
|
||||
@@ -0,0 +1,10 @@
|
||||
# Local Radio Environment Configuration
|
||||
# Override defaults here. Loaded by systemd services.
|
||||
|
||||
LOCALRADIO_BASE=/opt/localradio
|
||||
|
||||
# RSS poll interval in seconds (default: 300 = 5 minutes)
|
||||
POLL_INTERVAL=300
|
||||
|
||||
# Download timeout in seconds (default: 600)
|
||||
DOWNLOAD_TIMEOUT=600
|
||||
@@ -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
|
||||
)
|
||||
@@ -0,0 +1,67 @@
|
||||
# Local Radio Station Configuration
|
||||
|
||||
station:
|
||||
name: "Local Radio"
|
||||
description: "Personal automated radio station"
|
||||
|
||||
icecast:
|
||||
host: "localhost"
|
||||
port: 8000
|
||||
source_password: "localradio_source"
|
||||
mount: "/stream"
|
||||
|
||||
audio:
|
||||
# Output format: "mp3" or "opus"
|
||||
format: "mp3"
|
||||
# Bitrate in kbps (for mp3: 128, 192, 256, 320; for opus: 64-256)
|
||||
bitrate: 320
|
||||
samplerate: 44100
|
||||
channels: 2
|
||||
|
||||
music:
|
||||
base_path: "/opt/localradio/media/music"
|
||||
fallback_path: "/opt/localradio/media/fallback"
|
||||
# File extensions to include
|
||||
extensions:
|
||||
- "*.mp3"
|
||||
- "*.flac"
|
||||
- "*.ogg"
|
||||
- "*.opus"
|
||||
- "*.m4a"
|
||||
- "*.wav"
|
||||
|
||||
# Schedule blocks define which music folder plays at what times.
|
||||
# Times are in 24-hour format. Each block specifies:
|
||||
# days: list of weekday numbers (1=Monday ... 7=Sunday)
|
||||
# start: HH:MM
|
||||
# end: HH:MM
|
||||
# folder: subfolder name under music.base_path
|
||||
schedule:
|
||||
- name: "Weekday Morning"
|
||||
days: [1, 2, 3, 4, 5]
|
||||
start: "06:00"
|
||||
end: "12:00"
|
||||
folder: "morning"
|
||||
|
||||
- name: "Weekday Afternoon"
|
||||
days: [1, 2, 3, 4, 5]
|
||||
start: "12:00"
|
||||
end: "18:00"
|
||||
folder: "day"
|
||||
|
||||
- name: "Weekday Evening & Night"
|
||||
days: [1, 2, 3, 4, 5]
|
||||
start: "18:00"
|
||||
end: "06:00"
|
||||
folder: "night"
|
||||
|
||||
- name: "Weekend"
|
||||
days: [6, 7]
|
||||
start: "00:00"
|
||||
end: "23:59"
|
||||
folder: "weekend"
|
||||
|
||||
podcasts:
|
||||
queue_dir: "/opt/localradio/state/queue"
|
||||
download_dir: "/opt/localradio/media/podcasts"
|
||||
db_path: "/opt/localradio/state/radio.db"
|
||||
Reference in New Issue
Block a user