← All articles

Web Development/4 min read

CourseCraft, a course website from a folder of videos

How I built CourseCraft, a local-only Node.js app with no npm dependencies that turns downloaded video courses into an organised course site with progress, notes and subtitles.

Mohamad.B Alhakim

I had a few long video courses on my disk, and watching them in a media player was painful. No idea which lesson I was on, no progress, subtitles hit and miss, and the source code that came with the course was buried in a zip somewhere.

So I built CourseCraft: point it at a folder, and it builds a proper course page with sections, lessons, progress tracking, subtitles, a searchable transcript, notes per lesson and a code viewer. It runs on your own computer and never uploads anything.

The constraints I set

Three rules shaped every decision:

  1. No npm install. It's just Node.js 18+ and the standard library. Double-click a .bat file on Windows, or run node server.js.
  2. Local only. The server listens on 127.0.0.1, so nothing else on your network can reach it, and it never talks to the internet.
  3. Don't touch the course files. Progress, notes and the library live in CourseCraft's own data folder.

No dependencies sounds like a gimmick, but it makes the app trivial to install for non-developers and there's nothing to keep updated. Node's http, fs, crypto and child_process modules turned out to be enough.

Turning messy folders into a course

Downloaded courses are messy. A file might be called 1- Setting Up - coursedownloader.net.mp4 and sit next to 10- Generics.mp4, which a normal sort puts before 2-.

CourseCraft scans the folder tree and treats subfolders as sections and videos as lessons. Two small tricks do most of the work:

  • Natural sorting with Intl.Collator and numeric: true, so 2 comes before 10.
  • Name cleaning with a couple of regular expressions: strip the extension, remove a trailing - somesite.net, turn underscores into spaces, and split a leading number like 1-, 2. or 1.3) from the title.

Subtitles are matched by file name (Lesson.mp4 with Lesson.vtt or Lesson.srt), and .srt files are converted to WebVTT on the fly so the browser's <track> element can show them. Folders without videos, like source code and exercises, show up under a "Files & code" tab and open in a viewer with a file tree and syntax highlighting.

Streaming video properly

Seeking in a video only works if the server supports HTTP range requests. When the browser asks for Range: bytes=1000000-, the server has to answer with 206 Partial Content and just that slice of the file.

The file server does exactly that: it parses the range, answers 416 if it's out of bounds, sets Content-Range and Accept-Ranges, and streams the slice with fs.createReadStream(file, { start, end }). That's what makes scrubbing, resuming and speed changes feel instant even on multi-gigabyte files.

Not trusting paths

Even a local app should not serve arbitrary files. Every request for a lesson, subtitle or code file is resolved against the course's own folder, and rejected if it escapes it:

js
function resolveIn(course, rel) { const base = path.resolve(course.path); const abs = path.resolve(base, rel || '.'); if (abs !== base && !abs.startsWith(base + path.sep)) return null; return abs; }

path.resolve normalises .. segments first, so a request for ../../Windows/win.ini resolves outside the base and gets null.

Unzipping what Windows can't

Many course archives use LZMA compression, and Windows' built-in "Extract All" refuses to open them. Instead of adding a zip library, CourseCraft calls the tar.exe that ships with Windows 10 and later (it's bsdtar, which handles these zips).

Extraction goes into a temporary .extracting-… folder first and is only renamed into place when it finishes. If you close the window halfway, the half-finished folder is cleaned up on the next run, and the original zip is never deleted. Most course zips also wrap everything in one top folder, so the app unwraps that to avoid Course/Course/….

Saving progress without corrupting it

Progress is saved as you watch, which means lots of small writes. Two details keep that safe:

  • Writes are debounced: changes mark a store as dirty and are flushed 400 ms later in one go.
  • Each flush writes to a .tmp file and then renames it over the real file. A rename is atomic, so a crash mid-write leaves the old file intact instead of half a JSON document.

On SIGINT or SIGTERM the server flushes once more before exiting, so closing the window doesn't lose the last few seconds.

The player

The front end is plain HTML, CSS and JavaScript. The player adds what I missed most: speed from 0.5× to 3×, YouTube-style keyboard shortcuts (J/K/L, N for next lesson, D to mark complete), theater mode, autoplay of the next lesson, and resuming exactly where you stopped. The transcript is searchable, and clicking a line jumps to that moment. Notes can hold timestamps that work the same way.

What I learned

  • The standard library goes a long way. Range requests, atomic writes and a JSON store are a few dozen lines each.
  • Use what the OS already has. Shelling out to the system tar solved LZMA in a few lines.
  • Small details make it feel finished. Natural sorting and cleaned-up titles did more for the experience than any big feature.

Blog / Keep reading

Related articles.

CourseCraft, a course website from a folder of videos - Mohamad Bashar Alhakim