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,167 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate all configuration files for the local radio station."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
BASE_DIR = Path(os.environ.get("LOCALRADIO_BASE", "/opt/localradio"))
|
||||
errors = []
|
||||
warnings = []
|
||||
|
||||
|
||||
def err(msg):
|
||||
errors.append(msg)
|
||||
print(f" ERROR: {msg}")
|
||||
|
||||
|
||||
def warn(msg):
|
||||
warnings.append(msg)
|
||||
print(f" WARN: {msg}")
|
||||
|
||||
|
||||
def ok(msg):
|
||||
print(f" OK: {msg}")
|
||||
|
||||
|
||||
def validate_feeds():
|
||||
print("=== Validating feeds.yaml ===")
|
||||
path = BASE_DIR / "config" / "feeds.yaml"
|
||||
if not path.exists():
|
||||
err(f"Missing: {path}")
|
||||
return
|
||||
|
||||
with open(path) as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
if not data or "feeds" not in data:
|
||||
err("feeds.yaml must have a 'feeds' key")
|
||||
return
|
||||
|
||||
feeds = data["feeds"]
|
||||
enabled_count = 0
|
||||
for i, feed in enumerate(feeds):
|
||||
if not feed.get("name"):
|
||||
err(f"Feed [{i}]: missing 'name'")
|
||||
if not feed.get("url"):
|
||||
err(f"Feed [{i}]: missing 'url'")
|
||||
elif not feed["url"].startswith(("http://", "https://")):
|
||||
err(f"Feed '{feed.get('name', i)}': URL must start with http:// or https://")
|
||||
if feed.get("enabled", True):
|
||||
enabled_count += 1
|
||||
|
||||
if enabled_count == 0:
|
||||
warn("No feeds are enabled")
|
||||
else:
|
||||
ok(f"{enabled_count} feed(s) enabled")
|
||||
|
||||
|
||||
def validate_station():
|
||||
print("\n=== Validating station.yaml ===")
|
||||
path = BASE_DIR / "config" / "station.yaml"
|
||||
if not path.exists():
|
||||
err(f"Missing: {path}")
|
||||
return
|
||||
|
||||
with open(path) as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
if not data.get("schedule"):
|
||||
err("station.yaml must have a 'schedule' section")
|
||||
return
|
||||
|
||||
for block in data["schedule"]:
|
||||
name = block.get("name", "unnamed")
|
||||
if not block.get("folder"):
|
||||
err(f"Schedule '{name}': missing 'folder'")
|
||||
if not block.get("days"):
|
||||
err(f"Schedule '{name}': missing 'days'")
|
||||
if not block.get("start") or not block.get("end"):
|
||||
err(f"Schedule '{name}': missing 'start' or 'end'")
|
||||
|
||||
ok(f"{len(data['schedule'])} schedule block(s) defined")
|
||||
|
||||
fmt = data.get("audio", {}).get("format", "mp3")
|
||||
bitrate = data.get("audio", {}).get("bitrate", 320)
|
||||
ok(f"Audio: {fmt} @ {bitrate} kbps")
|
||||
|
||||
|
||||
def validate_directories():
|
||||
print("\n=== Validating directories ===")
|
||||
required = [
|
||||
"config",
|
||||
"scripts",
|
||||
"state",
|
||||
"state/queue",
|
||||
"media/music/morning",
|
||||
"media/music/day",
|
||||
"media/music/night",
|
||||
"media/music/weekend",
|
||||
"media/podcasts",
|
||||
"media/fallback",
|
||||
"logs",
|
||||
]
|
||||
for d in required:
|
||||
full = BASE_DIR / d
|
||||
if full.exists():
|
||||
ok(f"{d}/")
|
||||
else:
|
||||
err(f"Missing directory: {d}/")
|
||||
|
||||
# Check fallback has content
|
||||
fallback = BASE_DIR / "media" / "fallback"
|
||||
if fallback.exists():
|
||||
audio_exts = {".mp3", ".flac", ".ogg", ".opus", ".m4a", ".wav"}
|
||||
files = [f for f in fallback.iterdir() if f.suffix.lower() in audio_exts]
|
||||
if not files:
|
||||
warn("media/fallback/ has no audio files — stream may go silent!")
|
||||
else:
|
||||
ok(f"media/fallback/ has {len(files)} audio file(s)")
|
||||
|
||||
|
||||
def validate_icecast():
|
||||
print("\n=== Validating icecast.xml ===")
|
||||
path = BASE_DIR / "config" / "icecast.xml"
|
||||
if not path.exists():
|
||||
err(f"Missing: {path}")
|
||||
return
|
||||
|
||||
content = path.read_text()
|
||||
if "localradio_source" in content:
|
||||
warn("icecast.xml still uses default source password — change it for security")
|
||||
if "localradio_admin" in content:
|
||||
warn("icecast.xml still uses default admin password — change it for security")
|
||||
ok("icecast.xml exists and is parseable")
|
||||
|
||||
|
||||
def validate_liquidsoap():
|
||||
print("\n=== Validating station.liq ===")
|
||||
path = BASE_DIR / "config" / "station.liq"
|
||||
if not path.exists():
|
||||
err(f"Missing: {path}")
|
||||
return
|
||||
ok("station.liq exists")
|
||||
|
||||
|
||||
def main():
|
||||
print(f"Validating configuration at: {BASE_DIR}\n")
|
||||
validate_feeds()
|
||||
validate_station()
|
||||
validate_directories()
|
||||
validate_icecast()
|
||||
validate_liquidsoap()
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
if errors:
|
||||
print(f"RESULT: {len(errors)} error(s), {len(warnings)} warning(s)")
|
||||
sys.exit(1)
|
||||
elif warnings:
|
||||
print(f"RESULT: OK with {len(warnings)} warning(s)")
|
||||
else:
|
||||
print("RESULT: All checks passed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user