diff --git a/plugins/capabilities/playlist_generator.yaml b/plugins/capabilities/playlist_generator.yaml new file mode 100644 index 000000000..d17252c89 --- /dev/null +++ b/plugins/capabilities/playlist_generator.yaml @@ -0,0 +1,120 @@ +version: v1-draft +exports: + nd_playlist_generator_get_playlists: + description: GetPlaylists returns the list of playlists this plugin provides. + input: + $ref: '#/components/schemas/GetPlaylistsRequest' + contentType: application/json + output: + $ref: '#/components/schemas/GetPlaylistsResponse' + contentType: application/json + nd_playlist_generator_get_playlist: + description: GetPlaylist returns the full data for a single playlist (tracks, metadata). + input: + $ref: '#/components/schemas/GetPlaylistRequest' + contentType: application/json + output: + $ref: '#/components/schemas/GetPlaylistResponse' + contentType: application/json +components: + schemas: + GetPlaylistRequest: + description: GetPlaylistRequest is the request for GetPlaylist. + properties: + id: + type: string + description: ID is the plugin-scoped playlist ID. + required: + - id + GetPlaylistResponse: + description: GetPlaylistResponse is the response for GetPlaylist. + properties: + name: + type: string + description: Name is the display name of the playlist. + description: + type: string + description: Description is an optional description for the playlist. + coverArtUrl: + type: string + description: CoverArtURL is an optional external URL for the playlist cover art. + tracks: + type: array + description: Tracks is the list of songs in the playlist, using SongRef for matching. + items: + $ref: '#/components/schemas/SongRef' + validUntil: + type: integer + format: int64 + description: |- + ValidUntil is a unix timestamp indicating when this playlist data expires. + 0 means static (never refresh). + required: + - name + - tracks + - validUntil + GetPlaylistsRequest: + description: GetPlaylistsRequest is the request for GetPlaylists. + properties: {} + GetPlaylistsResponse: + description: GetPlaylistsResponse is the response for GetPlaylists. + properties: + playlists: + type: array + description: Playlists is the list of playlists provided by this plugin. + items: + $ref: '#/components/schemas/PlaylistInfo' + refreshInterval: + type: integer + format: int64 + description: |- + RefreshInterval is the number of seconds until the next GetPlaylists call. + 0 means never re-discover. + required: + - playlists + - refreshInterval + PlaylistInfo: + description: PlaylistInfo identifies a plugin playlist and its target user. + properties: + id: + type: string + description: ID is the plugin-scoped unique identifier for this playlist. + ownerUserId: + type: string + description: OwnerUserID is the Navidrome user ID that owns this playlist. + required: + - id + - ownerUserId + SongRef: + description: SongRef is a reference to a song with metadata for matching. + properties: + id: + type: string + description: ID is the internal Navidrome mediafile ID (if known). + name: + type: string + description: Name is the song name. + mbid: + type: string + description: MBID is the MusicBrainz ID for the song. + isrc: + type: string + description: ISRC is the International Standard Recording Code for the song. + artist: + type: string + description: Artist is the artist name. + artistMbid: + type: string + description: ArtistMBID is the MusicBrainz artist ID. + album: + type: string + description: Album is the album name. + albumMbid: + type: string + description: AlbumMBID is the MusicBrainz release ID. + duration: + type: number + format: float + description: Duration is the song duration in seconds. + required: + - name diff --git a/plugins/pdk/go/playlistgenerator/playlistgenerator.go b/plugins/pdk/go/playlistgenerator/playlistgenerator.go new file mode 100644 index 000000000..0c8f40428 --- /dev/null +++ b/plugins/pdk/go/playlistgenerator/playlistgenerator.go @@ -0,0 +1,157 @@ +// Code generated by ndpgen. DO NOT EDIT. +// +// This file contains export wrappers for the PlaylistGenerator capability. +// It is intended for use in Navidrome plugins built with TinyGo. +// +//go:build wasip1 + +package playlistgenerator + +import ( + "github.com/navidrome/navidrome/plugins/pdk/go/pdk" +) + +// GetPlaylistRequest is the request for GetPlaylist. +type GetPlaylistRequest struct { + // ID is the plugin-scoped playlist ID. + ID string `json:"id"` +} + +// GetPlaylistResponse is the response for GetPlaylist. +type GetPlaylistResponse struct { + // Name is the display name of the playlist. + Name string `json:"name"` + // Description is an optional description for the playlist. + Description string `json:"description,omitempty"` + // CoverArtURL is an optional external URL for the playlist cover art. + CoverArtURL string `json:"coverArtUrl,omitempty"` + // Tracks is the list of songs in the playlist, using SongRef for matching. + Tracks []SongRef `json:"tracks"` + // ValidUntil is a unix timestamp indicating when this playlist data expires. + // 0 means static (never refresh). + ValidUntil int64 `json:"validUntil"` +} + +// GetPlaylistsRequest is the request for GetPlaylists. +type GetPlaylistsRequest struct { +} + +// GetPlaylistsResponse is the response for GetPlaylists. +type GetPlaylistsResponse struct { + // Playlists is the list of playlists provided by this plugin. + Playlists []PlaylistInfo `json:"playlists"` + // RefreshInterval is the number of seconds until the next GetPlaylists call. + // 0 means never re-discover. + RefreshInterval int64 `json:"refreshInterval"` +} + +// PlaylistInfo identifies a plugin playlist and its target user. +type PlaylistInfo struct { + // ID is the plugin-scoped unique identifier for this playlist. + ID string `json:"id"` + // OwnerUserID is the Navidrome user ID that owns this playlist. + OwnerUserID string `json:"ownerUserId"` +} + +// SongRef is a reference to a song with metadata for matching. +type SongRef struct { + // ID is the internal Navidrome mediafile ID (if known). + ID string `json:"id,omitempty"` + // Name is the song name. + Name string `json:"name"` + // MBID is the MusicBrainz ID for the song. + MBID string `json:"mbid,omitempty"` + // ISRC is the International Standard Recording Code for the song. + ISRC string `json:"isrc,omitempty"` + // Artist is the artist name. + Artist string `json:"artist,omitempty"` + // ArtistMBID is the MusicBrainz artist ID. + ArtistMBID string `json:"artistMbid,omitempty"` + // Album is the album name. + Album string `json:"album,omitempty"` + // AlbumMBID is the MusicBrainz release ID. + AlbumMBID string `json:"albumMbid,omitempty"` + // Duration is the song duration in seconds. + Duration float32 `json:"duration,omitempty"` +} + +// PlaylistGenerator requires all methods to be implemented. +// PlaylistGenerator provides dynamically-generated playlists (e.g., "Daily Mix", +// personalized recommendations). Plugins implementing this capability expose two +// functions: GetPlaylists for lightweight discovery and GetPlaylist for fetching +// the heavy payload (tracks, metadata). +type PlaylistGenerator interface { + // GetPlaylists - GetPlaylists returns the list of playlists this plugin provides. + GetPlaylists(GetPlaylistsRequest) (GetPlaylistsResponse, error) + // GetPlaylist - GetPlaylist returns the full data for a single playlist (tracks, metadata). + GetPlaylist(GetPlaylistRequest) (GetPlaylistResponse, error) +} // Internal implementation holders +var ( + playlistsImpl func(GetPlaylistsRequest) (GetPlaylistsResponse, error) + playlistImpl func(GetPlaylistRequest) (GetPlaylistResponse, error) +) + +// Register registers a playlistgenerator implementation. +// All methods are required. +func Register(impl PlaylistGenerator) { + playlistsImpl = impl.GetPlaylists + playlistImpl = impl.GetPlaylist +} + +// NotImplementedCode is the standard return code for unimplemented functions. +// The host recognizes this and skips the plugin gracefully. +const NotImplementedCode int32 = -2 + +//go:wasmexport nd_playlist_generator_get_playlists +func _NdPlaylistGeneratorGetPlaylists() int32 { + if playlistsImpl == nil { + // Return standard code - host will skip this plugin gracefully + return NotImplementedCode + } + + var input GetPlaylistsRequest + if err := pdk.InputJSON(&input); err != nil { + pdk.SetError(err) + return -1 + } + + output, err := playlistsImpl(input) + if err != nil { + pdk.SetError(err) + return -1 + } + + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(err) + return -1 + } + + return 0 +} + +//go:wasmexport nd_playlist_generator_get_playlist +func _NdPlaylistGeneratorGetPlaylist() int32 { + if playlistImpl == nil { + // Return standard code - host will skip this plugin gracefully + return NotImplementedCode + } + + var input GetPlaylistRequest + if err := pdk.InputJSON(&input); err != nil { + pdk.SetError(err) + return -1 + } + + output, err := playlistImpl(input) + if err != nil { + pdk.SetError(err) + return -1 + } + + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(err) + return -1 + } + + return 0 +} diff --git a/plugins/pdk/go/playlistgenerator/playlistgenerator_stub.go b/plugins/pdk/go/playlistgenerator/playlistgenerator_stub.go new file mode 100644 index 000000000..83dc9a61b --- /dev/null +++ b/plugins/pdk/go/playlistgenerator/playlistgenerator_stub.go @@ -0,0 +1,92 @@ +// Code generated by ndpgen. DO NOT EDIT. +// +// This file provides stub implementations for non-WASM platforms. +// It allows Go plugins to compile and run tests outside of WASM, +// but the actual functionality is only available in WASM builds. +// +//go:build !wasip1 + +package playlistgenerator + +// GetPlaylistRequest is the request for GetPlaylist. +type GetPlaylistRequest struct { + // ID is the plugin-scoped playlist ID. + ID string `json:"id"` +} + +// GetPlaylistResponse is the response for GetPlaylist. +type GetPlaylistResponse struct { + // Name is the display name of the playlist. + Name string `json:"name"` + // Description is an optional description for the playlist. + Description string `json:"description,omitempty"` + // CoverArtURL is an optional external URL for the playlist cover art. + CoverArtURL string `json:"coverArtUrl,omitempty"` + // Tracks is the list of songs in the playlist, using SongRef for matching. + Tracks []SongRef `json:"tracks"` + // ValidUntil is a unix timestamp indicating when this playlist data expires. + // 0 means static (never refresh). + ValidUntil int64 `json:"validUntil"` +} + +// GetPlaylistsRequest is the request for GetPlaylists. +type GetPlaylistsRequest struct { +} + +// GetPlaylistsResponse is the response for GetPlaylists. +type GetPlaylistsResponse struct { + // Playlists is the list of playlists provided by this plugin. + Playlists []PlaylistInfo `json:"playlists"` + // RefreshInterval is the number of seconds until the next GetPlaylists call. + // 0 means never re-discover. + RefreshInterval int64 `json:"refreshInterval"` +} + +// PlaylistInfo identifies a plugin playlist and its target user. +type PlaylistInfo struct { + // ID is the plugin-scoped unique identifier for this playlist. + ID string `json:"id"` + // OwnerUserID is the Navidrome user ID that owns this playlist. + OwnerUserID string `json:"ownerUserId"` +} + +// SongRef is a reference to a song with metadata for matching. +type SongRef struct { + // ID is the internal Navidrome mediafile ID (if known). + ID string `json:"id,omitempty"` + // Name is the song name. + Name string `json:"name"` + // MBID is the MusicBrainz ID for the song. + MBID string `json:"mbid,omitempty"` + // ISRC is the International Standard Recording Code for the song. + ISRC string `json:"isrc,omitempty"` + // Artist is the artist name. + Artist string `json:"artist,omitempty"` + // ArtistMBID is the MusicBrainz artist ID. + ArtistMBID string `json:"artistMbid,omitempty"` + // Album is the album name. + Album string `json:"album,omitempty"` + // AlbumMBID is the MusicBrainz release ID. + AlbumMBID string `json:"albumMbid,omitempty"` + // Duration is the song duration in seconds. + Duration float32 `json:"duration,omitempty"` +} + +// PlaylistGenerator requires all methods to be implemented. +// PlaylistGenerator provides dynamically-generated playlists (e.g., "Daily Mix", +// personalized recommendations). Plugins implementing this capability expose two +// functions: GetPlaylists for lightweight discovery and GetPlaylist for fetching +// the heavy payload (tracks, metadata). +type PlaylistGenerator interface { + // GetPlaylists - GetPlaylists returns the list of playlists this plugin provides. + GetPlaylists(GetPlaylistsRequest) (GetPlaylistsResponse, error) + // GetPlaylist - GetPlaylist returns the full data for a single playlist (tracks, metadata). + GetPlaylist(GetPlaylistRequest) (GetPlaylistResponse, error) +} + +// NotImplementedCode is the standard return code for unimplemented functions. +const NotImplementedCode int32 = -2 + +// Register is a no-op on non-WASM platforms. +// This stub allows code to compile outside of WASM. +func Register(_ PlaylistGenerator) {} diff --git a/plugins/pdk/rust/nd-pdk-capabilities/src/lib.rs b/plugins/pdk/rust/nd-pdk-capabilities/src/lib.rs index 85375b525..f0f21b0e8 100644 --- a/plugins/pdk/rust/nd-pdk-capabilities/src/lib.rs +++ b/plugins/pdk/rust/nd-pdk-capabilities/src/lib.rs @@ -8,6 +8,7 @@ pub mod lifecycle; pub mod lyrics; pub mod metadata; +pub mod playlistgenerator; pub mod scheduler; pub mod scrobbler; pub mod taskworker; diff --git a/plugins/pdk/rust/nd-pdk-capabilities/src/playlistgenerator.rs b/plugins/pdk/rust/nd-pdk-capabilities/src/playlistgenerator.rs new file mode 100644 index 000000000..296fe2854 --- /dev/null +++ b/plugins/pdk/rust/nd-pdk-capabilities/src/playlistgenerator.rs @@ -0,0 +1,165 @@ +// Code generated by ndpgen. DO NOT EDIT. +// +// This file contains export wrappers for the PlaylistGenerator capability. +// It is intended for use in Navidrome plugins built with extism-pdk. + +use serde::{Deserialize, Serialize}; + +// Helper functions for skip_serializing_if with numeric types +#[allow(dead_code)] +fn is_zero_i32(value: &i32) -> bool { *value == 0 } +#[allow(dead_code)] +fn is_zero_u32(value: &u32) -> bool { *value == 0 } +#[allow(dead_code)] +fn is_zero_i64(value: &i64) -> bool { *value == 0 } +#[allow(dead_code)] +fn is_zero_u64(value: &u64) -> bool { *value == 0 } +#[allow(dead_code)] +fn is_zero_f32(value: &f32) -> bool { *value == 0.0 } +#[allow(dead_code)] +fn is_zero_f64(value: &f64) -> bool { *value == 0.0 } +/// GetPlaylistRequest is the request for GetPlaylist. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetPlaylistRequest { + /// ID is the plugin-scoped playlist ID. + #[serde(default)] + pub id: String, +} +/// GetPlaylistResponse is the response for GetPlaylist. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetPlaylistResponse { + /// Name is the display name of the playlist. + #[serde(default)] + pub name: String, + /// Description is an optional description for the playlist. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub description: String, + /// CoverArtURL is an optional external URL for the playlist cover art. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub cover_art_url: String, + /// Tracks is the list of songs in the playlist, using SongRef for matching. + #[serde(default)] + pub tracks: Vec, + /// ValidUntil is a unix timestamp indicating when this playlist data expires. + /// 0 means static (never refresh). + #[serde(default)] + pub valid_until: i64, +} +/// GetPlaylistsRequest is the request for GetPlaylists. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetPlaylistsRequest { +} +/// GetPlaylistsResponse is the response for GetPlaylists. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetPlaylistsResponse { + /// Playlists is the list of playlists provided by this plugin. + #[serde(default)] + pub playlists: Vec, + /// RefreshInterval is the number of seconds until the next GetPlaylists call. + /// 0 means never re-discover. + #[serde(default)] + pub refresh_interval: i64, +} +/// PlaylistInfo identifies a plugin playlist and its target user. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PlaylistInfo { + /// ID is the plugin-scoped unique identifier for this playlist. + #[serde(default)] + pub id: String, + /// OwnerUserID is the Navidrome user ID that owns this playlist. + #[serde(default)] + pub owner_user_id: String, +} +/// SongRef is a reference to a song with metadata for matching. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SongRef { + /// ID is the internal Navidrome mediafile ID (if known). + #[serde(default, skip_serializing_if = "String::is_empty")] + pub id: String, + /// Name is the song name. + #[serde(default)] + pub name: String, + /// MBID is the MusicBrainz ID for the song. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub mbid: String, + /// ISRC is the International Standard Recording Code for the song. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub isrc: String, + /// Artist is the artist name. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub artist: String, + /// ArtistMBID is the MusicBrainz artist ID. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub artist_mbid: String, + /// Album is the album name. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub album: String, + /// AlbumMBID is the MusicBrainz release ID. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub album_mbid: String, + /// Duration is the song duration in seconds. + #[serde(default, skip_serializing_if = "is_zero_f32")] + pub duration: f32, +} + +/// Error represents an error from a capability method. +#[derive(Debug)] +pub struct Error { + pub message: String, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.message) + } +} + +impl std::error::Error for Error {} + +impl Error { + pub fn new(message: impl Into) -> Self { + Self { message: message.into() } + } +} + +/// PlaylistGenerator requires all methods to be implemented. +/// PlaylistGenerator provides dynamically-generated playlists (e.g., "Daily Mix", +/// personalized recommendations). Plugins implementing this capability expose two +/// functions: GetPlaylists for lightweight discovery and GetPlaylist for fetching +/// the heavy payload (tracks, metadata). +pub trait PlaylistGenerator { + /// GetPlaylists - GetPlaylists returns the list of playlists this plugin provides. + fn get_playlists(&self, req: GetPlaylistsRequest) -> Result; + /// GetPlaylist - GetPlaylist returns the full data for a single playlist (tracks, metadata). + fn get_playlist(&self, req: GetPlaylistRequest) -> Result; +} + +/// Register all exports for the PlaylistGenerator capability. +/// This macro generates the WASM export functions for all trait methods. +#[macro_export] +macro_rules! register_playlistgenerator { + ($plugin_type:ty) => { + #[extism_pdk::plugin_fn] + pub fn nd_playlist_generator_get_playlists( + req: extism_pdk::Json<$crate::playlistgenerator::GetPlaylistsRequest> + ) -> extism_pdk::FnResult> { + let plugin = <$plugin_type>::default(); + let result = $crate::playlistgenerator::PlaylistGenerator::get_playlists(&plugin, req.into_inner())?; + Ok(extism_pdk::Json(result)) + } + #[extism_pdk::plugin_fn] + pub fn nd_playlist_generator_get_playlist( + req: extism_pdk::Json<$crate::playlistgenerator::GetPlaylistRequest> + ) -> extism_pdk::FnResult> { + let plugin = <$plugin_type>::default(); + let result = $crate::playlistgenerator::PlaylistGenerator::get_playlist(&plugin, req.into_inner())?; + Ok(extism_pdk::Json(result)) + } + }; +}