202 lines
5.2 KiB
Go
202 lines
5.2 KiB
Go
package emby
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/bjarneo/cliamp/config"
|
|
"github.com/bjarneo/cliamp/playlist"
|
|
"github.com/bjarneo/cliamp/provider"
|
|
)
|
|
|
|
var (
|
|
_ provider.ArtistBrowser = (*Provider)(nil)
|
|
_ provider.AlbumBrowser = (*Provider)(nil)
|
|
_ provider.AlbumTrackLoader = (*Provider)(nil)
|
|
_ provider.PlaybackReporter = (*Provider)(nil)
|
|
_ provider.Searcher = (*Provider)(nil)
|
|
)
|
|
|
|
// Provider implements playlist.Provider for an Emby server.
|
|
// Playlists() returns albums across all music views.
|
|
// Tracks() returns the tracks for a given album item.
|
|
type Provider struct {
|
|
client *Client
|
|
mu sync.Mutex
|
|
playlistCache []playlist.PlaylistInfo
|
|
trackCache map[string][]playlist.Track
|
|
}
|
|
|
|
func newProvider(client *Client) *Provider {
|
|
return &Provider{client: client}
|
|
}
|
|
|
|
// NewFromConfig returns a Provider from an EmbyConfig, or nil if URL or token is missing.
|
|
func NewFromConfig(cfg config.EmbyConfig) *Provider {
|
|
if !cfg.IsSet() {
|
|
return nil
|
|
}
|
|
return newProvider(NewClient(cfg.URL, cfg.Token, cfg.UserID, cfg.User, cfg.Password))
|
|
}
|
|
|
|
// Name returns the display name used in the provider selector.
|
|
func (p *Provider) Name() string { return "Emby" }
|
|
|
|
// Refresh clears cached playlist, track, and album data so the next call
|
|
// re-fetches from the server. Implements playlist.Refresher.
|
|
func (p *Provider) Refresh() {
|
|
p.mu.Lock()
|
|
p.playlistCache = nil
|
|
p.trackCache = nil
|
|
p.mu.Unlock()
|
|
p.client.ClearCache()
|
|
}
|
|
|
|
func (p *Provider) Artists() ([]provider.ArtistInfo, error) {
|
|
artists, err := p.client.Artists()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("artists: %w", err)
|
|
}
|
|
return artists, nil
|
|
}
|
|
|
|
func (p *Provider) ArtistAlbums(artistID string) ([]provider.AlbumInfo, error) {
|
|
albums, err := p.client.ArtistAlbums(artistID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("artist albums: %w", err)
|
|
}
|
|
return albums, nil
|
|
}
|
|
|
|
func (p *Provider) AlbumList(sortType string, offset, size int) ([]provider.AlbumInfo, error) {
|
|
albums, err := p.client.AlbumList(sortType, offset, size)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("album list: %w", err)
|
|
}
|
|
return albums, nil
|
|
}
|
|
|
|
func (p *Provider) AlbumSortTypes() []provider.SortType {
|
|
return p.client.AlbumSortTypes()
|
|
}
|
|
|
|
func (p *Provider) DefaultAlbumSort() string {
|
|
return p.client.DefaultAlbumSort()
|
|
}
|
|
|
|
func (p *Provider) AlbumTracks(albumID string) ([]playlist.Track, error) {
|
|
return p.Tracks(albumID)
|
|
}
|
|
|
|
func (p *Provider) CanReportPlayback(track playlist.Track) bool {
|
|
return track.Meta(provider.MetaEmbyID) != ""
|
|
}
|
|
|
|
func (p *Provider) ReportNowPlaying(track playlist.Track, position time.Duration, canSeek bool) {
|
|
_ = p.client.ReportNowPlaying(track, position, canSeek)
|
|
}
|
|
|
|
func (p *Provider) ReportScrobble(track playlist.Track, elapsed, _ time.Duration, canSeek bool) {
|
|
_ = p.client.ReportScrobble(track, elapsed, canSeek)
|
|
}
|
|
|
|
// Playlists returns all albums across all Emby music views.
|
|
// Results are cached after the first successful call.
|
|
func (p *Provider) Playlists() ([]playlist.PlaylistInfo, error) {
|
|
p.mu.Lock()
|
|
if p.playlistCache != nil {
|
|
cached := p.playlistCache
|
|
p.mu.Unlock()
|
|
return cached, nil
|
|
}
|
|
p.mu.Unlock()
|
|
|
|
albums, err := p.client.Albums()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("playlists: %w", err)
|
|
}
|
|
|
|
out := make([]playlist.PlaylistInfo, 0, len(albums))
|
|
for _, a := range albums {
|
|
name := a.Name
|
|
if a.Artist != "" {
|
|
name = a.Artist + " — " + a.Name
|
|
}
|
|
if a.Year > 0 {
|
|
name = fmt.Sprintf("%s (%d)", name, a.Year)
|
|
}
|
|
out = append(out, playlist.PlaylistInfo{
|
|
ID: a.ID,
|
|
Name: name,
|
|
TrackCount: a.TrackCount,
|
|
})
|
|
}
|
|
|
|
p.mu.Lock()
|
|
p.playlistCache = out
|
|
p.mu.Unlock()
|
|
|
|
return out, nil
|
|
}
|
|
|
|
// SearchTracks searches the Emby music library for tracks matching query.
|
|
// Implements provider.Searcher.
|
|
func (p *Provider) SearchTracks(_ context.Context, query string, limit int) ([]playlist.Track, error) {
|
|
embyTracks, err := p.client.Search(query, limit)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("search: %w", err)
|
|
}
|
|
return p.toPlaylistTracks(embyTracks), nil
|
|
}
|
|
|
|
// Tracks returns the tracks for one album item.
|
|
// Results are cached per album id.
|
|
func (p *Provider) Tracks(albumID string) ([]playlist.Track, error) {
|
|
p.mu.Lock()
|
|
if p.trackCache != nil {
|
|
if cached, ok := p.trackCache[albumID]; ok {
|
|
p.mu.Unlock()
|
|
return cached, nil
|
|
}
|
|
}
|
|
p.mu.Unlock()
|
|
|
|
embyTracks, err := p.client.Tracks(albumID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("tracks: %w", err)
|
|
}
|
|
|
|
out := p.toPlaylistTracks(embyTracks)
|
|
|
|
p.mu.Lock()
|
|
if p.trackCache == nil {
|
|
p.trackCache = make(map[string][]playlist.Track)
|
|
}
|
|
p.trackCache[albumID] = out
|
|
p.mu.Unlock()
|
|
|
|
return out, nil
|
|
}
|
|
|
|
// toPlaylistTracks converts Emby Tracks to playlist.Tracks, attaching the
|
|
// authenticated stream URL and Emby item ID metadata.
|
|
func (p *Provider) toPlaylistTracks(embyTracks []Track) []playlist.Track {
|
|
out := make([]playlist.Track, 0, len(embyTracks))
|
|
for _, t := range embyTracks {
|
|
out = append(out, playlist.Track{
|
|
Path: p.client.StreamURL(t.ID),
|
|
Title: t.Name,
|
|
Artist: t.Artist,
|
|
Album: t.Album,
|
|
Year: t.Year,
|
|
TrackNumber: t.TrackNumber,
|
|
DurationSecs: t.DurationSecs,
|
|
Stream: true,
|
|
ProviderMeta: map[string]string{provider.MetaEmbyID: t.ID},
|
|
})
|
|
}
|
|
return out
|
|
}
|