deda7c3c18
Scanner indexes quiz files found next to videos ([&] questions, "- " options, "-- Correct" markers; _notsolved twins ignored). Quizzes appear in the folder content list interleaved with videos by filename order. New api/quiz.php serves questions without answers and grades attempts server-side; per-user best/last score and attempts stored in quiz_progress. Folder/course progress now aggregates videos + quizzes with partial credit for partial scores. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
120 lines
4.5 KiB
Markdown
120 lines
4.5 KiB
Markdown
# CourseHub
|
|
|
|
Self-hosted course video player: PHP 8.0+ + MySQL/MariaDB + Bootstrap 5 + [Plyr](https://github.com/sampotts/plyr).
|
|
|
|
Single-page app: `index.php` is a static shell; all navigation, login, browsing,
|
|
playback, and scanning happen over AJAX against the JSON API in `api/`
|
|
(`auth.php`, `library.php`, `progress.php`, `scan.php`), with hash-based routes
|
|
(`#/`, `#/folder/N`, `#/watch/N`) so views switch without page reloads and
|
|
browser back/forward still work. 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).
|
|
- Per-user **resume**: log in from any device and continue where you left off.
|
|
- **Progress tracking at every level** — each folder shows completed/total and a
|
|
percentage bar computed over its whole subtree.
|
|
- "Continue watching" strip, auto-play next video, playback speed, keyboard
|
|
shortcuts (Plyr), completed checkmarks, natural sorting (`Video2` before `Video10`).
|
|
- Seekable streaming via HTTP Range requests; files are addressed by DB id only
|
|
(no path traversal).
|
|
|
|
## Layout on disk
|
|
|
|
```
|
|
videos/
|
|
├── Course 1/
|
|
│ ├── Intro/
|
|
│ │ ├── Video1.mp4
|
|
│ │ └── Video2.mp4
|
|
│ └── Module 2/
|
|
│ └── Advanced/
|
|
│ └── lesson.mp4
|
|
└── Course 2/
|
|
└── ...
|
|
```
|
|
|
|
Top-level folders become courses on the home page.
|
|
|
|
## Setup
|
|
|
|
1. **Database** — create the schema and an app user:
|
|
|
|
```bash
|
|
sudo mysql < schema.sql
|
|
sudo mysql -e "CREATE USER IF NOT EXISTS 'coursehub'@'localhost' IDENTIFIED BY 'coursehub';
|
|
GRANT ALL ON coursehub.* TO 'coursehub'@'localhost';"
|
|
```
|
|
|
|
2. **Configure** — edit `config.php`:
|
|
- `VIDEOS_ROOT`: absolute path to your course folders (must be readable by
|
|
the web server / PHP user).
|
|
- DB credentials (change the password in production).
|
|
- `ALLOW_REGISTRATION`: set `false` once your users have accounts.
|
|
|
|
3. **Run** — with the PHP built-in server for testing:
|
|
|
|
```bash
|
|
php -S 0.0.0.0:8080 -t /path/to/coursehub
|
|
```
|
|
|
|
or point an Apache/Nginx + PHP-FPM vhost at the directory.
|
|
|
|
4. **Index the library** — click **Rescan library** in the top bar, or:
|
|
|
|
```bash
|
|
php scan.php
|
|
```
|
|
|
|
Rescan whenever you add/remove files. Re-scans are idempotent: existing
|
|
videos keep their IDs (and everyone's progress); files that disappeared are
|
|
hidden, not deleted, so progress survives if they come back.
|
|
|
|
5. Open the site, create an account, and watch.
|
|
|
|
## Quizzes
|
|
|
|
Course folders may contain quiz text files next to the videos. Files named
|
|
`*_quiz_solved.txt` are indexed (a `*_quiz_notsolved.txt` twin is ignored —
|
|
it's the same content without answers). Format:
|
|
|
|
```
|
|
[&] Which two are supported CLI connectivity methods? (Select two)
|
|
- RDP
|
|
- rlogin
|
|
- GUI widget -- Correct
|
|
- FortiExplorer -- Correct
|
|
```
|
|
|
|
Questions start with `[&]`, options with `- `, and correct options end with
|
|
`-- Correct`. Questions with more than one correct option automatically render
|
|
as checkboxes instead of radios.
|
|
|
|
Quizzes are graded server-side (answers are never sent to the browser before
|
|
submission). A question counts only if the selected set exactly matches the
|
|
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.
|
|
|
|
## How progress works
|
|
|
|
- The player POSTs `{video_id, position, duration}` to `api/progress.php`
|
|
every 5 s while playing, plus on pause/seek, via `navigator.sendBeacon`
|
|
when the tab is hidden or closed, and when navigating to another view
|
|
in the SPA.
|
|
- A video is marked **completed** at ≥ 95 % watched (`COMPLETE_THRESHOLD`).
|
|
Completion is sticky — rewatching doesn't un-complete 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 captured from the browser on first play (no ffprobe
|
|
dependency) and cached on the `videos` row.
|
|
|
|
## Production notes
|
|
|
|
- PHP streams the video in 512 KB chunks with Range support, which is fine for
|
|
a handful of users. For heavier use, serve files with `X-Accel-Redirect`
|
|
(Nginx) or `X-Sendfile` (Apache) instead of streaming through PHP.
|
|
- Put the app behind HTTPS and set `session.cookie_secure=1`.
|
|
- Deny direct web access to `includes/`, `config.php`, and `schema.sql`
|
|
(or place the videos and includes outside the docroot).
|