Files
bjarneo--cliamp/docs/provider-development.md
T
coryshaw1 70dcc75035 refactor(provider): return reporting failures instead of discarding them
PlaybackReporter and ProgressReporter now return an error. The UI logs it at
the three fire-and-forget call sites, so jellyfin, emby, and navidrome gain
the observability audiobookshelf had — they were dropping their client errors
silently. Player state is read on the UI goroutine and passed into the
closures, so the reports stay race-free.
2026-08-17 21:06:10 -04:00

6.9 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, scrobbling
  • external/plex/: Plex Media Server, search, album tracks
  • external/spotify/: Spotify, search, playlist management, custom streaming
  • external/radio/: internet radio, favorites
  • external/local/: local TOML playlist files
  • external/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 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)
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)
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 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), implement CustomStreamer.
  • 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 ArtistBrowser or AlbumBrowser
  • Show the search overlay ("F") if any registered provider implements Searcher
  • Enable add-to-playlist in search results if the searched provider implements PlaylistWriter
  • Report playback at track start and finish if PlaybackReporter is implemented, logging any failure the provider returns
  • Run interactive auth on first use if Authenticator is implemented
  • Place the cursor on the in-progress track and start it at the stored position if ResumeTarget is implemented
  • Push an interim listening position every 15 seconds while a track plays if ProgressReporter is implemented
  • Label the browse overlay's two levels with your own nouns if BrowseLabeler is implemented
  • Call Close() on shutdown if Closer is implemented

The "N" and "F" shortcuts work regardless of which provider is currently active They find the first registered provider with the needed capability.