mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
refactor(plugins): streamline plugin function signatures and error handling
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
301b2c850c
commit
d43724c571
@ -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
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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"`
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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"`
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -154,12 +154,6 @@ struct SchedulerCallbackInput {
|
||||
is_recurring: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Default)]
|
||||
struct SchedulerCallbackOutput {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 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<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[allow(dead_code)]
|
||||
@ -186,12 +174,6 @@ struct OnBinaryMessageInput {
|
||||
message: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Default)]
|
||||
struct OnBinaryMessageOutput {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Scrobbler Plugin Exports
|
||||
// ============================================================================
|
||||
@ -248,7 +218,7 @@ pub fn nd_scrobbler_is_authorized(Json(input): Json<AuthInput>) -> FnResult<Json
|
||||
#[plugin_fn]
|
||||
pub fn nd_scrobbler_now_playing(
|
||||
Json(input): Json<NowPlayingInput>,
|
||||
) -> FnResult<Json<ScrobblerOutput>> {
|
||||
) -> FnResult<Json<Option<ScrobblerOutput>>> {
|
||||
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<ScrobbleInput>) -> FnResult<Json<ScrobblerOutput>> {
|
||||
// Discord Rich Presence doesn't need scrobble events
|
||||
Ok(Json(ScrobblerOutput::default()))
|
||||
pub fn nd_scrobbler_scrobble(_input: Json<ScrobbleInput>) -> FnResult<Json<Option<ScrobblerOutput>>> {
|
||||
// Discord Rich Presence doesn't need scrobble events - success
|
||||
Ok(Json(None))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@ -352,34 +323,23 @@ pub fn nd_scrobbler_scrobble(_input: Json<ScrobbleInput>) -> FnResult<Json<Scrob
|
||||
|
||||
/// Handles scheduler callbacks for heartbeat and activity clearing.
|
||||
#[plugin_fn]
|
||||
pub fn nd_scheduler_callback(
|
||||
Json(input): Json<SchedulerCallbackInput>,
|
||||
) -> FnResult<Json<SchedulerCallbackOutput>> {
|
||||
|
||||
pub fn nd_scheduler_callback(Json(input): Json<SchedulerCallbackInput>) -> 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<OnTextMessageInput>,
|
||||
) -> FnResult<Json<OnTextMessageOutput>> {
|
||||
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<OnTextMessageInput>) -> 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<OnBinaryMessageInput>,
|
||||
) -> FnResult<Json<OnBinaryMessageOutput>> {
|
||||
pub fn nd_websocket_on_binary_message(Json(_input): Json<OnBinaryMessageInput>) -> 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<OnErrorInput>) -> FnResult<Json<OnErrorOutput>> {
|
||||
pub fn nd_websocket_on_error(Json(input): Json<OnErrorInput>) -> 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<OnCloseInput>) -> FnResult<Json<OnCloseOutput>> {
|
||||
pub fn nd_websocket_on_close(Json(input): Json<OnCloseInput>) -> FnResult<()> {
|
||||
info!(
|
||||
"WebSocket connection '{}' closed with code {}: {}",
|
||||
input.connection_id, input.code, input.reason
|
||||
);
|
||||
Ok(Json(OnCloseOutput::default()))
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -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() {}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"))
|
||||
|
||||
22
plugins/testdata/test-scheduler/main.go
vendored
22
plugins/testdata/test-scheduler/main.go
vendored
@ -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:<id>": 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() {}
|
||||
|
||||
26
plugins/testdata/test-scheduler/pdk.gen.go
vendored
26
plugins/testdata/test-scheduler/pdk.gen.go
vendored
@ -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"`
|
||||
}
|
||||
|
||||
37
plugins/testdata/test-scrobbler/main.go
vendored
37
plugins/testdata/test-scrobbler/main.go
vendored
@ -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
|
||||
}
|
||||
|
||||
|
||||
67
plugins/testdata/test-websocket/main.go
vendored
67
plugins/testdata/test-websocket/main.go
vendored
@ -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
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user