A local-first course video player built with Next.js, SQLite, and a bold, compact UI courtesy of Boldkit. It scans a folder of downloaded courses, indexes sections, lessons, captions, attachments, and watch progress, then serves everything through stable ID-based routes.
- Local course library scanning into SQLite
- Section and lesson navigation
- HTML video playback with range requests
- Resume position and completion tracking
- Completed lessons shown with a success background and checkmark
- Matching
.vttsubtitle support, including language sidecars likeintro.en.vtt - Lesson attachments matched from section folders or course-level attachment folders
- Course covers and metadata support
Install dependencies:
pnpm installStart the development server:
pnpm devOpen http://localhost:3000.
The app reads courses and stores its database using these environment variables:
COURSES_DIR=./courses
DATA_DIR=./data
DATABASE_PATH=./data/course-player.sqliteDefaults are shown above. You can put these in .env.local.
Courses are discovered under an instructor folder:
courses/
Instructor Name/
Course Folder Name/
cover.jpg
00. Introduction/
01. Welcome.mp4
01. Welcome.en.vtt
01. The Basics/
01. Tutorial One.mp4
02. Tutorial Two.mp4
Attachments/
01-01brush.abr
01-01colorswatch.aco
02-01character.psdSection folders use a numeric prefix:
00. Introduction
01. The BasicsLesson videos use a numeric prefix and one of these extensions:
01. Lesson Title.mp4
02. Lesson Title.mkv
03. Lesson Title.webmSupported video extensions are mp4, mkv, webm, mov, and m4v.
Captions are matched beside the lesson video by filename:
01. Welcome.mp4
01. Welcome.vtt
01. Welcome.en.vttIf multiple sidecar .vtt files exist, English sidecars such as .en.vtt are preferred.
Course-level attachments use a section-lesson prefix. For example:
Attachments/
1-1brush.abrThis attaches to section 01, lesson 01.
The scanner also supports section-local attachments:
01. The Basics/
Attachments/
01. Brush.abr
01/
Extra File.zipStart the app, then scan the course library:
curl -X POST http://localhost:3000/api/scanAfter scanning, visit /courses and open a course.
Progress is stored in SQLite. The browser sends the current playback position and duration to the server:
- every few seconds during playback
- on pause
- after seeking
- when the tab is hidden
- when the page exits
- when the video ends
Lessons are marked complete when playback reaches 90% or the video ends.
Course tags are editable from the course detail page. Existing tags can be selected, and new tags can be created directly in the UI. Tags are stored in SQLite in courses.tags_json, and the scanner preserves existing user-created tags when a course is rescanned.
When deploying with Docker or Coolify, persist /app/data as a volume. Progress and course tags are both stored in the SQLite database there, so they survive container recreation.
pnpm dev # Start development server
pnpm build # Build production app
pnpm start # Start production server
pnpm lint # Run ESLint- Next.js 16
- React 19
- SQLite via
node:sqlite - Tailwind CSS
- Radix UI primitives
- Lucide icons
- BoldKit-inspired UI styling
Special thanks to the BoldKit creator for making a beautiful UI and for being very helpful with issues.