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>
168 lines
4.4 KiB
Python
168 lines
4.4 KiB
Python
#!/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()
|