Update Saqel Platform: 2026-09-02 07:52:23
This commit is contained in:
@@ -127,10 +127,10 @@ class CurriculumRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch lesson playback details (streams, checkpoints, and versions) from Live API
|
/// Fetch lesson playback details (streams, checkpoints, and versions) from Live API
|
||||||
Future<LessonPlaybackData> getLessonPlayback(String lessonId, {String? subjectId, String? unitId}) async {
|
Future<LessonPlaybackData> getLessonPlayback(String curriculumKey, {String? subjectId, String? unitId}) async {
|
||||||
AppLogger.log('Fetching live playback data for lesson $lessonId from /api/lessons/$lessonId/playback...', tag: 'CURRICULUM_REPO');
|
AppLogger.log('Fetching live playback data for curriculum key $curriculumKey...', tag: 'CURRICULUM_REPO');
|
||||||
|
|
||||||
final res = await _api.get('/api/lessons/$lessonId/playback');
|
final res = await _api.get('/api/lessons/playback', queryParams: {'curriculum_key': curriculumKey});
|
||||||
if (res is Map && res['data'] != null) {
|
if (res is Map && res['data'] != null) {
|
||||||
return LessonPlaybackData.fromJson(Map<String, dynamic>.from(res['data']));
|
return LessonPlaybackData.fromJson(Map<String, dynamic>.from(res['data']));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
|
|||||||
AppLogger.log('Loading Socratic playback for lesson: ${lesson.title}', tag: 'VIDEO_CUBIT');
|
AppLogger.log('Loading Socratic playback for lesson: ${lesson.title}', tag: 'VIDEO_CUBIT');
|
||||||
emit(VideoPlaybackLoading());
|
emit(VideoPlaybackLoading());
|
||||||
try {
|
try {
|
||||||
final playback = await _repo.getLessonPlayback(lesson.id, subjectId: subject?.id);
|
final playback = await _repo.getLessonPlayback(lesson.markdownFilePath ?? lesson.id, subjectId: subject?.id);
|
||||||
emit(VideoPlaybackReady(
|
emit(VideoPlaybackReady(
|
||||||
playbackData: playback,
|
playbackData: playback,
|
||||||
lessonItem: lesson,
|
lessonItem: lesson,
|
||||||
|
|||||||
@@ -267,8 +267,8 @@ class CurriculumController
|
|||||||
$manifestLesson = CurriculumService::findLessonByFile($file);
|
$manifestLesson = CurriculumService::findLessonByFile($file);
|
||||||
$title = $manifestLesson['title'] ?? basename($file, '.md');
|
$title = $manifestLesson['title'] ?? basename($file, '.md');
|
||||||
$existingLesson = \App\Core\Database::selectOne(
|
$existingLesson = \App\Core\Database::selectOne(
|
||||||
"SELECT id, course_id FROM lessons WHERE title = ? OR markdown_content LIKE ? LIMIT 1",
|
"SELECT id, course_id FROM lessons WHERE curriculum_key = ? OR title = ? OR markdown_content LIKE ? LIMIT 1",
|
||||||
[$title, '%' . $file . '%']
|
[$file, $title, '%' . $file . '%']
|
||||||
);
|
);
|
||||||
$courseId = $existingLesson
|
$courseId = $existingLesson
|
||||||
? (int)$existingLesson['course_id']
|
? (int)$existingLesson['course_id']
|
||||||
@@ -283,14 +283,14 @@ class CurriculumController
|
|||||||
if ($existingLesson) {
|
if ($existingLesson) {
|
||||||
$lessonId = (int)$existingLesson['id'];
|
$lessonId = (int)$existingLesson['id'];
|
||||||
\App\Core\Database::query(
|
\App\Core\Database::query(
|
||||||
"UPDATE lessons SET storage_type = 'api_upload', video_uuid = ?, bunny_video_id = '', local_path = ?, hls_url = ?, thumbnail_url = ?, duration_seconds = ?, encoding_status = 'ready', ai_video_url = ? WHERE id = ?",
|
"UPDATE lessons SET curriculum_key = ?, storage_type = 'api_upload', video_uuid = ?, bunny_video_id = '', local_path = ?, hls_url = ?, thumbnail_url = ?, duration_seconds = ?, encoding_status = 'ready', ai_video_url = ? WHERE id = ?",
|
||||||
[$uploadResult['video_uuid'], $uploadResult['local_path'], $uploadResult['hls_url'], $uploadResult['thumbnail_url'], $uploadResult['duration'] ?? 0, $videoUrl, $lessonId]
|
[$file, $uploadResult['video_uuid'], $uploadResult['local_path'], $uploadResult['hls_url'], $uploadResult['thumbnail_url'], $uploadResult['duration'] ?? 0, $videoUrl, $lessonId]
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$lessonId = \App\Core\Database::insert(
|
$lessonId = \App\Core\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, ai_video_url)
|
"INSERT INTO lessons (course_id, title, curriculum_key, sequence_order, storage_type, video_uuid, bunny_video_id, local_path, hls_url, thumbnail_url, duration_seconds, is_free_preview, encoding_status, ai_video_url)
|
||||||
VALUES (?, ?, 1, 'api_upload', ?, '', ?, ?, ?, ?, 0, 'ready', ?)",
|
VALUES (?, ?, ?, 1, 'api_upload', ?, '', ?, ?, ?, ?, 0, 'ready', ?)",
|
||||||
[$courseId, $title, $uploadResult['video_uuid'], $uploadResult['local_path'], $uploadResult['hls_url'], $uploadResult['thumbnail_url'], $uploadResult['duration'] ?? 0, $videoUrl]
|
[$courseId, $title, $file, $uploadResult['video_uuid'], $uploadResult['local_path'], $uploadResult['hls_url'], $uploadResult['thumbnail_url'], $uploadResult['duration'] ?? 0, $videoUrl]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -385,13 +385,16 @@ class VideoController
|
|||||||
VideoService::ensureSchema();
|
VideoService::ensureSchema();
|
||||||
CurriculumService::ensureSchema();
|
CurriculumService::ensureSchema();
|
||||||
|
|
||||||
$rawId = $request->getParam('id') ?? '';
|
$curriculumKey = trim((string)($request->getQuery('curriculum_key') ?? ''));
|
||||||
|
$rawId = $curriculumKey !== '' ? $curriculumKey : ($request->getParam('id') ?? '');
|
||||||
$lesson = null;
|
$lesson = null;
|
||||||
|
|
||||||
if (is_numeric($rawId) && (int)$rawId > 0) {
|
if ($curriculumKey !== '') {
|
||||||
|
$lesson = Database::selectOne("SELECT * FROM lessons WHERE curriculum_key = ? LIMIT 1", [$curriculumKey]);
|
||||||
|
} elseif (is_numeric($rawId) && (int)$rawId > 0) {
|
||||||
$lesson = Database::selectOne("SELECT * FROM lessons WHERE id = ? LIMIT 1", [(int)$rawId]);
|
$lesson = Database::selectOne("SELECT * FROM lessons WHERE id = ? LIMIT 1", [(int)$rawId]);
|
||||||
} elseif (!empty($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}%"]);
|
$lesson = Database::selectOne("SELECT * FROM lessons WHERE curriculum_key = ? OR title LIKE ? OR local_path LIKE ? OR markdown_content LIKE ? LIMIT 1", [$rawId, "%{$rawId}%", "%{$rawId}%", "%{$rawId}%"]);
|
||||||
// Flutter curriculum lessons use the manifest slug (e.g. u1_l1_*),
|
// Flutter curriculum lessons use the manifest slug (e.g. u1_l1_*),
|
||||||
// while the database stores the canonical lesson title.
|
// while the database stores the canonical lesson title.
|
||||||
if (!$lesson) {
|
if (!$lesson) {
|
||||||
|
|||||||
@@ -37,6 +37,12 @@ class VideoService
|
|||||||
Database::query("ALTER TABLE lessons ADD COLUMN video_uuid CHAR(36) NULL AFTER storage_type");
|
Database::query("ALTER TABLE lessons ADD COLUMN video_uuid CHAR(36) NULL AFTER storage_type");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$colsCurriculumKey = Database::select("SHOW COLUMNS FROM lessons LIKE 'curriculum_key'");
|
||||||
|
if (empty($colsCurriculumKey)) {
|
||||||
|
Database::query("ALTER TABLE lessons ADD COLUMN curriculum_key VARCHAR(500) NULL AFTER title");
|
||||||
|
Database::query("ALTER TABLE lessons ADD INDEX idx_lessons_curriculum_key (curriculum_key)");
|
||||||
|
}
|
||||||
|
|
||||||
$colsPath = Database::select("SHOW COLUMNS FROM lessons LIKE 'local_path'");
|
$colsPath = Database::select("SHOW COLUMNS FROM lessons LIKE 'local_path'");
|
||||||
if (empty($colsPath)) {
|
if (empty($colsPath)) {
|
||||||
Database::query("ALTER TABLE lessons ADD COLUMN local_path VARCHAR(500) NULL AFTER bunny_video_id");
|
Database::query("ALTER TABLE lessons ADD COLUMN local_path VARCHAR(500) NULL AFTER bunny_video_id");
|
||||||
|
|||||||
@@ -222,6 +222,7 @@ CREATE TABLE IF NOT EXISTS `lessons` (
|
|||||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
`course_id` BIGINT UNSIGNED NOT NULL,
|
`course_id` BIGINT UNSIGNED NOT NULL,
|
||||||
`title` VARCHAR(255) NOT NULL,
|
`title` VARCHAR(255) NOT NULL,
|
||||||
|
`curriculum_key` VARCHAR(500) DEFAULT NULL COMMENT 'المسار الفريد لدرس الـManifest داخل storage/curriculum',
|
||||||
`sequence_order` INT UNSIGNED NOT NULL DEFAULT 1,
|
`sequence_order` INT UNSIGNED NOT NULL DEFAULT 1,
|
||||||
|
|
||||||
-- المعلم الحقيقي (Teacher Video)
|
-- المعلم الحقيقي (Teacher Video)
|
||||||
@@ -249,6 +250,7 @@ CREATE TABLE IF NOT EXISTS `lessons` (
|
|||||||
`updated_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
`updated_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `idx_lessons_course` (`course_id`),
|
KEY `idx_lessons_course` (`course_id`),
|
||||||
|
KEY `idx_lessons_curriculum_key` (`curriculum_key`),
|
||||||
KEY `idx_lessons_video_uuid` (`video_uuid`),
|
KEY `idx_lessons_video_uuid` (`video_uuid`),
|
||||||
CONSTRAINT `fk_lessons_course` FOREIGN KEY (`course_id`) REFERENCES `courses` (`id`) ON DELETE CASCADE
|
CONSTRAINT `fk_lessons_course` FOREIGN KEY (`course_id`) REFERENCES `courses` (`id`) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ $router->post('/api/teacher/lessons/checkpoints', [\App\Controllers\VideoCon
|
|||||||
$router->get('/api/student/lessons', [\App\Controllers\VideoController::class, 'getStudentLessons']);
|
$router->get('/api/student/lessons', [\App\Controllers\VideoController::class, 'getStudentLessons']);
|
||||||
$router->get('/api/videos/stream/{uuid}', [\App\Controllers\VideoController::class, 'streamLocalVideo']);
|
$router->get('/api/videos/stream/{uuid}', [\App\Controllers\VideoController::class, 'streamLocalVideo']);
|
||||||
$router->get('/api/videos/hls/{uuid}/{file}', [\App\Controllers\VideoController::class, 'streamHls']);
|
$router->get('/api/videos/hls/{uuid}/{file}', [\App\Controllers\VideoController::class, 'streamHls']);
|
||||||
|
$router->get('/api/lessons/playback', [\App\Controllers\VideoController::class, 'getPlaybackData']);
|
||||||
$router->get('/api/lessons/{id}/playback', [\App\Controllers\VideoController::class, 'getPlaybackData']);
|
$router->get('/api/lessons/{id}/playback', [\App\Controllers\VideoController::class, 'getPlaybackData']);
|
||||||
|
|
||||||
// Student & Teacher Chat Routes (API-Driven, Authenticated)
|
// Student & Teacher Chat Routes (API-Driven, Authenticated)
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ use App\Services\CurriculumService;
|
|||||||
|
|
||||||
echo "=== SAQEL ENTERPRISE CURRICULUM DATABASE SYNC ===\n";
|
echo "=== SAQEL ENTERPRISE CURRICULUM DATABASE SYNC ===\n";
|
||||||
|
|
||||||
|
// Keep an existing production database compatible with the canonical
|
||||||
|
// curriculum-key linkage before querying or inserting synchronized lessons.
|
||||||
|
\App\Services\VideoService::ensureSchema();
|
||||||
|
|
||||||
$manifestFile = __DIR__ . '/../storage/curriculum/manifest.json';
|
$manifestFile = __DIR__ . '/../storage/curriculum/manifest.json';
|
||||||
if (!file_exists($manifestFile)) {
|
if (!file_exists($manifestFile)) {
|
||||||
die("Error: manifest.json not found at $manifestFile\n");
|
die("Error: manifest.json not found at $manifestFile\n");
|
||||||
@@ -99,24 +103,24 @@ foreach ($manifest as $gradeKey => $grade) {
|
|||||||
$cheatSheet = $les['cheat_sheet'] ?? null;
|
$cheatSheet = $les['cheat_sheet'] ?? null;
|
||||||
$socraticQuiz = isset($les['socratic_quiz']) ? json_encode($les['socratic_quiz'], JSON_UNESCAPED_UNICODE) : 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
|
// The manifest file path is the canonical, globally unique lesson key.
|
||||||
$dbLesson = Database::selectOne(
|
$dbLesson = Database::selectOne(
|
||||||
"SELECT id FROM lessons WHERE course_id = ? AND title = ? LIMIT 1",
|
"SELECT id FROM lessons WHERE curriculum_key = ? OR (course_id = ? AND title = ?) LIMIT 1",
|
||||||
[$courseId, $lessonTitle]
|
[$filePath, $courseId, $lessonTitle]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!$dbLesson) {
|
if (!$dbLesson) {
|
||||||
$lesId = Database::insert(
|
$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)
|
"INSERT INTO lessons (course_id, title, curriculum_key, sequence_order, ai_video_url, markdown_content, cheat_sheet_markdown, socratic_quiz_json, is_free_preview)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, 1)",
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1)",
|
||||||
[$courseId, $lessonTitle, $seqOrder, $aiVideoUrl, $mdContent, $cheatSheet, $socraticQuiz]
|
[$courseId, $lessonTitle, $filePath, $seqOrder, $aiVideoUrl, $mdContent, $cheatSheet, $socraticQuiz]
|
||||||
);
|
);
|
||||||
echo " 📄 [NEW] Lesson inserted: {$lessonTitle} (ID: {$lesId})\n";
|
echo " 📄 [NEW] Lesson inserted: {$lessonTitle} (ID: {$lesId})\n";
|
||||||
} else {
|
} else {
|
||||||
$lesId = (int)$dbLesson['id'];
|
$lesId = (int)$dbLesson['id'];
|
||||||
Database::query(
|
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 = ?",
|
"UPDATE lessons SET curriculum_key = ?, 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]
|
[$filePath, $seqOrder, $mdContent, $aiVideoUrl, $cheatSheet, $socraticQuiz, $lesId]
|
||||||
);
|
);
|
||||||
echo " 🔄 [UPDATED] Lesson: {$lessonTitle} (ID: {$lesId})\n";
|
echo " 🔄 [UPDATED] Lesson: {$lessonTitle} (ID: {$lesId})\n";
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user