From 22568fe220ceae289075efd9f195372a7d00dbb1 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Sat, 29 Aug 2026 05:40:45 +0300 Subject: [PATCH] feat: Implement semantic video naming, MySQL database curriculum synchronization, and live two-way binding between filesystem and DB --- .../app/Controllers/CurriculumController.php | 47 +++++- .../scripts/sync_curriculum_to_database.php | 157 ++++++++++++++++++ 2 files changed, 196 insertions(+), 8 deletions(-) create mode 100644 backend/scripts/sync_curriculum_to_database.php diff --git a/backend/app/Controllers/CurriculumController.php b/backend/app/Controllers/CurriculumController.php index 479e81d..c05b52b 100644 --- a/backend/app/Controllers/CurriculumController.php +++ b/backend/app/Controllers/CurriculumController.php @@ -190,11 +190,28 @@ class CurriculumController CurriculumService::saveLessonAiAssets($file, $aiAssets); } + // Sync to MySQL Database Table `lessons` if connected + try { + $aiVideoUrl = $aiAssets['ai_video_url'] ?? null; + $cheatSheet = $aiAssets['cheat_sheet'] ?? null; + $socraticJson = isset($aiAssets['socratic_quiz']) ? json_encode($aiAssets['socratic_quiz'], JSON_UNESCAPED_UNICODE) : null; + + // Search lesson by matching filename or title + $filename = basename($file, '.md'); + \App\Core\Database::query( + "UPDATE lessons SET markdown_content = ?, ai_video_url = COALESCE(?, ai_video_url), cheat_sheet_markdown = COALESCE(?, cheat_sheet_markdown), socratic_quiz_json = COALESCE(?, socratic_quiz_json) + WHERE title LIKE ? OR markdown_content LIKE ?", + [$content, $aiVideoUrl, $cheatSheet, $socraticJson, "%{$filename}%", "%{$file}%"] + ); + } catch (\Throwable $dbEx) { + error_log("Curriculum DB save sync note: " . $dbEx->getMessage()); + } + $serverFullPath = realpath(__DIR__ . '/../../storage/curriculum') . '/' . ltrim($file, '/'); $response->json([ 'status' => 'success', - 'message' => 'تم حفظ واعتماد محتوى الدرس في المنهاج بنجاح!', + 'message' => 'تم حفظ واعتماد محتوى الدرس في المنهاج وقاعدة البيانات بنجاح!', 'server_full_path' => $serverFullPath ]); } @@ -217,8 +234,6 @@ class CurriculumController // Generate Assets require_once __DIR__ . '/../Services/AiLessonEnhancerService.php'; - // We will adapt the service call here since AiLessonEnhancerService expects lessonId. - // Actually, let's create a custom static method in AiLessonEnhancerService to accept raw text. $assets = \App\Services\AiLessonEnhancerService::generateFromText('الدرس', $content); if ($assets) { @@ -230,7 +245,7 @@ class CurriculumController } /** - * Direct Video Upload to Platform Storage + * Direct Video Upload to Platform Storage with Semantic Lesson Binding */ public function uploadLessonVideo(Request $request, Response $response): void { @@ -252,7 +267,11 @@ class CurriculumController $videoTmp = $_FILES['video_file']['tmp_name']; $ext = strtolower(pathinfo($_FILES['video_file']['name'], PATHINFO_EXTENSION)) ?: 'mp4'; - $safeName = 'video_' . md5($file) . '_' . time() . '.' . $ext; + + // Semantic, human-readable file naming (e.g. video_grade_10_math_10_unit_01_lesson_01.mp4) + $cleanBase = preg_replace('/[^a-zA-Z0-9_-]+/', '_', str_replace(['.md', '/'], ['', '_'], $file)); + $cleanBase = trim($cleanBase, '_'); + $safeName = 'video_' . $cleanBase . '_' . time() . '.' . $ext; $destPath = $uploadDir . '/' . $safeName; if (!move_uploaded_file($videoTmp, $destPath)) { @@ -262,15 +281,27 @@ class CurriculumController $videoUrl = '/uploads/curriculum_videos/' . $safeName; - // Automatically update AI assets with the new video URL + // 1. Update Lesson AI assets in manifest.json & filesystem $currentAssets = CurriculumService::getLessonAiAssets($file); $currentAssets['ai_video_url'] = $videoUrl; CurriculumService::saveLessonAiAssets($file, $currentAssets); + // 2. Direct Sync to MySQL Database Table `lessons` + try { + $filename = basename($file, '.md'); + \App\Core\Database::query( + "UPDATE lessons SET ai_video_url = ? WHERE title LIKE ? OR markdown_content LIKE ?", + [$videoUrl, "%{$filename}%", "%{$file}%"] + ); + } catch (\Throwable $dbEx) { + error_log("Video DB sync note: " . $dbEx->getMessage()); + } + $response->json([ 'status' => 'success', - 'message' => 'تم رفع وتثبيت فيديو الشرح على منصة صَقِل بنجاح!', - 'video_url' => $videoUrl + 'message' => 'تم رفع وتثبيت فيديو الشرح وربطه بالدرس وقاعدة البيانات بنجاح!', + 'video_url' => $videoUrl, + 'file_name' => $safeName ]); } } diff --git a/backend/scripts/sync_curriculum_to_database.php b/backend/scripts/sync_curriculum_to_database.php new file mode 100644 index 0000000..3f9f789 --- /dev/null +++ b/backend/scripts/sync_curriculum_to_database.php @@ -0,0 +1,157 @@ + $teacherId, 'uuid' => $teachUuid]; +} +$teacherId = (int)$teacher['id']; + +// 2. Iterate through Grades and Subjects in manifest +foreach ($manifest as $gradeKey => $grade) { + $gradeName = $grade['name'] ?? $gradeKey; + + foreach ($grade['subjects'] as $subjectKey => $subjectData) { + $subjectName = $subjectData['name'] ?? $subjectKey; + $stream = 'scientific'; + if (str_contains($subjectKey, 'english')) { + $stream = 'common'; + } + + // A. Insert/Update Subject + $dbSubject = Database::selectOne("SELECT id FROM subjects WHERE code = ? LIMIT 1", [$subjectKey]); + if (!$dbSubject) { + $subId = Database::insert( + "INSERT INTO subjects (name, code, stream, is_active) VALUES (?, ?, ?, 1)", + [$subjectName, $subjectKey, $stream] + ); + echo "✅ Created Subject: {$subjectName} (code: {$subjectKey})\n"; + } else { + $subId = (int)$dbSubject['id']; + Database::query("UPDATE subjects SET name = ? WHERE id = ?", [$subjectName, $subId]); + } + + // B. Insert/Update Courses for each Semester + foreach ($subjectData['semesters'] as $semesterKey => $semesterData) { + $semType = ($semesterKey === 'semester_2') ? 'second' : 'first'; + $courseTitle = "{$subjectName} — {$gradeName} (" . ($semType === 'first' ? 'الفصل الأول' : 'الفصل الثاني') . ")"; + + $dbCourse = Database::selectOne("SELECT id FROM courses WHERE subject_id = ? AND semester = ? LIMIT 1", [$subId, $semType]); + if (!$dbCourse) { + $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)); + $courseId = Database::insert( + "INSERT INTO courses (uuid, subject_id, teacher_id, title, description, semester, price_jod, is_published) + VALUES (?, ?, ?, ?, ?, ?, 0.00, 1)", + [$cUuid, $subId, $teacherId, $courseTitle, "المساق الوزاري الرسمي المعتمد والمفرغ تفاعلياً لمنهاج {$subjectName}", $semType] + ); + echo " 📚 Created Course: {$courseTitle} (ID: {$courseId})\n"; + } else { + $courseId = (int)$dbCourse['id']; + Database::query("UPDATE courses SET title = ? WHERE id = ?", [$courseTitle, $courseId]); + } + + // C. Insert/Update Lessons under each Unit + $seqOrder = 1; + foreach ($semesterData['units'] as $unitKey => $unitData) { + $unitName = $unitData['name'] ?? $unitKey; + + foreach ($unitData['lessons'] as $les) { + $lessonTitle = $les['title']; + $filePath = $les['file']; + $fullDiskPath = __DIR__ . '/../storage/curriculum/' . $filePath; + + $mdContent = ''; + if (file_exists($fullDiskPath)) { + $mdContent = file_get_contents($fullDiskPath); + } + + $aiVideoUrl = $les['ai_video_url'] ?? null; + $cheatSheet = $les['cheat_sheet'] ?? null; + $socraticQuiz = isset($les['socratic_quiz']) ? json_encode($les['socratic_quiz'], JSON_UNESCAPED_UNICODE) : null; + + // Match existing lesson by course_id and title or file path + $dbLesson = Database::selectOne( + "SELECT id FROM lessons WHERE course_id = ? AND title = ? LIMIT 1", + [$courseId, $lessonTitle] + ); + + if (!$dbLesson) { + $lesId = Database::insert( + "INSERT INTO lessons (course_id, title, sequence_order, ai_video_url, markdown_content, cheat_sheet_markdown, socratic_quiz_json, is_free_preview) + VALUES (?, ?, ?, ?, ?, ?, ?, 1)", + [$courseId, $lessonTitle, $seqOrder, $aiVideoUrl, $mdContent, $cheatSheet, $socraticQuiz] + ); + echo " 📄 [NEW] Lesson inserted: {$lessonTitle} (ID: {$lesId})\n"; + } else { + $lesId = (int)$dbLesson['id']; + Database::query( + "UPDATE lessons SET sequence_order = ?, markdown_content = ?, ai_video_url = COALESCE(?, ai_video_url), cheat_sheet_markdown = COALESCE(?, cheat_sheet_markdown), socratic_quiz_json = COALESCE(?, socratic_quiz_json) WHERE id = ?", + [$seqOrder, $mdContent, $aiVideoUrl, $cheatSheet, $socraticQuiz, $lesId] + ); + echo " 🔄 [UPDATED] Lesson: {$lessonTitle} (ID: {$lesId})\n"; + } + $seqOrder++; + } + } + + // D. Insert Resources (Books, Activity Books) + if (!empty($semesterData['resources'])) { + foreach ($semesterData['resources'] as $resGroupKey => $resGroup) { + foreach ($resGroup['items'] as $resItem) { + $resTitle = $resItem['title']; + $resFilePath = $resItem['file']; + $resDiskPath = __DIR__ . '/../storage/curriculum/' . $resFilePath; + $resMd = file_exists($resDiskPath) ? file_get_contents($resDiskPath) : ''; + + // Add as special lesson / resource entry + $dbResLesson = Database::selectOne( + "SELECT id FROM lessons WHERE course_id = ? AND title = ? LIMIT 1", + [$courseId, $resTitle] + ); + if (!$dbResLesson) { + Database::insert( + "INSERT INTO lessons (course_id, title, sequence_order, markdown_content, is_free_preview) + VALUES (?, ?, ?, ?, 1)", + [$courseId, $resTitle, $seqOrder, $resMd] + ); + echo " 📎 [RESOURCE] Added Resource: {$resTitle}\n"; + } + $seqOrder++; + } + } + } + } + } +} + +echo "\n🎉 SUCCESS! All curriculum subjects, courses, and lessons are synchronized with MySQL database!\n";