Skip to content

Nick040791/LocalMediaServer

Repository files navigation

LocalMediaServer

A self-hosted, personal-Netflix-style video server for your home network. Point it at a folder of .mp4 / .mkv / .webm files, open the URL on your phone, and stream your library straight from disk — no cloud, no upload, no account, no tracking.

It's a single Node.js process plus a static front-end. There is no build step, no database to install (the watch-progress store is a single SQLite file), and no dependency beyond express and better-sqlite3.

Features

  • Recursive video library scan (mp4, mkv, webm, mov, m4v, avi).
  • Netflix-style home screen: Continue watching, Recently watched, Recently added, and one carousel per top-level folder.
  • Resumable playback — the player picks up where you left off, even after closing the tab.
  • Library grid with infinite scroll, plus a folder view and a search box.
  • HTTP range streaming — handles 4 GB+ movie files without buffering the whole thing.
  • Per-folder picker so you can repoint the library at a different drive from any device on the LAN.
  • Modern, accessible UI: ARIA roles, keyboard navigation (Esc closes the player), reduced-motion support, mobile-first responsive layout.

Security model

Concern Mitigation
Read access to files outside the library root Every media path is resolved with path.resolve and then path.relative is checked against the library root. The walk refuses to follow symbolic links. Bounded by LOCALMEDIA_MAX_DEPTH (default 12) and LOCALMEDIA_MAX_FILES (default 50,000).
Read access to host filesystem via /api/browse The folder picker is restricted to inside the current library root.
Unauthenticated folder changes POST /api/config requires a bearer token via the LOCALMEDIA_TOKEN environment variable. The token is compared with crypto.timingSafeEqual.
Unauthenticated writes POST /api/progress requires the same bearer token.
Watch-progress privacy Progress is keyed by libraryRoot + relativePath, so two users pointing at different libraries don't collide.
Read-only API for honest devices GET /api/config, GET /api/media, GET /api/home, GET /api/progress/*, and GET /media/* are open so phones on the LAN can browse without a login.
Cross-site scripting Every dynamic string (folder names, filenames, captions) is rendered with textContent. No innerHTML write ever receives user-controlled input.
MIME sniffing of error responses /media/* errors set Content-Type: text/plain; charset=utf-8.
Click-jacking / frame embedding X-Frame-Options: DENY and CSP frame-ancestors 'none'.
MIME type sniffing of responses X-Content-Type-Options: nosniff on every response.
Referrer leakage Referrer-Policy: no-referrer on every response.
Privilege-creep via geolocation / camera / mic Permissions-Policy denies them all.
Server DoS via body floods JSON body limit is 32 KB.
Walk DoS via huge trees LOCALMEDIA_MAX_DEPTH and LOCALMEDIA_MAX_FILES cap the recursion.

If LOCALMEDIA_TOKEN is not set, all mutating endpoints will refuse every request with HTTP 503. Set it to a long random string to enable them.

Quick start

Requires Node 18+ and a folder of video files on the host.

npm install
LOCALMEDIA_TOKEN="$(node -e 'console.log(require("crypto").randomBytes(32).toString("hex"))')" npm start

Then open the URL the server prints, e.g. http://192.168.1.42:3013, on any device on the same LAN. The library root defaults to ~/Videos; click the gear icon and paste the bearer token to change the folder.

Environment variables

Variable Default Purpose
PORT 3013 TCP port the server listens on.
LOCALMEDIA_PATH ~/Videos Initial library root. Overridden by data/settings.json once the user picks a folder.
LOCALMEDIA_TOKEN (unset) Bearer token required for POST /api/config, POST /api/progress, and GET /api/browse. When unset, those endpoints return 503.
LOCALMEDIA_MAX_DEPTH 12 Recursion limit for the library walk.
LOCALMEDIA_MAX_FILES 50000 Hard cap on the number of files in the index.

Supported video formats

mp4, mkv, webm, mov, m4v, avi.

Development

npm install
node --check server.js
node --check public/app.js
LOCALMEDIA_TOKEN=*** npm start

The project is small enough that the whole server fits in one file (server.js) and the whole front-end in public/.

License

MIT — see LICENSE.

About

Self-hosted, personal-Netflix-style LAN video server. Resumable playback, recently watched, and a hardened security model.

Topics

Resources

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages