mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
feat: rewrite the wikimedia plugin using the XTP CLI
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
7a9a63b219
commit
1a7ba7f293
@ -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).
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
376
plugins/examples/wikimedia/pdk.gen.go
Executable file
376
plugins/examples/wikimedia/pdk.gen.go
Executable 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"`
|
||||
}
|
||||
92
plugins/examples/wikimedia/prepare.sh
Normal file
92
plugins/examples/wikimedia/prepare.sh
Normal 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
|
||||
17
plugins/examples/wikimedia/xtp.toml
Executable file
17
plugins/examples/wikimedia/xtp.toml
Executable 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 ./..."
|
||||
Loading…
x
Reference in New Issue
Block a user