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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user