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

86 lines
4.2 KiB
Markdown

# 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
```fish
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)
```fish
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.