Creating a Provider
Put providers in external/<name>/, for example external/jellyfin/. A
provider is a Go package. It implements the base playlist.Provider interface
and can implement capability interfaces from the provider/ package. The UI
uses type assertions at run time to detect capabilities and enable features.
Use these providers as examples:
external/navidrome/: Subsonic API, browsing, scrobblingexternal/plex/: Plex Media Server, search, album tracksexternal/spotify/: Spotify, search, playlist management, custom streamingexternal/mixcloud/: public catalog, browse-entry shortcuts, creator jumps, genre search and local genre favoritesexternal/radio/: internet radio, favoritesexternal/local/: local TOML playlist filesexternal/audiobookshelf/: Audiobookshelf, sectioned playlists, resume
Base Interface (required)
Every provider must implement playlist.Provider:
type Provider interface {
Name() string
Playlists() ([]playlist.PlaylistInfo, error)
Tracks(playlistID string) ([]Track, error)
}
This interface provides a name, a playlist list, and tracks for a playlist. It is enough for basic playback.
Capability Interfaces (optional)
Implement any of these interfaces to enable more UI features. All interfaces
are in provider/interfaces.go.
| Interface | What it enables | Methods |
|---|---|---|
Searcher |
Track search overlay | SearchTracks(ctx, query, limit) |
ArtistBrowser |
Hierarchical artist browsing | Artists(), ArtistAlbums(id) |
TrackArtistResolver |
Jump from a highlighted provider track to its artist/creator with N |
ArtistForTrack(track) |
BrowseEntryProvider |
Add non-playable shortcuts into the provider playlist pane | BrowseEntries(); each entry can set AfterID, AfterSection, and OpenInPlaylist |
GenreBrowser |
Hierarchical category browsing with provider-defined sort views | Genres(), GenreSortTypes(), GenreTracks(genreID, sortType) |
GenreFavoriteToggler |
Favorite/unfavorite categories with f in the genre browser |
ToggleGenreFavorite(genreID) |
GenreSearcher |
Search beyond the initially loaded category catalogue | SearchGenres(ctx, query, limit) |
AlbumBrowser |
Paginated album browsing with sort | AlbumList(sort, offset, size), AlbumSortTypes() |
AlbumTrackLoader |
Album track listing | AlbumTracks(albumID) |
PlaybackReporter |
Playback reporting at track start and finish | CanReportPlayback(track), ReportNowPlaying(track, position, canSeek) error, ReportScrobble(track, elapsed, duration, canSeek) error |
PlaylistWriter |
Add track to playlist | AddTrackToPlaylist(ctx, playlistID, track) |
PlaylistCreator |
Create new playlist | CreatePlaylist(ctx, name) |
PlaylistDeleter |
Remove playlists/tracks | DeletePlaylist(name), RemoveTrack(name, index) |
CustomStreamer |
Custom URI decode pipeline | URISchemes(), NewStreamer(uri) |
FavoriteToggler |
Favorite toggling | ToggleFavorite(id) |
Closer |
Cleanup on shutdown | Close() |
Authenticator |
Interactive sign-in flow | Authenticate() error (in playlist package) |
ResumeTarget |
Server-side resume position | ResumeTarget(playlistID, tracks) |
TrackPosition |
Server-side position for one track, read on every play | CanTrackPosition(track), TrackPosition(track) |
ProgressReporter |
Interim position updates while playing, in addition to PlaybackReporter's start/finish reports |
ReportProgress(track, position) error |
BrowseLabeler |
Relabel the browse overlay's two levels (e.g. Authors/Books instead of Artists/Albums) | BrowseLabels() |
Steps
1. Create the package
Create external/<name>/provider.go:
package jellyfin
import (
"context"
"github.com/bjarneo/cliamp/playlist"
"github.com/bjarneo/cliamp/provider"
)
// Compile-time interface checks.
var (
_ provider.Searcher = (*Provider)(nil)
_ provider.AlbumTrackLoader = (*Provider)(nil)
)
type Provider struct {
baseURL string
token string
}
func New(baseURL, token string) *Provider {
return &Provider{baseURL: baseURL, token: token}
}
func (p *Provider) Name() string { return "Jellyfin" }
func (p *Provider) Playlists() ([]playlist.PlaylistInfo, error) {
// Fetch playlists from your server's API.
return nil, nil
}
func (p *Provider) Tracks(playlistID string) ([]playlist.Track, error) {
// Fetch tracks for a playlist.
return nil, nil
}
func (p *Provider) SearchTracks(ctx context.Context, query string, limit int) ([]playlist.Track, error) {
// Search the server's catalog.
return nil, nil
}
func (p *Provider) AlbumTracks(albumID string) ([]playlist.Track, error) {
// Fetch tracks for an album.
return nil, nil
}
2. Return tracks
When you create playlist.Track values:
Path: Set the playable URL or file path. For HTTP streams, use a full URL. For a custom URI scheme, for examplespotify:track:xxx, implementCustomStreamer.Stream: true: Set this for HTTP URLs. The player then uses the streaming pipeline.ProviderMeta: Add provider-specific metadata in a string map with namespaced keys. cliamp uses this metadata for features such as scrobbling:
playlist.Track{
Path: "https://my-server/stream/123",
Title: "Song Title",
Artist: "Artist Name",
Stream: true,
ProviderMeta: map[string]string{"jellyfin.id": "123"},
}
3. Add configuration
Add a configuration struct to config/config.go:
type JellyfinConfig struct {
URL string `toml:"url"`
Token string `toml:"token"`
}
Add the field to the top-level Config struct. Then add a TOML section:
[jellyfin]
url = "https://jellyfin.example.com"
token = "your-api-key"
4. Register in main.go
Register the provider in the run() function in main.go:
if cfg.Jellyfin.URL != "" && cfg.Jellyfin.Token != "" {
jfProv := jellyfin.New(cfg.Jellyfin.URL, cfg.Jellyfin.Token)
providers = append(providers, ui.ProviderEntry{
Key: "jellyfin", Name: "Jellyfin", Provider: jfProv,
})
}
If the provider needs a custom audio pipeline, such as Spotify spotify: URIs,
register a streamer factory:
if cs, ok := myProv.(provider.CustomStreamer); ok {
for _, scheme := range cs.URISchemes() {
p.RegisterStreamerFactory(scheme, cs.NewStreamer)
}
}
If the provider needs the buffered download pipeline for stream URLs, such as Navidrome Subsonic endpoints, register a URL matcher:
p.RegisterBufferedURLMatcher(jellyfin.IsStreamURL)
5. Add a --provider flag value
In the main.go help text, add the provider key to the --provider line. Users
can then set it as the default.
What the UI Does Automatically
Do not change the UI code. The UI uses the interfaces your provider implements to do the following:
- Show the browse overlay (
N) when any registered provider implementsArtistBrowser,AlbumBrowser, orGenreBrowser - Add
BrowseEntryProviderroutes to the provider pane without exposing them as playable playlists to IPC or other provider users.AfterIDputs a route after one list item.AfterSectionis its section fallback. cliamp omits a route that extends a missing section.OpenInPlaylistreplaces the main playlist with its non-empty final result instead of opening the browser track screen - Jump from a highlighted track to its artist or creator when the source
provider implements both
TrackArtistResolverandArtistBrowser - Add genre lists and sort views for
GenreBrowser, thefaction forGenreFavoriteToggler, and provider-side category search forGenreSearcher - Show the search overlay ("F") when a registered provider implements
Searcher - Enable add-to-playlist in search results when the searched provider implements
PlaylistWriter - Report playback at track start and finish when
PlaybackReporteris implemented. Log failures that the provider returns. - Run interactive authentication on first use when
Authenticatoris implemented - Put the cursor on the active track and start at its stored position when
ResumeTargetis implemented - Send the listening position every 15 seconds while a track plays when
ProgressReporteris implemented - Set the browse overlay levels to your own nouns when
BrowseLabeleris implemented - Call
Close()during shutdown whenCloseris implemented
The "N" and "F" shortcuts work with any active provider. They use the first registered provider with the required capability.