mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-17 16:50:53 +00:00
Enhancement: VOD start/stop notifications and system events.
Some checks are pending
CI Pipeline / prepare (push) Waiting to run
CI Pipeline / docker (amd64, ubuntu-24.04) (push) Blocked by required conditions
CI Pipeline / docker (arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
CI Pipeline / create-manifest (push) Blocked by required conditions
Build and Push Multi-Arch Docker Image / build-and-push (push) Waiting to run
Frontend Tests / test (push) Waiting to run
Some checks are pending
CI Pipeline / prepare (push) Waiting to run
CI Pipeline / docker (amd64, ubuntu-24.04) (push) Blocked by required conditions
CI Pipeline / docker (arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
CI Pipeline / create-manifest (push) Blocked by required conditions
Build and Push Multi-Arch Docker Image / build-and-push (push) Waiting to run
Frontend Tests / test (push) Waiting to run
This commit is contained in:
parent
60e82b7b01
commit
29739ede36
12 changed files with 229 additions and 41 deletions
|
|
@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Added
|
||||
|
||||
- **VOD start/stop notifications**: the frontend now shows a toast notification when a VOD stream starts or stops. `vod_started` and `vod_stopped` WebSocket events are fired from the backend when a new provider connection is opened or the last active stream on a session ends. The 1-second delayed-cleanup window is used as a settle period before firing `vod_stopped`, so seek reconnects that re-establish `active_streams` within that window suppress the notification. A `vod_stats` WebSocket push is sent alongside every event to keep the Stats page connection table in sync in real time. `vod_start` and `vod_stop` system events are also written to the system event log for each transition, and are now selectable as integration trigger events (webhook/script) in the Connect page.
|
||||
- **Channel Profiles column in Users table**: users can now see all channel profiles assigned to each user directly in the Users table. Added tooltips for profile names to handle overflow, adjusted column sizing to prevent overflow between columns, and improved the table layout for better readability. (Closes #819) — Thanks [@damien-alt-sudo](https://github.com/damien-alt-sudo)
|
||||
- **Plugin warning & disclaimer components**: extracted shared plugin warning UI into a new `PluginWarnings.jsx` component and normalized warning/disclaimer usage across all plugin action modals. New reusable components: `PluginSecurityWarning` (untrusted code), `PluginSupportDisclaimer` (community support scope), `PluginDowngradeWarning` (version downgrade), `PluginInfoNote` (informational), and `PluginRestartWarning` (backend restart on import). Updated `Plugins.jsx`, `AvailablePluginCard.jsx`, and `PluginCard.jsx` to use the shared components. — Thanks [@sethwv](https://github.com/sethwv)
|
||||
- **Plugin file sizes**: the plugin hub now displays the download size of a plugin directly on the Install / Update / Downgrade / Overwrite buttons (e.g. `Install 142 KB`) when the repository manifest includes size data. The size is also shown in the version detail panel. Falls back gracefully to a plain button when no size is provided. A `formatKB` utility was added to convert raw KB values to human-readable strings (KB/MB). A "Publish Your Plugin" button linking to the contributing guide was added to the plugin store toolbar. — Thanks [@sethwv](https://github.com/sethwv)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 6.0.4 on 2026-04-23 23:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dispatcharr_connect', '0002_alter_eventsubscription_event'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='eventsubscription',
|
||||
name='event',
|
||||
field=models.CharField(choices=[('channel_start', 'Channel Started'), ('channel_stop', 'Channel Stopped'), ('channel_reconnect', 'Channel Reconnected'), ('channel_error', 'Channel Error'), ('channel_failover', 'Channel Failover'), ('stream_switch', 'Stream Switch'), ('recording_start', 'Recording Started'), ('recording_end', 'Recording Ended'), ('epg_refresh', 'EPG Refreshed'), ('m3u_refresh', 'M3U Refreshed'), ('client_connect', 'Client Connected'), ('client_disconnect', 'Client Disconnected'), ('login_failed', 'Login Failed'), ('epg_blocked', 'EPG Blocked'), ('m3u_blocked', 'M3U Blocked'), ('vod_start', 'VOD Started'), ('vod_stop', 'VOD Stopped')], max_length=100),
|
||||
),
|
||||
]
|
||||
|
|
@ -16,6 +16,8 @@ SUPPORTED_EVENTS = {
|
|||
"login_failed": "Login Failed",
|
||||
"epg_blocked": "EPG Blocked",
|
||||
"m3u_blocked": "M3U Blocked",
|
||||
"vod_start": "VOD Started",
|
||||
"vod_stop": "VOD Stopped",
|
||||
}
|
||||
|
||||
class Integration(models.Model):
|
||||
|
|
|
|||
|
|
@ -797,6 +797,67 @@ class MultiWorkerVODConnectionManager:
|
|||
logger.error(f"Error incrementing profile connections: {e}")
|
||||
return None
|
||||
|
||||
def _trigger_vod_stats_update(self):
|
||||
"""Trigger a VOD stats WebSocket update in a background thread."""
|
||||
threading.Thread(target=self._do_vod_stats_update, daemon=True).start()
|
||||
|
||||
def _send_vod_event(self, event_type, session_id, content_name, content_uuid, client_ip, user_id, username=None):
|
||||
"""Send a vod_started or vod_stopped WebSocket event, log a system event, then update stats."""
|
||||
try:
|
||||
from core.utils import send_websocket_update, log_system_event
|
||||
if not self.redis_client:
|
||||
return
|
||||
|
||||
send_websocket_update(
|
||||
"updates",
|
||||
"update",
|
||||
{
|
||||
"type": event_type,
|
||||
"content_name": content_name,
|
||||
"content_uuid": content_uuid,
|
||||
"client_ip": client_ip,
|
||||
"user_id": user_id,
|
||||
}
|
||||
)
|
||||
|
||||
system_event_type = 'vod_start' if event_type == 'vod_started' else 'vod_stop'
|
||||
try:
|
||||
log_system_event(
|
||||
system_event_type,
|
||||
content_name=content_name,
|
||||
content_uuid=content_uuid,
|
||||
client_ip=client_ip,
|
||||
username=username,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Could not log system event {system_event_type}: {e}")
|
||||
|
||||
self._trigger_vod_stats_update()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send {event_type}: {e}")
|
||||
|
||||
def _do_vod_stats_update(self):
|
||||
"""Collect active VOD connections (with full DB metadata) and push via WebSocket."""
|
||||
try:
|
||||
from core.utils import send_websocket_update
|
||||
from apps.proxy.vod_proxy.views import build_vod_stats_data
|
||||
if not self.redis_client:
|
||||
return
|
||||
|
||||
stats = build_vod_stats_data(self.redis_client)
|
||||
|
||||
send_websocket_update(
|
||||
"updates",
|
||||
"update",
|
||||
{
|
||||
"type": "vod_stats",
|
||||
"stats": json.dumps(stats)
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to trigger VOD stats update: {e}")
|
||||
|
||||
def _decrement_profile_connections(self, m3u_profile_id: int):
|
||||
"""Decrement profile connection count.
|
||||
|
||||
|
|
@ -1005,6 +1066,16 @@ class MultiWorkerVODConnectionManager:
|
|||
# to prevent cleanup race conditions with GeneratorExit.
|
||||
if not existing_state:
|
||||
redis_connection.increment_active_streams()
|
||||
threading.Thread(
|
||||
target=self._send_vod_event,
|
||||
args=(
|
||||
'vod_started', client_id, content_name,
|
||||
content_uuid, client_ip,
|
||||
str(user.id) if user else '0',
|
||||
user.username if user else None
|
||||
),
|
||||
daemon=True
|
||||
).start()
|
||||
else:
|
||||
logger.debug(f"[{client_id}] Active streams already incremented in connection reuse path")
|
||||
|
||||
|
|
@ -1061,12 +1132,18 @@ class MultiWorkerVODConnectionManager:
|
|||
|
||||
def delayed_cleanup():
|
||||
time.sleep(1) # Wait 1 second
|
||||
# Smart cleanup: check active streams and ownership
|
||||
# Re-check active_streams: a seeking/reconnecting client may
|
||||
# have incremented it within the settle window.
|
||||
if not redis_connection.has_active_streams():
|
||||
self._send_vod_event(
|
||||
'vod_stopped', client_id, content_name,
|
||||
content_uuid, client_ip,
|
||||
str(user.id) if user else '0',
|
||||
user.username if user else None
|
||||
)
|
||||
logger.info(f"[{client_id}] Worker {self.worker_id} - Checking for smart cleanup after normal completion")
|
||||
# No connection_manager — profile already decremented above
|
||||
redis_connection.cleanup(current_worker_id=self.worker_id)
|
||||
|
||||
import threading
|
||||
cleanup_thread = threading.Thread(target=delayed_cleanup)
|
||||
cleanup_thread.daemon = True
|
||||
cleanup_thread.start()
|
||||
|
|
@ -1090,12 +1167,18 @@ class MultiWorkerVODConnectionManager:
|
|||
|
||||
def delayed_cleanup():
|
||||
time.sleep(1) # Wait 1 second
|
||||
# Smart cleanup: check active streams and ownership
|
||||
# Re-check active_streams: a seeking/reconnecting client may
|
||||
# have incremented it within the settle window.
|
||||
if not redis_connection.has_active_streams():
|
||||
self._send_vod_event(
|
||||
'vod_stopped', client_id, content_name,
|
||||
content_uuid, client_ip,
|
||||
str(user.id) if user else '0',
|
||||
user.username if user else None
|
||||
)
|
||||
logger.info(f"[{client_id}] Worker {self.worker_id} - Checking for smart cleanup after client disconnect")
|
||||
# No connection_manager — profile already decremented above
|
||||
redis_connection.cleanup(current_worker_id=self.worker_id)
|
||||
|
||||
import threading
|
||||
cleanup_thread = threading.Thread(target=delayed_cleanup)
|
||||
cleanup_thread.daemon = True
|
||||
cleanup_thread.start()
|
||||
|
|
@ -1142,7 +1225,6 @@ class MultiWorkerVODConnectionManager:
|
|||
# No connection_manager — profile already decremented above
|
||||
redis_connection.cleanup(current_worker_id=self.worker_id)
|
||||
|
||||
import threading
|
||||
cleanup_thread = threading.Thread(target=delayed_cleanup)
|
||||
cleanup_thread.daemon = True
|
||||
cleanup_thread.start()
|
||||
|
|
|
|||
|
|
@ -684,17 +684,13 @@ def head_vod(request, content_type, content_id, session_id=None, profile_id=None
|
|||
logger.error(f"[VOD-HEAD] Error in HEAD request: {e}", exc_info=True)
|
||||
return HttpResponse(f"HEAD error: {str(e)}", status=500)
|
||||
|
||||
@api_view(["GET"])
|
||||
@permission_classes([IsAdmin])
|
||||
def vod_stats(request):
|
||||
"""Get current VOD connection statistics"""
|
||||
def build_vod_stats_data(redis_client):
|
||||
"""
|
||||
Build the full VOD stats payload (with DB lookups) from Redis connection data.
|
||||
Returns a dict: {'vod_connections': [...], 'total_connections': N, 'timestamp': T}
|
||||
Used by both the vod_stats API view and the WebSocket push in _do_vod_stats_update.
|
||||
"""
|
||||
try:
|
||||
connection_manager = MultiWorkerVODConnectionManager.get_instance()
|
||||
redis_client = connection_manager.redis_client
|
||||
|
||||
if not redis_client:
|
||||
return JsonResponse({'error': 'Redis not available'}, status=500)
|
||||
|
||||
# Get all VOD persistent connections (consolidated data)
|
||||
pattern = "vod_persistent_connection:*"
|
||||
cursor = 0
|
||||
|
|
@ -936,11 +932,29 @@ def vod_stats(request):
|
|||
content_stats[content_key]['connection_count'] += 1
|
||||
content_stats[content_key]['connections'].append(conn)
|
||||
|
||||
return JsonResponse({
|
||||
return {
|
||||
'vod_connections': list(content_stats.values()),
|
||||
'total_connections': len(connections),
|
||||
'timestamp': current_time
|
||||
})
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error building VOD stats: {e}")
|
||||
return {'vod_connections': [], 'total_connections': 0, 'timestamp': time.time()}
|
||||
|
||||
|
||||
@api_view(["GET"])
|
||||
@permission_classes([IsAdmin])
|
||||
def vod_stats(request):
|
||||
"""Get current VOD connection statistics"""
|
||||
try:
|
||||
connection_manager = MultiWorkerVODConnectionManager.get_instance()
|
||||
redis_client = connection_manager.redis_client
|
||||
|
||||
if not redis_client:
|
||||
return JsonResponse({'error': 'Redis not available'}, status=500)
|
||||
|
||||
return JsonResponse(build_vod_stats_data(redis_client))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting VOD stats: {e}")
|
||||
|
|
|
|||
18
core/migrations/0023_alter_systemevent_event_type.py
Normal file
18
core/migrations/0023_alter_systemevent_event_type.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 6.0.4 on 2026-04-23 22:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '022_default_user_limit_settings'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='systemevent',
|
||||
name='event_type',
|
||||
field=models.CharField(choices=[('channel_start', 'Channel Started'), ('channel_stop', 'Channel Stopped'), ('channel_buffering', 'Channel Buffering'), ('channel_failover', 'Channel Failover'), ('channel_reconnect', 'Channel Reconnected'), ('channel_error', 'Channel Error'), ('client_connect', 'Client Connected'), ('client_disconnect', 'Client Disconnected'), ('recording_start', 'Recording Started'), ('recording_end', 'Recording Ended'), ('stream_switch', 'Stream Switched'), ('m3u_refresh', 'M3U Refreshed'), ('m3u_download', 'M3U Downloaded'), ('epg_refresh', 'EPG Refreshed'), ('epg_download', 'EPG Downloaded'), ('login_success', 'Login Successful'), ('login_failed', 'Login Failed'), ('logout', 'User Logged Out'), ('m3u_blocked', 'M3U Download Blocked'), ('epg_blocked', 'EPG Download Blocked'), ('vod_start', 'VOD Started'), ('vod_stop', 'VOD Stopped')], db_index=True, max_length=50),
|
||||
),
|
||||
]
|
||||
|
|
@ -399,6 +399,8 @@ class SystemEvent(models.Model):
|
|||
('logout', 'User Logged Out'),
|
||||
('m3u_blocked', 'M3U Download Blocked'),
|
||||
('epg_blocked', 'EPG Download Blocked'),
|
||||
('vod_start', 'VOD Started'),
|
||||
('vod_stop', 'VOD Stopped'),
|
||||
]
|
||||
|
||||
event_type = models.CharField(max_length=50, choices=EVENT_TYPES, db_index=True)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { Box, Button, Stack, Alert, Group } from '@mantine/core';
|
|||
import API from './api';
|
||||
import useSettingsStore from './store/settings';
|
||||
import useAuthStore from './store/auth';
|
||||
import useUsersStore from './store/users';
|
||||
|
||||
export const WebsocketContext = createContext([false, () => {}, null]);
|
||||
|
||||
|
|
@ -211,13 +212,18 @@ export const WebsocketProvider = ({ children }) => {
|
|||
scheduleRecordingFetch();
|
||||
} else if (status === 'skipped') {
|
||||
const reasonMap = {
|
||||
no_commercials_detected: 'No commercials were detected in this recording',
|
||||
no_commercials: 'No commercials were detected in this recording',
|
||||
no_commercials_detected:
|
||||
'No commercials were detected in this recording',
|
||||
no_commercials:
|
||||
'No commercials were detected in this recording',
|
||||
};
|
||||
notifications.update({
|
||||
id,
|
||||
title: 'No commercials to remove',
|
||||
message: reasonMap[parsedEvent.data.reason] || parsedEvent.data.reason || '',
|
||||
message:
|
||||
reasonMap[parsedEvent.data.reason] ||
|
||||
parsedEvent.data.reason ||
|
||||
'',
|
||||
color: 'teal',
|
||||
loading: false,
|
||||
autoClose: 3000,
|
||||
|
|
@ -311,6 +317,36 @@ export const WebsocketProvider = ({ children }) => {
|
|||
setChannelStats(JSON.parse(parsedEvent.data.stats));
|
||||
break;
|
||||
|
||||
case 'vod_stats':
|
||||
setVodStats(JSON.parse(parsedEvent.data.stats));
|
||||
break;
|
||||
|
||||
case 'vod_started':
|
||||
case 'vod_stopped': {
|
||||
const { content_name, client_ip, user_id } = parsedEvent.data;
|
||||
const isStart = parsedEvent.data.type === 'vod_started';
|
||||
let identity = client_ip || 'unknown';
|
||||
if (user_id && user_id !== '0') {
|
||||
const allUsers = useUsersStore.getState().users;
|
||||
const matched = allUsers.find(
|
||||
(u) => String(u.id) === String(user_id)
|
||||
);
|
||||
if (matched?.username)
|
||||
identity = `${matched.username} (${client_ip})`;
|
||||
}
|
||||
notifications.show({
|
||||
title: isStart ? 'VOD started' : 'VOD ended',
|
||||
message: (
|
||||
<>
|
||||
<div>{content_name}</div>
|
||||
<div style={{ marginTop: 2 }}>{identity}</div>
|
||||
</>
|
||||
),
|
||||
color: 'blue.5',
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'epg_channels':
|
||||
notifications.show({
|
||||
message: 'EPG channels updated!',
|
||||
|
|
@ -559,7 +595,9 @@ export const WebsocketProvider = ({ children }) => {
|
|||
|
||||
case 'recording_cancelled':
|
||||
notifications.show({
|
||||
title: parsedEvent.data.was_in_progress ? 'Recording cancelled' : 'Recording deleted',
|
||||
title: parsedEvent.data.was_in_progress
|
||||
? 'Recording cancelled'
|
||||
: 'Recording deleted',
|
||||
message: parsedEvent.data.was_in_progress
|
||||
? 'Recording cancelled and content removed.'
|
||||
: 'Recording deleted.',
|
||||
|
|
@ -568,7 +606,9 @@ export const WebsocketProvider = ({ children }) => {
|
|||
// Surgical removal by ID avoids a full fetchRecordings() re-render.
|
||||
// Fall back to a full refresh if the ID is missing (e.g. older server).
|
||||
if (parsedEvent.data.recording_id != null) {
|
||||
useChannelsStore.getState().removeRecording(parsedEvent.data.recording_id);
|
||||
useChannelsStore
|
||||
.getState()
|
||||
.removeRecording(parsedEvent.data.recording_id);
|
||||
} else {
|
||||
scheduleRecordingFetch();
|
||||
}
|
||||
|
|
@ -990,6 +1030,7 @@ export const WebsocketProvider = ({ children }) => {
|
|||
}, [connectWebSocket, clearReconnectTimer, isAuthenticated, accessToken]);
|
||||
|
||||
const setChannelStats = useChannelsStore((s) => s.setChannelStats);
|
||||
const setVodStats = useChannelsStore((s) => s.setVodStats);
|
||||
const fetchPlaylists = usePlaylistsStore((s) => s.fetchPlaylists);
|
||||
const setRefreshProgress = usePlaylistsStore((s) => s.setRefreshProgress);
|
||||
const setProfilePreview = usePlaylistsStore((s) => s.setProfilePreview);
|
||||
|
|
|
|||
|
|
@ -53,6 +53,10 @@ const getEventIcon = (eventType) => {
|
|||
return <Video size={16} />;
|
||||
case 'recording_end':
|
||||
return <Video size={16} />;
|
||||
case 'vod_start':
|
||||
return <CirclePlay size={16} />;
|
||||
case 'vod_stop':
|
||||
return <SquareX size={16} />;
|
||||
case 'stream_switch':
|
||||
return <HardDriveDownload size={16} />;
|
||||
case 'm3u_refresh':
|
||||
|
|
@ -83,6 +87,7 @@ const getEventColor = (eventType) => {
|
|||
case 'channel_start':
|
||||
case 'client_connect':
|
||||
case 'recording_start':
|
||||
case 'vod_start':
|
||||
case 'login_success':
|
||||
return 'green';
|
||||
case 'channel_reconnect':
|
||||
|
|
@ -90,6 +95,7 @@ const getEventColor = (eventType) => {
|
|||
case 'channel_stop':
|
||||
case 'client_disconnect':
|
||||
case 'recording_end':
|
||||
case 'vod_stop':
|
||||
case 'logout':
|
||||
return 'gray';
|
||||
case 'channel_buffering':
|
||||
|
|
@ -116,7 +122,7 @@ const getEventColor = (eventType) => {
|
|||
|
||||
const getSystemEvents = (eventsLimit, offset) => {
|
||||
return API.getSystemEvents(eventsLimit, offset);
|
||||
}
|
||||
};
|
||||
|
||||
const Event = ({ event }) => {
|
||||
const [dateFormatSetting] = useLocalStorage('date-format', 'mdy');
|
||||
|
|
@ -147,16 +153,14 @@ const Event = ({ event }) => {
|
|||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
{event.details &&
|
||||
Object.keys(event.details).length > 0 && (
|
||||
<Text size="xs" c="dimmed">
|
||||
{Object.entries(event.details)
|
||||
.filter(([key]) =>
|
||||
!['stream_url', 'new_url'].includes(key))
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join(', ')}
|
||||
</Text>
|
||||
)}
|
||||
{event.details && Object.keys(event.details).length > 0 && (
|
||||
<Text size="xs" c="dimmed">
|
||||
{Object.entries(event.details)
|
||||
.filter(([key]) => !['stream_url', 'new_url'].includes(key))
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join(', ')}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Group>
|
||||
<Text size="xs" c="dimmed" style={{ whiteSpace: 'nowrap' }}>
|
||||
|
|
@ -321,9 +325,7 @@ const SystemEvents = () => {
|
|||
No events recorded yet
|
||||
</Text>
|
||||
) : (
|
||||
events.map((event) => (
|
||||
<Event key={event.id} event={event} />
|
||||
))
|
||||
events.map((event) => <Event key={event.id} event={event} />)
|
||||
)}
|
||||
</Stack>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -403,4 +403,6 @@ export const SUBSCRIPTION_EVENTS = {
|
|||
login_failed: 'Login Failed',
|
||||
epg_blocked: 'EPG Blocked',
|
||||
m3u_blocked: 'M3U Blocked',
|
||||
vod_start: 'VOD Started',
|
||||
vod_stop: 'VOD Stopped',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -99,10 +99,11 @@ const Connections = ({
|
|||
const StatsPage = () => {
|
||||
const channelStats = useChannelsStore((s) => s.stats);
|
||||
const setChannelStats = useChannelsStore((s) => s.setChannelStats);
|
||||
const vodConnections = useChannelsStore((s) => s.activeVodConnections);
|
||||
const setVodStats = useChannelsStore((s) => s.setVodStats);
|
||||
const streamProfiles = useStreamProfilesStore((s) => s.profiles);
|
||||
|
||||
const [clients, setClients] = useState([]);
|
||||
const [vodConnections, setVodConnections] = useState([]);
|
||||
const [channelHistory, setChannelHistory] = useState({});
|
||||
const [isPollingActive, setIsPollingActive] = useState(false);
|
||||
const [currentPrograms, setCurrentPrograms] = useState({});
|
||||
|
|
@ -197,7 +198,7 @@ const StatsPage = () => {
|
|||
try {
|
||||
const response = await getVODStats();
|
||||
if (response) {
|
||||
setVodConnections(response.vod_connections || []);
|
||||
setVodStats(response);
|
||||
} else {
|
||||
console.log('VOD API response was empty or null');
|
||||
}
|
||||
|
|
@ -209,7 +210,7 @@ const StatsPage = () => {
|
|||
body: error.body,
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
}, [setVodStats]);
|
||||
|
||||
// Set up polling for stats when on stats page
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ const useChannelsStore = create((set, get) => ({
|
|||
stats: {},
|
||||
activeChannels: {},
|
||||
activeClients: {},
|
||||
activeVodConnections: [],
|
||||
recordings: [],
|
||||
recurringRules: [],
|
||||
isLoading: false,
|
||||
|
|
@ -502,6 +503,10 @@ const useChannelsStore = create((set, get) => ({
|
|||
});
|
||||
},
|
||||
|
||||
setVodStats: (stats) => {
|
||||
set({ activeVodConnections: stats.vod_connections || [] });
|
||||
},
|
||||
|
||||
fetchRecordings: async () => {
|
||||
try {
|
||||
set({ recordings: await api.getRecordings() });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue