Skip to content

nanwer/kinema

Repository files navigation

kinema

Self-hosted torrent streaming web app. Self-hosters point it at indexers they have access to, and stream content they have the rights to access — sequentially, in the browser, with watch-state and subtitles.

Use responsibly. This is a generic streaming engine — you supply the indexers, you supply the content. See DISCLAIMER.md.

Status: v1 complete. All backend services and routes implemented, frontend pages wired, dev-server smoke test passing (auth → profile CRUD → SQLite persistence). Untested end-to-end against real torrents; see Verification below.


What it does

  • Lookup metadata for movies and shows via TMDB.
  • Query your configured Prowlarr indexers for matching torrents, rank them, and start streaming the head of the file. No "download finishes, then watch" step.
  • Per-profile watch state, resume where you left off.
  • Auto-fetch subtitles from OpenSubtitles or Subdl.
  • Smart playback: direct-play for browser-friendly content, ffmpeg-transcode for HEVC/MKV/etc.
  • Stream-then-delete — the disk does not fill up.

Stack

Layer Choice
Language TypeScript on Node.js 22
Backend Fastify, WebTorrent, ffmpeg (system binary)
Frontend React 18, Vite, Tailwind CSS 3
DB SQLite via better-sqlite3 (WAL mode)
Indexers Prowlarr sidecar container
Metadata TMDB API v3
Subtitles OpenSubtitles v2 + Subdl
Player HTML5 <video> + hls.js for transcoded streams

Quick start (Docker, recommended)

cp .env.example .env
# Edit .env — at minimum set APP_PASSWORD, COOKIE_SECRET, TMDB_API_KEY.

docker compose up --build

Then:

  1. Open http://localhost:3000 — log in with APP_PASSWORD.
  2. Pick or create a profile.
  3. Configure Prowlarr (one-time):
    • On the server itself, open http://127.0.0.1:9696. The Prowlarr port is intentionally bound to 127.0.0.1 only and is not reachable from the network — see Security.
    • Add at least one indexer (Settings → Indexers → Add Indexer). 1337x and YTS are free and don't require credentials.
    • Copy Prowlarr's API key (Settings → General → API Key) into .env as PROWLARR_API_KEY.
  4. docker compose restart app so the new env var is picked up.

Quick start (development, no Docker)

You'll need a Prowlarr instance reachable separately. Easiest: run the Prowlarr container alone, point PROWLARR_URL at http://127.0.0.1:9696.

npm install
cp .env.example .env
# Edit .env

npm run dev

The dev script runs:

  • Fastify backend on :3000
  • Vite dev server on :5173 (proxies /api/* to :3000)

Open http://localhost:5173.


API keys

Service Required? How to get one
TMDB Yes Free at https://www.themoviedb.org/settings/api
OpenSubtitles Recommended https://www.opensubtitles.com/ — needs all three of OPENSUBS_API_KEY, OPENSUBS_USERNAME, OPENSUBS_PASSWORD. Search works with just the API key, but downloads fail without user credentials. Free tier is 20 downloads/day per user; the app caches aggressively (per <tmdb>-<season>-<episode>-<lang>) so switching torrent variants of the same content reuses the same subtitle file.
Subdl Optional fallback https://subdl.com/api — exact terms vary; verify before relying on it as primary. Set SUBTITLE_PRIMARY=subdl to flip the priority.
Prowlarr API key Yes Generated by Prowlarr itself — see Quick start step 3.

Security

The default port-binding strategy is built around the assumption that you'll reverse-proxy or tunnel the app's port (:3000) but keep Prowlarr local-only.

  • App port (:3000, configurable via HOST_APP_PORT) is bound to 0.0.0.0. Put it behind Cloudflare Tunnel, Tailscale, a reverse proxy, etc. The app's gate is APP_PASSWORD + login rate limit (5 attempts / 15 min per IP, with exponential backoff after lockout).
  • Prowlarr port (:9696, configurable via HOST_PROWLARR_PORT) is bound to 127.0.0.1 only. Do NOT add it to a Cloudflare Tunnel / reverse proxy / port forward without first enabling Prowlarr's built-in authentication. Prowlarr ships with no auth.

If you need remote Prowlarr access:

  1. Enable Prowlarr's built-in authentication (Settings → General → Authentication).
  2. Then change docker-compose.yml to bind it to 0.0.0.0 and add it to your tunnel.

Tuning

Var Default Notes
MAX_CONCURRENT_TRANSCODES 1 Caps simultaneous ffmpeg processes. Direct-play streams don't count against this. On a multi-core server (Xeon E5-2420 v2 6c/12t and similar), 2 is reasonable for two simultaneous viewers; higher will likely thrash. Monitor CPU after raising.
HOST_APP_PORT 3000 Override if :3000 collides with another service.
HOST_PROWLARR_PORT 9696 Override if :9696 collides.
SUBTITLE_PRIMARY opensubtitles opensubtitles or subdl. The other becomes automatic fallback.

Deployment notes

Real-world deployment patterns and gotchas (Portainer + GitOps, sharing an existing VPN gateway, firewall rules for LAN bypass, container restart ordering, anti-bot indexer proxies, etc.) live in docs/deployment.md. Read that before your first production deploy.

Verification

A 20-step end-to-end verification plan lives in the implementation spec (path ~/.claude/plans/heyyy-i-wana-work-twinkling-giraffe.md if you've kept the planning artifacts). Critical checks:

  1. docker compose up --build brings both containers up cleanly.
  2. Login + profile picker + create profile works.
  3. Prowlarr UI loads at http://127.0.0.1:9696 from the server (NOT from a remote browser — that's by design).
  4. Search a known title (any well-seeded torrent your indexers return — Big Buck Bunny, Sintel, and other public-domain titles work as a clean smoke test) → click play → buffer state shows peers → video plays.
  5. Pause, refresh, "Continue Watching" appears → click → resumes near the same timestamp.
  6. Tab-close cleanup: force-close the player tab (do not navigate away). Within ~10s, /data/torrents should be empty and the stream_sessions table row removed. Verifies navigator.sendBeacon-driven teardown.
  7. Stall recovery: throttle peers → after 30s of <100 KB/s and buffer <10s, a banner appears: "This source is slow. Try another?" — does NOT auto-switch.
  8. Concurrency cap: with MAX_CONCURRENT_TRANSCODES=1, two HEVC streams from different profiles → second shows "Queued" until the first frees the slot. Direct-play streams don't queue.
  9. Login rate limit: 6 wrong passwords from one IP returns 429 on the 6th.
  10. Prowlarr is NOT internet-reachable: curl http://<host>:9696 from another machine gets connection refused.

Project layout

src/
  shared/types.ts             — types shared by server + web
  server/
    env.ts                    — zod-validated env
    db.ts                     — SQLite (WAL), migration runner, backup() helper
    auth.ts                   — secure-session, login + rate limit
    routes/                   — auth, profiles, search, media, torrents,
                                 stream, subtitles, watch, settings
    services/
      tmdb.ts                 — TMDB v3 client w/ rate limit + retry
      prowlarr.ts             — indexer aggregator client
      torrent-ranker.ts       — score torrents by seeders/quality/codec
      torrent-engine.ts       — WebTorrent wrapper, sequential streaming,
                                 manual range responses (no createServer)
      transcoder.ts           — ffprobe + 6-pipeline decision matrix
      transcode-queue.ts      — semaphore for ffmpeg concurrency
      stall-detector.ts       — speed/peer/buffer watchdog (prompts user,
                                 never auto-switches)
      session-cleaner.ts      — reaps zombie streams via heartbeat timeout
      subtitles/              — SubtitleProvider interface, OpenSubtitles,
                                 Subdl, cache-aware fetcher
    repos/                    — prepared-statement wrappers per table
  web/
    main.tsx, App.tsx         — entry + router
    lib/api.ts                — typed fetch client
    lib/profile-store.ts      — Zustand store for current profile
    components/               — AppShell, PasswordGate, PosterGrid,
                                 FeaturedTile, EpisodeList, SourcePicker,
                                 VideoPlayer (sendBeacon teardown), etc.
    pages/                    — Login, ProfilePicker, Home, Search,
                                 Movie, Show, Player, Settings
    styles/globals.css        — Tailwind base + reduced-motion
migrations/0001_init.sql      — schema
docker-compose.yml            — app + prowlarr
Dockerfile                    — multi-stage (build deps for better-sqlite3
                                 in build stage; ffmpeg + Node in runtime)

Out of scope for v1

Tracked in the implementation spec. Highlights: cache+evict (currently stream-then-delete), private-tracker seeding, Sonarr/Radarr automation, Chromecast/AirPlay, native mobile apps, per-profile passwords, Trakt sync.


License

MIT License. See also DISCLAIMER.md for what this project is and isn't intended for.

About

Self-hosted media streaming web app

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages