mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
feat(plugins): implementation of the Navidrome Plugin Development Kit with generated client wrappers and service interfaces - Phase 3
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
6d3c29912b
commit
2e716ed780
56
plugins/capabilities/doc.go
Normal file
56
plugins/capabilities/doc.go
Normal file
@ -0,0 +1,56 @@
|
||||
// Package capabilities defines Go interfaces for Navidrome plugin capabilities.
|
||||
//
|
||||
// These interfaces serve as the source of truth for capability definitions.
|
||||
// The ndpgen tool generates:
|
||||
// - Go export wrappers in plugins/pdk/go/<capability>/ for Go plugins
|
||||
// - XTP YAML schemas for non-Go plugins (Rust, TypeScript, etc.)
|
||||
//
|
||||
// Each capability is defined as an annotated interface:
|
||||
//
|
||||
// //nd:capability name=metadata
|
||||
// type MetadataAgent interface {
|
||||
// //nd:export name=nd_get_artist_biography
|
||||
// GetArtistBiography(ArtistInput) (ArtistBiographyOutput, error)
|
||||
// }
|
||||
//
|
||||
// Annotation Reference:
|
||||
//
|
||||
// //nd:capability name=<pkg> [required=true]
|
||||
// - Marks an interface as a capability
|
||||
// - name: Generated package name (e.g., name=metadata → pdk/go/metadata/)
|
||||
// - required: If true, all methods must be implemented (default: false)
|
||||
//
|
||||
// //nd:export name=<func>
|
||||
// - Marks a method as an exported WASM function
|
||||
// - name: The export name (e.g., nd_get_artist_biography)
|
||||
//
|
||||
// Generated Code Structure:
|
||||
//
|
||||
// For a capability like MetadataAgent with required=false:
|
||||
//
|
||||
// package metadata
|
||||
//
|
||||
// // Agent is the marker interface
|
||||
// type Agent interface{}
|
||||
//
|
||||
// // Optional provider interfaces
|
||||
// type ArtistBiographyProvider interface {
|
||||
// GetArtistBiography(ArtistInput) (ArtistBiographyOutput, error)
|
||||
// }
|
||||
//
|
||||
// // Registration function
|
||||
// func Register(impl Agent) { ... }
|
||||
//
|
||||
// For a capability with required=true:
|
||||
//
|
||||
// package scrobbler
|
||||
//
|
||||
// // Scrobbler requires all methods
|
||||
// type Scrobbler interface {
|
||||
// IsAuthorized(AuthInput) (AuthOutput, error)
|
||||
// NowPlaying(NowPlayingInput) (ScrobblerOutput, error)
|
||||
// Scrobble(ScrobbleInput) (ScrobblerOutput, error)
|
||||
// }
|
||||
//
|
||||
// func Register(impl Scrobbler) { ... }
|
||||
package capabilities
|
||||
32
plugins/capabilities/lifecycle.go
Normal file
32
plugins/capabilities/lifecycle.go
Normal file
@ -0,0 +1,32 @@
|
||||
package capabilities
|
||||
|
||||
// Lifecycle provides plugin lifecycle hooks.
|
||||
// This capability allows plugins to perform initialization when loaded,
|
||||
// such as establishing connections, starting background processes, or
|
||||
// validating configuration.
|
||||
//
|
||||
// The OnInit function is called once when the plugin is loaded, and is NOT
|
||||
// called when the plugin is hot-reloaded. Plugins should not assume this
|
||||
// function will be called on every startup.
|
||||
//
|
||||
//nd:capability name=lifecycle
|
||||
type Lifecycle interface {
|
||||
// OnInit is called after a plugin is fully loaded with all services registered.
|
||||
// Plugins can use this function to perform one-time initialization tasks.
|
||||
// The output can contain an error string if initialization failed, which will be
|
||||
// logged but will not prevent the plugin from being loaded.
|
||||
//nd:export name=nd_on_init
|
||||
OnInit(OnInitInput) (OnInitOutput, error)
|
||||
}
|
||||
|
||||
// OnInitInput is the input provided to the init callback.
|
||||
// Currently empty, reserved for future use.
|
||||
type OnInitInput struct{}
|
||||
|
||||
// OnInitOutput is the output from the init callback.
|
||||
type OnInitOutput struct {
|
||||
// Error is the error message if initialization failed.
|
||||
// Empty or null indicates success.
|
||||
// The error is logged but does not prevent the plugin from being loaded.
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
173
plugins/capabilities/metadata_agent.go
Normal file
173
plugins/capabilities/metadata_agent.go
Normal file
@ -0,0 +1,173 @@
|
||||
package capabilities
|
||||
|
||||
// MetadataAgent provides artist and album metadata retrieval.
|
||||
// This capability allows plugins to provide external metadata for artists and albums,
|
||||
// such as biographies, images, similar artists, and top songs.
|
||||
//
|
||||
// Plugins implementing this capability can choose which methods to implement.
|
||||
// Each method is optional - plugins only need to provide the functionality they support.
|
||||
//
|
||||
//nd:capability name=metadata
|
||||
type MetadataAgent interface {
|
||||
// GetArtistMBID retrieves the MusicBrainz ID for an artist.
|
||||
//nd:export name=nd_get_artist_mbid
|
||||
GetArtistMBID(ArtistMBIDInput) (ArtistMBIDOutput, error)
|
||||
|
||||
// GetArtistURL retrieves the external URL for an artist.
|
||||
//nd:export name=nd_get_artist_url
|
||||
GetArtistURL(ArtistInput) (ArtistURLOutput, error)
|
||||
|
||||
// GetArtistBiography retrieves the biography for an artist.
|
||||
//nd:export name=nd_get_artist_biography
|
||||
GetArtistBiography(ArtistInput) (ArtistBiographyOutput, error)
|
||||
|
||||
// GetSimilarArtists retrieves similar artists for a given artist.
|
||||
//nd:export name=nd_get_similar_artists
|
||||
GetSimilarArtists(SimilarArtistsInput) (SimilarArtistsOutput, error)
|
||||
|
||||
// GetArtistImages retrieves images for an artist.
|
||||
//nd:export name=nd_get_artist_images
|
||||
GetArtistImages(ArtistInput) (ArtistImagesOutput, error)
|
||||
|
||||
// GetArtistTopSongs retrieves top songs for an artist.
|
||||
//nd:export name=nd_get_artist_top_songs
|
||||
GetArtistTopSongs(TopSongsInput) (TopSongsOutput, error)
|
||||
|
||||
// GetAlbumInfo retrieves album information.
|
||||
//nd:export name=nd_get_album_info
|
||||
GetAlbumInfo(AlbumInput) (AlbumInfoOutput, error)
|
||||
|
||||
// GetAlbumImages retrieves images for an album.
|
||||
//nd:export name=nd_get_album_images
|
||||
GetAlbumImages(AlbumInput) (AlbumImagesOutput, error)
|
||||
}
|
||||
|
||||
// ArtistMBIDInput is the input for GetArtistMBID.
|
||||
type ArtistMBIDInput struct {
|
||||
// ID is the internal Navidrome artist ID.
|
||||
ID string `json:"id"`
|
||||
// Name is the artist name.
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// ArtistMBIDOutput is the output for GetArtistMBID.
|
||||
type ArtistMBIDOutput struct {
|
||||
// MBID is the MusicBrainz ID for the artist.
|
||||
MBID string `json:"mbid"`
|
||||
}
|
||||
|
||||
// ArtistInput is the common input for artist-related functions.
|
||||
type ArtistInput struct {
|
||||
// ID is the internal Navidrome artist ID.
|
||||
ID string `json:"id"`
|
||||
// Name is the artist name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the artist (if known).
|
||||
MBID *string `json:"mbid,omitempty"`
|
||||
}
|
||||
|
||||
// ArtistURLOutput is the output for GetArtistURL.
|
||||
type ArtistURLOutput struct {
|
||||
// URL is the external URL for the artist.
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// ArtistBiographyOutput is the output for GetArtistBiography.
|
||||
type ArtistBiographyOutput struct {
|
||||
// Biography is the artist biography text.
|
||||
Biography string `json:"biography"`
|
||||
}
|
||||
|
||||
// SimilarArtistsInput is the input for GetSimilarArtists.
|
||||
type SimilarArtistsInput struct {
|
||||
// ID is the internal Navidrome artist ID.
|
||||
ID string `json:"id"`
|
||||
// Name is the artist name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the artist (if known).
|
||||
MBID *string `json:"mbid,omitempty"`
|
||||
// Limit is the maximum number of similar artists to return.
|
||||
Limit int32 `json:"limit"`
|
||||
}
|
||||
|
||||
// ArtistRef is a reference to an artist with name and optional MBID.
|
||||
type ArtistRef struct {
|
||||
// Name is the artist name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the artist.
|
||||
MBID *string `json:"mbid,omitempty"`
|
||||
}
|
||||
|
||||
// SimilarArtistsOutput is the output for GetSimilarArtists.
|
||||
type SimilarArtistsOutput struct {
|
||||
// Artists is the list of similar artists.
|
||||
Artists []ArtistRef `json:"artists"`
|
||||
}
|
||||
|
||||
// ImageInfo represents an image with URL and size.
|
||||
type ImageInfo struct {
|
||||
// URL is the URL of the image.
|
||||
URL string `json:"url"`
|
||||
// Size is the size of the image in pixels (width or height).
|
||||
Size int32 `json:"size"`
|
||||
}
|
||||
|
||||
// ArtistImagesOutput is the output for GetArtistImages.
|
||||
type ArtistImagesOutput struct {
|
||||
// Images is the list of artist images.
|
||||
Images []ImageInfo `json:"images"`
|
||||
}
|
||||
|
||||
// TopSongsInput is the input for GetArtistTopSongs.
|
||||
type TopSongsInput struct {
|
||||
// ID is the internal Navidrome artist ID.
|
||||
ID string `json:"id"`
|
||||
// Name is the artist name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the artist (if known).
|
||||
MBID *string `json:"mbid,omitempty"`
|
||||
// Count is the maximum number of top songs to return.
|
||||
Count int32 `json:"count"`
|
||||
}
|
||||
|
||||
// SongRef is a reference to a song with name and optional MBID.
|
||||
type SongRef struct {
|
||||
// Name is the song name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the song.
|
||||
MBID *string `json:"mbid,omitempty"`
|
||||
}
|
||||
|
||||
// TopSongsOutput is the output for GetArtistTopSongs.
|
||||
type TopSongsOutput struct {
|
||||
// Songs is the list of top songs.
|
||||
Songs []SongRef `json:"songs"`
|
||||
}
|
||||
|
||||
// AlbumInput is the common input for album-related functions.
|
||||
type AlbumInput struct {
|
||||
// Name is the album name.
|
||||
Name string `json:"name"`
|
||||
// Artist is the album artist name.
|
||||
Artist string `json:"artist"`
|
||||
// MBID is the MusicBrainz ID for the album (if known).
|
||||
MBID *string `json:"mbid,omitempty"`
|
||||
}
|
||||
|
||||
// AlbumInfoOutput is the output for GetAlbumInfo.
|
||||
type AlbumInfoOutput struct {
|
||||
// Name is the album name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the album.
|
||||
MBID string `json:"mbid"`
|
||||
// Description is the album description/notes.
|
||||
Description string `json:"description"`
|
||||
// URL is the external URL for the album.
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// AlbumImagesOutput is the output for GetAlbumImages.
|
||||
type AlbumImagesOutput struct {
|
||||
// Images is the list of album images.
|
||||
Images []ImageInfo `json:"images"`
|
||||
}
|
||||
34
plugins/capabilities/scheduler_callback.go
Normal file
34
plugins/capabilities/scheduler_callback.go
Normal file
@ -0,0 +1,34 @@
|
||||
package capabilities
|
||||
|
||||
// SchedulerCallback provides scheduled task handling.
|
||||
// This capability allows plugins to receive callbacks when their scheduled tasks execute.
|
||||
// Plugins that use the scheduler host service must implement this capability
|
||||
// to handle task execution.
|
||||
//
|
||||
//nd:capability name=scheduler
|
||||
type SchedulerCallback interface {
|
||||
// OnSchedulerCallback is called when a scheduled task fires.
|
||||
//nd:export name=nd_scheduler_callback
|
||||
OnSchedulerCallback(SchedulerCallbackInput) (SchedulerCallbackOutput, error)
|
||||
}
|
||||
|
||||
// SchedulerCallbackInput is the input provided when a scheduled task fires.
|
||||
type SchedulerCallbackInput struct {
|
||||
// ScheduleID is the unique identifier for this scheduled task.
|
||||
// This is either the ID provided when scheduling, or an auto-generated UUID if none was specified.
|
||||
ScheduleID string `json:"scheduleId"`
|
||||
// Payload is the payload data that was provided when the task was scheduled.
|
||||
// Can be used to pass context or parameters to the callback handler.
|
||||
Payload string `json:"payload"`
|
||||
// IsRecurring is true if this is a recurring schedule (created via ScheduleRecurring),
|
||||
// false if it's a one-time schedule (created via ScheduleOneTime).
|
||||
IsRecurring bool `json:"isRecurring"`
|
||||
}
|
||||
|
||||
// SchedulerCallbackOutput is the output from the scheduler callback.
|
||||
type SchedulerCallbackOutput struct {
|
||||
// Error is the error message if the callback failed to process the scheduled task.
|
||||
// Empty or null indicates success. The error is logged but does not
|
||||
// affect the scheduling system.
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
115
plugins/capabilities/scrobbler.go
Normal file
115
plugins/capabilities/scrobbler.go
Normal file
@ -0,0 +1,115 @@
|
||||
package capabilities
|
||||
|
||||
// Scrobbler provides scrobbling functionality to external services.
|
||||
// This capability allows plugins to submit listening history to services like Last.fm,
|
||||
// ListenBrainz, or custom scrobbling backends.
|
||||
//
|
||||
// All methods are required - plugins implementing this capability must provide
|
||||
// all three functions: IsAuthorized, NowPlaying, and Scrobble.
|
||||
//
|
||||
//nd:capability name=scrobbler required=true
|
||||
type Scrobbler interface {
|
||||
// IsAuthorized checks if a user is authorized to scrobble to this service.
|
||||
//nd:export name=nd_scrobbler_is_authorized
|
||||
IsAuthorized(AuthInput) (AuthOutput, error)
|
||||
|
||||
// NowPlaying sends a now playing notification to the scrobbling service.
|
||||
//nd:export name=nd_scrobbler_now_playing
|
||||
NowPlaying(NowPlayingInput) (ScrobblerOutput, error)
|
||||
|
||||
// Scrobble submits a completed scrobble to the scrobbling service.
|
||||
//nd:export name=nd_scrobbler_scrobble
|
||||
Scrobble(ScrobbleInput) (ScrobblerOutput, error)
|
||||
}
|
||||
|
||||
// AuthInput is the input for authorization check.
|
||||
type AuthInput struct {
|
||||
// UserID is the internal Navidrome user ID.
|
||||
UserID string `json:"userId"`
|
||||
// Username is the username of the user.
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// AuthOutput is the output for authorization check.
|
||||
type AuthOutput struct {
|
||||
// Authorized indicates whether the user is authorized to scrobble.
|
||||
Authorized bool `json:"authorized"`
|
||||
}
|
||||
|
||||
// TrackInfo contains track metadata for scrobbling.
|
||||
type TrackInfo struct {
|
||||
// ID is the internal Navidrome track ID.
|
||||
ID string `json:"id"`
|
||||
// Title is the track title.
|
||||
Title string `json:"title"`
|
||||
// Album is the album name.
|
||||
Album string `json:"album"`
|
||||
// Artist is the track artist.
|
||||
Artist string `json:"artist"`
|
||||
// AlbumArtist is the album artist.
|
||||
AlbumArtist string `json:"albumArtist"`
|
||||
// Duration is the track duration in seconds.
|
||||
Duration float64 `json:"duration"`
|
||||
// TrackNumber is the track number on the album.
|
||||
TrackNumber int32 `json:"trackNumber"`
|
||||
// DiscNumber is the disc number.
|
||||
DiscNumber int32 `json:"discNumber"`
|
||||
// MBZRecordingID is the MusicBrainz recording ID.
|
||||
MBZRecordingID *string `json:"mbzRecordingId,omitempty"`
|
||||
// MBZAlbumID is the MusicBrainz album/release ID.
|
||||
MBZAlbumID *string `json:"mbzAlbumId,omitempty"`
|
||||
// MBZArtistID is the MusicBrainz artist ID.
|
||||
MBZArtistID *string `json:"mbzArtistId,omitempty"`
|
||||
// MBZReleaseGroupID is the MusicBrainz release group ID.
|
||||
MBZReleaseGroupID *string `json:"mbzReleaseGroupId,omitempty"`
|
||||
// MBZAlbumArtistID is the MusicBrainz album artist ID.
|
||||
MBZAlbumArtistID *string `json:"mbzAlbumArtistId,omitempty"`
|
||||
// MBZReleaseTrackID is the MusicBrainz release track ID.
|
||||
MBZReleaseTrackID *string `json:"mbzReleaseTrackId,omitempty"`
|
||||
}
|
||||
|
||||
// NowPlayingInput is the input for now playing notification.
|
||||
type NowPlayingInput struct {
|
||||
// UserID is the internal Navidrome user ID.
|
||||
UserID string `json:"userId"`
|
||||
// Username is the username of the user.
|
||||
Username string `json:"username"`
|
||||
// Track is the track currently playing.
|
||||
Track TrackInfo `json:"track"`
|
||||
// Position is the current playback position in seconds.
|
||||
Position int32 `json:"position"`
|
||||
}
|
||||
|
||||
// ScrobbleInput is the input for submitting a scrobble.
|
||||
type ScrobbleInput struct {
|
||||
// UserID is the internal Navidrome user ID.
|
||||
UserID string `json:"userId"`
|
||||
// Username is the username of the user.
|
||||
Username string `json:"username"`
|
||||
// Track is the track that was played.
|
||||
Track TrackInfo `json:"track"`
|
||||
// Timestamp is the Unix timestamp when the track started playing.
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
// ScrobblerErrorType indicates how Navidrome should handle scrobbler errors.
|
||||
type ScrobblerErrorType string
|
||||
|
||||
const (
|
||||
// ScrobblerErrorNone indicates no error occurred.
|
||||
ScrobblerErrorNone ScrobblerErrorType = "none"
|
||||
// ScrobblerErrorNotAuthorized indicates the user is not authorized.
|
||||
ScrobblerErrorNotAuthorized ScrobblerErrorType = "not_authorized"
|
||||
// ScrobblerErrorRetryLater indicates the operation should be retried later.
|
||||
ScrobblerErrorRetryLater ScrobblerErrorType = "retry_later"
|
||||
// ScrobblerErrorUnrecoverable indicates an unrecoverable error.
|
||||
ScrobblerErrorUnrecoverable ScrobblerErrorType = "unrecoverable"
|
||||
)
|
||||
|
||||
// ScrobblerOutput is the output for scrobbler operations.
|
||||
type ScrobblerOutput struct {
|
||||
// Error is the error message if the operation failed.
|
||||
Error *string `json:"error,omitempty"`
|
||||
// ErrorType indicates how Navidrome should handle the error.
|
||||
ErrorType *ScrobblerErrorType `json:"errorType,omitempty"`
|
||||
}
|
||||
89
plugins/capabilities/websocket_callback.go
Normal file
89
plugins/capabilities/websocket_callback.go
Normal file
@ -0,0 +1,89 @@
|
||||
package capabilities
|
||||
|
||||
// WebSocketCallback provides WebSocket message handling.
|
||||
// This capability allows plugins to receive callbacks for WebSocket events
|
||||
// such as text messages, binary messages, errors, and connection closures.
|
||||
// Plugins that use the WebSocket host service must implement this capability
|
||||
// to handle incoming events.
|
||||
//
|
||||
//nd:capability name=websocket
|
||||
type WebSocketCallback interface {
|
||||
// OnTextMessage is called when a text message is received on a WebSocket connection.
|
||||
//nd:export name=nd_websocket_on_text_message
|
||||
OnTextMessage(OnTextMessageInput) (OnTextMessageOutput, error)
|
||||
|
||||
// OnBinaryMessage is called when a binary message is received on a WebSocket connection.
|
||||
//nd:export name=nd_websocket_on_binary_message
|
||||
OnBinaryMessage(OnBinaryMessageInput) (OnBinaryMessageOutput, error)
|
||||
|
||||
// OnError is called when an error occurs on a WebSocket connection.
|
||||
//nd:export name=nd_websocket_on_error
|
||||
OnError(OnErrorInput) (OnErrorOutput, error)
|
||||
|
||||
// OnClose is called when a WebSocket connection is closed.
|
||||
//nd:export name=nd_websocket_on_close
|
||||
OnClose(OnCloseInput) (OnCloseOutput, error)
|
||||
}
|
||||
|
||||
// OnTextMessageInput is the input provided when a text message is received.
|
||||
type OnTextMessageInput struct {
|
||||
// ConnectionID is the unique identifier for the WebSocket connection that received the message.
|
||||
ConnectionID string `json:"connectionId"`
|
||||
// Message is the text message content received from the WebSocket.
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// OnTextMessageOutput is the output from the text message handler.
|
||||
type OnTextMessageOutput struct {
|
||||
// Error is the error message if the callback failed.
|
||||
// Empty or null indicates success.
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// OnBinaryMessageInput is the input provided when a binary message is received.
|
||||
type OnBinaryMessageInput struct {
|
||||
// ConnectionID is the unique identifier for the WebSocket connection that received the message.
|
||||
ConnectionID string `json:"connectionId"`
|
||||
// Data is the binary data received from the WebSocket, encoded as base64.
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
// OnBinaryMessageOutput is the output from the binary message handler.
|
||||
type OnBinaryMessageOutput struct {
|
||||
// Error is the error message if the callback failed.
|
||||
// Empty or null indicates success.
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// OnErrorInput is the input provided when an error occurs on a WebSocket connection.
|
||||
type OnErrorInput struct {
|
||||
// ConnectionID is the unique identifier for the WebSocket connection where the error occurred.
|
||||
ConnectionID string `json:"connectionId"`
|
||||
// Error is the error message describing what went wrong.
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// OnErrorOutput is the output from the error handler.
|
||||
type OnErrorOutput struct {
|
||||
// Error is the error message if the callback failed.
|
||||
// Empty or null indicates success.
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// OnCloseInput is the input provided when a WebSocket connection is closed.
|
||||
type OnCloseInput struct {
|
||||
// ConnectionID is the unique identifier for the WebSocket connection that was closed.
|
||||
ConnectionID string `json:"connectionId"`
|
||||
// Code is the WebSocket close status code (e.g., 1000 for normal closure,
|
||||
// 1001 for going away, 1006 for abnormal closure).
|
||||
Code int32 `json:"code"`
|
||||
// Reason is the human-readable reason for the connection closure, if provided.
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// OnCloseOutput is the output from the close handler.
|
||||
type OnCloseOutput struct {
|
||||
// Error is the error message if the callback failed.
|
||||
// Empty or null indicates success.
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
@ -303,3 +303,101 @@ func GenerateGoMod() ([]byte, error) {
|
||||
}
|
||||
return tmplContent, nil
|
||||
}
|
||||
|
||||
// capabilityTemplateData holds data for capability template execution.
|
||||
type capabilityTemplateData struct {
|
||||
Package string
|
||||
Capability Capability
|
||||
}
|
||||
|
||||
// capabilityFuncMap returns template functions for capability code generation.
|
||||
func capabilityFuncMap(cap Capability) template.FuncMap {
|
||||
return template.FuncMap{
|
||||
"formatDoc": formatDoc,
|
||||
"indent": indentText,
|
||||
"agentName": capabilityAgentName,
|
||||
"providerInterface": func(e Export) string { return e.ProviderInterfaceName() },
|
||||
"implVar": func(e Export) string { return e.ImplVarName() },
|
||||
"exportFunc": func(e Export) string { return e.ExportFuncName() },
|
||||
}
|
||||
}
|
||||
|
||||
// indentText adds n tabs to each line of text.
|
||||
func indentText(n int, s string) string {
|
||||
indent := strings.Repeat("\t", n)
|
||||
lines := strings.Split(s, "\n")
|
||||
for i, line := range lines {
|
||||
if line != "" {
|
||||
lines[i] = indent + line
|
||||
}
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// capabilityAgentName returns the interface name for a capability.
|
||||
// Uses the Go interface name stripped of common suffixes.
|
||||
func capabilityAgentName(cap Capability) string {
|
||||
name := cap.Interface
|
||||
// Remove common suffixes to get a clean name
|
||||
for _, suffix := range []string{"Agent", "Callback", "Service"} {
|
||||
if strings.HasSuffix(name, suffix) {
|
||||
name = name[:len(name)-len(suffix)]
|
||||
break
|
||||
}
|
||||
}
|
||||
// Use the shortened name or the original if no suffix found
|
||||
if name == "" {
|
||||
name = cap.Interface
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// GenerateCapabilityGo generates Go export wrapper code for a capability.
|
||||
func GenerateCapabilityGo(cap Capability, pkgName string) ([]byte, error) {
|
||||
tmplContent, err := templatesFS.ReadFile("templates/capability.go.tmpl")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading capability template: %w", err)
|
||||
}
|
||||
|
||||
tmpl, err := template.New("capability").Funcs(capabilityFuncMap(cap)).Parse(string(tmplContent))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing template: %w", err)
|
||||
}
|
||||
|
||||
data := capabilityTemplateData{
|
||||
Package: pkgName,
|
||||
Capability: cap,
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, data); err != nil {
|
||||
return nil, fmt.Errorf("executing template: %w", err)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// GenerateCapabilityGoStub generates stub code for non-WASM platforms.
|
||||
func GenerateCapabilityGoStub(cap Capability, pkgName string) ([]byte, error) {
|
||||
tmplContent, err := templatesFS.ReadFile("templates/capability_stub.go.tmpl")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading capability stub template: %w", err)
|
||||
}
|
||||
|
||||
tmpl, err := template.New("capability_stub").Funcs(capabilityFuncMap(cap)).Parse(string(tmplContent))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing template: %w", err)
|
||||
}
|
||||
|
||||
data := capabilityTemplateData{
|
||||
Package: pkgName,
|
||||
Capability: cap,
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, data); err != nil {
|
||||
return nil, fmt.Errorf("executing template: %w", err)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
@ -672,6 +672,283 @@ type TestService interface {
|
||||
Expect(codeStr).To(ContainSubstring(`"test_dosomething"`))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GenerateCapabilityGo", func() {
|
||||
It("should generate valid Go code for a non-required capability", func() {
|
||||
cap := Capability{
|
||||
Name: "metadata",
|
||||
Interface: "MetadataAgent",
|
||||
Required: false,
|
||||
Doc: "MetadataAgent provides metadata retrieval.",
|
||||
Methods: []Export{
|
||||
{
|
||||
Name: "GetArtistBiography",
|
||||
ExportName: "nd_get_artist_biography",
|
||||
Input: Param{Type: "ArtistInput"},
|
||||
Output: Param{Type: "ArtistBiographyOutput"},
|
||||
Doc: "Returns artist biography",
|
||||
},
|
||||
{
|
||||
Name: "GetArtistImages",
|
||||
ExportName: "nd_get_artist_images",
|
||||
Input: Param{Type: "ArtistInput"},
|
||||
Output: Param{Type: "ArtistImagesOutput"},
|
||||
Doc: "Returns artist images",
|
||||
},
|
||||
},
|
||||
Structs: []StructDef{
|
||||
{
|
||||
Name: "ArtistInput",
|
||||
Fields: []FieldDef{
|
||||
{Name: "ID", Type: "string", JSONTag: "id"},
|
||||
{Name: "Name", Type: "string", JSONTag: "name"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ArtistBiographyOutput",
|
||||
Fields: []FieldDef{
|
||||
{Name: "Biography", Type: "string", JSONTag: "biography"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ArtistImagesOutput",
|
||||
Fields: []FieldDef{
|
||||
{Name: "Images", Type: "[]ImageInfo", JSONTag: "images"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ImageInfo",
|
||||
Fields: []FieldDef{
|
||||
{Name: "URL", Type: "string", JSONTag: "url"},
|
||||
{Name: "Size", Type: "int32", JSONTag: "size"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
code, err := GenerateCapabilityGo(cap, "metadata")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
codeStr := string(code)
|
||||
|
||||
// Check for build tag
|
||||
Expect(codeStr).To(ContainSubstring("//go:build wasip1"))
|
||||
|
||||
// Check for package declaration
|
||||
Expect(codeStr).To(ContainSubstring("package metadata"))
|
||||
|
||||
// Check for marker interface (non-required)
|
||||
Expect(codeStr).To(ContainSubstring("type Metadata interface{}"))
|
||||
|
||||
// Check for provider interfaces
|
||||
Expect(codeStr).To(ContainSubstring("type ArtistBiographyProvider interface"))
|
||||
Expect(codeStr).To(ContainSubstring("type ArtistImagesProvider interface"))
|
||||
|
||||
// Check for Register function with type assertions
|
||||
Expect(codeStr).To(ContainSubstring("func Register(impl Metadata)"))
|
||||
Expect(codeStr).To(ContainSubstring("impl.(ArtistBiographyProvider)"))
|
||||
|
||||
// Check for export wrappers
|
||||
Expect(codeStr).To(ContainSubstring("//export nd_get_artist_biography"))
|
||||
Expect(codeStr).To(ContainSubstring("func _NdGetArtistBiography()"))
|
||||
|
||||
// Check for NotImplementedCode handling
|
||||
Expect(codeStr).To(ContainSubstring("NotImplementedCode"))
|
||||
Expect(codeStr).To(ContainSubstring("return NotImplementedCode"))
|
||||
|
||||
// Check struct definitions
|
||||
Expect(codeStr).To(ContainSubstring("type ArtistInput struct"))
|
||||
Expect(codeStr).To(ContainSubstring("type ImageInfo struct"))
|
||||
})
|
||||
|
||||
It("should generate valid Go code for a required capability", func() {
|
||||
cap := Capability{
|
||||
Name: "scrobbler",
|
||||
Interface: "Scrobbler",
|
||||
Required: true,
|
||||
Methods: []Export{
|
||||
{
|
||||
Name: "IsAuthorized",
|
||||
ExportName: "nd_scrobbler_is_authorized",
|
||||
Input: Param{Type: "AuthInput"},
|
||||
Output: Param{Type: "AuthOutput"},
|
||||
},
|
||||
{
|
||||
Name: "Scrobble",
|
||||
ExportName: "nd_scrobbler_scrobble",
|
||||
Input: Param{Type: "ScrobbleInput"},
|
||||
Output: Param{Type: "ScrobblerOutput"},
|
||||
},
|
||||
},
|
||||
Structs: []StructDef{
|
||||
{Name: "AuthInput", Fields: []FieldDef{{Name: "UserID", Type: "string", JSONTag: "userId"}}},
|
||||
{Name: "AuthOutput", Fields: []FieldDef{{Name: "Authorized", Type: "bool", JSONTag: "authorized"}}},
|
||||
{Name: "ScrobbleInput", Fields: []FieldDef{{Name: "UserID", Type: "string", JSONTag: "userId"}}},
|
||||
{Name: "ScrobblerOutput", Fields: []FieldDef{{Name: "Error", Type: "*string", JSONTag: "error", OmitEmpty: true}}},
|
||||
},
|
||||
}
|
||||
|
||||
code, err := GenerateCapabilityGo(cap, "scrobbler")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
codeStr := string(code)
|
||||
|
||||
// Check for full interface (required capability)
|
||||
Expect(codeStr).To(ContainSubstring("type Scrobbler interface {"))
|
||||
Expect(codeStr).To(ContainSubstring("IsAuthorized(AuthInput) (AuthOutput, error)"))
|
||||
Expect(codeStr).To(ContainSubstring("Scrobble(ScrobbleInput) (ScrobblerOutput, error)"))
|
||||
|
||||
// Should NOT have provider interfaces for required capability
|
||||
Expect(codeStr).NotTo(ContainSubstring("AuthProvider interface"))
|
||||
|
||||
// Register should directly assign methods
|
||||
Expect(codeStr).To(ContainSubstring("func Register(impl Scrobbler)"))
|
||||
Expect(codeStr).To(ContainSubstring("impl.IsAuthorized"))
|
||||
})
|
||||
|
||||
It("should include type aliases and consts", func() {
|
||||
cap := Capability{
|
||||
Name: "scrobbler",
|
||||
Interface: "Scrobbler",
|
||||
Required: true,
|
||||
Methods: []Export{
|
||||
{
|
||||
Name: "Scrobble",
|
||||
ExportName: "nd_scrobble",
|
||||
Input: Param{Type: "ScrobbleInput"},
|
||||
Output: Param{Type: "ScrobblerOutput"},
|
||||
},
|
||||
},
|
||||
Structs: []StructDef{
|
||||
{Name: "ScrobbleInput", Fields: []FieldDef{{Name: "UserID", Type: "string", JSONTag: "userId"}}},
|
||||
{Name: "ScrobblerOutput", Fields: []FieldDef{{Name: "ErrorType", Type: "*ScrobblerErrorType", JSONTag: "errorType", OmitEmpty: true}}},
|
||||
},
|
||||
TypeAliases: []TypeAlias{
|
||||
{Name: "ScrobblerErrorType", Type: "string", Doc: "ScrobblerErrorType indicates error handling."},
|
||||
},
|
||||
Consts: []ConstGroup{
|
||||
{
|
||||
Type: "ScrobblerErrorType",
|
||||
Values: []ConstDef{
|
||||
{Name: "ScrobblerErrorNone", Value: `"none"`, Doc: "No error"},
|
||||
{Name: "ScrobblerErrorRetry", Value: `"retry"`, Doc: "Retry later"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
code, err := GenerateCapabilityGo(cap, "scrobbler")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
codeStr := string(code)
|
||||
|
||||
// Check type alias
|
||||
Expect(codeStr).To(ContainSubstring("type ScrobblerErrorType string"))
|
||||
|
||||
// Check consts
|
||||
Expect(codeStr).To(ContainSubstring("ScrobblerErrorNone"))
|
||||
Expect(codeStr).To(ContainSubstring(`"none"`))
|
||||
Expect(codeStr).To(ContainSubstring("ScrobblerErrorRetry"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GenerateCapabilityGoStub", func() {
|
||||
It("should generate valid stub code for non-WASM builds", func() {
|
||||
cap := Capability{
|
||||
Name: "metadata",
|
||||
Interface: "MetadataAgent",
|
||||
Required: false,
|
||||
Methods: []Export{
|
||||
{
|
||||
Name: "GetArtistBiography",
|
||||
ExportName: "nd_get_artist_biography",
|
||||
Input: Param{Type: "ArtistInput"},
|
||||
Output: Param{Type: "ArtistBiographyOutput"},
|
||||
},
|
||||
},
|
||||
Structs: []StructDef{
|
||||
{Name: "ArtistInput", Fields: []FieldDef{{Name: "ID", Type: "string", JSONTag: "id"}}},
|
||||
{Name: "ArtistBiographyOutput", Fields: []FieldDef{{Name: "Biography", Type: "string", JSONTag: "biography"}}},
|
||||
},
|
||||
}
|
||||
|
||||
code, err := GenerateCapabilityGoStub(cap, "metadata")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
codeStr := string(code)
|
||||
|
||||
// Check for non-WASM build tag
|
||||
Expect(codeStr).To(ContainSubstring("//go:build !wasip1"))
|
||||
|
||||
// Check for package declaration
|
||||
Expect(codeStr).To(ContainSubstring("package metadata"))
|
||||
|
||||
// Check for no-op Register
|
||||
Expect(codeStr).To(ContainSubstring("func Register(_ Metadata) {}"))
|
||||
|
||||
// Check struct definitions are present
|
||||
Expect(codeStr).To(ContainSubstring("type ArtistInput struct"))
|
||||
|
||||
// Check there are no export wrappers
|
||||
Expect(codeStr).NotTo(ContainSubstring("//export"))
|
||||
Expect(codeStr).NotTo(ContainSubstring("pdk.InputJSON"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("End-to-end capability generation", func() {
|
||||
It("should parse and generate capability code from source", func() {
|
||||
src := `package capabilities
|
||||
|
||||
// Lifecycle provides plugin lifecycle hooks.
|
||||
//nd:capability name=lifecycle
|
||||
type Lifecycle interface {
|
||||
// OnInit is called when the plugin is loaded.
|
||||
//nd:export name=nd_on_init
|
||||
OnInit(OnInitInput) (OnInitOutput, error)
|
||||
}
|
||||
|
||||
// OnInitInput is the input for OnInit.
|
||||
type OnInitInput struct {
|
||||
}
|
||||
|
||||
// OnInitOutput is the output for OnInit.
|
||||
type OnInitOutput struct {
|
||||
// Error is the error message if initialization failed.
|
||||
Error *string ` + "`json:\"error,omitempty\"`" + `
|
||||
}
|
||||
`
|
||||
// Create temporary directory
|
||||
tmpDir := GinkgoT().TempDir()
|
||||
path := tmpDir + "/lifecycle.go"
|
||||
err := writeFile(path, src)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Parse
|
||||
capabilities, err := ParseCapabilities(tmpDir)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(capabilities).To(HaveLen(1))
|
||||
|
||||
cap := capabilities[0]
|
||||
Expect(cap.Name).To(Equal("lifecycle"))
|
||||
Expect(cap.Methods).To(HaveLen(1))
|
||||
|
||||
// Generate WASM code
|
||||
code, err := GenerateCapabilityGo(cap, "lifecycle")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
codeStr := string(code)
|
||||
Expect(codeStr).To(ContainSubstring("//export nd_on_init"))
|
||||
Expect(codeStr).To(ContainSubstring("type InitProvider interface"))
|
||||
|
||||
// Generate stub code
|
||||
stubCode, err := GenerateCapabilityGoStub(cap, "lifecycle")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
stubStr := string(stubCode)
|
||||
Expect(stubStr).To(ContainSubstring("//go:build !wasip1"))
|
||||
Expect(stubStr).To(ContainSubstring("func Register(_ Lifecycle) {}"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
func writeFile(path, content string) error {
|
||||
|
||||
@ -17,6 +17,10 @@ var (
|
||||
hostServicePattern = regexp.MustCompile(`//nd:hostservice\s+(.*)`)
|
||||
// //nd:hostfunc [name=CustomName]
|
||||
hostFuncPattern = regexp.MustCompile(`//nd:hostfunc(?:\s+(.*))?`)
|
||||
// //nd:capability name=PackageName [required=true]
|
||||
capabilityPattern = regexp.MustCompile(`//nd:capability\s+(.*)`)
|
||||
// //nd:export name=ExportName
|
||||
exportPattern = regexp.MustCompile(`//nd:export\s+(.*)`)
|
||||
// key=value pairs
|
||||
keyValuePattern = regexp.MustCompile(`(\w+)=(\S+)`)
|
||||
)
|
||||
@ -51,6 +55,223 @@ func ParseDirectory(dir string) ([]Service, error) {
|
||||
return services, nil
|
||||
}
|
||||
|
||||
// ParseCapabilities parses all Go source files in a directory and extracts capabilities.
|
||||
func ParseCapabilities(dir string) ([]Capability, error) {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading directory: %w", err)
|
||||
}
|
||||
|
||||
var capabilities []Capability
|
||||
fset := token.NewFileSet()
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") {
|
||||
continue
|
||||
}
|
||||
// Skip generated files, test files, and doc.go
|
||||
if strings.HasSuffix(entry.Name(), "_gen.go") ||
|
||||
strings.HasSuffix(entry.Name(), "_test.go") ||
|
||||
entry.Name() == "doc.go" {
|
||||
continue
|
||||
}
|
||||
|
||||
path := filepath.Join(dir, entry.Name())
|
||||
parsed, err := parseCapabilityFile(fset, path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing %s: %w", entry.Name(), err)
|
||||
}
|
||||
capabilities = append(capabilities, parsed...)
|
||||
}
|
||||
|
||||
return capabilities, nil
|
||||
}
|
||||
|
||||
// parseCapabilityFile parses a single Go source file and extracts capabilities.
|
||||
func parseCapabilityFile(fset *token.FileSet, path string) ([]Capability, error) {
|
||||
f, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// First pass: collect all struct definitions in the file
|
||||
allStructs := parseStructs(f)
|
||||
structMap := make(map[string]StructDef)
|
||||
for _, s := range allStructs {
|
||||
structMap[s.Name] = s
|
||||
}
|
||||
|
||||
// Collect type aliases and consts
|
||||
allTypeAliases := parseTypeAliases(f)
|
||||
aliasMap := make(map[string]TypeAlias)
|
||||
for _, a := range allTypeAliases {
|
||||
aliasMap[a.Name] = a
|
||||
}
|
||||
allConstGroups := parseConstGroups(f)
|
||||
|
||||
var capabilities []Capability
|
||||
|
||||
for _, decl := range f.Decls {
|
||||
genDecl, ok := decl.(*ast.GenDecl)
|
||||
if !ok || genDecl.Tok != token.TYPE {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, spec := range genDecl.Specs {
|
||||
typeSpec, ok := spec.(*ast.TypeSpec)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
interfaceType, ok := typeSpec.Type.(*ast.InterfaceType)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for //nd:capability annotation in doc comment
|
||||
docText, rawDoc := getDocComment(genDecl, typeSpec)
|
||||
capAnnotation := parseCapabilityAnnotation(rawDoc)
|
||||
if capAnnotation == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
capability := Capability{
|
||||
Name: capAnnotation["name"],
|
||||
Interface: typeSpec.Name.Name,
|
||||
Required: capAnnotation["required"] == "true",
|
||||
Doc: cleanDoc(docText),
|
||||
}
|
||||
|
||||
// Parse methods and collect referenced types
|
||||
referencedTypes := make(map[string]bool)
|
||||
for _, method := range interfaceType.Methods.List {
|
||||
if len(method.Names) == 0 {
|
||||
continue // Embedded interface
|
||||
}
|
||||
|
||||
funcType, ok := method.Type.(*ast.FuncType)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for //nd:export annotation
|
||||
methodDocText, methodRawDoc := getMethodDocComment(method)
|
||||
exportAnnotation := parseExportAnnotation(methodRawDoc)
|
||||
if exportAnnotation == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
export, err := parseExport(method.Names[0].Name, funcType, exportAnnotation, cleanDoc(methodDocText))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing export %s.%s: %w", typeSpec.Name.Name, method.Names[0].Name, err)
|
||||
}
|
||||
capability.Methods = append(capability.Methods, export)
|
||||
|
||||
// Collect referenced types from input and output
|
||||
collectReferencedTypes(export.Input.Type, referencedTypes)
|
||||
collectReferencedTypes(export.Output.Type, referencedTypes)
|
||||
}
|
||||
|
||||
// Recursively collect all struct dependencies
|
||||
collectAllStructDependencies(referencedTypes, structMap)
|
||||
|
||||
// Attach referenced structs to the capability
|
||||
for typeName := range referencedTypes {
|
||||
if s, exists := structMap[typeName]; exists {
|
||||
capability.Structs = append(capability.Structs, s)
|
||||
}
|
||||
}
|
||||
|
||||
// Attach referenced type aliases
|
||||
for typeName := range referencedTypes {
|
||||
if a, exists := aliasMap[typeName]; exists {
|
||||
capability.TypeAliases = append(capability.TypeAliases, a)
|
||||
}
|
||||
}
|
||||
|
||||
// Attach const groups that match referenced type aliases
|
||||
for _, group := range allConstGroups {
|
||||
if group.Type == "" {
|
||||
continue
|
||||
}
|
||||
if referencedTypes[group.Type] {
|
||||
capability.Consts = append(capability.Consts, group)
|
||||
}
|
||||
}
|
||||
|
||||
if len(capability.Methods) > 0 {
|
||||
capabilities = append(capabilities, capability)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return capabilities, nil
|
||||
}
|
||||
|
||||
// collectAllStructDependencies recursively collects all struct types referenced by other structs.
|
||||
func collectAllStructDependencies(referencedTypes map[string]bool, structMap map[string]StructDef) {
|
||||
// Keep iterating until no new types are added
|
||||
for {
|
||||
newTypes := make(map[string]bool)
|
||||
for typeName := range referencedTypes {
|
||||
if s, exists := structMap[typeName]; exists {
|
||||
for _, field := range s.Fields {
|
||||
collectReferencedTypes(field.Type, newTypes)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check if any new types were found
|
||||
foundNew := false
|
||||
for t := range newTypes {
|
||||
if !referencedTypes[t] {
|
||||
referencedTypes[t] = true
|
||||
foundNew = true
|
||||
}
|
||||
}
|
||||
if !foundNew {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseExport parses an export method signature into an Export struct.
|
||||
func parseExport(name string, funcType *ast.FuncType, annotation map[string]string, doc string) (Export, error) {
|
||||
export := Export{
|
||||
Name: name,
|
||||
ExportName: annotation["name"],
|
||||
Doc: doc,
|
||||
}
|
||||
|
||||
// Capability exports have exactly one input parameter (the struct type)
|
||||
if funcType.Params != nil && len(funcType.Params.List) == 1 {
|
||||
field := funcType.Params.List[0]
|
||||
typeName := typeToString(field.Type)
|
||||
paramName := "input"
|
||||
if len(field.Names) > 0 {
|
||||
paramName = field.Names[0].Name
|
||||
}
|
||||
export.Input = NewParam(paramName, typeName)
|
||||
}
|
||||
|
||||
// Capability exports return (OutputType, error)
|
||||
if funcType.Results != nil {
|
||||
for _, field := range funcType.Results.List {
|
||||
typeName := typeToString(field.Type)
|
||||
if typeName == "error" {
|
||||
continue // Skip error return
|
||||
}
|
||||
paramName := "output"
|
||||
if len(field.Names) > 0 {
|
||||
paramName = field.Names[0].Name
|
||||
}
|
||||
export.Output = NewParam(paramName, typeName)
|
||||
break // Only take the first non-error return
|
||||
}
|
||||
}
|
||||
|
||||
return export, nil
|
||||
}
|
||||
|
||||
// parseFile parses a single Go source file and extracts host services.
|
||||
func parseFile(fset *token.FileSet, path string) ([]Service, error) {
|
||||
f, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
|
||||
@ -192,6 +413,104 @@ func parseStructs(f *ast.File) []StructDef {
|
||||
return structs
|
||||
}
|
||||
|
||||
// parseTypeAliases extracts all type alias definitions from a parsed Go file.
|
||||
// Type aliases are non-struct type declarations like: type MyType string
|
||||
func parseTypeAliases(f *ast.File) []TypeAlias {
|
||||
var aliases []TypeAlias
|
||||
|
||||
for _, decl := range f.Decls {
|
||||
genDecl, ok := decl.(*ast.GenDecl)
|
||||
if !ok || genDecl.Tok != token.TYPE {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, spec := range genDecl.Specs {
|
||||
typeSpec, ok := spec.(*ast.TypeSpec)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip struct and interface types
|
||||
if _, isStruct := typeSpec.Type.(*ast.StructType); isStruct {
|
||||
continue
|
||||
}
|
||||
if _, isInterface := typeSpec.Type.(*ast.InterfaceType); isInterface {
|
||||
continue
|
||||
}
|
||||
|
||||
docText, _ := getDocComment(genDecl, typeSpec)
|
||||
aliases = append(aliases, TypeAlias{
|
||||
Name: typeSpec.Name.Name,
|
||||
Type: typeToString(typeSpec.Type),
|
||||
Doc: cleanDoc(docText),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return aliases
|
||||
}
|
||||
|
||||
// parseConstGroups extracts const groups from a parsed Go file.
|
||||
func parseConstGroups(f *ast.File) []ConstGroup {
|
||||
var groups []ConstGroup
|
||||
|
||||
for _, decl := range f.Decls {
|
||||
genDecl, ok := decl.(*ast.GenDecl)
|
||||
if !ok || genDecl.Tok != token.CONST {
|
||||
continue
|
||||
}
|
||||
|
||||
group := ConstGroup{}
|
||||
for _, spec := range genDecl.Specs {
|
||||
valueSpec, ok := spec.(*ast.ValueSpec)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get type if specified
|
||||
if valueSpec.Type != nil && group.Type == "" {
|
||||
group.Type = typeToString(valueSpec.Type)
|
||||
}
|
||||
|
||||
// Extract values
|
||||
for i, name := range valueSpec.Names {
|
||||
def := ConstDef{
|
||||
Name: name.Name,
|
||||
}
|
||||
// Get value if present
|
||||
if i < len(valueSpec.Values) {
|
||||
def.Value = exprToString(valueSpec.Values[i])
|
||||
}
|
||||
// Get doc comment
|
||||
if valueSpec.Doc != nil {
|
||||
def.Doc = cleanDoc(valueSpec.Doc.Text())
|
||||
} else if valueSpec.Comment != nil {
|
||||
def.Doc = cleanDoc(valueSpec.Comment.Text())
|
||||
}
|
||||
group.Values = append(group.Values, def)
|
||||
}
|
||||
}
|
||||
|
||||
if len(group.Values) > 0 {
|
||||
groups = append(groups, group)
|
||||
}
|
||||
}
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
// exprToString converts an AST expression to a Go source string.
|
||||
func exprToString(expr ast.Expr) string {
|
||||
switch e := expr.(type) {
|
||||
case *ast.BasicLit:
|
||||
return e.Value
|
||||
case *ast.Ident:
|
||||
return e.Name
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// parseStructField parses a struct field and returns FieldDef for each name.
|
||||
func parseStructField(field *ast.Field) []FieldDef {
|
||||
var fields []FieldDef
|
||||
@ -371,6 +690,30 @@ func parseHostFuncAnnotation(doc string) map[string]string {
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseCapabilityAnnotation extracts //nd:capability annotation parameters.
|
||||
func parseCapabilityAnnotation(doc string) map[string]string {
|
||||
for _, line := range strings.Split(doc, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
match := capabilityPattern.FindStringSubmatch(line)
|
||||
if match != nil {
|
||||
return parseKeyValuePairs(match[1])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseExportAnnotation extracts //nd:export annotation parameters.
|
||||
func parseExportAnnotation(doc string) map[string]string {
|
||||
for _, line := range strings.Split(doc, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
match := exportPattern.FindStringSubmatch(line)
|
||||
if match != nil {
|
||||
return parseKeyValuePairs(match[1])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseKeyValuePairs extracts key=value pairs from annotation text.
|
||||
func parseKeyValuePairs(text string) map[string]string {
|
||||
matches := keyValuePattern.FindAllStringSubmatch(text, -1)
|
||||
|
||||
@ -289,4 +289,255 @@ type TestService interface {
|
||||
Expect(s.ExportPrefix()).To(Equal("subsonicapi"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("ParseCapabilities", func() {
|
||||
It("should parse a simple capability interface", func() {
|
||||
src := `package capabilities
|
||||
|
||||
// MetadataAgent provides metadata retrieval.
|
||||
//nd:capability name=metadata
|
||||
type MetadataAgent interface {
|
||||
// GetArtistBiography returns artist biography.
|
||||
//nd:export name=nd_get_artist_biography
|
||||
GetArtistBiography(ArtistInput) (ArtistBiographyOutput, error)
|
||||
}
|
||||
|
||||
// ArtistInput is the input for artist-related functions.
|
||||
type ArtistInput struct {
|
||||
// ID is the artist ID.
|
||||
ID string ` + "`json:\"id\"`" + `
|
||||
// Name is the artist name.
|
||||
Name string ` + "`json:\"name\"`" + `
|
||||
}
|
||||
|
||||
// ArtistBiographyOutput is the output for GetArtistBiography.
|
||||
type ArtistBiographyOutput struct {
|
||||
// Biography is the biography text.
|
||||
Biography string ` + "`json:\"biography\"`" + `
|
||||
}
|
||||
`
|
||||
err := os.WriteFile(filepath.Join(tmpDir, "metadata.go"), []byte(src), 0600)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
capabilities, err := ParseCapabilities(tmpDir)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(capabilities).To(HaveLen(1))
|
||||
|
||||
cap := capabilities[0]
|
||||
Expect(cap.Name).To(Equal("metadata"))
|
||||
Expect(cap.Interface).To(Equal("MetadataAgent"))
|
||||
Expect(cap.Required).To(BeFalse())
|
||||
Expect(cap.Doc).To(ContainSubstring("MetadataAgent provides metadata retrieval"))
|
||||
Expect(cap.Methods).To(HaveLen(1))
|
||||
|
||||
m := cap.Methods[0]
|
||||
Expect(m.Name).To(Equal("GetArtistBiography"))
|
||||
Expect(m.ExportName).To(Equal("nd_get_artist_biography"))
|
||||
Expect(m.Input.Type).To(Equal("ArtistInput"))
|
||||
Expect(m.Output.Type).To(Equal("ArtistBiographyOutput"))
|
||||
|
||||
// Check structs were collected
|
||||
Expect(cap.Structs).To(HaveLen(2))
|
||||
})
|
||||
|
||||
It("should parse a required capability", func() {
|
||||
src := `package capabilities
|
||||
|
||||
// Scrobbler requires all methods to be implemented.
|
||||
//nd:capability name=scrobbler required=true
|
||||
type Scrobbler interface {
|
||||
//nd:export name=nd_scrobbler_is_authorized
|
||||
IsAuthorized(AuthInput) (AuthOutput, error)
|
||||
|
||||
//nd:export name=nd_scrobbler_scrobble
|
||||
Scrobble(ScrobbleInput) (ScrobblerOutput, error)
|
||||
}
|
||||
|
||||
type AuthInput struct {
|
||||
UserID string ` + "`json:\"userId\"`" + `
|
||||
}
|
||||
|
||||
type AuthOutput struct {
|
||||
Authorized bool ` + "`json:\"authorized\"`" + `
|
||||
}
|
||||
|
||||
type ScrobbleInput struct {
|
||||
UserID string ` + "`json:\"userId\"`" + `
|
||||
}
|
||||
|
||||
type ScrobblerOutput struct {
|
||||
Error *string ` + "`json:\"error,omitempty\"`" + `
|
||||
}
|
||||
`
|
||||
err := os.WriteFile(filepath.Join(tmpDir, "scrobbler.go"), []byte(src), 0600)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
capabilities, err := ParseCapabilities(tmpDir)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(capabilities).To(HaveLen(1))
|
||||
|
||||
cap := capabilities[0]
|
||||
Expect(cap.Name).To(Equal("scrobbler"))
|
||||
Expect(cap.Required).To(BeTrue())
|
||||
Expect(cap.Methods).To(HaveLen(2))
|
||||
})
|
||||
|
||||
It("should parse type aliases and consts", func() {
|
||||
src := `package capabilities
|
||||
|
||||
//nd:capability name=scrobbler required=true
|
||||
type Scrobbler interface {
|
||||
//nd:export name=nd_scrobble
|
||||
Scrobble(ScrobbleInput) (ScrobblerOutput, error)
|
||||
}
|
||||
|
||||
type ScrobbleInput struct {
|
||||
UserID string ` + "`json:\"userId\"`" + `
|
||||
}
|
||||
|
||||
// ScrobblerErrorType indicates error handling behavior.
|
||||
type ScrobblerErrorType string
|
||||
|
||||
const (
|
||||
// ScrobblerErrorNone indicates no error.
|
||||
ScrobblerErrorNone ScrobblerErrorType = "none"
|
||||
// ScrobblerErrorRetry indicates retry later.
|
||||
ScrobblerErrorRetry ScrobblerErrorType = "retry"
|
||||
)
|
||||
|
||||
type ScrobblerOutput struct {
|
||||
ErrorType *ScrobblerErrorType ` + "`json:\"errorType,omitempty\"`" + `
|
||||
}
|
||||
`
|
||||
err := os.WriteFile(filepath.Join(tmpDir, "scrobbler.go"), []byte(src), 0600)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
capabilities, err := ParseCapabilities(tmpDir)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(capabilities).To(HaveLen(1))
|
||||
|
||||
cap := capabilities[0]
|
||||
// Type alias should be collected
|
||||
Expect(cap.TypeAliases).To(HaveLen(1))
|
||||
Expect(cap.TypeAliases[0].Name).To(Equal("ScrobblerErrorType"))
|
||||
Expect(cap.TypeAliases[0].Type).To(Equal("string"))
|
||||
|
||||
// Consts should be collected
|
||||
Expect(cap.Consts).To(HaveLen(1))
|
||||
Expect(cap.Consts[0].Type).To(Equal("ScrobblerErrorType"))
|
||||
Expect(cap.Consts[0].Values).To(HaveLen(2))
|
||||
Expect(cap.Consts[0].Values[0].Name).To(Equal("ScrobblerErrorNone"))
|
||||
Expect(cap.Consts[0].Values[0].Value).To(Equal(`"none"`))
|
||||
})
|
||||
|
||||
It("should collect nested struct dependencies", func() {
|
||||
src := `package capabilities
|
||||
|
||||
//nd:capability name=metadata
|
||||
type MetadataAgent interface {
|
||||
//nd:export name=nd_get_images
|
||||
GetImages(ArtistInput) (ImagesOutput, error)
|
||||
}
|
||||
|
||||
type ArtistInput struct {
|
||||
ID string ` + "`json:\"id\"`" + `
|
||||
}
|
||||
|
||||
type ImagesOutput struct {
|
||||
Images []ImageInfo ` + "`json:\"images\"`" + `
|
||||
}
|
||||
|
||||
type ImageInfo struct {
|
||||
URL string ` + "`json:\"url\"`" + `
|
||||
Size int32 ` + "`json:\"size\"`" + `
|
||||
}
|
||||
`
|
||||
err := os.WriteFile(filepath.Join(tmpDir, "metadata.go"), []byte(src), 0600)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
capabilities, err := ParseCapabilities(tmpDir)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(capabilities).To(HaveLen(1))
|
||||
|
||||
cap := capabilities[0]
|
||||
// Should collect all 3 structs: ArtistInput, ImagesOutput, and ImageInfo
|
||||
Expect(cap.Structs).To(HaveLen(3))
|
||||
|
||||
structNames := make([]string, len(cap.Structs))
|
||||
for i, s := range cap.Structs {
|
||||
structNames[i] = s.Name
|
||||
}
|
||||
Expect(structNames).To(ContainElements("ArtistInput", "ImagesOutput", "ImageInfo"))
|
||||
})
|
||||
|
||||
It("should return empty slice for directory with no capabilities", func() {
|
||||
src := `package capabilities
|
||||
|
||||
type RegularInterface interface {
|
||||
Method() error
|
||||
}
|
||||
`
|
||||
err := os.WriteFile(filepath.Join(tmpDir, "test.go"), []byte(src), 0600)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
capabilities, err := ParseCapabilities(tmpDir)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(capabilities).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("should ignore methods without export annotation", func() {
|
||||
src := `package capabilities
|
||||
|
||||
//nd:capability name=test
|
||||
type TestCapability interface {
|
||||
//nd:export name=nd_exported
|
||||
ExportedMethod(Input) (Output, error)
|
||||
|
||||
// This method has no export annotation
|
||||
NotExportedMethod(Input) (Output, error)
|
||||
}
|
||||
|
||||
type Input struct {
|
||||
Value string ` + "`json:\"value\"`" + `
|
||||
}
|
||||
|
||||
type Output struct {
|
||||
Result string ` + "`json:\"result\"`" + `
|
||||
}
|
||||
`
|
||||
err := os.WriteFile(filepath.Join(tmpDir, "test.go"), []byte(src), 0600)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
capabilities, err := ParseCapabilities(tmpDir)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(capabilities).To(HaveLen(1))
|
||||
|
||||
// Only the exported method should be captured
|
||||
Expect(capabilities[0].Methods).To(HaveLen(1))
|
||||
Expect(capabilities[0].Methods[0].Name).To(Equal("ExportedMethod"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Export helpers", func() {
|
||||
It("should generate correct provider interface name", func() {
|
||||
e := Export{Name: "GetArtistBiography"}
|
||||
Expect(e.ProviderInterfaceName()).To(Equal("ArtistBiographyProvider"))
|
||||
|
||||
e = Export{Name: "OnInit"}
|
||||
Expect(e.ProviderInterfaceName()).To(Equal("InitProvider"))
|
||||
})
|
||||
|
||||
It("should generate correct impl variable name", func() {
|
||||
e := Export{Name: "GetArtistBiography"}
|
||||
Expect(e.ImplVarName()).To(Equal("artistBiographyImpl"))
|
||||
|
||||
e = Export{Name: "OnInit"}
|
||||
Expect(e.ImplVarName()).To(Equal("initImpl"))
|
||||
})
|
||||
|
||||
It("should generate correct export function name", func() {
|
||||
e := Export{Name: "GetArtistBiography", ExportName: "nd_get_artist_biography"}
|
||||
Expect(e.ExportFuncName()).To(Equal("_NdGetArtistBiography"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
150
plugins/cmd/ndpgen/internal/templates/capability.go.tmpl
Normal file
150
plugins/cmd/ndpgen/internal/templates/capability.go.tmpl
Normal file
@ -0,0 +1,150 @@
|
||||
// Code generated by ndpgen. DO NOT EDIT.
|
||||
//
|
||||
// This file contains export wrappers for the {{.Capability.Interface}} capability.
|
||||
// It is intended for use in Navidrome plugins built with TinyGo.
|
||||
//
|
||||
//go:build wasip1
|
||||
|
||||
package {{.Package}}
|
||||
|
||||
import (
|
||||
pdk "github.com/extism/go-pdk"
|
||||
)
|
||||
|
||||
{{- /* Generate type alias definitions */ -}}
|
||||
{{- range .Capability.TypeAliases}}
|
||||
|
||||
{{- if .Doc}}
|
||||
{{formatDoc .Doc}}
|
||||
{{- end}}
|
||||
type {{.Name}} {{.Type}}
|
||||
{{- end}}
|
||||
|
||||
{{- /* Generate const definitions */ -}}
|
||||
{{- range .Capability.Consts}}
|
||||
{{- if .Values}}
|
||||
|
||||
const (
|
||||
{{- range .Values}}
|
||||
{{- if .Doc}}
|
||||
{{formatDoc .Doc | indent 1}}
|
||||
{{- end}}
|
||||
{{.Name}} = {{.Value}}
|
||||
{{- end}}
|
||||
)
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
|
||||
{{- /* Generate struct definitions */ -}}
|
||||
{{- range .Capability.Structs}}
|
||||
|
||||
// {{.Name}} represents the {{.Name}} data structure.
|
||||
{{- if .Doc}}
|
||||
{{formatDoc .Doc}}
|
||||
{{- end}}
|
||||
type {{.Name}} struct {
|
||||
{{- range .Fields}}
|
||||
{{- if .Doc}}
|
||||
{{formatDoc .Doc | indent 1}}
|
||||
{{- end}}
|
||||
{{.Name}} {{.Type}} `json:"{{.JSONTag}}{{if .OmitEmpty}},omitempty{{end}}"`
|
||||
{{- end}}
|
||||
}
|
||||
{{- end}}
|
||||
|
||||
{{- /* Generate main interface based on required flag */ -}}
|
||||
{{if .Capability.Required}}
|
||||
|
||||
// {{agentName .Capability}} requires all methods to be implemented.
|
||||
{{- if .Capability.Doc}}
|
||||
{{formatDoc .Capability.Doc}}
|
||||
{{- end}}
|
||||
type {{agentName .Capability}} interface {
|
||||
{{- range .Capability.Methods}}
|
||||
// {{.Name}}{{if .Doc}} - {{.Doc}}{{end}}
|
||||
{{.Name}}({{.Input.Type}}) ({{.Output.Type}}, error)
|
||||
{{- end}}
|
||||
}
|
||||
{{- else}}
|
||||
|
||||
// {{agentName .Capability}} is the marker interface for {{.Package}} plugins.
|
||||
// Implement one or more of the provider interfaces below.
|
||||
{{- if .Capability.Doc}}
|
||||
{{formatDoc .Capability.Doc}}
|
||||
{{- end}}
|
||||
type {{agentName .Capability}} interface{}
|
||||
{{- end}}
|
||||
|
||||
{{- /* Generate optional provider interfaces for non-required capabilities */ -}}
|
||||
{{- if not .Capability.Required}}
|
||||
{{- range .Capability.Methods}}
|
||||
|
||||
// {{providerInterface .}} provides the {{.Name}} function.
|
||||
type {{providerInterface .}} interface {
|
||||
{{.Name}}({{.Input.Type}}) ({{.Output.Type}}, error)
|
||||
}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
|
||||
{{- /* Generate implementation function holders */ -}}
|
||||
|
||||
// Internal implementation holders
|
||||
var (
|
||||
{{- range .Capability.Methods}}
|
||||
{{implVar .}} func({{.Input.Type}}) ({{.Output.Type}}, error)
|
||||
{{- end}}
|
||||
)
|
||||
|
||||
// Register registers a {{.Package}} implementation.
|
||||
{{- if .Capability.Required}}
|
||||
// All methods are required.
|
||||
func Register(impl {{agentName .Capability}}) {
|
||||
{{- range .Capability.Methods}}
|
||||
{{implVar .}} = impl.{{.Name}}
|
||||
{{- end}}
|
||||
}
|
||||
{{- else}}
|
||||
// The implementation is checked for optional provider interfaces.
|
||||
func Register(impl {{agentName .Capability}}) {
|
||||
{{- range .Capability.Methods}}
|
||||
if p, ok := impl.({{providerInterface .}}); ok {
|
||||
{{implVar .}} = p.{{.Name}}
|
||||
}
|
||||
{{- end}}
|
||||
}
|
||||
{{- end}}
|
||||
|
||||
// NotImplementedCode is the standard return code for unimplemented functions.
|
||||
// The host recognizes this and skips the plugin gracefully.
|
||||
const NotImplementedCode int32 = -2
|
||||
|
||||
{{- /* Generate export wrappers */ -}}
|
||||
{{range .Capability.Methods}}
|
||||
|
||||
//export {{.ExportName}}
|
||||
func {{exportFunc .}}() int32 {
|
||||
if {{implVar .}} == nil {
|
||||
// Return standard code - host will skip this plugin gracefully
|
||||
return NotImplementedCode
|
||||
}
|
||||
|
||||
var input {{.Input.Type}}
|
||||
if err := pdk.InputJSON(&input); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
output, err := {{implVar .}}(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
if err := pdk.OutputJSON(output); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
{{- end}}
|
||||
@ -0,0 +1,95 @@
|
||||
// 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 {{.Package}}
|
||||
|
||||
{{- /* Generate type alias definitions */ -}}
|
||||
{{- range .Capability.TypeAliases}}
|
||||
|
||||
{{- if .Doc}}
|
||||
{{formatDoc .Doc}}
|
||||
{{- end}}
|
||||
type {{.Name}} {{.Type}}
|
||||
{{- end}}
|
||||
|
||||
{{- /* Generate const definitions */ -}}
|
||||
{{- range .Capability.Consts}}
|
||||
{{- if .Values}}
|
||||
|
||||
const (
|
||||
{{- range .Values}}
|
||||
{{- if .Doc}}
|
||||
{{formatDoc .Doc | indent 1}}
|
||||
{{- end}}
|
||||
{{.Name}} = {{.Value}}
|
||||
{{- end}}
|
||||
)
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
|
||||
{{- /* Generate struct definitions */ -}}
|
||||
{{- range .Capability.Structs}}
|
||||
|
||||
// {{.Name}} represents the {{.Name}} data structure.
|
||||
{{- if .Doc}}
|
||||
{{formatDoc .Doc}}
|
||||
{{- end}}
|
||||
type {{.Name}} struct {
|
||||
{{- range .Fields}}
|
||||
{{- if .Doc}}
|
||||
{{formatDoc .Doc | indent 1}}
|
||||
{{- end}}
|
||||
{{.Name}} {{.Type}} `json:"{{.JSONTag}}{{if .OmitEmpty}},omitempty{{end}}"`
|
||||
{{- end}}
|
||||
}
|
||||
{{- end}}
|
||||
|
||||
{{- /* Generate main interface based on required flag */ -}}
|
||||
{{if .Capability.Required}}
|
||||
|
||||
// {{agentName .Capability}} requires all methods to be implemented.
|
||||
{{- if .Capability.Doc}}
|
||||
{{formatDoc .Capability.Doc}}
|
||||
{{- end}}
|
||||
type {{agentName .Capability}} interface {
|
||||
{{- range .Capability.Methods}}
|
||||
// {{.Name}}{{if .Doc}} - {{.Doc}}{{end}}
|
||||
{{.Name}}({{.Input.Type}}) ({{.Output.Type}}, error)
|
||||
{{- end}}
|
||||
}
|
||||
{{- else}}
|
||||
|
||||
// {{agentName .Capability}} is the marker interface for {{.Package}} plugins.
|
||||
// Implement one or more of the provider interfaces below.
|
||||
{{- if .Capability.Doc}}
|
||||
{{formatDoc .Capability.Doc}}
|
||||
{{- end}}
|
||||
type {{agentName .Capability}} interface{}
|
||||
{{- end}}
|
||||
|
||||
{{- /* Generate optional provider interfaces for non-required capabilities */ -}}
|
||||
{{- if not .Capability.Required}}
|
||||
{{- range .Capability.Methods}}
|
||||
|
||||
// {{providerInterface .}} provides the {{.Name}} function.
|
||||
type {{providerInterface .}} interface {
|
||||
{{.Name}}({{.Input.Type}}) ({{.Output.Type}}, error)
|
||||
}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
|
||||
// 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.
|
||||
{{- if .Capability.Required}}
|
||||
func Register(_ {{agentName .Capability}}) {}
|
||||
{{- else}}
|
||||
func Register(_ {{agentName .Capability}}) {}
|
||||
{{- end}}
|
||||
@ -15,6 +15,103 @@ type Service struct {
|
||||
Structs []StructDef // Structs used by this service
|
||||
}
|
||||
|
||||
// Capability represents a parsed capability interface for plugin exports.
|
||||
type Capability struct {
|
||||
Name string // Package name from annotation (e.g., "metadata")
|
||||
Interface string // Go interface name (e.g., "MetadataAgent")
|
||||
Required bool // If true, all methods must be implemented
|
||||
Methods []Export // Methods marked with //nd:export
|
||||
Doc string // Documentation comment for the capability
|
||||
Structs []StructDef // Structs used by this capability
|
||||
TypeAliases []TypeAlias // Type aliases used by this capability
|
||||
Consts []ConstGroup // Const groups used by this capability
|
||||
}
|
||||
|
||||
// TypeAlias represents a type alias definition (e.g., type ScrobblerErrorType string).
|
||||
type TypeAlias struct {
|
||||
Name string // Type name
|
||||
Type string // Underlying type
|
||||
Doc string // Documentation comment
|
||||
}
|
||||
|
||||
// ConstGroup represents a group of const definitions.
|
||||
type ConstGroup struct {
|
||||
Type string // Type name for typed consts (empty for untyped)
|
||||
Values []ConstDef // Const definitions
|
||||
}
|
||||
|
||||
// ConstDef represents a single const definition.
|
||||
type ConstDef struct {
|
||||
Name string // Const name
|
||||
Value string // Const value
|
||||
Doc string // Documentation comment
|
||||
}
|
||||
|
||||
// KnownStructs returns a map of struct names defined in this capability.
|
||||
func (c Capability) KnownStructs() map[string]bool {
|
||||
result := make(map[string]bool)
|
||||
for _, st := range c.Structs {
|
||||
result[st.Name] = true
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Export represents an exported WASM function within a capability.
|
||||
type Export struct {
|
||||
Name string // Go method name (e.g., "GetArtistBiography")
|
||||
ExportName string // WASM export name (e.g., "nd_get_artist_biography")
|
||||
Input Param // Single input parameter (the struct type)
|
||||
Output Param // Single output return value (the struct type)
|
||||
Doc string // Documentation comment for the method
|
||||
}
|
||||
|
||||
// ProviderInterfaceName returns the optional provider interface name.
|
||||
// For a method "GetArtistBiography", returns "ArtistBiographyProvider".
|
||||
func (e Export) ProviderInterfaceName() string {
|
||||
// Remove "Get", "On", etc. prefixes and add "Provider" suffix
|
||||
name := e.Name
|
||||
for _, prefix := range []string{"Get", "On"} {
|
||||
if strings.HasPrefix(name, prefix) {
|
||||
name = name[len(prefix):]
|
||||
break
|
||||
}
|
||||
}
|
||||
return name + "Provider"
|
||||
}
|
||||
|
||||
// ImplVarName returns the internal implementation variable name.
|
||||
// For "GetArtistBiography", returns "artistBiographyImpl".
|
||||
func (e Export) ImplVarName() string {
|
||||
name := e.Name
|
||||
for _, prefix := range []string{"Get", "On"} {
|
||||
if strings.HasPrefix(name, prefix) {
|
||||
name = name[len(prefix):]
|
||||
break
|
||||
}
|
||||
}
|
||||
// Convert to camelCase
|
||||
if len(name) > 0 {
|
||||
name = strings.ToLower(string(name[0])) + name[1:]
|
||||
}
|
||||
return name + "Impl"
|
||||
}
|
||||
|
||||
// ExportFuncName returns the unexported WASM export function name.
|
||||
// For "nd_get_artist_biography", returns "_ndGetArtistBiography".
|
||||
func (e Export) ExportFuncName() string {
|
||||
// Convert snake_case to PascalCase
|
||||
parts := strings.Split(e.ExportName, "_")
|
||||
var result strings.Builder
|
||||
result.WriteString("_")
|
||||
for _, part := range parts {
|
||||
if len(part) > 0 {
|
||||
result.WriteString(strings.ToUpper(string(part[0])))
|
||||
result.WriteString(part[1:])
|
||||
}
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// StructDef represents a Go struct type definition.
|
||||
type StructDef struct {
|
||||
Name string // Go struct name (e.g., "Library")
|
||||
|
||||
@ -1,28 +1,32 @@
|
||||
// ndpgen generates Navidrome Plugin Development Kit (PDK) code from annotated Go interfaces.
|
||||
//
|
||||
// This is the unified code generator that replaces hostgen and handles both host function
|
||||
// wrappers and capability wrappers (when implemented).
|
||||
// This is the unified code generator that handles both host function wrappers
|
||||
// and capability export wrappers.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// ndpgen -input=./plugins/host -output=./plugins/pdk
|
||||
// # Generate host wrappers (from plugins/host to plugins/pdk)
|
||||
// ndpgen -host-only -input=./plugins/host -output=./plugins/pdk
|
||||
//
|
||||
// This generates code into language-specific subdirectories:
|
||||
// - Go: $output/go/host/
|
||||
// - Python: $output/python/host/
|
||||
// - Rust: $output/rust/host/
|
||||
// # Generate capability wrappers (from plugins/capabilities to plugins/pdk)
|
||||
// ndpgen -capability-only -input=./plugins/capabilities -output=./plugins/pdk
|
||||
//
|
||||
// Output directories:
|
||||
// - Host functions: $output/go/host/, $output/python/host/, $output/rust/host/
|
||||
// - Capabilities: $output/go/<capability>/ (e.g., $output/go/metadata/)
|
||||
//
|
||||
// Flags:
|
||||
//
|
||||
// -input Input directory containing Go source files with annotated interfaces
|
||||
// -output Output directory base for generated files (default: same as input)
|
||||
// -package Output package name for Go (default: host)
|
||||
// -host-only Generate only host function wrappers (default: true, capability support TBD)
|
||||
// -go Generate Go client wrappers (default: true when not using -python/-rust)
|
||||
// -python Generate Python client wrappers (default: false)
|
||||
// -rust Generate Rust client wrappers (default: false)
|
||||
// -v Verbose output
|
||||
// -dry-run Preview generated code without writing files
|
||||
// -input Input directory containing Go source files with annotated interfaces
|
||||
// -output Output directory base for generated files (default: same as input)
|
||||
// -package Output package name for Go (default: host for host-only, auto for capabilities)
|
||||
// -host-only Generate only host function wrappers
|
||||
// -capability-only Generate only capability export wrappers
|
||||
// -go Generate Go client wrappers (default: true when not using -python/-rust)
|
||||
// -python Generate Python client wrappers (default: false)
|
||||
// -rust Generate Rust client wrappers (default: false)
|
||||
// -v Verbose output
|
||||
// -dry-run Preview generated code without writing files
|
||||
package main
|
||||
|
||||
import (
|
||||
@ -40,11 +44,12 @@ import (
|
||||
type config struct {
|
||||
inputDir string
|
||||
outputDir string // Base output directory (e.g., plugins/pdk)
|
||||
goOutputDir string // Go output: $outputDir/go/host
|
||||
goOutputDir string // Go output: $outputDir/go/host (for host-only)
|
||||
pythonOutputDir string // Python output: $outputDir/python/host
|
||||
rustOutputDir string // Rust output: $outputDir/rust/host
|
||||
pkgName string
|
||||
hostOnly bool
|
||||
capabilityOnly bool
|
||||
generateGoClient bool
|
||||
generatePyClient bool
|
||||
generateRsClient bool
|
||||
@ -59,6 +64,15 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if cfg.capabilityOnly {
|
||||
if err := runCapabilityGeneration(cfg); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Default: host-only mode
|
||||
services, err := parseServices(cfg)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
@ -74,25 +88,60 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// runCapabilityGeneration handles capability-only code generation.
|
||||
func runCapabilityGeneration(cfg *config) error {
|
||||
capabilities, err := parseCapabilities(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(capabilities) == 0 {
|
||||
if cfg.verbose {
|
||||
fmt.Println("No capabilities found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return generateCapabilityCode(cfg, capabilities)
|
||||
}
|
||||
|
||||
// parseConfig parses command-line flags and returns the configuration.
|
||||
func parseConfig() (*config, error) {
|
||||
var (
|
||||
inputDir = flag.String("input", ".", "Input directory containing Go source files")
|
||||
outputDir = flag.String("output", "", "Base output directory for generated files (default: same as input)")
|
||||
pkgName = flag.String("package", "host", "Output package name for Go (default: host)")
|
||||
hostOnly = flag.Bool("host-only", true, "Generate only host function wrappers (capability support TBD)")
|
||||
goClient = flag.Bool("go", false, "Generate Go client wrappers")
|
||||
pyClient = flag.Bool("python", false, "Generate Python client wrappers")
|
||||
rsClient = flag.Bool("rust", false, "Generate Rust client wrappers")
|
||||
verbose = flag.Bool("v", false, "Verbose output")
|
||||
dryRun = flag.Bool("dry-run", false, "Preview generated code without writing files")
|
||||
inputDir = flag.String("input", ".", "Input directory containing Go source files")
|
||||
outputDir = flag.String("output", "", "Base output directory for generated files (default: same as input)")
|
||||
pkgName = flag.String("package", "", "Output package name for Go (default: host for host-only, auto for capabilities)")
|
||||
hostOnly = flag.Bool("host-only", false, "Generate only host function wrappers")
|
||||
capabilityOnly = flag.Bool("capability-only", false, "Generate only capability export wrappers")
|
||||
goClient = flag.Bool("go", false, "Generate Go client wrappers")
|
||||
pyClient = flag.Bool("python", false, "Generate Python client wrappers")
|
||||
rsClient = flag.Bool("rust", false, "Generate Rust client wrappers")
|
||||
verbose = flag.Bool("v", false, "Verbose output")
|
||||
dryRun = flag.Bool("dry-run", false, "Preview generated code without writing files")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
// Default to host-only if neither mode is specified
|
||||
if !*hostOnly && !*capabilityOnly {
|
||||
*hostOnly = true
|
||||
}
|
||||
|
||||
// Cannot specify both modes
|
||||
if *hostOnly && *capabilityOnly {
|
||||
return nil, fmt.Errorf("cannot specify both -host-only and -capability-only")
|
||||
}
|
||||
|
||||
if *outputDir == "" {
|
||||
*outputDir = *inputDir
|
||||
}
|
||||
|
||||
// Default package name based on mode
|
||||
if *pkgName == "" {
|
||||
if *hostOnly {
|
||||
*pkgName = "host"
|
||||
}
|
||||
// For capability-only, package name is derived from capability annotation
|
||||
}
|
||||
|
||||
absInput, err := filepath.Abs(*inputDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolving input path: %w", err)
|
||||
@ -119,6 +168,7 @@ func parseConfig() (*config, error) {
|
||||
rustOutputDir: absRustOutput,
|
||||
pkgName: *pkgName,
|
||||
hostOnly: *hostOnly,
|
||||
capabilityOnly: *capabilityOnly,
|
||||
generateGoClient: *goClient || !anyLangFlag,
|
||||
generatePyClient: *pyClient,
|
||||
generateRsClient: *rsClient,
|
||||
@ -170,6 +220,109 @@ func parseServices(cfg *config) ([]internal.Service, error) {
|
||||
return services, nil
|
||||
}
|
||||
|
||||
// parseCapabilities parses source files and returns discovered capabilities.
|
||||
func parseCapabilities(cfg *config) ([]internal.Capability, error) {
|
||||
if cfg.verbose {
|
||||
fmt.Printf("Input directory: %s\n", cfg.inputDir)
|
||||
fmt.Printf("Base output directory: %s\n", cfg.outputDir)
|
||||
fmt.Printf("Capability-only mode: %v\n", cfg.capabilityOnly)
|
||||
}
|
||||
|
||||
capabilities, err := internal.ParseCapabilities(cfg.inputDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing capability files: %w", err)
|
||||
}
|
||||
|
||||
if len(capabilities) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if cfg.verbose {
|
||||
fmt.Printf("Found %d capability(ies)\n", len(capabilities))
|
||||
for _, cap := range capabilities {
|
||||
fmt.Printf(" - %s (%d exports, required=%v)\n", cap.Name, len(cap.Methods), cap.Required)
|
||||
}
|
||||
}
|
||||
|
||||
return capabilities, nil
|
||||
}
|
||||
|
||||
// generateCapabilityCode generates Go export wrappers for all capabilities.
|
||||
func generateCapabilityCode(cfg *config, capabilities []internal.Capability) error {
|
||||
for _, cap := range capabilities {
|
||||
// Output directory is $output/go/<capability_name>/
|
||||
outputDir := filepath.Join(cfg.outputDir, "go", cap.Name)
|
||||
|
||||
if err := generateCapabilityGoCode(cap, outputDir, cfg.dryRun, cfg.verbose); err != nil {
|
||||
return fmt.Errorf("generating capability code for %s: %w", cap.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateCapabilityGoCode generates Go export wrapper code for a capability.
|
||||
func generateCapabilityGoCode(cap internal.Capability, outputDir string, dryRun, verbose bool) error {
|
||||
// Use the capability name as the package name
|
||||
pkgName := cap.Name
|
||||
|
||||
// Generate the main WASM code
|
||||
code, err := internal.GenerateCapabilityGo(cap, pkgName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generating code: %w", err)
|
||||
}
|
||||
|
||||
formatted, err := format.Source(code)
|
||||
if err != nil {
|
||||
return fmt.Errorf("formatting code: %w\nRaw code:\n%s", err, code)
|
||||
}
|
||||
|
||||
mainFile := filepath.Join(outputDir, cap.Name+".go")
|
||||
|
||||
if dryRun {
|
||||
fmt.Printf("=== %s ===\n%s\n", mainFile, formatted)
|
||||
} else {
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
return fmt.Errorf("creating output directory: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(mainFile, formatted, 0600); err != nil {
|
||||
return fmt.Errorf("writing file: %w", err)
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Printf("Generated capability code: %s\n", mainFile)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate the stub code for non-WASM platforms
|
||||
stubCode, err := internal.GenerateCapabilityGoStub(cap, pkgName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generating stub code: %w", err)
|
||||
}
|
||||
|
||||
formattedStub, err := format.Source(stubCode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("formatting stub code: %w\nRaw code:\n%s", err, stubCode)
|
||||
}
|
||||
|
||||
stubFile := filepath.Join(outputDir, cap.Name+"_stub.go")
|
||||
|
||||
if dryRun {
|
||||
fmt.Printf("=== %s ===\n%s\n", stubFile, formattedStub)
|
||||
} else {
|
||||
if err := os.WriteFile(stubFile, formattedStub, 0600); err != nil {
|
||||
return fmt.Errorf("writing stub file: %w", err)
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Printf("Generated capability stub: %s\n", stubFile)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateAllCode generates all requested code for the services.
|
||||
func generateAllCode(cfg *config, services []internal.Service) error {
|
||||
for _, svc := range services {
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
module wikimedia-plugin
|
||||
|
||||
go 1.23
|
||||
go 1.25
|
||||
|
||||
require github.com/extism/go-pdk v1.1.3
|
||||
require (
|
||||
github.com/extism/go-pdk v1.1.3
|
||||
github.com/navidrome/navidrome v0.0.0-00010101000000-000000000000
|
||||
)
|
||||
|
||||
replace github.com/navidrome/navidrome => ../../..
|
||||
|
||||
@ -1,15 +1,7 @@
|
||||
// Wikimedia plugin for Navidrome - fetches artist metadata from Wikidata, DBpedia and Wikipedia.
|
||||
//
|
||||
// This plugin was generated using:
|
||||
//
|
||||
// xtp plugin init --schema-file plugins/schemas/metadata_agent.yaml --template go --path ./wikimedia --name wikimedia-plugin
|
||||
//
|
||||
// Build with:
|
||||
//
|
||||
// xtp plugin build
|
||||
//
|
||||
// Or manually:
|
||||
//
|
||||
// tinygo build -o wikimedia.wasm -target wasip1 -buildmode=c-shared .
|
||||
//
|
||||
// Install by copying the .ndp file to your Navidrome plugins folder.
|
||||
@ -22,7 +14,23 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/extism/go-pdk"
|
||||
pdk "github.com/extism/go-pdk"
|
||||
"github.com/navidrome/navidrome/plugins/pdk/go/metadata"
|
||||
)
|
||||
|
||||
// wikimediaPlugin implements the metadata provider interfaces for the methods we support.
|
||||
type wikimediaPlugin struct{}
|
||||
|
||||
// init registers the plugin implementation
|
||||
func init() {
|
||||
metadata.Register(&wikimediaPlugin{})
|
||||
}
|
||||
|
||||
// Ensure wikimediaPlugin implements the provider interfaces
|
||||
var (
|
||||
_ metadata.ArtistURLProvider = (*wikimediaPlugin)(nil)
|
||||
_ metadata.ArtistBiographyProvider = (*wikimediaPlugin)(nil)
|
||||
_ metadata.ArtistImagesProvider = (*wikimediaPlugin)(nil)
|
||||
)
|
||||
|
||||
const (
|
||||
@ -232,15 +240,15 @@ func getMBID(mbid *string) string {
|
||||
return *mbid
|
||||
}
|
||||
|
||||
// NdGetArtistUrl returns the Wikipedia URL for an artist
|
||||
func NdGetArtistUrl(input ArtistInput) (ArtistURLOutput, error) {
|
||||
mbid := getMBID(input.Mbid)
|
||||
// GetArtistURL returns the Wikipedia URL for an artist
|
||||
func (*wikimediaPlugin) GetArtistURL(input metadata.ArtistInput) (metadata.ArtistURLOutput, error) {
|
||||
mbid := getMBID(input.MBID)
|
||||
pdk.Log(pdk.LogDebug, fmt.Sprintf("GetArtistURL: name=%s, mbid=%s", input.Name, mbid))
|
||||
|
||||
// 1. Try Wikidata (MBID first, then name)
|
||||
wikiURL, err := getWikidataWikipediaURL(mbid, input.Name)
|
||||
if err == nil && wikiURL != "" {
|
||||
return ArtistURLOutput{Url: wikiURL}, nil
|
||||
return metadata.ArtistURLOutput{URL: wikiURL}, nil
|
||||
}
|
||||
if err != nil {
|
||||
pdk.Log(pdk.LogDebug, fmt.Sprintf("Wikidata URL failed: %v", err))
|
||||
@ -250,7 +258,7 @@ func NdGetArtistUrl(input ArtistInput) (ArtistURLOutput, error) {
|
||||
if input.Name != "" {
|
||||
wikiURL, err = getDBpediaWikipediaURL(input.Name)
|
||||
if err == nil && wikiURL != "" {
|
||||
return ArtistURLOutput{Url: wikiURL}, nil
|
||||
return metadata.ArtistURLOutput{URL: wikiURL}, nil
|
||||
}
|
||||
if err != nil {
|
||||
pdk.Log(pdk.LogDebug, fmt.Sprintf("DBpedia URL failed: %v", err))
|
||||
@ -261,15 +269,15 @@ func NdGetArtistUrl(input ArtistInput) (ArtistURLOutput, error) {
|
||||
if input.Name != "" {
|
||||
searchURL := fmt.Sprintf("https://en.wikipedia.org/w/index.php?search=%s", url.QueryEscape(input.Name))
|
||||
pdk.Log(pdk.LogInfo, fmt.Sprintf("URL not found, falling back to search URL: %s", searchURL))
|
||||
return ArtistURLOutput{Url: searchURL}, nil
|
||||
return metadata.ArtistURLOutput{URL: searchURL}, nil
|
||||
}
|
||||
|
||||
return ArtistURLOutput{}, errors.New("could not determine Wikipedia URL")
|
||||
return metadata.ArtistURLOutput{}, errors.New("could not determine Wikipedia URL")
|
||||
}
|
||||
|
||||
// NdGetArtistBiography returns the biography for an artist from Wikipedia
|
||||
func NdGetArtistBiography(input ArtistInput) (ArtistBiographyOutput, error) {
|
||||
mbid := getMBID(input.Mbid)
|
||||
// GetArtistBiography returns the biography for an artist from Wikipedia
|
||||
func (*wikimediaPlugin) GetArtistBiography(input metadata.ArtistInput) (metadata.ArtistBiographyOutput, error) {
|
||||
mbid := getMBID(input.MBID)
|
||||
pdk.Log(pdk.LogDebug, fmt.Sprintf("GetArtistBiography: name=%s, mbid=%s", input.Name, mbid))
|
||||
|
||||
// 1. Get Wikipedia URL (using the logic from GetArtistURL)
|
||||
@ -297,7 +305,7 @@ func NdGetArtistBiography(input ArtistInput) (ArtistBiographyOutput, error) {
|
||||
bio, err := getWikipediaExtract(pageTitle)
|
||||
if err == nil && bio != "" {
|
||||
pdk.Log(pdk.LogDebug, "Found Wikipedia extract")
|
||||
return ArtistBiographyOutput{Biography: bio}, nil
|
||||
return metadata.ArtistBiographyOutput{Biography: bio}, nil
|
||||
}
|
||||
pdk.Log(pdk.LogDebug, fmt.Sprintf("Wikipedia extract failed: %v", err))
|
||||
} else {
|
||||
@ -311,18 +319,18 @@ func NdGetArtistBiography(input ArtistInput) (ArtistBiographyOutput, error) {
|
||||
bio, err := getDBpediaComment(input.Name)
|
||||
if err == nil && bio != "" {
|
||||
pdk.Log(pdk.LogDebug, "Found DBpedia comment")
|
||||
return ArtistBiographyOutput{Biography: bio}, nil
|
||||
return metadata.ArtistBiographyOutput{Biography: bio}, nil
|
||||
}
|
||||
pdk.Log(pdk.LogDebug, fmt.Sprintf("DBpedia comment failed: %v", err))
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogInfo, fmt.Sprintf("Biography not found for: %s (%s)", input.Name, mbid))
|
||||
return ArtistBiographyOutput{}, errors.New("biography not found")
|
||||
return metadata.ArtistBiographyOutput{}, errors.New("biography not found")
|
||||
}
|
||||
|
||||
// NdGetArtistImages returns artist images from Wikidata
|
||||
func NdGetArtistImages(input ArtistInput) (ArtistImagesOutput, error) {
|
||||
mbid := getMBID(input.Mbid)
|
||||
// GetArtistImages returns artist images from Wikidata
|
||||
func (*wikimediaPlugin) GetArtistImages(input metadata.ArtistInput) (metadata.ArtistImagesOutput, error) {
|
||||
mbid := getMBID(input.MBID)
|
||||
pdk.Log(pdk.LogDebug, fmt.Sprintf("GetArtistImages: name=%s, mbid=%s", input.Name, mbid))
|
||||
|
||||
var q string
|
||||
@ -332,43 +340,23 @@ func NdGetArtistImages(input ArtistInput) (ArtistImagesOutput, error) {
|
||||
escapedName := strings.ReplaceAll(input.Name, "\"", "\\\"")
|
||||
q = fmt.Sprintf(`SELECT ?img WHERE { ?artist rdfs:label "%s"@en; wdt:P18 ?img } LIMIT 1`, escapedName)
|
||||
} else {
|
||||
return ArtistImagesOutput{}, errors.New("MBID or Name required for Wikidata Image lookup")
|
||||
return metadata.ArtistImagesOutput{}, errors.New("MBID or Name required for Wikidata Image lookup")
|
||||
}
|
||||
|
||||
result, err := sparqlQuery(wikidataEndpoint, q)
|
||||
if err != nil {
|
||||
pdk.Log(pdk.LogInfo, fmt.Sprintf("Image not found for: %s (%s)", input.Name, mbid))
|
||||
return ArtistImagesOutput{}, errors.New("image not found")
|
||||
return metadata.ArtistImagesOutput{}, errors.New("image not found")
|
||||
}
|
||||
if result.Results.Bindings[0].Img != nil {
|
||||
return ArtistImagesOutput{
|
||||
Images: []ImageInfo{{Url: result.Results.Bindings[0].Img.Value, Size: 0}},
|
||||
return metadata.ArtistImagesOutput{
|
||||
Images: []metadata.ImageInfo{{URL: result.Results.Bindings[0].Img.Value, Size: 0}},
|
||||
}, nil
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogInfo, fmt.Sprintf("Image not found for: %s (%s)", input.Name, mbid))
|
||||
return ArtistImagesOutput{}, errors.New("image not found")
|
||||
return metadata.ArtistImagesOutput{}, errors.New("image not found")
|
||||
}
|
||||
|
||||
// The functions below are not implemented - they return errors to indicate
|
||||
// Navidrome should fall back to other agents.
|
||||
|
||||
func NdGetAlbumImages(input AlbumInput) (AlbumImagesOutput, error) {
|
||||
return AlbumImagesOutput{}, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func NdGetAlbumInfo(input AlbumInput) (AlbumInfoOutput, error) {
|
||||
return AlbumInfoOutput{}, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func NdGetArtistMbid(input ArtistMBIDInput) (ArtistMBIDOutput, error) {
|
||||
return ArtistMBIDOutput{}, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func NdGetArtistTopSongs(input TopSongsInput) (TopSongsOutput, error) {
|
||||
return TopSongsOutput{}, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func NdGetSimilarArtists(input SimilarArtistsInput) (SimilarArtistsOutput, error) {
|
||||
return SimilarArtistsOutput{}, errors.New("not implemented")
|
||||
}
|
||||
// Required main function - init() handles registration
|
||||
func main() {}
|
||||
|
||||
@ -1,376 +0,0 @@
|
||||
// THIS FILE WAS GENERATED BY `xtp-go-bindgen`. DO NOT EDIT.
|
||||
package main
|
||||
|
||||
import (
|
||||
pdk "github.com/extism/go-pdk"
|
||||
)
|
||||
|
||||
//export nd_get_album_images
|
||||
func _NdGetAlbumImages() int32 {
|
||||
var err error
|
||||
_ = err
|
||||
pdk.Log(pdk.LogDebug, "NdGetAlbumImages: getting JSON input")
|
||||
var input AlbumInput
|
||||
err = pdk.InputJSON(&input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdGetAlbumImages: calling implementation function")
|
||||
output, err := NdGetAlbumImages(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdGetAlbumImages: setting JSON output")
|
||||
err = pdk.OutputJSON(output)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdGetAlbumImages: returning")
|
||||
return 0
|
||||
}
|
||||
|
||||
//export nd_get_album_info
|
||||
func _NdGetAlbumInfo() int32 {
|
||||
var err error
|
||||
_ = err
|
||||
pdk.Log(pdk.LogDebug, "NdGetAlbumInfo: getting JSON input")
|
||||
var input AlbumInput
|
||||
err = pdk.InputJSON(&input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdGetAlbumInfo: calling implementation function")
|
||||
output, err := NdGetAlbumInfo(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdGetAlbumInfo: setting JSON output")
|
||||
err = pdk.OutputJSON(output)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdGetAlbumInfo: returning")
|
||||
return 0
|
||||
}
|
||||
|
||||
//export nd_get_artist_biography
|
||||
func _NdGetArtistBiography() int32 {
|
||||
var err error
|
||||
_ = err
|
||||
pdk.Log(pdk.LogDebug, "NdGetArtistBiography: getting JSON input")
|
||||
var input ArtistInput
|
||||
err = pdk.InputJSON(&input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdGetArtistBiography: calling implementation function")
|
||||
output, err := NdGetArtistBiography(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdGetArtistBiography: setting JSON output")
|
||||
err = pdk.OutputJSON(output)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdGetArtistBiography: returning")
|
||||
return 0
|
||||
}
|
||||
|
||||
//export nd_get_artist_images
|
||||
func _NdGetArtistImages() int32 {
|
||||
var err error
|
||||
_ = err
|
||||
pdk.Log(pdk.LogDebug, "NdGetArtistImages: getting JSON input")
|
||||
var input ArtistInput
|
||||
err = pdk.InputJSON(&input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdGetArtistImages: calling implementation function")
|
||||
output, err := NdGetArtistImages(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdGetArtistImages: setting JSON output")
|
||||
err = pdk.OutputJSON(output)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdGetArtistImages: returning")
|
||||
return 0
|
||||
}
|
||||
|
||||
//export nd_get_artist_mbid
|
||||
func _NdGetArtistMbid() int32 {
|
||||
var err error
|
||||
_ = err
|
||||
pdk.Log(pdk.LogDebug, "NdGetArtistMbid: getting JSON input")
|
||||
var input ArtistMBIDInput
|
||||
err = pdk.InputJSON(&input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdGetArtistMbid: calling implementation function")
|
||||
output, err := NdGetArtistMbid(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdGetArtistMbid: setting JSON output")
|
||||
err = pdk.OutputJSON(output)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdGetArtistMbid: returning")
|
||||
return 0
|
||||
}
|
||||
|
||||
//export nd_get_artist_top_songs
|
||||
func _NdGetArtistTopSongs() int32 {
|
||||
var err error
|
||||
_ = err
|
||||
pdk.Log(pdk.LogDebug, "NdGetArtistTopSongs: getting JSON input")
|
||||
var input TopSongsInput
|
||||
err = pdk.InputJSON(&input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdGetArtistTopSongs: calling implementation function")
|
||||
output, err := NdGetArtistTopSongs(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdGetArtistTopSongs: setting JSON output")
|
||||
err = pdk.OutputJSON(output)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdGetArtistTopSongs: returning")
|
||||
return 0
|
||||
}
|
||||
|
||||
//export nd_get_artist_url
|
||||
func _NdGetArtistUrl() int32 {
|
||||
var err error
|
||||
_ = err
|
||||
pdk.Log(pdk.LogDebug, "NdGetArtistUrl: getting JSON input")
|
||||
var input ArtistInput
|
||||
err = pdk.InputJSON(&input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdGetArtistUrl: calling implementation function")
|
||||
output, err := NdGetArtistUrl(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdGetArtistUrl: setting JSON output")
|
||||
err = pdk.OutputJSON(output)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdGetArtistUrl: returning")
|
||||
return 0
|
||||
}
|
||||
|
||||
//export nd_get_similar_artists
|
||||
func _NdGetSimilarArtists() int32 {
|
||||
var err error
|
||||
_ = err
|
||||
pdk.Log(pdk.LogDebug, "NdGetSimilarArtists: getting JSON input")
|
||||
var input SimilarArtistsInput
|
||||
err = pdk.InputJSON(&input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdGetSimilarArtists: calling implementation function")
|
||||
output, err := NdGetSimilarArtists(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdGetSimilarArtists: setting JSON output")
|
||||
err = pdk.OutputJSON(output)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
pdk.Log(pdk.LogDebug, "NdGetSimilarArtists: returning")
|
||||
return 0
|
||||
}
|
||||
|
||||
// Output for GetAlbumImages
|
||||
type AlbumImagesOutput struct {
|
||||
// List of album images
|
||||
Images []ImageInfo `json:"images"`
|
||||
}
|
||||
|
||||
// Output for GetAlbumInfo
|
||||
type AlbumInfoOutput struct {
|
||||
// The album description/notes
|
||||
Description string `json:"description"`
|
||||
// The MusicBrainz ID for the album
|
||||
Mbid string `json:"mbid"`
|
||||
// The album name
|
||||
Name string `json:"name"`
|
||||
// The external URL for the album
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
// Common input for album-related functions
|
||||
type AlbumInput struct {
|
||||
// The album artist name
|
||||
Artist string `json:"artist"`
|
||||
// The MusicBrainz ID for the album (if known)
|
||||
Mbid *string `json:"mbid,omitempty"`
|
||||
// The album name
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Output for GetArtistBiography
|
||||
type ArtistBiographyOutput struct {
|
||||
// The artist biography text
|
||||
Biography string `json:"biography"`
|
||||
}
|
||||
|
||||
// Output for GetArtistImages
|
||||
type ArtistImagesOutput struct {
|
||||
// List of artist images
|
||||
Images []ImageInfo `json:"images"`
|
||||
}
|
||||
|
||||
// Common input for artist-related functions
|
||||
type ArtistInput struct {
|
||||
// The internal Navidrome artist ID
|
||||
Id string `json:"id"`
|
||||
// The MusicBrainz ID for the artist (if known)
|
||||
Mbid *string `json:"mbid,omitempty"`
|
||||
// The artist name
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Input for GetArtistMBID
|
||||
type ArtistMBIDInput struct {
|
||||
// The internal Navidrome artist ID
|
||||
Id string `json:"id"`
|
||||
// The artist name
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Output for GetArtistMBID
|
||||
type ArtistMBIDOutput struct {
|
||||
// The MusicBrainz ID for the artist
|
||||
Mbid string `json:"mbid"`
|
||||
}
|
||||
|
||||
// Reference to an artist with name and optional MBID
|
||||
type ArtistRef struct {
|
||||
// The MusicBrainz ID for the artist
|
||||
Mbid *string `json:"mbid,omitempty"`
|
||||
// The artist name
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Output for GetArtistURL
|
||||
type ArtistURLOutput struct {
|
||||
// The external URL for the artist
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
// Image with URL and size
|
||||
type ImageInfo struct {
|
||||
// The size of the image in pixels (width or height)
|
||||
Size int32 `json:"size"`
|
||||
// The URL of the image
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
// Input for GetSimilarArtists
|
||||
type SimilarArtistsInput struct {
|
||||
// The internal Navidrome artist ID
|
||||
Id string `json:"id"`
|
||||
// Maximum number of similar artists to return
|
||||
Limit int32 `json:"limit"`
|
||||
// The MusicBrainz ID for the artist (if known)
|
||||
Mbid *string `json:"mbid,omitempty"`
|
||||
// The artist name
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Output for GetSimilarArtists
|
||||
type SimilarArtistsOutput struct {
|
||||
// List of similar artists
|
||||
Artists []ArtistRef `json:"artists"`
|
||||
}
|
||||
|
||||
// Reference to a song with name and optional MBID
|
||||
type SongRef struct {
|
||||
// The MusicBrainz ID for the song
|
||||
Mbid *string `json:"mbid,omitempty"`
|
||||
// The song name
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Input for GetArtistTopSongs
|
||||
type TopSongsInput struct {
|
||||
// Maximum number of top songs to return
|
||||
Count int32 `json:"count"`
|
||||
// The internal Navidrome artist ID
|
||||
Id string `json:"id"`
|
||||
// The MusicBrainz ID for the artist (if known)
|
||||
Mbid *string `json:"mbid,omitempty"`
|
||||
// The artist name
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Output for GetArtistTopSongs
|
||||
type TopSongsOutput struct {
|
||||
// List of top songs
|
||||
Songs []SongRef `json:"songs"`
|
||||
}
|
||||
86
plugins/pdk/go/lifecycle/lifecycle.go
Normal file
86
plugins/pdk/go/lifecycle/lifecycle.go
Normal file
@ -0,0 +1,86 @@
|
||||
// Code generated by ndpgen. DO NOT EDIT.
|
||||
//
|
||||
// This file contains export wrappers for the Lifecycle capability.
|
||||
// It is intended for use in Navidrome plugins built with TinyGo.
|
||||
//
|
||||
//go:build wasip1
|
||||
|
||||
package lifecycle
|
||||
|
||||
import (
|
||||
pdk "github.com/extism/go-pdk"
|
||||
)
|
||||
|
||||
// OnInitInput represents the OnInitInput data structure.
|
||||
// OnInitInput is the input provided to the init callback.
|
||||
// Currently empty, reserved for future use.
|
||||
type OnInitInput struct {
|
||||
}
|
||||
|
||||
// OnInitOutput represents the OnInitOutput data structure.
|
||||
// OnInitOutput is the output from the init callback.
|
||||
type OnInitOutput struct {
|
||||
// Error is the error message if initialization failed.
|
||||
// Empty or null indicates success.
|
||||
// The error is logged but does not prevent the plugin from being loaded.
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// Lifecycle is the marker interface for lifecycle plugins.
|
||||
// Implement one or more of the provider interfaces below.
|
||||
// Lifecycle provides plugin lifecycle hooks.
|
||||
// This capability allows plugins to perform initialization when loaded,
|
||||
// such as establishing connections, starting background processes, or
|
||||
// validating configuration.
|
||||
//
|
||||
// The OnInit function is called once when the plugin is loaded, and is NOT
|
||||
// called when the plugin is hot-reloaded. Plugins should not assume this
|
||||
// function will be called on every startup.
|
||||
type Lifecycle interface{}
|
||||
|
||||
// InitProvider provides the OnInit function.
|
||||
type InitProvider interface {
|
||||
OnInit(OnInitInput) (OnInitOutput, error)
|
||||
} // Internal implementation holders
|
||||
var (
|
||||
initImpl func(OnInitInput) (OnInitOutput, error)
|
||||
)
|
||||
|
||||
// Register registers a lifecycle implementation.
|
||||
// The implementation is checked for optional provider interfaces.
|
||||
func Register(impl Lifecycle) {
|
||||
if p, ok := impl.(InitProvider); ok {
|
||||
initImpl = p.OnInit
|
||||
}
|
||||
}
|
||||
|
||||
// NotImplementedCode is the standard return code for unimplemented functions.
|
||||
// The host recognizes this and skips the plugin gracefully.
|
||||
const NotImplementedCode int32 = -2
|
||||
|
||||
//export nd_on_init
|
||||
func _NdOnInit() int32 {
|
||||
if initImpl == nil {
|
||||
// Return standard code - host will skip this plugin gracefully
|
||||
return NotImplementedCode
|
||||
}
|
||||
|
||||
var input OnInitInput
|
||||
if err := pdk.InputJSON(&input); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
output, err := initImpl(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
if err := pdk.OutputJSON(output); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
48
plugins/pdk/go/lifecycle/lifecycle_stub.go
Normal file
48
plugins/pdk/go/lifecycle/lifecycle_stub.go
Normal file
@ -0,0 +1,48 @@
|
||||
// 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 lifecycle
|
||||
|
||||
// OnInitInput represents the OnInitInput data structure.
|
||||
// OnInitInput is the input provided to the init callback.
|
||||
// Currently empty, reserved for future use.
|
||||
type OnInitInput struct {
|
||||
}
|
||||
|
||||
// OnInitOutput represents the OnInitOutput data structure.
|
||||
// OnInitOutput is the output from the init callback.
|
||||
type OnInitOutput struct {
|
||||
// Error is the error message if initialization failed.
|
||||
// Empty or null indicates success.
|
||||
// The error is logged but does not prevent the plugin from being loaded.
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// Lifecycle is the marker interface for lifecycle plugins.
|
||||
// Implement one or more of the provider interfaces below.
|
||||
// Lifecycle provides plugin lifecycle hooks.
|
||||
// This capability allows plugins to perform initialization when loaded,
|
||||
// such as establishing connections, starting background processes, or
|
||||
// validating configuration.
|
||||
//
|
||||
// The OnInit function is called once when the plugin is loaded, and is NOT
|
||||
// called when the plugin is hot-reloaded. Plugins should not assume this
|
||||
// function will be called on every startup.
|
||||
type Lifecycle interface{}
|
||||
|
||||
// InitProvider provides the OnInit function.
|
||||
type InitProvider interface {
|
||||
OnInit(OnInitInput) (OnInitOutput, 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(_ Lifecycle) {}
|
||||
467
plugins/pdk/go/metadata/metadata.go
Normal file
467
plugins/pdk/go/metadata/metadata.go
Normal file
@ -0,0 +1,467 @@
|
||||
// Code generated by ndpgen. DO NOT EDIT.
|
||||
//
|
||||
// This file contains export wrappers for the MetadataAgent capability.
|
||||
// It is intended for use in Navidrome plugins built with TinyGo.
|
||||
//
|
||||
//go:build wasip1
|
||||
|
||||
package metadata
|
||||
|
||||
import (
|
||||
pdk "github.com/extism/go-pdk"
|
||||
)
|
||||
|
||||
// ArtistMBIDInput represents the ArtistMBIDInput data structure.
|
||||
// ArtistMBIDInput is the input for GetArtistMBID.
|
||||
type ArtistMBIDInput struct {
|
||||
// ID is the internal Navidrome artist ID.
|
||||
ID string `json:"id"`
|
||||
// Name is the artist name.
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// TopSongsInput represents the TopSongsInput data structure.
|
||||
// TopSongsInput is the input for GetArtistTopSongs.
|
||||
type TopSongsInput struct {
|
||||
// ID is the internal Navidrome artist ID.
|
||||
ID string `json:"id"`
|
||||
// Name is the artist name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the artist (if known).
|
||||
MBID *string `json:"mbid,omitempty"`
|
||||
// Count is the maximum number of top songs to return.
|
||||
Count int32 `json:"count"`
|
||||
}
|
||||
|
||||
// SimilarArtistsInput represents the SimilarArtistsInput data structure.
|
||||
// SimilarArtistsInput is the input for GetSimilarArtists.
|
||||
type SimilarArtistsInput struct {
|
||||
// ID is the internal Navidrome artist ID.
|
||||
ID string `json:"id"`
|
||||
// Name is the artist name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the artist (if known).
|
||||
MBID *string `json:"mbid,omitempty"`
|
||||
// Limit is the maximum number of similar artists to return.
|
||||
Limit int32 `json:"limit"`
|
||||
}
|
||||
|
||||
// TopSongsOutput represents the TopSongsOutput data structure.
|
||||
// TopSongsOutput is the output for GetArtistTopSongs.
|
||||
type TopSongsOutput struct {
|
||||
// Songs is the list of top songs.
|
||||
Songs []SongRef `json:"songs"`
|
||||
}
|
||||
|
||||
// AlbumInput represents the AlbumInput data structure.
|
||||
// AlbumInput is the common input for album-related functions.
|
||||
type AlbumInput struct {
|
||||
// Name is the album name.
|
||||
Name string `json:"name"`
|
||||
// Artist is the album artist name.
|
||||
Artist string `json:"artist"`
|
||||
// MBID is the MusicBrainz ID for the album (if known).
|
||||
MBID *string `json:"mbid,omitempty"`
|
||||
}
|
||||
|
||||
// AlbumInfoOutput represents the AlbumInfoOutput data structure.
|
||||
// AlbumInfoOutput is the output for GetAlbumInfo.
|
||||
type AlbumInfoOutput struct {
|
||||
// Name is the album name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the album.
|
||||
MBID string `json:"mbid"`
|
||||
// Description is the album description/notes.
|
||||
Description string `json:"description"`
|
||||
// URL is the external URL for the album.
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// AlbumImagesOutput represents the AlbumImagesOutput data structure.
|
||||
// AlbumImagesOutput is the output for GetAlbumImages.
|
||||
type AlbumImagesOutput struct {
|
||||
// Images is the list of album images.
|
||||
Images []ImageInfo `json:"images"`
|
||||
}
|
||||
|
||||
// ArtistRef represents the ArtistRef data structure.
|
||||
// ArtistRef is a reference to an artist with name and optional MBID.
|
||||
type ArtistRef struct {
|
||||
// Name is the artist name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the artist.
|
||||
MBID *string `json:"mbid,omitempty"`
|
||||
}
|
||||
|
||||
// ArtistURLOutput represents the ArtistURLOutput data structure.
|
||||
// ArtistURLOutput is the output for GetArtistURL.
|
||||
type ArtistURLOutput struct {
|
||||
// URL is the external URL for the artist.
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// ArtistBiographyOutput represents the ArtistBiographyOutput data structure.
|
||||
// ArtistBiographyOutput is the output for GetArtistBiography.
|
||||
type ArtistBiographyOutput struct {
|
||||
// Biography is the artist biography text.
|
||||
Biography string `json:"biography"`
|
||||
}
|
||||
|
||||
// SimilarArtistsOutput represents the SimilarArtistsOutput data structure.
|
||||
// SimilarArtistsOutput is the output for GetSimilarArtists.
|
||||
type SimilarArtistsOutput struct {
|
||||
// Artists is the list of similar artists.
|
||||
Artists []ArtistRef `json:"artists"`
|
||||
}
|
||||
|
||||
// ArtistImagesOutput represents the ArtistImagesOutput data structure.
|
||||
// ArtistImagesOutput is the output for GetArtistImages.
|
||||
type ArtistImagesOutput struct {
|
||||
// Images is the list of artist images.
|
||||
Images []ImageInfo `json:"images"`
|
||||
}
|
||||
|
||||
// ImageInfo represents the ImageInfo data structure.
|
||||
// ImageInfo represents an image with URL and size.
|
||||
type ImageInfo struct {
|
||||
// URL is the URL of the image.
|
||||
URL string `json:"url"`
|
||||
// Size is the size of the image in pixels (width or height).
|
||||
Size int32 `json:"size"`
|
||||
}
|
||||
|
||||
// ArtistMBIDOutput represents the ArtistMBIDOutput data structure.
|
||||
// ArtistMBIDOutput is the output for GetArtistMBID.
|
||||
type ArtistMBIDOutput struct {
|
||||
// MBID is the MusicBrainz ID for the artist.
|
||||
MBID string `json:"mbid"`
|
||||
}
|
||||
|
||||
// ArtistInput represents the ArtistInput data structure.
|
||||
// ArtistInput is the common input for artist-related functions.
|
||||
type ArtistInput struct {
|
||||
// ID is the internal Navidrome artist ID.
|
||||
ID string `json:"id"`
|
||||
// Name is the artist name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the artist (if known).
|
||||
MBID *string `json:"mbid,omitempty"`
|
||||
}
|
||||
|
||||
// SongRef represents the SongRef data structure.
|
||||
// SongRef is a reference to a song with name and optional MBID.
|
||||
type SongRef struct {
|
||||
// Name is the song name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the song.
|
||||
MBID *string `json:"mbid,omitempty"`
|
||||
}
|
||||
|
||||
// Metadata is the marker interface for metadata plugins.
|
||||
// Implement one or more of the provider interfaces below.
|
||||
// MetadataAgent provides artist and album metadata retrieval.
|
||||
// This capability allows plugins to provide external metadata for artists and albums,
|
||||
// such as biographies, images, similar artists, and top songs.
|
||||
//
|
||||
// Plugins implementing this capability can choose which methods to implement.
|
||||
// Each method is optional - plugins only need to provide the functionality they support.
|
||||
type Metadata interface{}
|
||||
|
||||
// ArtistMBIDProvider provides the GetArtistMBID function.
|
||||
type ArtistMBIDProvider interface {
|
||||
GetArtistMBID(ArtistMBIDInput) (ArtistMBIDOutput, error)
|
||||
}
|
||||
|
||||
// ArtistURLProvider provides the GetArtistURL function.
|
||||
type ArtistURLProvider interface {
|
||||
GetArtistURL(ArtistInput) (ArtistURLOutput, error)
|
||||
}
|
||||
|
||||
// ArtistBiographyProvider provides the GetArtistBiography function.
|
||||
type ArtistBiographyProvider interface {
|
||||
GetArtistBiography(ArtistInput) (ArtistBiographyOutput, error)
|
||||
}
|
||||
|
||||
// SimilarArtistsProvider provides the GetSimilarArtists function.
|
||||
type SimilarArtistsProvider interface {
|
||||
GetSimilarArtists(SimilarArtistsInput) (SimilarArtistsOutput, error)
|
||||
}
|
||||
|
||||
// ArtistImagesProvider provides the GetArtistImages function.
|
||||
type ArtistImagesProvider interface {
|
||||
GetArtistImages(ArtistInput) (ArtistImagesOutput, error)
|
||||
}
|
||||
|
||||
// ArtistTopSongsProvider provides the GetArtistTopSongs function.
|
||||
type ArtistTopSongsProvider interface {
|
||||
GetArtistTopSongs(TopSongsInput) (TopSongsOutput, error)
|
||||
}
|
||||
|
||||
// AlbumInfoProvider provides the GetAlbumInfo function.
|
||||
type AlbumInfoProvider interface {
|
||||
GetAlbumInfo(AlbumInput) (AlbumInfoOutput, error)
|
||||
}
|
||||
|
||||
// AlbumImagesProvider provides the GetAlbumImages function.
|
||||
type AlbumImagesProvider interface {
|
||||
GetAlbumImages(AlbumInput) (AlbumImagesOutput, error)
|
||||
} // Internal implementation holders
|
||||
var (
|
||||
artistMBIDImpl func(ArtistMBIDInput) (ArtistMBIDOutput, error)
|
||||
artistURLImpl func(ArtistInput) (ArtistURLOutput, error)
|
||||
artistBiographyImpl func(ArtistInput) (ArtistBiographyOutput, error)
|
||||
similarArtistsImpl func(SimilarArtistsInput) (SimilarArtistsOutput, error)
|
||||
artistImagesImpl func(ArtistInput) (ArtistImagesOutput, error)
|
||||
artistTopSongsImpl func(TopSongsInput) (TopSongsOutput, error)
|
||||
albumInfoImpl func(AlbumInput) (AlbumInfoOutput, error)
|
||||
albumImagesImpl func(AlbumInput) (AlbumImagesOutput, error)
|
||||
)
|
||||
|
||||
// Register registers a metadata implementation.
|
||||
// The implementation is checked for optional provider interfaces.
|
||||
func Register(impl Metadata) {
|
||||
if p, ok := impl.(ArtistMBIDProvider); ok {
|
||||
artistMBIDImpl = p.GetArtistMBID
|
||||
}
|
||||
if p, ok := impl.(ArtistURLProvider); ok {
|
||||
artistURLImpl = p.GetArtistURL
|
||||
}
|
||||
if p, ok := impl.(ArtistBiographyProvider); ok {
|
||||
artistBiographyImpl = p.GetArtistBiography
|
||||
}
|
||||
if p, ok := impl.(SimilarArtistsProvider); ok {
|
||||
similarArtistsImpl = p.GetSimilarArtists
|
||||
}
|
||||
if p, ok := impl.(ArtistImagesProvider); ok {
|
||||
artistImagesImpl = p.GetArtistImages
|
||||
}
|
||||
if p, ok := impl.(ArtistTopSongsProvider); ok {
|
||||
artistTopSongsImpl = p.GetArtistTopSongs
|
||||
}
|
||||
if p, ok := impl.(AlbumInfoProvider); ok {
|
||||
albumInfoImpl = p.GetAlbumInfo
|
||||
}
|
||||
if p, ok := impl.(AlbumImagesProvider); ok {
|
||||
albumImagesImpl = p.GetAlbumImages
|
||||
}
|
||||
}
|
||||
|
||||
// NotImplementedCode is the standard return code for unimplemented functions.
|
||||
// The host recognizes this and skips the plugin gracefully.
|
||||
const NotImplementedCode int32 = -2
|
||||
|
||||
//export nd_get_artist_mbid
|
||||
func _NdGetArtistMbid() int32 {
|
||||
if artistMBIDImpl == nil {
|
||||
// Return standard code - host will skip this plugin gracefully
|
||||
return NotImplementedCode
|
||||
}
|
||||
|
||||
var input ArtistMBIDInput
|
||||
if err := pdk.InputJSON(&input); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
output, err := artistMBIDImpl(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
if err := pdk.OutputJSON(output); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
//export nd_get_artist_url
|
||||
func _NdGetArtistUrl() int32 {
|
||||
if artistURLImpl == nil {
|
||||
// Return standard code - host will skip this plugin gracefully
|
||||
return NotImplementedCode
|
||||
}
|
||||
|
||||
var input ArtistInput
|
||||
if err := pdk.InputJSON(&input); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
output, err := artistURLImpl(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
if err := pdk.OutputJSON(output); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
//export nd_get_artist_biography
|
||||
func _NdGetArtistBiography() int32 {
|
||||
if artistBiographyImpl == nil {
|
||||
// Return standard code - host will skip this plugin gracefully
|
||||
return NotImplementedCode
|
||||
}
|
||||
|
||||
var input ArtistInput
|
||||
if err := pdk.InputJSON(&input); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
output, err := artistBiographyImpl(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
if err := pdk.OutputJSON(output); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
//export nd_get_similar_artists
|
||||
func _NdGetSimilarArtists() int32 {
|
||||
if similarArtistsImpl == nil {
|
||||
// Return standard code - host will skip this plugin gracefully
|
||||
return NotImplementedCode
|
||||
}
|
||||
|
||||
var input SimilarArtistsInput
|
||||
if err := pdk.InputJSON(&input); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
output, err := similarArtistsImpl(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
if err := pdk.OutputJSON(output); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
//export nd_get_artist_images
|
||||
func _NdGetArtistImages() int32 {
|
||||
if artistImagesImpl == nil {
|
||||
// Return standard code - host will skip this plugin gracefully
|
||||
return NotImplementedCode
|
||||
}
|
||||
|
||||
var input ArtistInput
|
||||
if err := pdk.InputJSON(&input); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
output, err := artistImagesImpl(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
if err := pdk.OutputJSON(output); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
//export nd_get_artist_top_songs
|
||||
func _NdGetArtistTopSongs() int32 {
|
||||
if artistTopSongsImpl == nil {
|
||||
// Return standard code - host will skip this plugin gracefully
|
||||
return NotImplementedCode
|
||||
}
|
||||
|
||||
var input TopSongsInput
|
||||
if err := pdk.InputJSON(&input); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
output, err := artistTopSongsImpl(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
if err := pdk.OutputJSON(output); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
//export nd_get_album_info
|
||||
func _NdGetAlbumInfo() int32 {
|
||||
if albumInfoImpl == nil {
|
||||
// Return standard code - host will skip this plugin gracefully
|
||||
return NotImplementedCode
|
||||
}
|
||||
|
||||
var input AlbumInput
|
||||
if err := pdk.InputJSON(&input); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
output, err := albumInfoImpl(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
if err := pdk.OutputJSON(output); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
//export nd_get_album_images
|
||||
func _NdGetAlbumImages() int32 {
|
||||
if albumImagesImpl == nil {
|
||||
// Return standard code - host will skip this plugin gracefully
|
||||
return NotImplementedCode
|
||||
}
|
||||
|
||||
var input AlbumInput
|
||||
if err := pdk.InputJSON(&input); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
output, err := albumImagesImpl(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
if err := pdk.OutputJSON(output); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
212
plugins/pdk/go/metadata/metadata_stub.go
Normal file
212
plugins/pdk/go/metadata/metadata_stub.go
Normal file
@ -0,0 +1,212 @@
|
||||
// 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 metadata
|
||||
|
||||
// ArtistMBIDInput represents the ArtistMBIDInput data structure.
|
||||
// ArtistMBIDInput is the input for GetArtistMBID.
|
||||
type ArtistMBIDInput struct {
|
||||
// ID is the internal Navidrome artist ID.
|
||||
ID string `json:"id"`
|
||||
// Name is the artist name.
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// TopSongsInput represents the TopSongsInput data structure.
|
||||
// TopSongsInput is the input for GetArtistTopSongs.
|
||||
type TopSongsInput struct {
|
||||
// ID is the internal Navidrome artist ID.
|
||||
ID string `json:"id"`
|
||||
// Name is the artist name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the artist (if known).
|
||||
MBID *string `json:"mbid,omitempty"`
|
||||
// Count is the maximum number of top songs to return.
|
||||
Count int32 `json:"count"`
|
||||
}
|
||||
|
||||
// SimilarArtistsInput represents the SimilarArtistsInput data structure.
|
||||
// SimilarArtistsInput is the input for GetSimilarArtists.
|
||||
type SimilarArtistsInput struct {
|
||||
// ID is the internal Navidrome artist ID.
|
||||
ID string `json:"id"`
|
||||
// Name is the artist name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the artist (if known).
|
||||
MBID *string `json:"mbid,omitempty"`
|
||||
// Limit is the maximum number of similar artists to return.
|
||||
Limit int32 `json:"limit"`
|
||||
}
|
||||
|
||||
// TopSongsOutput represents the TopSongsOutput data structure.
|
||||
// TopSongsOutput is the output for GetArtistTopSongs.
|
||||
type TopSongsOutput struct {
|
||||
// Songs is the list of top songs.
|
||||
Songs []SongRef `json:"songs"`
|
||||
}
|
||||
|
||||
// AlbumInput represents the AlbumInput data structure.
|
||||
// AlbumInput is the common input for album-related functions.
|
||||
type AlbumInput struct {
|
||||
// Name is the album name.
|
||||
Name string `json:"name"`
|
||||
// Artist is the album artist name.
|
||||
Artist string `json:"artist"`
|
||||
// MBID is the MusicBrainz ID for the album (if known).
|
||||
MBID *string `json:"mbid,omitempty"`
|
||||
}
|
||||
|
||||
// AlbumInfoOutput represents the AlbumInfoOutput data structure.
|
||||
// AlbumInfoOutput is the output for GetAlbumInfo.
|
||||
type AlbumInfoOutput struct {
|
||||
// Name is the album name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the album.
|
||||
MBID string `json:"mbid"`
|
||||
// Description is the album description/notes.
|
||||
Description string `json:"description"`
|
||||
// URL is the external URL for the album.
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// AlbumImagesOutput represents the AlbumImagesOutput data structure.
|
||||
// AlbumImagesOutput is the output for GetAlbumImages.
|
||||
type AlbumImagesOutput struct {
|
||||
// Images is the list of album images.
|
||||
Images []ImageInfo `json:"images"`
|
||||
}
|
||||
|
||||
// ArtistRef represents the ArtistRef data structure.
|
||||
// ArtistRef is a reference to an artist with name and optional MBID.
|
||||
type ArtistRef struct {
|
||||
// Name is the artist name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the artist.
|
||||
MBID *string `json:"mbid,omitempty"`
|
||||
}
|
||||
|
||||
// ArtistURLOutput represents the ArtistURLOutput data structure.
|
||||
// ArtistURLOutput is the output for GetArtistURL.
|
||||
type ArtistURLOutput struct {
|
||||
// URL is the external URL for the artist.
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// ArtistBiographyOutput represents the ArtistBiographyOutput data structure.
|
||||
// ArtistBiographyOutput is the output for GetArtistBiography.
|
||||
type ArtistBiographyOutput struct {
|
||||
// Biography is the artist biography text.
|
||||
Biography string `json:"biography"`
|
||||
}
|
||||
|
||||
// SimilarArtistsOutput represents the SimilarArtistsOutput data structure.
|
||||
// SimilarArtistsOutput is the output for GetSimilarArtists.
|
||||
type SimilarArtistsOutput struct {
|
||||
// Artists is the list of similar artists.
|
||||
Artists []ArtistRef `json:"artists"`
|
||||
}
|
||||
|
||||
// ArtistImagesOutput represents the ArtistImagesOutput data structure.
|
||||
// ArtistImagesOutput is the output for GetArtistImages.
|
||||
type ArtistImagesOutput struct {
|
||||
// Images is the list of artist images.
|
||||
Images []ImageInfo `json:"images"`
|
||||
}
|
||||
|
||||
// ImageInfo represents the ImageInfo data structure.
|
||||
// ImageInfo represents an image with URL and size.
|
||||
type ImageInfo struct {
|
||||
// URL is the URL of the image.
|
||||
URL string `json:"url"`
|
||||
// Size is the size of the image in pixels (width or height).
|
||||
Size int32 `json:"size"`
|
||||
}
|
||||
|
||||
// ArtistMBIDOutput represents the ArtistMBIDOutput data structure.
|
||||
// ArtistMBIDOutput is the output for GetArtistMBID.
|
||||
type ArtistMBIDOutput struct {
|
||||
// MBID is the MusicBrainz ID for the artist.
|
||||
MBID string `json:"mbid"`
|
||||
}
|
||||
|
||||
// ArtistInput represents the ArtistInput data structure.
|
||||
// ArtistInput is the common input for artist-related functions.
|
||||
type ArtistInput struct {
|
||||
// ID is the internal Navidrome artist ID.
|
||||
ID string `json:"id"`
|
||||
// Name is the artist name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the artist (if known).
|
||||
MBID *string `json:"mbid,omitempty"`
|
||||
}
|
||||
|
||||
// SongRef represents the SongRef data structure.
|
||||
// SongRef is a reference to a song with name and optional MBID.
|
||||
type SongRef struct {
|
||||
// Name is the song name.
|
||||
Name string `json:"name"`
|
||||
// MBID is the MusicBrainz ID for the song.
|
||||
MBID *string `json:"mbid,omitempty"`
|
||||
}
|
||||
|
||||
// Metadata is the marker interface for metadata plugins.
|
||||
// Implement one or more of the provider interfaces below.
|
||||
// MetadataAgent provides artist and album metadata retrieval.
|
||||
// This capability allows plugins to provide external metadata for artists and albums,
|
||||
// such as biographies, images, similar artists, and top songs.
|
||||
//
|
||||
// Plugins implementing this capability can choose which methods to implement.
|
||||
// Each method is optional - plugins only need to provide the functionality they support.
|
||||
type Metadata interface{}
|
||||
|
||||
// ArtistMBIDProvider provides the GetArtistMBID function.
|
||||
type ArtistMBIDProvider interface {
|
||||
GetArtistMBID(ArtistMBIDInput) (ArtistMBIDOutput, error)
|
||||
}
|
||||
|
||||
// ArtistURLProvider provides the GetArtistURL function.
|
||||
type ArtistURLProvider interface {
|
||||
GetArtistURL(ArtistInput) (ArtistURLOutput, error)
|
||||
}
|
||||
|
||||
// ArtistBiographyProvider provides the GetArtistBiography function.
|
||||
type ArtistBiographyProvider interface {
|
||||
GetArtistBiography(ArtistInput) (ArtistBiographyOutput, error)
|
||||
}
|
||||
|
||||
// SimilarArtistsProvider provides the GetSimilarArtists function.
|
||||
type SimilarArtistsProvider interface {
|
||||
GetSimilarArtists(SimilarArtistsInput) (SimilarArtistsOutput, error)
|
||||
}
|
||||
|
||||
// ArtistImagesProvider provides the GetArtistImages function.
|
||||
type ArtistImagesProvider interface {
|
||||
GetArtistImages(ArtistInput) (ArtistImagesOutput, error)
|
||||
}
|
||||
|
||||
// ArtistTopSongsProvider provides the GetArtistTopSongs function.
|
||||
type ArtistTopSongsProvider interface {
|
||||
GetArtistTopSongs(TopSongsInput) (TopSongsOutput, error)
|
||||
}
|
||||
|
||||
// AlbumInfoProvider provides the GetAlbumInfo function.
|
||||
type AlbumInfoProvider interface {
|
||||
GetAlbumInfo(AlbumInput) (AlbumInfoOutput, error)
|
||||
}
|
||||
|
||||
// AlbumImagesProvider provides the GetAlbumImages function.
|
||||
type AlbumImagesProvider interface {
|
||||
GetAlbumImages(AlbumInput) (AlbumImagesOutput, 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(_ Metadata) {}
|
||||
90
plugins/pdk/go/scheduler/scheduler.go
Normal file
90
plugins/pdk/go/scheduler/scheduler.go
Normal file
@ -0,0 +1,90 @@
|
||||
// Code generated by ndpgen. DO NOT EDIT.
|
||||
//
|
||||
// This file contains export wrappers for the SchedulerCallback capability.
|
||||
// It is intended for use in Navidrome plugins built with TinyGo.
|
||||
//
|
||||
//go:build wasip1
|
||||
|
||||
package scheduler
|
||||
|
||||
import (
|
||||
pdk "github.com/extism/go-pdk"
|
||||
)
|
||||
|
||||
// SchedulerCallbackOutput represents the SchedulerCallbackOutput data structure.
|
||||
// SchedulerCallbackOutput is the output from the scheduler callback.
|
||||
type SchedulerCallbackOutput struct {
|
||||
// Error is the error message if the callback failed to process the scheduled task.
|
||||
// Empty or null indicates success. The error is logged but does not
|
||||
// affect the scheduling system.
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// SchedulerCallbackInput represents the SchedulerCallbackInput data structure.
|
||||
// SchedulerCallbackInput is the input provided when a scheduled task fires.
|
||||
type SchedulerCallbackInput struct {
|
||||
// ScheduleID is the unique identifier for this scheduled task.
|
||||
// This is either the ID provided when scheduling, or an auto-generated UUID if none was specified.
|
||||
ScheduleID string `json:"scheduleId"`
|
||||
// Payload is the payload data that was provided when the task was scheduled.
|
||||
// Can be used to pass context or parameters to the callback handler.
|
||||
Payload string `json:"payload"`
|
||||
// IsRecurring is true if this is a recurring schedule (created via ScheduleRecurring),
|
||||
// false if it's a one-time schedule (created via ScheduleOneTime).
|
||||
IsRecurring bool `json:"isRecurring"`
|
||||
}
|
||||
|
||||
// Scheduler is the marker interface for scheduler plugins.
|
||||
// Implement one or more of the provider interfaces below.
|
||||
// SchedulerCallback provides scheduled task handling.
|
||||
// This capability allows plugins to receive callbacks when their scheduled tasks execute.
|
||||
// Plugins that use the scheduler host service must implement this capability
|
||||
// to handle task execution.
|
||||
type Scheduler interface{}
|
||||
|
||||
// SchedulerCallbackProvider provides the OnSchedulerCallback function.
|
||||
type SchedulerCallbackProvider interface {
|
||||
OnSchedulerCallback(SchedulerCallbackInput) (SchedulerCallbackOutput, error)
|
||||
} // Internal implementation holders
|
||||
var (
|
||||
schedulerCallbackImpl func(SchedulerCallbackInput) (SchedulerCallbackOutput, error)
|
||||
)
|
||||
|
||||
// Register registers a scheduler implementation.
|
||||
// The implementation is checked for optional provider interfaces.
|
||||
func Register(impl Scheduler) {
|
||||
if p, ok := impl.(SchedulerCallbackProvider); ok {
|
||||
schedulerCallbackImpl = p.OnSchedulerCallback
|
||||
}
|
||||
}
|
||||
|
||||
// NotImplementedCode is the standard return code for unimplemented functions.
|
||||
// The host recognizes this and skips the plugin gracefully.
|
||||
const NotImplementedCode int32 = -2
|
||||
|
||||
//export nd_scheduler_callback
|
||||
func _NdSchedulerCallback() int32 {
|
||||
if schedulerCallbackImpl == nil {
|
||||
// Return standard code - host will skip this plugin gracefully
|
||||
return NotImplementedCode
|
||||
}
|
||||
|
||||
var input SchedulerCallbackInput
|
||||
if err := pdk.InputJSON(&input); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
output, err := schedulerCallbackImpl(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
if err := pdk.OutputJSON(output); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
52
plugins/pdk/go/scheduler/scheduler_stub.go
Normal file
52
plugins/pdk/go/scheduler/scheduler_stub.go
Normal file
@ -0,0 +1,52 @@
|
||||
// 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 scheduler
|
||||
|
||||
// SchedulerCallbackOutput represents the SchedulerCallbackOutput data structure.
|
||||
// SchedulerCallbackOutput is the output from the scheduler callback.
|
||||
type SchedulerCallbackOutput struct {
|
||||
// Error is the error message if the callback failed to process the scheduled task.
|
||||
// Empty or null indicates success. The error is logged but does not
|
||||
// affect the scheduling system.
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// SchedulerCallbackInput represents the SchedulerCallbackInput data structure.
|
||||
// SchedulerCallbackInput is the input provided when a scheduled task fires.
|
||||
type SchedulerCallbackInput struct {
|
||||
// ScheduleID is the unique identifier for this scheduled task.
|
||||
// This is either the ID provided when scheduling, or an auto-generated UUID if none was specified.
|
||||
ScheduleID string `json:"scheduleId"`
|
||||
// Payload is the payload data that was provided when the task was scheduled.
|
||||
// Can be used to pass context or parameters to the callback handler.
|
||||
Payload string `json:"payload"`
|
||||
// IsRecurring is true if this is a recurring schedule (created via ScheduleRecurring),
|
||||
// false if it's a one-time schedule (created via ScheduleOneTime).
|
||||
IsRecurring bool `json:"isRecurring"`
|
||||
}
|
||||
|
||||
// Scheduler is the marker interface for scheduler plugins.
|
||||
// Implement one or more of the provider interfaces below.
|
||||
// SchedulerCallback provides scheduled task handling.
|
||||
// This capability allows plugins to receive callbacks when their scheduled tasks execute.
|
||||
// Plugins that use the scheduler host service must implement this capability
|
||||
// to handle task execution.
|
||||
type Scheduler interface{}
|
||||
|
||||
// SchedulerCallbackProvider provides the OnSchedulerCallback function.
|
||||
type SchedulerCallbackProvider interface {
|
||||
OnSchedulerCallback(SchedulerCallbackInput) (SchedulerCallbackOutput, 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(_ Scheduler) {}
|
||||
224
plugins/pdk/go/scrobbler/scrobbler.go
Normal file
224
plugins/pdk/go/scrobbler/scrobbler.go
Normal file
@ -0,0 +1,224 @@
|
||||
// Code generated by ndpgen. DO NOT EDIT.
|
||||
//
|
||||
// This file contains export wrappers for the Scrobbler capability.
|
||||
// It is intended for use in Navidrome plugins built with TinyGo.
|
||||
//
|
||||
//go:build wasip1
|
||||
|
||||
package scrobbler
|
||||
|
||||
import (
|
||||
pdk "github.com/extism/go-pdk"
|
||||
)
|
||||
|
||||
// ScrobblerErrorType indicates how Navidrome should handle scrobbler errors.
|
||||
type ScrobblerErrorType string
|
||||
|
||||
const (
|
||||
// ScrobblerErrorNone indicates no error occurred.
|
||||
ScrobblerErrorNone = "none"
|
||||
// ScrobblerErrorNotAuthorized indicates the user is not authorized.
|
||||
ScrobblerErrorNotAuthorized = "not_authorized"
|
||||
// ScrobblerErrorRetryLater indicates the operation should be retried later.
|
||||
ScrobblerErrorRetryLater = "retry_later"
|
||||
// ScrobblerErrorUnrecoverable indicates an unrecoverable error.
|
||||
ScrobblerErrorUnrecoverable = "unrecoverable"
|
||||
)
|
||||
|
||||
// TrackInfo represents the TrackInfo data structure.
|
||||
// TrackInfo contains track metadata for scrobbling.
|
||||
type TrackInfo struct {
|
||||
// ID is the internal Navidrome track ID.
|
||||
ID string `json:"id"`
|
||||
// Title is the track title.
|
||||
Title string `json:"title"`
|
||||
// Album is the album name.
|
||||
Album string `json:"album"`
|
||||
// Artist is the track artist.
|
||||
Artist string `json:"artist"`
|
||||
// AlbumArtist is the album artist.
|
||||
AlbumArtist string `json:"albumArtist"`
|
||||
// Duration is the track duration in seconds.
|
||||
Duration float64 `json:"duration"`
|
||||
// TrackNumber is the track number on the album.
|
||||
TrackNumber int32 `json:"trackNumber"`
|
||||
// DiscNumber is the disc number.
|
||||
DiscNumber int32 `json:"discNumber"`
|
||||
// MBZRecordingID is the MusicBrainz recording ID.
|
||||
MBZRecordingID *string `json:"mbzRecordingId,omitempty"`
|
||||
// MBZAlbumID is the MusicBrainz album/release ID.
|
||||
MBZAlbumID *string `json:"mbzAlbumId,omitempty"`
|
||||
// MBZArtistID is the MusicBrainz artist ID.
|
||||
MBZArtistID *string `json:"mbzArtistId,omitempty"`
|
||||
// MBZReleaseGroupID is the MusicBrainz release group ID.
|
||||
MBZReleaseGroupID *string `json:"mbzReleaseGroupId,omitempty"`
|
||||
// MBZAlbumArtistID is the MusicBrainz album artist ID.
|
||||
MBZAlbumArtistID *string `json:"mbzAlbumArtistId,omitempty"`
|
||||
// MBZReleaseTrackID is the MusicBrainz release track ID.
|
||||
MBZReleaseTrackID *string `json:"mbzReleaseTrackId,omitempty"`
|
||||
}
|
||||
|
||||
// AuthInput represents the AuthInput data structure.
|
||||
// AuthInput is the input for authorization check.
|
||||
type AuthInput struct {
|
||||
// UserID is the internal Navidrome user ID.
|
||||
UserID string `json:"userId"`
|
||||
// Username is the username of the user.
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// AuthOutput represents the AuthOutput data structure.
|
||||
// AuthOutput is the output for authorization check.
|
||||
type AuthOutput struct {
|
||||
// Authorized indicates whether the user is authorized to scrobble.
|
||||
Authorized bool `json:"authorized"`
|
||||
}
|
||||
|
||||
// NowPlayingInput represents the NowPlayingInput data structure.
|
||||
// NowPlayingInput is the input for now playing notification.
|
||||
type NowPlayingInput struct {
|
||||
// UserID is the internal Navidrome user ID.
|
||||
UserID string `json:"userId"`
|
||||
// Username is the username of the user.
|
||||
Username string `json:"username"`
|
||||
// Track is the track currently playing.
|
||||
Track TrackInfo `json:"track"`
|
||||
// Position is the current playback position in seconds.
|
||||
Position int32 `json:"position"`
|
||||
}
|
||||
|
||||
// ScrobblerOutput represents the ScrobblerOutput data structure.
|
||||
// ScrobblerOutput is the output for scrobbler operations.
|
||||
type ScrobblerOutput struct {
|
||||
// Error is the error message if the operation failed.
|
||||
Error *string `json:"error,omitempty"`
|
||||
// ErrorType indicates how Navidrome should handle the error.
|
||||
ErrorType *ScrobblerErrorType `json:"errorType,omitempty"`
|
||||
}
|
||||
|
||||
// ScrobbleInput represents the ScrobbleInput data structure.
|
||||
// ScrobbleInput is the input for submitting a scrobble.
|
||||
type ScrobbleInput struct {
|
||||
// UserID is the internal Navidrome user ID.
|
||||
UserID string `json:"userId"`
|
||||
// Username is the username of the user.
|
||||
Username string `json:"username"`
|
||||
// Track is the track that was played.
|
||||
Track TrackInfo `json:"track"`
|
||||
// Timestamp is the Unix timestamp when the track started playing.
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
// Scrobbler requires all methods to be implemented.
|
||||
// Scrobbler provides scrobbling functionality to external services.
|
||||
// This capability allows plugins to submit listening history to services like Last.fm,
|
||||
// ListenBrainz, or custom scrobbling backends.
|
||||
//
|
||||
// All methods are required - plugins implementing this capability must provide
|
||||
// all three functions: IsAuthorized, NowPlaying, and Scrobble.
|
||||
type Scrobbler interface {
|
||||
// IsAuthorized - IsAuthorized checks if a user is authorized to scrobble to this service.
|
||||
IsAuthorized(AuthInput) (AuthOutput, error)
|
||||
// NowPlaying - NowPlaying sends a now playing notification to the scrobbling service.
|
||||
NowPlaying(NowPlayingInput) (ScrobblerOutput, error)
|
||||
// Scrobble - Scrobble submits a completed scrobble to the scrobbling service.
|
||||
Scrobble(ScrobbleInput) (ScrobblerOutput, error)
|
||||
} // Internal implementation holders
|
||||
var (
|
||||
isAuthorizedImpl func(AuthInput) (AuthOutput, error)
|
||||
nowPlayingImpl func(NowPlayingInput) (ScrobblerOutput, error)
|
||||
scrobbleImpl func(ScrobbleInput) (ScrobblerOutput, error)
|
||||
)
|
||||
|
||||
// Register registers a scrobbler implementation.
|
||||
// All methods are required.
|
||||
func Register(impl Scrobbler) {
|
||||
isAuthorizedImpl = impl.IsAuthorized
|
||||
nowPlayingImpl = impl.NowPlaying
|
||||
scrobbleImpl = impl.Scrobble
|
||||
}
|
||||
|
||||
// NotImplementedCode is the standard return code for unimplemented functions.
|
||||
// The host recognizes this and skips the plugin gracefully.
|
||||
const NotImplementedCode int32 = -2
|
||||
|
||||
//export nd_scrobbler_is_authorized
|
||||
func _NdScrobblerIsAuthorized() int32 {
|
||||
if isAuthorizedImpl == nil {
|
||||
// Return standard code - host will skip this plugin gracefully
|
||||
return NotImplementedCode
|
||||
}
|
||||
|
||||
var input AuthInput
|
||||
if err := pdk.InputJSON(&input); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
output, err := isAuthorizedImpl(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
if err := pdk.OutputJSON(output); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
//export nd_scrobbler_now_playing
|
||||
func _NdScrobblerNowPlaying() int32 {
|
||||
if nowPlayingImpl == nil {
|
||||
// Return standard code - host will skip this plugin gracefully
|
||||
return NotImplementedCode
|
||||
}
|
||||
|
||||
var input NowPlayingInput
|
||||
if err := pdk.InputJSON(&input); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
output, err := nowPlayingImpl(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
if err := pdk.OutputJSON(output); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
//export nd_scrobbler_scrobble
|
||||
func _NdScrobblerScrobble() int32 {
|
||||
if scrobbleImpl == nil {
|
||||
// Return standard code - host will skip this plugin gracefully
|
||||
return NotImplementedCode
|
||||
}
|
||||
|
||||
var input ScrobbleInput
|
||||
if err := pdk.InputJSON(&input); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
output, err := scrobbleImpl(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
if err := pdk.OutputJSON(output); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
130
plugins/pdk/go/scrobbler/scrobbler_stub.go
Normal file
130
plugins/pdk/go/scrobbler/scrobbler_stub.go
Normal file
@ -0,0 +1,130 @@
|
||||
// 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 scrobbler
|
||||
|
||||
// ScrobblerErrorType indicates how Navidrome should handle scrobbler errors.
|
||||
type ScrobblerErrorType string
|
||||
|
||||
const (
|
||||
// ScrobblerErrorNone indicates no error occurred.
|
||||
ScrobblerErrorNone = "none"
|
||||
// ScrobblerErrorNotAuthorized indicates the user is not authorized.
|
||||
ScrobblerErrorNotAuthorized = "not_authorized"
|
||||
// ScrobblerErrorRetryLater indicates the operation should be retried later.
|
||||
ScrobblerErrorRetryLater = "retry_later"
|
||||
// ScrobblerErrorUnrecoverable indicates an unrecoverable error.
|
||||
ScrobblerErrorUnrecoverable = "unrecoverable"
|
||||
)
|
||||
|
||||
// TrackInfo represents the TrackInfo data structure.
|
||||
// TrackInfo contains track metadata for scrobbling.
|
||||
type TrackInfo struct {
|
||||
// ID is the internal Navidrome track ID.
|
||||
ID string `json:"id"`
|
||||
// Title is the track title.
|
||||
Title string `json:"title"`
|
||||
// Album is the album name.
|
||||
Album string `json:"album"`
|
||||
// Artist is the track artist.
|
||||
Artist string `json:"artist"`
|
||||
// AlbumArtist is the album artist.
|
||||
AlbumArtist string `json:"albumArtist"`
|
||||
// Duration is the track duration in seconds.
|
||||
Duration float64 `json:"duration"`
|
||||
// TrackNumber is the track number on the album.
|
||||
TrackNumber int32 `json:"trackNumber"`
|
||||
// DiscNumber is the disc number.
|
||||
DiscNumber int32 `json:"discNumber"`
|
||||
// MBZRecordingID is the MusicBrainz recording ID.
|
||||
MBZRecordingID *string `json:"mbzRecordingId,omitempty"`
|
||||
// MBZAlbumID is the MusicBrainz album/release ID.
|
||||
MBZAlbumID *string `json:"mbzAlbumId,omitempty"`
|
||||
// MBZArtistID is the MusicBrainz artist ID.
|
||||
MBZArtistID *string `json:"mbzArtistId,omitempty"`
|
||||
// MBZReleaseGroupID is the MusicBrainz release group ID.
|
||||
MBZReleaseGroupID *string `json:"mbzReleaseGroupId,omitempty"`
|
||||
// MBZAlbumArtistID is the MusicBrainz album artist ID.
|
||||
MBZAlbumArtistID *string `json:"mbzAlbumArtistId,omitempty"`
|
||||
// MBZReleaseTrackID is the MusicBrainz release track ID.
|
||||
MBZReleaseTrackID *string `json:"mbzReleaseTrackId,omitempty"`
|
||||
}
|
||||
|
||||
// AuthInput represents the AuthInput data structure.
|
||||
// AuthInput is the input for authorization check.
|
||||
type AuthInput struct {
|
||||
// UserID is the internal Navidrome user ID.
|
||||
UserID string `json:"userId"`
|
||||
// Username is the username of the user.
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// AuthOutput represents the AuthOutput data structure.
|
||||
// AuthOutput is the output for authorization check.
|
||||
type AuthOutput struct {
|
||||
// Authorized indicates whether the user is authorized to scrobble.
|
||||
Authorized bool `json:"authorized"`
|
||||
}
|
||||
|
||||
// NowPlayingInput represents the NowPlayingInput data structure.
|
||||
// NowPlayingInput is the input for now playing notification.
|
||||
type NowPlayingInput struct {
|
||||
// UserID is the internal Navidrome user ID.
|
||||
UserID string `json:"userId"`
|
||||
// Username is the username of the user.
|
||||
Username string `json:"username"`
|
||||
// Track is the track currently playing.
|
||||
Track TrackInfo `json:"track"`
|
||||
// Position is the current playback position in seconds.
|
||||
Position int32 `json:"position"`
|
||||
}
|
||||
|
||||
// ScrobblerOutput represents the ScrobblerOutput data structure.
|
||||
// ScrobblerOutput is the output for scrobbler operations.
|
||||
type ScrobblerOutput struct {
|
||||
// Error is the error message if the operation failed.
|
||||
Error *string `json:"error,omitempty"`
|
||||
// ErrorType indicates how Navidrome should handle the error.
|
||||
ErrorType *ScrobblerErrorType `json:"errorType,omitempty"`
|
||||
}
|
||||
|
||||
// ScrobbleInput represents the ScrobbleInput data structure.
|
||||
// ScrobbleInput is the input for submitting a scrobble.
|
||||
type ScrobbleInput struct {
|
||||
// UserID is the internal Navidrome user ID.
|
||||
UserID string `json:"userId"`
|
||||
// Username is the username of the user.
|
||||
Username string `json:"username"`
|
||||
// Track is the track that was played.
|
||||
Track TrackInfo `json:"track"`
|
||||
// Timestamp is the Unix timestamp when the track started playing.
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
// Scrobbler requires all methods to be implemented.
|
||||
// Scrobbler provides scrobbling functionality to external services.
|
||||
// This capability allows plugins to submit listening history to services like Last.fm,
|
||||
// ListenBrainz, or custom scrobbling backends.
|
||||
//
|
||||
// All methods are required - plugins implementing this capability must provide
|
||||
// all three functions: IsAuthorized, NowPlaying, and Scrobble.
|
||||
type Scrobbler interface {
|
||||
// IsAuthorized - IsAuthorized checks if a user is authorized to scrobble to this service.
|
||||
IsAuthorized(AuthInput) (AuthOutput, error)
|
||||
// NowPlaying - NowPlaying sends a now playing notification to the scrobbling service.
|
||||
NowPlaying(NowPlayingInput) (ScrobblerOutput, error)
|
||||
// Scrobble - Scrobble submits a completed scrobble to the scrobbling service.
|
||||
Scrobble(ScrobbleInput) (ScrobblerOutput, 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(_ Scrobbler) {}
|
||||
247
plugins/pdk/go/websocket/websocket.go
Normal file
247
plugins/pdk/go/websocket/websocket.go
Normal file
@ -0,0 +1,247 @@
|
||||
// Code generated by ndpgen. DO NOT EDIT.
|
||||
//
|
||||
// This file contains export wrappers for the WebSocketCallback capability.
|
||||
// It is intended for use in Navidrome plugins built with TinyGo.
|
||||
//
|
||||
//go:build wasip1
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
pdk "github.com/extism/go-pdk"
|
||||
)
|
||||
|
||||
// OnErrorInput represents the OnErrorInput data structure.
|
||||
// OnErrorInput is the input provided when an error occurs on a WebSocket connection.
|
||||
type OnErrorInput struct {
|
||||
// ConnectionID is the unique identifier for the WebSocket connection where the error occurred.
|
||||
ConnectionID string `json:"connectionId"`
|
||||
// Error is the error message describing what went wrong.
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// OnErrorOutput represents the OnErrorOutput data structure.
|
||||
// OnErrorOutput is the output from the error handler.
|
||||
type OnErrorOutput struct {
|
||||
// Error is the error message if the callback failed.
|
||||
// Empty or null indicates success.
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// OnCloseInput represents the OnCloseInput data structure.
|
||||
// OnCloseInput is the input provided when a WebSocket connection is closed.
|
||||
type OnCloseInput struct {
|
||||
// ConnectionID is the unique identifier for the WebSocket connection that was closed.
|
||||
ConnectionID string `json:"connectionId"`
|
||||
// Code is the WebSocket close status code (e.g., 1000 for normal closure,
|
||||
// 1001 for going away, 1006 for abnormal closure).
|
||||
Code int32 `json:"code"`
|
||||
// Reason is the human-readable reason for the connection closure, if provided.
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// OnCloseOutput represents the OnCloseOutput data structure.
|
||||
// OnCloseOutput is the output from the close handler.
|
||||
type OnCloseOutput struct {
|
||||
// Error is the error message if the callback failed.
|
||||
// Empty or null indicates success.
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// OnTextMessageInput represents the OnTextMessageInput data structure.
|
||||
// OnTextMessageInput is the input provided when a text message is received.
|
||||
type OnTextMessageInput struct {
|
||||
// ConnectionID is the unique identifier for the WebSocket connection that received the message.
|
||||
ConnectionID string `json:"connectionId"`
|
||||
// Message is the text message content received from the WebSocket.
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// OnTextMessageOutput represents the OnTextMessageOutput data structure.
|
||||
// OnTextMessageOutput is the output from the text message handler.
|
||||
type OnTextMessageOutput struct {
|
||||
// Error is the error message if the callback failed.
|
||||
// Empty or null indicates success.
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// OnBinaryMessageInput represents the OnBinaryMessageInput data structure.
|
||||
// OnBinaryMessageInput is the input provided when a binary message is received.
|
||||
type OnBinaryMessageInput struct {
|
||||
// ConnectionID is the unique identifier for the WebSocket connection that received the message.
|
||||
ConnectionID string `json:"connectionId"`
|
||||
// Data is the binary data received from the WebSocket, encoded as base64.
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
// OnBinaryMessageOutput represents the OnBinaryMessageOutput data structure.
|
||||
// OnBinaryMessageOutput is the output from the binary message handler.
|
||||
type OnBinaryMessageOutput struct {
|
||||
// Error is the error message if the callback failed.
|
||||
// Empty or null indicates success.
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// WebSocket is the marker interface for websocket plugins.
|
||||
// Implement one or more of the provider interfaces below.
|
||||
// WebSocketCallback provides WebSocket message handling.
|
||||
// This capability allows plugins to receive callbacks for WebSocket events
|
||||
// such as text messages, binary messages, errors, and connection closures.
|
||||
// Plugins that use the WebSocket host service must implement this capability
|
||||
// to handle incoming events.
|
||||
type WebSocket interface{}
|
||||
|
||||
// TextMessageProvider provides the OnTextMessage function.
|
||||
type TextMessageProvider interface {
|
||||
OnTextMessage(OnTextMessageInput) (OnTextMessageOutput, error)
|
||||
}
|
||||
|
||||
// BinaryMessageProvider provides the OnBinaryMessage function.
|
||||
type BinaryMessageProvider interface {
|
||||
OnBinaryMessage(OnBinaryMessageInput) (OnBinaryMessageOutput, error)
|
||||
}
|
||||
|
||||
// ErrorProvider provides the OnError function.
|
||||
type ErrorProvider interface {
|
||||
OnError(OnErrorInput) (OnErrorOutput, error)
|
||||
}
|
||||
|
||||
// CloseProvider provides the OnClose function.
|
||||
type CloseProvider interface {
|
||||
OnClose(OnCloseInput) (OnCloseOutput, error)
|
||||
} // Internal implementation holders
|
||||
var (
|
||||
textMessageImpl func(OnTextMessageInput) (OnTextMessageOutput, error)
|
||||
binaryMessageImpl func(OnBinaryMessageInput) (OnBinaryMessageOutput, error)
|
||||
errorImpl func(OnErrorInput) (OnErrorOutput, error)
|
||||
closeImpl func(OnCloseInput) (OnCloseOutput, error)
|
||||
)
|
||||
|
||||
// Register registers a websocket implementation.
|
||||
// The implementation is checked for optional provider interfaces.
|
||||
func Register(impl WebSocket) {
|
||||
if p, ok := impl.(TextMessageProvider); ok {
|
||||
textMessageImpl = p.OnTextMessage
|
||||
}
|
||||
if p, ok := impl.(BinaryMessageProvider); ok {
|
||||
binaryMessageImpl = p.OnBinaryMessage
|
||||
}
|
||||
if p, ok := impl.(ErrorProvider); ok {
|
||||
errorImpl = p.OnError
|
||||
}
|
||||
if p, ok := impl.(CloseProvider); ok {
|
||||
closeImpl = p.OnClose
|
||||
}
|
||||
}
|
||||
|
||||
// NotImplementedCode is the standard return code for unimplemented functions.
|
||||
// The host recognizes this and skips the plugin gracefully.
|
||||
const NotImplementedCode int32 = -2
|
||||
|
||||
//export nd_websocket_on_text_message
|
||||
func _NdWebsocketOnTextMessage() int32 {
|
||||
if textMessageImpl == nil {
|
||||
// Return standard code - host will skip this plugin gracefully
|
||||
return NotImplementedCode
|
||||
}
|
||||
|
||||
var input OnTextMessageInput
|
||||
if err := pdk.InputJSON(&input); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
output, err := textMessageImpl(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
if err := pdk.OutputJSON(output); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
//export nd_websocket_on_binary_message
|
||||
func _NdWebsocketOnBinaryMessage() int32 {
|
||||
if binaryMessageImpl == nil {
|
||||
// Return standard code - host will skip this plugin gracefully
|
||||
return NotImplementedCode
|
||||
}
|
||||
|
||||
var input OnBinaryMessageInput
|
||||
if err := pdk.InputJSON(&input); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
output, err := binaryMessageImpl(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
if err := pdk.OutputJSON(output); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
//export nd_websocket_on_error
|
||||
func _NdWebsocketOnError() int32 {
|
||||
if errorImpl == nil {
|
||||
// Return standard code - host will skip this plugin gracefully
|
||||
return NotImplementedCode
|
||||
}
|
||||
|
||||
var input OnErrorInput
|
||||
if err := pdk.InputJSON(&input); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
output, err := errorImpl(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
if err := pdk.OutputJSON(output); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
//export nd_websocket_on_close
|
||||
func _NdWebsocketOnClose() int32 {
|
||||
if closeImpl == nil {
|
||||
// Return standard code - host will skip this plugin gracefully
|
||||
return NotImplementedCode
|
||||
}
|
||||
|
||||
var input OnCloseInput
|
||||
if err := pdk.InputJSON(&input); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
output, err := closeImpl(input)
|
||||
if err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
if err := pdk.OutputJSON(output); err != nil {
|
||||
pdk.SetError(err)
|
||||
return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
116
plugins/pdk/go/websocket/websocket_stub.go
Normal file
116
plugins/pdk/go/websocket/websocket_stub.go
Normal file
@ -0,0 +1,116 @@
|
||||
// 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 websocket
|
||||
|
||||
// OnErrorInput represents the OnErrorInput data structure.
|
||||
// OnErrorInput is the input provided when an error occurs on a WebSocket connection.
|
||||
type OnErrorInput struct {
|
||||
// ConnectionID is the unique identifier for the WebSocket connection where the error occurred.
|
||||
ConnectionID string `json:"connectionId"`
|
||||
// Error is the error message describing what went wrong.
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// OnErrorOutput represents the OnErrorOutput data structure.
|
||||
// OnErrorOutput is the output from the error handler.
|
||||
type OnErrorOutput struct {
|
||||
// Error is the error message if the callback failed.
|
||||
// Empty or null indicates success.
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// OnCloseInput represents the OnCloseInput data structure.
|
||||
// OnCloseInput is the input provided when a WebSocket connection is closed.
|
||||
type OnCloseInput struct {
|
||||
// ConnectionID is the unique identifier for the WebSocket connection that was closed.
|
||||
ConnectionID string `json:"connectionId"`
|
||||
// Code is the WebSocket close status code (e.g., 1000 for normal closure,
|
||||
// 1001 for going away, 1006 for abnormal closure).
|
||||
Code int32 `json:"code"`
|
||||
// Reason is the human-readable reason for the connection closure, if provided.
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// OnCloseOutput represents the OnCloseOutput data structure.
|
||||
// OnCloseOutput is the output from the close handler.
|
||||
type OnCloseOutput struct {
|
||||
// Error is the error message if the callback failed.
|
||||
// Empty or null indicates success.
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// OnTextMessageInput represents the OnTextMessageInput data structure.
|
||||
// OnTextMessageInput is the input provided when a text message is received.
|
||||
type OnTextMessageInput struct {
|
||||
// ConnectionID is the unique identifier for the WebSocket connection that received the message.
|
||||
ConnectionID string `json:"connectionId"`
|
||||
// Message is the text message content received from the WebSocket.
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// OnTextMessageOutput represents the OnTextMessageOutput data structure.
|
||||
// OnTextMessageOutput is the output from the text message handler.
|
||||
type OnTextMessageOutput struct {
|
||||
// Error is the error message if the callback failed.
|
||||
// Empty or null indicates success.
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// OnBinaryMessageInput represents the OnBinaryMessageInput data structure.
|
||||
// OnBinaryMessageInput is the input provided when a binary message is received.
|
||||
type OnBinaryMessageInput struct {
|
||||
// ConnectionID is the unique identifier for the WebSocket connection that received the message.
|
||||
ConnectionID string `json:"connectionId"`
|
||||
// Data is the binary data received from the WebSocket, encoded as base64.
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
// OnBinaryMessageOutput represents the OnBinaryMessageOutput data structure.
|
||||
// OnBinaryMessageOutput is the output from the binary message handler.
|
||||
type OnBinaryMessageOutput struct {
|
||||
// Error is the error message if the callback failed.
|
||||
// Empty or null indicates success.
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// WebSocket is the marker interface for websocket plugins.
|
||||
// Implement one or more of the provider interfaces below.
|
||||
// WebSocketCallback provides WebSocket message handling.
|
||||
// This capability allows plugins to receive callbacks for WebSocket events
|
||||
// such as text messages, binary messages, errors, and connection closures.
|
||||
// Plugins that use the WebSocket host service must implement this capability
|
||||
// to handle incoming events.
|
||||
type WebSocket interface{}
|
||||
|
||||
// TextMessageProvider provides the OnTextMessage function.
|
||||
type TextMessageProvider interface {
|
||||
OnTextMessage(OnTextMessageInput) (OnTextMessageOutput, error)
|
||||
}
|
||||
|
||||
// BinaryMessageProvider provides the OnBinaryMessage function.
|
||||
type BinaryMessageProvider interface {
|
||||
OnBinaryMessage(OnBinaryMessageInput) (OnBinaryMessageOutput, error)
|
||||
}
|
||||
|
||||
// ErrorProvider provides the OnError function.
|
||||
type ErrorProvider interface {
|
||||
OnError(OnErrorInput) (OnErrorOutput, error)
|
||||
}
|
||||
|
||||
// CloseProvider provides the OnClose function.
|
||||
type CloseProvider interface {
|
||||
OnClose(OnCloseInput) (OnCloseOutput, 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(_ WebSocket) {}
|
||||
Loading…
x
Reference in New Issue
Block a user