🏡 index : quine-global/quintodrome.git

# Navidrome Plugin Development Kit for Go

This directory contains the auto-generated Go PDK (Plugin Development Kit) for building Navidrome plugins.
The PDK provides both **host function wrappers** for interacting with Navidrome and 
**capability interfaces** for implementing plugin functionality.

## ⚠️ Auto-Generated Code

**Do not edit files in this directory manually.** They are generated by the `ndpgen` tool.

To regenerate:

```bash
make gen
```

## Module Structure

This is a consolidated Go module that includes:

- `host/` - Host function wrappers for calling Navidrome services from plugins
- `lifecycle/` - Plugin lifecycle hooks (initialization)
- `metadata/` - Metadata agent capability for artist/album info
- `scheduler/` - Scheduler callback capability for scheduled tasks
- `scrobbler/` - Scrobbler capability for play tracking
- `websocket/` - WebSocket callback capability for real-time messages

## Usage

Add this module as a dependency in your plugin's `go.mod`:

```go
require github.com/navidrome/navidrome/plugins/pdk/go v0.0.0

replace github.com/navidrome/navidrome/plugins/pdk/go => ../../pdk/go
```

Then import the packages you need:

```go
package main

import (
    "github.com/navidrome/navidrome/plugins/pdk/go/host"
    "github.com/navidrome/navidrome/plugins/pdk/go/lifecycle"
    "github.com/navidrome/navidrome/plugins/pdk/go/scheduler"
)

func init() {
    lifecycle.Register(&myPlugin{})
    scheduler.Register(&myPlugin{})
}

type myPlugin struct{}

func (p *myPlugin) OnInit() error {
    // Initialize your plugin
    return nil
}

func (p *myPlugin) OnCallback(req scheduler.SchedulerCallbackRequest) error {
    // Handle scheduled task
    return host.WebSocketBroadcast("task-complete", req.ScheduleID)
}

func main() {}
```

## Host Services

The `host` package provides wrappers for calling Navidrome's host services:

| Service       | 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                 |

### Example: Using Host Services

```go
package main

import (
    "github.com/navidrome/navidrome/plugins/pdk/go/host"
)

func myPluginFunction() error {
    // Use the cache service
    _, err := host.CacheSetString("my_key", "my_value", 3600)
    if err != nil {
        return err
    }

    // Schedule a recurring task  
    _, err = host.SchedulerScheduleRecurring("@every 5m", "payload", "task_id")
    if err != nil {
        return err
    }

    // Access library data with typed structs
    resp, err := host.LibraryGetAllLibraries()
    if err != nil {
        return err
    }
    for _, lib := range resp.Result {
        // Library: %s with %d songs", lib.Name, lib.TotalSongs
    }

    return nil
}
```

## Capabilities

Capabilities define what functionality your plugin implements. Register your implementations
in the `init()` function.

### Lifecycle

Provides plugin initialization hooks.

```go
import "github.com/navidrome/navidrome/plugins/pdk/go/lifecycle"

func init() {
    lifecycle.Register(&myPlugin{})
}

type myPlugin struct{}

func (p *myPlugin) OnInit() error {
    // Called once when plugin is loaded
    return nil
}
```

### MetadataAgent

Provides artist and album metadata from external sources.

```go
import "github.com/navidrome/navidrome/plugins/pdk/go/metadata"

func init() {
    metadata.Register(&myAgent{})
}

type myAgent struct{}

func (a *myAgent) GetArtistBiography(req metadata.ArtistRequest) (*metadata.ArtistBiographyResponse, error) {
    return &metadata.ArtistBiographyResponse{
        Biography: "Artist biography text...",
    }, nil
}

func (a *myAgent) GetArtistImages(req metadata.ArtistRequest) (*metadata.ArtistImagesResponse, error) {
    return &metadata.ArtistImagesResponse{
        Images: []metadata.ImageInfo{
            {URL: "https://example.com/image.jpg", Size: 1000},
        },
    }, nil
}
```

### Scheduler

Handles callbacks from scheduled tasks.

```go
import (
    "github.com/navidrome/navidrome/plugins/pdk/go/host"
    "github.com/navidrome/navidrome/plugins/pdk/go/scheduler"
)

func init() {
    scheduler.Register(&myScheduler{})
}

type myScheduler struct{}

func (s *myScheduler) OnCallback(req scheduler.SchedulerCallbackRequest) error {
    // Handle the scheduled task
    if req.Payload == "update-data" {
        // Do work...
        return host.WebSocketBroadcast("data-updated", "")
    }
    return nil
}
```

### Scrobbler

Tracks play activity.

```go
import "github.com/navidrome/navidrome/plugins/pdk/go/scrobbler"

func init() {
    scrobbler.Register(&myScrobbler{})
}

type myScrobbler struct{}

func (s *myScrobbler) Scrobble(req scrobbler.ScrobbleRequest) error {
    // Track the play
    return nil
}

func (s *myScrobbler) NowPlaying(req scrobbler.NowPlayingRequest) error {
    // Update now playing status
    return nil
}
```

### WebSocket

Handles incoming WebSocket messages.

```go
import "github.com/navidrome/navidrome/plugins/pdk/go/websocket"

func init() {
    websocket.Register(&myHandler{})
}

type myHandler struct{}

func (h *myHandler) OnWebSocketMessage(req websocket.WebSocketMessageRequest) error {
    // Handle incoming message
    return nil
}
```

## Building Plugins

Go plugins must be compiled to WebAssembly using TinyGo:

```bash
tinygo build -o plugin.wasm -target=wasip1 -buildmode=c-shared .
```

Or use the provided Makefile targets in plugin examples:

```bash
make plugin.wasm
```

## Testing Plugins

The PDK includes [testify/mock](https://github.com/stretchr/testify) implementations for all host services,
allowing you to unit test your plugin code on non-WASM platforms (your development machine).

### PDK Abstraction Layer

The `pdk` subpackage provides a testable wrapper around the Extism PDK functions. Instead of importing
`github.com/extism/go-pdk` directly, import the abstraction layer:

```go
import "github.com/navidrome/navidrome/plugins/pdk/go/pdk"

func myFunction() {
    // Use pdk functions - same API as extism/go-pdk
    config, ok := pdk.GetConfig("my_setting")
    if ok {
        pdk.Log(pdk.LogInfo, "Setting: " + config)
    }
    
    var input MyInput
    if err := pdk.InputJSON(&input); err != nil {
        pdk.SetError(err)
        return
    }
    
    output := processInput(input)
    pdk.OutputJSON(output)
}
```

For WASM builds, these functions delegate directly to `extism/go-pdk` with zero overhead.
For native builds (tests), they use mocks that you can configure:

```go
package myplugin

import (
    "testing"
    
    "github.com/navidrome/navidrome/plugins/pdk/go/pdk"
)

func TestMyFunction(t *testing.T) {
    // Reset mock state before each test
    pdk.ResetMock()
    
    // Set up expectations
    pdk.PDKMock.On("GetConfig", "my_setting").Return("test_value", true)
    pdk.PDKMock.On("Log", pdk.LogInfo, "Setting: test_value").Return()
    pdk.PDKMock.On("InputJSON", mock.Anything).Return(nil).Run(func(args mock.Arguments) {
        // Populate the input struct
        input := args.Get(0).(*MyInput)
        input.Name = "test"
    })
    pdk.PDKMock.On("OutputJSON", mock.Anything).Return(nil)
    
    // Call your function
    myFunction()
    
    // Verify expectations
    pdk.PDKMock.AssertExpectations(t)
}
```

### Mock Instances

Each host service has an auto-instantiated mock instance:

| Service       | Mock Instance            |
|---------------|--------------------------|
| `Artwork`     | `host.ArtworkMock`       |
| `Cache`       | `host.CacheMock`         |
| `Config`      | `host.ConfigMock`        |
| `KVStore`     | `host.KVStoreMock`       |
| `Library`     | `host.LibraryMock`       |
| `Scheduler`   | `host.SchedulerMock`     |
| `SubsonicAPI` | `host.SubsonicAPIMock`   |
| `WebSocket`   | `host.WebSocketMock`     |

### Example Test

```go
package myplugin

import (
    "testing"

    "github.com/navidrome/navidrome/plugins/pdk/go/host"
)

func TestMyPluginFunction(t *testing.T) {
    // Set expectations on the mock
    host.CacheMock.On("GetString", "my-key").Return("cached-value", true, nil)
    host.CacheMock.On("SetString", "new-key", "new-value", int64(3600)).Return(nil)

    // Call your plugin code that uses host.CacheGetString / host.CacheSetString
    result, err := myPluginFunction()
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }

    // Assert the result
    if result != "expected" {
        t.Errorf("unexpected result: %s", result)
    }

    // Verify all expected calls were made
    host.CacheMock.AssertExpectations(t)
}
```

### Running Tests

Since tests run on your development machine (not WASM), use standard Go testing:

```bash
go test ./...
```

The stub files with mocks are only compiled for non-WASM builds (`//go:build !wasip1`),
so they won't affect your production WASM binary.

### Complete Examples

For more comprehensive examples including HTTP requests, Memory handling, and various testing patterns,
see [pdk/example_test.go](pdk/example_test.go).