Changelog
Pulled straight from the project's
CHANGELOG.md.
The format follows
Keep a Changelog;
versions follow SemVer.
Changelog
All notable changes to Bòcan are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
1.11.0 (2026-06-21)
Added podcast support to Bòcan, including subscriptions, episode downloads, transcripts, show notes, and playback. The Podcasts sidebar shows subscribed shows with unread counts and artwork, and the search sheet lets you find new shows via iTunes Search and Podcast Index. Each show has a detail sheet with a list of episodes, and you can download episodes for offline listening or stream them directly. The Now Playing view now supports podcasts with chapter markers and skip intervals.
### Added
- app: wire AppPodcastActions, PodcastService conformance, FeedRefreshScheduler (d676046)
- app: wire AppPodcastResolver into QueuePlayer (4c5b3a8)
- app: wire Podcast Index credentials from Secrets.xcconfig (2ad8ea1)
- persistence: add podcast persistence layer (M023) (157ee92)
- persistence: query episode state by download state (5c54bf1)
- playback: add PlayableSource.podcast case (2753fe7)
- playback: add podcast skip-interval remote commands (4014714)
- playback: play podcast episodes via PodcastEpisodeResolving (90310e3)
- playback: podcast Now Playing media type (d105008)
- podcasts: “Support this show” funding link with confirmation (b592e01)
- podcasts: add manual and bulk episode downloads, wire auto-download (4f781c4)
- podcasts: add Mark All as Played to the podcasts toolbar (b8c8d40)
- podcasts: add markPlayed/markUnplayed by podcastID to PodcastService (86c27b3)
- podcasts: add PodcastIndexAuth SHA-1 request signer (f862735)
- podcasts: add PodcastIndexClient and ITunesSearchClient actors (c68af4d)
- podcasts: add PodcastSearchResult, PodcastSearchSource, PodcastIndexCredentials types (7f4b7cf)
- podcasts: add PodcastSearchService with dual-index fan-out and merge (569b304)
- podcasts: auto-download newest episodes on refresh (df53aeb)
- podcasts: chapters list and live current-chapter in Now Playing (938b4ae)
- podcasts: download storage management and stale-file handling (470ef43)
- podcasts: DownloadStore on-disk layout for episode downloads (e158785)
- podcasts: episode transcripts (fetch, cache, view, self-clean) (23674fa)
- podcasts: EpisodeDownloadManager with queue, progress, pause/resume (3b84cf2)
- podcasts: make chapters discoverable in the list and show notes (3d14703)
- podcasts: OPML subscription import and export (c2d5a9e)
- podcasts: parse and store podcast:person credits (data layer) (2f90828)
- podcasts: parse podcast:funding and podcast:chapters via a namespace supplement (bcdad5b)
- podcasts: per-show speed, sort, retention, and show type (264f09e)
- podcasts: phase 21-2 feed fetch and RSS/Atom parsing (6484b64)
- podcasts: phase 21-4 – PodcastService facade, artwork cache, refresh scheduler (5fcf70e)
- podcasts: show episode count in search results and subscribed grid (80bffd6)
- podcasts: surface podcast:person credits on shows and episodes (bc63549)
- podcasts: unread count badges and grid Mark all as played (b00ad68)
- podcasts: upgrade FeedKit 9.1.2 to 10.4.0 (fead155)
- podcasts: wire Podcast Index API credentials via Secrets.xcconfig (369a4cd)
- ui: add EpisodeList table with filter, context menu, and show notes (6158f46)
- ui: add EpisodeStatusIndicator with status derivation and ProgressRing (7e8e5aa)
- ui: add isPodcast mode and skip methods to NowPlayingViewModel (91e29e8)
- ui: add Podcasts settings pane (68dffc6)
- ui: add ShowNotesView with HTML rendering and per-guid cache (e90943a)
- ui: add Website and RSS Feed links to podcast detail header (06171fd)
- ui: extract music transport and add podcast transport controls to NowPlayingStrip (e9df427)
- ui: L10n keys and pseudolocale for podcast controls (a7419c0)
- ui: phase 21-7 Podcasts sidebar, seam protocols, VM, and views (ddb53ee)
- ui: phase 21-8 podcast search results, source badges, and detail sheet (7c76410)
- ui: replace PodcastShowView stub with header and episode list (e6f4901)
### Fixed
- app: suppress retroactive conformance warning with @retroactive (6ea05e0)
- audio: send our User-Agent when FFmpeg opens HTTP streams (444d074)
- audio: stop progress drifting ahead after repeated pause/resume (adce682)
- ensure all tests a run with make tests (4a9ecff)
- podcasts: add a visible Done button to transcript and show-notes sheets (8c9f400)
- podcasts: make the auto-download toggle work and clarify its control (740d914)
- podcasts: move artwork to Application Support and add Get Info to grid context menu (4aecd7a)
- podcasts: OPML import sheet renders empty and collapsed (df9c341)
- podcasts: parse feeds with an xml-stylesheet PI before the root (a288f12)
- podcasts: raise cover-art download cap from 5 MB to 15 MB (3ce0e0a)
- podcasts: raise feed size cap from 15 MB to 50 MB (536cc04)
- podcasts: stop refreshes from wiping cached cover art (e4af2ed)
- podcasts: widen transcript window and roomier episode list columns (973eaa4)
- ui: add VoiceOver names to the three unlabeled icon-only controls (bb2f56c)
- ui: center the now-playing scrubber between transport and divider (86721d4)
- ui: give the speed and lyrics-offset slider popovers proper VoiceOver values (d6eb094)
- ui: inject CSS into show notes HTML for legible system-font rendering (de0174a)
- ui: stop the podcast Auto-Download checkbox label truncating (509a5a2)
- ui: widen podcast detail sheet to 780pt min-width (15f1332)
- ui: widen show notes sheet to 500pt min-width to reduce line wrapping (abc3710)
### Changed
- observability: unify HTTP User-Agent across modules (86036b5)
- ui: make mini-player transport podcast-aware (3ec2395)
1.10.0 (2026-06-14)
Bòcan 1.10 moves the entire visualizer engine onto Metal, so every frame is drawn on the GPU and stays buttery smooth even on the busiest passages. Six visualizers now ship in the box: classic Spectrum Bars and Oscilloscope alongside four new ones, the scrolling Cascade spectrogram, the beat-rippling Halo, a frequency-warped Starfield, and the slow-burn Nebula. Cycle modes and color palettes right inside the visualizer, and let the music paint the window.
### Added
- ui: add Cascade scrolling spectrogram visualizer (5f82082)
- ui: add Halo radial spectrum visualizer with beat ripples (90b1d55)
- ui: add in-visualizer mode and palette steppers (a059283)
- ui: add MetalVisualizer protocol and runtime shader library (51de5ee)
- ui: add MTKView host and route VisualizerHost through the Metal factory (3147731)
- ui: add Nebula visualizer on Metal (phases 12.5, 12.12) (5f94710)
- ui: add OnsetEnvelope, PolylineRibbon, and FrameRing helpers (9b46325)
- ui: add Starfield visualizer (frequency-coloured warp field) (1cc84ab)
- ui: add trackpad haptics for love, rating, seek, volume, and end of queue (c3d581c), closes #330
- ui: cross-fade the mini player and main window swap (5126e08), closes #330
- ui: derive Recent Scrobbles filter segments from history (428b57d)
- ui: raise spectrum bar height so loud passages reach the top (870d891)
- ui: render Cascade through Metal (phase 12.8) (951168a)
- ui: render Halo through Metal (phase 12.10) (50add38)
- ui: render Spectrum Bars through Metal (phase 12.9) (160a38c)
- ui: render Starfield through Metal (phase 12.11) (47958d7)
- ui: render the Oscilloscope through Metal (phase 12.7) (639d12b)
- ui: soften spectrum bar display gain to 1.1 (4a4c9f9)
- ui: sync Up Next queue with live manual playlist edits (e67df5e)
- ui: visualizer Analysis v2, PaletteResolver, and Drift/Thermal palettes (68dab85)
### Fixed
- app: clear stale recent-scrobbles sheet flag at launch (929ccea)
- audio: rebuild the pump when resuming after a paused seek (6bb3b2f)
- audio: reschedule the pump in place on seek for a seamless jump (50ff84f)
- audio: serialize transport ops to stop the silent-playback race (b5eefbd)
- observability: scrub Subsonic and Last.fm wire-format auth params (fbb5770)
- observability: suppress unused-result warning in _removeSubscriber (ef5199f)
- scrobble: roll up queue row when ignored resolves last submission (3fd78ac)
- ui: centre the compact mini-player transport and scrubber (8c934b6)
- ui: clarify Recent Scrobbles empty-state copy (4dde406)
- ui: give each mini-player layout a fixed window size (1da8b49)
- ui: harden mini-player swap against a stale main-window reference (d72098e)
- ui: keep the mini player resizable, snap to size on layout switch (cd11783)
- ui: make the mini-player layout snap stick by dropping frame autosave (3582c6e)
- ui: mark SubsonicSongDrag.payloads(from:NSDraggingInfo) @MainActor (185bc30)
- ui: move the mini-player visualizer steppers to the top-left (d0c9e6f)
- ui: render Nebula at native resolution to stop the live collapse (7761232)
- ui: show Subsonic submission status in Recent Scrobbles (50115b2)
- ui: silence WindowFade strict-concurrency warnings in the fade completion (916613f)
- ui: start Recent Scrobbles observation from any presenting surface (8bbc60f)
- ui: step Metal spectrum peak markers on the analysis cadence (e1aa563)
- ui: stop the audio-tap publish storm starving the Metal draw loop (9ba48a3)
- ui: stop the Metal watchdog from auto-simplifying a fast renderer (3c898ee)
### Changed
- ui: drop Halo conic gradient that triggered FPS auto-simplify (1ce0a11)
- ui: extract ColorPacking and PaletteRampLUT from Cascade (fc4dc47)
1.9.0 (2026-06-09)
### Added
- ui: add en-XA pseudolocale and expansion checks (#314) (1f87af6)
- ui: localize always-visible chrome and first-run surfaces (#314) (994f867)
- ui: localize cross-module display strings (#314) (093ceed)
- ui: localize Fingerprint, Tools, Visualizers, PlaylistIO and Import (#314) (1aa138b)
- ui: localize playlist sidebar, rows, sheets and detail views (#314) (5a9ffb0)
- ui: localize remaining surfaces and enforce the guard module-wide (#314) (7f11b81)
- ui: localize the Browse area including AppKit table cells (#314) (6b6f58c)
- ui: localize the Diagnostics, Advanced and remaining settings panes (#314) (5ea14b3)
- ui: localize the DSP panes (#314) (fc4082c)
- ui: localize the General, Appearance, Playback and Library settings panes (#314) (45d153a)
- ui: localize the Lyrics pane, editor and settings (#314) (68457f5)
- ui: localize the metadata editor (#314) (69480cf)
- ui: localize the Scrobble surfaces (#314) (2d30aa4)
- ui: localize the smart-playlist editor and presets (#314) (0339658)
- ui: localize the Sources (Subsonic) settings pane (#314) (c78b4fa)
- ui: localize view-model copy (#314) (19e2e4a)
### Fixed
- playback: drop stale sleep-timer ticks after fire or reschedule (a6008a6)
- ui: regenerate en-XA pseudolocale after Xcode catalog re-sync (bab6699)
- ui: stop post-scan reload clobbering scoped detail views with the full library (266087f)
1.8.0 (2026-06-08)
### Added
- ui: pin now-playing track to top of Up Next (a1557b4)
### Fixed
- ui: give the playlist-row track-drop highlight room to breathe (7f903a1)
- ui: group Remove from Playlist under Add to Playlist (9548b66)
- ui: stop the scan overlay covering non-Songs views on launch (9f08a9b)
1.7.1 (2026-06-02)
### Fixed
- scrobble: store tokens in the login Keychain, not data-protection (57505bc)
1.7.0 (2026-06-02)
This has been a hairy one. I’ve removed the Airplay feature as it didn’t work, and to make it work, I’d have had to entirely re-write the playback engine. You’ll have to just rely on Bluetooth I guess. On a more positive note, I’ve added a Logging Console under Help if you’d like to see the internals of how the app runs. Also fixed showing album art via the Dock for any Subsonic clients you may use.
### Added
- cast: remove AirPlay / output-device routing entirely (7678a86)
- observability: complete the in-app log console (phase 20) (9a4c2d9)
### Fixed
- subsonic: store credentials in the data-protection keychain (a30e876)
- subsonic: store credentials in the login Keychain, not data-protection (508329e)
- ui: show album art in Dock for Subsonic playback (0b6d2b7)
1.6.0 (2026-06-01)
This release is heavily laden with fixes and minor improvements after a running a complex audit against the codebase. Of note is a redesigned settings panel.
### Added
- ui: add Will-o’-the-Wisp signature accent identity (ac2f5c9), closes #333
- ui: drag streamed Subsonic songs into the Up Next queue (#332) (a466ffd)
- ui: group Settings into sections, always show Sources, add a robust deep-link router (#305) (3111271)
- ui: lay localization foundation and localize the Albums grid (#314) (68d3dab)
- ui: Subsonic song tables follow the now-playing track (fa00923)
- website: add Homebrew install snippet to homepage Get it section (14094a8)
### Fixed
- acoustics: check cancellation after the rate-limiter wait (2d7e685), closes #273
- acoustics: move AcoustID API key from URL to POST body (85c6aef), closes #282
- acoustics: propagate cancellation through RateLimiter.wait() (e36c93a), closes #272
- acoustics: validate fpcalc path for file URL and NUL bytes (fd602fa)
- audio: break retain cycles keeping engine and pump alive across track loads (89b081b), closes #261
- audio: dispatch BufferPump onEnded without the extra @MainActor hop (655fccb), closes #262
- audio: document and harden FFmpeg partial-alloc cleanup (9d7bc45), closes #295
- audio: restrict FFmpeg to network protocols for remote inputs (137dc59), closes #280
- audio: use checkCancellation in SubsonicStreamCache download loop (b746b34), closes #263
- library: propagate cancellation into LibraryScanner.scan() (ee338eb), closes #266
- library: propagate cancellation into the FileWalker directory walk (45fd207), closes #265
- metadata: harden tag decoding against non-UTF-8 bytes (246d4cc), closes #259
- metadata: honor empty coverArt array to clear embedded art on write (89da436)
- metadata: log temp-file cleanup failures; guard large-art SHA-256 hash (58283c0), closes #317
- metadata: strip UTF-8 BOM before parsing LRC files (f4b59d3)
- observability: log silently-swallowed errors; drop @preconcurrency; add subsonic to standards (7579b08), closes #315
- observability: omit diagnostic payload JSON from OS log (c5362ef), closes #284
- persistence: escape LIKE metacharacters in albumsByArtistQuery (06dd26d)
- persistence: extract M016 migration struct; log silently-swallowed errors (f14a601), closes #316
- persistence: surface WAL pragma failure as a warning instead of swallowing it (ee0b5db)
- playback: clamp crossfade-out delay before the UInt64 cast (0ece82b), closes #271
- playback: guard SleepTimer against a double stop (7ed7724), closes #270
- playback: keep correct currentIndex when removing queue items (c919c3c), closes #257
- playback: make the crossfade fade-in task cancellable (84aad0d), closes #269
- scrobble: cancel reachability task on stop(); inject clock into observeStats (de6f3dd)
- scrobble: prevent double-submit when success confirmation write fails (1755a86), closes #292
- scrobble: show Subsonic-sourced rows in recent-scrobbles list (0322097), closes #291
- scrobble: strip query params from Last.fm auth URL before logging (0b6d4b4), closes #283
- security: harden security posture across multiple low-risk items (8b3f889), closes #286
- subsonic: add kSecAttrAccessible to Keychain update attributes (b581780), closes #285
- subsonic: manage the connection-monitor wake observer lifecycle (f7ac9c4), closes #274
- subsonic: replace try? with do/catch + log.warning at all capability and Keychain sites (f0430f2), closes #318
- ui,library: pin DateFormatter to en_US_POSIX locale; use Gregorian calendar for day-bucket math (6682581)
- ui: a11y polish – reduce-motion fallback, adjustable sliders, speed-picker focus ring (79d8218)
- ui: add a persistent Add Folder affordance to the Local Library sidebar header (#308) (1f71ff8)
- ui: add a View menu for the presentation toggles (#303) (0f8aa3b)
- ui: add accentColor/bgPrimary pairs to ContrastAudit and ContrastTests (8c0512e), closes #326
- ui: add accessibilityLabel to ShuffleCheckCell checkbox (ca7e3e1)
- ui: add dark-appearance variant to AccentColor for WCAG AA contrast (6399d4f)
- ui: cancel NowPlayingViewModel observation tasks in deinit (4525a93), closes #279
- ui: confirm before clearing a non-trivial playback queue (c1b7085), closes #260
- ui: confirm before removing albums or artists from the library (d916ace), closes #258
- ui: honor differentiateWithoutColor for transport toggles and Subsonic status dots (40c88e1)
- ui: let tracks be dragged out to Finder (1cfdeb1)
- ui: live-update Plays and Love in the Songs table (0e90f52)
- ui: log empty catch blocks; add WKWebView justification; remove stale comment (935f909), closes #319
- ui: make the empty Sources sidebar state a tappable ‘Add a Server’ CTA (#309) (36cb2db)
- ui: make the sidebar server Remove… a real confirm-delete (#306) (94cf3f7)
- ui: make transport and mini-player fonts honor Dynamic Type (5dea822)
- ui: persist the selected sidebar destination as the user navigates (14b309b)
- ui: prefix now-playing row label with 'Now playing, ’ for VoiceOver (20c165f)
- ui: promote status/affordance colours to semantic tokens (bd7837d), closes #327
- ui: restore Show/Hide Sidebar and converge on the system View menu (#304) (b5fb213)
- ui: reveal a reorder grip on Up Next rows (ba802aa)
- ui: smooth first-run by deferring consent, dimming idle transport, and improving the URL prompt (0643548)
- ui: use lovedTint token for love heart in strip and table cell (906bd38), closes #325
### Changed
- app: open the database off the launch path behind a loading shell (899009a), closes #276
- audio: shrink BufferPump in-flight window back to 4 buffers (bc16593), closes #277
- library: bound the on-disk cover-art cache with an LRU sweep (51fb7c5), closes #268
- library: cut redundant per-file syscalls in the scan import (fc47ba7), closes #278
- library: stream walk output into the import TaskGroup (7b3b384), closes #267
- ui: downsample cover art and bound the artwork cache (da0fafb), closes #275
1.5.0 (2026-05-27)
### Added
- subsonic: play internet radio stations + add info popover (0ec5ad8)
- subsonic: probe legacy-core endpoints to detect Radio/Podcasts/Bookmarks (12bc1b9)
- subsonic: rebuild search + add artist/album drill-down (3d54eea)
- ui: add Hidden Sources submenu for one-click sidebar re-enable (fe4eafc)
- ui: add hidden-by-default MBID column to the Songs table (63c39a2)
- ui: show Recording + Album MBIDs in the Get Info sheet (read-only) (740a913)
- website: add Open Graph / Twitter Card preview image (f477b70)
### Fixed
- audio: wait for full Subsonic download + sniff format extension (61b50b4)
- persistence: broaden local Album search to include track-title hits (66c8c71)
- subsonic: refresh capabilities on launch so the legacy-core probe runs (41d0621)
- subsonic: stable random shuffle for the Songs view (3579449)
1.4.0 (2026-05-25)
This release adds Subsonic / Navidrome / Airsonic servers as first-class sources alongside your local library. Federated search across every server, per-server status dots, offline banners with one-tap retry, and ⌘⇧1–⌘⇧9 to jump straight to a server.
### Added
- audio-engine: add SubsonicStreamCache and RemoteTrackLoader (7820de2)
- playback: introduce PlayableSource on QueueItem with v1->v2 migration (74934d6)
- playback: wire Subsonic stream resolver into QueuePlayer (af25e52)
- scrobble: Subsonic scrobble write-through (Phase 19 step 15) (7cb7681)
- subsonic: capability-driven sidebar refresh (Phase 19 step 16) (f11d6ed)
- subsonic: hydrate browse VMs from metadata cache; prune stale entries on launch (bd72c7b)
- subsonic: polish — status dots, offline banner, ⌘⇧1-9 shortcuts (Phase 19 step 17) (85eb86d)
- subsonic: scaffold Subsonic module with Navidrome/Subsonic client (7d02127)
- ui: add + button to Sources sidebar header to open Settings → Sources (dfbf47e)
- ui: add album art column to songs table and NSTableView-based Subsonic songs view (a94ebfc)
- ui: add optional per-server Subsonic destinations (2d5f1f8)
- ui: add per-server Subsonic browse views (7b9f859)
- ui: add Settings → Sources tab for Subsonic servers (b23576d)
- ui: add SidebarDestination.subsonicRoot(UUID) case (bced272), closes #242
- ui: add spec-mandated Sources sidebar context menus (2cb9fb2)
- ui: default row density to spacious; hide playlist menu indicator (dea7b2f)
- ui: federated Subsonic search panel with per-server cards (6a56482)
- ui: rename Library to Local Library, add Sources sidebar section (a6e222d)
- ui: show Subsonic cover art in NowPlaying (5a8faac)
- ui: sort local genres by track count descending (1d97dd8)
- ui: Subsonic star/rating annotation write-through (82aa01c)
### Fixed
- library: populate Album.year from track tags during import (e53362e)
- playback: add version field to QueuePayloadV2 for forward-compatibility (1c9ded7)
- subsonic: revoke capability flags on 404/501 and rebuild sidebar (c32ed30), closes #250
- subsonic: set ASCII client name to avoid URL-encoded display (581839a)
- subsonic: wire NWPathMonitor into stream cache and connection monitor (83317a0), closes #241
- ui: batch AlbumsViewModel published assignments to prevent year/count flash (f76ac3a)
- ui: constrain Subsonic album cover art to square via overlay layout (c24dc92)
- ui: drop unnecessary await on synchronous handleFailure (2df2142)
- ui: eliminate startup race causing spurious ‘No server with id’ alert (5ac4d8e)
- ui: keep Sources tab from overflowing Settings sidebar (a1d0113)
- ui: show Subsonic track metadata in NowPlaying (8cddfa2)
- ui: use spec wording for self-signed TLS warning (0182cd7)
### Changed
- ui: rewrite Settings as sidebar navigation (3255ec1)
- ui: split SubsonicSongTable into three files to fix lint violations (6a32263)
1.3.1 (2026-05-24)
### Fixed
- playback: register PlaybackQueue.changes subscribers synchronously (eaf7bb1)
- playback: skip and disable missing-file tracks during auto-advance (bcfa726)
- ui: high-contrast inverted pill for mini-player chrome buttons (311e255)
- ui: multicast PlaybackQueue.changes so all subscribers receive events (2a4c16d)
1.3.0 (2026-05-21)
### Added
- ui: add visualizer mini-player mode and toolbar pane toggles (02facaa)
### Fixed
- library: expand FSEvent directory events and suppress watcher reloads during scan (f27849b)
- library: fix two test failures in LibraryScannerTests (9e152ea)
- library: handle FSEvent deletions and prune missing files on full scan (ed701b9)
- playback: filter excludedFromShuffle tracks when building a new shuffle queue (18eadc0)
- playback: restore session resumes shuffled queue instead of replacing it (1a2002d)
- ui: allow FSEvents reloads during scan in scheduleWatcherReload (ad4b15b)
- ui: remove redundant @EnvironmentObject libraryEnv from TracksView (9ead8d5)
- ui: remove superfluous swiftlint disable in NowPlayingStrip (4d79861)
- ui: update scanCurrentPath during the processing phase of a scan (4bf3a89)
1.2.1 (2026-05-20)
### Fixed
- library: expand FSEvent directory events and suppress watcher reloads during scan (f27849b)
- library: fix two test failures in LibraryScannerTests (9e152ea)
- library: handle FSEvent deletions and prune missing files on full scan (ed701b9)
- playback: filter excludedFromShuffle tracks when building a new shuffle queue (18eadc0)
- playback: restore session resumes shuffled queue instead of replacing it (1a2002d)
- ui: allow FSEvents reloads during scan in scheduleWatcherReload (ad4b15b)
- ui: remove redundant @EnvironmentObject libraryEnv from TracksView (9ead8d5)
- ui: switch case newline style in RecentScrobblesView (dd54311)
- ui: update scanCurrentPath during the processing phase of a scan (4bf3a89)
1.2.0 (2026-05-18)
### Added
- scrobble: register Rocksky provider and wire up UI (f329956)
- scrobble: switch Rocksky to ListenBrainz-compatible API (c9ca221)
### Fixed
- playback: wire up periodic scrobble threshold check (556a438)
- scrobble: simplify Rocksky connect UI to API key only (72b9d00)
- ui: add Rocksky to Recent Scrobbles view and fix layout (30a7768)
1.1.0 (2026-05-16)
### Added
- build: re-establish macOS 15 Sequoia compatibility
- audio: add MP2/MP1, AC-3, DTS, WMA, Wave64, RF64, Matroska, AU support (5850066)
### Fixed
- lint: wrap long FFmpeg format string, extract sync-word checks to reduce cyclomatic complexity (480e5de)
1.0.1 (2026-05-15)
### Fixed
- ui: debounce FSEvents watcher reloads to prevent cascade (8563099)
- ui: pre-mark year as edited when file stores a full date string (b0f14db)
- ui: reduce spectrum bars top padding from 12 to 4 points (74bce4c)
1.0.0 (2026-05-13)
⚠ BREAKING CHANGES
- release v1.0.0
### Added
- release v1.0.0 (ef27d43)
- ui: add read-only track info floating panel (353b7e5)
- ui: complete keyboard focus phase — settings nav, sheet focus restore, tests (71045b7)
- ui: Dynamic Type support — semantic fonts, @ScaledMetric grid, NSTableView cell fonts (4467185)
- ui: keyboard focus — album grid arrow nav, transport focusSection, scrubber a11yValue (5d85ca3)
- ui: replace Help Viewer with in-app Help and Notices windows (3ca7a80)
- ui: respect Reduce Motion — freeze visualiser, instant track transitions (c1d9eb7)
- ui: respect Reduce Transparency — solid backgrounds for mini-player, strip, lyrics (0e7bbb4)
- ui: VoiceOver support — row labels, live track announcements, combined album cells (7eb709c)
- ui: warm light-mode backgrounds from cold white to cream/linen (0faa95e)
- ui: WCAG AA colour contrast audit and token adjustments (8d4ec14)
- ui: wire native Help Book — move to Bocan.help bundle, replace GitHub fallback (b82acec)
### Fixed
- ci: guard Sparkle steps, point DMG at export/ directly (8e25bac)
- metadata: use stable taglib opt symlink instead of versioned Cellar path (5662434)
- playback: lower activate() Task priority to .default to silence GRDB QoS inversion warning (2e5be75)
- test: tighten VisualizerViewModel performance toast timing (305084d)
- ui: consolidate toggleLovedForNowPlaying into Rating extension (3255859)
- ui: fix DSP settings layout — use safeAreaInset for segmented picker header (030dca1)
- ui: move DSP section picker into toolbar principal slot (f4e60b6)
- ui: push fullscreen visualizer overlay below traffic-light buttons (33daa34)
- ui: remove focusable TabView causing blue focus ring; increase Settings minHeight to 415 (88c4c6e)
- ui: remove grey toolbar bar from mini-player window (46a5fca)
- ui: remove iOS min-touch-target frames from playbar — restores icon density (c0cd107)
- ui: render About Third-Party Notices as HTML; extract shared NoticesHTMLView (6310289)
- ui: render Notices & Licences window as HTML — headings, links, bold (def4690)
- ui: resolve Swift 6 concurrency errors in AirPlayButton.Coordinator (eed641f)
- ui: restore original transport icon sizes — 24pt play, 18pt prev/next, 15pt secondary (09be29c)
- ui: silence IUO coercion warning — explicitly unwrap NSApp in accessibility post (94f7f43)
- ui: silence swiftlint violations — file/type length, force_unwrap, multiline_arguments (e39a5c0)
- ui: split DSP & EQ into three separate Settings tabs (a81132f)
0.12.0 (2026-05-12)
### Added
- ui: add album context menu in Artist view (play, gapless, shuffle, get info, remove) (a2557ce)
- ui: add Composer, BPM, Key, Bit Depth, Channels, Lossless, Skips, Last Played, File Size, Date Modified columns (hidden by default) (4d565a7)
- ui: add Disc and Discs columns to track table (hidden by default) (a47e29b)
- ui: add song counts to Artists list and detail section headers (9aab293)
### Fixed
- audio: correct gapless position bar by tracking per-track sample offset (2b47ceb)
- ci: split AudioEngine to pass file_length lint; fix ArtistsViewModel syntax (b5a638c)
- ui: make DSP settings pane scrollable so all sections are accessible (d74a59f)
- ui: preserve search query when drilling into album/artist detail (135e663)
- ui: replace nested TabView in DSPSettingsView with segmented picker (9b95b40)
- ui: show compilation albums/songs for track artists in Artists view (09c4af3)
0.11.0 (2026-05-11)
### Added
- ui: show song count on album cells in ArtistDetailView (18881ef)
### Fixed
- ui: Edit Lyrics context menu now edits the right-clicked track (f6b7135)
- ui: preserve search after delete, prune orphan albums/artists, add Remove context menus (5bea2fc)
0.10.0 (2026-05-11)
### Added
- scrobble,ui: wire loved toggle to scrobble services + add love button to play bar (db454d5)
- ui: add ♥ Loved column to track list (on by default) (e4ab248)
### Fixed
- sparkle: remove channel tag from stable appcast entries (37fec24)
0.9.4 (2026-05-11)
### Fixed
- ui: add missing Foundation import in LibraryViewModel+Delete.swift (f17b10b)
- ui: batch multi-select Delete from Disk with single DB reload (14c2a54)
### Changed
- ui: extract delete methods to LibraryViewModel+Delete.swift (57842bd)
0.9.3 (2026-05-10)
### Fixed
- audio: call AudioUnitReset before zeroing EQ band gains in reset() (fa6783b)
0.9.2 (2026-05-10)
### Fixed
- ci: trigger website redeploy via workflow_run instead of gh workflow run (eafbec1)
- release: strip 300dpi metadata from DMG background, stage only .app into DMG (0cb754a)
0.9.1 (2026-05-10)
### Fixed
- ci: checkout actual tagged commit on workflow_dispatch, not main HEAD (3c8e074)
- ci: switch to main before pushing appcast — detached HEAD at tag caused rejection (e8e57b4)
- release: fix doubled edSignature in appcast, trigger website redeploy after push (1ee7c82)
- release: make appcast update unconditional — was gated on SPARKLE_ED_PRIVATE_KEY (d12f5df)
0.9.0 (2026-05-10)
### Added
- app: crash recovery via LaunchSanity sentinel and recovery banner (#208) (866e9ca)
- app: single-instance enforcement via lock file and distributed notification (#207) (1b6de51)
- distribution: add PrivacyInfo.xcprivacy privacy manifest (#211) (e3fc5e8)
- distribution: add Sparkle EdDSA public key and SUFeedURL to Info.plist (56c0377), closes #219
- distribution: branded DMG background and volume icon (#212) (b7044eb)
- distribution: deploy appcast.xml to bocan.app on release (#216) (9bdd554)
- ui: add third-party credits and Notices & Licences menu item (#210) (f626da3)
- ui: wire About window and Check for Updates button (#206) (e86a115)
- updates: integrate Sparkle 2 — dependency, UpdateController, menu item (87fce62), closes #205
### Fixed
- distribution: use CURRENT_PROJECT_VERSION for CFBundleVersion (1c74b93), closes #217
- lint: resolve force-unwrap and line-length violations (512a649)
- lint: shorten fatalError message to survive swiftformat collapse (3afda99)
- observability: crash reporter — consent, disk writes, path redaction, report viewer (#209) (0f4f93b)
- ui: inject toastDismissalDuration to eliminate 6-second flaky test (dcb5e78)
- updates: wire Sparkle product to Bocan target in project.yml (237f713)
0.8.0 (2026-05-10)
### Added
- playlist import/export fixes, routing teardown, dock menu, and Phase 16 audit (ed8129b)
- ui: add ‘Choose Audio Output…’ menu item with ⌘⇧U shortcut (#202) (5893fe5)
- ui: dock right-click menu, play/pause badge, and album-art preference (06a14a2)
- ui: route dropped playlist files to importer instead of scanner (257df06), closes #188
### Fixed
- app: cleanly shut down routing subsystem on app termination (d5cc1ff)
- audio-engine: attach EQUnit node to AVAudioEngine in tests (9207b3e)
- ci: use workflow_dispatch tag input for GitHub Release tag_name (560243a)
- library,persistence: add step 3 filename-only fallback to TrackResolver (33b2599), closes #196
- library,playback: wire CUE sheet import and honour start/end offsets (868ad25), closes #192
- library,ui: populate matched/missed counts in import preview (e25e881), closes #194
- playback: properly store and remove CoreAudio HAL listener blocks (#200) (54191e4)
- ui: add accessibility labels and help tooltips to import/export sheets (d28ab2f), closes #197
- ui: localize ActiveRouteChip strings via xcstrings (1e24c89)
- ui: replace runModal() with async panel.begin in import/export sheets (dfea923), closes #187
0.7.0 (2026-05-09)
### Added
- scrobble: add pending indicator to transport strip (#175) (38b6dd1)
- scrobble: add Show Recent Scrobbles menu item and keyboard shortcut (#176) (f56c484)
- scrobble: implement RecentScrobblesView (#174) (5a120c3)
- ui: add drag-to-resize handle to VisualizerPane (#168) (5c57be3)
- ui: add now-playing overlay to visualizer pane and fullscreen (#169) (fea3627)
- ui: auto-simplify visualizer mode on sustained FPS drop (#172) (15134b1)
- ui: multi-display screen picker for fullscreen visualizer (#171) (94f9947)
- ui: remove Fluid Metal visualizer (fe50923)
- ui: show lyrics source badge in pane header (eadc958)
### Fixed
- audio: bypass EQ at Flat preset; skip redundant ramp tasks (d38dc5d)
- audio: bypass TimePitch at unity rate by default; add pump starvation logging (75b9770)
- audio: eliminate CoreAudio render-thread pops + community health files (978c0ad)
- audio: eliminate render-thread overhead and IIR pop sources (7bade91)
- audio: ensure each spectrum bar reads unique FFT bins (902c9df)
- audio: increase I/O buffer size to 1024 frames for pop resilience (1b398e6)
- audio: suppress file_length lint violation in AudioEngine.swift (f751bfb)
- library: pass resolved album title to LRClib fetch (218f01a)
- playback: honour startingAt when shuffle is enabled (a317121)
- playback: strip BookmarkBlob from queue persistence payload to prevent audio pops (255d7a8)
- ui: add .help() tooltips and .accessibilityHint to ScrobbleSettingsView (105363d)
- ui: add .help() tooltips and full accessibility labels to font size picker (d5381c1)
- ui: add accessibility labels and tooltips to ConnectSheet (bd43b60)
- ui: add lyrics actions to context menu and menu bar (c26c692)
- ui: add lyrics sync-offset slider to pane header (f3d878f)
- ui: add manual LRClib fetch and replace-lyrics buttons (a99e3db)
- ui: correct Edit Lyrics tooltip shortcut hint in pane header (a89c3cd)
- ui: detect LRC format when saving editor lyrics (7f1da9e)
- ui: fix Fluid Metal particle physics — correct bass direction, add ambient turbulence (9432d9c)
- ui: fix visualizer disconnect after track changes and size flutter (c352c6e)
- ui: make Fluid Metal audio reactivity direct and eliminate 30s burst (84c1ed2)
- ui: observe lyricsVM and visualizerVM in BocanCommands so menu labels update (7f5cf71)
- ui: ref-count tap so closing fullscreen doesn’t disconnect pane audio (1ecf01a)
- ui: remove ignoresSafeArea from VisualizerHost black fill (e03d212)
- ui: require confirmation before deleting lyrics in editor sheet (f131e9e)
- ui: show spinner instead of empty state while fetching lyrics (a48449f)
- ui: split lyrics pane header into two rows, add drag-to-resize handle (9b4fc61)
- ui: stop 60fps menu rebuilds from causing audio pops (902c9df)
- ui: stop Fluid Metal particles when no audio is playing (642ccbb)
- ui: unify lyrics font-size AppStorage key (6927e13)
- ui: use IOKit power source for battery detection in VisualizerViewModel (#170) (befa8c3)
- ui: wire audio analysis into Fluid Metal renderer each display tick (050daa3), closes #167
- ui: wire lyrics pane search bar to LyricsView filtering (ad01607)
### Changed
- audio: split AudioEngine.swift into extension files to fix file_length lint (d6942b8)
0.6.0 (2026-05-07)
### Added
- app: prompt before quit when scan or RG analysis is active (321ceb4)
- persistence,ui: add local backup with configurable rolling count (4e0c879)
- persistence,ui: wire iCloud backup toggle into Advanced Settings (2a15615)
- Phase 10 polish — mini player, quit guard, iCloud & local backups (0c5d06e)
- ui: add collapse/expand toggle to Playlists sidebar section (d6ef432)
- ui: add LoadingState and ErrorState reusable views (#145) (812feda)
- ui: implement MarqueeText scrolling for Mini Player and menu-bar extra (1e5e36f), closes #138
- ui: show scanning progress pane during initial library scan (854a18e)
- ui: spring-animate mini player layout transitions (ecbed96)
### Fixed
- audio: prevent pops from VFS contention and CPU bursts at playback start (c12c1ce)
- persistence: use requiresWriteAccess to avoid WAL snapshot deadlock (c6fda5e)
- ui: call windowMode.restoreIfNeeded() on launch to honour restore-last-mode setting (dc2a3f1), closes #139
- ui: convert HighContrastModifier comments to doc comments for SwiftFormat (5b36fdf)
- ui: inject libraryViewModel into DSP window; remove About from Settings tabs (6187626)
- ui: menu bar extra icon reflects playback state (7a311fd), closes #143
- ui: strengthen separators and materials under accessibilityIncreaseContrast (#141) (66b4f0e)
0.5.1 (2026-05-06)
### Fixed
- build: add stable Homebrew HEADER_SEARCH_PATHS to project.yml (a5099da)
- persistence: use .async(onQueue:main) scheduler to fix GRDB writer-queue deadlock (a2a8575)
0.5.0 (2026-05-06)
### Added
- ui: add accessibilityIdentifier to all NowPlayingStrip controls (2c20f4b)
- ui: add Compute Missing ReplayGain to Tools menu (8b43c9f)
- ui: add keyboard shortcuts for volume control (⌘↑/⌘↓) (346178b)
- ui: add mute button to transport bar (⌘⌥Z) (cc46904)
- ui: add Play Album, Shuffle Album, Play Artist to context menu and Track menu bar (8451bbf)
- ui: add Play Now, Play Next, Add to Queue to Track menu bar (9a8cc0d)
- ui: add Playback Speed menu and keyboard shortcuts (9f1aebb)
- ui: add Rate submenu to track context menu; remove dead ContextMenus.swift (2d28b80)
- ui: add Select All (⌘A) and Deselect All (⌘⇧A) to Track menu and table (e449c60)
- ui: add Sleep Timer submenu to Playback menu bar (4a54e16)
- ui: make artwork in NowPlayingStrip navigate to current album (05a2ef6)
- ui: make track title and artist clickable in NowPlayingStrip (23e2a5c)
- ui: previous button restarts track after 3 seconds (iTunes semantics) (ff79723)
- ui: replace DSP modal sheet with non-modal floating window (3d1650e)
### Fixed
- ci: redirect codesign stderr so hardened runtime grep works (cf03508)
- persistence: remove spurious await from DatabaseWriter.backup call (c43e521)
- persistence: set GRDB targetQueue to .userInitiated to prevent priority inversion (43ff5b2)
- scrobble: remove spurious await from synchronous authorisationURL call (1204638)
- ui: add VoiceOver accessibility labels to track table (b665a18)
- ui: copy action now includes all visible track fields as TSV (#98) (ceff30c)
- ui: DSP button shows persistent active state when EQ is processing (cd159cc)
- ui: fix love context-menu label for multi-track selection (2918bf1)
- ui: fix Swift 6 concurrency errors in MainWindowTracker.Coordinator (603fbaa)
- ui: migrate NowPlayingViewModel from ObservableObject to @Observable (#113) (e12e380)
- ui: migrate RouteViewModel to @Observable, remove @ObservedObject from usage sites (9d2fb83)
- ui: migrate TracksViewModel to @Observable to eliminate publish-during-update warnings (b385a2b)
- ui: persist playback rate to UserDefaults and restore on launch (636602c)
- ui: remove duplicate fade-out toggle from custom sleep timer popover (5e07473)
- ui: remove redundant as? URL cast in ArtworkEditor drop handler (c88d72b)
- ui: replace NSAlert.runModal() with non-blocking beginSheetModal continuations (1909661)
- ui: silence nonisolated(unsafe) warning on RouteViewModel.consumer (693dcf8)
- ui: use textTertiary for idle speed label instead of 0.4 opacity (84caa5d)
- ui: wire Love/Unlove context menu to toggleLovedForCurrentSelection (07e25f5)
- ui: wire Return/Enter key to play in track table (9ebed66)
0.4.0 (2026-05-04)
### Added
- tests: add DSPViewModel environment to NowPlayingStrip snapshots (4f2c448)
- ui: add ⌘⌥E keyboard shortcut and menu item for EQ/DSP panel (#94) (37d314b)
- ui: implement per-track and per-album EQ scope picker (#91) (7a1da10)
### Fixed
- audio: flush EQ IIR delay lines before un-bypass to prevent pop (#92) (5e82403)
- ui: A/B compare is press-and-hold, not a toggle (#93) (fe2379d)
- ui: add full EQ/Effects/ReplayGain tabs to DSP Settings view (#89) (3bf54a4)
- ui: eliminate ‘publishing during view update’ warnings in EQ scope picker (#95) (7b41aeb)
- ui: wire EQ output gain slider to preset mutation (#90) (8c18761)
0.3.0 (2026-05-04)
### Added
- playback: wire CrossfadeScheduler end-to-end (#87) (7a4f5ce)
- settings: add embed cover art preference (phase-8 audit H5) (08312f1), closes #67
- ui: add Bulk Actions section to multi-track editor (phase-8 audit H6) (0d987b1), closes #69
- ui: add CommandMenu(“Tools”) with batch cover art and duplicate finder (ea16b84), closes #68
- ui: add Compute Replay Gain to Track menu and right-click context menu (#88) (0f99e39)
- ui: add explicit Tab-key focus order to tag editor Details tab (#80) (768dc7d)
- ui: add File Info and Advanced tabs to tag editor (phase-8 audit) (ce942d0), closes #66
- ui: add Identify Track toolbar button (#83) (cf69fab)
- ui: add per-field apply-checkboxes for multi-track tag editing (cc9817c), closes #70
- ui: detect LRC timestamps in lyrics tab, save as synced lyrics (#74) (145a6a8)
- ui: show conflict-resolution banner in TagEditorSheet (#73) (2bde72e)
### Fixed
- app: declare playlist-drag UTType in Info.plist, fix conflict log level (3085d67)
- audio: ramp bass-boost gain/bypass to prevent audio pop (#86) (e4bb57f)
- dsp: improve EQ bypass transitions to prevent audible pops (f09e2d8)
- library: auto-renew stale security-scoped bookmarks on resolution (317f61c)
- library: hasCoverArt smart rule checks albums.cover_art_hash not tracks (e8ab684)
- library: stamp fileMtime/fileSize after tag write to prevent false-positive conflict (2340b1e)
- library: upgrade http CAA image URLs to https to satisfy ATS (73856a0)
- playback: fire-and-forget nowPlaying to unblock 15s playback delay (206cd24)
- ui,library: folder-not-found flash + recurring startup conflicts (5583df3)
- ui,library: properly fix folder-not-found flash and conflict re-flagging (ce4767e)
- ui,persistence: crash on playlist with duplicate track entries (9a0fdb9)
- ui: acquire security-scoped resource in .fileImporter completion (#78) (fa44a2f)
- ui: add .accessibilityLabel to TextField in TagFieldRow and IntFieldRow (#77) (bb091d7)
- ui: add .help() to CandidatePickerView buttons (#83) (394d099)
- ui: add .help() to IdentifyTrackSheet Close button (#83) (43e6a65)
- ui: add .help() tooltip to all ArtworkEditor action buttons (#82) (04d8816)
- ui: add low-confidence warning banner in CandidatePickerView (#83) (bb33629)
- ui: Edit Tags button in noMatchView opens tag editor (#83) (07730de)
- ui: enhance PlaylistSidebarViewModel to handle missing nodes gracefully (f09e2d8)
- ui: guard fieldBinding setter to prevent publish-during-render fault in lyrics tab (05881ce)
- ui: observe sidebar VM in ContentPane so isLoaded triggers re-render (90f6a34)
- ui: refactor TrackTable to simplify scroll view creation (f09e2d8)
- ui: remove duplicate ⌘I shortcut from context menu Get Info button (#81) (d9e2f9b)
- ui: replace DispatchQueue.main.async with Task in ArtworkEditor.handleDrop (a7b59c3), closes #71
- ui: replace NSOpenPanel.runModal() with .fileImporter() in ArtworkEditor (#76) (b7990ed)
- ui: streamline TrackTableCoordinator’s data handling (f09e2d8)
### Changed
0.2.0 (2026-05-01)
### Added
- acoustics: implement phase 8.5 AcoustID fingerprinting & MusicBrainz auto-tagging (2228b28)
- albums: artist + track count + exclude-from-shuffle toggle (b621b3b)
- app: add BocanApp entry point, RootView, resources, and UI test scaffold (3514516)
- app: expand Playback menu with Next/Previous/Shuffle/Repeat/Stop-After-Current/Clear Queue/Up Next (6fdbdab)
- app: phase-2 audit fixes #5 + #6 — vacuum on quit, iCloud backup at launch (c005e75)
- app: wire ⌘⇧N to File ▸ New Playlist… (1132639), closes #31
- audio: add AudioEngine module and integrate into project structure (511b965)
- audio: add AudioEngine module with AVFoundation and FFmpeg decoders (14b98aa)
- audio: implement Phase 9 DSP chain — EQ, crossfeed, stereo expander, limiter, ReplayGain (1f09183)
- docs: add post-phase checks for debugging and feature completeness (6432d16)
- enable column-header sorting on full Songs library (75ba115)
- import: add media to library — folder picker, file picker, drag-drop, scan banner (037aa46)
- library,ui: sort playlist contents by title / artist / date added (76122c9)
- library: add hasLyrics smart-playlist field (5ccac49)
- library: add inLastYears smart-playlist comparator (3b59d85)
- library: add Library scanning module (FileWalker, ChangeDetector, TrackImporter, FSWatcher, ScanCoordinator, LibraryScanner) (026d823)
- library: add PlaylistService with sparse-position reorder + folders (6204bcc)
- library: cap smart-playlist group nesting at 3 levels (133eebb)
- library: debounce smart playlist observation storms (a40fc7b)
- library: forbid smart-playlist refs from in_playlist rules (94712cd)
- library: graceful decode of unknown smart-playlist fields (a91f345)
- library: implement Phase 7 smart playlists (b7ffcca)
- library: live FSEvents watcher + move sources to Settings (54e68ed)
- library: Phase 5.5 – add/remove/rescan media, Track Inspector, force gapless per album (f796f30)
- library: scaffold playlist import/export (M3U, PLS, XSPF, CUE, iTunes XML) (12adfd5)
- library: snapshot mode for non-live smart playlists (ea56a01), closes #48
- lyrics: Phase 11 — lyrics display, editing, and LRClib fetch (fdc3b20)
- metadata: add TagLibBridge ObjC++ wrapper and Metadata Swift module (TagReader, ReplayGain, LRCParser, CoverArtExtractor) (7f3db4f)
- metadata: implement full metadata editor (phase 08) (09d5b41)
- metadata: preserve raw year/date tag text (cc4e5ed)
- mini-player: add shuffle toggle to all three layouts (0fd3263)
- mini-player: add track info button and album to compact/square layouts (11d27b7)
- mini-player: match main player order; add repeat & stop-after toggles (80807f4)
- observability: add AppLogger, LogCategory, Redaction, Telemetry, MetricKitListener (bd38754)
- persistence: add M012 scrobble dead-letter, unique queue index, submissions table (f746327)
- persistence: add M013 CUE virtual-track columns (bdfaae1)
- persistence: add Persistence module with SQLite/GRDB schema, repositories, FTS, and observation (418d965)
- persistence: add playlist kind + accent_color (M007) (1ddae2b)
- persistence: expose FTS search and smart-folder queries on repositories (dc8d65b)
- persistence: M002 migration — library_roots table and Track phase-3 fields (70567b1)
- playback: add Playback module — queue, shuffle, repeat, gapless, history, persistence (07d406e)
- playback: add Route, RouteManager, and CoreAudio output-device observer (d2f06e1)
- playback: expose ScrobbleSink hook from PlayHistoryRecorder + QueuePlayer (a1ea1ca)
- playback: persist and restore playback position across launches (f0dfa32)
- playback: Phase 5 – stop-after-current, playAlbum/playArtist, context menus, NowPlayingTests, gapless fixtures (1dfc97b)
- playlists: add smart reshuffle seed and creation-flow parity (9ccd43a)
- queue: show proper title/artist/genre in Up Next; fix percent-encoded filenames (53f9d41)
- scrobble: add Scrobble module with rules, providers, queue worker, service (389b2e3)
- scrobble: send now-playing on track start (4a9b42a)
- settings: add Smart Playlists preferences section (5f7a003)
- settings: add VSCode settings for terminal usage (9f9c78f)
- smart-playlists: implement true snapshot mode with refresh timestamp (7ae77a0)
- tracks: improved column layout, default sort, and column persistence (2f8bc9e)
- ui: add AirPlay route picker to now-playing strip (6cdd9ff)
- ui: add Sample Rate column to Songs table (a683b8d)
- ui: add UI module with library browser, search, and now-playing strip (6cd1194)
- ui: animated bouncing-bars now-playing indicator in QueueView (02c1ce6)
- ui: confirmation alerts before Remove from Library / Move to Trash (Phase 5 audit) (3795557)
- ui: fire track-change notifications when app is in background (4554240)
- ui: implement Phase 10 — mini player, speed control, sleep timer, settings window (7e32d1c)
- ui: manual playlist sidebar, detail view, and creation sheets (c8bac16)
- ui: mini player window toggling + menu bar extra wiring (69b68bc)
- ui: move scan progress to Library settings (9ff67e9)
- ui: new Songs table columns and Go-to context menu (5f3764b)
- ui: per-field opt-in selection for AcoustID identify sheet (5d6b86a)
- ui: persist expanded playlist folders in UIStateV2 (10e92b1)
- ui: persist Songs table column customization across launches (c968afc)
- ui: Phase 10 polish + fix @AppStorage startup freeze (5629308)
- ui: playlist cover art mosaic, user cover, and accent colour (d164107)
- ui: playlist drag-and-drop reparent to folder (05073be)
- ui: playlist import/export sheets and menu commands (da10c4a)
- ui: playlist picker for smart-playlist membership rules (bfe35cc), closes #49
- ui: polish playlist and smart-playlist creation flows (7d43a1b)
- ui: redesign Get Info window (f85d0a2)
- ui: richer Up Next row context menu (6fdbdab)
- ui: route playlist folders to dedicated PlaylistFolderView (a5cd060)
- ui: scrobble settings, connect sheet, app wiring (eb9a1a4)
- ui: toast on Re-scan success, error sheet on failure (Phase 5.5 audit M2) (8551904)
- ui: update app name and copyright in Info.plist; add settings.local.json for permissions (8a9a0c1)
- ui: wire drag-reorder for manual playlist tracks (cef2b9b), closes #30
- ui: wire QueuePlayer — Up Next view, transport controls, context menus (6b788ad)
- ui: wire up SleepTimerMenu Custom… button (e334587)
- use bundled AppIcon.icns for the app icon (a13abfb)
- visualizer: add fullscreen triggers — pane button + ⌘⇧F menu item (3834313)
- visualizer: implement Phase 12 — audio visualizer with Metal particle system (83d25ad)
### Fixed
- acoustics: bundle libchromaprint and add Homebrew sandbox exception (ebab225)
- acoustics: correct capitalized(with:) label in titleCased helper (d084814)
- acoustics: patch fpcalc rpath to resolve libchromaprint from Homebrew (e6dc546)
- acoustics: self-contained fpcalc bundle + AcoustID bug fixes (edd9885)
- acoustics: wire fpcalc binary, AcoustID key, and Secrets.xcconfig into build (40d3d28)
- app: declare Local Network usage and Bonjour services for AirPlay (cd4f57e)
- app: raise Task.detached priority to .userInitiated to prevent startup freeze (2c671f5)
- app: resolve MainActor deadlock and Swift 6 Sendability in app init (7fa5c85)
- app: stop CPU runaway and restore tracks view (ee108a3)
- audio: add userInitiated executor to FFmpegDecoder (9794760)
- audio: anti-pop fades, stereo-layout helper, insertion-point protocol (0926ea5)
- audio: convert AVFoundationDecoder to actor at userInitiated QoS (0cd4c7c)
- audio: don’t crash on negative frame delta in AVFoundationDecoder (7656e70)
- audio: fix ffError lint violations — optional_data_string_conversion, trailing_closure, file_length (d03ad2a)
- audio: guard empty sample input in EBUR128 and ReplayGain analyzer (deca1b2)
- audio: lower AudioEngine actor QoS to default to avoid priority inversions (a355dae)
- audio: move playerNode reconnection before engine.prepare() (ac205cc)
- audio: reconnect full playerNode→eq→mixer chain at hardware rate (2d9eee9)
- audio: reconnect playerNode at hardware rate after engine.prepare() (89d95a6)
- audio: resample decoded buffers to hardware rate in BufferPump (5c662d9)
- audio: resolve AVAudioFormat Sendable error and deprecated String(cString:) warning (500ee34)
- audio: resolve engine-not-running, pump deadlock, and resume judder (01bc19a)
- audio: return Bool from Task closures in AudioTapTests for Swift 6 Sendable (448a23e)
- audio: silence Swift 6 Sendable-capture warning in FormatConverter (97d3bf6)
- build errors in TrackTable + coordinator for Swift 6 strict concurrency (639dbc4)
- build: bundle Resources into app, declare Library dependency (1cf639d)
- build: rewrite @rpath refs to @loader_path in bundle-fpcalc script (b48fec9)
- build: run xcodegen generate after bundle-fpcalc (1cc07cd)
- column-header sort in NSTableView TrackTable (d43e2b3)
- docs: update minimum macOS version to 26.0 (Tahoe) (d122566)
- dsp: equaliser band sliders now persist and update the engine (ef51f4c)
- import: resolve scan infinite spinner, @retroactive conformances, nonisolated warning (f3194ae)
- library: allow tag editing for files added as individual roots (32c8d49)
- library: canonicalize symlinks via realpath in quick-scan seed (26fe1d8)
- library: disabled tracks disappear from FTS search; Remove From Library preserves search (1ebc627)
- library: handle unknown smart criteria enums safely (e0237e5)
- library: keep security scopes active for the duration of a scan (38ec74c)
- library: link cover art to albums, not just tracks (2fc121b)
- library: phase-3 audit H6 — multi-value tags + extended_tags column (b1cdda6)
- library: phase-3 audit high fixes (H1-H5, H7, H8) (8ec8e4e)
- library: phase-3 audit medium fixes (M1-M4) + L4 a11y (cbae7fb)
- library: re-enable user-edited tracks on rescan (00c0bdc)
- library: register UserDefaults defaults for all @AppStorage keys (dea4212)
- library: remove root soft-deletes tracks; FSEvents triggers UI reload (a921100)
- library: repair FSWatcher event delivery; clean Library warnings (7c1ffa5)
- library: replace deprecated String(cString:); restrict ARCHS to arm64 (6035c51)
- library: rescan security scope, disabled filter, gapless URL, inspector window (fd31970)
- library: scope change-detection to scanned roots only (e4327ef)
- library: skip redundant DB reads for smart playlists; suppress cancel error (eb17840)
- library: smart playlists exclude disabled tracks (09efdca)
- library: Unicode-aware case-insensitive text comparators (3d41284)
- library: Unrated preset matches NULL and 0 ratings (7f82670)
- lint: resolve all pre-existing SwiftLint violations (ee9b5fa)
- lyrics: move Show Lyrics to Window menu, fix LRClib bypass, implement file embed (b4f530d)
- lyrics: update non-goals section to clarify translation status (547c469)
- lyrics: wire LRClib auto-fetch on track change (d0d1352)
- menu,lyrics: stop menu redraw; source priority; auto-show pane (303e584)
- menu: extract commands to Commands struct to stop menu bar flashing (14db0dc)
- metadata-editor: Get Info shows correct rating, loved, and excluded-from-shuffle (f8237ed)
- metadata,library: phase-3 audit critical fixes (C1, C2, C3) (d9d409b)
- mini-player: increase compact layout height to 130 pt (f6dafef)
- mini-player: raise main window when info button is clicked (fc1b012)
- now-playing: show current track title/artist/artwork during playback (ef7f2df)
- observability: remove test for MXMetricPayload which is unavailable on macOS (c93c10d)
- observability: remove unavailable MXMetricPayload on macOS and fix OSSignpostIntervalState Sendable (f95a01a)
- persistence: phase-2 audit fixes #1–#4, #6, #7, #17 (be6bcc9)
- persistence: pin ValueObservation.start to non-MainActor scheduler (13695e1)
- persistence: update migration test assertions for M002 (c9cd53c)
- phase-1: debug audio view, WavPack fixture, TSan scheme patch (50ca5b9)
- playback: auto-load queue item on play, root-scope fallback for nil bookmarks, play-all from library (54f2fff)
- playback: credit outgoing play on gapless handoff so it scrobbles (454c9c2)
- playback: drop redundant ‘await’ on non-async engine.state access (7bbdf3e)
- playback: fallback to root bookmark when per-file bookmark is stale (766502f)
- playback: forward button, auto-repeat, gapless sandbox scope, UI sync (7df5457)
- playback: guard against double-advance when gapless transition + stale .ended race (4bc9da9)
- playback: harden bookmark fallback in QueuePlayer (3c90c7a)
- playback: honour gapless settings and detect missing restored items (95ac08f)
- playback: log stale root bookmark refresh failure instead of silencing (8cfa1c9)
- playback: pre-shuffle items before queue load so first track is random (3475f8f)
- playback: prevent double-song, stale-queue, and forward-button failures (17cbd7d)
- playback: queue full library on track play; wire volume slider to engine (16ff006)
- playback: replace lastGaplessAdvanceItemID with timestamp settle window (970dc4f)
- playback: restart exhausted queue on play; fix 1-item queue race; fix empty symbol fault (46a78bc)
- playback: root bookmark fallback was never matching due to URL vs path comparison (e6a80b7)
- playback: stop queue wrapping to index 0 on forward; add now-playing indicator (08976c6)
- plist: remove duplicate CFBundleIconFile key (7ad7402)
- prevent spurious queue.replace racing with track-end callbacks (2f4e3cd)
- project: disable sandbox/hardened-runtime on UITests target to fix ad-hoc signing mismatch (0039f30)
- project: update LastUpgradeCheck and MACOSX_DEPLOYMENT_TARGET values (36cb2bd)
- resolve Sendable warning in FormatConverter and pause blert (4a657cf)
- run AudioEngine and BufferPump actors at user-initiated QoS (2688f4d)
- search: correct reactivity, focus loss, and post-navigation state (83fd515)
- search: prefix matching per token; fix artwork frame in result rows (a3b82ca)
- search: stable focus via overlay; artist name + artwork in track rows (b6d159d)
- search: use .searchable() to permanently fix toolbar focus (0b9fa81)
- smart-playlists: show integer stepper for inLastDays / inLastMonths (d4eac1c)
- speed selector 1x corruption and playlist click passthrough (e829b39)
- tag-editor: normalise artist/album FKs on save; fix cover art, reload, and UI polish (d81e1b4)
- tests: fix snapshot flakiness under --enable-code-coverage (9ff1ef3)
- tooling: disable modifier_order lint rule to resolve SwiftFormat/SwiftLint conflict on nonisolated (aa59730)
- tooling: remove stale result bundle before test and add pipefail to Makefile (c97b3c7)
- tracks: bump UIState key to v2 to clear persisted addedAt sort (f45d83c)
- ui,audio: per-band spectrum normalization; fix UI test deps (0ee4721)
- ui: add @MainActor to notification delegate callbacks; add diagnostic logging (369c02e)
- ui: add hover text and accessibility metadata for normal playlist controls (d3dd773)
- ui: add hover text and accessibility metadata for smart rule editor controls (7f6cd5a)
- ui: announce Up Next sidebar row is also a drop target (Phase 5 audit L4) (7671e28)
- ui: attach playlist sheets to PlaylistSidebarSection; wire Add to Playlist menu (6806f37)
- ui: clarify Add Files / Add Folder picker copy (Phase 5.5 audit L1) (64ce199)
- ui: clearer ScanBanner summary wording with locale-aware numbers (Phase 5.5 audit M1) (535c97e)
- ui: copy artwork to temp dir before creating UNNotificationAttachment (e6759ff)
- ui: correct database filename in Reveal in Finder button (8532fc8)
- ui: cover art, self-load races, publishing warnings, styled artists/genres (1622b9e)
- ui: currently-playing row now highlights live, drop waveform icon (9e92952)
- ui: date-based default smart playlist names with sibling collision suffix (df25722)
- ui: defer onDidDelete to next tick to prevent crash during sheet dismiss (571871e)
- ui: defer selection publish in tableViewSelectionDidChange (17907da)
- ui: drag tracks to playlists, move-to-folder menu, confirm recursive delete (7a44cd0)
- ui: finish Phase 5 queue UX (drop-on-Up-Next, opt-dbl-click, missing items) (0e6c39d)
- ui: fix new-playlist-from-selection adding tracks and double-commit guard (b07acc5)
- ui: hoist playlist sidebar sheets onto parent List (819ceeb), closes #63
- ui: keep Artwork strictly square regardless of image aspect (db7fc12)
- ui: live watch toggle, transport hints, album multi-select & info, playlist menu (e7bba30)
- ui: make Track menu items reactive; fix sandbox file access in tag editor (e0afdad)
- ui: move Folders sidebar section below Playlists (0f8c937)
- ui: offer permanent-delete fallback when trash fails (Phase 5.5 audit M3) (c97f22f)
- ui: patch album in-memory instead of full reload on settings toggle (e3d4f03)
- ui: persist sidebar width, add Album shuffle, wire ⌘F to search (208acb5)
- ui: prevent hang when sorting large Songs table (491634a)
- ui: prewarm playlist sheet host to reduce first-surface audio hitch risk (b7ec875)
- ui: prewarm smart playlist sheets to reduce first-mount audio hitch risk (74ee75b)
- ui: QueueRow accessibility hint + double-click activation (Phase 5 audit L5) (c744884)
- ui: QueueView empty state offers Add-Music-Folder on fresh install (Phase 5 audit L*) (39602a3)
- ui: re-selection dead zone, UI test window query, cheap artist track count (fe5c40a)
- ui: remove .onDrag from Table cell — breaks row hit-testing (fdb989a)
- ui: remove auto-injected ‘Mini Player’ item from Window menu (0e6d73c)
- ui: remove dual-sort feedback loop in Songs table (923bc8f)
- ui: remove duplicate sidebar toggle button (0b82126)
- ui: resolve artist/album columns, file picker, playback bookmark, post-scan refresh (6b00dfb)
- ui: resolve menu shortcut conflicts and wire launchAtLogin (2783eb2)
- ui: row density and notification improvements (bd7f83b)
- ui: run Songs table sort off the main actor (6dda18b)
- ui: ScanBanner Cancel + Dismiss tooltips and a11y hints (Phase 5 audit L6) (bba3c11)
- ui: scrubber commits seek on release, not on every mouse move (69093b6)
- ui: set shuffle mode on QueuePlayer when Shuffle is pressed in SmartPlaylistDetailView (50d1e90)
- ui: simplify Songs table sort to avoid race conditions (0972828)
- ui: SleepTimerMenu accessibility — tooltips and a11y hint (Phase 5 audit L5) (da8104c)
- ui: stop crashing on launch when seeding sidebar autosave (7e86cec)
- ui: use app accent palette for shuffle/repeat/stop-after buttons (422c616)
- ui: use NSApp.appearance for theme switching to fix half-repaint bug (a5b92cd)
- ui: wire appearance settings to app UI (2a648d8)
- ui: wire TagEditorSheet to Get Info and ⌘I (2f4886e)
- visualizer: pass real AudioSamples to oscilloscope renderer (78f0060)
- visualizer: replace NSCursor.hide/unhide with setHiddenUntilMouseMoves (bbf8079)
- visualizer: rework FluidMetal — additive blending, larger points, correct HSV (8e3034d)
### Changed
- playback: eliminate DB round-trips when queueing full library (315b003)
- search: in-place filtering via the current view’s VM (e2ec51e)
- ui: coalesce scan progress + non-blocking Add Folder/Files panels (Phase 5.5 audit L2) (6c472b3)
- ui: remove unused read-only TrackInspectorPanel (Phase 5.5 audit H5) (b27fdb6)
- ui: replace PlaylistDetailView custom list with TracksView (d9be477)