Track format.tags.title and format.tags.comment on media_items via a new
containerTitle() helper producing "Name (Year)" for movies and
"Series (Year) - S01E02 - Title" for episodes. Analyzer and
recomputePlanAfterToggle now flag non-canonical container title and
non-empty comment as non-noop ("Fix container title", "Clear comment"),
and verifyDesiredState checks them post-ffmpeg. buildStreamFlags writes
the canonical title and clears comment on every run.
Existing libraries need a rescan to populate the new columns.
The button label said "← Stop" because backward-slot buttons normally
use a left arrow. Stopping isn't a backward action — use ■ instead so
the symbol matches the verb.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The /help and /paths nav links felt out of place — they're not workflow
destinations. Drop both routes and bring their content into the Settings
page where it belongs.
- AboutSection at the top: short, factual copy about what netfelix
actually does (strip non-OG audio, extract subtitles, rename audio
tracks, ask *arr to rename the file). Mentions the dub-curator path
via the existing Audio Languages list.
- PathsSection in the left column under Schedule: same status table as
the old /paths page, no behavior change.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three threads:
1. Drop the "⚡ Auto-approve" / "✋ Needs decision" pills on PipelineCard
and the "N auto · M need decisions" subtitle on ReviewColumn — noise
for a workflow that wants to be unattended. Card amber tint stays as
a softer cue. Remove the now-unused reviewManualCount field on the
pipeline payload.
2. New /help route in the nav. Documents what netfelix actually does
end-to-end, the folder/SxxExx/ID brackets we require, and that the
codec/quality/audio brackets are *arr's job — we trigger their rename
API instead of parsing them ourselves. Links to TRaSH guides.
3. Refactor triggerMovieRename / triggerSeriesRename to return a
basename → new-basename map instead of one path. Add a batched
triggerRenameFor in execute.ts that dedupes by movie and by series
(one Sonarr call covers every episode of a series). Hook into
processInbox: when an item becomes noop, fire a rename pass so
lying filenames on already-clean files self-heal. Idempotent —
*arr returns no work to do when names already match.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The toggle's disable branch only wrote the config, leaving runSequential
draining indefinitely — its loop only checked signal.aborted, and the
drain re-fetch at the bottom kept pulling new pending jobs.
Add stopQueueProcessor() (mirrors stopAutoProcessLoop for the inbox
sorter) and call it from the disable branch. Current ffmpeg job runs
to completion; no new jobs start. Use POST /stop for a hard kill.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After an FFmpeg job completes, send RescanMovie/RescanSeries followed by
RenameFiles to the appropriate *arr service so the filename reflects the
new codec/stream info. Updates media_items.file_path with the new name so
subsequent operations target the right file.
Path matching uses basename only since *arr services may see different
absolute paths than us due to Docker volume mappings. The directory part
of the path stays the same during a rename.
Non-blocking — rename failures only log a warning, never affect job status.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Episodes without a title in the filename (e.g. "Lost - S02E21 - [DSNP WEBDL-1080p]")
had their name set to "[DSNP WEBDL-1080p]" because the non-greedy (.+?) still matched
into the bracket. Changed capture group to ([^\[]+?) so it refuses to match "[", falling
back to the SxxExx identifier instead.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Items previously marked is_noop=1 by the old analyzer (before the title
check) have reasons=NULL. processInbox now reanalyzes these once — if the
title check flips them to non-noop, they enter the pipeline and get their
titles fixed by ffmpeg. One-time catch-up: subsequent runs skip them
because reasons is populated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Analyzer now computes structured reason tags (Remove tracks, Reorder,
Extract subs, Transcode, Fix default, Fix language tag, Fix title) and
stores them as JSON in review_plans.reasons. Pipeline cards show these
as badges next to the copy/transcode pill so users know why a file
needs processing. Replaces the old transcode_reasons computed from
stream_decisions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Every kept audio track must have our canonical title format (e.g.
"ENG - AAC · 5.1"). Files with missing or wrong titles are not noop.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- analyzer: files with non-harmonized audio titles (e.g. "Chinese - Dolby
Digital - 5.1" instead of "ZHO - EAC3 · 5.1") are no longer marked as
desired-state noop. Only fires when a title already exists — null titles
are left alone to avoid processing every file just to add titles.
- export trackTitle from ffmpeg.ts so analyzer can compute expected title
- settings: unchecking auto-processing now calls stopProcessInbox() to
abort the running inbox processor
- 3 new analyzer tests for title mismatch
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Single-item "← Back to inbox" (reopen, unapprove) now re-probes the file
via ffprobe and re-upserts media_items + media_streams. Covers cases where
the original scan errored or the file was replaced on disk. Bulk operations
(unsort-all, reopen-all) skip the rescan to avoid hammering ffprobe.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All three processInbox callers (manual button, auto-processing toggle,
post-scan auto-process) now go through startProcessInbox() which manages
the shared abort controller. Previously only the manual button set the
abort controller, so Stop Sorting had no effect when processing was
triggered from the settings toggle or after scan completion.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- ColumnShell: new sort row below header border with sortOptions/sortValue/onSortChange
- inbox: ↓↑ scan time, ↓↑ name (dropdown moved from button row to sort row)
- review: classification (default), ↓↑ scan time, ↓↑ name
- queue/done: ↓↑ added, ↓↑ name (client-side sort on already-fetched arrays)
- auto-process checkbox stays visible during inbox processing, progress shows below it
- backend: unified GroupSort type replaces InboxSort, review bucket accepts sort param
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- sort state lifted to PipelinePage so loadGroups includes the sort param
on every reload (scan SSE events no longer reset the sort)
- sort dropdown moved from subtitle to ColumnShell middle slot (left of
Process Inbox button)
- ColumnShell.skip renamed to middle, accepts ReactNode or ColumnAction
- per-item "Process →" button on inbox movie cards and series cards:
POST /:id/process resolves language + reanalyzes + sorts a single item
- dashboard stat pills refresh during scan (every 25 items)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
On resume, the scan filters out files already marked 'scanned' in the DB
so only unscanned files are probed. Also clears the scan_running flag on
DB init so a container killed mid-scan doesn't permanently block new scans.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The one-shot backfill for the inbox rollout ran on every startup,
setting sorted=1 on rows with sorted=0 AND auto_class IS NULL —
which is exactly the state of freshly scanned items. After the
drop-jellyfin migration wipes all tables, the backfill is obsolete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
all with title hover text for discoverability. saves horizontal space
so buttons never overflow their card boundaries.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
the button floats top-left on hover without displacing the action row
buttons (rescan, approve series) which stay in fixed positions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
previously stop only killed the running ffmpeg process — the loop
immediately picked up the next pending job. now the abort signal
breaks the loop between jobs, so remaining items stay in the queue
column as pending jobs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
the Dead Zone case: OG is Japanese mono, English 5.1 exists but is removed
by config (audio_languages=[]). the previous check only looked at kept
streams, so it never fired. now compares OG channels against all non-OG
audio in the file — if a superior dub exists, flag for review regardless
of whether it's currently configured to be kept.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
RescanConfig now only carries audioLanguages. Radarr/Sonarr library
loading, language resolution, and resolveSeriesTvdb callback removed
from rescan.ts, scan.ts, and webhook.ts. RescanResult no longer tracks
radarrHit/sonarrHit/missingProviderId counters. Tests updated: removed
authoritative-source and resolved-TVDB-enables-Sonarr tests (moving to
processInbox in a later task), added assertion that scan never sets
sonarr/radarr as language source.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
episodes without SeriesProviderIds.Tvdb (older Jellyfin, un-refreshed items)
fell back to the episode-level TVDB ID, which never matched Sonarr's
series-keyed library. add resolveSeriesTvdb callback that fetches the series
item from Jellyfin to get the correct series TVDB ID, with per-scan caching.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
escape hatch for items the audio pipeline can't usefully fix — e.g. a
release whose only audio track is English commentary and needs to be
purged so *arr can find a better one. two buttons on the audio detail
page:
🗑 Delete file — unlinks the file and drops our DB rows
(cascades streams, plans, decisions, jobs)
🗑 Delete & refetch — same, then asks Radarr/Sonarr to rescan
(to notice the deleted file) and trigger
an indexer search for a replacement
backend: POST /api/review/:id/delete { refetch }. the refetch step is
best-effort and its result ships under `refetch` on the response so the
UI can surface partial wins — file deleted + db clean even if Radarr
doesn't have the movie. helpers live on the existing *arr service
modules (triggerMovieRefetch, triggerEpisodeRefetch) and do the command
lookups + POST /api/v3/command calls themselves.
UI uses native confirm dialogs showing the file path. on success,
navigates back to /pipeline since the detail page points at a row that
no longer exists.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
the separate Library page was mostly duplicating what the pipeline
columns + the item detail page already show. move its two useful bits —
the stats row (total / scanned / needs action / no change / approved /
done / errors) and the scan control bar — into a compact two-row header
above the pipeline columns, drop the library items table entirely, and
redirect "/" to "/pipeline".
the scan SSE buffering logic moves verbatim into PipelineHeader.tsx so
the progress bar and the stats refresh on completion keep working. the
dead /api/scan/items endpoint and its parseScanItemsQuery +
buildScanItemsWhere helpers (plus their tests) go away with the UI;
/api/scan, /api/scan/start, /api/scan/stop, /api/scan/events stay.
nav loses "Library" — Pipeline is the only entry point now.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
adds stream_decisions.custom_language (ISO 639-2 code or null) so the
user can correct a mislabeled audio track — e.g. a Spanish dub tagged
"und" in the container — without going through Jellyfin. the override
wins over stream.language everywhere it matters: the analyzer reads it
for keep/remove decisions and track ordering, the ffmpeg command builder
writes it as both the language metadata tag and the harmonized track
title, and reanalyze preserves it across reruns and rescans.
on the audio detail page, each pending audio row swaps its language
cell for an inline <select> populated from LANG_NAMES. picking the raw
file language clears the override; anything else sets it and triggers a
server-side reanalyze so keep/remove + target_index update immediately.
a small ✎ hint marks overridden tracks. rebuilt commands tag the output
accordingly so Jellyfin reads the corrected language.
PATCH /api/review/:id/stream/:streamId/language validates the code
against LANG_NAMES (accepts ISO 639-1/2/2B aliases, rejects garbage)
and runs reanalyze inside.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
both auto-process toggles (Inbox, Queue) used a fully-controlled checkbox
whose checked prop was driven by data.autoProcessing / autoProcessQueue.
that made clicks feel frozen on anything slower than localhost: react
reconciles the DOM back to the pre-click value between onChange firing
and setData landing after loadAll, so a click could look like it snapped
back before the server answered. uncheck in particular showed up as
"can't turn it off" when the queue was idle.
mirror the prop in a local useState + sync via useEffect so the box
flips on click and the server value reconciles on the next pipeline
refresh. matches the pattern SettingsPage already uses for the same
toggle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
subtitle management (list/detail pages, /api/subtitles, subtitle_files
table, SubtitleFile types, predictExtractedFiles, nav link) moved to a
new sibling project at ~/Developer/netfelix-subtitles-manager/ where
it'll be rebuilt standalone later. this project now owns audio fixing
+ subtitle extraction only.
extraction still runs end to end: analyzeItem still marks every subtitle
stream as "remove from container", buildExtractionOutputs still wires
the -map 0:s:N + sidecar outputs into the ffmpeg command, and execute.ts
still flips review_plans.subs_extracted so verify.ts can check desired
state — just derived from the streams directly instead of by writing a
row per file to the now-gone subtitle_files table.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
column headers are now a fixed three-row layout (title / subtitle / button
row). every column always reserves all three rows so headers line up
regardless of contents; actions render disabled when their column is
empty instead of disappearing, which keeps the header height stable as
state changes.
the processing column gets a new "Auto-process Queue" checkbox that
mirrors the inbox's "Auto-process Inbox" toggle. backend adds an
auto_process_queue config, a maybeStartQueueProcessor() helper, a
POST /api/settings/auto-process-queue endpoint, and a hook in
enqueueAudioJob so approvals drain the queue hands-off when the toggle
is on.
reopen-all and per-item reopen now send items to the Inbox (sorted=0)
instead of back to Review. the done column's label and tooltip become
"← Back to inbox" to match, and the clear button moves to the right
slot so the header pattern (left=backward, right=forward) stays
consistent across columns.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sortInbox is now async, yields every 10 items, and emits inbox_sort_start
+ inbox_sort_progress via optional hooks. the pipeline route handler
wires those hooks to the existing job events stream and guards against
a second concurrent sort with a 409.
the inbox column swaps its Auto Review button for a live "Sorting N/T"
counter and progress bar while the sort is in flight; the auto-process
toggle hides to give the progress the full subtitle line. the previous
behaviour was a frozen button for the entire duration of the sort.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>