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 a open, community-maintained REST API specification for music streaming servers. There are several free software music servers that implement this specification such as Navidrome (GPL-3.0) and Gonic (GPL-3.0), 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 songs, albums and artists by adding:
- A
nameelement to songs. - A
infrasonic-typeelement is added to everything, which indicates if the item is a song, album or artist.
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
>=30.1or higher for built-in JSON support. 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
- 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
: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) HTTP request timeout for downloads of art and music
: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)
;; 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)
;; 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)
To run tests within Emacs:
- Require ERT:
M-: (require 'ert) - Add the project to your load path:
M-: (add-to-list 'load-path "/path/to/infrasonic") - Open the test file:
C-x C-f test/infrasonic-test.el - Load the test file:
M-x load-file - Run all tests:
M-x ert-run-tests-interactively
Annoyingly, the OpenSubsonic API requires a very insecure token generation method:
- Generate a salt string (good).
- Calculate the auth token:
token = md5(concat(password, salt))(pretty bad).
In practice, this may not be a huge issue, but the MD5 hash function is cryptographically weak and can easily be broken using a rainbow table or another simple brute-force attack. You should assume that your credentials CAN AND WILL be recovered when NOT USING HTTPS. You should always use HTTPS when using the OpenSubsonic API.
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), however I don't believe it has been adopted widely (Navidrome and Gonic don't currently support it).
Credentials are sourced from auth-source, and cached in memory in the
infrasonic-client struct. They are never written to disk by Infrasonic.
However, tokens and salts are visible within Emacs' process buffers while
API requests are being made.
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-stream-url |
stream |
Complete URL string for streaming |
infrasonic-ping |
ping |
nil (Displays success/failure message) |
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 |
Alist of names/IDs: ((name . id) ...) |
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-delete-playlist |
deletePlaylist |
t (if successful) |
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-songs |
| 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 |
| 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 |