refactor(plugins): rename PlaylistGenerator to PlaylistProvider

Rename the capability from PlaylistGenerator to PlaylistProvider and the
internal orchestrator struct from playlistGeneratorOrchestrator to
playlistSyncer. The new names better describe what the capability does
(provides playlists) rather than how it works internally. All source
files, test plugin, PDK packages (Go/Rust), YAML schemas, and exported
WASM function names are updated accordingly.
This commit is contained in:
Deluan 2026-03-05 16:51:07 -05:00
parent 03a36c5bdd
commit 04aa10f988
15 changed files with 150 additions and 150 deletions

View File

@ -1,18 +1,18 @@
package capabilities package capabilities
// PlaylistGenerator provides dynamically-generated playlists (e.g., "Daily Mix", // PlaylistProvider provides dynamically-generated playlists (e.g., "Daily Mix",
// personalized recommendations). Plugins implementing this capability expose two // personalized recommendations). Plugins implementing this capability expose two
// functions: GetAvailablePlaylists for lightweight discovery and GetPlaylist for // functions: GetAvailablePlaylists for lightweight discovery and GetPlaylist for
// fetching the heavy payload (tracks, metadata). // fetching the heavy payload (tracks, metadata).
// //
//nd:capability name=playlistgenerator required=true //nd:capability name=playlistprovider required=true
type PlaylistGenerator interface { type PlaylistProvider interface {
// GetAvailablePlaylists returns the list of playlists this plugin provides. // GetAvailablePlaylists returns the list of playlists this plugin provides.
//nd:export name=nd_playlist_generator_get_available_playlists //nd:export name=nd_playlist_provider_get_available_playlists
GetAvailablePlaylists(GetAvailablePlaylistsRequest) (GetAvailablePlaylistsResponse, error) GetAvailablePlaylists(GetAvailablePlaylistsRequest) (GetAvailablePlaylistsResponse, error)
// GetPlaylist returns the full data for a single playlist (tracks, metadata). // GetPlaylist returns the full data for a single playlist (tracks, metadata).
//nd:export name=nd_playlist_generator_get_playlist //nd:export name=nd_playlist_provider_get_playlist
GetPlaylist(GetPlaylistRequest) (GetPlaylistResponse, error) GetPlaylist(GetPlaylistRequest) (GetPlaylistResponse, error)
} }
@ -60,13 +60,13 @@ type GetPlaylistResponse struct {
ValidUntil int64 `json:"validUntil"` ValidUntil int64 `json:"validUntil"`
} }
// PlaylistGeneratorError represents an error type for playlist generator operations. // PlaylistProviderError represents an error type for playlist provider operations.
type PlaylistGeneratorError string type PlaylistProviderError string
const ( const (
// PlaylistGeneratorErrorNotFound indicates a playlist is currently unavailable. // PlaylistProviderErrorNotFound indicates a playlist is currently unavailable.
PlaylistGeneratorErrorNotFound PlaylistGeneratorError = "playlist_generator(not_found)" PlaylistProviderErrorNotFound PlaylistProviderError = "playlist_provider(not_found)"
) )
// Error implements the error interface for PlaylistGeneratorError. // Error implements the error interface for PlaylistProviderError.
func (e PlaylistGeneratorError) Error() string { return string(e) } func (e PlaylistProviderError) Error() string { return string(e) }

View File

@ -1,6 +1,6 @@
version: v1-draft version: v1-draft
exports: exports:
nd_playlist_generator_get_available_playlists: nd_playlist_provider_get_available_playlists:
description: GetAvailablePlaylists returns the list of playlists this plugin provides. description: GetAvailablePlaylists returns the list of playlists this plugin provides.
input: input:
$ref: '#/components/schemas/GetAvailablePlaylistsRequest' $ref: '#/components/schemas/GetAvailablePlaylistsRequest'
@ -8,7 +8,7 @@ exports:
output: output:
$ref: '#/components/schemas/GetAvailablePlaylistsResponse' $ref: '#/components/schemas/GetAvailablePlaylistsResponse'
contentType: application/json contentType: application/json
nd_playlist_generator_get_playlist: nd_playlist_provider_get_playlist:
description: GetPlaylist returns the full data for a single playlist (tracks, metadata). description: GetPlaylist returns the full data for a single playlist (tracks, metadata).
input: input:
$ref: '#/components/schemas/GetPlaylistRequest' $ref: '#/components/schemas/GetPlaylistRequest'

View File

@ -391,12 +391,12 @@ func (m *Manager) loadPluginWithConfig(p *model.Plugin) error {
// Call plugin init function // Call plugin init function
callPluginInit(ctx, m.plugins[p.ID]) callPluginInit(ctx, m.plugins[p.ID])
// Start PlaylistGenerator orchestrator if capability is detected // Start PlaylistProvider syncer if capability is detected
loadedPlugin := m.plugins[p.ID] loadedPlugin := m.plugins[p.ID]
if hasCapability(loadedPlugin.capabilities, CapabilityPlaylistGenerator) { if hasCapability(loadedPlugin.capabilities, CapabilityPlaylistProvider) {
orch := newPlaylistGeneratorOrchestrator(m.ctx, p.ID, loadedPlugin, m.ds, m.matcher) syncer := newPlaylistSyncer(m.ctx, p.ID, loadedPlugin, m.ds, m.matcher)
loadedPlugin.closers = append(loadedPlugin.closers, orch) loadedPlugin.closers = append(loadedPlugin.closers, syncer)
go orch.run() go syncer.run()
} }
return nil return nil

View File

@ -1,26 +1,26 @@
// Code generated by ndpgen. DO NOT EDIT. // Code generated by ndpgen. DO NOT EDIT.
// //
// This file contains export wrappers for the PlaylistGenerator capability. // This file contains export wrappers for the PlaylistProvider capability.
// It is intended for use in Navidrome plugins built with TinyGo. // It is intended for use in Navidrome plugins built with TinyGo.
// //
//go:build wasip1 //go:build wasip1
package playlistgenerator package playlistprovider
import ( import (
"github.com/navidrome/navidrome/plugins/pdk/go/pdk" "github.com/navidrome/navidrome/plugins/pdk/go/pdk"
) )
// PlaylistGeneratorError represents an error type for playlist generator operations. // PlaylistProviderError represents an error type for playlist provider operations.
type PlaylistGeneratorError string type PlaylistProviderError string
const ( const (
// PlaylistGeneratorErrorNotFound indicates a playlist is currently unavailable. // PlaylistProviderErrorNotFound indicates a playlist is currently unavailable.
PlaylistGeneratorErrorNotFound PlaylistGeneratorError = "playlist_generator(not_found)" PlaylistProviderErrorNotFound PlaylistProviderError = "playlist_provider(not_found)"
) )
// Error implements the error interface for PlaylistGeneratorError. // Error implements the error interface for PlaylistProviderError.
func (e PlaylistGeneratorError) Error() string { return string(e) } func (e PlaylistProviderError) Error() string { return string(e) }
// GetAvailablePlaylistsRequest is the request for GetAvailablePlaylists. // GetAvailablePlaylistsRequest is the request for GetAvailablePlaylists.
type GetAvailablePlaylistsRequest struct { type GetAvailablePlaylistsRequest struct {
@ -89,12 +89,12 @@ type SongRef struct {
Duration float32 `json:"duration,omitempty"` Duration float32 `json:"duration,omitempty"`
} }
// PlaylistGenerator requires all methods to be implemented. // PlaylistProvider requires all methods to be implemented.
// PlaylistGenerator provides dynamically-generated playlists (e.g., "Daily Mix", // PlaylistProvider provides dynamically-generated playlists (e.g., "Daily Mix",
// personalized recommendations). Plugins implementing this capability expose two // personalized recommendations). Plugins implementing this capability expose two
// functions: GetAvailablePlaylists for lightweight discovery and GetPlaylist for // functions: GetAvailablePlaylists for lightweight discovery and GetPlaylist for
// fetching the heavy payload (tracks, metadata). // fetching the heavy payload (tracks, metadata).
type PlaylistGenerator interface { type PlaylistProvider interface {
// GetAvailablePlaylists - GetAvailablePlaylists returns the list of playlists this plugin provides. // GetAvailablePlaylists - GetAvailablePlaylists returns the list of playlists this plugin provides.
GetAvailablePlaylists(GetAvailablePlaylistsRequest) (GetAvailablePlaylistsResponse, error) GetAvailablePlaylists(GetAvailablePlaylistsRequest) (GetAvailablePlaylistsResponse, error)
// GetPlaylist - GetPlaylist returns the full data for a single playlist (tracks, metadata). // GetPlaylist - GetPlaylist returns the full data for a single playlist (tracks, metadata).
@ -105,9 +105,9 @@ var (
playlistImpl func(GetPlaylistRequest) (GetPlaylistResponse, error) playlistImpl func(GetPlaylistRequest) (GetPlaylistResponse, error)
) )
// Register registers a playlistgenerator implementation. // Register registers a playlistprovider implementation.
// All methods are required. // All methods are required.
func Register(impl PlaylistGenerator) { func Register(impl PlaylistProvider) {
availablePlaylistsImpl = impl.GetAvailablePlaylists availablePlaylistsImpl = impl.GetAvailablePlaylists
playlistImpl = impl.GetPlaylist playlistImpl = impl.GetPlaylist
} }
@ -116,8 +116,8 @@ func Register(impl PlaylistGenerator) {
// The host recognizes this and skips the plugin gracefully. // The host recognizes this and skips the plugin gracefully.
const NotImplementedCode int32 = -2 const NotImplementedCode int32 = -2
//go:wasmexport nd_playlist_generator_get_available_playlists //go:wasmexport nd_playlist_provider_get_available_playlists
func _NdPlaylistGeneratorGetAvailablePlaylists() int32 { func _NdPlaylistProviderGetAvailablePlaylists() int32 {
if availablePlaylistsImpl == nil { if availablePlaylistsImpl == nil {
// Return standard code - host will skip this plugin gracefully // Return standard code - host will skip this plugin gracefully
return NotImplementedCode return NotImplementedCode
@ -143,8 +143,8 @@ func _NdPlaylistGeneratorGetAvailablePlaylists() int32 {
return 0 return 0
} }
//go:wasmexport nd_playlist_generator_get_playlist //go:wasmexport nd_playlist_provider_get_playlist
func _NdPlaylistGeneratorGetPlaylist() int32 { func _NdPlaylistProviderGetPlaylist() int32 {
if playlistImpl == nil { if playlistImpl == nil {
// Return standard code - host will skip this plugin gracefully // Return standard code - host will skip this plugin gracefully
return NotImplementedCode return NotImplementedCode

View File

@ -6,18 +6,18 @@
// //
//go:build !wasip1 //go:build !wasip1
package playlistgenerator package playlistprovider
// PlaylistGeneratorError represents an error type for playlist generator operations. // PlaylistProviderError represents an error type for playlist provider operations.
type PlaylistGeneratorError string type PlaylistProviderError string
const ( const (
// PlaylistGeneratorErrorNotFound indicates a playlist is currently unavailable. // PlaylistProviderErrorNotFound indicates a playlist is currently unavailable.
PlaylistGeneratorErrorNotFound PlaylistGeneratorError = "playlist_generator(not_found)" PlaylistProviderErrorNotFound PlaylistProviderError = "playlist_provider(not_found)"
) )
// Error implements the error interface for PlaylistGeneratorError. // Error implements the error interface for PlaylistProviderError.
func (e PlaylistGeneratorError) Error() string { return string(e) } func (e PlaylistProviderError) Error() string { return string(e) }
// GetAvailablePlaylistsRequest is the request for GetAvailablePlaylists. // GetAvailablePlaylistsRequest is the request for GetAvailablePlaylists.
type GetAvailablePlaylistsRequest struct { type GetAvailablePlaylistsRequest struct {
@ -86,12 +86,12 @@ type SongRef struct {
Duration float32 `json:"duration,omitempty"` Duration float32 `json:"duration,omitempty"`
} }
// PlaylistGenerator requires all methods to be implemented. // PlaylistProvider requires all methods to be implemented.
// PlaylistGenerator provides dynamically-generated playlists (e.g., "Daily Mix", // PlaylistProvider provides dynamically-generated playlists (e.g., "Daily Mix",
// personalized recommendations). Plugins implementing this capability expose two // personalized recommendations). Plugins implementing this capability expose two
// functions: GetAvailablePlaylists for lightweight discovery and GetPlaylist for // functions: GetAvailablePlaylists for lightweight discovery and GetPlaylist for
// fetching the heavy payload (tracks, metadata). // fetching the heavy payload (tracks, metadata).
type PlaylistGenerator interface { type PlaylistProvider interface {
// GetAvailablePlaylists - GetAvailablePlaylists returns the list of playlists this plugin provides. // GetAvailablePlaylists - GetAvailablePlaylists returns the list of playlists this plugin provides.
GetAvailablePlaylists(GetAvailablePlaylistsRequest) (GetAvailablePlaylistsResponse, error) GetAvailablePlaylists(GetAvailablePlaylistsRequest) (GetAvailablePlaylistsResponse, error)
// GetPlaylist - GetPlaylist returns the full data for a single playlist (tracks, metadata). // GetPlaylist - GetPlaylist returns the full data for a single playlist (tracks, metadata).
@ -103,4 +103,4 @@ const NotImplementedCode int32 = -2
// Register is a no-op on non-WASM platforms. // Register is a no-op on non-WASM platforms.
// This stub allows code to compile outside of WASM. // This stub allows code to compile outside of WASM.
func Register(_ PlaylistGenerator) {} func Register(_ PlaylistProvider) {}

View File

@ -8,7 +8,7 @@
pub mod lifecycle; pub mod lifecycle;
pub mod lyrics; pub mod lyrics;
pub mod metadata; pub mod metadata;
pub mod playlistgenerator; pub mod playlistprovider;
pub mod scheduler; pub mod scheduler;
pub mod scrobbler; pub mod scrobbler;
pub mod taskworker; pub mod taskworker;

View File

@ -1,6 +1,6 @@
// Code generated by ndpgen. DO NOT EDIT. // Code generated by ndpgen. DO NOT EDIT.
// //
// This file contains export wrappers for the PlaylistGenerator capability. // This file contains export wrappers for the PlaylistProvider capability.
// It is intended for use in Navidrome plugins built with extism-pdk. // It is intended for use in Navidrome plugins built with extism-pdk.
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -18,10 +18,10 @@ fn is_zero_u64(value: &u64) -> bool { *value == 0 }
fn is_zero_f32(value: &f32) -> bool { *value == 0.0 } fn is_zero_f32(value: &f32) -> bool { *value == 0.0 }
#[allow(dead_code)] #[allow(dead_code)]
fn is_zero_f64(value: &f64) -> bool { *value == 0.0 } fn is_zero_f64(value: &f64) -> bool { *value == 0.0 }
/// PlaylistGeneratorError represents an error type for playlist generator operations. /// PlaylistProviderError represents an error type for playlist provider operations.
pub type PlaylistGeneratorError = &'static str; pub type PlaylistProviderError = &'static str;
/// PlaylistGeneratorErrorNotFound indicates a playlist is currently unavailable. /// PlaylistProviderErrorNotFound indicates a playlist is currently unavailable.
pub const PLAYLIST_GENERATOR_ERROR_NOT_FOUND: PlaylistGeneratorError = "playlist_generator(not_found)"; pub const PLAYLIST_PROVIDER_ERROR_NOT_FOUND: PlaylistProviderError = "playlist_provider(not_found)";
/// GetAvailablePlaylistsRequest is the request for GetAvailablePlaylists. /// GetAvailablePlaylistsRequest is the request for GetAvailablePlaylists.
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@ -136,37 +136,37 @@ impl Error {
} }
} }
/// PlaylistGenerator requires all methods to be implemented. /// PlaylistProvider requires all methods to be implemented.
/// PlaylistGenerator provides dynamically-generated playlists (e.g., "Daily Mix", /// PlaylistProvider provides dynamically-generated playlists (e.g., "Daily Mix",
/// personalized recommendations). Plugins implementing this capability expose two /// personalized recommendations). Plugins implementing this capability expose two
/// functions: GetAvailablePlaylists for lightweight discovery and GetPlaylist for /// functions: GetAvailablePlaylists for lightweight discovery and GetPlaylist for
/// fetching the heavy payload (tracks, metadata). /// fetching the heavy payload (tracks, metadata).
pub trait PlaylistGenerator { pub trait PlaylistProvider {
/// GetAvailablePlaylists - GetAvailablePlaylists returns the list of playlists this plugin provides. /// GetAvailablePlaylists - GetAvailablePlaylists returns the list of playlists this plugin provides.
fn get_available_playlists(&self, req: GetAvailablePlaylistsRequest) -> Result<GetAvailablePlaylistsResponse, Error>; fn get_available_playlists(&self, req: GetAvailablePlaylistsRequest) -> Result<GetAvailablePlaylistsResponse, Error>;
/// GetPlaylist - GetPlaylist returns the full data for a single playlist (tracks, metadata). /// GetPlaylist - GetPlaylist returns the full data for a single playlist (tracks, metadata).
fn get_playlist(&self, req: GetPlaylistRequest) -> Result<GetPlaylistResponse, Error>; fn get_playlist(&self, req: GetPlaylistRequest) -> Result<GetPlaylistResponse, Error>;
} }
/// Register all exports for the PlaylistGenerator capability. /// Register all exports for the PlaylistProvider capability.
/// This macro generates the WASM export functions for all trait methods. /// This macro generates the WASM export functions for all trait methods.
#[macro_export] #[macro_export]
macro_rules! register_playlistgenerator { macro_rules! register_playlistprovider {
($plugin_type:ty) => { ($plugin_type:ty) => {
#[extism_pdk::plugin_fn] #[extism_pdk::plugin_fn]
pub fn nd_playlist_generator_get_available_playlists( pub fn nd_playlist_provider_get_available_playlists(
req: extism_pdk::Json<$crate::playlistgenerator::GetAvailablePlaylistsRequest> req: extism_pdk::Json<$crate::playlistprovider::GetAvailablePlaylistsRequest>
) -> extism_pdk::FnResult<extism_pdk::Json<$crate::playlistgenerator::GetAvailablePlaylistsResponse>> { ) -> extism_pdk::FnResult<extism_pdk::Json<$crate::playlistprovider::GetAvailablePlaylistsResponse>> {
let plugin = <$plugin_type>::default(); let plugin = <$plugin_type>::default();
let result = $crate::playlistgenerator::PlaylistGenerator::get_available_playlists(&plugin, req.into_inner())?; let result = $crate::playlistprovider::PlaylistProvider::get_available_playlists(&plugin, req.into_inner())?;
Ok(extism_pdk::Json(result)) Ok(extism_pdk::Json(result))
} }
#[extism_pdk::plugin_fn] #[extism_pdk::plugin_fn]
pub fn nd_playlist_generator_get_playlist( pub fn nd_playlist_provider_get_playlist(
req: extism_pdk::Json<$crate::playlistgenerator::GetPlaylistRequest> req: extism_pdk::Json<$crate::playlistprovider::GetPlaylistRequest>
) -> extism_pdk::FnResult<extism_pdk::Json<$crate::playlistgenerator::GetPlaylistResponse>> { ) -> extism_pdk::FnResult<extism_pdk::Json<$crate::playlistprovider::GetPlaylistResponse>> {
let plugin = <$plugin_type>::default(); let plugin = <$plugin_type>::default();
let result = $crate::playlistgenerator::PlaylistGenerator::get_playlist(&plugin, req.into_inner())?; let result = $crate::playlistprovider::PlaylistProvider::get_playlist(&plugin, req.into_inner())?;
Ok(extism_pdk::Json(result)) Ok(extism_pdk::Json(result))
} }
}; };

View File

@ -15,10 +15,10 @@ import (
) )
const ( const (
CapabilityPlaylistGenerator Capability = "PlaylistGenerator" CapabilityPlaylistProvider Capability = "PlaylistProvider"
FuncPlaylistGeneratorGetAvailablePlaylists = "nd_playlist_generator_get_available_playlists" FuncPlaylistProviderGetAvailablePlaylists = "nd_playlist_provider_get_available_playlists"
FuncPlaylistGeneratorGetPlaylist = "nd_playlist_generator_get_playlist" FuncPlaylistProviderGetPlaylist = "nd_playlist_provider_get_playlist"
// workChCapacity is the buffer size for the work channel. // workChCapacity is the buffer size for the work channel.
workChCapacity = 16 workChCapacity = 16
@ -29,9 +29,9 @@ const (
func init() { func init() {
registerCapability( registerCapability(
CapabilityPlaylistGenerator, CapabilityPlaylistProvider,
FuncPlaylistGeneratorGetAvailablePlaylists, FuncPlaylistProviderGetAvailablePlaylists,
FuncPlaylistGeneratorGetPlaylist, FuncPlaylistProviderGetPlaylist,
) )
} }
@ -49,11 +49,11 @@ type workItem struct {
ownerID string // only for workSync ownerID string // only for workSync
} }
// playlistGeneratorOrchestrator manages playlist generation for a single plugin. // playlistSyncer manages playlist synchronization for a single plugin.
// All mutable state (refreshTimers, discoveryTimer) is owned exclusively by the // All mutable state (refreshTimers, discoveryTimer) is owned exclusively by the
// worker goroutine — no synchronization needed. The retryInterval and // worker goroutine — no synchronization needed. The retryInterval and
// refreshTimerCount fields use atomics so tests can observe them race-free. // refreshTimerCount fields use atomics so tests can observe them race-free.
type playlistGeneratorOrchestrator struct { type playlistSyncer struct {
pluginName string pluginName string
plugin *plugin plugin *plugin
ds model.DataStore ds model.DataStore
@ -68,9 +68,9 @@ type playlistGeneratorOrchestrator struct {
done chan struct{} // closed when worker exits done chan struct{} // closed when worker exits
} }
func newPlaylistGeneratorOrchestrator(parentCtx context.Context, pluginName string, p *plugin, ds model.DataStore, m *matcher.Matcher) *playlistGeneratorOrchestrator { func newPlaylistSyncer(parentCtx context.Context, pluginName string, p *plugin, ds model.DataStore, m *matcher.Matcher) *playlistSyncer {
ctx, cancel := context.WithCancel(parentCtx) ctx, cancel := context.WithCancel(parentCtx)
return &playlistGeneratorOrchestrator{ return &playlistSyncer{
pluginName: pluginName, pluginName: pluginName,
plugin: p, plugin: p,
ds: ds, ds: ds,
@ -85,7 +85,7 @@ func newPlaylistGeneratorOrchestrator(parentCtx context.Context, pluginName stri
// run is the single worker goroutine that processes all work items sequentially. // run is the single worker goroutine that processes all work items sequentially.
// It performs an initial discovery before entering the main loop. // It performs an initial discovery before entering the main loop.
func (o *playlistGeneratorOrchestrator) run() { func (o *playlistSyncer) run() {
defer close(o.done) defer close(o.done)
// Run initial discovery before entering the loop // Run initial discovery before entering the loop
@ -108,10 +108,10 @@ func (o *playlistGeneratorOrchestrator) run() {
} }
// discoverAndSync calls GetAvailablePlaylists, then GetPlaylist for each, matches tracks, and upserts. // discoverAndSync calls GetAvailablePlaylists, then GetPlaylist for each, matches tracks, and upserts.
func (o *playlistGeneratorOrchestrator) discoverAndSync() { func (o *playlistSyncer) discoverAndSync() {
ctx := o.ctx ctx := o.ctx
resp, err := callPluginFunction[capabilities.GetAvailablePlaylistsRequest, capabilities.GetAvailablePlaylistsResponse]( resp, err := callPluginFunction[capabilities.GetAvailablePlaylistsRequest, capabilities.GetAvailablePlaylistsResponse](
ctx, o.plugin, FuncPlaylistGeneratorGetAvailablePlaylists, capabilities.GetAvailablePlaylistsRequest{}, ctx, o.plugin, FuncPlaylistProviderGetAvailablePlaylists, capabilities.GetAvailablePlaylistsRequest{},
) )
if err != nil { if err != nil {
log.Error(ctx, "Failed to call GetAvailablePlaylists, retrying later", "plugin", o.pluginName, err) log.Error(ctx, "Failed to call GetAvailablePlaylists, retrying later", "plugin", o.pluginName, err)
@ -144,10 +144,10 @@ func (o *playlistGeneratorOrchestrator) discoverAndSync() {
} }
// syncPlaylist calls GetPlaylist, matches tracks, and upserts the playlist in the DB. // syncPlaylist calls GetPlaylist, matches tracks, and upserts the playlist in the DB.
func (o *playlistGeneratorOrchestrator) syncPlaylist(info capabilities.PlaylistInfo, dbID string, ownerID string) { func (o *playlistSyncer) syncPlaylist(info capabilities.PlaylistInfo, dbID string, ownerID string) {
ctx := o.ctx ctx := o.ctx
resp, err := callPluginFunction[capabilities.GetPlaylistRequest, capabilities.GetPlaylistResponse]( resp, err := callPluginFunction[capabilities.GetPlaylistRequest, capabilities.GetPlaylistResponse](
ctx, o.plugin, FuncPlaylistGeneratorGetPlaylist, capabilities.GetPlaylistRequest{ID: info.ID}, ctx, o.plugin, FuncPlaylistProviderGetPlaylist, capabilities.GetPlaylistRequest{ID: info.ID},
) )
if err != nil { if err != nil {
if isPlaylistNotFoundError(err) { if isPlaylistNotFoundError(err) {
@ -221,7 +221,7 @@ func (o *playlistGeneratorOrchestrator) syncPlaylist(info capabilities.PlaylistI
} }
} }
func (o *playlistGeneratorOrchestrator) schedulePlaylistRefresh(info capabilities.PlaylistInfo, dbID string, ownerID string, delay time.Duration) { func (o *playlistSyncer) schedulePlaylistRefresh(info capabilities.PlaylistInfo, dbID string, ownerID string, delay time.Duration) {
// Cancel existing timer if any // Cancel existing timer if any
if timer, ok := o.refreshTimers[dbID]; ok { if timer, ok := o.refreshTimers[dbID]; ok {
timer.Stop() timer.Stop()
@ -235,7 +235,7 @@ func (o *playlistGeneratorOrchestrator) schedulePlaylistRefresh(info capabilitie
o.refreshTimerCount.Store(int32(len(o.refreshTimers))) o.refreshTimerCount.Store(int32(len(o.refreshTimers)))
} }
func (o *playlistGeneratorOrchestrator) scheduleDiscovery(delay time.Duration) { func (o *playlistSyncer) scheduleDiscovery(delay time.Duration) {
if o.discoveryTimer != nil { if o.discoveryTimer != nil {
o.discoveryTimer.Stop() o.discoveryTimer.Stop()
} }
@ -249,11 +249,11 @@ func (o *playlistGeneratorOrchestrator) scheduleDiscovery(delay time.Duration) {
// isPlaylistNotFoundError checks if the error contains a NotFound sentinel from the plugin. // isPlaylistNotFoundError checks if the error contains a NotFound sentinel from the plugin.
func isPlaylistNotFoundError(err error) bool { func isPlaylistNotFoundError(err error) bool {
return err != nil && strings.Contains(err.Error(), capabilities.PlaylistGeneratorErrorNotFound.Error()) return err != nil && strings.Contains(err.Error(), capabilities.PlaylistProviderErrorNotFound.Error())
} }
// stopAllTimers stops the discovery timer and all refresh timers. // stopAllTimers stops the discovery timer and all refresh timers.
func (o *playlistGeneratorOrchestrator) stopAllTimers() { func (o *playlistSyncer) stopAllTimers() {
if o.discoveryTimer != nil { if o.discoveryTimer != nil {
o.discoveryTimer.Stop() o.discoveryTimer.Stop()
} }
@ -263,7 +263,7 @@ func (o *playlistGeneratorOrchestrator) stopAllTimers() {
} }
// Close cancels the context and waits for the worker goroutine to finish. // Close cancels the context and waits for the worker goroutine to finish.
func (o *playlistGeneratorOrchestrator) Close() error { func (o *playlistSyncer) Close() error {
o.cancel() o.cancel()
<-o.done <-o.done
return nil return nil

View File

@ -12,8 +12,8 @@ import (
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
) )
// findOrchestrator finds the playlistGeneratorOrchestrator in a plugin's closers. // findSyncer finds the playlistSyncer in a plugin's closers.
func findOrchestrator(m *Manager, pluginName string) *playlistGeneratorOrchestrator { func findSyncer(m *Manager, pluginName string) *playlistSyncer {
m.mu.RLock() m.mu.RLock()
defer m.mu.RUnlock() defer m.mu.RUnlock()
p, ok := m.plugins[pluginName] p, ok := m.plugins[pluginName]
@ -21,14 +21,14 @@ func findOrchestrator(m *Manager, pluginName string) *playlistGeneratorOrchestra
return nil return nil
} }
for _, c := range p.closers { for _, c := range p.closers {
if orch, ok := c.(*playlistGeneratorOrchestrator); ok { if syncer, ok := c.(*playlistSyncer); ok {
return orch return syncer
} }
} }
return nil return nil
} }
var _ = Describe("PlaylistGenerator", Ordered, func() { var _ = Describe("PlaylistProvider", Ordered, func() {
var ( var (
pgManager *Manager pgManager *Manager
mockPlsRepo *tests.MockPlaylistRepo mockPlsRepo *tests.MockPlaylistRepo
@ -36,26 +36,26 @@ var _ = Describe("PlaylistGenerator", Ordered, func() {
BeforeAll(func() { BeforeAll(func() {
pgManager, _ = createTestManagerWithPlugins(nil, pgManager, _ = createTestManagerWithPlugins(nil,
"test-playlist-generator"+PackageExtension, "test-playlist-provider"+PackageExtension,
) )
mockPlsRepo = pgManager.ds.(*tests.MockDataStore).MockedPlaylist.(*tests.MockPlaylistRepo) mockPlsRepo = pgManager.ds.(*tests.MockDataStore).MockedPlaylist.(*tests.MockPlaylistRepo)
}) })
Describe("capability detection", func() { Describe("capability detection", func() {
It("detects the PlaylistGenerator capability", func() { It("detects the PlaylistProvider capability", func() {
names := pgManager.PluginNames(string(CapabilityPlaylistGenerator)) names := pgManager.PluginNames(string(CapabilityPlaylistProvider))
Expect(names).To(ContainElement("test-playlist-generator")) Expect(names).To(ContainElement("test-playlist-provider"))
}) })
}) })
Describe("orchestrator lifecycle", func() { Describe("syncer lifecycle", func() {
It("creates an orchestrator for the plugin", func() { It("creates a syncer for the plugin", func() {
Expect(findOrchestrator(pgManager, "test-playlist-generator")).ToNot(BeNil()) Expect(findSyncer(pgManager, "test-playlist-provider")).ToNot(BeNil())
}) })
It("discovers and syncs playlists from the plugin", func() { It("discovers and syncs playlists from the plugin", func() {
// The orchestrator runs discoverAndSync in a goroutine on Start(). // The syncer runs discoverAndSync in a goroutine on Start().
// Give it a moment to complete. // Give it a moment to complete.
Eventually(func() int { Eventually(func() int {
return mockPlsRepo.Len() return mockPlsRepo.Len()
@ -72,13 +72,13 @@ var _ = Describe("PlaylistGenerator", Ordered, func() {
Expect(dailyMix1.Comment).To(Equal("Your personalized daily mix")) Expect(dailyMix1.Comment).To(Equal("Your personalized daily mix"))
Expect(dailyMix1.ExternalImageURL).To(Equal("https://example.com/cover1.jpg")) Expect(dailyMix1.ExternalImageURL).To(Equal("https://example.com/cover1.jpg"))
Expect(dailyMix1.OwnerID).To(Equal("user-1")) Expect(dailyMix1.OwnerID).To(Equal("user-1"))
Expect(dailyMix1.PluginID).To(Equal("test-playlist-generator")) Expect(dailyMix1.PluginID).To(Equal("test-playlist-provider"))
Expect(dailyMix1.PluginPlaylistID).To(Equal("daily-mix-1")) Expect(dailyMix1.PluginPlaylistID).To(Equal("daily-mix-1"))
Expect(dailyMix1.Public).To(BeFalse()) Expect(dailyMix1.Public).To(BeFalse())
}) })
It("generates deterministic playlist IDs", func() { It("generates deterministic playlist IDs", func() {
expectedID := id.NewHash("test-playlist-generator", "daily-mix-1", "user-1") expectedID := id.NewHash("test-playlist-provider", "daily-mix-1", "user-1")
Eventually(func() bool { Eventually(func() bool {
_, exists := mockPlsRepo.GetData(expectedID) _, exists := mockPlsRepo.GetData(expectedID)
return exists return exists
@ -86,8 +86,8 @@ var _ = Describe("PlaylistGenerator", Ordered, func() {
}) })
It("creates distinct IDs for different playlists", func() { It("creates distinct IDs for different playlists", func() {
id1 := id.NewHash("test-playlist-generator", "daily-mix-1", "user-1") id1 := id.NewHash("test-playlist-provider", "daily-mix-1", "user-1")
id2 := id.NewHash("test-playlist-generator", "daily-mix-2", "user-1") id2 := id.NewHash("test-playlist-provider", "daily-mix-2", "user-1")
Expect(id1).ToNot(Equal(id2)) Expect(id1).ToNot(Equal(id2))
Eventually(func() bool { Eventually(func() bool {
@ -101,15 +101,15 @@ var _ = Describe("PlaylistGenerator", Ordered, func() {
Describe("GetAvailablePlaylists error handling", func() { Describe("GetAvailablePlaylists error handling", func() {
It("handles plugin errors gracefully", func() { It("handles plugin errors gracefully", func() {
errManager, _ := createTestManagerWithPlugins(map[string]map[string]string{ errManager, _ := createTestManagerWithPlugins(map[string]map[string]string{
"test-playlist-generator": {"error": "service unavailable"}, "test-playlist-provider": {"error": "service unavailable"},
}, "test-playlist-generator"+PackageExtension) }, "test-playlist-provider"+PackageExtension)
// Should still have the orchestrator (error is logged, not fatal) // Should still have the syncer (error is logged, not fatal)
Expect(findOrchestrator(errManager, "test-playlist-generator")).ToNot(BeNil()) Expect(findSyncer(errManager, "test-playlist-provider")).ToNot(BeNil())
// But no playlists created // But no playlists created
errPlsRepo := errManager.ds.(*tests.MockDataStore).MockedPlaylist.(*tests.MockPlaylistRepo) errPlsRepo := errManager.ds.(*tests.MockDataStore).MockedPlaylist.(*tests.MockPlaylistRepo)
// The orchestrator was started but GetAvailablePlaylists returned error, // The syncer was started but GetAvailablePlaylists returned error,
// so no playlists should be created // so no playlists should be created
Consistently(func() int { Consistently(func() int {
return errPlsRepo.Len() return errPlsRepo.Len()
@ -120,14 +120,14 @@ var _ = Describe("PlaylistGenerator", Ordered, func() {
Describe("GetPlaylist NotFound error", func() { Describe("GetPlaylist NotFound error", func() {
It("skips playlists when plugin returns NotFound", func() { It("skips playlists when plugin returns NotFound", func() {
notFoundManager, _ := createTestManagerWithPlugins(map[string]map[string]string{ notFoundManager, _ := createTestManagerWithPlugins(map[string]map[string]string{
"test-playlist-generator": { "test-playlist-provider": {
"get_playlist_error": "playlist temporarily unavailable", "get_playlist_error": "playlist temporarily unavailable",
"get_playlist_error_type": string(capabilities.PlaylistGeneratorErrorNotFound), "get_playlist_error_type": string(capabilities.PlaylistProviderErrorNotFound),
}, },
}, "test-playlist-generator"+PackageExtension) }, "test-playlist-provider"+PackageExtension)
// Should still have the orchestrator // Should still have the syncer
Expect(findOrchestrator(notFoundManager, "test-playlist-generator")).ToNot(BeNil()) Expect(findSyncer(notFoundManager, "test-playlist-provider")).ToNot(BeNil())
// No playlists should be created (all returned NotFound) // No playlists should be created (all returned NotFound)
notFoundPlsRepo := notFoundManager.ds.(*tests.MockDataStore).MockedPlaylist.(*tests.MockPlaylistRepo) notFoundPlsRepo := notFoundManager.ds.(*tests.MockDataStore).MockedPlaylist.(*tests.MockPlaylistRepo)
@ -136,9 +136,9 @@ var _ = Describe("PlaylistGenerator", Ordered, func() {
}, "500ms").Should(Equal(0)) }, "500ms").Should(Equal(0))
// No refresh timers should be scheduled for NotFound playlists // No refresh timers should be scheduled for NotFound playlists
orch := findOrchestrator(notFoundManager, "test-playlist-generator") syncer := findSyncer(notFoundManager, "test-playlist-provider")
Eventually(func() int32 { Eventually(func() int32 {
return orch.refreshTimerCount.Load() return syncer.refreshTimerCount.Load()
}).Should(Equal(int32(0))) }).Should(Equal(int32(0)))
}) })
}) })
@ -146,18 +146,18 @@ var _ = Describe("PlaylistGenerator", Ordered, func() {
Describe("GetPlaylist transient error with RetryInterval", func() { Describe("GetPlaylist transient error with RetryInterval", func() {
It("stores retryInterval and schedules retry on transient errors", func() { It("stores retryInterval and schedules retry on transient errors", func() {
retryManager, _ := createTestManagerWithPlugins(map[string]map[string]string{ retryManager, _ := createTestManagerWithPlugins(map[string]map[string]string{
"test-playlist-generator": { "test-playlist-provider": {
"get_playlist_error": "temporary failure", "get_playlist_error": "temporary failure",
"retry_interval": "60", "retry_interval": "60",
}, },
}, "test-playlist-generator"+PackageExtension) }, "test-playlist-provider"+PackageExtension)
orch := findOrchestrator(retryManager, "test-playlist-generator") syncer := findSyncer(retryManager, "test-playlist-provider")
Expect(orch).ToNot(BeNil()) Expect(syncer).ToNot(BeNil())
// retryInterval should be stored from the response // retryInterval should be stored from the response
Eventually(func() time.Duration { Eventually(func() time.Duration {
return time.Duration(orch.retryInterval.Load()) return time.Duration(syncer.retryInterval.Load())
}).Should(Equal(60 * time.Second)) }).Should(Equal(60 * time.Second))
// No playlists should be created (GetPlaylist failed) // No playlists should be created (GetPlaylist failed)
@ -168,22 +168,22 @@ var _ = Describe("PlaylistGenerator", Ordered, func() {
// Refresh timers should be scheduled for transient errors // Refresh timers should be scheduled for transient errors
Eventually(func() int32 { Eventually(func() int32 {
return orch.refreshTimerCount.Load() return syncer.refreshTimerCount.Load()
}).Should(BeNumerically(">=", int32(1))) }).Should(BeNumerically(">=", int32(1)))
}) })
}) })
Describe("stop", func() { Describe("stop", func() {
It("stops the orchestrator when the manager stops", func() { It("stops the syncer when the manager stops", func() {
stopManager, _ := createTestManagerWithPlugins(nil, stopManager, _ := createTestManagerWithPlugins(nil,
"test-playlist-generator"+PackageExtension, "test-playlist-provider"+PackageExtension,
) )
Expect(findOrchestrator(stopManager, "test-playlist-generator")).ToNot(BeNil()) Expect(findSyncer(stopManager, "test-playlist-provider")).ToNot(BeNil())
err := stopManager.Stop() err := stopManager.Stop()
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
// After Stop(), the plugin is unloaded so findOrchestrator returns nil // After Stop(), the plugin is unloaded so findSyncer returns nil
Expect(findOrchestrator(stopManager, "test-playlist-generator")).To(BeNil()) Expect(findSyncer(stopManager, "test-playlist-provider")).To(BeNil())
}) })
}) })
}) })

View File

@ -136,7 +136,7 @@ func createTestManagerWithPluginsAndMetrics(pluginConfig map[string]map[string]s
mockPluginRepo.SetData(enabledPlugins) mockPluginRepo.SetData(enabledPlugins)
// Pre-seed a mock user repo with a default user so that // Pre-seed a mock user repo with a default user so that
// PlaylistGenerator's discoverAndSync can resolve usernames. // PlaylistProvider's discoverAndSync can resolve usernames.
mockUserRepo := tests.CreateMockUserRepo() mockUserRepo := tests.CreateMockUserRepo()
_ = mockUserRepo.Put(&model.User{ID: "user-1", UserName: "admin"}) _ = mockUserRepo.Put(&model.User{ID: "user-1", UserName: "admin"})

View File

@ -1,6 +0,0 @@
{
"name": "Test Playlist Generator",
"author": "Navidrome Test",
"version": "1.0.0",
"description": "A test playlist generator plugin for integration testing"
}

View File

@ -1,4 +1,4 @@
module test-playlist-generator module test-playlist-provider
go 1.25 go 1.25

View File

@ -1,4 +1,4 @@
// Test playlist generator plugin for Navidrome plugin system integration tests. // Test playlist provider plugin for Navidrome plugin system integration tests.
package main package main
import ( import (
@ -6,20 +6,20 @@ import (
"strconv" "strconv"
"github.com/navidrome/navidrome/plugins/pdk/go/pdk" "github.com/navidrome/navidrome/plugins/pdk/go/pdk"
pg "github.com/navidrome/navidrome/plugins/pdk/go/playlistgenerator" pp "github.com/navidrome/navidrome/plugins/pdk/go/playlistprovider"
) )
func init() { func init() {
pg.Register(&testPlaylistGenerator{}) pp.Register(&testPlaylistProvider{})
} }
type testPlaylistGenerator struct{} type testPlaylistProvider struct{}
func (t *testPlaylistGenerator) GetAvailablePlaylists(_ pg.GetAvailablePlaylistsRequest) (pg.GetAvailablePlaylistsResponse, error) { func (t *testPlaylistProvider) GetAvailablePlaylists(_ pp.GetAvailablePlaylistsRequest) (pp.GetAvailablePlaylistsResponse, error) {
// Check for configured error // Check for configured error
errMsg, hasErr := pdk.GetConfig("error") errMsg, hasErr := pdk.GetConfig("error")
if hasErr && errMsg != "" { if hasErr && errMsg != "" {
return pg.GetAvailablePlaylistsResponse{}, fmt.Errorf("%s", errMsg) return pp.GetAvailablePlaylistsResponse{}, fmt.Errorf("%s", errMsg)
} }
// Get the owner username from config (defaults to "admin") // Get the owner username from config (defaults to "admin")
@ -28,8 +28,8 @@ func (t *testPlaylistGenerator) GetAvailablePlaylists(_ pg.GetAvailablePlaylists
ownerUsername = u ownerUsername = u
} }
resp := pg.GetAvailablePlaylistsResponse{ resp := pp.GetAvailablePlaylistsResponse{
Playlists: []pg.PlaylistInfo{ Playlists: []pp.PlaylistInfo{
{ID: "daily-mix-1", OwnerUsername: ownerUsername}, {ID: "daily-mix-1", OwnerUsername: ownerUsername},
{ID: "daily-mix-2", OwnerUsername: ownerUsername}, {ID: "daily-mix-2", OwnerUsername: ownerUsername},
}, },
@ -46,40 +46,40 @@ func (t *testPlaylistGenerator) GetAvailablePlaylists(_ pg.GetAvailablePlaylists
return resp, nil return resp, nil
} }
func (t *testPlaylistGenerator) GetPlaylist(req pg.GetPlaylistRequest) (pg.GetPlaylistResponse, error) { func (t *testPlaylistProvider) GetPlaylist(req pp.GetPlaylistRequest) (pp.GetPlaylistResponse, error) {
// Check for configured error // Check for configured error
errMsg, hasErr := pdk.GetConfig("get_playlist_error") errMsg, hasErr := pdk.GetConfig("get_playlist_error")
if hasErr && errMsg != "" { if hasErr && errMsg != "" {
// Check if the error should be typed (e.g., NotFound) // Check if the error should be typed (e.g., NotFound)
errType, _ := pdk.GetConfig("get_playlist_error_type") errType, _ := pdk.GetConfig("get_playlist_error_type")
if errType == pg.PlaylistGeneratorErrorNotFound.Error() { if errType == pp.PlaylistProviderErrorNotFound.Error() {
return pg.GetPlaylistResponse{}, fmt.Errorf("%w: %s", pg.PlaylistGeneratorErrorNotFound, errMsg) return pp.GetPlaylistResponse{}, fmt.Errorf("%w: %s", pp.PlaylistProviderErrorNotFound, errMsg)
} }
return pg.GetPlaylistResponse{}, fmt.Errorf("%s", errMsg) return pp.GetPlaylistResponse{}, fmt.Errorf("%s", errMsg)
} }
switch req.ID { switch req.ID {
case "daily-mix-1": case "daily-mix-1":
return pg.GetPlaylistResponse{ return pp.GetPlaylistResponse{
Name: "Daily Mix 1", Name: "Daily Mix 1",
Description: "Your personalized daily mix", Description: "Your personalized daily mix",
CoverArtURL: "https://example.com/cover1.jpg", CoverArtURL: "https://example.com/cover1.jpg",
Tracks: []pg.SongRef{ Tracks: []pp.SongRef{
{Name: "Song A", Artist: "Artist One"}, {Name: "Song A", Artist: "Artist One"},
{Name: "Song B", Artist: "Artist Two"}, {Name: "Song B", Artist: "Artist Two"},
}, },
ValidUntil: 0, // Static, no refresh ValidUntil: 0, // Static, no refresh
}, nil }, nil
case "daily-mix-2": case "daily-mix-2":
return pg.GetPlaylistResponse{ return pp.GetPlaylistResponse{
Name: "Daily Mix 2", Name: "Daily Mix 2",
Tracks: []pg.SongRef{ Tracks: []pp.SongRef{
{Name: "Song C", Artist: "Artist Three"}, {Name: "Song C", Artist: "Artist Three"},
}, },
ValidUntil: 0, ValidUntil: 0,
}, nil }, nil
default: default:
return pg.GetPlaylistResponse{}, fmt.Errorf("unknown playlist: %s", req.ID) return pp.GetPlaylistResponse{}, fmt.Errorf("unknown playlist: %s", req.ID)
} }
} }

View File

@ -0,0 +1,6 @@
{
"name": "Test Playlist Provider",
"author": "Navidrome Test",
"version": "1.0.0",
"description": "A test playlist provider plugin for integration testing"
}