Files
wehub-resource-sync ead81af521
CI / go (push) Waiting to run
CI / build (darwin, macos-14) (push) Waiting to run
CI / build (windows, windows-2025) (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Failing after 0s
chore: import upstream snapshot with attribution
2026-07-13 12:31:13 +08:00

172 lines
4.6 KiB
Go

package plex
import (
"context"
"fmt"
"slices"
"sync"
"github.com/bjarneo/cliamp/config"
"github.com/bjarneo/cliamp/playlist"
"github.com/bjarneo/cliamp/provider"
)
// Compile-time interface checks.
var (
_ provider.Searcher = (*Provider)(nil)
_ provider.AlbumTrackLoader = (*Provider)(nil)
)
// Provider implements playlist.Provider for a Plex Media Server.
// Playlists() returns all albums across all music library sections.
// Tracks() returns the tracks for a given album ratingKey.
type Provider struct {
client *Client
mu sync.Mutex
playlistCache []playlist.PlaylistInfo
trackCache map[string][]playlist.Track
}
// newProvider returns a Provider backed by the given Client.
func newProvider(client *Client) *Provider {
return &Provider{client: client}
}
// NewFromConfig returns a Provider from a PlexConfig, or nil if URL or Token is missing.
func NewFromConfig(cfg config.PlexConfig) *Provider {
if !cfg.IsSet() {
return nil
}
return newProvider(NewClient(cfg.URL, cfg.Token, cfg.Libraries...))
}
// Name returns the display name used in the provider selector.
func (p *Provider) Name() string { return "Plex" }
// Playlists returns all albums across all Plex music library sections.
// Each album is a PlaylistInfo whose ID is the album's Plex ratingKey.
// Results are cached after the first successful call.
func (p *Provider) Playlists() ([]playlist.PlaylistInfo, error) {
p.mu.Lock()
if p.playlistCache != nil {
cached := slices.Clone(p.playlistCache)
p.mu.Unlock()
return cached, nil
}
p.mu.Unlock()
sections, err := p.client.MusicSections()
if err != nil {
return nil, err
}
if len(sections) == 0 {
return nil, fmt.Errorf("plex: no matching music libraries found on this server")
}
var lists []playlist.PlaylistInfo
for _, sec := range sections {
albums, err := p.client.Albums(sec.Key)
if err != nil {
return nil, err
}
for _, a := range albums {
name := a.ArtistName + " — " + a.Title
if a.Year > 0 {
name = fmt.Sprintf("%s — %s (%d)", a.ArtistName, a.Title, a.Year)
}
lists = append(lists, playlist.PlaylistInfo{
ID: a.RatingKey,
Name: name,
TrackCount: a.TrackCount,
})
}
}
p.mu.Lock()
p.playlistCache = lists
p.mu.Unlock()
return slices.Clone(lists), nil
}
// Refresh clears cached playlist and track 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()
}
// Tracks returns the tracks for the album identified by albumRatingKey.
// Each track's Path is a complete authenticated HTTP URL ready for the player.
// Tracks with no streamable part (missing Media/Part data) are silently skipped.
// Results are cached per albumRatingKey.
func (p *Provider) Tracks(albumRatingKey string) ([]playlist.Track, error) {
p.mu.Lock()
if p.trackCache != nil {
if cached, ok := p.trackCache[albumRatingKey]; ok {
p.mu.Unlock()
return slices.Clone(cached), nil
}
}
p.mu.Unlock()
plexTracks, err := p.client.Tracks(albumRatingKey)
if err != nil {
return nil, err
}
tracks := p.convertTracks(plexTracks, 0)
p.mu.Lock()
if p.trackCache == nil {
p.trackCache = make(map[string][]playlist.Track)
}
p.trackCache[albumRatingKey] = tracks
p.mu.Unlock()
return slices.Clone(tracks), nil
}
// SearchTracks searches the Plex music library for tracks matching query.
// Implements provider.Searcher.
func (p *Provider) SearchTracks(_ context.Context, query string, limit int) ([]playlist.Track, error) {
plexTracks, err := p.client.Search(query)
if err != nil {
return nil, err
}
return p.convertTracks(plexTracks, limit), nil
}
// convertTracks converts Plex tracks to playlist tracks, skipping entries
// without a streamable part. If limit > 0, at most limit tracks are returned.
func (p *Provider) convertTracks(plexTracks []Track, limit int) []playlist.Track {
tracks := make([]playlist.Track, 0, len(plexTracks))
for _, t := range plexTracks {
if t.PartKey == "" {
continue
}
tracks = append(tracks, playlist.Track{
Path: p.client.StreamURL(t.PartKey),
Title: t.Title,
Artist: t.ArtistName,
Album: t.AlbumName,
Year: t.Year,
TrackNumber: t.TrackNumber,
DurationSecs: t.Duration / 1000,
Stream: true,
})
if limit > 0 && len(tracks) >= limit {
break
}
}
return tracks
}
// AlbumTracks returns the tracks for the given album (ratingKey).
// Implements provider.AlbumTrackLoader.
func (p *Provider) AlbumTracks(albumID string) ([]playlist.Track, error) {
return p.Tracks(albumID)
}