@ -4,6 +4,7 @@ import time
from datetime import datetime , timezone
from datetime import datetime , timezone
import random
import random
import threading
import threading
import psutil
import subprocess
import subprocess
import hashlib
import hashlib
import sqlite3
import sqlite3
@ -389,6 +390,47 @@ def _pump_decoder_to_wrapper(decoder_stdout, stop_event):
def write_silence_ms ( ms : int = SILENCE_MS_AFTER_TRACK ) :
def write_silence_ms ( ms : int = SILENCE_MS_AFTER_TRACK ) :
write_silence ( ms / 1000.0 )
write_silence ( ms / 1000.0 )
def is_zombie ( proc ) :
try :
return proc . status ( ) == psutil . STATUS_ZOMBIE
except psutil . NoSuchProcess :
return True
def restart_radio_chain ( ) :
print ( " [WATCHDOG] Restarting radio chain " )
# Kill ffmpeg
for proc in psutil . process_iter ( [ ' name ' , ' cmdline ' ] ) :
try :
cmd = proc . info [ ' cmdline ' ] or [ ]
if proc . info [ ' name ' ] == ' ffmpeg ' or ( cmd and ' ffmpeg ' in cmd [ 0 ] ) :
print ( f " [WATCHDOG] Killing ffmpeg PID { proc . pid } " )
proc . kill ( )
except Exception :
pass
# Kill pifmadv
for proc in psutil . process_iter ( [ ' name ' , ' cmdline ' ] ) :
try :
cmd = proc . info [ ' cmdline ' ] or [ ]
if proc . info [ ' name ' ] == ' pifmadv ' or ( cmd and ' pifmadv ' in cmd [ 0 ] ) :
print ( f " [WATCHDOG] Killing pifmadv PID { proc . pid } " )
proc . kill ( )
except Exception :
pass
time . sleep ( 0.5 )
print ( " [WATCHDOG] Restarting pifmadv " )
start_pifm ( )
# Your addition: broadcast station ID
id_file = pick_station_id_file ( )
if id_file :
print ( f " [WATCHDOG] Broadcasting recovery ID: { id_file } " )
stream_file ( id_file , allow_skip = False )
write_silence ( 0.25 )
# -----------------------------
# -----------------------------
# Robust stream_file implementation
# Robust stream_file implementation
# -----------------------------
# -----------------------------
@ -409,7 +451,10 @@ def stream_file(path: str, allow_skip=True) -> bool:
decoder_args = [
decoder_args = [
" ffmpeg " , " -nostdin " , " -v " , " error " ,
" ffmpeg " , " -nostdin " , " -v " , " error " ,
" -i " , path ,
" -i " , path ,
" -f " , " s16le " , " -ar " , str ( SAMPLE_RATE ) , " -ac " , str ( CHANNELS ) ,
" -f " , " s16le " , # raw PCM
" -af " , " acompressor=threshold=-18dB:ratio=4:attack=5:release=100,alimiter=limit=0.0dB " , # compress just like a real radio station, because this is a real radio station
" -ar " , str ( SAMPLE_RATE ) ,
" -ac " , str ( CHANNELS ) ,
" pipe:1 "
" pipe:1 "
]
]
@ -676,6 +721,42 @@ def worker():
print ( " [WORKER] Exception in worker loop: " , e )
print ( " [WORKER] Exception in worker loop: " , e )
time . sleep ( 1 )
time . sleep ( 1 )
restart_lock = threading . Lock ( )
def watchdog ( ) :
while True :
time . sleep ( 5 )
pifm_state = " missing "
ffmpeg_state = " missing "
for proc in psutil . process_iter ( [ ' name ' , ' cmdline ' , ' status ' ] ) :
try :
cmd = proc . info [ ' cmdline ' ] or [ ]
name = proc . info [ ' name ' ]
if name == " pifmadv " or ( cmd and " pifmadv " in cmd [ 0 ] ) :
if proc . status ( ) == psutil . STATUS_ZOMBIE :
pifm_state = " zombie "
else :
pifm_state = " ok "
if name == " ffmpeg " or ( cmd and " ffmpeg " in cmd [ 0 ] ) :
# Only count ffmpeg processes that belong to your pipeline
if any ( x in cmd for x in [ " -f " , " wav " , " -i " , " - " ] ) :
if proc . status ( ) == psutil . STATUS_ZOMBIE :
ffmpeg_state = " zombie "
else :
ffmpeg_state = " ok "
except Exception :
continue
if pifm_state != " ok " or ffmpeg_state != " ok " :
with restart_lock :
print ( f " [WATCHDOG] pifmadv= { pifm_state } , ffmpeg= { ffmpeg_state } " )
restart_radio_chain ( )
# -----------------------------
# -----------------------------
# API Endpoints
# API Endpoints
# -----------------------------
# -----------------------------
@ -767,7 +848,16 @@ if __name__ == "__main__":
discover_tracks ( )
discover_tracks ( )
start_pifm ( )
start_pifm ( )
# play station ID at startup so the listener(s) know we're up
id_file = pick_station_id_file ( )
if id_file :
set_rds ( " RT HushPupPi Station ID " )
stream_file ( id_file , allow_skip = False )
write_silence ( 0.25 )
threading . Thread ( target = worker , daemon = True ) . start ( )
threading . Thread ( target = worker , daemon = True ) . start ( )
threading . Thread ( target = watchdog , daemon = True ) . start ( )
# Reduce Flask logging noise in production
# Reduce Flask logging noise in production
import logging
import logging
logging . getLogger ( " werkzeug " ) . setLevel ( logging . ERROR )
logging . getLogger ( " werkzeug " ) . setLevel ( logging . ERROR )