mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
feat(plugins): add Go client library with host function wrappers and documentation
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
13ca6149a9
commit
3692a274b4
@ -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).
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}}
|
||||
|
||||
|
||||
51
plugins/cmd/hostgen/internal/templates/doc_go.go.tmpl
Normal file
51
plugins/cmd/hostgen/internal/templates/doc_go.go.tmpl
Normal 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
|
||||
5
plugins/cmd/hostgen/internal/templates/go.mod.tmpl
Normal file
5
plugins/cmd/hostgen/internal/templates/go.mod.tmpl
Normal 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
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
//
|
||||
//go:build wasip1
|
||||
|
||||
package main
|
||||
package ndhost
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
//
|
||||
//go:build wasip1
|
||||
|
||||
package main
|
||||
package ndhost
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
//
|
||||
//go:build wasip1
|
||||
|
||||
package main
|
||||
package ndhost
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
//
|
||||
//go:build wasip1
|
||||
|
||||
package main
|
||||
package ndhost
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
//
|
||||
//go:build wasip1
|
||||
|
||||
package main
|
||||
package ndhost
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
//
|
||||
//go:build wasip1
|
||||
|
||||
package main
|
||||
package ndhost
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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=
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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=
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
101
plugins/host/go/README.md
Normal 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
56
plugins/host/go/doc.go
Normal 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
5
plugins/host/go/go.mod
Normal 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
3
plugins/host/go/go.sum
Normal 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=
|
||||
@ -5,7 +5,7 @@
|
||||
//
|
||||
//go:build wasip1
|
||||
|
||||
package main
|
||||
package ndhost
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
//
|
||||
//go:build wasip1
|
||||
|
||||
package main
|
||||
package ndhost
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
//
|
||||
//go:build wasip1
|
||||
|
||||
package main
|
||||
package ndhost
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
//
|
||||
//go:build wasip1
|
||||
|
||||
package main
|
||||
package ndhost
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
//
|
||||
//go:build wasip1
|
||||
|
||||
package main
|
||||
package ndhost
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
//
|
||||
//go:build wasip1
|
||||
|
||||
package main
|
||||
package ndhost
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user