diff --git a/plugins/README.md b/plugins/README.md index c22626aec..760dc8032 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -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). diff --git a/plugins/cmd/hostgen/README.md b/plugins/cmd/hostgen/README.md index d22cba3d9..3b1f798af 100644 --- a/plugins/cmd/hostgen/README.md +++ b/plugins/cmd/hostgen/README.md @@ -182,27 +182,56 @@ Generated files are named `_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_.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_.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) diff --git a/plugins/cmd/hostgen/integration_test.go b/plugins/cmd/hostgen/integration_test.go index 2f981cb34..a1d0f7c9e 100644 --- a/plugins/cmd/hostgen/integration_test.go +++ b/plugins/cmd/hostgen/integration_test.go @@ -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() { diff --git a/plugins/cmd/hostgen/internal/generator.go b/plugins/cmd/hostgen/internal/generator.go index e940d5294..5902fd5ad 100644 --- a/plugins/cmd/hostgen/internal/generator.go +++ b/plugins/cmd/hostgen/internal/generator.go @@ -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 +} diff --git a/plugins/cmd/hostgen/internal/generator_test.go b/plugins/cmd/hostgen/internal/generator_test.go index d9054898e..5216a5b1b 100644 --- a/plugins/cmd/hostgen/internal/generator_test.go +++ b/plugins/cmd/hostgen/internal/generator_test.go @@ -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 diff --git a/plugins/cmd/hostgen/internal/templates/client_go.go.tmpl b/plugins/cmd/hostgen/internal/templates/client_go.go.tmpl index 43718026c..5cc53640a 100644 --- a/plugins/cmd/hostgen/internal/templates/client_go.go.tmpl +++ b/plugins/cmd/hostgen/internal/templates/client_go.go.tmpl @@ -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}} diff --git a/plugins/cmd/hostgen/internal/templates/doc_go.go.tmpl b/plugins/cmd/hostgen/internal/templates/doc_go.go.tmpl new file mode 100644 index 000000000..cf6d8e554 --- /dev/null +++ b/plugins/cmd/hostgen/internal/templates/doc_go.go.tmpl @@ -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 diff --git a/plugins/cmd/hostgen/internal/templates/go.mod.tmpl b/plugins/cmd/hostgen/internal/templates/go.mod.tmpl new file mode 100644 index 000000000..954c98071 --- /dev/null +++ b/plugins/cmd/hostgen/internal/templates/go.mod.tmpl @@ -0,0 +1,5 @@ +module github.com/navidrome/navidrome/plugins/host/go + +go 1.24 + +require github.com/extism/go-pdk v1.1.3 diff --git a/plugins/cmd/hostgen/main.go b/plugins/cmd/hostgen/main.go index b5d6922e9..fd5d281fc 100644 --- a/plugins/cmd/hostgen/main.go +++ b/plugins/cmd/hostgen/main.go @@ -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 +} diff --git a/plugins/cmd/hostgen/testdata/codec_client_expected.go b/plugins/cmd/hostgen/testdata/codec_client_expected.go index 508dff39d..4ae8795d8 100644 --- a/plugins/cmd/hostgen/testdata/codec_client_expected.go +++ b/plugins/cmd/hostgen/testdata/codec_client_expected.go @@ -5,7 +5,7 @@ // //go:build wasip1 -package main +package ndhost import ( "encoding/json" diff --git a/plugins/cmd/hostgen/testdata/counter_client_expected.go b/plugins/cmd/hostgen/testdata/counter_client_expected.go index 6c0f61fd9..ee9bea22a 100644 --- a/plugins/cmd/hostgen/testdata/counter_client_expected.go +++ b/plugins/cmd/hostgen/testdata/counter_client_expected.go @@ -5,7 +5,7 @@ // //go:build wasip1 -package main +package ndhost import ( "encoding/json" diff --git a/plugins/cmd/hostgen/testdata/echo_client_expected.go b/plugins/cmd/hostgen/testdata/echo_client_expected.go index 15ae0b2c1..6adbe2bb6 100644 --- a/plugins/cmd/hostgen/testdata/echo_client_expected.go +++ b/plugins/cmd/hostgen/testdata/echo_client_expected.go @@ -5,7 +5,7 @@ // //go:build wasip1 -package main +package ndhost import ( "encoding/json" diff --git a/plugins/cmd/hostgen/testdata/list_client_expected.go b/plugins/cmd/hostgen/testdata/list_client_expected.go index 0650f1b8c..3bf4244a9 100644 --- a/plugins/cmd/hostgen/testdata/list_client_expected.go +++ b/plugins/cmd/hostgen/testdata/list_client_expected.go @@ -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 diff --git a/plugins/cmd/hostgen/testdata/math_client_expected.go b/plugins/cmd/hostgen/testdata/math_client_expected.go index e27f455fc..f7b30579b 100644 --- a/plugins/cmd/hostgen/testdata/math_client_expected.go +++ b/plugins/cmd/hostgen/testdata/math_client_expected.go @@ -5,7 +5,7 @@ // //go:build wasip1 -package main +package ndhost import ( "encoding/json" diff --git a/plugins/cmd/hostgen/testdata/meta_client_expected.go b/plugins/cmd/hostgen/testdata/meta_client_expected.go index a43a1fecf..fb61c327e 100644 --- a/plugins/cmd/hostgen/testdata/meta_client_expected.go +++ b/plugins/cmd/hostgen/testdata/meta_client_expected.go @@ -5,7 +5,7 @@ // //go:build wasip1 -package main +package ndhost import ( "encoding/json" diff --git a/plugins/cmd/hostgen/testdata/ping_client_expected.go b/plugins/cmd/hostgen/testdata/ping_client_expected.go index 9c390b7ba..6adc4c798 100644 --- a/plugins/cmd/hostgen/testdata/ping_client_expected.go +++ b/plugins/cmd/hostgen/testdata/ping_client_expected.go @@ -5,7 +5,7 @@ // //go:build wasip1 -package main +package ndhost import ( "encoding/json" diff --git a/plugins/cmd/hostgen/testdata/search_client_expected.go b/plugins/cmd/hostgen/testdata/search_client_expected.go index 769b7883e..7e311856a 100644 --- a/plugins/cmd/hostgen/testdata/search_client_expected.go +++ b/plugins/cmd/hostgen/testdata/search_client_expected.go @@ -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 diff --git a/plugins/cmd/hostgen/testdata/store_client_expected.go b/plugins/cmd/hostgen/testdata/store_client_expected.go index 1509baea1..153813e95 100644 --- a/plugins/cmd/hostgen/testdata/store_client_expected.go +++ b/plugins/cmd/hostgen/testdata/store_client_expected.go @@ -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 diff --git a/plugins/cmd/hostgen/testdata/users_client_expected.go b/plugins/cmd/hostgen/testdata/users_client_expected.go index b564cab27..9f61bc447 100644 --- a/plugins/cmd/hostgen/testdata/users_client_expected.go +++ b/plugins/cmd/hostgen/testdata/users_client_expected.go @@ -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 diff --git a/plugins/examples/crypto-ticker/README.md b/plugins/examples/crypto-ticker/README.md index 821cce119..30c9327a8 100644 --- a/plugins/examples/crypto-ticker/README.md +++ b/plugins/examples/crypto-ticker/README.md @@ -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 +``` --- diff --git a/plugins/examples/crypto-ticker/go.mod b/plugins/examples/crypto-ticker/go.mod index ba1d64ce1..78f28a7e7 100755 --- a/plugins/examples/crypto-ticker/go.mod +++ b/plugins/examples/crypto-ticker/go.mod @@ -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 diff --git a/plugins/examples/crypto-ticker/go.sum b/plugins/examples/crypto-ticker/go.sum index e0fb44c64..c15d38292 100644 --- a/plugins/examples/crypto-ticker/go.sum +++ b/plugins/examples/crypto-ticker/go.sum @@ -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= diff --git a/plugins/examples/crypto-ticker/main.go b/plugins/examples/crypto-ticker/main.go index d6a095736..6f8285b46 100755 --- a/plugins/examples/crypto-ticker/main.go +++ b/plugins/examples/crypto-ticker/main.go @@ -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)) } diff --git a/plugins/examples/crypto-ticker/nd_host_scheduler.go b/plugins/examples/crypto-ticker/nd_host_scheduler.go deleted file mode 100644 index 78c2e73f1..000000000 --- a/plugins/examples/crypto-ticker/nd_host_scheduler.go +++ /dev/null @@ -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 -} diff --git a/plugins/examples/crypto-ticker/nd_host_websocket.go b/plugins/examples/crypto-ticker/nd_host_websocket.go deleted file mode 100644 index d0989de8e..000000000 --- a/plugins/examples/crypto-ticker/nd_host_websocket.go +++ /dev/null @@ -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 -} diff --git a/plugins/examples/discord-rich-presence/README.md b/plugins/examples/discord-rich-presence/README.md index 869e5ae62..cc4f90cc5 100644 --- a/plugins/examples/discord-rich-presence/README.md +++ b/plugins/examples/discord-rich-presence/README.md @@ -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 diff --git a/plugins/examples/discord-rich-presence/go.mod b/plugins/examples/discord-rich-presence/go.mod index c982a9e01..bc6bf30cc 100644 --- a/plugins/examples/discord-rich-presence/go.mod +++ b/plugins/examples/discord-rich-presence/go.mod @@ -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 diff --git a/plugins/examples/discord-rich-presence/go.sum b/plugins/examples/discord-rich-presence/go.sum index e0fb44c64..c15d38292 100644 --- a/plugins/examples/discord-rich-presence/go.sum +++ b/plugins/examples/discord-rich-presence/go.sum @@ -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= diff --git a/plugins/examples/discord-rich-presence/main.go b/plugins/examples/discord-rich-presence/main.go index 3c67c0402..8c8be5101 100644 --- a/plugins/examples/discord-rich-presence/main.go +++ b/plugins/examples/discord-rich-presence/main.go @@ -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)) } diff --git a/plugins/examples/discord-rich-presence/nd_host_artwork.go b/plugins/examples/discord-rich-presence/nd_host_artwork.go deleted file mode 100644 index 65577b877..000000000 --- a/plugins/examples/discord-rich-presence/nd_host_artwork.go +++ /dev/null @@ -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 -} diff --git a/plugins/examples/discord-rich-presence/nd_host_cache.go b/plugins/examples/discord-rich-presence/nd_host_cache.go deleted file mode 100644 index 81dbfa229..000000000 --- a/plugins/examples/discord-rich-presence/nd_host_cache.go +++ /dev/null @@ -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 -} diff --git a/plugins/examples/discord-rich-presence/nd_host_scheduler.go b/plugins/examples/discord-rich-presence/nd_host_scheduler.go deleted file mode 100644 index 78c2e73f1..000000000 --- a/plugins/examples/discord-rich-presence/nd_host_scheduler.go +++ /dev/null @@ -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 -} diff --git a/plugins/examples/discord-rich-presence/nd_host_websocket.go b/plugins/examples/discord-rich-presence/nd_host_websocket.go deleted file mode 100644 index d0989de8e..000000000 --- a/plugins/examples/discord-rich-presence/nd_host_websocket.go +++ /dev/null @@ -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 -} diff --git a/plugins/examples/discord-rich-presence/rpc.go b/plugins/examples/discord-rich-presence/rpc.go index f550cce97..e189a5aab 100644 --- a/plugins/examples/discord-rich-presence/rpc.go +++ b/plugins/examples/discord-rich-presence/rpc.go @@ -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) } } diff --git a/plugins/host/go/README.md b/plugins/host/go/README.md new file mode 100644 index 000000000..97a30ae56 --- /dev/null +++ b/plugins/host/go/README.md @@ -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. diff --git a/plugins/host/go/doc.go b/plugins/host/go/doc.go new file mode 100644 index 000000000..2f0c2f2d7 --- /dev/null +++ b/plugins/host/go/doc.go @@ -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 diff --git a/plugins/host/go/go.mod b/plugins/host/go/go.mod new file mode 100644 index 000000000..1a1e39c9b --- /dev/null +++ b/plugins/host/go/go.mod @@ -0,0 +1,5 @@ +module github.com/navidrome/navidrome/plugins/host/go + +go 1.23 + +require github.com/extism/go-pdk v1.1.3 diff --git a/plugins/host/go/go.sum b/plugins/host/go/go.sum new file mode 100644 index 000000000..bf78d8808 --- /dev/null +++ b/plugins/host/go/go.sum @@ -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= diff --git a/plugins/host/go/nd_host_artwork.go b/plugins/host/go/nd_host_artwork.go index 65577b877..51811ea5a 100644 --- a/plugins/host/go/nd_host_artwork.go +++ b/plugins/host/go/nd_host_artwork.go @@ -5,7 +5,7 @@ // //go:build wasip1 -package main +package ndhost import ( "encoding/json" diff --git a/plugins/host/go/nd_host_cache.go b/plugins/host/go/nd_host_cache.go index 81dbfa229..ee4183820 100644 --- a/plugins/host/go/nd_host_cache.go +++ b/plugins/host/go/nd_host_cache.go @@ -5,7 +5,7 @@ // //go:build wasip1 -package main +package ndhost import ( "encoding/json" diff --git a/plugins/host/go/nd_host_kvstore.go b/plugins/host/go/nd_host_kvstore.go index a59028a6b..52e9da911 100644 --- a/plugins/host/go/nd_host_kvstore.go +++ b/plugins/host/go/nd_host_kvstore.go @@ -5,7 +5,7 @@ // //go:build wasip1 -package main +package ndhost import ( "encoding/json" diff --git a/plugins/host/go/nd_host_library.go b/plugins/host/go/nd_host_library.go index 6c6daaf70..2d7da44c7 100644 --- a/plugins/host/go/nd_host_library.go +++ b/plugins/host/go/nd_host_library.go @@ -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 diff --git a/plugins/host/go/nd_host_scheduler.go b/plugins/host/go/nd_host_scheduler.go index 78c2e73f1..89411f2d1 100644 --- a/plugins/host/go/nd_host_scheduler.go +++ b/plugins/host/go/nd_host_scheduler.go @@ -5,7 +5,7 @@ // //go:build wasip1 -package main +package ndhost import ( "encoding/json" diff --git a/plugins/host/go/nd_host_subsonicapi.go b/plugins/host/go/nd_host_subsonicapi.go index 75f70c8a7..e845e617a 100644 --- a/plugins/host/go/nd_host_subsonicapi.go +++ b/plugins/host/go/nd_host_subsonicapi.go @@ -5,7 +5,7 @@ // //go:build wasip1 -package main +package ndhost import ( "encoding/json" diff --git a/plugins/host/go/nd_host_websocket.go b/plugins/host/go/nd_host_websocket.go index d0989de8e..d4ce707d7 100644 --- a/plugins/host/go/nd_host_websocket.go +++ b/plugins/host/go/nd_host_websocket.go @@ -5,7 +5,7 @@ // //go:build wasip1 -package main +package ndhost import ( "encoding/json"