Creates config.php from the example if missing, creates the database (and app DB user when admin credentials are given), applies schema.sql idempotently, optionally creates the first login account, and locks itself with .install.lock after a successful run.
CourseHub
Self-hosted course video player: PHP 8 + 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
.mp4files 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 (
Video2beforeVideo10). - 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
-
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';" -
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: setfalseonce your users have accounts.
-
Run — with the PHP built-in server for testing:
php -S 0.0.0.0:8080 -t /path/to/coursehubor point an Apache/Nginx + PHP-FPM vhost at the directory.
-
Index the library — click Rescan library in the top bar, or:
php scan.phpRescan 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.
-
Open the site, create an account, and watch.
How progress works
- The player POSTs
{video_id, position, duration}toapi/progress.phpevery 5 s while playing, plus on pause/seek, vianavigator.sendBeaconwhen 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
videosrow.
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) orX-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, andschema.sql(or place the videos and includes outside the docroot).