Major improvements to the Media Flow streaming detection module: Backend (RPCD): - Rewrite JSON handling to avoid subshell issues - Use jq for all JSON processing (more reliable) - Add delete_alert, clear_history, get_settings, set_settings methods - Expand streaming service patterns (more services detected) - Better bandwidth/quality estimation from netifyd data Data Collection: - Add media-flow-collector script for periodic data collection - Add init script with cron job management - History persists across service restarts - Configurable retention period Frontend: - Remove unused Theme imports - Fix history view to use correct field names - Add Clear History button - Add time period filter with refresh - Improved table display with category icons New streaming services detected: - Video: Peacock, Paramount+, Crunchyroll, Funimation - Audio: Amazon Music, YouTube Music - Video calls: FaceTime, WhatsApp Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
373 lines
11 KiB
Bash
Executable File
373 lines
11 KiB
Bash
Executable File
#!/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
|
|
|
|
local netifyd_running=0
|
|
pgrep -x netifyd > /dev/null 2>&1 && netifyd_running=1
|
|
|
|
local netifyd_data=$(get_netifyd_data)
|
|
local 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
|
|
|
|
local history_count=0
|
|
[ -f "$HISTORY_FILE" ] && history_count=$(jq 'length' "$HISTORY_FILE" 2>/dev/null || echo 0)
|
|
|
|
# Get settings
|
|
local enabled=$(uci -q get media_flow.global.enabled 2>/dev/null || echo "1")
|
|
local 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
|
|
|
|
local netifyd_data=$(get_netifyd_data)
|
|
local streams=$(build_active_streams_json "$netifyd_data")
|
|
|
|
cat <<-EOF
|
|
{"streams": $streams}
|
|
EOF
|
|
;;
|
|
|
|
get_stream_history)
|
|
read -r input
|
|
local hours=$(echo "$input" | jq -r '.hours // 24' 2>/dev/null)
|
|
[ -z "$hours" ] || [ "$hours" = "null" ] && hours=24
|
|
|
|
init_storage
|
|
|
|
local 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
|
|
|
|
local 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
|
|
|
|
local 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
|
|
local service=$(echo "$input" | jq -r '.service // ""' 2>/dev/null)
|
|
|
|
init_storage
|
|
|
|
local 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
|
|
local service=$(echo "$input" | jq -r '.service // ""' 2>/dev/null)
|
|
local threshold_hours=$(echo "$input" | jq -r '.threshold_hours // 4' 2>/dev/null)
|
|
local action=$(echo "$input" | jq -r '.action // "notify"' 2>/dev/null)
|
|
|
|
if [ -z "$service" ]; then
|
|
echo '{"success": false, "message": "Service name required"}'
|
|
exit 0
|
|
fi
|
|
|
|
local 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
|
|
local 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)
|
|
local 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
|
|
local section=$(echo "$line" | cut -d. -f2 | cut -d= -f1)
|
|
local service=$(uci -q get "media_flow.${section}.service")
|
|
local threshold=$(uci -q get "media_flow.${section}.threshold_hours")
|
|
local action=$(uci -q get "media_flow.${section}.action")
|
|
local enabled=$(uci -q get "media_flow.${section}.enabled")
|
|
[ -z "$enabled" ] && enabled="1"
|
|
|
|
cat <<-ALERT
|
|
{"id":"$section","service":"$service","threshold_hours":$threshold,"action":"$action","enabled":$enabled}
|
|
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)
|
|
local enabled=$(uci -q get media_flow.global.enabled 2>/dev/null || echo "1")
|
|
local retention=$(uci -q get media_flow.global.history_retention 2>/dev/null || echo "7")
|
|
local 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
|
|
local enabled=$(echo "$input" | jq -r '.enabled // 1' 2>/dev/null)
|
|
local retention=$(echo "$input" | jq -r '.history_retention // 7' 2>/dev/null)
|
|
local 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
|