Harwinder Sekhon deda7c3c18 Add quiz support: parse *_quiz_solved.txt files, take and grade quizzes
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>
2026-07-11 11:24:37 -04:00

CourseHub

Self-hosted course video player: PHP 8.0+ + MySQL/MariaDB + Bootstrap 5 + 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:

    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:

    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:

    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).
S
Description
No description provided
Readme 1.1 MiB
Languages
PHP 57.1%
JavaScript 28.4%
CSS 14.5%