Files
bjarneo--cliamp/docs/provider-development.md
wehub-resource-sync ead81af521
Deploy to GitHub Pages / deploy (push) Failing after 0s
CI / go (push) Has been cancelled
CI / build (darwin, macos-14) (push) Has been cancelled
CI / build (windows, windows-2025) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:31:13 +08:00

195 lines
6.0 KiB
Markdown

# 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
## Base Interface (required)
Every provider must implement `playlist.Provider`:
```go
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`:
```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:
```go
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`:
```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:
```toml
[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`:
```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:
```go
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:
```go
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`
- Scrobble playback if `Scrobbler` is implemented
- Run interactive auth on first use if `Authenticator` 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.