# Changelog All notable changes to Shrinkerr are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.9.162] — 2026-11-04 ### Fixed - **MP4s with text subtitles convert when a sidecar subtitle is merged**: with "Merge external subtitles into video" on, a file with MP4 text subtitles (mov_text) beside a sidecar such as `.eng.srt` failed with "Subtitle codec mov_text is not supported" — the merge's catch-all subtitle copy overrode converting the MP4's subtitles to SRT. ## [0.9.161] — 2026-11-03 ### Fixed - **Scans keep what they find**: the scan ran as a copy of the server process and shared SQLite's record of its locks; when the server's connections closed mid-scan, the scan's writes were lost — a rescan said it had saved the folder and left the old track lists, so clean-ups failed with "tracks changed" — and with the server busy they could corrupt the database. Scans now start in a fresh process. ## [0.9.160] — 2026-11-02 ### Fixed - **Intel Arc and Flex GPUs**: both images now ship Intel's media driver 25.2.4 instead of the older one from Debian / Ubuntu, which failed to start on Arc and Flex cards. Older Intel GPUs (Broadwell and newer) keep working. ## [0.9.159] — 2026-10-30 ### Security - The live-updates and log connections now need the API key when one is set and password login is off; they were open to anyone who could reach the port. ### Fixed - **An API key no longer locks you out**: with no username and password set, the sign-in page asks for the API key, and changing the key or switching the password login off in Settings keeps your browser signed in. A new key also replaces the old one at once instead of a minute later. ## [0.9.158] — 2026-10-10 ### Fixed - **Originals the trash couldn't take were deleted**: with "move to the trash", an original — a converted file, an ISO or a disc folder — that the trash refused (no trash folder on the share, a cross-device move) was deleted for good. It's kept in the backup folder now. - **Audio / subtitle cleanups ignored "keep originals"**: they only ever trashed or deleted the original. They now keep it in the backup folder like a conversion does, and the job can be undone. - An audio / subtitle cleanup whose original couldn't be removed afterwards was reported as failed, so Retry cleaned the already-cleaned file again; it now succeeds and keeps the original. ## [0.9.157] — 2026-10-08 ### Security - Custom ffmpeg flags can no longer add inputs, outputs or file names (they could overwrite files on the server), and encodes run in an empty scratch folder. - Custom ffmpeg flags and the post-conversion script can only be changed when signed in with the password — not with the API key alone, which is in your download-client scripts. - Settings import runs the same checks as saving in Settings and never imports credentials or auth settings. - A backup restored without signing in with the password no longer brings back its post-conversion script or custom flags. - Renaming only touches files and folders inside your media directories and never renames a media directory itself. - A target resolution or filename suffix can no longer place a converted file in another folder. ### Fixed - The System settings page shows why a save was rejected instead of failing silently. ## [0.9.156] — 2026-10-08 ### Fixed - **Ticking audio/subtitle tracks in the Scanner could save an outdated track list** (e.g. after relabeling a language), queueing the wrong tracks for removal — the panel now saves exactly what it shows. - Shrinkerr refuses to remove every audio track from a file (and the file panel warns when all are ticked), and Cancel now stops audio-cleanup jobs. ## [0.9.155] — 2026-10-08 ### Security - **Fixed two unauthenticated holes: a path traversal that could download any file the app can read (including its database), and a Plex image-proxy flaw that could send the Plex token to another host.** If your instance was reachable from outside your network, rotate your API key/password and Plex token. ### Fixed - Remote workers' audio cleanup kept the tracks marked for removal and dropped the rest. - Setting or detecting a track's language no longer marks the native-language tracks for removal. - Renaming never overwrites an existing file or folder, and the post-conversion audio-tag rename no longer alters titles ("Octopussy" → "OctEAC3sy"). - A job that can't find its own output no longer adopts another job's in-progress file. ## [0.9.154] — 2026-10-08 ### Fixed - **Queued Blu-ray/DVD folders show the title instead of "index.bdmv"**, and the Plex poster shrink no longer re-runs (and re-logs) on every restart for posters Plex can't return smaller. ## [0.9.153] — 2026-10-08 ### Added - **Disc images that can't be read now show up in the Scanner as "Unreadable"** with the reader's error and a workaround, instead of silently never appearing (they're also counted under Corrupt). ## [0.9.152] — 2026-10-08 ### Fixed - **DVD ISOs that never showed up in the Scanner are now picked up** — the main feature's VOBs are read straight out of the ISO (like DVD folders), so discs whose first title is padding or whose UDF directory is damaged no longer fail to probe. ## [0.9.151] — 2026-10-08 ### Fixed - **Plex posters are cached at poster size instead of full resolution** — they averaged ~0.8 MB each (up to 10 MB) and could make up most of the database and every backup; existing ones are shrunk in the background and the space is reclaimed on the next restart. ## [0.9.150] — 2026-10-08 ### Fixed - **The weekly backup no longer freezes Shrinkerr** — zipping a large database ran on the main loop, so pages (e.g. the Queue's Completed/Failed tabs) hung for minutes while it ran. ## [0.9.149] — 2026-10-08 ### Fixed - **A busy database no longer fails a running conversion** — a timed-out progress write is now skipped instead of failing the encode (and orphaning its ffmpeg); write transactions held over 5s are logged with their source to pin down the holder. ## [0.9.148] — 2026-10-04 ### Fixed - **Converted BR-DISK / DVD-R files now get a resolution in their name** (e.g. "Movie (1985) 1080p Bluray … h265"), and widescreen discs (1920×800) are labelled 1080p instead of 720p. ## [0.9.147] — 2026-10-03 ### Fixed - **.mpg/.mpeg, .wmv, .flv, .webm and .asf files are now picked up by the scanner and watcher** (they were silently ignored). **If auto-queue is on, existing files of these types will be queued as they're discovered.** Also, a never-saved "Convert from" setting now means H.264/MPEG-2/MPEG-4/VC-1 everywhere — scans, the watcher and *arr webhooks disagreed. ## [0.9.146] — 2026-10-03 ### Fixed - **Writing a track language could send the original file to the NAS recycle bin.** On some SMB shares, replacing a file in place moves the old one to the recycle bin and then fails; Shrinkerr now never replaces a file in place — it moves the original aside first and restores it if anything fails. **If you saw "Permission denied" language errors before, check your share's recycle bin for the originals.** ## [0.9.145] — 2026-10-03 ### Fixed - **Setting a track language on an MP4 (or other non-MKV) on an SMB/CIFS share no longer fails with "Permission denied"** when the share refuses to replace the file in place; if the file is in use (e.g. playing in Plex) it's left untouched, the language stays pending, and the log says to try again later. ## [0.9.144] — 2026-10-03 ### Fixed - **The log is no longer flooded with `[EXT-SUBS]` lines every few minutes.** Per-file external-subtitle matching detail is now debug-only (the watcher still logs when subtitles actually change), and VobSub pairing no longer makes an extra disk check per file. ## [0.9.143] — 2026-10-03 ### Fixed - **A language that couldn't be written into an MP4/MKV can now be applied with a quick remux.** The "Remux to MKV (apply language)" button only appeared for AVI-type files, so those files could only be fixed by a full conversion; the Scanner's savings label also now shows the selected encoder's settings. ## [0.9.142] — 2026-10-03 ### Changed - **The Conversion Guide in Settings now follows the selected encoder**, with a measured VideoToolbox quality/savings table, QSV and VAAPI guidance, and a correct "Current" summary; VideoToolbox quality help now reflects real-footage tests (q:v 55 ≈ VMAF 92, ~50% smaller). ## [0.9.141] — 2026-10-03 ### Fixed - **Job cards and details show the right encoder settings.** VideoToolbox, QSV and VAAPI jobs (and jobs a node switched to another encoder) were labelled with NVENC's "P6 / CQ 20"; they now show e.g. "q:v 55", "Medium / Q 22" or "QP 22". ## [0.9.140] — 2026-10-03 ### Added - **Running-job cards show encode speed** next to fps (e.g. "150 fps · 6.3x" — how many times faster than real time); audio/subtitle cleanup jobs now show their speed as "85x" instead of mislabelling it as fps. ## [0.9.139] — 2026-10-02 ### Changed - **Faster Queue page with large queues, and smaller downloads everywhere.** Responses are now gzip-compressed (a 5,000-job pending list went from 4.3 MB to 104 KB, the app bundle from 1.5 MB to 410 KB), and the pending list is only re-downloaded when jobs are added or reordered. ## [0.9.138] — 2026-10-02 ### Fixed - **Queue page no longer freezes with thousands of jobs.** Lists now render only the rows on screen (5,000 pending jobs: ~400 page elements instead of ~58,000), the pending list is re-downloaded only when it changes, and the Completed/Failed tabs stopped fetching the entire pending queue; also, browsers now pick up a new Shrinkerr version without a hard refresh. ## [0.9.137] — 2026-09-26 ### Changed - **Queue: the search field now matches the Scanner's** (fixed width, left-aligned), with each tab's Clear/Retry buttons on the same row, right-aligned. ## [0.9.136] — 2026-09-26 ### Fixed - **Queue: the Completed and Failed lists update live again.** Since v0.9.130 new completions only appeared after a page reload; they now slide in within the normal refresh (and right away when you return to the tab) without losing your scroll position. ## [0.9.135] — 2026-09-26 ### Changed - **VMAF now also runs on hardware-decoded jobs** (NVDEC, QSV, VAAPI, VideoToolbox) — the check was always a separate software pass, so the skip was unnecessary. **Hardware-decoded jobs now take a little longer and are subject to your VMAF minimum score.** On a Mac, VMAF needs Homebrew's ffmpeg 9+ (`brew upgrade ffmpeg`). ## [0.9.134] — 2026-09-26 ### Fixed - **The server's own Local node now swaps encoders like remote workers do.** A job tagged for an encoder the host can't run (e.g. NVENC on a Mac or CPU-only box) is encoded with the best available one instead of failing with ffmpeg exit 8; turn off *translate encoder* on the node to leave such jobs for a capable node. ## [0.9.133] — 2026-09-26 ### Added - **Apple VideoToolbox encoder for Macs.** Native macOS installs (not Docker) can pick VideoToolbox in Settings, rules and the queue dialog, with its own quality slider and hardware decode; Mac remote workers advertise it and encode NVENC jobs on it. Also fixes native macOS startup (hardcoded `/app/data`) and quiet-hours encodes on macOS (no `ionice`). ## [0.9.132] — 2026-09-26 ### Added - **The rest of the interface, server messages, and notifications are now translatable (Spanish included).** Every screen, activity/history entries, job errors, API errors and detection notes follow your UI language, and a new Settings → System → Notifications language setting controls outbound Discord/webhook/email text; history recorded before this version stays English. ## [0.9.131] — 2026-09-26 ### Added - **Interface localization, with Spanish as the first translation.** Pick a language under Settings → System → User Interface (English stays the default). The navigation, Dashboard, Queue, and Scanner toolbar/filters are translated; remaining screens and server-generated messages are still English and will follow. ## [0.9.130] — 2026-09-26 ### Added - **Filename search and infinite scroll on the queue tabs.** A search box (server-side, so it covers the full history) filters Pending/Completed/Failed by filename, and Completed/Failed now load more as you scroll instead of being capped — so you can find and page through any past conversion. ## [0.9.129] — 2026-09-26 ### Fixed - **Queue tab switches are fast again and no longer flash "No completed jobs yet" before loading.** A tab switch was blocked by an in-flight poll (deferring the fetch up to 10s), and the Completed/Failed tabs fetched the entire job history unbounded — now a tab change fetches immediately and history tabs load the most recent 200 (counts still reflect the true total). - **The live queue view updates immediately when you return to the tab.** The WebSocket reconnect timer was throttled to ~1 minute while the tab was backgrounded; it now reconnects on refocus and the socket is no longer torn down on every render. ## [0.9.128] — 2026-09-26 ### Fixed - **Shift-selecting episodes across seasons no longer selects the whole show (10+ season shows).** The range used a plain lexicographic sort, so "Season 2" sorted after "Season 10"…"Season 19" and a Season 1 → Season 2 pick spanned every season between them. It now sorts numerically (matching the display) and scopes the range to the clicked episodes' common folder. ## [0.9.127] — 2026-09-22 ### Fixed - **Trashed originals keep their real filename instead of a `..replacing` suffix.** v0.9.126 moved the original to a same-dir sidecar before placing the output, so the trashed copy carried the ugly suffix and had to be renamed by hand to restore. It now stages the original in a hidden subdir keeping its real name, so trash/backup preserve the filename; rollback-on-failure is unchanged. ## [0.9.126] — 2026-09-20 ### Fixed - **A failed output rename no longer trashes the original and strands the output as `.converting`/`.remuxing`.** The finalizer disposed the original before renaming the temp into place, so an EACCES rename (seen on some CIFS/SMB mounts) lost the original. It now places the output first — moving the original aside when it occupies the target name — and only then trashes/deletes it, rolling back so the original is never lost. Applies to both the convert and audio-remux paths. ## [0.9.125] — 2026-09-17 ### Fixed - **The per-rule CQ field now allows up to 40, matching the global CQ slider.** v0.9.123 raised the global/estimate CQ caps but missed the rules form, so a per-directory rule still couldn't set CQ above 30. Its cap is now encoder-aware (40 for NVENC CQ, 28 for libx265 CRF, same as the main sliders). ## [0.9.124] — 2026-09-17 ### Changed - **Relabel the slowest NVENC/libx265 presets "(Best compression)" instead of "(Best quality)".** In Shrinkerr's constant-quality (CQ/CRF) workflow the quality is pinned by CQ, so a slower preset yields a smaller file at the same quality, not a better-looking one — "best quality" was misleading and inconsistent with the p7 speed table. ## [0.9.123] — 2026-09-17 ### Changed - **Quality sliders (NVENC CQ, libx265 CRF, resolution-aware CQ, per-job override) now go up to 40 instead of 30** so you can push more aggressive compression on tolerant content (e.g. reality TV). The encoder always accepted higher; only the UI capped it. ## [0.9.122] — 2026-09-11 ### Fixed - **Disc images always need conversion regardless of codec, across every job-creation path.** v0.9.120/121 forced it at scan time and in Add-to-Queue; the webhook (nzbget/SABnzbd), add-by-path, and watcher auto-queue paths still codec-gated discs, so an HEVC/x265 Blu-ray was flagged cleanup-only. All now force conversion when `disc_type` is set. ## [0.9.121] — 2026-09-11 ### Fixed - **A disc row now shows as a conversion in Add-to-Queue immediately, without a re-scan.** v0.9.120 fixed the scan-time flag, but the queue estimate and add-from-scan classifier still read the stored `needs_conversion` column (stale on rows scanned earlier); both now force conversion from the stored `disc_type`. ## [0.9.120] — 2026-09-11 ### Fixed - **Disc images (ISO/VIDEO_TS/BDMV) now always need conversion instead of offering only audio cleanup.** `needs_conversion` was codec-only, and the codec probed through the bluray:/dvdvideo demuxer is often blank/unmatched, so a raw 56 GB Blu-ray was flagged cleanup-only and required ticking force-encode. Discs are forced to convert at scan time and in the bulk recompute. - **An audio-only cleanup job on a disc now fails with a clear message instead of ffmpeg exit 183** — a disc can't be stream-copy remuxed, so it says to queue a conversion. ## [0.9.119] — 2026-09-06 ### Fixed - **Audio/sub cleanup no longer fails with "source file no longer exists" when an encode is discarded as larger.** When a convert/combined encode grows the file but cleanup is still pending, the cleanup now runs inline in the same job instead of as a deferred follow-up that raced nzbget/Sonarr/Radarr moving the download out from under it. ## [0.9.118] — 2026-09-06 ### Fixed - **Scanner: "Select all" then deselecting one item no longer clears the whole selection.** Deselecting under select-all rebuilt the selection from only the expanded/loaded folders, dropping every unloaded item; it now starts from the full known set (all folder prefixes, or the advanced-search matches) and removes just the one item. ## [0.9.117] — 2026-09-06 ### Fixed - **1080p titles under a "4K"-named folder (or with a UHD edition tag) no longer count as 4K.** The v0.9.116 4K rule matched a 2160p/UHD/4K token anywhere in the path; now an explicit sub-4K tag (1080p/720p/…) in the path overrides a stray 4K token. The count and list filters share one generated predicate so they stay in lockstep. ## [0.9.116] — 2026-09-06 ### Fixed - **The 4K filter now counts scope-ratio 4K titles instead of only 16:9 ones.** 4K was detected by video height (the chip count required ≥2000), so letterboxed 4K (3840×1600, ×1634, …) was miscounted as 1080p — showing ~210 of 800+. 4K is now `height ≥ 1900 OR a 2160p/UHD/4K tag in the path`, applied consistently to the chip count and the list filters. ## [0.9.115] — 2026-09-05 ### Fixed - **A silent NVDEC green-screen decode failure no longer produces a kept-but-garbage output.** NVDEC can choke on certain x264 Blu-ray streams and emit green/blank frames with no ffmpeg error (exit 0), which NVENC compressed to a near-empty video track that the full-length duration guard then rescued. The encode now detects an implausibly-tiny video stream (<50 kbps over a feature-length runtime) and retries once with software decode; genuinely static sources stay tiny on CPU decode too and are still kept. ## [0.9.114] — 2026-09-05 ### Fixed - **A corrupt or empty external subtitle (e.g. a 0-byte `.eng.srt`) no longer fails the whole conversion with exit 183 "Invalid data found".** Each sidecar is now probed before being fed to ffmpeg as an input; unreadable ones are skipped and logged instead of aborting the encode. ## [0.9.113] — 2026-08-31 ### Fixed - **External-sub auto-detection no longer fails running conversions with "database is locked".** The v0.9.107 watcher reconcile held a write transaction open across its entire pass (UPDATE in the loop, commit only at the end), blocking the encoders' progress writes. It now reads, computes in memory, then writes the few changed rows in a short committed transaction with a busy_timeout. ## [0.9.112] — 2026-08-20 ### Fixed - **A re-scan whose TMDB lookup fails no longer re-drifts an API-matched title's tracks (or wipes manual edits).** When the fresh lookup times out, the scan classifies against the heuristic native (the first audio track) while the upsert preserves the stored authoritative native — so a Chinese dub got re-kept on an English show and manual track edits were lost. The persist path now keeps the stored tracks when the stream layout is unchanged, and the one-time authoritative-native heal re-runs once to fix already-drifted rows. ## [0.9.111] — 2026-08-20 ### Fixed - **NVDEC decode failures now fall back to software decode instead of failing the job.** Some interlaced/DVD-rip MPEG-2 streams make the GPU's NVDEC decoder error on every frame ("hardware accelerator failed to decode" / cuvidDecodePicture), which aborted the whole conversion. Those signatures now trigger the existing software-decode retry (NVENC still does the encode). ## [0.9.110] — 2026-08-20 ### Added - **A missing TMDB API key is now surfaced instead of failing silently.** Self-built images ship without the bundled key (it's injected from the maintainer's CI secret), so metadata never matched with no hint why. When no key is available (neither a saved key nor the bundled one), a startup log warning and an app-wide banner ("No TMDB API key configured") now point users to Settings → Metadata. ## [0.9.109] — 2026-08-20 ### Fixed - **Non-native audio tracks are no longer wrongly kept on API-matched titles.** A title whose native was resolved by the API after an older scan classified its tracks against the heuristic native (the first audio track's language) kept that track — e.g. a Chinese dub on an English show stayed marked-keep. A one-time startup heal re-derives audio/subtitle keep-flags for `api`/`manual`/`tmdb-manual` rows against their stored native (only rewriting rows that actually change). **Note:** on a healed row this recomputes keep/remove, so a manual per-track keep you set on such a title may be reset. ## [0.9.108] — 2026-08-20 ### Fixed - **Jobs stuck "running" with a spinner are now self-healed.** A worker task that ended without writing a terminal status left a stale `running` row — a permanent spinner, and the visible running count drifted above the parallel limit. The worker now reaps orphaned `running` rows (local jobs with no live task, past a short grace window) back to `pending` every 60s so they re-run cleanly. ## [0.9.107] — 2026-08-19 ### Added - **The watcher now auto-detects external subtitle files.** Dropping a sidecar sub (`.srt`/`.ass`/`.ssa`/`.sub`/`.vtt`/`.sup`/`.idx`) next to an already-known video is picked up on the next watcher cycle and updates that title's subtitle list — no manual folder rescan needed. Scoped to folders that actually gained subs (reuses the walk's file listing, writes only on change) and restart-safe (reconciles disk against each row). ## [0.9.106] — 2026-08-19 ### Fixed - **A folder rescan no longer health-checks the whole library.** The post-scan inline health-check (when `health_check_on_scan` is on) selected every recently-detected unchecked file across the library, so rescanning one folder swept hundreds of unrelated files. It's now scoped to the folders the scan actually covered. ## [0.9.105] — 2026-08-19 ### Fixed - **CI: fewer, more reliable builds.** Duplicate test runs from a release's paired main+tag push now auto-cancel (concurrency group), both workflows gained job timeouts so a stuck run can't hang for hours, and the ffmpeg download in both Dockerfiles retries and writes to a file before extracting — fixing the transient arm64 build failures under QEMU emulation. ## [0.9.104] — 2026-08-19 ### Fixed - **NVENC image builds again.** BtbN pruned the `n7.1` ffmpeg build from its rolling `latest` release (the pinned download 404'd, failing the tagged nvenc image). Bumped the pinned ffmpeg to `n8.1`. **The nvenc image now requires NVIDIA driver 570+** (n8.1's NVENC SDK); driver 525-era users should stay on an older nvenc tag or update. Carries the v0.9.103 fix. ## [0.9.103] — 2026-08-14 ### Fixed - **Efficient re-encodes are no longer misflagged as "corrupt source."** The undersized-output guard declared any output <5% of the source corrupt — false-flagging a legitimate MPEG-2 DVD/Blu-ray that shrinks >95% into HEVC while keeping its full runtime. It now checks the output's DURATION and only flags a genuinely truncated encode. A startup backfill clears marks the old size-only check set so those titles can be retried. ## [0.9.102] — 2026-08-14 ### Fixed - **Titles with a bogus future timestamp no longer pin the top of the "Newest" sort.** Ripped media dated years ahead (a `VIDEO_TS.IFO` or mkv stamped 2036) sorted above every genuinely-new addition. Future mtimes are now clamped to "now" on scan, and a one-time startup backfill fixes rows already stored. A subsequent full re-scan re-stamps them at scan time. ## [0.9.101] — 2026-08-14 ### Fixed - **Files with a future-dated timestamp are no longer skipped by the watcher forever.** The "skip files newer than N minutes" guard computed a negative age for a bogus future mtime (common on DVD/ISO rips — a `VIDEO_TS.IFO` dated 2036), which counts as "still being written," so the disc/file was skipped on every cycle and never registered. Future/negative ages are now excluded from the skip. ## [0.9.100] — 2026-08-14 ### Fixed - **DVD (VIDEO_TS) folders no longer stay invisible after one slow probe.** A probe failure was blocklisted in memory until the app restarted, so a disc `concat:` probe that blew the 30s ffprobe limit under load (200MB read over CIFS) hid the DVD permanently — how a whole library ended up with zero DVDs. Probe failures now retry after a 30-minute cooldown, and disc probes get a 180s timeout (matching the subtitle-extraction allowance for cold networked reads). ## [0.9.99] — 2026-08-14 ### Fixed - **Empty "NO SUBS" placeholder subtitles can now be cleaned up.** A forced or unknown-language subtitle with ≤1 cue (the junk placeholder some releases ship) is now flagged removable on scan, so an audio/sub cleanup strips it across a whole season instead of always keeping it as a forced track. - **Unchecking a locked (forced / keep-language) subtitle in the detail panel is now honored.** Cleanup derived removals as "not-kept AND not-locked", which silently discarded an explicit untick of a locked track; it now removes any track you've unchecked. ## [0.9.98] — 2026-07-23 ### Fixed - **Language detection no longer hangs on m2ts/transport-stream files.** Detecting on a raw Blu-ray `.m2ts`/`.mts`/`.ts` routed its PGS subtitles through a full-movie OCR run in a thread that cancellation couldn't kill (a 15-min unkillable hang over a network mount). Detection is now skipped for these convert-first stream containers, with a clear per-track note instead. ## [0.9.97] — 2026-07-23 ### Fixed - **Subtitle language detection no longer times out on large files over a network mount.** Extracting text from a big Blu-ray subtitle track reads ~GBs cold over CIFS/SMB, which blew the 60s extraction timeout (killed → empty → stuck `und`, though a warm retry worked). Raised the timeout to 180s so cold networked reads finish. ## [0.9.96] — 2026-07-23 ### Fixed - **Manually-matched items no longer show as "heuristic".** The file detail labeled any source other than `api` as "heuristic", so `manual`/`tmdb-manual` matches looked unmatched even though they were correctly recorded. It now shows "manual match" (and treats all three as authoritative). This is why re-matching seemed not to "stick" — the match was registering, just mislabeled. - **Manual match now stores the native language in the right format.** `override_poster` saved TMDB's 2-letter code (e.g. `es`/`de`) instead of the 3-letter ISO 639-2/B (`spa`/`ger`) the rest of the app uses, which broke keep-language matching; it now maps it like the API path does. (Re-match affected items once to correct their stored native.) ## [0.9.95] — 2026-07-23 ### Fixed - **Metadata refresh now matches items that have no ID in their path**, via the title/year search (previously the lookup returned early when there was no IMDb/TVDB/TMDB id, so id-less items never got searched). - **Manual matching now sticks.** A manual (poster) match always marks the item `tmdb-manual` — even when TMDB has no original_language for the pick — and it now also matches a loose single file (the grid passes the file's own path, whose `/%` prefix matched nothing), so re-matching no longer leaves items on `heuristic`. ## [0.9.94] — 2026-07-23 ### Fixed - **Re-scanning no longer wipes API/manual language matches back to "heuristic".** The scan upsert overwrote native_language + language_source with the freshly-scanned (heuristic) values, so a folder rescan or the watcher re-seeing a file downgraded every TMDB (api) or manual/tmdb-manual match to heuristic. The upsert now preserves an authoritative source/native and only refreshes heuristic/unset rows. ## [0.9.93] — 2026-07-23 ### Fixed - **Original-language lookup falls back to a title/year search when the ID doesn't resolve.** TMDB's `/find` often has no IMDb/TVDB external-ID link for newer titles even though the title exists, so ID-tagged items came back "no API data" and stayed unmatched. When an ID lookup is empty, Shrinkerr now searches TMDB by the parsed title + year (year-gated to avoid wrong matches, the same path manual matching uses), resolving the vast majority of these. ## [0.9.92] — 2026-07-22 ### Changed - **Metadata lookups now log why they return no data** (HTTP status, empty results, or request error), capped per run. Previously IMDb/TVDB `/find` failures were swallowed silently and surfaced only as "no API data", making them undiagnosable. ## [0.9.91] — 2026-07-22 ### Fixed - **Metadata refresh retries unmatched items again instead of skipping them forever.** v0.9.85 permanently marked failed lookups so refresh converged, but that also stranded items a later lookup improvement could resolve (e.g. v0.9.89's TMDB-id support) — so refresh became an instant no-op and TMDB-tagged items stayed unmatched. Refresh now re-tries all still-heuristic items each run; the metadata cache throttles real API load (cached results instant, failed lookups re-hit TMDB only after 24h), and no-id items return instantly. Speed still comes from keeping the api-row re-classification behind `deep`. ## [0.9.90] — 2026-07-22 ### Fixed - **Queue page loaded empty (regression from v0.9.88).** The memoized row lists were placed after the `if (initialLoading)` early return, so their hooks were skipped on the first render and then run on the next — a React hook-order violation (error #310) that crashed the page to blank. Moved them above the early return. ## [0.9.89] — 2026-07-22 ### Fixed - **Original-language lookup now resolves TMDB-tagged items (fewer stuck in "Not API-matched").** The language lookup only recognised `[tt…]`/`[tvdb-…]` ids, while the poster system also reads TMDB ids and `{…}`/`tmdbid-`/bare forms — so items tagged with a TMDB id got a poster but never resolved a language and stayed heuristic. Lookup now uses the same id extraction as posters and supports TMDB-id lookups, so a metadata refresh auto-matches them. (Items with no id in their path still need manual matching.) ## [0.9.88] — 2026-07-22 ### Fixed - **Queue page no longer freezes with a large queue of fast jobs.** Every job-progress WebSocket tick re-rendered the whole page, rebuilding all ~1K pending/completed row elements each time; with many fast audio/sub cleanup jobs firing rapid progress this pegged the browser main thread for minutes. The row lists are now memoized so progress ticks only re-render the active job cards, not the full list. ## [0.9.87] — 2026-07-22 ### Added - **"Disc / ISO" filter.** Shows disc-image content (DVD + Blu-ray, both `.iso` images and BDMV/VIDEO_TS folders) — items Plex can't play directly and that need converting. ## [0.9.86] — 2026-07-22 ### Added - **Loading spinner while manually setting a track language.** Writing a language to an mp4 remuxes the whole file and can take a moment; the file detail view now shows a "Setting track language…" spinner and disables the track's language controls until it finishes, so it's clear it's working and users don't retry. ## [0.9.85] — 2026-07-22 ### Added - **"Dubbed" and "Not API-matched" filters, and incremental metadata refresh.** Dubbed flags items whose TMDB/manual original language has no matching audio track (heuristic-native items excluded — their native is derived from the audio); Not-API-matched shows items still awaiting a TMDB match, split into not-yet-tried vs tried-no-match. Refresh now processes only untried unmatched items and marks TMDB no-matches so it converges instead of reprocessing the whole library; a `deep` option restores the full re-heal. ## [0.9.84] — 2026-07-22 ### Fixed - **You can now reset a track's language back to "und" to re-detect it.** Setting a track to Undetermined was rejected ("A language must be provided"), so a track the old model mis-detected couldn't be cleared and re-run — detection only touches und tracks. Setting und now writes und to the file (and verifies it) so the next detection picks the track up again. ## [0.9.83] — 2026-07-22 ### Fixed - **source_codecs changes now actually recompute needs_conversion.** The recompute read rows by column name, but the settings-update handler's DB connection returns plain tuples, so it raised a TypeError and silently no-op'd on every settings save (since v0.5.4) — leaving needs_conversion stale after widening/narrowing "Convert From". Recompute now reads rows positionally, so it works regardless of the caller's row factory. ## [0.9.82] — 2026-07-22 ### Fixed - **The Whisper device line ("loaded on cuda"/"cpu") now shows in the in-app log.** It was written to the detached worker's stderr, which bypasses the log interceptor and only reached `docker logs`; the worker now sends diagnostics back through the parent so GPU use is verifiable in the UI. ## [0.9.81] — 2026-07-21 ### Added - **GPU-accelerated audio detection on the :nvenc image, plus medium/large-v3 models.** The nvenc image now ships cuDNN so faster-whisper runs on the GPU (`device="auto"`, falls back to CPU), and the Settings dropdown exposes medium and large-v3 — making reliable detection of low-resource languages (e.g. Icelandic) practical. Default model unchanged (tiny). ## [0.9.80] — 2026-07-21 ### Changed - **Audio language detection loads the Whisper model once instead of per clip.** A single persistent, killable worker now serves all clips (released after idle, respawned on timeout/crash/model change), so detection is much faster over large libraries and larger models become practical. Killability (v0.9.21) is unchanged. ## [0.9.79] — 2026-07-21 ### Fixed - **VobSub OCR-yield logging now actually covers VobSub, and PGS OCR is no longer broken.** v0.9.77 added the "srt written but nothing recognised" diagnostic to the wrong helper — it landed in the PGS path (with a `tess_lang`/`tess_langs` name mismatch that raised on every successful PGS OCR) while the VobSub path stayed silent. The diagnostic is now in the VobSub helper, and PGS OCR works again; both paths log their OCR yield. ## [0.9.78] — 2026-07-21 ### Fixed - **Image-subtitle detection no longer fails silently.** A raise inside the OCR call (setup error before the OCR helpers' own logging) was swallowed with no log, leaving the track at `und` with nothing to diagnose; the exception and traceback are now logged as an `[IMG-OCR]` line. ## [0.9.77] — 2026-07-21 ### Fixed - **VobSub (dvd_subtitle) tracks that OCR to nothing now say why.** subtile-ocr can exit cleanly yet write an srt of only timestamps when tesseract recognises no characters in the rendered bitmaps (e.g. an unusual palette — black text — that binarises with too little contrast); that was the one silent path to `und` with no `[IMG-OCR]` line. It now logs the OCR yield (char count + sample, or "no readable text"). ## [0.9.76] — 2026-07-21 ### Fixed - **More `.ass` subtitles now detect correctly (e.g. Tagalog).** ffmpeg's ass→srt fallback wraps each line in `` tags (from the ASS style) and leaves `{\an…}` overrides; over a full episode the repeated "font size color" tokens skewed langdetect to English (en@0.57) so the track stayed und. Subtitle cleaning now strips HTML-like tags and residual override tags before detection. ## [0.9.75] — 2026-07-21 ### Fixed - **A detected subtitle language can now be written to an MP4-that's-named-.mkv.** Such files carry `mov_text` subs, which mkvpropedit rejects ("not a Matroska file") and the matroska remux can't copy ("Subtitle codec … is not supported") — so detection succeeded but the write silently failed. The remux now transcodes `mov_text`/`tx3g` subs to srt, setting the language and normalizing the file into a real mkv. ## [0.9.74] — 2026-07-21 ### Fixed - **VobSub (dvd_subtitle) subtitles that fail to extract no longer go silently to und.** `_extract_vobsub` ran mkvextract with its output discarded and returned nothing on failure with no log; it now logs the reason (exit code, whether the .idx/.sub were produced, and mkvextract's message) so extraction failures are diagnosable. ## [0.9.73] — 2026-07-21 ### Fixed - **A subtitle language write that mkvpropedit rejects (rc=2) now falls back to the ffmpeg remux instead of silently giving up**, and mkvpropedit's error reason is now logged (mkvtoolnix writes it to stdout, which the previous stderr-only capture missed — the log showed a blank reason). ## [0.9.72] — 2026-07-21 ### Fixed - **PGS subtitles that start at 00:00:00 no longer fail to OCR.** pgsrip's `to_time` treated a zero timestamp as "no time" and returned None, so it built a subtitle with a null start and crashed (`SubRipTime() argument after * must be an iterable`), aborting the whole rip — the track stayed und. Patched to treat 0 as a valid time. ## [0.9.71] — 2026-07-21 ### Fixed - **Language detection on `.ass` subtitles no longer mis-reads them as English.** The extractor returned the raw ASS script (the English `[Script Info]`/`Style:`/`Dialogue:` structure), so langdetect saw mostly English and left the track und; it now strips ASS structure to dialogue-only text. - **Malay subtitles are now detected as Malay (`may`).** langdetect has no Malay model and read Malay as Indonesian or und; a Malay-vs-Indonesian discriminator now tags clear Malay, without mis-tagging genuine Indonesian. ## [0.9.70] — 2026-07-19 ### Fixed - **Metadata refresh now updates which tracks are kept when it corrects a native language.** It only changed the language label, leaving the keep/remove flags computed against the old (wrong) native — e.g. a show heuristically matched as Portuguese kept its Portuguese track and marked the real Korean one for removal even after the refresh set the native to Korean. The refresh now re-classifies the tracks against the corrected native, and also heals already-API titles whose classification had drifted (rewriting only when it actually changed). ## [0.9.68] — 2026-07-19 ### Changed - **Language detection no longer masquerades as a native-language source.** It set `language_source='detected'`, conflating "detection ran" with the native language's provenance — which hid that the native was still a heuristic guess and excluded the title from the heuristic→API refresh. Split into a separate `tracks_detected` flag; `language_source` now means only native provenance (api/heuristic/manual). Detection also no longer downgrades a TMDB- or user-set native to a heuristic guess. Existing `detected` rows migrate to `heuristic` + `tracks_detected=1`. ### Fixed - **A cleanup job that removes both audio and subtitle tracks is now labelled "Audio + Sub cleanup" instead of "Remux."** The both-removals case fell through to the remux label. ## [0.9.67] — 2026-07-19 ### Changed - **A single-folder rescan no longer triggers the full-library Plex sync + poster prefetch.** Those are full-scan concerns (rule-referenced labels/collections/watch-status across the whole library); re-running them for one folder was redundant. Full scans are unchanged. ### Fixed - Made two ffmpeg-progress unit tests deterministic (they derived a start time from `time.monotonic()`, which is small in a fresh CI container and made the tests flaky). ## [0.9.66] — 2026-07-19 ### Fixed - **A scan no longer sits at "Scanning… 100%" for minutes after probing finishes.** The post-scan Plex watch-status sync and poster prefetch ran inline before the scan task returned — so on a large Plex library the UI stayed "scanning" (with no feedback) for minutes while a full network sync ran, even for a small folder rescan with no jobs running. Those best-effort steps now run detached, so the scan reports done as soon as the files are probed. ## [0.9.65] — 2026-07-19 ### Fixed - **"Refresh metadata" now also upgrades titles you'd run language detection on.** It only re-checked `heuristic` rows, but running detection flips a title to `detected` (its native language is still a heuristic guess, and the UI shows both as "heuristic") — so detected TV shows were silently skipped and only a rescan fixed them. The refresh now includes `detected` rows too (`manual` overrides are still left alone). ## [0.9.64] — 2026-07-19 ### Fixed - **A folder rescan no longer appears to hang for minutes under write contention.** The scan's best-effort end-of-scan maintenance sweeps (orphan cleanup, duplicate detection, converted-flag restore) waited up to 60s each on a contended write lock — ~3 minutes stuck at "100%" while conversions ran. They now fail fast (10s) and are redone by the next full scan; the file re-probe/upsert (which carries the language fix) still uses the full timeout, so the rescan's actual work is unaffected. ## [0.9.63] — 2026-07-19 ### Fixed - **"Refresh metadata" (heuristic → API) no longer aborts on the first DB-lock and now completes.** A batch write that hit "database is locked" under write contention (conversions + a scan) propagated out and killed the entire refresh, leaving most heuristic rows — much of the TV library — never upgraded to API-sourced language. Batch writes now retry on a transient lock and defer only the failed batch instead of aborting the whole run. ## [0.9.62] — 2026-07-19 ### Fixed - **A stalled media mount can no longer freeze the whole app.** The conversion worker made synchronous `stat`/`getsize`/`exists` calls on media files directly on the asyncio event loop; when media lives on a slow/saturated network mount (CIFS/SMB/NFS), any one of those blocked the entire event loop for the duration of the stall — making the UI unresponsive, scan results fail to load, and conversions' DB writes back up into "database is locked". All media file I/O in the worker now runs off the event loop. ## [0.9.61] — 2026-07-19 ### Fixed - **Cancelling a bulk detection now still offers to remux the AVIs detected before the cancel.** The post-detect "remux to MKV to apply language" prompt was suppressed on cancel, silently stranding the titles whose language was already detected; it now appears whenever there are pending titles, cancelled or not. ## [0.9.60] — 2026-07-19 ### Fixed - **Language detection no longer records a language the file doesn't actually carry.** When the post-write verify probe failed (e.g. ffprobe stalling on busy storage), the write was assumed successful and the track left the Unknown-language filter while staying und on disk. The verify now retries and only commits the detected language when the tag is confirmed present; otherwise the track stays und/pending and gets retried. ## [0.9.59] — 2026-07-19 ### Fixed - **A conversion that fails on a transient "database is locked" now requeues instead of dying.** Under heavy concurrency (a rescan holding the write lock past the 60s timeout while conversions run), jobs were permanently failed; they now go back to pending and retry (bounded), so a lock storm no longer discards a running encode. - **A single-folder rescan no longer runs full-library write sweeps.** The "restore converted flags" and duplicate-detection passes reset/reloaded the entire scan_results table under the write lock on every scan; they're now scoped to the walked paths (unchanged for a full scan), which stops a folder rescan from locking out running conversions. ## [0.9.58] — 2026-07-18 ### Changed - **Guaranteed (with tests) that force-re-encoding an already-h265 file queues as a Convert job, not a no-op "Remux."** Extracted the add-from-scan job-type decision into a pure, unit-tested function; behaviour is unchanged, but the forced-x265→convert case is now locked in against regression. ## [0.9.57] — 2026-07-18 ### Fixed - **Scheduled backups no longer fail with "cannot VACUUM - SQL statements in progress."** Backups used `VACUUM INTO`, which aborts whenever the database has active statements — under normal load (conversions + scans + a large queue) that is almost always, so every scheduled backup failed. Switched to SQLite's online backup API, which is built to copy a live, concurrently-written database. ## [0.9.56] — 2026-07-18 ### Fixed - **Audio/sub cleanup on an mkv now updates the database.** The post-remux reconcile only ran when the container changed (AVI→mkv); an mkv cleanup writes back to the same path, so it was skipped — leaving the removed und sub tracks in the record and the title stuck in the Unknown-language/cleanup filters. It now re-probes and rewrites tracks + flags in place after any audio remux. ## [0.9.55] — 2026-07-18 ### Fixed - **Fixed the v0.9.54 image build failure** — `addJobsFromScan`'s return type didn't declare `skipped_existing`, which `tsc -b` (the production build) rejected. No behaviour change; v0.9.54's fixes only reach a pullable image from this release on. ## [0.9.54] — 2026-07-18 ### Fixed - **.m4v/.m4a files now get the "Remux to MKV to apply language" button.** ffmpeg's ipod muxer can't write language tags to them in place, so they now take the same stream-copy remux-to-mkv path as AVI (the detected language is kept pending and applied by the remux) instead of showing a hint with no way to act on it. - **"Remux to MKV" for a file already in the queue now says "Already in the queue" instead of "Nothing queued".** A language_remux that resolves the path but adds no job (duplicate already pending/running) is now reported honestly, and a missing scan row is logged. ## [0.9.53] — 2026-07-18 ### Fixed - **Image-subtitle detection no longer wastes minutes retrying a PGS track pgsrip can't decode.** When pgsrip reports "Error while trying to rip" (a hard decode failure), the second language pass and the full-track re-extraction fail identically — so detection now stops after the first failure instead of running all four passes. ## [0.9.52] — 2026-07-18 ### Fixed - **AVI "Remux to MKV to apply language" no longer says "Nothing queued".** The remux was being run through the video-conversion filters (min file size, min/max bitrate) and skip rules, which dropped files that fell outside those bounds. A stream-copy tag fix has no conversion cost, so those gates no longer apply to it. ## [0.9.51] — 2026-07-18 ### Fixed - **Detection no longer silently fails on .m4v (and other taggable files whose in-place write fails).** It detected the language but, because .m4v isn't "untaggable", reverted to und with no hint when the ipod-muxer remux failed. Now any failed write keeps the detected language as pending — showing the "detected → convert to MKV to apply" hint and offering it in the post-detect remux dialog. ## [0.9.50] — 2026-07-18 ### Fixed - **Track keep/remove checkboxes in the file detail work again.** v0.9.45 made the detail render freshly-fetched tracks, but the checkbox toggle only updated the scanner cache the view no longer reads — so it looked frozen. The toggle now flips the displayed tracks too (and still persists). ## [0.9.49] — 2026-07-16 ### Fixed - **Setting/detecting a language on mkvs with a `LANGUAGE` SimpleTag now sticks.** mkvpropedit edits the track-header language, but some files carry a track `LANGUAGE` SimpleTag that overrides it for ffmpeg/Plex — so the change didn't show (and detection reverted to und with no reason). When the in-place edit doesn't verify, it now falls back to an ffmpeg `-c copy` remux whose `-metadata` rewrites the effective language. Re-run detection to fix affected files. ## [0.9.48] — 2026-07-16 ### Fixed - **Setting a language on some mkv files silently didn't stick.** mkvpropedit set the legacy `language` element, but those files carry a `language-ietf` (BCP-47) element that overrides it, so ffprobe/players still read und (mkvpropedit reported success; the tag verify caught it). The write now also deletes `language-ietf`, making the language authoritative — fixes both manual override and auto-detection on affected files. ## [0.9.47] — 2026-07-16 ### Fixed - **A manual language set is never silently lost.** If the in-place write didn't stick (untaggable AVI, an mp4 the muxer won't tag, an mkvpropedit hiccup), the choice is now remembered and applied via the Remux/Convert-to-MKV flow instead of reverting to und while claiming "Language set". The toast now reflects the real outcome. - Manual language picker is sorted alphabetically. ## [0.9.46] — 2026-07-16 ### Fixed - **Every track detection can't resolve now gets a reason.** The audio path left the note empty when detection *errored* (vs. low-confidence), so those tracks showed a bare und; errors are now captured ("audio detection error: …") and both audio/subtitle paths fall back to a generic reason. Re-run detection to populate it. ## [0.9.45] — 2026-07-16 ### Fixed - **The per-track detection reason now shows in the file detail.** The detail reused the (possibly stale) tracks passed in from the list instead of refetching, and a notes-only detection run (nothing resolved) didn't update the view — so `detect_note` never appeared. It now always refetches fresh tracks and applies detect results even when no language changed. ## [0.9.44] — 2026-07-16 ### Added - **Tracks that detection couldn't resolve now show why**, next to the und track — e.g. `eng 49% — below 0.60 threshold`, `no text in subtitle`, `detected "nn" — no ISO-639-2 code`, `audio detection timed out`. Pairs with the manual override (pencil) so you can see the reason and set the language yourself. ## [0.9.43] — 2026-07-16 ### Added - **Manually set a track's language** (for tracks detection can't resolve). A pencil icon on each audio/subtitle track opens a language picker; saving writes the tag in place for mkv/mp4, or — for untaggable containers (AVI) — stores it to apply via the Remux-to-MKV flow. External sidecar subs are renamed. ## [0.9.42] — 2026-07-16 ### Fixed - **The post-detection "remux to apply language" dialog is no longer missed when you navigate away during a bulk detect.** The finished batch result is kept server-side, so the dialog is offered once when you return (then acknowledged so it doesn't repeat). ## [0.9.41] — 2026-07-16 ### Fixed - **Retrying a failed "apply language" remux no longer turns into a re-encode.** A failed audio job was escalated to a full convert on retry (right for a cleanup follow-up on a source that needs conversion, wrong for a pure remux). Escalation now only applies when the job actually removes tracks; a pure remux retries as a remux. ## [0.9.40] — 2026-07-16 ### Fixed - **Remux to MKV no longer fails (ffmpeg 234) on AVIs with missing timestamps.** Many AVIs carry packets with no presentation timestamps, which matroska rejects on stream copy ("Can't write packet with unknown timestamp"). The remux now passes `-fflags +genpts` to generate them. ## [0.9.39] — 2026-07-16 ### Fixed - **After a remux applies a language, the title updates instead of lingering as the original AVI.** The audio/remux job left `scan_results` pointing at the now-deleted `.avi` (still `und`), so the title stayed in the Unknown-language filter and the completed job showed the old name. It now re-points the row to the `.mkv`, re-probes (language applied → flag cleared → leaves the filter), the completed job shows the `.mkv` name, and the report notes the language that was applied. ## [0.9.38] — 2026-07-16 ### Fixed - **"Remux to apply language" no longer no-ops on AVIs.** The audio job only remuxed when tracks were being removed, so a pure language-fix (no track change) did nothing and the file stayed und. It now also remuxes when there's a detected language to stamp. The queue badge for these jobs reads **Remux** (no misleading encoder-settings badge) and the progress step shows "applying language." ## [0.9.37] — 2026-07-16 ### Added - **"Apply detected language" for untaggable files and discs.** Bulk detection now ends with a dialog offering to remux the detected AVIs to mkv (stream copy — applies the language, no re-encode/size increase) in one click; there's a per-title "Remux / Convert to MKV (apply language)" button and a per-track "X detected → convert to MKV to apply" hint. Flat files go through a size-safe remux; discs route to a full conversion (language stamped during the encode). ## [0.9.36] — 2026-07-16 ### Added - **Remux to MKV now stamps the detected language.** A stream-copy remux (no re-encode) applies a language detected for an untaggable source (AVI etc.) onto the mkv output — the size-safe way to fix these, since re-encoding old SD files often produced a *larger* file that got discarded. (Backend; the per-title remux action + post-detect dialog follow.) ## [0.9.35] — 2026-07-16 ### Added - **Languages detected for untaggable containers (AVI etc.) are now remembered and applied when the file is converted to mkv.** Detection stores the result on the track (leaving it `und` on disk, so the title stays in the Unknown-language filter until converted); converting stamps the stored language onto the output — no re-detection. (Backend; the per-track hint and post-bulk "convert to apply" dialog follow.) ## [0.9.34] — 2026-07-16 ### Fixed - **Bulk language detection now processes titles in the on-screen order** — the poster grid / file tree's current sort and direction — instead of scan order. The UI sends folders in display order and the backend preserves it (files within a folder stay alphabetical). Supersedes v0.9.33's plain alphabetical sort, which ignored the view's chosen sort. ## [0.9.33] — 2026-07-16 ### Fixed - **Bulk language detection now processes titles in alphabetical order.** It followed DB rowid (scan) order, which drifts from alphabetical after multi-pass scans and made ignored/foreign titles look like they were left for last; the batch is now sorted by path. ## [0.9.32] — 2026-07-16 ### Fixed - **The UI no longer freezes for tens of seconds while a conversion finishes.** Disposing of the original file (send-to-trash, backup move, disc rmtree) ran synchronously on the event loop, so a multi-GB cross-filesystem copy blocked every API request until it completed. Those file operations now run off the loop. ## [0.9.31] — 2026-07-16 ### Changed - **Ignored titles now show in the Audio-cleanup and Subtitle-cleanup filters** (matching Unknown-language) — an ignore rule means "don't convert", not "don't tidy tracks". The conversion filters (Needs conversion, high/low bitrate) still exclude ignored. ## [0.9.30] — 2026-07-16 ### Fixed - **Scans no longer look frozen on the last file of each directory.** The parallel ffprobe pre-pass fills the progress bar, then a serial classify + DB-write pass ran with no feedback — freezing the UI on the last probed file for minutes on large directories. That pass now reports a "Reading track metadata: X/Y" phase so the count and current file keep moving. ## [0.9.29] — 2026-07-16 ### Fixed - **"Update available" no longer appears when you're running a newer version than the latest published Release.** The check used string inequality, so a lagging or failed Release-notes job (latest < current) prompted a phantom "downgrade"; it now uses a semver comparison and flags an update only when the latest release is genuinely newer. ## [0.9.28] — 2026-07-16 ### Fixed - **Multi-directory scans no longer look stuck between directories.** Each directory's `os.walk` (disk spin-up + tree walk) emitted no progress, freezing the UI on the previous directory's last file — now it shows "Discovering files in …" at the start of every directory. - **A scan no longer signals completion after each directory.** `scan_directory` emitted a terminal `done` per directory (not just at the end), prematurely flipping the UI to idle mid-scan; it now emits a non-terminal update and the single terminal `done` comes from the worker after all directories finish. ## [0.9.27] — 2026-07-16 ### Fixed - **The Scan button no longer reverts to "Scan" during the initial file-discovery phase** (a scan walks every configured path before emitting progress — minutes on spun-down disks — so the UI had no signal and looked like it failed, inviting repeat clicks). It now shows "Discovering files…" immediately and derives its state from a polled backend scan status, staying "Scanning…" throughout. ## [0.9.26] — 2026-07-16 ### Fixed - **Detection no longer drops a title from the Unknown-language filter unless the tag actually landed in the file.** It now verifies the language is present on disk before clearing the flag — containers with no per-track language field (AVI, MPG, WMV, …) copy fine but silently drop the tag, so they'd leave the filter while the file and Plex stayed `und`. They now stay flagged (fix them by converting to mkv). - **The Unknown-language filter now includes ignored titles** — an ignore rule means "don't convert", not "hide that the audio is untagged"; bulk detection reaches them too. ### Changed - **Bulk detection fires one deduped full Plex section refresh per affected library** instead of a folder-scoped scan per file, so freshly-tagged files reliably stop showing `und` in Plex. - Skip the (multi-GB) `-c copy` remux for containers that can't store per-track language instead of copying a temp only to discard it. ## [0.9.25] — 2026-07-16 ### Fixed - **Converted titles with `und` tracks wrongly disappeared from the Unknown-language filter.** The post-conversion update rewrote the track JSON but never recomputed `has_und_tracks_flag`, so a converted file whose output still had `und` tracks kept a stale flag and vanished from the filter (still `und` in the file/Plex). Now recomputed on conversion, plus a one-time startup re-sync that corrects the flag against the stored tracks for every existing row. ## [0.9.24] — 2026-07-16 ### Added - **Bulk language detection now has a persistent progress indicator and a Cancel button.** The batch runs server-side as a background job with live `N/total` progress that survives navigating away and back (previously the spinner vanished on navigation or during large runs), plus a Cancel that stops after the current file. ## [0.9.23] — 2026-07-16 ### Fixed - **Embedded VobSub OCR now works.** mkvextract writes the `.idx` `palette:` line with a bare `,` (no space) after every 4th colour, which subtile-ocr's strict parser rejected ("error during palette parsing" → exit 1 → track stayed `und`). The `.idx` palette is now normalized to a uniform `, ` before OCR. Also surface subtile-ocr's stderr on failure instead of just "exit status 1." ## [0.9.22] — 2026-07-14 ### Fixed - **PGS subtitle OCR that pgsrip aborted on now works.** pgsrip 0.1.11 crashes with `int('', 16)` on an empty byte read (a truncated/padded trailing segment in a `.sup`), which aborted the whole rip and discarded the valid subtitles — surfacing only as a misleading "no text." Patched pgsrip's `from_hex` to be empty-safe so parsing finishes on the good segments, and the real pgsrip error is now logged instead of "no text." ## [0.9.21] — 2026-07-14 ### Fixed - **Audio detection runs in a killable subprocess so it can't hang the app.** v0.9.19's timeout let a wedged whisper transcribe keep running in an un-cancellable thread; across a batch these piled up, pegged every core, and starved the event loop (the app went fully unresponsive — proven by health-checks that never returned). Detection now runs in a subprocess that's **killed** on timeout, freeing the CPU. **Trade-off:** the model is loaded per detection, so bulk runs with `base`/`small` are slower — use `tiny` for large batches, or accept the cost. ## [0.9.20] — 2026-07-14 ### Changed - **The detect button follows your selection.** It now reads **"Detect selected (N)"** when titles are selected (detecting just those) and **"Detect all unknown (N)"** when nothing is selected. Previously the per-selection detect action was a separate, easy-to-miss button in the bulk toolbar; that's been consolidated into the one prominent button. ## [0.9.19] — 2026-07-14 ### Fixed - **Audio detection can no longer hang the whole app.** A specific track wedged whisper's transcribe, which (defaulting to all CPU cores) starved the event loop until a manual restart. Now: whisper's CPU threads are capped (leaving cores for the app), each transcribe is bounded by a timeout that abandons the track instead of retrying more windows, and the clip-extract ffmpeg is killed on timeout instead of orphaned. A pathological track is skipped (stays `und`) and the app stays responsive. ## [0.9.18] — 2026-07-14 ### Added - **Language-detection tuning in Settings → Video.** The audio detection model (tiny/base/small) and the audio/subtitle confidence thresholds are now UI settings instead of env-only. Defaults unchanged (tiny / 0.6 / 0.7); the model reloads live on change (no restart). The `SHRINKERR_*` env vars still work and override the UI values. ## [0.9.17] — 2026-07-14 ### Fixed - **On-demand detect no longer skips subtitles it can actually detect.** `probe_file` detects `und` text subs inline and fills in the language; `detect_languages` then only acted on tracks still `und` in that result, so any sub `probe_file` resolved was silently skipped — never saved to the DB or written to the file, with no log. Detect now probes with inline detection off (`detect_und_subs=False`) so it detects, persists, and writes the tag itself. ## [0.9.16] — 2026-07-14 ### Fixed - **Text-sub extraction no longer times out on large files.** v0.9.15 dropped the extraction time cap, so ffmpeg demuxed the whole multi-GB file and blew the 15s timeout → empty → `und`. Restored a 30-min cap (covers dialogue start; longer than v0.8.1's 10 min) and raised the timeout to 60s. ### Added - **Configurable Whisper model** via `SHRINKERR_WHISPER_MODEL` (default `tiny`). `tiny` is under-confident on non-English speech (Nordic tracks land ~0.5–0.58, just under the 0.6 gate); set `base`/`small` for better accuracy. Threshold remains tunable via `SHRINKERR_LANG_DETECT_AUDIO_MIN`. ## [0.9.15] — 2026-07-14 ### Fixed - **Correctly-detected languages no longer dropped for a missing mapping.** The ISO 639-1→639-2 table only had ~50 common languages, so detections like `nn` (Nynorsk) were discarded. Expanded to faster-whisper's full set, with a babelfish fallback for anything else. - **More languages recognized from track titles.** Added ~45 languages (Wolof, Swahili, Tamil, Welsh, …) to title detection — fixes `und` tracks whose title named a language the map didn't cover. - **Forced subtitles with late dialogue now detect.** Embedded text-sub extraction no longer caps at the first 10 minutes, so a forced sub whose lines start later isn't extracted empty. ## [0.9.14] — 2026-07-14 ### Changed - **Language detection now logs why a track stays `und`.** Detection is fail-open, so failures (below-confidence guess, empty text/OCR extraction, silent `mkvextract`/`ffmpeg` errors) previously left a track `und` with no explanation. Each of those now logs a concise `[LANG-DETECT]`/`[IMG-OCR]` reason (with the confidence value, char/OCR counts, or subprocess stderr). Diagnostic only — no change to detection behavior. ## [0.9.13] — 2026-07-14 ### Changed - **PGS subtitle OCR samples the track instead of OCR'ing the whole movie.** pgsrip OCRs every image, so a full Blu-ray PGS track took 10+ minutes when language detection only needs a few dozen lines. It now OCRs the first 20 minutes (`SHRINKERR_PGS_SAMPLE_SECONDS`, 0 to disable) and falls back to the full track only if the sample has no text (sparse/forced subs). Extraction uses ffmpeg's `sup` muxer with an mkvextract fallback. ## [0.9.12] — 2026-07-14 ### Fixed - **"Detect all unknown" now covers the whole filter, not just loaded cards.** The count came from files whose tracks were loaded in the browser (expanded cards), so it showed e.g. `(8)` against a 1,440-item filter. It now runs across every title in the active Unknown-language filter — count and action match what you see — with a confirmation first (a large filter with image subs can take a while). ## [0.9.11] — 2026-07-14 ### Added - **Language detection for external VobSub subtitles (`.idx`/`.sub`).** External detection previously only read text sidecars; an external VobSub pair stayed `und`. It's now OCR'd (subtile-ocr) like embedded VobSub, and the detected language is written back by renaming both `.idx` and `.sub` together. (VobSub is a two-file format — the `.idx` correctly represents the pair; the `.sub` is its data partner, not a separate track.) ## [0.9.10] — 2026-07-14 ### Added - **Language detection reads the track title.** A language named in a track's title ("Traditional Chinese", "Romanian", "English SDH") is now used directly, tried before content/OCR detection. Fixes `und` forced/SDH subtitles that carry too little text for langdetect but name the language in their title. Applies to audio and subtitle tracks. ## [0.9.9] — 2026-07-14 ### Fixed - **VobSub OCR tool (`subtile-ocr`) now actually builds into the NVENC image.** v0.9.8 built it with Ubuntu 24.04's apt `cargo` (1.75), but the crate's deps need Rust edition 2024 (Cargo ≥ 1.85), so it silently failed the guarded build and VobSub stayed unavailable. Now built via a rustup stable toolchain, installed and purged within the same layer so the image stays lean. ## [0.9.8] — 2026-07-13 ### Fixed - **VobSub (DVD, `dvd_subtitle`) subtitle OCR now actually works.** It was routed through `pgsrip`, which is PGS/Blu-ray-only and rejected VobSub instantly (no OCR ran). VobSub now uses its own pipeline (`mkvextract` → `.idx`/`.sub` → `subtile-ocr`/tesseract, Latin-first then CJK/Cyrillic/Arabic), converging on the same detector + confidence gate as PGS. `subtile-ocr` is bundled in the NVENC images; on the portable/CPU images VobSub falls back to `und` (fail-open). PGS behavior is unchanged. ## [0.9.7] — 2026-07-13 ### Added - **Language detection for external (sidecar) subtitles.** `und` external `.srt`/`.ass` files are now detected too (they live next to the video, not in the container, so detection never touched them before). The sidecar is renamed to embed the ISO-639-2 code (`Movie.srt` → `Movie.eng.srt`), which is how media servers read the language. Also fixes a latent bug where re-persisting detection results could drop external subs from a file's track list. ## [0.9.6] — 2026-07-13 ### Added - **Bulk "Detect languages" action on the scanner selection.** Select titles (or Select all under the Unknown-language filter) and detect languages for their und tracks in one go. The old "Detect all unknown" button only worked on already-expanded files, so in the poster grid there was no way to bulk-detect; the new action passes the selection to the backend, which expands folder selections to their und-track files server-side. ## [0.9.5] — 2026-07-13 ### Fixed - **Tooltips on the poster-grid detail icons.** The v0.9.1 tooltip pass only covered the file-tree view; the expanded poster-card panel's converted / ignore / remove-from-list / move-to-trash (and unignore) icons still had none. Added the same hover labels there. ## [0.9.4] — 2026-07-13 ### Fixed - **Converted disc titles no longer show a disc badge or the wrong name.** When a Blu-ray/DVD folder was converted to a single .mkv, the post-conversion handler repointed the disc row at the .mkv but never cleared `disc_type` — so the title kept a "Blu-ray disc folder" badge and displayed the category dir ("Movies2") instead of the movie name. Now cleared on conversion, hardened the display-name logic to only treat real marker files as discs, and added a one-time backfill for already-converted titles. ## [0.9.3] — 2026-07-13 ### Fixed - **Unknown-language filter now covers the whole library.** `has_und_tracks_flag` was only set at scan time, so the filter counted just the handful of re-scanned files (~1 instead of thousands). Added a one-time startup backfill that recomputes the flag from stored track metadata — no full re-scan needed. - **Clicking a title in the Unknown-language filter no longer shows "No files found."** The file enricher wasn't populating `has_und_tracks`, so the file-level matcher never matched; the tree view also wrongly dropped und-only files from Audio cleanup. Both fixed. - **Moved the "Unknown language" filter chip out of the Audio group** (it covers audio *and* subtitles) to sit beside Subtitle cleanup. ## [0.9.2] — 2026-07-13 ### Changed - **Batch language detection coalesces Plex refreshes by folder.** "Detect all unknown" used to fire one Plex folder refresh per file; it now suppresses the per-file notify and refreshes each unique parent folder once, so a season of episodes triggers a single refresh instead of one per episode. ## [0.9.1] — 2026-07-13 ### Added - **Live progress for image-sub OCR detection.** The on-demand "Detect languages" action now streams coarse stages over websocket ("Extracting subtitle track N…", "OCR (Latin)…", "OCR (CJK/Cyrillic/Arabic)…") so the minutes-long PGS/VobSub OCR shows what it's doing instead of a silent wait. - **Dedicated "Unknown language" filter** in the scanner, listing titles with any `und` audio or subtitle track (separate from the audio-cleanup filter), with a count. - **Plex notified after a track-language change.** When detection writes corrected language tags to a file, Shrinkerr triggers a Plex library refresh for that item so Plex re-reads them. Gated on the default-on `plex_notify_on_lang_change` setting; reuses the existing rename/rescan path; fail-open. - **Tooltips on the title-detail icon buttons** (codec badge, converted indicator, ignore, remove-from-list, move-to-trash) — each now explains what it does on hover. ## [0.9.0] — 2026-07-13 ### Added - **Language detection for image-based subtitles (PGS/VobSub) via OCR.** On-demand "Detect languages" now handles `und` PGS (Blu-ray) and VobSub (DVD) subtitles: the track is extracted with mkvextract and OCR'd with pgsrip (tesseract). Multi-script without a separate detection step — OCR runs first with the Latin (`eng`) model (clean and fast for the common case); if the result can't be identified, it re-OCRs with the CJK/Cyrillic/Arabic packs. The recognized text feeds the existing language detector, and a detected language is written to the file via the v0.8.3 mkvpropedit path, same as text subs. Confidence-gated and fail-open (bad OCR leaves the track `und`, never mistags). **On-demand only** — not at scan, not before conversion (OCR is slow). Adds tesseract-ocr + language packs + pgsrip to the images. Verified end-to-end on a real Blu-ray PGS sub (Italian → `ita`). ## [0.8.4] — 2026-07-13 ### Changed - **Audio language detection samples multiple positions.** A single 30s window at 33% into a track could land on music/action/silence and yield a low-confidence guess that failed the gate (observed: a 5.1 track returning `pt@0.40`, left `und` even though it has speech elsewhere). Detection now tries several spread-out windows (33%/55%/15%/72%) and takes the most confident, short-circuiting as soon as one clears the threshold — so clear tracks still cost a single sample while weak-first-window tracks get detected from a better window. Applies to on-demand and pre-conversion audio detection. ## [0.8.3] — 2026-07-13 ### Added - **Detected languages are now written back to the file, not just Shrinkerr's DB.** Clicking "Detect languages" (and the batch action) now also tags the file's tracks so the fix is real on disk without a re-encode — for the "just fix the unknown tracks" case. mkv files are edited in place via `mkvpropedit` (instant, no rewrite, no disk churn even on huge files); other containers fall back to an ffmpeg `-c copy` remux with atomic replace. Only tracks upgraded from `und` are written; already-tagged tracks are left alone. Fail-open — if the file write fails, the original is preserved and the DB detection is retained. Adds `mkvtoolnix` to the images. ## [0.8.2] — 2026-07-13 ### Added - **Charset detection for legacy non-Unicode subtitles.** Subtitles in GB2312/GBK/Big5 (Chinese), Shift-JIS (Japanese), EUC-KR (Korean), or Windows-1252/Latin-1 were being blind-decoded as Latin-1 — turning CJK double-byte text into mojibake that language detection couldn't read, leaving the track `und`. Now uses charset-normalizer to detect the real encoding before decoding, so these subs detect correctly (e.g. a GB2312 Chinese sub → `chi`). langdetect's regional codes (`zh-cn`/`zh-tw`) are normalized to `chi`. ### Changed - **Conversion shows a "Detecting language…" status step** while faster-whisper runs on und audio tracks, instead of a silent pause before the encode. - **The "Auto-detect languages" setting now appears in both the audio and subtitle sections** (one shared setting, shown in both places). ## [0.8.1] — 2026-07-13 ### Fixed - **v0.8.0 language detection didn't work.** Two bugs: (1) `faster-whisper` imports `requests`, which wasn't installed, so the model never loaded and every audio detection returned instantly with nothing found — added `requests` to requirements. (2) Subtitle text extraction used ffmpeg's srt **decoder** (`-f srt`), which rejects non-UTF-8 subtitles (Windows-1252/Latin-1, common in older/non-English releases) with "Invalid UTF-8 in decoded subtitles text" and produced zero output, leaving the track `und`. Now extracts with `-c:s copy` (raw bytes, no decode) and decodes tolerantly in Python (utf-8 → cp1252 → latin-1). **Note:** the faster-whisper tiny model still downloads on first audio-detection use — needs outbound network + a writable `/app/data/models`. ## [0.8.0] — 2026-07-13 ### Added - **Language detection for unknown (`und`) tracks.** Text subtitles are detected at scan time via langdetect; audio tracks via a faster-whisper tiny language-ID model — on-demand (a "Detect languages" button per file + a "Detect all unknown" batch action) and automatically before conversion when the new default-on **Auto-detect languages for unknown tracks** setting is enabled. Detected languages are confidence-gated (leave `und` rather than guess wrong), applied with `language_source="detected"`, and written to converted output as `-metadata:s:a/s:N language=` tags for both disc and regular files. Titles with unknown tracks surface in the audio-cleanup filter. faster-whisper's tiny model (~75 MB) downloads on first audio-detection use into the data volume; CUDA-accelerated on NVENC images, CPU elsewhere. Detection is fail-open — a failure never blocks a scan or conversion. **Image-based subtitles (PGS/VobSub) are deferred to v0.8.1** (they need OCR). ## [0.7.34] — 2026-07-08 ### Fixed - **AppleDouble `._*` sidecars still crashed conversions on v0.7.33** for already-scanned files. v0.7.33 fixed detection for *new* scans, but external subs are merged from **stored** `scan_results.subtitle_tracks_json` — rows scanned earlier still carried the `._` paths, so `._*.srt` / `._*.idx` were still fed to ffmpeg (exit 183/234) with no re-scan. Now guarded at the point of use: the convert-time external-sub merge skips hidden/AppleDouble paths via a shared `is_hidden_sidecar` helper, covering every stale row. Also made the external-subtitle map optional (`{idx}:s?`) so a sidecar that opens but exposes no subtitle stream can't hard-fail the job. ## [0.7.33] — 2026-07-08 ### Fixed - **Conversion failed (exit 183) when a macOS AppleDouble `._*.srt` sat next to the video.** External-subtitle detection matched `._.eng.srt` resource-fork companions (they share the `.srt` extension and carry the episode key), then fed them to ffmpeg as `-i` inputs → `Invalid data found when processing input`, killing the whole conversion. Detection now skips hidden / dotfile subtitle candidates, mirroring the scanner walk's existing `._`/dotfile filter. ## [0.7.32] — 2026-07-08 ### Fixed - **Watcher stuck on "Scan in progress, skipping cycle" with no scan actually running.** When a scan subprocess hangs (e.g. `os.walk` blocked on a dead/slow network mount), `proc.is_alive()` stays True forever, the async monitor never returns, and `_scan_task` never completes — so the watcher skips every cycle indefinitely and no new items get picked up. New `scan_is_actively_running()` helper detects a hung scan via a stale progress file (no update in `SHRINKERR_STALE_SCAN_MINUTES`, default 15), kills the stuck subprocess, and clears the task so the watcher resumes — no container restart needed. The manual Scan / Rescan-folder endpoints use the same check, so hitting "Scan" also recovers. ## [0.7.31] — 2026-07-02 ### Fixed - **Conversion of a video-only source (no audio stream) failed with exit 234** (`Stream map '0:a' matches no streams`). Files with only video + subtitles — a video-only Blu-ray remux, or a sample clip — hit the default `-map 0:a` which ffmpeg treats as fatal when nothing matches. Changed to `-map 0:a?` (optional map) in both the main convert path and the subtitle-prestrip pass. ## [0.7.30] — 2026-06-28 ### Fixed - **Disc conversions left subtitle tracks tagged `und`** — the subtitle half of the v0.7.29 audio fix. The `bluray:` / `concat:` protocol strips embedded-subtitle language tags too, and the detected language was being dropped before it reached the ffmpeg command anyway (`subtitle_streams` was built with only codec + index, no `language`). Now carries the detected language through and stamps `-metadata:s:s:N language=X` on disc-conversion output subtitles. Regular files unaffected (ffmpeg copies the container tag). und/empty skipped. ## [0.7.29] — 2026-06-28 ### Fixed - **Disc conversions left audio tracks tagged `und` even when the language was detected.** Shrinkerr's IFO/mpls/libbluray parsers detect disc audio languages at scan time, but the conversion never wrote them to the output — the `bluray:` / `concat:` input protocol strips per-stream language tags, so `-c:a copy` produced `und` audio. The converter now injects `-metadata:s:a:N language=X` from the detected languages for disc conversions (both the map-all path and the track-removal/reorder keep-list path). Regular files are unaffected — they still rely on ffmpeg copying the tag from the source container. und/empty detections are skipped rather than written as bogus tags. ## [0.7.28] — 2026-06-28 ### Fixed - **Audio-cleanup remux failed with `dimensions not set` / `Could not write header` (exit 234)** on files with PNG/MJPEG cover art. The remux command mapped video with `-map 0:v?` (all video streams), which sweeps in attached-pic cover-art streams that the matroska muxer can't write a header for. Changed to `-map 0:v:0?` (first real video stream only), matching what the convert path already does. Cover art is dropped from the remux output, same as the convert path's deliberate behavior. ## [0.7.27] — 2026-05-31 ### Added - **macOS native install documentation** (`docs/native-install-mac.md`). Docker on Mac runs Linux containers in a VM that can't reach Apple's VideoToolbox encoder — performance is roughly 1 fps for software x265 vs. 30–100 fps native with VideoToolbox. New page covers brew prereqs, Python venv setup, frontend build, a `custom_ffmpeg_flags` recipe to invoke `hevc_videotoolbox` until it's a first-class encoder option, launchd service file, and troubleshooting. README + installation.md + troubleshooting.md updated to point Mac users at the native path. ## [0.7.26] — 2026-05-31 ### Changed - **Per-subfolder belt now triggers on absolute row count instead of percentage.** v0.7.22–25's `>50% of subfolder missing` trigger was the wrong signal — it fires on any entirely-deleted folder regardless of size, so deleting a single show (100% of that subfolder gone) still tripped preservation. The belt should only catch **catastrophic loss** (mount unmounted, drive failed) — typically 1000+ files lost from one subfolder. Switching to `≥ SHRINKERR_BELT_MIN_SIZE rows lost in one cycle` (default 1000) lets multi-show deletes, bulk renames, and even full-show wipes (100-500 rows) clean up normally while preserving genuine mount-loss patterns. **Tunable via the existing `SHRINKERR_BELT_MIN_SIZE` env var; semantics changed from "min subfolder size to protect" to "min absolute row-loss to trigger".** ## [0.7.25] — 2026-05-31 ### Fixed - **Per-subfolder belt was over-protective on small folders.** v0.7.22/23 preserved rows whenever a subfolder lost >50% in one cycle — which correctly caught the 20K-file partial-mount loss, but also caught "deleted a movie folder with 1 file" (100% missing for that subfolder) and refused to clean those rows. v0.7.25 adds a `MIN_BELT_PROTECTED_SIZE` threshold (default 5, env-tunable via `SHRINKERR_BELT_MIN_SIZE`): the belt only fires for subfolders with at least 5 known rows. Below that, recovery is trivial (one rescan, handful of files) and cleanup proceeds normally. Catastrophic-loss protection unchanged for large folders. ## [0.7.24] — 2026-05-31 ### Fixed - **Downscaled output filenames now reflect the new resolution.** A 2160p→1080p conversion produced `…2160p UHD x265…` filenames because `get_output_path` (and the disc-equivalent `build_disc_output_filename`) only rewrote the codec tag — the resolution tag passed through unchanged. v0.7.24 adds `rename_resolution_in_filename` which rewrites `2160p` / `1080p` / `720p` / `576p` / `480p` (any pixel-suffix form) plus colloquial `4K` / `UHD` markers when the encoder is downscaling. `target_resolution` is now threaded through both the regular-file and disc-file output-path builders and matches the value actually fed to the encoder. `target_resolution="copy"` (no scaling) leaves the filename's resolution tag alone. ## [0.7.23] — 2026-05-31 ### Fixed - **Manual scans now have the same per-subfolder partial-mount protection as the watcher.** v0.7.22 added a per-subfolder >50%-stale belt to the watcher's auto-removal, but `_scan_worker_process`'s orphan cleanup (the cleanup that runs after a user-initiated scan) didn't have it — a manual scan while a nested mount was pending would still delete every row under the affected subfolder. v0.7.23 mirrors the belt to the scanner side with the same threshold and log shape. **Known trade-off:** legitimate bulk moves now leave stale rows at the old path until manually cleaned from the UI — biased toward not-losing-data given the partial-mount loss has recurred three times. ## [0.7.22] — 2026-05-31 ### Fixed - **Per-subfolder protection against partial-mount stale-row deletion.** v0.7.7 added a global >50% sanity belt that aborts stale-row removal when more than half the library would be flagged stale in one cycle — fixes the unmounted-media_dir case. But that belt misses when a single subfolder under a walked media_dir is the unmounted one: a delayed Synology nested mount under `/media/.../TV1` leaves TV1 empty during the walk, every TV1 row gets flagged stale, and if TV1 is <50% of the library the global belt doesn't trip. v0.7.22 adds a per-subfolder sanity belt: any immediate subdirectory of a walked media_dir losing >50% of its known rows in one cycle is preserved (treated as likely partial-mount). Trigger a manual rescan once the mount is back to refresh those rows. ## [0.7.21] — 2026-05-31 ### Fixed - **Retrying a failed combined-encode job now actually re-runs the convert.** When a combined convert+cleanup job's video re-encode got discarded (e.g. NVENC crash, or the encoded output was larger than the original), the queue spawned an audio-only follow-up to apply the track cleanup without re-encoding. If that follow-up failed and the user clicked "retry", the old code re-ran the same audio-only job — so the source stayed h264 even though the user expected a fresh h265 convert. v0.7.21 escalates type='audio' retries to type='combined' when the source's `scan_results.needs_conversion` flag is still set. Audio-only retries on already-h265 sources stay as-is (legitimate track-cleanup use case). The response now returns `{"escalated": "audio→combined"}` when the upgrade fires. ## [0.7.20] — 2026-05-31 ### Fixed - **Broken watcher tests cleanup.** The v0.7.5–v0.7.8 watcher backfill tests (added under "skip local testing") INSERT'd into `scan_results` with a `file_name` column that doesn't exist and omitted the NOT NULL `file_size` + `scan_timestamp` columns — every one of them raised `OperationalError` immediately. CI never ran pytest so the breakage was silent. All 14 INSERT sites in `backend/tests/test_watcher.py` now use the real schema (drop `file_name`, supply `file_size=0` + sentinel `scan_timestamp`). Full watcher suite (16 tests) and database/audio/converter suites (42 combined) now pass locally. No production code touched. ## [0.7.19] — 2026-05-21 ### Fixed - **"database is locked" failures under concurrent writes.** SQLite's `busy_timeout` is per-connection (defaults to 0 = fail immediately) and doesn't persist with the DB file like WAL does. An audit found 118 of 134 `aiosqlite.connect` call sites didn't set the PRAGMA — every one of those would error instantly the moment a concurrent writer held the transaction. v0.7.19 monkey-patches `aiosqlite.connect` at module load in `backend/database.py` so every new connection applies `PRAGMA busy_timeout=60000` automatically. One surgical change, zero call-site edits, fully backward-compatible. Locked down with three tests against the `await` and `async with` invocation patterns. ## [0.7.18] — 2026-05-21 ### Fixed - **Audio-cleanup / remux failed with `Subtitle codec 94213 is not supported`** on sources containing `mov_text` subtitles (codec 94213 = `AV_CODEC_ID_MOV_TEXT`, typically present when an mp4 was previously remuxed to mkv). The remux path used `-map 0:s?` + global `-c copy`, which Matroska's muxer rejects for mov_text. v0.7.18 probes subtitle codecs upfront and builds per-stream maps so mov_text/tx3g get `-c:s:N srt` (lossless for text content) while everything else still copies cleanly. Main convert path already had this logic — this brings the remux path to parity. ## [0.7.17] — 2026-05-21 ### Changed - **Reworded the corrupt-source-detected message** from "Output file suspiciously small (… bytes vs … bytes) — likely corrupt" to lead with the actual diagnosis: "Original file likely corrupt — conversion produced an unusably-small output (N MB from N.NN GB source). ffmpeg hit data corruption ffprobe doesn't catch upfront. Source row marked corrupt; original preserved." The OUTPUT is the symptom; the ORIGINAL is what's corrupt. Same trigger (v0.7.11's <5%-of-original safety check), same DB side-effects (`health_status` / `probe_status` set on the source row). ## [0.7.16] — 2026-05-21 ### Changed - **Re-queuing an already-converted file gives a clear message** instead of a cryptic "Failed to probe file". When a conversion job's source no longer exists — almost always because it was already converted (the HEVC output replaced the h264 original) and the job is a stale retry or a queue from a pre-conversion scan row — the worker now reports "Source file no longer exists — most likely already converted; rescan to refresh" rather than treating it as a probe failure. ## [0.7.15] — 2026-05-21 ### Fixed - **v0.7.14 broke conversions** with `name 'encode_start_time' is not defined`. The v0.7.14 refactor moved the encode run-loop into a nested `_run_encode` helper, which scoped `encode_start_time` inside it — but the post-encode stats path (encode_seconds / encode_time) reads it from the outer function. Hoisted it to the outer scope and marked it `nonlocal` in the helper, alongside `all_lines`. Added an AST scope check to the dev process to catch this leak class. ## [0.7.14] — 2026-05-21 ### Fixed - **NVENC conversions that crash mid-stream now auto-retry with software decode.** NVDEC silently falls back to CPU decode for frames it can't handle on-GPU (sources with "unknown" colour metadata, certain WEB-DLs), and that mid-stream CUDA→CPU switch ("hwaccel changed") breaks the `scale_cuda` filter graph with `Error reinitializing filters!` (exit 218) — no input flag prevents it (v0.7.12's `-noautoscale` didn't). v0.7.14 detects that specific failure on the NVDEC-native CUDA path and retries the encode once with software decode + NVENC (software decoders handle every mid-stream change; the GPU still encodes). The fast NVDEC path stays the default for the 99% of files that work. Adds CI tests asserting the rebuilt command's structure (no `scale_cuda` on the software path; `-noautoscale` correctly positioned as an output option). ## [0.7.13] — 2026-05-21 ### Fixed - **v0.7.12 broke all conversions** with `exit 234 / Error parsing options for input file … Invalid argument`. The `-noautoscale` flag added in v0.7.12 is an ffmpeg *output* option but was placed before `-i`, so ffmpeg rejected it as a misplaced input option. Moved it after all `-i` inputs (still scoped to the NVDEC-native CUDA path). The v0.7.12 mid-stream-crash fix now actually works. ## [0.7.12] — 2026-05-21 ### Fixed - **NVENC conversion crashed mid-stream on some x264 WEB-DLs** with `Error reinitializing filters! / Impossible to convert between the formats supported by the filter 'Parsed_scale_cuda_0' and the filter 'auto_scale_0'`. ffmpeg auto-inserts an `auto_scale_0` CPU filter to bridge a mid-stream parameter reconfiguration ("hwaccel changed"), but that filter can't accept CUDA frames from our explicit `scale_cuda`. The encode dies after gigabytes of successful output. v0.7.12 adds `-noautoscale` on the NVDEC-native CUDA path so the auto-scaler isn't inserted — our explicit `scale_cuda=format=...` already handles all format alignment the encoder needs. QSV/VAAPI/CPU paths unaffected. ## [0.7.11] — 2026-05-21 ### Fixed - **Files corrupt enough to fail conversion now get flagged as corrupt in the UI** instead of staying labeled healthy. ffprobe sees container headers + first-frames metadata, so a source with corrupt data mid-stream passes the scan-time probe and shows up with "Convert to x265 (est. save N GB)" — until the user tries, the conversion produces a suspiciously-small output (the existing fail-safe catches this and preserves the original), and the row stayed labeled as healthy so the user could try again. v0.7.11 propagates the diagnosis: when the "output too small" check fires, the source row's `health_status` + `probe_status` get set to `corrupt` and the failure details go into `health_errors_json`. UI flags it via the existing isCorrupt indicator on next refresh. ## [0.7.10] — 2026-05-21 ### Changed - **ISO scan rows now display the `.iso` filename** (e.g. `rz0u.iso`) instead of the movie folder name (e.g. `Elephant (2003) [tt0363589]`). Folder discs unchanged (still show the disc-root folder, since the marker basename is useless as a label). Disambiguates folders holding multiple ISOs and matches what the file is actually named on disk. Affects both the scanner write path (`_disc_display_name`) and the API enricher (`_disc_aware_file_name`); `file_name` is computed at API time so existing rows pick up the new label immediately on next page load. ## [0.7.9] — 2026-05-21 ### Changed - **Scanner runs ffprobe in parallel** (default 4 concurrent). Pre-v0.7.9 scans were serial — a 120K-file library took ~12 h on a NUC because every ffprobe runs at ~300 ms. v0.7.9 pre-probes files in `asyncio.Semaphore`-bounded chunks, then runs the existing serial classify+emit loop with cached probe results. ~4× speedup on cold scans, no new DB-write contention with concurrent queue workers (probes are subprocess I/O only; writes still flow through the single-writer batched path). Tune via `SHRINKERR_SCAN_CONCURRENCY` (env var, default 4); set to 1 to restore serial behavior. ## [0.7.8] — 2026-05-21 ### Fixed - **v0.6.5 folder-disc backfill silently matched zero rows** (same JSON LIKE bug v0.7.5 had for BD ISOs, fixed in v0.7.6). New `_backfill_disc_languages_v078` runs the equivalent sweep for folder discs with the corrected selector and a fresh idempotency flag so existing installs re-run. - **Stale corrupt markers persisted after a successful disc re-probe.** The `_clear_stale_disc_health_status` helper only cleared `health_status`, but the UI's `isCorrupt` is `probe_status === 'corrupt' || health_status === 'corrupt'`, so a probe that cleared `health_status` left the "ffprobe couldn't read a video stream" banner showing whenever `probe_status` was also stuck. Helper now resets `probe_status` (to `'ok'`) and `health_errors_json` alongside `health_status` in one statement; v0.7.8 backfill calls the helper on every successful row update. ## [0.7.7] — 2026-05-21 ### Fixed - **Watcher hard-DELETEd rows under temporarily-unmounted media volumes.** The watcher's stale-row pass computed `stale = known_paths - disk_files` across all configured media_dirs as one set. When one media_dir was missing from disk (e.g. running a docker-compose with a subset of volumes for RC testing), every row under it was flagged stale and deleted — recoverable only by a full rescan. v0.7.7 scopes the stale set to media_dirs that exist on disk this cycle, matching the scanner's existing `completed_paths` rule. Adds a sanity belt that aborts stale-removal if >50% of walked-dir rows would be deleted in one cycle. Loudly logs both events. ## [0.7.6] — 2026-05-21 ### Fixed - **v0.7.5 BD ISO backfill silently matched zero rows.** The selector used a SQL `LIKE '%"language":"und"%'` clause (no spaces) but `json.dumps()` defaults to `(', ', ': ')` separators, so production rows always store `"language": "und"` with a space after the colon. The sweep ran on every fresh install, pulled zero candidates, set the idempotency flag, and exited — no log line, no error, no update. v0.7.6 drops the SQL JSON LIKE clause entirely and does all language-shape filtering in Python where the parse is correct regardless of separator style. A new flag name (`iso_lang_backfilled_v076`) re-runs the sweep on installs that already had the broken v0.7.5 flag set. Adds a regression test that constructs fixtures via the production `json.dumps` serializer so this class of bug can't slip past again. ## [0.7.5] — 2026-05-21 ### Added - **One-shot startup backfill for BD ISO language metadata.** v0.7.4 added the libbluray ctypes path that finally extracts languages from UDF-only BD ISOs, but existing scan_results rows that pre-dated the fix kept their stale `und` track tags — users would have had to manually delete each affected row and let it re-discover. v0.7.5 sweeps those rows on first watcher cycle after upgrade: BD ISO rows with all-und (or empty) `audio_tracks_json` are re-probed, re-classified, and written back in-place. DVD ISOs and folder discs are unaffected (their pre-v0.7.4 path already worked); partial-coverage rows (e.g. `[eng, und]`) are left alone per scope. Idempotent via the `iso_lang_backfilled_v075` settings flag. Silent log-only signal (`[WATCHER] v0.7.5 backfill: …`). ## [0.7.4] — 2026-05-21 ### Added - **Language metadata for UDF-only Blu-ray ISOs** (closes v0.7.3's known limitation). bsdtar/libarchive 3.7.2 silently returns empty on UDF 2.50+ BD ISOs, so v0.7.1's fallback didn't actually help the discs it was added for. v0.7.4 adds a tier-3 fallback using libbluray directly via Python ctypes — the same library ffmpeg already uses for `bluray:` protocol encoding, so we know it can read these ISOs. Opens the ISO with `bd_open`, walks titles → clips → streams, reads per-track `lang[4]` from `BLURAY_STREAM_INFO`. ISO 639-2 codes normalized T→B form (`fra`→`fre`, `deu`→`ger`, etc.) to match the rest of the codebase. Adds `libbluray-bin` to both Dockerfiles. Verified against Elephant (2003) UDF-only BD ISO: `audio: [fre, eng]`, `subtitle: [fre, fre]`. ## [0.7.3] — 2026-05-21 ### Fixed - ISO disc rows showed the media_dir name (e.g. `Movies2`) instead of the movie-folder name in the scanner UI. v0.7.2 fixed `_disc_display_name` in the scanner/watcher write paths, but `file_name` is computed on-the-fly by `backend/routes/scan.py:_disc_aware_file_name` (the API enricher) and isn't stored in `scan_results`. The enricher used `parts[-3]` for all disc types, which for ISO inputs (`file_path` IS the .iso) points one level too high. Now branches on `.iso` suffix → uses `parts[-2]`. ### Known limitations - **UDF-only Blu-ray ISO language metadata.** v0.7.1 added a bsdtar (libarchive) fallback for ISOs pycdlib can't open. Real-world testing showed libarchive 3.7.2 still can't list contents of some UDF 2.50/2.60 BD ISOs (returns empty). For those discs, language metadata falls back to `und` and the user manually selects tracks. The encoding path works fine via ffmpeg's `bluray:` protocol (libbluray reads these ISOs cleanly). v0.7.4 candidate: use `libbluray-bin` tools (`bd_info`, `mpls_dump`) which share the same libbluray that already works for encoding. ## [0.7.2] — 2026-05-21 ### Fixed - ISO disc rows showed the media_dir name (e.g. `Movies2`) instead of the movie-folder name in the scanner UI, and reported the **main-title bytes** (~19 GB) instead of the **ISO file's actual bytes** (~30 GB) for size. Two bugs from v0.7.0's ISO-folder handling: (1) `display_name` used `parent.parent.name` unconditionally, which for ISO inputs (where `file_path` IS the .iso file) points one level too high; (2) the `file_size` patch in `probe_file` only fired for folder discs via `_disc_total_size`, so ISO rows kept ffprobe's `format.size` which only counts the main title libbluray surfaces. New `_disc_display_name` helper handles both folder and ISO cases; `probe_file` now also stats the ISO file directly for accurate on-disk size. ## [0.7.1] — 2026-05-21 ### Added - **Language metadata for UDF-only Blu-ray ISOs.** Some BD ISOs ship without an ISO 9660 layer (BD-Video spec doesn't require one), which pycdlib rejects with "Valid ISO9660 filesystems must have at least one PVD". v0.7.1 adds `libarchive-tools` (`bsdtar`) to the image and a bsdtar-based fallback for sidecar extraction: when pycdlib can't open an ISO, the parsers enumerate `/BDMV/PLAYLIST/*.mpls` (or `/VIDEO_TS/VTS_NN_*.VOB`) via bsdtar, extract the relevant bytes, and feed them to the existing `_parse_bdmv_mpls_bytes` / `_parse_dvd_ifo_bytes` parsers. Real-world BD ISOs (UDF 2.50/2.60) now produce real language codes instead of `und`. DVD ISOs continue using the fast pycdlib path. ### Fixed - Disc rows inherited stale `health_status='corrupt'` after re-discovery. When a previous conversion's post-source-handling deleted `VIDEO_TS/` or `BDMV/`, a concurrent health check would mark the row corrupt; restoring the source folder later caused the watcher to re-INSERT the row with the stale flag still attached. Scanner and watcher now explicitly clear `health_status` on successful disc re-probe (`disc_type IS NOT NULL` only — non-disc rows untouched, file-level health checks unaffected). ## [0.7.0] — 2026-05-21 ### Added - **DVD and Blu-ray ISO file support.** `.iso` files containing VIDEO_TS or BDMV structures are now first-class scan items alongside the v0.6.x unpacked folder support. ffmpeg reads the ISO directly via `-f dvdvideo -i /path.iso` (DVD) or `bluray:/path.iso` (BD) — no kernel mount, no extraction, no Dockerfile changes. Output MKV lands in the ISO's parent folder (or alongside the ISO when it's loose at a media_dir root), with the same scene-style naming + metadata-ID strip applied as folder discs. Post-conversion source-handling unlinks / trashes / backs-up the ISO file per the existing setting. Non-video ISOs (Linux installers, games) are silently ignored. **Limitation:** DVD ISO language metadata is fully extracted via pycdlib + IFO parser; BD ISO language metadata works for ISOs that carry an ISO 9660 + UDF layer but falls back to `und` for UDF-only BD ISOs (a known pycdlib limitation — Blu-Ray-spec doesn't require ISO 9660). The disc still converts correctly; the user just manually selects which audio/subtitle tracks to keep. Classification falls back to ffmpeg's `bluray:` protocol for UDF-only ISOs so they're correctly identified as BDs. ## [0.6.8] — 2026-05-21 ### Fixed - Disc conversion output filenames inherited the parent folder's metadata-ID tags — e.g. `Elephant (2003) [tt0363589] 1080p Bluray EAC3 5.1 h265.mkv` instead of the convention `Elephant (2003) 1080p Bluray EAC3 5.1 h265.mkv`. IDs belong on FOLDERS (for *arr cataloguing) but not duplicated into the file name. `build_disc_output_filename` now strips `[tt...]`, `[imdb-...]`, `[tmdb-...]`, `[tvdb-...]`, and `{tmdb-...}` / `{tvdb-...}` forms from the filename token while leaving the folder structure unchanged. Folders that don't carry an ID tag are unaffected. ## [0.6.7] — 2026-05-21 ### Fixed - Estimated savings shown in the file-detail panel (e.g. "Convert to x265 10-bit (est. save ~8.4 GB)") used a stale 0.30 reduction default while the queue-estimate modal used a CQ-calibrated empirical curve — producing wildly different numbers for the same file. Both now route through a single `backend/encoding_estimates` helper that applies the CQ curve calibrated against real NVENC results. Scan-time `estimated_savings_bytes` + new `video_conv_savings_bytes` column reflect the user's global CQ setting. Existing rows auto-backfill on first watcher cycle. ## [0.6.6] — 2026-05-20 ### Fixed - DVD IFO parser missed languages on discs where the audio/subp count byte was 0 but the attr entries were populated (a known authoring-tool quirk libdvdread has documented fallback for) — e.g. Fast-Walking (1982) surfaced its English track as `und`. Parser now falls back to scanning attrs for the first all-zero gap when the count byte is 0; positive counts are still trusted as-is. ## [0.6.5] — 2026-05-20 ### Added - **Disc track language detection.** Hand-rolled binary parsers for DVD `VTS_NN_0.IFO` and Blu-ray `.mpls` files now extract per-track language codes and patch them onto disc probe results before classify_audio_tracks runs. Discs no longer surface every audio/subtitle track as `und`; your existing `always_keep_languages` filter now selects correct tracks. Existing disc rows auto-backfill on first watcher cycle (idempotent via `settings.disc_lang_backfilled_v065`); the backfill runs the full classify pipeline so re-tagged rows carry the same schema as freshly-scanned ones. Parser failures fail open — disc still gets added with `und` tracks and a `[DISC-META]` warning logged. ## [0.6.4] — 2026-05-20 ### Fixed - Disc conversions reported as **failed despite succeeding**. After the encode landed and post-source-handling deleted the `VIDEO_TS/` (or `BDMV/`) subdirectory per the user's delete/trash setting, `rename_external_subtitles()` ran on the now-vanished parent and raised `FileNotFoundError`, which propagated up as a job failure. The encoded MKV was intact — only the job status was wrong. Helper now no-ops cleanly when the parent dir is missing (discs don't have sidecar subs anyway; their subtitles are internal PGS/VobSub streams). Also defends against any future race where the source disappears between encode and rename. ## [0.6.3] — 2026-05-20 ### Fixed - Disc rows sorted as "oldest" in date-based views because `os.path.getmtime` on the marker file (`VIDEO_TS.IFO` / `BDMV/index.bdmv`) returned the original DVD/BDMV authoring timestamp — often decades old — instead of when the user added the disc to their library. Both scanner and watcher now use the disc-root folder's mtime for `file_mtime` on disc rows, which reflects the actual "added to library" time. Regular file rows unchanged. ## [0.6.2] — 2026-05-20 ### Fixed - **DVD folder conversion (v0.6.0)** was non-functional: the design used a fictional `dvd:` ffmpeg input protocol that doesn't exist (the real libdvdread integration in ffmpeg is the `dvdvideo` demuxer, which only accepts ISO/block-device input, not folders). Every DVD probe failed with `Protocol not found`, the watcher silently shelved the path into an in-memory failure set, and no disc ever surfaced. v0.6.2 replaces it with ffmpeg's `concat:` protocol over the main-feature VOBs (identified by largest VTS_NN_1..N total size, excluding the VTS_NN_0 menu chunk). Verified end-to-end against a real disc (`Fast-Walking (1982)`, NTSC, 116 min, MPEG-2 + AC-3). Watcher now logs disc-probe failures loudly instead of dropping them on the floor. Blu-ray (`bluray:` protocol via libbluray) was already a real protocol and is unchanged. ## [0.6.1] — 2026-05-20 ### Fixed - Watcher not auto-discovering newly-dropped DVD `VIDEO_TS/` or Blu-ray `BDMV/` folders. The polling walk filtered files by video extension before the v0.6.0 disc-marker pre-pass could see them, so `.IFO` / `.VOB` / `.m2ts` were dropped at the walk stage and disc folders never registered. Watcher walk now classifies each directory for disc structure first (matching the scanner), records the marker file, and prunes recursion into the disc subdirectory. Manual rescan was unaffected. ## [0.6.0] — 2026-05-20 ### Added - **DVD and Blu-ray folder support.** Raw `VIDEO_TS/` and `BDMV/` folder structures are now scannable items: Shrinkerr detects them automatically, picks the longest title via ffmpeg's `dvd:/` / `bluray:/` protocols (libdvdread/libdvdnav/libbluray, already in the image), and transcodes that title to HEVC. Output is a scene-style MKV in the disc's parent folder; the original `VIDEO_TS/` or `BDMV/` subdirectory follows your existing post-conversion source-handling setting (delete / trash / backup). Combo discs with both VIDEO_TS and BDMV convert as Blu-ray. Main-feature only — extras and menus discarded. ## [0.5.26] — 2026-05-20 ### Fixed - Conversion failing on **10-bit H.264 sources (Hi10p, `yuv420p10le`)** with `ffmpeg exited with code 218` and "Impossible to convert between the formats supported by the filter 'graph -1 input from stream 0:0' and the filter 'auto_scale_0'". Pascal NVDEC (and older) don't support 10-bit H.264; QSV decode is 8-bit-only for H.264; most VAAPI drivers don't either. Pre-v0.5.26 our codec gate only checked the codec name (`h264` ∈ supported set → True), so the cmd builder emitted `-hwaccel cuda -hwaccel_output_format cuda` and a `scale_cuda=format=p010le` filter. ffmpeg then tried NVDEC, silently fell back to software decode (good), but the now-CPU frames couldn't bridge to the still-present CUDA filter (bad). Extended `hw_decode_supports()` to also check `source_pix_fmt`: any H.264 source with a `p10` / `10le` / `p12` / `12le` pix_fmt now takes the pure-software-decode path on all three HW backends. 10-bit HEVC is unaffected (Pascal+ NVDEC, Gen11+ QSV, and most VAAPI drivers handle it fine). Worker log now also reports the source pix_fmt alongside the codec when HW decode is skipped. ## [0.5.25] — 2026-05-19 ### Fixed - Dashboard "Source Types" pie and "Avg Reduction by Source" under-counted Remux jobs (showed only 1 even with many remuxes converted). Two compounding causes: (1) `_source_type()` checked `BluRay` before `Remux`, so scene-named files like `…1080p.BluRay.Remux.AVC…` always bucketed as Blu-ray; (2) v0.5.5's `rename_source_to_target_codec` strips the `Remux` tag post-conversion, so even with a fixed order the current `file_path` no longer carried the marker. Now categorizes Remux first, and reads the source-type from `original_file_path` when available (falling back to `file_path` for legacy jobs). ## [0.5.24] — 2026-05-19 ### Fixed - Audit pass for the v0.5.23 silent-fail pattern (unchunked SQL `IN (…)` / OR'd `LIKE` clauses): chunked the remaining at-risk sites that could exceed SQLite's variable / expression-depth limits with 1000+ items. Fixed: `queue_health_checks` folder→file expansion (same bug as v0.5.23 in the Scanner's bulk Health Check button), `rule_resolver.resolve_rules_for_batch` (called from estimate/queue add with the full user selection), `bulk job priority update`, `batch rename probe load`, `poster cache batch lookup`, watcher's `stale_paths` DELETE. Behaviour unchanged for typical selections; only adds resilience at scale. ## [0.5.23] — 2026-05-19 ### Fixed - Scanner's "Select all + Add to queue" with 1000+ folders silently returned `0 Files to process` in the estimate modal. The folder→file expansion built one OR'd LIKE clause per folder in a single SQL query — with ~1000+ folders the expression depth blew past SQLite's `SQLITE_LIMIT_EXPR_DEPTH` default of 1000 and the SELECT returned no rows. Chunked the expansion to 800 folders per query in both the estimate and add-from-scan paths. ## [0.5.22] — 2026-05-19 ### Fixed - Watcher's auto-queue path used the pre-v0.5.4 `is_x264()` codec check — only H.264 was treated as "needs conversion", so MPEG-2 / MPEG-4 / VC-1 / WMV files auto-discovered via filesystem watching never got a `convert` job even when those codecs were in the user's source_codecs list. Now uses `codec_matches_source(video_codec, source_codecs)` like the scanner and webhook paths. HEVC files were unaffected (correctly skipped either way). ## [0.5.21] — 2026-05-18 ### Fixed - "Merge external subtitles into video" and "Delete external subtitle files after merging" toggles weren't persisting — both fields existed in the UI but had zero wiring through `models.py` / `_ENCODING_DEFAULTS` / GET / PUT (same shape as the `reorder_native_audio` bug from issue #11). The worker's `_is_cleanup_enabled` missing-row fallback defaulted True, so external-sub merging happened invisibly even when the UI showed the toggles off. Wired both fields end-to-end, defaulted both off, and switched the worker's reads to `default=False` so unset DB rows match the UI's rendering. **Behaviour note**: existing installs that never explicitly toggled these were getting silent merging — after upgrade, merging stops unless the user opts in. ## [0.5.20] — 2026-05-18 ### Fixed - Subtitle tracks in the file's native language were kept by default even when they weren't in "Keep Subtitle Languages" — German subs on a German-audio movie etc. Pre-v0.5.20 audio and subs shared the `keep_native_language` rule, which makes sense for audio (keep the original audio track) but is rarely useful for subs. Split into a separate **"Auto-keep native language subtitle tracks"** toggle (Settings → Subtitles), default **off**. Existing users who relied on the old behaviour (SDH on same-language audio) can opt back in. - Existing audio toggle label clarified from "Auto-keep native language tracks" to "Auto-keep native language **audio** tracks" — same setting, just no longer ambiguous about which media type it controls. ## [0.5.19] — 2026-05-18 ### Fixed - Conversion failing with `ffmpeg exited with code 234` / `Subtitle encoding currently only possible from text to text or bitmap to bitmap` on files with a multi-stream external sidecar subtitle (notably VobSub `.idx`/`.sub` pairs carrying multiple language streams in one file). The cmd builder emitted `-map 1:s` to pull every sub stream from each external input but only set a codec for the first output sub-stream index per file — ffmpeg picked its matroska default (`ass`/`ssa`, a text codec) for the remaining streams and tried to encode dvdsub bitmap → ass text. Added a catch-all `-c:s copy` after the per-stream specs so any unset external sub stream defaults to byte-copy; the per-stream specifiers above still win for the streams they name (webvtt → srt path preserved). ## [0.5.18] — 2026-05-18 ### Fixed - Disc-tier source tags now get normalized in the post-conversion filename: `BR-DISK` / `BD25` / `BD50` / `BD100` → `Bluray`, `DVD-R` / `DVDR` / `DVD5` / `DVD9` → `DVDRip`. The re-encoded file is no longer a disc rip, so the tag should reflect what the file actually is. Already-encoded forms (`Bluray`, `DVDRip`, `WEB-DL`, `HDTV` etc.) are left alone, as are unrelated tokens (`DVD-RW`, `DVD-RAM`). Scanner sibling-detection updated to match the new naming chain so disc-tier originals get skip-flagged when their re-encoded sibling already exists. ## [0.5.17] — 2026-05-18 ### Added - "Keep only the best track per always-keep language" toggle in Settings → Audio. Default on (preserves v0.5.16's smart-selection behaviour); turn off to keep every track in always-keep languages, pre-v0.5.16 style. Per-track overrides via the file editor still work in either mode. ## [0.5.16] — 2026-05-18 ### Changed - "Always Keep Languages" no longer locks every track in those languages. When a file has multiple tracks in the same always-keep language (e.g. 3 English tracks: EAC3 5.1, AAC 2.0, AAC commentary), only the highest-quality one — ranked by channels desc then codec quality (TrueHD > FLAC/PCM > DTS-HD MA > DTS > EAC3 > AC3 > AAC) — is kept by default; the others fall through to the standard rules. All tracks render as editable checkboxes so users can override per-track, even on always-keep languages. **Behaviour change for users with multi-track libraries**: re-scanning will mark redundant same-language tracks for removal. Issue #11. ## [0.5.15] — 2026-05-18 ### Fixed - "Reorder native language to first audio stream" toggle was entirely cosmetic — the field had no model definition, no DB default, no GET wiring, and no PUT branch, so saves were silently dropped and the worker's missing-row fallback always returned True. Now fully wired through the standard model → DEFAULTS → GET → PUT → cache-invalidation pipeline (issue #11). Default preserved as on; users who want the native track left in place can now actually turn it off. - `keep_native_language` save did not invalidate the worker's in-memory cache, so changes only took effect after a container restart. Added to the cache-invalidation set alongside `reorder_native_audio`. ## [0.5.14] — 2026-05-14 ### Fixed - NVENC + NVDEC jobs failing with `CUDA_ERROR_INVALID_VALUE` on x264 sources with many reference frames. NVDEC's 32-surface hardware limit was exceeded because the H.264 decoder's thread count (defaulting to `nproc`) drove surface allocation past 32. Pin decoder threads to 1 whenever NVDEC is on — encoder threads still respect the `FFmpeg Threads Per Job` setting. ## [0.5.13] — 2026-05-14 ### Changed - Image hardening pass: `apt-get upgrade` at build time, NVENC base bumped from CUDA 12.3.1 / Ubuntu 22.04 to CUDA 12.6.3 / Ubuntu 24.04 (and Python 3.12). Same NVIDIA driver floor (525.60.13+). Knocks Docker Scout's High/Medium CVE counts down materially without changing runtime behaviour. ### Added - `pip-audit` CI workflow scans `requirements.txt` weekly + on PRs that touch it. Informational (non-blocking) — surfaces vulnerable Python deps in the Actions tab. ## [0.5.12] — 2026-05-14 ### Fixed - "Saved by Library" no longer shows two rows for case-differing historical paths (e.g. `downloads` + `Downloads` from a directory rename). Fallback labels are normalized to start with a capital when entirely lowercase; mixed-case names (M2T2, TV1, user-set labels) untouched. ## [0.5.11] — 2026-05-14 ### Fixed - Dashboard "Saved by Library" now uses `MediaDir.label` (set via Settings) and falls back to the same `/media//...` volume-name logic the disk-space card uses — labels now match between the two cards. **Side effect**: nested MediaDirs (e.g. `/media/Misc/tv`) without an explicit label merge into the parent volume's row. ## [0.5.10] — 2026-05-11 ### Fixed - NVENC+NVDEC: drop `-pix_fmt` from encoder — `scale_cuda=format=X` already sets the surface format; leaving `-pix_fmt` made ffmpeg try and fail to bridge CUDA→CPU. ## [0.5.9] — 2026-05-11 ### Added - NVENC bit-depth dropdown (10-bit / 8-bit / Match source). 8-bit unblocks Maxwell cards; Match source picks per-job from probed pix_fmt. ### Fixed - `-threads N` emitted post-encoder too — pre-input only capped decoder threads, leaving libx265 encode uncapped. ## [0.5.8] — 2026-05-11 ### Fixed - NVENC+NVDEC jobs failed on first encode. Added `scale_cuda=format=p010le` to convert nv12 CUDA surfaces to 10-bit on-GPU. ## [0.5.7] — 2026-05-11 ### Added - Hardware decode (NVDEC / QSV / VAAPI) paired with each encoder, plus opt-in libx265+NVDEC mixed mode. Native pairs default on; unsupported codecs fall back silently to software. - Capability probe via `ffmpeg -hwaccels`; each toggle gated on matching encoder + backend availability. ### Changed - VMAF is skipped on hardware-decoded jobs (needs software-decoded source frames). Surfaced via warning chip, per-toggle help text, and worker log. ### Fixed - VAAPI native-pair filter chain no longer emits redundant `format=nv12,hwupload`. - Remote workers now receive the 4 HW decode settings via the job payload. ## [0.5.6] — 2026-05-11 ### Added - `FFmpeg Threads Per Job` Settings slider (caps `-threads N`). Default `0` = ffmpeg auto. ### Changed - Auth UX: Enable toggle moved to left side as a labelled chip, username/password fields always rendered (disabled when off), green active-auth banner. - README troubleshooting entry with `sqlite3` one-liners to recover from auth lockout without nuking the data volume. ## [0.5.5] — 2026-05-11 ### Fixed - Filename rename after conversion now covers all non-h264 sources (MPEG-2, MPEG-4, XviD, DivX, VC-1, WMV, VP9). Was only matching x264/h264/AVC. ## [0.5.4] — 2026-05-11 ### Fixed - `scan_results.needs_conversion` recomputes when `source_codecs` setting changes (PUT handler + one-shot heal on upgrade). Existing rows scanned under a narrower source list were stuck at `needs_conversion=0`. ## [0.5.3] — 2026-05-11 ### Fixed - Date Added rule condition now appears in the rule-builder dropdown (v0.5.1 wired the backend but missed the hardcoded `