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.
[Unreleased]
2.17.1 (2026-09-21)
Right-clicking a song inside an album now always brings up its menu. After you opened an album from the Albums grid, a right-click on one of its songs could do nothing at all until you had clicked a song first, which made it look random.
For developers
Fixed
- ui: right-click in an album opened from the grid raises its menu (#556)
2.17.0 (2026-09-20)
Deleting a song, or removing one from the library, no longer rebuilds the whole songs list. The list keeps your place, so you can work down a long list and delete as you go instead of being thrown back to the top after every one. Removing several songs at once now happens in a single pass rather than one redraw per song, and the songs you did not touch stay selected. If a song cannot be removed, Bòcan now tells you instead of leaving it in the list without a word. An artist’s page also keeps up: remove the last song of an album there and the album leaves the page straight away.
Sorting the songs list now sorts all the way down instead of stopping at the column you clicked. Click Artist and each artist’s albums stay together with their tracks in order, rather than the songs falling wherever the import happened to leave them. Every column works this way: sort by play count and the songs sharing a count are grouped by artist and album, so a half-played album reads as an album. Your choice is remembered the next time you open Bòcan.
The lyrics items in the Track menu now keep up with what is happening. Clear Lyrics becomes available as soon as lyrics arrive for the song that is playing, and greys out again once you clear them, instead of waiting for the next song. Fetch Lyrics greys out while a fetch is under way, so it cannot be started twice.
Downloading a podcast episode now shows how far it has got. The small download mark beside an episode fills as the file arrives, and hovering over it reads out the percentage, so a long episode on a slow connection no longer just sits there saying it is downloading.
When a scrobbling service is refusing submissions, Bòcan now waits the full backoff before trying again. A timer left over from an earlier wait could end the next one early, so it sometimes retried sooner than intended against a service that had asked it to slow down.
Up Next now marks missing songs reliably. When a song in a restored queue can no longer be found on disk, its row is greyed out. That marking only reached whichever copy of the list happened to be listening first, so the full screen view could disagree with the main window, and after you left Up Next and came back the marking stopped arriving at all until the next restart.
Phone Sync no longer treats finishing a song as a library change. Playing a song used to tell your phone that the library had changed, so the phone fetched the whole list again and found nothing different, and on a Mac preparing smaller copies of your songs that also started a fresh pass over the library. Both now wait for a change the phone can actually see, such as an edit, a rating or a new song. If you sync chosen playlists rather than everything, nothing changes, because a smart playlist can be built on play counts.
Songs from a Subsonic server now download with far less work for your Mac. A large lossless song starts a little sooner, and the download no longer competes with the song that is already playing.
The keyboard shortcut list in Help now includes Immersive Mode. The shortcut worked all along, but the list did not mention it, so the only way to find it was the View menu.
For developers
Added
- ui: sort the songs table by more than one column (#541)
Fixed
- ui: keep the songs list in place when a track is deleted (#543)
- the seven defects found while auditing the architecture map (#552)
- e2e: the sidebar-collapse leak, and the drift the E2E run exposed (#554)
Changed
- standards: bring the code and the engineering charter into agreement (#544)
2.16.0 (2026-09-16)
The play bar now says what it is playing. Under the title sits a row of small coloured boxes for the format, the bitrate, the sample rate, the bit depth and the channels of the current song, and hovering over any of them explains what that fact means. Streams and podcasts show what their own source reports. Narrow the window and the row keeps the boxes that fit.
Songs with more than two channels, such as 5.1 mixes in Apple Lossless, AAC, FLAC or Dolby Digital Plus, now play with every channel folded into stereo. Before, such a song at your output’s own sample rate stopped with an error, and one at another rate played only the front pair with the surround channels silent. Bòcan does not render Dolby Atmos objects; it plays the mix as stereo.
Songs with more than two channels now get a correct loudness value the next time they are analysed. Bòcan measured the first two channels of the file, which in most surround layouts are not the left and right pair, so a mix carried mostly in the surround channels could measure as near silence and get the wrong volume. It now measures the same stereo fold it plays. Loudness values measured before this release for such songs were wrong; analyse those songs again to replace them.
Dolby Digital and Dolby Digital Plus files with no container, the raw kind that end in .ac3, .eac3 or .ec3, are now found by a library scan and play. They carry no tags, so they are named after the file and show their real length, sample rate and channel count. A song in an MP4 container whose sound the system decoder refuses is now offered to the built-in decoder before Bòcan gives up on it.
The Channels row in Get Info and the track panel now names the layout, such as Stereo, 5.1 or 7.1, instead of showing a bare number. Hovering over it on a surround song says that the mix plays folded to stereo and that Dolby Atmos objects are not rendered.
Surround files that play through the built-in decoder, such as raw Dolby Digital Plus, TrueHD and multichannel Matroska, are now folded to stereo by the same mixer as every other file, so the same mix plays at the same loudness whichever way it was packaged. Loudness analysis now measures that same fold, and it now works for the formats the system cannot open on its own, such as Ogg Vorbis, Opus, WavPack, Monkey’s Audio, DSD, Matroska and TrueHD, which could not be analysed before.
A song in an MP4 container that the system decoder opens but cannot actually play, such as Opus in MP4, now plays through the built-in decoder instead of stopping with an error on its first moment.
For developers
Added
- ui: say what is playing, as source badges in the play bar (#533)
- ui: go to a new playlist on create, and re-centre the play bar title (#537)
Fixed
- audio: fold multichannel files to stereo, with loudness, raw Dolby scanning and channel labels (ADR-091) (#521)
- audio: one stereo fold for every route, loudness for every format, and the ADR-091 follow-ups (#532)
- ui: keep the row selected, name a playlist once, and move the count (#534)
2.15.0 (2026-09-13)
A podcast served over plain http from a computer on your home network now keeps refreshing after you subscribe. If one you added earlier stopped updating, add it again by its address: it picks up where it left off, with its episodes and settings.
Adding a podcast whose directory listing gives a plain-http feed address now works when the show is also served securely, which most are. When it is not, the message says the feed is unencrypted-only instead of showing an error number. We don’t support plain-http feeds from the internet, but an unencrypted home server is fine.
Saving tags works again when your music lives on an external drive or a network share. Bòcan wrote the new version of a file into a scratch area on the startup disk and then moved it into place, which the system refuses to do between two different drives, so every tag save on such a library failed even though the files were perfectly writable. The new version is now prepared next to the original, on the same drive.
Changing only the cover art no longer rewrites your audio files. Unless you have chosen to embed art into the files, the picture belongs to Bòcan alone, so saving it now touches nothing on disk but Bòcan’s own store. Art now saves on albums you cannot write to, such as read-only files or a network drive.
When saving tags or cover art fails, the message now says which file failed and why, such as the file being read-only, instead of an error code.
For developers
Added
- a11y: give every interactive control hover text, and make the audit fail without it (#516)
Fixed
- library: say which file failed and why when a tag or cover-art save fails (#473)
- podcasts: upgrade plain-http feed URLs to https and surface real error reasons (#486)
- podcasts: store the feed address that answered, so a plain-http feed on the local network keeps refreshing (#489)
- errors: report the failures the app was swallowing instead of appearing to succeed (#499)
- log the failures these modules recovered from silently, and enforce the try? rule (#500)
- errors: say what went wrong instead of showing a Foundation error code (#512)
- ui: give the Subsonic song tables an owner for their rows (#514)
- library: tag saves on external drives, and no rewrite for art-only edits (#517)
Changed
- ui: gate the Subsonic song table’s per-row work on a rows version through the shared update plan (#477)
- ui: stop the tracks view and the root observing the whole lyrics model (#478)
2.14.1 (2026-09-08)
The now-playing display no longer misses a queue change that lands in the first instants after launch while nothing is playing, which could leave it on “Not playing” until the next track change.
Immersive Mode and the visualizer no longer stutter twice a second while a song plays with the full Songs list open. The main window was quietly redrawing every row of the songs table on each playback tick; it now leaves the table alone until something in it actually changes. The table also stopped re-checking every one of its rows whenever anything else in the window updated, such as a synced lyric line changing, so those moments no longer cause a stutter either.
Phone Sync no longer leaves a converted song stuck as pending forever. If the phone lost a song after the Mac had already sent it (for example when Android stopped the app mid-transfer to save battery), the Mac now converts that song again the next time the phone asks for it, instead of answering busy on every sync.
This release pairs with Bòcan Music for Android 0.4.0, which carries the phone side of that fix: the phone now waits while the Mac converts the song again instead of giving up on it until the next sync. Install it from https://github.com/bocan/bocan-music-android/releases/tag/v0.4.0 so both ends match.
For developers
Fixed
- sync: re-encode a released transcode artifact on an urgent request (#446)
- ui: keep the playback position tick out of the root body and the songs table (#452)
- ui: subscribe to queue changes before the first reads so a seed cannot fall between them (#462)
Changed
- ui: skip the songs table’s per-row walks when its inputs are unchanged (#457)
2.14.0 (2026-09-05)
Experimental: Immersive Mode. Press Shift-Command-I, pick View, Enter Immersive Mode, or click the new three-column button in the toolbar, and Bòcan opens a full-screen window with nothing on it but the music: the oscilloscope running edge to edge in the Drift palette, and three cards over it with the artwork, title and player controls, the next ten songs in your queue (scroll for the rest), and the lyrics, synced where you have them. Esc brings you back. It is marked experimental on purpose: the visualizer behind the cards is heavier than the one in the side pane, and if it turns out to stutter on too many Macs the mode may change shape or be removed in a later update.
Phone Sync conversion no longer takes the whole app down on a file whose sound switches between stereo and mono part way through, which some damaged MP3s do. The converter now adapts mid-file and carries on, so a single odd track cannot stop every sync.
For developers
Added
- ui: Immersive Mode, an experimental full-screen now-playing window (ADR-089) (#444)
Fixed
- audio: rebuild the transcode resampler when a frame’s shape changes (#443)
2.13.0 (2026-09-02)
Phone Sync can now shrink your music on the way to the phone. Pick a quality in Settings, from MP3 320 down to Opus 128, and anything bigger is converted before it syncs, so a lossless library fits in a fraction of the space. Each choice shows what your selection would cost the phone in storage, a progress line follows the conversion as it runs ahead of the sync, and your files on the Mac are never touched. Converted copies are made just in time and cleaned up once the phone has them, so the Mac does not fill up either: conversion stays a few gigabytes ahead of the phone, and the progress line says so while the next batch waits. A switch keeps the copies around if you would rather re-sync instantly, and with it on the whole selection converts up front.
For developers
Added
- sync: transcode to MP3/Opus for Phone Sync (ADR-088) (#437)
2.12.0 (2026-08-31)
Deep Dive can now find an album or song on MusicBrainz by name when your files carry no MusicBrainz tags, instead of giving up. A match found this way is labelled in the report, and searches ignore stray spaces around names.
When you agree with a match, one click on Save to Tags writes the MusicBrainz id into the files themselves, with a backup kept, so the match survives rescans and is there for every other tool that reads your tags.
Search now understands release years. Type a year like 1984 over the album gallery and it filters down to the albums released that year, and a fuller date like 2004-06 narrows it to the month, when your files carry full release dates. Your chosen sort order stays exactly as it was.
For developers
Added
- library: save a Deep Dive name match into the file’s tags (#434)
- persistence: album search matches release years and full dates (#435)
Fixed
- ci: stop xcbeautify masking build failures, and build ad-hoc signed on runners (#429)
- library: Deep Dive searches MusicBrainz by name for albums and tracks without ids (#433)
2.11.0 (2026-08-29)
Deep Dive (off by default)
“Get Info” on any artist, album or song now has a Deep Dive tab: a short, readable report built from MusicBrainz and Wikipedia. For an artist you get a bio, current and past members with their years and instruments, and a full discography with the releases you already own ticked. For an album: label, catalogue number, format, country, release date, barcode, and the artist’s other releases from around the same time. For a song: who wrote it, its ISRC, when it was first released, every release it appears on, and a link to its AcoustID. Artists finally have a Get Info of their own (right-click any artist, or press ⌘I on an artist page), with an Info tab alongside the report. Reports are cached on your Mac for a week, so reopening one is instant and they still work offline.
It is deliverately off unless you turn it on, in Settings > Library.
Bòcan promises that, out of the box, the only thing it ever talks to is the update-checking feed, and Deep Dive can’t work without asking MusicBrainz and Wikipedia about the music you own. Turning it on sends MusicBrainz identifiers (or an artist’s name, for files that carry none) to those two sites, and starts a slow, one-off pass that looks up each artist in your library on MusicBrainz, one request every second and a half, to fill in sort names and “which Nirvana?” disambiguations. A quiet progress line above the library shows how far that has got. My 15000 tracks took about an hour and a half.
The Deep Dive tab explains all this and links to the switch while it’s off, and nothing is sent until you flip it. The privacy page has been updated to say exactly that.
If an artist’s files carry no MusicBrainz id, Deep Dive matches by name and tells you so; one click keeps that match, and a properly tagged file will override it later. If MusicBrainz asks us to slow down, the report quietly retries three times before giving up.
Your library, tidied underneath
A long audit of the database turned up a lot of information that was being read from your files and then dropped on the floor. It is now kept: artist sort names (so The Beatles file under B), MusicBrainz ids for artists, albums and songs, album release types (album, single, EP, live, compilation), track and disc totals per album, cover-art dimensions and where the art came from, the musical key of a track, podcast guests and credits, and your lyrics sync offset. A full rescan no longer wipes things Bòcan itself worked out, like ReplayGain and skip counts, and a couple of updates will ask for a one-time silent rescan to fill the new fields in. A few columns that nothing ever used have been removed.
Fixes
- Right-click a song and choose Get Info (or Play, Love, Rate…) without selecting it first: it now acts on the song you clicked instead of doing nothing or acting on an old selection.
- Podcast episode artwork now shows in Now Playing.
- Subsonic scrobbles now include the album.
- Podcast subscriptions keep their directory ids across refreshes.
Added
- library: fill artist disambiguation from MusicBrainz in the background (42c2644), closes #401
- library: migrations can request a one-time silent full rescan (6dd5d96), closes #425
- library: store the MusicBrainz release type on albums (eead339), closes #403
- library: store track-artist MusicBrainz IDs on tracks and artists (c0145dd), closes #399
- podcasts: cache episode artwork on first play and show it in Now Playing (b4b06a2), closes #410
- tooling: add make audit-db for data-level schema audits (52dbfb9)
- ui: album Deep Dive, works and AcoustID on tracks, tab placed after Details (c5d0118), closes #413
- ui: Deep Dive for artists and tracks, and Get Info for artists (522f04a)
- ui: Deep Dive retries a rate limit, confirms a guessed artist id, and answers ⌘I on an artist page (cbcf649), closes #413
- ui: put Deep Dive behind a setting, off by default (6bf7c6a)
- ui: show the MusicBrainz artist lookup progress in the scan banner area (97de309)
Fixed
- app: retain ArtistEnrichmentService so its launch pass actually runs (7578aa3), closes #401
- library: keep app-computed and user state across a full rescan (3aee0fb), closes #423
- library: pace artist enrichment and back off on 503 instead of parking (218779b), closes #401
- library: record cover-art dimensions, size, and provenance (3ffa86c), closes #417
- library: roll MusicBrainz release IDs up to the album row (5dd36c6), closes #402
- library: roll track and disc totals up to the album row (dae4e45), closes #404
- library: store artist sort names so ‘The Beatles’ files under B (ea5deee), closes #400
- lyrics: persist the sync-offset slider per track (ff4df24), closes #415
- metadata: prefer a known MusicBrainz release type over junk first values (0f899a2), closes #403
- metadata: read a bare KEY Vorbis comment as the musical key (f0c68ac), closes #407
- metadata: read bit depth from the container instead of a phantom tag (001bcf2), closes #405
- metadata: unknown release types resolve to nil, and M048 clears stored junk (ad2904a), closes #403
- playback: send the album with Subsonic scrobbles (a815124), closes #408
- podcasts: carry directory IDs through subscribe and keep them across refreshes (4a15ffd), closes #409
- podcasts: persist episode-level podcast:person credits (678ff0a), closes #411
- ui: let NSTableView set clickedRow before the track context menu is built (cb2bcd9)
- ui: track context menu acts on the right-clicked row when nothing was selected (9487596)
Changed
- acoustics: one MusicBrainz client, one shared limiter, artist and Wikipedia endpoints (a031700), closes #412
- library: one MusicBrainz lookup per artist MBID, not per artist row (5c5acfe), closes #401
- library: skip MBIDs already resolved within an enrichment batch (b8ec952), closes #401
2.10.0 (2026-08-27)
Search works everywhere now as you’d expect it to. Until now, the search box only did anything in Songs, Albums, and Artists. Type in Genres, Composers, Radio, or Podcasts and it just sat there, dead. Now it filters every view it appears in: genre and composer lists, radio stations (by name or stream address), your podcast shows, the Continue Listening rail, and even the episode list inside a show. If nothing matches, you get a proper “no results” message instead of a mysteriously empty screen.
Escape always does something sensible too. Esc now peels back one layer at a time, wherever your keyboard focus happens to be: inside an album, it steps you back out; on a filtered view, it clears the search. Previously Esc would only clear a search if the search box itself still had focus, which felt broken the moment you clicked anywhere else.
And the rest. Updated docs, a security-scanner bump, and a sad note: in the README.md, we’ve marked Robert Kaye, founder of MusicBrainz and one of the giants Bòcan stands on, as “the late” in our credits as he passed earlier this year. His work powers the app’s tagging, lookup, and scrobbling to this day.
Added
- ui: make the toolbar search filter every browse view (e171fff)
- ui: restore the pre-search scroll position when a search clears (9762dac)
Fixed
- tooling: release-note cleanup no longer strands you on the bot branch (3da96e4)
2.9.0 (2026-08-22)
Just a small one. The main fix is that history entries now carry the search query that was active alongside the destination. Drilling into a detail page still clears the search (the album page stays clean), but Esc / the mouse back button / the toolbar arrows restore the query the user left behind.
Also, the podcasts home view now has a Continue Listening rail, and the “Rebuild Full-Text Search Index” and “Export Diagnostics” buttons in Settings -> Advanced now work.
Added
- podcasts: context menu on Continue Listening cards (53f9b5e)
- podcasts: Continue Listening rail on the Podcasts home view (969348b)
- ui: back/forward restores the search filter left at each view (2893376)
- ui: make the Advanced pane’s two stub buttons real (0d65cb1)
2.8.0 (2026-08-17)
CUE sheets: I’ve refactored how these work. CUE sheets (files) are a bit of a legacy thing. The original idea is that if someone rips an entire CD to a single file, it can have a CUE sheet that lets you play the individual tracks within it. In practice, people just use them as playlists because single file rips are rare these days. So what I’ve done is this:
- If you manually import a cue file, it treats it as a playlist.
- If you add a queue file into your library tree as a file along with the music files, it tries to detect if it’s a true cue sheet. If it is indeed a multi track file, it creates “markers” in the now playing bar and you can move forward and backwards through the tracks like any other. If it’s just a playlist pretending to be a cue sheet, it ignores it.
- Added a tab to the Track Info page that shows any real cues.
Internet radio and streaming:
- Fixed Now-playing titles for Ogg Vorbis/Opus stations (#386)
- The TLS saga (#393): FFmpeg 9 turned certificate verification on, which the sandbox broke. The proper fix exports the macOS system trust store to a PEM in the app container so every stream, HLS included, verifies against real roots.
- Format sniffing for AIFF, Musepack, and TTA magic numbers (#387).
Artwork
- Sidecar cover art (cover.jpg, folder.png and friends) is picked up during scans (#388) if there is no embedded art in the tags.
- HEIC and AVIF are accepted in the artwork picker and mime map (#389).
UI and window polish
- The visualizer pane no longer bleeds into the toolbar.
- Tabs have been disabled. They didn’t work anyway and much work needs to be done to make them useful.
- Back/Forward hover text mentions the bracket shortcuts.
- The Help shortcut tables were corrected and completed with a new Mouse Buttons page (including the Logitech notes). Thanks to @go1968 for the assistance in testing that! I have one ordered for home.
- Added the Exclude from Shuffle to the Song context menu.
Housekeeping
- Documentation cleanup and standardization across the ADRs.
Added
- audio: now-playing titles for Ogg Vorbis/Opus radio streams (d85df4c)
- e2e: CUE marker journey and fixture (ADR-087) (6942ed8)
- library: attach CUE sheets as in-track markers (ADR-087) (f957f99)
- library: pick up external sidecar cover art during scans (#388) (0d78bf2)
- persistence: track_markers table, record, and repository (ADR-087) (adbe3e1)
- playback: marker-aware transport (ADR-087) (30c0ec3)
- ui: marker line and scrubber ticks in the player bar (ADR-087) (2b2d51c)
- ui: shuffle-exclusion toggle in the track context menu (10285e1)
- ui: surface CUE markers in Get Info and the track info panel (cfad7d8)
Fixed
- app: disable native window tabbing (855c438)
- app: strip auto-injected Window-menu items from secondary windows (322edc8)
- audio: compute the CUE segment frame budget in the decoder’s rate (dad305f)
- audio: disable FFmpeg TLS verification broken by the sandbox (adbb63a)
- audio: read Ogg titles from stream-level metadata (#386) (ba8259a)
- audio: sniff AIFF, Musepack, and TTA magics (#387) (7203bec)
- audio: verify TLS against exported system roots (#393) (c799792)
- library: assign CUE TRACKs to the FILE of their INDEX 01 (9795c7e)
- library: materialise CUE PERFORMER and album metadata (#390) (641c781)
- library: mint security-scope bookmarks for CUE audio at import (#391) (75f5e9c)
- library: probe the last CUE track’s real duration instead of storing 0 (612afeb)
- library: run the cue pass after a dropped folder’s audio imports (4f58480)
- persistence: delete virtual-row dependents explicitly in M038 (140559e)
- ui: accept HEIC/AVIF artwork in the picker and the mime map (#389) (5319369)
- ui: ask for folder access only when a CUE’s audio is truly unreachable (#391) (f8a3c78)
- ui: drop locale grouping separators from smart playlist integer fields (8b7a611)
- ui: guard EditMenuRouting against a nil NSApp (abbd587)
- ui: identify the scrobble provider Connect button (da5699d)
- ui: mention .cue in the playlist import sheet copy (bf792af)
- ui: resolve Sendable capture and QoS inversion diagnostics (1f8bf77)
- ui: show the bracket shortcuts in Back and Forward hover text (174fec2)
- ui: stop the visualizer pane background bleeding into the toolbar (91b6986)
2.7.0 (2026-08-15)
The only user-visible change in this release is that now next/previous buttons move forward and backwards through the station lists. Behind the scenes, a lot of work went into making the E2E test suite more robust and comprehensive, including a new loopback fixture server for podcasts and ICY radio streams. The E2E suite now covers almost every surface of the app. In the process of building the E2E coverage, a few minor bugs were found and fixed, including a couple of gaps in queue restoration that could cause playback to stop unexpectedly.
Also, users relying on accessibility features should see a big improvement in the app’s accessibility identifiers, which are now complete and consistent across the entire UI. This will make it easier to navigate the app using assistive technologies.
Added
- e2e: add whole-app E2E foundations (phase 28) (aa513b5)
- e2e: help-text audit in warning mode (phase 29) (4f00ca1)
- e2e: local-only smoke subset for phase 35, CI scope deferred (2d8367e)
- e2e: loopback ICY radio fixture server (phase 34, part 1) (37be372)
- e2e: loopback podcast fixture server (phase 34, part 2) (e7a3350)
- e2e: menu enablement matrix with a permanent Cmd-A row (phase 30) (a2c3bf4)
- e2e: menu invocation pass (phase 30) (634e092)
- e2e: menu manifest, structural crawl, shortcut parity (phase 30) (3f26af3)
- e2e: more surface crawls - Radio, smart playlist, search (phase 31) (7d0b097)
- e2e: podcast journeys against the loopback fixture server (phase 34, part 3) (be191e2)
- e2e: radio add-by-URL journey against the fake ICY server (phase 34) (22fda4c)
- e2e: radio dial-file import journey (phase 34) (29f2c57)
- e2e: radio playlist-URL indirection journey (phase 34) (28b859e)
- e2e: radio reconnect journeys (phase 34) (5538466)
- e2e: radio stream-details journey, live and offline (phase 34) (e521448)
- e2e: Recently Added surface crawl (phase 31) (ea6c5d1)
- e2e: surface crawl foundation (phase 31) (96852e7)
- e2e: total accessibility-identifier coverage (phase 29, audit slice) (a041dc7)
- e2e: visualizer liveness matrix (phase 33) (9d7db37)
- e2e: windows and settings crawl (phase 32) (564b516)
- ui: accessibility identifiers for the radio add and info sheets (6f8ebf2)
- ui: next/previous transport buttons skip radio stations, list selection tracks playback (56f60b2)
Fixed
- app: keep menu-bar enablement live, not frozen at body build (fc2476e)
- build: propagate FFmpeg include path to packages that transitively import CFFmpeg (d2d2cbf)
- e2e: isolate scrobble credentials from the real keychain in E2E runs (92f9fe7)
- library: full rescan no longer wipes the library (4c96e02)
- playback: close two queue-restore gaps surfaced by the E2E journeys (937bf6e)
- settings: centre the Sources empty state (1fce068)
- ui: centre the Duplicate Review empty states (871acf0)
- ui: centre the Phone Sync empty state in its settings card (7587873)
- ui: Jump to Current Track works from grids and self-loading views (99f26d3)
- ui: make sidebar destination rows reachable by accessibility (48d71c9)
- ui: populate the now-playing display from a resting queue (f492d2d)
- ui: stop playback-rate labels rounding away the final digit (97ca86a)
- ui: stop redundant UserDefaults writes that crackled playback during navigation (873b8fe)
- ui: stop the Contrast Audit view retitling the Settings window (5bf6e09)
- ui: stop the mini player wedging or misplacing the next launch (7406dd1)
2.6.2 (2026-08-09)
Fixed
- release: stop doubling the changelog section headings (f419b35)
2.6.1 (2026-08-09)
A big one: in-app updates now actually work on macOS 15. A hardcoded value in our release pipeline told Sparkle that every update required macOS 26, so anyone on Sequoia has been quietly informed they were “up to date” since the very first release. That’s fixed, both for this release and retroactively for the published feed, so if this update appeared out of nowhere after months of silence: hello, welcome back, you have some catching up to do.
Small polish. While a radio station or podcast plays, the title line in the player is now properly bright instead of dimmed like the subtitle under it. And the website got a cleanup: the front page no longer displays raw arrow codes and HTML where links should be, the feature cards sit in tidy balanced columns, and the screenshot strip lost its stray scrollbar.
Under the hood, a full dependency spring-clean. Every library Bòcan builds on is now at its latest release, including a Sparkle security patch and a major-version move of the cryptography package behind Phone Sync (verified byte-for-byte against the pairing test vectors, so paired phones notice nothing). The third-party notices file now reports true versions and credits three Apache-licensed components it had missed, and a weekly automated check now files an issue the moment any dependency falls behind, so none of this drifts quietly again.
### Fixed
- release: appcast minimum system version must match the app’s real floor (feed4d3)
- scripts: make gen-notices read real pins and cover the Apache packages (654f2d2)
- ui: keep the now-playing title at full strength for radio and podcasts (7a10ded)
- website: render feature bodies as HTML and tidy the front page (a4990e8)
2.6.0 (2026-08-08)
Internet radio, done properly. There’s a new Radio section in the sidebar where you can build your own station list: add a stream by hand, paste a .m3u or .pls playlist link into the Add Station sheet and every station inside gets offered with its name intact, or just drop (or import) a playlist file on Bòcan and its streams become stations instead of “missing tracks”. While a station plays, the song title arrives live from the stream and shows in the player and the macOS Now Playing widget, and the player’s info button tells you exactly what you’re hearing: codec, sample rate, channels, and the bitrate the station claims. Bòcan remembers each station’s details, so the info sheet works even offline. Radio never scrobbles, never pretends live audio can rewind, and reconnects by itself if a stream hiccups. There’s a starter pack of twelve stations to try. Thanks to @DanaturCode for requesting this one (#376) and pointing out that a playlist full of radio streams deserved better than becoming an empty playlist.
Getting around is quicker. The back and forward side buttons on your mouse should now walk your browsing history, just like a browser. The Escape key backs you out of whatever you’ve drilled into: an album returns to the artist you opened it from, then to Artists. Esc still does all its old jobs first, so leaving the search box, cancelling a rename, closing sheets, and exiting full screen all work exactly as before (#378).
And one small fix that felt bigger than it was: pressing Cmd+A while typing in the search field now selects your typed text, like every Mac app should, instead of quietly selecting every track in a list you weren’t looking at (#379). Both this and the navigation work were suggested by @go1968, so thanks Guy!
### Added
- audio: capture stream details and ICY now-playing titles in FFmpegDecoder (phase 27-5) (bffc1b4)
- library: import playlist stream entries as radio stations (phase 27-4) (1baf399), closes #376
- library: remote playlist resolver for radio station URLs (phase 27-3) (2892cf9)
- persistence: add radio station catalog (phase 27-1) (7e034dd)
- persistence: add stream-detail profile columns to radio stations (phase 27-5) (a8bf227)
- playback: forward stream titles and details through QueuePlayer (phase 27-5) (64e6e3f)
- ui: live radio titles and the station stream-details sheet (phase 27-5) (14e6dce)
- ui: local Radio destination with station catalog (phase 27-3) (a24df75)
- ui: mouse back/forward buttons and Esc drill-out navigation (866ebc4), closes #378
- ui: shared internet radio queue-item factory and play path (phase 27-2) (82eb7d5)
### Fixed
- audio: detect ICY title support from evidence, not the consumed icy-metaint header (a7546b4)
- playback: never seek a live radio stream on queue restore (d41044f)
- ui: advertise playlist URLs in the Add Station sheet (a027f71)
- ui: Esc drill-out prefers the artist (or other container) you came from (11a30b8), closes #378
- ui: route Cmd+A to the focused text editor before selecting tracks (e19b693), closes #379
2.5.0 (2026-08-04)
New: Library Summary
Open Tools → Library Summary (⌘⇧Y) for six tabs that tell you the truth about your music collection. Everything is computed on your Mac, from data Bòcan already keeps. Nothing is sent anywhere.
- Basic Info: how many songs, albums, and artists you have, and how many days it would take to play everything once.
- Library Hygiene finds the problems worth fixing: missing track numbers, albums split apart by messy tags, impossible years, missing artwork, and files that have vanished from disk. Click any problem and Bòcan takes you straight to the song.
- Audio Quality shows what your library is made of: formats, sample rates, and your lossless-to-lossy ratio in both songs and gigabytes. It can also analyse your lossless files for fakes: FLACs that were secretly made from MP3s leave a telltale fingerprint in the audio, and Bòcan can spot it. Files are only ever “suspected”, and nothing is changed or deleted.
- Collection Shape charts when your music was made against when you actually listen to it, your one-song artists versus your twenty-album favourites, and how album lengths ballooned in the CD era.
- Listening Behaviour can import your Last.fm history, bringing years of listening home and matching it to your library. Then it shows how much of your collection you have actually ever played, which songs you skip more than you finish, the old favourites you have not touched in two years, and a heat map of exactly when in the week you listen.
- Podcasts adds up your unheard backlog in hours and estimates how long it would really take you to clear it, finds subscribed shows that stopped publishing, shows what you download but never play, and includes a Reap button to delete episodes you finished months ago that are still taking up disk space. It always asks first.
Also in this release: fixed a bug where certain FLAC files (ones missing a length header) would fail analysis and could end playback instantly.
### Added
- audio: ProvenanceAnalyzer spectral-shelf transcode detection (phase 24-1) (a78fe26)
- library: Last.fm listening-history import (phase 25-1) (40c5d1d)
- persistence: provenance verdict storage and scanner invalidation (phase 24-2) (70fbc7f)
- persistence: tiered listen matching from real-export evidence (25-1) (1c7764f)
- ui: Analyse Provenance batch job in the Tools menu (phase 24-3) (810ffdd)
- ui: Audio Quality tab with distributions and offenders (#373) (a0669fe)
- ui: collapsible, navigable hygiene sections; comma-free years (#373) (7e25cbd)
- ui: Collection Shape tab with owned-vs-played decades (#373) (dbc02c7)
- ui: Library Hygiene tab with actionable library problems (#373) (604dbfc)
- ui: Library Summary window with whole-library stats (#373) (3a78326)
- ui: Listening Behaviour counter analytics (phase 25-2) (3038412)
- ui: listening time analytics: heatmap, discovery, seasons (25-3) (a4cae18)
- ui: make Analyse Provenance visible and triggerable in place (6247395)
- ui: podcast behaviour: completion, creep, time-to-listen (26-2) (84156f0)
- ui: Podcasts tab: backlog debt, dead feeds, hoards, reapable (26-1) (ae5884c)
- ui: Reap Now and dead-feed unsubscribe (26-3): phase 26 complete (2a2540b)
- ui: Suspected Transcodes report in the Audio Quality tab (phase 24-4) (e1d3b40)
### Fixed
- app: merge Library Summary into the existing Tools menu (26c15ac)
- app: stop the Tools menu appearing twice (95a4655)
- audio: decode length-0 FLACs; thermal heatmap; honest failures (843cdce)
- ui: correct the episodes-and-bytes catalog key; retire stale copy (2efd49c)
- ui: restore saved column order in song tables (7a01984)
- ui: summary rows land on the right album and select the song (11e415f)
2.4.0 (2026-07-30)
### Added
- ui: double-click an album cover to play it in place (#369) (cb5f892)
- ui: filter the Artists list to album artists only (#369) (e373f4b)
- ui: type-to-search from any view (#369) (874947d)
- website: send GNU Terry Pratchett on every page (5d806af)
### Fixed
- playback: invalidate in-flight artwork loads on podcast updates (544b1c2)
- playback: show album artwork without Now Playing crashes (#372) (37f68d1)
- ui: add missing Open artist accessibility-hint catalog key (ad92d55)
- ui: make View-menu List/Grid switch reliably redraw listings (#363) (3c09047)
### Changed
- persistence: fold fetch-by-id-or-throw into a Database helper (083de5a)
- scrobble: share a ListenBrainz-compatible transport (889d78d)
- subsonic: fold the request wrapper into withClient helpers (cd7b4fb)
- ui: share loadErrorAlert across the Subsonic browse views (c82de1a)
- ui: share the alpha-blended Metal pipeline setup (06fc680)
2.3.1 (2026-07-19)
### Fixed
- release: point the 2.3.0 appcast at the canonical v2.3.0 release (770871b)
- release: point the 2.3.0 appcast entry at the real DMG (0340175)
- ui: navigate from artist rows directly (b7bcb84)
2.3.0 (2026-07-19)
This one adds a new card-grid view mode to Artists, Genres, and Composers, and adds a new “View” menu to switch between the list and card-grid modes. The card-grid mode shows album art for each artist, genre, or composer, and lets you browse their details by clicking on the album art.
### Added
- app: mirror collection view modes in the View menu (#363) (aa88b52)
- ui: add card-grid view mode to Artists (#363) (33284bf)
- ui: add card-grid view mode to Genres and Composers (#363) (db21e47)
- ui: browse genre and composer destinations by album (#363) (f0a1820)
### Fixed
- app: never start the Sparkle updater in debug builds (7ae9571)
### Changed
- app: extract the Tools menu into BocanCommands+Tools.swift (105d408)
- ui: extract the shared collection mode toggles (#363) (742b8db)
- ui: share the collection list row and sort between Genres and Composers (f01d13a)
- ui: split GenresComposersView into per-view files (18c1212)
2.2.0 (2026-07-18)
This one’s a community release: half of what’s below started life as a sharp-eyed issue or a pull request from users. Thank you both! THIS is why I make these things open source.
Smart playlists learned to sort. The Sort By setting in the rule editor now actually orders the playlist (it was being quietly thrown away before, and sorting by Artist or Album could even error out). Better still, sorting is no longer a single choice: build an ordered list of sort keys with priorities, like artist, then track number, then title, each ascending or descending. Track Number is a new sort key, and the column headers in a smart playlist’s track list are now clickable for on-the-fly re-sorting.
Regular playlists too, without losing your order. Click any column header in a manual playlist to browse it sorted. Your hand-arranged order is kept safe underneath: a Playlist Order button snaps right back to it, and drag-to-reorder politely pauses while a column sort is active so your saved order can never be scrambled.
Compilations stay together. Thanks to a great report from @lukesutton (#362): a compilation with no Album Artist tag used to shatter into one album per track artist. Files flagged as compilations now group under a single “Various Artists” album, and if your library was already split, it heals itself automatically on the next scan. No manual cleanup needed.
Clicking an artist works again. @0gnev tracked down exactly why clicking a row in the a view sometimes did nothing (#360) and sent a tidy fix, regression test included.
The scrubber stays put when you pause. Also @0gnev (#361): a quick play-then-pause could leave the progress bar showing a stale position, or zero. The paused position is now captured precisely, so playback resumes exactly where the scrubber says it will.
And a peek ahead: @lukesutton’s idea to browse Artists, Genres, and Composers as album-art card grids (#363) is fully specced and on the list for 2.3.0.
### Added
- library: multi-key smart playlist sort with working headers (3ed00f2)
- ui: sortable manual playlists with preserved order (44262f6)
### Fixed
- library: group compilations without an album artist (#362) (cfd1ee2)
- ui: restore artist row navigation (42f42e8)
- ui: restore artist row navigation (d3154e8)
2.1.0 (2026-07-13)
### Added
- persistence: add podcasts.artwork_hash for Phone Sync artwork (22-10 step 1) (9b7cbfe)
- podcasts: hash show artwork at cache time and backfill (22-10 step 2) (6f62ecd)
- sync: advertise podcast artwork hashes in the manifest (22-10 step 3) (e17b4e0)
- sync: serve podcast show art from /v1/artwork (22-10 step 4) (464b3e8)
- sync: show library hashing readiness in Phone Sync settings (7c4b7c0)
- ui: make the empty Paired Phones state a pairing call to action (71d35b6)
### Fixed
- ui: move Paired Phones above Sync Profile in Phone Sync settings (a994953)
2.0.0 (2026-07-12)
⚠ BREAKING CHANGES
- release v2.0.0
This isn’t really breaking changes, but I’m building a free, open source Android companion and its pairing and sync protocol requires all track and episode files to have a content hash stored in the database. This release adds that content hash to all tracks and episodes, and adds a new sync_meta table to store the sync manifest generation state. If you are upgrading from v1.17.1 or earlier, Bòcan will automatically backfill all of your track and episode hashes in the background, and will also create the new sync_meta table.
Bòcan music will let you sync some or all of your music to you phone, let you play it in your car via Android Auto, and let you listen to your podcasts on the go. The sync protocol is end-to-end encrypted, and uses mutual TLS with a self-signed certificate to authenticate the server to the client. The Mac side generates a pairing code that you enter into the phone, and the Mac then generates a permanent key that is stored in the Keychain. The client can then use this permanent key to authenticate to the server without having to enter the pairing code again.
### Added
- library: backfill track content hashes in the background (76793d2)
- persistence: add missing-content-hash queries and a single-column hash write (606a0ce)
- persistence: add Phone Sync tables (M031) and TrustedDevice (b6d8883)
- persistence: add sync_meta and sync_profile repositories (62f07ea)
- podcasts: store an episode content hash at download time (b0353b2)
- release v2.0.0 (96448dd)
- sync: add file serving (track, episode, artwork, lyrics) (9b925a4)
- sync: add podcasts to the manifest, the generation observer, and routes (f936dc1)
- sync: add ServerIdentity with a self-signed P-256 TLS certificate (c13c2c1)
- sync: add SyncServer module with PairingCode and golden vectors (4cb23ca)
- sync: add the HTTP/1.1 parser, router, and connection context (c4f3967)
- sync: add the manifest DTOs, SyncProfile, and ManifestBuilder core (e1b7c30)
- sync: add the mutual-TLS listener, connection driver, and /v1/ping (c664b3c)
- sync: add the pairing ceremony (server side) (b24e1cb)
- sync: add the TrustedDevices trust store (941afeb)
- sync: assemble the SyncServer lifecycle with Bonjour advertising and app wiring (9303bab)
- sync: gzip the manifest when the client requests it (ca54e5c)
- sync: use the stored episode hash for the manifest and If-Match (f835dbb)
- ui: add the Phone Sync settings pane and pairing sheet (20b6946)
### Fixed
- sync: serialize permanent key generation in the login Keychain (c70b2a9)
- sync: trigger the Local Network prompt so Bonjour advertising works (f076ca8)
- ui: use checkboxes for the Phone Sync playlist picker (dcf941e)
1.17.1 (2026-07-06)
### Fixed
- ci: retry Pages deployment once on transient failure (87c7f00)
- ui: hand the idle dock tile back to the Dock’s native rendering (341a96a)
1.17.0 (2026-07-06)
### Added
- app: adopt Icon Composer app icon (714ac12)
- app: give the app icon distinct light and dark appearances (e6aa088)
- website: light and dark responsive favicon (ca57321)
- website: match favicon and brand mark to the new app icon (9910499)
1.16.1 (2026-07-05)
### Fixed
- library: ignore metadata-only FSEvents and skip no-op conflict writes (3e49b23)
- metadata: open files read-only when reading tags (5a093ca)
1.16.0 (2026-07-05)
This release contains a lot of fixes for the metadata editor, including making sure that the cover art you apply to an album actually replaces the existing art, and that the track list shows the new art. It also makes sure that lyrics are actually written to disk when you choose “Embed in file”, and that stored lyrics are shown in the editor for non-playing tracks and in the Get Info lyrics tab.
### Added
- acoustics: fetch and rank all MusicBrainz releases with ISRCs (1e5b9b0)
- library: thread MusicBrainz IDs through the tag-edit pipeline (c77d6b2)
- ui: release picker and advanced identifier fields in Identify Track (f93f863)
### Fixed
- library: decode Cover Art Archive image ids so art search works (b21a7f3)
- library: link saved cover art to the album so the track list shows it (f8c67cd)
- library: make Embed in file actually write lyrics to disk (1e5d930)
- ui: make cover-art Apply responsive and album edits replace album art (f3367fb)
- ui: make identify-track rows fully clickable and legible (ba2845a)
- ui: show stored lyrics in editor for non-playing tracks (89dadf6)
- ui: show stored lyrics in the Get Info lyrics tab (b85a8de)
1.15.0 (2026-07-01)
I’ve gone through the entire UI and made sure that all the browse lists (Albums, Artists, Genres, Composers, and Podcasts) now restore their scroll position when you return to them. You can even go into Artist view, then into an Album, and when you go back to the Artist view, it will restore your scroll position in the album list. This latter one was mostly a problem for Artists with eleventy thousand albums. I’m looking at you - The Beatles.
I’ve also added sort choosers to all grids and lists, so you can sort by name, date added, or other criteria. Finally, I’ve fixed a small UI issue where the visualizer toggle icon was too similar to the Identify Track icon.
### Added
- ui: scroll-position restore and sort choosers across browse lists (69a37ea)
### Fixed
- ui: distinguish the visualizer toggle icon from Identify Track (12f0a8f)
1.14.1 (2026-06-28)
### Fixed
- ui: read the correct Info.plist key for PodcastIndex configured-state (6bb8769)
### Changed
- ui: drop dead default arms in MetalVisualizerFactory switches (a920402)
1.14.0 (2026-06-28)
Many thanks to @go1968 and @rjp for their contributions to this release, which stops us loosing the existing song list, adds a sort-order chooser to the Albums grid, restores the album-grid scroll position on return from an album, and splits Play Album / View Album in the album grid.
### Added
- ui: add a sort-order chooser to the Albums grid (#349) (476bee7)
- ui: restore album-grid scroll position on return from an album (#349) (9bc3b0b)
- ui: split Play Album / View Album in the album grid (#349) (f9f1639)
### Fixed
1.13.0 (2026-06-28)
### Added
- ui: add hover tooltips and VoiceOver labels to podcast episode status icons (b18ad30)
### Fixed
- audio: recover from or surface a dropped stream instead of silent fake-playback (0efb073)
- ui: give the Podcasts settings pane a visible sidebar icon (2db73a9)
- ui: surface a terminal playback failure instead of pretending to play (2f61db4)
### Changed
- library: skip redundant post-scan reload when nothing changed (214e3da)
- ui: coalesce playlist-sidebar reloads at startup (18dd40e)
- ui: dedupe shuffle toggle to satisfy the body-length gate (6cf55d0)
- ui: drop the redundant startup load from the initial search emission (c813119)
- ui: extract playlist cover-art file helpers to satisfy file-length gate (3ab426e)
1.12.1 (2026-06-23)
### Fixed
- persistence: force one re-parse of podcast feeds to backfill podroll and persons (7c00087)
1.12.0 (2026-06-23)
Fixing a few podcast bugs, and importantly added a new Podcasting 2.0 feature podcast:podroll which shows a list of recommended podcasts at the top of a show’s show notes. This is a new namespace in the podcasting spec, and Bòcan now parses and persists these recommendations.
### Added
- podcasts: parse and persist podcast:podroll recommendations (3f97a3c)
- ui: add a Show Notes button to the podcast player bar (b5771e3)
- ui: show a podroll recommendation shelf atop podcast show notes (669b15e)
### Fixed
- podcasts: accept legacy GitHub-docs podcast namespace URI (33f8792)
- ui: move podcast Show Notes button to the left of the transport row (818e69e)
- ui: re-resolve now-playing episode when podcastID lags the GUID (a3f6bd5)
- ui: render podcast show notes with a safe in-house HTML parser (1e58e6b)
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)