# Changelog

## 2026-08-30 (34) - About window: Check for Updates button

The About window now has a "⟳ Check for Updates" button next to Website/
Email/Ko-fi, same as AbigailsMediaRenamer's - runs the same UpdateService the
startup check uses, shows "You're already running the latest version" when
there's nothing new, or the Skip/Later/Update Now window when there is.
AboutWindow now takes AppSettings + a save callback (for Skip This Version to
persist), matching how MainWindow's startup check already wires it up.

## 2026-08-30 (33) - First public release: installer, auto-update, and the website page

Trackula v0.1.0 is now a real, downloadable release, same pipeline
AbigailsMediaRenamer uses:

- **Installer**: `Installer/Trackula.iss` (Inno Setup, win-x64 only -
  `ArchitecturesAllowed`/`ArchitecturesInstallIn64BitMode = x64compatible`),
  with a Start Menu uninstall icon, a desktop-icon task, an optional
  "start with Windows" task, a .NET 10 Desktop Runtime detection/prompt, and
  an uninstall-time offer to delete `%AppData%\Trackula` (settings/sync
  state/logs - your actual music files are never touched either way).
  `Trackula.App.csproj` now targets `win-x64` explicitly
  (`RuntimeIdentifier`/`SelfContained=false`, framework-dependent, same as
  AMR) rather than building generically.
- **In-app auto-update**: a new `UpdateService` checks
  `abnormalitysoftware.com/api/update-check?app=trackula` a few seconds
  after startup (toggleable in Settings - "Check for updates on startup"),
  and a new `UpdateAvailableWindow` offers Skip/Later/Update Now - Update Now
  downloads the installer and relaunches it silently, then exits so Inno can
  overwrite the running files. No server-side changes were needed: the
  update-check endpoint is already fully generic across apps, driven by
  `versions.json` - Trackula just needed an entry.
- **Release script**: `Installer/build-release.ps1` builds Release, compiles
  the installer, uploads it to R2, updates the download page and
  `versions.json` on the live site, and pushes - unlike AMR's, it does NOT
  try to auto-generate the on-page changelog block from `CHANGELOG.md`,
  since this file is dated dev-diary entries rather than one block per
  shipped version; that block is hand-edited before each release instead.
- **Live**: [trackula.html](https://abnormalitysoftware.com/apps/trackula)
  is up, with a homepage card, poster art, and two screenshots. `<Version>`
  in `Trackula.App.csproj` is now the source of truth for the app's version -
  see the "Releasing" section in README.md.
- Settings: fixed the Source Library folder label, which still said
  "read-only, never modified" - the Library Fixer has been able to write to
  those files (with confirmation and optional backups) for a while now.

## 2026-08-30 (32) - Header right-click, explicit horizontal scrollbars, and an About window

- **Right-clicking a column header** on the Local Music Collection grid now
  opens the show/hide-columns menu (same one the ▤ Columns button opens)
  instead of the row menu (Add to Sync List, the audit fixes, etc) - those
  act on a selected track, which a header click has none of, so it used to
  just be eleven items that did nothing useful up there.
- **Horizontal scrollbars are now explicit** on all three DataGrids (the
  collection grid, the sync list, and the Audit & Fix window's grid) rather
  than relying on the default template - visible whenever there are more
  columns than fit.
- **New About window** (ℹ About, next to Settings): version (read from the
  new `<Version>0.1.0</Version>` in Trackula.App.csproj), a short blurb about
  why Trackula exists, and Website/Email/Ko-fi links - same shape as
  AbigailsMediaRenamer's About window, trimmed down: no update checker
  (Trackula has no release pipeline yet), and the "Metadata & Art Provided
  by" section lists only the three providers Trackula actually calls -
  Deezer, iTunes, MusicBrainz (which covers the Cover Art Archive too) -
  instead of the dozen a movie/TV/book renamer needs. No logo image assets
  exist in this project, so the provider cards are text rather than tiles.

## 2026-08-30 (31) - Quick fixes from the collection grid, and room to breathe

**The Audit & Fix window's right-click actions are now on the Local Music
Collection grid too**, below the sync commands and separated by a divider - so
one obviously-wrong album can be fixed without auditing the whole library
first:

- 🖼 Embed Folder Art / (Album), 🌐 Find Art Online / (Album), 🔎 Find Metadata
  Online / (Album), 🎼 Set Compilation (Album), ✅ Approve as Correct / (Album),
  ↩ Remove Approval. "Save Tag Edits" isn't offered: that grid is read-only, so
  there are no edits to save.
- "(Album)" acts on every track of the focused row's album, including tracks
  the current filters are hiding - an album fix that skipped them would leave
  the album half-written.
- Rather than a second copy of "confirm, progress window, report, refresh",
  those flows moved out of LibraryFixWindow into a new `LibraryFixActions`
  that both windows own. Same confirm prompt, same backups, same fix history.
  LibraryFixWindow lost ~285 lines and gained nothing but delegation.
- After any write, the collection rescans (cache-backed, so only the changed
  files are re-read) and the grid keeps its filters, sort and column layout.

**Screen real estate**, all of it to the grid:

- The "Local Music Collection" / "Synchronise List" pane titles and their
  explanatory blurbs are gone - the tab headers already name them.
- So is the always-on track count under the collection grid (the status bar
  says it) - the filtered "N of M tracks shown" now appears beside the active
  filter chips, which are only there while filtering.
- The Sync List's "Add a batch at once..." caption is gone too.
- Net: three more rows of library visible at 1440x852.

**The window is full screen or it's in the tray.** The restore button is gone,
`ResizeMode` is CanMinimize, and anything that would leave the window restored
on screen snaps back to maximized. Minimize now hides to the tray rather than
the taskbar, which also means it always comes back full screen.

## 2026-08-30 (30) - First run of the new collection grid: three fixes

Built it, ran it against the real 19,243-track library, and fixed what showed
up:

- **The Duration column was blank.** `StringFormat={}{0:mm\:ss}` sits inside a
  markup extension, where a backslash is itself an escape character - so the
  format string reaching .NET was `mm:ss`, which isn't a valid TimeSpan format
  and silently produced nothing. It needs `mm\:ss`, exactly as the sync-list
  grid already had it.
- **The window now opens maximized** (`WindowState="Maximized"`). The
  minimize/maximize/close buttons still work as before.
- **Refresh and Settings are half the height they were** - 46px, icon beside
  label rather than stacked, so they read as counterparts of the device-status
  card across from them instead of two oversized tiles. New
  `TopBarActionButtonStyle`; the Device tab's Start/Abort Sync buttons keep the
  original big style.

## 2026-08-30 (29) - The Local Music Collection is a detail grid now

The Artist → Album → Track tree is gone. The collection tab is a full detail
grid of every track in the Source Library - the same shape as the sync list,
with everything the tree could never do:

- **All the fields as columns**: #, Disc, Title, Artist, Album, Album Artist,
  Genre, Year, Duration, Format, Art (the embedded cover's real dimensions),
  Sync, Purity, File and Folder.
- **Sortable** by any column, **reorderable** by dragging headers, resizable,
  and a **▤ Columns** button to hide the ones you don't care about. The whole
  arrangement (order, widths, visibility) is saved on close and restored next
  launch, keyed by column name - see `AppSettings.CollectionColumnLayout`.
- **Quick filters** above it: Genre, Artist, Album, Year (by decade), Format
  and In-sync dropdowns, each listing the values that actually occur in your
  library with track counts, multi-select, and searchable in place because
  Artist on a real library is thousands of entries. Ticking nothing in a facet
  means "don't filter on this", so they combine without an "All" entry.
- **Active filters as removable chips** under the bar, including the search
  box, plus a "Clear all" - one line that accounts for why the grid isn't
  showing everything. The footer reads "1,203 of 19,412 tracks shown".
- **Group by** Artist / Album / Genre / Decade / Format / In-sync list, or
  none at all (the default - a library opening on a thousand collapsed artist
  headers is just the tree again).
- Rows already in the sync list are marked in the row itself, the hover card
  still works, and right-click Add/Remove-to-sync-list and Add-to-Playlist plus
  drag-to-playlist all carry over. A drag now only starts from an
  already-selected row, so drag-to-extend-selection still works.

Under the hood: `LibraryBrowserViewModel` filters one `ICollectionView` with a
predicate instead of rebuilding a collection, so typing in the search box never
re-allocates the twenty-thousand rows, and a pin/unpin now updates two flags in
place rather than rebuilding the whole view (which used to throw away scroll
position, selection and expansion state with it).

## 2026-08-30 (28) - Fill metadata and embed the cover in one pass

The online metadata review dialog now has an **"Also embed this album's cover
art"** checkbox. The matching album has already been chosen in the picker, so
its art is right there - no reason to run the whole online lookup a second time
from the art action just to get it.

- Pre-ticked only when the album actually needs art (a track with an
  EmbeddedArt issue); otherwise it's offered but off, with a note that ticking
  it replaces the artwork already there.
- Disabled with an explanation when the matched candidate has no art at all,
  and carries the same "⚠ smaller than 1000x1000" warning the picker shows when
  the art is under the Purity Gate's bar.
- Ticking only the art box, with no metadata field accepted, is a valid
  outcome - the dialog no longer treats "no tags accepted" as a skip.
- Both writes happen behind ONE confirm prompt and in ONE progress window
  (tags first, then the cover), and the audit list refreshes for every track
  either write touched.

## 2026-08-30 (27) - Empty art tiles in the picker are explained, not broken

Some picker results - most visibly iTunes ones - showed an empty tile, and
picking one failed with "An invalid request URI was provided". Cause: when the
iTunes and Deezer clients measured a candidate's art and found it under
1000x1000, they replaced BOTH its URLs with empty strings but still returned
the candidate. Nothing downstream filtered those out, so the tile rendered
blank and the embed tried to download "". MusicBrainz releases with no Cover
Art Archive entry hit the same wall (deliberately blank, same crash).

Now:

- A candidate that misses the size bar keeps its real URLs and carries an
  `ArtMeetsQualityBar` flag instead. Its cover shows in the picker with
  "⚠ Art is smaller than 1000x1000 - it will still be flagged by the audit",
  and it can be chosen if that's what you want.
- A candidate with genuinely no art (`HasArt` false) says "No cover art
  available" on the tile, and is greyed out in the art-embed picker while
  staying pickable in the fill-metadata picker, where art is irrelevant. The
  picker now takes an `artRequired` flag to tell those two uses apart.
- `DownloadAndEmbedAlbumArtAsync` refuses an art-less candidate with a plain
  message rather than letting HttpClient throw about the URI.

## 2026-08-30 (26) - The audit progress window says what it's checking

The "Auditing library..." progress window had a big log box that never got a
single line in it, and a status line that read `(1234/19000)` with nothing
after it - so a multi-minute audit looked like a window sitting there doing
nothing. It now narrates the whole run:

- **Scanning** - shows the folder currently being walked, and logs a `SCAN`
  line each time it enters a new one, then a "N tracks found" line.
- **Auditing** - shows the track being checked right now ("Artist - Title"),
  and logs an `ISSUE` line for every track that gets flagged, with the names
  of the checks that flagged it. Album-wide checks log their own `ALBUM`
  lines as they run.
- **Preparing results** - an `AUDIT` summary line ("N of M tracks have
  issues"), then per-file progress while the fix list is built.

To do that, `LibraryAuditor.RunAsync` now reports a `LibraryAuditProgress`
record (checked count, total, current item, flagged count, optional log line)
instead of a bare `IProgress<int>`; the splash screen's startup audit shows
the current track in its detail line for free. The Library Fixer's re-audit
also switched from `ScanAll()` to `ScanAllWithProgress()` so the scan half of
the run is visible too.

The "currently checking" line refreshes at most every 20 tracks (the audit
reports per track, and ~19,000 dispatcher posts cost more than the checks
between them) - but a flagged track is *always* forwarded, so no issue is
ever skipped in the log. The log itself keeps a bounded 2,000-line tail
rather than growing for the whole run.

## 2026-08-30 (25) - Three tabs, and one layout language for all of them

**The main window is now three tabs** instead of one crowded split view:

- **🎵 Local Music Collection** - the Source Library tree, its search box and
  scan progress, plus "Audit & Fix Library".
- **🔁 Synchronise List** - Sync List and Playlists as sub-tabs (plain,
  smaller tab style, so they read as two views of one screen rather than as
  more top-level destinations), with the "Add ..." batch buttons, "Run
  Resolver Now" and "Clear All" on the Sync List side.
- **📱 Device** - disabled outright until something is plugged in, so the tab
  header itself answers "can I sync right now". Shows the device, where the
  files are copied *from* (the sync folder, not the library - the thing
  people get wrong), a picker when more than one device is connected, the
  Start/Abort buttons, and a plain-English note on how that particular kind
  of device is written to (real file copy / MTP / Apple handoff).

The top toolbar keeps only what's app-wide - Refresh, Settings, and the
connected-device readout, which stays visible from every tab because it's
also the answer to "why is the Device tab greyed out?".

**Drag-and-drop survives the split.** The source tree and the playlists are
now on different tabs, so dragging tracks over a tab header switches to it
mid-drag - main tabs and the Playlists sub-tab alike - and the drop lands as
before. The tree's right-click menu also
gained "Add to Playlist ▸", built as the menu opens since playlists come and
go at runtime.

**One layout language.** New shared styles in the theme - `CardPanelStyle`,
`AccentCardPanelStyle`, `PaneTitleStyle`, `PaneSubtitleStyle`,
`ToolButtonStyle`, `FieldLabelStyle`, `MainTabItemStyle` - and every tab is
built the same way: pane title + subtitle, then an actions card, then its
content. Panes used to hand-roll their own margins, paddings and font sizes,
so nothing lined up; anything laid out by hand drifts. TabItem also gained a
disabled state, so the Device tab reads as "not available yet" rather than as
a tab that failed to draw.

## 2026-08-30 (24) - Fix history, and rolling a fix back

**History button.** Audit & Fix gains a **🕘 History** button opening a new
Fix History window: every change the fixer has made to the Source Library,
newest first - when, what action, which track, and exactly what changed.
Tag writes record a real field-level diff (`Genre: '' -> 'Rock'`), read from
the file in the same open as the write, because a moment later those old
values exist nowhere but the .bak.

**Rolling back.** Select one or more rows and **Roll Back Selected** puts
them back: a tag or art fix is undone by restoring the backup taken
immediately before it, an approval by reversing it. Rows are unwound
newest-first, so two fixes to the same file don't undo each other in the
wrong order. Backups are left in place, and rolled-back rows are stamped
rather than deleted - the history stays a complete account of what happened
to the library. A row written while backups were switched off says so and
can't be rolled back; nor can one whose .bak has since been deleted. Rolling
anything back re-runs the audit on the way out, since the list behind it is
then describing a library that no longer exists.

**Where the trail comes from.** `MetadataFixService` records each write
itself rather than each call site doing it, so no fix path can forget;
approvals are recorded from the view model since they touch no file. New
`FixHistory` table and `FixHistoryRepository`, plus
`SourceLibraryWriteGateway.RestoreBackup`, which applies the same
inside-the-root guard as every other write - a history row is just text in a
database and must never become a way to write outside the library.

Eight new tests: history round-trip, null backup path for approvals,
newest-first ordering and the row cap, rollback stamping, restore returning
the original bytes (and keeping the backup), and the gateway refusing both a
path outside the library and a missing .bak. 41 tests pass.

## 2026-08-29 (23) - Approve an album as correct, and stop the gate blocking it

**"Approve as Correct".** The Audit & Fix grid's right-click menu gains
**Approve as Correct**, **Approve as Correct (Album)** and, on a row
that's already approved, **Remove Approval**. Approving records the track
in a new `TrackApproval` table in `state.db` - it writes nothing to the
music files, so there's no backup to take and no confirmation dialog.

**What approval actually does.** `ActiveSyncSetResolver` still runs the
full Purity Gate over an approved track and still records the result in
`PurityCheckHistory` - the history stays honest - but it no longer
quarantines it. The findings are logged as overridden by user approval and
the track syncs. Some checks are simply wrong for a particular album
(deliberate numbering, a genuinely non-square cover, a "compilation" that
isn't), and until now the only way past one was editing the file to
satisfy a heuristic that was mistaken.

**Where approved tracks go.** They're left out of the audit list on the
next run, with the status line saying how many were hidden. A row approved
in the current session stays on screen marked "Approved" so a mis-click on
the wrong album can be undone from the same menu, rather than the row
vanishing and taking its undo with it. The album variant approves every
track in the album, including ones the audit didn't flag - the user is
vouching for the album, and a track that's clean today can be flagged by a
later check.

Six new tests cover the approval round-trip: persistence, idempotent
re-approval, case-insensitive path matching (a fresh scan's casing needn't
match what was stored), and removal. 33 tests pass.

## 2026-08-29 (22) - Device sync, sync-list management, and iTunes as a third art provider

**Phone sync.** Trackula can now put the sync folder onto a device. Three
kinds, distinguished by how bytes actually get there:
- **Removable drive** - a real file copy into `<drive>\Music`. Skips files
  already current, prunes what no longer belongs, checks free space before
  starting, and copies via a temp file that's moved into place so an abort
  or a pulled cable never leaves a truncated track that looks complete.
- **Android** - over MTP through the Windows shell (`Folder.CopyHere` plus
  a poll for the file appearing, since CopyHere is fire-and-forget).
  Deliberately does *not* prune: copying over MTP is reliable enough,
  deleting is not, and silently removing tracks from someone's phone on a
  best-effort COM API isn't a risk worth taking.
- **iPhone/iPad** - a handoff, not a transfer. iOS gives third-party
  software no supported way to write to the music library, so Apple
  Devices does the copying and Trackula's job is keeping the folder it
  reads correct. The UI says exactly that rather than implying otherwise.

**Apple prerequisites.** New `AppleSoftwareRequirements` detects Apple
Devices, iTunes and Apple Mobile Device Support, and `AppleSetupWindow`
shows what's missing and offers a `winget` install (Store id
`9NP83LWLPZ9K`, falling back to `Apple.iTunes`) with live output. Only
ever on an explicit click - it downloads a large package and can raise a
UAC prompt. Detection reads `%LOCALAPPDATA%\Packages`, since
`C:\Program Files\WindowsApps` is ACL'd unreadable even when elevated and
shelling out to `Get-AppxPackage` costs about a second per check.

**Toolbar.** Start Sync / Abort Sync sit right of Settings - they swap
places rather than one button changing meaning, so the destructive-sounding
action is never where the safe one just was - with a device readout beside
them ("No device connected" / "Abby's iPhone connected" / free space for a
drive). Devices are polled every 4s, since Windows raises no simple managed
event for an MTP phone being plugged in.

**Sync list management.** A "Clear All" button in the sync area (spaced
away from the "Add ..." group so it isn't a mis-click, confirms first, and
leaves playlists intact), plus context-menu "Remove Album" and "Remove
Playlist" - click any track and drop everything that came with it.

**iTunes as a third art provider.** MusicBrainz was a weak fallback. The
iTunes Search API is keyless and its artwork URLs carry the size in the
path as a live resize instruction rather than a fixed asset name, so any
size can be requested - Apple downscales from the master and never
upscales. Art is fetched at **1000x1000**: exactly the Purity Gate's
recommended minimum and no more, because embedded art is copied into every
track of an album and then again into the conversion cache, sync folder and
device. Apple will serve 3000x3000 (~1.8 MB against ~300 KB), which on a
twelve-track album is ~21 MB of cover art instead of ~3.6 MB for no visible
benefit on a phone.

Deezer's `cover_xl` is nominally 1000 but not guaranteed, and Cover Art
Archive is whatever a contributor uploaded, so iTunes now leads the picker
(iTunes -> Deezer -> MusicBrainz). Its candidates are measured with
`AlbumArtQualityChecker` like Deezer's, since a release whose master is
under 1000px quietly comes back smaller.

iTunes serves **metadata as well as art**: genre, year and the full
tracklist *with real disc numbers* in a single lookup, where the other two
need a second request. It's also in the picker's "browse full discography"
fallback (in parallel with Deezer, iTunes first) for when a targeted album
search comes up empty or wrong. MusicBrainz is deliberately excluded from
that browse path only - it lists every pressing of every release
separately, which would turn a browse list into hundreds of near-identical
rows; it stays in the targeted search where that granularity occasionally
helps.

## 2026-08-29 (21) - Codebase audit acted on: album checks, database, scanning, and a data-loss guard

Full audit of the app for speed, correctness and feature gaps, then fixes
for everything it turned up. Measured against the real library: 18,959
audio files across 2,059 directories.

**Album-wide purity checks ran once per track (the big one).** Spelling
consistency, track/disc numbering and the compilation flag are properties
of an *album*, but were invoked from inside the per-track loop, so each
re-scanned its whole album once for every track in it - quadratic in album
size, and it emitted one duplicate copy of every album finding per track.
`PurityGate.CheckAlbum` now runs them once per album, and
`LibraryAuditor`/`ActiveSyncSetResolver`/`RefreshIssuesAsync` all
precompute per album instead of per track. Measured on a real audit run:

| Check              | Before | After | Duplication removed |
|--------------------|--------|-------|---------------------|
| CompilationFlag    | 5,172  | 90    | 57x                 |
| TrackDiscNumbering | 2,631  | 64    | 41x                 |

A full audit's log went from ~42,000 lines to ~11,300, and every check now
logs each finding exactly once.

**Album findings were attributed to the wrong track (correctness).**
`CheckTrackDiscNumbering` yielded issues for tracks *other* than the one
being checked, and `CheckCompilationFlagCorrectness` always blamed
`albumTracks[0]`, so a track's `PurityCheckResult` routinely carried
issues belonging to its album-mates. Two real consequences: a track was
quarantined from a sync because a *different* track had a duplicate
number, and the Library Fixer could never clear a fixed row while any
album-mate was still dirty (`RefreshIssuesAsync` clears on
`Issues.Count == 0`). A result now only ever contains issues whose
SourcePath is that track. Duplicate numbers are attributed to the tracks
actually holding them; disc gaps and the compilation flag get one
deterministic representative each rather than a copy per track.

**Sync folder cleanup could delete unrelated files (data-loss risk).**
`RemoveFilesNotInTargetSet` deleted every file under the sync root that
wasn't in the target set - no extension filter, no ownership check, no
recycle bin, and nothing in Settings stops the sync folder being pointed
at an arbitrary directory. New `SyncFolderOwnership` marker file: Trackula
only deletes inside a folder it claimed, only claims a folder that's empty
(or holds only audio and has prior sync history, so existing setups aren't
stranded), never claims a folder overlapping the Source Library in either
direction, and only ever deletes extensions it writes itself.

**Database.** No pragmas were set, so SQLite ran at `journal_mode=DELETE`
and `synchronous=FULL` - every write outside a transaction was its own
durable commit with a disk flush, and a cold scan performed ~19,000 of
them. Now WAL + `synchronous=NORMAL`, with cache upserts batched into a
single transaction (`UpsertMany`).

**Scanning.** Four separate wins:
- The tag cache did one `SELECT` per file (18,959 queries against a 6.2 MB
  table). `LoadAll()` now reads it once into a dictionary.
- `EnumerateAudioFiles` ran one full recursive walk *per recognized
  extension* - twelve traversals of 2,059 directories. Now one walk,
  filtered in memory.
- Progress was reported per file, marshalling ~19,000 times onto the UI
  thread. Now every 50 files plus a final report.
- The walk now carries `FileInfo`, so the mtime comes off the directory
  entry instead of a separate stat + `Path.GetFullPath` per file, and tag
  reads for changed files run in parallel.

**Smaller items.** Search box debounced (150 ms) instead of re-walking the
whole tree per keystroke; playlist lookup in the resolver's materialize
loop is a dictionary rather than a linear scan; cache timestamps parsed
with `ParseExact(..., "O", InvariantCulture, RoundtripKind)` instead of a
culture-sensitive `DateTime.Parse` whose failure mode would have been a
silent full re-read on every launch.

Twelve new unit tests: four on album-issue attribution and determinism,
eight on the sync-folder ownership rules.

## 2026-08-29 (20) - Online art picker now verifies the actual image, not just the label

- Same fix as the previous entry's `FolderArtFinder` bug, applied to the
  online picker: neither Deezer nor MusicBrainz candidates were checked
  before being shown, so a low-res result could be picked, embedded, and
  immediately re-flagged by the next audit. New
  `AlbumArtQualityChecker.MeetsQualityBarAsync` downloads and measures a
  candidate's actual full-res image (same 1000px/roughly-square bar as
  PurityGate's EmbeddedArt check) - Deezer's cover_xl is nominally always
  1000x1000, but a label isn't a guarantee, and Cover Art Archive images
  are arbitrary fan-submitted sizes with no guarantee at all.
- A candidate that fails the check still shows up (it may still be the
  right album for a metadata-only match) with its art blanked out rather
  than being dropped outright.
- Deliberately NOT applied to Deezer's "browse full artist discography"
  fallback (up to 100 candidates) - downloading and measuring all of them
  just to populate a browse list would turn a quick fallback into a real
  wait; a bad pick there still gets caught by the next audit re-check.

## 2026-08-29 (19) - "Embed Folder Art" no longer offers low-res local art as a fix

- **Real bug**: `FolderArtFinder` never checked the folder image's
  dimensions at all - it offered whatever it found (`cover.jpg`/`folder.jpg`/
  etc., or the sole image in the folder) regardless of quality. A folder
  image that only cleared the 600px *blocking* minimum (not the 1000px
  *recommended* one) would embed fine and clear the blocking issue, but
  leave the warning - so "Embed Folder Art" looked like it worked, then
  the very next audit flagged the same track again. Confirmed live:
  `Limp Bizkit - Gold Cobra`'s `cover.jpg` is exactly 600x600.
- `FolderArtFinder.TryFindFolderArt` now requires the candidate image to
  be at least 1000px on each side and roughly square (matching
  PurityGate's own recommended-size/aspect checks) before offering it at
  all - a folder image that doesn't clear the bar is treated as "no
  usable folder art found," routing the track to the online lookup
  instead of a fix that would just reappear.

## 2026-08-29 (18) - MusicBrainz added as a fallback provider alongside Deezer

- Deezer's catalog is clean but small - obscure/regional/weird releases
  often just aren't in it. New `MusicBrainzAlbumClient` (no API key,
  MusicBrainz's public API + Cover Art Archive, ported from
  AbigailsMediaRenamer's MusicBrainzClient) runs in parallel with every
  Deezer search and its results are appended to the same picker, each
  candidate labeled with its source. A MusicBrainz match with no Cover
  Art Archive entry still shows up (blank thumbnail) rather than being
  dropped - excluding art-less matches would defeat the point of a
  fallback aimed at exactly the releases least likely to have fan-
  submitted cover art.
- `AlbumArtCandidate` gained a `Provider` field (`Deezer`/`MusicBrainz`)
  so the follow-up album-details fetch and image download dispatch to
  the right client. The artist-full-discography browse fallback stays
  Deezer-only for now.

## 2026-08-29 (17) - New "Set Compilation (Album)" context menu action

- Fixes the "Album has multiple distinct track artists but is not flagged
  as a compilation" warning - marks every track in the album as a
  compilation and saves, same confirm+progress-window+RefreshIssuesAsync
  path as the other album-wide actions.
- `CompilationTagAccessor` (the one place that reads/writes the format-
  specific compilation flag) only handled MP4's "cpil" atom and ID3v2's
  TCMP frame - FLAC/OGG's Vorbis "COMPILATION" comment field was never
  read or written at all, so this fix (and the underlying flag in
  general) silently did nothing for FLAC, which is most of the library.
  Added Xiph/Vorbis comment support to both the getter and setter.
- `TrackTagEdits` gained an `IsCompilation` field (the tag-write record
  was previously missing it entirely); `TrackFixItemViewModel` exposes it
  so `Save Tag Edits`/`Save Tag Edits (Album)` now also persist it like
  every other core tag field.

## 2026-08-29 (16) - Fix rows sticking around after a successful metadata fix

- **Real bug**: `RefreshIssuesAsync` looked up each track's fresh, post-fix
  data using the row's OLD (pre-fix) `AlbumKey` - but `AlbumKey` is derived
  from AlbumArtist/Album/Year, so a fix that corrects exactly one of those
  (the single most common case: "missing Album Artist") changes the key
  itself. Looking the fresh album group up under the stale key silently
  found nothing, so the row never got re-checked and stayed listed even
  though the write to disk succeeded - verified via `ffprobe` that the
  file genuinely had the correct Album Artist tag afterward. Now looks
  each track up by SourcePath first (never affected by a tag edit), then
  reads that fresh track's OWN AlbumKey to gather its album-mates for the
  cross-track checks.

## 2026-08-29 (15) - Metadata review dialog now actually saves, instead of just filling the grid

- "Apply Selected" in the metadata review dialog now writes immediately -
  same confirm-then-progress-window path as the online art embed
  (`BulkFixProgressWindow` shows each track being saved), followed by
  `RefreshIssuesAsync` so the row only drops out of the grid once every
  issue on it is actually resolved. Previously it only filled the grid's
  editable fields and left saving as a separate manual "Save Tag Edits"
  step. Only tracks that actually had something accepted get written -
  `MetadataReviewDialog.AppliedItems` tracks exactly which ones.
- "Save Tag Edits" / "Save Tag Edits (Album)" stay as they are - they're
  still how a track edited by hand directly in the grid (no online
  lookup involved) gets written.

## 2026-08-29 (14) - Metadata review dialog, fixed rows only drop once ALL issues are resolved

- **New review-and-approve step for online metadata fills**, modeled on
  AbigailsMediaRenamer's music "Fetch & Compare" editor: after picking a
  Deezer album match, a new `MetadataReviewDialog` shows Current vs
  Proposed for Album/Album Artist/Artist/Genre/Year plus a per-track
  Title/Track#/Disc# grid, each with its own accept checkbox (defaulted
  on only when it would fill something currently blank - never defaults
  to silently overwriting an existing value). Nothing is applied until
  "Apply Selected"; "Skip This Album" backs out with no changes. Replaces
  the old silent auto-fill-blanks behavior for both the per-row and
  album-wide "Find Metadata Online" actions.
- **Fixed rows now only drop out of the audit grid once every issue on
  them is actually resolved**, not the instant any one write succeeds -
  new `LibraryFixViewModel.RefreshIssuesAsync` re-reads the affected
  track(s) fresh off disk after a write and re-runs the real Purity Gate
  check (with the album's current, post-fix track list for cross-track
  checks) instead of assuming success means clean. A row with a
  remaining issue is replaced in place showing that issue, instead of
  vanishing early.

## 2026-08-29 (13) - Same-titled but different-year releases no longer merged into one "album"

- **Root cause of a real audit bug**: `AlbumKey` (the identity used to group
  tracks into "one album" for the Purity Gate's cross-track checks, Smart
  Rules, and the Active Sync Set Resolver) was only `(AlbumArtist, Album)` -
  no year, no folder. Two genuinely separate releases sharing an album
  title (e.g. Chappell Roan's "School Nights" EP - a 2017 release and a
  2018 one) got merged into a single logical album, so their track numbers
  collided and the audit reported bogus "duplicate track number"/
  numbering-gap errors instead of auditing them as the two distinct
  releases they are. `AlbumKey` now also includes `Year`, so a title
  shared across different years correctly splits into separate albums for
  every check/rule that groups by it (Purity Gate, Smart Rules, sync
  resolver, Library Fixer). `SmartRuleCandidateFactory`'s album candidate
  key was updated the same way to avoid a matching collision there (this
  key isn't persisted anywhere, so no migration needed).
- Left as ordinal/case-sensitive on Album/AlbumArtist as before, since
  that's what catches the "one track spells the album differently" bug -
  only Year was added, not a fuzzy/tolerant match.

## 2026-08-29 (12) - "(Album)" online lookups now cover the whole album, not just flagged tracks

- **"Find Art Online (Album)" and "Find Metadata Online (Album)" were
  silently skipping clean tracks** - they gathered album membership from
  `Items`, which only ever holds tracks the audit flagged with an issue.
  A track that already passed every Purity Gate check was never added to
  `Items` at all, so it was invisibly excluded from "the whole album"
  even though the action's name and Abigail's intent both said otherwise.
  New `LibraryFixViewModel.GetAllTracksInAlbum` pulls every track sharing
  the album key from the actual Source Library scan instead, reusing the
  existing row (with its edited fields/status) for ones already flagged
  and building a fresh wrapper for ones that weren't. "Embed Folder Art
  (Album)" and "Save Tag Edits (Album)" are unaffected - the former's
  CanEmbedFolderArt filter would exclude an already-fine track anyway,
  and the latter is specifically about saving what's already shown in
  the grid.

## 2026-08-29 (11) - Single-instance guard, fixed rows drop out of the audit list immediately

- **Trackula now refuses to run a second instance** - launching it while
  one's already open shows "Trackula is already running - check your
  system tray" and brings the running instance's window to the front,
  instead of two copies fighting over the same source files, database,
  and (as this whole session kept hitting) the same locked exe/dll during
  rebuilds. A named Mutex guards startup; a named EventWaitHandle lets the
  second launch ask the first one to show itself. Verified live by
  launching two copies - the second showed the message and exited, the
  first kept running.
- **Fixed tracks drop out of the Library Fixer's grid immediately** -
  embedding art, saving tag edits, and the online art lookup (per-row,
  per-album, and bulk) all now remove a row from the list the moment its
  write succeeds, instead of leaving it listed until the next full
  "Re-run Audit". A row with more than one distinct issue (e.g. missing
  art AND missing tags) disappears once ANY of its issues is fixed, even
  if another goes unaddressed - a full re-audit still catches that case.
  The online metadata lookup does NOT remove rows, since it only pre-
  fills fields for review and doesn't write anything until "Save Tag
  Edits" is clicked.

## 2026-08-29 (10) - Album-wide online lookups un-gated too, confirm dialog skipped when backups are off

- The previous entry only removed the eligibility gate from the per-row
  "Find Art Online" / "Find Metadata Online" - the "(Album)" versions
  still filtered to tracks the audit considered eligible, so clicking
  "Find Art Online (Album)" on an album that already had some art
  reported "No tracks in this album need it" and did nothing. Now
  album-wide online lookups apply to every track in the album
  unconditionally, same as the per-row versions - if you click it, it
  runs.
- ConfirmFixDialog is skipped entirely when "Back up files before every
  Library Fixer write" (Settings > Purity Gate) is off - unticking that
  is already an informed choice, so asking again before every single fix
  was pure friction with no new information in it. It still shows
  normally when backups are on.

## 2026-08-29 (9) - Context menu actually fixed (verified live), online lookups no longer purity-gated

- **Context menu fixed for real** - all three prior attempts (a
  `PlacementTarget.DataContext` binding, an `ElementName` binding to
  `ItemsGrid.SelectedItem`, an imperative per-row `DataContext`
  assignment) were trying to make the ContextMenu itself carry a
  DataContext, which a detached popup won't reliably do. Switched to the
  pattern already proven working in AbigailsMediaRenamer's MovieGrid:
  `ContextMenu` set once, directly on the DataGrid (not per-row via a
  shared Style instance), and every Click handler reads
  `ItemsGrid.SelectedItem` directly - right-clicking a row already
  selects it via WPF's default DataGrid behavior. Verified live: right-
  clicked "The Hollow" (A Perfect Circle - Mer De Noms), "Find Art
  Online" opened the picker with three real Deezer results.
- **"Find Art Online" / "Find Metadata Online" no longer require the
  audit to consider a track eligible** - if you explicitly click the
  button, it runs regardless of whether the track already has some
  (possibly low-quality) art or metadata. "Embed Folder Art" stays
  conditional since it's a hard capability check (no folder art file
  found = nothing to embed), not a purity rule.

## 2026-08-29 (8) - Banner reverted, context menu fixed for real this time

- **Banner reverted to plain proportional scaling** - the MaxHeight cap +
  gradient-margin approach from earlier today looked worse than just
  letting it scale with the window, so it's gone; back to the simple
  aspect-locked Viewbox with no cap. Actually fixing the "huge on a wide
  monitor" complaint properly needs the banner.jpg canvas itself extended
  (still a pending follow-up, not done in this entry).
- **Context menu items were STILL no-ops after the previous fix** - an
  ElementName binding to `ItemsGrid.SelectedItem` doesn't reliably resolve
  either, because a ContextMenu is a detached popup, not part of the
  normal visual/name-scope tree until it opens. Fixed for real this time
  by setting the row's `ContextMenu.DataContext` imperatively in a
  `ContextMenuOpening` handler, where `sender` is guaranteed to be the
  exact `DataGridRow` being right-clicked - no binding involved at all.

## 2026-08-29 (7) - Fix the new context menu actually doing nothing, dialog clipping

- **The Library Fixer's new right-click context menu items did nothing** -
  confirmed via the log (zero Deezer activity after clicking "Find Art
  Online" on a real, common album). The `ContextMenu.DataContext="{Binding
  PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}"`
  trick wasn't resolving reliably against a Style-shared ContextMenu
  instance. Replaced with an explicit row-selection EventSetter
  (`PreviewMouseRightButtonDown` sets `ItemsGrid.SelectedItem` to that
  row) plus binding the menu's DataContext to `ItemsGrid.SelectedItem` -
  a plain ElementName binding instead of the fragile PlacementTarget path.
- **ConfirmFixDialog was clipping its own buttons** ("two little half
  bubbles") after the backup-mode text was added and the window's fixed
  Height was shrunk in the same change - now `SizeToContent="Height"`
  instead of a hardcoded height, so it can't clip again regardless of how
  long a given message ends up being.

## 2026-08-29 (6) - Context menu fixes, album-wide fixes, theme selection color, backup/auto-close settings

- **Library Fixer's "Fix" column is gone** - right-click a row instead for
  the same fix actions (Embed Folder Art, Find Art/Metadata Online, Save
  Tag Edits), plus new **album-wide versions of each** ("...( Album)") that
  apply the same fix to every track in that row's album at once, including
  a new bulk "Save Tag Edits (Album)".
- **Selection highlight is no longer OS blue** - overrode the relevant
  SystemColors brushes app-wide to match the pink theme (affects every
  DataGrid/ListBox/TreeView selection, not just the Fixer's audit grid).
- **New Settings (Purity Gate tab)**: "Back up files before every Library
  Fixer write" (persisted preference - the per-action backup checkbox is
  gone, ConfirmFixDialog just states which mode is active) and
  "Automatically close a fix's progress window once it finishes with no
  errors" (a run with any failures always stays open so the log is still
  readable).
- Banner backdrop refined again: the flat 2-color gradient behind the
  height-capped banner is now a 6-stop vertical gradient sampled from the
  banner's own real sky-color transition, closer to "the sky continuing"
  than a flat block. A real fix (extending the actual banner.jpg canvas)
  is still pending - see currenttask.md for the math on why no single
  aspect ratio can be both "right at 1200px" and "right at 1920px+" with
  the current source art.

## 2026-08-29 (5) - Online lookup progress window, banner margin fix, picker re-search

- **Online art embed now shows real progress**: confirming a Deezer cover
  used to go silent while it downloaded and wrote files - now runs in the
  same progress window as the bulk folder-art embed (download step, then
  each track's embed).
- **Banner height cap no longer leaves bare void margins**: the MaxHeight
  cap added earlier today (to stop the banner ballooning on a maximized/
  wide window) left plain background showing on either side once the cap
  kicked in. The margins are now filled with a gradient sampled from the
  banner's own edge colors so they read as the image continuing rather
  than empty space.
- **"Choose Matching Album" picker can now re-search**: editable Artist/
  Album/Year fields (any may be blank) plus a Search button, so a wrong
  or empty match (e.g. a folder named "Abba - Abba (2022)" for a
  remastered reissue that doesn't text-match Deezer's plain "Abba") isn't
  a dead end. Leaving Album and Year blank with just Artist filled pulls
  that artist's full Deezer discography (up to 100 albums) to browse
  instead of a narrow keyword search. The picker now opens even when the
  initial search finds nothing, instead of a dead-end message box.

## 2026-08-29 (4) - Library Fixer: online (Deezer) core-tags lookup

- **New: online metadata lookup** for tracks missing core tags (Title,
  Artist, Album, Album Artist, Genre, Year, Track #) - a per-row "Find
  Metadata Online" button and a bulk "Fill All Missing Metadata Online"
  button, reusing the same Deezer album search + picker as the online art
  lookup (one search + picker per album, never an automatic pick). Chosen
  album's Genre/Year fill blank fields directly; Title/Track#/Disc# are
  matched per-track from the album's tracklist (by title text match, or
  track number as a fallback) and only fill blanks - fields left unmatched
  are left alone rather than guessed.
- Unlike the art lookup, this doesn't write to disk on its own - it only
  pre-fills the Library Fixer grid's already-editable fields. You still
  review the row and click the existing "Save Tag Edits" to write it,
  exactly like a manual edit.
- Also manually fixed `A Perfect Circle - Three Sixty\Disc 01\03 -
  Judith.flac` (the file corrupted during the art-lookup testing earlier
  today, since re-sourced) - its `ALBUM` tag read "Three Sixty Disc 1" and
  `GENRE` read "Alternative", both inconsistent with the other 9 tracks in
  the same folder ("Three Sixty" / "Alt. Rock"). Fixed via an
  ffmpeg metadata-only remux (no re-encode) with a `.bak` kept alongside.

## 2026-08-29 (3) - Library Fixer: broader folder-art embed + online (Deezer) art lookup

- **"Embed Folder Art" now also covers low-res and non-square embedded
  art**, not just tracks with no embedded art at all - any track whose
  audit raised an "EmbeddedArt" issue is now eligible whenever folder art
  exists to embed from. Previously it only offered this fix when a track
  had zero embedded art.
- **New: online cover-art lookup** for tracks with neither usable embedded
  art nor folder art to fall back on - a per-row "Find Online" button and
  a bulk "Find All Missing Art Online" button in the Library Fixer search
  Deezer's public album API (no API key needed) and show a picker
  (thumbnails + title/artist) so you always choose which cover to use,
  never an automatic pick. The bulk action walks album-by-album (one
  search + picker per distinct album, applied to every track in it at
  once) and has a "Stop (Cancel Remaining Albums)" button on the picker to
  bail out of the batch early. The chosen art is saved as `cover.jpg` in
  the album's folder (so future audits/fixes pick it up too) and embedded
  into every track in that album.
- Live-tested against the real library - see currenttask.md for the full
  writeup, including a real incident: force-killing the app mid-test
  landed a `taskkill` mid-write and corrupted one FLAC that had no backup
  (backup checkbox was off for that run). Everything else embedded
  cleanly. No code changes were made in response since this mid-write
  corruption risk is systemic to every existing TagLib.Save() path in the
  app, not specific to this feature.

## 2026-08-29 (2) - Banner crop fix

- The earlier same-day crop (1256x308 -> 1256x154) shrank the header banner
  too aggressively and clipped Trackula's bow tip and shoes. Re-cropped to
  1256x240 - still noticeably shorter than the original, but the bow, ears,
  wings, and note stay fully in frame and the "Trackula" text stays fully
  readable. Confirmed live that it still rescales smoothly with the window
  (already handled by the existing `Viewbox` wrapping the banner - no new
  wiring needed).

## 2026-08-29 - Source Library search/filter

- **Added a search box above the Source Library tree** that filters it in
  place as you type (2+ characters), matching artist name, album name, or
  track title/artist. Matching branches force-expand and everything else
  hides, without rebuilding the tree - scroll position and selection
  survive typing. A "✕" button clears the search.
- Added a shared `SourceTreeNodeViewModel` base (`IsVisible`/`IsExpanded`)
  used by the Artist/Album/Track node view models, and a lightweight
  `PlaceholderText` attached property (WPF has no built-in watermark
  support) for the search box's hint text.
- Turned on `TreeView` virtualization (Recycling mode) since a search match
  can now force-expand large parts of the tree at once.
- Cosmetic: shrank the top banner image's rendered height (308px -> 154px).

## 2026-08-22 - Fixed Plex/Last.fm provider bugs, added connection test UI

- **Plex "invalid request URI" fix**: `PlexListenDataProvider` now normalizes
  the configured server URL, prepending `http://` when the user enters just
  `host:port` (e.g. `127.0.0.1:32400`) with no scheme - previously this
  threw "the request URI must be an absolute URI or BaseAddress must be
  set" on every request, silently disabling Plex.
- **Fixed the real cause of the Last.fm 500-error storm**: `HoverCardService`
  fires listen-stats lookups per hover tooltip without awaiting them, and
  `LastFmListenDataProvider`/`PlexListenDataProvider`/
  `ListenBrainzListenDataProvider` all lazily cache their first fetch via a
  bare `??=`, which isn't concurrency-safe - fast hovering/scrolling across
  many tracks fired dozens of concurrent duplicate fetches before the first
  one could populate the cache, and Last.fm started rejecting the resulting
  request burst with 500s. All three providers now guard their lazy cache
  with a `SemaphoreSlim` double-checked lock so only one fetch is ever
  in flight.
- **Added "Test Connection" to the Providers settings tab**: a button per
  provider (Plex/Last.fm/ListenBrainz) that builds a throwaway provider from
  the current (possibly unsaved) textbox values and calls
  `IsAvailableAsync`, showing a pass/fail status inline instead of requiring
  a dig through the log file.
- Added a one-line explainer to the ListenBrainz section in Settings (free,
  MetaBrainz-run Last.fm alternative, no API key needed) since it wasn't
  obvious what it was.

## 2026-08-21 (15) - Actually closed the splash-to-MainWindow gap

- Every prior fix in this thread made the PREPARE work faster, but never
  addressed the actual structural gap: `App.xaml.cs` called
  `_mainWindow.Show()` only AFTER `splash.ShowDialog()` had already
  returned (i.e., after the splash's HWND was gone) - so no matter how fast
  prep got, there was still a window where `Show()`'s first layout/render
  pass ran with nothing on screen to cover it.
- **Fix**: `MainWindow.PrepareAsync()` now calls `Show()` itself, at the end,
  BEFORE returning control to the splash - and `App.xaml.cs` no longer calls
  `Show()` at all (removed the now-redundant, wrongly-ordered call).
  `Dispatcher.Yield(DispatcherPriority.Render)` right after `Show()` forces
  that first paint to actually complete before `PrepareAsync` returns,
  rather than merely being queued - so by the time the splash's `RunAsync`
  reaches its own `Close()`, MainWindow is already fully painted underneath
  it. Splash also gained `Topmost="True"` so `Show()`ing MainWindow
  mid-sequence can't pop it in front of the still-open splash and reveal the
  paint happening.

## 2026-08-21 (14) - Pre-warm the tray context menu's first-open JIT cost

- The tray menu's "only slow on the very first open, fine after that" was
  identified as normal WPF behavior, not a bug - the first `Popup`/
  `ContextMenu` shown in a .NET process pays a one-time JIT/render warm-up
  cost. Now paid proactively instead of on the user's first real right-click:
  `SetUpTrayIcon` opens and immediately closes the menu off-screen
  (`Placement.Absolute` at (-10000,-10000)) at `DispatcherPriority.ApplicationIdle`,
  so it happens once startup has settled rather than fighting the splash
  sequence for the UI thread. Placement/offset are restored to their originals
  afterward so real right-clicks position normally.

## 2026-08-21 (13) - Fixed a crash in the new Icon code; moved tree-build off the UI thread

- **Crash fix**: `ThemedWindow`'s new per-window `Icon` (added in (12)) used a
  short-form pack URI (`/Trackula;component/app.ico`) in code - that only
  auto-resolves inside XAML; in code it needs the full
  `pack://application:,,,/` prefix, and without it WPF tried to open it as a
  literal file path and crashed. Since this ran in a `static readonly` field
  initializer, the failure was a `TypeInitializationException` that
  permanently poisoned `ThemedWindow` (and therefore every secondary window)
  for the rest of the process. Fixed the URI and wrapped the loader in a
  try/catch that falls back to no icon rather than ever crashing again.
- **Cut the remaining splash-to-MainWindow delay further**: building the
  Source Library tree's ArtistNode/AlbumNode/TrackNode ViewModel graph
  (tens of thousands of objects for a big library) ran synchronously on the
  UI thread even after fixing the N+1 query in (12) - which also meant the
  splash's own fade/twinkle animations froze for that whole stretch, since
  it's all the same message pump. `MainViewModel.ApplyScannedTracksAsync`
  now builds that graph via `Task.Run` (the ViewModels are plain
  `ObservableObject`s with no thread affinity until attached to the
  UI-bound `SourceTree` collection, so this is safe) - only the final
  attach-to-collection step, and the (already cheap, single-query) sync-list/
  playlist rebuild, happen back on the UI thread.
- Tray icon context menu still reported as slow to open, even at rest -
  investigated and it's a fully static, pre-built `ContextMenu` (no DB
  calls, no dynamic content), so it doesn't match either bug found today.
  Left open pending more specific repro info (unclear if pre-existing).

## 2026-08-21 (12) - Fixed the real splash-to-window delay: an N+1 SQLite query

- **Root cause of the "big delay between splash closing and MainWindow
  opening"**: `MainViewModel.BuildSourceTree` called
  `SyncStateRepository.TryGetLastPurityPassed(track.SourcePath)` once per
  track to get its pass/fail badge - and that method opens a fresh SQLite
  connection and runs a query on every single call. For a ~19,000-track
  library that's ~19,000 individual DB round-trips, synchronously, on the
  UI thread, every time the tree rebuilds (startup, Refresh, and again after
  closing the Library Fixer). This also explains the sluggish tray icon
  context menu reported separately - it's the same UI thread, blocked by the
  same query storm, regardless of what's asking it to respond.
- **Fix**: new `SyncStateRepository.GetAllPurityPassedStatuses()` - one query
  for every track's last result, returned as a `Dictionary<string, bool>` -
  used instead of the per-track calls. `TryGetLastPurityPassed` itself is
  kept for genuine single-track lookups (still has other legitimate callers).
- Didn't need to add granular progress reporting through this phase (the
  original ask) - once it stopped doing 19,000 synchronous queries, it's
  fast enough that there's nothing to report progress on. Flagged this
  explicitly in case a real delay resurfaces elsewhere and needs revisiting.
- **Also fixed**: every secondary window (Library Fixer, bulk-embed
  progress, confirm dialogs, Settings, etc.) now gets an explicit `Icon`
  from the shared `ThemedWindow` base class, cached once as a frozen
  `BitmapImage`. Previously only `MainWindow` set one explicitly - when
  Windows picked a different window as the taskbar button's "representative"
  (e.g., while the bulk-embed progress window was open), it fell back to a
  generic icon instead of Trackula's.

## 2026-08-21 (11) - Safe cancel for bulk embed; fixed taskbar icon rendering

- **Cancel button on the bulk-embed progress window.** Safe by design: it
  only stops new files from starting - anything already in flight (up to
  `MaxConcurrentTagWrites` of them) finishes normally rather than being torn
  out of a half-written `TagLib.Save()`. Plumbed as a real
  `CancellationToken` into `Parallel.ForEachAsync` via
  `BulkFixProgressWindow`'s work delegate and
  `LibraryFixViewModel.EmbedAllAvailableFolderArtAsync`.
- **Fixed `app.ico` for the taskbar.** The earlier regeneration stored every
  size - including 16/32/48px - as PNG-compressed frames; some Windows shell
  icon-cache paths (notably taskbar buttons) are inconsistent with
  PNG-in-ICO at small sizes even though it's technically spec-legal.
  Rebuilt with the traditional format instead: raw 32bpp BMP/DIB (with AND
  mask) for every size except 256, which stays PNG (required at that size,
  and never what the taskbar actually renders). If it still looks off after
  updating, that's most likely Windows' icon cache holding onto the old one
  rather than the file itself - worth a taskbar unpin/re-pin or an Explorer
  restart to confirm.

## 2026-08-21 (10) - Configurable concurrent tag writes for bulk fixes

- **New Settings option** (Purity Gate tab): "Library Fixer - limit tag
  writes to this many files at a time" (`AppSettings.MaxConcurrentTagWrites`,
  default 2). "Embed All Available Folder Art" now writes up to that many
  files concurrently via `Parallel.ForEachAsync` instead of strictly one at
  a time - each write already runs off the UI thread (see (9)), so this is
  purely a speed/disk-load tradeoff; 1 keeps the old strictly-sequential
  behavior.
- `TrackFixItemViewModel.StatusText`'s setter now marshals onto the UI
  thread if called from elsewhere - with concurrency > 1, a write's
  `Task.Run` can genuinely complete on a thread-pool thread instead of
  resuming on the UI thread, and this is a WPF-bound property.

## 2026-08-21 (9) - Fixed the actual focus-flicker/hang: file writes blocking the UI thread

- **(7) and (8) weren't wrong, but weren't the whole story either.** After
  both, bulk embed still periodically froze the window long enough for
  Windows to mark it "Not Responding" and ghost it - which is what actually
  reads as random window/focus flicker (confirmed by polling
  `GetForegroundWindow()` during a real run: the title was alternating
  between "Trackula" and "Trackula (Not Responding)", not swapping between
  distinct windows).
- **Root cause**: `MetadataFixService.EmbedArt`/`UpdateCoreTags` ran
  synchronously on the UI thread inside the fixer's loop. Embedding NEW art
  into a FLAC (going from no picture to one) has no slack space to grow
  into - TagLib has to rewrite the *entire* file. For a large FLAC, or a
  disk/antivirus-contended moment, that single call can take several real
  seconds - and `Task.Yield()` between files (the loop's only concession to
  responsiveness) only yields *between* iterations; it does nothing for one
  file's I/O blocking the thread for its whole duration. Any one slow file
  froze the window long enough to get ghosted.
- **Fix**: the actual `TagLib` write now runs via `Task.Run(...)` and is
  awaited - `LibraryFixViewModel.EmbedFolderArtAsync`/`SaveTagEditsAsync`
  (and the bulk loop's internal `EmbedFolderArtCoreAsync`) all do this; only
  `item.StatusText` (a bound property) is touched after the await, back on
  the UI thread. Still strictly sequential file-by-file per the intended
  design - just non-blocking per file instead of blocking.

## 2026-08-21 (8) - Fixed the actual "Embed All" slowdown: log file reopened on every line

- **The resolver-storm fix (7) wasn't the whole story.** After it, bulk embed
  was still visibly grinding to a crawl - 15ms/file at the start, 12+
  seconds/file later in the same run. Root cause: `RollingFileLogService`
  called `File.AppendAllLines` on every single log line, which opens and
  closes the file from scratch each time. Every embed, every purity check,
  every resolver line logs at least once - across a day of heavy testing
  today's log file had grown to **100MB / 649,000 lines**, and re-opening a
  file that size on every line (worse if antivirus real-time-scans it on
  each open) gets slower as the file grows *within the same run* - which
  matches the observed timings exactly.
- **Fix**: `RollingFileLogService` now keeps one `StreamWriter` open for the
  whole day (opened once at startup, at whatever size the file already is)
  instead of reopening per line - `AutoFlush=true` so nothing's lost on a
  crash, `FileShare.ReadWrite` so "Open Log" can still read it live. Rolls to
  a fresh handle at midnight like before. Disposed on app exit.
- File size was never the actual problem once the handle stays open, so no
  need to clear today's existing 100MB log - it'll just keep growing (still
  pruned after 14 days) without the per-line reopen cost.

## 2026-08-21 (7) - Fixed "Embed All" hammering the PC via a self-triggered resolver storm

- **Root cause found**: every file the Library Fixer wrote tripped the
  Source Library's `FileSystemWatcher`, which (after its 3s debounce) kicked
  off a full background resolver run - real ffmpeg conversions, real CPU.
  With bulk-embed writing one file every fraction of a second, that meant
  dozens of resolver runs queuing up and competing with the fixer for the
  same files, while ffmpeg processes hammered the CPU - read by the user as
  "flicking between subwindows," "nails my whole PC," and an eventual apparent
  hang (stuck hand cursor from UI-thread starvation, not necessarily a true
  deadlock). Confirmed via the log: the last successful embed
  (`David Bowie - Fill Your Heart.flac`) completed and logged cleanly: the
  hang was the resolver's ffmpeg pipeline, not a half-written source file.
- **Fix**: new `AppServices.PauseAutoResolve()`/`ResumeAutoResolve()` -
  stops the file watcher (not just a flag) for the duration of any Library
  Fixer write. `EmbedFolderArt`/`SaveTagEdits` each pause/resume around their
  single write; `EmbedAllAvailableFolderArtAsync` pauses once for the whole
  batch (not per-file - toggling every iteration would just reopen the same
  race window every time) and resumes only once the batch finishes.
- Confirmed the bulk-embed loop was already doing the right thing otherwise -
  a snapshot list (`Items.Where(i => i.CanEmbedFolderArt).ToList()`)
  processed strictly sequentially with a status callback per file; that
  wasn't the bug.

## 2026-08-21 (6) - Splash now covers the whole startup, not just scan/audit

- **Fixed the "mystery hang" after the splash closes.** MainWindow used to be
  Show()n and only then build its Source Library tree/sync list/playlists on
  its Loaded event - for a large library that's real synchronous UI-thread
  work, so the splash would close and the freshly-shown MainWindow would just
  sit frozen for a beat with no explanation. `MainWindow` now exposes
  `PrepareAsync()` (the old Loaded handler's work, minus the Loaded coupling)
  and isn't `Show()`n until the splash has awaited it - the splash's status
  text now reads "Preparing your library view..." for this last stretch, and
  MainWindow only appears once it's fully interactive.
- **Removed the star decorations, then brought them back better**: the first
  pass reused Abigail's Media Renamer's pink ✦ placement almost verbatim,
  which didn't fit this palette and sat awkwardly near the text. Replaced
  with plain white twinkles tucked into the actual empty gaps in the
  background (above the mascot's head, in the space between the subtitle and
  the bottom bar) so nothing overlaps text or art, 7 of them now, twinkling
  roughly 2x faster than the first pass.

## 2026-08-21 (5) - Splash screen redesign: movable, black/red mascot card

- **Rewrote the splash screen** to match the Abigail's Media Renamer splash
  pattern the user actually wanted: borderless + `AllowsTransparency` card
  with real rounded corners and a red drop shadow, **draggable** anywhere on
  the card (`DragMove()` on left-click, no title bar at all), fade-in +
  pulsing star decorations.
- **Palette now matches the banner art** (black/deep-purple-to-red gradient)
  instead of the app's default pink theme, with a glowing white/red
  "Trackula" wordmark and a red-gradient progress bar - the earlier version's
  tiny 48px icon.png thumbnail is gone.
- **New `splash-mascot.png`**: cropped the vampire-girl character out of the
  user's `icon.png` (top ~650 of 840px, before the baked-in wordmark band)
  so the splash's hero art is the actual mascot at a real size, anchored
  bottom-right like AMR's Abby. The App project's redundant standalone copy
  of `icon.png` was removed (the repo-root copy stays as the source asset
  for `app.ico` and this crop).
- Verified visually this time - `PrintWindow` (not `CopyFromScreen`, which
  fails with "invalid handle" in this headless session) captures a live HWND
  directly and worked where full-desktop screen capture didn't.

## 2026-08-21 (4) - Borderless, rounded-corner window chrome across the app

- **Ditched the native title bar everywhere**, ported from the same pattern
  used in aMud: `Trackula.App.UI.ThemedWindow` (borderless `WindowStyle="None"`
  + `WindowChrome` + a custom pink-themed title bar template with
  minimize/maximize/close) is now the base class for every secondary window
  (Settings, the Library Fixer, the bulk-fix progress window, the smart-action
  review dialog, the confirm-fix dialog). `WindowChromeFix` (also ported from
  aMud) fixes the well-known borderless-window-overhangs-the-taskbar-when-
  maximized bug via a `WM_GETMINMAXINFO` hook.
- **MainWindow's banner doubles as its title bar** instead of using
  `ThemedWindow` - minimize/maximize/close float top-right over the banner
  art itself (`WindowChrome.IsHitTestVisibleInChrome`), dragged from the
  banner's top strip.
- **Real rounded corners, not a drawn approximation**: new `DwmRoundedCorners`
  calls `DwmSetWindowAttribute`/`DWMWA_WINDOW_CORNER_PREFERENCE` so Windows 11
  clips the whole borderless window to its native rounded shape (silently a
  no-op on older Windows - square corners there, not a crash).
- Verified end-to-end via UI Automation (no interactive desktop available for
  screenshots in this environment): launched the app, drove the new
  min/max/close buttons, confirmed maximize respects the taskbar work area
  (doesn't overhang), and confirmed the button/banner/toolbar layout renders
  at the expected screen coordinates.

## 2026-08-21 (3) - Splash screen; higher-res app icon; scaling banner; renamed exe

- **New splash screen** (`SplashScreenWindow`) runs the initial Source Library
  scan and a full library audit once at startup, with its own progress bar -
  MainWindow's first load and the first "Audit & Fix Library" open both reuse
  those results instantly instead of redoing the same scan/audit (one-shot
  cache on `AppServices`: `InitialScannedTracks`/`InitialAuditReport`, each
  consumed once via `InitialScanConsumedByMainWindow`/`InitialAuditConsumedByFixWindow`
  - every later Refresh/Re-run Audit still does a real pass). `LibraryAuditor`
  gained an overload that audits an already-scanned track list with
  per-track `IProgress<int>` reporting instead of re-scanning internally.
- **`app.ico` regenerated as a proper multi-resolution icon** (256/128/64/48/32/16,
  PNG-in-ICO) from the higher-res `icon.png` the user supplied, padded to
  square on a transparent canvas rather than stretched. Same icon.png is also
  embedded and shown as the splash screen's logo.
- **Header banner now scales with the window instead of cropping.** Wrapped in
  a `Viewbox` (`Stretch="Uniform"`) at the banner's native 1256x308 aspect
  ratio, so maximizing no longer "zooms in" on the art - the whole image
  stays visible and the banner's height scales with window width.
- **Renamed the built executable to `Trackula.exe`** (was `Trackula.App.exe`)
  via `<AssemblyName>Trackula</AssemblyName>` - the project file itself stays
  `Trackula.App.csproj`. Fixed up the two `/Trackula.App;component/...` pack
  URIs in the new splash screen that broke from the assembly rename (the tray
  icon's `pack://application:,,,/` form doesn't encode an assembly name, so
  it needed no change).

## 2026-08-21 (2) - Bulk-fix progress window; fixed tray-icon cross-thread crash

- **"Embed All Available Folder Art" now shows a progress window.** New
  `BulkFixProgressWindow` opens as soon as the bulk fix is confirmed: a
  progress bar, which file is currently being written, and a scrolling
  OK/FAILED log per track, driven by `LibraryFixViewModel.EmbedAllAvailableFolderArtAsync`
  reporting via `IProgress<BulkFixProgressUpdate>` and yielding between files
  so the window stays responsive across a big batch.
- **Fixed a real crash**: the background resolver (triggered by the Source
  Library FileSystemWatcher and the periodic timer, both thread-pool threads)
  was setting the tray icon's `ToolTipText` directly, which WPF forbids from
  off the UI thread (`InvalidOperationException` - "calling thread cannot
  access this object"). All tray tooltip updates now go through a
  `SetTrayTooltip` helper that marshals via `Dispatcher.Invoke`.

## 2026-08-21 - app.ico/banner.jpg branding, big action buttons, Audit & Fix Library

- **App icon and header banner.** `app.ico` is now the exe/window/tray icon
  (`ApplicationIcon` in the csproj, `Window.Icon`, and the tray `TaskbarIcon`'s
  `IconSource` all point at it). `banner.jpg` is the main window's header,
  full-bleed at 300px tall (`UniformToFill`, no overlay text).
- **Toolbar restyled into big action buttons.** Replaced the old `ToolBar`
  strip with a row of large squarish icon+label buttons (new
  `BigActionButtonStyle` in `TrackulaTheme.xaml`, ported from Abigail's Media
  Renamer's `TaskbarButtonStyle`) for Refresh / Run Resolver Now / Audit & Fix
  Library / Settings.
- **"Audit My Library" is now a real fixer, not just a report.** New
  `LibraryFixWindow` lists every track the audit flagged, with the issue text,
  editable core tags (Title/Artist/Album/Album Artist/Genre/Year/Track#/Disc#),
  and a one-click "Embed Folder Art" fix for tracks missing embedded art when
  a cover image (`cover`/`folder`/`front`/`album`, or the folder's only image)
  is sitting next to them (`FolderArtFinder`). A bulk "Embed All Available
  Folder Art" button does the same across every eligible track at once.
- **Deliberate, explicit write path for fixes.** `SourceLibraryGateway` stays
  read-only for everything else in the app; a new `SourceLibraryWriteGateway` +
  `MetadataFixService` are the only things that ever write to Source Library
  files, and only in response to a fix action. Every fix - single or bulk -
  shows a `ConfirmFixDialog` first ("this will modify your source files, are
  you sure?") with an optional "make a backup first" checkbox; backups land
  under `%AppData%\Trackula\backups\...`, timestamped, mirroring the library's
  relative path.

## 2026-08-20 - Review-before-you-commit dialog; song dedup; version filters; Remove All/Regenerate; hover cards

- **Every "Add ..." smart action now shows a review dialog before touching
  anything.** Rules no longer cut themselves off at N results - each
  `ISmartRule.EvaluateCandidatesAsync` returns the *full*, unbounded, ranked
  candidate list (`SmartActionCandidate`: one track, or one whole album's
  worth of tracks). `SmartActionService` is two-phase: `PrepareXAsync`
  evaluates and returns a `SmartActionProposal` (nothing persisted yet),
  and a new `SmartActionReviewWindow` shows the top-N proposal with a "Skip"
  button per row - skipping backfills from further down the same ranked
  pool (no rule re-run) or drops the row if the pool's exhausted. Only on
  "Add" does `SmartActionService.Commit` actually persist.
- **"Add Favorite Tracks"/"Add Hidden Gems Tracks" no longer put multiple
  versions of the same song in the playlist** (e.g. a studio cut, a live
  recording, and a "Best Of" compilation cut of the same song all showing
  up). New `TrackDeduplication.DeduplicateBySong` collapses same-song
  candidates (normalized Title+Artist) to the single best one (highest play
  count, tie-broken toward non-compilation/non-live) before ranking.
- **New Settings: "Avoid Live versions" / "Avoid Compilation versions"**
  (`AppSettings.AvoidLiveVersions`/`AvoidCompilationVersions`), applied as a
  pool-wide pre-filter in `SmartActionService.BuildContextAsync` so every
  "Add ..." action respects them uniformly. Live detection is a new
  `TrackVersionFilters.IsLikelyLiveVersion` heuristic (regex `\blive\b`
  against title/album - there's no reliable tag for this, unlike
  compilation which already has a real flag).
- **"Remove All in This Section" / "Regenerate This Section..."** added to
  the sync-list grid's right-click menu (keyed off the clicked row's rule),
  and **"Clear Selected Playlist" / "Regenerate Selected Playlist..."**
  added to the Playlists tab (regenerate only does anything for the two
  auto-generated playlists) - the "I don't like what got generated" escape
  hatches, both going through the same review dialog as the original
  "Add ..." action. New `SyncStateRepository.RemoveAllWithKind`.
- **Hover-card tooltips** - cover art, artist/album/title, and any
  available listen-provider stats (Plex/Last.fm/ListenBrainz, whichever are
  enabled) - now show up over tracks in the Source Library tree, the
  sync-list grid, and playlist tracks. New Core-side `EmbeddedArtReader`
  (on-demand TagLib picture read - Track itself doesn't carry picture bytes,
  that would bloat a whole-library scan's memory footprint) plus App-side
  `EmbeddedArtLoader` (decode + in-memory cache), `HoverCardService` (static,
  session-lifetime cache; listen stats fetched lazily and the provider
  reachability check itself is cached once per session, not once per hover),
  and `TrackToHoverCardConverter` wired as each control's `ToolTip`.

## 2026-08-20 - Track-level Favorite/Hidden Gems playlists; album-based Hidden Gems; sortable sync list grid

- **"Add Hidden Gems" now adds whole albums**, mirroring "Add Favorite
  Albums" instead of individual tracks. `HiddenGemsRule` was rewritten to
  score/rank whole albums (summed play count ascending, oldest-added
  tie-break, same genre-diversity round-robin option now bucketed by album).
  `AppSettings.HiddenGemsTrackCount` -> `HiddenGemsAlbumCount`.
- **New "Add Favorite Tracks" and "Add Hidden Gems Tracks" buttons** - the
  individual-track equivalents of Favorite Albums / Hidden Gems. Unlike the
  album actions, these don't add to the plain sync list - they materialize a
  reserved-ID playlist ("Favorite Tracks" / "Hidden Gems Tracks") through the
  existing fake-compilation trick, so Apple Music sees a real playlist. Each
  click fully replaces that playlist's tracks with the current top-N/bottom-N
  snapshot rather than accumulating duplicates. New `FavoriteTracksRule` and
  `HiddenGemsTracksRule` (the old track-level HiddenGemsRule logic, renamed);
  new `SmartActionService.PersistAsPlaylist` helper; new settings
  `FavoriteTracksCount` and `HiddenGemsTracksCount`.
- **"Your Sync List" is now a sortable DataGrid instead of a name-only
  TreeView** - columns for Title, Artist, Album, Album Artist, Genre, Year,
  Track #, Duration, Format, Reason, and Manual Pin, all bound straight to
  `Track` and sortable by clicking any column header (WPF DataGrid's
  built-in sort). Still grouped by reason (via a `CollectionViewSource`),
  still supports multi-select "Remove from Sync List".

## 2026-08-20 - Renamed to Trackula; manual sync list; multi-provider listen data; per-check Purity Gate toggles

- **Renamed the whole project BlackjackSync -> Trackula**: repo contents,
  namespaces (`BlackjackSync.Core`/`BlackjackSync.App` -> `Trackula.Core`/
  `Trackula.App`), solution/project file names (`BlackjackSync.slnx` ->
  `Trackula.slnx`, etc.), window titles, tray text, %AppData% folder name,
  log file prefix, and all docs. The containing folder on disk
  (`X:\repos\BlackjackSync`) could not be renamed from inside this session -
  the harness pins the working directory and Windows refuses to rename a
  directory that's any process's current directory - so that one rename is
  left for the user to do outside the session (or a fresh session started at
  a `Trackula`-named path).
- **Sync list is now entirely hand-built, not automatically curated.** The
  resolver no longer evaluates Smart Rules itself - it only ever
  materializes whatever's already in the user's sync selection (manual
  pins/drags + user playlists). New Releases / Top Most Played / Hidden Gems
  became one-shot "Add ..." buttons (`SmartActionService`) that run once,
  on click, and add whatever they find to the persisted selection
  (`SyncSelection` table, replacing the old `ManualPin` table) - nothing
  re-evaluates or auto-evicts on its own afterward. The GUI's bottom pane
  ("Your Sync List") now reads directly from that selection so additions
  show up immediately, before the next resolver run has materialized
  anything.
- **Listen-data providers can now all be enabled simultaneously** (Plex AND
  Last.fm AND ListenBrainz at once) instead of picking one "active"
  provider, mirroring how Abigail's Media Renamer's metadata-provider list
  works. `ListenDataAggregator`/`ListenDataAggregatorFactory` replace
  `ListenDataProviderResolver`: every enabled/configured/reachable provider
  is queried per track and their results are merged (play counts summed,
  most-recent last-played kept); the Heuristic provider is always the
  automatic fallback for tracks none of them have data on, not a toggle.
- **Purity Gate gained real per-check toggles** (`PurityCheckToggles` +
  `AppSettings.EnabledPurityChecks`), not just the strict/warn-only mode -
  each of the eight checks (core tags, embedded art, UTF-8 cleanliness,
  album spelling consistency, track/disc numbering, compilation flag,
  converted duration match, converted-file-decodes-cleanly) can be switched
  off individually from Settings.

## 2026-08-20 - Persistent library cache, scan progress UI, and visual theme

- **Persistent track cache** (`LibraryTrackCache` table + `LibraryTrackCacheRepository`):
  scanning the Source Library was re-reading every file's tags via TagLib#
  on every single app launch, which doesn't scale to a real library. Now
  `LibraryScanner` checks a SQLite-backed cache (keyed by source path +
  mtime, same pattern as the Conversion Cache) before touching TagLib# at
  all - unchanged files are skipped entirely, and stale entries for deleted/
  moved files are pruned automatically each scan.
- **Scan progress reporting**: `LibraryScanner.ScanAllWithProgress` reports
  `LibraryScanProgress` (files scanned / total, current folder, cache-hit
  count) via `IProgress<T>` as it walks the library; `MainViewModel` surfaces
  this as bindable `ScanProgressPercent` / `ScanCurrentFolder` / `ScanCountText`
  properties.
- **Visual theme**: ported the pink/rose theme + animated "sparkly bubble"
  ProgressBar template from Abigail's Media Renamer's `AbigailTheme.xaml`
  into `Themes/TrackulaTheme.xaml` (rounded buttons/textboxes/tabs/
  groupboxes, the same gradient+bubble progress bar used there for file-copy
  progress, now reused for library-scan progress) and merged it app-wide via
  `App.xaml`. `MainWindow` got a header banner, themed toolbar/status bar,
  and a live scan-progress panel (progress bar + "which folder it's looking
  at" text) above the Source Library tree, visible only while a scan is
  running.

## 2026-08-20 - Initial build

Built Trackula from scratch per the design spec: a Windows app that
maintains a curated, metadata-verified, always-on-device mirror of a subset
of a personal music library for Apple Devices app's auto-sync to pick up.

- Scaffolded the solution: `Trackula.Core` (class library, all logic),
  `Trackula.App` (WPF GUI + tray), `Trackula.Tests` (xUnit).
  Targets **.NET 10** (`net10.0` / `net10.0-windows`) - switched mid-build
  from the spec's originally-stated .NET 8 at the user's explicit request;
  global Claude instructions were updated to default new C# projects to
  .NET 10 going forward.
- **Source Library**: `SourceLibraryGateway` is the sole read-only gateway to
  the user's real music folder (defense-in-depth path guard included);
  `TrackTagReader`/`LibraryScanner` read tags via TagLib# through a
  read-only `IFileAbstraction` so the tag-reading path can never write back
  to a source file; `SourceLibraryMonitor` wraps `FileSystemWatcher` with
  debouncing.
- **Conversion Cache**: `ConversionCache` (SQLite-indexed by source path +
  mtime, temp-file-then-rename writes, orphan pruning) + `FfmpegAudioConverter`
  (shells out to ffmpeg, clear `FfmpegNotFoundException` on a missing binary)
  implementing the format conversion matrix from the spec (MP3/AAC
  passthrough, lossless->ALAC, lossy->AAC at a configurable bitrate).
- **Purity Gate**: `PurityGate` runs core-tag-presence, embedded-art
  size/aspect, UTF-8/mojibake, album-spelling-consistency,
  track/disc-numbering, compilation-flag, converted-duration-match, and an
  actual headless ffmpeg decode-pass check (`FfmpegDecodeValidator`, not just
  an exit-code trust). Hard-blocks by default with a warn-only toggle in
  Settings; `LibraryAuditor` reuses the same checks for a standalone,
  read-only "Audit my library" mode.
- **Listen-data providers**: `IListenDataProvider` + `HeuristicListenDataProvider`
  (zero-config default, proxies off Trackula's own sync history),
  `PlexListenDataProvider` (plain Plex API session history, no Plex Pass
  needed), `LastFmListenDataProvider`, `ListenBrainzListenDataProvider`.
  `ListenDataProviderResolver` always falls back to Heuristic when the
  user's chosen provider is unconfigured or unreachable.
- **Smart Rules**: `NewReleasesRule`, `TopNMostPlayedRule`, `HiddenGemsRule`
  (genre-diversity-weighted round-robin selection), wired through
  `SmartRuleEngine`; manual pins and user playlists are handled directly by
  the resolver since they don't depend on rule evaluation.
- **Active Sync Set Resolver**: `ActiveSyncSetResolver` orchestrates
  scan -> rule evaluation -> conversion -> Purity Gate -> storage-budget trim
  (`StorageBudgetTrimmer`, priority order Hidden Gems -> Most Played -> New
  Releases -> playlists -> manual pins, every eviction logged) -> diff
  against the actual sync folder contents and materialize/remove files.
- **Playlists**: `PlaylistTagStamper` implements the "fake compilation"
  trick - stamps Album/Album Artist/Compilation/Track#/Year on a disposable
  sync-facing copy only, never touching the Source Library or the
  Conversion Cache master copy; playlist edits just regenerate the stamped
  copy rather than patching incrementally, per spec.
- **GUI**: WPF dual-pane `MainWindow` (read-only Source Library tree on top
  with purity/sync-status badges; Active Sync Set + Playlists tabs on the
  bottom), drag-and-drop from source tree to pins/playlists, a `SettingsWindow`
  covering every tunable from the spec, and a tray icon (Hardcodet
  NotifyIcon.Wpf) with idle/converting status text and the full right-click
  menu (run resolver now, open log, run audit, pause/resume watcher,
  settings, exit). Closing the main window hides it rather than exiting -
  only the tray's Exit does that.
- Rolling daily log file (`RollingFileLogService`) used throughout for the
  "log everything" requirement (files detected, conversions
  started/succeeded/failed with reason, purity results, sync-set diffs).
- Added an initial Core unit test suite (15 tests): Purity Gate standalone
  checks, storage-budget trim priority ordering (including the
  multi-reason-survives-partial-trim case), and Source Library read-only
  enforcement (including a reflection-based guard against ever adding a
  write/delete/move method to the gateway).
- `README.md` covers prerequisites, build/run instructions, first-run setup,
  provider configuration, and pointing Apple Devices app's auto-sync at the
  sync folder.

Not yet done / left for a follow-up session: end-to-end manual verification
against a real ffmpeg binary and a real Source Library (no live audio files
were available in this environment to test conversion/purity end-to-end);
the GUI has not been run/screenshotted, only built.
