feat(plugins): add Go client library with host function wrappers and documentation

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan 2025-12-29 16:41:19 -05:00
parent b8ff660485
commit 1d18b3a092
45 changed files with 675 additions and 1877 deletions

View File

@ -327,17 +327,26 @@ func ndSchedulerCallback() int32 {
**Scheduling tasks (using generated SDK):**
Copy `plugins/host/go/nd_host_scheduler.go` to your plugin and use:
Add the generated SDK to your `go.mod`:
```
require github.com/navidrome/navidrome/plugins/host/go v0.0.0
replace github.com/navidrome/navidrome/plugins/host/go => ../../host/go
```
Then import and use:
```go
import ndhost "github.com/navidrome/navidrome/plugins/host/go"
// Schedule one-time task in 60 seconds
scheduleID, err := SchedulerScheduleOneTime(60, "my-payload", "")
scheduleID, err := ndhost.SchedulerScheduleOneTime(60, "my-payload", "")
// Schedule recurring task with cron expression (every hour)
scheduleID, err := SchedulerScheduleRecurring("0 * * * *", "hourly-task", "")
scheduleID, err := ndhost.SchedulerScheduleRecurring("0 * * * *", "hourly-task", "")
// Cancel a task
err := SchedulerCancelSchedule(scheduleID)
err := ndhost.SchedulerCancelSchedule(scheduleID)
```
### Cache
@ -375,14 +384,16 @@ Store and retrieve data in an in-memory TTL-based cache. Each plugin has its own
**Usage (with generated SDK):**
Copy `plugins/host/go/nd_host_cache.go` to your plugin:
Import the Go SDK (see [Scheduler](#scheduler) for `go.mod` setup):
```go
import ndhost "github.com/navidrome/navidrome/plugins/host/go"
// Cache a value for 1 hour
CacheSetString("api-response", responseData, 3600)
ndhost.CacheSetString("api-response", responseData, 3600)
// Retrieve (check Exists before using Value)
result, err := CacheGetString("api-response")
result, err := ndhost.CacheGetString("api-response")
if result.Exists {
data := result.Value
}
@ -427,22 +438,24 @@ Persistent key-value storage that survives server restarts. Each plugin has its
**Usage (with generated SDK):**
Copy `plugins/host/go/nd_host_kvstore.go` to your plugin:
Import the Go SDK (see [Scheduler](#scheduler) for `go.mod` setup):
```go
import ndhost "github.com/navidrome/navidrome/plugins/host/go"
// Store a value (as raw bytes)
token := []byte(`{"access_token": "xyz", "refresh_token": "abc"}`)
_, err := KVStoreSet("oauth:spotify", token)
_, err := ndhost.KVStoreSet("oauth:spotify", token)
// Retrieve a value
result, err := KVStoreGet("oauth:spotify")
result, err := ndhost.KVStoreGet("oauth:spotify")
if result.Exists {
var tokenData map[string]string
json.Unmarshal(result.Value, &tokenData)
}
// List all keys with prefix
keysResult, err := KVStoreList("user:")
keysResult, err := ndhost.KVStoreList("user:")
for _, key := range keysResult.Keys {
// Process each key
}
@ -555,33 +568,22 @@ entries, err := os.ReadDir("/libraries/1/Artist")
**Usage (with generated SDK):**
Copy `plugins/host/go/nd_host_library.go` to your plugin. You'll also need to add the `Library` struct definition:
Import the Go SDK (see [Scheduler](#scheduler) for `go.mod` setup). The `Library` struct is provided by the SDK:
```go
// Library represents a music library with metadata.
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"`
}
import ndhost "github.com/navidrome/navidrome/plugins/host/go"
// Get a specific library
resp, err := LibraryGetLibrary(1)
resp, err := ndhost.LibraryGetLibrary(1)
if err != nil {
// Handle error
}
library := resp.Result
// Get all libraries
resp, err := LibraryGetAllLibraries()
resp, err := ndhost.LibraryGetAllLibraries()
for _, lib := range resp.Result {
// lib is of type ndhost.Library
fmt.Printf("Library: %s (%d songs)\n", lib.Name, lib.TotalSongs)
}
```
@ -746,9 +748,22 @@ See [schemas/README.md](schemas/README.md) for available schemas.
### Using Host Service SDKs
Generated SDKs for calling host services are in `plugins/host/go/` and `plugins/host/python/`.
Generated SDKs for calling host services are in `plugins/host/go/`, `plugins/host/python/` and `plugins/host/rust`.
**For Go plugins:** Copy the needed `nd_host_*.go` file to your plugin directory.
**For Go plugins:** Import the SDK as a Go module:
```go
import ndhost "github.com/navidrome/navidrome/plugins/host/go"
```
Add to your `go.mod`:
```
require github.com/navidrome/navidrome/plugins/host/go v0.0.0
replace github.com/navidrome/navidrome/plugins/host/go => ../../host/go
```
See [plugins/host/go/README.md](host/go/README.md) for detailed documentation.
**For Python plugins:** Copy functions from `nd_host_*.py` into your `__init__.py` (see comments in those files for extism-py limitations).

View File

@ -182,27 +182,56 @@ Generated files are named `<servicename>_gen.go` (lowercase) and placed in the o
- Host function wrappers
- Helper functions (`writeResponse`, `writeErrorResponse`)
### Plugin/Client Code (TinyGo WASM)
### Go Client Library (Go/TinyGo WASM)
Generated files are named `nd_host_<servicename>.go` (lowercase) and placed in the `go/` subdirectory of the output directory. These files are intended for use in Navidrome plugins built with TinyGo. Each file includes:
Generated files are named `nd_host_<servicename>.go` (lowercase) and placed in the `go/` subdirectory of the output directory. The `go/` directory is a complete Go module (`github.com/navidrome/navidrome/plugins/host/go`) with package name `ndhost`, intended for import by Navidrome plugins built with TinyGo.
The generator also creates:
- `doc.go` - Package documentation listing all available services
- `go.mod` - Go module file with required dependencies
Each service file includes:
- `// Code generated by hostgen. DO NOT EDIT.` header
- Required imports (`encoding/json`, `errors`, `github.com/extism/go-pdk`)
- `//go:wasmimport` declarations for each host function
- Response struct types
- Response struct types and any struct definitions from the service
- Wrapper functions that handle memory allocation and JSON parsing
#### Using the Go SDK
Import the SDK in your plugin:
```go
import ndhost "github.com/navidrome/navidrome/plugins/host/go"
// Use host services with the ndhost prefix
result, err := ndhost.CacheGetString("my-key")
scheduleID, err := ndhost.SchedulerScheduleOneTime(60, "payload", "")
```
Add to your `go.mod`:
```
require github.com/navidrome/navidrome/plugins/host/go v0.0.0
replace github.com/navidrome/navidrome/plugins/host/go => ../../host/go
```
See [plugins/host/go/README.md](../../host/go/README.md) for detailed documentation.
### Example Output Structure
```
output/
├── subsonicapi_gen.go # Host-side code (for Navidrome)
├── go/
│ └── nd_host_subsonicapi.go # Plugin-side code (for TinyGo plugins)
│ ├── doc.go # Package documentation
│ ├── go.mod # Go module file
│ └── nd_host_subsonicapi.go # Plugin-side code (for TinyGo plugins)
├── python/
│ └── nd_host_subsonicapi.py # Plugin-side code (for Python plugins)
│ └── nd_host_subsonicapi.py # Plugin-side code (for Python plugins)
└── rust/
└── nd_host_subsonicapi.rs # Plugin-side code (for Rust plugins)
└── nd_host_subsonicapi.rs # Plugin-side code (for Rust plugins)
```
### Python Client Code (extism-py WASM)

View File

@ -1,6 +1,7 @@
package main
import (
"fmt"
"go/format"
"os"
"os/exec"
@ -238,9 +239,19 @@ type ServiceB interface {
goDir := filepath.Join(outputDir, "go")
goClientEntries, err := os.ReadDir(goDir)
Expect(err).ToNot(HaveOccurred())
Expect(goClientEntries).To(HaveLen(1), "Expected exactly one Go client file")
Expect(goClientEntries).To(HaveLen(3), "Expected Go client file, doc.go, and go.mod")
goClientActual, err := os.ReadFile(filepath.Join(goDir, goClientEntries[0].Name()))
// Find the client file (not doc.go or go.mod)
var goClientName string
for _, entry := range goClientEntries {
if entry.Name() != "doc.go" && entry.Name() != "go.mod" {
goClientName = entry.Name()
break
}
}
Expect(goClientName).ToNot(BeEmpty(), "Expected to find Go client file")
goClientActual, err := os.ReadFile(filepath.Join(goDir, goClientName))
Expect(err).ToNot(HaveOccurred())
formattedGoClientActual, err := format.Source(goClientActual)
@ -357,9 +368,19 @@ type ServiceB interface {
goDir := filepath.Join(clientDir, "go")
entries, err := os.ReadDir(goDir)
Expect(err).ToNot(HaveOccurred())
Expect(entries).To(HaveLen(1), "Expected exactly one generated client file")
Expect(entries).To(HaveLen(3), "Expected Go client file, doc.go, and go.mod")
content, err := os.ReadFile(filepath.Join(goDir, entries[0].Name()))
// Find the client file (not doc.go or go.mod)
var clientFileName string
for _, entry := range entries {
if entry.Name() != "doc.go" && entry.Name() != "go.mod" {
clientFileName = entry.Name()
break
}
}
Expect(clientFileName).ToNot(BeEmpty(), "Expected to find Go client file")
content, err := os.ReadFile(filepath.Join(goDir, clientFileName))
Expect(err).ToNot(HaveOccurred())
// Verify key expected content first
@ -380,49 +401,55 @@ type ServiceB interface {
Expect(contentStr).To(ContainSubstring("func ComprehensiveNoParams()"))
Expect(contentStr).To(ContainSubstring("func ComprehensiveNoParamsNoReturns()"))
// Move generated file to clientDir root for compilation
Expect(os.Rename(filepath.Join(goDir, entries[0].Name()), filepath.Join(clientDir, "nd_host.go"))).To(Succeed())
// The generated code is now package ndhost, so we need to import it
// Create a plugin directory with proper import structure
pluginDir := filepath.Join(clientDir, "plugin")
Expect(os.MkdirAll(pluginDir, 0750)).To(Succeed())
// Create go.mod for client code
goMod := "module main\n\ngo 1.23\n\nrequire github.com/extism/go-pdk v1.1.1\n"
Expect(os.WriteFile(filepath.Join(clientDir, "go.mod"), []byte(goMod), 0600)).To(Succeed())
// Create go.mod for the plugin that imports the generated library
goMod := fmt.Sprintf(`module testplugin
// Add a simple main function for the plugin
go 1.24
require github.com/navidrome/navidrome/plugins/host/go v0.0.0
replace github.com/navidrome/navidrome/plugins/host/go => %s
`, goDir)
Expect(os.WriteFile(filepath.Join(pluginDir, "go.mod"), []byte(goMod), 0600)).To(Succeed())
// Add a simple main function that imports and uses the ndhost package
mainGo := `package main
import ndhost "github.com/navidrome/navidrome/plugins/host/go"
func main() {}
// Use some functions to ensure import is not unused
var _ = ndhost.ComprehensiveNoParams
`
Expect(os.WriteFile(filepath.Join(clientDir, "main.go"), []byte(mainGo), 0600)).To(Succeed())
Expect(os.WriteFile(filepath.Join(pluginDir, "main.go"), []byte(mainGo), 0600)).To(Succeed())
// Add type definitions needed by the generated code
typesGo := `package main
// Tidy dependencies for the generated go library
goTidyLibCmd := exec.Command("go", "mod", "tidy")
goTidyLibCmd.Dir = goDir
goTidyLibOutput, err := goTidyLibCmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "go mod tidy (library) failed: %s", goTidyLibOutput)
type User2 struct {
ID string
Name string
}
type Filter2 struct {
Active bool
}
`
Expect(os.WriteFile(filepath.Join(clientDir, "types.go"), []byte(typesGo), 0600)).To(Succeed())
// Tidy dependencies
// Tidy dependencies for the plugin
goTidyCmd := exec.Command("go", "mod", "tidy")
goTidyCmd.Dir = clientDir
goTidyCmd.Dir = pluginDir
goTidyOutput, err := goTidyCmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "go mod tidy failed: %s", goTidyOutput)
Expect(err).ToNot(HaveOccurred(), "go mod tidy (plugin) failed: %s", goTidyOutput)
// Build as WASM plugin - this validates the client code compiles correctly
buildCmd := exec.Command("go", "build", "-buildmode=c-shared", "-o", "plugin.wasm", ".")
buildCmd.Dir = clientDir
buildCmd.Dir = pluginDir
buildCmd.Env = append(os.Environ(), "GOOS=wasip1", "GOARCH=wasm")
buildOutput, err := buildCmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "WASM build failed: %s", buildOutput)
// Verify .wasm file was created
Expect(filepath.Join(clientDir, "plugin.wasm")).To(BeAnExistingFile())
Expect(filepath.Join(pluginDir, "plugin.wasm")).To(BeAnExistingFile())
})
It("generates Python client code with -python flag", func() {

View File

@ -236,3 +236,40 @@ func GenerateRustLib(services []Service) ([]byte, error) {
return buf.Bytes(), nil
}
// GenerateGoDoc generates the doc.go file that provides package documentation.
func GenerateGoDoc(services []Service) ([]byte, error) {
tmplContent, err := templatesFS.ReadFile("templates/doc_go.go.tmpl")
if err != nil {
return nil, fmt.Errorf("reading Go doc template: %w", err)
}
tmpl, err := template.New("doc_go").Funcs(template.FuncMap{
"firstLine": firstLine,
}).Parse(string(tmplContent))
if err != nil {
return nil, fmt.Errorf("parsing template: %w", err)
}
data := struct {
Services []Service
}{
Services: services,
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return nil, fmt.Errorf("executing template: %w", err)
}
return buf.Bytes(), nil
}
// GenerateGoMod generates the go.mod file for the Go client library.
func GenerateGoMod() ([]byte, error) {
tmplContent, err := templatesFS.ReadFile("templates/go.mod.tmpl")
if err != nil {
return nil, fmt.Errorf("reading go.mod template: %w", err)
}
return tmplContent, nil
}

View File

@ -525,6 +525,68 @@ var _ = Describe("Generator", func() {
})
})
Describe("GenerateGoDoc", func() {
It("should generate valid doc.go content for multiple services", func() {
services := []Service{
{
Name: "Cache",
Permission: "cache",
Interface: "CacheService",
Doc: "CacheService provides temporary key-value storage with TTL.",
},
{
Name: "Scheduler",
Permission: "scheduler",
Interface: "SchedulerService",
Doc: "SchedulerService manages scheduled tasks.",
},
}
code, err := GenerateGoDoc(services)
Expect(err).NotTo(HaveOccurred())
// Verify it's valid Go code
_, err = format.Source(code)
Expect(err).NotTo(HaveOccurred())
codeStr := string(code)
// Check for generated header
Expect(codeStr).To(ContainSubstring("Code generated by hostgen. DO NOT EDIT."))
// Check for package declaration
Expect(codeStr).To(ContainSubstring("package ndhost"))
// Check for build tag
Expect(codeStr).To(ContainSubstring("//go:build wasip1"))
// Check for package documentation
Expect(codeStr).To(ContainSubstring("Package ndhost provides Navidrome host function wrappers"))
// Check that services are listed
Expect(codeStr).To(ContainSubstring("Cache:"))
Expect(codeStr).To(ContainSubstring("Scheduler:"))
})
})
Describe("GenerateGoMod", func() {
It("should generate valid go.mod content", func() {
code, err := GenerateGoMod()
Expect(err).NotTo(HaveOccurred())
codeStr := string(code)
// Check for module declaration
Expect(codeStr).To(ContainSubstring("module github.com/navidrome/navidrome/plugins/host/go"))
// Check for Go version
Expect(codeStr).To(ContainSubstring("go 1.24"))
// Check for extism-go-pdk dependency
Expect(codeStr).To(ContainSubstring("github.com/extism/go-pdk"))
})
})
Describe("Integration", func() {
It("should generate compilable code from parsed source", func() {
// This is an integration test that verifies the full pipeline

View File

@ -5,7 +5,7 @@
//
//go:build wasip1
package main
package ndhost
import (
"encoding/json"
@ -14,6 +14,20 @@ import (
"github.com/extism/go-pdk"
)
{{- /* Generate struct definitions */ -}}
{{- range .Service.Structs}}
// {{.Name}} represents the {{.Name}} data structure.
{{- if .Doc}}
{{formatDoc .Doc}}
{{- end}}
type {{.Name}} struct {
{{- range .Fields}}
{{.Name}} {{.Type}} `json:"{{.JSONTag}}"`
{{- end}}
}
{{- end}}
{{- /* Generate wasmimport declarations for each method */ -}}
{{range .Service.Methods}}

View File

@ -0,0 +1,51 @@
// Code generated by hostgen. DO NOT EDIT.
//go:build wasip1
/*
Package ndhost provides Navidrome host function wrappers for Go/TinyGo plugins.
This package is auto-generated by the hostgen tool and should not be edited manually.
# Usage
Add this module as a dependency in your plugin's go.mod:
require github.com/navidrome/navidrome/plugins/host/go v0.0.0
Then import the package in your plugin code:
import ndhost "github.com/navidrome/navidrome/plugins/host/go"
func myPluginFunction() error {
// Use the cache service
_, err := ndhost.CacheSetString("my_key", "my_value", 3600)
if err != nil {
return err
}
// Schedule a recurring task
_, err = ndhost.SchedulerScheduleRecurring("@every 5m", "payload", "task_id")
if err != nil {
return err
}
return nil
}
# Available Services
The following host services are available:
{{range .Services}}
- {{.Name}}: {{if .Doc}}{{.Doc | firstLine}}{{else}}{{.Name}} service{{end}}
{{- end}}
# Building Plugins
Go plugins must be compiled to WebAssembly using TinyGo:
tinygo build -o plugin.wasm -target=wasip1 -buildmode=c-shared .
See the examples directory for complete plugin implementations.
*/
package ndhost

View File

@ -0,0 +1,5 @@
module github.com/navidrome/navidrome/plugins/host/go
go 1.24
require github.com/extism/go-pdk v1.1.3

View File

@ -183,6 +183,15 @@ func generateAllCode(cfg *config, services []internal.Service) error {
}
}
if cfg.generateGoClient && len(services) > 0 {
if err := generateGoDocFile(services, cfg.outputDir, cfg.dryRun, cfg.verbose); err != nil {
return fmt.Errorf("generating Go doc.go: %w", err)
}
if err := generateGoModFile(cfg.outputDir, cfg.dryRun, cfg.verbose); err != nil {
return fmt.Errorf("generating Go go.mod: %w", err)
}
}
return nil
}
@ -342,3 +351,68 @@ func generateRustLibFile(services []internal.Service, outputDir string, dryRun,
}
return nil
}
// generateGoDocFile generates the doc.go file for the Go library.
func generateGoDocFile(services []internal.Service, outputDir string, dryRun, verbose bool) error {
code, err := internal.GenerateGoDoc(services)
if err != nil {
return fmt.Errorf("generating doc.go: %w", err)
}
formatted, err := format.Source(code)
if err != nil {
return fmt.Errorf("formatting doc.go: %w\nRaw code:\n%s", err, code)
}
clientDir := filepath.Join(outputDir, "go")
docFile := filepath.Join(clientDir, "doc.go")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", docFile, formatted)
return nil
}
// Create go/ subdirectory if needed
if err := os.MkdirAll(clientDir, 0755); err != nil {
return fmt.Errorf("creating go client directory: %w", err)
}
if err := os.WriteFile(docFile, formatted, 0600); err != nil {
return fmt.Errorf("writing file: %w", err)
}
if verbose {
fmt.Printf("Generated Go doc.go: %s\n", docFile)
}
return nil
}
// generateGoModFile generates the go.mod file for the Go library.
func generateGoModFile(outputDir string, dryRun, verbose bool) error {
code, err := internal.GenerateGoMod()
if err != nil {
return fmt.Errorf("generating go.mod: %w", err)
}
clientDir := filepath.Join(outputDir, "go")
modFile := filepath.Join(clientDir, "go.mod")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", modFile, code)
return nil
}
// Create go/ subdirectory if needed
if err := os.MkdirAll(clientDir, 0755); err != nil {
return fmt.Errorf("creating go client directory: %w", err)
}
if err := os.WriteFile(modFile, code, 0600); err != nil {
return fmt.Errorf("writing file: %w", err)
}
if verbose {
fmt.Printf("Generated Go go.mod: %s\n", modFile)
}
return nil
}

View File

@ -5,7 +5,7 @@
//
//go:build wasip1
package main
package ndhost
import (
"encoding/json"

View File

@ -5,7 +5,7 @@
//
//go:build wasip1
package main
package ndhost
import (
"encoding/json"

View File

@ -5,7 +5,7 @@
//
//go:build wasip1
package main
package ndhost
import (
"encoding/json"

View File

@ -5,7 +5,7 @@
//
//go:build wasip1
package main
package ndhost
import (
"encoding/json"
@ -14,6 +14,11 @@ import (
"github.com/extism/go-pdk"
)
// Filter represents the Filter data structure.
type Filter struct {
Active bool `json:"active"`
}
// list_items is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user list_items

View File

@ -5,7 +5,7 @@
//
//go:build wasip1
package main
package ndhost
import (
"encoding/json"

View File

@ -5,7 +5,7 @@
//
//go:build wasip1
package main
package ndhost
import (
"encoding/json"

View File

@ -5,7 +5,7 @@
//
//go:build wasip1
package main
package ndhost
import (
"encoding/json"

View File

@ -5,7 +5,7 @@
//
//go:build wasip1
package main
package ndhost
import (
"encoding/json"
@ -14,6 +14,11 @@ import (
"github.com/extism/go-pdk"
)
// Result represents the Result data structure.
type Result struct {
ID string `json:"id"`
}
// search_find is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user search_find

View File

@ -5,7 +5,7 @@
//
//go:build wasip1
package main
package ndhost
import (
"encoding/json"
@ -14,6 +14,12 @@ import (
"github.com/extism/go-pdk"
)
// Item represents the Item data structure.
type Item struct {
ID string `json:"id"`
Name string `json:"name"`
}
// store_save is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user store_save

View File

@ -5,7 +5,7 @@
//
//go:build wasip1
package main
package ndhost
import (
"encoding/json"
@ -14,6 +14,12 @@ import (
"github.com/extism/go-pdk"
)
// User represents the User data structure.
type User struct {
ID string `json:"id"`
Name string `json:"name"`
}
// users_get is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user users_get

View File

@ -76,7 +76,22 @@ Copy the resulting `crypto-ticker.ndp` to your Navidrome plugins folder.
- `main.go` - Main plugin implementation
- `pdk.gen.go` - Generated WebSocket callback types (from XTP)
- `nd_host.go` - Host function wrappers for WebSocket and Scheduler services
- `go.mod` - Go module file (imports `ndhost` SDK)
## Host SDK
This plugin imports the Go host SDK directly:
```go
import ndhost "github.com/navidrome/navidrome/plugins/host/go"
```
The `go.mod` file uses a `replace` directive to point to the local SDK:
```
require github.com/navidrome/navidrome/plugins/host/go v0.0.0
replace github.com/navidrome/navidrome/plugins/host/go => ../../host/go
```
---

View File

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

View File

@ -1,2 +1,2 @@
github.com/extism/go-pdk v1.1.0 h1:K2On6XOERxrYdsgu0uLzCxeu/FYRHE8jId/hdEVSYoY=
github.com/extism/go-pdk v1.1.0/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4=
github.com/extism/go-pdk v1.1.3 h1:hfViMPWrqjN6u67cIYRALZTZLk/enSPpNKa+rZ9X2SQ=
github.com/extism/go-pdk v1.1.3/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4=

View File

@ -13,6 +13,7 @@ import (
"strings"
pdk "github.com/extism/go-pdk"
ndhost "github.com/navidrome/navidrome/plugins/host/go"
)
const (
@ -115,7 +116,7 @@ func parseTickerSymbols(tickerConfig string) []string {
// connectAndSubscribe connects to Coinbase WebSocket and subscribes to tickers
func connectAndSubscribe(tickers []string) error {
// Connect to WebSocket using host function
resp, err := WebSocketConnect(coinbaseWSEndpoint, nil, connectionID)
resp, err := ndhost.WebSocketConnect(coinbaseWSEndpoint, nil, connectionID)
if err != nil {
return fmt.Errorf("WebSocket connection error: %w", err)
}
@ -134,7 +135,7 @@ func connectAndSubscribe(tickers []string) error {
}
// Send subscription message
_, err = WebSocketSendText(connectionID, string(subscriptionJSON))
_, err = ndhost.WebSocketSendText(connectionID, string(subscriptionJSON))
if err != nil {
return fmt.Errorf("WebSocket send error: %w", err)
}
@ -205,7 +206,7 @@ func NdWebsocketOnClose(input OnCloseInput) (OnCloseOutput, error) {
pdk.Log(pdk.LogInfo, "Scheduling reconnection attempt in 5 seconds...")
// Schedule a one-time reconnection attempt
_, err := SchedulerScheduleOneTime(5, "reconnect", reconnectScheduleID)
_, err := ndhost.SchedulerScheduleOneTime(5, "reconnect", reconnectScheduleID)
if err != nil {
pdk.Log(pdk.LogError, fmt.Sprintf("Failed to schedule reconnection: %v", err))
}
@ -256,7 +257,7 @@ func ndSchedulerCallback() int32 {
pdk.Log(pdk.LogError, fmt.Sprintf("Reconnection failed: %v - will retry in 10 seconds", err))
// Schedule another attempt
_, err := SchedulerScheduleOneTime(10, "reconnect", reconnectScheduleID)
_, err := ndhost.SchedulerScheduleOneTime(10, "reconnect", reconnectScheduleID)
if err != nil {
pdk.Log(pdk.LogError, fmt.Sprintf("Failed to schedule retry: %v", 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,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
}

View File

@ -96,12 +96,27 @@ Folder = "/path/to/plugins"
## Files
| File | Description |
|----------------|---------------------------------------------------------|
| `main.go` | Plugin entry point, manifest, scrobbler implementation |
| `rpc.go` | Discord gateway communication and RPC logic |
| `pdk.gen.go` | Generated types from XTP schemas (combined) |
| `nd_host_*.go` | Host function wrappers (copied from `plugins/host/go/`) |
| File | Description |
|--------------|--------------------------------------------------------|
| `main.go` | Plugin entry point, manifest, scrobbler implementation |
| `rpc.go` | Discord gateway communication and RPC logic |
| `pdk.gen.go` | Generated types from XTP schemas (combined) |
| `go.mod` | Go module file (imports `ndhost` SDK) |
## Host SDK
This plugin imports the Go host SDK directly:
```go
import ndhost "github.com/navidrome/navidrome/plugins/host/go"
```
The `go.mod` file uses a `replace` directive to point to the local SDK:
```
require github.com/navidrome/navidrome/plugins/host/go v0.0.0
replace github.com/navidrome/navidrome/plugins/host/go => ../../host/go
```
## Host Services Used

View File

@ -1,5 +1,10 @@
module discord-rich-presence
go 1.22.1
go 1.24
require github.com/extism/go-pdk v1.1.0
require (
github.com/extism/go-pdk v1.1.3
github.com/navidrome/navidrome/plugins/host/go v0.0.0
)
replace github.com/navidrome/navidrome/plugins/host/go => ../../host/go

View File

@ -1,2 +1,2 @@
github.com/extism/go-pdk v1.1.0 h1:K2On6XOERxrYdsgu0uLzCxeu/FYRHE8jId/hdEVSYoY=
github.com/extism/go-pdk v1.1.0/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4=
github.com/extism/go-pdk v1.1.3 h1:hfViMPWrqjN6u67cIYRALZTZLk/enSPpNKa+rZ9X2SQ=
github.com/extism/go-pdk v1.1.3/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4=

View File

@ -16,6 +16,7 @@ import (
"time"
"github.com/extism/go-pdk"
ndhost "github.com/navidrome/navidrome/plugins/host/go"
)
// Configuration keys
@ -51,7 +52,7 @@ func getConfig() (clientID string, users map[string]string, err error) {
// getImageURL retrieves the track artwork URL.
func getImageURL(trackID string) string {
resp, err := ArtworkGetTrackUrl(trackID, 300)
resp, err := ndhost.ArtworkGetTrackUrl(trackID, 300)
if err != nil {
pdk.Log(pdk.LogWarn, fmt.Sprintf("Failed to get artwork URL: %v", err))
return ""
@ -104,7 +105,7 @@ func NdScrobblerNowPlaying(input NowPlayingInput) (ScrobblerOutput, error) {
}
// Cancel any existing completion schedule
_, _ = SchedulerCancelSchedule(fmt.Sprintf("%s-clear", input.Username))
_, _ = ndhost.SchedulerCancelSchedule(fmt.Sprintf("%s-clear", input.Username))
// Calculate timestamps
now := time.Now().Unix()
@ -133,7 +134,7 @@ func NdScrobblerNowPlaying(input NowPlayingInput) (ScrobblerOutput, error) {
// Schedule a timer to clear the activity after the track completes
remainingSeconds := int32(input.Track.Duration) - input.Position + 5
_, err = SchedulerScheduleOneTime(remainingSeconds, payloadClearActivity, fmt.Sprintf("%s-clear", input.Username))
_, err = ndhost.SchedulerScheduleOneTime(remainingSeconds, payloadClearActivity, fmt.Sprintf("%s-clear", input.Username))
if err != nil {
pdk.Log(pdk.LogWarn, fmt.Sprintf("Failed to schedule completion timer: %v", err))
}

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

View File

@ -11,6 +11,7 @@ import (
"time"
"github.com/extism/go-pdk"
ndhost "github.com/navidrome/navidrome/plugins/host/go"
)
// Discord WebSocket Gateway constants
@ -88,7 +89,7 @@ func processImage(imageURL, clientID, token string, isDefaultImage bool) (string
// Check cache first
cacheKey := fmt.Sprintf("discord.image.%x", imageURL)
cacheResp, err := CacheGetString(cacheKey)
cacheResp, err := ndhost.CacheGetString(cacheKey)
if err == nil && cacheResp.Exists {
pdk.Log(pdk.LogDebug, fmt.Sprintf("Cache hit for image URL: %s", imageURL))
return cacheResp.Value, nil
@ -140,7 +141,7 @@ func processImage(imageURL, clientID, token string, isDefaultImage bool) (string
ttl = 48 * 60 * 60 // 48 hours for default image
}
_, _ = CacheSetString(cacheKey, processedImage, ttl)
_, _ = ndhost.CacheSetString(cacheKey, processedImage, ttl)
pdk.Log(pdk.LogDebug, fmt.Sprintf("Cached processed image URL for %s (TTL: %ds)", imageURL, ttl))
return processedImage, nil
@ -183,7 +184,7 @@ func sendMessage(username string, opCode int, payload any) error {
return fmt.Errorf("failed to marshal message: %w", err)
}
_, err = WebSocketSendText(username, string(b))
_, err = ndhost.WebSocketSendText(username, string(b))
if err != nil {
return fmt.Errorf("failed to send message: %w", err)
}
@ -207,7 +208,7 @@ func getDiscordGateway() (string, error) {
// sendHeartbeat sends a heartbeat to Discord.
func sendHeartbeat(username string) error {
cacheResp, err := CacheGetInt(fmt.Sprintf("discord.seq.%s", username))
cacheResp, err := ndhost.CacheGetInt(fmt.Sprintf("discord.seq.%s", username))
if err != nil {
return fmt.Errorf("failed to get sequence number: %w", err)
}
@ -221,17 +222,17 @@ func cleanupFailedConnection(username string) {
pdk.Log(pdk.LogInfo, fmt.Sprintf("Cleaning up failed connection for user %s", username))
// Cancel the heartbeat schedule
if _, err := SchedulerCancelSchedule(username); err != nil {
if _, err := ndhost.SchedulerCancelSchedule(username); err != nil {
pdk.Log(pdk.LogWarn, fmt.Sprintf("Failed to cancel heartbeat schedule for user %s: %v", username, err))
}
// Close the WebSocket connection
if _, err := WebSocketCloseConnection(username, 1000, "Connection lost"); err != nil {
if _, err := ndhost.WebSocketCloseConnection(username, 1000, "Connection lost"); err != nil {
pdk.Log(pdk.LogWarn, fmt.Sprintf("Failed to close WebSocket connection for user %s: %v", username, err))
}
// Clean up cache entries
_, _ = CacheRemove(fmt.Sprintf("discord.seq.%s", username))
_, _ = ndhost.CacheRemove(fmt.Sprintf("discord.seq.%s", username))
pdk.Log(pdk.LogInfo, fmt.Sprintf("Cleaned up connection for user %s", username))
}
@ -262,7 +263,7 @@ func connect(username, token string) error {
pdk.Log(pdk.LogDebug, fmt.Sprintf("Using gateway: %s", gateway))
// Connect to Discord Gateway
_, err = WebSocketConnect(gateway, nil, username)
_, err = ndhost.WebSocketConnect(gateway, nil, username)
if err != nil {
return fmt.Errorf("failed to connect to WebSocket: %w", err)
}
@ -283,7 +284,7 @@ func connect(username, token string) error {
// Schedule heartbeats for this user/connection
cronExpr := fmt.Sprintf("@every %ds", heartbeatInterval)
schedResp, err := SchedulerScheduleRecurring(cronExpr, payloadHeartbeat, username)
schedResp, err := ndhost.SchedulerScheduleRecurring(cronExpr, payloadHeartbeat, username)
if err != nil {
return fmt.Errorf("failed to schedule heartbeat: %w", err)
}
@ -295,11 +296,11 @@ func connect(username, token string) error {
// disconnect closes the Discord connection for a user.
func disconnect(username string) error {
if _, err := SchedulerCancelSchedule(username); err != nil {
if _, err := ndhost.SchedulerCancelSchedule(username); err != nil {
return fmt.Errorf("failed to cancel schedule: %w", err)
}
if _, err := WebSocketCloseConnection(username, 1000, "Navidrome disconnect"); err != nil {
if _, err := ndhost.WebSocketCloseConnection(username, 1000, "Navidrome disconnect"); err != nil {
return fmt.Errorf("failed to close WebSocket connection: %w", err)
}
return nil
@ -323,7 +324,7 @@ func handleWebSocketMessage(connectionID, message string) error {
if v := msg["s"]; v != nil {
seq := int64(v.(float64))
pdk.Log(pdk.LogTrace, fmt.Sprintf("Received sequence number for connection '%s': %d", connectionID, seq))
if _, err := CacheSetInt(fmt.Sprintf("discord.seq.%s", connectionID), seq, int64(heartbeatInterval*2)); err != nil {
if _, err := ndhost.CacheSetInt(fmt.Sprintf("discord.seq.%s", connectionID), seq, int64(heartbeatInterval*2)); err != nil {
return fmt.Errorf("failed to store sequence number for user %s: %w", connectionID, err)
}
}

101
plugins/host/go/README.md Normal file
View File

@ -0,0 +1,101 @@
# Navidrome Host Function Wrappers for Go
This directory contains auto-generated Go wrappers for Navidrome's host services.
These wrappers provide idiomatic Go APIs for interacting with Navidrome from WASM plugins.
## ⚠️ Auto-Generated Code
**Do not edit these files manually.** They are generated by the `hostgen` tool.
To regenerate:
```bash
cd plugins/host
go generate
```
## Usage
Add this module as a dependency in your plugin's `go.mod`:
```go
require github.com/navidrome/navidrome/plugins/host/go v0.0.0
replace github.com/navidrome/navidrome/plugins/host/go => ../../host/go
```
Then import the package in your plugin code:
```go
package main
import (
ndhost "github.com/navidrome/navidrome/plugins/host/go"
"github.com/extism/go-pdk"
)
func myPluginFunction() error {
// Use the cache service
_, err := ndhost.CacheSetString("my_key", "my_value", 3600)
if err != nil {
return err
}
// Schedule a recurring task
_, err = ndhost.SchedulerScheduleRecurring("@every 5m", "payload", "task_id")
if err != nil {
return err
}
// Access library data with typed structs
resp, err := ndhost.LibraryGetAllLibraries()
if err != nil {
return err
}
for _, lib := range resp.Result {
pdk.Log(pdk.LogInfo, fmt.Sprintf("Library: %s with %d songs", lib.Name, lib.TotalSongs))
}
return nil
}
```
## Typed Structs
Services that work with domain objects provide typed Go structs instead of
`map[string]interface{}`. This enables compile-time type checking and IDE
autocompletion.
For example, the library service provides a `Library` struct:
```go
resp, err := ndhost.LibraryGetAllLibraries()
if err != nil {
return err
}
for _, lib := range resp.Result {
fmt.Printf("Library: %s (%d songs)\n", lib.Name, lib.TotalSongs)
}
```
## Available Services
| Module | Description |
|---------------|----------------------------------------------------|
| `Artwork` | Access album and artist artwork |
| `Cache` | Temporary key-value storage with TTL |
| `KVStore` | Persistent key-value storage |
| `Library` | Access the music library (albums, artists, tracks) |
| `Scheduler` | Schedule one-time and recurring tasks |
| `SubsonicAPI` | Make Subsonic API calls |
| `WebSocket` | Send real-time messages to clients |
## Building Plugins
Go plugins must be compiled to WebAssembly using TinyGo:
```bash
tinygo build -o plugin.wasm -target=wasip1 -buildmode=c-shared .
```
See the [discord-rich-presence](../../examples/discord-rich-presence/) example for a complete plugin implementation.

56
plugins/host/go/doc.go Normal file
View File

@ -0,0 +1,56 @@
// Code generated by hostgen. DO NOT EDIT.
//go:build wasip1
/*
Package ndhost provides Navidrome host function wrappers for Go/TinyGo plugins.
This package is auto-generated by the hostgen tool and should not be edited manually.
# Usage
Add this module as a dependency in your plugin's go.mod:
require github.com/navidrome/navidrome/plugins/host/go v0.0.0
Then import the package in your plugin code:
import ndhost "github.com/navidrome/navidrome/plugins/host/go"
func myPluginFunction() error {
// Use the cache service
_, err := ndhost.CacheSetString("my_key", "my_value", 3600)
if err != nil {
return err
}
// Schedule a recurring task
_, err = ndhost.SchedulerScheduleRecurring("@every 5m", "payload", "task_id")
if err != nil {
return err
}
return nil
}
# Available Services
The following host services are available:
- Artwork: provides artwork URL generation capabilities for plugins.
- Cache: provides in-memory TTL-based caching capabilities for plugins.
- KVStore: provides persistent key-value storage for plugins.
- Library: provides access to music library metadata for plugins.
- Scheduler: provides task scheduling capabilities for plugins.
- SubsonicAPI: provides access to Navidrome's Subsonic API from plugins.
- WebSocket: provides WebSocket communication capabilities for plugins.
# Building Plugins
Go plugins must be compiled to WebAssembly using TinyGo:
tinygo build -o plugin.wasm -target=wasip1 -buildmode=c-shared .
See the examples directory for complete plugin implementations.
*/
package ndhost

5
plugins/host/go/go.mod Normal file
View File

@ -0,0 +1,5 @@
module github.com/navidrome/navidrome/plugins/host/go
go 1.23
require github.com/extism/go-pdk v1.1.3

3
plugins/host/go/go.sum Normal file
View File

@ -0,0 +1,3 @@
github.com/extism/go-pdk v1.1.0 h1:K2On6XOERxrYdsgu0uLzCxeu/FYRHE8jId/hdEVSYoY=
github.com/extism/go-pdk v1.1.0/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4=
github.com/extism/go-pdk v1.1.3/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4=

View File

@ -5,7 +5,7 @@
//
//go:build wasip1
package main
package ndhost
import (
"encoding/json"

View File

@ -5,7 +5,7 @@
//
//go:build wasip1
package main
package ndhost
import (
"encoding/json"

View File

@ -5,7 +5,7 @@
//
//go:build wasip1
package main
package ndhost
import (
"encoding/json"

View File

@ -5,7 +5,7 @@
//
//go:build wasip1
package main
package ndhost
import (
"encoding/json"
@ -14,6 +14,21 @@ import (
"github.com/extism/go-pdk"
)
// Library represents the Library data structure.
// Library represents a music library with metadata.
type Library struct {
ID int32 `json:"id"`
Name string `json:"name"`
Path string `json:"path"`
MountPoint string `json:"mountPoint"`
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

View File

@ -5,7 +5,7 @@
//
//go:build wasip1
package main
package ndhost
import (
"encoding/json"

View File

@ -5,7 +5,7 @@
//
//go:build wasip1
package main
package ndhost
import (
"encoding/json"

View File

@ -5,7 +5,7 @@
//
//go:build wasip1
package main
package ndhost
import (
"encoding/json"