Skip to content

Latest commit

 

History

66 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Infrasonic

Summary

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:

  1. A name element to songs.
  2. An infrasonic-type element 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).

AI Usage Disclaimer

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?".

Requirements

  • Emacs >=27.1, the minimum shared with plz. 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.).

Installation

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

Usage

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.

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

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

Developing

I have included a few ERT tests to prevent myself from cooking things.

Requirements

  • ERT (built-in to Emacs >24.1)

Running Tests

With plz installed through package.el:

make test
make check

For another installation location, pass it explicitly, for example:

make test PLZ_LOAD_PATH="$HOME/.config/emacs/elpaca/builds/plz"

Testing against a server

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 integration

The 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-stop

Security

The standard OpenSubsonic authentication method derives each request token as:

  1. Generate a random salt.
  2. 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.

Functions:

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)

API Implementation Status

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 |

About

An Emacs package for communicating with OpenSubsonic compatible music servers.

Topics

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages