Skip to content

kaibagley/infrasonic

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

62 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Infrasonic

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:

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

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 >=30.1 or 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.).

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

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

To run tests within Emacs:

  1. Require ERT: M-: (require 'ert)
  2. Add the project to your load path: M-: (add-to-list 'load-path "/path/to/infrasonic")
  3. Open the test file: C-x C-f test/infrasonic-test.el
  4. Load the test file: M-x load-file
  5. Run all tests: M-x ert-run-tests-interactively

Security

Annoyingly, the OpenSubsonic API requires a very insecure token generation method:

  1. Generate a salt string (good).
  2. 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.

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

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

About

An Emacs package for communicating with OpenSubsonic compatible music servers.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors