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:
Deluan 2025-12-29 23:38:08 -05:00
parent 05455de90b
commit b4b7b91cf7
27 changed files with 3708 additions and 456 deletions

View 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

View 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"`
}

View 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"`
}

View 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"`
}

View 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"`
}

View 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"`
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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)

View File

@ -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"))
})
})
})

View 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}}

View File

@ -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}}

View File

@ -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")

View File

@ -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 {

View File

@ -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 => ../../..

View File

@ -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() {}

View File

@ -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"`
}

View 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
}

View 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) {}

View 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
}

View 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) {}

View 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
}

View 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) {}

View 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
}

View 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) {}

View 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
}

View 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) {}