Media Organizer
  • TypeScript 98.2%
  • Shell 0.6%
  • JavaScript 0.6%
  • CSS 0.5%
Find a file
Janpeter Visser cc835776d7
Some checks failed
CI / test (historical-bootstrap) (push) Successful in 1m7s
CI / test (suite) (push) Successful in 3m20s
CI / test (video-migration) (push) Successful in 1m4s
CI / docker-build (push) Successful in 2m51s
CI / test (empty-trash) (push) Failing after 11m9s
Merge pull request 'M44 — Vis-design voor de Media-Organizer (PR D)' (#101) from feat/m44-vis-design into main
Reviewed-on: #101
2026-10-04 16:17:25 +02:00
.forgejo/workflows test: verify folder trash lifecycle and preserve partial restore outcomes 2026-09-28 02:08:30 +02:00
.superpowers/sdd/2026-09-11-idea-211-implementatieplan fix(viewer): pas werkzame video-offsets toe 2026-09-12 09:48:42 +02:00
deploy fix(disk-delete): validate deploy identity before timer activation 2026-09-21 22:23:51 +02:00
docs docs(T-236): stylingdocument voor het Vis-thema en verwijzing in CLAUDE.md 2026-10-04 15:20:20 +02:00
exports feat: add MEDIA_THUMBNAIL_HOST_PATH and update metadata in environment configuration 2026-05-23 14:42:17 +02:00
prisma feat: add folder trash manifest schema with legacy worker compatibility 2026-09-27 22:40:04 +02:00
public feat: use media organizer icon set 2026-05-22 20:05:35 +02:00
reviews docs: move media organizer planning docs into repo 2026-05-21 21:35:25 +02:00
scripts feat(folder-trash): remove empty directories with durable cleanup binding 2026-09-28 00:36:52 +02:00
src feat(T-235): themakleur, routecontrole en screenshots increment 1 2026-10-04 15:19:38 +02:00
vendor build: vendor scrum4me-copilot kit + webpack build plumbing 2026-06-18 18:50:48 +02:00
.dockerignore ops(media): borg streaming en videoworker met releasegates en herstelpad 2026-09-12 14:48:19 +02:00
.DS_Store feat: add MEDIA_THUMBNAIL_HOST_PATH and update metadata in environment configuration 2026-05-23 14:42:17 +02:00
.env.example chore(deploy): env en compose opgeschoond, /media/scraper read-only, runbook lean A1 2026-09-19 23:42:22 +02:00
.gitignore ops(media): borg streaming en videoworker met releasegates en herstelpad 2026-09-12 14:48:19 +02:00
.gitmodules build: vendor scrum4me-copilot kit + webpack build plumbing 2026-06-18 18:50:48 +02:00
.sops.yaml secrets: SOPS+age encrypted env for media-organizer 2026-05-21 21:12:37 +02:00
AGENTS.md docs: Scrum4Me-product-binding (methodiek + product_id) 2026-05-29 09:05:44 +02:00
CLAUDE.md docs(T-236): stylingdocument voor het Vis-thema en verwijzing in CLAUDE.md 2026-10-04 15:20:20 +02:00
components.json Rebuild media organizer as library app 2026-06-05 16:40:41 +02:00
Dockerfile ops(media): borg streaming en videoworker met releasegates en herstelpad 2026-09-12 14:48:19 +02:00
eslint.config.mjs feat: expose folder trash and restore in the library 2026-09-28 01:52:52 +02:00
next-env.d.ts chore: scaffold media organizer app 2026-05-20 19:56:18 +02:00
next.config.ts build: vendor scrum4me-copilot kit + webpack build plumbing 2026-06-18 18:50:48 +02:00
package-lock.json fix: bind viewer resume and disabled playback to active context 2026-09-12 16:22:19 +02:00
package.json package.json bijwerken 2026-09-27 17:23:19 +02:00
postcss.config.mjs chore: scaffold media organizer app 2026-05-20 19:56:18 +02:00
prisma.config.ts Rebuild media organizer as library app 2026-06-05 16:40:41 +02:00
README.md docs(audit): mapprullenbak in mountbeleid en migratie-overzicht (PR #95) 2026-09-28 07:41:26 +02:00
tsconfig.json build: vendor scrum4me-copilot kit + webpack build plumbing 2026-06-18 18:50:48 +02:00

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_PASSWORD and the password inside DATABASE_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.