Files
netfelix-audio-fix/.claude/memory/MEMORY.md
Felix Förtsch 93ed0ac33c fix analyzer + api boundary + perf + scheduler hardening
- analyzer: rewrite checkAudioOrderChanged to compare actual output order, unify assignTargetOrder with a shared sortKeptStreams util in ffmpeg builder
- review: recompute is_noop via full audio removed/reordered/transcode/subs check on toggle, preserve custom_title across rescan by matching (type,lang,stream_index,title), batch pipeline transcode-reasons query to avoid N+1
- validate: add lib/validate.ts with parseId + isOneOf helpers; replace bare Number(c.req.param('id')) with 400 on invalid ids across review/subtitles
- scan: atomic CAS on scan_running config to prevent concurrent scans
- subtitles: path-traversal guard — only unlink sidecars within the media item's directory; log-and-orphan DB entries pointing outside
- schedule: include end minute in window (<= vs <)
- db: add indexes on review_plans(status,is_noop), stream_decisions(plan_id), media_items(series_jellyfin_id,series_name,type), media_streams(item_id,type), subtitle_files(item_id), jobs(status,item_id)
2026-04-13 07:31:48 +02:00

4.2 KiB

netfelix-audio-fix — Project Memory

What it does

Bun + Hono JSON REST API + React 19 SPA to scan a Jellyfin library, compute which audio/subtitle tracks to remove from each file, let you review and edit those decisions, then execute FFmpeg (copy mode) to strip/reorder streams. Remote nodes via SSH.

Key technical decisions

  • Runtime: Bun + Hono JSON REST API backend; React 19 SPA frontend via Vite
  • DB: bun:sqlite WAL mode → data/netfelix.db (server-side; PGlite not applicable)
  • Frontend stack: React 19 + TanStack Router + Zustand + Tailwind v4 + cn() utilities
  • Code quality: Biome (formatting + linting) — no ESLint/Prettier
  • Path alias: ~/src/ (vite.config.ts resolve.alias + tsconfig.json paths)
  • Dev: bun run dev runs Hono API (:3000) + Vite (:5173) concurrently; Vite proxies /api/*
  • Prod: bun run build → Vite builds to dist/; Hono serves dist/ + /api/* routes
  • SSH keys: uploaded via file input, stored as PEM text in nodes.private_key
  • Two tsconfigs: tsconfig.json (frontend, DOM lib), tsconfig.server.json (backend, bun-types)

Project structure

server/                   ← Backend (Bun + Hono, JSON API at /api/*)
  index.tsx               ← entry point, Bun.serve, CORS for dev, static SPA serve
  types.ts                ← server-side interfaces
  db/index.ts             ← getDb(), getConfig(), setConfig(), getEnvLockedKeys()
  db/schema.ts            ← SCHEMA DDL string + DEFAULT_CONFIG
  services/
    jellyfin.ts / radarr.ts / sonarr.ts / analyzer.ts / ffmpeg.ts / ssh.ts
  api/
    dashboard.ts / scan.ts / review.ts / execute.ts / nodes.ts / setup.ts / subtitles.ts
src/                      ← Frontend (React SPA, built with Vite)
  main.tsx                ← entry, RouterProvider
  index.css               ← Tailwind v4 @import
  routeTree.gen.ts        ← auto-generated by TanStack Router (gitignored)
  routes/
    __root.tsx            ← nav layout with Link components
    index.tsx / scan.tsx / execute.tsx / nodes.tsx / setup.tsx
    review.tsx            ← layout route with Audio/Subtitles tab bar + Outlet
    review/index.tsx (redirect → /review/audio)
    review/audio/index.tsx ($filter) / review/audio/$id.tsx
    review/subtitles/index.tsx ($filter) / review/subtitles/$id.tsx
  features/
    dashboard/DashboardPage.tsx
    scan/ScanPage.tsx (SSE for live progress)
    review/AudioListPage.tsx / AudioDetailPage.tsx
    subtitles/SubtitleListPage.tsx / SubtitleDetailPage.tsx
    execute/ExecutePage.tsx (SSE for job updates)
    nodes/NodesPage.tsx
    setup/SetupPage.tsx
  shared/
    lib/utils.ts (cn()) / api.ts (typed fetch) / types.ts / lang.ts (LANG_NAMES)
    components/ui/badge.tsx / button.tsx / input.tsx / select.tsx / textarea.tsx / alert.tsx
biome.json / vite.config.ts / tsconfig.json / tsconfig.server.json / index.html

Rules: what gets kept

  • Video/Data streams: always keep
  • Audio: keep original_language + configured audio_languages (if OG unknown → keep all, flag needs_review)
  • Audio order: OG first, then additional languages in audio_languages config order
  • Subtitles: ALL removed from container, extracted to sidecar files on disk
  • subtitle_files table tracks extracted sidecar files (file manager UI)
  • review_plans.subs_extracted flag tracks extraction status
  • is_noop only considers audio changes (subtitle extraction is implicit)

Scan flow

  1. Jellyfin paginated API → upsert media_items + media_streams
  2. Cross-check with Radarr (movies) / Sonarr (episodes) for language
  3. analyzeItem() → upsert review_plans + stream_decisions
  4. SSE events stream progress to browser (React EventSource in ScanPage)

Running locally

mise exec bun -- bun run dev   # concurrent: Hono API :3000 + Vite :5173
mise exec bun -- bun run build # build frontend to dist/
mise exec bun -- bun start     # production: Hono serves dist/ + API on :3000

Workflow rules

  • Always bump version in package.json before committing/pushing. CalVer format: YYYY.MM.DD (append .N suffix for same-day releases).

Docker deploy (Unraid)

docker compose up -d   # port 3000, data volume at ./data/

Note: Docker must serve the built dist/ — run bun run build before building the Docker image.