Enhancement: Add VOD client stop functionality to Stats page

This commit is contained in:
SergeantPanda 2025-12-17 16:54:10 -06:00
parent 865ba432d3
commit 2558ea0b0b
6 changed files with 121 additions and 5 deletions

View file

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- VOD client stop button in Stats page: Users can now disconnect individual VOD clients from the Stats view, similar to the existing channel client disconnect functionality.
- Automated configuration backup/restore system with scheduled backups, retention policies, and async task processing - Thanks [@stlalpha](https://github.com/stlalpha) (Closes #153)
### Changed

View file

@ -24,6 +24,11 @@ from apps.m3u.models import M3UAccountProfile
logger = logging.getLogger("vod_proxy")
def get_vod_client_stop_key(client_id):
"""Get the Redis key for signaling a VOD client to stop"""
return f"vod_proxy:client:{client_id}:stop"
def infer_content_type_from_url(url: str) -> Optional[str]:
"""
Infer MIME type from file extension in URL
@ -832,6 +837,7 @@ class MultiWorkerVODConnectionManager:
# Create streaming generator
def stream_generator():
decremented = False
stop_signal_detected = False
try:
logger.info(f"[{client_id}] Worker {self.worker_id} - Starting Redis-backed stream")
@ -846,14 +852,25 @@ class MultiWorkerVODConnectionManager:
bytes_sent = 0
chunk_count = 0
# Get the stop signal key for this client
stop_key = get_vod_client_stop_key(client_id)
for chunk in upstream_response.iter_content(chunk_size=8192):
if chunk:
yield chunk
bytes_sent += len(chunk)
chunk_count += 1
# Update activity every 100 chunks in consolidated connection state
# Check for stop signal every 100 chunks
if chunk_count % 100 == 0:
# Check if stop signal has been set
if self.redis_client and self.redis_client.exists(stop_key):
logger.info(f"[{client_id}] Worker {self.worker_id} - Stop signal detected, terminating stream")
# Delete the stop key
self.redis_client.delete(stop_key)
stop_signal_detected = True
break
# Update the connection state
logger.debug(f"Client: [{client_id}] Worker: {self.worker_id} sent {chunk_count} chunks for VOD: {content_name}")
if redis_connection._acquire_lock():
@ -867,7 +884,10 @@ class MultiWorkerVODConnectionManager:
finally:
redis_connection._release_lock()
logger.info(f"[{client_id}] Worker {self.worker_id} - Redis-backed stream completed: {bytes_sent} bytes sent")
if stop_signal_detected:
logger.info(f"[{client_id}] Worker {self.worker_id} - Stream stopped by signal: {bytes_sent} bytes sent")
else:
logger.info(f"[{client_id}] Worker {self.worker_id} - Redis-backed stream completed: {bytes_sent} bytes sent")
redis_connection.decrement_active_streams()
decremented = True

View file

@ -21,4 +21,7 @@ urlpatterns = [
# VOD Stats
path('stats/', views.VODStatsView.as_view(), name='vod_stats'),
# Stop VOD client connection
path('stop_client/', views.stop_vod_client, name='stop_vod_client'),
]

View file

@ -15,7 +15,7 @@ from django.views import View
from apps.vod.models import Movie, Series, Episode
from apps.m3u.models import M3UAccount, M3UAccountProfile
from apps.proxy.vod_proxy.connection_manager import VODConnectionManager
from apps.proxy.vod_proxy.multi_worker_connection_manager import MultiWorkerVODConnectionManager, infer_content_type_from_url
from apps.proxy.vod_proxy.multi_worker_connection_manager import MultiWorkerVODConnectionManager, infer_content_type_from_url, get_vod_client_stop_key
from .utils import get_client_info, create_vod_response
logger = logging.getLogger(__name__)
@ -1011,3 +1011,59 @@ class VODStatsView(View):
except Exception as e:
logger.error(f"Error getting VOD stats: {e}")
return JsonResponse({'error': str(e)}, status=500)
from rest_framework.decorators import api_view, permission_classes
from apps.accounts.permissions import IsAdmin
@csrf_exempt
@api_view(["POST"])
@permission_classes([IsAdmin])
def stop_vod_client(request):
"""Stop a specific VOD client connection using stop signal mechanism"""
try:
# Parse request body
import json
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({'error': 'Invalid JSON'}, status=400)
client_id = data.get('client_id')
if not client_id:
return JsonResponse({'error': 'No client_id provided'}, status=400)
logger.info(f"Request to stop VOD client: {client_id}")
# Get Redis client
connection_manager = MultiWorkerVODConnectionManager.get_instance()
redis_client = connection_manager.redis_client
if not redis_client:
return JsonResponse({'error': 'Redis not available'}, status=500)
# Check if connection exists
connection_key = f"vod_persistent_connection:{client_id}"
connection_data = redis_client.hgetall(connection_key)
if not connection_data:
logger.warning(f"VOD connection not found: {client_id}")
return JsonResponse({'error': 'Connection not found'}, status=404)
# Set a stop signal key that the worker will check
stop_key = get_vod_client_stop_key(client_id)
redis_client.setex(stop_key, 60, "true") # 60 second TTL
logger.info(f"Set stop signal for VOD client: {client_id}")
return JsonResponse({
'message': 'VOD client stop signal sent',
'client_id': client_id,
'stop_key': stop_key
})
except Exception as e:
logger.error(f"Error stopping VOD client: {e}", exc_info=True)
return JsonResponse({'error': str(e)}, status=500)

View file

@ -1691,6 +1691,19 @@ export default class API {
}
}
static async stopVODClient(clientId) {
try {
const response = await request(`${host}/proxy/vod/stop_client/`, {
method: 'POST',
body: { client_id: clientId },
});
return response;
} catch (e) {
errorNotification('Failed to stop VOD client', e);
}
}
static async stopChannel(id) {
try {
const response = await request(`${host}/proxy/ts/stop/${id}`, {

View file

@ -89,7 +89,7 @@ const getStartDate = (uptime) => {
};
// Create a VOD Card component similar to ChannelCard
const VODCard = ({ vodContent }) => {
const VODCard = ({ vodContent, stopVODClient }) => {
const [dateFormatSetting] = useLocalStorage('date-format', 'mdy');
const dateFormat = dateFormatSetting === 'mdy' ? 'MM/DD' : 'DD/MM';
const [isClientExpanded, setIsClientExpanded] = useState(false);
@ -329,6 +329,19 @@ const VODCard = ({ vodContent }) => {
</Center>
</Tooltip>
)}
{connection && stopVODClient && (
<Center>
<Tooltip label="Stop VOD Connection">
<ActionIcon
variant="transparent"
color="red.9"
onClick={() => stopVODClient(connection.client_id)}
>
<SquareX size="24" />
</ActionIcon>
</Tooltip>
</Center>
)}
</Group>
</Group>
@ -1297,6 +1310,12 @@ const ChannelsPage = () => {
await API.stopClient(channelId, clientId);
};
const stopVODClient = async (clientId) => {
await API.stopVODClient(clientId);
// Refresh VOD stats after stopping to update the UI
fetchVODStats();
};
// Function to fetch channel stats from API
const fetchChannelStats = useCallback(async () => {
try {
@ -1585,7 +1604,11 @@ const ChannelsPage = () => {
);
} else if (connection.type === 'vod') {
return (
<VODCard key={connection.id} vodContent={connection.data} />
<VODCard
key={connection.id}
vodContent={connection.data}
stopVODClient={stopVODClient}
/>
);
}
return null;