Skip to content

Music providers

A music provider fills the library, like the built-in Local files and Navidrome. Users pick it in Settings → Library → Music provider.

local provider = {
id = "my_server",
name = "My server",
description = "Songs from my server",
icon = "icon.svg",
streams = true, -- songs play from URLs, and can be downloaded for offline use
}
function provider.get_tracks(self, done)
nari.http({ url = "https://example.com/tracks" }, function(response)
done(response and nari.json_decode(response.body) or {})
end)
end
function provider.playable(self, tracks)
local list = {}
for _, track in ipairs(tracks) do
table.insert(list, {
id = track.id, title = track.title, artist = track.artist, album = track.album,
duration = track.duration, uri = "https://example.com/stream/" .. track.id,
})
end
return list
end
nari.music_provider(provider)

Every function gets the provider as its first argument. nari.music_provider returns the same table: emit its library_changed, library_ready(success, error), library_progress and settings_changed signals when the library or the provider’s settings change.

The last argument of async functions is done: return the result right away, or call done(result) later.

Every function is optional; missing ones return empty values.

Function Kind Returns
is_configured() sync true when the provider can load, like when it has a server address.
is_loading(), is_ready() sync The library’s loading state.
get_status() sync A dictionary shown while loading.
setup_entries(), settings_entries() sync Settings entries for the welcome screen and Settings → Library.
get_tracks(), get_albums(), get_artists() async Lists of songs, albums and artists.
get_album(id), get_artist(id) async One album (with tracks) or artist (with albums and tracks, plus the keys in Artist screens).
get_song(id) async One song.
get_raw_lyrics(id) async Embedded lyrics, {} when there are none.
playable(tracks) async What the player needs to play tracks. See Playable songs.
media_uri(track, quality) sync The uri to download track with, for the Download quality setting (original, high, normal or low). Giving it also shows Stream quality: read SettingsManager.playback.stream_quality in playable.
get_cover_path(kind, id, thumb) async A local image path for an "album", "artist", "song" or "playlist".
search(query, filters) async { tracks, albums, artists } for the Search screen. filters has the tags the user added: title, artist, album, playlist, date (a year or a range like 2010-2015) and genre. Leave it out to let Nari search your lists.
get_genres() async Genre names for the Search screen, most relevant first.
get_playlists(), get_playlist(id), create_playlist(name) async Playlists (id, name, song_count, duration, modified, cover_album_ids, image; get_playlist adds tracks). create_playlist returns the new id.
delete_playlist(id), rename_playlist(id, name), add_to_playlist(id, song_ids), remove_from_playlist(id, song_id), set_playlist_image(id, path) async Call done(true) when the change is saved. Nari reloads the playlist then.
get_discover() async Sections for Discover, like nari.discover_section specs with items as a list. Set auto_discover = false on the provider to show only these.
get_radio(kind, id) async Songs for a radio started from a "track" or an "artist". Leave it out and Nari builds one from the library.
get_page(kind, context_id, offset, limit) async For big libraries.
get_liked(kind), set_liked(kind, id, liked) async Likes kept by the service (track, album or artist). Without them, Nari keeps likes on the device.

Songs use the same keys as the song fields. Albums have id, title, artist, artist_id, year, song_count and duration; artists have id, name, album_count and song_count.

playable entries have id, title, artist, artist_id, album, album_id, duration (ms) and:

Key
uri A URL, a local path, or scheme:id for a resolver or byte source.
file_name Its extension helps pick the decoder.
live true for radio streams.
cover, cover_url A local image path, or a link.
replay_gain In dB, or nil.
public_cover_url, share_url Optional https:// image and page anyone can open, shown in Discord Rich Presence. Leave them out when they need sign in.

HTTP streams are downloaded one mebibyte at a time when the server supports it, so hosts that throttle long downloads still play smoothly, and seeking ahead jumps the download there.

The artist screen shows a header (photo, name, listeners) and sections: Popular, Albums, Singles & EPs, Compilations, Appears on, Songs, Fans also like and About. get_artist can fill them with these optional keys:

Key
top_tracks Songs for Popular, most played first.
listeners A number shown as “6.4M listeners”.
bio Plain text for About.
related Artists for Fans also like. Their id opens their screen through get_artist.
similar Artist names instead of related; Nari shows the ones in the library.
sections More sections, like Discover sections with items as a list. order places them among the built-in ones (Popular 0, Albums 10, Singles 20, Compilations 30, Appears on 35, Songs 40, Fans also like 50, About 90), 45 by default.

Albums are split by their release_type: single and ep go to Singles & EPs, compilation to Compilations, appears_on to Appears on, everything else to Albums. With Artist info from the internet on, Nari fills what’s missing from Deezer and Last.fm.

function provider.get_artist(self, id, done)
fetch_artist(id, function(artist)
artist.top_tracks = artist.popular
artist.listeners = artist.monthly_listeners
artist.sections = {
{ title = "This Is " .. artist.name, kind = "playlists", items = { artist.this_is } },
}
done(artist)
end)
end

Services with huge libraries shouldn’t return every song from get_tracks. Set paged = true and give get_page instead: lists then load as they’re scrolled.

provider.paged = true
function provider.get_page(self, kind, context_id, offset, limit, done)
-- kind: "tracks", "albums", "artists", or "playlist" (then context_id is the playlist)
nari.http({ url = "https://example.com/" .. kind .. "?offset=" .. offset .. "&limit=" .. limit }, function(response)
local page = nari.json_decode(response.body)
done({ items = page.items, more = page.next ~= nil })
end)
end

more says whether there’s another page; when left out, a full page means there’s more. With a paged provider:

  • give search, since Nari can’t search a library it doesn’t have
  • get_album, get_artist and get_song still return whole items
  • playing or shuffling a whole list loads up to 1000 songs of it