Remove course thumbnails and the ffmpeg dependency

Course cards go back to the colour-block art that previously sat under the
captured frame, so the UI is unchanged apart from losing the photo.

- Drop thumb.php, generateThumbnails(), captureThumbnail() and
  ensureThumbnailColumns(), plus the first-video tracking the scanner only
  kept to pick a frame source.
- Drop the ffprobe fallback in probeDuration(); the pure-PHP mvhd parse is
  now the only scan-time probe, with the player's report on first play as
  the remaining fallback.
- Drop THUMBNAILS_DIR/THUMBNAIL_SEEK_PERCENT from config, the
  thumbnail_* columns from schema.sql, the thumbnail counters from scan
  output, .thumb img from the stylesheet, and the matching README parts.

Existing databases keep two now-unused folders.thumbnail_* columns; they
are inert and can be dropped by hand if wanted.

Claude-Session: https://claude.ai/code/session_014oyi9DMdhALrGz989MNNZJ
This commit is contained in:
Harwinder Sekhon
2026-07-27 14:06:06 -04:00
parent c4c88e52da
commit 91edba56d6
9 changed files with 31 additions and 275 deletions
+6 -39
View File
@@ -20,8 +20,7 @@ views switch without page reloads and browser back/forward still work:
| `#/search/Q` | courses, videos and quizzes matching `Q` |
| `#/settings` | library folder, preferences, reset |
Only `stream.php` (the mp4 bytes) and `thumb.php` (cached course thumbnails)
are plain HTTP resources.
Only `stream.php` (the mp4 bytes) is a plain HTTP resource.
- Recursively scans a videos directory for `.mp4` files and mirrors the folder
hierarchy (courses → sections → videos, any nesting depth).
@@ -37,8 +36,6 @@ are plain HTTP resources.
library between **shelves and catalog**; both choices are remembered.
- **Shelves** group courses by how far you are: in progress, finished, not
started. The app has no course categories, so progress is what sorts them.
- **Course thumbnails** — a real frame captured from each course's first
video, 40% of the way through it.
- Seekable streaming via HTTP Range requests; files are addressed by DB id only
(no path traversal).
@@ -74,15 +71,9 @@ Top-level folders become courses on the home page.
the web server / PHP user).
- DB credentials (change the password in production).
- `ALLOW_REGISTRATION`: set `false` once your users have accounts.
- `THUMBNAILS_DIR`: where generated course thumbnails are cached — must be
*writable* by the web server / PHP user (default: `cache/thumbnails`
next to `config.php`).
- `THUMBNAIL_SEEK_PERCENT`: how far into a course's first video to capture
its thumbnail (default `0.4`, i.e. 40%).
An `ffmpeg` binary on `$PATH` is required for thumbnails (and is the
fallback for video duration if the pure-PHP mp4 parser can't read a file).
Without it, courses just keep their colour-block placeholder.
No external binaries are needed — PHP with PDO/MySQL is the whole
dependency list.
3. **Run** — with the PHP built-in server for testing:
@@ -128,29 +119,6 @@ correct set. Best score, last score, and attempt count are stored per user;
a quiz is *completed* once every question is answered correctly in one
attempt, and quiz scores count toward folder progress alongside videos.
## Thumbnails
Each course's card is a real frame, not just a colour block: the scanner finds
the course's first video — in the same order you'd browse to it (subfolders in
listing order, then that folder's videos) — and asks `ffmpeg` for a frame
`THUMBNAIL_SEEK_PERCENT` (default 40%) of the way through it, scaled and
cropped to 640×360. The result is cached at `THUMBNAILS_DIR/<course id>.jpg`
and served by `thumb.php?course_id=N`, which looks the file up by that ID only
(same containment check as `stream.php`) and sends normal `ETag`/
`Last-Modified` caching headers.
- Regeneration only happens when a course's first video has changed since the
last scan (tracked via `folders.thumbnail_video_id`), so rescans stay fast.
- If `ffmpeg` is missing, a video's duration can't be read, or generation
otherwise fails, the course simply keeps its colour-block placeholder — the
`<img>` 404s and the client removes it (`onerror="this.remove()"`), no error
is shown to the user. Failures are logged (`error_log`) and rolled up into
the scan's `thumbnails_failed` count.
- `thumbnail_video_id`/`thumbnail_generated_at` on `folders` are added
automatically on first use (`ensureThumbnailColumns()`) if a database
created before this feature doesn't have them yet — no manual migration
needed, the same self-healing `app_meta` uses.
## Search
The top-bar box searches as you type (from two characters, debounced) and also
@@ -201,7 +169,7 @@ line up without a separate monospace face.
Courses have no cover art, so each one gets a hue from the design's six-colour
set (clay, sage, slate, sand, plum, sky), picked from its folder id so it stays
the same across renders and reloads. That hue drives its thumbnail, its initial
the same across renders and reloads. That hue drives its card art, its initial
badge and its progress bar.
anime.js drives the motion: views fade and stagger in, progress bars grow from
@@ -241,9 +209,8 @@ next scan creates it.
- Folder progress is computed over the folder's entire subtree using a path
prefix match, with partial credit for videos in progress.
- Video duration is read at scan time by parsing the mp4's `mvhd` header in
pure PHP (ffprobe fallback if the parse fails), so listings show durations
before anyone plays the video. The browser still reports the duration on
first play as a last-resort fallback.
pure PHP, so listings show durations before anyone plays the video. If a file
can't be parsed, the browser reports its duration on first play instead.
## Production notes