6.0 KiB
Creating a Provider
Providers live in external/<name>/ (e.g. external/jellyfin/). A provider is
a Go package that implements the base playlist.Provider interface and
optionally implements capability interfaces from the provider/ package. The UI
discovers capabilities at runtime via type assertions and enables features
accordingly.
See the existing providers for reference:
external/navidrome/: Subsonic API, browsing, scrobblingexternal/plex/: Plex Media Server, search, album tracksexternal/spotify/: Spotify, search, playlist management, custom streamingexternal/radio/: internet radio, favoritesexternal/local/: local TOML playlist files
Base Interface (required)
Every provider must implement playlist.Provider:
type Provider interface {
Name() string
Playlists() ([]playlist.PlaylistInfo, error)
Tracks(playlistID string) ([]Track, error)
}
This gives the provider a name, a list of playlists, and the ability to return tracks for a playlist. That's enough for basic playback.
Capability Interfaces (optional)
Implement any combination of these to unlock additional UI features. All
interfaces are defined in provider/interfaces.go.
| Interface | What it enables | Methods |
|---|---|---|
Searcher |
Track search overlay | SearchTracks(ctx, query, limit) |
ArtistBrowser |
Hierarchical artist browsing | Artists(), ArtistAlbums(id) |
AlbumBrowser |
Paginated album browsing with sort | AlbumList(sort, offset, size), AlbumSortTypes() |
AlbumTrackLoader |
Album track listing | AlbumTracks(albumID) |
Scrobbler |
Playback reporting | Scrobble(track, submission) |
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) |
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 building playlist.Track values:
Path: the playable URL or file path. For HTTP streams, use a full URL. For custom URI schemes (e.g.spotify:track:xxx), implementCustomStreamer.Stream: true: set this for HTTP URLs so the player uses the streaming pipeline.ProviderMeta: attach provider-specific metadata as a string map with namespaced keys. This is used for features like 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 config 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 and a TOML section:
[jellyfin]
url = "https://jellyfin.example.com"
token = "your-api-key"
4. Register in main.go
Wire up 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 your provider needs a custom audio pipeline (like Spotify's spotify: URIs),
register a streamer factory:
if cs, ok := myProv.(provider.CustomStreamer); ok {
for _, scheme := range cs.URISchemes() {
p.RegisterStreamerFactory(scheme, cs.NewStreamer)
}
}
If your provider needs the buffered download pipeline for its stream URLs (like Navidrome's Subsonic endpoints), register a URL matcher:
p.RegisterBufferedURLMatcher(jellyfin.IsStreamURL)
5. Add a --provider flag value
In main.go's help text, add your provider key to the --provider line so
users can set it as their default.
What the UI Does Automatically
You don't need to touch the UI code. Based on which interfaces your provider implements, the UI will automatically:
- Show the browse overlay ("N") if any registered provider implements
ArtistBrowserorAlbumBrowser - Show the search overlay ("F") if any registered provider implements
Searcher - Enable add-to-playlist in search results if the searched provider implements
PlaylistWriter - Scrobble playback if
Scrobbleris implemented - Run interactive auth on first use if
Authenticatoris implemented - Call
Close()on shutdown ifCloseris implemented
The "N" and "F" shortcuts work regardless of which provider is currently active They find the first registered provider with the needed capability.