#!/usr/bin/with-contenv bashio # ============================================================================== # Busch-Radio Spotify Bridge – Startup-Script # # Startet: # 1. Icecast2 – HTTP-Stream-Server # 2. librespot – Virtueller Spotify Connect-Empfänger (gibt PCM auf stdout aus) # 3. ffmpeg – Kodiert PCM → MP3 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_PORT=$(bashio::config 'stream_port') STREAM_MOUNT=$(bashio::config 'stream_mount') ICECAST_PASSWORD=$(bashio::config 'icecast_password') bashio::log.info "╔══════════════════════════════════════════════╗" bashio::log.info "║ Busch-Radio Spotify Bridge v1.0.0 ║" bashio::log.info "╚══════════════════════════════════════════════╝" bashio::log.info "Gerätename : ${DEVICE_NAME}" bashio::log.info "Bitrate : ${BITRATE} kbps" 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 10 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} audio/mpeg 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}" # ── Librespot-Argumente zusammenstellen ─────────────────────────────────────── # Zeroconf/mDNS funktioniert nicht zuverlässig in Containern, da Port 5353 # vom Host (systemd-resolved/avahi) bereits belegt ist. # Stattdessen: OAuth-Authentifizierung. Das Gerät registriert sich bei # Spotifys Servern und erscheint in der App ohne lokales mDNS. LIBRESPOT_ARGS=( "--name" "${DEVICE_NAME}" "--bitrate" "${BITRATE}" "--backend" "pipe" "--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 "librespot gibt gleich eine URL aus. Öffne diese" bashio::log.info "im Browser und melde dich mit deinem Spotify-Konto an." bashio::log.info "Danach startet das Add-on automatisch durch." 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://:${STREAM_PORT}${STREAM_MOUNT}" bashio::log.info "" bashio::log.info "Icecast-Statusseite:" bashio::log.info " http://:${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}" 2>/dev/null || true exit 0 } trap cleanup SIGTERM SIGINT # ── Haupt-Schleife: Pipeline starten und bei Absturz neu starten ────────────── # # Signalfluss: # librespot (Spotify Connect) → stdout (raw PCM 44100Hz/S16LE/Stereo) # ↓ # ffmpeg (PCM → MP3 Encoding) → Icecast (HTTP-Stream) # ↓ # Busch-Jäger Radio (HTTP-Client) # # 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 "${LIBRESPOT_ARGS[@]}" \ | ffmpeg \ -hide_banner \ -loglevel warning \ -f s16le -ar 44100 -ac 2 \ -i pipe:0 \ -codec:a libmp3lame \ -b:a "${BITRATE}k" \ -q:a 2 \ -f mp3 \ "${ICECAST_SOURCE_URL}" \ || true bashio::log.warning "Pipeline beendet. Neustart in ${RESTART_DELAY}s..." sleep "${RESTART_DELAY}" done