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>
- delete server/services/jellyfin.ts, webhook.ts, mqtt.ts and their tests
- strip jellyfin/mqtt imports and startup calls from index.tsx and settings.ts
- remove /jellyfin, /mqtt, /mqtt/status, /mqtt/test, /jellyfin/webhook-plugin endpoints from settings router
- clean ENV_MAP and isEnvConfigured of jellyfin/mqtt keys
- add db/index.ts migrations for series_key, duration_seconds, scan_status, scan_error, last_scanned_at (new columns absent on older dev DBs)
- move idx_media_items_series_key out of SCHEMA into migrate() so it runs after the column is added
- fix all test fixtures: drop jellyfin_id/series_jellyfin_id column refs, update MediaItem/MediaStream object literals to match current types
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
remove resolveSeriesTvdb from LanguageResolverConfig, rename jellyfinFallback
to probeFallback, replace Jellyfin-based TVDB resolution with series_name
title search over Sonarr library, update tests accordingly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move language normalization out of jellyfin.ts into its own module so
non-Jellyfin services (ffmpeg, radarr, sonarr, analyzer) no longer
depend on the Jellyfin service file. jellyfin.ts re-exports
normalizeLanguage for backward compatibility.
Co-Authored-By: Claude Sonnet 4.6 <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>
every "← Back to inbox" button (review, queue, done, per-item unapprove,
per-item reopen) now does the same thing: reset plan to pending/unsorted,
clear auto_class, delete non-running jobs. previously unapprove left
sorted=1 (sent to review instead of inbox) and each column had its own
SQL. now consistent: back to inbox always means "needs re-processing."
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
when the original language track has fewer channels than a kept non-OG
track (e.g. Japanese mono vs English 5.1), classify as auto_heuristic
instead of auto so the user can decide whether to prefer the dub.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
upsertJellyfinItem no longer runs analyzeItem or creates stream_decisions.
it inserts a minimal review_plans stub (pending, unsorted). all analysis
happens in processInbox. this means after scan, ALL items land in the
inbox — the "needs action" count equals the inbox count until processing
classifies them.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
scan emits pipeline_changed every 25 items, rescan endpoints emit on
completion. pipeline page listens and throttle-reloads all column data
(1s debounce) so inbox fills progressively without manual navigation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
processing runs in background, items appear in columns progressively via
SSE-triggered reloads. stop button aborts mid-run, remaining items stay in
inbox. remove skip/skip-all from inbox. fix column header height jitter by
giving subtitle slot a fixed height.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
the resolver was calling sonarrLang with episode-level TVDB IDs first,
missing the library, triggering an HTTP round-trip per episode. now checks
the library and resolves the correct series TVDB before calling sonarrLang.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Per-item rescan now resets plan to inbox (sorted=0, auto_class=NULL),
clears external_raw, deletes pending jobs, then lets processInbox
handle language resolution + analysis when auto_processing is on
- Add getSeriesEpisodes() to jellyfin service for bulk episode discovery
- Add POST /api/review/rescan-series endpoint accepting seriesJellyfinId
+ optional seasonNumber — discovers new episodes, resets all matching
items to inbox, refreshes streams from Jellyfin, auto-processes if on
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>
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>
sortInbox used to distribute plans by the auto_class and stream_decisions
captured at scan time, which meant toggling an audio_languages entry and
then running "back to inbox" + "auto review" re-queued the item with the
stale decisions. now sortInbox re-runs the analyzer per plan against the
current audio_languages before distributing, matching the user's mental
model that auto review = re-apply the rules.
reanalyze() takes audioLanguages explicitly so callers can pass it in
once and tests can drive it without reaching into the singleton db.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>