secubox-openwrt/package/secubox/secubox-app-webradio/files/usr/sbin/webradioctl
CyberMind-FR 6db547f7f8 feat: Add WebRadio, TURN server, and Lyrion streaming integration
New packages:
- luci-app-webradio: Web radio management with Lyrion bridge tab
- luci-app-turn: TURN/STUN server UI for WebRTC (Jitsi integration)
- secubox-app-lyrion-bridge: Lyrion → Squeezelite → FFmpeg → Icecast pipeline
- secubox-app-squeezelite: Squeezelite audio player with FIFO output
- secubox-app-turn: TURN server with ACME SSL and Jitsi setup
- secubox-app-webradio: Icecast/ezstream web radio server

Features:
- HTTPS streaming via HAProxy (stream.gk2.secubox.in)
- Lyrion Music Server bridge for streaming playlists to Icecast
- TURN server with time-limited credential generation
- CrowdSec integration for WebRadio security
- Schedule-based radio programming with jingles

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-21 17:46:54 +01:00

676 lines
17 KiB
Bash

#!/bin/sh
# WebRadio Controller - SecuBox Backend CLI
# Manages Icecast, Ezstream, DarkIce streaming services
set -e
CONF_DIR="/srv/webradio/config"
MUSIC_DIR="/srv/webradio/music"
JINGLE_DIR="/srv/webradio/jingles"
PLAYLIST_FILE="/tmp/webradio_playlist.m3u"
ICECAST_XML="$CONF_DIR/icecast.xml"
EZSTREAM_XML="$CONF_DIR/ezstream.xml"
DARKICE_CFG="$CONF_DIR/darkice.cfg"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
log() { echo -e "${GREEN}[WebRadio]${NC} $1"; }
warn() { echo -e "${YELLOW}[WebRadio]${NC} $1"; }
error() { echo -e "${RED}[WebRadio]${NC} $1" >&2; }
uci_get() { uci -q get "webradio.$1" 2>/dev/null || echo "$2"; }
#--- Status ---
cmd_status() {
echo -e "${CYAN}=== WebRadio Status ===${NC}"
local enabled=$(uci_get main.enabled 0)
local name=$(uci_get main.name "SecuBox Radio")
local port=$(uci_get main.port 8000)
echo "Station: $name"
echo "Enabled: $([ "$enabled" = "1" ] && echo "Yes" || echo "No")"
echo ""
# Icecast status
if pgrep -f "icecast" >/dev/null 2>&1; then
echo -e "Icecast: ${GREEN}Running${NC} (port $port)"
# Get listener count
local listeners=$(curl -s "http://127.0.0.1:$port/status-json.xsl" 2>/dev/null | jsonfilter -e '@.icestats.source.listeners' 2>/dev/null || echo "0")
echo "Listeners: $listeners"
else
echo -e "Icecast: ${RED}Stopped${NC}"
fi
# Ezstream status
if pgrep -f "ezstream" >/dev/null 2>&1; then
echo -e "Ezstream: ${GREEN}Running${NC} (playlist mode)"
else
echo -e "Ezstream: ${YELLOW}Stopped${NC}"
fi
# DarkIce status
if pgrep -f "darkice" >/dev/null 2>&1; then
echo -e "DarkIce: ${GREEN}Running${NC} (live mode)"
else
echo -e "DarkIce: ${YELLOW}Stopped${NC}"
fi
# Exposure status
local domain=$(uci_get exposure.domain "")
if [ -n "$domain" ]; then
echo ""
echo "Exposed at: https://$domain/"
fi
}
#--- Generate Configs ---
cmd_genconfig() {
log "Generating configuration files..."
mkdir -p "$CONF_DIR"
# Generate Icecast config
generate_icecast_config
# Generate Ezstream config
generate_ezstream_config
# Generate DarkIce config
generate_darkice_config
log "Configuration files generated in $CONF_DIR"
}
generate_icecast_config() {
local name=$(uci_get main.name "SecuBox Radio")
local port=$(uci_get main.port 8000)
local max_listeners=$(uci_get main.max_listeners 100)
local source_pass=$(uci_get main.source_password "hackme")
local admin_pass=$(uci_get main.admin_password "admin123")
local relay_pass=$(uci_get main.relay_password "relay123")
cat > "$ICECAST_XML" <<EOF
<icecast>
<location>SecuBox</location>
<admin>admin@secubox.local</admin>
<limits>
<clients>$max_listeners</clients>
<sources>5</sources>
<queue-size>524288</queue-size>
<client-timeout>30</client-timeout>
<header-timeout>15</header-timeout>
<source-timeout>10</source-timeout>
<burst-on-connect>1</burst-on-connect>
<burst-size>65535</burst-size>
</limits>
<authentication>
<source-password>$source_pass</source-password>
<relay-password>$relay_pass</relay-password>
<admin-user>admin</admin-user>
<admin-password>$admin_pass</admin-password>
</authentication>
<hostname>localhost</hostname>
<listen-socket>
<port>$port</port>
<bind-address>0.0.0.0</bind-address>
</listen-socket>
<mount>
<mount-name>/stream</mount-name>
<fallback-mount>/silence.mp3</fallback-mount>
<fallback-override>1</fallback-override>
<stream-name>$name</stream-name>
<stream-description>SecuBox Community Radio</stream-description>
<genre>Various</genre>
<public>0</public>
</mount>
<mount>
<mount-name>/live</mount-name>
<stream-name>$name - Live</stream-name>
<public>0</public>
</mount>
<fileserve>1</fileserve>
<paths>
<basedir>/usr/share/icecast</basedir>
<logdir>/var/log/webradio</logdir>
<webroot>/usr/share/icecast/web</webroot>
<adminroot>/usr/share/icecast/admin</adminroot>
<pidfile>/var/run/icecast.pid</pidfile>
</paths>
<logging>
<accesslog>access.log</accesslog>
<errorlog>error.log</errorlog>
<loglevel>3</loglevel>
</logging>
<security>
<chroot>0</chroot>
<changeowner>
<user>icecast</user>
<group>icecast</group>
</changeowner>
</security>
</icecast>
EOF
}
generate_ezstream_config() {
local port=$(uci_get main.port 8000)
local source_pass=$(uci_get main.source_password "hackme")
local format=$(uci_get stream.format "mp3")
local bitrate=$(uci_get stream.bitrate 128)
local samplerate=$(uci_get stream.samplerate 44100)
local channels=$(uci_get stream.channels 2)
local shuffle=$(uci_get playlist.shuffle 1)
cat > "$EZSTREAM_XML" <<EOF
<ezstream>
<url>http://localhost:$port/stream</url>
<sourcepassword>$source_pass</sourcepassword>
<format>MP3</format>
<filename>$PLAYLIST_FILE</filename>
<playlist_program>0</playlist_program>
<shuffle>$shuffle</shuffle>
<stream_once>0</stream_once>
<reconnect_attempts>-1</reconnect_attempts>
<svrinfoname>SecuBox Radio</svrinfoname>
<svrinfourl>https://secubox.in</svrinfourl>
<svrinfogenre>Various</svrinfogenre>
<svrinfodescription>SecuBox Community Radio</svrinfodescription>
<svrinfobitrate>$bitrate</svrinfobitrate>
<svrinfochannels>$channels</svrinfochannels>
<svrinfosamplerate>$samplerate</svrinfosamplerate>
<svrinfopublic>0</svrinfopublic>
</ezstream>
EOF
}
generate_darkice_config() {
local port=$(uci_get main.port 8000)
local source_pass=$(uci_get main.source_password "hackme")
local device=$(uci_get live.device "default")
local bitrate=$(uci_get live.bitrate 192)
local samplerate=$(uci_get stream.samplerate 44100)
local channels=$(uci_get stream.channels 2)
cat > "$DARKICE_CFG" <<EOF
[general]
duration = 0
bufferSecs = 5
reconnect = yes
[input]
device = $device
sampleRate = $samplerate
bitsPerSample = 16
channel = $channels
[icecast2-0]
bitrateMode = cbr
format = mp3
bitrate = $bitrate
server = localhost
port = $port
password = $source_pass
mountPoint = live
name = SecuBox Radio - Live
description = Live broadcast
genre = Live
public = no
EOF
}
#--- Playlist Management ---
cmd_playlist() {
case "$1" in
generate)
cmd_playlist_generate
;;
list)
cmd_playlist_list
;;
add)
shift
cmd_playlist_add "$@"
;;
*)
cmd_playlist_list
;;
esac
}
cmd_playlist_generate() {
log "Generating playlist..."
mkdir -p "$MUSIC_DIR"
local shuffle=$(uci_get playlist.shuffle 1)
local jingle_interval=$(uci_get playlist.jingle_interval 4)
local jingle_dir=$(uci_get playlist.jingle_directory "$JINGLE_DIR")
# Find all audio files
find "$MUSIC_DIR" -type f \( -name "*.mp3" -o -name "*.ogg" -o -name "*.flac" \) > /tmp/music_files.txt
# Shuffle if enabled (using awk for BusyBox compatibility)
if [ "$shuffle" = "1" ]; then
awk 'BEGIN{srand()} {print rand()"\t"$0}' /tmp/music_files.txt | sort -n | cut -f2- > /tmp/music_shuffled.txt
mv /tmp/music_shuffled.txt /tmp/music_files.txt
fi
# Generate playlist with jingles
rm -f "$PLAYLIST_FILE"
local count=0
while read -r file; do
echo "$file" >> "$PLAYLIST_FILE"
count=$((count + 1))
# Insert jingle every N tracks
if [ "$jingle_interval" -gt 0 ] && [ $((count % jingle_interval)) -eq 0 ]; then
local jingle=$(find "$jingle_dir" -type f -name "*.mp3" 2>/dev/null | sort -R | head -1)
[ -n "$jingle" ] && echo "$jingle" >> "$PLAYLIST_FILE"
fi
done < /tmp/music_files.txt
rm -f /tmp/music_files.txt
local total=$(wc -l < "$PLAYLIST_FILE" 2>/dev/null || echo 0)
log "Playlist generated: $total tracks"
}
cmd_playlist_list() {
echo -e "${CYAN}=== Playlist ===${NC}"
if [ -f "$PLAYLIST_FILE" ]; then
awk '{print NR": "$0}' "$PLAYLIST_FILE" | head -20
local total=$(wc -l < "$PLAYLIST_FILE")
echo "..."
echo "Total: $total tracks"
else
warn "No playlist generated. Run: webradioctl playlist generate"
fi
}
cmd_playlist_add() {
local file="$1"
if [ -f "$file" ]; then
cp "$file" "$MUSIC_DIR/"
log "Added: $(basename "$file")"
else
error "File not found: $file"
return 1
fi
}
#--- Stream Mode (FFmpeg) ---
cmd_stream() {
case "$1" in
start)
cmd_stream_start
;;
stop)
cmd_stream_stop
;;
*)
cmd_stream_status
;;
esac
}
cmd_stream_start() {
if pgrep -f "ffmpeg.*icecast" >/dev/null 2>&1; then
warn "Stream already running"
return 0
fi
# Ensure playlist exists
if [ ! -f "$PLAYLIST_FILE" ] || [ ! -s "$PLAYLIST_FILE" ]; then
cmd_playlist_generate
fi
if [ ! -s "$PLAYLIST_FILE" ]; then
error "No music files found in $MUSIC_DIR"
return 1
fi
local port=$(uci_get main.port 8000)
local source_pass=$(uci_get main.source_password "hackme")
local bitrate=$(uci_get stream.bitrate 128)
local name=$(uci_get main.name "SecuBox Radio")
log "Starting stream to icecast..."
# Create a loop script for continuous streaming
cat > /tmp/webradio_stream.sh << 'STREAMEOF'
#!/bin/sh
PLAYLIST="$1"
PORT="$2"
PASS="$3"
BITRATE="$4"
NAME="$5"
while true; do
while read -r file; do
[ -f "$file" ] || continue
ffmpeg -re -i "$file" \
-vn -acodec libmp3lame -ab ${BITRATE}k -ar 44100 -ac 2 \
-content_type audio/mpeg \
-f mp3 "icecast://source:${PASS}@127.0.0.1:${PORT}/stream" \
2>/var/log/webradio/ffmpeg.log
done < "$PLAYLIST"
# Re-shuffle for next loop (BusyBox compatible)
awk 'BEGIN{srand()} {print rand()"\t"$0}' "$PLAYLIST" | sort -n | cut -f2- > "${PLAYLIST}.tmp" && mv "${PLAYLIST}.tmp" "$PLAYLIST"
done
STREAMEOF
chmod +x /tmp/webradio_stream.sh
nohup /tmp/webradio_stream.sh "$PLAYLIST_FILE" "$port" "$source_pass" "$bitrate" "$name" \
>/dev/null 2>&1 &
sleep 3
if pgrep -f "webradio_stream.sh" >/dev/null 2>&1 || pgrep -f "ffmpeg" >/dev/null 2>&1; then
log "Stream started on http://127.0.0.1:$port/stream"
else
error "Failed to start stream - check /var/log/webradio/ffmpeg.log"
return 1
fi
}
cmd_stream_status() {
local port=$(uci_get main.port 8000)
if pgrep -f "webradio_stream.sh" >/dev/null 2>&1 || pgrep -f "ffmpeg" >/dev/null 2>&1; then
echo -e "FFmpeg Stream: ${GREEN}Running${NC}"
echo "URL: http://127.0.0.1:$port/stream"
# Get current listener count
local listeners=$(curl -s "http://127.0.0.1:$port/status-json.xsl" 2>/dev/null | jsonfilter -e '@.icestats.source.listeners' 2>/dev/null || echo "0")
echo "Listeners: $listeners"
else
echo -e "FFmpeg Stream: ${YELLOW}Stopped${NC}"
fi
}
cmd_stream_stop() {
log "Stopping stream..."
pkill -f "webradio_stream.sh" 2>/dev/null || true
pkill -f "ffmpeg.*icecast" 2>/dev/null || true
log "Stream stopped"
}
#--- Live Mode ---
cmd_live() {
case "$1" in
start)
cmd_live_start
;;
stop)
cmd_live_stop
;;
status)
cmd_live_status
;;
devices)
cmd_live_devices
;;
*)
cmd_live_status
;;
esac
}
cmd_live_start() {
if pgrep -f "darkice" >/dev/null 2>&1; then
warn "DarkIce already running"
return 0
fi
if [ ! -f "$DARKICE_CFG" ]; then
cmd_genconfig
fi
log "Starting live broadcast..."
darkice -c "$DARKICE_CFG" &
sleep 1
if pgrep -f "darkice" >/dev/null 2>&1; then
log "Live broadcast started on /live"
else
error "Failed to start DarkIce"
return 1
fi
}
cmd_live_stop() {
log "Stopping live broadcast..."
pkill -f "darkice" 2>/dev/null || true
log "Live broadcast stopped"
}
cmd_live_status() {
if pgrep -f "darkice" >/dev/null 2>&1; then
echo -e "DarkIce: ${GREEN}Running${NC}"
local device=$(uci_get live.device "default")
echo "Device: $device"
else
echo -e "DarkIce: ${YELLOW}Stopped${NC}"
fi
}
cmd_live_devices() {
echo -e "${CYAN}=== Audio Devices ===${NC}"
if command -v arecord >/dev/null 2>&1; then
arecord -l 2>/dev/null || echo "No capture devices found"
else
echo "alsa-utils not installed"
fi
}
#--- Exposure (Punk Model) ---
cmd_expose() {
local domain="$1"
local channel="${2:-dns}" # dns, tor, mesh, all
if [ -z "$domain" ]; then
error "Usage: webradioctl expose <domain> [dns|tor|mesh|all]"
return 1
fi
local port=$(uci_get main.port 8000)
log "Exposing WebRadio on $domain (channel: $channel)..."
case "$channel" in
dns|all)
# Create HAProxy vhost
if command -v haproxyctl >/dev/null 2>&1; then
# Create backend
haproxyctl backend add webradio_stream 127.0.0.1 "$port" 2>/dev/null || true
# Create vhost
haproxyctl vhost add "$domain" webradio_stream
haproxyctl reload
log "DNS exposure: https://$domain/"
else
error "haproxyctl not available"
fi
;;
esac
case "$channel" in
tor|all)
# Add Tor hidden service
if command -v torctl >/dev/null 2>&1; then
torctl hidden-service add webradio "$port"
local onion=$(torctl hidden-service get webradio 2>/dev/null)
[ -n "$onion" ] && log "Tor exposure: http://$onion/"
else
warn "torctl not available - skipping Tor"
fi
;;
esac
case "$channel" in
mesh|all)
# Publish to mesh
if command -v vortexctl >/dev/null 2>&1; then
vortexctl mesh publish webradio "$domain" "$port"
log "Mesh exposure: Published to P2P network"
else
warn "vortexctl not available - skipping Mesh"
fi
;;
esac
# Save exposure config
uci set webradio.exposure.domain="$domain"
[ "$channel" = "tor" ] || [ "$channel" = "all" ] && uci set webradio.exposure.tor='1'
[ "$channel" = "mesh" ] || [ "$channel" = "all" ] && uci set webradio.exposure.mesh='1'
uci commit webradio
log "Exposure complete!"
}
cmd_unexpose() {
local domain=$(uci_get exposure.domain "")
if [ -z "$domain" ]; then
warn "No exposure configured"
return 0
fi
log "Removing exposure for $domain..."
# Remove HAProxy vhost
if command -v haproxyctl >/dev/null 2>&1; then
haproxyctl vhost del "$domain" 2>/dev/null || true
haproxyctl reload
fi
# Remove Tor hidden service
if command -v torctl >/dev/null 2>&1; then
torctl hidden-service del webradio 2>/dev/null || true
fi
# Remove from mesh
if command -v vortexctl >/dev/null 2>&1; then
vortexctl mesh unpublish webradio 2>/dev/null || true
fi
# Clear config
uci set webradio.exposure.domain=''
uci set webradio.exposure.tor='0'
uci set webradio.exposure.mesh='0'
uci commit webradio
log "Exposure removed"
}
#--- Service Control ---
cmd_start() {
/etc/init.d/webradio start
}
cmd_stop() {
/etc/init.d/webradio stop
}
cmd_restart() {
/etc/init.d/webradio restart
}
cmd_enable() {
uci set webradio.main.enabled='1'
uci commit webradio
/etc/init.d/webradio enable
log "WebRadio enabled"
}
cmd_disable() {
uci set webradio.main.enabled='0'
uci commit webradio
/etc/init.d/webradio disable
log "WebRadio disabled"
}
#--- Skip Track ---
cmd_skip() {
if pgrep -f "ezstream" >/dev/null 2>&1; then
pkill -USR1 -f "ezstream"
log "Skipped to next track"
else
warn "Ezstream not running"
fi
}
#--- Help ---
cmd_help() {
cat <<EOF
${CYAN}WebRadio Controller - SecuBox Backend CLI${NC}
Usage: webradioctl <command> [options]
${GREEN}Service Commands:${NC}
status Show service status
start Start WebRadio services
stop Stop WebRadio services
restart Restart services
enable Enable autostart
disable Disable autostart
${GREEN}Configuration:${NC}
genconfig Generate Icecast/Ezstream/DarkIce configs
${GREEN}Playlist:${NC}
playlist List current playlist
playlist generate Regenerate playlist from music directory
playlist add <file> Add file to music directory
${GREEN}Streaming:${NC}
stream Show stream status
stream start Start FFmpeg stream to Icecast
stream stop Stop streaming
skip Skip to next track
${GREEN}Live Broadcast:${NC}
live Show live status
live start Start DarkIce live input
live stop Stop live broadcast
live devices List audio capture devices
${GREEN}Exposure (Punk Model):${NC}
expose <domain> [channel] Expose radio (dns|tor|mesh|all)
unexpose Remove all exposure
${GREEN}Examples:${NC}
webradioctl enable
webradioctl playlist generate
webradioctl stream start
webradioctl expose radio.secubox.in dns
webradioctl expose radio.secubox.in all
EOF
}
#--- Main ---
case "$1" in
status) cmd_status ;;
start) cmd_start ;;
stop) cmd_stop ;;
restart) cmd_restart ;;
enable) cmd_enable ;;
disable) cmd_disable ;;
genconfig) cmd_genconfig ;;
playlist) shift; cmd_playlist "$@" ;;
stream) shift; cmd_stream "$@" ;;
skip) cmd_skip ;;
live) shift; cmd_live "$@" ;;
expose) shift; cmd_expose "$@" ;;
unexpose) cmd_unexpose ;;
help|--help|-h|"")
cmd_help
;;
*)
error "Unknown command: $1"
cmd_help
exit 1
;;
esac