sidebar/header chrome: totals, shelves, last scan // GET api/library.php?view=home -> resume card, stats, in progress, recent additions // GET api/library.php?view=library -> every course, grouped into shelves // GET api/library.php?view=folder&id=N -> folder contents + progress // GET api/library.php?view=video&id=N -> video + playlist + resume info // GET api/library.php?view=search&q=text -> matching folders, videos, quizzes // GET api/library.php?view=settings -> library paths, thresholds, progress summary require_once __DIR__ . '/../includes/auth.php'; require_once __DIR__ . '/../includes/functions.php'; header('Content-Type: application/json'); $userId = requireLoginApi(); function folderSummary(array $f, int $userId): array { return [ 'id' => (int)$f['id'], 'name' => $f['name'], 'progress' => folderProgress($userId, $f['path']), ]; } function videoSummary(array $v): array { $duration = $v['duration_seconds'] ?? $v['watched_duration'] ?? null; $position = (float)($v['position_seconds'] ?? 0); return [ 'id' => (int)$v['id'], 'name' => $v['name'], 'filename' => $v['filename'], 'duration' => $duration !== null ? (float)$duration : null, 'position' => $position, 'completed' => (bool)($v['completed'] ?? false), 'percent' => ($duration ?? 0) > 0 ? min(100, (int)round(100 * $position / $duration)) : 0, ]; } function quizSummary(array $q): array { return [ 'id' => (int)$q['id'], 'name' => $q['name'], 'filename' => $q['filename'], 'questions' => (int)$q['num_questions'], 'attempts' => (int)($q['attempts'] ?? 0), 'best_score' => (int)($q['best_score'] ?? 0), 'completed' => (bool)($q['completed'] ?? false), ]; } function crumbList(array $folder): array { return array_map( fn($c) => ['id' => (int)$c['id'], 'name' => $c['name']], folderBreadcrumbs($folder) ); } /** The three shelves the library groups courses into. */ function shelveCourses(array $courses): array { $shelves = [ ['key' => 'progress', 'name' => 'In progress', 'courses' => []], ['key' => 'finished', 'name' => 'Finished', 'courses' => []], ['key' => 'fresh', 'name' => 'Not started', 'courses' => []], ]; foreach ($courses as $c) { foreach ($shelves as $i => $shelf) { if ($shelf['key'] === $c['state']) { $shelves[$i]['courses'][] = $c; } } } return array_values(array_filter($shelves, fn($s) => $s['courses'])); } $view = $_GET['view'] ?? 'home'; switch ($view) { case 'meta': $courses = courseCards($userId); $counts = ['progress' => 0, 'finished' => 0, 'fresh' => 0]; foreach ($courses as $c) { $counts[$c['state']]++; } echo json_encode([ 'totals' => libraryTotals(), 'shelves' => [ ['key' => 'progress', 'name' => 'In progress', 'count' => $counts['progress']], ['key' => 'finished', 'name' => 'Finished', 'count' => $counts['finished']], ['key' => 'fresh', 'name' => 'Not started', 'count' => $counts['fresh']], ], 'last_scan' => appMetaGet('last_scan_at'), 'username' => currentUsername(), ]); break; case 'home': $courses = courseCards($userId); $resume = continueWatching($userId, 1); $resumeCourseId = null; if ($resume) { $resumeFolder = getFolder((int)$resume[0]['folder_id']); if ($resumeFolder) $resumeCourseId = courseIdOf($resumeFolder); } echo json_encode([ 'stats' => userStats($userId, $courses), 'resume' => $resume ? [ 'id' => (int)$resume[0]['id'], 'course_id' => $resumeCourseId, 'name' => $resume[0]['name'], 'folder_path' => $resume[0]['folder_path'], 'position' => (float)$resume[0]['position_seconds'], 'duration' => $resume[0]['duration_seconds'] !== null ? (float)$resume[0]['duration_seconds'] : null, ] : null, 'continue' => array_map(fn($r) => [ 'id' => (int)$r['id'], 'name' => $r['name'], 'folder_path' => $r['folder_path'], 'position' => (float)$r['position_seconds'], 'duration' => $r['duration_seconds'] !== null ? (float)$r['duration_seconds'] : null, ], continueWatching($userId)), 'in_progress' => array_values(array_filter($courses, fn($c) => $c['state'] === 'progress')), 'recent' => recentCourses($userId), 'courses' => $courses, ]); break; case 'library': $courses = courseCards($userId); echo json_encode([ 'courses' => $courses, 'shelves' => shelveCourses($courses), 'totals' => libraryTotals(), ]); break; case 'folder': $folder = getFolder((int)($_GET['id'] ?? 0)); if (!$folder) { http_response_code(404); echo json_encode(['error' => 'Folder not found']); break; } $crumbs = crumbList($folder); echo json_encode([ 'folder' => ['id' => (int)$folder['id'], 'name' => $folder['name'], 'path' => $folder['path']], 'course_id' => $crumbs ? $crumbs[0]['id'] : (int)$folder['id'], 'breadcrumbs' => $crumbs, 'rollup' => folderRollup($userId, $folder['path']), 'progress' => folderProgress($userId, $folder['path']), 'subfolders' => array_map(fn($f) => courseSummary($userId, $f), childFolders((int)$folder['id'])), 'videos' => array_map('videoSummary', folderVideos((int)$folder['id'], $userId)), 'quizzes' => array_map('quizSummary', folderQuizzes((int)$folder['id'], $userId)), ]); break; case 'video': $video = getVideo((int)($_GET['id'] ?? 0)); if (!$video) { http_response_code(404); echo json_encode(['error' => 'Video not found']); break; } $folder = getFolder((int)$video['folder_id']); $playlist = folderVideos((int)$folder['id'], $userId); $crumbs = crumbList($folder); $stmt = db()->prepare( 'SELECT position_seconds, completed FROM video_progress WHERE user_id = ? AND video_id = ?' ); $stmt->execute([$userId, (int)$video['id']]); $saved = $stmt->fetch() ?: null; $resumeAt = ($saved && !$saved['completed'] && $saved['position_seconds'] > 5) ? (float)$saved['position_seconds'] : 0; $prevId = $nextId = null; foreach ($playlist as $i => $p) { if ((int)$p['id'] === (int)$video['id']) { $prevId = isset($playlist[$i - 1]) ? (int)$playlist[$i - 1]['id'] : null; $nextId = isset($playlist[$i + 1]) ? (int)$playlist[$i + 1]['id'] : null; break; } } echo json_encode([ 'video' => ['id' => (int)$video['id'], 'name' => $video['name'], 'duration' => $video['duration_seconds'] !== null ? (float)$video['duration_seconds'] : null], 'folder' => ['id' => (int)$folder['id'], 'name' => $folder['name']], 'course_id' => $crumbs ? $crumbs[0]['id'] : (int)$folder['id'], 'breadcrumbs' => $crumbs, 'playlist' => array_map('videoSummary', $playlist), 'quizzes' => array_map('quizSummary', folderQuizzes((int)$folder['id'], $userId)), 'resume_at' => $resumeAt, 'prev_id' => $prevId, 'next_id' => $nextId, 'stream_url' => 'stream.php?id=' . (int)$video['id'], 'captions_url' => srtFilePath($video) !== null ? 'captions.php?id=' . (int)$video['id'] : null, ]); break; case 'search': $q = trim((string)($_GET['q'] ?? '')); if ($q === '') { echo json_encode(['query' => '', 'folders' => [], 'videos' => [], 'quizzes' => []]); break; } $withPath = fn(array $row, array $summary) => $summary + ['folder_path' => $row['folder_path']]; echo json_encode([ 'query' => $q, 'folders' => array_map( fn($f) => courseSummary($userId, $f), searchFolders($q) ), 'videos' => array_map( fn($v) => $withPath($v, videoSummary($v)), searchVideos($userId, $q) ), 'quizzes' => array_map( fn($q2) => $withPath($q2, quizSummary($q2)), searchQuizzes($userId, $q) ), ]); break; case 'settings': $courses = courseCards($userId); $withSome = array_values(array_filter($courses, fn($c) => $c['progress']['percent'] > 0)); echo json_encode([ 'root' => rtrim(VIDEOS_ROOT, '/'), 'root_exists' => is_dir(VIDEOS_ROOT), 'totals' => libraryTotals(), 'last_scan' => appMetaGet('last_scan_at'), 'complete_threshold' => (int)round(100 * COMPLETE_THRESHOLD), 'registration_open' => (bool)ALLOW_REGISTRATION, 'username' => currentUsername(), 'progress_courses' => count($withSome), 'courses' => count($courses), ]); break; default: http_response_code(400); echo json_encode(['error' => 'Unknown view']); }