From d43724c57151a54dd36089da14fa9bb622c67efc Mon Sep 17 00:00:00 2001 From: Deluan Date: Tue, 30 Dec 2025 10:09:18 -0500 Subject: [PATCH] refactor(plugins): streamline plugin function signatures and error handling Signed-off-by: Deluan --- plugins/capabilities/lifecycle.go | 17 +- plugins/capabilities/lifecycle.yaml | 27 +-- plugins/capabilities/scheduler_callback.go | 11 +- plugins/capabilities/scheduler_callback.yaml | 17 +- plugins/capabilities/scrobbler.go | 6 +- plugins/capabilities/websocket_callback.go | 36 +--- plugins/capabilities/websocket_callback.yaml | 48 ----- plugins/capability_lifecycle.go | 8 +- .../internal/templates/capability.go.tmpl | 52 +++++ .../templates/capability_stub.go.tmpl | 16 ++ plugins/cmd/ndpgen/internal/types.go | 15 ++ plugins/cmd/ndpgen/internal/xtp_schema.go | 4 +- plugins/examples/crypto-ticker/main.go | 41 ++-- .../discord-rich-presence-rs/src/lib.rs | 102 +++------ .../examples/discord-rich-presence/main.go | 49 ++--- .../examples/nowplaying-py/plugin/__init__.py | 13 +- plugins/host_scheduler.go | 7 +- plugins/host_websocket.go | 8 +- plugins/manager_call.go | 67 ++++++ plugins/pdk/go/lifecycle/lifecycle.go | 31 +-- plugins/pdk/go/lifecycle/lifecycle_stub.go | 15 +- plugins/pdk/go/metadata/metadata.go | 204 +++++++++--------- plugins/pdk/go/metadata/metadata_stub.go | 204 +++++++++--------- plugins/pdk/go/scheduler/scheduler.go | 20 +- plugins/pdk/go/scheduler/scheduler_stub.go | 10 +- plugins/pdk/go/scrobbler/scrobbler.go | 12 +- plugins/pdk/go/scrobbler/scrobbler_stub.go | 6 +- plugins/pdk/go/websocket/websocket.go | 88 ++------ plugins/pdk/go/websocket/websocket_stub.go | 48 +---- plugins/pdk/rust/host/README.md | 2 +- plugins/scrobbler_adapter.go | 13 +- plugins/scrobbler_adapter_test.go | 18 +- plugins/testdata/test-scheduler/main.go | 22 +- plugins/testdata/test-scheduler/pdk.gen.go | 26 +-- plugins/testdata/test-scrobbler/main.go | 37 ++-- plugins/testdata/test-websocket/main.go | 67 ++---- 36 files changed, 566 insertions(+), 801 deletions(-) diff --git a/plugins/capabilities/lifecycle.go b/plugins/capabilities/lifecycle.go index 7c12260b4..b5f19ec5b 100644 --- a/plugins/capabilities/lifecycle.go +++ b/plugins/capabilities/lifecycle.go @@ -13,20 +13,7 @@ package capabilities 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. + // Errors are logged but will not prevent the plugin from being loaded. //nd:export name=nd_on_init - OnInit(InitRequest) (InitResponse, error) -} - -// InitRequest is the request provided to the init callback. -// Currently empty, reserved for future use. -type InitRequest struct{} - -// InitResponse is the response from the init callback. -type InitResponse struct { - // Error is the error message if initialization failed. - // Empty string indicates success. - // The error is logged but does not prevent the plugin from being loaded. - Error string `json:"error,omitempty"` + OnInit() error } diff --git a/plugins/capabilities/lifecycle.yaml b/plugins/capabilities/lifecycle.yaml index 933107f84..7c6af62b8 100644 --- a/plugins/capabilities/lifecycle.yaml +++ b/plugins/capabilities/lifecycle.yaml @@ -4,29 +4,4 @@ exports: description: |- 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. - input: - $ref: '#/components/schemas/InitRequest' - contentType: application/json - output: - $ref: '#/components/schemas/InitResponse' - contentType: application/json -components: - schemas: - InitRequest: - description: |- - InitRequest is the request provided to the init callback. - Currently empty, reserved for future use. - type: object - properties: {} - InitResponse: - description: InitResponse is the response from the init callback. - type: object - properties: - error: - type: string - description: |- - Error is the error message if initialization failed. - Empty string indicates success. - The error is logged but does not prevent the plugin from being loaded. + Errors are logged but will not prevent the plugin from being loaded. diff --git a/plugins/capabilities/scheduler_callback.go b/plugins/capabilities/scheduler_callback.go index b722a2fab..cd141121d 100644 --- a/plugins/capabilities/scheduler_callback.go +++ b/plugins/capabilities/scheduler_callback.go @@ -8,8 +8,9 @@ package capabilities //nd:capability name=scheduler type SchedulerCallback interface { // OnSchedulerCallback is called when a scheduled task fires. + // Errors are logged but do not affect the scheduling system. //nd:export name=nd_scheduler_callback - OnSchedulerCallback(SchedulerCallbackRequest) (SchedulerCallbackResponse, error) + OnSchedulerCallback(SchedulerCallbackRequest) error } // SchedulerCallbackRequest is the request provided when a scheduled task fires. @@ -24,11 +25,3 @@ type SchedulerCallbackRequest struct { // false if it's a one-time schedule (created via ScheduleOneTime). IsRecurring bool `json:"isRecurring"` } - -// SchedulerCallbackResponse is the response from the scheduler callback. -type SchedulerCallbackResponse struct { - // Error is the error message if the callback failed to process the scheduled task. - // Empty string indicates success. The error is logged but does not - // affect the scheduling system. - Error string `json:"error,omitempty"` -} diff --git a/plugins/capabilities/scheduler_callback.yaml b/plugins/capabilities/scheduler_callback.yaml index 6f1eb836a..34358a697 100644 --- a/plugins/capabilities/scheduler_callback.yaml +++ b/plugins/capabilities/scheduler_callback.yaml @@ -1,13 +1,12 @@ version: v1-draft exports: nd_scheduler_callback: - description: OnSchedulerCallback is called when a scheduled task fires. + description: |- + OnSchedulerCallback is called when a scheduled task fires. + Errors are logged but do not affect the scheduling system. input: $ref: '#/components/schemas/SchedulerCallbackRequest' contentType: application/json - output: - $ref: '#/components/schemas/SchedulerCallbackResponse' - contentType: application/json components: schemas: SchedulerCallbackRequest: @@ -33,13 +32,3 @@ components: - scheduleId - payload - isRecurring - SchedulerCallbackResponse: - description: SchedulerCallbackResponse is the response from the scheduler callback. - type: object - properties: - error: - type: string - description: |- - Error is the error message if the callback failed to process the scheduled task. - Empty string indicates success. The error is logged but does not - affect the scheduling system. diff --git a/plugins/capabilities/scrobbler.go b/plugins/capabilities/scrobbler.go index 5c6b686bd..e39bad89e 100644 --- a/plugins/capabilities/scrobbler.go +++ b/plugins/capabilities/scrobbler.go @@ -11,15 +11,15 @@ package capabilities type Scrobbler interface { // IsAuthorized checks if a user is authorized to scrobble to this service. //nd:export name=nd_scrobbler_is_authorized - IsAuthorized(IsAuthorizedRequest) (IsAuthorizedResponse, error) + IsAuthorized(IsAuthorizedRequest) (*IsAuthorizedResponse, error) // NowPlaying sends a now playing notification to the scrobbling service. //nd:export name=nd_scrobbler_now_playing - NowPlaying(NowPlayingRequest) (ScrobblerResponse, error) + NowPlaying(NowPlayingRequest) (*ScrobblerResponse, error) // Scrobble submits a completed scrobble to the scrobbling service. //nd:export name=nd_scrobbler_scrobble - Scrobble(ScrobbleRequest) (ScrobblerResponse, error) + Scrobble(ScrobbleRequest) (*ScrobblerResponse, error) } // IsAuthorizedRequest is the request for authorization check. diff --git a/plugins/capabilities/websocket_callback.go b/plugins/capabilities/websocket_callback.go index 7f04e74d3..07db029f0 100644 --- a/plugins/capabilities/websocket_callback.go +++ b/plugins/capabilities/websocket_callback.go @@ -10,19 +10,19 @@ package capabilities 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(OnTextMessageRequest) (OnTextMessageResponse, error) + OnTextMessage(OnTextMessageRequest) error // OnBinaryMessage is called when a binary message is received on a WebSocket connection. //nd:export name=nd_websocket_on_binary_message - OnBinaryMessage(OnBinaryMessageRequest) (OnBinaryMessageResponse, error) + OnBinaryMessage(OnBinaryMessageRequest) error // OnError is called when an error occurs on a WebSocket connection. //nd:export name=nd_websocket_on_error - OnError(OnErrorRequest) (OnErrorResponse, error) + OnError(OnErrorRequest) error // OnClose is called when a WebSocket connection is closed. //nd:export name=nd_websocket_on_close - OnClose(OnCloseRequest) (OnCloseResponse, error) + OnClose(OnCloseRequest) error } // OnTextMessageRequest is the request provided when a text message is received. @@ -33,13 +33,6 @@ type OnTextMessageRequest struct { Message string `json:"message"` } -// OnTextMessageResponse is the response from the text message handler. -type OnTextMessageResponse struct { - // Error is the error message if the callback failed. - // Empty string indicates success. - Error string `json:"error,omitempty"` -} - // OnBinaryMessageRequest is the request provided when a binary message is received. type OnBinaryMessageRequest struct { // ConnectionID is the unique identifier for the WebSocket connection that received the message. @@ -48,13 +41,6 @@ type OnBinaryMessageRequest struct { Data string `json:"data"` } -// OnBinaryMessageResponse is the response from the binary message handler. -type OnBinaryMessageResponse struct { - // Error is the error message if the callback failed. - // Empty string indicates success. - Error string `json:"error,omitempty"` -} - // OnErrorRequest is the request provided when an error occurs on a WebSocket connection. type OnErrorRequest struct { // ConnectionID is the unique identifier for the WebSocket connection where the error occurred. @@ -63,13 +49,6 @@ type OnErrorRequest struct { Error string `json:"error"` } -// OnErrorResponse is the response from the error handler. -type OnErrorResponse struct { - // Error is the error message if the callback failed. - // Empty string indicates success. - Error string `json:"error,omitempty"` -} - // OnCloseRequest is the request provided when a WebSocket connection is closed. type OnCloseRequest struct { // ConnectionID is the unique identifier for the WebSocket connection that was closed. @@ -80,10 +59,3 @@ type OnCloseRequest struct { // Reason is the human-readable reason for the connection closure, if provided. Reason string `json:"reason"` } - -// OnCloseResponse is the response from the close handler. -type OnCloseResponse struct { - // Error is the error message if the callback failed. - // Empty string indicates success. - Error string `json:"error,omitempty"` -} diff --git a/plugins/capabilities/websocket_callback.yaml b/plugins/capabilities/websocket_callback.yaml index 9121e17cf..e80c792c6 100644 --- a/plugins/capabilities/websocket_callback.yaml +++ b/plugins/capabilities/websocket_callback.yaml @@ -5,33 +5,21 @@ exports: input: $ref: '#/components/schemas/OnTextMessageRequest' contentType: application/json - output: - $ref: '#/components/schemas/OnTextMessageResponse' - contentType: application/json nd_websocket_on_binary_message: description: OnBinaryMessage is called when a binary message is received on a WebSocket connection. input: $ref: '#/components/schemas/OnBinaryMessageRequest' contentType: application/json - output: - $ref: '#/components/schemas/OnBinaryMessageResponse' - contentType: application/json nd_websocket_on_error: description: OnError is called when an error occurs on a WebSocket connection. input: $ref: '#/components/schemas/OnErrorRequest' contentType: application/json - output: - $ref: '#/components/schemas/OnErrorResponse' - contentType: application/json nd_websocket_on_close: description: OnClose is called when a WebSocket connection is closed. input: $ref: '#/components/schemas/OnCloseRequest' contentType: application/json - output: - $ref: '#/components/schemas/OnCloseResponse' - contentType: application/json components: schemas: OnBinaryMessageRequest: @@ -47,15 +35,6 @@ components: required: - connectionId - data - OnBinaryMessageResponse: - description: OnBinaryMessageResponse is the response from the binary message handler. - type: object - properties: - error: - type: string - description: |- - Error is the error message if the callback failed. - Empty string indicates success. OnCloseRequest: description: OnCloseRequest is the request provided when a WebSocket connection is closed. type: object @@ -76,15 +55,6 @@ components: - connectionId - code - reason - OnCloseResponse: - description: OnCloseResponse is the response from the close handler. - type: object - properties: - error: - type: string - description: |- - Error is the error message if the callback failed. - Empty string indicates success. OnErrorRequest: description: OnErrorRequest is the request provided when an error occurs on a WebSocket connection. type: object @@ -98,15 +68,6 @@ components: required: - connectionId - error - OnErrorResponse: - description: OnErrorResponse is the response from the error handler. - type: object - properties: - error: - type: string - description: |- - Error is the error message if the callback failed. - Empty string indicates success. OnTextMessageRequest: description: OnTextMessageRequest is the request provided when a text message is received. type: object @@ -120,12 +81,3 @@ components: required: - connectionId - message - OnTextMessageResponse: - description: OnTextMessageResponse is the response from the text message handler. - type: object - properties: - error: - type: string - description: |- - Error is the error message if the callback failed. - Empty string indicates success. diff --git a/plugins/capability_lifecycle.go b/plugins/capability_lifecycle.go index 0d42cab4f..499e3916d 100644 --- a/plugins/capability_lifecycle.go +++ b/plugins/capability_lifecycle.go @@ -4,7 +4,6 @@ import ( "context" "github.com/navidrome/navidrome/log" - "github.com/navidrome/navidrome/plugins/capabilities" ) // CapabilityLifecycle indicates the plugin has lifecycle callback functions. @@ -29,16 +28,11 @@ func callPluginInit(ctx context.Context, instance *plugin) { log.Debug(ctx, "Calling plugin init function", "plugin", instance.name) - result, err := callPluginFunction[capabilities.InitRequest, capabilities.InitResponse](ctx, instance, FuncOnInit, capabilities.InitRequest{}) + err := callPluginFunctionNoInput(ctx, instance, FuncOnInit) if err != nil { log.Error(ctx, "Plugin init function failed", "plugin", instance.name, err) return } - if result.Error != "" { - log.Error(ctx, "Plugin init function returned error", "plugin", instance.name, "error", result.Error) - return - } - log.Debug(ctx, "Plugin init function completed", "plugin", instance.name) } diff --git a/plugins/cmd/ndpgen/internal/templates/capability.go.tmpl b/plugins/cmd/ndpgen/internal/templates/capability.go.tmpl index 42495737d..265627b6b 100644 --- a/plugins/cmd/ndpgen/internal/templates/capability.go.tmpl +++ b/plugins/cmd/ndpgen/internal/templates/capability.go.tmpl @@ -68,7 +68,15 @@ type {{.Name}} struct { type {{agentName .Capability}} interface { {{- range .Capability.Methods}} // {{.Name}}{{if .Doc}} - {{.Doc}}{{end}} + {{- if and .HasInput .HasOutput}} {{.Name}}({{.Input.Type}}) ({{.Output.Type}}, error) + {{- else if .HasInput}} + {{.Name}}({{.Input.Type}}) error + {{- else if .HasOutput}} + {{.Name}}() ({{.Output.Type}}, error) + {{- else}} + {{.Name}}() error + {{- end}} {{- end}} } {{- else}} @@ -87,7 +95,15 @@ type {{agentName .Capability}} interface{} // {{providerInterface .}} provides the {{.Name}} function. type {{providerInterface .}} interface { + {{- if and .HasInput .HasOutput}} {{.Name}}({{.Input.Type}}) ({{.Output.Type}}, error) + {{- else if .HasInput}} + {{.Name}}({{.Input.Type}}) error + {{- else if .HasOutput}} + {{.Name}}() ({{.Output.Type}}, error) + {{- else}} + {{.Name}}() error + {{- end}} } {{- end}} {{- end}} @@ -97,7 +113,15 @@ type {{providerInterface .}} interface { // Internal implementation holders var ( {{- range .Capability.Methods}} + {{- if and .HasInput .HasOutput}} {{implVar .}} func({{.Input.Type}}) ({{.Output.Type}}, error) + {{- else if .HasInput}} + {{implVar .}} func({{.Input.Type}}) error + {{- else if .HasOutput}} + {{implVar .}} func() ({{.Output.Type}}, error) + {{- else}} + {{implVar .}} func() error + {{- end}} {{- end}} ) @@ -133,12 +157,15 @@ func {{exportFunc .}}() int32 { // Return standard code - host will skip this plugin gracefully return NotImplementedCode } +{{- if .HasInput}} var input {{.Input.Type}} if err := pdk.InputJSON(&input); err != nil { pdk.SetError(err) return -1 } +{{- end}} +{{- if and .HasInput .HasOutput}} output, err := {{implVar .}}(input) if err != nil { @@ -150,6 +177,31 @@ func {{exportFunc .}}() int32 { pdk.SetError(err) return -1 } +{{- else if .HasInput}} + + if err := {{implVar .}}(input); err != nil { + pdk.SetError(err) + return -1 + } +{{- else if .HasOutput}} + + output, err := {{implVar .}}() + if err != nil { + pdk.SetError(err) + return -1 + } + + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(err) + return -1 + } +{{- else}} + + if err := {{implVar .}}(); err != nil { + pdk.SetError(err) + return -1 + } +{{- end}} return 0 } diff --git a/plugins/cmd/ndpgen/internal/templates/capability_stub.go.tmpl b/plugins/cmd/ndpgen/internal/templates/capability_stub.go.tmpl index ea3a210fe..e997bec2c 100644 --- a/plugins/cmd/ndpgen/internal/templates/capability_stub.go.tmpl +++ b/plugins/cmd/ndpgen/internal/templates/capability_stub.go.tmpl @@ -65,7 +65,15 @@ type {{.Name}} struct { type {{agentName .Capability}} interface { {{- range .Capability.Methods}} // {{.Name}}{{if .Doc}} - {{.Doc}}{{end}} + {{- if and .HasInput .HasOutput}} {{.Name}}({{.Input.Type}}) ({{.Output.Type}}, error) + {{- else if .HasInput}} + {{.Name}}({{.Input.Type}}) error + {{- else if .HasOutput}} + {{.Name}}() ({{.Output.Type}}, error) + {{- else}} + {{.Name}}() error + {{- end}} {{- end}} } {{- else}} @@ -84,7 +92,15 @@ type {{agentName .Capability}} interface{} // {{providerInterface .}} provides the {{.Name}} function. type {{providerInterface .}} interface { + {{- if and .HasInput .HasOutput}} {{.Name}}({{.Input.Type}}) ({{.Output.Type}}, error) + {{- else if .HasInput}} + {{.Name}}({{.Input.Type}}) error + {{- else if .HasOutput}} + {{.Name}}() ({{.Output.Type}}, error) + {{- else}} + {{.Name}}() error + {{- end}} } {{- end}} {{- end}} diff --git a/plugins/cmd/ndpgen/internal/types.go b/plugins/cmd/ndpgen/internal/types.go index 0ac4f6852..6cd6c8215 100644 --- a/plugins/cmd/ndpgen/internal/types.go +++ b/plugins/cmd/ndpgen/internal/types.go @@ -113,6 +113,21 @@ func (e Export) ExportFuncName() string { return result.String() } +// HasInput returns true if the method has an input parameter. +func (e Export) HasInput() bool { + return e.Input.Type != "" +} + +// HasOutput returns true if the method has a non-error return value. +func (e Export) HasOutput() bool { + return e.Output.Type != "" +} + +// IsPointerOutput returns true if the output type is a pointer. +func (e Export) IsPointerOutput() bool { + return strings.HasPrefix(e.Output.Type, "*") +} + // StructDef represents a Go struct type definition. type StructDef struct { Name string // Go struct name (e.g., "Library") diff --git a/plugins/cmd/ndpgen/internal/xtp_schema.go b/plugins/cmd/ndpgen/internal/xtp_schema.go index ced42dcb1..17e473c0b 100644 --- a/plugins/cmd/ndpgen/internal/xtp_schema.go +++ b/plugins/cmd/ndpgen/internal/xtp_schema.go @@ -82,13 +82,13 @@ func buildExport(export Export) xtpExport { e := xtpExport{Description: cleanDocForYAML(export.Doc)} if export.Input.Type != "" { e.Input = &xtpIOParam{ - Ref: "#/components/schemas/" + export.Input.Type, + Ref: "#/components/schemas/" + strings.TrimPrefix(export.Input.Type, "*"), ContentType: "application/json", } } if export.Output.Type != "" { e.Output = &xtpIOParam{ - Ref: "#/components/schemas/" + export.Output.Type, + Ref: "#/components/schemas/" + strings.TrimPrefix(export.Output.Type, "*"), ContentType: "application/json", } } diff --git a/plugins/examples/crypto-ticker/main.go b/plugins/examples/crypto-ticker/main.go index 52f8d821d..711634d52 100755 --- a/plugins/examples/crypto-ticker/main.go +++ b/plugins/examples/crypto-ticker/main.go @@ -9,7 +9,7 @@ import ( "fmt" "strings" - pdk "github.com/extism/go-pdk" + "github.com/extism/go-pdk" "github.com/navidrome/navidrome/plugins/pdk/go/host" "github.com/navidrome/navidrome/plugins/pdk/go/lifecycle" "github.com/navidrome/navidrome/plugins/pdk/go/scheduler" @@ -71,7 +71,7 @@ var ( // OnInit is called when the plugin is loaded. // We use this to establish the initial WebSocket connection. -func (p *cryptoTickerPlugin) OnInit(_ lifecycle.InitRequest) (lifecycle.InitResponse, error) { +func (p *cryptoTickerPlugin) OnInit() error { pdk.Log(pdk.LogInfo, "Crypto Ticker Plugin initializing...") // Get ticker configuration @@ -84,13 +84,8 @@ func (p *cryptoTickerPlugin) OnInit(_ lifecycle.InitRequest) (lifecycle.InitResp pdk.Log(pdk.LogInfo, fmt.Sprintf("Configured tickers: %v", tickers)) // Connect to WebSocket - err := connectAndSubscribe(tickers) - if err != nil { - pdk.Log(pdk.LogError, fmt.Sprintf("Failed to connect: %v", err)) - // Don't fail init - let reconnect logic handle it - } - - return lifecycle.InitResponse{}, nil + // Errors won't fail init - reconnect logic will handle it + return connectAndSubscribe(tickers) } // parseTickerSymbols parses a comma-separated list of ticker symbols @@ -143,10 +138,10 @@ func connectAndSubscribe(tickers []string) error { } // OnTextMessage is called when a text message is received -func (p *cryptoTickerPlugin) OnTextMessage(input websocket.OnTextMessageRequest) (websocket.OnTextMessageResponse, error) { +func (p *cryptoTickerPlugin) OnTextMessage(input websocket.OnTextMessageRequest) error { // Only process messages from our connection if input.ConnectionID != connectionID { - return websocket.OnTextMessageResponse{}, nil + return nil } // Try to parse as a ticker message @@ -154,7 +149,7 @@ func (p *cryptoTickerPlugin) OnTextMessage(input websocket.OnTextMessageRequest) err := json.Unmarshal([]byte(input.Message), &ticker) if err != nil { // Not a valid JSON message, ignore - return websocket.OnTextMessageResponse{}, nil + return nil } // Only process ticker messages @@ -163,7 +158,7 @@ func (p *cryptoTickerPlugin) OnTextMessage(input websocket.OnTextMessageRequest) if ticker.Type != "" { pdk.Log(pdk.LogDebug, fmt.Sprintf("Received %s message", ticker.Type)) } - return websocket.OnTextMessageResponse{}, nil + return nil } // Calculate 24h change percentage @@ -178,24 +173,24 @@ func (p *cryptoTickerPlugin) OnTextMessage(input websocket.OnTextMessageRequest) ticker.BestAsk, )) - return websocket.OnTextMessageResponse{}, nil + return nil } // OnBinaryMessage is called when a binary message is received -func (p *cryptoTickerPlugin) OnBinaryMessage(input websocket.OnBinaryMessageRequest) (websocket.OnBinaryMessageResponse, error) { +func (p *cryptoTickerPlugin) OnBinaryMessage(input websocket.OnBinaryMessageRequest) error { // Coinbase doesn't send binary messages, but we implement the handler anyway pdk.Log(pdk.LogWarn, fmt.Sprintf("Received unexpected binary message on connection %s", input.ConnectionID)) - return websocket.OnBinaryMessageResponse{}, nil + return nil } // OnError is called when an error occurs on the WebSocket connection -func (p *cryptoTickerPlugin) OnError(input websocket.OnErrorRequest) (websocket.OnErrorResponse, error) { +func (p *cryptoTickerPlugin) OnError(input websocket.OnErrorRequest) error { pdk.Log(pdk.LogError, fmt.Sprintf("WebSocket error on connection %s: %s", input.ConnectionID, input.Error)) - return websocket.OnErrorResponse{}, nil + return nil } // OnClose is called when the WebSocket connection is closed -func (p *cryptoTickerPlugin) OnClose(input websocket.OnCloseRequest) (websocket.OnCloseResponse, error) { +func (p *cryptoTickerPlugin) OnClose(input websocket.OnCloseRequest) error { pdk.Log(pdk.LogInfo, fmt.Sprintf("WebSocket connection %s closed (code: %d, reason: %s)", input.ConnectionID, input.Code, input.Reason)) @@ -210,14 +205,14 @@ func (p *cryptoTickerPlugin) OnClose(input websocket.OnCloseRequest) (websocket. } } - return websocket.OnCloseResponse{}, nil + return nil } // OnSchedulerCallback is called when a scheduled task fires -func (p *cryptoTickerPlugin) OnSchedulerCallback(input scheduler.SchedulerCallbackRequest) (scheduler.SchedulerCallbackResponse, error) { +func (p *cryptoTickerPlugin) OnSchedulerCallback(input scheduler.SchedulerCallbackRequest) error { // Only handle our reconnection schedule if input.ScheduleID != reconnectScheduleID { - return scheduler.SchedulerCallbackResponse{}, nil + return nil } pdk.Log(pdk.LogInfo, "Attempting to reconnect to Coinbase WebSocket API...") @@ -244,7 +239,7 @@ func (p *cryptoTickerPlugin) OnSchedulerCallback(input scheduler.SchedulerCallba pdk.Log(pdk.LogInfo, "Successfully reconnected!") } - return scheduler.SchedulerCallbackResponse{}, nil + return nil } // calculatePercentChange calculates the percentage change between open and current price diff --git a/plugins/examples/discord-rich-presence-rs/src/lib.rs b/plugins/examples/discord-rich-presence-rs/src/lib.rs index 30c304881..72fa10eb4 100644 --- a/plugins/examples/discord-rich-presence-rs/src/lib.rs +++ b/plugins/examples/discord-rich-presence-rs/src/lib.rs @@ -154,12 +154,6 @@ struct SchedulerCallbackInput { is_recurring: bool, } -#[derive(Serialize, Default)] -struct SchedulerCallbackOutput { - #[serde(skip_serializing_if = "Option::is_none")] - error: Option, -} - // ============================================================================ // WebSocket Callback Types // ============================================================================ @@ -172,12 +166,6 @@ struct OnTextMessageInput { message: String, } -#[derive(Serialize, Default)] -struct OnTextMessageOutput { - #[serde(skip_serializing_if = "Option::is_none")] - error: Option, -} - #[derive(Deserialize)] #[serde(rename_all = "camelCase")] #[allow(dead_code)] @@ -186,12 +174,6 @@ struct OnBinaryMessageInput { message: Vec, } -#[derive(Serialize, Default)] -struct OnBinaryMessageOutput { - #[serde(skip_serializing_if = "Option::is_none")] - error: Option, -} - #[derive(Deserialize)] #[serde(rename_all = "camelCase")] #[allow(dead_code)] @@ -200,12 +182,6 @@ struct OnErrorInput { error: String, } -#[derive(Serialize, Default)] -struct OnErrorOutput { - #[serde(skip_serializing_if = "Option::is_none")] - error: Option, -} - #[derive(Deserialize)] #[serde(rename_all = "camelCase")] #[allow(dead_code)] @@ -215,12 +191,6 @@ struct OnCloseInput { reason: String, } -#[derive(Serialize, Default)] -struct OnCloseOutput { - #[serde(skip_serializing_if = "Option::is_none")] - error: Option, -} - // ============================================================================ // Scrobbler Plugin Exports // ============================================================================ @@ -248,7 +218,7 @@ pub fn nd_scrobbler_is_authorized(Json(input): Json) -> FnResult, -) -> FnResult> { +) -> FnResult>> { info!( "Setting presence for user {}, track: {}", input.username, input.track.title @@ -259,10 +229,10 @@ pub fn nd_scrobbler_now_playing( Ok(config) => config, Err(e) => { let err_msg = format!("failed to get config: {:?}", e); - return Ok(Json(ScrobblerOutput { + return Ok(Json(Some(ScrobblerOutput { error: Some(err_msg), error_type: Some(ERROR_TYPE_RETRY_LATER.to_string()), - })); + }))); } }; @@ -271,20 +241,20 @@ pub fn nd_scrobbler_now_playing( Some(token) => token.clone(), None => { let err_msg = format!("user '{}' not authorized", input.username); - return Ok(Json(ScrobblerOutput { + return Ok(Json(Some(ScrobblerOutput { error: Some(err_msg), error_type: Some(ERROR_TYPE_NOT_AUTHORIZED.to_string()), - })); + }))); } }; // Connect to Discord if let Err(e) = rpc::connect(&input.username, &user_token) { let err_msg = format!("failed to connect to Discord: {:?}", e); - return Ok(Json(ScrobblerOutput { + return Ok(Json(Some(ScrobblerOutput { error: Some(err_msg), error_type: Some(ERROR_TYPE_RETRY_LATER.to_string()), - })); + }))); } // Cancel any existing completion schedule @@ -320,10 +290,10 @@ pub fn nd_scrobbler_now_playing( }, ) { let err_msg = format!("failed to send activity: {:?}", e); - return Ok(Json(ScrobblerOutput { + return Ok(Json(Some(ScrobblerOutput { error: Some(err_msg), error_type: Some(ERROR_TYPE_RETRY_LATER.to_string()), - })); + }))); } // Schedule a timer to clear the activity after the track completes @@ -336,14 +306,15 @@ pub fn nd_scrobbler_now_playing( warn!("Failed to schedule completion timer: {:?}", e); } - Ok(Json(ScrobblerOutput::default())) + // Success - return None to indicate no error + Ok(Json(None)) } /// Handles scrobble requests (no-op for Discord Rich Presence). #[plugin_fn] -pub fn nd_scrobbler_scrobble(_input: Json) -> FnResult> { - // Discord Rich Presence doesn't need scrobble events - Ok(Json(ScrobblerOutput::default())) +pub fn nd_scrobbler_scrobble(_input: Json) -> FnResult>> { + // Discord Rich Presence doesn't need scrobble events - success + Ok(Json(None)) } // ============================================================================ @@ -352,34 +323,23 @@ pub fn nd_scrobbler_scrobble(_input: Json) -> FnResult, -) -> FnResult> { - +pub fn nd_scheduler_callback(Json(input): Json) -> FnResult<()> { match input.payload.as_str() { PAYLOAD_HEARTBEAT => { // Heartbeat callback - schedule_id is the username - if let Err(e) = rpc::handle_heartbeat_callback(&input.schedule_id) { - return Ok(Json(SchedulerCallbackOutput { - error: Some(e.to_string()), - })); - } + rpc::handle_heartbeat_callback(&input.schedule_id)?; } PAYLOAD_CLEAR_ACTIVITY => { // Clear activity callback - schedule_id is "username-clear" let username = input.schedule_id.trim_end_matches("-clear"); - if let Err(e) = rpc::handle_clear_activity_callback(username) { - return Ok(Json(SchedulerCallbackOutput { - error: Some(e.to_string()), - })); - } + rpc::handle_clear_activity_callback(username)?; } _ => { warn!("Unknown scheduler callback payload: {}", input.payload); } } - Ok(Json(SchedulerCallbackOutput::default())) + Ok(()) } // ============================================================================ @@ -388,42 +348,34 @@ pub fn nd_scheduler_callback( /// Handles incoming WebSocket text messages. #[plugin_fn] -pub fn nd_websocket_on_text_message( - Json(input): Json, -) -> FnResult> { - if let Err(e) = rpc::handle_websocket_message(&input.connection_id, &input.message) { - return Ok(Json(OnTextMessageOutput { - error: Some(e.to_string()), - })); - } - Ok(Json(OnTextMessageOutput::default())) +pub fn nd_websocket_on_text_message(Json(input): Json) -> FnResult<()> { + rpc::handle_websocket_message(&input.connection_id, &input.message)?; + Ok(()) } /// Handles incoming WebSocket binary messages. #[plugin_fn] -pub fn nd_websocket_on_binary_message( - Json(_input): Json, -) -> FnResult> { +pub fn nd_websocket_on_binary_message(Json(_input): Json) -> FnResult<()> { // Binary messages are not expected from Discord - Ok(Json(OnBinaryMessageOutput::default())) + Ok(()) } /// Handles WebSocket errors. #[plugin_fn] -pub fn nd_websocket_on_error(Json(input): Json) -> FnResult> { +pub fn nd_websocket_on_error(Json(input): Json) -> FnResult<()> { warn!( "WebSocket error for connection '{}': {}", input.connection_id, input.error ); - Ok(Json(OnErrorOutput::default())) + Ok(()) } /// Handles WebSocket connection closure. #[plugin_fn] -pub fn nd_websocket_on_close(Json(input): Json) -> FnResult> { +pub fn nd_websocket_on_close(Json(input): Json) -> FnResult<()> { info!( "WebSocket connection '{}' closed with code {}: {}", input.connection_id, input.code, input.reason ); - Ok(Json(OnCloseOutput::default())) + Ok(()) } diff --git a/plugins/examples/discord-rich-presence/main.go b/plugins/examples/discord-rich-presence/main.go index dd1bcfa1a..4626a7328 100644 --- a/plugins/examples/discord-rich-presence/main.go +++ b/plugins/examples/discord-rich-presence/main.go @@ -93,31 +93,31 @@ func getImageURL(trackID string) string { // ============================================================================ // IsAuthorized checks if a user is authorized for Discord Rich Presence. -func (p *discordPlugin) IsAuthorized(input scrobbler.IsAuthorizedRequest) (scrobbler.IsAuthorizedResponse, error) { +func (p *discordPlugin) IsAuthorized(input scrobbler.IsAuthorizedRequest) (*scrobbler.IsAuthorizedResponse, error) { _, users, err := getConfig() if err != nil { - return scrobbler.IsAuthorizedResponse{}, fmt.Errorf("failed to check user authorization: %w", err) + return nil, fmt.Errorf("failed to check user authorization: %w", err) } _, authorized := users[input.Username] pdk.Log(pdk.LogInfo, fmt.Sprintf("IsAuthorized for user %s: %v", input.Username, authorized)) - return scrobbler.IsAuthorizedResponse{Authorized: authorized}, nil + return &scrobbler.IsAuthorizedResponse{Authorized: authorized}, nil } // NowPlaying sends a now playing notification to Discord. -func (p *discordPlugin) NowPlaying(input scrobbler.NowPlayingRequest) (scrobbler.ScrobblerResponse, error) { +func (p *discordPlugin) NowPlaying(input scrobbler.NowPlayingRequest) (*scrobbler.ScrobblerResponse, error) { pdk.Log(pdk.LogInfo, fmt.Sprintf("Setting presence for user %s, track: %s", input.Username, input.Track.Title)) // Load configuration clientID, users, err := getConfig() if err != nil { - return scrobbler.ScrobblerResponse{}, fmt.Errorf("failed to get config: %w", err) + return nil, fmt.Errorf("failed to get config: %w", err) } // Check authorization userToken, authorized := users[input.Username] if !authorized { - return scrobbler.ScrobblerResponse{ + return &scrobbler.ScrobblerResponse{ Error: fmt.Sprintf("user '%s' not authorized", input.Username), ErrorType: scrobbler.ScrobblerErrorNotAuthorized, }, nil @@ -125,7 +125,7 @@ func (p *discordPlugin) NowPlaying(input scrobbler.NowPlayingRequest) (scrobbler // Connect to Discord if err := connect(input.Username, userToken); err != nil { - return scrobbler.ScrobblerResponse{ + return &scrobbler.ScrobblerResponse{ Error: fmt.Sprintf("failed to connect to Discord: %v", err), ErrorType: scrobbler.ScrobblerErrorRetryLater, }, nil @@ -155,7 +155,7 @@ func (p *discordPlugin) NowPlaying(input scrobbler.NowPlayingRequest) (scrobbler LargeText: input.Track.Album, }, }); err != nil { - return scrobbler.ScrobblerResponse{ + return &scrobbler.ScrobblerResponse{ Error: fmt.Sprintf("failed to send activity: %v", err), ErrorType: scrobbler.ScrobblerErrorRetryLater, }, nil @@ -168,13 +168,13 @@ func (p *discordPlugin) NowPlaying(input scrobbler.NowPlayingRequest) (scrobbler pdk.Log(pdk.LogWarn, fmt.Sprintf("Failed to schedule completion timer: %v", err)) } - return scrobbler.ScrobblerResponse{}, nil + return &scrobbler.ScrobblerResponse{}, nil } // Scrobble handles scrobble requests (no-op for Discord). -func (p *discordPlugin) Scrobble(_ scrobbler.ScrobbleRequest) (scrobbler.ScrobblerResponse, error) { +func (p *discordPlugin) Scrobble(_ scrobbler.ScrobbleRequest) (*scrobbler.ScrobblerResponse, error) { // Discord Rich Presence doesn't need scrobble events - return scrobbler.ScrobblerResponse{}, nil + return &scrobbler.ScrobblerResponse{}, nil } // ============================================================================ @@ -182,7 +182,7 @@ func (p *discordPlugin) Scrobble(_ scrobbler.ScrobbleRequest) (scrobbler.Scrobbl // ============================================================================ // OnSchedulerCallback handles scheduler callbacks. -func (p *discordPlugin) OnSchedulerCallback(input scheduler.SchedulerCallbackRequest) (scheduler.SchedulerCallbackResponse, error) { +func (p *discordPlugin) OnSchedulerCallback(input scheduler.SchedulerCallbackRequest) error { pdk.Log(pdk.LogDebug, fmt.Sprintf("Scheduler callback: id=%s, payload=%s, recurring=%v", input.ScheduleID, input.Payload, input.IsRecurring)) // Route based on payload @@ -190,21 +190,21 @@ func (p *discordPlugin) OnSchedulerCallback(input scheduler.SchedulerCallbackReq case payloadHeartbeat: // Heartbeat callback - scheduleId is the username if err := handleHeartbeatCallback(input.ScheduleID); err != nil { - return scheduler.SchedulerCallbackResponse{Error: err.Error()}, nil + return err } case payloadClearActivity: // Clear activity callback - scheduleId is "username-clear" username := strings.TrimSuffix(input.ScheduleID, "-clear") if err := handleClearActivityCallback(username); err != nil { - return scheduler.SchedulerCallbackResponse{Error: err.Error()}, nil + return err } default: pdk.Log(pdk.LogWarn, fmt.Sprintf("Unknown scheduler callback payload: %s", input.Payload)) } - return scheduler.SchedulerCallbackResponse{}, nil + return nil } // ============================================================================ @@ -212,29 +212,26 @@ func (p *discordPlugin) OnSchedulerCallback(input scheduler.SchedulerCallbackReq // ============================================================================ // OnTextMessage handles incoming WebSocket text messages. -func (p *discordPlugin) OnTextMessage(input websocket.OnTextMessageRequest) (websocket.OnTextMessageResponse, error) { - if err := handleWebSocketMessage(input.ConnectionID, input.Message); err != nil { - return websocket.OnTextMessageResponse{Error: err.Error()}, nil - } - return websocket.OnTextMessageResponse{}, nil +func (p *discordPlugin) OnTextMessage(input websocket.OnTextMessageRequest) error { + return handleWebSocketMessage(input.ConnectionID, input.Message) } // OnBinaryMessage handles incoming WebSocket binary messages. -func (p *discordPlugin) OnBinaryMessage(input websocket.OnBinaryMessageRequest) (websocket.OnBinaryMessageResponse, error) { +func (p *discordPlugin) OnBinaryMessage(input websocket.OnBinaryMessageRequest) error { pdk.Log(pdk.LogDebug, fmt.Sprintf("Received unexpected binary message for connection '%s'", input.ConnectionID)) - return websocket.OnBinaryMessageResponse{}, nil + return nil } // OnError handles WebSocket errors. -func (p *discordPlugin) OnError(input websocket.OnErrorRequest) (websocket.OnErrorResponse, error) { +func (p *discordPlugin) OnError(input websocket.OnErrorRequest) error { pdk.Log(pdk.LogWarn, fmt.Sprintf("WebSocket error for connection '%s': %s", input.ConnectionID, input.Error)) - return websocket.OnErrorResponse{}, nil + return nil } // OnClose handles WebSocket connection closure. -func (p *discordPlugin) OnClose(input websocket.OnCloseRequest) (websocket.OnCloseResponse, error) { +func (p *discordPlugin) OnClose(input websocket.OnCloseRequest) error { pdk.Log(pdk.LogInfo, fmt.Sprintf("WebSocket connection '%s' closed with code %d: %s", input.ConnectionID, input.Code, input.Reason)) - return websocket.OnCloseResponse{}, nil + return nil } func main() {} diff --git a/plugins/examples/nowplaying-py/plugin/__init__.py b/plugins/examples/nowplaying-py/plugin/__init__.py index bb8e132b2..f7453fdb7 100644 --- a/plugins/examples/nowplaying-py/plugin/__init__.py +++ b/plugins/examples/nowplaying-py/plugin/__init__.py @@ -117,9 +117,7 @@ def nd_on_init(): except Exception as e: extism.log(extism.LogLevel.Error, f"Failed to schedule task: {e}") raise - - # Return empty success response - extism.output_str(json.dumps({})) + # No output - lifecycle callbacks don't return responses @extism.plugin_fn @@ -130,7 +128,6 @@ def nd_scheduler_callback(): # Only handle our schedule if schedule_id != SCHEDULE_ID: - extism.output_str(json.dumps({})) return try: @@ -164,10 +161,8 @@ def nd_scheduler_callback(): extism.LogLevel.Info, f"🎵 {username} is playing: {artist} - {title} ({album})" ) - - extism.output_str(json.dumps({})) + # No output - scheduler callbacks don't return responses except Exception as e: - error_msg = str(e) - extism.log(extism.LogLevel.Error, f"Failed to get now playing: {error_msg}") - extism.output_str(json.dumps({"error": error_msg})) + extism.log(extism.LogLevel.Error, f"Failed to get now playing: {e}") + # Errors are logged but scheduler callbacks don't return responses diff --git a/plugins/host_scheduler.go b/plugins/host_scheduler.go index 340a36987..e7c97c271 100644 --- a/plugins/host_scheduler.go +++ b/plugins/host_scheduler.go @@ -202,17 +202,12 @@ func (s *schedulerServiceImpl) invokeCallback(ctx context.Context, scheduleID st } start := time.Now() - result, err := callPluginFunction[capabilities.SchedulerCallbackRequest, capabilities.SchedulerCallbackResponse](ctx, instance, FuncSchedulerCallback, input) + err := callPluginFunctionNoOutput(ctx, instance, FuncSchedulerCallback, input) if err != nil { log.Error(ctx, "Scheduler callback failed", "plugin", s.pluginName, "scheduleID", scheduleID, "duration", time.Since(start), err) return } - if result.Error != "" { - log.Error(ctx, "Scheduler callback returned error", "plugin", s.pluginName, "scheduleID", scheduleID, "error", result.Error, "duration", time.Since(start)) - return - } - log.Debug(ctx, "Scheduler callback completed", "plugin", s.pluginName, "scheduleID", scheduleID, "duration", time.Since(start)) } diff --git a/plugins/host_websocket.go b/plugins/host_websocket.go index d89211735..64bdee484 100644 --- a/plugins/host_websocket.go +++ b/plugins/host_websocket.go @@ -330,7 +330,7 @@ func (s *webSocketServiceImpl) invokeOnTextMessage(ctx context.Context, connecti } start := time.Now() - _, err := callPluginFunction[capabilities.OnTextMessageRequest, capabilities.OnTextMessageResponse](ctx, instance, FuncWebSocketOnTextMessage, input) + err := callPluginFunctionNoOutput(ctx, instance, FuncWebSocketOnTextMessage, input) if err != nil { // Don't log error if function simply doesn't exist (optional callback) if !errors.Is(errFunctionNotFound, err) { @@ -351,7 +351,7 @@ func (s *webSocketServiceImpl) invokeOnBinaryMessage(ctx context.Context, connec } start := time.Now() - _, err := callPluginFunction[capabilities.OnBinaryMessageRequest, capabilities.OnBinaryMessageResponse](ctx, instance, FuncWebSocketOnBinaryMessage, input) + err := callPluginFunctionNoOutput(ctx, instance, FuncWebSocketOnBinaryMessage, input) if err != nil { // Don't log error if function simply doesn't exist (optional callback) if !errors.Is(errFunctionNotFound, err) { @@ -372,7 +372,7 @@ func (s *webSocketServiceImpl) invokeOnError(ctx context.Context, connectionID, } start := time.Now() - _, err := callPluginFunction[capabilities.OnErrorRequest, capabilities.OnErrorResponse](ctx, instance, FuncWebSocketOnError, input) + err := callPluginFunctionNoOutput(ctx, instance, FuncWebSocketOnError, input) if err != nil { // Don't log error if function simply doesn't exist (optional callback) if !errors.Is(errFunctionNotFound, err) { @@ -394,7 +394,7 @@ func (s *webSocketServiceImpl) invokeOnClose(ctx context.Context, connectionID s } start := time.Now() - _, err := callPluginFunction[capabilities.OnCloseRequest, capabilities.OnCloseResponse](ctx, instance, FuncWebSocketOnClose, input) + err := callPluginFunctionNoOutput(ctx, instance, FuncWebSocketOnClose, input) if err != nil { // Don't log error if function simply doesn't exist (optional callback) if !errors.Is(errFunctionNotFound, err) { diff --git a/plugins/manager_call.go b/plugins/manager_call.go index 1fa701d0a..63f0f0ce0 100644 --- a/plugins/manager_call.go +++ b/plugins/manager_call.go @@ -13,6 +13,73 @@ import ( var errFunctionNotFound = errors.New("function not found") +// callPluginFunctionNoInput calls a plugin function that takes no input. +// It only checks for errors, with no response expected. +func callPluginFunctionNoInput(ctx context.Context, plugin *plugin, funcName string) error { + start := time.Now() + + // Create plugin instance + p, err := plugin.instance() + if err != nil { + return fmt.Errorf("failed to create plugin: %w", err) + } + defer p.Close(ctx) + + if !p.FunctionExists(funcName) { + log.Trace(ctx, "Plugin function not found", "plugin", plugin.name, "function", funcName) + return fmt.Errorf("%w: %s", errFunctionNotFound, funcName) + } + + startCall := time.Now() + exit, _, err := p.Call(funcName, nil) + if err != nil { + log.Trace(ctx, "Plugin call failed", "plugin", plugin.name, "function", funcName, "pluginDuration", time.Since(startCall), "navidromeDuration", startCall.Sub(start), err) + return fmt.Errorf("plugin call failed: %w", err) + } + if exit != 0 { + return fmt.Errorf("plugin call exited with code %d", exit) + } + + log.Trace(ctx, "Plugin call succeeded", "plugin", plugin.name, "function", funcName, "pluginDuration", time.Since(startCall), "navidromeDuration", startCall.Sub(start)) + return nil +} + +// callPluginFunctionNoOutput calls a plugin function with input but no response expected. +// It handles JSON marshalling for input and only checks for errors. +func callPluginFunctionNoOutput[I any](ctx context.Context, plugin *plugin, funcName string, input I) error { + start := time.Now() + + // Create plugin instance + p, err := plugin.instance() + if err != nil { + return fmt.Errorf("failed to create plugin: %w", err) + } + defer p.Close(ctx) + + if !p.FunctionExists(funcName) { + log.Trace(ctx, "Plugin function not found", "plugin", plugin.name, "function", funcName) + return fmt.Errorf("%w: %s", errFunctionNotFound, funcName) + } + + inputBytes, err := json.Marshal(input) + if err != nil { + return fmt.Errorf("failed to marshal input: %w", err) + } + + startCall := time.Now() + exit, _, err := p.Call(funcName, inputBytes) + if err != nil { + log.Trace(ctx, "Plugin call failed", "plugin", plugin.name, "function", funcName, "pluginDuration", time.Since(startCall), "navidromeDuration", startCall.Sub(start), err) + return fmt.Errorf("plugin call failed: %w", err) + } + if exit != 0 { + return fmt.Errorf("plugin call exited with code %d", exit) + } + + log.Trace(ctx, "Plugin call succeeded", "plugin", plugin.name, "function", funcName, "pluginDuration", time.Since(startCall), "navidromeDuration", startCall.Sub(start)) + return nil +} + // callPluginFunction is a helper to call a plugin function with input and output types. // It handles JSON marshalling/unmarshalling and error checking. func callPluginFunction[I any, O any](ctx context.Context, plugin *plugin, funcName string, input I) (O, error) { diff --git a/plugins/pdk/go/lifecycle/lifecycle.go b/plugins/pdk/go/lifecycle/lifecycle.go index a6fdcb6c7..271603083 100644 --- a/plugins/pdk/go/lifecycle/lifecycle.go +++ b/plugins/pdk/go/lifecycle/lifecycle.go @@ -11,19 +11,6 @@ import ( pdk "github.com/extism/go-pdk" ) -// InitRequest is the request provided to the init callback. -// Currently empty, reserved for future use. -type InitRequest struct { -} - -// InitResponse is the response from the init callback. -type InitResponse struct { - // Error is the error message if initialization failed. - // Empty string 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. @@ -38,10 +25,10 @@ type Lifecycle interface{} // InitProvider provides the OnInit function. type InitProvider interface { - OnInit(InitRequest) (InitResponse, error) + OnInit() error } // Internal implementation holders var ( - initImpl func(InitRequest) (InitResponse, error) + initImpl func() error ) // Register registers a lifecycle implementation. @@ -63,19 +50,7 @@ func _NdOnInit() int32 { return NotImplementedCode } - var input InitRequest - 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 { + if err := initImpl(); err != nil { pdk.SetError(err) return -1 } diff --git a/plugins/pdk/go/lifecycle/lifecycle_stub.go b/plugins/pdk/go/lifecycle/lifecycle_stub.go index 30c2b6c54..8d392f6c6 100644 --- a/plugins/pdk/go/lifecycle/lifecycle_stub.go +++ b/plugins/pdk/go/lifecycle/lifecycle_stub.go @@ -8,19 +8,6 @@ package lifecycle -// InitRequest is the request provided to the init callback. -// Currently empty, reserved for future use. -type InitRequest struct { -} - -// InitResponse is the response from the init callback. -type InitResponse struct { - // Error is the error message if initialization failed. - // Empty string 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. @@ -35,7 +22,7 @@ type Lifecycle interface{} // InitProvider provides the OnInit function. type InitProvider interface { - OnInit(InitRequest) (InitResponse, error) + OnInit() error } // NotImplementedCode is the standard return code for unimplemented functions. diff --git a/plugins/pdk/go/metadata/metadata.go b/plugins/pdk/go/metadata/metadata.go index f9efd7a3d..1a5ab2b14 100644 --- a/plugins/pdk/go/metadata/metadata.go +++ b/plugins/pdk/go/metadata/metadata.go @@ -11,108 +11,6 @@ import ( pdk "github.com/extism/go-pdk" ) -// ArtistRequest is the common request for artist-related functions. -type ArtistRequest 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"` -} - -// ArtistBiographyResponse is the response for GetArtistBiography. -type ArtistBiographyResponse struct { - // Biography is the artist biography text. - Biography string `json:"biography"` -} - -// AlbumInfoResponse is the response for GetAlbumInfo. -type AlbumInfoResponse 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"` -} - -// ArtistMBIDRequest is the request for GetArtistMBID. -type ArtistMBIDRequest struct { - // ID is the internal Navidrome artist ID. - ID string `json:"id"` - // Name is the artist name. - Name string `json:"name"` -} - -// TopSongsResponse is the response for GetArtistTopSongs. -type TopSongsResponse struct { - // Songs is the list of top songs. - Songs []SongRef `json:"songs"` -} - -// 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"` -} - -// ArtistMBIDResponse is the response for GetArtistMBID. -type ArtistMBIDResponse struct { - // MBID is the MusicBrainz ID for the artist. - MBID string `json:"mbid"` -} - -// SimilarArtistsResponse is the response for GetSimilarArtists. -type SimilarArtistsResponse struct { - // Artists is the list of similar artists. - Artists []ArtistRef `json:"artists"` -} - -// 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"` -} - -// 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"` -} - -// ArtistURLResponse is the response for GetArtistURL. -type ArtistURLResponse struct { - // URL is the external URL for the artist. - URL string `json:"url"` -} - -// SimilarArtistsRequest is the request for GetSimilarArtists. -type SimilarArtistsRequest 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"` -} - -// ArtistImagesResponse is the response for GetArtistImages. -type ArtistImagesResponse struct { - // Images is the list of artist images. - Images []ImageInfo `json:"images"` -} - // TopSongsRequest is the request for GetArtistTopSongs. type TopSongsRequest struct { // ID is the internal Navidrome artist ID. @@ -141,6 +39,108 @@ type AlbumImagesResponse struct { Images []ImageInfo `json:"images"` } +// 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"` +} + +// ArtistRequest is the common request for artist-related functions. +type ArtistRequest 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"` +} + +// ArtistBiographyResponse is the response for GetArtistBiography. +type ArtistBiographyResponse struct { + // Biography is the artist biography text. + Biography string `json:"biography"` +} + +// SimilarArtistsRequest is the request for GetSimilarArtists. +type SimilarArtistsRequest 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"` +} + +// TopSongsResponse is the response for GetArtistTopSongs. +type TopSongsResponse struct { + // Songs is the list of top songs. + Songs []SongRef `json:"songs"` +} + +// AlbumInfoResponse is the response for GetAlbumInfo. +type AlbumInfoResponse 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"` +} + +// 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"` +} + +// ArtistURLResponse is the response for GetArtistURL. +type ArtistURLResponse struct { + // URL is the external URL for the artist. + URL string `json:"url"` +} + +// SimilarArtistsResponse is the response for GetSimilarArtists. +type SimilarArtistsResponse struct { + // Artists is the list of similar artists. + Artists []ArtistRef `json:"artists"` +} + +// ArtistImagesResponse is the response for GetArtistImages. +type ArtistImagesResponse struct { + // Images is the list of artist images. + Images []ImageInfo `json:"images"` +} + +// 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"` +} + +// ArtistMBIDRequest is the request for GetArtistMBID. +type ArtistMBIDRequest struct { + // ID is the internal Navidrome artist ID. + ID string `json:"id"` + // Name is the artist name. + Name string `json:"name"` +} + +// ArtistMBIDResponse is the response for GetArtistMBID. +type ArtistMBIDResponse struct { + // MBID is the MusicBrainz ID for the artist. + MBID string `json:"mbid"` +} + // Metadata is the marker interface for metadata plugins. // Implement one or more of the provider interfaces below. // MetadataAgent provides artist and album metadata retrieval. diff --git a/plugins/pdk/go/metadata/metadata_stub.go b/plugins/pdk/go/metadata/metadata_stub.go index 8dc348c38..87bf45f7b 100644 --- a/plugins/pdk/go/metadata/metadata_stub.go +++ b/plugins/pdk/go/metadata/metadata_stub.go @@ -8,108 +8,6 @@ package metadata -// ArtistRequest is the common request for artist-related functions. -type ArtistRequest 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"` -} - -// ArtistBiographyResponse is the response for GetArtistBiography. -type ArtistBiographyResponse struct { - // Biography is the artist biography text. - Biography string `json:"biography"` -} - -// AlbumInfoResponse is the response for GetAlbumInfo. -type AlbumInfoResponse 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"` -} - -// ArtistMBIDRequest is the request for GetArtistMBID. -type ArtistMBIDRequest struct { - // ID is the internal Navidrome artist ID. - ID string `json:"id"` - // Name is the artist name. - Name string `json:"name"` -} - -// TopSongsResponse is the response for GetArtistTopSongs. -type TopSongsResponse struct { - // Songs is the list of top songs. - Songs []SongRef `json:"songs"` -} - -// 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"` -} - -// ArtistMBIDResponse is the response for GetArtistMBID. -type ArtistMBIDResponse struct { - // MBID is the MusicBrainz ID for the artist. - MBID string `json:"mbid"` -} - -// SimilarArtistsResponse is the response for GetSimilarArtists. -type SimilarArtistsResponse struct { - // Artists is the list of similar artists. - Artists []ArtistRef `json:"artists"` -} - -// 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"` -} - -// 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"` -} - -// ArtistURLResponse is the response for GetArtistURL. -type ArtistURLResponse struct { - // URL is the external URL for the artist. - URL string `json:"url"` -} - -// SimilarArtistsRequest is the request for GetSimilarArtists. -type SimilarArtistsRequest 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"` -} - -// ArtistImagesResponse is the response for GetArtistImages. -type ArtistImagesResponse struct { - // Images is the list of artist images. - Images []ImageInfo `json:"images"` -} - // TopSongsRequest is the request for GetArtistTopSongs. type TopSongsRequest struct { // ID is the internal Navidrome artist ID. @@ -138,6 +36,108 @@ type AlbumImagesResponse struct { Images []ImageInfo `json:"images"` } +// 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"` +} + +// ArtistRequest is the common request for artist-related functions. +type ArtistRequest 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"` +} + +// ArtistBiographyResponse is the response for GetArtistBiography. +type ArtistBiographyResponse struct { + // Biography is the artist biography text. + Biography string `json:"biography"` +} + +// SimilarArtistsRequest is the request for GetSimilarArtists. +type SimilarArtistsRequest 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"` +} + +// TopSongsResponse is the response for GetArtistTopSongs. +type TopSongsResponse struct { + // Songs is the list of top songs. + Songs []SongRef `json:"songs"` +} + +// AlbumInfoResponse is the response for GetAlbumInfo. +type AlbumInfoResponse 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"` +} + +// 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"` +} + +// ArtistURLResponse is the response for GetArtistURL. +type ArtistURLResponse struct { + // URL is the external URL for the artist. + URL string `json:"url"` +} + +// SimilarArtistsResponse is the response for GetSimilarArtists. +type SimilarArtistsResponse struct { + // Artists is the list of similar artists. + Artists []ArtistRef `json:"artists"` +} + +// ArtistImagesResponse is the response for GetArtistImages. +type ArtistImagesResponse struct { + // Images is the list of artist images. + Images []ImageInfo `json:"images"` +} + +// 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"` +} + +// ArtistMBIDRequest is the request for GetArtistMBID. +type ArtistMBIDRequest struct { + // ID is the internal Navidrome artist ID. + ID string `json:"id"` + // Name is the artist name. + Name string `json:"name"` +} + +// ArtistMBIDResponse is the response for GetArtistMBID. +type ArtistMBIDResponse struct { + // MBID is the MusicBrainz ID for the artist. + MBID string `json:"mbid"` +} + // Metadata is the marker interface for metadata plugins. // Implement one or more of the provider interfaces below. // MetadataAgent provides artist and album metadata retrieval. diff --git a/plugins/pdk/go/scheduler/scheduler.go b/plugins/pdk/go/scheduler/scheduler.go index 6d0a44a66..862c044de 100644 --- a/plugins/pdk/go/scheduler/scheduler.go +++ b/plugins/pdk/go/scheduler/scheduler.go @@ -24,14 +24,6 @@ type SchedulerCallbackRequest struct { IsRecurring bool `json:"isRecurring"` } -// SchedulerCallbackResponse is the response from the scheduler callback. -type SchedulerCallbackResponse struct { - // Error is the error message if the callback failed to process the scheduled task. - // Empty string indicates success. The error is logged but does not - // affect the scheduling system. - Error string `json:"error,omitempty"` -} - // Scheduler is the marker interface for scheduler plugins. // Implement one or more of the provider interfaces below. // SchedulerCallback provides scheduled task handling. @@ -42,10 +34,10 @@ type Scheduler interface{} // SchedulerCallbackProvider provides the OnSchedulerCallback function. type SchedulerCallbackProvider interface { - OnSchedulerCallback(SchedulerCallbackRequest) (SchedulerCallbackResponse, error) + OnSchedulerCallback(SchedulerCallbackRequest) error } // Internal implementation holders var ( - schedulerCallbackImpl func(SchedulerCallbackRequest) (SchedulerCallbackResponse, error) + schedulerCallbackImpl func(SchedulerCallbackRequest) error ) // Register registers a scheduler implementation. @@ -73,13 +65,7 @@ func _NdSchedulerCallback() int32 { return -1 } - output, err := schedulerCallbackImpl(input) - if err != nil { - pdk.SetError(err) - return -1 - } - - if err := pdk.OutputJSON(output); err != nil { + if err := schedulerCallbackImpl(input); err != nil { pdk.SetError(err) return -1 } diff --git a/plugins/pdk/go/scheduler/scheduler_stub.go b/plugins/pdk/go/scheduler/scheduler_stub.go index 013540e6d..c07a0d28a 100644 --- a/plugins/pdk/go/scheduler/scheduler_stub.go +++ b/plugins/pdk/go/scheduler/scheduler_stub.go @@ -21,14 +21,6 @@ type SchedulerCallbackRequest struct { IsRecurring bool `json:"isRecurring"` } -// SchedulerCallbackResponse is the response from the scheduler callback. -type SchedulerCallbackResponse struct { - // Error is the error message if the callback failed to process the scheduled task. - // Empty string indicates success. The error is logged but does not - // affect the scheduling system. - Error string `json:"error,omitempty"` -} - // Scheduler is the marker interface for scheduler plugins. // Implement one or more of the provider interfaces below. // SchedulerCallback provides scheduled task handling. @@ -39,7 +31,7 @@ type Scheduler interface{} // SchedulerCallbackProvider provides the OnSchedulerCallback function. type SchedulerCallbackProvider interface { - OnSchedulerCallback(SchedulerCallbackRequest) (SchedulerCallbackResponse, error) + OnSchedulerCallback(SchedulerCallbackRequest) error } // NotImplementedCode is the standard return code for unimplemented functions. diff --git a/plugins/pdk/go/scrobbler/scrobbler.go b/plugins/pdk/go/scrobbler/scrobbler.go index 8b04e2bdf..d6ffd71d3 100644 --- a/plugins/pdk/go/scrobbler/scrobbler.go +++ b/plugins/pdk/go/scrobbler/scrobbler.go @@ -112,16 +112,16 @@ type TrackInfo struct { // all three functions: IsAuthorized, NowPlaying, and Scrobble. type Scrobbler interface { // IsAuthorized - IsAuthorized checks if a user is authorized to scrobble to this service. - IsAuthorized(IsAuthorizedRequest) (IsAuthorizedResponse, error) + IsAuthorized(IsAuthorizedRequest) (*IsAuthorizedResponse, error) // NowPlaying - NowPlaying sends a now playing notification to the scrobbling service. - NowPlaying(NowPlayingRequest) (ScrobblerResponse, error) + NowPlaying(NowPlayingRequest) (*ScrobblerResponse, error) // Scrobble - Scrobble submits a completed scrobble to the scrobbling service. - Scrobble(ScrobbleRequest) (ScrobblerResponse, error) + Scrobble(ScrobbleRequest) (*ScrobblerResponse, error) } // Internal implementation holders var ( - isAuthorizedImpl func(IsAuthorizedRequest) (IsAuthorizedResponse, error) - nowPlayingImpl func(NowPlayingRequest) (ScrobblerResponse, error) - scrobbleImpl func(ScrobbleRequest) (ScrobblerResponse, error) + isAuthorizedImpl func(IsAuthorizedRequest) (*IsAuthorizedResponse, error) + nowPlayingImpl func(NowPlayingRequest) (*ScrobblerResponse, error) + scrobbleImpl func(ScrobbleRequest) (*ScrobblerResponse, error) ) // Register registers a scrobbler implementation. diff --git a/plugins/pdk/go/scrobbler/scrobbler_stub.go b/plugins/pdk/go/scrobbler/scrobbler_stub.go index e5b92961b..1f64aa891 100644 --- a/plugins/pdk/go/scrobbler/scrobbler_stub.go +++ b/plugins/pdk/go/scrobbler/scrobbler_stub.go @@ -109,11 +109,11 @@ type TrackInfo struct { // all three functions: IsAuthorized, NowPlaying, and Scrobble. type Scrobbler interface { // IsAuthorized - IsAuthorized checks if a user is authorized to scrobble to this service. - IsAuthorized(IsAuthorizedRequest) (IsAuthorizedResponse, error) + IsAuthorized(IsAuthorizedRequest) (*IsAuthorizedResponse, error) // NowPlaying - NowPlaying sends a now playing notification to the scrobbling service. - NowPlaying(NowPlayingRequest) (ScrobblerResponse, error) + NowPlaying(NowPlayingRequest) (*ScrobblerResponse, error) // Scrobble - Scrobble submits a completed scrobble to the scrobbling service. - Scrobble(ScrobbleRequest) (ScrobblerResponse, error) + Scrobble(ScrobbleRequest) (*ScrobblerResponse, error) } // NotImplementedCode is the standard return code for unimplemented functions. diff --git a/plugins/pdk/go/websocket/websocket.go b/plugins/pdk/go/websocket/websocket.go index b17fa3f42..bcaed3126 100644 --- a/plugins/pdk/go/websocket/websocket.go +++ b/plugins/pdk/go/websocket/websocket.go @@ -11,11 +11,12 @@ import ( pdk "github.com/extism/go-pdk" ) -// OnErrorResponse is the response from the error handler. -type OnErrorResponse struct { - // Error is the error message if the callback failed. - // Empty string indicates success. - Error string `json:"error,omitempty"` +// OnErrorRequest is the request provided when an error occurs on a WebSocket connection. +type OnErrorRequest 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"` } // OnCloseRequest is the request provided when a WebSocket connection is closed. @@ -29,13 +30,6 @@ type OnCloseRequest struct { Reason string `json:"reason"` } -// OnCloseResponse is the response from the close handler. -type OnCloseResponse struct { - // Error is the error message if the callback failed. - // Empty string indicates success. - Error string `json:"error,omitempty"` -} - // OnTextMessageRequest is the request provided when a text message is received. type OnTextMessageRequest struct { // ConnectionID is the unique identifier for the WebSocket connection that received the message. @@ -44,13 +38,6 @@ type OnTextMessageRequest struct { Message string `json:"message"` } -// OnTextMessageResponse is the response from the text message handler. -type OnTextMessageResponse struct { - // Error is the error message if the callback failed. - // Empty string indicates success. - Error string `json:"error,omitempty"` -} - // OnBinaryMessageRequest is the request provided when a binary message is received. type OnBinaryMessageRequest struct { // ConnectionID is the unique identifier for the WebSocket connection that received the message. @@ -59,21 +46,6 @@ type OnBinaryMessageRequest struct { Data string `json:"data"` } -// OnBinaryMessageResponse is the response from the binary message handler. -type OnBinaryMessageResponse struct { - // Error is the error message if the callback failed. - // Empty string indicates success. - Error string `json:"error,omitempty"` -} - -// OnErrorRequest is the request provided when an error occurs on a WebSocket connection. -type OnErrorRequest 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"` -} - // WebSocket is the marker interface for websocket plugins. // Implement one or more of the provider interfaces below. // WebSocketCallback provides WebSocket message handling. @@ -85,28 +57,28 @@ type WebSocket interface{} // TextMessageProvider provides the OnTextMessage function. type TextMessageProvider interface { - OnTextMessage(OnTextMessageRequest) (OnTextMessageResponse, error) + OnTextMessage(OnTextMessageRequest) error } // BinaryMessageProvider provides the OnBinaryMessage function. type BinaryMessageProvider interface { - OnBinaryMessage(OnBinaryMessageRequest) (OnBinaryMessageResponse, error) + OnBinaryMessage(OnBinaryMessageRequest) error } // ErrorProvider provides the OnError function. type ErrorProvider interface { - OnError(OnErrorRequest) (OnErrorResponse, error) + OnError(OnErrorRequest) error } // CloseProvider provides the OnClose function. type CloseProvider interface { - OnClose(OnCloseRequest) (OnCloseResponse, error) + OnClose(OnCloseRequest) error } // Internal implementation holders var ( - textMessageImpl func(OnTextMessageRequest) (OnTextMessageResponse, error) - binaryMessageImpl func(OnBinaryMessageRequest) (OnBinaryMessageResponse, error) - errorImpl func(OnErrorRequest) (OnErrorResponse, error) - closeImpl func(OnCloseRequest) (OnCloseResponse, error) + textMessageImpl func(OnTextMessageRequest) error + binaryMessageImpl func(OnBinaryMessageRequest) error + errorImpl func(OnErrorRequest) error + closeImpl func(OnCloseRequest) error ) // Register registers a websocket implementation. @@ -143,13 +115,7 @@ func _NdWebsocketOnTextMessage() int32 { return -1 } - output, err := textMessageImpl(input) - if err != nil { - pdk.SetError(err) - return -1 - } - - if err := pdk.OutputJSON(output); err != nil { + if err := textMessageImpl(input); err != nil { pdk.SetError(err) return -1 } @@ -170,13 +136,7 @@ func _NdWebsocketOnBinaryMessage() int32 { return -1 } - output, err := binaryMessageImpl(input) - if err != nil { - pdk.SetError(err) - return -1 - } - - if err := pdk.OutputJSON(output); err != nil { + if err := binaryMessageImpl(input); err != nil { pdk.SetError(err) return -1 } @@ -197,13 +157,7 @@ func _NdWebsocketOnError() int32 { return -1 } - output, err := errorImpl(input) - if err != nil { - pdk.SetError(err) - return -1 - } - - if err := pdk.OutputJSON(output); err != nil { + if err := errorImpl(input); err != nil { pdk.SetError(err) return -1 } @@ -224,13 +178,7 @@ func _NdWebsocketOnClose() int32 { return -1 } - output, err := closeImpl(input) - if err != nil { - pdk.SetError(err) - return -1 - } - - if err := pdk.OutputJSON(output); err != nil { + if err := closeImpl(input); err != nil { pdk.SetError(err) return -1 } diff --git a/plugins/pdk/go/websocket/websocket_stub.go b/plugins/pdk/go/websocket/websocket_stub.go index 987c2f5d2..487f0dd26 100644 --- a/plugins/pdk/go/websocket/websocket_stub.go +++ b/plugins/pdk/go/websocket/websocket_stub.go @@ -8,11 +8,12 @@ package websocket -// OnErrorResponse is the response from the error handler. -type OnErrorResponse struct { - // Error is the error message if the callback failed. - // Empty string indicates success. - Error string `json:"error,omitempty"` +// OnErrorRequest is the request provided when an error occurs on a WebSocket connection. +type OnErrorRequest 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"` } // OnCloseRequest is the request provided when a WebSocket connection is closed. @@ -26,13 +27,6 @@ type OnCloseRequest struct { Reason string `json:"reason"` } -// OnCloseResponse is the response from the close handler. -type OnCloseResponse struct { - // Error is the error message if the callback failed. - // Empty string indicates success. - Error string `json:"error,omitempty"` -} - // OnTextMessageRequest is the request provided when a text message is received. type OnTextMessageRequest struct { // ConnectionID is the unique identifier for the WebSocket connection that received the message. @@ -41,13 +35,6 @@ type OnTextMessageRequest struct { Message string `json:"message"` } -// OnTextMessageResponse is the response from the text message handler. -type OnTextMessageResponse struct { - // Error is the error message if the callback failed. - // Empty string indicates success. - Error string `json:"error,omitempty"` -} - // OnBinaryMessageRequest is the request provided when a binary message is received. type OnBinaryMessageRequest struct { // ConnectionID is the unique identifier for the WebSocket connection that received the message. @@ -56,21 +43,6 @@ type OnBinaryMessageRequest struct { Data string `json:"data"` } -// OnBinaryMessageResponse is the response from the binary message handler. -type OnBinaryMessageResponse struct { - // Error is the error message if the callback failed. - // Empty string indicates success. - Error string `json:"error,omitempty"` -} - -// OnErrorRequest is the request provided when an error occurs on a WebSocket connection. -type OnErrorRequest 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"` -} - // WebSocket is the marker interface for websocket plugins. // Implement one or more of the provider interfaces below. // WebSocketCallback provides WebSocket message handling. @@ -82,22 +54,22 @@ type WebSocket interface{} // TextMessageProvider provides the OnTextMessage function. type TextMessageProvider interface { - OnTextMessage(OnTextMessageRequest) (OnTextMessageResponse, error) + OnTextMessage(OnTextMessageRequest) error } // BinaryMessageProvider provides the OnBinaryMessage function. type BinaryMessageProvider interface { - OnBinaryMessage(OnBinaryMessageRequest) (OnBinaryMessageResponse, error) + OnBinaryMessage(OnBinaryMessageRequest) error } // ErrorProvider provides the OnError function. type ErrorProvider interface { - OnError(OnErrorRequest) (OnErrorResponse, error) + OnError(OnErrorRequest) error } // CloseProvider provides the OnClose function. type CloseProvider interface { - OnClose(OnCloseRequest) (OnCloseResponse, error) + OnClose(OnCloseRequest) error } // NotImplementedCode is the standard return code for unimplemented functions. diff --git a/plugins/pdk/rust/host/README.md b/plugins/pdk/rust/host/README.md index 45463575d..f722b2e5a 100644 --- a/plugins/pdk/rust/host/README.md +++ b/plugins/pdk/rust/host/README.md @@ -10,7 +10,7 @@ These wrappers provide idiomatic Rust APIs for interacting with Navidrome from W To regenerate: ```bash -make generate-pdk +make gen ``` ## Usage diff --git a/plugins/scrobbler_adapter.go b/plugins/scrobbler_adapter.go index 37aa21d7c..4753f3784 100644 --- a/plugins/scrobbler_adapter.go +++ b/plugins/scrobbler_adapter.go @@ -45,8 +45,8 @@ func (s *ScrobblerPlugin) IsAuthorized(ctx context.Context, userId string) bool Username: username, } - result, err := callPluginFunction[capabilities.IsAuthorizedRequest, capabilities.IsAuthorizedResponse](ctx, s.plugin, FuncScrobblerIsAuthorized, input) - if err != nil { + result, err := callPluginFunction[capabilities.IsAuthorizedRequest, *capabilities.IsAuthorizedResponse](ctx, s.plugin, FuncScrobblerIsAuthorized, input) + if err != nil || result == nil { return false } @@ -63,7 +63,7 @@ func (s *ScrobblerPlugin) NowPlaying(ctx context.Context, userId string, track * Position: int32(position), } - result, err := callPluginFunction[capabilities.NowPlayingRequest, capabilities.ScrobblerResponse](ctx, s.plugin, FuncScrobblerNowPlaying, input) + result, err := callPluginFunction[capabilities.NowPlayingRequest, *capabilities.ScrobblerResponse](ctx, s.plugin, FuncScrobblerNowPlaying, input) if err != nil { return err } @@ -81,7 +81,7 @@ func (s *ScrobblerPlugin) Scrobble(ctx context.Context, userId string, sc scrobb Timestamp: sc.TimeStamp.Unix(), } - result, err := callPluginFunction[capabilities.ScrobbleRequest, capabilities.ScrobblerResponse](ctx, s.plugin, FuncScrobblerScrobble, input) + result, err := callPluginFunction[capabilities.ScrobbleRequest, *capabilities.ScrobblerResponse](ctx, s.plugin, FuncScrobblerScrobble, input) if err != nil { return err } @@ -118,7 +118,10 @@ func mediaFileToTrackInfo(mf *model.MediaFile) capabilities.TrackInfo { } // mapScrobblerError converts the plugin output error to a scrobbler error -func mapScrobblerError(output capabilities.ScrobblerResponse) error { +func mapScrobblerError(output *capabilities.ScrobblerResponse) error { + if output == nil { + return nil + } switch output.ErrorType { case capabilities.ScrobblerErrorNone, "": return nil diff --git a/plugins/scrobbler_adapter_test.go b/plugins/scrobbler_adapter_test.go index f1dc4bcd4..81adc0221 100644 --- a/plugins/scrobbler_adapter_test.go +++ b/plugins/scrobbler_adapter_test.go @@ -170,43 +170,47 @@ var _ = Describe("ScrobblerPlugin", Ordered, func() { }) var _ = Describe("mapScrobblerError", func() { + It("returns nil for nil output", func() { + Expect(mapScrobblerError(nil)).ToNot(HaveOccurred()) + }) + It("returns nil for empty error type", func() { - output := capabilities.ScrobblerResponse{ErrorType: ""} + output := &capabilities.ScrobblerResponse{ErrorType: ""} Expect(mapScrobblerError(output)).ToNot(HaveOccurred()) }) It("returns nil for 'none' error type", func() { - output := capabilities.ScrobblerResponse{ErrorType: capabilities.ScrobblerErrorNone} + output := &capabilities.ScrobblerResponse{ErrorType: capabilities.ScrobblerErrorNone} Expect(mapScrobblerError(output)).ToNot(HaveOccurred()) }) It("returns ErrNotAuthorized for 'not_authorized' error type", func() { - output := capabilities.ScrobblerResponse{ErrorType: capabilities.ScrobblerErrorNotAuthorized} + output := &capabilities.ScrobblerResponse{ErrorType: capabilities.ScrobblerErrorNotAuthorized} err := mapScrobblerError(output) Expect(err).To(MatchError(scrobbler.ErrNotAuthorized)) }) It("returns ErrNotAuthorized with message", func() { - output := capabilities.ScrobblerResponse{ErrorType: capabilities.ScrobblerErrorNotAuthorized, Error: "user not linked"} + output := &capabilities.ScrobblerResponse{ErrorType: capabilities.ScrobblerErrorNotAuthorized, Error: "user not linked"} err := mapScrobblerError(output) Expect(err).To(MatchError(ContainSubstring("not authorized"))) Expect(err).To(MatchError(ContainSubstring("user not linked"))) }) It("returns ErrRetryLater for 'retry_later' error type", func() { - output := capabilities.ScrobblerResponse{ErrorType: capabilities.ScrobblerErrorRetryLater} + output := &capabilities.ScrobblerResponse{ErrorType: capabilities.ScrobblerErrorRetryLater} err := mapScrobblerError(output) Expect(err).To(MatchError(scrobbler.ErrRetryLater)) }) It("returns ErrUnrecoverable for 'unrecoverable' error type", func() { - output := capabilities.ScrobblerResponse{ErrorType: capabilities.ScrobblerErrorUnrecoverable} + output := &capabilities.ScrobblerResponse{ErrorType: capabilities.ScrobblerErrorUnrecoverable} err := mapScrobblerError(output) Expect(err).To(MatchError(scrobbler.ErrUnrecoverable)) }) It("returns error for unknown error type", func() { - output := capabilities.ScrobblerResponse{ErrorType: "unknown"} + output := &capabilities.ScrobblerResponse{ErrorType: "unknown"} err := mapScrobblerError(output) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("unknown error type")) diff --git a/plugins/testdata/test-scheduler/main.go b/plugins/testdata/test-scheduler/main.go index 52837e8ec..aaa2be2b5 100644 --- a/plugins/testdata/test-scheduler/main.go +++ b/plugins/testdata/test-scheduler/main.go @@ -7,29 +7,23 @@ package main // - "schedule-followup": schedules a one-time task via host function // - "schedule-recurring": schedules a recurring task via host function // - "schedule-duplicate:": attempts to schedule with the given ID (for testing duplicate detection) -func NdSchedulerCallback(input SchedulerCallbackInput) (SchedulerCallbackOutput, error) { +func NdSchedulerCallback(input SchedulerCallbackInput) error { switch { case input.Payload == "schedule-followup": - _, err := SchedulerScheduleOneTime(1, "followup-created", "followup-id") - if err != nil { - errStr := err.Error() - return SchedulerCallbackOutput{Error: &errStr}, nil + if _, err := SchedulerScheduleOneTime(1, "followup-created", "followup-id"); err != nil { + return err } case input.Payload == "schedule-recurring": - _, err := SchedulerScheduleRecurring("@every 1s", "recurring-created", "recurring-from-plugin") - if err != nil { - errStr := err.Error() - return SchedulerCallbackOutput{Error: &errStr}, nil + if _, err := SchedulerScheduleRecurring("@every 1s", "recurring-created", "recurring-from-plugin"); err != nil { + return err } case len(input.Payload) > 19 && input.Payload[:19] == "schedule-duplicate:": duplicateID := input.Payload[19:] - _, err := SchedulerScheduleOneTime(60, "duplicate-attempt", duplicateID) - if err != nil { - errStr := err.Error() - return SchedulerCallbackOutput{Error: &errStr}, nil + if _, err := SchedulerScheduleOneTime(60, "duplicate-attempt", duplicateID); err != nil { + return err } } - return SchedulerCallbackOutput{}, nil + return nil } func main() {} diff --git a/plugins/testdata/test-scheduler/pdk.gen.go b/plugins/testdata/test-scheduler/pdk.gen.go index 6658c876a..681bea057 100644 --- a/plugins/testdata/test-scheduler/pdk.gen.go +++ b/plugins/testdata/test-scheduler/pdk.gen.go @@ -7,31 +7,17 @@ import ( //go:wasmexport nd_scheduler_callback func _NdSchedulerCallback() int32 { - var err error - _ = err - pdk.Log(pdk.LogDebug, "NdSchedulerCallback: getting JSON input") var input SchedulerCallbackInput - err = pdk.InputJSON(&input) - if err != nil { + if err := pdk.InputJSON(&input); err != nil { pdk.SetError(err) return -1 } - pdk.Log(pdk.LogDebug, "NdSchedulerCallback: calling implementation function") - output, err := NdSchedulerCallback(input) - if err != nil { + if err := NdSchedulerCallback(input); err != nil { pdk.SetError(err) return -1 } - pdk.Log(pdk.LogDebug, "NdSchedulerCallback: setting JSON output") - err = pdk.OutputJSON(output) - if err != nil { - pdk.SetError(err) - return -1 - } - - pdk.Log(pdk.LogDebug, "NdSchedulerCallback: returning") return 0 } @@ -47,11 +33,3 @@ type SchedulerCallbackInput struct { // provided when scheduling, or an auto-generated UUID if none was specified. ScheduleId string `json:"scheduleId"` } - -// Output from the scheduler callback -type SchedulerCallbackOutput struct { - // 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"` -} diff --git a/plugins/testdata/test-scrobbler/main.go b/plugins/testdata/test-scrobbler/main.go index 51d65c0ad..19e4ea7d1 100644 --- a/plugins/testdata/test-scrobbler/main.go +++ b/plugins/testdata/test-scrobbler/main.go @@ -48,6 +48,8 @@ type ScrobbleInput struct { Timestamp int64 `json:"timestamp"` } +// ScrobblerOutput contains error information from scrobble operations. +// A nil pointer indicates success, non-nil indicates an error with details. type ScrobblerOutput struct { Error string `json:"error,omitempty"` ErrorType string `json:"errorType,omitempty"` @@ -87,16 +89,17 @@ func ndScrobblerIsAuthorized() int32 { var input AuthInput if err := pdk.InputJSON(&input); err != nil { pdk.SetError(err) - return 1 + return -1 } - output := AuthOutput{ + // Return pointer to output + output := &AuthOutput{ Authorized: checkAuthConfig(), } if err := pdk.OutputJSON(output); err != nil { pdk.SetError(err) - return 1 + return -1 } return 0 } @@ -106,19 +109,19 @@ func ndScrobblerNowPlaying() int32 { var input NowPlayingInput if err := pdk.InputJSON(&input); err != nil { pdk.SetError(err) - return 1 + return -1 } - // Check for configured error + // Check for configured error - return pointer to error output hasErr, errMsg, errType := checkConfigError() if hasErr { - output := ScrobblerOutput{ + output := &ScrobblerOutput{ Error: errMsg, ErrorType: errType, } if err := pdk.OutputJSON(output); err != nil { pdk.SetError(err) - return 1 + return -1 } return 0 } @@ -127,7 +130,11 @@ func ndScrobblerNowPlaying() int32 { // In a real plugin, this would send to an external service pdk.Log(pdk.LogInfo, "NowPlaying: "+input.Track.Title+" by "+input.Track.Artist) - // Success - no output needed + // Success - output nil (empty response) + if err := pdk.OutputJSON(nil); err != nil { + pdk.SetError(err) + return -1 + } return 0 } @@ -136,19 +143,19 @@ func ndScrobblerScrobble() int32 { var input ScrobbleInput if err := pdk.InputJSON(&input); err != nil { pdk.SetError(err) - return 1 + return -1 } - // Check for configured error + // Check for configured error - return pointer to error output hasErr, errMsg, errType := checkConfigError() if hasErr { - output := ScrobblerOutput{ + output := &ScrobblerOutput{ Error: errMsg, ErrorType: errType, } if err := pdk.OutputJSON(output); err != nil { pdk.SetError(err) - return 1 + return -1 } return 0 } @@ -157,7 +164,11 @@ func ndScrobblerScrobble() int32 { // In a real plugin, this would send to an external service pdk.Log(pdk.LogInfo, "Scrobble: "+input.Track.Title+" by "+input.Track.Artist) - // Success - no output needed + // Success - output nil (empty response) + if err := pdk.OutputJSON(nil); err != nil { + pdk.SetError(err) + return -1 + } return 0 } diff --git a/plugins/testdata/test-websocket/main.go b/plugins/testdata/test-websocket/main.go index 6c4b105db..6b958dd5f 100644 --- a/plugins/testdata/test-websocket/main.go +++ b/plugins/testdata/test-websocket/main.go @@ -3,6 +3,8 @@ package main import ( + "errors" + pdk "github.com/extism/go-pdk" ) @@ -12,11 +14,6 @@ type OnTextMessageInput struct { Message string `json:"message"` } -// OnTextMessageOutput is the output from nd_websocket_on_text_message callback. -type OnTextMessageOutput struct { - Error *string `json:"error,omitempty"` -} - // nd_websocket_on_text_message is called when a text message is received. // Magic messages trigger specific behaviors to test host functions: // - "echo": sends back the same message using SendText host function @@ -28,9 +25,8 @@ type OnTextMessageOutput struct { func ndWebSocketOnTextMessage() int32 { var input OnTextMessageInput if err := pdk.InputJSON(&input); err != nil { - errStr := err.Error() - pdk.OutputJSON(OnTextMessageOutput{Error: &errStr}) - return 0 + pdk.SetError(err) + return -1 } // Store all received messages for test verification @@ -38,28 +34,22 @@ func ndWebSocketOnTextMessage() int32 { switch input.Message { case "echo": - _, err := WebSocketSendText(input.ConnectionID, "echo:"+input.Message) - if err != nil { - errStr := err.Error() - pdk.OutputJSON(OnTextMessageOutput{Error: &errStr}) - return 0 + if _, err := WebSocketSendText(input.ConnectionID, "echo:"+input.Message); err != nil { + pdk.SetError(err) + return -1 } case "close": - _, err := WebSocketCloseConnection(input.ConnectionID, 1000, "closed by plugin") - if err != nil { - errStr := err.Error() - pdk.OutputJSON(OnTextMessageOutput{Error: &errStr}) - return 0 + if _, err := WebSocketCloseConnection(input.ConnectionID, 1000, "closed by plugin"); err != nil { + pdk.SetError(err) + return -1 } case "fail": - errStr := "intentional test failure" - pdk.OutputJSON(OnTextMessageOutput{Error: &errStr}) - return 0 + pdk.SetError(errors.New("intentional test failure")) + return -1 } - pdk.OutputJSON(OnTextMessageOutput{}) return 0 } @@ -69,26 +59,19 @@ type OnBinaryMessageInput struct { Data string `json:"data"` // Base64 encoded } -// OnBinaryMessageOutput is the output from nd_websocket_on_binary_message callback. -type OnBinaryMessageOutput struct { - Error *string `json:"error,omitempty"` -} - // nd_websocket_on_binary_message is called when a binary message is received. // //go:wasmexport nd_websocket_on_binary_message func ndWebSocketOnBinaryMessage() int32 { var input OnBinaryMessageInput if err := pdk.InputJSON(&input); err != nil { - errStr := err.Error() - pdk.OutputJSON(OnBinaryMessageOutput{Error: &errStr}) - return 0 + pdk.SetError(err) + return -1 } // Store received binary data for test verification storeReceivedMessage("binary:" + input.Data) - pdk.OutputJSON(OnBinaryMessageOutput{}) return 0 } @@ -98,26 +81,19 @@ type OnErrorInput struct { Error string `json:"error"` } -// OnErrorOutput is the output from nd_websocket_on_error callback. -type OnErrorOutput struct { - Error *string `json:"error,omitempty"` -} - // nd_websocket_on_error is called when an error occurs on a WebSocket connection. // //go:wasmexport nd_websocket_on_error func ndWebSocketOnError() int32 { var input OnErrorInput if err := pdk.InputJSON(&input); err != nil { - errStr := err.Error() - pdk.OutputJSON(OnErrorOutput{Error: &errStr}) - return 0 + pdk.SetError(err) + return -1 } // Store error for test verification storeReceivedMessage("error:" + input.Error) - pdk.OutputJSON(OnErrorOutput{}) return 0 } @@ -128,26 +104,19 @@ type OnCloseInput struct { Reason string `json:"reason"` } -// OnCloseOutput is the output from nd_websocket_on_close callback. -type OnCloseOutput struct { - Error *string `json:"error,omitempty"` -} - // nd_websocket_on_close is called when a WebSocket connection is closed. // //go:wasmexport nd_websocket_on_close func ndWebSocketOnClose() int32 { var input OnCloseInput if err := pdk.InputJSON(&input); err != nil { - errStr := err.Error() - pdk.OutputJSON(OnCloseOutput{Error: &errStr}) - return 0 + pdk.SetError(err) + return -1 } // Store close event for test verification storeReceivedMessage("close:" + input.Reason) - pdk.OutputJSON(OnCloseOutput{}) return 0 }