From 4019594d57211fb2f4e5f86cfbaa88527f37063b Mon Sep 17 00:00:00 2001 From: mohsentaba Date: Sun, 14 Jun 2026 15:59:52 +0330 Subject: [PATCH] online class enhancements add hook to does not loose the last recording while class is ending participants logic restored syncing lessons of course logic added --- apps/course/services/web_egress.py | 64 ++++++++++++++++++++++++++ apps/course/views/admin.py | 2 + apps/course/views/course.py | 74 ++++++++++++++++++++++++++++++ apps/course/views/live_session.py | 38 ++++++++++++++- apps/course/views/webhook.py | 3 +- 5 files changed, 179 insertions(+), 2 deletions(-) diff --git a/apps/course/services/web_egress.py b/apps/course/services/web_egress.py index e2a60e0..b1af20a 100644 --- a/apps/course/services/web_egress.py +++ b/apps/course/services/web_egress.py @@ -6,6 +6,7 @@ from typing import Any, Dict, Optional import requests from django.conf import settings from django.core.exceptions import ImproperlyConfigured +from django.utils import timezone logger = logging.getLogger(__name__) @@ -126,3 +127,66 @@ class WebEgressClient: return response.json() except ValueError: return None + + +def is_web_egress_recording_status(status_value: str) -> bool: + return str(status_value or "").strip().lower() in { + "starting", + "recording", + "stopping", + } + + +def stop_session_web_egress_if_active(session) -> bool: + """ + Best-effort stop for an active WebEgress recording. + + Returns True when a stop request was sent, otherwise False. + This helper never raises and is safe to call from close-session flows. + """ + if not getattr(settings, "ONLINE_CLASS_WEB_EGRESS_ENABLED", False): + return False + + if not session or not is_web_egress_recording_status( + getattr(session, "web_egress_status", "") + ): + return False + + try: + client = WebEgressClient() + client.stop_recording( + { + "session_id": session.id, + "room_id": session.room_id, + "egress_id": session.web_egress_id, + } + ) + except (ImproperlyConfigured, WebEgressError) as exc: + logger.warning( + "[WebEgress] Failed to stop active recording during session close - session_id=%s room_id=%s error=%s", + session.id, + session.room_id, + str(exc), + ) + return False + + session.web_egress_status = "stopping" + if not session.web_egress_stopped_at: + session.web_egress_stopped_at = timezone.now() + session.save( + update_fields=[ + "web_egress_status", + "web_egress_stopped_at", + "updated_at", + ] + ) + else: + session.save(update_fields=["web_egress_status", "updated_at"]) + + logger.info( + "[WebEgress] Stop requested during session close - session_id=%s room_id=%s egress_id=%s", + session.id, + session.room_id, + session.web_egress_id, + ) + return True diff --git a/apps/course/views/admin.py b/apps/course/views/admin.py index 5e3ca52..d0f2af3 100644 --- a/apps/course/views/admin.py +++ b/apps/course/views/admin.py @@ -11,6 +11,7 @@ from rest_framework.exceptions import PermissionDenied from utils.pagination import StandardResultsSetPagination from apps.account.permissions import IsPanelUser, IsSuperAdminOrReadOnlyForProfessor from apps.course.services.plugnmeet import PlugNMeetClient, PlugNMeetError +from apps.course.services.web_egress import stop_session_web_egress_if_active from apps.course.models import ( Course, CourseCategory, @@ -465,6 +466,7 @@ class AdminLiveSessionViewSet(viewsets.ModelViewSet): if self._is_room_still_active(session.room_id): continue + stop_session_web_egress_if_active(session) now = timezone.now() session.ended_at = now session.save(update_fields=['ended_at', 'updated_at']) diff --git a/apps/course/views/course.py b/apps/course/views/course.py index d2ee7a9..8dafbb1 100644 --- a/apps/course/views/course.py +++ b/apps/course/views/course.py @@ -41,6 +41,7 @@ from apps.course.models import ( from apps.course.models.course import extract_text_from_json from apps.course.doc import * from apps.course.services.plugnmeet import PlugNMeetClient, PlugNMeetError +from apps.course.services.web_egress import stop_session_web_egress_if_active from apps.account.serializers import UserProfileSerializer from utils.exceptions import AppAPIException from utils.redis import OnlineClassTokenManager @@ -909,9 +910,81 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): if not access_token: return None + self._register_fail_safe_live_session_user( + course=course, + room_id=room_id, + user=user, + is_admin=is_admin, + ) + frontend_base = get_online_class_frontend_base() return f"{frontend_base}/?access_token={access_token}" + @staticmethod + def _register_fail_safe_live_session_user( + course: Course, + room_id: str, + user, + is_admin: bool, + ) -> None: + try: + session = ( + CourseLiveSession.objects.filter( + course=course, + room_id=room_id, + ended_at__isnull=True, + ) + .order_by('-started_at', '-id') + .first() + ) + if not session: + logger.warning( + "[Online Validate] Fail-safe registration skipped - no active session found for course=%s room_id=%s user_id=%s", + course.slug, + room_id, + user.id, + ) + return + + role = 'moderator' if is_admin else 'participant' + LiveSessionUser.objects.update_or_create( + session=session, + user=user, + defaults={ + 'role': role, + 'is_online': True, + 'exited_at': None, + 'entered_at': timezone.now(), + }, + ) + + if is_admin: + session.last_moderator_left_at = None + session.auto_close_after_moderator_exit_at = None + session.save( + update_fields=[ + 'last_moderator_left_at', + 'auto_close_after_moderator_exit_at', + 'updated_at', + ] + ) + + logger.info( + "[Online Validate] Fail-safe registered user entry - session_id=%s room_id=%s user_id=%s role=%s", + session.id, + room_id, + user.id, + role, + ) + except Exception as exc: + logger.error( + "[Online Validate] Failed to register fail-safe user entry - course=%s room_id=%s user_id=%s error=%s", + course.slug, + room_id, + user.id, + str(exc), + ) + def _generate_entry_token(self, course: Course, user) -> str: manager = OnlineClassTokenManager() user_token, _ = Token.objects.get_or_create(user=user) @@ -1131,6 +1204,7 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): Close a live session and all related user entries. Sets ended_at for session and exited_at/is_online for users. """ + stop_session_web_egress_if_active(session) now = timezone.now() session.ended_at = now diff --git a/apps/course/views/live_session.py b/apps/course/views/live_session.py index fbc9d7d..3691f8d 100644 --- a/apps/course/views/live_session.py +++ b/apps/course/views/live_session.py @@ -15,9 +15,10 @@ from drf_yasg import openapi import time import jwt from apps.course.models import Course, CourseLiveSession, Participant, LiveSessionRecording, LiveSessionUser +from apps.course.models.lesson import Lesson, CourseLesson from apps.course.serializers import LiveSessionRoomCreateSerializer, LiveSessionTokenSerializer, LiveSessionRecordedFileSerializer, LiveSessionRecordingSerializer from apps.course.services.plugnmeet import PlugNMeetClient, PlugNMeetError -from apps.course.services.web_egress import WebEgressClient, WebEgressError +from apps.course.services.web_egress import WebEgressClient, WebEgressError, stop_session_web_egress_if_active from utils.exceptions import AppAPIException from django.conf import settings logger = logging.getLogger(__name__) @@ -138,6 +139,38 @@ def build_web_egress_title(session: CourseLiveSession) -> str: return session.recording_title or f"{session.subject} - Recording" +def sync_session_recording_titles(session: CourseLiveSession) -> None: + base_title = session.recording_title or session.subject + recordings = list( + LiveSessionRecording.objects.filter(session=session).order_by( + "created_at", + "id", + ) + ) + total = len(recordings) + if not total: + return + + for index, recording in enumerate(recordings, start=1): + title = base_title if total == 1 else f"{base_title} part {index}" + + if recording.title != title: + recording.title = title + recording.save(update_fields=["title", "updated_at"]) + + if not recording.file: + continue + + lesson = Lesson.objects.filter(content_file=recording.file.name).first() + if lesson and lesson.title != title: + lesson.title = title + lesson.save(update_fields=["title", "updated_at"]) + CourseLesson.objects.filter(lesson=lesson).update( + title=title, + updated_at=timezone.now(), + ) + + class CourseLiveSessionRoomCreateAPIView(GenericAPIView): permission_classes = [IsAuthenticated] authentication_classes = [TokenAuthentication] @@ -647,6 +680,7 @@ class CourseLiveSessionTokenAPIView(GenericAPIView): else: logger.warning(f"[Room Verify] ✗ Room is NOT active - room_id={session.room_id} session_id={session.id} msg={response_msg}") # Auto-close the session since room is not active + stop_session_web_egress_if_active(session) now = timezone.now() session.ended_at = now session.save(update_fields=['ended_at', 'updated_at']) @@ -670,6 +704,7 @@ class CourseLiveSessionTokenAPIView(GenericAPIView): # If room not found, close the session if 'not found' in error_msg.lower() or 'does not exist' in error_msg.lower(): + stop_session_web_egress_if_active(session) now = timezone.now() session.ended_at = now session.save(update_fields=['ended_at', 'updated_at']) @@ -1272,6 +1307,7 @@ class CourseLiveSessionSetRecordingTitleAPIView(GenericAPIView): session.recording_title = title.strip() session.save(update_fields=['recording_title', 'updated_at']) + sync_session_recording_titles(session) logger.info(f"[LiveSession Title] Set recording title for room_id={room_id} to '{title}'") return Response({'success': True, 'message': 'Recording title updated successfully.'}, status=status.HTTP_200_OK) diff --git a/apps/course/views/webhook.py b/apps/course/views/webhook.py index 90e2b01..3cce51f 100644 --- a/apps/course/views/webhook.py +++ b/apps/course/views/webhook.py @@ -28,6 +28,7 @@ from drf_yasg import openapi from apps.course.models import CourseLiveSession, LiveSessionUser, Course, Participant, LiveSessionRecording from apps.account.models import User from apps.course.services.plugnmeet import PlugNMeetClient, PlugNMeetError +from apps.course.services.web_egress import stop_session_web_egress_if_active from utils.exceptions import AppAPIException logger = logging.getLogger(__name__) @@ -208,13 +209,13 @@ class PlugNMeetWebhookAPIView(APIView): try: session = CourseLiveSession.objects.get(room_id=room_id, ended_at__isnull=True) + stop_session_web_egress_if_active(session) now = timezone.now() session.ended_at = now update_fields = ['ended_at', 'updated_at'] if session.web_egress_status in { CourseLiveSession.WEB_EGRESS_STATUS_STARTING, CourseLiveSession.WEB_EGRESS_STATUS_RECORDING, - CourseLiveSession.WEB_EGRESS_STATUS_STOPPING, }: session.web_egress_status = CourseLiveSession.WEB_EGRESS_STATUS_PROCESSING update_fields.append('web_egress_status')