test: make all test plugins use the PDK

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan 2025-12-30 11:58:20 -05:00
parent f1b85e6a19
commit f221c01bd9
24 changed files with 194 additions and 2224 deletions

View File

@ -1,5 +1,10 @@
module test-artwork
go 1.23
go 1.24
require github.com/extism/go-pdk v1.1.3
require (
github.com/extism/go-pdk v1.1.3
github.com/navidrome/navidrome/plugins/pdk/go/host v0.0.0
)
replace github.com/navidrome/navidrome/plugins/pdk/go/host => ../../pdk/go/host

View File

@ -6,6 +6,7 @@ import (
"strings"
pdk "github.com/extism/go-pdk"
"github.com/navidrome/navidrome/plugins/pdk/go/host"
)
// TestInput is the input for nd_test_artwork callback.
@ -37,28 +38,28 @@ func ndTestArtwork() int32 {
switch strings.ToLower(input.ArtworkType) {
case "artist":
resp, e := ArtworkGetArtistUrl(input.ID, input.Size)
resp, e := host.ArtworkGetArtistUrl(input.ID, input.Size)
if e != nil {
err = e
} else {
url = resp.Url
}
case "album":
resp, e := ArtworkGetAlbumUrl(input.ID, input.Size)
resp, e := host.ArtworkGetAlbumUrl(input.ID, input.Size)
if e != nil {
err = e
} else {
url = resp.Url
}
case "track":
resp, e := ArtworkGetTrackUrl(input.ID, input.Size)
resp, e := host.ArtworkGetTrackUrl(input.ID, input.Size)
if e != nil {
err = e
} else {
url = resp.Url
}
case "playlist":
resp, e := ArtworkGetPlaylistUrl(input.ID, input.Size)
resp, e := host.ArtworkGetPlaylistUrl(input.ID, input.Size)
if e != nil {
err = e
} else {

View File

@ -1,251 +0,0 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains client wrappers for the Artwork host service.
// It is intended for use in Navidrome plugins built with TinyGo.
//
//go:build wasip1
package main
import (
"encoding/json"
"errors"
"github.com/extism/go-pdk"
)
// artwork_getartisturl is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user artwork_getartisturl
func artwork_getartisturl(uint64) uint64
// artwork_getalbumurl is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user artwork_getalbumurl
func artwork_getalbumurl(uint64) uint64
// artwork_gettrackurl is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user artwork_gettrackurl
func artwork_gettrackurl(uint64) uint64
// artwork_getplaylisturl is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user artwork_getplaylisturl
func artwork_getplaylisturl(uint64) uint64
// ArtworkGetArtistUrlRequest is the request type for Artwork.GetArtistUrl.
type ArtworkGetArtistUrlRequest struct {
Id string `json:"id"`
Size int32 `json:"size"`
}
// ArtworkGetArtistUrlResponse is the response type for Artwork.GetArtistUrl.
type ArtworkGetArtistUrlResponse struct {
Url string `json:"url,omitempty"`
Error string `json:"error,omitempty"`
}
// ArtworkGetAlbumUrlRequest is the request type for Artwork.GetAlbumUrl.
type ArtworkGetAlbumUrlRequest struct {
Id string `json:"id"`
Size int32 `json:"size"`
}
// ArtworkGetAlbumUrlResponse is the response type for Artwork.GetAlbumUrl.
type ArtworkGetAlbumUrlResponse struct {
Url string `json:"url,omitempty"`
Error string `json:"error,omitempty"`
}
// ArtworkGetTrackUrlRequest is the request type for Artwork.GetTrackUrl.
type ArtworkGetTrackUrlRequest struct {
Id string `json:"id"`
Size int32 `json:"size"`
}
// ArtworkGetTrackUrlResponse is the response type for Artwork.GetTrackUrl.
type ArtworkGetTrackUrlResponse struct {
Url string `json:"url,omitempty"`
Error string `json:"error,omitempty"`
}
// ArtworkGetPlaylistUrlRequest is the request type for Artwork.GetPlaylistUrl.
type ArtworkGetPlaylistUrlRequest struct {
Id string `json:"id"`
Size int32 `json:"size"`
}
// ArtworkGetPlaylistUrlResponse is the response type for Artwork.GetPlaylistUrl.
type ArtworkGetPlaylistUrlResponse struct {
Url string `json:"url,omitempty"`
Error string `json:"error,omitempty"`
}
// ArtworkGetArtistUrl calls the artwork_getartisturl host function.
// GetArtistUrl generates a public URL for an artist's artwork.
//
// Parameters:
// - id: The artist's unique identifier
// - size: Desired image size in pixels (0 for original size)
//
// Returns the public URL for the artwork, or an error if generation fails.
func ArtworkGetArtistUrl(id string, size int32) (*ArtworkGetArtistUrlResponse, error) {
// Marshal request to JSON
req := ArtworkGetArtistUrlRequest{
Id: id,
Size: size,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := artwork_getartisturl(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response ArtworkGetArtistUrlResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// ArtworkGetAlbumUrl calls the artwork_getalbumurl host function.
// GetAlbumUrl generates a public URL for an album's artwork.
//
// Parameters:
// - id: The album's unique identifier
// - size: Desired image size in pixels (0 for original size)
//
// Returns the public URL for the artwork, or an error if generation fails.
func ArtworkGetAlbumUrl(id string, size int32) (*ArtworkGetAlbumUrlResponse, error) {
// Marshal request to JSON
req := ArtworkGetAlbumUrlRequest{
Id: id,
Size: size,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := artwork_getalbumurl(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response ArtworkGetAlbumUrlResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// ArtworkGetTrackUrl calls the artwork_gettrackurl host function.
// GetTrackUrl generates a public URL for a track's artwork.
//
// Parameters:
// - id: The track's (media file) unique identifier
// - size: Desired image size in pixels (0 for original size)
//
// Returns the public URL for the artwork, or an error if generation fails.
func ArtworkGetTrackUrl(id string, size int32) (*ArtworkGetTrackUrlResponse, error) {
// Marshal request to JSON
req := ArtworkGetTrackUrlRequest{
Id: id,
Size: size,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := artwork_gettrackurl(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response ArtworkGetTrackUrlResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// ArtworkGetPlaylistUrl calls the artwork_getplaylisturl host function.
// GetPlaylistUrl generates a public URL for a playlist's artwork.
//
// Parameters:
// - id: The playlist's unique identifier
// - size: Desired image size in pixels (0 for original size)
//
// Returns the public URL for the artwork, or an error if generation fails.
func ArtworkGetPlaylistUrl(id string, size int32) (*ArtworkGetPlaylistUrlResponse, error) {
// Marshal request to JSON
req := ArtworkGetPlaylistUrlRequest{
Id: id,
Size: size,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := artwork_getplaylisturl(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response ArtworkGetPlaylistUrlResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}

View File

@ -1,5 +1,10 @@
module test-cache-plugin
go 1.23
go 1.25
require github.com/extism/go-pdk v1.1.3
require (
github.com/extism/go-pdk v1.1.3
github.com/navidrome/navidrome/plugins/pdk/go/host v0.0.0
)
replace github.com/navidrome/navidrome/plugins/pdk/go/host => ../../pdk/go/host

View File

@ -4,6 +4,7 @@ package main
import (
pdk "github.com/extism/go-pdk"
"github.com/navidrome/navidrome/plugins/pdk/go/host"
)
// TestCacheInput is the input for nd_test_cache callback.
@ -40,7 +41,7 @@ func ndTestCache() int32 {
switch input.Operation {
case "set_string":
_, err := CacheSetString(input.Key, input.StringVal, input.TTLSeconds)
_, err := host.CacheSetString(input.Key, input.StringVal, input.TTLSeconds)
if err != nil {
errStr := err.Error()
pdk.OutputJSON(TestCacheOutput{Error: &errStr})
@ -50,7 +51,7 @@ func ndTestCache() int32 {
return 0
case "get_string":
resp, err := CacheGetString(input.Key)
resp, err := host.CacheGetString(input.Key)
if err != nil {
errStr := err.Error()
pdk.OutputJSON(TestCacheOutput{Error: &errStr})
@ -60,7 +61,7 @@ func ndTestCache() int32 {
return 0
case "set_int":
_, err := CacheSetInt(input.Key, input.IntVal, input.TTLSeconds)
_, err := host.CacheSetInt(input.Key, input.IntVal, input.TTLSeconds)
if err != nil {
errStr := err.Error()
pdk.OutputJSON(TestCacheOutput{Error: &errStr})
@ -70,7 +71,7 @@ func ndTestCache() int32 {
return 0
case "get_int":
resp, err := CacheGetInt(input.Key)
resp, err := host.CacheGetInt(input.Key)
if err != nil {
errStr := err.Error()
pdk.OutputJSON(TestCacheOutput{Error: &errStr})
@ -80,7 +81,7 @@ func ndTestCache() int32 {
return 0
case "set_float":
_, err := CacheSetFloat(input.Key, input.FloatVal, input.TTLSeconds)
_, err := host.CacheSetFloat(input.Key, input.FloatVal, input.TTLSeconds)
if err != nil {
errStr := err.Error()
pdk.OutputJSON(TestCacheOutput{Error: &errStr})
@ -90,7 +91,7 @@ func ndTestCache() int32 {
return 0
case "get_float":
resp, err := CacheGetFloat(input.Key)
resp, err := host.CacheGetFloat(input.Key)
if err != nil {
errStr := err.Error()
pdk.OutputJSON(TestCacheOutput{Error: &errStr})
@ -100,7 +101,7 @@ func ndTestCache() int32 {
return 0
case "set_bytes":
_, err := CacheSetBytes(input.Key, input.BytesVal, input.TTLSeconds)
_, err := host.CacheSetBytes(input.Key, input.BytesVal, input.TTLSeconds)
if err != nil {
errStr := err.Error()
pdk.OutputJSON(TestCacheOutput{Error: &errStr})
@ -110,7 +111,7 @@ func ndTestCache() int32 {
return 0
case "get_bytes":
resp, err := CacheGetBytes(input.Key)
resp, err := host.CacheGetBytes(input.Key)
if err != nil {
errStr := err.Error()
pdk.OutputJSON(TestCacheOutput{Error: &errStr})
@ -120,7 +121,7 @@ func ndTestCache() int32 {
return 0
case "has":
resp, err := CacheHas(input.Key)
resp, err := host.CacheHas(input.Key)
if err != nil {
errStr := err.Error()
pdk.OutputJSON(TestCacheOutput{Error: &errStr})
@ -130,7 +131,7 @@ func ndTestCache() int32 {
return 0
case "remove":
_, err := CacheRemove(input.Key)
_, err := host.CacheRemove(input.Key)
if err != nil {
errStr := err.Error()
pdk.OutputJSON(TestCacheOutput{Error: &errStr})

View File

@ -1,602 +0,0 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains client wrappers for the Cache host service.
// It is intended for use in Navidrome plugins built with TinyGo.
//
//go:build wasip1
package main
import (
"encoding/json"
"errors"
"github.com/extism/go-pdk"
)
// cache_setstring is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user cache_setstring
func cache_setstring(uint64) uint64
// cache_getstring is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user cache_getstring
func cache_getstring(uint64) uint64
// cache_setint is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user cache_setint
func cache_setint(uint64) uint64
// cache_getint is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user cache_getint
func cache_getint(uint64) uint64
// cache_setfloat is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user cache_setfloat
func cache_setfloat(uint64) uint64
// cache_getfloat is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user cache_getfloat
func cache_getfloat(uint64) uint64
// cache_setbytes is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user cache_setbytes
func cache_setbytes(uint64) uint64
// cache_getbytes is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user cache_getbytes
func cache_getbytes(uint64) uint64
// cache_has is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user cache_has
func cache_has(uint64) uint64
// cache_remove is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user cache_remove
func cache_remove(uint64) uint64
// CacheSetStringRequest is the request type for Cache.SetString.
type CacheSetStringRequest struct {
Key string `json:"key"`
Value string `json:"value"`
TtlSeconds int64 `json:"ttlSeconds"`
}
// CacheSetStringResponse is the response type for Cache.SetString.
type CacheSetStringResponse struct {
Error string `json:"error,omitempty"`
}
// CacheGetStringRequest is the request type for Cache.GetString.
type CacheGetStringRequest struct {
Key string `json:"key"`
}
// CacheGetStringResponse is the response type for Cache.GetString.
type CacheGetStringResponse struct {
Value string `json:"value,omitempty"`
Exists bool `json:"exists,omitempty"`
Error string `json:"error,omitempty"`
}
// CacheSetIntRequest is the request type for Cache.SetInt.
type CacheSetIntRequest struct {
Key string `json:"key"`
Value int64 `json:"value"`
TtlSeconds int64 `json:"ttlSeconds"`
}
// CacheSetIntResponse is the response type for Cache.SetInt.
type CacheSetIntResponse struct {
Error string `json:"error,omitempty"`
}
// CacheGetIntRequest is the request type for Cache.GetInt.
type CacheGetIntRequest struct {
Key string `json:"key"`
}
// CacheGetIntResponse is the response type for Cache.GetInt.
type CacheGetIntResponse struct {
Value int64 `json:"value,omitempty"`
Exists bool `json:"exists,omitempty"`
Error string `json:"error,omitempty"`
}
// CacheSetFloatRequest is the request type for Cache.SetFloat.
type CacheSetFloatRequest struct {
Key string `json:"key"`
Value float64 `json:"value"`
TtlSeconds int64 `json:"ttlSeconds"`
}
// CacheSetFloatResponse is the response type for Cache.SetFloat.
type CacheSetFloatResponse struct {
Error string `json:"error,omitempty"`
}
// CacheGetFloatRequest is the request type for Cache.GetFloat.
type CacheGetFloatRequest struct {
Key string `json:"key"`
}
// CacheGetFloatResponse is the response type for Cache.GetFloat.
type CacheGetFloatResponse struct {
Value float64 `json:"value,omitempty"`
Exists bool `json:"exists,omitempty"`
Error string `json:"error,omitempty"`
}
// CacheSetBytesRequest is the request type for Cache.SetBytes.
type CacheSetBytesRequest struct {
Key string `json:"key"`
Value []byte `json:"value"`
TtlSeconds int64 `json:"ttlSeconds"`
}
// CacheSetBytesResponse is the response type for Cache.SetBytes.
type CacheSetBytesResponse struct {
Error string `json:"error,omitempty"`
}
// CacheGetBytesRequest is the request type for Cache.GetBytes.
type CacheGetBytesRequest struct {
Key string `json:"key"`
}
// CacheGetBytesResponse is the response type for Cache.GetBytes.
type CacheGetBytesResponse struct {
Value []byte `json:"value,omitempty"`
Exists bool `json:"exists,omitempty"`
Error string `json:"error,omitempty"`
}
// CacheHasRequest is the request type for Cache.Has.
type CacheHasRequest struct {
Key string `json:"key"`
}
// CacheHasResponse is the response type for Cache.Has.
type CacheHasResponse struct {
Exists bool `json:"exists,omitempty"`
Error string `json:"error,omitempty"`
}
// CacheRemoveRequest is the request type for Cache.Remove.
type CacheRemoveRequest struct {
Key string `json:"key"`
}
// CacheRemoveResponse is the response type for Cache.Remove.
type CacheRemoveResponse struct {
Error string `json:"error,omitempty"`
}
// CacheSetString calls the cache_setstring host function.
// SetString stores a string value in the cache.
//
// Parameters:
// - key: The cache key (will be namespaced with plugin ID)
// - value: The string value to store
// - ttlSeconds: Time-to-live in seconds (0 uses default of 24 hours)
//
// Returns an error if the operation fails.
func CacheSetString(key string, value string, ttlSeconds int64) (*CacheSetStringResponse, error) {
// Marshal request to JSON
req := CacheSetStringRequest{
Key: key,
Value: value,
TtlSeconds: ttlSeconds,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := cache_setstring(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response CacheSetStringResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// CacheGetString calls the cache_getstring host function.
// GetString retrieves a string value from the cache.
//
// Parameters:
// - key: The cache key (will be namespaced with plugin ID)
//
// Returns the value and whether the key exists. If the key doesn't exist
// or the stored value is not a string, exists will be false.
func CacheGetString(key string) (*CacheGetStringResponse, error) {
// Marshal request to JSON
req := CacheGetStringRequest{
Key: key,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := cache_getstring(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response CacheGetStringResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// CacheSetInt calls the cache_setint host function.
// SetInt stores an integer value in the cache.
//
// Parameters:
// - key: The cache key (will be namespaced with plugin ID)
// - value: The integer value to store
// - ttlSeconds: Time-to-live in seconds (0 uses default of 24 hours)
//
// Returns an error if the operation fails.
func CacheSetInt(key string, value int64, ttlSeconds int64) (*CacheSetIntResponse, error) {
// Marshal request to JSON
req := CacheSetIntRequest{
Key: key,
Value: value,
TtlSeconds: ttlSeconds,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := cache_setint(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response CacheSetIntResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// CacheGetInt calls the cache_getint host function.
// GetInt retrieves an integer value from the cache.
//
// Parameters:
// - key: The cache key (will be namespaced with plugin ID)
//
// Returns the value and whether the key exists. If the key doesn't exist
// or the stored value is not an integer, exists will be false.
func CacheGetInt(key string) (*CacheGetIntResponse, error) {
// Marshal request to JSON
req := CacheGetIntRequest{
Key: key,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := cache_getint(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response CacheGetIntResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// CacheSetFloat calls the cache_setfloat host function.
// SetFloat stores a float value in the cache.
//
// Parameters:
// - key: The cache key (will be namespaced with plugin ID)
// - value: The float value to store
// - ttlSeconds: Time-to-live in seconds (0 uses default of 24 hours)
//
// Returns an error if the operation fails.
func CacheSetFloat(key string, value float64, ttlSeconds int64) (*CacheSetFloatResponse, error) {
// Marshal request to JSON
req := CacheSetFloatRequest{
Key: key,
Value: value,
TtlSeconds: ttlSeconds,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := cache_setfloat(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response CacheSetFloatResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// CacheGetFloat calls the cache_getfloat host function.
// GetFloat retrieves a float value from the cache.
//
// Parameters:
// - key: The cache key (will be namespaced with plugin ID)
//
// Returns the value and whether the key exists. If the key doesn't exist
// or the stored value is not a float, exists will be false.
func CacheGetFloat(key string) (*CacheGetFloatResponse, error) {
// Marshal request to JSON
req := CacheGetFloatRequest{
Key: key,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := cache_getfloat(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response CacheGetFloatResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// CacheSetBytes calls the cache_setbytes host function.
// SetBytes stores a byte slice in the cache.
//
// Parameters:
// - key: The cache key (will be namespaced with plugin ID)
// - value: The byte slice to store
// - ttlSeconds: Time-to-live in seconds (0 uses default of 24 hours)
//
// Returns an error if the operation fails.
func CacheSetBytes(key string, value []byte, ttlSeconds int64) (*CacheSetBytesResponse, error) {
// Marshal request to JSON
req := CacheSetBytesRequest{
Key: key,
Value: value,
TtlSeconds: ttlSeconds,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := cache_setbytes(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response CacheSetBytesResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// CacheGetBytes calls the cache_getbytes host function.
// GetBytes retrieves a byte slice from the cache.
//
// Parameters:
// - key: The cache key (will be namespaced with plugin ID)
//
// Returns the value and whether the key exists. If the key doesn't exist
// or the stored value is not a byte slice, exists will be false.
func CacheGetBytes(key string) (*CacheGetBytesResponse, error) {
// Marshal request to JSON
req := CacheGetBytesRequest{
Key: key,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := cache_getbytes(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response CacheGetBytesResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// CacheHas calls the cache_has host function.
// Has checks if a key exists in the cache.
//
// Parameters:
// - key: The cache key (will be namespaced with plugin ID)
//
// Returns true if the key exists and has not expired.
func CacheHas(key string) (*CacheHasResponse, error) {
// Marshal request to JSON
req := CacheHasRequest{
Key: key,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := cache_has(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response CacheHasResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// CacheRemove calls the cache_remove host function.
// Remove deletes a value from the cache.
//
// Parameters:
// - key: The cache key (will be namespaced with plugin ID)
//
// Returns an error if the operation fails. Does not return an error if the key doesn't exist.
func CacheRemove(key string) (*CacheRemoveResponse, error) {
// Marshal request to JSON
req := CacheRemoveRequest{
Key: key,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := cache_remove(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response CacheRemoveResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}

View File

@ -1,5 +1,10 @@
module test-kvstore
go 1.23
go 1.24
require github.com/extism/go-pdk v1.1.3
require (
github.com/extism/go-pdk v1.1.3
github.com/navidrome/navidrome/plugins/pdk/go/host v0.0.0
)
replace github.com/navidrome/navidrome/plugins/pdk/go/host => ../../pdk/go/host

View File

@ -4,6 +4,7 @@ package main
import (
pdk "github.com/extism/go-pdk"
"github.com/navidrome/navidrome/plugins/pdk/go/host"
)
// TestKVStoreInput is the input for nd_test_kvstore callback.
@ -36,7 +37,7 @@ func ndTestKVStore() int32 {
switch input.Operation {
case "set":
_, err := KVStoreSet(input.Key, input.Value)
_, err := host.KVStoreSet(input.Key, input.Value)
if err != nil {
errStr := err.Error()
pdk.OutputJSON(TestKVStoreOutput{Error: &errStr})
@ -46,7 +47,7 @@ func ndTestKVStore() int32 {
return 0
case "get":
resp, err := KVStoreGet(input.Key)
resp, err := host.KVStoreGet(input.Key)
if err != nil {
errStr := err.Error()
pdk.OutputJSON(TestKVStoreOutput{Error: &errStr})
@ -56,7 +57,7 @@ func ndTestKVStore() int32 {
return 0
case "delete":
_, err := KVStoreDelete(input.Key)
_, err := host.KVStoreDelete(input.Key)
if err != nil {
errStr := err.Error()
pdk.OutputJSON(TestKVStoreOutput{Error: &errStr})
@ -66,7 +67,7 @@ func ndTestKVStore() int32 {
return 0
case "has":
resp, err := KVStoreHas(input.Key)
resp, err := host.KVStoreHas(input.Key)
if err != nil {
errStr := err.Error()
pdk.OutputJSON(TestKVStoreOutput{Error: &errStr})
@ -76,7 +77,7 @@ func ndTestKVStore() int32 {
return 0
case "list":
resp, err := KVStoreList(input.Prefix)
resp, err := host.KVStoreList(input.Prefix)
if err != nil {
errStr := err.Error()
pdk.OutputJSON(TestKVStoreOutput{Error: &errStr})
@ -86,7 +87,7 @@ func ndTestKVStore() int32 {
return 0
case "get_storage_used":
resp, err := KVStoreGetStorageUsed()
resp, err := host.KVStoreGetStorageUsed()
if err != nil {
errStr := err.Error()
pdk.OutputJSON(TestKVStoreOutput{Error: &errStr})

View File

@ -1,336 +0,0 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains client wrappers for the KVStore host service.
// It is intended for use in Navidrome plugins built with TinyGo.
//
//go:build wasip1
package main
import (
"encoding/json"
"errors"
"github.com/extism/go-pdk"
)
// kvstore_set is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user kvstore_set
func kvstore_set(uint64) uint64
// kvstore_get is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user kvstore_get
func kvstore_get(uint64) uint64
// kvstore_delete is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user kvstore_delete
func kvstore_delete(uint64) uint64
// kvstore_has is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user kvstore_has
func kvstore_has(uint64) uint64
// kvstore_list is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user kvstore_list
func kvstore_list(uint64) uint64
// kvstore_getstorageused is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user kvstore_getstorageused
func kvstore_getstorageused(uint64) uint64
// KVStoreSetRequest is the request type for KVStore.Set.
type KVStoreSetRequest struct {
Key string `json:"key"`
Value []byte `json:"value"`
}
// KVStoreSetResponse is the response type for KVStore.Set.
type KVStoreSetResponse struct {
Error string `json:"error,omitempty"`
}
// KVStoreGetRequest is the request type for KVStore.Get.
type KVStoreGetRequest struct {
Key string `json:"key"`
}
// KVStoreGetResponse is the response type for KVStore.Get.
type KVStoreGetResponse struct {
Value []byte `json:"value,omitempty"`
Exists bool `json:"exists,omitempty"`
Error string `json:"error,omitempty"`
}
// KVStoreDeleteRequest is the request type for KVStore.Delete.
type KVStoreDeleteRequest struct {
Key string `json:"key"`
}
// KVStoreDeleteResponse is the response type for KVStore.Delete.
type KVStoreDeleteResponse struct {
Error string `json:"error,omitempty"`
}
// KVStoreHasRequest is the request type for KVStore.Has.
type KVStoreHasRequest struct {
Key string `json:"key"`
}
// KVStoreHasResponse is the response type for KVStore.Has.
type KVStoreHasResponse struct {
Exists bool `json:"exists,omitempty"`
Error string `json:"error,omitempty"`
}
// KVStoreListRequest is the request type for KVStore.List.
type KVStoreListRequest struct {
Prefix string `json:"prefix"`
}
// KVStoreListResponse is the response type for KVStore.List.
type KVStoreListResponse struct {
Keys []string `json:"keys,omitempty"`
Error string `json:"error,omitempty"`
}
// KVStoreGetStorageUsedResponse is the response type for KVStore.GetStorageUsed.
type KVStoreGetStorageUsedResponse struct {
Bytes int64 `json:"bytes,omitempty"`
Error string `json:"error,omitempty"`
}
// KVStoreSet calls the kvstore_set host function.
// Set stores a byte value with the given key.
//
// Parameters:
// - key: The storage key (max 256 bytes, UTF-8)
// - value: The byte slice to store
//
// Returns an error if the storage limit would be exceeded or the operation fails.
func KVStoreSet(key string, value []byte) (*KVStoreSetResponse, error) {
// Marshal request to JSON
req := KVStoreSetRequest{
Key: key,
Value: value,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := kvstore_set(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response KVStoreSetResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// KVStoreGet calls the kvstore_get host function.
// Get retrieves a byte value from storage.
//
// Parameters:
// - key: The storage key
//
// Returns the value and whether the key exists.
func KVStoreGet(key string) (*KVStoreGetResponse, error) {
// Marshal request to JSON
req := KVStoreGetRequest{
Key: key,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := kvstore_get(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response KVStoreGetResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// KVStoreDelete calls the kvstore_delete host function.
// Delete removes a value from storage.
//
// Parameters:
// - key: The storage key
//
// Returns an error if the operation fails. Does not return an error if the key doesn't exist.
func KVStoreDelete(key string) (*KVStoreDeleteResponse, error) {
// Marshal request to JSON
req := KVStoreDeleteRequest{
Key: key,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := kvstore_delete(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response KVStoreDeleteResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// KVStoreHas calls the kvstore_has host function.
// Has checks if a key exists in storage.
//
// Parameters:
// - key: The storage key
//
// Returns true if the key exists.
func KVStoreHas(key string) (*KVStoreHasResponse, error) {
// Marshal request to JSON
req := KVStoreHasRequest{
Key: key,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := kvstore_has(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response KVStoreHasResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// KVStoreList calls the kvstore_list host function.
// List returns all keys matching the given prefix.
//
// Parameters:
// - prefix: Key prefix to filter by (empty string returns all keys)
//
// Returns a slice of matching keys.
func KVStoreList(prefix string) (*KVStoreListResponse, error) {
// Marshal request to JSON
req := KVStoreListRequest{
Prefix: prefix,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := kvstore_list(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response KVStoreListResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// KVStoreGetStorageUsed calls the kvstore_getstorageused host function.
// GetStorageUsed returns the total storage used by this plugin in bytes.
func KVStoreGetStorageUsed() (*KVStoreGetStorageUsedResponse, error) {
// No parameters - allocate empty JSON object
reqMem := pdk.AllocateBytes([]byte("{}"))
defer reqMem.Free()
// Call the host function
responsePtr := kvstore_getstorageused(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response KVStoreGetStorageUsedResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}

View File

@ -1,5 +1,10 @@
module test-library
go 1.23
go 1.24
require github.com/extism/go-pdk v1.1.3
require (
github.com/extism/go-pdk v1.1.3
github.com/navidrome/navidrome/plugins/pdk/go/host v0.0.0
)
replace github.com/navidrome/navidrome/plugins/pdk/go/host => ../../pdk/go/host

View File

@ -9,6 +9,7 @@ import (
"path/filepath"
pdk "github.com/extism/go-pdk"
"github.com/navidrome/navidrome/plugins/pdk/go/host"
)
// TestLibraryInput is the input for nd_test_library callback.
@ -21,11 +22,11 @@ type TestLibraryInput struct {
// TestLibraryOutput is the output from nd_test_library callback.
type TestLibraryOutput struct {
Library *Library `json:"library,omitempty"`
Libraries []Library `json:"libraries,omitempty"`
FileContent string `json:"file_content,omitempty"`
DirEntries []string `json:"dir_entries,omitempty"`
Error *string `json:"error,omitempty"`
Library *host.Library `json:"library,omitempty"`
Libraries []host.Library `json:"libraries,omitempty"`
FileContent string `json:"file_content,omitempty"`
DirEntries []string `json:"dir_entries,omitempty"`
Error *string `json:"error,omitempty"`
}
// nd_test_library is the test callback that tests the library host functions.
@ -41,7 +42,7 @@ func ndTestLibrary() int32 {
switch input.Operation {
case "get_library":
resp, err := LibraryGetLibrary(input.LibraryID)
resp, err := host.LibraryGetLibrary(input.LibraryID)
if err != nil {
errStr := err.Error()
pdk.OutputJSON(TestLibraryOutput{Error: &errStr})
@ -51,7 +52,7 @@ func ndTestLibrary() int32 {
return 0
case "get_all_libraries":
resp, err := LibraryGetAllLibraries()
resp, err := host.LibraryGetAllLibraries()
if err != nil {
errStr := err.Error()
pdk.OutputJSON(TestLibraryOutput{Error: &errStr})

View File

@ -1,127 +0,0 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains client wrappers for the Library host service.
// It is intended for use in Navidrome plugins built with TinyGo.
//
//go:build wasip1
package main
import (
"encoding/json"
"errors"
"github.com/extism/go-pdk"
)
// Library represents a music library with metadata.
// This type mirrors the host.Library type from plugins/host/library.go.
type Library struct {
ID int32 `json:"id"`
Name string `json:"name"`
Path string `json:"path,omitempty"`
MountPoint string `json:"mountPoint,omitempty"`
LastScanAt int64 `json:"lastScanAt"`
TotalSongs int32 `json:"totalSongs"`
TotalAlbums int32 `json:"totalAlbums"`
TotalArtists int32 `json:"totalArtists"`
TotalSize int64 `json:"totalSize"`
TotalDuration float64 `json:"totalDuration"`
}
// library_getlibrary is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user library_getlibrary
func library_getlibrary(uint64) uint64
// library_getalllibraries is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user library_getalllibraries
func library_getalllibraries(uint64) uint64
// LibraryGetLibraryRequest is the request type for Library.GetLibrary.
type LibraryGetLibraryRequest struct {
Id int32 `json:"id"`
}
// LibraryGetLibraryResponse is the response type for Library.GetLibrary.
type LibraryGetLibraryResponse struct {
Result *Library `json:"result,omitempty"`
Error string `json:"error,omitempty"`
}
// LibraryGetAllLibrariesResponse is the response type for Library.GetAllLibraries.
type LibraryGetAllLibrariesResponse struct {
Result []Library `json:"result,omitempty"`
Error string `json:"error,omitempty"`
}
// LibraryGetLibrary calls the library_getlibrary host function.
// GetLibrary retrieves metadata for a specific library by ID.
//
// Parameters:
// - id: The library's unique identifier
//
// Returns the library metadata, or an error if the library is not found.
func LibraryGetLibrary(id int32) (*LibraryGetLibraryResponse, error) {
// Marshal request to JSON
req := LibraryGetLibraryRequest{
Id: id,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := library_getlibrary(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response LibraryGetLibraryResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// LibraryGetAllLibraries calls the library_getalllibraries host function.
// GetAllLibraries retrieves metadata for all configured libraries.
//
// Returns a slice of all libraries with their metadata.
func LibraryGetAllLibraries() (*LibraryGetAllLibrariesResponse, error) {
// No parameters - allocate empty JSON object
reqMem := pdk.AllocateBytes([]byte("{}"))
defer reqMem.Free()
// Call the host function
responsePtr := library_getalllibraries(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response LibraryGetAllLibrariesResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}

View File

@ -1,5 +1,10 @@
module test-plugin
module test-metadata-agent
go 1.23
go 1.25
require github.com/extism/go-pdk v1.1.3
require (
github.com/extism/go-pdk v1.1.3
github.com/navidrome/navidrome v0.0.0
)
replace github.com/navidrome/navidrome => ../../..

View File

@ -1,283 +1,119 @@
// Test plugin for Navidrome plugin system integration tests.
// Build with: tinygo build -o ../test-plugin.wasm -target wasip1 -buildmode=c-shared ./main.go
// Build with: tinygo build -o ../test-metadata-agent.wasm -target wasip1 -buildmode=c-shared .
package main
import (
"errors"
"strconv"
"github.com/extism/go-pdk"
"github.com/navidrome/navidrome/plugins/pdk/go/metadata"
)
func init() {
metadata.Register(&testMetadataAgent{})
}
type testMetadataAgent struct{}
// checkConfigError checks if the plugin is configured to return an error.
// If "error" config is set, it returns the error message and exit code.
// If "exitcode" is also set, it uses that value (default: 1).
func checkConfigError() (bool, int32) {
// If "error" config is set, it returns an error with that message.
func checkConfigError() error {
errMsg, hasErr := pdk.GetConfig("error")
if !hasErr || errMsg == "" {
return false, 0
return nil
}
exitCode := int32(1)
if code, hasCode := pdk.GetConfig("exitcode"); hasCode {
if parsed, err := strconv.Atoi(code); err == nil {
exitCode = int32(parsed)
}
return errors.New(errMsg)
}
func (t *testMetadataAgent) GetArtistMBID(input metadata.ArtistMBIDRequest) (metadata.ArtistMBIDResponse, error) {
if err := checkConfigError(); err != nil {
return metadata.ArtistMBIDResponse{}, err
}
pdk.SetErrorString(errMsg)
return true, exitCode
return metadata.ArtistMBIDResponse{MBID: "test-mbid-" + input.Name}, nil
}
type ArtistInput struct {
ID string `json:"id"`
Name string `json:"name"`
MBID string `json:"mbid,omitempty"`
}
type ArtistInputWithLimit struct {
ID string `json:"id"`
Name string `json:"name"`
MBID string `json:"mbid,omitempty"`
Limit int `json:"limit,omitempty"`
}
type ArtistInputWithCount struct {
ID string `json:"id"`
Name string `json:"name"`
MBID string `json:"mbid,omitempty"`
Count int `json:"count,omitempty"`
}
type AlbumInput struct {
Name string `json:"name"`
Artist string `json:"artist"`
MBID string `json:"mbid,omitempty"`
}
type MBIDOutput struct {
MBID string `json:"mbid"`
}
type URLOutput struct {
URL string `json:"url"`
}
type BiographyOutput struct {
Biography string `json:"biography"`
}
type ArtistImage struct {
URL string `json:"url"`
Size int `json:"size"`
}
type ImagesOutput struct {
Images []ArtistImage `json:"images"`
}
type SimilarArtist struct {
Name string `json:"name"`
MBID string `json:"mbid,omitempty"`
}
type SimilarArtistsOutput struct {
Artists []SimilarArtist `json:"artists"`
}
type TopSong struct {
Name string `json:"name"`
MBID string `json:"mbid,omitempty"`
}
type TopSongsOutput struct {
Songs []TopSong `json:"songs"`
}
type AlbumInfoOutput struct {
Name string `json:"name"`
MBID string `json:"mbid,omitempty"`
Description string `json:"description,omitempty"`
URL string `json:"url,omitempty"`
}
type AlbumImagesOutput struct {
Images []ArtistImage `json:"images"`
}
//go:wasmexport nd_get_artist_mbid
func ndGetArtistMBID() int32 {
if hasErr, code := checkConfigError(); hasErr {
return code
func (t *testMetadataAgent) GetArtistURL(input metadata.ArtistRequest) (metadata.ArtistURLResponse, error) {
if err := checkConfigError(); err != nil {
return metadata.ArtistURLResponse{}, err
}
var input ArtistInput
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(err)
return 1
}
output := MBIDOutput{MBID: "test-mbid-" + input.Name}
if err := pdk.OutputJSON(output); err != nil {
pdk.SetError(err)
return 1
}
return 0
return metadata.ArtistURLResponse{URL: "https://test.example.com/artist/" + input.Name}, nil
}
//go:wasmexport nd_get_artist_url
func ndGetArtistURL() int32 {
if hasErr, code := checkConfigError(); hasErr {
return code
func (t *testMetadataAgent) GetArtistBiography(input metadata.ArtistRequest) (metadata.ArtistBiographyResponse, error) {
if err := checkConfigError(); err != nil {
return metadata.ArtistBiographyResponse{}, err
}
var input ArtistInput
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(err)
return 1
}
output := URLOutput{URL: "https://test.example.com/artist/" + input.Name}
if err := pdk.OutputJSON(output); err != nil {
pdk.SetError(err)
return 1
}
return 0
return metadata.ArtistBiographyResponse{Biography: "Biography for " + input.Name}, nil
}
//go:wasmexport nd_get_artist_biography
func ndGetArtistBiography() int32 {
if hasErr, code := checkConfigError(); hasErr {
return code
func (t *testMetadataAgent) GetArtistImages(input metadata.ArtistRequest) (metadata.ArtistImagesResponse, error) {
if err := checkConfigError(); err != nil {
return metadata.ArtistImagesResponse{}, err
}
var input ArtistInput
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(err)
return 1
}
output := BiographyOutput{Biography: "Biography for " + input.Name}
if err := pdk.OutputJSON(output); err != nil {
pdk.SetError(err)
return 1
}
return 0
}
//go:wasmexport nd_get_artist_images
func ndGetArtistImages() int32 {
if hasErr, code := checkConfigError(); hasErr {
return code
}
var input ArtistInput
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(err)
return 1
}
output := ImagesOutput{
Images: []ArtistImage{
return metadata.ArtistImagesResponse{
Images: []metadata.ImageInfo{
{URL: "https://test.example.com/images/" + input.Name + "/large.jpg", Size: 500},
{URL: "https://test.example.com/images/" + input.Name + "/small.jpg", Size: 100},
},
}
if err := pdk.OutputJSON(output); err != nil {
pdk.SetError(err)
return 1
}
return 0
}, nil
}
//go:wasmexport nd_get_similar_artists
func ndGetSimilarArtists() int32 {
if hasErr, code := checkConfigError(); hasErr {
return code
func (t *testMetadataAgent) GetSimilarArtists(input metadata.SimilarArtistsRequest) (metadata.SimilarArtistsResponse, error) {
if err := checkConfigError(); err != nil {
return metadata.SimilarArtistsResponse{}, err
}
var input ArtistInputWithLimit
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(err)
return 1
}
limit := input.Limit
limit := int(input.Limit)
if limit == 0 {
limit = 5
}
artists := make([]SimilarArtist, 0, limit)
artists := make([]metadata.ArtistRef, 0, limit)
for i := range limit {
artists = append(artists, SimilarArtist{
artists = append(artists, metadata.ArtistRef{
Name: input.Name + " Similar " + string(rune('A'+i)),
})
}
output := SimilarArtistsOutput{Artists: artists}
if err := pdk.OutputJSON(output); err != nil {
pdk.SetError(err)
return 1
}
return 0
return metadata.SimilarArtistsResponse{Artists: artists}, nil
}
//go:wasmexport nd_get_artist_top_songs
func ndGetArtistTopSongs() int32 {
if hasErr, code := checkConfigError(); hasErr {
return code
func (t *testMetadataAgent) GetArtistTopSongs(input metadata.TopSongsRequest) (metadata.TopSongsResponse, error) {
if err := checkConfigError(); err != nil {
return metadata.TopSongsResponse{}, err
}
var input ArtistInputWithCount
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(err)
return 1
}
count := input.Count
count := int(input.Count)
if count == 0 {
count = 5
}
songs := make([]TopSong, 0, count)
songs := make([]metadata.SongRef, 0, count)
for i := range count {
songs = append(songs, TopSong{
Name: input.Name + " Song " + string(rune('1'+i)),
songs = append(songs, metadata.SongRef{
Name: input.Name + " Song " + strconv.Itoa(i+1),
})
}
output := TopSongsOutput{Songs: songs}
if err := pdk.OutputJSON(output); err != nil {
pdk.SetError(err)
return 1
}
return 0
return metadata.TopSongsResponse{Songs: songs}, nil
}
//go:wasmexport nd_get_album_info
func ndGetAlbumInfo() int32 {
if hasErr, code := checkConfigError(); hasErr {
return code
func (t *testMetadataAgent) GetAlbumInfo(input metadata.AlbumRequest) (metadata.AlbumInfoResponse, error) {
if err := checkConfigError(); err != nil {
return metadata.AlbumInfoResponse{}, err
}
var input AlbumInput
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(err)
return 1
}
output := AlbumInfoOutput{
return metadata.AlbumInfoResponse{
Name: input.Name,
MBID: "test-album-mbid-" + input.Name,
Description: "Description for " + input.Name + " by " + input.Artist,
URL: "https://test.example.com/album/" + input.Name,
}
if err := pdk.OutputJSON(output); err != nil {
pdk.SetError(err)
return 1
}
return 0
}, nil
}
//go:wasmexport nd_get_album_images
func ndGetAlbumImages() int32 {
if hasErr, code := checkConfigError(); hasErr {
return code
func (t *testMetadataAgent) GetAlbumImages(input metadata.AlbumRequest) (metadata.AlbumImagesResponse, error) {
if err := checkConfigError(); err != nil {
return metadata.AlbumImagesResponse{}, err
}
var input AlbumInput
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(err)
return 1
}
output := AlbumImagesOutput{
Images: []ArtistImage{
return metadata.AlbumImagesResponse{
Images: []metadata.ImageInfo{
{URL: "https://test.example.com/albums/" + input.Name + "/cover.jpg", Size: 500},
},
}
if err := pdk.OutputJSON(output); err != nil {
pdk.SetError(err)
return 1
}
return 0
}, nil
}
func main() {}

View File

@ -1,5 +1,14 @@
module test-scheduler
go 1.23
go 1.25
require github.com/extism/go-pdk v1.1.3
require (
github.com/navidrome/navidrome v0.0.0
github.com/navidrome/navidrome/plugins/pdk/go/host v0.0.0
)
require github.com/extism/go-pdk v1.1.3 // indirect
replace github.com/navidrome/navidrome => ../../..
replace github.com/navidrome/navidrome/plugins/pdk/go/host => ../../pdk/go/host

View File

@ -2,24 +2,35 @@
// Build with: tinygo build -o ../test-scheduler.wasm -target wasip1 -buildmode=c-shared .
package main
// NdSchedulerCallback is called when a scheduled task fires.
import (
"github.com/navidrome/navidrome/plugins/pdk/go/host"
"github.com/navidrome/navidrome/plugins/pdk/go/scheduler"
)
func init() {
scheduler.Register(&testScheduler{})
}
type testScheduler struct{}
// OnSchedulerCallback is called when a scheduled task fires.
// Magic payloads trigger specific behaviors to test host functions:
// - "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) error {
func (t *testScheduler) OnSchedulerCallback(input scheduler.SchedulerCallbackRequest) error {
switch {
case input.Payload == "schedule-followup":
if _, err := SchedulerScheduleOneTime(1, "followup-created", "followup-id"); err != nil {
if _, err := host.SchedulerScheduleOneTime(1, "followup-created", "followup-id"); err != nil {
return err
}
case input.Payload == "schedule-recurring":
if _, err := SchedulerScheduleRecurring("@every 1s", "recurring-created", "recurring-from-plugin"); err != nil {
if _, err := host.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:]
if _, err := SchedulerScheduleOneTime(60, "duplicate-attempt", duplicateID); err != nil {
if _, err := host.SchedulerScheduleOneTime(60, "duplicate-attempt", duplicateID); err != nil {
return err
}
}

View File

@ -1,196 +0,0 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains client wrappers for the Scheduler host service.
// It is intended for use in Navidrome plugins built with TinyGo.
//
//go:build wasip1
package main
import (
"encoding/json"
"errors"
"github.com/extism/go-pdk"
)
// scheduler_scheduleonetime is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user scheduler_scheduleonetime
func scheduler_scheduleonetime(uint64) uint64
// scheduler_schedulerecurring is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user scheduler_schedulerecurring
func scheduler_schedulerecurring(uint64) uint64
// scheduler_cancelschedule is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user scheduler_cancelschedule
func scheduler_cancelschedule(uint64) uint64
// SchedulerScheduleOneTimeRequest is the request type for Scheduler.ScheduleOneTime.
type SchedulerScheduleOneTimeRequest struct {
DelaySeconds int32 `json:"delaySeconds"`
Payload string `json:"payload"`
ScheduleID string `json:"scheduleId"`
}
// SchedulerScheduleOneTimeResponse is the response type for Scheduler.ScheduleOneTime.
type SchedulerScheduleOneTimeResponse struct {
NewScheduleID string `json:"newScheduleId,omitempty"`
Error string `json:"error,omitempty"`
}
// SchedulerScheduleRecurringRequest is the request type for Scheduler.ScheduleRecurring.
type SchedulerScheduleRecurringRequest struct {
CronExpression string `json:"cronExpression"`
Payload string `json:"payload"`
ScheduleID string `json:"scheduleId"`
}
// SchedulerScheduleRecurringResponse is the response type for Scheduler.ScheduleRecurring.
type SchedulerScheduleRecurringResponse struct {
NewScheduleID string `json:"newScheduleId,omitempty"`
Error string `json:"error,omitempty"`
}
// SchedulerCancelScheduleRequest is the request type for Scheduler.CancelSchedule.
type SchedulerCancelScheduleRequest struct {
ScheduleID string `json:"scheduleId"`
}
// SchedulerCancelScheduleResponse is the response type for Scheduler.CancelSchedule.
type SchedulerCancelScheduleResponse struct {
Error string `json:"error,omitempty"`
}
// SchedulerScheduleOneTime calls the scheduler_scheduleonetime host function.
// ScheduleOneTime schedules a one-time event to be triggered after the specified delay.
// Plugins that use this function must also implement the SchedulerCallback capability
//
// Parameters:
// - delaySeconds: Number of seconds to wait before triggering the event
// - payload: Data to be passed to the scheduled event handler
// - scheduleID: Optional unique identifier for the scheduled job. If empty, one will be generated
//
// Returns the schedule ID that can be used to cancel the job, or an error if scheduling fails.
func SchedulerScheduleOneTime(delaySeconds int32, payload string, scheduleID string) (*SchedulerScheduleOneTimeResponse, error) {
// Marshal request to JSON
req := SchedulerScheduleOneTimeRequest{
DelaySeconds: delaySeconds,
Payload: payload,
ScheduleID: scheduleID,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := scheduler_scheduleonetime(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response SchedulerScheduleOneTimeResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// SchedulerScheduleRecurring calls the scheduler_schedulerecurring host function.
// ScheduleRecurring schedules a recurring event using a cron expression.
// Plugins that use this function must also implement the SchedulerCallback capability
//
// Parameters:
// - cronExpression: Standard cron format expression (e.g., "0 0 * * *" for daily at midnight)
// - payload: Data to be passed to each scheduled event handler invocation
// - scheduleID: Optional unique identifier for the scheduled job. If empty, one will be generated
//
// Returns the schedule ID that can be used to cancel the job, or an error if scheduling fails.
func SchedulerScheduleRecurring(cronExpression string, payload string, scheduleID string) (*SchedulerScheduleRecurringResponse, error) {
// Marshal request to JSON
req := SchedulerScheduleRecurringRequest{
CronExpression: cronExpression,
Payload: payload,
ScheduleID: scheduleID,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := scheduler_schedulerecurring(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response SchedulerScheduleRecurringResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// SchedulerCancelSchedule calls the scheduler_cancelschedule host function.
// CancelSchedule cancels a scheduled job identified by its schedule ID.
//
// This works for both one-time and recurring schedules. Once cancelled, the job will not trigger
// any future events.
//
// Returns an error if the schedule ID is not found or if cancellation fails.
func SchedulerCancelSchedule(scheduleID string) (*SchedulerCancelScheduleResponse, error) {
// Marshal request to JSON
req := SchedulerCancelScheduleRequest{
ScheduleID: scheduleID,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := scheduler_cancelschedule(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response SchedulerCancelScheduleResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}

View File

@ -1,35 +0,0 @@
// THIS FILE WAS GENERATED BY `xtp-go-bindgen`. DO NOT EDIT.
package main
import (
pdk "github.com/extism/go-pdk"
)
//go:wasmexport nd_scheduler_callback
func _NdSchedulerCallback() int32 {
var input SchedulerCallbackInput
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(err)
return -1
}
if err := NdSchedulerCallback(input); err != nil {
pdk.SetError(err)
return -1
}
return 0
}
// Input provided to the scheduler callback when a scheduled task fires
type SchedulerCallbackInput struct {
// True if this is a recurring schedule (created via ScheduleRecurring),
// false if it's a one-time schedule (created via ScheduleOneTime).
IsRecurring bool `json:"isRecurring"`
// The payload data that was provided when the task was scheduled.
// Can be used to pass context or parameters to the callback handler.
Payload string `json:"payload"`
// The unique identifier for this scheduled task. This is either the ID
// provided when scheduling, or an auto-generated UUID if none was specified.
ScheduleId string `json:"scheduleId"`
}

View File

@ -2,4 +2,9 @@ module test-subsonicapi-plugin
go 1.24
require github.com/extism/go-pdk v1.1.3
require (
github.com/extism/go-pdk v1.1.3
github.com/navidrome/navidrome/plugins/pdk/go/host v0.0.0
)
replace github.com/navidrome/navidrome/plugins/pdk/go/host => ../../pdk/go/host

View File

@ -4,6 +4,7 @@ package main
import (
"github.com/extism/go-pdk"
"github.com/navidrome/navidrome/plugins/pdk/go/host"
)
// call_subsonic_api is the exported function that tests the SubsonicAPI host function.
@ -16,7 +17,7 @@ func callSubsonicAPIExport() int32 {
uri := pdk.InputString()
// Call the Subsonic API via host function
response, err := SubsonicAPICall(uri)
response, err := host.SubsonicAPICall(uri)
if err != nil {
pdk.SetErrorString("failed to call SubsonicAPI: " + err.Error())
return 1

View File

@ -1,69 +0,0 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains client wrappers for the SubsonicAPI host service.
// It is intended for use in Navidrome plugins built with TinyGo.
//
//go:build wasip1
package main
import (
"encoding/json"
"errors"
"github.com/extism/go-pdk"
)
// subsonicapi_call is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user subsonicapi_call
func subsonicapi_call(uint64) uint64
// SubsonicAPICallRequest is the request type for SubsonicAPI.Call.
type SubsonicAPICallRequest struct {
Uri string `json:"uri"`
}
// SubsonicAPICallResponse is the response type for SubsonicAPI.Call.
type SubsonicAPICallResponse struct {
ResponseJSON string `json:"responseJson,omitempty"`
Error string `json:"error,omitempty"`
}
// SubsonicAPICall calls the subsonicapi_call host function.
// Call executes a Subsonic API request and returns the JSON response.
//
// The uri parameter should be the Subsonic API path without the server prefix,
// e.g., "getAlbumList2?type=random&size=10". The response is returned as raw JSON.
func SubsonicAPICall(uri string) (*SubsonicAPICallResponse, error) {
// Marshal request to JSON
req := SubsonicAPICallRequest{
Uri: uri,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := subsonicapi_call(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response SubsonicAPICallResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}

View File

@ -1,5 +1,13 @@
module test-websocket
go 1.23
go 1.25
require github.com/extism/go-pdk v1.1.3
require (
github.com/extism/go-pdk v1.1.3
github.com/navidrome/navidrome v0.0.0
github.com/navidrome/navidrome/plugins/pdk/go/host v0.0.0
)
replace github.com/navidrome/navidrome => ../../..
replace github.com/navidrome/navidrome/plugins/pdk/go/host => ../../pdk/go/host

View File

@ -6,118 +6,63 @@ import (
"errors"
pdk "github.com/extism/go-pdk"
"github.com/navidrome/navidrome/plugins/pdk/go/host"
"github.com/navidrome/navidrome/plugins/pdk/go/websocket"
)
// OnTextMessageInput is the input for nd_websocket_on_text_message callback.
type OnTextMessageInput struct {
ConnectionID string `json:"connectionId"`
Message string `json:"message"`
func init() {
websocket.Register(&testWebSocket{})
}
// nd_websocket_on_text_message is called when a text message is received.
type testWebSocket struct{}
// OnTextMessage 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
// - "close": closes the connection using CloseConnection host function
// - "store:MESSAGE": stores MESSAGE in plugin config for later retrieval
// - "fail": returns an error to test error handling
//
//go:wasmexport nd_websocket_on_text_message
func ndWebSocketOnTextMessage() int32 {
var input OnTextMessageInput
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(err)
return -1
}
func (t *testWebSocket) OnTextMessage(input websocket.OnTextMessageRequest) error {
// Store all received messages for test verification
storeReceivedMessage("text:" + input.Message)
switch input.Message {
case "echo":
if _, err := WebSocketSendText(input.ConnectionID, "echo:"+input.Message); err != nil {
pdk.SetError(err)
return -1
if _, err := host.WebSocketSendText(input.ConnectionID, "echo:"+input.Message); err != nil {
return err
}
case "close":
if _, err := WebSocketCloseConnection(input.ConnectionID, 1000, "closed by plugin"); err != nil {
pdk.SetError(err)
return -1
if _, err := host.WebSocketCloseConnection(input.ConnectionID, 1000, "closed by plugin"); err != nil {
return err
}
case "fail":
pdk.SetError(errors.New("intentional test failure"))
return -1
return errors.New("intentional test failure")
}
return 0
return nil
}
// OnBinaryMessageInput is the input for nd_websocket_on_binary_message callback.
type OnBinaryMessageInput struct {
ConnectionID string `json:"connectionId"`
Data string `json:"data"` // Base64 encoded
}
// 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 {
pdk.SetError(err)
return -1
}
// OnBinaryMessage is called when a binary message is received.
func (t *testWebSocket) OnBinaryMessage(input websocket.OnBinaryMessageRequest) error {
// Store received binary data for test verification
storeReceivedMessage("binary:" + input.Data)
return 0
return nil
}
// OnErrorInput is the input for nd_websocket_on_error callback.
type OnErrorInput struct {
ConnectionID string `json:"connectionId"`
Error string `json:"error"`
}
// 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 {
pdk.SetError(err)
return -1
}
// OnError is called when an error occurs on a WebSocket connection.
func (t *testWebSocket) OnError(input websocket.OnErrorRequest) error {
// Store error for test verification
storeReceivedMessage("error:" + input.Error)
return 0
return nil
}
// OnCloseInput is the input for nd_websocket_on_close callback.
type OnCloseInput struct {
ConnectionID string `json:"connectionId"`
Code int `json:"code"`
Reason string `json:"reason"`
}
// 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 {
pdk.SetError(err)
return -1
}
// OnClose is called when a WebSocket connection is closed.
func (t *testWebSocket) OnClose(input websocket.OnCloseRequest) error {
// Store close event for test verification
storeReceivedMessage("close:" + input.Reason)
return 0
return nil
}
// storeReceivedMessage stores messages in plugin variable storage for test verification.

View File

@ -1,258 +0,0 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains client wrappers for the WebSocket host service.
// It is intended for use in Navidrome plugins built with TinyGo.
//
//go:build wasip1
package main
import (
"encoding/json"
"errors"
"github.com/extism/go-pdk"
)
// websocket_connect is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user websocket_connect
func websocket_connect(uint64) uint64
// websocket_sendtext is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user websocket_sendtext
func websocket_sendtext(uint64) uint64
// websocket_sendbinary is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user websocket_sendbinary
func websocket_sendbinary(uint64) uint64
// websocket_closeconnection is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user websocket_closeconnection
func websocket_closeconnection(uint64) uint64
// WebSocketConnectRequest is the request type for WebSocket.Connect.
type WebSocketConnectRequest struct {
Url string `json:"url"`
Headers map[string]string `json:"headers"`
ConnectionID string `json:"connectionId"`
}
// WebSocketConnectResponse is the response type for WebSocket.Connect.
type WebSocketConnectResponse struct {
NewConnectionID string `json:"newConnectionId,omitempty"`
Error string `json:"error,omitempty"`
}
// WebSocketSendTextRequest is the request type for WebSocket.SendText.
type WebSocketSendTextRequest struct {
ConnectionID string `json:"connectionId"`
Message string `json:"message"`
}
// WebSocketSendTextResponse is the response type for WebSocket.SendText.
type WebSocketSendTextResponse struct {
Error string `json:"error,omitempty"`
}
// WebSocketSendBinaryRequest is the request type for WebSocket.SendBinary.
type WebSocketSendBinaryRequest struct {
ConnectionID string `json:"connectionId"`
Data []byte `json:"data"`
}
// WebSocketSendBinaryResponse is the response type for WebSocket.SendBinary.
type WebSocketSendBinaryResponse struct {
Error string `json:"error,omitempty"`
}
// WebSocketCloseConnectionRequest is the request type for WebSocket.CloseConnection.
type WebSocketCloseConnectionRequest struct {
ConnectionID string `json:"connectionId"`
Code int32 `json:"code"`
Reason string `json:"reason"`
}
// WebSocketCloseConnectionResponse is the response type for WebSocket.CloseConnection.
type WebSocketCloseConnectionResponse struct {
Error string `json:"error,omitempty"`
}
// WebSocketConnect calls the websocket_connect host function.
// Connect establishes a WebSocket connection to the specified URL.
//
// Plugins that use this function must also implement the WebSocketCallback capability
// to receive incoming messages and connection events.
//
// Parameters:
// - url: The WebSocket URL to connect to (ws:// or wss://)
// - headers: Optional HTTP headers to include in the handshake request
// - connectionID: Optional unique identifier for the connection. If empty, one will be generated
//
// Returns the connection ID that can be used to send messages or close the connection,
// or an error if the connection fails.
func WebSocketConnect(url string, headers map[string]string, connectionID string) (*WebSocketConnectResponse, error) {
// Marshal request to JSON
req := WebSocketConnectRequest{
Url: url,
Headers: headers,
ConnectionID: connectionID,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := websocket_connect(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response WebSocketConnectResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// WebSocketSendText calls the websocket_sendtext host function.
// SendText sends a text message over an established WebSocket connection.
//
// Parameters:
// - connectionID: The connection identifier returned by Connect
// - message: The text message to send
//
// Returns an error if the connection is not found or if sending fails.
func WebSocketSendText(connectionID string, message string) (*WebSocketSendTextResponse, error) {
// Marshal request to JSON
req := WebSocketSendTextRequest{
ConnectionID: connectionID,
Message: message,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := websocket_sendtext(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response WebSocketSendTextResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// WebSocketSendBinary calls the websocket_sendbinary host function.
// SendBinary sends binary data over an established WebSocket connection.
//
// Parameters:
// - connectionID: The connection identifier returned by Connect
// - data: The binary data to send
//
// Returns an error if the connection is not found or if sending fails.
func WebSocketSendBinary(connectionID string, data []byte) (*WebSocketSendBinaryResponse, error) {
// Marshal request to JSON
req := WebSocketSendBinaryRequest{
ConnectionID: connectionID,
Data: data,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := websocket_sendbinary(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response WebSocketSendBinaryResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// WebSocketCloseConnection calls the websocket_closeconnection host function.
// CloseConnection gracefully closes a WebSocket connection.
//
// Parameters:
// - connectionID: The connection identifier returned by Connect
// - code: WebSocket close status code (e.g., 1000 for normal closure)
// - reason: Optional human-readable reason for closing
//
// Returns an error if the connection is not found or if closing fails.
func WebSocketCloseConnection(connectionID string, code int32, reason string) (*WebSocketCloseConnectionResponse, error) {
// Marshal request to JSON
req := WebSocketCloseConnectionRequest{
ConnectionID: connectionID,
Code: code,
Reason: reason,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := websocket_closeconnection(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response WebSocketCloseConnectionResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}