Detect course roots per branch and add a category layer above them

The scanner no longer assumes courses sit at a fixed depth. It walks
each branch and marks a folder is_course when it holds media directly
or its children read as chapters (01_, 1. , Module 2, Week 3);
everything above that is a category, and every folder still gets a
course_id pointing at its nearest course for cheap rollups. An empty
.course/.category marker file overrides the heuristic per folder.

Also:
- Roll up progress for the whole library in two grouped queries
  instead of one prefix-matched query per course/folder card.
- Store folders/videos/quizzes paths as utf8mb4_bin so folders that
  differ only in case (CCNP Notes vs CCNP NOtes) aren't merged by a
  case-insensitive UNIQUE key.
- migrateLibrarySchema() brings an older install's tables up to date
  at scan time, so there's no separate migration step.
- Library UI gains a categories view alongside shelves/catalog; the
  continue-watching strip's play button now seeks to the saved
  position on open.

Claude-Session: https://claude.ai/code/session_01FfMPJK7TQV18JhFv6yTKev
This commit is contained in:
root
2026-09-08 18:51:04 +00:00
parent 6a4c972c47
commit 04314327db
7 changed files with 849 additions and 115 deletions
+2 -1
View File
@@ -9,6 +9,7 @@ if (PHP_SAPI !== 'cli') {
}
$stats = scanLibrary();
echo "Scan complete: {$stats['folders']} folders, {$stats['videos']} videos, {$stats['quizzes']} quizzes"
echo "Scan complete: {$stats['courses']} courses in {$stats['folders']} folders,"
. " {$stats['videos']} videos, {$stats['quizzes']} quizzes"
. " ({$stats['missing_folders']} folders / {$stats['missing_videos']} videos"
. " / {$stats['missing_quizzes']} quizzes flagged missing)\n";