- 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)
86 lines
4.2 KiB
Markdown
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.
|