feat: Update Media Flow for netifyd 5.x compatibility (v0.5.2)

- Adapt RPCD backend to use netifyd 5.x status.json structure
- Read flows_active/flow_count from proper fields
- Extract agent_version instead of version
- Parse interface stats from .stats object
- Add get_network_stats endpoint with CPU/memory metrics
- Update dashboard to show netifyd limitation notice
- Display flow count and network statistics instead of streams

Note: netifyd 5.x requires cloud subscription for application
detection. Local mode only provides aggregate flow statistics.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-01-08 18:54:19 +01:00
parent 17bdd6e80b
commit c9f719a8de
3 changed files with 102 additions and 144 deletions

View File

@ -4,7 +4,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-media-flow
PKG_VERSION:=0.5.1
PKG_VERSION:=0.5.2
PKG_RELEASE:=1
PKG_ARCH:=all
PKG_LICENSE:=Apache-2.0

View File

@ -15,13 +15,13 @@ return L.view.extend({
render: function(data) {
var status = data[0] || {};
var activeStreams = data[1] || [];
var streamsData = data[1] || {};
var statsByService = data[2] || {};
var v = E('div', { 'class': 'cbi-map' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('h2', {}, _('Media Flow Dashboard')),
E('div', { 'class': 'cbi-map-descr' }, _('Real-time detection and monitoring of streaming services'))
E('div', { 'class': 'cbi-map-descr' }, _('Network flow monitoring and statistics'))
]);
// Status overview
@ -35,85 +35,66 @@ return L.view.extend({
]),
E('div', { 'class': 'td left', 'width': '33%' }, [
E('strong', {}, _('Netifyd: ')),
E('span', {}, status.netifyd_running ?
E('span', { 'style': 'color: green' }, '● ' + _('Running')) :
E('span', {}, status.netifyd_running ?
E('span', { 'style': 'color: green' }, '● ' + _('Running') + ' (v' + (status.netifyd_version || '?') + ')') :
E('span', { 'style': 'color: red' }, '● ' + _('Stopped'))
)
]),
E('div', { 'class': 'td left', 'width': '33%' }, [
E('strong', {}, _('Active Streams: ')),
E('span', { 'style': 'font-size: 1.5em; color: #0088cc' }, String(status.active_streams || 0))
E('strong', {}, _('Active Flows: ')),
E('span', { 'style': 'font-size: 1.5em; color: #0088cc' }, String(status.active_flows || 0))
])
])
])
]);
v.appendChild(statusSection);
// Active streams
var activeSection = E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Active Streams')),
E('div', { 'id': 'active-streams-table' })
// Netifyd 5.x limitation notice
var noticeSection = E('div', { 'class': 'cbi-section' }, [
E('div', { 'class': 'alert-message warning', 'style': 'background: #fff3cd; border: 1px solid #ffc107; padding: 15px; border-radius: 4px; margin-bottom: 15px;' }, [
E('strong', {}, _('Notice: ')),
E('span', {}, _('Netifyd 5.x requires a cloud subscription for streaming service detection. Currently showing network flow statistics only.'))
])
]);
v.appendChild(noticeSection);
// Network flow stats
var flowSection = E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Network Flows')),
E('div', { 'id': 'flow-stats-container' })
]);
var updateActiveStreams = function() {
API.getActiveStreams().then(function(streams) {
var table = E('table', { 'class': 'table' }, [
E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th' }, _('Service')),
E('th', { 'class': 'th' }, _('Category')),
E('th', { 'class': 'th' }, _('Client')),
E('th', { 'class': 'th' }, _('Quality')),
E('th', { 'class': 'th' }, _('Bandwidth'))
var updateFlowStats = function() {
API.getActiveStreams().then(function(data) {
var container = document.getElementById('flow-stats-container');
if (!container) return;
var flowCount = data.flow_count || 0;
var note = data.note || '';
container.innerHTML = '';
container.appendChild(E('div', { 'class': 'table', 'style': 'background: #f8f9fa; padding: 20px; border-radius: 8px;' }, [
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td', 'style': 'text-align: center;' }, [
E('div', { 'style': 'font-size: 3em; color: #0088cc; font-weight: bold;' }, String(flowCount)),
E('div', { 'style': 'color: #666; margin-top: 5px;' }, _('Active Network Flows'))
])
])
]);
]));
if (streams && streams.length > 0) {
streams.forEach(function(stream) {
var qualityColor = {
'SD': '#999',
'HD': '#0088cc',
'FHD': '#00cc00',
'4K': '#cc0000'
}[stream.quality] || '#666';
var categoryIcon = {
'video': '🎬',
'audio': '🎵',
'visio': '📹'
}[stream.category] || '📊';
table.appendChild(E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td' }, categoryIcon + ' ' + stream.application),
E('td', { 'class': 'td' }, stream.category),
E('td', { 'class': 'td' }, stream.client_ip),
E('td', { 'class': 'td' },
E('span', { 'style': 'color: ' + qualityColor + '; font-weight: bold' }, stream.quality)
),
E('td', { 'class': 'td' }, stream.bandwidth_kbps + ' kbps')
]));
});
} else {
table.appendChild(E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td', 'colspan': '5', 'style': 'text-align: center; font-style: italic' },
_('No active streams detected')
)
]));
}
var container = document.getElementById('active-streams-table');
if (container) {
container.innerHTML = '';
container.appendChild(table);
if (note) {
container.appendChild(E('p', { 'style': 'font-style: italic; color: #666; text-align: center; margin-top: 10px;' }, note));
}
});
};
updateActiveStreams();
v.appendChild(activeSection);
updateFlowStats();
v.appendChild(flowSection);
// Stats by service (donut chart + bars)
// Stats by service (from history)
var statsSection = E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Usage by Service')),
E('h3', {}, _('Historical Usage by Service')),
E('p', { 'style': 'color: #666; font-size: 0.9em;' }, _('Data collected from previous sessions (if available)')),
E('div', { 'style': 'display: flex; gap: 20px;' }, [
E('div', { 'style': 'flex: 0 0 300px;' }, [
E('canvas', {
@ -205,7 +186,7 @@ return L.view.extend({
var servicesList = Object.keys(services);
if (servicesList.length === 0) {
container.appendChild(E('p', { 'style': 'font-style: italic' }, _('No historical data available')));
container.appendChild(E('p', { 'style': 'font-style: italic' }, _('No historical data available. Stream detection requires netifyd cloud subscription.')));
drawDonutChart({}, [], 0);
return;
}
@ -265,7 +246,7 @@ return L.view.extend({
// Setup auto-refresh
poll.add(L.bind(function() {
updateActiveStreams();
updateFlowStats();
updateServiceStats();
}, this), 5);

View File

@ -1,6 +1,8 @@
#!/bin/sh
# RPCD backend for Media Flow
# Provides ubus interface: luci.media-flow
# Note: netifyd 5.x does not export per-flow application data locally
# This module shows available network statistics from netifyd
. /lib/functions.sh
. /usr/share/libubox/jshn.sh
@ -14,42 +16,6 @@ init_storage() {
[ ! -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
@ -59,39 +25,32 @@ get_netifyd_data() {
fi
}
# Build active streams JSON array
build_active_streams_json() {
local netifyd_data="$1"
local result="[]"
# Build network stats from netifyd status.json
build_network_stats_json() {
netifyd_data="$1"
# 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"
# Extract interface stats from netifyd
echo "$netifyd_data" | jq -c '{
interfaces: (if .stats then
[.stats | to_entries[] | {
name: .key,
rx_bytes: (.value.ip_bytes // 0),
tx_bytes: (.value.wire_bytes // 0),
rx_packets: (.value.ip // 0),
tx_packets: (.value.raw // 0),
tcp: (.value.tcp // 0),
udp: (.value.udp // 0),
icmp: (.value.icmp // 0)
}]
else [] end),
flows_active: (.flows_active // 0),
flow_count: (.flow_count // 0),
uptime: (.uptime // 0),
agent_version: (.agent_version // 0),
cpu_system: (.cpu_system // 0),
cpu_user: (.cpu_user // 0),
memrss_kb: (.memrss_kb // 0)
}' 2>/dev/null || echo '{"interfaces":[],"flows_active":0,"flow_count":0,"uptime":0,"agent_version":0}'
}
case "$1" in
@ -100,6 +59,7 @@ case "$1" in
{
"status": {},
"get_active_streams": {},
"get_network_stats": {},
"get_stream_history": {"hours": 24},
"get_stats_by_service": {},
"get_stats_by_client": {},
@ -123,16 +83,17 @@ case "$1" in
pgrep netifyd > /dev/null 2>&1 && netifyd_running=1
netifyd_data=$(get_netifyd_data)
active_count=0
flow_count=0
netifyd_version="unknown"
if [ "$netifyd_running" = "1" ] && [ -n "$netifyd_data" ]; then
active_count=$(build_active_streams_json "$netifyd_data" | jq 'length' 2>/dev/null || echo 0)
if [ "$netifyd_running" = "1" ] && [ -n "$netifyd_data" ] && [ "$netifyd_data" != "{}" ]; then
flow_count=$(echo "$netifyd_data" | jq '.flows_active // .flow_count // 0' 2>/dev/null || echo 0)
netifyd_version=$(echo "$netifyd_data" | jq -r '.agent_version // "unknown"' 2>/dev/null || echo "unknown")
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")
@ -140,11 +101,13 @@ case "$1" in
{
"enabled": $enabled,
"module": "media-flow",
"version": "0.5.0",
"version": "0.5.2",
"netifyd_running": $netifyd_running,
"active_streams": $active_count,
"netifyd_version": "$netifyd_version",
"active_flows": $flow_count,
"history_entries": $history_count,
"refresh_interval": $refresh
"refresh_interval": $refresh,
"note": "netifyd 5.x requires cloud subscription for streaming detection"
}
EOF
;;
@ -153,13 +116,28 @@ case "$1" in
init_storage
netifyd_data=$(get_netifyd_data)
streams=$(build_active_streams_json "$netifyd_data")
flow_count=$(echo "$netifyd_data" | jq '.flows_active // .flow_count // 0' 2>/dev/null || echo 0)
# netifyd 5.x doesn't export application detection locally
# Return empty streams with explanation
cat <<-EOF
{"streams": $streams}
{
"streams": [],
"note": "Application detection requires netifyd cloud subscription",
"flow_count": $flow_count
}
EOF
;;
get_network_stats)
init_storage
netifyd_data=$(get_netifyd_data)
stats=$(build_network_stats_json "$netifyd_data")
echo "{\"stats\": $stats}"
;;
get_stream_history)
read -r input
hours=$(echo "$input" | jq -r '.hours // 24' 2>/dev/null)
@ -169,7 +147,6 @@ case "$1" in
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
@ -308,7 +285,7 @@ case "$1" in
list_alerts)
alerts="[]"
# Use jq to build the alerts array from UCI
# Build 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")