mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
feat: add Cover Art Archive plugin as an example of Python plugin
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
513c969c40
commit
870cd49307
@ -6,7 +6,8 @@ This folder contains example plugins for Navidrome that demonstrate how to build
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [TinyGo](https://tinygo.org/getting-started/install/) (recommended) or Go 1.23+
|
||||
- [TinyGo](https://tinygo.org/getting-started/install/) (recommended) or Go 1.23+ (for Go plugins)
|
||||
- [extism-py](https://github.com/extism/python-pdk) (for Python plugins)
|
||||
- [Extism CLI](https://extism.org/docs/install) (optional, for testing)
|
||||
|
||||
### Build all plugins
|
||||
@ -32,12 +33,13 @@ make clean
|
||||
|
||||
## Available Examples
|
||||
|
||||
| Plugin | Description |
|
||||
|-------------------------------------------------|-------------------------------------------------------------------------|
|
||||
| [minimal](minimal/) | A minimal example showing the basic plugin structure |
|
||||
| [wikimedia](wikimedia/) | Fetches artist metadata from Wikidata, DBpedia, and Wikipedia |
|
||||
| [crypto-ticker](crypto-ticker/) | Real-time cryptocurrency prices from Coinbase using WebSocket |
|
||||
| [discord-rich-presence](discord-rich-presence/) | Discord Rich Presence integration using Scrobbler, WebSocket, Scheduler |
|
||||
| Plugin | Language | Description |
|
||||
|-------------------------------------------------|----------|-------------------------------------------------------------------------|
|
||||
| [minimal](minimal/) | Go | A minimal example showing the basic plugin structure |
|
||||
| [wikimedia](wikimedia/) | Go | Fetches artist metadata from Wikidata, DBpedia, and Wikipedia |
|
||||
| [crypto-ticker](crypto-ticker/) | Go | Real-time cryptocurrency prices from Coinbase using WebSocket |
|
||||
| [discord-rich-presence](discord-rich-presence/) | Go | Discord Rich Presence integration using Scrobbler, WebSocket, Scheduler |
|
||||
| [coverartarchive-py](coverartarchive-py/) | Python | Album cover art from Cover Art Archive (Python example) |
|
||||
|
||||
## Testing with Extism CLI
|
||||
|
||||
@ -84,8 +86,9 @@ 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, created with the [XTP CLI]((https://docs.xtp.dylibso.com/docs/cli).
|
||||
The plugin system supports multiple languages. See the [minimal](minimal/) example for the simplest Go starting point,
|
||||
[discord-rich-presence](discord-rich-presence/) for a more complete Go example with HTTP requests, or [coverartarchive-py](coverartarchive-py/)
|
||||
for a Python example.
|
||||
|
||||
### Bootstrapping a New Plugin
|
||||
Use the XTP CLI to bootstrap a new plugin from a schema:
|
||||
|
||||
27
plugins/examples/coverartarchive-py/Makefile
Normal file
27
plugins/examples/coverartarchive-py/Makefile
Normal file
@ -0,0 +1,27 @@
|
||||
# Build the Cover Art Archive Python plugin
|
||||
.PHONY: build test clean
|
||||
|
||||
WASM_FILE = coverartarchive-py.wasm
|
||||
|
||||
build: $(WASM_FILE)
|
||||
|
||||
$(WASM_FILE): plugin/__init__.py
|
||||
extism-py plugin/__init__.py -o $(WASM_FILE)
|
||||
|
||||
test: build
|
||||
@echo "Testing nd_manifest..."
|
||||
extism call $(WASM_FILE) nd_manifest --wasi
|
||||
@echo ""
|
||||
@echo "Testing nd_get_album_images with Portishead's Dummy MBID..."
|
||||
extism call $(WASM_FILE) nd_get_album_images --wasi \
|
||||
--input '{"name":"Dummy","artist":"Portishead","mbid":"76df3287-6cda-33eb-8e9a-044b5e15ffdd"}' \
|
||||
--allow-host "coverartarchive.org" --allow-host "archive.org"
|
||||
|
||||
test-error: build
|
||||
@echo "Testing error case (missing MBID)..."
|
||||
-extism call $(WASM_FILE) nd_get_album_images --wasi \
|
||||
--input '{"name":"Test Album","artist":"Test Artist"}' \
|
||||
--allow-host "coverartarchive.org"
|
||||
|
||||
clean:
|
||||
rm -f $(WASM_FILE)
|
||||
96
plugins/examples/coverartarchive-py/README.md
Normal file
96
plugins/examples/coverartarchive-py/README.md
Normal file
@ -0,0 +1,96 @@
|
||||
# Cover Art Archive Plugin (Python)
|
||||
|
||||
This plugin provides album cover images for Navidrome by querying the [Cover Art Archive](https://coverartarchive.org/) API using the MusicBrainz Release MBID.
|
||||
|
||||
**This is a Python example** demonstrating that Navidrome's plugin system supports multiple programming languages.
|
||||
|
||||
## Features
|
||||
|
||||
- Implements the `nd_get_album_images` method of the MetadataAgent plugin interface
|
||||
- Returns front cover images for a given release MBID
|
||||
- Returns `not found` if no MBID is provided or no images are found
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **extism-py** - Python to WASM compiler
|
||||
|
||||
Install using the official script:
|
||||
```bash
|
||||
curl -Ls https://raw.githubusercontent.com/extism/python-pdk/main/install.sh | bash
|
||||
```
|
||||
|
||||
Or download from [extism/python-pdk releases](https://github.com/extism/python-pdk/releases).
|
||||
|
||||
2. **Extism CLI** (optional, for testing)
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
brew install extism/tap/extism
|
||||
|
||||
# Or see https://extism.org/docs/install
|
||||
```
|
||||
|
||||
## How to Build
|
||||
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
Or manually:
|
||||
|
||||
```bash
|
||||
extism-py plugin/__init__.py -o coverartarchive-py.wasm
|
||||
```
|
||||
|
||||
This produces `coverartarchive-py.wasm` in this directory.
|
||||
|
||||
## Testing with Extism CLI
|
||||
|
||||
Test the manifest:
|
||||
|
||||
```bash
|
||||
extism call coverartarchive-py.wasm nd_manifest --wasi
|
||||
```
|
||||
|
||||
Test album image retrieval (using Portishead's "Dummy" MBID):
|
||||
|
||||
```bash
|
||||
extism call coverartarchive-py.wasm nd_get_album_images --wasi \
|
||||
--input '{"name":"Dummy","artist":"Portishead","mbid":"76df3287-6cda-33eb-8e9a-044b5e15ffdd"}' \
|
||||
--allow-host "coverartarchive.org" --allow-host "archive.org"
|
||||
```
|
||||
|
||||
Run all tests:
|
||||
|
||||
```bash
|
||||
make test
|
||||
```
|
||||
|
||||
## Installation in Navidrome
|
||||
|
||||
1. Build the plugin:
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
2. Copy to your Navidrome plugins folder:
|
||||
```bash
|
||||
cp coverartarchive-py.wasm /path/to/navidrome/plugins/
|
||||
```
|
||||
|
||||
3. Enable plugins in `navidrome.toml`:
|
||||
```toml
|
||||
[Plugins]
|
||||
Enabled = true
|
||||
Folder = "/path/to/navidrome/plugins"
|
||||
```
|
||||
|
||||
4. Add to your agents list:
|
||||
```toml
|
||||
Agents = "coverartarchive-py,spotify,lastfm"
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- [Cover Art Archive API](https://musicbrainz.org/doc/Cover_Art_Archive/API)
|
||||
- Endpoint used: `https://coverartarchive.org/release/{mbid}`
|
||||
128
plugins/examples/coverartarchive-py/plugin/__init__.py
Normal file
128
plugins/examples/coverartarchive-py/plugin/__init__.py
Normal file
@ -0,0 +1,128 @@
|
||||
# Cover Art Archive Plugin for Navidrome
|
||||
#
|
||||
# This plugin fetches album cover art from the Cover Art Archive (https://coverartarchive.org/)
|
||||
# using the MusicBrainz album MBID.
|
||||
#
|
||||
# Build with:
|
||||
# extism-py plugin/__init__.py -o coverartarchive-py.wasm
|
||||
#
|
||||
# Test with:
|
||||
# extism call coverartarchive-py.wasm nd_manifest --wasi
|
||||
# extism call coverartarchive-py.wasm nd_get_album_images --wasi \
|
||||
# --input '{"name":"Dummy","artist":"Portishead","mbid":"76df3287-6cda-33eb-8e9a-044b5e15ffdd"}' \
|
||||
# --allow-host "coverartarchive.org" --allow-host "archive.org"
|
||||
|
||||
import extism
|
||||
import json
|
||||
|
||||
|
||||
# Plugin manifest - identifies this plugin to Navidrome
|
||||
@extism.plugin_fn
|
||||
def nd_manifest():
|
||||
manifest = {
|
||||
"name": "Cover Art Archive (Python)",
|
||||
"author": "Navidrome",
|
||||
"version": "1.0.0",
|
||||
"description": "Album cover art from the Cover Art Archive - Python example",
|
||||
"website": "https://coverartarchive.org",
|
||||
"permissions": {
|
||||
"http": {
|
||||
"reason": "Fetch album cover art from Cover Art Archive API",
|
||||
"allowedHosts": [
|
||||
"coverartarchive.org",
|
||||
"*.archive.org"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
extism.output_str(json.dumps(manifest))
|
||||
|
||||
|
||||
@extism.plugin_fn
|
||||
def nd_get_album_images():
|
||||
"""Retrieve album cover images from Cover Art Archive."""
|
||||
input_data = extism.input_json()
|
||||
mbid = input_data.get("mbid", "")
|
||||
|
||||
if not mbid:
|
||||
raise Exception("not found: MBID required")
|
||||
|
||||
# Query Cover Art Archive API
|
||||
url = f"https://coverartarchive.org/release/{mbid}"
|
||||
response = extism.Http.request(url, meth="GET")
|
||||
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"not found: CAA returned status {response.status_code}")
|
||||
|
||||
try:
|
||||
data = json.loads(response.data_str())
|
||||
except json.JSONDecodeError:
|
||||
raise Exception("not found: invalid JSON response")
|
||||
|
||||
caa_images = data.get("images", [])
|
||||
if not caa_images:
|
||||
raise Exception("not found: no images in response")
|
||||
|
||||
# Find the front cover image
|
||||
front_image = find_front_image(caa_images)
|
||||
if not front_image:
|
||||
raise Exception("not found: no front cover image")
|
||||
|
||||
# Build the response with available image sizes
|
||||
images = build_image_list(front_image)
|
||||
if not images:
|
||||
raise Exception("not found: no usable image URLs")
|
||||
|
||||
extism.output_str(json.dumps({"images": images}))
|
||||
|
||||
|
||||
def find_front_image(images):
|
||||
"""Find the front cover image from CAA response."""
|
||||
# First, look for an image explicitly marked as front
|
||||
for img in images:
|
||||
if img.get("front", False):
|
||||
return img
|
||||
|
||||
# Second, look for an image with "Front" in types
|
||||
for img in images:
|
||||
types = img.get("types", [])
|
||||
if "Front" in types:
|
||||
return img
|
||||
|
||||
# Fallback to first image
|
||||
if images:
|
||||
return images[0]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def build_image_list(img):
|
||||
"""Build list of images with URLs and sizes from CAA image data."""
|
||||
images = []
|
||||
thumbnails = img.get("thumbnails", {})
|
||||
|
||||
# First, try numeric sizes (250, 500, 1200, etc.)
|
||||
for size_str, url in thumbnails.items():
|
||||
if not url:
|
||||
continue
|
||||
try:
|
||||
size = int(size_str)
|
||||
images.append({"url": url, "size": size})
|
||||
except ValueError:
|
||||
pass # Not a numeric size
|
||||
|
||||
# If no numeric sizes, fallback to named sizes
|
||||
if not images:
|
||||
size_map = {"large": 500, "small": 250}
|
||||
for size_name, size in size_map.items():
|
||||
url = thumbnails.get(size_name)
|
||||
if url:
|
||||
images.append({"url": url, "size": size})
|
||||
|
||||
# If still no images, use the main image URL
|
||||
if not images:
|
||||
main_url = img.get("image")
|
||||
if main_url:
|
||||
images.append({"url": main_url, "size": 0})
|
||||
|
||||
return images
|
||||
Loading…
x
Reference in New Issue
Block a user