feat: rewrite the wikimedia plugin using the XTP CLI

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan 2025-12-24 10:38:25 -05:00
parent 7a9a63b219
commit 1a7ba7f293
6 changed files with 635 additions and 147 deletions

View File

@ -53,9 +53,9 @@ extism call minimal.wasm nd_get_artist_biography --wasi \
For plugins that make HTTP requests, use `--allow-host` to permit access:
```bash
extism call wikimedia.wasm nd_get_artist_url --wasi \
--input '{"id":"1","name":"The Beatles"}' \
--allow-host "query.wikidata.org"
extism call wikimedia.wasm nd_get_artist_biography --wasi \  3s   ▼
--input '{"id":"1","name":"Yussef Dayes"}' \
--allow-host "query.wikidata.org" --allow-host "en.wikipedia.org"
```
## Installation
@ -63,7 +63,7 @@ extism call wikimedia.wasm nd_get_artist_url --wasi \
Copy any `.wasm` file to your Navidrome plugins folder:
```bash
cp minimal.wasm /path/to/navidrome/plugins/
cp wikimedia.wasm /path/to/navidrome/plugins/
```
Then enable plugins in your `navidrome.toml`:
@ -82,6 +82,25 @@ Agents = "lastfm,spotify,wikimedia"
## Creating Your Own Plugin
See the [minimal](minimal/) example for the simplest starting point, or [wikimedia](wikimedia/) for a more complete example with HTTP requests.
See the [minimal](minimal/) example for the simplest starting point, or [wikimedia](wikimedia/) for a more complete
example with HTTP requests, created with the [XTP CLI]((https://docs.xtp.dylibso.com/docs/cli).
### Bootstrapping a New Plugin
Use the XTP CLI to bootstrap a new plugin from a schema:
```bash
xtp plugin init \
--schema-file plugins/schemas/metadata_agent.yaml \
--template go \
--path ./my-plugin \
--name my-plugin
```
See the [schemas README](../schemas/README.md) for more information about available schemas
and supported languages.
For the simplest starting point, look at [minimal](minimal/). For a more complete example
with HTTP requests, see [wikimedia](wikimedia/).
For full documentation, see the [Plugin System README](../README.md).

View File

@ -2,6 +2,18 @@
A Navidrome plugin that fetches artist metadata from Wikidata, DBpedia, and Wikipedia.
## Generating the Plugin
This plugin was generated using the XTP CLI:
```bash
xtp plugin init \
--schema-file plugins/schemas/metadata_agent.yaml \
--template go \
--path ./wikimedia \
--name wikimedia-plugin
```
## Features
- **Artist URL**: Fetches Wikipedia URL for an artist using Wikidata (by MBID or name), DBpedia, or falls back to a Wikipedia search URL
@ -10,37 +22,32 @@ A Navidrome plugin that fetches artist metadata from Wikidata, DBpedia, and Wiki
## Building
### Prerequisites
- [TinyGo](https://tinygo.org/getting-started/install/) (recommended) or Go 1.23+
### Build using the Makefile (recommended)
### Using XTP CLI (recommended)
```bash
xtp plugin build
```
### Using TinyGo
```bash
tinygo build -target wasip1 -buildmode=c-shared -o dist/plugin.wasm .
```
### Using the Makefile
From the `plugins/examples` directory:
```bash
cd plugins/examples
make wikimedia.wasm
```
### Build manually with TinyGo
```bash
cd plugins/examples/wikimedia
tinygo build -target wasip1 -buildmode=c-shared -o ../wikimedia.wasm .
```
### Build manually with Go
```bash
cd plugins/examples/wikimedia
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o ../wikimedia.wasm .
```
## Installation
Copy `wikimedia.wasm` from the examples folder to your Navidrome plugins folder:
Copy the `.wasm` file to your Navidrome plugins folder:
```bash
cp plugins/examples/wikimedia.wasm /path/to/navidrome/plugins/
cp dist/plugin.wasm /path/to/navidrome/plugins/wikimedia.wasm
```
Then enable plugins in your `navidrome.toml`:
@ -115,6 +122,18 @@ Expected output:
{"images":[{"url":"http://commons.wikimedia.org/wiki/Special:FilePath/Beatles%20ad%201965%20just%20the%20beatles%20crop.jpg","size":0}]}
```
## Project Structure
```
wikimedia/
├── main.go # Plugin implementation with Wikimedia API logic
├── pdk.gen.go # Generated types and export wrappers (DO NOT EDIT)
├── go.mod # Go module file
├── go.sum # Go module checksums
├── prepare.sh # Build preparation script
└── xtp.toml # XTP plugin configuration
```
## API Endpoints Used
| Service | Endpoint | Purpose |

View File

@ -1,14 +1,23 @@
// Wikimedia plugin for Navidrome - fetches artist metadata from Wikidata, DBpedia and Wikipedia.
//
// This plugin was generated using:
//
// xtp plugin init --schema-file plugins/schemas/metadata_agent.yaml --template go --path ./wikimedia --name wikimedia-plugin
//
// Build with:
//
// tinygo build -o wikimedia.wasm -target wasip1 -buildmode=c-shared ./main.go
// xtp plugin build
//
// Install by copying wikimedia.wasm to your Navidrome plugins folder.
// Or manually:
//
// tinygo build -o wikimedia.wasm -target wasip1 -buildmode=c-shared .
//
// Install by copying the .wasm file to your Navidrome plugins folder.
package main
import (
"encoding/json"
"errors"
"fmt"
"net/url"
"strings"
@ -22,7 +31,7 @@ const (
mediawikiAPIEndpoint = "https://en.wikipedia.org/w/api.php"
)
// Manifest types
// Plugin manifest containing metadata about this plugin
type Manifest struct {
Name string `json:"name"`
Author string `json:"author"`
@ -41,31 +50,6 @@ type HTTPPermission struct {
AllowedHosts []string `json:"allowedHosts,omitempty"`
}
// Input types
type ArtistInput struct {
ID string `json:"id"`
Name string `json:"name"`
MBID string `json:"mbid,omitempty"`
}
// Output types
type URLOutput struct {
URL string `json:"url"`
}
type BiographyOutput struct {
Biography string `json:"biography"`
}
type ImageInfo struct {
URL string `json:"url"`
Size int `json:"size"`
}
type ImagesOutput struct {
Images []ImageInfo `json:"images"`
}
// SPARQL response types
type SPARQLResult struct {
Results struct {
@ -99,7 +83,9 @@ type MediaWikiPage struct {
Missing bool `json:"missing"`
}
//go:wasmexport nd_manifest
// nd_manifest is required by Navidrome to identify the plugin.
//
//export nd_manifest
func ndManifest() int32 {
manifest := Manifest{
Name: "Wikimedia",
@ -150,7 +136,7 @@ func sparqlQuery(endpoint, query string) (*SPARQLResult, error) {
return nil, fmt.Errorf("failed to parse SPARQL response: %w", err)
}
if len(result.Results.Bindings) == 0 {
return nil, fmt.Errorf("not found")
return nil, errors.New("not found")
}
return &result, nil
}
@ -179,7 +165,7 @@ func getWikidataWikipediaURL(mbid, name string) (string, error) {
escapedName := strings.ReplaceAll(name, "\"", "\\\"")
q = fmt.Sprintf(`SELECT ?sitelink WHERE { ?artist rdfs:label "%s"@en. ?sitelink schema:about ?artist; schema:isPartOf <https://en.wikipedia.org/>. } LIMIT 1`, escapedName)
} else {
return "", fmt.Errorf("MBID or Name required for Wikidata URL lookup")
return "", errors.New("MBID or Name required for Wikidata URL lookup")
}
result, err := sparqlQuery(wikidataEndpoint, q)
@ -189,13 +175,13 @@ func getWikidataWikipediaURL(mbid, name string) (string, error) {
if result.Results.Bindings[0].Sitelink != nil {
return result.Results.Bindings[0].Sitelink.Value, nil
}
return "", fmt.Errorf("not found")
return "", errors.New("not found")
}
// getDBpediaWikipediaURL fetches the Wikipedia URL from DBpedia using name
func getDBpediaWikipediaURL(name string) (string, error) {
if name == "" {
return "", fmt.Errorf("not found")
return "", errors.New("not found")
}
escapedName := strings.ReplaceAll(name, "\"", "\\\"")
q := fmt.Sprintf(`SELECT ?wiki WHERE { ?artist foaf:name "%s"@en; foaf:isPrimaryTopicOf ?wiki. FILTER regex(str(?wiki), "^https://en.wikipedia.org/") } LIMIT 1`, escapedName)
@ -207,13 +193,13 @@ func getDBpediaWikipediaURL(name string) (string, error) {
if result.Results.Bindings[0].Wiki != nil {
return result.Results.Bindings[0].Wiki.Value, nil
}
return "", fmt.Errorf("not found")
return "", errors.New("not found")
}
// getDBpediaComment fetches the DBpedia comment (short bio) for an artist
func getDBpediaComment(name string) (string, error) {
if name == "" {
return "", fmt.Errorf("not found")
return "", errors.New("not found")
}
escapedName := strings.ReplaceAll(name, "\"", "\\\"")
q := fmt.Sprintf(`SELECT ?comment WHERE { ?artist foaf:name "%s"@en; rdfs:comment ?comment. FILTER (lang(?comment) = 'en') } LIMIT 1`, escapedName)
@ -225,13 +211,13 @@ func getDBpediaComment(name string) (string, error) {
if result.Results.Bindings[0].Comment != nil {
return result.Results.Bindings[0].Comment.Value, nil
}
return "", fmt.Errorf("not found")
return "", errors.New("not found")
}
// getWikipediaExtract fetches the intro text from Wikipedia
func getWikipediaExtract(pageTitle string) (string, error) {
if pageTitle == "" {
return "", fmt.Errorf("page title required")
return "", errors.New("page title required")
}
params := url.Values{}
params.Set("action", "query")
@ -260,7 +246,7 @@ func getWikipediaExtract(pageTitle string) (string, error) {
return strings.TrimSpace(page.Extract), nil
}
}
return "", fmt.Errorf("not found")
return "", errors.New("not found")
}
// extractPageTitleFromURL extracts the page title from a Wikipedia URL
@ -278,7 +264,7 @@ func extractPageTitleFromURL(wikiURL string) (string, error) {
}
title := pathParts[1]
if title == "" {
return "", fmt.Errorf("extracted title is empty")
return "", errors.New("extracted title is empty")
}
decodedTitle, err := url.PathUnescape(title)
if err != nil {
@ -287,25 +273,23 @@ func extractPageTitleFromURL(wikiURL string) (string, error) {
return decodedTitle, nil
}
//go:wasmexport nd_get_artist_url
func ndGetArtistURL() int32 {
var input ArtistInput
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(err)
return 1
// getMBID extracts the MBID from an optional pointer
func getMBID(mbid *string) string {
if mbid == nil {
return ""
}
return *mbid
}
pdk.Log(pdk.LogDebug, fmt.Sprintf("GetArtistURL: name=%s, mbid=%s", input.Name, input.MBID))
// NdGetArtistUrl returns the Wikipedia URL for an artist
func NdGetArtistUrl(input ArtistInput) (ArtistURLOutput, error) {
mbid := getMBID(input.Mbid)
pdk.Log(pdk.LogDebug, fmt.Sprintf("GetArtistURL: name=%s, mbid=%s", input.Name, mbid))
// 1. Try Wikidata (MBID first, then name)
wikiURL, err := getWikidataWikipediaURL(input.MBID, input.Name)
wikiURL, err := getWikidataWikipediaURL(mbid, input.Name)
if err == nil && wikiURL != "" {
output := URLOutput{URL: wikiURL}
if err := pdk.OutputJSON(output); err != nil {
pdk.SetError(err)
return 1
}
return 0
return ArtistURLOutput{Url: wikiURL}, nil
}
if err != nil {
pdk.Log(pdk.LogDebug, fmt.Sprintf("Wikidata URL failed: %v", err))
@ -315,12 +299,7 @@ func ndGetArtistURL() int32 {
if input.Name != "" {
wikiURL, err = getDBpediaWikipediaURL(input.Name)
if err == nil && wikiURL != "" {
output := URLOutput{URL: wikiURL}
if err := pdk.OutputJSON(output); err != nil {
pdk.SetError(err)
return 1
}
return 0
return ArtistURLOutput{Url: wikiURL}, nil
}
if err != nil {
pdk.Log(pdk.LogDebug, fmt.Sprintf("DBpedia URL failed: %v", err))
@ -331,31 +310,20 @@ func ndGetArtistURL() int32 {
if input.Name != "" {
searchURL := fmt.Sprintf("https://en.wikipedia.org/w/index.php?search=%s", url.QueryEscape(input.Name))
pdk.Log(pdk.LogInfo, fmt.Sprintf("URL not found, falling back to search URL: %s", searchURL))
output := URLOutput{URL: searchURL}
if err := pdk.OutputJSON(output); err != nil {
pdk.SetError(err)
return 1
}
return 0
return ArtistURLOutput{Url: searchURL}, nil
}
pdk.SetErrorString("could not determine Wikipedia URL")
return 1
return ArtistURLOutput{}, errors.New("could not determine Wikipedia URL")
}
//go:wasmexport nd_get_artist_biography
func ndGetArtistBiography() int32 {
var input ArtistInput
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(err)
return 1
}
pdk.Log(pdk.LogDebug, fmt.Sprintf("GetArtistBiography: name=%s, mbid=%s", input.Name, input.MBID))
// NdGetArtistBiography returns the biography for an artist from Wikipedia
func NdGetArtistBiography(input ArtistInput) (ArtistBiographyOutput, error) {
mbid := getMBID(input.Mbid)
pdk.Log(pdk.LogDebug, fmt.Sprintf("GetArtistBiography: name=%s, mbid=%s", input.Name, mbid))
// 1. Get Wikipedia URL (using the logic from GetArtistURL)
wikiURL := ""
tempURL, wdErr := getWikidataWikipediaURL(input.MBID, input.Name)
tempURL, wdErr := getWikidataWikipediaURL(mbid, input.Name)
if wdErr == nil && tempURL != "" {
pdk.Log(pdk.LogDebug, fmt.Sprintf("Found Wikidata URL: %s", tempURL))
wikiURL = tempURL
@ -378,12 +346,7 @@ func ndGetArtistBiography() int32 {
bio, err := getWikipediaExtract(pageTitle)
if err == nil && bio != "" {
pdk.Log(pdk.LogDebug, "Found Wikipedia extract")
output := BiographyOutput{Biography: bio}
if err := pdk.OutputJSON(output); err != nil {
pdk.SetError(err)
return 1
}
return 0
return ArtistBiographyOutput{Biography: bio}, nil
}
pdk.Log(pdk.LogDebug, fmt.Sprintf("Wikipedia extract failed: %v", err))
} else {
@ -397,62 +360,64 @@ func ndGetArtistBiography() int32 {
bio, err := getDBpediaComment(input.Name)
if err == nil && bio != "" {
pdk.Log(pdk.LogDebug, "Found DBpedia comment")
output := BiographyOutput{Biography: bio}
if err := pdk.OutputJSON(output); err != nil {
pdk.SetError(err)
return 1
}
return 0
return ArtistBiographyOutput{Biography: bio}, nil
}
pdk.Log(pdk.LogDebug, fmt.Sprintf("DBpedia comment failed: %v", err))
}
pdk.Log(pdk.LogInfo, fmt.Sprintf("Biography not found for: %s (%s)", input.Name, input.MBID))
pdk.SetErrorString("biography not found")
return 1
pdk.Log(pdk.LogInfo, fmt.Sprintf("Biography not found for: %s (%s)", input.Name, mbid))
return ArtistBiographyOutput{}, errors.New("biography not found")
}
//go:wasmexport nd_get_artist_images
func ndGetArtistImages() int32 {
var input ArtistInput
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(err)
return 1
}
pdk.Log(pdk.LogDebug, fmt.Sprintf("GetArtistImages: name=%s, mbid=%s", input.Name, input.MBID))
// NdGetArtistImages returns artist images from Wikidata
func NdGetArtistImages(input ArtistInput) (ArtistImagesOutput, error) {
mbid := getMBID(input.Mbid)
pdk.Log(pdk.LogDebug, fmt.Sprintf("GetArtistImages: name=%s, mbid=%s", input.Name, mbid))
var q string
if input.MBID != "" {
q = fmt.Sprintf(`SELECT ?img WHERE { ?artist wdt:P434 "%s"; wdt:P18 ?img } LIMIT 1`, input.MBID)
if mbid != "" {
q = fmt.Sprintf(`SELECT ?img WHERE { ?artist wdt:P434 "%s"; wdt:P18 ?img } LIMIT 1`, mbid)
} else if input.Name != "" {
escapedName := strings.ReplaceAll(input.Name, "\"", "\\\"")
q = fmt.Sprintf(`SELECT ?img WHERE { ?artist rdfs:label "%s"@en; wdt:P18 ?img } LIMIT 1`, escapedName)
} else {
pdk.SetErrorString("MBID or Name required for Wikidata Image lookup")
return 1
return ArtistImagesOutput{}, errors.New("MBID or Name required for Wikidata Image lookup")
}
result, err := sparqlQuery(wikidataEndpoint, q)
if err != nil {
pdk.Log(pdk.LogInfo, fmt.Sprintf("Image not found for: %s (%s)", input.Name, input.MBID))
pdk.SetErrorString("image not found")
return 1
pdk.Log(pdk.LogInfo, fmt.Sprintf("Image not found for: %s (%s)", input.Name, mbid))
return ArtistImagesOutput{}, errors.New("image not found")
}
if result.Results.Bindings[0].Img != nil {
output := ImagesOutput{
Images: []ImageInfo{{URL: result.Results.Bindings[0].Img.Value, Size: 0}},
}
if err := pdk.OutputJSON(output); err != nil {
pdk.SetError(err)
return 1
}
return 0
return ArtistImagesOutput{
Images: []ImageInfo{{Url: result.Results.Bindings[0].Img.Value, Size: 0}},
}, nil
}
pdk.Log(pdk.LogInfo, fmt.Sprintf("Image not found for: %s (%s)", input.Name, input.MBID))
pdk.SetErrorString("image not found")
return 1
pdk.Log(pdk.LogInfo, fmt.Sprintf("Image not found for: %s (%s)", input.Name, mbid))
return ArtistImagesOutput{}, errors.New("image not found")
}
func main() {}
// The functions below are not implemented - they return errors to indicate
// Navidrome should fall back to other agents.
func NdGetAlbumImages(input AlbumInput) (AlbumImagesOutput, error) {
return AlbumImagesOutput{}, errors.New("not implemented")
}
func NdGetAlbumInfo(input AlbumInput) (AlbumInfoOutput, error) {
return AlbumInfoOutput{}, errors.New("not implemented")
}
func NdGetArtistMbid(input ArtistMBIDInput) (ArtistMBIDOutput, error) {
return ArtistMBIDOutput{}, errors.New("not implemented")
}
func NdGetArtistTopSongs(input TopSongsInput) (TopSongsOutput, error) {
return TopSongsOutput{}, errors.New("not implemented")
}
func NdGetSimilarArtists(input SimilarArtistsInput) (SimilarArtistsOutput, error) {
return SimilarArtistsOutput{}, errors.New("not implemented")
}

View File

@ -0,0 +1,376 @@
// THIS FILE WAS GENERATED BY `xtp-go-bindgen`. DO NOT EDIT.
package main
import (
pdk "github.com/extism/go-pdk"
)
//export nd_get_album_images
func _NdGetAlbumImages() int32 {
var err error
_ = err
pdk.Log(pdk.LogDebug, "NdGetAlbumImages: getting JSON input")
var input AlbumInput
err = pdk.InputJSON(&input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdGetAlbumImages: calling implementation function")
output, err := NdGetAlbumImages(input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdGetAlbumImages: setting JSON output")
err = pdk.OutputJSON(output)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdGetAlbumImages: returning")
return 0
}
//export nd_get_album_info
func _NdGetAlbumInfo() int32 {
var err error
_ = err
pdk.Log(pdk.LogDebug, "NdGetAlbumInfo: getting JSON input")
var input AlbumInput
err = pdk.InputJSON(&input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdGetAlbumInfo: calling implementation function")
output, err := NdGetAlbumInfo(input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdGetAlbumInfo: setting JSON output")
err = pdk.OutputJSON(output)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdGetAlbumInfo: returning")
return 0
}
//export nd_get_artist_biography
func _NdGetArtistBiography() int32 {
var err error
_ = err
pdk.Log(pdk.LogDebug, "NdGetArtistBiography: getting JSON input")
var input ArtistInput
err = pdk.InputJSON(&input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdGetArtistBiography: calling implementation function")
output, err := NdGetArtistBiography(input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdGetArtistBiography: setting JSON output")
err = pdk.OutputJSON(output)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdGetArtistBiography: returning")
return 0
}
//export nd_get_artist_images
func _NdGetArtistImages() int32 {
var err error
_ = err
pdk.Log(pdk.LogDebug, "NdGetArtistImages: getting JSON input")
var input ArtistInput
err = pdk.InputJSON(&input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdGetArtistImages: calling implementation function")
output, err := NdGetArtistImages(input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdGetArtistImages: setting JSON output")
err = pdk.OutputJSON(output)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdGetArtistImages: returning")
return 0
}
//export nd_get_artist_mbid
func _NdGetArtistMbid() int32 {
var err error
_ = err
pdk.Log(pdk.LogDebug, "NdGetArtistMbid: getting JSON input")
var input ArtistMBIDInput
err = pdk.InputJSON(&input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdGetArtistMbid: calling implementation function")
output, err := NdGetArtistMbid(input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdGetArtistMbid: setting JSON output")
err = pdk.OutputJSON(output)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdGetArtistMbid: returning")
return 0
}
//export nd_get_artist_top_songs
func _NdGetArtistTopSongs() int32 {
var err error
_ = err
pdk.Log(pdk.LogDebug, "NdGetArtistTopSongs: getting JSON input")
var input TopSongsInput
err = pdk.InputJSON(&input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdGetArtistTopSongs: calling implementation function")
output, err := NdGetArtistTopSongs(input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdGetArtistTopSongs: setting JSON output")
err = pdk.OutputJSON(output)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdGetArtistTopSongs: returning")
return 0
}
//export nd_get_artist_url
func _NdGetArtistUrl() int32 {
var err error
_ = err
pdk.Log(pdk.LogDebug, "NdGetArtistUrl: getting JSON input")
var input ArtistInput
err = pdk.InputJSON(&input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdGetArtistUrl: calling implementation function")
output, err := NdGetArtistUrl(input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdGetArtistUrl: setting JSON output")
err = pdk.OutputJSON(output)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdGetArtistUrl: returning")
return 0
}
//export nd_get_similar_artists
func _NdGetSimilarArtists() int32 {
var err error
_ = err
pdk.Log(pdk.LogDebug, "NdGetSimilarArtists: getting JSON input")
var input SimilarArtistsInput
err = pdk.InputJSON(&input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdGetSimilarArtists: calling implementation function")
output, err := NdGetSimilarArtists(input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdGetSimilarArtists: setting JSON output")
err = pdk.OutputJSON(output)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdGetSimilarArtists: returning")
return 0
}
// Output for GetAlbumImages
type AlbumImagesOutput struct {
// List of album images
Images []ImageInfo `json:"images"`
}
// Output for GetAlbumInfo
type AlbumInfoOutput struct {
// The album description/notes
Description string `json:"description"`
// The MusicBrainz ID for the album
Mbid string `json:"mbid"`
// The album name
Name string `json:"name"`
// The external URL for the album
Url string `json:"url"`
}
// Common input for album-related functions
type AlbumInput struct {
// The album artist name
Artist string `json:"artist"`
// The MusicBrainz ID for the album (if known)
Mbid *string `json:"mbid,omitempty"`
// The album name
Name string `json:"name"`
}
// Output for GetArtistBiography
type ArtistBiographyOutput struct {
// The artist biography text
Biography string `json:"biography"`
}
// Output for GetArtistImages
type ArtistImagesOutput struct {
// List of artist images
Images []ImageInfo `json:"images"`
}
// Common input for artist-related functions
type ArtistInput struct {
// The internal Navidrome artist ID
Id string `json:"id"`
// The MusicBrainz ID for the artist (if known)
Mbid *string `json:"mbid,omitempty"`
// The artist name
Name string `json:"name"`
}
// Input for GetArtistMBID
type ArtistMBIDInput struct {
// The internal Navidrome artist ID
Id string `json:"id"`
// The artist name
Name string `json:"name"`
}
// Output for GetArtistMBID
type ArtistMBIDOutput struct {
// The MusicBrainz ID for the artist
Mbid string `json:"mbid"`
}
// Reference to an artist with name and optional MBID
type ArtistRef struct {
// The MusicBrainz ID for the artist
Mbid *string `json:"mbid,omitempty"`
// The artist name
Name string `json:"name"`
}
// Output for GetArtistURL
type ArtistURLOutput struct {
// The external URL for the artist
Url string `json:"url"`
}
// Image with URL and size
type ImageInfo struct {
// The size of the image in pixels (width or height)
Size int32 `json:"size"`
// The URL of the image
Url string `json:"url"`
}
// Input for GetSimilarArtists
type SimilarArtistsInput struct {
// The internal Navidrome artist ID
Id string `json:"id"`
// Maximum number of similar artists to return
Limit int32 `json:"limit"`
// The MusicBrainz ID for the artist (if known)
Mbid *string `json:"mbid,omitempty"`
// The artist name
Name string `json:"name"`
}
// Output for GetSimilarArtists
type SimilarArtistsOutput struct {
// List of similar artists
Artists []ArtistRef `json:"artists"`
}
// Reference to a song with name and optional MBID
type SongRef struct {
// The MusicBrainz ID for the song
Mbid *string `json:"mbid,omitempty"`
// The song name
Name string `json:"name"`
}
// Input for GetArtistTopSongs
type TopSongsInput struct {
// Maximum number of top songs to return
Count int32 `json:"count"`
// The internal Navidrome artist ID
Id string `json:"id"`
// The MusicBrainz ID for the artist (if known)
Mbid *string `json:"mbid,omitempty"`
// The artist name
Name string `json:"name"`
}
// Output for GetArtistTopSongs
type TopSongsOutput struct {
// List of top songs
Songs []SongRef `json:"songs"`
}

View File

@ -0,0 +1,92 @@
#!/bin/bash
set -eou pipefail
# Function to check if a command exists
command_exists () {
command -v "$1" >/dev/null 2>&1
}
# Function to compare version numbers for "less than"
version_lt() {
test "$(echo "$@" | tr " " "\n" | sort -V | head -n 1)" = "$1" && test "$1" != "$2"
}
missing_deps=0
# Check for Go
if ! (command_exists go); then
missing_deps=1
echo "❌ Go (supported version between 1.20 - 1.24) is not installed."
echo ""
echo "To install Go, visit the official download page:"
echo "👉 https://go.dev/dl/"
echo ""
echo "Or install it using a package manager:"
echo ""
echo "🔹 macOS (Homebrew):"
echo " brew install go"
echo ""
echo "🔹 Ubuntu/Debian:"
echo " sudo apt-get -y install golang-go"
echo ""
echo "🔹 Arch Linux:"
echo " sudo pacman -S go"
echo ""
echo "🔹 Windows:"
echo " scoop install go"
echo ""
fi
# Check for the right version of Go, needed by TinyGo (supports go 1.20 - 1.24)
if (command_exists go); then
compat=0
for v in `seq 20 24`; do
if (go version | grep -q "go1.$v"); then
compat=1
fi
done
if [ $compat -eq 0 ]; then
echo "❌ Supported Go version is not installed. Must be Go 1.20 - 1.24."
echo ""
fi
fi
ARCH=$(arch)
# Check for TinyGo and its version
if ! (command_exists tinygo); then
missing_deps=1
echo "❌ TinyGo is not installed."
echo ""
echo "To install TinyGo, visit the official download page:"
echo "👉 https://tinygo.org/getting-started/install/"
echo ""
echo "Or install it using a package manager:"
echo ""
echo "🔹 macOS (Homebrew):"
echo " brew tap tinygo-org/tools"
echo " brew install tinygo"
echo ""
echo "🔹 Ubuntu/Debian:"
echo " wget https://github.com/tinygo-org/tinygo/releases/download/v0.34.0/tinygo_0.34.0_$ARCH.deb"
echo " sudo dpkg -i tinygo_0.34.0_$ARCH.deb"
echo ""
echo "🔹 Arch Linux:"
echo " pacman -S extra/tinygo"
echo ""
echo "🔹 Windows:"
echo " scoop install tinygo"
echo ""
else
# Check TinyGo version
tinygo_version=$(tinygo version | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' | head -n1)
if version_lt "$tinygo_version" "0.34.0"; then
missing_deps=1
echo "❌ TinyGo version must be >= 0.34.0 (current version: $tinygo_version)"
echo "Please update TinyGo to a newer version."
echo ""
fi
fi
go install golang.org/x/tools/cmd/goimports@latest

View File

@ -0,0 +1,17 @@
app_id = ""
# This is where 'xtp plugin push' expects to find the wasm file after the build script has run.
bin = "dist/plugin.wasm"
extension_point_id = ""
name = "wikimedia-plugin"
[scripts]
# xtp plugin build runs this script to generate the wasm file
build = "mkdir -p dist && tinygo build -buildmode c-shared -target wasip1 -o dist/plugin.wasm ."
# xtp plugin init runs this script to format the plugin code
format = "go fmt && go mod tidy && goimports -w main.go"
# xtp plugin init runs this script before running the format script
prepare = "bash prepare.sh && go get ./..."