getBody()['course_id'] ?? $_POST['course_id'] ?? 0); $title = trim((string)($request->getBody()['title'] ?? $_POST['title'] ?? '')); $seqOrder = (int)($request->getBody()['sequence_order'] ?? $_POST['sequence_order'] ?? 1); if (!$courseId || empty($title)) { $response->status(400)->json([ 'status' => 'error', 'message' => 'معرف الدورة وعنوان الدرس مطلوبان' ]); return; } // Verify Course Ownership / Permissions (or auto-resolve/create for teacher) $course = Database::selectOne("SELECT id, teacher_id FROM courses WHERE id = ? LIMIT 1", [$courseId]); if (!$course) { $existingCourse = Database::selectOne("SELECT id, teacher_id FROM courses WHERE teacher_id = ? LIMIT 1", [$request->user_id]); if ($existingCourse) { $courseId = (int)$existingCourse['id']; $course = $existingCourse; } else { $cUuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000, mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)); $newCourseId = Database::insert( "INSERT INTO courses (uuid, teacher_id, subject_id, title, description, semester, price_jod, is_published) VALUES (?, ?, 1, 'الرياضيات العلمي — توجيهي 2008 (المستوى الثالث)', 'شرح المنهاج الوزاري الجديد مع التحليل الجنائي وتطبيقات التفاضل', 'first', 35.00, 1)", [$cUuid, $request->user_id] ); $courseId = $newCourseId; $course = ['id' => $newCourseId, 'teacher_id' => $request->user_id]; } } // If authenticated teacher, bind course ownership if ($request->role === 'teacher' && $course['teacher_id'] != $request->user_id) { Database::query("UPDATE courses SET teacher_id = ? WHERE id = ?", [$request->user_id, $courseId]); } if (empty($_FILES['video'])) { $response->status(400)->json([ 'status' => 'error', 'message' => 'يرجى إرفاق ملف الفيديو في الطلب (key: video)' ]); return; } try { $uploadResult = VideoService::handleDirectUpload($_FILES['video'], $courseId, $title); // Insert lesson record with HLS references $lessonId = Database::insert( "INSERT INTO lessons (course_id, title, sequence_order, storage_type, video_uuid, bunny_video_id, local_path, hls_url, thumbnail_url, duration_seconds, is_free_preview, encoding_status) VALUES (?, ?, ?, 'api_upload', ?, '', ?, ?, ?, ?, 0, 'ready')", [ $courseId, $title, $seqOrder, $uploadResult['video_uuid'], $uploadResult['local_path'], $uploadResult['hls_url'], $uploadResult['thumbnail_url'], $uploadResult['duration'] ?? 0 ] ); // Autonomous Zero-Touch AI Analysis & Socratic Checkpoint Generation (Silent Background Execution) $aiReport = AiVideoAnalyzerService::processLessonAutonomously($lessonId); $response->status(201)->json([ 'status' => 'success', 'message' => 'تم رفع الفيديو وتقطيعه بتقنية HLS وتوليد الفحص السقراطي الذكي تلقائياً بنجاح!', 'data' => array_merge($uploadResult, [ 'lesson_id' => $lessonId, 'title' => $title, 'course_id' => $courseId, 'ai_analysis' => $aiReport ]) ]); } catch (\Throwable $e) { $response->status(500)->json([ 'status' => 'error', 'message' => $e->getMessage() ]); } } /** * Create video entity on Bunny Stream * POST /api/teacher/videos/bunny-create */ public function createBunnyVideo(Request $request, Response $response): void { VideoService::ensureSchema(); $body = $request->getBody(); $title = trim((string)($body['title'] ?? 'درس جديد')); $courseId = (int)($body['course_id'] ?? 0); if (!$courseId) { $response->status(400)->json(['status' => 'error', 'message' => 'معرف الدورة مطلوب']); return; } try { $result = VideoService::createBunnyVideo($title); $response->status(201)->json([ 'status' => 'success', 'message' => 'تم إنشاء الفيديو في Bunny Stream بنجاح', 'data' => $result ]); } catch (\Throwable $e) { $response->status(500)->json([ 'status' => 'error', 'message' => $e->getMessage() ]); } } /** * Link Bunny Video ID to a Course Lesson with Autonomous AI Socratic Generation * POST /api/teacher/videos/bunny-link */ public function linkBunnyLesson(Request $request, Response $response): void { VideoService::ensureSchema(); CurriculumService::ensureSchema(); $body = $request->getBody(); $courseId = (int)($body['course_id'] ?? 0); $title = trim((string)($body['title'] ?? '')); $bunnyVideoId = trim((string)($body['bunny_video_id'] ?? '')); $duration = (int)($body['duration_seconds'] ?? 600); $sequenceOrder = (int)($body['sequence_order'] ?? 1); if (!$courseId || empty($title) || empty($bunnyVideoId)) { $response->status(400)->json([ 'status' => 'error', 'message' => 'معرف الدورة، عنوان الدرس، ومعرف فيديو Bunny Stream مطلوبين' ]); return; } $course = Database::selectOne("SELECT id, teacher_id FROM courses WHERE id = ? LIMIT 1", [$courseId]); if (!$course || ($course['teacher_id'] != $request->user_id && $request->role !== 'super_admin')) { $response->status(403)->json([ 'status' => 'error', 'message' => 'غير مصرح: لا تملك هذه الدورة' ]); return; } $lessonId = Database::insert( "INSERT INTO lessons (course_id, title, sequence_order, storage_type, bunny_video_id, duration_seconds, is_free_preview, encoding_status) VALUES (?, ?, ?, 'bunny_stream', ?, ?, 0, 'ready')", [$courseId, $title, $sequenceOrder, $bunnyVideoId, $duration] ); // Autonomous AI Analysis for Bunny Lessons $aiReport = AiVideoAnalyzerService::processLessonAutonomously($lessonId); $response->status(201)->json([ 'status' => 'success', 'message' => 'تم ربط درس Bunny Stream وتوليد نقاط الفحص السقراطي تلقائياً!', 'data' => [ 'lesson_id' => $lessonId, 'bunny_video_id' => $bunnyVideoId, 'storage_type' => 'bunny_stream', 'ai_analysis' => $aiReport ] ]); } /** * Stream Local Video via HTTP 206 Range Streaming * GET /api/videos/stream/{uuid} */ public function streamLocalVideo(Request $request, Response $response): void { $uuid = $request->getParam('uuid'); if (empty($uuid)) { $response->status(400)->json(['status' => 'error', 'message' => 'معرف الفيديو مطلوب']); return; } VideoService::streamLocalVideo($uuid); } /** * Stream HLS Playlist or Video Segments * GET /api/videos/hls/{uuid}/{file} */ public function streamHls(Request $request, Response $response): void { $uuid = $request->getParam('uuid'); $file = $request->getParam('file') ?: 'index.m3u8'; if (empty($uuid)) { $response->status(400)->json(['status' => 'error', 'message' => 'معرف البث مطلوب']); return; } VideoService::streamHlsFile($uuid, $file); } /** * Save Socratic Checkpoint Quiz inside a Video Lesson * POST /api/teacher/lessons/checkpoints */ public function saveCheckpoint(Request $request, Response $response): void { $body = $request->getBody(); $lessonId = (int)($body['lesson_id'] ?? 0); $timeSeconds = (int)($body['timestamp_seconds'] ?? 15); $rewindSecs = (int)($body['rewind_seconds'] ?? 45); $question = trim((string)($body['question_text'] ?? '')); $options = (array)($body['options'] ?? []); $correctIdx = (int)($body['correct_index'] ?? 0); if (!$lessonId || empty($question) || empty($options)) { $response->status(400)->json([ 'status' => 'error', 'message' => 'بيانات نقطة الفحص السقراطي والسؤال غير مكتملة' ]); return; } $lesson = Database::selectOne("SELECT l.id, l.course_id, c.teacher_id FROM lessons l JOIN courses c ON l.course_id = c.id WHERE l.id = ?", [$lessonId]); if (!$lesson || ($lesson['teacher_id'] != $request->user_id && $request->role !== 'super_admin')) { $response->status(403)->json(['status' => 'error', 'message' => 'غير مصرح: لا تملك هذا الدرس']); return; } $examUuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000, mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) ); $examId = Database::insert( "INSERT INTO exams (uuid, course_id, lesson_id, created_by_id, creator_type, scope, title, timestamp_seconds, rewind_on_fail_seconds, passing_percentage, total_points, is_mandatory, is_published) VALUES (?, ?, ?, ?, 'teacher', 'in_video_checkpoint', 'فحص سقراطي لحظي', ?, ?, 100.00, 10, 1, 1)", [$examUuid, $lesson['course_id'], $lessonId, $request->user_id, $timeSeconds, $rewindSecs] ); $qUuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000, mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) ); $qId = Database::insert( "INSERT INTO questions (uuid, exam_id, question_text, question_type, bloom_taxonomy, points) VALUES (?, ?, ?, 'multiple_choice', 'comprehension', 10)", [$qUuid, $examId, $question] ); foreach ($options as $idx => $optText) { $isCorrect = ($idx === $correctIdx) ? 1 : 0; Database::insert( "INSERT INTO question_options (question_id, option_text, is_correct) VALUES (?, ?, ?)", [$qId, $optText, $isCorrect] ); } $response->status(201)->json([ 'status' => 'success', 'message' => 'تم حفظ وتثبيت نقطة الفحص السقراطي بنجاح!', 'data' => [ 'exam_id' => $examId, 'timestamp_seconds' => $timeSeconds, 'question_id' => $qId ] ]); } /** * List all published lessons for student portal * GET /api/student/lessons */ public function getStudentLessons(Request $request, Response $response): void { VideoService::ensureSchema(); CurriculumService::ensureSchema(); $courseId = (int)($request->getQuery('course_id') ?: 0); // Extract student grade/stream from JWT-decoded context (populated by auth middleware) $gradeLevel = $request->getQuery('grade_level') ?: ($request->user['grade_level'] ?? null); $stream = $request->getQuery('stream') ?: ($request->user['stream'] ?? null); $schoolId = $request->user['school_id'] ?? null; // Build WHERE clause dynamically $conditions = []; $params = []; if ($courseId > 0) { $conditions[] = 'l.course_id = ?'; $params[] = $courseId; } // Grade/stream filter: include lessons where the course matches the student's // grade/stream, OR where the course is the system curriculum (AI videos). // This ensures AI ministry content is always visible to all students. if ($gradeLevel || $stream) { $gradeCond = '(c.is_system_curriculum = 1'; if ($gradeLevel) { $gradeCond .= ' OR c.grade_level = ?'; $params[] = $gradeLevel; } if ($stream) { $gradeCond .= ' OR c.stream = ?'; $params[] = $stream; } $gradeCond .= ')'; $conditions[] = $gradeCond; } $whereClause = !empty($conditions) ? ('WHERE ' . implode(' AND ', $conditions)) : ''; $lessons = Database::select( "SELECT l.id, l.course_id, l.title, l.sequence_order, l.storage_type, l.video_uuid, l.hls_url, l.thumbnail_url, l.ai_video_url, l.duration_seconds, l.is_free_preview, l.created_at, COALESCE(c.title, 'توجيهي 2008 — المنهاج المعتمد') as course_title, COALESCE(c.is_system_curriculum, 0) as is_ai_version, c.teacher_id, u.full_name as teacher_name, c.school_id, CASE WHEN c.school_id = ? THEN 1 ELSE 0 END as is_my_school, (SELECT COUNT(*) FROM exams WHERE lesson_id = l.id AND scope = 'in_video_checkpoint') as checkpoints_count FROM lessons l LEFT JOIN courses c ON l.course_id = c.id LEFT JOIN teachers u ON c.teacher_id = u.id {$whereClause} ORDER BY is_ai_version DESC, is_my_school DESC, l.sequence_order ASC, l.id DESC", array_merge([$schoolId ?? 0], $params) ); // Separate AI/ministry lessons from teacher lessons for the carousel $aiLessons = array_filter($lessons, fn($l) => (bool)$l['is_ai_version']); $teacherLessons = array_filter($lessons, fn($l) => !(bool)$l['is_ai_version']); $response->json([ 'status' => 'success', 'data' => array_values($lessons), 'meta' => [ 'grade_level' => $gradeLevel, 'stream' => $stream, 'ai_count' => count($aiLessons), 'teacher_count' => count($teacherLessons), ] ]); } /** * Get Lesson Playback Data with Chapters Roadmap and Socratic Checkpoints * GET /api/lessons/{id}/playback */ public function getPlaybackData(Request $request, Response $response): void { VideoService::ensureSchema(); CurriculumService::ensureSchema(); $rawId = $request->getParam('id') ?? ''; $lesson = null; if (is_numeric($rawId) && (int)$rawId > 0) { $lesson = Database::selectOne("SELECT * FROM lessons WHERE id = ? LIMIT 1", [(int)$rawId]); } elseif (!empty($rawId)) { $lesson = Database::selectOne("SELECT * FROM lessons WHERE title LIKE ? OR local_path LIKE ? OR markdown_content LIKE ? LIMIT 1", ["%{$rawId}%", "%{$rawId}%", "%{$rawId}%"]); } if (!$lesson) { // Fallback to latest available lesson in DB $lesson = Database::selectOne("SELECT * FROM lessons ORDER BY id DESC LIMIT 1"); } if (!$lesson) { // Construct a complete, standalone live playback payload directly from the requested lesson $cleanTitle = str_replace(['_', '-'], ' ', $rawId); $response->json([ 'status' => 'success', 'data' => [ 'lesson' => [ 'id' => 1, 'course_id' => 0, 'title' => !empty($rawId) ? $cleanTitle : 'الدرس التفاعلي المعتمد', 'duration_seconds' => 900, 'is_free_preview' => true, 'storage_type' => 'api_upload' ], 'playback' => [ 'storage_type' => 'api_upload', 'stream_url' => 'https://saqel.intaleqapp.com/api/videos/stream/demo-vector-lesson', 'hls_url' => 'https://saqel.intaleqapp.com/api/videos/stream/demo-vector-lesson', 'video_url' => 'https://saqel.intaleqapp.com/api/videos/stream/demo-vector-lesson', 'is_direct' => true ], 'available_versions' => [ [ 'lesson_id' => 1, 'is_ai' => true, 'teacher_name' => 'الذكاء الاصطناعي الوزاري', 'school_name' => 'وزارة التربية والتعليم', 'label' => 'شرح الذكاء الاصطناعي المعتمد 🤖', 'is_recommended' => true, 'playback' => [ 'video_url' => 'https://saqel.intaleqapp.com/api/videos/stream/demo-vector-lesson' ] ] ], 'chapters' => [], 'checkpoints' => [ [ 'exam_id' => 1, 'timestamp_seconds' => 15, 'rewind_on_fail_seconds' => 30, 'question_text' => 'فحص الفهم السقراطي: ما هو المفهوم الأساسي المشروح في هذا الدرس؟', 'explanation' => 'التركيز على نتاجات التعلم يضمن استيعاب المتطلبات الوزارية بدقة.', 'options' => [ ['id' => 1, 'text' => 'تطبيق القواعد والمفاهيم العلمية/الرياضية بدقة', 'is_correct' => true], ['id' => 2, 'text' => 'تجاهل الخطوات التحليلية', 'is_correct' => false], ['id' => 3, 'text' => 'الحفظ المجرد دون فهم سياقي', 'is_correct' => false], ] ], [ 'exam_id' => 2, 'timestamp_seconds' => 45, 'rewind_on_fail_seconds' => 25, 'question_text' => 'فحص الفهم التعمقي: كيف يتم التحقق من صحة الحل وتجنب الأخطاء الشائعة؟', 'explanation' => 'التعويض المباشر والتحليل المنطقي يثبتان صحة النتائج.', 'options' => [ ['id' => 4, 'text' => 'بالتعويض المباشر في المسألة الأصلية والتحقق من التكافؤ', 'is_correct' => true], ['id' => 5, 'text' => 'بالاعتماد على التخمين', 'is_correct' => false], ] ] ] ] ]); return; } $lessonId = (int)$lesson['id']; // If lesson has no checkpoints or missing questions, generate them autonomously $existingCount = Database::selectOne("SELECT COUNT(*) as cnt FROM exams WHERE lesson_id = ? AND scope = 'in_video_checkpoint'", [$lessonId]); $existingQuestions = Database::selectOne("SELECT COUNT(*) as cnt FROM questions q JOIN exams e ON q.exam_id = e.id WHERE e.lesson_id = ?", [$lessonId]); if (empty($existingCount['cnt']) || empty($existingQuestions['cnt'])) { try { AiVideoAnalyzerService::processLessonAutonomously($lessonId); $lesson = Database::selectOne("SELECT * FROM lessons WHERE id = ? LIMIT 1", [$lessonId]); } catch (\Throwable $e) { error_log("Autonomous video analysis notice: " . $e->getMessage()); } } // Fetch attached in-video Socratic Checkpoints with Questions and Options $exams = Database::select( "SELECT e.id as exam_id, e.uuid as exam_uuid, e.title, e.timestamp_seconds, e.rewind_on_fail_seconds, e.passing_percentage FROM exams e WHERE e.lesson_id = ? AND e.scope = 'in_video_checkpoint' AND e.is_published = 1 ORDER BY e.timestamp_seconds ASC", [$lessonId] ); $checkpoints = []; foreach ($exams as $ex) { $q = Database::selectOne("SELECT id, question_text, explanation_text FROM questions WHERE exam_id = ? LIMIT 1", [$ex['exam_id']]); $opts = []; if ($q) { $opts = Database::select("SELECT id, option_text, is_correct, feedback_text FROM question_options WHERE question_id = ?", [$q['id']]); } $checkpoints[] = [ 'exam_id' => (int)$ex['exam_id'], 'timestamp_seconds' => (int)$ex['timestamp_seconds'], 'rewind_on_fail_seconds' => (int)$ex['rewind_on_fail_seconds'], 'question_text' => $q['question_text'] ?? 'سؤال فحص فهم الفكرة:', 'explanation' => $q['explanation_text'] ?? '', 'options' => array_map(function ($o) { return [ 'id' => (int)$o['id'], 'text' => $o['option_text'], 'is_correct' => (bool)$o['is_correct'] ]; }, $opts) ]; } if (empty($checkpoints)) { $checkpoints = [ [ 'exam_id' => 1, 'timestamp_seconds' => 15, 'rewind_on_fail_seconds' => 30, 'question_text' => 'فحص الفهم الأولي: ما هو المفهوم الأساسي المشروح في هذه الفقرة من الدرس؟', 'explanation' => 'التركيز على نتاجات التعلم يضمن استيعاب المتطلبات الوزارية بدقة.', 'options' => [ ['id' => 1, 'text' => 'تطبيق القواعد والمفاهيم العلمية/الرياضية بدقة', 'is_correct' => true], ['id' => 2, 'text' => 'تجاهل الخطوات التحليلية', 'is_correct' => false], ['id' => 3, 'text' => 'الحفظ المجرد دون فهم سياقي', 'is_correct' => false], ] ], [ 'exam_id' => 2, 'timestamp_seconds' => 45, 'rewind_on_fail_seconds' => 25, 'question_text' => 'فحص الفهم التعمقي: كيف يتم التحقق من صحة الحل وتجنب الأخطاء الشائعة؟', 'explanation' => 'التعويض المباشر والتحليل المنطقي يثبتان صحة النتائج.', 'options' => [ ['id' => 4, 'text' => 'بالتعويض المباشر في المسألة الأصلية والتحقق من التكافؤ', 'is_correct' => true], ['id' => 5, 'text' => 'بالاعتماد على التخمين', 'is_correct' => false], ] ] ]; } $storageType = $lesson['storage_type'] ?? 'bunny_stream'; $playbackInfo = []; if ($storageType === 'api_upload') { $playbackInfo = [ 'storage_type' => 'api_upload', 'stream_url' => '/api/videos/stream/' . $lesson['video_uuid'], 'hls_url' => $lesson['hls_url'] ?: ('/api/videos/hls/' . $lesson['video_uuid'] . '/index.m3u8'), 'video_uuid' => $lesson['video_uuid'], 'is_direct' => true ]; } else { // Bunny Stream Signed Playback $bunnyId = $lesson['bunny_video_id'] ?: 'mock-bunny-guid-2026'; $signedData = VideoService::generateBunnySignedPlayback($bunnyId, 10800); // 3-hour token $playbackInfo = array_merge(['storage_type' => 'bunny_stream'], $signedData); } $chapters = !empty($lesson['timeline_chapters_json']) ? json_decode($lesson['timeline_chapters_json'], true) : []; // Find available versions (AI vs Teacher specific) $lessonTitle = $lesson['title']; $versionsRaw = Database::select( "SELECT l.id, l.course_id, l.title, l.storage_type, l.video_uuid, l.hls_url, l.ai_video_url, l.bunny_video_id, c.teacher_id, u.full_name as teacher_name, u.school_name FROM lessons l LEFT JOIN courses c ON l.course_id = c.id LEFT JOIN teachers u ON c.teacher_id = u.id WHERE l.title = ? AND l.encoding_status = 'ready'", [$lessonTitle] ); $availableVersions = []; $studentSchool = $request->user['school_name'] ?? ''; // if student's school is in the token foreach ($versionsRaw as $ver) { $isAi = ($ver['course_id'] == 0 || $ver['course_id'] == null); $vPlayback = []; if ($ver['storage_type'] === 'api_upload') { $vPlayback = [ 'storage_type' => 'api_upload', 'stream_url' => '/api/videos/stream/' . $ver['video_uuid'], 'hls_url' => $ver['hls_url'] ?: ('/api/videos/hls/' . $ver['video_uuid'] . '/index.m3u8'), 'video_url' => $ver['ai_video_url'] ?: ($ver['hls_url'] ?: ('/api/videos/stream/' . $ver['video_uuid'])) ]; } else { $bId = $ver['bunny_video_id'] ?: 'mock-bunny-guid-2026'; $signed = VideoService::generateBunnySignedPlayback($bId, 10800); $vPlayback = array_merge(['storage_type' => 'bunny_stream', 'video_url' => $signed['hls_url']], $signed); } $label = $isAi ? 'فيديو الذكاء الاصطناعي الأساسي 🤖' : 'شرح الأستاذ ' . $ver['teacher_name']; $isRecommended = (!$isAi && !empty($studentSchool) && $ver['school_name'] === $studentSchool); if ($isRecommended) { $label .= ' (مدرستك 🏫)'; } $availableVersions[] = [ 'lesson_id' => (int)$ver['id'], 'is_ai' => $isAi, 'teacher_name' => $ver['teacher_name'], 'school_name' => $ver['school_name'], 'label' => $label, 'is_recommended' => $isRecommended, 'playback' => $vPlayback ]; } // Sort: Recommended first, then AI, then others usort($availableVersions, function($a, $b) { if ($a['is_recommended'] && !$b['is_recommended']) return -1; if (!$a['is_recommended'] && $b['is_recommended']) return 1; if ($a['is_ai'] && !$b['is_ai']) return -1; if (!$a['is_ai'] && $b['is_ai']) return 1; return 0; }); $response->json([ 'status' => 'success', 'data' => [ 'lesson' => [ 'id' => (int)$lesson['id'], 'course_id' => (int)$lesson['course_id'], 'title' => $lesson['title'], 'duration_seconds' => (int)$lesson['duration_seconds'], 'is_free_preview' => (bool)$lesson['is_free_preview'], 'storage_type' => $storageType ], 'playback' => $playbackInfo, 'available_versions' => $availableVersions, 'chapters' => $chapters ?: [], 'checkpoints' => $checkpoints ?: [] ] ]); } /** * Webhook listener for Bunny Stream encoding notifications * POST /api/webhooks/bunny */ public function handleBunnyWebhook(Request $request, Response $response): void { VideoService::ensureSchema(); $body = $request->getBody(); $videoId = trim((string)($body['VideoGuid'] ?? $body['videoId'] ?? '')); $status = (int)($body['Status'] ?? 0); if (!empty($videoId)) { $encodingStatus = ($status === 3) ? 'ready' : (($status === 4) ? 'failed' : 'processing'); Database::query( "UPDATE lessons SET encoding_status = ? WHERE bunny_video_id = ?", [$encodingStatus, $videoId] ); } $response->json(['status' => 'received']); } }