From 1a7ba7f2933bb85d4d5de21e9139075149a5ff28 Mon Sep 17 00:00:00 2001 From: Deluan Date: Wed, 24 Dec 2025 10:38:25 -0500 Subject: [PATCH] feat: rewrite the wikimedia plugin using the XTP CLI Signed-off-by: Deluan --- plugins/examples/README.md | 29 +- plugins/examples/wikimedia/README.md | 63 +++-- plugins/examples/wikimedia/main.go | 205 ++++++-------- plugins/examples/wikimedia/pdk.gen.go | 376 ++++++++++++++++++++++++++ plugins/examples/wikimedia/prepare.sh | 92 +++++++ plugins/examples/wikimedia/xtp.toml | 17 ++ 6 files changed, 635 insertions(+), 147 deletions(-) create mode 100755 plugins/examples/wikimedia/pdk.gen.go create mode 100644 plugins/examples/wikimedia/prepare.sh create mode 100755 plugins/examples/wikimedia/xtp.toml diff --git a/plugins/examples/README.md b/plugins/examples/README.md index 168788e11..887b37c4e 100644 --- a/plugins/examples/README.md +++ b/plugins/examples/README.md @@ -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). diff --git a/plugins/examples/wikimedia/README.md b/plugins/examples/wikimedia/README.md index ae78fd467..6151eeb38 100644 --- a/plugins/examples/wikimedia/README.md +++ b/plugins/examples/wikimedia/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 | diff --git a/plugins/examples/wikimedia/main.go b/plugins/examples/wikimedia/main.go index b85ca4be0..290d86f1b 100644 --- a/plugins/examples/wikimedia/main.go +++ b/plugins/examples/wikimedia/main.go @@ -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 . } 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") +} diff --git a/plugins/examples/wikimedia/pdk.gen.go b/plugins/examples/wikimedia/pdk.gen.go new file mode 100755 index 000000000..029063d7e --- /dev/null +++ b/plugins/examples/wikimedia/pdk.gen.go @@ -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"` +} diff --git a/plugins/examples/wikimedia/prepare.sh b/plugins/examples/wikimedia/prepare.sh new file mode 100644 index 000000000..9fbb93cfb --- /dev/null +++ b/plugins/examples/wikimedia/prepare.sh @@ -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 diff --git a/plugins/examples/wikimedia/xtp.toml b/plugins/examples/wikimedia/xtp.toml new file mode 100755 index 000000000..73000ebcd --- /dev/null +++ b/plugins/examples/wikimedia/xtp.toml @@ -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 ./..."