Infrasonic is an Emacs library for interacting with OpenSubsonic-compatible music servers such as Gonic, Navidrome, etc. It is designed for use in other Emacs packages wishing to implement compatibility with the OpenSubsonic API.
OpenSubsonic is an open, community-maintained REST API specification for music streaming servers. There are several free software music servers that implement this specification such as Navidrome (GNU GPL version 3 only) and Gonic (GNU GPL version 3 only), which allow users to stream personal music libraries from their own hardware. Infrasonic is a client library for communicating with this API in emacs-lisp. Details on the OpenSubsonic API can be found here.
Infrasonic handles authentication, request signing, and JSON parsing.
Authentication is handled with auth-source using the OpenSubsonic token/salt
authentication method.
Infrasonic also ensures consistency between returned media objects by adding:
- A
nameelement to songs. - An
infrasonic-typeelement indicating whether an item is a song, album, artist, playlist, or genre.
Other than this, the API response is untouched (other than the JSON -> alist parsing).
I'm aware that many people have strong opinions about the use of AI in software projects. I will try to best outline how I used it in the creation of this package.
The ERT test suite is more-or-less 100% AI created. I find writing unit tests boring, and I'm sure many others would agree, so I don't see a real issue with just mass-creating unit tests. The tests have been reviewed by me (an emacs-lisp rookie), and I would say they have pretty good coverage and seem quite sane.
The main code is 100% written by me, with infrequent Q&A's with one AI chatbot or another for things I am unfamiliar with in Elisp. At worst, I may have followed a general algorithm suggested by an AI, but 99% of my AI use in this file is simply "Is this an idiomatic way to write this function?", or "Do you see any issues with this?", or "How do I do in elisp?".
- Emacs
>=27.1, the minimum shared withplz. Infrasonic uses the native JSON parser when available and falls back to the built-in Lisp parser. plz>=0.9(ELPA) for HTTP requests.- An OpenSubsonic-compatible server (Navidrome (GPL-3.0), Gonic (GPL-3.0), etc.).
Hopefully at some point, this package will be available on a package repository. For now:
Manual: Clone the repository and add it to your load-path:
(add-to-list 'load-path "/path/to/infrasonic_repo")
(require 'infrasonic)straight:
(straight-use-package
'(infrasonic :type git :host github :repo "kaibagley/infrasonic"))elpaca
(use-package infrasonic
:ensure '(infrasonic :repo "kaibagley/infrasonic")
...)infrasonic requires you to create a client, which is a struct containing a
few important details. The client is passed as the first argument to the
infrasonic API functions, allowing multiple clients to be used at once.
- Authentication
infrasonic uses auth-source for secrets. Add a line to your ~/.authinfo
containing your OpenSubsonic server's URL.
machine music.example.com login my_username password my_secret_password
If auth-source contains multiple logins for the same host, set the client's
:username so the intended entry is selected.
- Client
In your package, a client must be created for each server you wish to access. It can be done as follows:
;; (default values)
(defvar my-music-client
(infrasonic-make-client
:url ; (no default) FQDN for OpenSubsonic server
:username ; (nil) Optional auth-source login selector
:protocol ; ("https") "http" or "https"
:user-agent ; ("infrasonic") Name of your player
;; Implemented OpenSubsonic REST API version.
;; See https://opensubsonic.netlify.app/docs/subsonic-versions/
;; for the mapping between OpenSubsonic version and REST API version.
:api-version ; ("1.16.1")
:queue-limit ; (5) Limit of concurrent downloads of art and music
:timeout ; (300) Timeout for every HTTP request
:art-size ; (64) Edge size in pixels to download cover art
:search-max-results ; (200)) Max results to return in query searches- Low-level Functions
Below are some basic examples of the API functions
;; Test connection
(infrasonic-ping my-music-client)
;; Search for a query (returns artists, albums, and songs)
(infrasonic-search my-music-client "Bilmuri")
;; Get all playlists
(infrasonic-get-playlists my-music-client)
;; Rename a playlist or make it private without replacing its songs
(infrasonic-update-playlist my-music-client "playlist-123"
:name "Road trip"
:visibility :private)
;; Replace a playlist's complete track list
(infrasonic-replace-playlist my-music-client "playlist-123"
'("song-1" "song-2"))
;; Get 50 random songs
(infrasonic-get-random-songs my-music-client 50)
;; Scrobble a song (ID "id-123") as "Now Playing"
(infrasonic-scrobble my-music-client "id-123" :playing)
;; Star a song
(infrasonic-star my-music-client "id-123" t)
;; Star an ID3 album (raw IDs default to songs)
(infrasonic-star my-music-client "album-123" t nil nil :album)
;; Discover extensions such as formPost
(infrasonic-get-open-subsonic-extensions my-music-client)
;; API call with callback and errback (callback enables async requests)
;; Most functions support callbacks for asynchronous execution
(infrasonic-api-call my-music-client
"ping" ; Endpoint
nil ; Parameters
(lambda (resp) ; Callback function
(message "Server says: %s" (alist-get 'status resp)))
(lambda (err) ; Error callback function
(user-error "Server errored: %S" err)))- High-level functions
infrasonic also provides some higher-level functions:
;; Get all songs recursively under an artist or album
(infrasonic-get-all-songs my-music-client "id-123" :artist)I have included a few ERT tests to prevent myself from cooking things.
- ERT (built-in to Emacs >24.1)
With plz installed through package.el:
make test
make checkFor another installation location, pass it explicitly, for example:
make test PLZ_LOAD_PATH="$HOME/.config/emacs/elpaca/builds/plz"There are some integration tests against a containerised Navidrome server that
may be run. The server is on 127.0.0.1:45556, with temporary data at /tmp.
The tests require Podman or Docker, ffmpeg, and curl. Run the server and
tests using:
make integrationThe container is automatically stopped after the tests, including when a test fails. You may leave the test server running for manual testing with Infrasonic using:
make navidrome-start
# Test manually, using the URL and credentials printed by the command.
make navidrome-stopThe standard OpenSubsonic authentication method derives each request token as:
- Generate a random salt.
- Calculate
token = md5(concat(password, salt)).
The salt defeats precomputed rainbow tables, but MD5 is deliberately fast. An attacker who obtains a token and salt can therefore test password guesses offline very cheaply. HTTPS protects those values in transit and should always be used across untrusted networks. A strong, unique server password still matters.
This is an issue with the OpenSubsonic API specifications, not any particular client implementation. OpenSubsonic has specified an API key extension to alleviate this issue (see this OpenSubsonic extension); server support varies.
Credentials are sourced from auth-source and cached in the
infrasonic-client struct; call infrasonic-clear-credentials to discard the
cache. Infrasonic does not write them to disk. Credentials, request URLs,
tokens, and salts necessarily exist in Emacs memory while requests are active.
Streaming URLs also contain authentication parameters, so applications that
pass them to an external player should consider local process inspection and
player logs part of their threat model.
Playlist creation and POST-based playlist changes require the OpenSubsonic
formPost extension. Use infrasonic-get-open-subsonic-extensions and
infrasonic-extension-supported-p when an application needs to check support
before showing those operations.
Click for a full data structure example
(album), (artist), (song), etc. refer to the full parsed list construct.
;; This is an example of the full parsed JSON returned from `infrasonic-search'
;; to my gonic server.
(((infrasonic-type . :artist)
(id . "ar-461")
(name . "The Human Abstract")
(albumCount . 5))
((infrasonic-type . :album)
(id . "al-6872")
(created . "2024-03-14T19:47:03.288353577+08:00")
(artistId . "ar-479")
(artist . "Tigercub")
(artists ((id . "ar-479") (name . "Tigercub")))
(displayArtist . "Tigercub")
(title . "Abstract Figures in the Dark")
(album . "Abstract Figures in the Dark")
(coverArt . "al-6872")
(name . "Abstract Figures in the Dark")
(songCount . 0)
(duration . 0)
(playCount . 0)
(genre . "Rock")
(genres ((name . "Rock")))
(year . 2016)
(isCompilation)
(releaseTypes "Album"))
((infrasonic-type . :song)
(name . "The Abstract of a Planet in Resolve (instrumental)")
(id . "tr-7252")
(album . "To Speak, To Listen")
(albumId . "al-2004")
(artist . "Eidola")
(artistId . "ar-127")
(artists ((id . "ar-127") (name . "Eidola")))
(displayArtist . "Eidola")
(albumArtists ((id . "ar-127") (name . "Eidola")))
(displayAlbumArtist . "Eidola")
(bitRate . 320)
(contentType . "audio/mpeg")
(coverArt . "al-2004")
(created . "2025-12-09T20:59:09.034293055+08:00")
(duration . 159)
(genre . "Experimental;Experimental Rock;Post-Hardcore;Progressive Metal")
(genres ((name . "Experimental;Experimental Rock;Post-Hardcore;Progressive Metal")))
(isDir)
(isVideo)
(parent . "al-2004")
(path . "Eidola/Eidola - Album - 2017 - To Speak To Listen/0101 - The Abstract of a Planet in Resolve instrumental.mp3")
(size . 6371345)
(suffix . "mp3")
(title . "The Abstract of a Planet in Resolve (instrumental)")
(track . 1)
(discNumber . 1)
(type . "music")
(year . 2017)
(musicBrainzId . "9b2c86c9-e2f5-476a-ba9b-7c65282fc954")
(replayGain)))| Function name | API endpoint | Returns |
|---|---|---|
infrasonic-get-license |
getLicense |
License alist |
infrasonic-get-open-subsonic-extensions |
getOpenSubsonicExtensions |
List of advertised extensions |
infrasonic-get-stream-url |
stream |
Complete URL string for streaming |
infrasonic-ping |
ping |
t on synchronous success |
infrasonic-get-artists |
getArtists |
Nested index of artists: (("a" . (artist1)...) ...) |
infrasonic-get-artists-flat |
getArtists |
Flat list of artists: (("a" . (artist1)...) ...) |
infrasonic-get-artist |
getArtist |
Flat list of albums: ((album1) (album2) ...) |
infrasonic-get-artist-info |
getArtistInfo2 |
Artist info alist |
infrasonic-get-album |
getAlbum |
Flat list of songs: ((song1) (song2) ...) |
infrasonic-get-album-info |
getAlbumInfo2 |
Album info alist |
infrasonic-get-album-list |
getAlbumList2 |
List of albums |
infrasonic-get-playlists |
getPlaylists |
List of complete playlist objects |
infrasonic-get-playlist |
getPlaylist |
Playlist object with nested songs |
infrasonic-get-playlist-songs |
getPlaylist |
List of songs: ((song1) (song2) ...) |
infrasonic-get-starred-songs |
getStarred2 |
List of songs: ((song1) (song2) ...) |
infrasonic-star |
star / unstar |
Parsed response |
infrasonic-create-bookmark |
createBookmark |
Parsed response |
infrasonic-delete-bookmark |
deleteBookmark |
Parsed response |
infrasonic-get-bookmarks |
getBookmarks |
Parsed response |
infrasonic-scrobble |
scrobble |
Parsed response |
infrasonic-set-rating |
setRating |
Parsed response |
infrasonic-get-random-songs |
getRandomSongs |
List of n songs: ((song1) ...) |
infrasonic-search |
search3 |
Flat list of results: ((artist1) (album1) (song1) ...) |
infrasonic-search-songs |
search3 |
List of songs: ((song1) (song2) ...) |
infrasonic-get-song |
getSong |
Song alist |
infrasonic-get-song-async |
getSong |
plz process object |
infrasonic-get-all-songs |
getArtist / getAlbum |
List of songs: ((song1) (song2) ...) |
infrasonic-get-similar-songs |
getSimilarSongs2 |
List of songs |
infrasonic-get-songs-by-genre |
getSongsByGenre |
List of songs |
infrasonic-get-top-songs |
getTopSongs |
List of songs |
infrasonic-get-now-playing |
getNowPlaying |
List of songs with usernames |
infrasonic-create-playlist |
createPlaylist |
Playlist object: ((id . "1") (name . "foo") ...) |
infrasonic-replace-playlist |
createPlaylist |
Updated playlist object |
infrasonic-delete-playlist |
deletePlaylist |
Parsed response |
infrasonic-get-genres |
getGenres |
List of genres: ((genre1) (genre2) ...) |
infrasonic-update-playlist |
updatePlaylist |
Parsed response |
infrasonic-get-play-queue |
getPlayQueue |
Play queue alist |
infrasonic-save-play-queue |
savePlayQueue |
Parsed response |
infrasonic-get-lyrics |
getLyrics |
Lyrics alist |
infrasonic-download-art |
getCoverArt |
A plz-queue |
infrasonic-download-music |
getSong and download |
A plz-queue |
infrasonic-children |
getArtists/getArtist/getAlbum |
List of items (artists, albums, or songs) |
infrasonic only plans to support ID3 endpoints (e.g. search and search2
will likely not be implemented). infrasonic will only support music and has
no plans to support podcasts or music videos.
Click to expand the API implementation checklist
| 1.0.0 | |
| download | infrasonic-download-music |
| getCoverArt | infrasonic-get-art(-url) |
| getIndexes | Not planned |
| getLicense | infrasonic-get-license |
| getMusicDirectory | Not planned |
| getMusicFolders | Not planned |
| getNowPlaying | infrasonic-get-now-playing |
| getPlaylist | infrasonic-get-playlist |
| getPlaylists | infrasonic-get-playlists |
| ping | infrasonic-ping |
| search | Not planned |
| stream | infrasonic-get-stream-url |
| 1.1.0 | |
| changePassword | Not planned |
| createUser | Not planned |
| 1.2.0 | |
| addChatMessage | Not planned |
| createPlaylist | infrasonic-create-playlist, infrasonic-replace-playlist |
| deletePlaylist | infrasonic-delete-playlist |
| getAlbumList | Not planned |
| getChatMessages | Not planned |
| getLyrics | infrasonic-get-lyrics |
| getRandomSongs | infrasonic-get-random-songs |
| jukeboxControl | Not planned |
| 1.3.0 | |
| deleteUser | Not planned |
| getUser | Not planned |
| 1.4.0 | |
| search2 | Not planned |
| 1.5.0 | |
| scrobble | infrasonic-scrobble |
| 1.6.0 | |
| createShare | Not planned |
| deleteShare | Not planned |
| getPodcasts | |
| getShares | Not planned |
| setRating | infrasonic-set-rating |
| updateShare | Not planned |
| 1.8.0 | |
| getAlbum | infrasonic-get-album |
| getAlbumList2 | infrasonic-get-album-list |
| getArtist | infrasonic-get-artist |
| getArtists | infrasonic-get-artists |
| getAvatar | |
| getSong | infrasonic-get-song |
| getStarred | Not planned |
| getStarred2 | infrasonic-get-starred-songs |
| getUsers | Not planned |
| getVideos | Not planned |
| hls | |
| search3 | infrasonic-search |
| star | infrasonic-star |
| unstar | infrasonic-star |
| updatePlaylist | infrasonic-update-playlist |
| 1.9.0 | |
| createBookmark | infrasonic-create-bookmark |
| createPodcastChannel | Maybe? |
| deleteBookmark | infrasonic-delete-bookmark |
| deletePodcastChannel | Maybe? |
| deletePodcastEpisode | Maybe? |
| downloadPodcastEpisode | Maybe? |
| getBookmarks | infrasonic-get-bookmarks |
| getGenres | infrasonic-get-genres |
| getInternetRadioStations | Not planned |
| getSongsByGenre | infrasonic-get-songs-by-genre |
| refreshPodcasts | Maybe? |
| 1.10.1 | |
| updateUser | Not planned |
| 1.11.0 | |
| getArtistInfo | Not planned |
| getArtistInfo2 | infrasonic-get-artist-info |
| getSimilarSongs | Not planned |
| getSimilarSongs2 | infrasonic-get-similar-songs |
| 1.12.0 | |
| getPlayQueue | infrasonic-get-play-queue |
| savePlayQueue | infrasonic-save-play-queue |
| 1.13.0 | |
| getNewestPodcasts | Maybe |
| getTopSongs | infrasonic-get-top-songs |
| 1.14.0 | |
| getAlbumInfo | Not planned |
| getAlbumInfo2 | infrasonic-get-album-info |
| getCaptions | Not planned |
| getVideoInfo | Not planned |
| 1.15.0 | |
| getScanStatus | Not planned |
| startScan | Not planned |
| 1.16.0 | |
| createInternetRadioStation | Not planned |
| deleteInternetRadioStation | Not planned |
| updateInternetRadioStation | Not planned |
| OpenSubsonic additions | |
| getOpenSubsonicExtensions | infrasonic-get-open-subsonic-extensions |