From 9481ba3662deb3f66dcf91689a38965baedddc8d Mon Sep 17 00:00:00 2001 From: Deluan Date: Mon, 22 Dec 2025 22:00:40 -0500 Subject: [PATCH] feat(plugins): add metadata agent and scrobbler schemas for bootstrapping plugins Signed-off-by: Deluan --- plugins/schemas/README.md | 176 ++++++++++++++++ plugins/schemas/metadata_agent.yaml | 299 ++++++++++++++++++++++++++++ plugins/schemas/scrobbler.yaml | 182 +++++++++++++++++ 3 files changed, 657 insertions(+) create mode 100644 plugins/schemas/README.md create mode 100644 plugins/schemas/metadata_agent.yaml create mode 100644 plugins/schemas/scrobbler.yaml diff --git a/plugins/schemas/README.md b/plugins/schemas/README.md new file mode 100644 index 000000000..712491582 --- /dev/null +++ b/plugins/schemas/README.md @@ -0,0 +1,176 @@ +# Navidrome Plugin Schemas + +This directory contains [XTP schemas](https://docs.xtp.dylibso.com/docs/concepts/xtp-schema) +that define the plugin capabilities for Navidrome. These schemas can be used to bootstrap +new plugins using the `xtp` CLI tool. + +## Available Schemas + +| Schema | Description | +|--------------------------------------------|-----------------------------------------------------------------| +| [metadata_agent.yaml](metadata_agent.yaml) | Metadata agent for retrieving artist/album information | +| [scrobbler.yaml](scrobbler.yaml) | Scrobbler capability for sending play data to external services | + +## Prerequisites + +Install the `xtp` CLI tool. See the [XTP CLI documentation](https://docs.xtp.dylibso.com/docs/cli) +for installation instructions, or install via: + +```bash +curl -fsSL https://static.dylibso.com/cli/install.sh | bash +``` + +## Bootstrapping a Plugin + +Use the `xtp plugin init` command to generate boilerplate code from a schema. + +### Supported Languages + +The XTP CLI supports multiple languages via bindgen templates: +- Go +- Rust +- TypeScript +- Python +- C# +- Zig +- C++ + +### Examples + +**Create a Go scrobbler plugin:** + +```bash +xtp plugin init \ + --schema-file plugins/schemas/scrobbler.yaml \ + --template go \ + --path ./my-scrobbler \ + --name my-scrobbler +``` + +**Create a Rust metadata agent plugin:** + +```bash +xtp plugin init \ + --schema-file plugins/schemas/metadata_agent.yaml \ + --template rust \ + --path ./my-agent \ + --name my-agent +``` + +**Create a TypeScript scrobbler plugin:** + +```bash +xtp plugin init \ + --schema-file plugins/schemas/scrobbler.yaml \ + --template typescript \ + --path ./ts-scrobbler \ + --name ts-scrobbler +``` + +### Generated Files + +After running `xtp plugin init`, you'll get a project structure with: + +- `main.go` (or equivalent for your language) - Plugin implementation with stub functions +- `pdk.gen.go` - Generated types from the schema +- `xtp.toml` - Plugin configuration +- Build scripts for your language + +### Implementing the Plugin + +Edit the generated `main.go` file and replace the `panic()` calls with your implementation. + +> **Note:** You don't need to implement all generated functions. Remove any functions that +> your plugin doesn't need. Navidrome will only call the functions that are exported by your +> plugin, and will gracefully handle missing capabilities. + +#### Required: The `nd_manifest` Function + +In addition to the capability functions generated from the schema, **every plugin must +implement the `nd_manifest` function**. This function returns metadata about your plugin +that Navidrome uses to identify and describe it. + +**Go example:** + +```go +import ( + "encoding/json" + "github.com/extism/go-pdk" +) + +type Manifest struct { + Name string `json:"name"` + Author string `json:"author"` + Version string `json:"version"` + Description string `json:"description"` +} + +//go:wasmexport nd_manifest +func ndManifest() int32 { + manifest := Manifest{ + Name: "My Scrobbler Plugin", + Author: "Your Name", + Version: "1.0.0", + Description: "A custom scrobbler for My Service", + } + out, err := json.Marshal(manifest) + if err != nil { + pdk.SetError(err) + return 1 + } + pdk.Output(out) + return 0 +} +``` + +**Python example:** + +```python +import extism + +@extism.plugin_fn +def nd_manifest(): + import json + manifest = { + "name": "My Scrobbler Plugin", + "author": "Your Name", + "version": "1.0.0", + "description": "A custom scrobbler for My Service" + } + extism.output_str(json.dumps(manifest)) +``` + +#### Implementing Capability Functions + +Replace the `panic()` calls in the generated stubs with your implementation: + +```go +// Example: Implement the IsAuthorized function +func NdScrobblerIsAuthorized(input AuthInput) (AuthOutput, error) { + // Your authorization logic here + authorized := checkUserAuthorization(input.UserID, input.Username) + return AuthOutput{Authorized: authorized}, nil +} +``` + +### Building the Plugin + +Build the plugin to WebAssembly: + +```bash +xtp plugin build +``` + +This creates a `.wasm` file that can be loaded by Navidrome. + +## Schema Format + +These schemas use the [XTP Schema v1-draft](https://docs.xtp.dylibso.com/docs/concepts/xtp-schema) format, +which is based on JSON Schema with extensions for defining plugin exports and imports. + +## Resources + +- [XTP Documentation](https://docs.xtp.dylibso.com/) +- [XTP Bindgen Repository](https://github.com/dylibso/xtp-bindgen) +- [Extism Plugin Development Kit](https://extism.org/docs/concepts/pdk) +- [XTP Schema Definition](https://raw.githubusercontent.com/dylibso/xtp-bindgen/5090518dd86ba5e734dc225a33066ecc0ed2e12d/plugin/schema.json) diff --git a/plugins/schemas/metadata_agent.yaml b/plugins/schemas/metadata_agent.yaml new file mode 100644 index 000000000..f25f1f4c2 --- /dev/null +++ b/plugins/schemas/metadata_agent.yaml @@ -0,0 +1,299 @@ +version: v1-draft + +exports: + nd_get_artist_mbid: + description: Retrieve the MusicBrainz ID for an artist + input: + $ref: "#/components/schemas/ArtistMBIDInput" + contentType: application/json + output: + $ref: "#/components/schemas/ArtistMBIDOutput" + contentType: application/json + + nd_get_artist_url: + description: Retrieve the external URL for an artist + input: + $ref: "#/components/schemas/ArtistInput" + contentType: application/json + output: + $ref: "#/components/schemas/ArtistURLOutput" + contentType: application/json + + nd_get_artist_biography: + description: Retrieve the biography for an artist + input: + $ref: "#/components/schemas/ArtistInput" + contentType: application/json + output: + $ref: "#/components/schemas/ArtistBiographyOutput" + contentType: application/json + + nd_get_similar_artists: + description: Retrieve similar artists for a given artist + input: + $ref: "#/components/schemas/SimilarArtistsInput" + contentType: application/json + output: + $ref: "#/components/schemas/SimilarArtistsOutput" + contentType: application/json + + nd_get_artist_images: + description: Retrieve images for an artist + input: + $ref: "#/components/schemas/ArtistInput" + contentType: application/json + output: + $ref: "#/components/schemas/ArtistImagesOutput" + contentType: application/json + + nd_get_artist_top_songs: + description: Retrieve top songs for an artist + input: + $ref: "#/components/schemas/TopSongsInput" + contentType: application/json + output: + $ref: "#/components/schemas/TopSongsOutput" + contentType: application/json + + nd_get_album_info: + description: Retrieve album information + input: + $ref: "#/components/schemas/AlbumInput" + contentType: application/json + output: + $ref: "#/components/schemas/AlbumInfoOutput" + contentType: application/json + + nd_get_album_images: + description: Retrieve images for an album + input: + $ref: "#/components/schemas/AlbumInput" + contentType: application/json + output: + $ref: "#/components/schemas/AlbumImagesOutput" + contentType: application/json + +components: + schemas: + ArtistMBIDInput: + description: Input for GetArtistMBID + properties: + id: + type: string + description: The internal Navidrome artist ID + name: + type: string + description: The artist name + required: + - id + - name + + ArtistMBIDOutput: + description: Output for GetArtistMBID + properties: + mbid: + type: string + description: The MusicBrainz ID for the artist + required: + - mbid + + ArtistInput: + description: Common input for artist-related functions + properties: + id: + type: string + description: The internal Navidrome artist ID + name: + type: string + description: The artist name + mbid: + type: string + nullable: true + description: The MusicBrainz ID for the artist (if known) + required: + - id + - name + + ArtistURLOutput: + description: Output for GetArtistURL + properties: + url: + type: string + description: The external URL for the artist + required: + - url + + ArtistBiographyOutput: + description: Output for GetArtistBiography + properties: + biography: + type: string + description: The artist biography text + required: + - biography + + SimilarArtistsInput: + description: Input for GetSimilarArtists + properties: + id: + type: string + description: The internal Navidrome artist ID + name: + type: string + description: The artist name + mbid: + type: string + nullable: true + description: The MusicBrainz ID for the artist (if known) + limit: + type: integer + format: int32 + description: Maximum number of similar artists to return + required: + - id + - name + - limit + + ArtistRef: + description: Reference to an artist with name and optional MBID + properties: + name: + type: string + description: The artist name + mbid: + type: string + nullable: true + description: The MusicBrainz ID for the artist + required: + - name + + SimilarArtistsOutput: + description: Output for GetSimilarArtists + properties: + artists: + type: array + items: + $ref: "#/components/schemas/ArtistRef" + description: List of similar artists + required: + - artists + + ImageInfo: + description: Image with URL and size + properties: + url: + type: string + description: The URL of the image + size: + type: integer + format: int32 + description: The size of the image in pixels (width or height) + required: + - url + - size + + ArtistImagesOutput: + description: Output for GetArtistImages + properties: + images: + type: array + items: + $ref: "#/components/schemas/ImageInfo" + description: List of artist images + required: + - images + + TopSongsInput: + description: Input for GetArtistTopSongs + properties: + id: + type: string + description: The internal Navidrome artist ID + name: + type: string + description: The artist name + mbid: + type: string + nullable: true + description: The MusicBrainz ID for the artist (if known) + count: + type: integer + format: int32 + description: Maximum number of top songs to return + required: + - id + - name + - count + + SongRef: + description: Reference to a song with name and optional MBID + properties: + name: + type: string + description: The song name + mbid: + type: string + nullable: true + description: The MusicBrainz ID for the song + required: + - name + + TopSongsOutput: + description: Output for GetArtistTopSongs + properties: + songs: + type: array + items: + $ref: "#/components/schemas/SongRef" + description: List of top songs + required: + - songs + + AlbumInput: + description: Common input for album-related functions + properties: + name: + type: string + description: The album name + artist: + type: string + description: The album artist name + mbid: + type: string + nullable: true + description: The MusicBrainz ID for the album (if known) + required: + - name + - artist + + AlbumInfoOutput: + description: Output for GetAlbumInfo + properties: + name: + type: string + description: The album name + mbid: + type: string + description: The MusicBrainz ID for the album + description: + type: string + description: The album description/notes + url: + type: string + description: The external URL for the album + required: + - name + - mbid + - description + - url + + AlbumImagesOutput: + description: Output for GetAlbumImages + properties: + images: + type: array + items: + $ref: "#/components/schemas/ImageInfo" + description: List of album images + required: + - images diff --git a/plugins/schemas/scrobbler.yaml b/plugins/schemas/scrobbler.yaml new file mode 100644 index 000000000..5785f7bba --- /dev/null +++ b/plugins/schemas/scrobbler.yaml @@ -0,0 +1,182 @@ +version: v1-draft + +exports: + nd_scrobbler_is_authorized: + description: Check if a user is authorized to scrobble to this service + input: + $ref: "#/components/schemas/AuthInput" + contentType: application/json + output: + $ref: "#/components/schemas/AuthOutput" + contentType: application/json + + nd_scrobbler_now_playing: + description: Send a now playing notification to the scrobbling service + input: + $ref: "#/components/schemas/NowPlayingInput" + contentType: application/json + output: + $ref: "#/components/schemas/ScrobblerOutput" + contentType: application/json + + nd_scrobbler_scrobble: + description: Submit a completed scrobble to the scrobbling service + input: + $ref: "#/components/schemas/ScrobbleInput" + contentType: application/json + output: + $ref: "#/components/schemas/ScrobblerOutput" + contentType: application/json + +components: + schemas: + AuthInput: + description: Input for authorization check + properties: + user_id: + type: string + description: The internal Navidrome user ID + username: + type: string + description: The username of the user + required: + - user_id + - username + + AuthOutput: + description: Output for authorization check + properties: + authorized: + type: boolean + description: Whether the user is authorized to scrobble + required: + - authorized + + TrackInfo: + description: Track metadata for scrobbling + properties: + id: + type: string + description: The internal Navidrome track ID + title: + type: string + description: Track title + album: + type: string + description: Album name + artist: + type: string + description: Track artist + album_artist: + type: string + description: Album artist + duration: + type: number + format: float + description: Track duration in seconds + track_number: + type: integer + format: int32 + description: Track number on the album + disc_number: + type: integer + format: int32 + description: Disc number + mbz_recording_id: + type: string + nullable: true + description: MusicBrainz recording ID + mbz_album_id: + type: string + nullable: true + description: MusicBrainz album/release ID + mbz_artist_id: + type: string + nullable: true + description: MusicBrainz artist ID + mbz_release_group_id: + type: string + nullable: true + description: MusicBrainz release group ID + mbz_album_artist_id: + type: string + nullable: true + description: MusicBrainz album artist ID + mbz_release_track_id: + type: string + nullable: true + description: MusicBrainz release track ID + required: + - id + - title + - album + - artist + - album_artist + - duration + - track_number + - disc_number + + NowPlayingInput: + description: Input for now playing notification + properties: + user_id: + type: string + description: The internal Navidrome user ID + username: + type: string + description: The username of the user + track: + $ref: "#/components/schemas/TrackInfo" + description: The track currently playing + position: + type: integer + format: int32 + description: Current playback position in seconds + required: + - user_id + - username + - track + - position + + ScrobbleInput: + description: Input for submitting a scrobble + properties: + user_id: + type: string + description: The internal Navidrome user ID + username: + type: string + description: The username of the user + track: + $ref: "#/components/schemas/TrackInfo" + description: The track that was played + timestamp: + type: integer + format: int64 + description: Unix timestamp when the track started playing + required: + - user_id + - username + - track + - timestamp + + ScrobblerOutput: + description: Output for scrobbler operations (now_playing and scrobble) + properties: + error: + type: string + nullable: true + description: Error message if the operation failed + error_type: + $ref: "#/components/schemas/ScrobblerErrorType" + nullable: true + description: Type of error for handling + + ScrobblerErrorType: + type: string + description: Error type indicating how Navidrome should handle the error + enum: + - none + - not_authorized + - retry_later + - unrecoverable