﻿## v0.9.22

### Fixed: Image Cache settings hint showed the current path instead of the actual default

**The "Default:" label under Settings → Image Cache echoed `EffectiveImageCachePath`**, which resolves to whatever path is currently set — so once a user picked a custom folder, the label just repeated that custom folder back at them labeled "Default," which is meaningless. Added `AppSettings.DefaultImageCachePath` (the real static `%APPDATA%\AbigailsMediaRenamer\art_cache` fallback) and pointed the hint at that instead, with clearer wording ("Default (used when this is left empty): ...").

---

## v0.9.21

### Renamed "Automatically repackage MP4 files as MKV" setting to "Convert Incoming Video Files to MKV"

**The label and description in Settings → Transcode still said "MP4" even though the underlying `ConvertToMkvService.RemuxableExtensions` list was widened to `.mp4/.avi/.mov/.wmv/.m4v` a while back** — the feature already converted anything not already MKV, the wording just hadn't caught up. Updated the checkbox text and helper text in `SettingsWindow.xaml` to match, and renamed the backing property `AppSettings.AutoRemuxMp4ToMkv` → `AutoConvertToMkv` (and its two call sites in `AutoRenameService.RenameAndMoveAsync`/`RenameService.RenameAsync`) so the name doesn't lie either. No functional change to the conversion logic itself. Note: renaming the property changes its JSON key in `settings.json`, so the saved on/off state resets to the default (off) once — a deliberate tradeoff since there's no back-compat need here.

---

## v0.9.20

### Added automated test coverage for the pure-logic parsing/scoring/path-building services

**New `AbigailsMediaRenamer.Tests` xUnit project** covering `ParserUtilities` (JaroWinkler, non-video title cleaning/scoring, audiobook duration/candidate scoring, tag completeness), `MediaParser` (movie/TV title cleaning, casing, storage-name formatting), `TvParser`/`MovieParser`'s `ScoreConfidence` formulas, `NamingConventionService` (path building, live preview, template validation), and `DirectoryCacheService` (fuzzy folder-name matching). 191 tests, deliberately adversarial sample data (mixed dot/underscore/space separators, boundary confidence values, colon-vs-comma title conventions, tiny/substring/duration-mismatched audiobook candidates, malformed templates) chosen specifically to catch the shape of bug that's shipped before rather than just happy-path cases. Added `.slnx` entry, `AbigailsMediaRenamer/AssemblyInfo.cs` `[InternalsVisibleTo("AbigailsMediaRenamer.Tests")]`, and bumped a handful of `private` scoring/normalization methods to `internal` specifically so the suite can exercise them directly (`TvParser`/`MovieParser.ScoreConfidence`, `DirectoryCacheService.ComputeMatchScore`/`NormalizeForComparison`) — no behavior changes, visibility only. `MediaParser.FormatTitleForStorage` had its logic extracted into a new `internal static` overload taking `movePrefixesToEnd` as a parameter, since the instance method otherwise required constructing a full `MediaParser` (and transitively `MetadataProviderRegistry`, which reads/writes the real `%APPDATA%` settings file — not something a test run should touch).

### Fixed: comma-article title convention ("Walking Dead, The - Dead City") didn't fuzzy-match its colon-subtitle equivalent

**Found by the new test suite, not a user report.** The v0.9.15 fix added leading- and trailing-article stripping to `DirectoryCacheService.NormalizeForComparison` specifically so `"Walking Dead, The - Dead City"` (on-disk storage convention) and `"The Walking Dead: Dead City"` (freshly-parsed title) would normalize to the same string and fuzzy-match as the same folder. They didn't, for exactly the scenario the fix targeted: once the generic punctuation-to-space pass converts the comma and dash to spaces, `"The"` ends up in the *middle* of the string (`"walking dead the dead city"`), past where either the leading or trailing article regex can reach it — only a `the`/`a`/`an` at a true string boundary gets stripped. Fixed by adding an explicit `,\s*(?:the|a|an)\b` strip that removes the comma-article pair as a unit *before* the generic punctuation pass runs, so it no longer depends on where the article happens to land afterward.

---

## v0.9.19

### Fixed: movie title cleaning left fragments like "DDP5 1 H 264" behind instead of stripping them

**Scene tags and dotted quality tokens (`DDP5.1`, `H.264`, the built-in `5.1`/`7.1` entries, or any custom tag a user adds with a literal `.` in it) were being matched against the title AFTER punctuation had already been normalized to spaces.** `MediaParser.CleanMovieTitle` converted `.`/`_` separators to spaces before running the scene-tag stripping loop, so a tag pattern like `5\.1` (from `Regex.Escape("5.1")`) no longer had a literal dot to match against — by that point the text read "5 1", not "5.1" — leaving mangled fragments in the cleaned title instead of removing them. Moved the dot/underscore-to-space normalization to run after scene-tag stripping (and the release-group suffix strip) instead of before, so tags are matched against the original text while their internal punctuation is still intact. TV title cleaning (`CleanTvTitle`) doesn't do scene-tag stripping at all, so it wasn't affected.

---

## v0.9.18

### Audiobook matching, round 2 — adopted AudiobookShelf's actual techniques (still no AI)

**Investigated AudiobookShelf's real matching source** (`BookFinder.js`, `Audible.js`, `Audnexus.js`, `scandir.js` from github.com/advplyr/audiobookshelf, not just a summary) at the user's request, since ABS finds audiobooks far more reliably than this app did even after last session's fixes.

**Search now hits Audible's own catalog directly, not just Kindle-store scraping.** Found that ABS's "Audible integration" is nothing more than Audible's own public, unauthenticated catalog endpoint (`api.audible{tld}/1.0/catalog/products`) — no partner API, no secret key, the same endpoint Audible's own site uses. Our primary search source was `KindleSearchClient` scraping Amazon's *Kindle e-book* store results (not audiobook-specific, fragile HTML parsing). Added `AudibleCatalogClient` hitting that same endpoint as the new primary search tier in `AudiobookProvider`, with each returned ASIN immediately enriched via the existing Audnexus client (gives every candidate full data, including duration, right from search). Kindle-scrape and Audnexus-search are now fallback tiers 2 and 3, unchanged otherwise.

**Duration is now part of match confidence, weighted 70%.** ABS's `calculateMatchConfidence` blends `0.7×duration + 0.2×title + 0.1×author` — duration dominates because recording length is close to a fingerprint for a specific edition, where title/author text can't distinguish abridged vs. unabridged, wrong-but-similar-titled books, or omnibus editions. Added `ParserUtilities.ScoreDurationMatch` (same piecewise-linear curve ABS uses) and blended it into `ScoreAudiobookCandidate` with the same weights when both the local file's duration (read via TagLib) and a candidate's runtime are known — falls back to last session's title/author-only blend otherwise. Guarded against a real gotcha: a single file's duration is only meaningful for a consolidated single-file audiobook (typically `.m4b`) — for a folder split into many chapter/part files, one file's duration has nothing to do with the book's total runtime, so that case is detected and duration scoring is skipped for it.

**Folder-derived author guesses are now validated instead of trusted blindly.** Added `AudnexusClient.ValidateAuthorAsync` (mirrors ABS's `Audnexus.findAuthorByName`) — checks a guessed author name against Audnexus's real author-search endpoint before using it as a search filter; a bad guess (e.g. a series name mistaken for an author) now gets dropped instead of polluting the search. Tag-sourced authors are already trustworthy and skip this.

**Structured folder-name tokens**, mirroring ABS's `scandir.js` grammar: trailing `{Narrator}`, a leading `(YYYY) - ` / `YYYY - ` published year, and a leading `Book N -` / `Vol. N` / bare-number series sequence — each now extracted from the folder name as fallback signals (last session only used the whole folder name as a title fallback). Caught and fixed a regex edge case during implementation where a plain year-like title (e.g. "1984") would have been misparsed as a bogus sequence number — the sequence pattern now requires an explicit `.`/` - ` separator after the number, matching ABS's own documented exclusion rule.

**Small scoring guards**: a substring short-circuit (if the query is literally contained in the candidate title/author or vice versa, treat as a strong match rather than trusting edit-distance alone) and a tiny-query guard (titles under 5 characters now require an exact match rather than a fuzzy one) — both taken directly from `BookFinder.js`'s `filterSearchResults`.

Deliberately not adopted: ABS's full multi-provider fan-out (Google/iTunes/OpenLibrary/FantLab/AudiobookCovers — FantLab is Russian-only, AudiobookCovers is cover-art-only and out of scope this round) and its combinatorial fuzzy title×author retry cascade (too large/risky a change for uncertain marginal benefit now that the primary search-source mismatch is fixed).

---

## v0.9.17

### Improved audiobook identification/matching quality (no AI involved)

**Investigated `philipvox/audiobook-tagger-web`, an AI-based AudiobookShelf metadata cleaner, at the user's request** ("our audiobook lookups are REALLY not great"). It turned out to be a post-identification cleanup tool (assumes AudiobookShelf already matched the book, then uses GPT/Claude to reconcile fields) rather than a better matcher — not adopted, and no LLM/AI dependency was added, per explicit instruction. Comparing its prompt-engineered rules against our own `AudiobookParser`/`ParserUtilities` did surface several concrete, non-AI gaps that were fixed:

**Search results were never re-ranked — we always took the first hit.** `MetadataService.SearchAudiobookAsync` returns a full candidate list, but `AudiobookParser` blindly used `hits[0]` regardless of whether its author matched the file's tags, so a same-ish-titled book by a different author could win over the actually-correct match. Added `ParserUtilities.ScoreAudiobookCandidate` (title similarity blended with author similarity when both sides have one) and score every returned candidate instead of just the first.

**Junk-suffix stripping was thin.** `CleanNonVideoTitle` (shared by Audiobook/Book/Music/Comic parsers) only stripped leading track numbers — now also strips "(Unabridged)", "(Abridged)", bitrate tags (128/256/320kbps), "(MP3)", "(M4B)", "(Full Cast)", "(Complete)", "(HQ)" in bracketed or bare form.

**Narrator was never read from the filename.** Added "Read by X" / "Narrated by X" / "Performed by X" extraction from the raw filename as a last-resort fallback (ASIN and API-sourced narrator data still take priority).

**Folder structure was never consulted.** Audiobook releases are commonly organized `Author/Book Title/file.m4b` or `Author/Series/Book Title/file.m4b`. When the embedded tag title is missing or generic ("Track 1", "Audiobook", "CD 2", a bare number), the parser now falls back to the folder name; when no author tag exists, the grandparent folder is used as a soft signal for search/scoring only — never overwrites real tag data.

**Series names could drift across books in the same series.** Added `AudiobookParser.NormalizeSeriesName`, which checks the library for this author's other known series spellings (reusing the `MovieParser`/`TvParser` precedent of a parser opening its own `MediaDbContext`) and normalizes a newly-fetched series name to the already-established one when they're a near-exact Jaro-Winkler match but not identical — so book 3 doesn't end up with a subtly different series string than books 1-2.

---

## v0.9.16

### Comprehensive Kodi/Jellyfin metadata export pass for Movies and TV

**Poster art now writes `poster.jpg` for Kodi setups too, not just Jellyfin.** Art always downloaded to `cover.jpg`, and was only ever copied to the Kodi-standard `poster.jpg` filename when a Jellyfin-specific setting was on — Kodi-only setups never got recognized poster art at all. `NfoWriterService` now writes `poster.jpg` whenever Kodi export is enabled, independent of the Jellyfin setting.

**Auto-rename NFOs now carry full metadata instead of a bare stub.** The automatic watcher pipeline previously wrote `movie.nfo`/`tvshow.nfo` straight from the parsed filename — no rating, director, IMDB id, or cast/crew — even when the database already had all of that from a prior manual fetch. It now looks up the just-saved database row and reuses the same full export path the manual "Export NFO" action uses.

**Added an "auto-download art during auto-rename" option (off by default).** Previously art only ever downloaded via manual Library Manager actions — the watcher never touched poster/fanart/logo/banner/clearart on its own. New Settings → Metadata Export toggle wires the watcher into the same art pipeline.

**Fixed TV shows and movies identified via TVDB never getting an IMDB id or cast.** Only the TMDB identification path populated either — the TVDB path silently left both empty. Added TVDB's `remoteIds`/`characters` data to close the gap.

**Added per-episode `.nfo` export — the biggest gap.** No `.nfo` was ever written for individual episodes anywhere in the app, so episode synopsis/air-date/rating already sitting in the database, and guest-star/episode-crew data TMDB/TVDB can provide, never reached Kodi/Jellyfin at all. Two new settings (both off by default): one writes a Kodi `<episodedetails>` `.nfo` next to each episode during auto-rename and library backfill; the other opts into an extra per-episode TMDB/TVDB request for guest stars, episode director/writer, and the episode thumbnail image — both providers now supported, not just TMDB.

**Wired up a dead setting.** `ExportIncludeCast` existed in Settings but was never actually read anywhere — cast now genuinely gets included or excluded from NFOs based on it.

**Rounded out remaining NFO fields.** Original title, tagline, MPAA/content certification, keywords, trailer, and movie collection/set data — all things TMDB already returns but the app previously never requested or stored — now flow through to `movie.nfo`/`tvshow.nfo` when available. Deliberately did *not* add non-standard tags Kodi doesn't recognize (producer/composer/cinematographer/editor credits, sorttitle, outline, country, userrating, votes, top250) — that data stays in the database/UI but isn't invented as NFO XML nothing will read.

---

## v0.9.15

### Fixed: colon-subtitled TV/movie titles (e.g. "The Walking Dead: Dead City") didn't match their existing library folder, creating a mangled duplicate instead

**Reported: an existing folder `Walking Dead, The - Dead City (2023)` was ignored for new episodes of "The Walking Dead: Dead City"** — the app instead created `Walking Dead Dead City, The (2023)`, with "Walking Dead" and "Dead City" mashed together and "The" stranded at the very end. Two independent bugs compounded.

**`MediaParser.FormatTitleForStorage` moved a leading article ("The"/"A"/"An") to the end of the entire title string without first splitting off a colon-separated subtitle.** For "The Walking Dead: Dead City" this produced `"Walking Dead: Dead City, The"` — the colon (not a valid filename character) then got silently stripped by `NamingConventionService.SanitizeComponent`, collapsing the double space left behind and mashing the two title segments together with no separator at all. Fixed by splitting off the `": Subtitle"` portion first, moving the article only on the base title, then rejoining with `" - "` — reproducing the existing on-disk convention (`"Walking Dead, The - Dead City"`) instead of destroying it.

**`DirectoryCacheService.FindBestMatchingDirectory` (the "does an existing series folder already exist?" check used before building a new one) normalized names via a completely separate, simpler function that only stripped a leading article and no punctuation at all.** So `"walking dead, the - dead city"` tokenized into `dead,`/`-`/`the` fragments that didn't line up with the (already-mangled) query, dropping the match score below threshold even before the bug above is accounted for. `NormalizeForComparison` now also strips `,`/`:`/`-` before word-splitting and strips a trailing `"... the"`/`"... a"`/`"... an"` in addition to a leading one, so both the `"The X: Y"` and `"X, The - Y"` conventions normalize to the same word set.

Both fixes are in shared, single-implementation functions used for movies and TV alike, so movies with colon subtitles (e.g. "Kill Bill: Vol. 1") get the same fix.

---

## v0.9.14

### Added: "Fetch Metadata…" button for Movies and TV, automatic TV episode-detail backfill, and wider Convert-to-MKV support

**"Fetch Metadata…" button.** Both `MoviesLibraryControl` and `TvLibraryControl` previously only exposed the "Fetch & Compare" flow (side-by-side review before applying) as a visible button — the direct-apply fetch (search, pick a match, apply immediately) only existed buried in the card context menu as "Fetch Metadata…". Added a matching "🔍 Fetch Metadata…" button next to "🔍 Fetch & Compare" in both controls' detail panels, reusing the existing `MovieLibraryOperations.FetchMetadataAsync` and the equivalent episode-level fetch+`SaveEpisodeAsync` path — no new fetch/apply logic, just a shortcut to the flow that already existed.

**Automatic TV episode-detail backfill.** Added `TvLibraryOperations.StartAutoBackfillTimer()`, a settings-gated (`AppSettings.AutoBackfillEpisodeDetails`, default off; `AutoBackfillEpisodeIntervalHours`, default 12) periodic timer that reuses the existing `RescanEpisodesAsync` sweep (previously only reachable via the manual "Re-fetch" button) to keep every identified show's episode titles/overviews/air dates/ratings current automatically, for as long as the TV library view is open. New toggle + interval field added to Settings → Downloads/TV episode tracking section.

**Convert to MKV now handles more than .mp4.** `Mp4RemuxService.RemuxAsync` itself never restricted source format — the `.mp4`-only gating was hardcoded at five separate call sites (`AutoRenameService`, `RenameService`, `MovieMenuBuilder`, `TvMenuBuilders`, `MovieLibraryOperations`), so `.avi`/`.mov`/`.wmv`/`.m4v` source files never got the option even though `mkvmerge` remuxes them just as well. Added `Mp4RemuxService.RemuxableExtensions`/`IsRemuxable()` and switched all five gates to use it.

### Fixed: Movies "Fetch Metadata…" opened the edit form and required a manual Save click

**The new "Fetch Metadata…" button (above) initially called `BuildEditor(entry, null)` after applying, which opens the movie edit form and leaves it waiting for the user to press Save — even though `FetchMetadataAsync` had already persisted the fetched values to the DB.** Changed `AutoFetchMovie_Click` to call `PopulateDetail(movie)` (the plain, non-editing detail view) instead, matching what the fetch already did: one click, no confirmation step. The TV episode equivalent already saved in one click and didn't need this fix.

### Fixed: art/match changes left the currently open detail panel stale until navigating away and back

**Comics "Download Art" wrote the new cover to disk but never updated anything in memory** (`ComicsLibraryManagerWindow.xaml.cs` `DownloadArt_Click`) — the series card and the open issue panel kept showing the old cover (or placeholder) until a rescan or re-navigation forced a reload. Now loads the downloaded file into a `BitmapImage` immediately and pushes it into both `series.CoverImage` (grid card, via its existing `PropertyChanged` subscription) and `FilmstripSeriesCover.Source` when that series' issue panel is the one currently open.

**TV "Edit Metadata" (re-identify/rematch a series) only refreshed the top-level Series grid.** `OpenFullEditor` called `ReloadSeriesAsync()`, which rebuilds the Series grid with fresh card instances but never touches the currently drilled-into Seasons/Episodes panel — if a series was re-matched while its own Seasons view was open, the breadcrumb, reorganize button, and hero banner kept showing the pre-match title/poster. Now also refreshes the breadcrumb, reorganize button label, and re-runs `PopulateSeasonCards()` (which rebuilds the hero banner) when the just-edited series is the one currently drilled into.

Movies/TV/Music/Audiobooks/Books "Assign Art"/"Download Art" and the inline metadata-save/fetch flows were already found to self-refresh correctly (poster-path null-reset + explicit reload, or `PopulateDetail`/`PopulateEpisodeDetail` after save) — only the two cases above were actually stale.

### Renamed: `Mp4RemuxService` → `ConvertToMkvService`

**The class name still said "MP4" after it was widened to remux `.avi`/`.mov`/`.wmv`/`.m4v` too, which misdescribed what the type actually does.** Renamed the file and class to `ConvertToMkvService` (matches the existing "Convert to MKV" UI/menu naming) and updated all eight call sites (`MainWindow.xaml.cs`, `TvLibraryControl.xaml.cs`, `AutoRenameService.cs`, `RenameService.cs`, `MovieLibraryOperations.cs`, `MovieMenuBuilder.cs`, `TvMenuBuilders.cs`). Log tags switched from `[Remux]` to `[ConvertToMKV]`; internal parameter/comment wording generalized from "mp4" to "source file". No behavior change.

---

## v0.9.13

### Fixed: proposed name for any content type collapsed to just one folder segment when an optional token (e.g. Year) had no value

**Any item missing an optional field used in a naming convention template — most commonly ebooks/PDFs with no Year metadata — got a proposed name with the required title dropped entirely, e.g. `D:\ebooks\GinaKleinworth.pdf` instead of `D:\ebooks\Gina Kleinworth\Air Fryer Cookbook for Two.pdf`.** `NamingConventionService.BuildSubfolderStructure` treated any `{Key?}` conditional token with no value as a signal to drop the *entire path segment* it appeared in, including any other required tokens sharing that segment. The built-in default templates all mix a required token with a trailing optional one in the same segment (`{Title} ({Year?})` for Books, `{SeriesTitle} ({Year?})` for TV, `{Title} ({Year?})` for Movies, `{Series} ({Year?})` for Comics) specifically so the `(Year)` part is dropped gracefully when unknown — instead the whole segment vanished, leaving only the segment before it (e.g. just `{Author}`), which then got treated by `PathBuilderService.ResolveConventionPath` as a single-segment (flat file) convention. This went unnoticed in the Settings naming-convention live preview because its sample data always includes a Year value. Fixed by having a missing `{Key?}` token substitute to empty text instead of nuking the segment, then cleaning up the now-empty wrapper punctuation it leaves behind (`"Title ()"` → `"Title"`, `"Harry Potter #"` → `"Harry Potter"`) — applied to both `BuildSubfolderStructure` (actual renames) and `GetPreview` (Settings UI) so they now agree.

### Fixed: Ebook proposed names ignored the {Series}/{SeriesNumber}/{ISBN}/{Genre} naming tokens

**The Books hold-queue's proposed name always rendered `{Series}`, `{SeriesNumber}`, `{SeriesNumberPadded}`, `{ISBN}`, and `{Genre}` as blank, even when the parser had already found those values.** `PathBuilderService.BuildGenericMetadata`'s `"Books"` branch hardcoded those tokens to `""` instead of reading them off the parsed item — `ParsedBookMedia.Series`/`SeriesPosition`/`Isbn` (populated by `BookParser` from tag/API lookups) and `parsed.Genres` were simply never wired in, so any naming convention template using those tokens silently dropped them from the proposed filename regardless of what metadata the queue displayed. Added a `FormatSeriesPosition` helper (mirrors whole vs. fractional series positions, e.g. `3` vs `3.5`) and wired `Series`/`SeriesNumber`/`SeriesNumberPadded`/`ISBN`/`Genre` through to the actual parsed values.

### Fixed: Books grid context-menu actions could silently operate on a different queue's item

**Right-clicking in the Books grid (Fetch Metadata, Rename, Edit Metadata, Download Art, Assign Art) could act on whatever item was last right-clicked in the Music, Audiobook, or Comics grid instead of the book actually clicked.** All four non-video grids share a single `_contextMenuTarget` field (`MainWindow.xaml.cs`), refreshed only by `NonVideoGrid_PreviewMouseRightButtonDown` — but that handler was wired up on `MusicGrid`, `AudiobookGrid`, and `ComicsGrid` and missing from `BooksGrid` (`MainWindow.xaml`). Since `GetNonVideoItemFromContextMenu` just returns the stale field with no per-grid check, right-clicking a book row never updated the target, so context-menu actions kept using the last item right-clicked elsewhere — e.g. fetching metadata for a book could pull in a completely different book/audiobook's data (ISBN, cover, description), and the overwrite-collision check would then correctly report *that* stale item's destination already existing. Fixed by wiring `PreviewMouseRightButtonDown="NonVideoGrid_PreviewMouseRightButtonDown"` on `BooksGrid` to match the other three grids.

---

## v0.9.12

### Automatic ASIN detection + Hold Queue ASIN lookup for audiobooks

**`AudiobookParser` (the auto-watcher's initial parse) now detects an ASIN from tags or the filename and, when found, does a direct Audnexus lookup before falling back to the fuzzy title/author search.** Some Audible rips (e.g. from Libation) embed the ASIN in the filename (`Title [B017V4IM1G].m4b`) or as a freeform tag; previously nothing read either. `AudiobookParser.TryExtractAsinFromTags` reads ID3v2 `TXXX:ASIN` (mp3) and — the gap that mattered most, since Audible's native format is m4b — the MP4 `----:com.apple.iTunes:ASIN` freeform atom via `TagLib.Mpeg4.AppleTag.GetDashBox`, which nothing in the codebase read before (`AudiobookLocalMetadataReader`, used by the library "Fetch and Compare" flow, had the same mp3-only gap — fixed there too). `TryExtractAsinFromFilename` matches a bracketed/braced 10-char token or a bare `B0` + 8 chars pattern. A successful ASIN lookup sets confidence to 100 and populates title/author/narrator/series/year/genres/overview/cover in one shot — more reliable than fuzzy matching, since the ASIN is close to a ground-truth identifier.

**Extended ASIN-aware lookup to the Hold Queue's "Fix Match".** Last session's fix only updated the main manual queue's "Fetch Metadata…"; the Hold Queue (`HoldFixMatch_Click` in `MainWindow.xaml.cs`) had its own separate audiobook branch still going through the generic fuzzy-search dialog. Split it into its own `ContentType.Audiobook` branch using `UnifiedMetadataHandler.FetchAudiobookMetadata` (same `AudiblePickerDialog` as the manual queue), applying the full `AudiobookDetails` (including `Asin`/`Series`/`SeriesPosition`) instead of just title/creator.

---

## v0.9.11

### Audiobook fixes: list-view Reorganize, stale "Open in Explorer" path, ASIN lookup + rename token; manual metadata editing for every queue type

**Audiobook list-view Reorganize was missing.** The per-card context menus (`AudiobookAuthorMenuBuilder`/`AudiobookBookMenuBuilder`) already had "📁 Reorganize Files", but the separate list-level context menus (`BuildAuthorContextMenu`/`BuildBookContextMenu` in `AudiobooksLibraryControl.xaml.cs`, used by `AuthorsListView`/`BooksListView` right-click) were hand-built and never got the item — added it to both, wired to the same `ReorganizeAuthorWithPreview`/`ReorganizeBookWithPreview` methods.

**"Open in Explorer" opened a stale path after Reorganize.** `AudiobookLibraryEntry.FolderPath`/`FolderName` (and `AudiobookAuthorEntry`'s) were `init`-only, so after `AudiobookLibraryOperations.ReorganizeBook`/`ReorganizeAuthor` moved the folder on disk and updated the DB row, the in-memory entry the UI was still holding kept pointing at the old, now-nonexistent path — and the post-reorganize UI refresh (`_vm.RefreshBooksAsync()`) ran fire-and-forget, so clicking Explorer before it completed hit the stale path. Both properties are now settable and are updated in place immediately after the `Directory.Move`, so the same `AudiobookLibraryEntry`/`AudiobookAuthorEntry` instance the UI already holds a reference to is correct right away regardless of refresh timing.

**Manual-queue audiobook metadata lookup couldn't use an ASIN.** The queue's "Fetch Metadata…" routed through the generic `NonVideoFixMatchDialog` (title/author fuzzy search only, shared with Music/Books). `AudiblePickerDialog` (used elsewhere in the library view) already had ASIN-aware lookup (`AudiblePickerDialog.AsinPattern` + direct Audnexus lookup via `MetadataService.GetAudiobookDetailsAsync("audiobook", asin)`) but was never reachable from the manual queue. Added `UnifiedMetadataHandler.FetchAudiobookMetadata` and `ManualRenameOperations.FetchAudiobookMetadata`, and pointed the Audiobooks grid's "Fetch Metadata…" item at it instead — paste a 10-character ASIN and it does a direct lookup instead of a fuzzy title search.

**Added `{Asin}` as an Audiobooks naming-convention token**, since there was previously no way to include it in a folder/file pattern even though the field already existed on `AudiobookLibraryEntry`/DB. Wired through `NamingConventionService` (token list + live-preview sample data), `PathBuilderService.BuildGenericMetadata`, and all four `AudiobookLibraryOperations` metadata dicts (`PreviewReorganizeAuthor`/`Book`, `ReorganizeAuthor`/`Book`). Also added an `Asin` property to `ParsedMedia`/`ParsedAudiobookMedia` so it flows from the new ASIN-lookup dialog through to a manual-queue rename.

**New: manual "Edit Metadata…" for every manual-queue grid** (Movie/TV/Music/Audiobook/Book/Comic) — previously the only way to correct a bad or failed automatic match was Fetch Metadata's fuzzy search; there was no way to just type the right values in by hand. Added `Views/ManualMetadataEditDialog` (a small generic label+textbox dialog driven by a per-content-type field list built by the caller — no per-type XAML needed) plus `ManualRenameOperations.EditMetadataManually` for Music/Audiobook/Book/Comic and dedicated `EditMetadataMovie_Click`/`EditMetadataTv_Click` handlers in `MainWindow.xaml.cs` for Movie/TV (mirroring the existing Fetch Metadata code paths), wired into every manual-queue grid's context menu. Saving recomputes `NewPath` via the same `PathBuilderService` call the "Rename All" loops use, so edits actually affect where the file lands.

---

## v0.9.10

### Per-copy progress bars, individual Stop buttons, and a "pause queue" panic button

**Previously there was a single shared `GlobalProgressBar`** fed by two independent copy pipelines (the auto-watcher queue in `AutoRenameService` and the manual "Rename All" loops in `MainWindow`/`ManualRenameOperations`, both funneling through `RenameService`/`FileCopyService`). When more than one copy happened to be in flight at once — e.g. a manual batch rename running while the watcher auto-processed another file — the one bar visibly jumped between whichever copy last reported progress, and there was no way to cancel a specific stuck copy or pause the queue without closing the app.

Added `CopyOperationsTracker` (`Services/CopyOperationsTracker.cs`) as the single choke point both `RenameService.RenameAsync` and `AutoRenameService`'s internal `RenameAsync` now go through for every copy — since literally every copy path in the app (manual single/batch renames, hold-queue retries, the delayed auto-queue) calls one of those two methods, this required no changes to the ~20 individual "Rename All" call sites in `MainWindow.xaml.cs`.

- Each in-flight copy gets its own `CopyOperation` (`Models/CopyOperation.cs`) — a labelled, colour-coded progress row in a new panel above the log, with its own **⛔ Stop** button.
- **Stop** cancels that copy's `CancellationToken`, deletes the (now-partial) destination file, leaves the source file completely untouched, and — for auto-queue items — sends the item to the Hold Queue with reason "Cancelled by user" so it's easy to find and retry.
- New **⏸ Pause Queue** button pauses the start of any *new* copy for 5 minutes (via `CopyOperationsTracker.WaitWhilePausedAsync`, awaited at the top of both `RenameAsync` methods before the file stream opens); copies already past that point keep running and must be stopped individually. Click again (now "▶ Resume Queue") to resume early; the button shows a live countdown.
- Removed the old `GlobalProgressBar`/`_progressSmoothTimer`/`AutoRenameService.CopyStarted`/`CopyProgressChanged`/`CopyFinished` machinery, now fully superseded by the tracker.

---

## v0.9.9

### Restored Edition field to the movie metadata editor

**The inline "Edit Metadata" screen for movies (`MoviesLibraryControl`'s dynamically-built editor) never had an Edition field**, even though `Edition` was fully wired everywhere else (DB column, `MediaParser.ExtractEdition`, `PathBuilderService`'s Plex `{edition-…}` tag, and the older standalone `MetadataDetailWindow`). Users could no longer set/see a movie's Edition (e.g. "Director's Cut", "Extended Edition") from the library's main edit form, so `PlexEditionsEnabled` naming had no way to be populated for existing library items. Added an Edition row to `BuildEditor`/`AddEditorRow` in `MoviesLibraryControl.xaml.cs`, and added an `edition` parameter to `MovieLibraryOperations.SaveMovieAsync` (previously discarded) so it's persisted via the existing `entry.Edition`/`row.Edition` path in `PersistMovieToDbAsync`.

---

## v0.9.8

### Fixed duplicate auto-downloads and repeated quality-upgrade re-triggers

**Automatic downloads and quality upgrades were sometimes being queued more than once for the same item.** `app.log` showed repeated `InvalidOperationException`s ("a second operation was started on this context instance" and "collection was modified") thrown out of `WantListService.CheckLibraryAndRemoveAcquiredAsync` — caused by `WantListService` and `QualityUpgradeService` sharing a single `MediaDbContext` (`App.Database`) with the rest of the app (UI thread) and with `EpisodeSyncService`'s own independent periodic timer. EF Core `DbContext` instances aren't thread-safe, so concurrent access crashed the "remove items already in the library" cleanup step mid-run; the item then stayed in the want list, still marked not-acquired, and got re-downloaded on a later sweep. Separately, `WantListService`'s and `QualityUpgradeService`'s periodic timers had no reentrancy guard, so a slow provider search could overlap with the next tick and queue the same item twice.

Fixes:
- `WantListService` and `QualityUpgradeService` now each own a **dedicated `MediaDbContext`** instead of sharing `App.Database`, isolating their background-timer DB access from the rest of the app.
- Both `CheckAutoDownloadsAsync` and `ScanAndQueueAsync` now have a **reentrancy guard** (`SemaphoreSlim`) that skips a tick instead of racing an already-running one.
- Every raw `_db` call inside `WantListService` is now guarded by a fine-grained `SemaphoreSlim` gate, since `EpisodeSyncService.PruneWantListAcquiredAsync()` calls into `WantListService` from its own independent timer and could otherwise still race `WantListService`'s own periodic check.
- `CheckLibraryAndRemoveAcquiredAsync` now snapshots the want list before iterating, avoiding a "collection was modified" crash if an item is added/removed mid-scan.

**New: explicit 24-hour "queued download" block.** Once a torrent/NZB is successfully queued for a want-list item (including quality-upgrade candidates), `DownloadQueuedAt` is stamped and the item is excluded from future auto-download sweeps for `DownloaderSettings.QueuedDownloadBlockHours` (default 24h) — independent of, and longer than, the existing `RetryIntervalHours` "no results" cooldown (default 6h), which was short enough that a download still in progress could get re-queued again before it had time to land in the library. If the download stalls or fails and nothing lands within the block window, the item becomes eligible for a fresh search again. When the file does land, the existing `NotifyTvEpisodeAcquired`/`NotifyMovieAcquired` removal (now made reliable by the fixes above) clears the item from the want list immediately, so it can't be re-downloaded. New setting exposed in Settings → Downloads as "Queued download block (hours)".

---

## v0.9.7

### Restored Edit Tracks and Transcode to x265 for TV, with proper hierarchy

**These context-menu actions existed for Movies but were never carried over to the TV library control** during the Comics-style migration, so they silently disappeared for TV series/season/episode. `TvLibraryOperations` gains `TranscodeSeries`/`TranscodeSeason` (bulk-queue every video file in the folder for x265 encoding, no per-file dialog — mirrors the existing `NormalizeAllAudio`/`NormalizeSeasonAudio` pattern) plus `TranscodeEpisodeAsync` and `EditEpisodeTracksAsync` (single-file, with the `TrackEditWindow` picker — mirrors the Movie behavior). "Transcode to x265" now appears at Series, Season, and Episode level; "Edit Tracks" is Episode-only and only offered for a single selected episode (remuxing multiple files at once via one track selection doesn't make sense).

---

## v0.9.6

### Auto-downloader no longer re-downloads the same episode every 30 minutes in manual mode

**In manual mode (`AutoRenameEnabled = false`), the want-list auto-downloader kept re-downloading episodes it had already fetched.** `CheckAutoDownloadsAsync` ran on its own 30-minute timer completely independent of manual/auto mode, and downloaded files in manual mode sit in the watch folder / manual-review grid until the user clicks Rename — they never land in `TvRootFolder`/`MovieRootFolder`. Since `IsItemInLibrary` only recognizes files already inside the library folders, the episode never registered as acquired, so each periodic sweep treated it as still missing and queued another download. `CheckAutoDownloadsAsync` now also checks `AppSettings.AutoRenameEnabled` and skips the run entirely when the app is in manual mode, alongside the existing `DownloaderSettings.AutoDownloadEnabled` check.

---

## v0.9.5

### Comics: series year now correct in proposed names, incomplete metadata held from queue

**`{SeriesFirstYear}` was always blank in the manual queue.** The watcher-path parser was setting `result.Year` from the CV volume's start year but never writing to the separate `SeriesFirstYear` field — so the proposed folder was `X-Force ()` instead of `X-Force (1991)`. The fix populates `SeriesFirstYear` directly from the API volume search result, the issue details, and pinned-series DB lookups. The `{Year}` and `{SeriesFirstYear}` tokens remain semantically distinct: `{SeriesFirstYear}` is the year the series first published (for the folder name), `{Year}` is the issue cover year (for display). The library reorganize path had the same bug — `BuildComicMeta` was mapping the issue's `CoverYear` to the `{SeriesFirstYear}` token; this now correctly uses the series's `SeriesStartYear` field.

**Comics no longer appear in the queue until they have a fully qualified proposed name.** Files are held if `Publisher`, `SeriesFirstYear`, or `IssueNumber` are missing after a full metadata scrape. Held files are automatically retried every 5 minutes so a rate-limiter pause or a slow API response doesn't lose the file. Any file held for more than 15 minutes is force-queued regardless of completeness so nothing disappears permanently.

---

## v0.9.4

### Missing albums now actually appear — DiscographyEnabled wired up end to end

**Missing albums were never showing because DiscographyEnabled was always false.** `DbArtist.DiscographyEnabled` defaults to `false` (DB column `DEFAULT 0`), and `RefreshAllArtistsAsync` filters on it — so no discography fetch ever ran, `MusicReleases` stayed empty, and the missing albums panel was always blank. The fix has two parts: (1) Fetching metadata in the Music Library for an artist now automatically sets `DiscographyEnabled = true` the moment a MusicBrainz ID or Discogs ID is found, so existing artists pick up tracking the next time you click "Fetch Metadata". (2) The Artist Editor window now shows a "Track discography (fetch missing albums)" checkbox that loads the current value and saves it back — so you can also toggle it manually or turn it off for artists you don't want tracked.

---

## v0.9.3

### Reorganize now catches episodes loose in the series root

**TV Reorganize now handles episodes not yet in season subfolders.** Previously, `PreviewReorganize` and `ReorganizeEntry` only scanned season subdirectories — any video files sitting directly in the series folder (e.g. `House of the Dragon (2022)\S01E01 - Title.mkv`) were invisible and the operation would report "nothing to do." Both methods now also sweep the series root for video files matching the `S##E##` pattern, create the season subfolder if it doesn't exist, and move the files in. The preview dialog now shows relative paths (e.g. `Season 01\S01E01 - The Heirs of the Dragon.mkv`) so it's obvious when a file is being moved into a subfolder rather than just renamed in place. Movies are unaffected — each movie already has its own folder and the reorganize correctly processes files within it.

---

## v0.9.2

### AudioDB removed; Upcoming Albums section now visible on the General tab

**AudioDB removed.** TheAudioDB has been dropped as a metadata provider — MusicBrainz and Discogs cover everything it did, better. `AudioDbClient.cs` is deleted. The AudioDB ID fields are gone from the Artist Editor window, the `DbArtist` and `DbAlbum` entities, and artist NFO exports. Fetch Metadata now runs Last.fm, MusicBrainz, and Discogs in parallel; artist photo auto-pick now uses Discogs only (via the existing art aggregator). The `AudioDbArtistId` and `AudioDbAlbumId` columns remain in existing databases to avoid destructive migrations — they are simply ignored.

**Upcoming Albums section was missing from the General tab.** The ViewModel logic and data binding were complete but the XAML section was never added to `MainWindow.xaml`. The "UPCOMING ALBUMS" cards now appear below the TV Shows section, gated by Music tab visibility and the "Show upcoming music" setting. Each card shows square album art, artist name, album title, a type badge (Album/EP/Single), and the relative release date, with "Add to Want List" and "Ignore" buttons.

---

## v0.9.1

### Reorganize button now scopes to the current level — no more accidental whole-library renames

**Reorganize targets what you're looking at, not the whole library.** In the TV library, clicking Reorganize while inside a series (Seasons or Episodes view) now reorganizes only that series — showing the same preview confirmation dialog the right-click context menu uses — instead of silently reorganizing every series in the library. In the Movies library, clicking Reorganize while a movie's detail pane is open now reorganizes only that movie. At the top-level grid view (no series or movie selected) the button still batch-reorganizes the whole library as before.

**Button label reflects scope.** When you navigate into a TV series, the toolbar button changes from "📁 Reorganize All" to "📁 Reorganize 'Series Name'" so it's always clear what clicking it will affect. Navigating back to the series grid restores "📁 Reorganize All". Movies work the same way — the button reads "📁 Reorganize 'Movie Title'" while you're in the detail pane.

---

## v0.9.0

### Music discography tracking, upcoming album releases, and missing album detection

**Artist discography tracking from MusicBrainz and Discogs.** When a music artist in your library has a MusicBrainz or Discogs artist ID (assigned via Library Manager → Music → Edit Artist), you can now opt-in to discography tracking per-artist. The new `MusicDiscographyService` fetches the full release group list from MusicBrainz (paginated, ~1 req/sec) or Discogs releases (fallback when no MBID), stores them in a new `MusicReleases` table, and then fuzzy-matches each release against your local `Albums` table to determine what is and isn't in your library. Cover art thumbnails are fetched from the Cover Art Archive during the scan and stored alongside each release. Refresh runs in the background on startup and on a configurable interval (default: every 7 days).

**Upcoming album releases on the General tab.** The General tab now includes an "Upcoming Albums" section showing releases from tracked artists that fall within a configurable lookahead window (default: 180 days). Each card shows cover art, artist, album title, type badge, and relative date. "Add to Want List" queues the album with a music-specific search query (`"Artist Album Year FLAC"`). "Ignore" permanently hides the release (stored in settings). Album release events are also merged into the weekly episode calendar view, appearing as secondary rows on matching days.

**Missing albums panel in the Music Library.** Navigating into an artist in the Music Library now reveals a collapsible "Missing Albums" expander at the bottom of the artist drill-down view when discography tracking is enabled for that artist. Each row shows the release year, album title, type badge, and buttons to add to the Want List or open Download Search pre-filled with the artist and album name.

**Music Tracking settings.** A new "Music Tracking" expander in Settings → Downloaders controls all discography settings: enable/disable tracking, show upcoming music, show missing albums, refresh interval, upcoming lookahead, and which album types to include (studio albums only by default — EPs, Singles, Live, Compilations off). A "Refresh Discography Now" button triggers an immediate re-scan of all enabled artists.

**Want List music support.** `WantListItem` now has an `ArtistName` field. `DisplayTitle` renders music items as "Artist — Album" and `SearchQuery` produces "Artist Album Year FLAC". `WantListService.AddMissingAlbum` de-duplicates by artist+title and `IsItemInLibrary` checks the `Albums` DB table for music items.

---

## v0.8.5

### TV folder routing by ID + MP4→MKV filename fix in logs and toasts

**TV folder matching now uses TMDB/TVDB ID instead of fuzzy name matching.** When a TV series folder has been identified in Library Manager (assigned a TMDB or TVDB ID), new episodes for that series always route to the correct existing folder regardless of what the folder is named on disk. Previously, fuzzy name matching would fail when the TMDB title differed from your folder name (e.g. "Last Week Tonight with John Oliver" not matching your "Last Week Tonight (2014)" folder). The preferred metadata provider (`Settings → Metadata → Preferred TV Provider`) controls which ID is tried first; the other is used as a fallback. For shows with no DB entry yet (first-time arrivals not yet scanned into Library Manager), a fresh folder is created using the TMDB/TVDB title — no fuzzy guessing.

**MP4→MKV log and toast now show the correct filename.** When `Auto Remux MP4 to MKV` is enabled, the "Moved" toast and rename history entry previously used the original `.mp4` filename instead of the actual `.mkv` destination. `RenameService.RenameAsync` now returns the actual destination path used (which may differ from the input `newPath` when remux fires), and all callers use that path for toasts, logs, and rename history.

---

## v0.8.4

### Release-readiness: log level, plugin trust, art cache, first-run wizard

**Default log level changed to Minimal.** New installs start with `LoggingLevel.Minimal` instead of `Extra`, so the log only records errors, warnings, and rename outcomes by default. Existing users who have already saved a log level preference are unaffected.

**Newznab plugin auto-authorized on first load.** Any DLL whose filename contains "newznab" (case-insensitive) is now automatically added to `TrustedPluginHashes` without a user prompt. All other third-party plugins still require explicit user consent. The auto-authorization is logged as `[PluginManager] … — auto-authorized (first-party bundled plugin)`.

**Art cache default directory changed to `art_cache`.** `EffectiveImageCachePath` now resolves to `%AppData%\AbigailsMediaRenamer\art_cache\` (previously `ImageCache`). The directory is created automatically on first access. Users with a custom `ImageCachePath` in settings are unaffected. Note: any art already cached in the old `ImageCache` folder will not be moved; if you need the old art, copy it manually.

**First-run setup wizard.** On a fresh install (no settings file, or settings file with no watch/API key configured), a three-step setup wizard appears before the main window. Step 1 (Folders): choose watch folder, movies root, and TV root — all three are required and must exist. Step 2 (API Keys): enter TMDB and TVDB API keys with links to the sign-up pages. Step 3 (Naming): adjust Movie and TV naming templates using the same token/preview UI as Settings → Naming. On completing the wizard ("Go"), settings are saved, the file watcher starts, and the main window opens. Existing users are detected via the presence of a configured watch folder or API key and never see the wizard.

---

## v0.8.3

### Movies — Artwork Tab inline + Cast & Crew repositioned

**Artwork manager is now an inline tab** on the movie detail panel instead of opening a separate window. The movie detail view has two tabs: **Info** (poster + metadata, same as before) and **Artwork** (poster, fanart, banner, clearlogo, clearart slots — right-click each to download, assign from file, or clear; "Download All Art" button downloads everything at once). The artwork slots lazy-load the first time you switch to the tab.

**"🎨 Artwork Manager" context menu item** on movie cards now navigates to the detail view and switches directly to the Artwork tab rather than opening a separate window.

**Cast & Crew moved** from below the two-column layout (where it was a full-width block) to inside the right info column, directly underneath the plot summary. Director, writers, and cast now appear inline with the rest of the metadata.

**"⬇ Download Art" (Info tab poster button)** refreshes the Artwork tab's poster slot too if it's already loaded.

---

## v0.8.2

### Movies — Artwork Manager + Poster Picker

**"Fetch Art" now shows the picker dialog instead of auto-downloading.** Clicking "⬇ Download Art" (inline panel or context menu) previously silently downloaded the first TMDB poster. It now opens the `ArtPickerDialog` with all TMDB + Fanart.tv poster candidates, matching the TV and movie `MetadataDetailWindow` behaviour.

**New "🎨 Artwork Manager" button and context menu item** on the Movies detail panel. Opens `MetadataDetailWindow` for the selected movie — giving access to the full Artwork tab (poster, fanart, logo, banner, clearart, season posters) just like TV series already had.

**After closing the Artwork Manager**, the inline panel's cover image refreshes automatically if `cover.jpg` changed on disk.

**`MovieMenuBuilder.Actions`** gains an `OpenArtworkManager` delegate alongside `DownloadArt` and `AssignArt`. Both the inline button and the context menu use shared helper methods (`ShowPosterPickerAsync`, `OpenArtworkManagerForCard`) so the logic isn't duplicated.

---

## v0.8.1

### Discogs Artist Disambiguation + Bio Source Switch

**Discogs ID now used directly for art fetching.** Previously, "Fetch Artist Photo" always did a Discogs name search and took the first result — so if you had manually corrected the Discogs ID (e.g. to "Blazy (2)"), the picker ignored it and fetched the wrong artist's photos. Both the context-menu picker and the Artist Editor "🖼 Fetch Photo" button now pass the stored `DiscogsArtistId` to the aggregator, which bypasses the search entirely and hits `/artists/{id}` directly.

**New `ICanProvideArtistArtById` interface.** `DiscogsClient` implements this alongside `ICanProvideArtistArt`. `ArtAggregatorService.GetArtistArtCandidatesAsync` now accepts an optional `discogsArtistId` parameter — when set, providers implementing `ICanProvideArtistArtById` use the ID path; other providers (AudioDB) still use the name.

**Discogs name search now returns candidates from all matching artists.** When no ID is stored and a name search is needed, up to 5 matching Discogs artists are queried in parallel. Each artist's photos are labelled with their Discogs name (e.g. `"Discogs (Blazy)"` vs `"Discogs (Blazy (2))"`) so the picker makes disambiguation visible at a glance.

**Discogs is now the primary bio source.** Last.fm bios are unreliable for disambiguation — when multiple artists share a name, Last.fm blends results from all of them. `FetchArtistMetadataAsync` and the Artist Editor's "🔍 Fetch Metadata" button now use the Discogs `profile` field as the bio (fetched via `GetArtistInfoAsync`, using the stored ID directly if known), falling back to AudioDB biography only if Discogs returned nothing. Last.fm still provides tags, listener count, and Last.fm URL — just not the bio.

**Manually set Discogs IDs are now preserved.** Both "Fetch Metadata" flows previously overwrote `DiscogsArtistId` with whatever the name search returned, even if you had corrected it manually. Now the ID is only filled in if the field is currently empty.

**New `DiscogsClient.GetArtistInfoAsync(name, knownId?)`** — convenience method returning `(string? Id, string? Profile)`. Uses the known ID directly if supplied; otherwise searches by name. Replaces separate `SearchArtistAsync` + `GetArtistDetailAsync` calls at the fetch sites.

**`DiscogsArtistDetail` DTO gains `Profile` field** — maps to the `profile` JSON key returned by the Discogs artist detail endpoint.

---

## v0.8.0

### Art Capability System — All Content Types Now Have Art Pickers

This is a large infrastructure change that brings multi-source art discovery and a picker dialog to every content type in the library.

**New: `ArtAggregatorService`** — a central service that queries all art-capable providers in parallel and merges the results. Replaces ad-hoc single-source downloads everywhere.

**New art capability interfaces** (`IArtCapabilities.cs`): `ICanProvideArtistArt`, `ICanProvideAlbumArt`, `ICanProvideMovieArt`, `ICanProvideTvArt`, `ICanProvideBookArt`, `ICanProvideAudiobookArt`, `ICanProvideComicArt`. Providers opt-in by implementing the interfaces they support.

**Providers now contributing art:**

**Music artist photos:** TheAudioDB (existing) + Discogs (new — searches by name, fetches `/artists/{id}` for primary/secondary images)

**Album covers:** Deezer (existing) + MusicBrainz/CoverArtArchive (existing) — now both surface in the same picker

**Movie art (poster/fanart/logo/banner/clearart):** TMDB + Fanart.tv — both surface through the aggregator

**TV art:** TMDB + Fanart.tv — both surface through the aggregator

**Book covers:** Google Books + Open Library + GoodReads

**Audiobook covers:** Audible/Audnexus (via Amazon CDN cover URL from ASIN)

**Comic covers:** ComicVine + Metron

**Picker dialog wired to:**

**Music artist "🖼 Fetch Artist Photo"** (new context-menu action) — picks from TheAudioDB + Discogs

**Artist Editor "🖼 Fetch Photo" button** — now shows picker instead of silently downloading first result

**Music "🔍 Fetch Metadata" auto-path** — now uses aggregator (covers AudioDB + Discogs) for auto-pick on first fetch

**Books "Download Art" action** — now shows picker from Google Books + Open Library + GoodReads

**Audiobooks "Download Art" action** — now shows picker from Audible

**Comics "Download Art" button** — was a stub; now shows picker from ComicVine + Metron

**New client: `AudioDbClient`** — split out from `LastFmClient`. AudioDB is now its own service. `LastFmClient` is Last.fm only.

**`DiscogsClient`** gains `GetArtistDetailAsync(id)` hitting `/artists/{id}` and implements `ICanProvideArtistArt`.

---

## v0.7.9
**Discogs Artist ID now fetched during "Fetch Metadata".** The artist metadata fetch (context menu, hero banner re-fetch, and the new editor button) now also queries Discogs `database/search?type=artist` and stores the matched artist's Discogs ID. Requires a Discogs token in Settings (same token used for album metadata).

**"Fetch Metadata" button added to Artist Editor window.** The "Edit Metadata" dialog now has a "🔍 Fetch Metadata" button that fires Last.fm, TheAudioDB, MusicBrainz, and Discogs in parallel and populates all editable fields (MBID, AudioDB ID, Discogs ID, bio, tags, listeners, Last.fm URL, country, formed, disbanded, style). Fields are overwritten with fresh API data; hit Cancel to discard if the results look wrong.

**Fixed MusicBrainz crash when `life-span.ended` is JSON null.** Calling `GetBoolean()` on a null JSON element threw "target element has type 'Null'"; now checks `ValueKind == JsonValueKind.True` instead.

## v0.7.8
**Artist metadata fetch now pulls from three sources simultaneously.** "Fetch from Last.fm" has been renamed to "🔍 Fetch Metadata" everywhere (context menu, inline hero button, re-fetch button). Clicking it now fires three providers in parallel: Last.fm provides bio summary, genre tags, listener count, and Last.fm URL (unchanged); TheAudioDB provides AudioDB Artist ID, photo thumbnail, biography (fallback when Last.fm bio is empty), formed/disbanded dates, country, and style/genre; MusicBrainz provides the Artist MBID, ISO country code (takes precedence over AudioDB text country), and formed/disbanded dates.

**All new fields are saved to the DB and are immediately visible if you re-open Edit Metadata for that artist.**

## v0.7.7
**Music library: MusicBrainz, AudioDB, and Discogs IDs stored in DB.** `DbArtist` gains `MusicBrainzArtistId`, `AudioDbArtistId`, `DiscogsArtistId`, `Formed`, `Disbanded`, `Country`, and `Style`. `DbAlbum` gains `MusicBrainzAlbumId`, `MusicBrainzAlbumArtistId`, `MusicBrainzReleaseGroupId`, `AudioDbAlbumId`, `DiscogsAlbumId`, `DiscogsUrl`, `Description`, `Style`, `Mood`, `ReleaseDate`, and `Compilation`. All new columns are added via the standard `TryAddColumn` migration so existing databases upgrade silently on first launch.

**Artist Editor now exposes all new fields.** The "Edit Metadata" dialog for artists has been expanded to include editable fields for MusicBrainz Artist ID, AudioDB Artist ID, Discogs Artist ID, Formed, Disbanded, Country, and Style. The Bio text box has been retained and the window height adjusted to fit all fields.

**NFO export for music: `artist.nfo` and `album.nfo`.** Two new methods (`WriteArtistNfoAsync`, `WriteAlbumNfoAsync`) added to `NfoWriterService`. Output complies with both Kodi and Jellyfin music NFO specs. `artist.nfo` includes: `<name>`, `<musicbrainzartistid>`, `<audiodbartistid>`, `<biography>`, `<formed>`, `<disbanded>`, `<country>`, `<style>`, `<tag>` (from comma-separated Tags), `<thumb>`. `album.nfo` includes: `<name>` (not `<title>` — per Kodi/Jellyfin music spec), `<artist>`, `<musicbrainzalbumid>`, `<musicbrainzalbumartistid>`, `<musicbrainzreleasegroupid>`, `<audiodbalbumid>`, `<label>`, `<releasedate>`, `<rating>`, `<biography>`, `<style>`, `<mood>`, `<compilation>`, `<genre>`.

**Music context menus: Export NFO.** Right-clicking an artist card shows "📄 Export artist.nfo" and right-clicking an album card shows "📄 Export album.nfo" when NFO writing is enabled (both items are hidden when `NfoWriterService` is not injected).

## v0.7.6
**NFO exports aligned with Jellyfin and Kodi specs.** Cross-checked against the Jellyfin NFO documentation. Three missing fields added and one non-standard field corrected: `<runtime>` is now written for movies (from `DurationMinutes` in DB); `<premiered>` is now written for both movies (`YYYY-01-01` from release year) and TV shows (full `YYYY-MM-DD` from `FirstAirDate` when available), used by Jellyfin/Kodi for date sorting and display; `<status>` is now written for TV shows (e.g. "Ended" / "Returning Series") from `DbTvShow.Status`. `<imdbrating>` has been removed (not a recognised field in either Jellyfin or Kodi) — the IMDB rating is now written as `<criticrating>` (scaled to 0–100 by multiplying by 10), which is the correct field for both media centres.

## v0.7.5
**Encode queue prompt after normalise/transcode/remux.** After queuing any encode job (Normalize Audio, Transcode to x265, Edit Tracks / remux, or Attach Subtitle) from any context menu or button — in the Movies library, TV library, or Metadata Detail Window — a yes/no dialog now appears confirming the file was queued and offering to open the Encode Queue window immediately. The app log also records a `[NormalizeAudio]`, `[Transcode]`, or `[Remux]` entry at the point of queuing so there is a durable audit trail even if the user says No.

## v0.7.4
**Cast/crew now visible in metadata view and editor screens.** After running "Fetch Metadata" (right-click context menu) or "Fetch & Compare" (blue button in detail pane) on a movie, the cast and crew are displayed in three places: the movie detail pane (a collapsible "Cast & Crew" section below the overview), the inline metadata editor (read-only section below the editable fields), and the Metadata Detail Window modal (available for both movies and TV series). TV series data is populated the same way via the existing "Fetch Metadata" flow.

**IMDB ID and rating visible in Metadata Detail Window.** The Details tab now shows read-only "IMDB ID" and "IMDB Rating" fields populated from the DB (filled by the `api.imdbapi.dev` enrichment step that runs as part of Fetch Metadata).

**Fetch & Compare now saves cast/crew for movies.** Previously only "Fetch Metadata" (context menu) saved the cast/crew tables; clicking "🔍 Fetch & Compare" in the detail pane did not. Now both paths save cast and crew and trigger IMDB enrichment.

## v0.7.3
**Cast and crew metadata for movies and TV shows.** Two new DB tables (`CastMembers`, `CrewMembers`) store rich people data fetched from TMDB alongside library enrichment. Top 15 cast members (by billing order) and key crew (Director, Writer, Screenplay, Story, Producer, Executive Producer, Original Music Composer, Director of Photography, Editor, Casting Director) are saved whenever "Fetch Metadata" is run in the library manager. Each person record includes their name, character/role, billing order, and TMDB profile photo URL.

**IMDB IDs for movies and TV shows.** TMDB's `external_ids` endpoint is now appended to both movie and series detail calls, and the returned IMDB ID (`tt1234567`) is stored in the `ImdbId` column on `Movies` and `TvShows`. Fixed: `Movies.ImdbId` was previously typed as `INTEGER` (wrong — IMDB IDs are strings like "tt0111161"); the column now stores text.

**IMDB rating enrichment.** After fetching TMDB data (which provides IMDB IDs), the app calls `api.imdbapi.dev` (free, no API key needed) to retrieve the IMDB community rating and stores it in the new `ImdbRating` column on both `Movies` and `TvShows`.

**NFO exports now include full cast/crew and IMDB data.** `movie.nfo` and `tvshow.nfo` exports now emit `<actor>` blocks (name, role, order, thumb URL) for each cast member stored in DB, `<director>` and `<credits>` (writer) tags from the crew table superseding the old single-director legacy field, `<uniqueid type="imdb">` alongside the existing TMDB/TVDB uniqueids with IMDB set as default when available, and `<imdbrating>` when an IMDB rating is stored.

**New `ImdbApiClient` service.** Thin wrapper around `api.imdbapi.dev` REST API (v2.7.12). Called automatically during library Fetch Metadata; not invoked during the rename pipeline.

## v0.7.2
**Fix: Fetch Metadata pre-populates title with year in parentheses, leaving year field blank.** `FixMatchWindow` now strips any trailing `(YYYY)` from the initial title and moves it to the year field. Previously, entries whose title was stored as e.g. `"12 Feet Under (2018)"` with no separate year value would populate the search box as `"12 Feet Under (2018)"` and leave the year box empty, requiring the user to manually fix it before searching.

## v0.7.1
**Fix: rename-all could file episodes/movies to wrong folder when title was missing.** When TMDB enrichment fails, `BuildTvPath` and `BuildMoviePath` now return an empty string instead of building a broken path like `{TvRoot}/Season 02/` (no series subfolder) or `{MovieRoot}/.mkv`. All rename-all handlers now recompute the path from current metadata immediately before moving each file, so manual grid edits are respected and stale paths can't corrupt the destination. Items with no resolvable series/movie title are skipped with a log message and a toast notification instead of being moved to the wrong location; use Fix Match to resolve them first.

**Fix: comic proposed paths missing year when API enrichment didn't run.** `ComicParser` now uses the year extracted from the filename or parent folder name as a last-resort fallback for `result.Year`, so the proposed path includes a year-disambiguated folder (e.g. `The Walking Dead (2003)`) even when no API enrichment ran. API enrichment year still takes priority; this only fills the gap when the API returned no start year.

## v0.7.0
**Comic series saved to DB on watch folder fetch.** When `ComicParser` matches a series via ComicVine or Metron during watch folder processing, it now upserts a `ComicSeries` row immediately — even before the file is renamed or moved to a library folder. Previously, series-level metadata was only persisted to the provider cache tables (ComicVineSeries/MetronSeries); the library-level `ComicSeries` row was only created when a file physically landed in a destination folder. Now the series identity (name, publisher, start year, provider IDs) is preserved regardless of whether the file is auto-renamed, held for review, or dismissed. If a folder-based series row already exists but is missing the provider ID, it gets backfilled.

## v0.6.9
**Fix: Fuzzy folder matching false positives.** `DirectoryCacheService.FindBestMatchingDirectory` no longer matches folders that share no whole words with the query. Previously, raw Jaro-Winkler character similarity caused matches like "The Simpsons" → "The Son" (after article stripping, "simpsons" vs "son" scored high on character overlap). The new scoring uses a hard gate that rejects candidates with zero whole-word overlap, then blends word-level F1 (precision × recall) with Jaro-Winkler — anchoring on exact word matches while preserving tolerance for minor spelling/punctuation differences.

**TV series grouping in manual queue.** The TV tab now groups episodes by series name (matching how Music groups by album, Audiobooks by title, and Comics by folder). Each group shows an expandable header with series name, year, and episode count. Group name stays in sync when metadata is fetched. Two new context menu items: **Rename Series** batch-renames all episodes in the same series group, handling rename history, library cache, and want list per episode; **Fetch Series Metadata** looks up the series on TMDB/TVDB once, then applies the series info + per-episode titles to all episodes in the group.

**Comic auto-rename confidence tightened.** Comics with weak metadata now fall to the hold queue instead of being auto-renamed into the wrong folders. Specific changes: no issue number caps confidence at 50; no publisher caps at 55; reading-order prefixed filenames (e.g. "027 - Wolverine & The X-Men 018") cap at 30; no API match caps at 40 instead of self-scoring ~80. Pinned series scoring (92/70) is unchanged.

**Fix: Library sync was dead on arrival.** `LibrarySyncService.LibraryChanged` event was never subscribed to — the filesystem watchers detected new folders in library roots but nothing triggered a resync. Now wired up: filesystem changes trigger `SyncAllAsync`, and an initial sync runs at startup. This means series/movies manually copied into routed library folders (Animated TV, 4K, Documentaries, etc.) now appear in the library manager without needing to save settings.

**Fix: Directory cache only warmed for default libraries.** `WatcherOrchestrationService.ResetAndRescanAsync` only refreshed the cache for the default TV/Movie roots. Now refreshes all configured library paths so fuzzy folder matching works correctly for routed libraries.

**Comic rename logging.** CBR→CBZ conversion now logs `Converting {file}.cbr → {file}.cbz` before starting and per-issue progress during batch renames (`Processing ComicInfo: {file}`). Suppressed noisy `.tmp` file messages from the watcher.

**Fix: Fetch Series Metadata passed seed episode's season/episode** to the search dialog. Now passes 0/0 so the dialog opens with empty season/episode fields for series-wide searches.

**Fix: FixMatchWindow search error now includes stack trace** to help diagnose NullRef crashes in the picker dialog.

**Fix: comic filename parsing for "N (of M)" pattern.** Files like `Amazing X-Men 01 (of 4) (1995).cbz` now correctly detect issue number 1. Previously, only `N of M` (without parens) and `(N of M)` (both inside parens) were handled — the common `N (of M)` pattern where the issue precedes a parenthesized "of" was missed.

**Comic picker: clean search input + year filter.** The comic search dialog now strips issue ranges ("01-20"), years, volume tags ("v3"), scene junk, and file extensions from the pre-populated series name — e.g. "Age of Apocalypse 1-4 (2012)" becomes just "Age of Apocalypse" with year 2012 in the new Year field. Results are filtered by year when provided (falls back to all results if the year matches nothing).

**Comic series metadata: StartYear + per-issue enrichment.** `FetchSeriesMetadata` and `FetchComicMetadata` now propagate `StartYear` from the provider candidate through to `ParsedComicMedia.SeriesFirstYear`, so the `{SeriesFirstYear}` naming token works in path templates. Both `ComicDetails` model and all provider return paths (ComicVine, Metron, picker fallback) now carry `StartYear`. Additionally, `FetchSeriesMetadata` reads the bulk-fetched issue cache from the DB to populate per-issue `IssueTitle` and `CoverDate` on each file in the batch — no additional provider API calls, just DB reads from data already cached by the initial series lookup.

## v0.6.8
**"Convert to MKV" context menu for MP4 files.** Movies and TV episodes now show a "📦 Convert to MKV" context menu item when the underlying video file is an MP4. Uses the existing `Mp4RemuxService` (mkvmerge) to remux the container without re-encoding, automatically attaching any subtitle files found in the same folder. The menu item only appears when the file is actually an MP4 — MKV files won't see it.

**Fix: Cross-library folder matching threshold.** The v0.6.7 cross-library folder fallback used a 0.95 fuzzy match threshold — too strict compared to the 0.85 threshold used everywhere else. Series matching at e.g. 0.88 would be found in the primary library but missed by the fallback, creating duplicate folders in the default library. Now uses 0.85 consistently.

**"Rename All" button on all hold queues.** All 6 hold queue sections (Movies, TV, Music, Audiobooks, Books, Comics) now have a "Rename All" button that approves and renames every item in the queue at once. Also added missing "Reprocess All" buttons to the Music, Audiobooks, Books, and Comics hold queues (previously only Movies and TV had them).

**Watcher orchestration extracted from MainWindow.** New `WatcherOrchestrationService` owns the `FileWatcherService` lifecycle, auto/manual routing, and status management. MainWindow is now a thin subscriber for UI updates — it no longer touches the watcher directly. Web API endpoints updated to use the orchestrator. No behavior changes.

**MainWindow refactor: 5 service extractions (~950 lines removed).** MainWindow.xaml.cs reduced from ~2750 to ~1800 lines by extracting self-contained business logic into four dedicated services: `EpisodeSyncService` owns startup and periodic episode sync, TMDB/TVDB season refresh, missing episode scanning, and want list population; `ManualRenameOperations` owns non-video rename handlers (single, album batch, chapter batch, series batch), fetch metadata, and download/assign art; `LibraryCacheUpdateService` owns DB upsert after manual movie/TV rename (was duplicated between MainWindow and AutoRenameService); `LoggingService` owns message filtering, log level gating, and file logging. MainWindow retains only UI concerns: collection binding, click handler delegation, progress bar animation, and window management.

## v0.6.7
**Fix: Genre-based routing resilience.** TV and Movie parsers now load genres from the local database when a cache hit occurs, so routing rules (e.g., Animation → Animated TV) work even when the TMDB genre-detail API call fails transiently (rate limit, network error). Previously, genres were only populated from a live API call — a cache hit for the series ID would skip the genre fetch entirely (movies) or leave genres empty if the API failed (TV), causing files to fall through to the default library.

**Cross-library folder fallback.** When routing rules don't match (e.g., genres unavailable), the path builder now scans all non-default libraries of the same type for an existing folder matching the series/movie name before falling back to the default library. This prevents files from creating duplicate folders in the wrong library when the content was previously routed correctly.

**Reorganize preview confirmation for all content types.** All Reorganize actions (TV, Movies, Books) now show a preview dialog listing every file/folder rename before applying. Uses the existing `CascadingActionConfirmDialog` with old name → new name rows. If nothing needs changing, shows "Already organized" instead. New `PreviewReorganize` dry-run methods added to `TvLibraryOperations`, `MovieLibraryOperations`, and `BookLibraryOperations`.

**Reorganize added to TV season and episode context menus.** The "Reorganize Files" action was previously only available at the series level; it now appears on season and individual episode right-click menus as well. Season-level reorganize scopes to that season only; episode-level scopes to that single episode.

**Reorganize added to Music, Audiobooks, and Comics.** "Reorganize Files" now appears on all drill-down levels: Music (Artist, Album), Audiobooks (Author, Book), Comics (Publisher, Series, Issue). Each level renames just that level's folder or file according to the naming convention. All show the preview confirmation dialog.

**Provider logos on About screen.** All 12 metadata providers now have logo tiles in the About window (was 3). Logos moved to `Resources/Providers/` subfolder. Cards use a `WrapPanel` so they flow across multiple rows. Each card links to the provider's website. The 3 old per-provider click handlers replaced with a single `Provider_Click` that reads the URL from the element's `Tag`.

**New `DeezerClient.cs` and `MusicBrainzClient.cs` API clients.** Extracted inline HTTP calls from `MusicLibraryOperations.GetAlbumArtCandidatesAsync()` into proper dedicated client classes matching the pattern of the existing 13 API clients (TmdbClient, DiscogsClient, etc.). `DeezerClient` handles keyless album art search. `MusicBrainzClient` handles release search + CoverArtArchive cover art lookup with parallel requests.

**New `ComicLibraryOperations.cs` service class.** Comics now follows the same architectural pattern as the other 5 content types: thin UI control backed by a dedicated operations service. Business logic (DB queries, metadata fetch, file moves, save, delete) extracted from `ComicsLibraryManagerWindow.xaml.cs` (~340 lines removed) and `ComicLibraryWriter.cs` (absorbed entirely, file deleted) into the new consolidated service. No behavior changes — pure structural refactor for uniformity.

## v0.6.6
**Codebase audit and tidy-up.** Full repository sweep covering dead code removal, duplicate logic consolidation, context menu audit, optimization review, and security review. See `CODEBASE_AUDIT_REPORT.md` for full details.

**API key obfuscation in logs.** All HTTP API clients (TMDB, OMDb, Fanart.tv, Discogs, Google Books) now sanitize URIs before logging — query-string parameters like `api_key`, `apikey`, `client_key`, `token`, and `key` are replaced with `***`. New shared helper `PathHelper.SanitizeLogUri()`. ComicVine already had its own redaction; Last.fm and GoodReads don't log full URIs.

**EF Core and SQLite package updates.** Updated Microsoft.EntityFrameworkCore from 9.0.0 to 10.0.9 (matches net10.0 target), SQLitePCLRaw.bundle_winsqlite3 from 2.1.10 to 2.1.11. NU1903 advisory for transitive `SQLitePCLRaw.lib.e_sqlite3` suppressed — project uses Windows system SQLite via `bundle_winsqlite3`, so the vulnerable bundled native lib is never loaded.

**JaroWinkler consolidation.** Removed 5 private copies of the Jaro-Winkler fuzzy matching algorithm (in DirectoryCacheService, TvLibraryOperations, MovieLibraryOperations, MissingEpisodesWindow, MetronMetadataProvider) — all now use the single canonical implementation in `ParserUtilities.JaroWinkler()`. ~220 lines of duplicate code removed.

**OverwriteDecisionService cleanup.** `DecideForManual` and `DecideForHold` were byte-for-byte identical; consolidated into a shared `DecideWithPrompt` method.

**Context menu architecture overhaul (Parts 1-6).** All six library controls (Comics, TV, Movies, Music, Audiobooks, Books) now use a shared `ContextMenuBuilderBase<T>` + `MenuGroup` enum system that enforces consistent grouping: Identity → Art → FileOps → Export → BulkOps → LibraryState → Destructive. Empty groups produce no separator. 13 new builder classes in `Services/ContextMenus/` replace the per-type hand-written menu construction. New `CascadingActionConfirmDialog` with preview list and "Also delete files from disk" checkbox (unchecked by default, matching existing TV RemoveAsync behavior). New `LibraryDeleteHelper` handles DB removal + optional file deletion with logging.

**Movies migrated to builder architecture.** New `MovieMenuBuilder` with all 16 original actions (Fetch, Edit, Reorganize, Download/Assign Art, Export WMC/Kodi/Jellyfin NFO, Transcode x265, Normalize Audio, Edit Tracks, Attach Subtitle, Open File, Lock/Unlock, Remove) organized by MenuGroup. Delete now routes through the shared confirmation dialog.

**TV Season menu expanded.** Season context menu now has 8 items (up from 2): Download Season Art, Assign Season Art, Normalize Season Audio, Download Missing (Season), Download Season Pack, Export Kodi/Jellyfin NFO (Season), Delete Season. Season Delete removes all episode DB rows for that season and optionally the season folder.

**Unified grid/list context menu wiring.** All card-view context menus now rebuild on each right-click (ContextMenuOpening) instead of being built once at card-creation time, ensuring the menu reflects current state (lock/unlock labels, art presence, selection). List-mode context menus added for Comics (Publisher/Series) and TV (Series/Season) — these previously had no right-click menu in list view.

**Multi-select batch editing.** Comics IssueListView, TV EpisodeListView, Music TrackListView, and Audiobook PartListView now support `SelectionMode="Extended"` (Ctrl/Shift-click). When 2+ items are selected, the context menu shows "✏ Edit Selected (N)…" instead of the single-item Edit. Batch-editable fields per type: Comics = Writer, Penciller, Inker, Colorist, Letterer, CoverArtist, Editor, Genre, ComicFormat, Variant, StoryArc, Teams, Locations, Characters; TV Episodes = Rating; Music Tracks = Artist, Album Artist, Album, Year, Genre, Label (writes directly to ID3/Vorbis tags via TagLib); Audiobook Parts = Author, Book Title, Year, Genre, Narrator, Series (writes to tags). New `BatchEditDialog` with per-field checkboxes — only checked fields are applied. Changes go through `CascadingActionConfirmDialog` for preview before committing.

**Delete capability at every library tier.** New Delete menu item on every navigable level across all media types: Comics (Publisher/Series/Issue), TV (Series/Episode), Music (Artist/Album), Audiobooks (Author/Book), Books (Author/Book). All route through the shared confirmation dialog. DB-only removal is the default; "Also delete files from disk" is opt-in. Publisher/Author-level deletes cascade to all child records.

**Episode detail backfill on series re-fetch.** When using "Fetch Metadata" or "Re-fetch" on a TV series, the app now checks for existing episodes missing overview or rating and backfills them from the preferred provider. Uses the season-level API (one call per season, not per episode) so it's efficient even for large series. TVDB is tried first for overview when preferred; TMDB is always checked for ratings since TVDB doesn't expose per-episode scores. Only updates rows that actually lack data — existing overviews/ratings are never overwritten.

**TV episode overview and rating now fetched and persisted.** When episodes are added to the database from TMDB, the episode overview and vote average are now saved (previously dropped). `TmdbEpisodeSummary` and `TmdbEpisodeDetails` DTOs gain `Overview` and `VoteAverage` fields matching the TMDB API response. TVDB episodes already had `Overview` in the DTO but it wasn't mapped on insert — now it is. TVDB doesn't expose per-episode ratings so those remain TMDB-only. The per-episode "Fetch & Compare" editor now shows proposed rating alongside title, air date, and overview. Provider preference is respected: TVDB-preferred series fetch episode details from TVDB first with TMDB fallback.

**Title bar simplified.** Removed the build timestamp from the window title — now shows just "Abigail's Media Renamer v0.6.6".

**TV series hero banner now has Edit and Fetch buttons.** Matches the pattern from Music/Audiobooks/Books — the series hero banner in the Seasons view now shows "✏ Edit Metadata" (opens the series metadata editor) and "🔍 Fetch Metadata" / "🔄 Re-fetch" buttons. The series card context menu item is renamed from "📝 Open Metadata" to "✏ Edit Metadata" for consistency.

**"Open File" button on all library detail views.** New "▶ Open File" button in the detail pane of all six library controls (TV, Movies, Music, Audiobooks, Books, Comics). Opens the media file in the system's default application. Movies and Books show a picker dialog when multiple files exist in the folder (e.g., multiple video files or epub + pdf). Music and Audiobooks offer "Play all as playlist" (generates a temp .m3u and opens it in the default player) or "Choose a single file". Shared logic in `PathHelper.OpenMediaFile` / `OpenMediaFilesWithPlaylistOption`.

**Fix: TV season art now saves to season subfolder.** Season poster art downloaded from the series-level metadata editor was being saved to the series root as `season01-poster.jpg` instead of into the season subfolder as `poster.jpg`. This meant Jellyfin/Kodi/Plex couldn't find it, and the season-level view in the app also couldn't display it. Both `ArtCacheService.DownloadSeasonPosterAsync` and `MetadataDetailWindow.AddSeasonPosterTile` now save to `{series}/Season 01/poster.jpg` following the standard media server convention. `FindSeasonPoster` search order updated to prefer `poster.jpg` first.

**Context menus on list-mode ListViews.** All four library controls (Movies, Music, Audiobooks, Books) now have context menus in list view mode, matching the context menus already present on their card-view counterparts. Right-clicking a row in list mode shows the same actions as right-clicking the card (Fetch, Edit, Download Art, Export, Open in Explorer, etc.). The menus are built dynamically from the selected item using the existing `Build*ContextMenu` methods.

**Plugin security hardening.** Three improvements to the plugin loading system: (1) SHA-256 hash of every plugin DLL is computed and logged at startup for auditability. (2) New/modified plugins now require user consent — a confirmation dialog appears when an unrecognized DLL is detected, and accepted hashes are persisted in `TrustedPluginHashes` in settings. (3) Capability validation warns in the log if a plugin's declared `PluginType` doesn't match its implemented interfaces (e.g., declares `DownloadProvider` but doesn't implement `IDownloadProvider`).

**Dead code removal.** Removed dead `SourceRank(null)` call in QualityUpgradeService, empty `LibraryTab_SelectionChanged` handler and trivial `OnClosed` override in LibraryManagerWindow, unused `using System.Threading` import.

## v0.6.5
**Settings window reorganization.** Tabs reordered to: General, Libraries, Routing, Metadata, Scene Tags, Naming, Metadata Export, File Tester, Downloaders, Transcode, Appearance, Web, Plugins. Automation settings (overwrite, queue delay, confidence threshold, rescue options) moved from General to Naming. Tab visibility checkboxes moved from General to Libraries (top). Excluded folder names moved from Scene Tags to Libraries (bottom). Comic edition preference moved from Metadata to Downloaders.

**Content Type Preferences section on Downloaders tab.** New collapsible section replaces the old Quality Preferences section, adding per-content-type filetype priority lists for Music (FLAC/ALAC/WAV/MP3/...), Audiobooks (M4B/MP3/M4A/...), Books (EPUB/AZW3/MOBI/PDF/...), and Comics (CBZ/CBR/CB7/PDF/...). Priority lists are reorderable with Up/Down buttons. All existing Movie & TV quality settings (resolution, audio, source, codec, HDR, seeders, release groups) are preserved under a "Movie & TV" sub-heading within Content Type Preferences.

**Container preference for Movie & TV downloads.** New dropdown (MKV/MP4/AVI/No preference) acts as a tie-breaker when selecting between otherwise-equal release candidates during automatic download selection. Container format is now parsed from torrent/NZB titles.

**Automatic Search controls moved** from Quality Preferences into the Automatic Downloads section, directly after the retry cooldown controls.

**Container and filetype preference logging.** When container preference meaningfully affects candidate selection, a `[FILTER]` log line is emitted for debuggability, consistent with existing quality preference logging.

## v0.6.4
**Deezer album cover art for music.** Music cover art now searches Deezer first (no API key needed) before falling back to MusicBrainz/CoverArtArchive. Deezer returns high-quality 1000×1000 direct image URLs instantly — no per-release lookup needed. Both the art picker dialog (shows Deezer candidates alongside MusicBrainz) and the quick "Download Art" action benefit.

## v0.6.3
**TV series hero banner.** When drilling into a TV series in the Library Manager, the Seasons panel now shows a hero banner with the series poster, title, year, status, rating, genres, and overview (click to expand). Mirrors the author hero banner pattern from Books/Audiobooks.

**New `Artists` database table — persistent music artist metadata.** Artists are promoted from a plain string field on `DbAlbum` to a first-class entity with bio, tags, photo, Last.fm URL, and listener stats. On first launch after upgrade, existing `Artist` strings from album rows are automatically deduplicated into `DbArtist` rows, and `ArtistId` foreign keys are backfilled. Same pattern as the `Authors` table for Books/Audiobooks.

**Music artist hero banner with Last.fm + TheAudioDB integration.** When drilling into an artist's albums, a hero banner shows the artist name, tags, bio, listener count, and Last.fm link. All metadata is persisted in the `DbArtist` table — data survives navigation and app restarts. "Fetch from Last.fm" pulls bio/tags/stats from Last.fm and the artist photo from TheAudioDB. "Edit Metadata" opens the artist editor for manual corrections.

**Artist editor window.** New `ArtistEditorWindow` for editing artist metadata: name, tags, Last.fm URL, listeners, and bio. Left panel shows the artist photo (circular) with "Fetch Photo" (from TheAudioDB) and "Assign Art…" buttons. Saves directly to the `DbArtist` table.

**Artist photos on parent-level cards.** Artist cards in the grid view now show cached photos (from the image cache) as the card cover image, same as book author cards.

**Context menu on artist cards.** Right-clicking an artist card shows "Edit Metadata", "Fetch from Last.fm", and "Open in Explorer".

**New Last.fm API client.** `LastFmClient` wraps `artist.getinfo` for bios, tags, and listener stats (hardcoded developer API key). Artist images are sourced from TheAudioDB (free API, no key needed) since Last.fm deprecated their artist image API in 2020.

**Album art picker dialog.** "Download Art" for music albums now opens the art picker dialog showing all matching releases from MusicBrainz/CoverArtArchive (up to 15 results). Each candidate shows artist, title, year, and country. Pick the correct cover instead of blindly taking the first MusicBrainz match. Works from both the album detail view and the album card context menu.

**Web UI: artist photos in music library.** The music browser now shows circular artist thumbnails next to each artist row when a cached photo exists. New `/api/art` image proxy endpoint serves images from the image cache with token auth (supports query-param token for `<img>` tags).

## v0.6.2
**Centralized Image Cache Location.** New `ImageCachePath` setting on the General tab (Settings → General → Image Cache) with Browse and Open buttons. All cached images (author photos, comic cover thumbnails) are now stored in a single configurable folder with content-type subdirectories (`authors/`, `comics/`). Defaults to `%APPDATA%\AbigailsMediaRenamer\ImageCache`. The old `ComicCoverCacheFolder` setting is automatically migrated.

**Automatic image resizing on save.** New `ImageResizeHelper` utility resizes all saved images to sensible UI sizes — posters 600px, fanart 720px, banners 200px, logos 300px, clearart 500px, author photos 400px, comic thumbnails 320px. Supports both JPEG (quality 85) and PNG (transparency preserved). Falls back to raw save if decode fails. Applied to all art downloads (ArtCacheService), all WriteCover methods (TV, Movies, Music, Books, Audiobooks), author photo downloads, and the author editor's Assign Art.

## v0.6.1
**New `Authors` database table — shared author metadata for Books and Audiobooks.** Authors are promoted from a plain string field to a first-class entity with bio, birth/death dates, photo, and Open Library ID. On first launch after upgrade, existing `Author` strings from both Books and Audiobooks tables are automatically deduplicated into `DbAuthor` rows, and `AuthorId` foreign keys are backfilled on all matching records. The same author appearing in both libraries shares a single DB row.

**Open Library author search and detail API.** `OpenLibraryClient` gains `SearchAuthorsAsync` and `GetAuthorAsync` endpoints for fetching author metadata from openlibrary.org (no API key required). Author photos are downloaded and cached locally at `%APPDATA%\AbigailsMediaRenamer\author_photos\{OLID}.jpg`.

**Author hero banner in Books and Audiobooks.** When drilling into an author's books, the simple text header is replaced by a full-width banner showing the author's photo (100x100, rounded), name, birth/death dates, bio snippet (click to expand), and book count. Falls back gracefully to just name + count when no metadata has been fetched.

**Context menu on author cards.** Right-clicking an author card in Books or Audiobooks shows "Edit Metadata" (opens the author editor), "Fetch from Open Library" (opens the picker), and "Open in Explorer".

**Author editor window.** New `AuthorEditorWindow` for manually editing author metadata: name, Open Library ID, birth/death dates, website URL, known-for, and bio. Left panel shows the author photo with "Fetch Art from Open Library" and "Assign Art…" buttons. Saves directly to the `DbAuthor` table.

**Author picker dialog.** `AuthorPickerDialog` searches Open Library with photo thumbnails, OLID display, and direct OLID lookup (paste e.g. `OL272947A` to look up by ID). Shows dates, top work, and work count for each result.

**Markdown stripping for bios and book descriptions.** `OpenLibraryClient.StripMarkup()` removes markdown formatting (`*italic*`, `**bold**`, `[links](url)`, HTML tags, headings) from Open Library text. Applied to author bios at fetch time and book plot summaries at display time.

**Fixed: Open Library bio deserialization.** Added `OpenLibraryTextConverter` to handle the OL API returning `bio` as either a plain string or a `{type, value}` object. Previously plain-string bios (most authors) silently deserialized to null.

**DbAuthor gains `Website` field** for author web URLs, auto-populated from Open Library links on fetch.

## v0.5.113
**New `ComicSeries` database table — series metadata is no longer duplicated across issue rows.** Series-level data (name, publisher, start year, volume, genre, description, provider IDs, cover) now lives in a dedicated `ComicSeries` table. Each `DbComic` issue row gets a `ComicSeriesId` foreign key. On first launch after upgrade, existing data is automatically migrated: one `DbComicSeries` row is created per unique series folder, and all issue rows are linked to their series. The "representative row" hack is gone — series cards are built directly from `DbComicSeries`.

**Series editor now saves to the series record only.** The "Save to Database" button is renamed to "Save". Saving updates the `DbComicSeries` row — it no longer writes series-level fields (publisher, year, genre, description, provider IDs) to every issue row. Issue data is untouched by series saves.

**Provider sync decoupled from Save — "Re-Fetch Issues" button added.** Saving the series no longer automatically triggers a Metron/ComicVine sync. A new "Re-Fetch Issues" button next to the provider ID fields lets you manually refresh the cached issue list on demand — useful for ongoing series without penalising saves on finished series.

**Art picker added to the series editor.** Three cover buttons: "Download Cover" shows issue covers from the Metron/ComicVine provider cache. "Assign Cover" extracts covers from every issue archive in the local library and shows them in the picker — pick any issue's cover art as the series cover. "Browse…" opens a file picker for a specific image file from disk. All three save to `cover.jpg` in the series folder and update `DbComicSeries.CoverLocal`.

**ComicSeriesSyncService writes series data to `DbComicSeries`.** Provider-pulled series-level fields (publisher, start year, genre, description) now go to the series row, not sprayed across issue rows. Per-issue matching (title, cover date, credits) continues to write to individual `DbComic` rows.

**Parser pipelines query `DbComicSeries` for enrichment.** Both `ComicParser` and `MediaParser` now look up `MetronSeriesId`/`ComicVineSeriesId` from the series table instead of scanning issue rows.

**ComicLibraryWriter links new issues to their series row.** Auto-parsed comics get `ComicSeriesId` set, and a `DbComicSeries` row is created on the fly if the series folder is new.

**Folder rename updates `DbComicSeries.FolderPath`.** The series editor's convention-rename step now repoints the series row alongside the issue rows.

## v0.5.112
**Series card now shows the series start year and series description, not the most recently saved issue's.** The series grid picks a "representative" DB row to populate the card — previously sorted by `LastUpdated`, so saving any issue made *that* issue's cover year and description appear on the series card. Now `CoverYear` and `Description` are overridden with series-level data from the provider cache (`DbMetronSeries.YearBegan` / `.Description`, `DbComicVineSeries.StartYear`), falling back to the folder-parsed year. Issue-level data no longer bleeds into the series card.

## v0.5.111
**Filmstrip scroll position snaps back to the selected item after fetch/save/reload.** After a metadata fetch, issue save, or any operation that reloads the filmstrip list, the previously selected item is now scrolled into view (via `ScrollIntoView`) in addition to being re-selected. Previously the item was highlighted but the scroll position reset to the top, requiring manual scrolling to find it again. Fixed in the Comics issue filmstrip and TV episode filmstrip — the two content types with item-level selection in their lists. Music and Audiobooks have album/book-level saves (no per-track selection to restore).

## v0.5.110
**Per-issue series names are now preserved across provider fetches and series edits.** Previously the series editor blanket-wrote the folder-level series name to every issue's DB row, and the issue list view overrode each issue's `SeriesName` in memory with the folder name. This erased provider-sourced per-issue titles — e.g. X-Men (1963) issues #114+ should be titled "Uncanny X-Men" (as Metron/ComicVine return), but were being flattened to "X-Men". Now: (1) the series editor only overwrites an issue's `SeriesName` when it still matches the old folder-level name or is blank, leaving provider-sourced names intact; (2) the issue list view uses the DB's per-issue `SeriesName` when available, falling back to the folder name only for blank rows. New files discovered in a folder still default to the folder-level name until a provider fetch populates them.

## v0.5.109
**MetronInfo.xml now injected alongside ComicInfo.xml on every comic save/repack.** All three write paths (in-place CBZ update, managed repack, and external-extractor repack) now produce both metadata XMLs. MetronInfo.xml uses the v1.0 schema: proper array elements instead of comma-delimited strings (Characters, Teams, Locations, Genres, Arcs), a structured `<Credits>` block mapping each creator to their role(s) (Writer, Penciller, Inker, Colorist, Letterer, Cover, Editor), typed `<IDS>` entries with `source` attributes for Metron and Comic Vine, a complex `<Series>` element (Name, SortName, Volume, Format, StartYear, IssueCount), and `<Publisher><Name>` nesting. CoverDate uses ISO `YYYY-MM-DD` format. `<LastModified>` is always written with the current UTC timestamp. Same authoritative vs. merge semantics as ComicInfo.xml — user "Save" rewrites everything (clearing empties), background metadata fetches only fill/overwrite. Existing MetronInfo.xml in an archive is preserved in merge mode and fully rewritten in authoritative mode.

**Fixed: series rename left the UI pointing at the old (deleted) folder path.** `RenameSeriesFolderToConventionAsync` moved the folder and updated DB rows but didn't update the in-memory `ComicLibraryEntry.FolderPath`, so clicking into the series after a rename showed an empty issue list. The entry's `FolderPath` is now updated immediately after the move.

**Fixed: Metron provider silently skipped in interactive dialogs when ComicVine was preferred.** Both the series editor lookup and the comic picker dialog passed `stopAfterPreferred: true`, so when ComicVine (the preferred provider) returned results, the loop broke before Metron was ever queried. Changed to `false` for both interactive dialogs — all configured providers now run. The automated scan paths (`MediaParser`, `ComicParser`) still use `true` to avoid unnecessary rate-limit pressure during bulk operations.

**Improved Metron diagnostic logging.** All `MetronClient` entry points now log explicitly when credentials are missing (was a silent `return new()`). Non-2xx API responses now log the response body snippet (was discarded — a 401 just said "HTTP 401" with no detail).

## v0.5.108
**Comic series editor save is now fully authoritative — clearing a field actually clears it.** Previously, guard clauses like `if (!string.IsNullOrWhiteSpace(publisher))` prevented writing empty/cleared values back to the DB, so you couldn't remove a wrong publisher or genre once set. The save now writes the user's value unconditionally (empty string → null in DB). The in-memory `ComicLibraryEntry` is also updated authoritatively to match.

**Provider sync no longer overwrites user edits.** After saving, the Metron/ComicVine sync pass only fills fields the user left blank — if you typed a publisher and the provider disagrees, yours wins. Previously the sync blindly overwrote publisher, year, and genres.

**Series description now pulled from Metron.** `MetronClient` captures the `desc` field from the series-detail response, stored in `DbMetronSeries.Description` (auto-added via `TryAddColumn`). `ComicSeriesSyncService` exposes it as `SyncResult.SeriesDescription` and the series editor populates the Description box from it (only when the user left it blank).

**Series cover now uses issue #1 instead of the alphabetically-first archive.** `ComicCoverService.EnsureSeriesCoverAsync` sorts archives by parsed issue number (regex: `#NNN`, `N of M`, bare trailing number) with alphabetical fallback, so issue #1's cover is used for the series card.

**Covers are resized to thumbnail dimensions on cache.** Extracted covers are resized to 320px tall (aspect-preserved) and saved as JPEG, keeping the cache compact (was storing full 1–2 MB scans verbatim). Uses WPF's `BitmapImage` + `TransformedBitmap` + `JpegBitmapEncoder`.

**Configurable cover cache.** New settings `ComicCoverFromArchive` (always extract fresh, bypass cache) and `ComicCoverCacheFolder` (custom cache directory; defaults to `%APPDATA%\…\CoverCache`).

**Quick refresh after series rename.** Closing the series editor after a save no longer triggers a full publisher rescan. The card's display properties (`SeriesName`, `CoverYear`, `Genre`) update in place via `INotifyPropertyChanged`, and the series grid rebuilds sort/filter order. If the on-disk folder was renamed, `FolderPath` is repointed and the cover is re-extracted from the new location.

**"N of M" issue number now parsed.** `ParserUtilitiesComics` extracts the issue number `N` from limited-series patterns like `"Batman 1 of 4"` or `"She-Hulk 003 of 006"` (requires M ≥ 2 to avoid false positives). The `"N of M"` text is stripped from the series name.

**New naming tokens: `{SeriesFirstYear}` and `{CoverDate}`.** `{SeriesFirstYear}` is the series-level start year from metadata (useful for folder naming to disambiguate same-named volumes). `{CoverDate}` formats the issue's cover date as `Mon YYYY` (e.g. `Jan 1963`). Both appear in the Comics token picker. The existing `{Year}` token is unchanged (backward compat). `PathBuilderService` wires them from `ParsedComicMedia.SeriesFirstYear` and a new `FormatCoverDate` helper.

**Comic library search works on every level.** The toolbar search box now filters the **Publisher** grid (by name), the **Series** grid (name / genre / publisher / writer), and the **Issue** filmstrip (number / title / writer / characters) — applied to whichever drill-down level is on screen. Previously only matched series name/genre.

**Series folder auto-renamed to naming convention on save.** After a series metadata edit/save in the editor, the leaf series folder is renamed in place to match the active comics naming convention (e.g. `Series (Year)`), and all DB rows under it are repointed. Skips if the target already exists; never relocates across publisher folders; best-effort (never fails the save).

**Fixed: series rename left the UI pointing at the old (deleted) folder path.** `RenameSeriesFolderToConventionAsync` moved the folder and updated DB rows but didn't update the in-memory `ComicLibraryEntry.FolderPath`, so clicking into the series after a rename showed an empty issue list (`Directory.Exists` on the old path returned false). The entry's `FolderPath` is now updated immediately after the move, and the property was changed from `init`-only to `set` so it can be repointed at runtime.

## v0.5.107
**Comic "Save" now authoritatively rewrites ComicInfo.xml (final-commit semantics).** The library Save button is the *last step* — "I'm done, write the final copy." It now persists the entry to the DB **and** completely rewrites the file's managed ComicInfo.xml fields to match: changed values overwrite, **fields you cleared are actually removed**, and stale/duplicate elements are collapsed (non-managed elements like `<Pages>` are preserved). Previously the in-place `.cbz` path only *merged* — it could overwrite a value but never clear one and could leave stale data, so edits often didn't fully "take." Background metadata fetches still use the gentle **merge** (fill/overwrite, never clear) so a provider that lacks a field can't wipe your manual edits. Implemented via a new `authoritative` flag on `ComicInfoService.UpsertComicInfo` (→ `ApplyMetadata`/`SetOrRemove`); the editor Save passes `authoritative: true`.

**Assigning a comic series match now renames the on-disk folder to the naming convention.** After a series metadata lookup/save in the series editor (or any series-metadata edit), the leaf series folder is renamed in place to the comics convention's series-folder form (e.g. `{Publisher}/{Series} ({Year})/…` → folder becomes `Series (Year)`), and every issue's `FolderPath`/`FilePath` in the DB is repointed to the new path. Renames only when the name actually differs, skips if the target already exists, never relocates across publisher folders, and is best-effort (a failure won't fail the save). The series grid reloads from disk when the editor closes, so the new name shows immediately.

**Comic library search box now works on every level.** The toolbar search ("series, issues, writers…") previously did nothing except on the Series panel (and only matched series name/genre). It now filters the **Publisher** grid (by name), the **Series** grid (name / genre / publisher / writer), and the **Issue** filmstrip (number / title / writer / characters) — applied to whichever level is on screen.

## v0.5.106
**Books library migrated to the Comics-style browser (standalone control) — completes the Library Manager migration.** The old two-pane Books tab is replaced by `BooksLibraryControl`: breadcrumb **Author → Book → detail**, cover-art cards (grid/list toggle, author sort Name/Books/Size, search), and a **full-width book detail/editor** (no filmstrip — books are file(s); the detail lists each e-book file with its format/size).

**Side-by-side compare — new for books, visually identical to the other editors.** "Fetch & Compare" opens the existing `NonVideoFixMatchDialog` (GoogleBooks/OpenLibrary/GoodReads/Kindle), enriches the pick with full provider details, then shows Current | Proposed | ✓ accept-per-field rows (Title, Author, Series, Series #, Year, ISBN, Publisher, Genre, Description). Save writes the `metadata.json` sidecar, the cover, and the DB row.

**No capability lost.** Fetch Metadata, **Reorganize Folder** (naming-convention rename), Download Art, Assign Art, and Open in Explorer all carry over (detail buttons + book card context menu). `BookMetadataWindow` superseded.

**Logic extracted to a reusable service.** Author/book scanning (sidecar-aware), provider fetch, art, folder reorganize, and the sidecar save moved into `Services/BookLibraryOperations.cs`; view models under `ViewModels/Books/`. Old handlers removed from `LibraryManagerWindow`; the tab is now `<ContentControl x:Name="BooksTabContent"/>`. Implements `docs/library-migration/05-books.md`.

**All six Library Manager sections (Comics, TV, Movies, Music, Audiobooks, Books) now use the unified Comics-style browser.** Each is a standalone `UserControl` + `*LibraryOperations` service; `LibraryManagerWindow` is now just a tab host (~2900 lines of per-type handlers removed across the migration).

## v0.5.105
**Audiobooks library migrated to the Comics-style browser (standalone control).** The old two-pane Audiobooks tab (author grid over title grid) is replaced by `AudiobooksLibraryControl` matching the other migrated sections: breadcrumb **Author → Book → parts**, cover-art cards (grid/list toggle, author sort Name/Books/Size, search), and a book-detail view with a **parts/files filmstrip** plus an inline editor.

**Side-by-side compare — new for audiobooks, visually identical to the TV/Movies/Music editors.** "Fetch & Compare" opens the existing `AudiblePickerDialog`, then renders the same Current | Proposed | ✓ accept-per-field rows for every field (Title, Author, Narrator, Series, Series #, Year, Genre, ASIN, Description). Per-file part titles are editable in the same editor. Save writes ID3 tags (incl. TXXX `ASIN`/`DESCRIPTION`, series in Comment), the cover, the DB row, and — when enabled — the Audiobookshelf `metadata.json`. Local-metadata supplementation (`AudiobookLocalMetadataReader`: tags/OPF/JSON/desc.txt) is preserved.

**No capability lost.** Audible fetch, Download Art, Assign Art, Export Audiobookshelf metadata.json, and Open in Explorer all carry over (detail buttons + book card context menu). Books with art only embedded in tags still show a cover. `AudiobookMetadataWindow` is superseded.

**Logic extracted to a reusable service.** Author/book/part scanning, Audible fetch, art, the tag-writing save, and the Audiobookshelf export moved into `Services/AudiobookLibraryOperations.cs`; view models under `ViewModels/Audiobooks/`. Old handlers removed from `LibraryManagerWindow`; the tab is now `<ContentControl x:Name="AudiobooksTabContent"/>`. Implements `docs/library-migration/04-audiobooks.md`. (Flat Author→Book like the previous scan; series shown in the card caption — a series-folder drill level remains a future nicety.)

## v0.5.104
**Music library migrated to the Comics-style browser (standalone control).** The old two-pane Music tab (artist grid over album grid) is replaced by a new `MusicLibraryControl` with the same look as the other migrated sections: breadcrumb drill-down **Artist → Album → tracks**, cover-art cards (grid/list toggle, artist sort Name/Albums/Size, search), and an album-detail view with a **track filmstrip** (track #, title, duration) plus an inline editor.

**Side-by-side metadata compare — new for music, visually identical to the TV/Movies editors.** "Fetch & Compare" runs a Discogs search, shows the candidate releases inline (preserving the old multi-release choice), then renders the same Current | Proposed | ✓ accept-per-field rows for the album fields **and every track title** (Discogs tracklist mapped to local files by disc/track, else ordinal). Save writes ID3 tags to all tracks, the cover, and the DB row — same as the old window.

**No capability lost.** Album field editing, auto-search, Download Art (MusicBrainz/CoverArtArchive), Assign Art (local file), and Open in Explorer all carry over (detail buttons + album card context menu). Albums with art only embedded in tags still show a cover (extracted for the detail view). The `MusicAlbumMetadataWindow` is fully superseded.

**Logic extracted to a reusable service.** Artist/album/track scanning (TagLib), Discogs search, art, and the tag-writing save moved into `Services/MusicLibraryOperations.cs`; view models under `ViewModels/Music/`. The old music handlers were removed from `LibraryManagerWindow`; the tab is now `<ContentControl x:Name="MusicTabContent"/>`. Implements `docs/library-migration/03-music.md`.
Artist images (last.fm/Plex) remain a deferred enhancement — artist cards use a ♪ placeholder for now.

## v0.5.103
**Movies library migrated to the Comics-style browser (standalone control).** The old Movies DataGrid tab is replaced by a new `MoviesLibraryControl` matching the comics/TV look: a poster-card grid (grid/list toggle, sidebar sort/status/year/genre filters, search) and a full-width **detail/editor** view (no filmstrip — movies are single items). The detail view has an **inline editor with side-by-side metadata compare** (Current | Proposed | ✓ accept-per-field) reached via "Fetch & Compare". Poster cards use the taller 2:3 cover (240px) so art isn't cropped, with status/lock badges and a genre pill.

**No capability lost.** Toolbar: Identify All, Reorganize. Per-movie (card context menu + detail buttons): Fetch Metadata, Open/Edit, Reorganize, Download/Assign Art, Export WMC/Kodi/Jellyfin, **Transcode to x265, Normalize Audio, Edit Tracks, Attach Subtitle File**, Lock/Unlock Quality, Remove from Library, Open in Explorer. The Scan-Folder button was intentionally dropped.

**Logic extracted to a reusable service.** All movie business logic moved out of `LibraryManagerWindow` (~750 more lines / 31 handlers removed) into self-contained `Services/MovieLibraryOperations.cs`; view models under `ViewModels/Movies/`. The window now hosts `<ContentControl x:Name="MoviesTabContent"/>`. Implements `docs/library-migration/01-movies.md`.

## v0.5.102
**TV library migrated to the Comics-style browser (standalone control).** The old TV Series DataGrid tab in the Library Manager is replaced by a new `TvLibraryControl` with the same look & feel as the comics section: breadcrumb drill-down **Series → Seasons → Episodes**, cover-art grid/list cards, a virtualized episode **filmstrip** (missing episodes shown dimmed, like missing comic issues) with an owned/total progress header, and a right-hand detail pane that swaps into an **inline editor with side-by-side metadata compare** (Current | Proposed | ✓ accept-per-field) for episodes. Series cards carry poster art, a status/lock badge, and a genre pill; the sidebar offers sort (Name/Year/Seasons), status (All/Identified/Unidentified), year-range, and genre filters; plus a search box and grid/list toggle.

**No capability lost.** Every old TV feature is preserved: Identify All, Reorganize, Rescan Episodes (toolbar); and per-series Fetch Metadata, Open Metadata (full `MetadataDetailWindow`), Reorganize, Download/Assign Art, Export WMC/Kodi/Jellyfin, Flush Metadata, Normalize All Audio, Download Missing Episodes, Download Season Pack, Lock/Unlock Quality, Remove from Library, and Open in Explorer (context menu). Episodes add per-episode Edit, Fetch & Compare, Normalize Audio, and Open in Explorer. The Scan-Folder button was intentionally dropped (unused).

**Logic extracted to a reusable service.** All TV business logic was moved out of `LibraryManagerWindow` (~1140 lines removed, including ~32 now-dead handlers) into a self-contained `Services/TvLibraryOperations.cs`, mirroring how the comics control delegates to `ComicCoverService`/`ComicInfoService`. New view models live under `ViewModels/Tv/`. The window now just hosts `<ContentControl x:Name="TvTabContent"/>`.

**Docs:** added `docs/library-migration/` — the scoping plan for migrating all remaining content types (Movies/TV/Music/Audiobooks/Books) to the comics style, with a per-type capability-parity checklist. This release implements the TV plan (`02-tv.md`).

## v0.5.101
**Comics no longer get misfiled into a similarly-named existing folder ("The Bellybuttons" → "The Boys").** Two root causes, both fixed:

**Fuzzy folder matching false-positive.** `DirectoryCacheService.NormalizeForComparison` didn't strip the leading article, so Jaro-Winkler's prefix bonus made *any* `"The …"` title score over the 0.80 threshold against `"The Boys"` (verified: `"The Bellybuttons"` and even `"The Boondock Saints"` both matched `"The Boys"`). This is why the proposed destination stayed "The Boys" even *after* a correct metadata fetch — `BuildComicPath` re-matched the wrong folder. Now strips a leading `the`/`a`/`an` before scoring. Verified: the false matches return *(none)* while legitimate matches (`"The Walking Dead"` ↔ `"The Walking Dead (2003)"`, exact self-matches) still work.

**The watch folder was treated as a comic-collection folder.** A comic sitting loose directly in the watch root would (a) take the watch folder's name as its series via folder context, and (b) get **pinned** to whatever series a *previous* comic in that same folder had been assigned in the DB (`TryApplyPinnedComicSeriesAsync` matches on `FolderPath`) — so a loose issue dropped where Boys issues once sat inherited the Boys series IDs. The folder-collection heuristics (folder-context series naming **and** folder pinning) are now skipped when a comic's parent **is** the watch folder; such files are parsed as individual issues. The collection behavior still applies to real sub-folders (the feature's intended use: batch-processing a downloaded collection).

**Loose watch-folder comics are no longer grouped/batched as one series.** In the comics grid they now get a unique group key (their filename) instead of all collapsing under the watch folder name, so "Fetch Series Metadata" / "Rename Series" can't accidentally treat unrelated loose issues as a single series.

## v0.5.100
**Uses an already-installed 7-Zip / WinRAR when present, before falling back to bsdtar.** New `CliArchiveExtractor` detects a console extractor the user *already has* — 7-Zip/NanaZip (`7z.exe`/`7za.exe`) or WinRAR (`UnRAR.exe`/`Rar.exe`) — by checking standard install dirs then PATH (cached, logged once). Nothing is ever downloaded/installed: if none is found, behavior is exactly as before. The archive read order for files SharpCompress can't open is now **managed (SharpCompress) → installed extractor (if any) → Windows bsdtar**. 7-Zip/WinRAR are preferred over bsdtar because they have more complete RAR support (RAR5, solid, recovery records).
`ComicInfoService.RepackToCbz` gained a `TryCliRepack` tier; the bsdtar and CLI fallbacks now share one `TryRepackFromExtraction` helper (extract-to-dir → repack, requiring ≥1 image so pages can't be lost). `ComicCoverService` tries the installed extractor before bsdtar too.
Verified against real files: `The Boys #002` (a RAR SharpCompress rejects with "Unknown Rar Header: 0") — 7-Zip extracted all 6 listed pages (exit code 2 is a benign quirk of the broken RAR header; success is judged by files extracted, and 7-Zip's listing confirms 6/6, not a truncation).

## v0.5.99
**CBR→CBZ conversion now succeeds on valid RARs that SharpCompress can't read (bsdtar fallback).** Some genuine RAR4 `.cbr` files make SharpCompress throw `Unknown Rar Header: 0` on the very first entry — verified against a real file (`The Boys #001`): `ArchiveFactory.OpenArchive`, `RarArchive.OpenArchive` (the pre-v0.5.96 path), and the streaming `RarReader` **all** fail identically, so it's a SharpCompress limitation, **not** a regression from the v0.5.96 magic-byte change (the old code failed the same way; that file simply was never convertible). On save this surfaced as `[ComicInfo] Failed to repack…` (silent) plus the loud `[ComicCover] Extraction failed … Unknown Rar Header: 0`, because both the conversion and the cover read use the same reader.
Added `BsdtarExtractor` — a fallback that shells out to **Windows' built-in `tar.exe`** (BSD `bsdtar`/libarchive, present on every Win10 1803+ / Win11). It reads the RAR libarchive-style and extracts the pages. **Self-contained: nothing to install, no bundled binary, not tied to any third-party archiver.**
`ComicInfoService.RepackToCbz` is now two-stage: **managed primary** (`TryManagedRepack`, SharpCompress — fast, unchanged for everything it can already read) → **bsdtar fallback** (`TryBsdtarRepack` — extracts to a temp folder, repacks the images + injects ComicInfo.xml, requires ≥1 image so it can never drop all pages). `ComicCoverService` gets the same fallback for cover extraction.
Verified the full round trip on the real file: bsdtar extracted all 11 pages → repacked `.cbz` → SharpCompress reads the result 11/11. After save converts it, cover extraction and browsing work normally.
If `tar.exe` isn't present (pre-1803 Windows), it logs a clear message and degrades gracefully.

## v0.5.98
**New optional `{Version}` comic naming token (c2c / noads).** The scan-edition marker was previously stripped as a scene tag and discarded, so keeping both a cover-to-cover and an ads-removed copy of the same issue produced identical target filenames (a collision — one would clobber/skip the other). The marker is now captured at parse time onto `ParsedComicMedia.ScanVersion` (reusing the duplicate-finder's `DetectVersion`, which normalises ctc→c2c) while still being stripped from the metadata-search query. It's exposed as a new **Scan Edition** `{Version}` token in the Comics naming template (auto-listed in the token picker). Default templates are unchanged — purely additive — but a template like `#{IssuePadded}[ ({Version})]` now yields `#001.cbz` and `#001 (c2c).cbz` side by side instead of colliding. Complements the duplicate-version finder (which removes redundant editions); this keeps them.

## v0.5.97
**Adopted comic-handling heuristics from malor89's Perl scripts** (a long-standing community toolkit for comic library oddities). Four additions:

**Mini-series "(of N)" parsing** — the parser now extracts the limited-series total from filenames like `Daytripper 03 (of 10)` / `1 of 6` (`ParserUtilitiesComics.ExtractLimitedSeriesTotal`, two conservative patterns so it won't misfire on titles like "Tales of 1001 Nights"). Stored on `ParsedComicMedia.LimitedSeriesTotal` and written to ComicInfo.xml's canonical `<Count>` element; also read back from `<Count>` (ComicInfo wins over filename). Wired into both comic parse paths.

**More scanner tags stripped** — added `ctc`, `noads`, `fixed` to the default scene-tag list (alongside the existing `c2c`) so bare scanner markers don't pollute the metadata-search query.

**Optional in-archive junk stripping** — new setting **Strip non-image junk files from comic archives on save** (off by default). During the save/repack it drops non-image junk entries (`.nfo`/`.txt`/`.url`/`.sfv`/`.diz`/`.ini`/`.log`/`.md5`, `Thumbs.db`, `desktop.ini`, `.DS_Store`). **It never removes an image** — so it cannot delete a comic page. Applied in both `UpsertCbz` and `RepackToCbz`; `ComicInfoService` now takes an optional `AppSettings` to gate it.

**Duplicate-version finder** — new **⧉ Duplicates** tool in the Comics Library Manager. Scans the configured comic library, groups files that are the same issue scanned more than once (chiefly a cover-to-cover vs an ads-removed copy, plus plain dupes — matching by name after removing the c2c/ctc/noads marker, extension, and all non-alphanumerics), and lets you tick copies to delete. Detection only — nothing is deleted without an explicit confirm, and a group can never be fully wiped (at least one copy must remain).

**New "Preferred comic edition" setting** (Settings → Metadata, next to the comic provider): **Ask** / **Cover-to-cover** / **No ads**. Drives the duplicate-version finder's default tick — when a clean c2c↔noads pair is found, the non-preferred edition is pre-ticked for deletion (still requires confirmation).

**Mislabeled-archive detection (checktype.pl)** was already delivered in v0.5.96 (`ArchiveFormatDetector`); the malor review confirmed our content-sniffing approach supersedes it.

Known follow-up: release-group ad pages can't be caught by extension (they're real `.jpg`/`.png`). The planned detector hashes every image across the library — an image byte-identical across many *different* series is almost certainly an ad — and surfaces candidates in a thumbnail-review window for confirmation (never auto-deletes an image). Not in this release.

## v0.5.96
**Mislabeled comic archives no longer break cover extraction or saving.** Log error `[ComicCover] Extraction failed for "#001 - The Boys ….cbr": Unknown Rar Header: 0` was a `.cbr` file that is actually a **ZIP** — a common mislabel. Every archive open in the comic code dispatched by **file extension** rather than actual content, so a ZIP-named-`.cbr` was force-fed to the RAR reader (and the reverse, a RAR-named-`.cbz`, only had a partial fallback). All three open sites now detect by content:
New `ArchiveFormatDetector` (magic-byte sniff → `Zip`/`Rar`/`SevenZip`/`Tar`/`Unknown`; ZIP `50 4B`, RAR `52 61 72 21`, 7z `37 7A BC AF 27 1C`, TAR `ustar`@257).
`ComicCoverService.TryExtractFirstImage` now opens via `ArchiveFactory.OpenArchive` (auto-detects ZIP/RAR/7z/TAR), so cover extraction works for any container regardless of extension — and now also supports `.cb7`/`.cbt` for free.
`ComicInfoService.UpsertComicInfo` (the **save** path) dispatches on real content: a true ZIP already named `.cbz` is updated in place; everything else — real RAR/7z/TAR, **or a ZIP wearing a `.cbr` extension** — is repacked into a proper `.cbz`. The old `ConvertCbrToCbz` (which assumed RAR and threw "Unknown Rar Header" on a mislabeled file) is generalized into `RepackToCbz`, reading the source via `ArchiveFactory` and writing via a temp file + atomic swap. It also skips any pre-existing `ComicInfo.xml` when copying (no more duplicate entry) and won't clobber a different pre-existing `.cbz`.
`ComicInfoService.TryReadComicInfo` now reads `ComicInfo.xml` whenever the file is actually a ZIP, regardless of extension (so a ZIP-mislabeled-`.cbr` still yields embedded metadata).

## v0.5.95
**ComicVine queue/parse path no longer makes per-issue detail calls — series-level bulk only.** While comics sit in the manual/automatic scan queue, each file was enriched by `ComicVineMetadataProvider.GetComicDetailsAsync` → `ComicVineClient.GetIssueAsync`, which for **every issue** made a volume call **plus** the rich `/issue/4000-{id}/` detail call (person/character/story-arc/team/location credits, page count). A queue of N comics therefore cost ~2–3 ComicVine API requests *per file* and pulled heavy per-issue data nothing at the queue stage needs — hammering the API rate limit. `GetComicDetailsAsync` now mirrors the Metron provider: on the first lookup for a volume it bulk-fetches the whole issue list once (`BulkFetchVolumeAsync`, caching issue numbers + titles + covers into `DbComicVineIssues`), then serves every subsequent issue from the local cache with no further API calls. So a queue of N issues costs **one** bulk fetch instead of 2–3 calls each. The rich per-issue detail call is now reserved exclusively for an explicit single-issue **"Fetch Metadata"** in the Library Manager, which uses a separate path (`GetComicVineIssueByIdAsync` → `GetIssueByIdAsync`) and is unchanged.
Added private `TryGetFromCache(volumeId, issueNumber)` to read a cached issue (+ its series-level fields) from `DbComicVineSeries`/`DbComicVineIssues`, normalising issue numbers via `ParserUtilitiesComics.NormalizeIssueNumber`.
When a volume is already `FullyFetched` and the requested issue isn't in the list, returns a series-level fallback (or null for a specific-issue request) instead of hitting the network.
`ComicVineClient.GetIssueAsync` (the volume + rich per-issue detail method) is no longer wired to the parse path. It remains in the client as a capability but is not called during queue scanning.

## v0.5.94
**ComicVine "hang" root cause finally identified: .NET thread-pool starvation — NOT the comic code/transport.** The v0.5.91–v0.5.93 saga ("`/issue/4000-{id}/` hangs 60–145s in-process but is instant from curl/console; root cause undiagnosed") kept swapping the HTTP transport (fresh `HttpClient`, curl subprocess, FlareSolverr, different endpoints) because the symptom looked like an HTTP problem. It isn't. Proof gathered this session:
The exact failing endpoint returns in **~250 ms** from a fresh console process AND from a real WPF UI thread (STA + `DispatcherSynchronizationContext`) running the *real* `ComicVineClient` — same machine, same network, same code.
Yet the live app logs the request as `--> GET … <-- FAILED after 152736 ms: HttpClient.Timeout of 30 seconds elapsing`. **A 30 s timeout taking 152 s to fire is the fingerprint of thread-pool starvation**: the timeout's own timer callback is a thread-pool work item, and async HTTP *completions* are too — when the pool is starved they can't run. This is why no transport ever helped (they all need the pool for completions) and why fresh processes (healthy pool) are instant.
The starvation comes from heavy **sync-over-async** (`.Result`/`.Wait()`/`.GetAwaiter().GetResult()`) in the long-running app's background subsystems — `DownloaderQualityScorer` (17 sites), `WebServerService` (9), `DownloadSearchWindow` (9), `DownloadSearchService` (7), `WantListService` (5). The comic lookup is just the victim that happens to need the pool while it's blocked.

**ComicVine transport rewritten anyway (it was also wrong) + dedicated-thread mitigation.** Removed the curl/`Process`/`MakeFreshClient()` machinery entirely (per-call `new HttpClient()` exhausts sockets; the curl subprocess deadlocks on inherited stdout pipe handles when spawned concurrently — exactly why curl worked at the command line but not in-app). `ComicVineClient` now uses a single shared `HttpClient` (`SocketsHttpHandler`, HTTP/1.1, gzip, real UA). The actual request runs **synchronously on a dedicated OS thread** (`DedicatedThreadHttp`) so the network I/O completion no longer depends on the starved pool — this removes the *catastrophic, indefinite* hang (verified: I/O completes immediately even with the pool saturated). **Caveat:** this is a mitigation, not a complete cure — the post-I/O continuation (JSON parse) and the rate limiter's `Task.Delay` still touch the pool, so under *sustained* starvation a lookup is still delayed by pool-recovery time (not by the network). The definitive fix is eliminating the sync-over-async above.

**Provider priority now respected in interactive lookups.** `ComicSeriesEditorWindow` (Series Editor "Lookup") queried providers in registry order with no preferred key, so it hit Metron first; `ComicPickerDialog` passed the preferred key but still waited on the secondary provider. Both now pass `PreferredComicsProvider` + `stopAfterPreferred: true`, so the preferred provider (ComicVine by default) is queried first and a slow/blocked secondary (e.g. an IP-banned Metron) never delays results.

**Metron client: fail-fast + rate-limit awareness.** Metron is currently **unreachable from this machine — the IP is firewall-blackholed by Metron** (all packets to metron.cloud:443/:80 time out, ICMP 100% loss, while other DigitalOcean hosts respond in ~60 ms; DNS resolves fine). This is a network/IP ban (Metron silently blackholes abusers — they removed ComicTagger for 2–14× request volume), **not** a code bug. Hardened so a blocked/slow Metron can't freeze comic ops: `SocketsHttpHandler` with an 8 s `ConnectTimeout`; `GetApiJsonAsync` now **fails fast on connect/timeout/connection errors** (retries only on HTTP 429) and runs its send on the same dedicated-thread helper. Added `LogRateLimitHeaders` to surface Metron's documented `X-RateLimit-Burst/Sustained-*` headers (limits: 20/min, 5000/day) and warn as the burst window runs low, to avoid re-triggering a ban once reachable.

**Issue-thumbnail loading throttled — fixes "Fetch Metadata doesn't use ComicVine".** It always *did* use ComicVine (log: `ComicVine fast-path: issue id=…`), but opening a large series fired one task per issue (300+ at once), each extracting a CBZ/CBR page and decoding an image on the thread pool — flooding it. A metadata fetch clicked right after found the pool jammed and timed out at 25 s, so it silently skipped (looking like it ignored ComicVine). Issue-thumbnail loading is now bounded to 4 concurrent via a `SemaphoreSlim`, so browsing a big series no longer starves the pool and the fetch completes. (This is also a concrete instance of the broader root cause below.)

**Per-issue synopsis fixed — no more identical summary on every issue.** ComicVine's per-issue plot is the `description` field (+ a short `deck`); for many older series (e.g. Hellblazer) it's empty. The old code fell back to the **volume/series description**, stamping the same blurb onto every issue (confirmed: all 301 Hellblazer rows held one identical 8,902-char text). Now: `description` → `deck` (both per-issue, `deck` newly added to the field list) → otherwise left empty, and the UI shows **"No Synopsis"** instead of the repeated series blurb. Removed the `?? vol.Description` fallback in `GetIssueAsync`. Metron already supplies real per-issue synopses (`desc`), so once Metron is reachable again it will fill these in. One-time cleanup nulled **411 duplicated descriptions** across 5 series (Hellblazer 297, Walking Dead 92, Uncanny X-Men 15, Watchmen 4, Transmetropolitan 3); the DB was backed up first.

Known follow-up: the root-cause cure is to remove the sync-over-async hotspots in the download/web subsystems so the thread pool is never starved. Until then, the dedicated-thread mitigation + thumbnail throttle make ComicVine reliable in normal use but not bulletproof under heavy background load (e.g. an active auto-download/upgrade scan).

## v0.5.93
**ComicVine `issue/4000-{id}/` endpoint: fresh HttpClient per call** — the `/issue/4000-{id}/` individual resource endpoint consistently hangs for 60-145 seconds inside the WPF process while working instantly from fresh processes (curl, PowerShell, console apps). The root cause is undiagnosed (not Windows Defender — other endpoints work; not server-side — external tests pass), but empirically the shared `_http` instance's connection state causes this specific endpoint to hang. Both `GetIssueByIdAsync` and the detail call inside `GetIssueAsync` now create a fresh `HttpClient` per call via `MakeFreshClient()`. Fresh instances never exhibit the hang. The shared `_http` is still used for search, volume, and bulk issue list calls which are not affected.

**ComicVine rate limiter burst size raised to 100** — burst bucket was 10, which was too small for interactive use (a single issue detail path could burn 3 tokens in sequence, exhausting the burst after 3-4 issues and then waiting 20s per call). Raised `TokenLimit` from 10 → 100. Long-term rate is unchanged (1 token per 20s = 180/hr, safely under the 200/hr API limit).

## v0.5.92
**ComicVine issue detail: switched from `/issue/4000-{id}/` to `/issues/?filter=id:{id}`** — the individual resource endpoint (`/issue/4000-{id}/`) consistently times out on rich `field_list` requests (person_credits, character_credits, story_arc_credits, team_credits, location_credits). The `/issues/` list endpoint with an ID filter is the same endpoint used by bulk sync and responds much faster. Applied to both `GetIssueByIdAsync` and the final detail call inside `GetIssueAsync`.

## v0.5.91
**Hard crash (stack overflow) on "Fetch Metadata" fixed** — `CvGetAsync` in `ComicVineClient` was accidentally calling itself recursively instead of `_http.GetStringAsync`, causing an immediate stack overflow (`0xc00000fd` / `STATUS_STACK_OVERFLOW` in `ntdll.dll`) the moment the button was clicked. One-line fix.

**ComicVine HTTP cancellation fixed — no more 2-minute hangs** — `HttpClient.GetStringAsync` was taking 141 seconds to surface `TaskCanceledException` after the 15s `HttpClient.Timeout` fired (TCP socket drain delay). Switched `CvGetAsync` to `HttpClient.SendAsync` with `HttpCompletionOption.ResponseHeadersRead` + `ReadAsStringAsync`: cancellation now propagates immediately when the token fires because the body read is a separate, promptly-cancellable step. `FetchComicVineAsync` now creates a 25-second `CancellationTokenSource` and passes the token all the way down to the HTTP layer, replacing the old `Task.WhenAny` + abandoned background task approach. Timeout now surfaces in ~25 s instead of 141 s. `HttpClient.Timeout` bumped to 60 s (was 15 s, which was too aggressive for CV's variable response times) — the 25s CTS is the real controller; the 60s is a hard safety net.

**"Re-fetch Metadata" renamed to "Fetch Metadata"** — updated the context menu item, the button in the issue detail panel, and all MessageBox title strings to match.

## v0.5.90
**Re-fetch Metadata now respects the preferred provider setting** — previously always tried Metron first regardless of the user's preference, causing hangs when Metron was unreliable. Now checks `PreferredComicsProvider`: if ComicVine is preferred (the default), CV is fetched first as primary and Metron only runs as a secondary gap-fill. Metron-preferred users get the old Metron-first order.

**Metron hard timeout in re-fetch** — each Metron call in `RefetchIssueAsync` now runs under a 12-second `CancellationTokenSource`. If Metron times out (down, banned, or slow), it's skipped and ComicVine proceeds normally. The log shows `[LibraryUI] Metron timed out after 12 s — skipping`.

**ComicVine `GetIssueAsync`: cached issue ID avoids extra API call** — when the series has already been bulk-synced, the CV issue ID is already in `DbComicVineIssues`. The issue list API call is now skipped for cached series; the client looks up the `CvIssueId` from the local DB and goes straight to the `/issue/4000-{id}/` detail endpoint. For a synced series this reduces from 3 CV API calls to 2 (volume + detail) — or 1 if the volume data was already on hand. Falls back to the API list call only when the issue isn't in the local cache.

**CBR→CBZ conversion: `NoCompression` instead of `Optimal`** — comic pages are already-compressed JPEG/PNG images; re-compressing them with `CompressionLevel.Optimal` wasted significant CPU time with no benefit. Using `NoCompression` makes CBR→CBZ repacking much faster (pure byte copy per entry). This was the primary cause of save appearing to hang for CBR files.

**Save logging** — `SaveIssueAsync` now logs each step (`Save: writing DB`, `Save: updating ComicInfo.xml`, `Save: ComicInfo.xml done`) so the app log clearly shows which step is slow if a future hang occurs.

## v0.5.89
**ComicVine: characters, story arcs, teams, locations, and page count now retrieved** — these fields are not available on the `/issues/` list endpoint (a ComicVine API limitation). `GetIssueAsync` now makes a third call to `/issue/4000-{id}/` (the individual issue detail endpoint) after using the list to find the issue ID. Characters, story arcs, teams, locations, person credits, and page count all come from this detail call.

**ComicVine: variant cover descriptions no longer pollute plot summaries** — ComicVine's `description` field is full HTML that frequently embeds variant cover sections (`<h4>Variant Covers</h4>…`) after the main plot. The provider now uses the `deck` field (a clean one-line summary ComicVine maintains separately) as the primary description. When `deck` is empty, the fallback `description` is now HTML-preprocessed to strip any `<h*>Variant*</h*>` heading and the content that follows it before converting to plain text. Added `deck` field to both the `CvIssueResult` and `CvVolumeResult` DTOs and to all relevant `field_list` request parameters.

**ComicParser: year hint from parent folder name** — if the filename contains no year, the parser now checks the parent folder name for a `(YYYY)` pattern before searching the API. Fixes the case where `#01 - Watchmen.cbr` inside `Watchmen (1986)/` was matching "Before Watchmen: Comedian" instead of the 1986 volume.

**FileWatcherService: CBR→CBZ race condition fixed** — when a `.cbr` file is renamed, it is first converted to `.cbz` in the watch folder. The resulting `.cbz` was being detected by the FSW as a new file, creating ghost queue entries. The auto-rename path and manual rename path both now pre-register the expected `.cbz` path in `_trackedFiles` before triggering conversion, so the FSW ignores it. `UntrackFile` is called if conversion fails.

**Startup performance regression fixed** — the `_inStartupScan` flag and `skipApiEnrichment` parameter chain (`FileWatcherService` → `ParseNonVideoAsync` → `ParseComicInternalAsync`) that short-circuit ComicVine/Metron API calls during the initial folder scan were accidentally removed in a refactor. Restored; startup no longer makes one API call per comic file.

**DirectoryCacheService: `RegisterDirectory` for eager batch registration** — new method allows `PathBuilderService` to register a newly-created series folder name in the in-memory cache immediately, so subsequent issues in the same batch find the folder without waiting for a disk rescan.

## v0.5.88
**ComicVine search: publisher-based result sorting** — search results are now sorted into three tiers to deprioritise foreign-language editions. Major English publishers (Marvel, DC, Image, Dark Horse, IDW, Boom! Studios, Dynamite, Valiant, Fantagraphics, Oni, Archie, Top Cow, WildStorm, Vertigo, Avatar, AfterShock, Vault, AWA, Titan) float to the top; known non-English publishers (Panini, Glénat, Dargaud, Delcourt, Urban Comics, Soleil, Casterman, Dupuis, Bonelli, Planeta, manga publishers, etc.) sink to the bottom. Unknown publishers stay in ComicVine's native relevance order within their tier.

**ComicVine / Metron picker: "★ Best match" highlight** — the first result in the picker dialog is now marked with a small "★ Best match" label in accent pink and a left-side accent border, making the top candidate immediately obvious without changing which result is selected.

**Comics manual queue: series grouping and batch actions** — comic files in the manual queue are now grouped by series name (same expander pattern as music albums and audiobooks), showing publisher, start year, and issue count in the group header. Two new context-menu actions: **Rename Series** renames all issues in the group in one go (with CBR→CBZ conversion and ComicInfo.xml embedding per issue, matching the existing single-file path); **Fetch Series Metadata…** opens the picker once with no issue number to do a series-level search, then applies the resolved series name, publisher, year, and provider IDs (ComicVine/Metron) to every issue in the group while preserving each issue's own number, title, and path.

## v0.5.87
**Watcher service state sync fixes** — `FileWatcherService` now fires a `StateChanged` event on every start (success or folder-not-found) and stop. `MainWindow` subscribes and always syncs `_watcherRunning` and the status bar, so changes triggered by the web API are immediately reflected on the desktop. Previously, starting or stopping from the web UI left `_watcherRunning` stale, causing the desktop to show "Folder Not Found" after a web-triggered stop. `/api/watcher/auto` now restarts the watcher when it is running (matching the desktop's full `ResetAndRescanAsync()` path) so existing queued files are rescanned under the new mode. The Settings page in the web UI now polls watcher state every 15 seconds so it stays in sync when the desktop changes state.

**Comics issue editor: Volume field** — Volume is now shown and editable in the inline issue editor (between Issue Number and Issue Title), matching what is already stored in the DB and retrieved from Metron/ComicVine. Clearing the field nulls the value; entering a number updates `entry.Volume` and saves to the DB.

**Comics library: rename logging** — the post-metadata-save rename prompt now logs full source and destination paths (`[LibraryUI] Comic renamed/moved: From: ... To: ...`) instead of just the new filename. A "Rename skipped by user" entry is also logged when the user clicks No at the prompt.

**Comics publisher card: layout redesign** — the publisher card is now a two-column layout: a 200×280 px image on the left (same dimensions as the issue detail cover) with the publisher name below it, and a series-count pill in a dedicated 110 px right panel. Text never overlaps the image. Cards without art fall back to the original accent-bar style at the same total width. The "Set / Replace Publisher Image" dialog now shows "ideal size: 200 × 280 px" to match the display.

## v0.5.86
**TV grid: Fetch Metadata no longer resets episode to S00E00** — when the item's parsed object was a `ParsedTvMedia` (not `ParsedMedia`), the fetch-metadata handler created a brand-new blank `ParsedMedia` as a replacement but never copied `Season` or `Episode` across, so every rename after a manual fix-match produced S00E00. The replacement now seeds `Season`, `Episode`, `OriginalName`, and `ContentType` from the original object before the handler overwrites title/year/IDs.

## v0.5.85
**Comics UI: publisher context menu — Clear Image option and size guidance** — right-clicking a publisher card now shows "Set / Replace Publisher Image…" (renamed for clarity) and a new "✕ Clear Publisher Image" item (disabled when no `folder.*` image exists). A separator visually groups image actions above "Open in Explorer". The file browse dialog title now includes the recommended source size: **400 × 220 px, 2:1 landscape ratio** — matching the publisher card display dimensions at ×2 for HiDPI screens.

## v0.5.84
**Comics UI: hover now shows edge glow instead of a full-card overlay** — publisher and series cards were using a full-card `CardHoverBackground` overlay that faded in on hover, producing a muddy filled box effect. Replaced with a bright accent-coloured border (`WindowAccentLineBrush`) that snaps to 2px on hover and reverts on leave. The filmstrip issue list rows do the same: the old `ToolbarButtonHoverBackground` fill is gone; hover now shows a 2px accent border around the row. All effects are theme-aware (pink on Abigail theme, grey on Boring, blue on BoringDark).

**Comics UI: series card text no longer overlaps cover art** — the series card was using `VerticalAlignment.Bottom` to float the title/year/genre block over the bottom of the 200px cover image. Replaced the flat Grid with a two-row layout: row 0 is the 200px cover, row 1 is the text area on the card background, so text and art are always cleanly separated.

**Comics UI: removed the non-functional "⊞ Grid" button from the issue detail panel** — the view-mode toggle only applies to Publishers and Series panels (switching between card grid and list view). When navigated into an issue filmstrip there is no grid/list concept, so the button is now hidden while the Issues panel is active and restored when navigating back.

## v0.5.83
**Comics: dash-suffix in filename no longer pollutes the Format field** — `ParseComicFilename` previously treated any text after a dash (e.g. `Batman 001 - The Killing Joke (1988).cbz`) as the "Edition", so "The Killing Joke" ended up in `ComicFormat` and was written to `<Format>` in ComicInfo.xml. It now checks whether the dash-suffix matches a real edition keyword (Annual, Omnibus, TPB, HC, Deluxe Edition, etc.) — if yes, it's still the edition; if no, it's treated as the issue title and goes into `IssueTitle` instead. `ComicParser` and `MediaParser` both populate `IssueTitle` from this value. Existing files with garbage in their `<Format>` tags can be corrected by opening the inline editor, clearing the Format field, and saving.

## v0.5.82
**Comics: Re-fetch Metadata now fetches from both providers** — replaced the broken implementation (which re-ran the full parse pipeline and only used the first provider that responded) with a direct two-provider fetch. Reads `MetronSeriesId`/`ComicVineSeriesId` already on the entry, calls `GetComicDetailsAsync` for Metron first (primary — overrides all fields with API values), then ComicVine second (fills in any gaps left by Metron). If neither provider has data, shows a diagnostic message with the series IDs and issue number to help debug. After a successful fetch, shows a summary dialog (providers used, issue title, writer, cover date) so you can see what was pulled. Logs each step to the app log.

## v0.5.81
**Comics: Re-fetch Metadata now actually fetches** — `MediaParser.ParseComicInternalAsync` was missing the pinned-series check that `ComicParser` already had. It went straight to a name search, ignoring the folder's assigned `MetronSeriesId`/`ComicVineSeriesId`. Added `TryApplyPinnedComicSeriesAsync`: looks up the folder's pinned series IDs, applies cached series identity (name, publisher, year), then calls `GetComicDetailsAsync` with the exact series ID + issue number. If the issue is found, confidence=92 and the name search is skipped entirely; if the series is pinned but the issue isn't in the provider list, the series identity is still applied and the name search is still skipped (to avoid matching the wrong volume). Mirrors the same logic in `ComicParser`.

**Comics: deleted dead `ComicIssueEditorWindow`** — the old pop-out issue editor window was never opened after the new inline editing UI (`BeginIssueEdit` in `ComicsLibraryManagerWindow`) replaced it. Removed both `.xaml` and `.xaml.cs`.

**Comics: removed `ApplyComicDetailsToEntry` from `LibraryManagerWindow`** — the only callers were in the deleted `ComicIssueEditorWindow`.

**Comics: `PersistComicIssueToDbAsync` now uses a fresh `MediaDbContext`** — was using `App.Database` (the shared EF Core context) which could return stale cached rows due to identity-map caching after another context had modified them. Now opens and disposes its own context.

**`ComicsLibraryManagerWindow` dead code removed** — `_pathBuilder` field (constructed in ctor but `RenameComicFileAsync` always creates its own local instance), `NavigationDirection` property (set in 4 places but never read; `ShowPanel` always receives the enum value directly), and `FormatBytes` static method (the ViewModel has its own copy; the window's copy was never called).

## v0.5.80
**Comics library: series card always uses the folder name** — `LoadSeriesAsync` now derives the display name from the folder (`ParseComicFolderName`) unconditionally, instead of reading it from the DB row. Fixes stale DB rows with truncated scan names (e.g. "X-Men '92" in folder "X-Men '92 - House of XCII"). Also added `AsNoTracking()` to the series-level DB query so EF Core's identity map doesn't serve stale cached entities after the series editor saves through a separate context.

**`ComicSeriesSyncService`: stop overwriting series name from the API** — `SyncAsync` was writing the provider's canonical series name onto every DB row (e.g. Metron returns "X-Men '92" for the House of XCII volume), immediately undoing any name the user typed in the series editor and making edits appear not to stick. The series name is now owned by the user's folder organisation; `SyncAsync` enriches publisher/year/genre/issue data only. The series editor also no longer reverts the name text box to the API name after a save.

**Comics library: series name now comes from the folder, not the DB row** — browsing to a series now overrides each issue's `SeriesName` with the series folder's canonical name, so issues that were pre-scanned to a wrong series (e.g. "House of X" instead of "House of XCII") no longer display under the wrong name in the detail pane.

**Comics library: series IDs propagated to new rows** — when `LoadIssuesAsync` discovers new files in a folder that already has pinned `MetronSeriesId`/`ComicVineSeriesId`, the newly created `DbComic` rows now inherit those IDs. Previously new files were invisible to the provider issue list merge until the series editor was re-saved.

**Comics library: null IssueNumber backfilled on browse** — existing DB rows with a null `IssueNumber` now get a filename-parsed issue number on the next browse, persisted back to the DB. This fixes issues appearing as owned but unnumbered (and therefore failing to match against the provider list).

**Comics library: better representative row selection** — `LoadSeriesAsync` now prefers the row with series IDs (Metron/CV) when picking the representative row for a series card, rather than taking the first arbitrary row. The series card's `SourceEntry` now reliably carries the pinned IDs.

**`ParseComicFilename` — fixed dash-split for `Series '92 - Subtitle IssueNum` filenames** — the step-5 dash split previously triggered whenever any digit appeared before the dash (e.g. `X-Men '92` has `92`), which caused `X-Men '92 - House of XCII 002 (2022)` to split at the dash, treating "House of XCII 002" as a subtitle/edition and leaving the issue number null. The condition now requires the pre-dash part to end with a *whitespace-preceded* number (an actual issue number), so `X-Men '92` does not trigger it. After the fix: series = `"X-Men '92 - House of XCII"`, issue = `"2"`. Split into its own `ParserUtilitiesComics` class. `ParserUtilities` stays for shared helpers (JaroWinkler, scoring, etc.).

## v0.5.79
**Download search: streaming results** — fast providers (EZTV, Newznab, etc.) now populate the results grid/table within ~1 second; slow FlareSolverr-backed providers trickle in as they finish rather than blocking the whole search. The footer shows "X results (searching…)" while providers are still running, then a final count when done. WPF window and Web UI both stream. **Per-provider timeout** (default 30 s, configurable as `ProviderTimeoutSeconds` in Downloader Settings) prevents a hung provider from blocking the others — it logs a timeout and returns empty rather than stalling. Implementation: `SearchAsync` gains an optional `IProgress<List<DownloadResult>>` callback; providers report results immediately on completion via a per-provider linked `CancellationTokenSource`; the web SSE endpoint (`GET /api/search/downloads/stream`) uses a `Channel<T>` to serialize concurrent callbacks onto the response stream; the WPF window uses `Progress<T>` (UI sync context) to add each batch to backing lists and re-sort. Extracted shared `ConvertBatch` helper on `DownloadSearchService` used by both. Web UI uses `fetch` + `ReadableStream` (not `EventSource`) so the Bearer token header works.

## v0.5.78
**Comics: consolidated the issue actions into one "Save"** — replaced the separate "Update Database" and "Update File Metadata" buttons with a single **💾 Save**. Saving now persists to the database, writes the file's ComicInfo.xml (repacking CBR→CBZ if needed), then prompts *"Rename the file to match the updated metadata?"* — the same save-then-offer-rename flow used by the movie/TV/book editors. Renaming builds the destination via `PathBuilderService.BuildComicPath`, moves the file, and updates the DB path. The inline Edit-Metadata form's Save now routes through this same unified flow, so editing an issue and saving does everything in one step. (`SaveIssueAsync` / `RenameComicFileAsync`.)

## v0.5.77
**Comics: issues now resolve against a series' pinned Metron/ComicVine IDs** — restored the behaviour where, if a file's series folder already has provider IDs assigned, the issue is looked up against that *exact* series/volume (cache-first) instead of doing a fresh name search that could match the wrong series/volume. New `ComicParser.TryApplyPinnedSeriesAsync`: reads the folder's series IDs + cached series details from the DB, resolves the issue via `GetComicDetailsAsync(seriesId, issueNumber)`, and — even if that specific issue isn't in the provider list — keeps the known series identity and skips the name search. Applies to re-fetch and re-scans of organised folders. Factored the shared detail-application into `ApplyIssueDetails`.

**Comics: right-click context menu on issue rows** — matching the rest of the app. Owned issues: Edit Metadata, Re-fetch, Open in Explorer. Missing (provider-only) issues: **Add to Want List**, **Search for Issue** (opens the download search prefilled with `Series #NN`), Open in Explorer. Right-click selects the row under the cursor; items show contextually.

**TV: fall back to the folder name when a season-pack file has no series title** — files named just `S01E05.mkv` parsed to an empty title and found nothing. `TvParser` now derives the series name (and year) from the parent folder, stepping up out of a `Season NN`/`Specials` subfolder to the series folder and stripping a trailing `(YYYY)`. Title-similarity scoring was also fixed to compare against the title portion / folder name, so these episodes can auto-rename with proper confidence instead of always landing in manual review.

## v0.5.76
**Comics: fixed the faded/washed-out text** — breadcrumb labels, issue numbers (filmstrip + detail), the "Clear Filters" and "Show more" links all used `WindowAccentLineBrush` as their font colour. That brush is meant for thin decorative accent *lines* — it's pale grey (`#ADADAD`) in the light theme and a left-to-right **gradient** in the Abigail theme, so as text it looked faded/weird. Added a solid `AccentTextBrush` to all three themes (Abigail `#D04080`, Boring `#0067C0`, Boring Dark `#4DA6FF`) and pointed that text at it. The decorative accent bars on cards still use the line brush.

**Comics: "Update Database" and "Update File Metadata" buttons on the issue detail pane** — next to "Open in Explorer". *Update Database* saves the issue's current metadata to the DB (`PersistComicIssueToDbAsync`). *Update File Metadata* writes the metadata into the archive's `ComicInfo.xml` (`ComicInfoService.UpsertComicInfo`); if it converts a CBR→CBZ, the DB row's path is updated and the filmstrip refreshes. Both guard against provider-only "missing" issues that have no file/DB row.

## v0.5.75
**Comics: the issue list now shows the full run, with missing issues marked** — browsing a series previously listed only the local files; it never merged in the cached provider issue list, so the issues you *don't* own were invisible. `ComicLibraryViewModel.LoadIssuesAsync` now merges the local files with the Metron/ComicVine cached issue list (keyed by normalised issue number — "094" matches "94"). Owned issues use the local file; issues in the provider list with no local file are shown as **Missing** (dimmed, red "❌ Missing" status). The filmstrip "owned / total" count reflects the canonical run. Missing issues show their provider cover (fetched on demand in the detail pane) and metadata, but can't be edited until acquired.

**Comics: browse now recognises .cb7 and .cbt** — the library view only matched `.cbz`/`.cbr` (scanning already accepted all four), so 7-Zip/Tar comic archives in a folder were silently absent from the issue list.

**Comics: deleted the obsolete `ComicSeriesDetailWindow`** — the old tabbed series/issues window is no longer referenced anywhere (series editing moved to `ComicSeriesEditorWindow` via right-click, issue editing is inline). Removed `ComicSeriesDetailWindow.xaml` + `.xaml.cs`.

## v0.5.74
**Comics: assigning Metron/ComicVine series IDs now pulls full series data + the issue list** — new `ComicSeriesSyncService`. When you save a series in the editor with a Metron or ComicVine ID set, the app fetches the provider's series details (title, publisher, start year, genres) and the complete issue list into the local cache (`DbMetronSeries`/`DbMetronIssues`, `DbComicVineSeries`/`DbComicVineIssues`), populates the series-level fields onto every local issue row, and matches the cached issue list against the files you own. The save status reports e.g. *"Pulled from Metron + ComicVine — 142 issue(s) in series, 37 owned locally, 105 not present"* — the cached list is the canonical set used to identify missing issues. A network pull only happens when an ID was newly added/changed or no complete cache exists yet; otherwise it just re-applies cached data. Series editing no longer requires any issues to exist locally first, so you can pre-seed the issue list for an empty/partial series.

## v0.5.73
**Comics: Scan Folder actually scans now** — `ScanButton_Click` created the scan queue and results window but never called `ComicScanQueue.StartScan`, so the results window just sat empty. The scan is now kicked off with the chosen folder + subfolder option (with a folder-exists guard).

**Comics: breadcrumb navigation fixed** — `ShowPanel` determined the outgoing panel from `_vm.CurrentLevel` *after* it had already been mutated (wrong panel), and multi-level back-jumps fired two slide animations where the second was swallowed by the slide lock (landed on the wrong level). `ShowPanel` now derives the outgoing panel from actual visibility and falls back to an instant switch when a slide is mid-flight; "★ Library" jumps straight to the root via `NavigateToRoot` in a single slide.

**Comics: right-click a series → Edit Series Metadata** — series cards now have a context menu opening the existing `ComicSeriesEditorWindow` (Title / Publisher / Year / Volume / Genre / Description + ComicVine & Metron **series** IDs, with provider lookup). The series grid reloads afterward to reflect changes. Plus "Open in Explorer".

**Comics: right-click a publisher → Set Publisher Image / Open in Explorer** — publisher cards now render a folder image (`folder.*` / `poster.jpg` / `logo.png` / `cover.jpg`) as a low-opacity background, and the context menu lets you pick an image file which is copied into the publisher folder as `folder.*`.

**Comics: issue "Edit Metadata" is now inline** — instead of opening the old tabbed `ComicSeriesDetailWindow`, the detail pane swaps to an in-place editable form (issue number, title, cover date, format, variant, all credits, characters/story arc/teams/locations, description, ComicVine & Metron **issue** IDs) with Save/Cancel. Save persists via `PersistComicIssueToDbAsync` and reloads the filmstrip, reselecting the edited issue. "Re-fetch Metadata" now re-runs the parser for the issue inline (no window) and refreshes.

**Comics: .cbr→.cbz extension fix** — when "update ComicInfo.xml" converts a CBR to CBZ during a scan, the destination path is now realigned to the actual working file's extension, so a converted file is no longer written under a `.cbr` name containing zip data.

## v0.5.72
**Comics: folder-scan now persists metadata to the database** — `ScanResultsWindow.ApplyRow` previously moved/renamed files and (optionally) wrote ComicInfo.xml but never created a `DbComic` row, so every browse re-derived bare data from filenames and API lookups were thrown away. New `ComicLibraryWriter` service upserts a full `DbComic` (series, publisher, issue, credits, dates, provider IDs, description, cover) after each apply. The watcher/auto-rename path keeps its own persistence but now also caches covers.

**Comics: series detection uses the filename year to pick the right volume** — `ComicParser` parsed the year from the filename but ignored it when selecting a series, always taking the provider's first hit. Multi-volume titles (e.g. *Uncanny X-Men* 1963 / 1981 / 2011 / 2018 / 2024) now match the volume whose start year equals the filename year (exact, then within ±1 year, then first hit). New `ComicParser.SelectBestHit` helper.

**Comics: cover art from the original archive now works automatically** — new `ComicCoverService` extracts the first image page from a `.cbz`/`.cbr` into `%APPDATA%\AbigailMediaRenamer\CoverCache` (resolution order: existing cache → archive first page → downloaded API cover URL). Wired into the scan-apply flow, the auto-rename flow, and the library UI loaders. Previously `CoverLocal` was only ever set by the manual "Pick from archive" button, so grids and detail panels were blank; the loaders also never fell back to the API `CoverUrl`. `LoadIssueThumbnailAsync` / `LoadDetailCoverAsync` / `LoadSeriesCoverAsync` now resolve and persist a cover when one is missing.

**Comics: ComicInfo.xml is read back when browsing** — `ComicLibraryViewModel.LoadIssuesAsync` now reads embedded `ComicInfo.xml` (series, publisher, issue, credits, dates, genres, page count, etc.) when first creating a DB row for a file, instead of a filename-only stub — so metadata written into archives during scanning is surfaced and saved.

## v0.5.71
**Comics tab: embedded library control** — `ComicsLibraryManagerWindow` converted from `Window` to `UserControl` and embedded directly in LibraryManagerWindow's Comics tab via `ContentControl`. No separate window opens. Series Alias Manager removed from the tab — now accessible via the "⚙ Aliases" toolbar button inside the comics control, which opens a new `ComicAliasManagerWindow` dialog. All `Owner = this` calls in the control updated to `Window.GetWindow(this)`. Old `OpenComicsVisualLibrary_Click`, `LoadAliases`, `AliasAdd_Click`, `AliasDelete_Click` removed from `LibraryManagerWindow.xaml.cs`; alias expander and launch button removed from `LibraryManagerWindow.xaml`.

## v0.5.70
**Comics Library: replaced dual-system with single new UI** — removed the old 3-level DataGrid browser (Publishers / Series / Issues grids) from `LibraryManagerWindow`. Comics tab now shows only a launch button for `ComicsLibraryManagerWindow` plus the Series Alias Manager. All comics browsing, scanning, metadata editing, and file reorganisation happens in the new visual library window.

**Metron search fix: year-in-query stripping** — `SearchSeriesAsync` now strips trailing `(YYYY)` from the query before URL-encoding. Metron stores bare series names; sending "Uncanny X-Men (1963)" searched for a field value that never exists — it now correctly sends "Uncanny X-Men".

**Metron search fix: pagination** — `SearchSeriesAsync` now paginates up to 5 pages so results beyond the first 25 are no longer silently dropped.

**Metron cache fix: over-eager cache false positives** — `IsCloseEnough` in `MetronMetadataProvider` added a 75% minimum length ratio to the `query.Contains(cached)` branch. Previously "X-Men" (5 chars) would match query "Uncanny X-Men" (13 chars) and return the wrong cached entry, preventing the live API from being called.

## v0.5.69
**Comics Library Visual Manager** — new `ComicsLibraryManagerWindow` replaces the plain DataGrid UI with a cover-art-first, progressive-disclosure design. Three drill-down levels (Publishers → Series → Issues) with 200ms slide transitions (CubicEase). Publisher view: 200×110 cards with accent bar, series count pill, hover overlay animation. Series view: 160×250 cover-art cards with async image loading (placeholder → crossfade), status badge overlays, genre pills, year/issue caption; collapsible 220px sidebar with sort/status/year/genre filters; list mode (DataGrid) toggle. Issue view: resizable split pane — left filmstrip (virtualised ListView, 56px rows with thumbnail, issue number, date, status colour-coding) + right detail panel (cover image, identity, description expand/collapse, credits grid, file info, confidence bar, action buttons). Scan status bar animates in/out (height animation). All colours use DynamicResource; 20 new brush keys added to all three themes (AbigailTheme, BoringTheme, BoringDarkTheme): CardBackground, CardBorderBrush, CardHoverBackground, CardSelectedBackground, StatusBadgeOk/Warn/ErrBg/Fg, Shimmer, CoverPlaceholder, Filmstrip, Sidebar, ProgressOwnedFill. "★ Visual Library" button added to existing Library Manager Comics tab toolbar.

ViewModels: `ComicLibraryViewModel`, `ComicPublisherCardViewModel`, `ComicSeriesCardViewModel`, `ComicIssueRowViewModel`, `ComicIssueDetailViewModel`, `ComicScanStatusViewModel` — all INotifyPropertyChanged, XML-documented.

## v0.5.68
**Comics overhaul — Part 1: Series Alias System** — new `DbComicSeriesAlias` DB entity + `ComicSeriesAliases` SQLite table (auto-created on startup). `ComicSeriesAliasSeeder` seeds 25+ built-in aliases for known title variants (e.g. "The X-Men" → "Uncanny X-Men", "New X-Men" → "X-Men", "West Coast Avengers"/"Avengers West Coast"). `ComicAliasService` provides `ResolveAlias(name, yearHint)` with year-disambiguated lookup plus CRUD methods. Alias resolution wired into `MediaParser.ParseComicInternalAsync` — names are normalised to canonical form before the API search, so "The X-Men #1" finds "Uncanny X-Men Vol. 1 (1963)" rather than failing to match.

**Comics overhaul — Part 1.3: Alias Manager UI** — collapsible "Series Alias Manager" panel in Library Manager → Comics tab. DataGrid shows all aliases (Alias Name / Canonical Name / Start Year / Publisher / Notes / Type). Built-in seeds shown as read-only ("Built-in" type label, Delete disabled). Users can add custom aliases via inline form and delete custom aliases with confirmation.

**Comics overhaul — Part 2.1/2.2: Folder-context-aware parsing** — new `FolderContextParser` extracts series name, volume number, and start year from a file's parent folder hierarchy. Handles patterns: "Vol. 1 (1963)", "v1 001-046 (2001-2004)", "X-Treme X-Men v1 01-46 (2001)", sub-series " - Savage Land" suffixes. Priority order now: ComicInfo.xml > folder context > filename > API.

**Comics overhaul — Part 2.3: Comic scene tags** — comic release-group tags added to `AppSettings.SceneTags` defaults: Digital, c2c, Hybrid, FB-DCP, DCP, Shadowcat-Empire, Zone-Empire, Nahga-Empire, Minutemen-Excelsior, Glorith-HD, Gird, GetComics, ComiXology. Scene tags pre-stripped from filename before `ParseComicFilename` runs (parenthesized form first, then word-boundary pass).

**Comics overhaul — Part 3: {StoryArc} token + Writer fix** — `{StoryArc}` token added to NamingConventionService Comics token set and sample data. `PathBuilderService.BuildGenericMetadata` comics section: `["Writer"]` was hardcoded `""` — now reads `comic.Writer`; `["StoryArc"]` added reading `comic.StoryArc`.

**Comics overhaul — Part 4.1: ComicScanQueue background service** — `ComicScanQueue` uses `System.Threading.Channels` (bounded, capacity 2000). Worker parallelism = 2. Events: `FileScanned(ScanResult)`, `ProgressChanged(done, total)`, `ScanCompleted`. `MediaParser.ParseComicAsync` public wrapper added.

**Comics overhaul — Part 4.2/4.3: Library Manager improvements** — "Scan Comics…" button added to Comics tab toolbar. Progress bar visible during active scan. Issues DataGrid now uses `VirtualizingPanel` (Recycling mode) + scroll-triggered pagination: loads first 100 issues, loads more as user scrolls past 70% of current list.

**Comics overhaul — Part 5: Scan pipeline UI** — new `ScanOptionsDialog` (folder picker, include subfolders, convert CBR, update ComicInfo.xml, move vs rename in place). New `ScanResultsWindow` (DataGrid with file/series/issue/year/publisher/writer/confidence/status, Apply All/Apply Selected, colour-coded rows for errors and applied results).

## v0.5.67
**Fix web UI: watcher auto-rename toggle broke Start/Stop buttons** — `POST /api/watcher/auto`, `/watcher/start`, and `/watcher/stop` all returned a partial `WatcherStatus` shape (each missing one field), causing the frontend to lose either `isRunning` or `autoRename` after every mutation. All three now always return `{ isRunning, autoRename }`.

**Web UI: download search timeout raised from 30s to 90s** — the hardcoded 30s `CancellationTokenSource` in the web API's download search handler was killing searches before slow providers (1337x, NZBGeek) had time to respond.

**Download search: providers now run in parallel** — `DownloadSearchService.SearchAsync` previously queried each provider sequentially, so total search time was the sum of all provider response times. Providers now fire concurrently via `Task.WhenAll`; total time is the slowest single provider instead.

**CLAUDE.md: web server and web UI section added** — documents `WebServerService`, `WebContext`, `TokenService`, `WebSettings`, all API endpoints, and the React SPA page routes. Dev rule added to keep the endpoint table in sync when routes change.

## v0.5.66
**FlareSolverr infrastructure** — `DownloaderSettings.FlareSolverrUrl` added; Settings → Downloaders → FlareSolverr section with URL field and Test button. `PluginManager` injects the URL into every plugin's config dict as `FlareSolverrUrl` automatically. `IDownloadProvider` gains `bool RequiresFlareSolverr => false` (backward-compatible default). `DownloadSearchWindow` shows an amber warning banner when any enabled plugin has `RequiresFlareSolverr = true` but the URL is unconfigured, with an "Open Settings" shortcut.

**AudiobookBay plugin v2.0** — rewritten to use FlareSolverr (removed curl/WinHttpHandler workarounds). Marks `RequiresFlareSolverr = true`. Debug log file removed from production build.

**New plugin: 1337x** — `AbigailsMediaRenamer.1337x.dll` searches 1337x via FlareSolverr. Returns all content types, sorted by seeders. Magnet links fetched from detail pages.

**New plugin: EZTV** — `AbigailsMediaRenamer.Eztv.dll` searches EZTV via FlareSolverr. TV content type only. Results parsed directly from search page (no detail-page fetch needed).

## v0.5.65
**Fix AudiobookBay: use WinHttpHandler instead of SocketsHttpHandler** — the site (audiobookbay.lu) is Cloudflare-protected and silently drops connections from .NET's default SocketsHttpHandler because it has a fingerprinted TLS handshake that triggers Cloudflare bot detection. Switching to `WinHttpHandler` (Windows-native HTTP/WinHTTP stack) gives the same TLS fingerprint as Windows/Edge and passes through Cloudflare. Debug logging stays in this build; the `%TEMP%\ABBayDebug.log` file is still written on each search.

## v0.5.64
**Fix AudiobookBay plugin: actually returns results now** — rewrote the base64 decode logic that was silently failing: the old regex used a lazy alternation that couldn't handle whitespace embedded within the base64 blob (site line-wraps it), so `Convert.FromBase64String()` threw and the catch returned nothing. New approach: `(.*?)` with Singleline + strip ALL whitespace before decoding. Also added page-2 pagination (Jackett searches both pages), lowercased query normalisation (Jackett-confirmed), simpler `h2 > a` title extractor, and cleaner domain filter via `Uri` parse. Version bumped to 1.2.0.

## v0.5.63
**Download search: content type filter** — new "Content type" dropdown (All Types / Movie / TV / Music / Audiobook / Book / Comic) in the Download Search window filters which providers are queried. General-purpose providers (empty `SupportedContentTypes`) always run; niche plugins like AudiobookBay only run when their type is selected or "All Types" is chosen.

**Want list: editable content type per item** — each want list card now has an inline dropdown to change the item's Media Type (Movie/TV/Music/Audiobook/Book/Comic), persisted immediately to the database. Clicking "Search" from a want list card auto-sets the content type filter to match that item.

**Want list auto-download: respects content type** — periodic auto-download checks now pass each item's `MediaType` as the content type filter, so AudiobookBay is never queried for Movie/TV items and vice versa.

**`IDownloadProvider.SupportedContentTypes`** — new default interface property (empty = all types). AudiobookBay declares `["Audiobook"]`. Existing compiled plugins that don't implement it get the default and continue to work unchanged.

## v0.5.61
**New plugin: AudiobookBay download provider** — drop `AbigailsMediaRenamer.AudiobookBay.dll` into the `plugins/` folder and enable it in Settings. Searches AudiobookBay, fetches detail pages in parallel for magnet links. No extra DLL dependencies. Config: Base URL (default `audiobookbay.lu`) and Max Results (default 3).

**Fix AudiobookBay plugin: base64 obfuscation + wrong selectors** — the site encodes most search results as base64 inside `<div class="post re-ab">` elements; the original version couldn't decode these so returned 0 results. Also fixed: missing `&tt=1` query param, wrong hash label (`Info Hash:` not `Hash`), default URL updated to `.lu`, default MaxResults reduced from 5 to 3 to reduce wait time.

## v0.5.60
**Fix stale built-in Comics library in Settings → Libraries** — a `built-in-comics` entry (empty path, IsDefault=True) left over from an older version was persisting in `settings.json` and showing up as a built-in library alongside TV Shows and Movies. `EnsureDefaultLibraries()` now strips any `built-in-comics` entry on load; the user's own Comics library (non-default, with a real path) is unaffected.

## v0.5.59
**Comic folder scan: backfill existing rows + CBR→CBZ conversion** — on every rescan, existing DB rows with null `IssueNumber` (or other blank fields) are now backfilled from ComicInfo.xml and filename parsing. CBR files found in a series folder are converted to CBZ (with ComicInfo.xml injected) on the background thread during the scan — matching the same convert-on-ingest behaviour the rename queue applies. If a CBR was converted, the DB row's `FilePath` is updated to the new `.cbz` path. New private helper `BackfillComicRowFromScan` handles the null-safe field merge for both the CBR→CBZ path-update case and the ordinary rescan case.

## v0.5.58
**Comic folder scan: parse issue numbers from files dropped outside the app** — when the library manager scans a series folder and finds files not yet in the database, it now pre-parses each file (ComicInfo.xml embedded in CBZ → filename regex fallback) on the background thread before creating the DB row. New rows get `IssueNumber`, `IssueTitle`, `Volume`, `CoverDate`, credits, and other fields populated immediately so the issues tab can match them against the Metron/CV cache without needing to run them through the rename queue first. This covers the common case of dropping hundreds of issues directly into a series folder.

## v0.5.57
**Issues tab: ComicVine as parallel issue source** — series with a CV Series ID can now fetch and display their issue list from ComicVine, not just Metron. "Fetch from Metron" button replaced with "Fetch Issues" which fetches from whichever provider IDs are set (Metron, CV, or both). When both are set the lists are merged — CV fills gaps Metron doesn't cover. Status line shows which sources contributed (e.g. `[Metron+CV]`). New `ComicVineIssues` + `ComicVineSeries` tables store the CV bulk cache. CV bulk fetch paginates `issues/?filter=volume:{id}` with 100-per-page offset pagination. Applying CV data to local comics sets `ComicVineId` and fills blank issue title/cover date without overwriting existing Metron data.

## v0.5.56
**Comic picker: search Metron by ComicVine ID** — type `cv:12345` in the series search box to look up a Metron series via ComicVine ID (`series/?cv_id=`) instead of the name search; useful for titles like "Uncanny X-Men" where the name search returns sparse results; hint text in the dialog updated to advertise the syntax

## v0.5.55
**Comic series detail window** — double-clicking a series in the library now opens a full detail window (absorbs `ComicSeriesEditorWindow`). The Series tab shows all metadata fields (name, publisher, start year, volume, genre, description, CV series ID, Metron series ID) with provider ID lookup/apply — identical to the old editor. The Issues tab shows all issues from the Metron cache for this series with status (✓ Owned / ✗ Missing / ? Unmatched): Owned = local file matched by issue number or MetronId; Missing = Metron knows about it but no local file; Unmatched = local file exists but no Metron match. Right-click missing issues to "Add to Want List" or "Search Now" (opens `DownloadSearchWindow` pre-filled); right-click owned/unmatched to "Edit Metadata". "Fetch from Metron" button refreshes the full issue list and applies metadata to matched local files.

**Comic subfolder scanning** — issue loader now recurses into volume subfolders (e.g. `Volume 1/`, `Volume 2/`) rather than scanning the series root only; `FilePath` stores the full subpath

**Want list: comic support** — added `IssueNumber` string field to `WantListItem`/`DbWantListItem`; `MediaType = "Comic"` supported throughout; `DisplayTitle` shows `{SeriesName} #{IssueNumber}`; `SearchQuery` builds `"{SeriesName} #{IssueNumber}"` for torrent/usenet search; new `AddMissingComicIssue(seriesName, issueNumber)` method on `WantListService`; DB migration adds `IssueNumber TEXT` column to `WantListItems`

## v0.5.54
**Fix transcode output unplayable in Plex/hardware decoders.** Three root causes addressed: (1) Color space metadata was missing — `ProbeService` now reads `color_space`, `color_transfer`, `color_primaries` from source and `BuildEncodeArgs` emits `-colorspace`/`-color_trc`/`-color_primaries` on the output; SDR HD sources with no existing tags default to bt709. (2) `CustomX265Params` default had `colorprim=2:transfer=2:colormatrix=2` (all "Unspecified"), which took precedence over FFmpeg's color flags for software encoding — these overrides were removed from defaults. (3) `cropdetect` round parameter changed from 16 → 32 so cropped heights are always divisible by 32, as required by many hardware HEVC decoders on Roku, Fire TV, Apple TV, and smart TVs.

**x265-params compatibility fixes** — `no-repeat-headers` 1 → 0 (headers now repeated at keyframes, fixes seeking in Plex); `open-gop` 1 → 0 (closed GOPs, required by many hardware decoders); `bframes` 8 → 5 (older hardware decoders cap at 4–5); added `level-idc=41` (Level 4.1, standard target for 1080p)

**New transcode setting: Force 8-bit output for SDR** — adds `-pix_fmt yuv420p` when encoding non-HDR content, preventing accidental 10-bit output from 10-bit sources; off by default; exposed in Settings → Transcode

## v0.5.53
**CBZ cover picker: RAR fallback for .cbz files that are actually RAR archives** — some files use a `.cbz` extension but are RAR-encoded (magic bytes `Rar!`), causing `ZipFile.OpenRead` to fail with "End of Central Directory record could not be found"; `ArchiveCoverPickerDialog` and `ComicIssueEditorWindow` now fall back to SharpCompress RAR reading when ZIP open fails on a `.cbz`; confirmed against issue #164 which was RAR-in-CBZ

## v0.5.52
**Fix comic JPEG cover decode failure caused by embedded ICC color profile** — WPF's WIC decoder calls `get_ColorContexts()` during `EndInit()` to read the JPEG's ICC color profile and throws `WINCODEC_ERR_STREAMREAD` (0x88982F72) for certain EXIF-embedded profiles; fixed by adding `BitmapCreateOptions.IgnoreColorProfile` to both `ArchiveCoverPickerDialog.LoadBitmap` and `ComicIssueEditorWindow.LoadBitmapFromBytes`; confirmed the fix against the actual file before applying

## v0.5.51
**Fix "Cannot read from the stream" for comic cover images** — WIC (Windows Imaging Component) holds a COM reference to the `MemoryStream` beyond `EndInit()`/`OnLoad`, so disposing it via `using` before WIC is finished caused the IOException; fixed in both `ArchiveCoverPickerDialog` and `ComicIssueEditorWindow` by passing `byte[]` directly into the bitmap loader and letting it create the stream inline without disposal, leaving it for GC once WIC is done

## v0.5.50
**Fix "Cannot read from the stream" decoding JPEG covers in CBZ** — `DecodePixelWidth` on `BitmapImage` triggers WIC's JPEG thumbnail decode path which fails for certain JPEG variants; replaced with `BitmapDecoder.Create` + `TransformedBitmap` scaling; also widened `SelectedImage` from `BitmapImage` to `BitmapSource` throughout `ArchiveCoverPickerDialog`

## v0.5.49
**Fix comic cover loading on background threads** — `BitmapImage` requires an STA thread; `ArchiveCoverPickerDialog` and `ComicIssueEditorWindow` were creating it on thread-pool (MTA) threads via `Task.Run`, causing all image decode attempts to silently fail; fixed by splitting archive access into two phases: raw bytes are extracted on the background thread, then `BitmapImage` is decoded on the calling (STA UI) thread; also improved "no images" hint text in the picker to show actual file extensions found in the archive

## v0.5.48
**Comic cover art always from local archive** — `ComicIssueEditorWindow` no longer fetches cover art from remote URLs; on open it tries `CoverLocal` first, then extracts the first image from the CBZ/CBR archive; after any metadata fetch (Metron, ComicVine, picker dialog) the cover display is likewise updated from the archive only; `LoadCoverFromUrlAsync` and the static `HttpClient` removed; `CoverUrl` is still stored in the DB for reference but never used for display

## v0.5.47
**CBZ cover picker: wider image format support + better "no images" diagnostic** — `ArchiveCoverPickerDialog` and `ComicIssueEditorWindow` now recognise `.avif`, `.tiff`, `.tif`, `.bmp`, `.jpe`, `.jfif` in addition to the previous `.jpg`/`.jpeg`/`.png`/`.webp`/`.gif`; the "no image files found" message now includes the total non-directory entry count so it's clear whether the archive failed to open entirely vs. simply containing no recognised images

## v0.5.46
**Scroll position memory in Library Manager** — opening a detail window (movie/TV metadata, comic issue editor, comic series editor, audiobook metadata, book metadata, music album metadata) no longer resets the parent list's scroll position; the `ScrollViewer` offset is captured just before `ShowDialog()` and restored after the dialog closes; implemented via a private `FindScrollViewer` visual-tree walker + `SaveScroll`/`RestoreScroll` helpers; covers all 7 list-to-detail flows in `LibraryManagerWindow`

## v0.5.45
**Combined Metron+ComicVine scrape for IssueTitle** — when a comic series has both `MetronSeriesId` and `ComicVineSeriesId` set, `FetchMetadata_Click` in `ComicIssueEditorWindow` now performs a combined fetch automatically: Metron is queried first (full enrich for credits, description, characters, story arcs), then if `IssueTitle` is still empty after Metron enrichment, ComicVine is queried and its `IssueTitle` overlaid onto the result; if Metron returns no result at all, ComicVine is used as a complete fallback; status bar messages show which providers were used; single-provider path (only one ID set) is unchanged; also added `story_titles` field to `MetronIssueBrief` so the issue list endpoint now captures titles during bulk series cache when Metron has them, at no extra API cost

## v0.5.44
**Comic series editor: built-in provider ID lookup** — `ComicSeriesEditorWindow` now accepts `MetadataService` (passed from `LibraryManagerWindow`); a "Look Up Provider IDs" panel at the bottom of the editor shows a Search button that calls `SearchComicAsync` with the current series name and displays results in a ListBox (provider badge, series name, year, publisher, issue count); double-clicking or selecting a result and clicking "Apply Selected ID" populates the CV Series ID or Metron ID field depending on the result's provider key; the Search button is disabled when no metadata service is available

## v0.5.43
**Comic series-level provider IDs** — `ComicSeriesEditorWindow` now has "CV Series ID" and "Metron ID" text fields; saving writes `ComicVineSeriesId` and `MetronSeriesId` to every issue in the series folder (allowing clearing by blanking the field); two new TEXT columns added to the Comics table via `TryAddColumn`; `ComicLibraryEntry` carries both fields from the DB; in `ComicIssueEditorWindow.FetchMetadata_Click`, if either series ID is set, the picker dialog is skipped and metadata is fetched directly via `GetComicDetailsAsync` using the known series ID + current issue number (Metron preferred; full credits enriched via `EnrichMetronIssueAsync` as normal); if the direct fetch returns nothing or throws, falls through to the picker dialog as usual

## v0.5.42
**Reorganize now respects routing rules and library roots** — `ReorganizeMovieEntry` and `ReorganizeTvEntry` previously derived the destination parent from the entry's *current* folder path, meaning a movie or series already in the wrong root (or the wrong genre subfolder) would be renamed in place rather than moved to the correct location; both functions now use `PathBuilderService` to compute the fully-routed parent directory from the configured library roots and routing rules (genre routing, alternate library roots, etc.); if the expected parent differs from the current one, the folder is moved cross-directory before applying the naming convention rename; `Directory.CreateDirectory` ensures new genre/routing subfolders are created automatically; two new helpers `GetExpectedMovieParent` / `GetExpectedTvSeriesParent` build minimal `ParsedMovieMedia` / `ParsedTvMedia` objects from library entries (populating Title, Year, Genres list, Edition, VideoCodec) and delegate to `PathBuilderService`; Comics and Books reorganize are unchanged (no routing for those types)

## v0.5.41
**Comic cover persistence** — archive cover selections are now saved and reloaded automatically: when the user picks a cover from "Cover from Archive…" the selected image is extracted at full resolution to `%APPDATA%\AbigailMediaRenamer\CoverCache\comic_{id}.ext`; `_entry.CoverLocal` is set to the cache path; "Save to Database" persists `CoverLocal` to a new `Comics.CoverLocal` TEXT column (added via `TryAddColumn` on startup); `LoadCoverAsync` now checks `CoverLocal` first — if the cached file exists it loads that and skips the remote URL, so the chosen cover appears every time the editor opens without a network request

## v0.5.40
**"Cover from Archive…" picker in comic metadata editor** — new `ArchiveCoverPickerDialog` opens when the "Cover from Archive…" button is clicked in the comic issue editor (button is only enabled for `.cbz`/`.cbr` files); dialog extracts every image from the archive (CBZ via `ZipFile`, CBR via SharpCompress), renders them as 100×140 thumbnails in a scrollable wrap grid, and lets the user click to select or double-click to confirm; images are decoded at 200px wide for fast loading and low memory; selected cover is set on the editor's cover image control and logged; button disabled when the file path is missing or not an archive format

## v0.5.39
**BUGFIX — comic cover image now loads in metadata viewer** — `BitmapImage.UriSource` set on a thread-pool thread (inside `Task.Run`) silently does nothing for HTTP URLs because WPF's network download requires the dispatcher; both `LoadCoverAsync` and `LoadCoverFromUrlAsync` in `ComicIssueEditorWindow` now download bytes via `HttpClient.GetByteArrayAsync` on the UI thread first, then decode from a `MemoryStream` off-thread — the reliable WPF pattern; local file path loading also switched to `MemoryStream` for consistency; cover area widened from 200→290px column, fixed `Width` and `MaxHeight` constraints removed, `StretchDirection` changed from `DownOnly` to `Both` so the cover fills the available panel at correct aspect ratio; window default width nudged 920→960px

## v0.5.38
**Reorganize + utility operations now emit proper app-log output** — all reorganize methods (`ReorganizeMovieEntry`, `ReorganizeTvEntry`, `ReorganizeComicEntry`, `ReorganizeBookEntry`) replaced `Debug.WriteLine` (debugger-only) with `_log` calls so every folder rename, season rename, and episode/file rename appears in the main app log with `[Reorganize]` prefix; batch reorganize error paths also log with `[Reorganize] ERROR`; `LibraryManagerWindow` now accepts an optional `RenameHistoryService` parameter (wired from `MainWindow`) so individual file renames record rollback entries — folder renames are log-only since the history service uses `File.Move` for rollback; `TranscodeService.EncodeAsync` logs start, probe results (codec/resolution/duration/size/HDR), key decisions (encoder, CRF, preset, two-pass, crop, loudnorm, audio codec), and completion; `TranscodeService.NormalizeAudioAsync` logs start, probe summary, measured loudness values (I/LRA/TP), applied codec, and completion; `TranscodeQueueManager.RunJobAsync` logs the job type (Remux / Normalize Audio / Transcode) and filename at queue pickup

## v0.5.37
**BUGFIX — TV reorganize crashes with EF Core translation error** — `LibraryManagerWindow` TV reorganize path was using `string.Equals(s.FolderPath, currentPath, OrdinalIgnoreCase)` in a LINQ query against `DbSet<DbTvShow>`, which EF Core cannot translate to SQL; fixed to `s.FolderPath != null && s.FolderPath.ToLower() == currentPath.ToLower()` (same pattern used in v0.5.30 fix)

## v0.5.36
**Section 4: Music / Audiobook / Book parser migration** — created `MusicParser`, `AudiobookParser`, and `BookParser` (one file each) in `Services/Parsers/`, all implementing `IContentTypeParser` and returning their typed DTOs (`ParsedMusicMedia`, `ParsedAudiobookMedia`, `ParsedBookMedia`); `MediaParser.ParseNonVideoAsync` is now a simple switch-expression that delegates to each parser; private duplicate helpers removed from `MediaParser` (`CleanNonVideoTitle`, `BuildMusicSearchQuery`, `ScoreTagCompleteness`, `ScoreNonVideoConfidence` — all already in `ParserUtilities`); remaining `ParseComicInternalAsync` usages in `MediaParser` updated to call `Parsers.ParserUtilities.*`; all callers updated with typed casts: `MainWindow.OnMediaParsed` (`NonVideoFileItem` construction uses `music?.Artist`, `audiobook?.Author`, etc.), `MainWindow.HoldFixMatch_Click` non-video branch now reads/writes artist/author on the correct typed object, `PathBuilderService.BuildGenericPath` author fallback uses `audiobook?.Author ?? book?.Author`, `BuildGenericMetadata` dictionaries updated for Music/Audiobook/Book with full typed-cast chain

## v0.5.35
**Section 3: Movie parser migration — `MovieParser` and `ParsedMovieMedia` fully wired** — new `MovieParser : IContentTypeParser` in `Services/Parsers/` extracts all movie parsing/enrichment logic from `MediaParser` (`ParseMovieFilename`, `TryApplyCachedMovie`, `EnrichMovieAsync`, `PickBestMovieMatch`, confidence scoring); `MediaParser.ParseAndEnrichAsync` now delegates movie files to `MovieParser.ParseMovieAsync` (returns `ParsedMovieMedia`); `CleanMovieTitle` made `internal static` with a `sceneTags` parameter so `MovieParser` can call it; `MovieYearPattern`, `RomanNumeralPattern`, `KnownLangCodes` made `internal static`; private `JaroWinkler` removed from `MediaParser` (now uses `ParserUtilities.JaroWinkler`); all callers updated with `movie?.Field ?? pm?.Field` dual-cast pattern (`PathBuilderService`, `AutoRenameService`, `NfoWriterService`, `RoutingRule`, `MainWindow`); `HoldFixMatch_Click` also fixed for TV branch where `ParsedTvMedia` wasn't being handled (season/episode and series ID were silently lost for hold-queue TV items)

## v0.5.34
**BUGFIX — TV series with year in title no longer produce doubled-year folders** — `TvParser.ParseFilename` now has a third year-extraction fallback: if neither the `SxxExx` separator regex nor the parenthesised-year pattern captured a year, a bare trailing year is detected at the end of the raw title (e.g. `"Legends 2026"`), stripped from the title, and stored as the series year; this prevents `PathBuilderService.FormatTitleForStorage` from producing `"Legends (2026) (2026)"` when the year is used to disambiguate same-named series

## v0.5.33
**Section 2: TV parser migration — `TvParser` and `ParsedTvMedia` fully wired** — new `TvParser : IContentTypeParser` extracts all TV parsing/enrichment logic from `MediaParser` (filename regex, `EnrichAsync`, `TryApplyCachedTv`, `PickBestSeriesMatch`, confidence scoring); `MediaParser.ParseAndEnrichAsync` now returns `Task<ParsedMediaBase>` and delegates TV files to `TvParser.ParseTvAsync` (returns `ParsedTvMedia`) while Movie files continue returning `ParsedMedia`; `FixTitleCasing` made `public static` and `TvPattern` made `internal static` so `TvParser` can call them without a circular dependency; all callers updated (`FileWatcherService`, `HoldQueueService`, `MainWindow`, `ParserTester`, `SettingsWindow`, `PathBuilderService`, `MetadataDetailWindow`, `LibraryManagerWindow`)

## v0.5.32
**Section 1: Foundation + Comics — typed DTO split** — introduced `ParsedMediaBase` as a common base class; each content type now has its own DTO (`ParsedComicMedia`, plus shells for `ParsedTvMedia`, `ParsedMovieMedia`, `ParsedMusicMedia`, `ParsedAudiobookMedia`, `ParsedBookMedia`); `ParsedComicMedia` fully wired through all services (`AutoRenameService`, `FileWatcherService`, `HoldQueueService`, `PathBuilderService`, `ComicInfoService`, `NfoWriterService`, `WmcMetadataWriter`, `MainWindow`, `ComicIssueEditorWindow`); new `IContentTypeParser` interface and `ComicParser` class replace the inline comic branch in `MediaParser.ParseNonVideoAsync`; `ParseNonVideoAsync` return type changed to `Task<ParsedMediaBase>`; all TV/Movie code still uses `ParsedMedia` (no breakage)

## v0.5.31
**Multi-volume comic series folder disambiguation (X-Men fix)** — `PathBuilderService` now handles collections like X-Men that span many volumes (`X-Men v2`, `X-Men v3`, `X-Men Blue v1`, etc.) without mis-filing issues: (1) `seriesFolderName` now includes the volume marker when `parsed.ComicVolume` is set (`X-Men v2 (1991)` not `X-Men (1991)`), so the fuzzy query scores highest against the correct folder from the start; (2) new `IsComicVolumeConflict` check rejects any folder whose `vN` token disagrees with the parsed volume even when Jaro-Winkler scores above threshold (e.g. `v2` vs `v3` ≈ 0.97 is now a hard reject); (3) `SplitComicTitle` now strips issue-range tokens (`01-300+`, `01-544+`, `600`, etc.) and year-range suffixes `(1963-2011)` before the prefix-conflict check, fixing a regression where `"X-Men v2"` was incorrectly rejected against `"X-Men v2 01-300+ (1991-2012)"` because the issue range token made it look like a 3-word vs 2-word prefix; folders without a `vN` token are treated as volume-compatible so existing single-volume series (Walking Dead, Transmetropolitan) are unaffected

## v0.5.30
**Auto-rename queue overhaul — serialized operations and folder-aware cleanup** — concurrent file-lock and SQLite conflicts when processing hundreds of comics in auto mode are now prevented by a `SemaphoreSlim(1,1)` that serializes all rename + DB-write operations; cleanup of the source folder is deferred until the last file from that folder finishes (per-folder pending counter in `_pendingByFolder`); `UpdateLibraryCache` converted from a fire-and-forget `Task.Run` to an awaited `Task` so DB writes are serialized through the same semaphore; EF Core LINQ query fixed (`string.Equals(..., OrdinalIgnoreCase)` → `.ToLower() ==`) to avoid translator exception at runtime

**Hold queue comics now use full Metron enrichment on "Fetch Metadata"** — the comics hold queue context menu has a new "Fetch Metadata" item (`HoldFetchComicMetadata_Click`) that mirrors the Library Manager issue fetch: opens the comic picker dialog, then calls `EnrichMetronIssueAsync` if the selected result came from Metron so credits, description, and story arcs are fully populated; all enriched fields are applied to `parsed` and the proposed path is rebuilt; previously hold-queue comics could only use the generic "Fix Match" path which skipped Metron enrichment

## v0.5.29
**BUGFIX — comics one-shots and spinoffs no longer land in the base series folder** — `PathBuilderService.FindComicSeriesAcrossPublishers` now applies a word-prefix conflict check after every fuzzy folder match; if the matched folder's title words are a strict leading-word prefix of the query (e.g. existing `"The Walking Dead (2003)"` matched against query `"The Walking Dead: The Alien"`) the match is rejected and a new folder is created instead; the year-stripped deduplication case (`"The Walking Dead"` query → `"The Walking Dead (2003)"` folder) is unaffected because after year-stripping both sides produce the same word list; rejected prefix matches are logged at `[PathBuilder] Comic rejected prefix match` for debugging

## v0.5.28
**Cover date shows month + year in comics library** — the issues datagrid "Year" column is now "Cover Date" and displays month + year (e.g. "May 2023") when the stored `CoverDate` has month data, falling back to year-only for older records; the issue editor replaces the plain "Year" field with a "Cover Date" field that shows and accepts `YYYY-MM` format (tooltip shows accepted formats); on save, both `CoverDate` and `CoverYear` are updated so the datagrid refreshes immediately; plain year entry ("2023") still works and leaves `CoverDate` unchanged; "Read from File" and Metron/provider fetch already set `CoverDate` correctly so the new display is populated automatically

## v0.5.27
**"Read from File" button in comic issue editor** — new button reads `ComicInfo.xml` directly from the CBZ archive and populates all available fields (series, publisher, issue number/title, volume, year, cover date, description, variant, writer, penciller, inker, colorist, letterer, cover artist, editor, characters, story arc, teams, locations, genre, page count, language); only fields present in the XML overwrite existing values; status bar reports how many fields were applied; click "Save to Database" afterwards to persist; CBR not supported (RAR is read-only)

## v0.5.26
**Removed Format column from comics library** — the `ComicFormat` field was never populated through normal use; removed the "Format" column from the issues datagrid and the Format field from the issue editor (field kept in DB and path-builder `{Edition}` token for any edge cases); the "Format" GroupBox in the issue editor is now a simple "Language" row

**Comic series editor** — double-clicking a series row (or right-click → "Edit Series…") opens a new `ComicSeriesEditorWindow` with fields for Series Name, Publisher, Start Year, Volume, Genre, and Description; saving propagates changes to every issue in the series (all `Comics` rows sharing the same `FolderPath`); blank fields are left as-is so you only overwrite what you fill in; the series list refreshes automatically after close

## v0.5.25
**Metron "Fetch Metadata" now populates full credits, description, and story arcs** — previously, fetching metadata for an individual comic issue via the "Fetch Metadata" context menu (Library Manager issues grid) or the Fetch button in the issue editor returned only stub data (issue number, cover date, cover URL) because `GetIssueDetailByIdAsync` was never called for individual fetches; `MetronMetadataProvider` now has `EnrichWithFullDetailsAsync(metronIssueId)` which calls `/api/issue/{id}/`, stores all credits (writer, penciller, inker, colorist, letterer, cover artist, editor), characters, story arcs, teams, and description back into the `MetronIssues` cache, and marks `CredentialsFetched = true` so repeat calls are instant; `MetadataService` exposes `EnrichMetronIssueAsync`; both `FetchMetadataComicIssueContext_Click` (Library Manager) and `FetchMetadata_Click` (Comic Issue Editor) now call enrichment automatically after the picker confirms a Metron match, so all fields are populated without any extra user action

## v0.5.24
**BUGFIX — Reorganize and post-save rename now respect naming convention templates** — all four reorganize methods (`ReorganizeMovieEntry`, `ReorganizeTvEntry`, `ReorganizeComicEntry`, `ReorganizeBookEntry`) previously hardcoded their own naming patterns and ignored the `NamingConventionService`; `LibraryManagerWindow` now instantiates `NamingConventionService` and every reorganize calls `BuildSubfolderStructure` for its content type, so custom templates are respected; the most visible fix is comics: filenames now match the convention's last segment (default `#{IssuePadded}[ - {Variant}]`) rather than the old `{Series} #{NNN}` pattern; TV series folder, season folder, and episode filename all derive from the convention's path segments; Plex edition tags for movies are appended after the convention-produced name (after the year) preserving the `Title (Year) {edition-xxx}` Plex format

## v0.5.23
**BUGFIX — comic issue edits not reflected in datagrid after save** — two root causes fixed: (1) new comic rows (files not previously in the DB) were assigned `DbId = 0` because `ComicLibraryEntry` was built from a `DbComic` row before the `SaveChangesAsync` that generates the EF auto-increment PK; as a result `App.Database.Comics.Find(0)` always returned null in `ComicIssueEditorWindow.SaveDb_Click`, silently discarding the save; fixed by awaiting the save in `ComicSeries_SelectionChanged` and building `ComicLibraryEntry` objects only after the await so all rows have valid IDs; (2) `OpenComicIssueEditor` only reloaded the issues pane when a rename was also requested, so any save-only edit left the grid showing stale in-memory data; fixed by always calling `ComicSeries_SelectionChanged` when the editor closes regardless of rename choice

**Comic issues pane always reads fresh DB data** — the issues query in `ComicSeries_SelectionChanged` now uses `.AsNoTracking()` so every navigation to a series re-reads committed SQLite values rather than serving potentially-stale entities from EF Core's identity-map cache

## v0.5.22
**Rename prompt after metadata save** — saving changes in the metadata editor (Movies, TV, Comics, Books) now asks "rename / reorganise files to match the updated metadata?"; accepting triggers the appropriate reorganise logic for that content type rather than requiring a separate context-menu action

**Reorganise for Comics** — new "Reorganize File…" context menu on the comic issues grid renames the file in-place to `{Series} #{NNN}` canonical form using `MediaParser.FixTitleCasing`; DB `FilePath` updated; issues pane refreshed after rename

**Reorganise for Books** — new "Reorganize Folder…" context menu on the books grid renames the book's folder to `{Title (Year)}` canonical form within its current parent; DB `FolderPath` updated; grid reloads after rename

## v0.5.21
**PERF — comic startup scan no longer rate-limited by secondary providers** — `MetadataService.SearchComicAsync` now accepts a `stopAfterPreferred` flag; the `MediaParser` comic enrichment path passes `true`, so when the preferred provider (e.g. Metron) returns results — instantly from its DB cache for previously-seen series — the search stops without firing a rate-limited network call to ComicVine or other secondary providers; picker dialogs still pass the default `false` and continue to aggregate all providers

## v0.5.20
**"Fetch All Issues from Metron…" always force-refreshes** — the context menu action now always calls `ForceRefreshAsync` (clears old `MetronSeries`/`MetronIssues` rows, re-fetches from the API) regardless of whether the series was already cached; this ensures updates to issue titles, cover dates, credits, and other list-level data are picked up on demand; the window is disabled during the refresh to prevent double-clicks; after the refresh, matched `Comics` DB rows are overwritten with the fresh Metron data rather than only filling previously-empty fields

## v0.5.19
**"Fetch All Issues from Metron…" context menu on series grid** — right-clicking a series in the Library Manager Comics tab now offers a bulk Metron fetch; if the series is already in the Metron cache the series ID is used directly, otherwise the comic picker opens to find it; once the cache is populated, all `Comics` DB rows whose folder matches the series are updated with the Metron data (issue title, cover date, cover URL, description, credits, characters, story arcs, teams, ComicVine cross-ref ID); only fills empty fields so manually-edited values are preserved; issues pane refreshes automatically after

**Metron search cache** — `MetronMetadataProvider.SearchComicAsync` now checks the local `MetronSeries` DB table first (fuzzy match: case-insensitive contains + Jaro-Winkler ≥ 0.88 fallback); cached series return instantly without any API call, eliminating the per-file network round-trip during startup scan for previously-seen series

## v0.5.18
**Metron series bulk-fetch cache** — on the first lookup for any Metron series, the entire issue list is fetched fast (1 series detail call + 1 call per page of the issue list — typically 3-4 calls total for a 60-issue series) and stored in two new DB tables (`MetronSeries`, `MetronIssues`); all subsequent lookups for that series — including startup scans — return immediately from the local cache without any API calls; full issue detail (description, credits, characters, story arcs) is fetched on demand via `GetIssueDetailByIdAsync` when the user opens the metadata viewer, rather than upfront during the bulk pass

**Removed startup scan comic skip** — `_inStartupScan` flag, `_comicFolderCache`, and `ParseComicIssueFromContext` removed from `FileWatcherService` and `MediaParser`; the Metron bulk cache makes the startup skip unnecessary — a series' first file triggers a fast 3-4 call bulk fetch, every sibling file in the same series is then a zero-API DB lookup; the `skipApiEnrichment` parameter is also removed from `ParseNonVideoAsync`

**Metron 429 handling** — all Metron HTTP calls now go through `GetApiJsonAsync` which honours the `Retry-After` response header on 429 (defaulting to 30s if absent) and retries up to 3 times per request before giving up; successful retries are logged

**`MetronCreator` DTO fix** — the `creator` field in issue credits was assumed to be `{id, name}` but the actual API may return a string URL or a differently shaped object; field is now `JsonElement?` with a `CreatorName` computed property that handles both object and string forms gracefully

**`MetronClient` refactor** — extracted `BuildComicDetails`, `FetchSeriesDetailAsync`, `FetchIssueByNumberAsync`, and `ExtractGenres` private helpers; `GetIssueAsync` and the new `BulkFetchSeriesAsync` both use them, eliminating duplication; added `next` field to `MetronPagedResponse<T>` for correct pagination termination; added `cv_id` to `MetronIssueFull` DTO for ComicVine cross-referencing

## v0.5.17
**BUGFIX — Metron series name returned as "(No Title)"** — The list endpoint (`GET /api/series/?name=...`) returns the field `"series"` (not `"name"` or `"display_name"`), and its value includes the year like `"Transmetropolitan (1997)"`; updated `MetronSeriesBrief` DTO to read from `[JsonPropertyName("series")]` and strip the trailing year in `ResolvedName`; also removed `Publisher` and `Image` from the brief DTO (those fields do not appear in the list response) and removed the stale references from `SearchSeriesAsync`

## v0.5.16
**All comic providers shown in picker, preferred first** — `SearchComicAsync` now queries every active comic provider and aggregates results rather than returning early after the first success; preferred provider's results appear at the top; auto-rename still uses `hits[0]` (preferred provider's best match) with graceful fallback to other providers if preferred returns nothing

**Comic picker dialog forwards preferred provider** — `ComicPickerDialog` previously called `SearchComicAsync` with no `preferredKey`, so the provider ordering was always ignored and ComicVine always ran first; the preferred key now flows through `UnifiedMetadataHandler` → `ComicPickerDialog` → `SearchComicAsync`

**TV Fix Match dialog respects preferred provider** — `FixMatchWindow` already queried TMDB and TVDB in parallel but always listed TMDB results first; it now accepts `preferredTvProvider` from `UnifiedMetadataHandler` (which reads `AppSettings.PreferredTvProvider`) and puts the preferred provider's results at the top of the list

## v0.5.15
**Startup scan folder-level cache for large comic collections** — `FileWatcherService.ScanExistingAsync` now uses a per-folder template cache during startup: the first comic file in each folder does a full local parse (opens the archive to read ComicInfo.xml), and every subsequent comic in the same folder uses `ParseComicIssueFromContext` (filename-only parsing with series name/publisher/year inherited from the template); comic API providers (Metron/ComicVine) are still skipped during startup scan to prevent hammering rate limits on collections with thousands of issues; manual "Fetch Metadata" from the context menu still triggers a full API lookup as before

**BUGFIX — comics in Hold Queue re-parsed by video parser** — `HoldQueueService.RetryAsync` and `ReprocessAsync` were routing `ContentType.Comic` to `ParseAndEnrichAsync` (the video/TV parser) instead of `ParseNonVideoAsync`; adding `or ContentType.Comic` to the existing pattern match fixes it

**Diagnostic log for misconfigured preferred provider** — if the preferred comic provider is not in `ActiveComicProviders` (credentials missing or provider disabled), `SearchComicAsync` now logs an actionable message pointing the user to check credentials and the Metadata Providers settings tab

## v0.5.14
**Populate Missing DB Values — comics now read ComicInfo.xml per file** — the comics phase previously had its own partial XML reader that only extracted 13 fields and searched the series folder for any file; it now uses `ComicInfoService.TryReadComicInfo` (the canonical reader), prefers `FilePath` when set so the right file is read per row, and covers all fields: `IssueNumber`, `IssueTitle`, `Volume`, `CoverYear`, `CoverDate`, `Publisher`, `Genre`, `Writer`, `Penciller`, `Inker`, `Colorist`, `Letterer`, `CoverArtist`, `Editor`, `Characters`, `StoryArc`, `Teams`, `Locations`, `ComicFormat`, `Variant`, `Description`, `LanguageISO`, `PageCount`; WHERE clause updated to match

## v0.5.13
**BUGFIX — comic metadata not appearing in Library Manager** — `AutoRenameService` was writing one `DbComic` row per series folder (`FolderPath` key, `FilePath = null`); renaming a second issue from the same series overwrote the same row, so only the last renamed issue's metadata survived; Library Manager's per-issue view then had no per-file rows to bind to; fixed by keying on `FilePath = newPath` — every renamed file now gets its own row with full metadata

## v0.5.12
**Metron provider fixes** — `PreferredComicsProvider` is now actually honoured: `SearchComicAsync` accepts an optional `preferredKey` and sorts the preferred provider to the front of the iteration order; `MediaParser` passes `_settings.PreferredComicsProvider` through; previously the setting was saved to disk but ignored at search time so ComicVine always ran first regardless of user selection

**Provider registry reload on settings save** — `_metaRegistry.Reload()` is now called after `_settings.Save()` in `Settings_Click`; previously, entering Metron credentials and saving required an app restart before the registry picked them up and added Metron to `ActiveComicProviders`

**PasswordBox theme styling** — all three themes (AbigailTheme, BoringTheme, BoringDarkTheme) now include a `PasswordBox` style that matches the `TextBox` style; previously PasswordBox fell back to the default WPF control template, causing height/border mismatches next to TextBox fields in Settings

## v0.5.11
**Comics — fix issue number leaking into series name** — root cause: `ParseNonVideoAsync` initialised `result.Title` from the raw cleaned filename ("The Walking Dead 010") so the `parsedSeries` guard was never triggered; fixed by treating `result.Title == cleanQuery` as "no real title yet" so `parsedSeries` always wins over a filename fallback; also fixed for underscore/dot-separated filenames (e.g. `The_Walking_Dead_002_(2003).cbz`): after year-paren extraction the trailing `_`/`.` is now trimmed and the bare-issue lookbehind is widened from `(?<=\s)` to `(?<=[\s._])`; `LibraryCacheService.ParseComicFolderName` added — strips trailing zero-padded issue numbers from comic series folder names so per-issue folders like `The Walking Dead 002 (2003)/` yield series name "The Walking Dead"; library manager comic series loading now calls `ParseComicFolderName`

## v0.5.10
**Comics Library Manager — 3-pane issue browser with per-issue metadata editor** — Library Manager Comics tab now drills down Publisher → Series → Issues; the Issues pane lists one row per `.cbz`/`.cbr` file with issue #, title, year, format, and writer columns; double-clicking an issue opens `ComicIssueEditorWindow` showing cover art, all metadata fields (identification, format, credits, story, description), and two action buttons: **Save to Database** persists all edits to the `DbComic` row and **Update Embedded Metadata** rewrites the `ComicInfo.xml` inside the archive; existing series-level DB rows (with `FilePath = NULL`) are transparently upgraded to file-level rows on first access

**Comics DB schema** — `Comics` table gains `FilePath TEXT` column (safe `TryAddColumn` migration; NULL for pre-migration rows)

**`ComicLibraryEntry` model** — extended with all per-issue fields: `DbId`, `FilePath`, `FileName`, `IssueCount`, `Description`, `CoverUrl`, `CoverDate`, `Letterer`, `CoverArtist`, `Editor`, `Teams`, `Locations`, `LanguageISO`, `ComicVineId`, `MetronId`

## RARBG Plugin (AbigailsMediaRenamer.RarbgPlugin — standalone DLL, not versioned with app)
**New `IDownloadProvider` plugin that searches a local RARBG SQLite dump (`rarbg_compact.sqlite`, 1.5M torrents) offline — no internet required.**

**Config: database file path (required), max results (default 50), category filter (default: tv + all movie video categories).**

**Searches by splitting query on spaces and ANDing one `LIKE '%term%'` per word against the title column; results sorted by upload date descending.**

**Builds standard magnet links from the stored info hash + a default tracker list.**

**Ships with `Microsoft.Data.Sqlite` 9.0.4; MSBuild targets copy managed assemblies and `e_sqlite3.dll` (win-x64/x86/arm64) into the `plugins/` folder automatically on build.**

**Static constructor pre-loads the native SQLite DLL from the plugin folder because `Assembly.LoadFrom` doesn't probe plugin directories for native P/Invoke targets.**

## v0.5.9
**Comics — full ComicInfo.xml metadata injection** — `MergeMetadata` now writes all available fields: `<Summary>` (plot description), `<Web>` (ComicVine URL `https://comicvine.gamespot.com/issue/4000-N/` if ComicVineId is set, Metron URL `https://metron.cloud/issue/N/` otherwise); CoverDate now also writes `<Year>` (previously Year was written from `parsed.Year` only; if that was null but CoverDate was set, no Year element was emitted); `TryReadComicInfo` now reads `<Summary>` and `<Web>` back; `ParseNonVideoAsync` maps `comicInfo.Summary` → `result.Description` so descriptions from pre-existing ComicInfo.xml files are preserved

## v0.5.8
**CRITICAL BUG FIX — watch folder cleanup destroying comic collections** — `WatchFolderCleanupService.IsRealMediaFile` only recognised video and audio extensions as "real media"; `.cbz`/`.cbr` files were invisible to it, so renaming one issue from a folder full of comics caused the entire remaining collection to be treated as fluff and deleted; fixed by injecting `IContentDetectionService` into the cleanup service and using `IsSupportedFormat` — all content types (comics, ebooks, audiobooks) now correctly block folder deletion

## v0.5.7
**Comics — skip API lookup on startup scan** — `FileWatcherService.ScanExistingAsync` sets `_inStartupScan` for the duration of the startup pass; `ParseFileAsync` passes `skipApiEnrichment: true` to `ParseNonVideoAsync` for any comic file encountered during that pass; both `SearchComicAsync` and `GetComicDetailsAsync` calls are bypassed — only local enrichment (ComicInfo.xml + filename parsing) runs; prevents hammering the ComicVine/Metron rate limit on large collection imports at startup

**Comics — provider auto-activation** — `MetadataProviderRegistry.Reload()` now activates any provider whose `IsConfigured` returns `true` (i.e. an API key is set) even if the entry in `metadata-providers.json` has `Enabled = false`; fixes "No comic providers are enabled" appearing despite a ComicVine API key being configured

**Comics — cleaner Fetch Metadata seed** — `FetchComicMetadata` now calls `MediaParser.ParseComicFilename` on the raw filename as an intermediate fallback, so the picker dialog opens pre-filled with the clean series name (e.g. "The Walking Dead") rather than the full raw filename when there is no ComicInfo.xml

## v0.5.6
**Comics — filename parser rewrite** — `ParseComicFilename` completely overhauled to handle real-world digitizer formats: (1) trailing non-year parenthesised groups (`(digital)`, `(Minutemen-Resin)`, `(retail)` etc.) are now stripped iteratively before year extraction, so `(2013)` is correctly found even when it's not the last token; (2) bare 3–4 digit issue numbers (e.g. `001`) are now extracted without requiring a `#` prefix; (3) em-dash / en-dash / spaced-hyphen separators are detected — if a digit precedes the dash the text after it is captured as the edition (`– 10th Anniversary Edition` → `ComicFormat`), while series subtitles like `Batman - The Killing Joke` (no preceding digit) are left untouched; (4) `[scanner-group]` brackets are stripped anywhere in the filename, not just at the end; (5) the publisher prefix strip now requires ALL-CAPS to avoid incorrectly splitting series titles; returns a 5-tuple now including `Edition`

**Comics — edition wired from filename** — `ParseNonVideoAsync` applies `parsedEdition` to `result.ComicFormat` (fills only if ComicInfo.xml didn't already set it), so `{Edition}` naming token works without needing an API lookup

**Comics — fuzzy folder matching** — `BuildComicPath` replaces the inline comic block in `BuildGenericPath`; for both the naming-convention path and the fallback path it fuzzy-matches (Jaro-Winkler ≥ 0.85) each intermediate folder level — Publisher then Series — so a new issue of "The Walking Dead" finds the existing `The Walking Dead (2003)` folder rather than creating a duplicate; mirrors how TV series folder matching works

## v0.5.5
**Comics — Fetch Metadata picker dialog** — "Fetch Metadata" in the Comics manual queue now opens a dedicated `ComicPickerDialog` (previously incorrectly used the generic non-video dialog); the picker shows series name + issue # search boxes pre-filled from parsed data, lists `ComicCandidate` results (series name, publisher, start year, issue count, overview snippet, provider badge), and does a two-phase lookup: select a series candidate → fetches full `ComicDetails` (credits, characters, story arcs, page count, variant, etc.) before closing; all fields are applied to the item's `ParsedMedia` and the proposed rename path is rebuilt

**Comics — ComicInfo.xml pre-enrichment** — `ComicInfoService.TryReadComicInfo(filePath)` reads `ComicInfo.xml` from CBZ archives (static, read-only, no write); `MediaParser.ParseNonVideoAsync` now reads ComicInfo.xml as step 1 for comics, fills all fields it can (series, issue, volume, year, publisher, credits, genres, characters, story arc, etc.), then filename parsing fills any remaining gaps; ComicInfo.xml wins over filename parsing but API results still take priority over both for final enrichment

**Comics naming convention** — `Edition` and `Variant` tokens added to the Comics token set (mapped to `parsed.ComicFormat` and `parsed.ComicVariant` respectively); useful for organising Annuals, Trade Paperbacks, Omnibuses, and variant covers; both support the `{Token?}` conditional syntax so they only appear in paths when the value is present

## v0.5.4
**Comics metadata — variant/edition field** — `DbComic`, `ParsedMedia`, `ComicDetails`, and `ComicLibraryEntry` all gain `Variant` (free-text description of a variant cover, alternate printing, etc.) and `ComicFormat` (e.g. "Annual", "Trade Paperback"); `ComicInfo.xml` injection writes `Variant` to `<Notes>` and `ComicFormat` to `<Format>`, following the ComicInfo.xml convention used by Komga, Kavita, and most comic readers

**Full metadata capture — ComicVine** — enrichment now fetches `character_credits`, `story_arc_credits`, `team_credits`, `location_credits`, and `page_count` per issue; `ParsedMedia` and `DbComic` gain: `Writer`, `Penciller`, `Inker`, `Colorist`, `Letterer`, `CoverArtist`, `Editor`, `Characters`, `StoryArc`, `Teams`, `Locations`, `PageCount`, `LanguageISO`, `CoverDate`, `ComicVineId`; all fields written to ComicInfo.xml on rename

**Full metadata capture — Metron** — enrichment upgraded from 2 to 3 API calls (series detail → issue list → full issue detail via `/api/issue/{id}/`); full issue detail supplies credits (grouped by role), characters, story arcs, teams, variants array, reprints array, page count, and description; `MetronId` persisted to DB

**Comic API rate limiter** (`ComicApiRateLimiter`) — `TokenBucketRateLimiter`-based service wired into both clients; ComicVine: 1 token/20 s (≈180/hr, within 200/hr quota), burst 10, queue 200; Metron: 1 token/18 s (≈4800/day, within 5000/day quota), burst 20, queue 500; `MainWindow` owns the limiter instance and passes it to both clients on construction; `Dispose()` called on window close

**DB schema migration** — `MediaDbContext.EnsureSchemaUpToDate` adds 17 new columns to the Comics table via idempotent `TryAddColumn` calls (safe on existing databases)

**MissingValuePopulatorService** — `PopulateComicsAsync` now fills all new fields from ComicInfo.xml (Penciller, Inker, Colorist, Letterer, CoverArtist, Characters, StoryArc, Variant, ComicFormat, PageCount)

## v0.5.3
**Fixed Comics library type** — removed the "built-in-comics" default library entry; Comics now behaves like Books/Music/Audiobooks — the user creates a Comic-type library entry in Settings → Libraries with their own path. Previously an empty built-in entry was auto-created on startup alongside the real TV/Movie built-ins, which was wrong.

**Comics DB persistence on rename** — `AutoRenameService.UpdateLibraryCache` now upserts a `DbComic` row (keyed by series folder path) whenever a comic file is successfully renamed; subsequent renames of different issues within the same series folder update the existing row

**Missing value populator — Comics** — `MissingValuePopulatorService.PopulateComicsAsync` added; queries all `DbComic` rows missing `Publisher` or `Genre`, finds the first `.cbz` in each folder, reads `ComicInfo.xml` from the archive, and back-fills any missing fields

**Library Manager — Comics tab** — new Comics tab in Library Manager (after Books); Publisher pane (top, from filesystem) drives Series pane (bottom, from DB); selecting a publisher queries `_db.Comics` for known series under that path and creates `DbComic` stub rows for any newly-discovered series folders, keeping the DB as the single source of truth; `ComicLibraryEntry` gains `SizeDisplay`; search filter and tab search hint wired up

## v0.5.2
**Comics content type** — full end-to-end implementation: `.cbz`/`.cbr` files are now detected as `ContentType.Comic`, parsed with `ParseComicFilename` (extracts series, volume, issue #, year), enriched via ComicVine (primary) or Metron (fallback), and routed to a new Comics tab in the main window

**Comics tab** — manual queue grid (Original File, Publisher, Series, Issue #, Issue Title, Proposed Name) + hold queue; toggle visibility in Settings → General → Comics checkbox

**Comics naming convention** — new expander in Settings → Naming tab with tokens: Publisher, Series, Volume, VolumePadded, Issue, IssuePadded, IssueTitle, Year, Genre, Writer, Format; default template `{Publisher}/{Series} ({Year?})/#{IssuePadded}`

**ComicInfo.xml injection** (`ComicInfoService`) — before every rename (manual and auto), the service reads any existing `ComicInfo.xml`, merges in pulled metadata (Series, Publisher, Number, Title, Year, Volume, Genre), and writes it back; if none exists, one is created. **CBZ: updated in-place. CBR: extracted via SharpCompress (0.49.1, added), repacked as CBZ with ComicInfo.xml injected, original CBR deleted.** The rename pipeline updates the path to `.cbz` automatically.

**`ComicVineClient` + `ComicVineMetadataProvider`, `MetronClient` + `MetronMetadataProvider` — wired into constructor and `MetadataProviderRegistry`.**

**Metron auth corrected** — uses HTTP Basic Auth (username + password from your metron.cloud account), not token; `AppSettings` replaces `MetronApiKey` with `MetronUsername` + `MetronPassword`; Settings → Metadata shows two separate fields with a `PasswordBox` for the password

**Comics library type** — `DefaultComicsLibraryId` ("built-in-comics") added; `EnsureDefaultLibraries` creates a default Comics library entry on first run; `EditLibraryDialog` type picker now includes "Comics"; `PathBuilderService.SelectLibrary` resolves `"Comic"` to the built-in Comics library

**`AppSettings.ComicsTabVisible`, `MediaTypeTabVisibility["Comics"]` — persisted tab preference.**

**`NonVideoFileItem` gains `Publisher`, `IssueNumber`, `IssueTitle` fields.**

**`ParsedMedia` gains `Publisher`, `IssueNumber`, `IssueTitle`, `ComicVolume` fields.**

**`ContentType.Comic = 5` added to enum; `ContentDetectionService` maps `.cbz`/`.cbr` to Comic; `NamingConventionService` wired; `PathBuilderService.BuildGenericPath` handles Comic fallback path.**

**`AutoRenameService` path switch extended with `ContentType.Comic`; `SetComicInfoService` optional wiring.**

**Fixed `MediaParser.ParseComicFilename` using unqualified `Path` (should be `System.IO.Path`).**

## v0.5.1
**Settings → Metadata tab cleanup** — removed the Provider Priority Order DataGrid (the underlying registry still uses its defaults; the UI was confusing and largely cosmetic); removed the GoodReads API key field (embedded key is still used automatically — no user action needed)

**ComicVine + Metron API keys** added to the Metadata tab — groundwork for upcoming Comics content type support

**Comics added to Preferred Provider selector** — ComicVine (primary) and Metron (fallback)

**`AppSettings`: removed `GoodReadsApiKey`, added `ComicVineApiKey`, `MetronApiKey`, `PreferredComicsProvider`.**

**`MetadataProviderRegistry`: added `comicvine` and `metron` to the default provider list (disabled by default until Comics content type is wired up).**

## v0.5.0
**Single-instance guard** — launching a second copy now shows a warning dialog ("Already Running — check the system tray") and exits cleanly instead of opening a duplicate window

## v0.4.66
**Removed redundant minimum quality settings** — previous session had added duplicate `MinimumResolution`/`MinimumAudio`/`MinimumSourceQuality` settings alongside the existing quality preferences; replaced with three shared hard-requirement settings (`MinimumResolution`, `MinimumAudio`, `MinimumSourceType`) used by download search, auto-download, want list, AND quality upgrader — no separate copies anywhere

**Resolution requirement is now exact** — "1080p" means only 1080p, not "1080p or better"; 4K results are rejected when 1080p is set; source type correctly uses floor semantics ("or better")

**Source type dropdown is now 5-tier** — Any / DVDRip+ / WEBRip+ / Web-DL+ / Blu-ray+

**Quality upgrader uses same shared settings** — no separate `UpgradeTargetResolution`; reads `MinimumResolution`/`MinimumAudio`/`MinimumSourceType` directly

**Fixed want list flooding on startup** — scanner no longer fires immediately on startup; replaced with `RunStartupCleanupAsync` which probes all library files missing codec/resolution, purges upgrade candidates that already meet requirements, then the periodic scanner starts on schedule

**Fixed want list re-adding deleted items** — removing an upgrade candidate now immediately sets `UpgradeBlockedUntil` on the entity

**Want list business logic queries DB directly** — `IsInList`/`IsEpisodeInList`/`IsUpgradeInList` hit `_db.WantListItems`; `ObservableCollection` is UI-only

**Want list cleared on every rename** — auto-rename, manual rename (movie + TV), and hold queue approve all call `NotifyMovieAcquired`/`NotifyTvEpisodeAcquired` with both TMDB and TVDB IDs

## v0.4.65
**Fixed upgrade scanner flagging already-good copies** — the resolution check was using `ResolutionPriority[0]` (the download search preference) as its target, so having 4K at the top of the download priority list caused every 1080p file to be flagged; the upgrade scanner now has its own dedicated `UpgradeTargetResolution` setting (Disabled / 720p / 1080p / 4K) that is completely independent of the download priority order

**"1080p or better" means only files strictly below 1080p (720p, SD, etc.) are flagged — 1080p and 4K copies are never touched.**

**Default is "1080p"; upgrade search terms now use the target resolution, not the download preference.**

**Settings → Downloaders → upgrade section now shows a "Minimum acceptable resolution" dropdown instead of inheriting from the download priority list.**

## v0.4.64
**Fixed immediate re-queuing of existing library on first run** — `UpgradeBlockedUntil` was `NULL` for every record already in the DB when the column was first added, so all pre-existing files were treated as immediately eligible on the next startup; `EnsureSchemaUpToDate` now runs a one-time `UPDATE` that stamps `now + 30 days` on any `Movies` or `TvEpisodes` record where `UpgradeBlockedUntil IS NULL` and quality data already exists — the `WHERE` clause makes it a no-op on all subsequent startups

## v0.4.63
**Fixed aggressive upgrade re-queuing** — added `UpgradeBlockedUntil` (DateTime?) to `DbMovie` and `DbTvEpisode`; both the auto-rename and manual-rename paths now stamp this field to `now + UpgradeCooldownDays` (default 30 days) whenever a file is successfully renamed into the library; the upgrade scanner filters these records out at the DB query level, so recently-acquired files are never considered for upgrade until the window expires

**New `DownloaderSettings.UpgradeCooldownDays` setting (default 30) exposed in Settings → Downloaders → Automatic Downloads → upgrade section.**

**Removed the previous `ClearEpisodeQualityAsync`/`ClearMovieQualityAsync` approach — those methods were nulling out quality fields that `UpdateLibraryCache` had just probed and written, causing a data-corruption race; the cooldown window replaces them correctly.**

## v0.4.62
**MP4 → MKV repackaging** — new `Mp4RemuxService` wraps mkvmerge to losslessly remux MP4 files into MKV containers and attach subtitle files found in the same folder. In automatic mode: enable "Automatically repackage MP4 files as MKV before renaming" in Settings → Transcode; when an MP4 arrives in the watch folder, it is remuxed before `PathBuilderService` builds the destination path so the `.mkv` extension flows through correctly; the original MP4 is deleted on success. In manual mode: right-click any MP4 item in the Movies or TV queue and choose "Repackage as MKV"; the grid item is updated in-place (path, name, proposed name) so a normal Rename click moves the `.mkv`; menu item is hidden for non-MP4 files. Subtitle language is auto-detected from filenames (`movie.en.srt` → `--language 0:en`); mkvmerge exit code 1 (warnings) is treated as success; requires mkvmerge path configured in Settings → Transcode.

## v0.4.61
**Fixed Featurettes / extras folders not being copied** — `FileWatcherService` was recursively scanning all subdirectories and treating files inside `Featurettes`, `Behind the Scenes`, `Extras`, `Specials`, etc. as individual media files; they would fail the 50 MB sample-size check and be silently dropped, so `CopyExtrasFoldersAsync` never ran; extras files are now filtered out at all three watcher scan points (startup scan, directory scan, periodic rescan) and in `OnCreated`, so they are never entered into the processing pipeline individually — the whole folder is still copied by `CopyExtrasFoldersAsync` when the parent movie/episode file is renamed

## v0.4.60
**Fixed upgrade re-download loop** — after a successful upgrade download is renamed into the library, the episode/movie's `Resolution`, `VideoCodec`, and `OriginalFilename` fields are now nulled out in the DB; this prevents the upgrade scanner from immediately re-queuing the same item on the next pass (fields are re-populated when LibrarySyncService next processes the folder)

**Fixed same-quality re-downloads** — added `ExistingResolution` and `ExistingCodec` fields to `WantListItem`; upgrade candidates now carry the quality of the file being replaced, and results are rejected at download time unless they demonstrably improve at least one dimension (higher resolution tier, or non-HEVC → HEVC codec); results with no parseable quality metadata are allowed through rather than blocked

## v0.4.59
**Added **"Automatically upgrade below-quality library files"** checkbox to Settings → Downloaders → Automatic Downloads section, below the episode tracking toggle; includes a scan interval field (hours); both bind directly to `DownloaderSettings.UpgradeScanEnabled` / `UpgradeScanIntervalHours`.**

## v0.4.58
**Quality upgrade lock** — right-click any movie or TV series in Library Manager and choose "🔒 Lock Quality (No Upgrades)" to permanently protect it from the upgrade scanner; the menu item toggles to "🔓 Unlock Quality (Allow Upgrades)" when the item is already locked

**Locked items show a small 🔒 badge overlaid on the poster thumbnail in the Movies and TV Series grids.**

**Lock state is persisted to the DB (`IsUpgradeLocked` on `DbMovie` / `DbTvShow`); locking a TV series protects all its episodes.**

**`QualityUpgradeService` skips locked movies and episodes of locked series at the DB query level, so they never appear in the want list as upgrade candidates.**

## v0.4.57
**Quality upgrade scanning** — new `QualityUpgradeService` periodically scans the library DB and queues upgrade candidates via the want list when a file falls below quality preferences. Resolution upgrades flag files whose stored resolution is lower in quality than `ResolutionPriority[0]` (e.g. 720p file when preferred is 1080p). Codec upgrades flag non-HEVC files when `PreferX265` is enabled (H.264/AVC → x265). Source quality upgrades flag files below `MinimumSourceQuality` by parsing `OriginalFilename` for BluRay / WEB-DL / WEBRip / HDTV tier. Upgrade entries are flagged with `IsUpgradeCandidate = true` and use a quality-suffixed `CustomSearchTerms` (e.g. `"The Matrix 1999 1080p x265"`) so the search targets the right copy. Once an upgrade lands in the library, the old lower-quality file is deleted only when it is a genuine orphan (last-modified before the upgrade was queued); files overwritten in-place are detected and left alone. New `DownloaderSettings` fields: `UpgradeScanEnabled` (default true), `UpgradeScanIntervalHours` (24), `UpgradeResolution`, `UpgradeCodec`, `UpgradeSourceQuality`, `UpgradeMaxPerScan` (50). Scan runs once on startup then on the configured interval; up to `UpgradeMaxPerScan` new entries per pass to prevent flooding the want list.

**New `WantListItem` / `DbWantListItem` fields: `IsUpgradeCandidate`, `ExistingFilePath`.**

## v0.4.56
**Want list search now always includes year** — TV items without a specific season/episode (whole-series tracking) now append the show's release year to the search query, matching the existing movie behaviour; this narrows provider results to the right show before any filtering happens

**Want list result filtering now applies to all items** — previously, title filtering was only enforced for TV episode items (season + episode both set); items with no episode (or movies) returned every search result unfiltered, which caused "Stalked" to grab _The Reef: Stalked_ and "Legends" to grab _DC's Legends of Tomorrow_; all non-episode results now go through `IsTitleMatch` (word-overlap ≥ 0.80 + year gate)

**Added `IsTitleMatch` and `ExtractYear` helpers in `WantListService`; year gate rejects any torrent whose embedded year differs from the wanted year by more than 1.**

## v0.4.55
**Added **Audiobookshelf** metadata export in Settings → Metadata Export: writes a `metadata.json` sidecar file in the exact format Audiobookshelf expects (title, authors, narrators, series/sequence, genres, publishedYear, publisher, description, ASIN, language) — Audiobookshelf reads this during library scans instead of re-scraping.**

**"Export Audiobookshelf metadata.json" right-click option added to the audiobook grid in Library Manager for on-demand export of individual titles.**

**If "Write metadata.json when saving audiobook metadata" is enabled, `metadata.json` is written automatically each time you save in the Audiobook Metadata window.**

**Bumped `AudiobookMetadataWindow` constructor to accept `AppSettings` for exporter access.**

## v0.4.54
**Consolidated audiobook metadata into a single **Audiobooks** provider (key `audiobook`) that replaces the old separate Kindle and Audnexus entries — search hits Audible/Kindle store first for real ASINs and cover URLs, then Audnexus for full metadata on the one title the user picks; if Kindle scraping returns nothing it falls back to Audnexus search directly.**

**Added `AudiobookLocalMetadataReader`: before any external API is consulted, the `AudiobookMetadataWindow` now pre-populates all empty fields from local sources in priority order: ID3 audio tags → OPF sidecar → JSON sidecar (Audiobookshelf `metadata.json` and similar) → `desc.txt` (overview) → `reader.txt` (narrator) → folder/parent folder name parsing (author, year, series sequence prefix, narrator in `{braces}`).**

**Sidecar files distributed with audiobooks (`.opf`, `metadata.json`, `desc.txt`, `reader.txt`, `cover.jpg/png`) are now copied alongside the audio file when it is renamed/moved.**

**Removed defunct standalone `AudnexusMetadataProvider`; `KindleMetadataProvider` is now books-only (ebook scraping) and no longer implements `IAudiobookMetadataProvider`.**

**Registry migration: saved `metadata-providers.json` files with `audnexus` entries are automatically cleaned up on next load and replaced with the new `audiobook` entry.**

## v0.4.53
**Upcoming tab now shows only genuinely discoverable titles — items already in the library (movie or TV), already in the want list, or previously ignored are filtered out entirely rather than shown with a disabled/checked button.**

**"Add to Want List" on the Upcoming tab now removes the card immediately (no page reload needed).**

**Added **Ignore** button to every card on the Upcoming tab — clicking it permanently hides that title from the Upcoming tab; ignored TMDB IDs are stored in settings.json and survive restarts.**

**`AppSettings.IgnoredUpcomingIds` (new `HashSet<int>`) stores the persisted ignore list.**

## v0.4.52
**Eliminated TMDB-ID bias throughout the want list system — TV shows identified via TVDB (without a TMDB ID) now fully participate in all want list flows.**

**`WantListItem` / `DbWantListItem` now carry a `TvdbId` field; existing databases gain the column automatically on startup via `EnsureSchemaUpToDate`.**

**`NotifyTvEpisodeAcquired` now accepts both `tmdbSeriesId` and `tvdbSeriesId`; want list entries are matched and removed correctly when either ID is present — both the auto-rename and manual rename paths pass the TVDB ID through.**

**`AddUpcomingEpisodesAsync` now includes TVDB-primary shows in the seeded want list; the episode existence check also matches against TVDB show records.**

**Missing-episode want list seeding (startup scan and periodic tracker) now builds lookups for both TMDB and TVDB IDs so items are stored with whichever identifier the show has, enabling proper library checks and duplicate detection later.**

**`IsItemInLibrary` now checks the DB using either provider ID (whichever is set on the item) before falling back to the fuzzy filesystem scan.**

**`TryAddItem` deduplication now also checks `TvdbId` so TVDB-primary shows can't be added to the want list multiple times.**

## v0.4.51
**Fixed missing episodes scanner producing false positives for shows whose folder path in the DB is null or no longer exists on disk — the scanner now skips those shows rather than reporting every aired episode as absent, preventing spurious want-list entries at startup and during periodic checks.**

**Fixed `WantListService.IsItemInLibrary` not falling back to the filesystem scan for TV episodes that lack a TmdbId (e.g. TVDB-only shows or items added before TmdbId tracking), which caused those items to permanently accumulate in the want list even when the files were already on disk.**

## v0.4.50
**Want list auto-download now does a two-stage library existence check before queuing any download: (1) DB record + `FilePath` check, (2) filesystem scan of the TV/Movie root folders (including one level deep for genre subdirectories) — catches files that landed on disk but weren't yet indexed in the database.**

**Added a per-item re-check inside the download loop itself, so a file that arrives mid-run (while earlier items in the same batch are being processed) is caught before its download is queued rather than only on the next periodic sweep.**

**`CheckLibraryAndRemoveAcquiredAsync` now uses the same unified `IsItemInLibrary` helper so both the sweep and the per-item guard apply identical logic.**

## v0.4.49
**Added **Move extra content folders** setting (General Settings, default on) — when enabled, subfolders named Featurettes, Extras, Specials, Behind the Scenes, Deleted Scenes, Interviews, Trailers, Shorts, Bonus, or Scenes are copied from the source folder to the destination alongside the moved media file; when disabled they are treated as fluff and deleted with the source folder cleanup.**

**`WatchFolderCleanupService` no longer treats video files inside extras subfolders as "real media" that blocks source folder deletion, fixing orphaned extras folders being left in the watch directory.**

## v0.4.48
**Fixed want list items reappearing after restart for episodes already in the library — `RunStartupMissingScanAsync` was re-adding them because `CheckLibraryAndRemoveAcquiredAsync` was only called inside the periodic auto-download check (and only when auto-download was enabled); startup now runs a dedicated prune pass (Step 11) after the missing-episode scan, unconditionally.**

## v0.4.47
**Manual TV and movie renames now write the full DB record on success — `FilePath`, `OriginalFilename`, `VideoCodec`, `Resolution`, `FileSizeBytes` are populated via ffprobe, and `DbTvEpisode` rows (including Season, Episode, EpisodeTitle, AirDate) are now created/updated; previously only the show/movie folder row was touched.**

**`UpdateLibraryCacheAfterRename` for both TV and movie now mirrors `AutoRenameService.UpdateLibraryCache` exactly, ensuring manual and automatic renames produce identical DB state.**

## v0.4.46
**Fixed repeated auto-downloads of episodes already in the library — `CheckLibraryAndRemoveAcquiredAsync` and `AddUpcomingEpisodesAsync` were gating the "in library" check on `FilePath != null` in the DB; both methods now fall back to a filesystem scan (via new `MissingEpisodesScanner.HasLocalEpisode`) when `FilePath` is not yet populated.**

**Removed one-time settings.json → DB want list migration code: `AppSettings.WantList` property dropped, migration block removed from `WantListService.LoadFromDatabase()`.**

**Renamed `MediaDbContext.RunColumnMigrations()` → `EnsureSchemaUpToDate()` and updated all related comments to reflect that this is normal schema maintenance, not a one-time migration.**

**Removed all "migration" language from comments throughout (`MissingValuePopulatorService`, `MissingEpisodesScanner`, `WantListService`, `MetadataProviderRegistry`).**

## v0.4.45
**Fixed auto-queue missing files after NZB unpack when the download client (NZBGet/SABnzbd) moves the completed folder into the watch folder from a separate intermediate directory — the FSW fires `Created` for the directory but not for files already inside it; `OnCreated` now scans any new non-unpack directory for media files immediately.**

**Added 60-second periodic rescan as a safety net: any file in the watch folder that isn't yet tracked (e.g. because an unpack-complete rename event was dropped) is picked up automatically; already-tracked files are skipped at zero cost.**

## v0.4.44
**Attribution badge (bottom of Library Manager and Metadata Detail windows) now shows TVDB, TMDB, and Fanart.tv — each links to the respective site.**

**About window now has a "Metadata Provided by" section with clickable cards for TVDB, TMDB, and Fanart.tv.**

## v0.4.43
**Library Manager now opens immediately — database entries are shown right away, and the filesystem sync runs in the background afterward; if new or removed folders are detected the lists refresh automatically.**

## v0.4.42
**Fixed art overwriting in the metadata viewer — `DownloadToFileAsync` and `DownloadUrlAsync` now strip the read-only file attribute before writing, matching the behaviour of `TryCopyArtFile`; previously a read-only existing art file silently blocked the write and the picker appeared to do nothing.**

**Fixed stale art display after replacement — `LoadArtSlot` now sets `BitmapCreateOptions.IgnoreImageCache` so WPF reloads from disk instead of serving the cached bitmap for the same file path.**

**Metadata viewer now auto-populates plot, genre, rating, and other fields on open when a TMDB or TVDB ID is already set but the fields are empty — removes the need to go through Fix Match just to populate a blank entry; data is saved to the database automatically.**

## v0.4.41
**About window bottom bar now has three link buttons: Ko-Fi (coral ☕), Website (blue 🌐), and Email (green ✉) — contact details removed from the text body and replaced with these clickable buttons.**

## v0.4.40
**Added Ko-Fi donate button to the About window — coral "☕ Support me on Ko-Fi" button in the bottom-left opens https://ko-fi.com/abnormalitysoftware in the default browser.**

## v0.4.39
**Encode Queue window now opens at 75% of screen width / 80% of screen height and re-centres itself, giving enough room for filenames to show without truncation.**

**TV suggested targets now shows a "Series / Season" column (e.g. "Breaking Bad (2008) / Season 3") derived from the folder structure, so you know which series each file belongs to without hovering.**

**Removed the hardcoded 240px max-width on filenames in the pending queue list so they use the available space in the wider window.**

## v0.4.38
**Added "Keep encode even if output is larger than source" setting in Settings → Transcode (file operations section) — when enabled, transcodes that produce a larger file are still kept and replace the original, useful for codec-upgrading to x265 regardless of file size; defaults to off (existing behaviour unchanged).**

## v0.4.37
**Fixed "Queue NZB" button not appearing in the desktop web UI table view — long NZB release titles were pushing the Action column off-screen; the title column now truncates with `…` and shows the full name in a hover tooltip.**

## v0.4.36
**Fixed SABnzbd API auth — added a dedicated `ApiKey` field to `UsenetClientConfig`; settings UI now shows Username + Password + API Key for SABnzbd (password optional, only needed if SABnzbd has HTTP Basic Auth enabled) and Username + Password for NZBGet, switching dynamically when the client type changes; `PushToSabnzbdAsync` now uses `cfg.ApiKey` for the API key parameter and sends HTTP Basic Auth headers when a username is configured.**

**Fixed "Open NZB" in the download search window always saving to folder and opening in the browser — it now pushes directly via the configured usenet client API when NZBGet or SABnzbd is set up; the folder/browser fallback is only used when no API client is configured.**

**Added configurable SABnzbd API path (`SabApiPath`, default `/sabnzbd/api`) — change to `/api` if SABnzbd returns an HTML redirect page instead of a JSON response (happens when the install doesn't use the default `/sabnzbd/` prefix).**

## v0.4.35
**Fixed crash (`UnauthorizedAccessException`) when using "Assign local file" for fanart (or any art type) when the destination file already existed with a read-only attribute — `TryCopyArtFile` now strips the read-only flag before overwriting, and all `Assign*`/`Clear*` art handlers show a friendly error dialog instead of crashing the app.**

## v0.4.34
**Fixed NZB downloads queuing twice — `QueueNzbAsync` was unconditionally saving to the watch folder even after a successful NZBGet/SABnzbd push, causing the usenet client to receive it via API and again via folder pickup.**

**Fixed toolbar buttons staying in their pressed/checked colour after opening a window — all "open window" toolbar buttons now clear `IsChecked` immediately on click so the hover state resets correctly when the child window closes.**

## v0.4.33
**"Download All Art" in Metadata Detail no longer errors when only a TVDB ID is set — it now proceeds if either TMDB or TVDB ID is present (Fanart.tv covers fanart/logo/banner/clearart via TVDB ID for TV shows; poster falls back to a title search).**

## v0.4.32
**Want list now seeds the next 7 days of upcoming episodes at startup — episodes whose air date falls within the next 7 days are added automatically (with auto-download enabled) so downloads can begin as soon as they're available, rather than waiting for them to appear in the missing-episodes scanner.**

## v0.4.31
**Splash screen now shows the version number (e.g. "v0.4.31") beneath the title accent line.**

## v0.4.30
**Library Manager now refreshes the cover art thumbnail immediately when the Metadata Detail window is closed after attaching or downloading art — `cover.jpg` is re-read from disk as soon as `ShowDialog()` returns for both movie and TV entries.**

## v0.4.29
**Fixed all sub-windows dropping focus to a background application (browser, desktop) when a dialog was dismissed — every `MessageBox.Show` and `ShowDialog` call in sub-windows now specifies `this` as owner, and `Activate()` is called after picker/detail dialogs return.**

**Replaced two `Microsoft.VisualBasic.Interaction.InputBox` calls in Settings (Add Scene Tag, Exclude Folder) with the existing WPF `PromptInput` helper that correctly sets `Owner = this`, eliminating the last dependency on the VisualBasic interop namespace.**

## v0.4.28
**Season poster tiles in the TV metadata editor now use the image picker — right-clicking "Download Season N Poster" opens the picker dialog showing all TMDB + Fanart.tv candidates instead of auto-downloading.**

**Fixed right-click not registering on season poster tiles that have no image yet (grid background was transparent/null so mouse events weren't captured).**

**Art picker window height now adjusts to content via SizeToContent instead of fixed 560px.**

**"Identify All" buttons in Library Manager now show the active provider in parentheses — "(TMDB)" or "(TVDB)".**

## v0.4.27
**Fixed ClearArt download from Fanart.tv always returning nothing for movies — `FanartTvMovieResult.HdClearArt` had the wrong JSON property name (`hdclearart`, the TV field) instead of `hdmovieclearart` (the correct movie field); also added SD `movieart` as a fallback.**

**Added image picker dialog when downloading art from the metadata viewer — instead of auto-picking the highest-scored result, "Download Poster/Fanart/Banner/Logo/ClearArt" now opens a scrollable grid of all available candidates (TMDB + Fanart.tv) so the user can choose.**

**Renamed art download menu items from "Download from TMDB" / "Download from FanArt.tv" to "Download Poster", "Download Fanart", "Download Banner", "Download Logo", "Download ClearArt".**

## v0.4.26
**Language routing now treats `und` (undefined) audio tracks as English — files where ffprobe reports no recognised language tag will match English-based routing rules instead of being skipped.**

## v0.4.25
**Replaced useless "Retry Lookup" context menu item in all 5 hold queues (TV, Movie, Music, Audiobooks, Books) with "Fix Match" — opens the appropriate Fix Match dialog (FixMatchWindow for TV/Movie, NonVideoFixMatchDialog for Music/Audiobook/Book), applies the chosen result to the queued item's ParsedMedia, and immediately approves the rename.**

## v0.4.24
**Fixed post-build step wiping the `plugins\` folder on every build — removed the `rd /S /Q` that deleted the entire Program Files deploy directory before xcopy; xcopy's `/Y` flag now overwrites changed files in-place without touching unrelated subdirectories like `plugins\`.**

## v0.4.23
**Fixed FanartTV image downloads always failing with "JSON property name collides" — `FanartTvImage.Likes` (computed int) clashed with `LikesRaw`'s `[JsonPropertyName("likes")]` under case-insensitive deserialization, causing every `GetSeriesArtAsync` / `GetMovieArtAsync` call to throw and return null; fixed by adding `[JsonIgnore]` to the computed property.**

**Removed misleading "developer key not configured in FanartTvClient.cs" hint from the Banner and ClearArt "no result" error dialogs — the developer key is present in source; the messages now only point to the TMDB/TVDB ID check.**

**Fixed NZB queuing from the web interface: the web endpoint was passing the display title (e.g. "Breaking Bad S01E01 720p") as the NZB filename — titles contain invalid filename characters (colons etc.) so the watch-folder save failed, and the file lacked a `.nzb` extension so SABnzbd ignored it even when the save succeeded; web endpoint now sanitizes the title and appends `.nzb` before calling `QueueNzbAsync`.**

**Fixed `DropToWatchFolder` not appending `.nzb` extension — SABnzbd's watch folder ignores files without that extension.**

**Fixed SABnzbd API push using `mode=addurl` — SABnzbd was trying to re-fetch the indexer URL but couldn't supply the required auth headers; switched to `mode=addfile` (multipart POST) using the already-downloaded NZB bytes.**

## v0.4.22
**Fixed startup crash (`FileNotFoundException: Microsoft.EntityFrameworkCore`) when third-party DLLs live only in `Libraries\`.** Root cause: the static-constructor `AssemblyResolve` trick in `App.xaml.cs` was a chicken-and-egg deadlock — the CLR must resolve `MediaDbContext`'s base class (EF Core's `DbContext`) when JIT-ting `App..cctor()`, before the first line of the static constructor can register the handler. Fix: new `Program.cs` custom entry point; `Main()` references only `System.*` types so it can be JIT'd cleanly, registers the resolver, then calls `RunApp()` in a separate `[MethodImpl(NoInlining)]` method (NoInlining prevents the JIT from merging it into `Main()` and loading `App`'s type tokens too early); `<StartupObject>AbigailsMediaRenamer.Program</StartupObject>` added to csproj. PostBuild `del *.dll` was also deleting `AbigailsMediaRenamer.dll` — PostBuild now uses PowerShell to preserve `AbigailsMediaRenamer*.dll` files in the root. Removed redundant `AssemblyResolve` static constructor and helpers from `App.xaml.cs`.

## v0.4.21
**Added `<RuntimeIdentifier>win-x64</RuntimeIdentifier>` and `<SelfContained>false</SelfContained>` to the .csproj — eliminates cross-platform native asset folders (linux-x64, osx-x64, etc.) that NuGet was pulling in from SQLite and EF Core packages; build output now contains only Windows x64 binaries.**

## v0.4.20
**Added todo.md to track known bugs.**

**DLL organisation: build target `CopyDllsToLibraries` copies all managed DLLs into a `Libraries\` subfolder after each build, keeping the app root tidy on deployed machines.**

**`AssemblyResolve` handler wired in `App.xaml.cs` static constructor (fires before WPF startup) redirects the CLR to `Libraries\` for any assembly not found beside the EXE; logs each resolution attempt to the app log.**

**Plugin DLLs in the `plugins\` subfolder are unaffected — the glob is non-recursive and the plugin project's own `CopyToPluginsFolder` target manages that path independently.**

**PostBuild now runs as admin to xcopy output (including `Libraries\` and `plugins\` subdirs) to `C:\Program Files\AbigailsMediaRenamer\`, then strips root-level `*.dll` files from the deployed folder so only the EXE, config files, and subdirs remain.**

## v0.4.19
**Want list migrated from settings.json to the SQLite database (WantListItems table) — one source of truth, no more "instanced" behaviour between UI, web API, and auto-downloader.**

**One-time migration on first launch: existing items are ported from settings.WantList to the database and cleared from the JSON file automatically.**

**Auto-downloader periodic check now verifies the library before searching — any want-list item whose episode/movie is already present in the library (FilePath/FolderPath non-null in the DB) is silently removed, so nothing re-queued that's already there.**

**Removed premature `IsAcquired = true` on download queue — items stay on the list until the file actually lands in the library and the library check confirms it, which gives accurate pending-download visibility in the UI and web interface.**

**Auto-add of newly aired episodes continues via the existing `StartPeriodicEpisodeScan` / `RunPeriodicEpisodeScanAsync` path (unchanged).**

## v0.4.18
**Rollback in auto mode now sends the file straight to the hold queue for rematching instead of letting it flow back through the auto-rename pipeline (which could immediately re-rename it to the same wrong destination).**

**Fixed a race condition in the rollback path: the watcher now pre-tracks the original path before the file is moved back, so OnCreated is suppressed and the hold-queue insertion is the sole handler.**

**Consolidated rollback logic — RenameHistoryWindow.Rollback_Click now goes through RenameHistoryService.TryUndo, so UI rollbacks are now persisted to rename-history.json like API rollbacks.**

**Fixed: files in _UNPACK_ folders (SABnzbd, NZBGet) were not picked up after unpacking completed if the unpack folder was renamed. Root cause: NotifyFilters.DirectoryName was missing from the FileSystemWatcher filter, so directory renames never fired OnRenamed. Added DirectoryName to the filter; existing OnRenamed handling was already correct for this case.**

## v0.4.17
**Want list search queries now append the release year for movies (e.g. "Mortal Kombat II 2026" instead of just "Mortal Kombat II"), disambiguating between remakes and sequels; TV searches are unchanged. Also fixed: items added with a `ReleaseDate` but no explicit `ReleaseYear` now correctly use the date's year as fallback.**

## v0.4.16
**Fixed `.txt` files appearing in the Movie grid: `.txt` was listed as a supported Book extension in `ContentDetectionService`, causing the watcher to pick up readme/description files and misclassify them; removed `.txt` from Book extensions (it's already in the watch-folder cleanup fluff list).**

## v0.4.15
**Fixed technical routing "NOT Language: en" condition incorrectly routing English-only files to the foreign destination when the audio stream has no language tag (common for single-track rips): `AudioLanguages` was empty so `MatchLanguage` returned false, then `Negate` flipped it to true. Fix: language conditions are now skipped entirely (return false, not negated) when `VideoInfo` is absent or no audio stream carried a language tag.**

**Reverted the multi-track `Any()` change — primary-track-only semantics are correct and intentional: "NOT Language: en" should route a Japanese anime with an English dub to foreign, since the primary track is Japanese.**

**Root cause confirmed via routing log (`audio=[no VideoInfo]`): `FileWatcherService` was passing `Path.GetFileName(filePath)` (bare filename) to `ParseAndEnrichAsync`, so `File.Exists` always returned false and ffprobe never ran; fixed to pass the full path.**

**Added per-file routing decision logging to `PathBuilderService.SelectLibrary`: each routing rule evaluation now logs `[Route] filename | rule 'X' (criteria) → MATCH/skip | audio=[...]` to help diagnose routing surprises.**

## v0.4.14
**Merged `TorrentSearchService` into `DownloadSearchService` — the former was an empty pass-through (its built-in provider list was intentionally empty; all actual search sources live in plugins); the merge eliminates a redundant class and ensures all search paths go through the plugin system.**

**Deleted `TorrentSearchService.cs` and `TorrentProviderRegistry.cs`; `DownloadSearchService` now owns `TorrentLog`, `SelectBestForAutoDownload`, `RunDownloadAllAsync`, and `GetClientWithFallbackAsync`.**

**Fixed "Download All" in Missing Episodes and Library Manager: was calling the dead `AutoSelectAsync`/`Registry.ActiveProviders` path (always empty); now does per-episode plugin searches via `SearchAsViewModelsAsync`.**

**Cleaned up `SettingsWindow` constructors: removed the dead `TorrentProviderRegistry registry` parameter that was accepted but never stored or used.**

**`WantListService` auto-download and `TorrentSearchWindow` already used `DownloadSearchService` correctly; no change needed there.**

## v0.4.13
**Fixed torrent search returning no results when opened from Missing Episodes window or Library Manager — `DownloadSearchService` (plugin-based providers) was not being passed through to `TorrentSearchWindow` in either case, so only built-in providers were queried (which have no results for most users).**

**Same fix applied to the Library Manager's season download search button.**

## v0.4.12
**Fixed episode calendar not showing episodes for actively airing series (Euphoria, Last Week Tonight, etc.).**

**Root cause: `RebuildCalendarIndexAsync` (which populates the `EpisodeAirDates` table the calendar reads from) was never called anywhere — the table was permanently empty; now called on app startup and after every episode rescan.**

**`FillMissingDataAsync` now respects `PreferredTvProvider` setting: uses TVDB first when configured as preferred, falls back to TMDB (previously always TMDB-first regardless of user preference).**

**Series details (`NumberOfSeasons`, `Status`) are now always re-fetched for airing/continuing shows so new seasons are discovered — previously `NumberOfSeasons` was frozen after the first successful fetch.**

**The latest season of an airing show is now refreshed on rescan instead of skipped: only episode numbers already in the DB are excluded (delta-update), so newly aired episodes are picked up without duplicating existing ones.**

**Added `IsAiringStatus()` helper that matches TVDB ("Continuing") and TMDB ("Returning Series") active-status strings.**

**Also resolved a pre-existing merge conflict in `CHANGELOG.md` (dangling `<<<<<<< HEAD` / `=======` / `>>>>>>>` markers).**

## v0.4.11
**Introduced `AppSettings.AppDataFolder` as the single source of truth for `%APPDATA%\Roaming\AbigailsMediaRenamer`; all 13 scattered `GetFolderPath` + hardcoded folder-name constructions now reference it.**

**Made `LogFilePath` `[JsonIgnore]` so it is always computed fresh — a stale `settings.json` value can no longer silently redirect the log to an old directory.**

**`TranscodeSettings.TempPath`, `UsenetSettings.NzbSavePath`, `MediaDbContext`, `TokenService`, `RenameHistoryService`, `TorrentLogService`, `UsenetLogService`, `TorrentProviderRegistry`, `MetadataProviderRegistry`, `UpcomingReleasesService`, and `TranscodeQueueManager` all updated to use `AppSettings.AppDataFolder`.**

## v0.4.10
**Redesigned episode calendar on General tab: shows a 7-day window starting from today, with ← / → navigation buttons to move between weeks.**

**Header now reads "RELEASE CALENDAR / This Week" (or Last Week / Next Week / date range for further offsets).**

**Added `NavigatePreviousWeekAsync` / `NavigateNextWeekAsync` to `GeneralTabViewModel`.**

**Added `GetEpisodesInRangeAsync(start, end)` to `EpisodeCalendarQueryService` to support arbitrary date windows.**

**Bumped `AbigailsMediaRenamer.csproj` `<Version>` and `<AssemblyInformationalVersion>` to 0.4.10 (was stale at 0.4.0).**

**Strengthened version-bump rule in CLAUDE.md: all three locations (CHANGELOG, CLAUDE.md header, csproj) must be updated in the same session with no exceptions.**

## v0.4.9
**Fixed periodic episode tracker: now fetches fresh metadata from TMDB/TVDB for each show before scanning, so newly aired episodes are discovered without the user manually running "Fetch Metadata"; previously it only compared against stale DB records and would never find new episodes.**

**Added 1-second delay between per-show API calls to respect rate limits.**

**Re-reads DB after all refreshes before running the missing-episodes scan so the scanner sees the updated episode rows.**

## v0.4.8
**Reduced toast text: title FontSize 20→16, body FontSize 17→13.**

**Fixed hard crash on minimize: `OnStateChanged` now defers `Hide()` via `Dispatcher.BeginInvoke` to avoid re-entering the state-change call stack synchronously.**

## v0.4.7
**Enlarged toast notifications to 1.5× original size: MinWidth 330, MaxWidth 540, Padding 21/15, title FontSize 20, body FontSize 17, icon 42×42 (started at 3×, walked back to 1.5×).**

**Added `LaunchOnStartup` and `MinimizeToTray` settings to `AppSettings`.**

**Added `StartupHelper` service: writes/deletes `HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\AbigailsMediaRenamer` when `LaunchOnStartup` is toggled; synced on every Settings save.**

**Minimize-to-tray: `OnStateChanged` hides the window when minimized and `MinimizeToTray` is enabled; tray icon double-click and "Show" menu item restore it.**

**Added tray context menu (Show / Exit) to the existing `TaskbarIcon`.**

**Added "Startup & System Tray" section to Settings → General Settings tab.**

## v0.4.6
**Added global auto-download control to `DownloaderSettings`: `AutoDownloadEnabled` (master toggle), `AutoDownloadIntervalMinutes` (default 30), `RetryIntervalHours` (default 6).**

**Added episode tracking settings: `EpisodeTrackingEnabled` (default true), `EpisodeCheckIntervalHours` (default 24).**

**Wantlist auto-downloads now respect the master toggle and use the configurable interval instead of hardcoded 30 min / 6 hr values.**

**Removed per-item Auto-DL filter from `CheckAutoDownloadsAsync` — all wantlist items are eligible when the global switch is on; existing `AutoDownloadEnabled` field on `WantListItem` is retained for backwards compatibility only.**

**Added periodic episode tracker: `StartPeriodicEpisodeScan()` runs `MissingEpisodesScanner` every `EpisodeCheckIntervalHours` and adds newly aired episodes to the wantlist (same logic as the startup scan, now repeated on schedule).**

**Added "Automatic Downloads" expander section to Settings → Downloaders tab with controls for all new settings.**

**Removed per-item "Auto-DL" green badge from web UI wantlist view (now irrelevant since the global setting controls all items).**

## v0.4.5
**Fixed wrong-movie match bug (e.g. "The Crash (2026)" → "The Gates (2026)").** Two root causes: (1) DB cache false positive — `TryApplyCachedMovie` and `TryApplyCachedTv` were adding the +0.05 year-match bonus to the JW score *before* comparing against `CacheMatchThreshold`; titles like "The Crash" vs "The Gates" scored 0.867+0.05=0.917 ≥ 0.90 and produced false cache hits that bypassed TMDB entirely; fixed so raw JW is used for the threshold check and the year bonus is kept only for ranking. (2) Movie picker was missing an exact-match shortcut — `PickBestMovieMatch` had no exact-match fast-path, so a year-boosted fuzzy candidate (e.g. score 1.067) could outscore an exact title match whose TMDB entry had no confirmed release date yet (score 1.0); added same exact-match-priority logic that `PickBestSeriesMatch` already had.

## v0.4.4
**Fixed web server OOM risk: replaced unbounded `Directory.EnumerateFiles(..., AllDirectories)` in music artist and audiobook author summary scans with a depth-limited helper (max 4 levels: root/artist/album/disc/file).**

**Removed `try { totalBytes += new FileInfo(f).Length; } catch { }` per-file guards in those same loops — the depth-limited helper uses `SafeGetFiles` which already swallows directory errors.**

**Fixed `web_sessions.json` readable by other local users: after each write, ACL is restricted to the current Windows user only (best-effort; silently skips on non-NTFS).**

**Removed dead second `PROPER|REPACK|REAL|...` regex pass in `MediaParser.CleanMovieTitle` — the first pass (before the SceneTags loop) already strips these tokens; the second pass after the loop was unreachable dead code.**

**`HybridThreshold` (fuzzy match acceptance) and `CacheMatchThreshold` (DB cache hit threshold) are no longer hardcoded in `MediaParser` — exposed as `FuzzyMatchThreshold` (default 0.70) and `CacheHitThreshold` (default 0.90) in `AppSettings`, persisted to settings.json.**

## v0.4.3
**Fixed silent failure in `HoldQueueService`: path-building exceptions now logged instead of swallowed.**

**Fixed silent failures in `TranscodeQueueManager`: probe/insertion errors in the target scan and `PersistQueue` failures now logged.**

**Fixed silent failures in `MainWindow`: movie and TV library-cache DB update failures now logged.**

**Fixed `AppSettings.Load()`: after deserialization, all collection and nested-settings properties are null-coalesced before use, preventing `NullReferenceException` from partially-corrupt settings.json.**

## v0.4.2
**Replaced unsalted SHA-256 password hashing with PBKDF2 (100k iterations, random 16-byte salt); stored format is now `pbkdf2:base64salt:base64hash` — existing SHA-256 hashes are rejected and users must reset their password.**

**Reduced web session token lifetime from 30 days to 24 hours.**

**Fixed path traversal in `/api/library/tv/{series}`: `Path.GetFullPath` validation now rejects paths that escape the TV root folder.**

**Restricted CORS from `AllowAnyOrigin` to `localhost` and `127.0.0.1` origins only; same-origin requests from the served SPA are unaffected.**

**Added `LoopbackOnly` option to web settings: when enabled, server binds to `127.0.0.1` instead of `0.0.0.0` (intended for use behind a local reverse proxy for HTTPS).**

**Added audit log entry when the web UI password is changed (logs IP and UTC timestamp).**

**Updated Settings → Web tab: description text clarifies PBKDF2 and the migration requirement; added "Bind to loopback only" checkbox; updated note to explain HTTPS via reverse proxy.**

## v0.4.1
**Fixed `_activeCopies` counter underflow in `AutoRenameService`: wrap copy call in try/finally so the Interlocked decrement runs exactly once regardless of where an exception is thrown.**

**Fixed silent failure in `AutoRenameService.UpdateLibraryCache`: DB exceptions now logged instead of swallowed.**

**Fixed unhandled exceptions crashing the process: `FileWatcherService.OnCreated` and `OnRenamed` now wrapped in top-level try/catch; `App.OnStartup` has a top-level try/catch that logs and shows a fatal error dialog.**

**Added `TaskScheduler.UnobservedTaskException` handler in `App.OnStartup` so unobserved task-pool exceptions are logged and marked observed instead of terminating the process.**

**Fixed `AggregateException` crash in `LibraryManagerWindow`: `.Result` on `musicTask`, `abTask`, `bkTask` is now guarded by `IsCompletedSuccessfully`.**

**Fixed `MissingValuePopulatorService`: all `SaveChangesAsync(CancellationToken.None)` calls now pass the caller's `ct` so cancellation is honoured during DB writes.**

**Fixed `MainWindow.OnClosing` blocking the UI thread: `WebServer.StopAsync()` is now fire-and-forget on a background thread instead of `.Wait(3000)` on the UI thread.**

## v0.4.0
**Replaced per-library genre/tech/language routing rules with a single global Routing Rules system.**

**Routing rules now live in AppSettings (not per library); each rule has Name, Enabled, Priority, a target library name, and AND-logic criteria (Genre, Language, Resolution, Content Type).**

**Files are evaluated against rules in priority order; first match wins; no match falls through to the default library for that content type.**

**Old per-library RoutingRules and legacy GenreDestinationRules/TechnicalDestinationRules are silently dropped on settings load.**

**Added Routing tab to Settings window with Add/Edit/Remove rule management.**

**Removed routing rules section from Edit Library dialog (libraries are now just name/type/path).**

**Removed TechnicalConditionsDialog and GenreDestinationRule/TechnicalDestinationRule model classes.**

## v0.3.9
**Art is now automatically downloaded (poster, fanart, clearlogo, banner, clearart) immediately after any successful Fetch Metadata (Library Manager) or Fix Match (MetadataDetailWindow) — no need to separately trigger "Download Art" afterwards; the Details tab inline poster and Artwork tab slots both refresh immediately if already loaded.**

**Fixed "Fetch Metadata" context menu for Movie and TV entries in Library Manager not pulling full details: previously only saved ID/title/year; now fetches overview, genres, rating, status, season count, director, runtime from TMDB or TVDB (whichever the match came from) before persisting — same result as using Fix Match inside MetadataDetailWindow.**

**Added inline poster (cover.jpg, 180×270) to the left of metadata fields in the Movie/TV Details tab of MetadataDetailWindow — mirrors the album art panel in MusicAlbumMetadataWindow; shows "No Poster" placeholder when no cover.jpg exists in the folder.**

**Fixed wrong entries appearing in Library Manager (e.g. genre subfolders like `Animation/` stored as TV shows by the old depth-1 sync): sync now prunes any DB row whose folder path no longer passes the current media detection test — genre containers fail `IsTvShowFolder` and are removed.**

**Sync now logs disk-found count alongside added/pruned counts so discrepancies are visible in the app log.**

**Populate Missing Database Values status now shows a sync summary line (added/pruned per content type) before the populate results, making it easy to see whether the sync found any new content.**

## v0.3.8
**Audiobooks and Music album sync now also match folders whose immediate subdirectories contain audio files — handles disc/part structures (e.g. `Book/Disc 1/part.mp3`) without incorrectly adding the disc subfolder as the library entry.**

**Added MediaMonkey-style side-by-side comparison to music metadata window.**

**Clicking a Discogs candidate now shows a "Current | → | Discogs Suggests" layout rather than immediately applying — metadata fields expand to show suggested values (italic) alongside current values.**

**Track grid gains a "Suggested Title" column (italic, muted) next to the editable Title column.**

**"← Apply All" button copies all suggestions into current fields; "✕" dismisses without applying.**

**Window auto-widens from 820→960px when comparison columns appear.**

**`TrackViewModel` gains `SuggestedTitle` bindable property.**

## v0.3.7
**Fixed library sync missing the vast majority of content: all content types (Movies, TV, Music, Audiobooks, Books) now use a fully recursive folder scan instead of fixed-depth enumeration; the scan descends into subdirectories until it finds a folder that directly contains media files of the right type (video/audio/ebook), or a folder that matches TV show structure (contains Season subdirs or S##E## files), then stops — so genre subfolders, technical routing subfolders, and any other nesting at any depth are handled correctly.**

**"Populate Missing Database Values" now runs a full library sync before scanning for missing values, so on-disk content not yet in the DB is discovered first.**

**Removed the previous one/two-level-deep heuristics (`EnumerateFolders`, `EnumerateDepthTwoPaths`) in favour of the single `ScanForMediaFolders` recursive method used by all content types.**

**Implemented music metadata lookup via Discogs in `MusicAlbumMetadataWindow`.**

**"Search Discogs…" button fetches top 5 release candidates, loads each tracklist, and scores by track count match (✓ exact / ~ close / ✗ off) — sorted best match first.**

**Selecting a candidate fills album title, artist, year, genre, label and remaps track titles in the grid by disc+track number with positional fallback.**

**Context menu "Fetch Metadata" now auto-triggers the search on window open (`autoSearch: true`).**

**`MusicAlbumDetails.Tracks` upgraded from `List<string>` to `List<MusicTrack>` (carries disc, track number, position string).**

**`DiscogsMetadataProvider` now parses Discogs position strings (CD numeric, X-Y multi-disc, vinyl A1/B2 sides) into structured `MusicTrack` objects.**

**`DiscogsTrack` DTO gains `Type` field to filter out heading entries from tracklists.**

## v0.3.6
**Fixed music album year never appearing in the Library Manager albums grid.**

**`PopulateAlbumsAsync` now reads `tag.Year` from audio files and stores it as `ReleaseYear` on the DB row.**

**Added `|| a.ReleaseYear == null` to the WHERE filter so albums with year missing are included in populate runs even if genre/tracks/size are already filled.**

**Also unified the sync log format: the `+added/-pruned` branch now also logs `db has:` count to match the `no changes` branch.**

## v0.3.5
**Fixed "Populate Missing Database Values" hanging immediately on click: the button was creating a new `MediaDbContext` that competed for the SQLite file lock against the always-open `App.Database` singleton; fixed by passing `App.Database` directly to `MissingValuePopulatorService` instead of opening a second context.**

**Fixed TV library sync adding Season/Specials/Extras folders as individual show entries.**

**`IsTvShowFolder` now immediately returns false if the folder's own name matches the season pattern (e.g. "Season 01", "Specials") — previously these passed because they contain episode files matching `S01E02`, so they'd be added as show entries and the scanner would never reach the actual show root.**

**Existing stale season-folder rows in the DB will be pruned on next sync run.**

## v0.3.4
**Fixed crash in "Populate Missing Database Values" when hitting the Books or Albums phase.** `Books.Genre`, `Books.PlotSummary`, `Books.PageCount`, `Books.Language`, `Books.Isbn`, `Books.Publisher`, `Books.Series`, `Books.SeriesIndex`, `Books.ReleaseDate`, `Books.Rating`, `Albums.Genre`, `Audiobooks.Genre`, `Audiobooks.PlotSummary`, and `Audiobooks.Asin` were not covered by `RunColumnMigrations` — databases created before those properties were added to the EF entities would hit "no such column" on the WHERE query. `EbookMetadataReader` (epub OPF / Calibre metadata.opf parser) was already the correct non-TagLib handler for books — the crash was the missing column migrations, not the reader.

**Populate `Run` wrapper now has per-category try/catch** so one failing category shows `ERROR — <message>` in the summary instead of aborting all remaining categories.

**Fixed `LibrarySyncService` drastically under-counting albums and audiobooks on disk.** Root cause: `IsAudioContainerFolder` was matching *any* folder that had a child directory containing audio — so `Artist/` would match (because `Album/` inside it has tracks), causing the scanner to add the artist folder and stop recursing without reaching individual albums. Fixed by tightening the sub-folder check: a folder now only qualifies via its sub-folders if *all* sub-folders match a disc/CD/part naming pattern (e.g. "Disc 1", "CD2") — pure multi-disc content. Added `DiscSubdirPattern` regex for that check; single-disc albums and audiobooks with audio files directly in the folder continue to work as before. The pruning pass in `SyncTable` will clean up the stale artist/author rows from the DB on next sync.

## v0.3.3
**"Populate Missing Database Values" now shows a full per-category breakdown after each run instead of a single aggregate total.** Previously the status text was overwritten by each phase and only the final aggregate was visible, making it impossible to tell which categories were examined and what happened to each. Example output:

```
Done — 53 updated, 162 skipped
Movies        : 14 found  →  5 updated, 9 skipped
TV Episodes   : 0 records with missing values
TV Shows      : 48 found  →  48 updated, 0 skipped
Audiobooks    : 0 records with missing values
Music Albums  : 200 found  →  0 updated, 200 skipped
Books         : 0 records with missing values
```

## v0.3.2
**Fixed "Populate Missing Database Values" not running TV or music phases.** For TV episodes, expanded WHERE to also catch episodes with `FilePath == null` (episodes added from TMDB/TVDB season fetch but never auto-renamed have a file path that was never stored). For TV shows, added new `PopulateShowsAsync` step — `DbTvShow.VideoCodec` is a migration column that no code path ever sets; now aggregated from the most-common episode codec for each show. For music albums, expanded WHERE to include `Genre == null`; `PopulateAlbumsAsync` now reads the genre tag from TagLib when scanning audio files and stores it on the album row — genre was never captured during album add in either `LibrarySyncService` or `LibraryManagerWindow`.

## v0.3.1
**"Populate Missing Database Values" button now covers all enabled media types.** TV episodes: scans show folders to discover episode files by S##E## pattern, even when `FilePath` was never stored (migration column); populates `VideoCodec`, `Resolution`, `FileSizeBytes`, `OriginalFilename`. Audiobooks: populates `DurationHours` via ffprobe on audio files in the folder. Music albums: populates `TrackCount`, `FileSizeBytes`, `DiscCount`, `HasCoverArt` by scanning album folder. Books: populates `Author`, `Publisher`, `Isbn`, `Genre`, `PlotSummary`, `Language` from epub/calibre OPF metadata. Previously only Movies were scanned; TV was supposed to be included but was broken by a `FilePath != null` filter that excluded all pre-existing episode rows.

**Fixed `PopulateMissingValues_Click` to call `db.EnsureCreated()` so migration columns (e.g. `FileSizeBytes`, `HasCoverArt` on Albums) exist before being queried.**

**`FfprobeService` refactored: `RunFfprobeAsync` now accepts full args string; added `ProbeAudioDurationAsync` using `-show_format`.**

**Music library — album list: `DbAlbum` now stores `FileSizeBytes` and `HasCoverArt`; `AlbumEntry.FromDb` maps these so DB-cached albums show correct year, discs, tracks, size, and cover art ✓ symbol.**

**Music library — artist list: `LibraryCacheService.GetMusicEntries` is now DB-first — known artists load instantly from aggregated DB stats; only new artists (not yet in DB) do a filesystem scan.**

**`SizeDisplay` on `AlbumEntry` is now a computed property from `FileSizeBytes` rather than a stored string.**

## v0.3.0
**Massive refactor to SQLite-backed library database (EF Core).**

**Separated web server from watcher service for cleaner startup/shutdown.**

**Added year-strict matching option to reduce false positives in TMDB enrichment.**

**Splash screen pre-scans library on startup for new episodes.**

**Smooth progress bars throughout the UI.**

## v0.2.9
**Implemented a plugin system to add new metadata providers and download search providers as dll files.**

**Removed all the previous stuff for download search providers (this is mostly to keep things legal for distribution).**

## v0.2.8
**Added a web interface, using quite simple auth, lets users search for things to add to wantlist, search for downloads etc.**

**Added themes to the web interface.**

## v0.2.7
**I actually totally forgot to write changelogs for the last bunch of changes.... bad Abby.**

**All manner of fixes to database handling, missing episode scanner, and episode calendar.**

**Added artwork downloads and an artwork tab to the metadata viewer (uses TMDB, TVDB, Fanart.tv currently).**

**Added a splash screen that initializes the database, initiates the metadata providers and runs a scan for new episodes for currently airing shows.**

**Added metadata exports to Windows Media Centre (using tags), Kodi/Jellyfin (via nfo's).**

**Made context menus much more uniform throughout.**

**Fixed confidence scoring and added a general settings field to specify in case you want to loosen it up a bit.**

**Added all the other metadata providers to the debug logging system.**

## v0.2.1
**Switched to SQLite database to improve speeds and persistence.**

**Added a save folder for directing NZB's for pickup by clients.**

**Added conditional tokens to the naming system (this handles things like IF there is a series, or disc number etc it is used).**

**Fixed - want list items now store the year and episode number where appropriate to locate better matches.**

**Fixed - resolution/release group etc preferences are respected by downloader (preferred results are moved to the top of the list and bolded).**

**Fixed - reorganize files maybe - need to create some test data for that.**

## v0.2.0
**Huge changes to UI, we now have tabs on the main screen for each content type.**

**General tab shows upcoming movies and TV shows that can be added to a wanted list (need to polish that a bit).**

**General tab also has an episode calendar.**

**Added NZB/Usenet downloading and adjusted the entire downloading area in settings.**

**Added Audible, Amazon, Discogs, IMDB as metadata providers.**

**Added token based naming conventions for all enabled media types.**

**Added support for music, audiobooks, ebooks.**

**Added ID3 tag support to pull initial metadata from music files.**

**Added rename handling to do an entire album.**

**Added other content types to library manager.**

## v0.1.9
**Added filesize and codec columns for library items.**

**Added Potential Targets to Encode Queue which allows users to locate things that are not x265 encoded and sorts by filesize.**

**Added Normalize Audio to context menus.**

**Fixed automatic queue and adjusted the main UI for it to look more like the manual queues and replace buttons with context menu entries.**

**Attempted to fix TVDB language stuff, not there yet... Alice in Borderland 2020 still shows a bunch of Japanese characters.**

**Complete rework of paths... shifted to a Plex style library system, you can add a library, give it a content type.**

**Updated routing rules to apply to libraries.**

**Updated library manager to use libraries... so now we can see ALL movies and TV even the stuff routed to other directories.**

**Fixed toast notifications to all be the same type... future proofing for adding cute branding.**

## v0.1.8
**Fixed title bar versions.**

**Fixed logging path so now everything uses AppData/local.**

**Fixed system files/folders being detected in library manager.**

**Fixed transcoder will no longer keep an encode which is larger than its source file.**

## v0.1.7
**Added context menu items for Transcode to x265, Edit Streams, Add Subtitles.**

**Added torrent search with a set of default providers, currently only one works.**

**Added context menu to TV series to Download Missing Episodes and Download Season Packs.**

**Fixed missing episodes functionality and added context menu directly on that screen to ignore episodes and open series folder.**

**Fixed some aspects of metadata lookup to improve likelihood of a correct result without interactions.**

**Issue - broke automatic mode, it probably wont get fixed for a week or two.**

## v0.1.5
**Added genre based destinations (assignable for Movies/TV/Both) as well as a button to pull known genres from TMDB.**

**Added local caches for metadata lookups, to facilitate library reorganization automation as well as missing episode scans.**

**Added a missing episode scanner, and on the TV tab of library manager when editing the metadata of a TV series there is a new tab to show all seasons/episodes where you can right click to include or ignore.**

**Added TVDB back to the app, added attribution information on screens which show TVDB metadata.**

**Added episode order options to TV metadata when using TVDB (i.e Aired Order, DVD Order, SuperMegaAnimeNerd Order etc).**

**Added right click reorganize files to library manager lines.**

**Added double click to open metadata editor for library manager items.**

**Fixed missing episode scanner so that it can use metadata from either provider.**

**Added file technical destinations (assignable for Movies/TV/Both) to use things like resolution, audio streams, HDR, 3D as destination filters.**

## v0.1.0
**Let Claude Opus take a run at it.**

**Fixed old classes and methods that had been deprecated.**

**Optimized regex and parser workflows, made everything match between manual and automatic.**

**Made sure that the app always starts in manual mode and the switch to automatic is more clean.**

**Fixed the autocleanupfolder routine so that it handles things like sample videos, only removes folders for files we've processed.**

**Added a library manager screen to add TMDB ID's and ensure matches, and then added some logic to check there before running a full TMDB query.**

## v0.0.9
**Repaired lots of things that got lost during refactoring.**

**Added clear log, view log, open log location buttons.**

## v0.0.8
**Fixed overwrite logic, fixed confidence logic, automatic mode now works.**

**Added file stability testing to prevent handling of files which are still copying into the watch folder.**

**Refactored the entire application to be more modular to aid in debugging.**

**Annoyed my girlfriend by sitting on the couch being boring instead of providing cuddles.**

## v0.0.7
**Started working on hold queues and automatic mode... its a bit of a shambles and not tested, need to finish this tomorrow.**

**Fixed yet more regex issues.**

**Added a file tester so you can provide a theoretical filename and watch every step of file cleaning it goes through, as well as test TMDB enrichment.**

**Tidied layout and added vertical scrollbars.**

**Added destination file/folder checks.**

**Added queue delay setting to general settings tab.**

**Added directory/file caching to allow for smoother filing of TV series where the metadata generated location might already exist with different formatting locally.**

## v0.0.6
**Added config checkbox to move prefixes to the end of a file, at this stage only for "The" but may be expanded for A, An, etc.**

**Made the watch folder stuff a dedicated service instead of being part of the main window.**

**Fixed a bunch of annoying regex stuff for fringe cases.**

## v0.0.5
**Removed TVDB because its API was annoying and not well maintained.**

**Expanded TMDB lookup to handle TV Series as well, tested and so far seems to be working well.**

**Added a debug logging checkbox in settings to log JSON information during metadata lookups.**

## v0.0.4
**Added TMDB lookup for movies (not really necessary unless you download a movie withou a release year in the filename).**

**Added TVDB lookup for TV episodes (untested).**

**Fixed various bugs relating to config save/load.**

**Added exception handling for roman numerals in filenames.**

**Added parsing to move "The" to the end of movie titles, just helps searching if you want to avoid 100 movies starting with The...**
  Might make this an option at some stage when I get around to adding metadata preferences and stuff

## v0.0.3
**Added logging to screen in app, as well as file.**

**Added a "Confidence" rating for likely correctness of metadata.**

**Added toast notifications when the application starts and for each file renamed and moved.**

**Added a cute application icon.**

## v0.0.2
**Basic file renaming based on regex.**

**Browse to paths for watch folder, movie folder, tv folder.**

**Placeholders for metadata lookup.**
