- 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>
- 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>
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>
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>
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>
- 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>
- execute/clear now also resets sorted=0 so cleared items land back in the
inbox where the distributor can re-classify them; previously they got
stranded in Review with auto_class='auto', unreachable by both
"Approve all ready" and "Auto Review"
- pipelinecard: action row moves to the top so Skip/Approve/Back-to-review
sit in the same place regardless of card body height; title row follows;
file info (copy/transcode reasons) gets a dedicated row with the
ready/needs-decision badge pushed to the right
- tests: clearQueue preserves running/completed jobs, only pending plans
flip back to sorted=0 pending
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The scalar subquery I added in 7d30e6c ran one aggregate scan of
media_streams per row. On a real library (33k items / 212k streams)
a single page took 500+ seconds synchronously, blocking the event
loop and timing out every other request — Library AND Pipeline both
stopped loading.
Swap it for a single batched `GROUP_CONCAT ... WHERE item_id IN (?...)`
query over the current page's ids (max 25), then merge back into rows.
v2026.04.15.10
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per-row audio codec summary (distinct lowercased codecs across an
item's audio streams) via scalar subquery on media_streams, rendered
as "ac3 · aac" in a new monospace Audio column.
v2026.04.15.9
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
/api/review/pipeline no longer ships the review array — it now only
carries queue state + reviewItemsTotal. Review items live behind
/api/review/groups?offset=N&limit=25 which returns complete series
(every pending non-noop episode, bucketed by season) so the UI never
sees a split group.
Lifted enrichWithStreamsAndReasons + PipelineAudioStream to module
scope so both /pipeline (queued column) and /groups (review page)
can share the same enrichment.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GET /api/settings now returns jellyfin_api_key, radarr_api_key,
sonarr_api_key, mqtt_password as "***" when set (empty string when
unset). Real values only reach the client via an explicit
GET /api/settings/reveal?key=<key> call, wired to an eye icon on
each secret input in the Settings page.
Save endpoints treat an incoming "***" as a sentinel meaning "user
didn't touch this field, keep stored value", so saving without
revealing preserves the existing secret.
Addresses audit finding #3 (settings endpoint leaks secrets).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Queue previously processed a snapshot of pending jobs — anything approved
after Run-all clicked sat idle until the user clicked again. Now, when
the local queue drains, re-poll the DB once for newly-approved jobs
before exiting.
Also swap the looser local parseId (Number.parseInt accepted '42abc')
for the strict shared parseId in server/lib/validate.ts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>