Skip to content

About

Foobar2000 component to play music from Navidrome using Subsonic API as backend

Resources

Stars

11 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

170 Commits

Folders and files

Repository files navigation

foo_navidrome

A foobar2000 component that lets you browse and stream music from a Navidrome server (or any Subsonic-compatible server) directly inside foobar2000.

Installation (end users)

  1. Download foo_navidrome_<version>.fb2k-component from 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.
  2. Drag the file onto foobar2000, or double-click it β€” foobar2000 installs it automatically
  3. Restart foobar2000
  4. Go to Preferences β€Ί Tools β€Ί Navidrome and enter your server URL and credentials

Features

  • 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's getBookmarks.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 transcodeOffset extension, 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), navidrome in 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

Lyrics (Windows)

foo_navidrome has its own Navidrome Lyrics panel on Windows:

  1. 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.
  2. 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:

ESLyric (optional)

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.

  1. Download the latest ESLyric release from https://github.com/ESLyric/release and install it the same way as foo_navidrome (drag the .fb2k-component onto foobar2000). Match the architecture to your foo_navidrome install (x64 / ARM64EC).
  2. 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.
  3. Add the ESLyric panel to your layout (Default UI / Columns UI element picker).
  4. Play a track that foo_navidrome added (a navidrome://track/<id> URI). ESLyric calls Navidrome's getLyricsBySongId.view for that song, falling back to the classic artist/title getLyrics.view lookup 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).

Lyrics (macOS)

ESLyric doesn't exist on macOS, so foo_navidrome ships its own lyrics panel:

  1. View β€Ί Layout β€Ί Edit Layout… β†’ add a navidrome-lyrics line wherever you want the panel.
  2. 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.

Instant Mix and AudioMuse-AI

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/chatPlaylist and 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.

Testing against a local AudioMuse-AI (developers)

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 search

Then 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 Support

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-component ships 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).


Building on macOS

Prerequisites

  • foobar2000 v2 for Mac
  • Xcode 15+ (developed with 15.4; the component targets macOS 12.0+)
  • foobar2000 SDK SDK-2026-09-16 or newer β€” source the SDK subdirs and pfc/ from reupen/foobar2000-sdk-unmodified (an unmodified mirror of the official SDK; crucially it ships helpers-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 pfc is in a different location, create a symlink:

ln -s /path/to/pfc /path/to/personal/pfc

Build steps

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:

  1. Open foo_navidrome.xcworkspace in Xcode
  2. Select the foo_navidrome scheme (top-left scheme selector)
  3. Build: Product β€Ί Build or Cmd+B
  4. Run the install script:
    ./scripts/install-macos.sh
  5. Restart foobar2000

Unit tests

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.

Configuration

  1. Open Preferences β€Ί Tools β€Ί Navidrome
  2. Enter your server URL (e.g. http://navidrome.local:4533/)
  3. Enter your username and password
  4. Click Test Connection β€” you should see "Connected!"

Usage

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 navidrome line 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.

Play queue sync

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.

Showing ratings in the playlist

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.

Managing server playlists

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.

Streaming quality

Preferences β€Ί Tools β€Ί Navidrome has two settings that apply to every track:

  • Stream as β€” Server default leaves 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.

Offline playback

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.

Language

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.


Building on Windows

Prerequisites

  • 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-16 or newer β€” same directory layout as above

Build steps

  1. Open src/platform/win/foo_navidrome.vcxproj in Visual Studio
  2. Update the <ProjectReference> GUIDs in the .vcxproj to match your local SDK project GUIDs
  3. Build in Release | x64 configuration (the .vcxproj also defines Win32 and ARM64EC if you need those binaries; CI builds all three)
  4. Copy foo_navidrome.dll to your foobar2000 components folder:
    %APPDATA%\foobar2000\user-components\foo_navidrome\
    
  5. Restart foobar2000

CI builds all three Windows arches (x86, x64, ARM64EC) and merges them with the macOS bundle into one foo_navidrome_<version>.fb2k-component per release. No Windows machine? Cross-compile the x64 DLL on Linux (see below), or run ./scripts/win-test.sh to build on a GitHub Actions Windows runner (real MSVC/MSBuild) and download the artifact.


Building on Linux (Wine)

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.

Prerequisites

  • foobar2000 for Windows installed under Wine (e.g. the AUR foobar2000 package)
  • llvm, clang, lld (provide clang-cl / lld-link), plus git, curl, unzip, zip

One-time toolchain setup

./scripts/win-setup-toolchain.sh

Installs 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).

Build + install loop

./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.


Scripts

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

Linux β€” build & test the Windows component (current dev environment)

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.

macOS β€” build & install the native Mac component

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)

macOS β€” build & test the Windows component in a Windows 11 ARM VM

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

Linux β€” runtime-test the macOS component in a local macOS VM

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, …

Linux β€” runtime-test the Windows component on real Windows 11

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, …

CI (GitHub Actions β€” not run by hand)

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.


Project Structure

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)

Troubleshooting and bug reports

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.

Contributing

Pull requests are welcome. This section is the fast path from a fresh clone to a merged change.

Development environment

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++).

Happy path: build it and try it live

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 Browser

Windows (native, Visual Studio):

  1. Build tests/MediaEnrichmentTests.vcxproj (Release | x64) and run it: same suite as make test.
  2. Build src/platform/win/foo_navidrome.vcxproj (Release | x64), see Building on Windows.
  3. Copy foo_navidrome.dll to %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.

Cross-platform testing in VMs (Docker)

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.

Automated UI tests, and letting an LLM agent drive them

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.

Getting set up

  1. Fork and clone the repo, then lay out the sibling SDK directories described under Prerequisites β€” the project will not build without pfc/ and foobar2000/{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.
  2. 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).
  3. Read CLAUDE.md before 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-mount NSViewController rules, CI log handling, etc.). It will save you hours.

How the code is laid out

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.cpp plus one Navidrome*.cpp per 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.cpp and the *PrefsWin.cpp pages, BrowserWindow.*.

Platform folders only hold transport, widget and wiring code β€” a data-layer fix belongs in src/core/ so both platforms get it.

Key architectural concepts to know

  • 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's is_our_path, etc. The host-vs-path RFC-3986 trap is documented in CLAUDE.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/ERR macros (src/core/NavidromeDebugLog.h, enabled with NAVIDROME_DEBUG_LOG; they write /tmp/foo_navidrome_debug.log, line by line so it survives a crash; make win-logs/make mac-logs tail it). Remove temporary logs before submitting.

Coding conventions

  • 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 #ifdef platform branches into the shared core unless there's no alternative.
  • Update CLAUDE.md when you make a decision or hit a gotcha worth recording for the next contributor.

Commits, versioning, and PRs

  • 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 a BREAKING 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 main through 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.

Good places to start

  • Offline / caching support for streamed tracks.

Releasing

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):

  1. 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 includes helpers-mac/.
  2. Runs semantic-release per .releaserc.json as a dry run: it inspects commits since the last tag and decides whether a release is needed, and which version.
  3. If a release is needed:
    • The reusable build-windows.yml workflow 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, runs xcodebuild -configuration Release, ad-hoc signs, and packages foo_navidrome_<version>.fb2k-component; a v<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.md holds the history up to 1.18.0; later releases are on the Releases page.

Commit message convention

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).

Manual release (fallback)

# Build, install locally, package, and create a GitHub release in one shot
./scripts/mac-dev-build.sh --patch --new-release

This bypasses the workflow and uses the --new-release path of scripts/install-macos.sh.

License

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.

About

Foobar2000 component to play music from Navidrome using Subsonic API as backend

Resources

Stars

11 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages