mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
feat: implement PlaylistGenerator orchestration logic
Orchestrates plugin playlist lifecycle: discovery via GetPlaylists, data fetch via GetPlaylist, track matching via core/matcher, DB upsert, and timer-based refresh (ValidUntil per playlist, RefreshInterval for re-discovery).
This commit is contained in:
parent
002c7612bf
commit
22ae5bac54
@ -1,5 +1,17 @@
|
|||||||
package plugins
|
package plugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/core/matcher"
|
||||||
|
"github.com/navidrome/navidrome/log"
|
||||||
|
"github.com/navidrome/navidrome/model"
|
||||||
|
"github.com/navidrome/navidrome/model/id"
|
||||||
|
"github.com/navidrome/navidrome/plugins/capabilities"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
CapabilityPlaylistGenerator Capability = "PlaylistGenerator"
|
CapabilityPlaylistGenerator Capability = "PlaylistGenerator"
|
||||||
|
|
||||||
@ -14,3 +26,136 @@ func init() {
|
|||||||
FuncPlaylistGeneratorGetPlaylist,
|
FuncPlaylistGeneratorGetPlaylist,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// playlistGeneratorOrchestrator manages playlist generation for a single plugin.
|
||||||
|
type playlistGeneratorOrchestrator struct {
|
||||||
|
pluginName string
|
||||||
|
plugin *plugin
|
||||||
|
ds model.DataStore
|
||||||
|
matcher *matcher.Matcher
|
||||||
|
refreshTimers map[string]*time.Timer // keyed by playlist DB ID
|
||||||
|
discoveryTimer *time.Timer
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPlaylistGeneratorOrchestrator(pluginName string, p *plugin, ds model.DataStore) *playlistGeneratorOrchestrator {
|
||||||
|
return &playlistGeneratorOrchestrator{
|
||||||
|
pluginName: pluginName,
|
||||||
|
plugin: p,
|
||||||
|
ds: ds,
|
||||||
|
matcher: matcher.New(ds),
|
||||||
|
refreshTimers: make(map[string]*time.Timer),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// discoverAndSync calls GetPlaylists, then GetPlaylist for each, matches tracks, and upserts.
|
||||||
|
func (o *playlistGeneratorOrchestrator) discoverAndSync(ctx context.Context) {
|
||||||
|
resp, err := callPluginFunction[capabilities.GetPlaylistsRequest, capabilities.GetPlaylistsResponse](
|
||||||
|
ctx, o.plugin, FuncPlaylistGeneratorGetPlaylists, capabilities.GetPlaylistsRequest{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(ctx, "Failed to call GetPlaylists", "plugin", o.pluginName, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, info := range resp.Playlists {
|
||||||
|
dbID := id.NewHash(o.pluginName, info.ID, info.OwnerUserID)
|
||||||
|
o.syncPlaylist(ctx, info, dbID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule re-discovery if RefreshInterval > 0
|
||||||
|
if resp.RefreshInterval > 0 {
|
||||||
|
o.scheduleDiscovery(ctx, time.Duration(resp.RefreshInterval)*time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// syncPlaylist calls GetPlaylist, matches tracks, and upserts the playlist in the DB.
|
||||||
|
func (o *playlistGeneratorOrchestrator) syncPlaylist(ctx context.Context, info capabilities.PlaylistInfo, dbID string) {
|
||||||
|
resp, err := callPluginFunction[capabilities.GetPlaylistRequest, capabilities.GetPlaylistResponse](
|
||||||
|
ctx, o.plugin, FuncPlaylistGeneratorGetPlaylist, capabilities.GetPlaylistRequest{ID: info.ID},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(ctx, "Failed to call GetPlaylist", "plugin", o.pluginName, "playlistID", info.ID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert SongRef → agents.Song and match against library
|
||||||
|
songs := songRefsToAgentSongs(resp.Tracks)
|
||||||
|
matched, err := o.matcher.MatchSongsToLibrary(ctx, songs, len(songs))
|
||||||
|
if err != nil {
|
||||||
|
log.Error(ctx, "Failed to match songs to library", "plugin", o.pluginName, "playlistID", info.ID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build playlist model
|
||||||
|
pls := &model.Playlist{
|
||||||
|
ID: dbID,
|
||||||
|
Name: resp.Name,
|
||||||
|
Comment: resp.Description,
|
||||||
|
OwnerID: info.OwnerUserID,
|
||||||
|
Public: false,
|
||||||
|
ExternalImageURL: resp.CoverArtURL,
|
||||||
|
PluginID: o.pluginName,
|
||||||
|
PluginPlaylistID: info.ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set tracks from matched media files
|
||||||
|
tracks := make(model.PlaylistTracks, len(matched))
|
||||||
|
for i, mf := range matched {
|
||||||
|
tracks[i] = model.PlaylistTrack{
|
||||||
|
ID: fmt.Sprintf("%d", i+1),
|
||||||
|
MediaFileID: mf.ID,
|
||||||
|
PlaylistID: dbID,
|
||||||
|
MediaFile: mf,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pls.Tracks = tracks
|
||||||
|
|
||||||
|
// Upsert via repository
|
||||||
|
plsRepo := o.ds.Playlist(ctx)
|
||||||
|
if err := plsRepo.Put(pls); err != nil {
|
||||||
|
log.Error(ctx, "Failed to upsert plugin playlist", "plugin", o.pluginName, "playlistID", info.ID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info(ctx, "Synced plugin playlist", "plugin", o.pluginName, "playlistID", info.ID,
|
||||||
|
"name", resp.Name, "tracks", len(matched), "owner", info.OwnerUserID)
|
||||||
|
|
||||||
|
// Schedule refresh if ValidUntil > 0
|
||||||
|
if resp.ValidUntil > 0 {
|
||||||
|
validUntil := time.Unix(resp.ValidUntil, 0)
|
||||||
|
delay := time.Until(validUntil)
|
||||||
|
if delay <= 0 {
|
||||||
|
delay = 1 * time.Second // Already expired, refresh soon
|
||||||
|
}
|
||||||
|
o.schedulePlaylistRefresh(ctx, info, dbID, delay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *playlistGeneratorOrchestrator) schedulePlaylistRefresh(_ context.Context, info capabilities.PlaylistInfo, dbID string, delay time.Duration) {
|
||||||
|
// Cancel existing timer if any
|
||||||
|
if timer, ok := o.refreshTimers[dbID]; ok {
|
||||||
|
timer.Stop()
|
||||||
|
}
|
||||||
|
o.refreshTimers[dbID] = time.AfterFunc(delay, func() {
|
||||||
|
o.syncPlaylist(context.Background(), info, dbID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *playlistGeneratorOrchestrator) scheduleDiscovery(_ context.Context, delay time.Duration) {
|
||||||
|
if o.discoveryTimer != nil {
|
||||||
|
o.discoveryTimer.Stop()
|
||||||
|
}
|
||||||
|
o.discoveryTimer = time.AfterFunc(delay, func() {
|
||||||
|
o.discoverAndSync(context.Background())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop cancels all timers.
|
||||||
|
func (o *playlistGeneratorOrchestrator) stop() {
|
||||||
|
if o.discoveryTimer != nil {
|
||||||
|
o.discoveryTimer.Stop()
|
||||||
|
}
|
||||||
|
for _, timer := range o.refreshTimers {
|
||||||
|
timer.Stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user