A foobar2000 component that lets you browse and stream music from a Navidrome server (or any Subsonic-compatible server) directly inside foobar2000.
- Download
foo_navidrome_<version>.fb2k-componentfrom the Releases page. It's a single multi-platform component β one file ships macOS, Windows x86, Windows x64, and native Windows-on-ARM (ARM64EC). foobar2000 picks the right binary at load time. - Drag the file onto foobar2000, or double-click it β foobar2000 installs it automatically
- Restart foobar2000
- Go to Preferences βΊ Tools βΊ Navidrome and enter your server URL and credentials
- Browse your entire music library: Artists β Albums β Songs
- All Songs at the top of the tree β Add to Playlist / Play Now (or double-click) queues your whole library in one go. It doesn't expand in the tree (tens of thousands of rows would freeze it); the songs are fetched in pages of 500 via
search3.view, honouring the library filter, and queued in album order (album artist, year, album, then disc and track number) - Multi-select on both platforms: Ctrl+click to add/remove rows, Shift+click (or Shift+arrows) for a range, then Add / Play / any right-click action applies to all of them. Tracks that two selected rows share (an artist and one of its albums) are queued once
- Smart lists at the top of the tree: β Starred, Recently Added, Most Played, Recently Played, Random Albums, π Random Mix (a fresh batch of random tracks), Genres, and your server-side playlists
- Decades: expand it for one entry per decade your library spans (2020s, 2010s, β¦ down to your oldest album); expand a decade for every album released in it, band by band (sorted by artist, then year), shown as Artist β Album with the year. Add / Play on a decade queues all of it. Albums without a year aren't listed; the library filter applies
- Scrobbling: plays are reported back to Navidrome, so play counts, "Recently Played" and any Last.fm / ListenBrainz relay the server has configured stay in sync (toggle in Preferences)
- Favorites and ratings from the right-click menu β Star / Unstar and a 0-5 star rating, stored per-user on the server so they show up in the web UI and on your phone, and available in the playlist as
%navidrome_rating%/%navidrome_starred%for a custom column. Also available straight from the playlist's own right-click menu (rate/star a track without going back to the browser), and kept in sync in already-added playlist entries β see Showing ratings in the playlist - Manage server playlists from the right-click menu β add the selection to any existing playlist or a New Playlistβ¦, remove tracks, rename, delete. Changes land on the server, so they show up everywhere
- Send Active Playlist to Navidrome from the right-click menu β uploads the active foobar2000 playlist under the same name
- Bookmarks (resume playback) β bookmark the currently playing track's position with File βΊ Bookmark Current Position (Windows and macOS), browse them under the Bookmarks smart list (shows a trailing
β± m:ss), remove one from its right-click menu. Backed by Subsonic'sgetBookmarks.view/createBookmark.view/deleteBookmark.view - Internet radio stations β browse/play stations under the Radio smart list, or manage them (New/Edit/Delete) from the tree's right-click menu or the dedicated Preferences βΊ Media Library βΊ Navidrome βΊ Radio Stations sub-page
- Podcasts β browse subscribed channels and their episodes under the Podcasts smart list; subscribe by RSS feed URL or unsubscribe from the tree's right-click menu. Episodes the server hasn't finished downloading yet show their status instead of a play option
- Now Playing β see what everyone else on the server is listening to right now under the Now Playing smart list
- Multi-library support β if your Navidrome exposes more than one library, the browser tree groups by library automatically: one node per library at the top, artists nested underneath, so each library's contents stay separate all the way down to the album list. The Preferences βΊ Media Library βΊ Navidrome βΊ Libraries sub-page lets you restrict browsing/search to a chosen subset (tick "Only include selected libraries" and pick two or more); off by default (all libraries shown), and inert when the server only has one
- Choose which sections the tree shows β Preferences βΊ Tools βΊ Navidrome βΊ Browser Sections has a checkbox per top-level section (All Songs, β Starred, Recently Added, β¦ Radio, Podcasts, Now Playing). Untick the ones you never use and the tree keeps just those plus your artists (and libraries); open browsers update on Apply (Windows) or as you click (macOS). Everything is shown by default
- Streaming quality: ask the server to transcode on the fly (MP3 / Opus / AAC) and cap the bitrate β useful on slow links. Set in Preferences; "Original" always sends the stored file. Seeking works in transcoded streams too: when the stream itself can't seek, the track is reopened at the new position (needs a server with the OpenSubsonic
transcodeOffsetextension, which current Navidrome has) - Full tags from the server: besides title / artist / album / track / date, every track carries album artist, disc number, genre (all of them, as a multi-value field), composer, BPM and its MusicBrainz recording id (
MUSICBRAINZ_TRACKID), so compilations and multi-disc albums group and sort properly - ReplayGain: the server's track/album gain and peak values are passed to foobar2000, so Playback βΊ ReplayGain evens out the volume of streamed tracks like it does for local files (needs an OpenSubsonic server that reports
replayGain, such as current Navidrome, and files with ReplayGain tags) - Play queue sync β File βΊ Save Play Queue to Navidrome stores the playing (or active) playlist, the current track and its position on the server; File βΊ Restore Play Queue from Navidrome loads the queue saved there by any Subsonic client (Symfonium, Feishin, Tempo, β¦) into a Navidrome Play Queue playlist and resumes where it left off. See Play queue sync
- Download Original Filesβ¦ from the right-click menu β saves the selected tracks to a folder, always in their stored format regardless of the streaming setting
- Offline playback β Make Available Offline (browser right-click menu, or right-click βΊ Navidrome on playlist tracks) keeps a local copy of the original files; those tracks then play, and seek, without the server. Optional prefetch downloads the next track while the current one plays, into a size-capped cache. See Offline playback
- English and Spanish UI β follows the system language, or pick one in Preferences βΊ Tools βΊ Navidrome βΊ Language. See Language
- Add albums or artists to playlist in one click (loads all songs automatically)
- Right-click any row for a Play Now / Add to Playlist context menu. Play Now (button, menu, double-click or Enter) replaces the active playlist with the selection and plays it from the first track; Add to Playlist appends without touching playback
- Artist Info from the right-click menu on an artist row β shows the last.fm biography and a link to the full page (
getArtistInfo2.view) - Every artist has Top Songs and Similar Artists child nodes, so you can browse an artist's most popular tracks or jump straight to related artists without leaving the tree (
getTopSongs.view,getArtistInfo2.view) - Double-click a song to play it immediately (replaces the active playlist, like Play Now)
- Live search across artists, albums and songs β results update as you type (debounced, no per-keystroke server hammering). Clear it with the Γ beside the field or Esc to get the full tree back
- Rescan button (Rescan Navidrome Library section of Preferences βΊ Tools βΊ Navidrome) β triggers a server-side scan and shows live progress, for when files were added/removed server-side and you don't want to wait for Navidrome's own scan schedule
- Album artwork displayed in Now Playing and playlists (fetched from Navidrome)
- Credentials saved in foobar2000's config (persistent across restarts)
- Test Connection button to verify server connectivity
- Native
navidrome://URI scheme: tracks added to playlists store a stable URI, not a transient HTTP URL β playlists survive credential rotation or server URL changes - Auto-skips tracks deleted from the server: once a track 404s during playback (e.g. a stale playlist entry after a library reorg), it's skipped automatically for the rest of the session on any later shuffle/repeat/Random Mix β no more retrying the same dead stream over and over
- Appears under Preferences βΊ Media Library βΊ Library viewers alongside Album List / Artist View, and can also be docked as a panel in the main window layout: Navidrome Browser in the Default UI element picker on Windows (View βΊ Layout βΊ Enable layout editing mode, right-click βΊ Replace UI Elementβ¦) or as a Columns UI panel (Panels βΊ Navidrome Browser),
navidromein View βΊ Layout βΊ Edit Layoutβ¦ on macOS - Lyrics on Windows in a native Navidrome Lyrics panel (Default UI and Columns UI), or through ESLyric β see Lyrics (Windows)
- Lyrics on macOS in a native Navidrome Lyrics layout panel β see Lyrics (macOS)
- Instant Mix from any playlist track (right-click βΊ Navidrome βΊ Instant Mix) or any song, album or artist in the browser β plays similar songs in a dedicated Instant Mix playlist β plus AudioMuse-AI Text Search, Instant Playlist and Song Alchemy β see Instant Mix and AudioMuse-AI
foo_navidrome has its own Navidrome Lyrics panel on Windows:
- Add it to your layout: Default UI β View βΊ Layout βΊ Enable layout editing mode, right-click βΊ Replace UI Elementβ¦ βΊ Navidrome Lyrics; Columns UI β Live layout editing, right-click βΊ Add after βΊ Panels βΊ Navidrome Lyrics.
- Play a track that foo_navidrome added (a
navidrome://track/<id>URI). The panel follows the now-playing track: with synced lyrics the current line is bold and kept centred (scroll by hand and it waits a few seconds before following again), otherwise the lyrics show as plain text. Right-click βΊ Copy lyrics puts them on the clipboard. It uses foobar2000's colours and font and follows Dark Mode.
The lyrics come from the same lookup as the macOS panel below. If you'd rather use ESLyric, that keeps working too:
foo_navidrome can also feed lyrics into ESLyric, a separate foobar2000 component that displays synced/plain lyrics panels. For ESLyric, foo_navidrome generates the config + lookup script ESLyric needs to talk to your Navidrome server.
- Download the latest ESLyric release from https://github.com/ESLyric/release and install it the same way as foo_navidrome (drag the
.fb2k-componentonto foobar2000). Match the architecture to your foo_navidrome install (x64 / ARM64EC). - Open Preferences βΊ Tools βΊ Navidrome once and click OK/Apply β even with no changes, this is what makes foo_navidrome (re)generate the ESLyric config/script for your current server + credentials. It also runs automatically on every foobar2000 startup once ESLyric is detected, and again whenever you save Custom Headers.
- Add the ESLyric panel to your layout (Default UI / Columns UI element picker).
- Play a track that foo_navidrome added (a
navidrome://track/<id>URI). ESLyric calls Navidrome'sgetLyricsBySongId.viewfor that song, falling back to the classic artist/titlegetLyrics.viewlookup on servers that don't support by-id lookup. Synced lyrics are converted to LRC timing when the server provides them.
Generated files live under your foobar2000 profile:
%APPDATA%\foobar2000-v2\eslyric-data\scripts\lib\foo_navidrome\config.js # server URL + Subsonic token β never your raw password
%APPDATA%\foobar2000-v2\eslyric-data\scripts\searcher\navidrome.js # the lyrics-fetching script
Don't hand-edit them β they're overwritten on the next config save/startup. If ESLyric isn't installed, foo_navidrome silently skips this step (check View βΊ Console for a one-line "not detected" note on startup).
ESLyric doesn't exist on macOS, so foo_navidrome ships its own lyrics panel:
- View βΊ Layout βΊ Edit Layoutβ¦ β add a
navidrome-lyricsline wherever you want the panel. - Play a track that foo_navidrome added (a
navidrome://track/<id>URI). The panel follows the now-playing track.
Lyrics come from Navidrome's getLyricsBySongId.view (embedded or sidecar .lrc lyrics on the server). When the server has timings, the current line is highlighted and kept centred as the song plays; otherwise the lyrics show as plain text. On servers without that endpoint it falls back to the classic artist/title getLyrics.view lookup. Lyrics are cached for the session, so replaying a track doesn't refetch. Windows' panel works the same way.
Other components can read the same lyrics through the navidrome_lyrics_api service (both platforms). foo_ui_panels' lyrics panel uses it, so Panels UI skins show your server's lyrics on Windows and macOS.
AudioMuse-AI analyses your Navidrome library's audio and answers "songs like this" questions. foo_navidrome uses it two ways.
Instant Mix (right-click a playlist track βΊ Navidrome βΊ Instant Mix, or a song, album or artist in the Navidrome browser) asks Navidrome for similar songs (getSimilarSongs2.view) and plays them in a playlist called Instant Mix: the song you picked first, then the similar ones. Each new mix replaces that playlist, so your own playlists are never touched. If the song you picked is already playing, it keeps playing and the mix follows it. It needs no extra setup. With the AudioMuse-AI Navidrome plugin installed (and audiomuseai listed first in ND_AGENTS), Navidrome answers from AudioMuse's sonic similarity. Otherwise it answers from last.fm, if configured, which often has nothing for less-played tracks. When Navidrome returns no similar songs for a song or artist and an AudioMuse-AI server URL is set (below), Instant Mix asks AudioMuse-AI directly, so it works without the Navidrome plugin too.
The other three features talk to the AudioMuse-AI server directly. Set it up under Preferences βΊ Tools βΊ Navidrome βΊ AudioMuse-AI:
| Field | What to enter |
|---|---|
| Server URL | AudioMuse-AI's address, e.g. http://audiomuse:8000 |
| API token | Only if AudioMuse-AI has authentication enabled: an API token created in AudioMuse-AI |
| Server name | Only if AudioMuse-AI serves several media servers: the name it uses for this Navidrome |
| Tracks | How many songs each mix or playlist gets (default 50). It also sizes Instant Mix. |
Then:
- File βΊ AudioMuse-AI βΊ Text Searchβ¦: describe the sound ("calm piano with rain"). Uses AudioMuse's CLAP search (
/api/clap/search), which must be enabled on the AudioMuse side. - File βΊ AudioMuse-AI βΊ Instant Playlistβ¦: ask AudioMuse's AI assistant for a playlist ("90s road trip rock"). Uses
/chat/api/chatPlaylistand the AI provider configured in AudioMuse-AI. Expect this one to take a minute. - Song Alchemy: right-click tracks in a playlist (Navidrome βΊ Song Alchemy (AudioMuse-AI)), or songs/artists in the Navidrome browser. AudioMuse blends them (
/api/alchemy) into songs that sit between them.
Each result opens in a new foobar2000 playlist named after the request (e.g. AudioMuse: rainy jazz) and starts playing. A progress window with Abort shows while it runs. Songs AudioMuse knows about but Navidrome no longer has are skipped; the console says how many. The playlist is an ordinary foobar2000 playlist and stays until you delete it. To also have it on the server, use the browser's Send Active Playlist to Navidrome.
dev/audiomuse/ holds a Docker Compose stack (AudioMuse-AI flask + worker + PostgreSQL) for testing these features on a dev machine. It analyses your Navidrome over the Subsonic API and installs nothing on the Navidrome side. Instant Playlist uses the Ollama already running on the host.
cp dev/audiomuse/.env.example dev/audiomuse/.env # Navidrome URL/user/password, API token (gitignored)
ollama pull qwen3.5:9b # Instant Playlist's model, on the host Ollama
make audiomuse-up # start, wait for the API, check the model
make audiomuse-analyze # analyse the NUM_RECENT_ALBUMS newest albums
make audiomuse-status # task progress + a sample text searchThen point foobar2000 at http://127.0.0.1:8000 with the AUDIOMUSE_API_TOKEN from .env. make audiomuse-down stops it; ARGS=-v also deletes the analysis.
| Platform | Status |
|---|---|
| macOS | β Supported β shipped (Xcode, foobar2000 v2 for Mac) |
| Windows x64 | β Supported β runtime-verified on native foobar2000 for Windows (Visual Studio 2022, or cross-compiled on Linux) |
| Windows x86 | β Supported β same build, 32-bit binary shipped in the component |
| Windows ARM64 | β Supported β native ARM64EC binary, runtime-verified |
| Linux | βοΈ Runs under Wine β loads the Windows x64 build |
A single
.fb2k-componentships every platform: macOS bundle, Windows x86, x64, and a native ARM64EC binary; foobar2000 selects the right one at load. The Windows binaries build natively with Visual Studio, on CI, or cross-compiled on Linux with clang-cl (see Building on Linux).
- foobar2000 v2 for Mac
- Xcode 15+ (developed with 15.4; the component targets macOS 12.0+)
- foobar2000 SDK
SDK-2026-09-16or newer β source the SDK subdirs andpfc/from reupen/foobar2000-sdk-unmodified (an unmodified mirror of the official SDK; crucially it shipshelpers-mac/, which the macOS build needs and some other mirrors omit). Arrange them as siblings of this repo:
foobar2000/
SDK/
helpers/
helpers-mac/ β macOS only (NSView+embed.m); from the reupen mirror
shared/
foobar2000_component_client/
foo_navidrome/ β this repo
pfc/ β sibling of foobar2000/ (also from the reupen mirror)
The expected layout relative to foo_navidrome/:
../SDK/
../helpers/
../shared/
../foobar2000_component_client/
../../pfc/
If
pfcis in a different location, create a symlink:ln -s /path/to/pfc /path/to/personal/pfc
The fastest dev loop is ./scripts/mac-dev-build.sh β runs xcodebuild and installs to
your local foobar2000. The build is stamped with git describe (e.g. 1.21.2-dev.3+2a46400),
so Preferences βΊ Components tells a local build from a release:
./scripts/mac-dev-build.sh # build + install (see Scripts for --patch/--minor/β¦ flags)Restart foobar2000 after the script finishes to pick up the new version.
If you prefer Xcode directly:
- Open
foo_navidrome.xcworkspacein Xcode - Select the
foo_navidromescheme (top-left scheme selector) - Build: Product βΊ Build or
Cmd+B - Run the install script:
./scripts/install-macos.sh
- Restart foobar2000
make mac-test builds and runs the shared logic test suite
(tests/*.cpp, one file per shared module β covers SubsonicTypes.h,
SubsonicCore, the browser model and src/core/MediaEnrichmentLogic.cpp) with a
plain clang++, no Xcode or SDK needed. It is the same suite the Windows CI runs; release.yml runs it on
the macOS runner before every xcodebuild. On Linux the equivalent is
make test (clang-cl + Wine).
mac-dev-build.sh and win-build-local.sh run this suite before building the
component and abort on failure β pass --no-test to skip. All paths call
scripts/run-unit-tests.sh, the one place the compile command lives.
The same script first runs make i18n-check (scripts/i18n-check.py): every
UI string wrapped in tr() / trf() / trw() / NDTr() / N_() must have an
entry in the Spanish table (src/core/I18nSpanish.cpp), or the run fails and
lists what's missing (make i18n-missing prints just the keys).
No Xcode, only the Command Line Tools? make mac-syntax compiles the shared SDK
glue and every macOS .mm file against the foobar2000 SDK with -fsyntax-only
(no link, no bundle), which catches compile errors before a VM or CI build.
- Open Preferences βΊ Tools βΊ Navidrome
- Enter your server URL (e.g.
http://navidrome.local:4533/) - Enter your username and password
- Click Test Connection β you should see "Connected!"
Three ways to open the browser:
- File βΊ Open Navidrome Browser
- Preferences βΊ Media Library βΊ Library viewers βΊ Navidrome βΊ Activate
- As a panel in your layout, always open: on Windows (Default UI) enable View ⺠Layout ⺠Enable layout editing mode, right-click an element ⺠Replace UI Element⦠(or add a splitter first) and pick Navidrome Browser under Media Library Viewers; with Columns UI turn on Live layout editing (toolbar button, or Preferences ⺠Columns UI ⺠Layout), right-click a panel ⺠Add before/after ⺠Panels ⺠Navidrome Browser; on macOS open View ⺠Layout ⺠Edit Layout⦠and add a
navidromeline where you want the panel (names are case-insensitive)
Then:
- Expand an artist to see albums, expand an album to see songs
- Expand a smart list (β Starred, Recently Added, Most Played, Recently Played, Random Albums), Decades, Genres or Playlists to browse without digging through artists
- Select one or more items (Ctrl/Shift+click; Cmd/Shift on macOS) and click Add to Playlist or Play Now
- Select All Songs and click Add to Playlist to load the whole library
- Double-click a song to play it immediately (replaces the active playlist, like Play Now)
- Use the search field to search across your library; Γ or Esc clears it
- Right-click for Star / Unstar, a Rating submenu (None, 1-5 stars), Send Active Playlist to Navidrome and Download Original Filesβ¦
Any of those actions work on whole albums or artists too β the component expands the selection to tracks for you. Ratings apply to songs only, which is what Subsonic supports.
The list keeps itself current: reopening a browser window that has been closed for more than 30 minutes reloads it from the server, and if Add to Playlist / Play Now finds nothing because the server rescanned its library since the list loaded (old artist/album ids), the list reloads by itself and asks you to select again. Whenever Add/Play can't load some or all of the tracks, an error window explains what happened, shows the server's error and suggests Refresh, which still reloads the list on demand.
Subsonic keeps one play queue per user on the server, which phone and desktop clients use to hand playback over to each other. File βΊ Save Play Queue to Navidrome saves the playlist that's playing (or the active one, with its focused track, when nothing plays) together with the position in the current track. Items that aren't Navidrome tracks are skipped, and a playlist longer than 100 tracks is saved as the 100 starting at the current one.
File βΊ Restore Play Queue from Navidrome reads the saved queue, puts it in a playlist called Navidrome Play Queue (replaced each time) and starts playing the saved track at the saved position. Tracks added this way carry the same tags as tracks added from the browser.
Tracks added before this version don't have the new tags (album artist, disc, genre, ReplayGain, β¦) in their playlist entry; add them again from the browser to pick them up.
The server-side rating and favorite flag travel with the track into foobar's
playlist, as the title-formatting fields %navidrome_rating% (1-5) and
%navidrome_starred% (1 when starred). Add a column for them under
Preferences βΊ Display βΊ Playlist View βΊ Custom Playlist Columns:
| Name | Pattern |
|---|---|
| Navidrome Rating | $if(%navidrome_rating%,$repeat(β
,%navidrome_rating%),) |
| Navidrome Starred | [%navidrome_starred%] |
Both fields are absent for unrated / unstarred tracks, so $if() and
[%navidrome_rating%] behave as you would expect. They are deliberately not
called %rating% β that name belongs to foobar2000's own Playback Statistics,
which stores a separate local rating.
Playlist entries follow the server. The value in the playlist is refreshed in place β same entry, no duplicate β whenever the component learns a newer one:
-
you rate or star a track from the browser's right-click menu
-
you open the album in the browser (or search, or expand a playlist or genre) β the browse response already carries the current values, so this costs no extra request
-
the track starts playing β one lookup per played track. This is how a rating you changed in the Navidrome web UI reaches a playlist you never browsed.
-
foobar2000 starts β a full refresh of every playlist, so a rating column can be sorted on rather than being right only for the tracks you happened to play. This is grouped by album: one request per distinct album across all playlists, not one per track. An album-shaped playlist costs a handful of requests; one spanning your whole library costs as many as it has albums, so it can be turned off under Preferences βΊ Advanced βΊ Tools βΊ Navidrome: refresh ratings on startup.
Upgrading: tracks that were already in a playlist before this version don't carry the album id the startup refresh groups by, so that one pass skips them and says how many it skipped. They are not stuck β the other three triggers still reach them, and opening their album in the browser updates them immediately, no re-adding needed. Re-adding a track is only needed if you want that entry to take part in the startup pass as well, since the album id is written into the URI when the track is added.
The right-click menu edits the playlists stored on Navidrome, not just foobar's local ones:
- Add to Navidrome Playlist ⸠lists every playlist on the server; pick one to append the selection to it, or New Playlist⦠to create one from the selection.
- Remove from Playlist works on song rows sitting inside a Playlists βΊ node β that's where a track has a position on the server to remove.
- Rename Playlist⦠/ Delete Playlist⦠act on a selected playlist row. Deleting removes it for every client; the tracks themselves are untouched.
Preferences βΊ Tools βΊ Navidrome has two settings that apply to every track:
- Stream as β
Server defaultleaves it to Navidrome's own transcoding rules,Original (no transcoding)always sends the stored file, or pick a target format to have the server transcode on the fly: MP3, Opus, AAC, FLAC or WAV. - Max bitrate β a kbps ceiling the server may not exceed. Ignored when the stream isn't being transcoded, and when the target is lossless.
The server has to be able to produce the format you pick. Navidrome ships transcodings for MP3, Opus and AAC only. To use FLAC or WAV, add a matching transcoding under Navidrome βΊ Settings βΊ Transcoding first β otherwise the server ignores the request and sends the original file.
Lossless targets aren't about saving bandwidth (WAV is bigger than almost any source): they're for playing formats foobar2000 can't decode itself, by having the server convert them without quality loss.
Both take effect on the next track; nothing needs restarting. Download Original Filesβ¦ ignores them by design β downloads are always the file as stored on the server.
Plays are reported to the server as you listen (scrobble.view): "now playing"
when a track starts, then a play once half of it β or four minutes, whichever
comes first β has been heard. Turn it off with Report plays to Navidrome
(scrobbling) in Preferences βΊ Tools βΊ Navidrome.
Tracks land in the playlist as navidrome://track/<id>?... URIs. The component resolves these to the current HTTP stream when playback starts, so your playlists keep working if you change credentials or move the server.
Every Navidrome track is checked against a local cache before it is streamed:
when a copy is there, it plays from disk, seeks instantly and needs no server.
The cache lives in the foobar2000 profile, under foo_navidrome-cache/, in two
parts:
- Pinned copies (
pinned/) β right-click songs, albums, artists or playlists in the browser, or tracks in a foobar2000 playlist (right-click βΊ Navidrome), and pick Make Available Offline. The original files are downloaded (download.view, like Download Original Filesβ¦) with a progress window you can abort. They stay until you pick Remove Offline Copy on the same tracks; nothing ever evicts them. On Windows, the copy of a track that's playing can't be deleted while it's open: it's removed right away from foo_navidrome's point of view and deleted from disk when that track stops. - Prefetch (
prefetch/) β off by default. Turn it on in Preferences βΊ Advanced βΊ Tools βΊ Navidrome: prefetch the next track to a local cache. While a track plays, the next one is downloaded in the background, in the current Stream as format, so it starts without buffering and survives a dropped connection. The next track is known when foobar2000 plays in order: the playback queue, then the following playlist entry with the Default or Repeat (playlist) order (shuffle and random orders can't be predicted, so nothing is prefetched there). Navidrome: prefetch cache size (MB) (default 1024) caps this part; the oldest files go first, and the two newest are always kept. Turning prefetch off empties it at the next start.
Playback, ratings and scrobbling still need the server: an offline track plays, but its play report is lost while the server is unreachable. Copy Diagnostics reports how many files each part holds and their size.
The component's windows, menus, preferences and messages are in English or Spanish. By default it follows the system language (macOS: the first preferred language; Windows: the display language). To pick one, set Preferences βΊ Tools βΊ Navidrome βΊ Language and restart foobar2000. Log lines and Copy Diagnostics stay in English, since they're pasted into bug reports.
- foobar2000 v2 for Windows
- Visual Studio 2022 (Desktop development with C++ workload, MSVC v143 toolset + ATL; the project builds as C++20)
- foobar2000 SDK
SDK-2026-09-16or newer β same directory layout as above
- Open
src/platform/win/foo_navidrome.vcxprojin Visual Studio - Update the
<ProjectReference>GUIDs in the.vcxprojto match your local SDK project GUIDs - Build in Release | x64 configuration (the
.vcxprojalso defines Win32 and ARM64EC if you need those binaries; CI builds all three) - Copy
foo_navidrome.dllto your foobar2000 components folder:%APPDATA%\foobar2000\user-components\foo_navidrome\ - Restart foobar2000
CI builds all three Windows arches (x86, x64, ARM64EC) and merges them with the macOS bundle into one
foo_navidrome_<version>.fb2k-componentper release. No Windows machine? Cross-compile the x64 DLL on Linux (see below), or run./scripts/win-test.shto build on a GitHub Actions Windows runner (real MSVC/MSBuild) and download the artifact.
foobar2000 has no native Linux build, but it runs well under Wine (it is the
Windows build). This repo can cross-compile the Windows x64 component natively on
Linux with clang-cl + lld-link β no MSVC and no Wine needed for the build
itself; Wine only runs foobar2000.
- foobar2000 for Windows installed under Wine (e.g. the AUR
foobar2000package) llvm,clang,lld(provideclang-cl/lld-link), plusgit,curl,unzip,zip
./scripts/win-setup-toolchain.shInstalls the LLVM toolchain, fetches the Windows SDK/CRT/ATL via
xwin, downloads WTL,
and clones the foobar2000 SDK into the sibling layout (../foobar2000, ../pfc, ../libPPUI).
./scripts/win-build-local.sh # cross-compile + install into the Wine foobar2000 (see Scripts for --launch/--clean)The DLL is installed to ~/.foobar2000/profile/user-components-x64/foo_navidrome/
and a distributable foo_navidrome_<version>_win-x64.fb2k-component is written to the
repo root. scripts/install-windows.sh handles the install + packaging and can be run
standalone after a build.
./scripts/win-build-local.sh --x86 (make win-build-x86) builds the 32-bit DLL
instead, into build-win-x86/, packaged as foo_navidrome_<version>_win-x86.fb2k-component
(DLL at the zip root, where 32-bit foobar2000 looks). It isn't installed into the x64 Wine
foobar2000; set FB2K_COMPONENTS to a 32-bit foobar2000's profile/user-components to
install it there.
All helper scripts live in scripts/, grouped below by the OS you run
them on. Note the cross-platform twist: this project is currently developed on
Linux, and the Windows component is built from Linux (foobar2000 runs under
Wine; the DLL is cross-compiled with clang-cl) β so the "Windows" scripts run on a
Linux host. A native Windows build uses Visual Studio directly and needs no scripts.
A root Makefile wraps the most common ones as make targets β run
make help for the full list. It's a thin convenience wrapper; the scripts below
remain the source of truth. Highlights:
Targets follow an <os>-<action> naming scheme β win-* for the Linux
cross-compile, mac-* for the native macOS build, mac-vm-* for the macOS-on-Linux VM
flow, win11-* for the Windows-11-on-Linux VM flow and win11-arm-* for the same VM on an
Apple Silicon Mac.
make test # Linux: fast clang-cl+wine build/run of the unit tests
make mac-test # macOS: native clang++ build/run of the SAME test suite
make mac-syntax # macOS without Xcode: compile-check the component sources against the SDK
make i18n-check # every UI string has a Spanish translation
make win-build # Linux: cross-compile the Windows x64 component
make win-build-launch # β¦same, then relaunch Wine foobar2000
make win-logs # follow the colourised component debug log (Wine)
make win-install # Linux: install the built DLL + package the .fb2k-component
make win-test ARGS="--launch" # dispatch a build-windows.yml run on GH, install, relaunch
make mac-build # macOS: bump patch, xcodebuild Release, install
make mac-logs # follow the colourised component debug log (macOS)
make mac-release # macOS: bump, build, install, package, gh release create
make mac-ci-build VERSION=1.11.0 # hermetic macOS CI build
make win-build-mac # macOS: cross-compile the Windows x64 DLL
make win11-arm-up # macOS (Apple Silicon): Windows 11 ARM VM (QEMU + HVF); make win11-arm-web for its screen
make mac-vm-release # macOS-on-Linux VM (../macos-devbox): deploy the latest release, relaunch
make win11-smoke # Windows 11 VM on Linux (../windows-devbox): build, deploy, UI smoke test| Command | What it does |
|---|---|
./scripts/win-setup-toolchain.sh |
One-time setup: install clang-cl/lld-link, fetch the Windows SDK/CRT/ATL (via xwin) + WTL, clone the SDK siblings |
./scripts/win-build-local.sh |
Cross-compile the x64 DLL and install it into the Wine foobar2000 |
./scripts/win-build-local.sh --launch |
β¦same, then relaunch foobar2000 |
./scripts/win-build-local.sh --clean |
Wipe the object cache and rebuild from scratch |
./scripts/ui-test.sh <wine|win11|mac> <scenario> |
One UI test driver for every platform: smoke, playqueue, search, cui, prefs PAGE; ./scripts/ui-test.sh compare <scenario> wine win11 mac runs one on several platforms and prints a step matrix (make ui-compare ARGS=β¦, PARALLEL=1 for all at once). The Wine smoke (make win-ui-smoke): opens the browser, expands an artist, plays an album, then checks the debug log, the process and a screenshot in build/ui-test/. Other subcommands drive it by hand: click, key, wait, shot |
./scripts/navidrome-logs.sh |
Follow the local debug log, colourised by level/tag (run in a second pane; also make win-logs / make mac-logs) |
./scripts/install-windows.sh |
Install an already-built DLL + package the .fb2k-component (called by win-build-local.sh; --new-release to publish) |
./scripts/win-test.sh |
Build the DLL on a GitHub Actions Windows runner (real MSVC/MSBuild), download the artifact, install it locally |
Both local dev builds (win-build-local.sh, mac-dev-build.sh β but not
--new-release or CI) define NAVIDROME_DEBUG_LOG, so the component writes structured
traces (HTTP requests with auth values redacted, Subsonic status errors, input
decode steps, UI actions) to /tmp/foo_navidrome_debug.log (Wine writes it via
Z:\tmp). The build clears that file and marks it with the version; make win-logs / make mac-logs (scripts/navidrome-logs.sh) tail it live. Release / CI
builds never define the flag β the tracing compiles to nothing.
Each line is HH:MM:SS.mmm LEVEL TAG [tNNNN] message, where tNNNN is a
short per-thread id so interleaved lines from background workers can be told
apart. NAVIDROME_TIMER also emits <label> took <n>ms when its scope exits
(HTTP requests are timed this way). Three environment knobs are read once at the
first log line β no rebuild:
| Variable | Effect |
|---|---|
NAVIDROME_LOG_LEVEL |
INFO (default) Β· WARN Β· ERROR β drop lines below the threshold |
NAVIDROME_LOG_TAGS |
comma list, e.g. HTTP,Input β when set, only those tags are written |
NAVIDROME_LOG_MAX_MB |
integer (default 8) β truncate the file at process start if it is already larger |
Both HTTP clients classify every failure into a shared navidrome::ErrorKind
(Auth / NotFound / Timeout / Network / Tls / RateLimited /
ServerError / Parse / β¦), retry the transient kinds up to 3Γ with jittered
backoff, and expose the classified outcome via lastError() so a caller can
tell "credentials rejected" from "connection reset" without string-matching.
| Command | What it does |
|---|---|
./scripts/mac-dev-build.sh |
xcodebuild Release, install locally (version = git describe, no bump) |
./scripts/mac-dev-build.sh --patch Β· --minor Β· --major |
Stamp the last release tag + 1 (patch / minor / major) |
./scripts/mac-dev-build.sh --no-bump |
Same as no flag (kept for old habits) |
./scripts/mac-dev-build.sh --no-install |
Build only (skip install) |
./scripts/mac-dev-build.sh --new-release |
With a bump flag: build, install, package, then create a GitHub release (no debug tracing β matches CI) |
./scripts/install-macos.sh |
Install an already-built component + package the .fb2k-component (called by mac-dev-build.sh) |
./scripts/navidrome-logs.sh |
Follow /tmp/foo_navidrome_debug.log, colourised (also make mac-logs; needs a build from mac-dev-build.sh without --new-release) |
Same commands as the Windows 11 VM on Linux: one make target starts it and opens its
screen in a browser tab. On a Mac the VM is QEMU with Apple's own hypervisor (Windows
doesn't run in a container there: under colima's nested virtualization it hangs at
boot). dockur is used once, in Docker, to
download Windows 11 ARM from Microsoft and build its unattended setup. The DLL is
cross-compiled on the Mac (clang-cl + lld-link + xwin); it's the x64 build, which
Windows on ARM runs emulated.
| Command | What it does |
|---|---|
make win-setup-toolchain-mac |
One-time: Homebrew clang-cl/lld/xwin + WTL + foobar SDK + colima/docker |
make win-build-mac |
Cross-compile build-win-mac/foo_navidrome.dll |
make win11-arm |
Start the VM and open its screen at http://127.0.0.1:8006 (VNC=0: no browser). The first run downloads Windows (win11-arm-prepare, ~8 GB) and installs it unattended (~20-30 min) |
make win11-arm-web |
Open the screen once it answers (also win11-arm-open; win11-arm-vnc = start + open) |
make win11-arm-up / -down |
Start / stop the VM only; it runs under launchd, so closing the terminal doesn't stop it |
make win11-arm-ssh / -logs / -status / -shot |
Shell in the guest (tester/tester), QEMU log, state, screenshot |
make win11-arm-reset |
Delete the Windows disk; the next start installs again |
make win11-arm-provision |
One-time after the install: your ssh key and foobar2000 (ARM build) in the guest. FB2K_ARCH=x86 also installs a portable 32-bit foobar2000 in C:\fb2k-x86 (profile copied from the ARM one) for x86 builds; with the same FB2K_ARCH=x86, fb2k, seed and scripts/win11-arm.sh deploy <x86 dll> use it |
make win11-arm-seed |
Copy this Mac's foo_navidrome settings (server, account) into the guest; a server on your LAN is relayed through the Mac |
make win11-arm-deploy |
Build the DLL and install it in the guest's foobar2000, then restart it |
make win11-arm-fb2k ARGS='start|stop|cmd <menu command>' / win11-arm-log |
Drive foobar2000 in the guest's desktop session / read its foo_navidrome.log |
make win11-arm-cfg ARGS="SQL" |
Change a guest setting in its config.sqlite (stops foobar2000 first) |
make win11-arm-test |
Build the unit tests for Windows and run them in the guest |
No Mac needed. The VM is the sibling repo ../macos-devbox (its mvm CLI): x86_64 macOS
on QEMU/KVM (dockur/macos), with the disk on a host
bind mount, instant snapshots and a browser screen. The component comes from the CI
release (a universal binary, so it has the x86_64 slice the guest needs) or a local
.fb2k-component. It can also be built in the guest once Xcode is installed there. Needs
a Linux host with /dev/kvm. Apple's macOS EULA only permits virtualization on Apple
hardware. See scripts/mac-vm/README.md.
| Command | What it does |
|---|---|
make mac-vm [VNC=0] |
Boot the VM, open its screen (noVNC) in the browser, deploy the latest GitHub release, launch foobar2000 |
make mac-vm-vnc |
Boot the VM (no-op if it's already up) and open its screen in the browser. Deploys nothing |
make mac-vm-open |
Open the VM screen once macOS has booted (waits for the guest's sshd first) |
make mac-vm-test [COMPONENT=β¦] |
Deploy a local .fb2k-component (default: newest in repo root), re-sign, relaunch |
make mac-vm-smoke |
Build in the guest with the debug log, deploy, then run the scripted UI smoke test (open browser β expand artist β play album) with log/crash assertions and screenshots |
make mac-vm-ui ARGS=β¦ |
ui-test.sh mac without the build: smoke [COMPONENT], playqueue, browser, log 50 |
make mac-vm-build[-test] |
Build in the guest with xcodebuild, pull the .fb2k-component back (then deploy) |
make mac-vm-<cmd> |
Any mvm command: -up, -down, -ssh, -shot, -snapshot ARGS=name, β¦ |
Wine runs the component fine, but its GUI isn't Windows: it reports Dark Mode as unsupported,
draws some controls differently and needs workarounds for DPI scaling. For those cases there's
a real Windows 11 VM, the sibling repo ../windows-devbox (its wvm CLI):
dockur/windows on QEMU/KVM, installed unattended (no
clicks), with SSH, snapshots, a browser screen, VNC input and foobar2000 x64. The Wine targets
above stay the fast loop. The guest's Z: is a host folder, so the debug build's log
(Z:\tmp\foo_navidrome_debug.log) is read on the host as-is.
| Command | What it does |
|---|---|
make win11 [VNC=0] |
Boot the VM, open its screen (noVNC) in the browser, wait for the desktop, deploy the latest GitHub release |
make win11-vnc |
Boot the VM (no-op if it's already up) and open its screen in the browser. Deploys nothing |
make win11-open |
Open the VM screen once the container's web viewer answers |
make win11-seed |
Copy foo_navidrome's settings (server, account, AudioMuseβ¦) from the Wine profile into the VM's foobar2000 |
make win11-test |
Cross-build the x64 DLL (debug log on), deploy it into the VM, relaunch |
make win11-smoke |
Build, deploy, then run the scripted UI smoke test (open browser β expand artist β play album) with log/crash assertions and screenshots |
make win11-ui ARGS=β¦ |
ui-test.sh win11 without the build: smoke [DLL], playqueue, search, cui, prefs radio, log 50 |
make win11-logs |
Follow the guest's debug log |
make win11-<cmd> |
Any wvm command: -up, -down, -shot, -snapshot ARGS=name, -dpi ARGS=144, -theme ARGS=dark, β¦ |
| Script | What it does |
|---|---|
scripts/mac-ci-build.sh <version> |
macOS Release build + packaging, run by the release workflow's build-mac job (in parallel with the Windows builds) |
scripts/release-package.sh <version> |
merges the prebuilt macOS component and Windows DLLs into the release asset, invoked by semantic-release during a release |
The Windows CI build is driven by .github/workflows/build-windows.yml (MSBuild on a
windows-latest runner) β no shell script involved.
foo_navidrome/
βββ src/
β βββ core/ # Shared by both platforms β all logic lives here
β β βββ main.cpp # Component version, advconfig switches, log init, Copy Diagnostics (SDK)
β β βββ Navidrome*.cpp # SDK glue, one file per topic: Input, TrackInput, ArtExtractor, Scrobbler,
β β β # BrowserEnqueue, PlaylistSync, Menus, AudioMuse, PlayQueue, Services, Settings
β β βββ NavidromeGuids.h # Every GUID the component registers
β β βββ NavidromeSettings.h # Shared cfg_* settings
β β βββ stdafx.h # Shared precompiled/prefix header
β β βββ SubsonicTypes.h # Data types, URI codec, JSON parser, Subsonic mappers (SDK-free)
β β βββ SubsonicCore.h/.cpp # Subsonic API core over IHttpTransport/ISettingsProvider (SDK-free)
β β βββ CoreBrowserClient.h # IBrowserClient over SubsonicCore, used by both platforms (SDK-free)
β β βββ NavidromeBrowserModel.h/.cpp # Browser tree model behind IBrowserClient (SDK-free)
β β βββ NavidromeDebugLog.h # NAVIDROME_LOG/WARN/ERR file logger
β β βββ MediaEnrichmentLogic.h/.cpp # Cover-art/URL/ESLyric-config helpers (SDK-free; MD5 is the only #ifdef)
β β βββ NavidromeLibraryPlatform.h # Seams each platform implements (core, client, cover bytes, open browser)
β β βββ Navidrome{Rating,Library,Lyrics}Service.h # Cross-component service contracts
β βββ platform/
β βββ mac/ # macOS: ObjC++ adapters + AppKit UI
β β βββ SubsonicClient.h/.mm # NSURLSession transport over SubsonicCore
β β βββ NavidromePlugin.mm # registration: main prefs page, library_viewer, ui_elements
β β βββ Navidrome*PrefsController.mm # Radio / Libraries / Browser Sections / AudioMuse-AI prefs pages
β β βββ MacSubsonicBrowserClient.* # platform seams (core, client, cover bytes)
β β βββ NavidromeBrowserController.* # browser NSViewController
β β βββ NavidromeLyricsController.* # Navidrome Lyrics panel
β β βββ NavidromePreferencesController.* # main prefs NSViewController
β βββ win/ # Windows: Win32/ATL adapters + UI
β βββ foo_navidrome.vcxproj # Visual Studio project
β βββ stdafx.h/.cpp # Windows precompiled header
β βββ SubsonicClientWin.h/.cpp # WinHTTP transport over SubsonicCore
β βββ WinHttpClient.h/.cpp # the one WinHTTP request helper
β βββ NavidromePluginWin.cpp # main prefs page, Custom Headers window, Media Library page
β βββ *PrefsWin.cpp # Radio / Libraries / Browser Sections / AudioMuse-AI prefs pages
β βββ NavidromeLibraryServiceWin.cpp # platform seams (core, cover bytes)
β βββ BrowserWindow.h/.cpp # ATL browser window
β βββ EsLyricBridge.* / EsLyricScript.h # ESLyric config + searcher script
βββ third_party/wtl/ # Vendored WTL headers (Windows builds)
βββ third_party/columns_ui-sdk/ # Vendored Columns UI SDK (0BSD; the Columns UI panel)
βββ tests/ # cross-platform unit tests β Windows (vcxproj), Linux (make test), macOS (make mac-test)
βββ scripts/ # build / install / toolchain helpers
β βββ mac-dev-build.sh # macOS dev loop (bump + xcodebuild + install)
β βββ mac-ci-build.sh # macOS CI build (called by semantic-release)
β βββ install-macos.sh # macOS install + package helper
β βββ win-setup-toolchain.sh # Linux: provision clang-cl + xwin SDK/ATL + WTL
β βββ win-build-local.sh # Linux: cross-compile the Windows DLL + install
β βββ component-sources.sh # Windows source list, parsed from the vcxproj
β βββ run-unit-tests.sh # build + run the unit tests (per-host toolchain; called by the build scripts)
β βββ navidrome-logs.sh # follow the colourised component debug log (win + mac)
β βββ install-windows.sh # Windows install + package helper
β βββ win-test.sh # build the Windows DLL on CI + install locally
βββ foo_navidrome.xcodeproj/ # Xcode project
βββ foo_navidrome.xcworkspace/ # Xcode workspace (includes SDK projects)
foo_navidrome keeps its own log, foo_navidrome.log, in your foobar2000 profile
folder. It records warnings and errors by default. To get more detail while you
reproduce a problem, enable Preferences βΊ Advanced βΊ Tools βΊ Navidrome: verbose
logging. The change applies immediately, and you can turn it back off afterwards.
When you report a bug, open Preferences βΊ Tools βΊ Navidrome and click Copy
Diagnostics, then paste the result into the issue. It contains the component,
foobar2000 and OS versions, your server's type and version, the relevant settings
and the last part of the log. Credentials and custom header values are never
included, and your server's address is replaced by <server>. Open Log Folder
(Show Log in Finder on macOS) shows the whole file.
Open a bug report: the form walks you through these steps and has a field for the diagnostics.
Pull requests are welcome. This section is the fast path from a fresh clone to a merged change.
You don't need a Mac and a Windows PC to contribute. Any one of the setups below can build and run the component; CI builds every platform on each PR. For reference, this is what the project is developed and tested with today:
| Used for development | Minimum / notes | |
|---|---|---|
| Dev OS | Linux (Omarchy, Arch-based) | macOS or Windows work too (see the table below) |
| foobar2000 | v2.26 for Windows (x64, under Wine 11 Staging) and v2 for Mac (latest) | v2.0+ (auto-skipping tracks deleted on the server needs 2.26) |
| foobar2000 SDK | SDK-2026-10-01 from reupen/foobar2000-sdk-unmodified (CI clones the latest tag) |
SDK-2026-09-16 or newer: it adds skipTrack.h and requires C++20 |
| C++ standard | C++20 for the component, C++17 for the SDK-free unit tests | |
| Windows compiler | clang-cl / lld-link (LLVM 22) cross-compiling on Linux | Visual Studio 2022 (MSVC toolset v143, Desktop development with C++ + ATL); this is what CI uses (windows-2022 runner) |
| Windows SDK / CRT / ATL | Windows SDK 10.0.26100 + MSVC CRT/ATL, fetched by xwin 0.9.0 | whatever VS 2022 installs |
| WTL | WTL 10 (10.01 vendored in third_party/wtl/ for CI; the Linux setup script fetches 10.0.10320) |
WTL 10 |
| Columns UI SDK | reupen/columns_ui-sdk 8.1.0, vendored in third_party/columns_ui-sdk/ (VERSION = commit); only ui_extension.cpp is compiled in |
nothing to install; tested against Columns UI 3.7.0 |
| macOS toolchain | Xcode 15.4 (macOS 14.5 SDK) | Xcode 15+; deployment target macOS 12.0 |
| Runtime testing | Wine (fast loop), a Windows 11 VM (dockur/windows), and a macOS 14 Sonoma VM (dockur/macos) | any real Windows 10/11 or macOS 12+ machine with foobar2000 v2 |
| Build tooling | GNU Make, bash, git, curl, unzip/zip | macOS ships bash 3.2; the scripts stay compatible with it |
Pick whichever path matches the machine you have:
| You have | Build with | Run foobar2000 on |
|---|---|---|
| Linux | ./scripts/win-setup-toolchain.sh once, then make win-build-launch (details) |
Wine |
| Windows | Visual Studio 2022 β src/platform/win/foo_navidrome.vcxproj (details) |
native |
| macOS | Xcode β foo_navidrome.xcworkspace, or ./scripts/mac-dev-build.sh (details) |
native |
The unit tests (tests/) don't need the SDK or foobar2000 at all: make test (Linux,
clang-cl + Wine) or make mac-test (plain clang++).
Every local build compiles in the debug log (/tmp/foo_navidrome_debug.log), so keep a
second terminal tailing it while you click around. make help lists every target.
Linux (Windows build, running under Wine), the main dev loop:
./scripts/win-setup-toolchain.sh # once: LLVM, xwin SDK/CRT/ATL, WTL, foobar2000 SDK siblings
make test # unit tests (clang-cl + Wine), seconds
make win-build-launch # cross-compile the x64 DLL, install it, restart Wine foobar2000
make win-logs # second terminal: follow the debug log, colourised
make win-ui-smoke # optional: scripted UI run + log assertions (see below)Configure your server once under Preferences βΊ Tools βΊ Navidrome, then File βΊ Open
Navidrome Browser. After each code change, make win-build-launch again; foobar2000 only
loads a new DLL on restart, which is why plain make win-build isn't enough. If a build
behaves strangely after editing a shared header, rebuild with
./scripts/win-build-local.sh --clean.
macOS (native):
make mac-test # unit tests (plain clang++)
# quit foobar2000 first: replacing the bundle under a running process gets it killed
make mac-build # xcodebuild Release + install into ~/Library/foobar2000-v2
make mac-logs # second terminal: follow the debug log
# start foobar2000, then File βΊ Open Navidrome BrowserWindows (native, Visual Studio):
- Build
tests/MediaEnrichmentTests.vcxproj(Release | x64) and run it: same suite asmake test. - Build
src/platform/win/foo_navidrome.vcxproj(Release | x64), see Building on Windows. - Copy
foo_navidrome.dllto%APPDATA%\foobar2000\user-components\foo_navidrome\and restart foobar2000.
No Windows machine and no Wine? make win-test builds on a GitHub Actions Windows runner
(real MSVC) and downloads the artifact.
Each change has to work on both platforms, and you rarely have both at hand. The maintainer runs one in a container on the same Linux box:
| VM | Based on | Used for | Typical loop |
|---|---|---|---|
Windows 11 (../windows-devbox, wvm) |
dockur/windows, unattended install | what Wine fakes or lacks: Dark Mode, DPI scaling, native control theming | make win11-seed once (copies your server settings from the Wine profile), then make win11-test / make win11-smoke, make win11-logs |
macOS 14 Sonoma (../macos-devbox, mvm) |
dockur/macos | the macOS build from a Linux machine: it compiles with Xcode inside the guest and runs there | make mac-vm (boot, open the screen, deploy the latest release), make mac-vm-build-test, make mac-vm-smoke |
Both VMs keep their disk on a host bind mount with reflink snapshots (make mac-vm-snapshot,
make win11-snapshot), show their screen in the browser over noVNC (make mac-vm-open,
make win11-open), and accept host-side input and screenshots (mvm click|key|shot,
wvm click|shot), so they can be driven without touching them by hand. Every mvm/wvm
subcommand is also reachable as make mac-vm-<cmd> / make win11-<cmd>, e.g.
make win11-dpi ARGS=144 or make win11-theme ARGS=dark.
The macos-devbox/windows-devbox helper repos are the maintainer's own wrappers around the
dockur images and expected as siblings of this repo. Without them you can still test on any
real Windows or Mac machine; the VM targets are there to cover the platform you don't have.
The UI smoke scripts launch the real foobar2000 and go through a fixed scenario: open the
Navidrome Browser, expand an artist, press Enter on an album. They then check the debug log,
that the process is still alive and that there are no crash reports, and save screenshots
under build/ui-test/:
| Platform | Run the scenario | Drive it step by step |
|---|---|---|
| Wine | make win-ui-smoke, make win-ui ARGS=playqueue / search / i18n / offline / solo |
make win-ui ARGS='click X Y' / 'key down' / 'shot' / 'prefs main' |
| Windows 11 VM | make win11-smoke, make win11-ui ARGS=playqueue / search / cui / i18n / offline / solo |
make win11-ui ARGS='prefs radio' / 'browser' / 'log 50' |
| macOS VM | make mac-vm-smoke, make mac-vm-ui ARGS=playqueue / i18n / solo |
make mac-vm-ui ARGS='browser' / 'log 50', make mac-vm-click ARGS='x y' |
i18n runs once in Spanish; offline turns prefetch on and checks that the next track plays
from the cache; solo moves the sister component Panels UI
out of the profile for the run and checks foo_navidrome still works without it (Panels UI's
own ui-test.sh <platform> solo checks the other direction). Every scenario restores what it
changed. The scenarios expect an English UI: a profile set to Spanish is switched to English
for the run.
All three rows are the same script, scripts/ui-test.sh <platform> <scenario>: a scenario is
written once and each platform only supplies the low-level actions. To see whether Wine
behaves like real Windows (it's an emulation, and it doesn't always), run the same scenario
on both and compare: make ui-compare ARGS='playqueue wine win11' prints a step-by-step
matrix and marks the rows where the platforms differ (PARALLEL=1 runs them at the same
time; add mac for the macOS VM).
Under Wine, input goes to the browser window as posted messages (tools/wclick.c), so your
own mouse and keyboard stay free. Screenshots use grim + hyprctl, i.e. a Hyprland host.
The VMs take VNC input.
These commands are built so a coding agent (the maintainer uses Claude
Code) can verify a change end to end without help: it
builds with one make target, drives the UI with win-ui/win11-ui/mac-vm-ui, reads the
debug log to check what the component did, and looks at the screenshots to check what it
drew. CLAUDE.md holds the project rules for such an agent: every change on
both platforms, tests run, smoke test extended for new UI. When you add a feature, extend
the smoke scenario so it covers that feature, and the next change (yours or an agent's) gets
checked against it too.
- Fork and clone the repo, then lay out the sibling SDK directories described under Prerequisites β the project will not build without
pfc/andfoobar2000/{SDK,helpers,helpers-mac,shared,foobar2000_component_client}next to it. The CI workflow clones reupen/foobar2000-sdk-unmodified into exactly that layout if you want a reference. - Pick your platform's build path: macOS (Xcode), Windows (Visual Studio 2022), or Linux β Windows cross-compile (clang-cl + Wine, the current primary dev environment).
- Read
CLAUDE.mdbefore touching code β it documents the architecture, the per-platform file map, the design decisions, and a long list of hard-won gotchas (URI parsing traps, dual-mountNSViewControllerrules, CI log handling, etc.). It will save you hours.
The component is a shared C++ core + per-platform UI/HTTP layers β see Project Structure for the full file map. Quick orientation:
src/core/β shared by both platforms: Subsonic API (SubsonicCore), data types/URI/JSON (SubsonicTypes.h), browser tree model (NavidromeBrowserModel), and the SDK glue (main.cppplus oneNavidrome*.cppper topic: input handler, art extractor, scrobbler, menus, play queue, services). New behaviour goes here.src/platform/mac/(ObjC++):SubsonicClient.mm(NSURLSession transport),NavidromePlugin.mm(prefs page and UI element registration), plus the AppKit browser, lyrics panel and preferences UI.src/platform/win/(Win32/ATL):SubsonicClientWin.cpp+WinHttpClient.cpp(WinHTTP transport),NavidromePluginWin.cppand the*PrefsWin.cpppages,BrowserWindow.*.
Platform folders only hold transport, widget and wiring code β a data-layer fix belongs in src/core/ so both platforms get it.
navidrome://track/<id>?...URI scheme. Tracks are queued as these URIs, not raw HTTP URLs, so playlists survive credential/server changes. Metadata is embedded in the query string; the stream URL is resolved at decode time. Any code that parses these URIs must be updated together when the scheme changes βNavidromeInput, the art extractor'sis_our_path, etc. The host-vs-path RFC-3986 trap is documented inCLAUDE.md.- GUIDs. Every registered service has a hardcoded GUID. If you fork this component you must regenerate all of them (they're all in
src/core/NavidromeGuids.h) β two components sharing a GUID will collide in foobar2000. - Logging is the debugger. There's no practical debugger-attach for foobar2000 components (Mac or Wine); use the
NAVIDROME_LOG/WARN/ERRmacros (src/core/NavidromeDebugLog.h, enabled withNAVIDROME_DEBUG_LOG; they write/tmp/foo_navidrome_debug.log, line by line so it survives a crash;make win-logs/make mac-logstail it). Remove temporary logs before submitting.
- Match the surrounding style (naming, comment density, indentation) rather than introducing a new one.
- Keep platform-specific code in
src/platform/{mac,win}/β don't#ifdefplatform branches into the shared core unless there's no alternative. - Update
CLAUDE.mdwhen you make a decision or hit a gotcha worth recording for the next contributor.
- Commit messages must follow Conventional Commits β the release pipeline parses them to decide the version bump. See Commit message convention. In short:
feat:β minor,fix:/perf:/refactor:β patch,chore:/docs:/ci:/etc. β no release,!or aBREAKING CHANGE:footer β major. - Never bump a version by hand β releases are tags; the pipeline stamps the version. Release notes come from the commits; your job is just well-typed commits. Changes reach
mainthrough pull requests. - Test on at least one platform and say which one in the PR. Cross-platform changes ideally get verified on both macOS and a Windows build (native, or cross-compiled + run under Wine β see the Linux section).
- Keep PRs focused; one logical change per PR makes review and the auto-generated changelog far cleaner.
- Offline / caching support for streamed tracks.
Releases are automated. Every push to main triggers .github/workflows/release.yml, which (nothing is published unless both platforms build, and nothing is committed back to main):
- Lays out the sibling-directory tree the project expects (
pfc/,foobar2000/{SDK,helpers,helpers-mac,shared,foobar2000_component_client}) by cloning reupen/foobar2000-sdk-unmodified β an unmodified mirror of the official SDK that includeshelpers-mac/. - Runs
semantic-releaseper.releaserc.jsonas a dry run: it inspects commits since the last tag and decides whether a release is needed, and which version. - If a release is needed:
- The reusable
build-windows.ymlworkflow builds the Windows x86/x64/ARM64EC DLLs of that commit (MSVC/MSBuild), stamped with the new version. - Only then, on macOS: the unit tests run, and semantic-release for real β
scripts/mac-ci-build.sh <next-version>stamps that version, runsxcodebuild -configuration Release, ad-hoc signs, and packagesfoo_navidrome_<version>.fb2k-component; av<version>tag and a GitHub release (notes = changelog) are created with it attached. - The Windows DLLs are merged into that same
.fb2k-component(macOS + Windows in one file).CHANGELOG.mdholds the history up to 1.18.0; later releases are on the Releases page.
- The reusable
The pipeline reads Conventional Commits. The commit type determines whether (and how) the version is bumped:
| Type | Effect |
|---|---|
feat: β¦ |
minor bump |
fix: β¦ / perf: β¦ |
patch bump |
refactor: β¦ |
patch bump |
chore: β¦ / docs: β¦ / style: β¦ / test: β¦ / ci: β¦ |
no release |
feat!: β¦ or footer BREAKING CHANGE: |
major bump |
The version a build reports comes from scripts/version.sh: the release version in CI, git describe for any other build (1.21.2-dev.3+2a46400), and, in a source zip without .git, version.txt β which git archive fills in from the tags (export-subst), so nobody bumps a version by hand. It writes the gitignored version_generated.h (the Xcode "Generate Version Header" phase and the Windows build scripts all call it).
# Build, install locally, package, and create a GitHub release in one shot
./scripts/mac-dev-build.sh --patch --new-releaseThis bypasses the workflow and uses the --new-release path of scripts/install-macos.sh.
This component's own source code is licensed under the MIT License β see the LICENSE file.
It builds against the foobar2000 SDK and PFC, which are not covered by this license β they are distributed under their own terms by the foobar2000 author and are not included in this repository (the build fetches them separately; see the build instructions above). Redistribution of the SDK is subject to those terms.