commit a5c6fd5fe481028c7da8edf6d41412f6682cd985 Author: Jos Vooges | STH Date: Tue Aug 25 00:16:42 2026 +0200 Initial commit: Distributed Stremio Media Cluster V1 Master API, Admin UI, Go media-node, Docker/Dokploy deploy configs. Co-authored-by: Cursor diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cd51654 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +node_modules/ +dist/ +.next/ +.env +.env.local +*.log +.DS_Store +coverage/ +.turbo/ +*.db +*.db-journal +*.tsbuildinfo +node/media-node/media-node +node/media-node/media-node.exe +apps/admin-ui/.next/ +apps/master-api/dist/ +packages/*/dist/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..ff7c363 --- /dev/null +++ b/README.md @@ -0,0 +1,128 @@ +# Distributed Stremio Media Cluster + +Privé gedistribueerd mediaplatform voor **Stremio**: één Master, onbeperkt Media Nodes, video rechtstreeks van node naar player. + +## Architectuur + +``` +Stremio ──HTTPS──► Master (catalog / meta / stream) + ▲ + │ WSS control + Media Nodes + │ +Stremio ◄──HTTPS video──┘ (via Nginx Proxy Manager) +``` + +Harde eisen: + +- geen VPN / WireGuard / SMB / NFS tussen locaties +- geen Docker op nodes +- geen transcoding in V1 +- geen raw filesystem paths publiek +- server-side playback sessions (geen simpele 1-uur signed URLs) + +## Stack + +| Component | Techniek | +|-----------|----------| +| Master API | TypeScript, Fastify, Prisma, PostgreSQL, Redis | +| Admin UI | Next.js | +| Media Node | Go binary + SQLite + systemd | +| Proxy | Nginx Proxy Manager (GUI only) | + +## Repository + +``` +apps/master-api/ Master + Stremio addon +apps/admin-ui/ Admin dashboard +node/media-node/ Go media node +packages/ Gedeelde types + protocol +deploy/docker/ Docker Compose Master +deploy/node/ Installer + systemd +docs/ Deployment + NPM setup +``` + +## Snel starten (dev) + +```bash +npx pnpm install + +# Postgres + Redis +cd deploy/docker +docker compose up -d postgres redis + +# Master +cd ../../apps/master-api +cp .env.example .env +# DATABASE_URL=postgresql://media:media_secret@localhost:5432/media_cluster +npx prisma migrate deploy +npx pnpm db:seed +npx pnpm dev + +# Admin UI +cd ../admin-ui +npx pnpm dev +``` + +- API: http://localhost:3000 +- UI: http://localhost:3001 +- Login: `admin@localhost` / `changeme123` + +## Media Node + +Op Linux (productie): + +```bash +cd node/media-node +GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o media-node ./cmd/media-node +sudo ./media-node install +``` + +CLI: + +``` +media-node install|register|run|scan|diagnostics|config|start|stop|restart|status|version +``` + +## Productie + +Zie: + +- [docs/deployment.md](docs/deployment.md) +- [docs/dokploy.md](docs/dokploy.md) — **Master op Dokploy (aanbevolen)** +- [docs/npm-setup.md](docs/npm-setup.md) + +```bash +cd deploy/docker +cp ../master/.env.example ../master/.env +docker compose up -d --build +``` + +Of in Dokploy: Compose-app met pad `deploy/docker/docker-compose.dokploy.yml`. + +## Stremio installeren + +1. Admin UI → **Instellingen** → addon token +2. URL in Stremio: + `https://master.example.com/stremio//manifest.json` + +## V1 features + +- Node enrollment + unieke credentials +- WebSocket control channel + heartbeat + reconnect +- Movies/Series scanner + library sync +- Filename parsing + TMDB matching (optioneel) +- Stremio catalogs / meta / streams +- Playback sessions met idle + absolute expiry +- HTTP Range streaming (direct play) +- Offline node detectie + duplicate media +- Admin: dashboard, nodes, library, streams, matching ignore/refresh +- Rate limits, security headers, redacted token logs +- Backup script + release build script + +## Tests + +```bash +cd apps/master-api && npx pnpm test +cd node/media-node && go test ./... +``` diff --git a/apps/admin-ui/next-env.d.ts b/apps/admin-ui/next-env.d.ts new file mode 100644 index 0000000..830fb59 --- /dev/null +++ b/apps/admin-ui/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/admin-ui/next.config.js b/apps/admin-ui/next.config.js new file mode 100644 index 0000000..66e1074 --- /dev/null +++ b/apps/admin-ui/next.config.js @@ -0,0 +1,16 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + // standalone only needed for Docker; skip on Windows to avoid symlink EPERM + output: process.env.DOCKER_BUILD === "1" ? "standalone" : undefined, + async rewrites() { + const apiUrl = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3000"; + return [ + { + source: "/api/:path*", + destination: `${apiUrl}/api/:path*`, + }, + ]; + }, +}; + +module.exports = nextConfig; diff --git a/apps/admin-ui/package.json b/apps/admin-ui/package.json new file mode 100644 index 0000000..01e6522 --- /dev/null +++ b/apps/admin-ui/package.json @@ -0,0 +1,21 @@ +{ + "name": "@media-cluster/admin-ui", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "next dev -p 3001", + "build": "next build", + "start": "next start -p 3001" + }, + "dependencies": { + "next": "^15.1.6", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^22.10.7", + "@types/react": "^19.0.7", + "@types/react-dom": "^19.0.3", + "typescript": "^5.7.3" + } +} diff --git a/apps/admin-ui/public/.gitkeep b/apps/admin-ui/public/.gitkeep new file mode 100644 index 0000000..c0a8ea4 --- /dev/null +++ b/apps/admin-ui/public/.gitkeep @@ -0,0 +1 @@ +# Keep this file so Next.js public assets directory exists diff --git a/apps/admin-ui/src/app/dashboard/page.tsx b/apps/admin-ui/src/app/dashboard/page.tsx new file mode 100644 index 0000000..c868f19 --- /dev/null +++ b/apps/admin-ui/src/app/dashboard/page.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Nav, useAuth } from "@/components/Nav"; + +interface Dashboard { + nodesOnline: number; + nodesOffline: number; + totalStorage: string; + freeStorage: string; + movies: number; + series: number; + episodes: number; + activeStreams: number; + currentBandwidth: string; +} + +function formatBytes(bytes: string): string { + const n = BigInt(bytes); + const units = ["B", "KB", "MB", "GB", "TB"]; + let val = Number(n); + let i = 0; + while (val >= 1024 && i < units.length - 1) { + val /= 1024; + i++; + } + return `${val.toFixed(1)} ${units[i]}`; +} + +export default function DashboardPage() { + useAuth(); + const [data, setData] = useState(null); + + useEffect(() => { + fetch("/api/v1/admin/dashboard", { credentials: "include" }) + .then((r) => r.json()) + .then(setData) + .catch(console.error); + }, []); + + return ( + <> +