#!/bin/sh # RPCD backend for Media Flow # Provides ubus interface: luci.media-flow . /lib/functions.sh . /usr/share/libubox/jshn.sh HISTORY_FILE="/tmp/media-flow-history.json" STATS_DIR="/tmp/media-flow-stats" # Initialize storage init_storage() { mkdir -p "$STATS_DIR" [ ! -f "$HISTORY_FILE" ] && echo '[]' > "$HISTORY_FILE" } # Streaming services patterns STREAMING_VIDEO="netflix|youtube|disney|primevideo|amazon.*video|twitch|hulu|hbo|vimeo|peacock|paramount|crunchyroll|funimation" STREAMING_AUDIO="spotify|apple.*music|deezer|soundcloud|tidal|pandora|amazon.*music|youtube.*music" STREAMING_VISIO="zoom|teams|meet|discord|skype|webex|facetime|whatsapp" # Detect if application is a streaming service is_streaming_service() { local app="$1" echo "$app" | grep -qiE "$STREAMING_VIDEO|$STREAMING_AUDIO|$STREAMING_VISIO" } # Get service category get_service_category() { local app="$1" echo "$app" | grep -qiE "$STREAMING_VIDEO" && echo "video" && return echo "$app" | grep -qiE "$STREAMING_AUDIO" && echo "audio" && return echo "$app" | grep -qiE "$STREAMING_VISIO" && echo "visio" && return echo "other" } # Estimate quality based on bandwidth (kbps) estimate_quality() { local bandwidth="$1" [ -z "$bandwidth" ] && bandwidth=0 if [ "$bandwidth" -lt 1000 ] 2>/dev/null; then echo "SD" elif [ "$bandwidth" -lt 3000 ] 2>/dev/null; then echo "HD" elif [ "$bandwidth" -lt 8000 ] 2>/dev/null; then echo "FHD" else echo "4K" fi } # Get netifyd status data get_netifyd_data() { if [ -f /var/run/netifyd/status.json ]; then cat /var/run/netifyd/status.json else echo '{}' fi } # Build active streams JSON array build_active_streams_json() { local netifyd_data="$1" local result="[]" # Extract flows from netifyd data local flows=$(echo "$netifyd_data" | jq -c '.flows // []' 2>/dev/null) [ -z "$flows" ] || [ "$flows" = "null" ] && flows="[]" # Process each flow and filter streaming services result=$(echo "$flows" | jq -c ' [.[] | select(.detected_application != null and .detected_application != "") | select(.detected_application | test("netflix|youtube|disney|primevideo|amazon.*video|twitch|hulu|hbo|vimeo|spotify|apple.*music|deezer|soundcloud|tidal|zoom|teams|meet|discord|skype|webex"; "i")) | { application: .detected_application, client_ip: (.local_ip // .src_ip // "unknown"), server_ip: (.other_ip // .dst_ip // "unknown"), total_bytes: (.total_bytes // 0), total_packets: (.total_packets // 0), bandwidth_kbps: (if .total_packets > 0 then ((.total_bytes * 8) / 1000 / (if .duration > 0 then .duration else 1 end)) else 0 end | floor), category: (if (.detected_application | test("netflix|youtube|disney|primevideo|twitch|hulu|hbo|vimeo"; "i")) then "video" elif (.detected_application | test("spotify|apple.*music|deezer|soundcloud|tidal"; "i")) then "audio" elif (.detected_application | test("zoom|teams|meet|discord|skype|webex"; "i")) then "visio" else "other" end), quality: (if .total_packets > 0 then (if ((.total_bytes * 8) / 1000 / (if .duration > 0 then .duration else 1 end)) < 1000 then "SD" elif ((.total_bytes * 8) / 1000 / (if .duration > 0 then .duration else 1 end)) < 3000 then "HD" elif ((.total_bytes * 8) / 1000 / (if .duration > 0 then .duration else 1 end)) < 8000 then "FHD" else "4K" end) else "SD" end) }]' 2>/dev/null) || result="[]" echo "$result" } case "$1" in list) cat <<-'EOF' { "status": {}, "get_active_streams": {}, "get_stream_history": {"hours": 24}, "get_stats_by_service": {}, "get_stats_by_client": {}, "get_service_details": {"service": "string"}, "set_alert": {"service": "string", "threshold_hours": 4, "action": "notify"}, "delete_alert": {"alert_id": "string"}, "list_alerts": {}, "clear_history": {}, "get_settings": {}, "set_settings": {"enabled": 1, "history_retention": 7, "refresh_interval": 5} } EOF ;; call) case "$2" in status) init_storage netifyd_running=0 pgrep netifyd > /dev/null 2>&1 && netifyd_running=1 netifyd_data=$(get_netifyd_data) active_count=0 if [ "$netifyd_running" = "1" ] && [ -n "$netifyd_data" ]; then active_count=$(build_active_streams_json "$netifyd_data" | jq 'length' 2>/dev/null || echo 0) fi history_count=0 [ -f "$HISTORY_FILE" ] && history_count=$(jq 'length' "$HISTORY_FILE" 2>/dev/null || echo 0) # Get settings enabled=$(uci -q get media_flow.global.enabled 2>/dev/null || echo "1") refresh=$(uci -q get media_flow.global.refresh_interval 2>/dev/null || echo "5") cat <<-EOF { "enabled": $enabled, "module": "media-flow", "version": "0.5.0", "netifyd_running": $netifyd_running, "active_streams": $active_count, "history_entries": $history_count, "refresh_interval": $refresh } EOF ;; get_active_streams) init_storage netifyd_data=$(get_netifyd_data) streams=$(build_active_streams_json "$netifyd_data") cat <<-EOF {"streams": $streams} EOF ;; get_stream_history) read -r input hours=$(echo "$input" | jq -r '.hours // 24' 2>/dev/null) [ -z "$hours" ] || [ "$hours" = "null" ] && hours=24 init_storage history="[]" if [ -f "$HISTORY_FILE" ]; then # Get history (cutoff filtering done client-side for simplicity) history=$(jq -c '.' "$HISTORY_FILE" 2>/dev/null || echo "[]") fi cat <<-EOF {"history": $history, "hours_requested": $hours} EOF ;; get_stats_by_service) init_storage services="{}" if [ -f "$HISTORY_FILE" ] && [ -s "$HISTORY_FILE" ]; then services=$(jq -c ' group_by(.app) | map({ key: .[0].app, value: { sessions: length, total_bandwidth_kbps: (map(.bandwidth) | add // 0), total_duration_seconds: (map(.duration) | add // 0), category: .[0].category } }) | from_entries ' "$HISTORY_FILE" 2>/dev/null) || services="{}" fi cat <<-EOF {"services": $services} EOF ;; get_stats_by_client) init_storage clients="{}" if [ -f "$HISTORY_FILE" ] && [ -s "$HISTORY_FILE" ]; then clients=$(jq -c ' group_by(.client) | map({ key: .[0].client, value: { sessions: length, total_bandwidth_kbps: (map(.bandwidth) | add // 0), total_duration_seconds: (map(.duration) | add // 0), top_service: (group_by(.app) | max_by(length) | .[0].app // "unknown") } }) | from_entries ' "$HISTORY_FILE" 2>/dev/null) || clients="{}" fi cat <<-EOF {"clients": $clients} EOF ;; get_service_details) read -r input service=$(echo "$input" | jq -r '.service // ""' 2>/dev/null) init_storage result='{}' if [ -n "$service" ] && [ -f "$HISTORY_FILE" ] && [ -s "$HISTORY_FILE" ]; then result=$(jq -c --arg svc "$service" ' [.[] | select(.app == $svc)] | { service: $svc, total_sessions: length, avg_bandwidth_kbps: (if length > 0 then (map(.bandwidth) | add / length | floor) else 0 end), total_duration_seconds: (map(.duration) | add // 0), category: (.[0].category // "unknown"), typical_quality: (.[0].quality // "unknown"), recent_sessions: (.[-10:] | map({ timestamp: .timestamp, client: .client, bandwidth_kbps: .bandwidth, duration_seconds: .duration, quality: .quality })) } ' "$HISTORY_FILE" 2>/dev/null) || result='{"service":"'$service'","total_sessions":0,"avg_bandwidth_kbps":0,"total_duration_seconds":0,"category":"unknown","typical_quality":"unknown","recent_sessions":[]}' else result='{"service":"'$service'","total_sessions":0,"avg_bandwidth_kbps":0,"total_duration_seconds":0,"category":"unknown","typical_quality":"unknown","recent_sessions":[]}' fi echo "$result" ;; set_alert) read -r input service=$(echo "$input" | jq -r '.service // ""' 2>/dev/null) threshold_hours=$(echo "$input" | jq -r '.threshold_hours // 4' 2>/dev/null) action=$(echo "$input" | jq -r '.action // "notify"' 2>/dev/null) if [ -z "$service" ]; then echo '{"success": false, "message": "Service name required"}' exit 0 fi alert_id="alert_$(echo "$service" | tr -d ' ' | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9_')" uci -q delete "media_flow.${alert_id}" 2>/dev/null uci set "media_flow.${alert_id}=alert" uci set "media_flow.${alert_id}.service=${service}" uci set "media_flow.${alert_id}.threshold_hours=${threshold_hours}" uci set "media_flow.${alert_id}.action=${action}" uci set "media_flow.${alert_id}.enabled=1" uci commit media_flow cat <<-EOF {"success": true, "message": "Alert configured for $service", "alert_id": "$alert_id"} EOF ;; delete_alert) read -r input alert_id=$(echo "$input" | jq -r '.alert_id // ""' 2>/dev/null) if [ -z "$alert_id" ]; then echo '{"success": false, "message": "Alert ID required"}' exit 0 fi if uci -q get "media_flow.${alert_id}" >/dev/null 2>&1; then uci delete "media_flow.${alert_id}" uci commit media_flow echo '{"success": true, "message": "Alert deleted"}' else echo '{"success": false, "message": "Alert not found"}' fi ;; list_alerts) alerts="[]" # Use jq to build the alerts array from UCI alerts=$(uci show media_flow 2>/dev/null | grep "=alert$" | while read -r line; do section=$(echo "$line" | cut -d. -f2 | cut -d= -f1) svc=$(uci -q get "media_flow.${section}.service") threshold=$(uci -q get "media_flow.${section}.threshold_hours") act=$(uci -q get "media_flow.${section}.action") en=$(uci -q get "media_flow.${section}.enabled") [ -z "$en" ] && en="1" cat <<-ALERT {"id":"$section","service":"$svc","threshold_hours":$threshold,"action":"$act","enabled":$en} ALERT done | jq -s '.' 2>/dev/null) || alerts="[]" [ -z "$alerts" ] || [ "$alerts" = "null" ] && alerts="[]" cat <<-EOF {"alerts": $alerts} EOF ;; clear_history) echo '[]' > "$HISTORY_FILE" echo '{"success": true, "message": "History cleared"}' ;; get_settings) enabled=$(uci -q get media_flow.global.enabled 2>/dev/null || echo "1") retention=$(uci -q get media_flow.global.history_retention 2>/dev/null || echo "7") refresh=$(uci -q get media_flow.global.refresh_interval 2>/dev/null || echo "5") cat <<-EOF { "enabled": $enabled, "history_retention": $retention, "refresh_interval": $refresh } EOF ;; set_settings) read -r input enabled=$(echo "$input" | jq -r '.enabled // 1' 2>/dev/null) retention=$(echo "$input" | jq -r '.history_retention // 7' 2>/dev/null) refresh=$(echo "$input" | jq -r '.refresh_interval // 5' 2>/dev/null) uci set media_flow.global.enabled="$enabled" uci set media_flow.global.history_retention="$retention" uci set media_flow.global.refresh_interval="$refresh" uci commit media_flow echo '{"success": true, "message": "Settings saved"}' ;; *) cat <<-EOF {"error": -32601, "message": "Method not found: $2"} EOF ;; esac ;; esac