Files
Niklas Gühne a463403c43 Release v1.0.11: Pause-Fix, OAuth-Assistent, Docs, Cleanup
Pause-Fix (Silence-Injektor):
- Python-Skript zwischen librespot und ffmpeg
- Bei Pause füllt der Injektor die Stille mit Null-Bytes
- Icecast bleibt verbunden, Stream reißt nicht ab

OAuth-Einrichtungs-Assistent (Port 5589):
- oauth_helper.py: HTTP-Server mit geführtem Anmelde-Prozess
- Zeigt Spotify-Auth-Link automatisch sobald librespot ihn ausgibt
- URL-Fixer: Nutzer fügt 127.0.0.1-URL ein, Assistent korrigiert auf HA-IP
- Erkennt bereits vorhandene Credentials und zeigt Hinweis

Dokumentation:
- DOCS.md für die HA Add-on Detailseite erstellt
- README.md vollständig überarbeitet und aktualisiert

Cleanup:
- Leere Testdatei entfernt
- Veraltete example-config.yaml entfernt
- manifest.json der Custom Component auf v1.0.11 gebracht

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 12:14:56 +02:00

299 lines
13 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 <<ICECAST_EOF
<icecast>
<location>Home Assistant</location>
<admin>admin@localhost</admin>
<limits>
<clients>10</clients>
<sources>2</sources>
<queue-size>524288</queue-size>
<client-timeout>30</client-timeout>
<header-timeout>15</header-timeout>
<source-timeout>30</source-timeout>
<burst-on-connect>0</burst-on-connect>
<burst-size>65536</burst-size>
</limits>
<authentication>
<source-password>${ICECAST_PASSWORD}</source-password>
<relay-password>${ICECAST_PASSWORD}</relay-password>
<admin-user>admin</admin-user>
<admin-password>${ICECAST_PASSWORD}</admin-password>
</authentication>
<hostname>0.0.0.0</hostname>
<listen-socket>
<port>${STREAM_PORT}</port>
</listen-socket>
<mount>
<mount-name>${STREAM_MOUNT}</mount-name>
<public>0</public>
<stream-name>Busch-Radio Spotify Bridge</stream-name>
<stream-description>Spotify via Home Assistant</stream-description>
<bitrate>${BITRATE}</bitrate>
<type>${ICECAST_MIME}</type>
</mount>
<fileserve>1</fileserve>
<paths>
<basedir>${ICECAST_WEBROOT}</basedir>
<logdir>/var/log/icecast</logdir>
<webroot>${ICECAST_WEBROOT}/web</webroot>
<adminroot>${ICECAST_WEBROOT}/admin</adminroot>
<pidfile>/run/icecast/icecast.pid</pidfile>
</paths>
<logging>
<!-- "-" loggt auf stderr → wird von HA erfasst -->
<accesslog>-</accesslog>
<errorlog>-</errorlog>
<loglevel>2</loglevel>
<logsize>10000</logsize>
</logging>
<security>
<chroot>0</chroot>
<!-- Icecast wechselt nach dem Start zu nobody verhindert den root-Fehler -->
<changeowner>
<user>nobody</user>
<group>nobody</group>
</changeowner>
</security>
</icecast>
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="<homeassistant-ip>"
fi
# ── OAuth-Hilfsserver starten ─────────────────────────────────────────────────
# Stellt unter http://<ha-ip>: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