- TypeScript 98.2%
- Shell 0.6%
- JavaScript 0.6%
- CSS 0.5%
|
|
||
|---|---|---|
| .forgejo/workflows | ||
| .superpowers/sdd/2026-09-11-idea-211-implementatieplan | ||
| deploy | ||
| docs | ||
| exports | ||
| prisma | ||
| public | ||
| reviews | ||
| scripts | ||
| src | ||
| vendor | ||
| .dockerignore | ||
| .DS_Store | ||
| .env.example | ||
| .gitignore | ||
| .gitmodules | ||
| .sops.yaml | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| components.json | ||
| Dockerfile | ||
| eslint.config.mjs | ||
| next-env.d.ts | ||
| next.config.ts | ||
| package-lock.json | ||
| package.json | ||
| postcss.config.mjs | ||
| prisma.config.ts | ||
| README.md | ||
| tsconfig.json | ||
Media Organizer
Media Organizer is a Next.js app that keeps a read-only index of the media files on one or more mounts. A separate scan-worker process walks each mount and records folders and files; nothing is ever copied or moved on disk. You browse that index and manage favorites, trash and custom collections. Deleting trashed files for good — including the contents of a trashed folder, whose then-empty directories are removed with a non-recursive rmdir — is the single exception to the read-only rule: it is carried out by the short-lived delete-worker, the only component with read-write binds, run on demand by a systemd timer — see the disk-delete runbook and the folder-trash runbook. Mounts are added under /mounts and need an accepted identity before their first scan.
The scan core lives in src/lib/mount-index/; the long-running background processes are scan:worker (indexing) and the optional video:worker (playback derivatives); delete-worker is short-lived and never started by up. Every /api/* route handler checks the session itself.
Docker Compose
Create a local environment file from the example:
cp deploy/media-organizer.env.example deploy/media-organizer.env
Set POSTGRES_PASSWORD, DATABASE_URL, and SESSION_SECRET before the first start.
For an authorized deployment, first complete the release preflight, including schema/backup checks and the local cache directory. The ordinary ops command must invoke the source-owned wrapper:
bash /srv/apps/media-organizer/repo/scripts/deploy-media-organizer.sh
It builds one verified git archive, preserves untracked backups, reads deploy/.env followed by deploy/media-organizer.env, and keeps the managed video flag across redeploys. GNU timeout, flock, Git, tar, find and Docker are required on the host. The wrapper refuses a checkout containing a path not owned by the invoking user (marker repo_ownership_drift on stderr). The only optional argument is --remove-orphans; no free Docker arguments are accepted. Replacing/reloading the live ops allowlist remains a separate authorized action, documented in the runbook.
After the first migration, open the site and fill in the setup screen: with zero accounts every page
redirects to /setup, where you create the single account (password: at least 12 characters). There is
no create-admin script any more.
Forgotten the password? From the web container:
npm run reset-password -- '<nieuw wachtwoord>'
That replaces the hash of the one account and revokes all sessions and paired devices.
The Compose stack publishes the web service on ${WEB_BIND_ADDR:-0.0.0.0}:3107:3000; set WEB_BIND_ADDR in the Compose environment when a host-specific bind address is required. Caddy can proxy that to https://media.jp-visser.nl. Health endpoints are available at /health, /api/health, and /api/ready.
Runtime Paths
MOUNT_ALLOWED_ROOTS lists the absolute container paths a mount may live under (default /mnt/nas/public,/mnt/nas/ssd,/mnt/nas/multimedia,/media/scraper). The Docker Compose stack bind-mounts each of those four paths at the same container path, read-only, in both web and scan-worker — the app never writes to a mount. On macOS development, map a canonical path to a local folder with MOUNT_PATH_ALIASES.
Thumbnails (lean A2)
The grid is served by GET|HEAD /api/media/[fileId]/thumb, which generates 400×400 WebP thumbnails for photos and videos in a short-lived child process and keeps them in a file cache. That cache is the only writable bind of web (/media/thumbnails on the host, /thumbnails in the container, MEDIA_THUMBNAIL_CACHE_ROOT); the mounts stay read-only. Because the bind uses create_host_path: false, web does not start until the host directory exists, and the cache must never lie inside or above a root of MOUNT_ALLOWED_ROOTS (the app answers 503 on such a configuration). Optional knobs MEDIA_THUMBNAIL_CACHE_MAX_BYTES and MEDIA_THUMBNAIL_CONCURRENCY are documented in deploy/media-organizer.env.example. See the A2 deploy runbook for the host preparation and the resource gate.
Video metadata
Every manual or scheduled scan also collects technical metadata for videos that are new, changed, or not analysed yet: after each index batch the scan-worker runs one bounded, read-only ffprobe at a time (ffmpeg is already in the image) and stores container, main stream, codec, resolution, duration, bitrate and versioned JSON with all streams, timing, colour/HDR, rotation and chapters in video_metadata. This does not depend on MEDIA_VIDEO_ENABLED: metadata is collected even when video playback is off. An unchanged next scan reuses ready/partial observations; a source or collector version change invalidates them. The source fingerprint hashes stat fields only — no media bytes are read for it — and nothing on a mount is written. When ffprobe is unavailable the scan defers the remaining candidates instead of failing.
The details popup shows the stored values for a video through the session-checked GET /api/media/[fileId]/metadata; photos and legacy files keep their existing details. For coverage and errors per mount, the read-only report is:
npx tsx scripts/report-video-metadata.ts --mount-id <id> --limit 20
--mount-id is required; --limit defaults to 20 and is capped at 500. The report opens a read-only transaction, reads no media, and creates no work.
Secrets (SOPS + age)
On the scrum4me-srv host the runtime env is not hand-edited. The real values live encrypted in deploy/media-organizer.env.sops (SOPS + age), committed to this repo. At deploy time the host decrypts it to deploy/media-organizer.env, so the .sops file is the single source of truth and the plaintext env is a derived, gitignored artifact.
- Edit or add a secret with
npm run secrets:edit, then commit, push, and redeploy. - View decrypted values with
npm run secrets:view. - Rotate the DB password on the live DB first, then update both
POSTGRES_PASSWORDand the password insideDATABASE_URL.
IDEA-211 streaming en optionele videoworker
Zie release en herstel. Video is standaard uit. Web, migrate en worker
gebruiken hetzelfde expliciete MEDIA_ORGANIZER_IMAGE en SOURCE_COMMIT. De worker
staat in het bestaande deploy/docker-compose.yml onder --profile video;
activering vereist de gecontroleerde beperkte media-organizer.video.env, een vooraf
aangemaakte lokale ext4-cache en alle releasegates. De beheerde herdeploy stopt en
verifieert de worker wanneer de duurzame video-instelling uit staat. Alleen losse
Compose-aanroepen zonder videoprofiel stoppen een al actieve worker niet automatisch. Geen tweede Compose,
automatische deploy of GPU-reservering in web. Nieuwe-image, query≤2s, proxy-, herstel-
en fysieke apparaatresultaten blijven vereist; bronimplementatie is geen release-GO.