secubox-openwrt/package/secubox/luci-app-media-flow/root/usr/libexec/rpcd/luci.media-flow
CyberMind-FR c536c9c0f8 feat: Fix and enhance Media Flow module (v0.5.0)
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>
2026-01-08 18:02:39 +01:00

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