mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
feat: add Now Playing Logger plugin to showcase calling host functions from Python plugins
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
3b9d426c5c
commit
37f3b838d2
@ -2,8 +2,12 @@
|
|||||||
# Auto-discover all plugin folders (folders containing go.mod)
|
# Auto-discover all plugin folders (folders containing go.mod)
|
||||||
PLUGINS := $(patsubst %/go.mod,%,$(wildcard */go.mod))
|
PLUGINS := $(patsubst %/go.mod,%,$(wildcard */go.mod))
|
||||||
|
|
||||||
|
# Auto-discover Python plugins (folders containing plugin/__init__.py)
|
||||||
|
PYTHON_PLUGINS := $(patsubst %/plugin/__init__.py,%,$(wildcard */plugin/__init__.py))
|
||||||
|
|
||||||
# Prefer tinygo if available, it produces smaller wasm binaries.
|
# Prefer tinygo if available, it produces smaller wasm binaries.
|
||||||
TINYGO := $(shell command -v tinygo 2> /dev/null)
|
TINYGO := $(shell command -v tinygo 2> /dev/null)
|
||||||
|
EXTISM_PY := $(shell command -v extism-py 2> /dev/null)
|
||||||
|
|
||||||
# Default target: show available plugins
|
# Default target: show available plugins
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
@ -12,15 +16,24 @@ help:
|
|||||||
@echo "Available Go plugins:"
|
@echo "Available Go plugins:"
|
||||||
@$(foreach p,$(PLUGINS),echo " $(p)";)
|
@$(foreach p,$(PLUGINS),echo " $(p)";)
|
||||||
@echo ""
|
@echo ""
|
||||||
|
@echo "Available Python plugins:"
|
||||||
|
@$(foreach p,$(PYTHON_PLUGINS),echo " $(p)";)
|
||||||
|
@echo ""
|
||||||
@echo "Usage:"
|
@echo "Usage:"
|
||||||
@echo " make <plugin>.wasm Build a specific plugin (e.g., make $(firstword $(PLUGINS)).wasm)"
|
@echo " make <plugin>.wasm Build a specific plugin (e.g., make $(firstword $(PLUGINS)).wasm)"
|
||||||
@echo " make all Build all Go plugins"
|
@echo " make all Build all plugins"
|
||||||
@echo " make clean Remove all built plugins"
|
@echo " make all-go Build all Go plugins"
|
||||||
|
@echo " make all-python Build all Python plugins (requires extism-py)"
|
||||||
|
@echo " make clean Remove all built plugins"
|
||||||
|
|
||||||
all: $(PLUGINS:%=%.wasm)
|
all: all-go all-python
|
||||||
|
|
||||||
|
all-go: $(PLUGINS:%=%.wasm)
|
||||||
|
|
||||||
|
all-python: $(PYTHON_PLUGINS:%=%.wasm)
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f $(PLUGINS:%=%.wasm)
|
rm -f $(PLUGINS:%=%.wasm) $(PYTHON_PLUGINS:%=%.wasm)
|
||||||
|
|
||||||
%.wasm: %/*.go %/go.mod
|
%.wasm: %/*.go %/go.mod
|
||||||
ifdef TINYGO
|
ifdef TINYGO
|
||||||
@ -28,3 +41,10 @@ ifdef TINYGO
|
|||||||
else
|
else
|
||||||
cd $* && GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o ../$@ .
|
cd $* && GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o ../$@ .
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
# Python plugin builds (generic rule for any folder with plugin/__init__.py)
|
||||||
|
$(PYTHON_PLUGINS:%=%.wasm): %.wasm: %/plugin/__init__.py
|
||||||
|
ifndef EXTISM_PY
|
||||||
|
$(error extism-py is not installed. Install from https://github.com/extism/python-pdk)
|
||||||
|
endif
|
||||||
|
cd $* && extism-py plugin/__init__.py -o ../$@
|
||||||
|
|||||||
@ -40,6 +40,7 @@ make clean
|
|||||||
| [crypto-ticker](crypto-ticker/) | Go | Real-time cryptocurrency prices from Coinbase using WebSocket |
|
| [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 |
|
| [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) |
|
| [coverartarchive-py](coverartarchive-py/) | Python | Album cover art from Cover Art Archive (Python example) |
|
||||||
|
| [nowplaying-py](nowplaying-py/) | Python | Logs currently playing tracks using Scheduler and SubsonicAPI |
|
||||||
|
|
||||||
## Testing with Extism CLI
|
## Testing with Extism CLI
|
||||||
|
|
||||||
|
|||||||
@ -1,50 +1,54 @@
|
|||||||
# Cover Art Archive Plugin (Python)
|
# 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.
|
A Python example plugin that fetches album cover images from 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
|
## Features
|
||||||
|
|
||||||
- Implements the `nd_get_album_images` method of the MetadataAgent plugin interface
|
- Implements the `nd_get_album_images` method of the MetadataAgent plugin interface
|
||||||
- Returns front cover images for a given release MBID
|
- Returns front cover images for a given release MBID
|
||||||
- Returns `not found` if no MBID is provided or no images are found
|
- Returns `not found` if no MBID is provided or no images are found
|
||||||
|
- Demonstrates Python plugin development for Navidrome
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
1. **extism-py** - Python to WASM compiler
|
- [extism-py](https://github.com/extism/python-pdk) - Python PDK compiler
|
||||||
|
```bash
|
||||||
|
curl -Ls https://raw.githubusercontent.com/extism/python-pdk/main/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
Install using the official script:
|
> **Note:** `extism-py` requires [Binaryen](https://github.com/WebAssembly/binaryen/) (`wasm-merge`, `wasm-opt`) to be installed.
|
||||||
```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).
|
## Building
|
||||||
|
|
||||||
2. **Extism CLI** (optional, for testing)
|
From the `plugins/examples` directory:
|
||||||
|
|
||||||
```bash
|
|
||||||
# macOS
|
|
||||||
brew install extism/tap/extism
|
|
||||||
|
|
||||||
# Or see https://extism.org/docs/install
|
|
||||||
```
|
|
||||||
|
|
||||||
## How to Build
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make build
|
make coverartarchive-py.wasm
|
||||||
```
|
```
|
||||||
|
|
||||||
Or manually:
|
Or directly:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
extism-py plugin/__init__.py -o coverartarchive-py.wasm
|
extism-py plugin/__init__.py -o coverartarchive-py.wasm
|
||||||
```
|
```
|
||||||
|
|
||||||
This produces `coverartarchive-py.wasm` in this directory.
|
## Installation
|
||||||
|
|
||||||
## Testing with Extism CLI
|
1. Copy `coverartarchive-py.wasm` to your Navidrome plugins folder
|
||||||
|
|
||||||
|
2. Enable plugins in `navidrome.toml`:
|
||||||
|
```toml
|
||||||
|
[Plugins]
|
||||||
|
Enabled = true
|
||||||
|
Folder = "/path/to/plugins"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Add to your agents list:
|
||||||
|
```toml
|
||||||
|
Agents = "coverartarchive-py,spotify,lastfm"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
Test the manifest:
|
Test the manifest:
|
||||||
|
|
||||||
@ -60,37 +64,14 @@ extism call coverartarchive-py.wasm nd_get_album_images --wasi \
|
|||||||
--allow-host "coverartarchive.org" --allow-host "archive.org"
|
--allow-host "coverartarchive.org" --allow-host "archive.org"
|
||||||
```
|
```
|
||||||
|
|
||||||
Run all tests:
|
## How It Works
|
||||||
|
|
||||||
```bash
|
1. **Album Image Request (`nd_get_album_images`)**: Receives album metadata including the MusicBrainz Release MBID.
|
||||||
make test
|
|
||||||
```
|
|
||||||
|
|
||||||
## Installation in Navidrome
|
2. **API Query**: Fetches cover art metadata from `https://coverartarchive.org/release/{mbid}`.
|
||||||
|
|
||||||
1. Build the plugin:
|
3. **Response**: Returns the front cover image URL if found.
|
||||||
```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
|
## API Reference
|
||||||
|
|
||||||
- [Cover Art Archive API](https://musicbrainz.org/doc/Cover_Art_Archive/API)
|
- [Cover Art Archive API](https://musicbrainz.org/doc/Cover_Art_Archive/API)
|
||||||
- Endpoint used: `https://coverartarchive.org/release/{mbid}`
|
|
||||||
|
|||||||
12
plugins/examples/nowplaying-py/Makefile
Normal file
12
plugins/examples/nowplaying-py/Makefile
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Build the Now Playing Logger Python plugin
|
||||||
|
.PHONY: build test clean
|
||||||
|
|
||||||
|
WASM_FILE = nowplaying-py.wasm
|
||||||
|
|
||||||
|
build: $(WASM_FILE)
|
||||||
|
|
||||||
|
$(WASM_FILE): plugin/__init__.py
|
||||||
|
extism-py plugin/__init__.py -o $(WASM_FILE)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(WASM_FILE)
|
||||||
116
plugins/examples/nowplaying-py/README.md
Normal file
116
plugins/examples/nowplaying-py/README.md
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
# Now Playing Logger Plugin (Python)
|
||||||
|
|
||||||
|
A Python example plugin that demonstrates the **Scheduler** and **SubsonicAPI** host services by periodically logging what is currently playing in Navidrome.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Uses `scheduler_schedulerecurring` host function to set up a recurring task
|
||||||
|
- Uses `subsonicapi_call` host function to query the `getNowPlaying` API
|
||||||
|
- Configurable cron expression and user via plugin config
|
||||||
|
- Demonstrates Python host function imports using `@extism.import_fn`
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- [extism-py](https://github.com/extism/python-pdk) - Python PDK compiler
|
||||||
|
```bash
|
||||||
|
curl -Ls https://raw.githubusercontent.com/extism/python-pdk/main/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** `extism-py` requires [Binaryen](https://github.com/WebAssembly/binaryen/) (`wasm-merge`, `wasm-opt`) to be installed.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
From the `plugins/examples` directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make nowplaying-py.wasm
|
||||||
|
```
|
||||||
|
|
||||||
|
Or directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
extism-py plugin/__init__.py -o nowplaying-py.wasm
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Copy `nowplaying-py.wasm` to your Navidrome plugins folder
|
||||||
|
|
||||||
|
2. Enable plugins in `navidrome.toml`:
|
||||||
|
```toml
|
||||||
|
[Plugins]
|
||||||
|
Enabled = true
|
||||||
|
Folder = "/path/to/plugins"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Configure the plugin (optional):
|
||||||
|
```toml
|
||||||
|
[PluginConfig.nowplaying-py]
|
||||||
|
cron = "*/1 * * * *" # Check every minute (default)
|
||||||
|
user = "admin" # Navidrome user for API calls (default)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Options
|
||||||
|
|
||||||
|
| Key | Description | Default |
|
||||||
|
|--------|-------------------------------------|------------------------------|
|
||||||
|
| `cron` | Cron expression for check frequency | `*/1 * * * *` (every minute) |
|
||||||
|
| `user` | Navidrome user for SubsonicAPI | `admin` |
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Test the manifest:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
extism call nowplaying-py.wasm nd_manifest --wasi
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
When running, the plugin logs messages like:
|
||||||
|
|
||||||
|
```
|
||||||
|
🎵 john is playing: Pink Floyd - Comfortably Numb (The Wall)
|
||||||
|
🎵 jane is playing: Radiohead - Paranoid Android (OK Computer)
|
||||||
|
```
|
||||||
|
|
||||||
|
Or when no one is playing:
|
||||||
|
|
||||||
|
```
|
||||||
|
🎵 No users currently playing music
|
||||||
|
```
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. **Initialization (`nd_on_init`)**: Reads the cron expression from config and schedules a recurring task using the Scheduler host service.
|
||||||
|
|
||||||
|
2. **Callback (`nd_scheduler_callback`)**: When the scheduled task fires, calls the SubsonicAPI `getNowPlaying` endpoint and logs the results.
|
||||||
|
|
||||||
|
## Host Function Usage (Python)
|
||||||
|
|
||||||
|
This plugin demonstrates how to call Navidrome host functions from Python:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import extism
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Import the host function
|
||||||
|
@extism.import_fn("extism:host/user", "subsonicapi_call")
|
||||||
|
def _subsonicapi_call(offset: int) -> int:
|
||||||
|
"""Raw host function - returns memory offset."""
|
||||||
|
...
|
||||||
|
|
||||||
|
# Wrapper for JSON marshalling
|
||||||
|
def subsonicapi_call(uri: str) -> dict:
|
||||||
|
request = {"uri": uri}
|
||||||
|
request_bytes = json.dumps(request).encode('utf-8')
|
||||||
|
request_mem = extism.memory.alloc(request_bytes)
|
||||||
|
response_offset = _subsonicapi_call(request_mem.offset)
|
||||||
|
response_mem = extism.memory.find(response_offset)
|
||||||
|
response = json.loads(extism.memory.string(response_mem))
|
||||||
|
|
||||||
|
if response.get("error"):
|
||||||
|
raise Exception(response["error"])
|
||||||
|
|
||||||
|
return json.loads(response.get("responseJSON", "{}"))
|
||||||
|
```
|
||||||
197
plugins/examples/nowplaying-py/plugin/__init__.py
Normal file
197
plugins/examples/nowplaying-py/plugin/__init__.py
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
# Now Playing Logger Plugin for Navidrome
|
||||||
|
#
|
||||||
|
# This plugin demonstrates the Scheduler and SubsonicAPI host services by
|
||||||
|
# periodically logging what is currently playing in Navidrome.
|
||||||
|
#
|
||||||
|
# Build with:
|
||||||
|
# extism-py plugin/__init__.py -o nowplaying-py.wasm
|
||||||
|
#
|
||||||
|
# Test manifest with:
|
||||||
|
# extism call nowplaying-py.wasm nd_manifest --wasi
|
||||||
|
#
|
||||||
|
# Configuration:
|
||||||
|
# [PluginConfig.nowplaying-py]
|
||||||
|
# cron = "*/1 * * * *" # Every minute (default)
|
||||||
|
# user = "admin" # User to query getNowPlaying (default)
|
||||||
|
|
||||||
|
import extism
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Schedule ID for our recurring task
|
||||||
|
SCHEDULE_ID = "nowplaying-check"
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Host Function Imports
|
||||||
|
# =============================================================================
|
||||||
|
# These are custom host functions provided by Navidrome.
|
||||||
|
# We import them using the extism:host/user namespace.
|
||||||
|
|
||||||
|
|
||||||
|
@extism.import_fn("extism:host/user", "scheduler_schedulerecurring")
|
||||||
|
def _scheduler_schedulerecurring(offset: int) -> int:
|
||||||
|
"""Raw host function - do not call directly."""
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@extism.import_fn("extism:host/user", "subsonicapi_call")
|
||||||
|
def _subsonicapi_call(offset: int) -> int:
|
||||||
|
"""Raw host function - do not call directly."""
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Host Function Wrappers
|
||||||
|
# =============================================================================
|
||||||
|
# These wrappers handle JSON marshalling/unmarshalling and memory management.
|
||||||
|
|
||||||
|
|
||||||
|
def scheduler_schedule_recurring(cron_expression: str, payload: str, schedule_id: str) -> str:
|
||||||
|
"""Schedule a recurring task using a cron expression.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cron_expression: Cron format (e.g., "*/1 * * * *" for every minute)
|
||||||
|
payload: Data to pass to the callback
|
||||||
|
schedule_id: Unique identifier for the schedule
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The schedule ID (same as input or auto-generated)
|
||||||
|
"""
|
||||||
|
request = {
|
||||||
|
"cronExpression": cron_expression,
|
||||||
|
"payload": payload,
|
||||||
|
"scheduleID": schedule_id
|
||||||
|
}
|
||||||
|
request_bytes = json.dumps(request).encode('utf-8')
|
||||||
|
request_mem = extism.memory.alloc(request_bytes)
|
||||||
|
response_offset = _scheduler_schedulerecurring(request_mem.offset)
|
||||||
|
response_mem = extism.memory.find(response_offset)
|
||||||
|
response = json.loads(extism.memory.string(response_mem))
|
||||||
|
|
||||||
|
if response.get("error"):
|
||||||
|
raise Exception(response["error"])
|
||||||
|
|
||||||
|
return response.get("newScheduleID", schedule_id)
|
||||||
|
|
||||||
|
|
||||||
|
def subsonicapi_call(uri: str) -> dict:
|
||||||
|
"""Call a Subsonic API endpoint.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
uri: API path (e.g., "getNowPlaying")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Parsed JSON response from the API
|
||||||
|
"""
|
||||||
|
request = {"uri": uri}
|
||||||
|
request_bytes = json.dumps(request).encode('utf-8')
|
||||||
|
request_mem = extism.memory.alloc(request_bytes)
|
||||||
|
response_offset = _subsonicapi_call(request_mem.offset)
|
||||||
|
response_mem = extism.memory.find(response_offset)
|
||||||
|
response = json.loads(extism.memory.string(response_mem))
|
||||||
|
|
||||||
|
if response.get("error"):
|
||||||
|
raise Exception(response["error"])
|
||||||
|
|
||||||
|
# Parse the nested JSON response
|
||||||
|
response_json = response.get("responseJSON", "{}")
|
||||||
|
return json.loads(response_json)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Plugin Exports
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@extism.plugin_fn
|
||||||
|
def nd_manifest():
|
||||||
|
"""Return the plugin manifest with metadata and permissions."""
|
||||||
|
manifest = {
|
||||||
|
"name": "Now Playing Logger (Python)",
|
||||||
|
"author": "Navidrome",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Periodically logs currently playing tracks - Python example demonstrating Scheduler and SubsonicAPI host services",
|
||||||
|
"website": "https://github.com/navidrome/navidrome/tree/master/plugins/examples/nowplaying-py",
|
||||||
|
"permissions": {
|
||||||
|
"scheduler": {
|
||||||
|
"reason": "Schedule periodic checks for now playing status"
|
||||||
|
},
|
||||||
|
"subsonicapi": {
|
||||||
|
"reason": "Query the getNowPlaying API endpoint",
|
||||||
|
"allowAdmins": True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
extism.output_str(json.dumps(manifest))
|
||||||
|
|
||||||
|
|
||||||
|
@extism.plugin_fn
|
||||||
|
def nd_on_init():
|
||||||
|
"""Initialize the plugin by scheduling the recurring task."""
|
||||||
|
# Read cron expression from config, default to every minute
|
||||||
|
cron = extism.Config.get_str("cron")
|
||||||
|
if not cron:
|
||||||
|
cron = "*/1 * * * *"
|
||||||
|
|
||||||
|
extism.log(extism.LogLevel.Info, f"Now Playing Logger initializing with cron: {cron}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
schedule_id = scheduler_schedule_recurring(cron, "check", SCHEDULE_ID)
|
||||||
|
extism.log(extism.LogLevel.Info, f"Scheduled recurring task with ID: {schedule_id}")
|
||||||
|
except Exception as e:
|
||||||
|
extism.log(extism.LogLevel.Error, f"Failed to schedule task: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Return empty success response
|
||||||
|
extism.output_str(json.dumps({}))
|
||||||
|
|
||||||
|
|
||||||
|
@extism.plugin_fn
|
||||||
|
def nd_scheduler_callback():
|
||||||
|
"""Handle scheduler callback - check and log now playing tracks."""
|
||||||
|
input_data = extism.input_json()
|
||||||
|
schedule_id = input_data.get("schedule_id", "")
|
||||||
|
|
||||||
|
# Only handle our schedule
|
||||||
|
if schedule_id != SCHEDULE_ID:
|
||||||
|
extism.output_str(json.dumps({}))
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Read user from config, default to admin
|
||||||
|
user = extism.Config.get_str("user")
|
||||||
|
if not user:
|
||||||
|
user = "admin"
|
||||||
|
|
||||||
|
# Call the getNowPlaying API
|
||||||
|
response = subsonicapi_call(f"getNowPlaying?u={user}")
|
||||||
|
|
||||||
|
# Extract the subsonic-response
|
||||||
|
subsonic_response = response.get("subsonic-response", {})
|
||||||
|
now_playing = subsonic_response.get("nowPlaying", {})
|
||||||
|
entries = now_playing.get("entry", [])
|
||||||
|
|
||||||
|
if not entries:
|
||||||
|
extism.log(extism.LogLevel.Info, "🎵 No users currently playing music")
|
||||||
|
else:
|
||||||
|
# Handle both single entry and list of entries
|
||||||
|
if isinstance(entries, dict):
|
||||||
|
entries = [entries]
|
||||||
|
|
||||||
|
for entry in entries:
|
||||||
|
artist = entry.get("artist", "Unknown Artist")
|
||||||
|
title = entry.get("title", "Unknown Title")
|
||||||
|
album = entry.get("album", "Unknown Album")
|
||||||
|
username = entry.get("username", "Unknown User")
|
||||||
|
|
||||||
|
extism.log(
|
||||||
|
extism.LogLevel.Info,
|
||||||
|
f"🎵 {username} is playing: {artist} - {title} ({album})"
|
||||||
|
)
|
||||||
|
|
||||||
|
extism.output_str(json.dumps({}))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = str(e)
|
||||||
|
extism.log(extism.LogLevel.Error, f"Failed to get now playing: {error_msg}")
|
||||||
|
extism.output_str(json.dumps({"error": error_msg}))
|
||||||
@ -150,7 +150,3 @@ wikimedia/
|
|||||||
| `nd_get_artist_url` | Returns Wikipedia URL for an artist |
|
| `nd_get_artist_url` | Returns Wikipedia URL for an artist |
|
||||||
| `nd_get_artist_biography` | Returns artist biography from Wikipedia |
|
| `nd_get_artist_biography` | Returns artist biography from Wikipedia |
|
||||||
| `nd_get_artist_images` | Returns artist image URLs from Wikidata |
|
| `nd_get_artist_images` | Returns artist image URLs from Wikidata |
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
Same as Navidrome - GPL-3.0
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user