Subsonic / Navidrome Integration

SkillSecurity

Subsonic/Navidrome API, authentication, music folder selection, streaming, and scrobbling. Use when working on Subsonic/Navidrome integration, library browsing, music folder filtering, or playback reporting.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Subsonic / Navidrome Integration skill

What this skill tells your AI

The instructions your AI receives, as published by ad-repo/nullplayer in skills/subsonic-integration/SKILL.md and read by ahel’s review.

NullPlayer supports Subsonic-compatible servers (including Navidrome) for music streaming, browsing, and scrobbling.

Architecture

FilePurpose
Subsonic/SubsonicModels.swiftDomain models (Server, Artist, Album, Song, Playlist, MusicFolder) and API DTOs
Subsonic/SubsonicServerClient.swiftHTTP client for Subsonic REST API
Subsonic/SubsonicManager.swiftSingleton managing connections, caching, music folder selection, and track conversion

Authentication

Token-based auth is used (Subsonic API 1.13.0+):

GET /rest/{endpoint}?u={username}&t={md5(password+salt)}&s={salt}&v=1.16.1&c=NullPlayer&f=json
  • t = MD5 hash of password + salt
  • s = random salt string (16 chars)
  • f=json added to all API requests (omitted for stream/binary endpoints)

Music Folders

Navidrome/Subsonic can have multiple music folders (root library directories).

  • Fetch: GET /rest/getMusicFolders — returns all configured music folders
  • SubsonicMusicFolder: { id: String, name: String }
  • SubsonicManager.musicFolders — available folders fetched on connect
  • SubsonicManager.currentMusicFolder — nil means all folders; posts musicFolderDidChangeNotification on change
  • selectMusicFolder(_ folder:) — sets folder, clears cache, triggers preload
  • clearMusicFolderSelection() — resets to nil (all folders), clears cache, triggers preload
  • Persisted via SubsonicCurrentMusicFolderID UserDefaults key
  • Auto-selection on connect: saved ID → nil (all folders, valid default)
  • musicFolderId is passed to getArtists and getAlbumList2 when a folder is selected

Library Browser UI

The "Lib:" zone in the status bar shows the current folder name ("All" when nil). Click to open a folder picker menu.

API Endpoints

Library Browsing

  • Artists: GET /rest/getArtists?musicFolderId={id} — indexed A-Z; musicFolderId optional
  • Artist detail: GET /rest/getArtist?id={artistId} — returns artist + album list
  • Albums: GET /rest/getAlbumList2?type={type}&size={n}&offset={n}&musicFolderId={id}musicFolderId optional
  • Album detail: GET /rest/getAlbum?id={albumId} — returns album + track list
  • Song: GET /rest/getSong?id={songId}
  • Search: GET /rest/search3?query={q}&artistCount={n}&albumCount={n}&songCount={n}
  • Playlists: GET /rest/getPlaylists
  • Playlist detail: GET /rest/getPlaylist?id={playlistId}
  • Starred: GET /rest/getStarred2

Album List Types (getAlbumList2)

TypeDescription
alphabeticalByNameA-Z by album title
alphabeticalByArtistA-Z by artist
newestRecently added
frequentMost played
recentRecently played
starredFavorited albums
randomRandom selection
byYearBy release year
byGenreBy genre

Streaming & Images

  • Stream: GET /rest/stream?id={songId} — omit f=json; Navidrome returns binary
  • Cover art: GET /rest/getCoverArt?id={coverArtId}&size={px} — omit f=json

User Actions

  • Star: GET /rest/star?id={songId} / albumId={id} / artistId={id}
  • Unstar: GET /rest/unstar?id={songId} / albumId={id} / artistId={id}
  • Rate: GET /rest/setRating?id={songId}&rating={1-5} (0 = remove)
  • Scrobble: GET /rest/scrobble?id={songId}&submission=true

State Persistence

  • Current server ID: SubsonicCurrentServerID (UserDefaults)
  • Current music folder ID: SubsonicCurrentMusicFolderID (UserDefaults) — nil = all folders
  • Credentials: macOS login keychain via KeychainHelper (key: subsonic_servers) — uses permissive SecAccessCreate ACL; do NOT add kSecUseDataProtectionKeychain (breaks ad-hoc DMG builds with -34018)

Notifications

NotificationPosted when
serversDidChangeNotificationservers array changes
connectionStateDidChangeNotificationConnection state changes
libraryContentDidPreloadNotificationBackground preload completes
musicFolderDidChangeNotificationcurrentMusicFolder changes

Track Identification

Subsonic tracks in the playlist are identified by:

  • track.subsonicId — the Subsonic song ID
  • track.subsonicServerId — which server the track belongs to

Scrobbling

Scrobble threshold: 50% of track duration or 4 minutes, whichever comes first. Reports "now playing" immediately on start via scrobble(id:submission:false).

Casting (Sonos)

Subsonic streaming URLs contain auth query parameters which Sonos cannot handle. NullPlayer proxies audio through LocalMediaServer before casting. Stream URLs also omit f=json so Navidrome returns binary audio data.

Key Gotchas

  • f=json on stream endpoints: Always use streamAuthParams() (not authParams()) for stream/image URLs — these omit f=json which would otherwise cause Navidrome to return JSON instead of binary data
  • musicFolderId scoping: When a music folder is selected, pass its ID to both getArtists and getAlbumList2. The folder filter is applied server-side; client cache is cleared on folder change to avoid showing stale cross-folder content
  • Folder IDs are integers server-side: The Subsonic API returns id as an integer in getMusicFolders. SubsonicMusicFolder stores it as String (converted from Int in the DTO) for consistency with other IDs
  • Library selector in browser: The "Lib:" click zone in both ModernLibraryBrowserView and classic PlexBrowserView opens an "All Folders" + folders picker for Subsonic/Navidrome sources. SubsonicManager tracks musicFolders: [SubsonicMusicFolder] and currentMusicFolder: SubsonicMusicFolder? (nil = all folders). Posts musicFolderDidChangeNotification on change; persisted via SubsonicCurrentMusicFolderID UserDefaults key.
  • Streaming URL content type (Sonos): Subsonic stream URLs (/rest/stream?id=...) have no file extension, so detectAudioContentType(for:) defaults to audio/mpeg. This breaks Sonos casting for non-MP3 formats and can let high-resolution lossless tracks bypass format filtering. Prefer Track.contentType set by the server client from API metadata. Preserve samplingRate too: strict Sonos compatibility rejects extensionless FLAC/WAV above 48 kHz, and rejects unknown-rate FLAC/WAV conservatively when no sample rate is available.

Signals

GitHub stars
120
Forks
9
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
subsonic-integration
Source
github.com/ad-repo/nullplayer