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.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfMPJK7TQV18JhFv6yTKev
This commit is contained in:
root
2026-09-08 18:51:04 +00:00
parent 2fa7452253
commit 6a09ae3833
7 changed files with 849 additions and 115 deletions
+12 -1
View File
@@ -734,7 +734,18 @@ button, input { font-family: inherit; font-size: inherit; }
text-decoration: none;
}
.hero:hover { color: var(--surface); }
.hero-art { width: 280px; flex: none; position: relative; border-radius: 10px; overflow: hidden; }
.hero-art {
display: block;
width: 280px; flex: none;
position: relative;
border-radius: 10px;
overflow: hidden;
}
/* The hero art is a link straight into the player, so its play badge has to
read as a button — the rest of the thumb art is decoration. */
.hero-art .thumb-play span { transition: transform .12s ease, background-color .12s ease; }
.hero-art:hover .thumb-play span { background: var(--surface); transform: scale(1.06); }
.hero-art:focus-visible { outline: 2px solid var(--surface); outline-offset: 2px; }
.hero-body {
flex: 1;
min-width: 0;