568 lines
24 KiB
PHP
568 lines
24 KiB
PHP
<?php
|
|
|
|
namespace App\Controllers;
|
|
|
|
use App\Core\Request;
|
|
use App\Core\Response;
|
|
use App\Core\Database;
|
|
use App\Core\Security;
|
|
use App\Services\VideoService;
|
|
use App\Services\AiVideoAnalyzerService;
|
|
use App\Services\CurriculumService;
|
|
|
|
class VideoController
|
|
{
|
|
/**
|
|
* Upload Video directly via API, transcode HLS, and trigger Autonomous Gemini AI Analysis
|
|
* POST /api/teacher/videos/upload-direct
|
|
*/
|
|
public function uploadDirect(Request $request, Response $response): void
|
|
{
|
|
VideoService::ensureSchema();
|
|
CurriculumService::ensureSchema();
|
|
|
|
$courseId = (int)($request->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();
|
|
|
|
$lessonId = (int)$request->getParam('id');
|
|
$lesson = null;
|
|
|
|
if ($lessonId > 0) {
|
|
$lesson = Database::selectOne("SELECT * FROM lessons WHERE id = ? LIMIT 1", [$lessonId]);
|
|
}
|
|
|
|
if (!$lesson) {
|
|
// Fallback to latest available lesson
|
|
$lesson = Database::selectOne("SELECT * FROM lessons ORDER BY id DESC LIMIT 1");
|
|
}
|
|
|
|
if (!$lesson) {
|
|
$response->status(404)->json(['status' => 'error', 'message' => 'لا توجد دروس متاحة حالياً']);
|
|
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'])) {
|
|
AiVideoAnalyzerService::processLessonAutonomously($lessonId);
|
|
$lesson = Database::selectOne("SELECT * FROM lessons WHERE id = ? LIMIT 1", [$lessonId]);
|
|
}
|
|
|
|
// 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)
|
|
];
|
|
}
|
|
|
|
$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']);
|
|
}
|
|
}
|