mirror of
https://github.com/navidrome/navidrome.git
synced 2025-08-10 00:52:20 +00:00
feat(plugins): allow Plugins to call the Subsonic API (#4260)
* chore: .gitignore any navidrome binary Signed-off-by: Deluan <deluan@navidrome.org> * feat: implement internal authentication handling in middleware Signed-off-by: Deluan <deluan@navidrome.org> * feat(manager): add SubsonicRouter to Manager for API routing Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): add SubsonicAPI Host service for plugins and an example plugin Signed-off-by: Deluan <deluan@navidrome.org> * fix lint Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): refactor path handling in SubsonicAPI to extract endpoint correctly Signed-off-by: Deluan <deluan@navidrome.org> * docs(plugins): add SubsonicAPI service documentation to README Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): implement permission checks for SubsonicAPI service Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): enhance SubsonicAPI service initialization with atomic router handling Signed-off-by: Deluan <deluan@navidrome.org> * refactor(plugins): better encapsulated dependency injection Signed-off-by: Deluan <deluan@navidrome.org> * refactor(plugins): rename parameter in WithInternalAuth for clarity Signed-off-by: Deluan <deluan@navidrome.org> * docs(plugins): update SubsonicAPI permissions section in README for clarity and detail Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): enhance SubsonicAPI permissions output with allowed usernames and admin flag Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): add schema reference to example plugins Signed-off-by: Deluan <deluan@navidrome.org> * remove import alias Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
all: wikimedia coverartarchive crypto-ticker discord-rich-presence
|
||||
all: wikimedia coverartarchive crypto-ticker discord-rich-presence subsonicapi-demo
|
||||
|
||||
wikimedia: wikimedia/plugin.wasm
|
||||
coverartarchive: coverartarchive/plugin.wasm
|
||||
crypto-ticker: crypto-ticker/plugin.wasm
|
||||
discord-rich-presence: discord-rich-presence/plugin.wasm
|
||||
subsonicapi-demo: subsonicapi-demo/plugin.wasm
|
||||
|
||||
wikimedia/plugin.wasm: wikimedia/plugin.go
|
||||
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o $@ ./wikimedia
|
||||
@@ -18,5 +19,9 @@ DISCORD_RP_FILES=$(shell find discord-rich-presence -type f -name "*.go")
|
||||
discord-rich-presence/plugin.wasm: $(DISCORD_RP_FILES)
|
||||
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o $@ ./discord-rich-presence/...
|
||||
|
||||
subsonicapi-demo/plugin.wasm: subsonicapi-demo/plugin.go
|
||||
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o $@ ./subsonicapi-demo
|
||||
|
||||
clean:
|
||||
rm -f wikimedia/plugin.wasm coverartarchive/plugin.wasm crypto-ticker/plugin.wasm discord-rich-presence/plugin.wasm
|
||||
rm -f wikimedia/plugin.wasm coverartarchive/plugin.wasm crypto-ticker/plugin.wasm \
|
||||
discord-rich-presence/plugin.wasm subsonicapi-demo/plugin.wasm
|
||||
@@ -6,8 +6,9 @@ This directory contains example plugins for Navidrome, intended for demonstratio
|
||||
|
||||
- `wikimedia/`: Example plugin that retrieves artist information from Wikidata.
|
||||
- `coverartarchive/`: Example plugin that retrieves album cover images from the Cover Art Archive.
|
||||
- `crypto-ticker/`: Example plugin using websockets to log real-time crypto currency prices.
|
||||
- `crypto-ticker/`: Example plugin using websockets to log real-time cryptocurrency prices.
|
||||
- `discord-rich-presence/`: Example plugin that integrates with Discord Rich Presence to display currently playing tracks on Discord profiles.
|
||||
- `subsonicapi-demo/`: Example plugin that demonstrates how to interact with the Navidrome's Subsonic API from a plugin.
|
||||
|
||||
## Building
|
||||
|
||||
@@ -24,6 +25,7 @@ make wikimedia
|
||||
make coverartarchive
|
||||
make crypto-ticker
|
||||
make discord-rich-presence
|
||||
make subsonicapi-demo
|
||||
```
|
||||
|
||||
This will produce the corresponding `plugin.wasm` files in each plugin's directory.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/navidrome/navidrome/refs/heads/master/plugins/schema/manifest.schema.json",
|
||||
"name": "coverartarchive",
|
||||
"author": "Navidrome",
|
||||
"version": "1.0.0",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/navidrome/navidrome/refs/heads/master/plugins/schema/manifest.schema.json",
|
||||
"name": "discord-rich-presence",
|
||||
"author": "Navidrome Team",
|
||||
"version": "1.0.0",
|
||||
|
||||
88
plugins/examples/subsonicapi-demo/README.md
Normal file
88
plugins/examples/subsonicapi-demo/README.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# SubsonicAPI Demo Plugin
|
||||
|
||||
This example plugin demonstrates how to use the SubsonicAPI host service to access Navidrome's Subsonic API from within a plugin.
|
||||
|
||||
## What it does
|
||||
|
||||
The plugin performs the following operations during initialization:
|
||||
|
||||
1. **Ping the server**: Calls `/rest/ping` to check if the Subsonic API is responding
|
||||
2. **Get license info**: Calls `/rest/getLicense` to retrieve server license information
|
||||
|
||||
## Key Features
|
||||
|
||||
- Shows how to request `subsonicapi` permission in the manifest
|
||||
- Demonstrates making Subsonic API calls using the `subsonicapi.Call()` method
|
||||
- Handles both successful responses and errors
|
||||
- Uses proper lifecycle management with `OnInit`
|
||||
|
||||
## Usage
|
||||
|
||||
### Manifest Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"permissions": {
|
||||
"subsonicapi": {
|
||||
"reason": "Demonstrate accessing Navidrome's Subsonic API from within plugins",
|
||||
"allowAdmins": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Plugin Implementation
|
||||
|
||||
```go
|
||||
import "github.com/navidrome/navidrome/plugins/host/subsonicapi"
|
||||
|
||||
var subsonicService = subsonicapi.NewSubsonicAPIService()
|
||||
|
||||
// OnInit is called when the plugin is loaded
|
||||
func (SubsonicAPIDemoPlugin) OnInit(ctx context.Context, req *api.InitRequest) (*api.InitResponse, error) {
|
||||
// Make API calls
|
||||
response, err := subsonicService.Call(ctx, &subsonicapi.CallRequest{
|
||||
Url: "/rest/ping?u=admin",
|
||||
})
|
||||
// Handle response...
|
||||
}
|
||||
```
|
||||
|
||||
When running Navidrome with this plugin installed, it will automatically call the Subsonic API endpoints during the
|
||||
server startup, and you can see the results in the logs:
|
||||
|
||||
```agsl
|
||||
INFO[0000] 2022/01/01 00:00:00 SubsonicAPI Demo Plugin initializing...
|
||||
DEBU[0000] API: New request /ping client=subsonicapi-demo username=admin version=1.16.1
|
||||
DEBU[0000] API: Successful response endpoint=/ping status=OK
|
||||
DEBU[0000] API: New request /getLicense client=subsonicapi-demo username=admin version=1.16.1
|
||||
INFO[0000] 2022/01/01 00:00:00 SubsonicAPI ping response: {"subsonic-response":{"status":"ok","version":"1.16.1","type":"navidrome","serverVersion":"dev","openSubsonic":true}}
|
||||
DEBU[0000] API: Successful response endpoint=/getLicense status=OK
|
||||
DEBU[0000] Plugin initialized successfully elapsed=41.9ms plugin=subsonicapi-demo
|
||||
INFO[0000] 2022/01/01 00:00:00 SubsonicAPI license info: {"subsonic-response":{"status":"ok","version":"1.16.1","type":"navidrome","serverVersion":"dev","openSubsonic":true,"license":{"valid":true}}}
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **Authentication**: The plugin must provide valid authentication parameters in the URL:
|
||||
- **Required**: `u` (username) - The service validates this parameter is present
|
||||
- Example: `"/rest/ping?u=admin"`
|
||||
2. **URL Format**: Only the path and query parameters from the URL are used - host, protocol, and method are ignored
|
||||
3. **Automatic Parameters**: The service automatically adds:
|
||||
- `c`: Plugin name (client identifier)
|
||||
- `v`: Subsonic API version (1.16.1)
|
||||
- `f`: Response format (json)
|
||||
4. **Internal Authentication**: The service sets up internal authentication using the `u` parameter
|
||||
5. **Lifecycle**: This plugin uses `LifecycleManagement` with only the `OnInit` method
|
||||
|
||||
## Building
|
||||
|
||||
This plugin uses the `wasip1` build constraint and must be compiled for WebAssembly:
|
||||
|
||||
```bash
|
||||
# Using the project's make target (recommended)
|
||||
make plugin-examples
|
||||
|
||||
# Manual compilation (when using the proper toolchain)
|
||||
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o plugin.wasm plugin.go
|
||||
```
|
||||
16
plugins/examples/subsonicapi-demo/manifest.json
Normal file
16
plugins/examples/subsonicapi-demo/manifest.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/navidrome/navidrome/refs/heads/master/plugins/schema/manifest.schema.json",
|
||||
"name": "subsonicapi-demo",
|
||||
"author": "Navidrome Team",
|
||||
"version": "1.0.0",
|
||||
"description": "Example plugin demonstrating SubsonicAPI host service usage",
|
||||
"website": "https://github.com/navidrome/navidrome",
|
||||
"capabilities": ["LifecycleManagement"],
|
||||
"permissions": {
|
||||
"subsonicapi": {
|
||||
"reason": "Demonstrate accessing Navidrome's Subsonic API from within plugins",
|
||||
"allowAdmins": true,
|
||||
"allowedUsernames": ["admin"]
|
||||
}
|
||||
}
|
||||
}
|
||||
64
plugins/examples/subsonicapi-demo/plugin.go
Normal file
64
plugins/examples/subsonicapi-demo/plugin.go
Normal file
@@ -0,0 +1,64 @@
|
||||
//go:build wasip1
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/navidrome/navidrome/plugins/api"
|
||||
"github.com/navidrome/navidrome/plugins/host/subsonicapi"
|
||||
)
|
||||
|
||||
// SubsonicAPIService instance for making API calls
|
||||
var subsonicService = subsonicapi.NewSubsonicAPIService()
|
||||
|
||||
// SubsonicAPIDemoPlugin implements LifecycleManagement interface
|
||||
type SubsonicAPIDemoPlugin struct{}
|
||||
|
||||
// OnInit is called when the plugin is loaded
|
||||
func (SubsonicAPIDemoPlugin) OnInit(ctx context.Context, req *api.InitRequest) (*api.InitResponse, error) {
|
||||
log.Printf("SubsonicAPI Demo Plugin initializing...")
|
||||
|
||||
// Example: Call the ping endpoint to check if the server is alive
|
||||
response, err := subsonicService.Call(ctx, &subsonicapi.CallRequest{
|
||||
Url: "/rest/ping?u=admin",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Printf("SubsonicAPI call failed: %v", err)
|
||||
return &api.InitResponse{Error: err.Error()}, nil
|
||||
}
|
||||
|
||||
if response.Error != "" {
|
||||
log.Printf("SubsonicAPI returned error: %s", response.Error)
|
||||
return &api.InitResponse{Error: response.Error}, nil
|
||||
}
|
||||
|
||||
log.Printf("SubsonicAPI ping response: %s", response.Json)
|
||||
|
||||
// Example: Get server info
|
||||
infoResponse, err := subsonicService.Call(ctx, &subsonicapi.CallRequest{
|
||||
Url: "/rest/getLicense?u=admin",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Printf("SubsonicAPI getLicense call failed: %v", err)
|
||||
return &api.InitResponse{Error: err.Error()}, nil
|
||||
}
|
||||
|
||||
if infoResponse.Error != "" {
|
||||
log.Printf("SubsonicAPI getLicense returned error: %s", infoResponse.Error)
|
||||
return &api.InitResponse{Error: infoResponse.Error}, nil
|
||||
}
|
||||
|
||||
log.Printf("SubsonicAPI license info: %s", infoResponse.Json)
|
||||
|
||||
return &api.InitResponse{}, nil
|
||||
}
|
||||
|
||||
func main() {}
|
||||
|
||||
func init() {
|
||||
api.RegisterLifecycleManagement(&SubsonicAPIDemoPlugin{})
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/navidrome/navidrome/refs/heads/master/plugins/schema/manifest.schema.json",
|
||||
"name": "wikimedia",
|
||||
"author": "Navidrome",
|
||||
"version": "1.0.0",
|
||||
|
||||
Reference in New Issue
Block a user