#!/usr/bin/with-contenv bashio # ============================================================================== # Busch-Radio Spotify Bridge – Startup-Script # # Startet: # 1. Icecast2 – HTTP-Stream-Server # 2. OAuth-Hilfe – Einrichtungs-Assistent auf Port 5589 # 3. librespot – Virtueller Spotify Connect-Empfänger (PCM auf stdout) # 4. Silence-Injektor– Fügt Stille ein wenn Spotify pausiert (hält Stream aufrecht) # 5. ffmpeg – Kodiert PCM → MP3/AAC und schickt es als Icecast-Quelle # # Bei Absturz wird die komplette Pipeline nach 5 Sekunden neu gestartet. # ============================================================================== set -u # ── Konfiguration lesen ─────────────────────────────────────────────────────── DEVICE_NAME=$(bashio::config 'device_name') BITRATE=$(bashio::config 'bitrate') STREAM_FORMAT=$(bashio::config 'stream_format') STREAM_PORT=$(bashio::config 'stream_port') STREAM_MOUNT=$(bashio::config 'stream_mount') ICECAST_PASSWORD=$(bashio::config 'icecast_password') # ── Format-spezifische Einstellungen ────────────────────────────────────────── if [ "${STREAM_FORMAT}" = "aac" ]; then FFMPEG_CODEC="aac" FFMPEG_FORMAT="adts" ICECAST_MIME="audio/aac" else FFMPEG_CODEC="libmp3lame" FFMPEG_FORMAT="mp3" ICECAST_MIME="audio/mpeg" fi bashio::log.info "╔══════════════════════════════════════════════╗" bashio::log.info "║ Busch-Radio Spotify Bridge v1.0.11 ║" bashio::log.info "╚══════════════════════════════════════════════╝" bashio::log.info "Gerätename : ${DEVICE_NAME}" bashio::log.info "Bitrate : ${BITRATE} kbps" bashio::log.info "Format : ${STREAM_FORMAT}" bashio::log.info "Stream-Port : ${STREAM_PORT}" bashio::log.info "Stream-Pfad : ${STREAM_MOUNT}" # ── Verzeichnisse anlegen + Berechtigungen für nobody-User setzen ──────────── # Icecast läuft im Container als root, wechselt aber via changeowner zu nobody. mkdir -p /var/log/icecast /run/icecast /tmp/busch-radio chown -R nobody:nobody /var/log/icecast /run/icecast /tmp/busch-radio # ── Icecast-Webroot ermitteln ───────────────────────────────────────────────── # Alpine und Debian legen Icecast in unterschiedliche Pfade ICECAST_WEBROOT="" for candidate in /usr/share/icecast /usr/share/icecast2; do if [ -d "$candidate" ]; then ICECAST_WEBROOT="$candidate" break fi done if [ -z "$ICECAST_WEBROOT" ]; then bashio::log.warning "Icecast-Webroot nicht gefunden, verwende /usr/share/icecast" ICECAST_WEBROOT="/usr/share/icecast" mkdir -p "${ICECAST_WEBROOT}/web" "${ICECAST_WEBROOT}/admin" fi bashio::log.debug "Icecast-Webroot: ${ICECAST_WEBROOT}" # ── Icecast-Konfiguration generieren ───────────────────────────────────────── bashio::log.info "Icecast-Konfiguration wird erstellt..." cat > /tmp/busch-radio/icecast.xml < Home Assistant admin@localhost 10 2 524288 30 15 30 0 65536 ${ICECAST_PASSWORD} ${ICECAST_PASSWORD} admin ${ICECAST_PASSWORD} 0.0.0.0 ${STREAM_PORT} ${STREAM_MOUNT} 0 Busch-Radio Spotify Bridge Spotify via Home Assistant ${BITRATE} ${ICECAST_MIME} 1 ${ICECAST_WEBROOT} /var/log/icecast ${ICECAST_WEBROOT}/web ${ICECAST_WEBROOT}/admin /run/icecast/icecast.pid - - 2 10000 0 nobody nobody ICECAST_EOF # ── Icecast starten ─────────────────────────────────────────────────────────── bashio::log.info "Icecast wird gestartet (Port: ${STREAM_PORT})..." icecast -c /tmp/busch-radio/icecast.xml & ICECAST_PID=$! # Kurz warten bis Icecast hochgefahren ist sleep 3 if ! kill -0 "${ICECAST_PID}" 2>/dev/null; then bashio::log.fatal "Icecast konnte nicht gestartet werden! Prüfe die Konfiguration." exit 1 fi bashio::log.info "Icecast läuft (PID: ${ICECAST_PID})" # ── Credential-Cache-Verzeichnis ───────────────────────────────────────────── # Credentials werden in /data gespeichert (persistentes Add-on Verzeichnis). CREDENTIAL_CACHE="/data/librespot-cache" mkdir -p "${CREDENTIAL_CACHE}" # ── IP-Adresse des Hosts ermitteln ─────────────────────────────────────────── HA_IP=$(ip route get 1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src") print $(i+1)}' | head -1) if [ -z "${HA_IP}" ]; then HA_IP=$(hostname -I 2>/dev/null | awk '{print $1}') fi if [ -z "${HA_IP}" ]; then HA_IP="" fi # ── OAuth-Hilfsserver starten ───────────────────────────────────────────────── # Stellt unter http://:5589 einen Einrichtungs-Assistenten bereit, # der den Spotify-OAuth-Prozess Schritt für Schritt erklärt. python3 /oauth_helper.py "${HA_IP}" & OAUTH_HELPER_PID=$! bashio::log.info "OAuth-Hilfsseite: http://${HA_IP}:5589" # ── Librespot-Argumente zusammenstellen ─────────────────────────────────────── LIBRESPOT_ARGS=( "--name" "${DEVICE_NAME}" "--bitrate" "${BITRATE}" "--backend" "pipe" "--device-type" "computer" "--disable-audio-cache" "--initial-volume" "100" "--disable-discovery" "--enable-oauth" "--system-cache" "${CREDENTIAL_CACHE}" ) # Prüfen ob bereits Credentials gecacht sind if [ -f "${CREDENTIAL_CACHE}/credentials.json" ]; then bashio::log.info "Gespeicherte Credentials gefunden – Direktstart." else bashio::log.info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" bashio::log.info "ERSTER START – Spotify-Autorisierung erforderlich!" bashio::log.info "" bashio::log.info "Öffne die Einrichtungs-Seite im Browser:" bashio::log.info " http://${HA_IP}:5589" bashio::log.info "" bashio::log.info "Der Assistent führt dich durch die Anmeldung." bashio::log.info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" fi # ── Icecast-Source-URL ──────────────────────────────────────────────────────── ICECAST_SOURCE_URL="icecast://source:${ICECAST_PASSWORD}@localhost:${STREAM_PORT}${STREAM_MOUNT}" # ── Stream-URL ausgeben ─────────────────────────────────────────────────────── bashio::log.info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" bashio::log.info "Stream-URL für das Busch-Jäger Radio:" bashio::log.info " http://${HA_IP}:${STREAM_PORT}${STREAM_MOUNT}" bashio::log.info "" bashio::log.info "Icecast-Statusseite:" bashio::log.info " http://${HA_IP}:${STREAM_PORT}" bashio::log.info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" bashio::log.info "Wähle '${DEVICE_NAME}' in der Spotify-App als Abspielgerät." bashio::log.info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" # ── Aufräumen bei SIGTERM ───────────────────────────────────────────────────── cleanup() { bashio::log.info "Add-on wird beendet..." kill "${ICECAST_PID}" "${OAUTH_HELPER_PID}" 2>/dev/null || true exit 0 } trap cleanup SIGTERM SIGINT # ── Silence-Injektor ───────────────────────────────────────────────────────── # Sitzt zwischen librespot und ffmpeg. # Wenn Spotify pausiert, hört librespot auf zu schreiben. Ohne Injektor würde # ffmpeg/Icecast den Stream nach einigen Sekunden Stille trennen. # Der Injektor füllt die Pause mit Stille-Bytes, damit der Stream bestehen bleibt. SILENCE_INJECTOR=' import sys, select, os CHUNK = 8820 # 50 ms @ 44100 Hz, S16LE, Stereo (44100*2*2/20) silence = bytes(CHUNK) fd = sys.stdin.buffer.fileno() while True: r, _, _ = select.select([fd], [], [], 0.05) if r: data = os.read(fd, CHUNK) if not data: break sys.stdout.buffer.write(data) else: sys.stdout.buffer.write(silence) sys.stdout.buffer.flush() ' # ── Haupt-Schleife: Pipeline starten und bei Absturz neu starten ────────────── # # Signalfluss: # librespot (Spotify Connect) → stdout (raw PCM 44100Hz/S16LE/Stereo) # ↓ # silence-injector (Python) → leitet durch; bei Pause: sendet Stille # ↓ # ffmpeg -re (Echtzeit) → PCM → MP3/AAC Encoding → Icecast # ↓ # Busch-Jäger Radio (HTTP-Client) # # -re: Liest den Input in Echtzeit – verhindert zu schnelles Vorspulen # und gibt librespot korrekte Positions-Rückmeldung (Pause/Skip). # # Wenn entweder librespot oder ffmpeg abstürzt, wird die komplette Pipeline # nach RESTART_DELAY Sekunden neu gestartet. RESTART_DELAY=5 ATTEMPT=0 while true; do ATTEMPT=$((ATTEMPT + 1)) bashio::log.info "Pipeline-Start (Versuch ${ATTEMPT})..." # Librespot stderr: Zeilen werden an HA-Log weitergeleitet UND auf # "Browse to:"-URL geprüft → wird in Datei für OAuth-Hilfsserver gespeichert. librespot "${LIBRESPOT_ARGS[@]}" \ 2> >(while IFS= read -r line; do echo "$line" >&2 case "$line" in *"Browse to:"*) echo "$line" | sed 's/.*Browse to: //' | tr -d '\r\n ' \ > /tmp/busch-radio/auth_url.txt bashio::log.info "Spotify-Anmeldung: http://${HA_IP}:5589 öffnen" ;; esac done) \ | python3 -c "${SILENCE_INJECTOR}" \ | ffmpeg \ -re \ -hide_banner \ -loglevel warning \ -f s16le -ar 44100 -ac 2 \ -i pipe:0 \ -codec:a "${FFMPEG_CODEC}" \ -b:a "${BITRATE}k" \ -f "${FFMPEG_FORMAT}" \ "${ICECAST_SOURCE_URL}" \ || true # Auth-URL-Datei leeren damit die Hilfsseite beim Neustart wieder wartet rm -f /tmp/busch-radio/auth_url.txt bashio::log.warning "Pipeline beendet. Neustart in ${RESTART_DELAY}s..." sleep "${RESTART_DELAY}" done