mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
feat: add generated host function wrappers for Scheduler and SubsonicAPI services
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
e2ab381cac
commit
dfa7e0d9fc
@ -5,8 +5,8 @@ WASM_FILE = nowplaying-py.wasm
|
||||
|
||||
build: $(WASM_FILE)
|
||||
|
||||
$(WASM_FILE): plugin/__init__.py
|
||||
extism-py plugin/__init__.py -o $(WASM_FILE)
|
||||
$(WASM_FILE): plugin/__init__.py plugin/nd_host_scheduler.py plugin/nd_host_subsonicapi.py
|
||||
PYTHONPATH=plugin extism-py plugin/__init__.py -o $(WASM_FILE)
|
||||
|
||||
clean:
|
||||
rm -f $(WASM_FILE)
|
||||
|
||||
@ -4,10 +4,10 @@ A Python example plugin that demonstrates the **Scheduler** and **SubsonicAPI**
|
||||
|
||||
## Features
|
||||
|
||||
- Uses `scheduler_schedulerecurring` host function to set up a recurring task
|
||||
- Uses `scheduler_schedule_recurring` 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`
|
||||
- Uses generated Python host function wrappers from `plugins/host/python/`
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@ -29,7 +29,7 @@ make nowplaying-py.wasm
|
||||
Or directly:
|
||||
|
||||
```bash
|
||||
extism-py plugin/__init__.py -o nowplaying-py.wasm
|
||||
PYTHONPATH=plugin extism-py plugin/__init__.py -o nowplaying-py.wasm
|
||||
```
|
||||
|
||||
## Installation
|
||||
@ -86,31 +86,27 @@ Or when no one is playing:
|
||||
|
||||
2. **Callback (`nd_scheduler_callback`)**: When the scheduled task fires, calls the SubsonicAPI `getNowPlaying` endpoint and logs the results.
|
||||
|
||||
## Host Function Usage (Python)
|
||||
## Using Generated Host Function Wrappers
|
||||
|
||||
This plugin demonstrates how to call Navidrome host functions from Python:
|
||||
This plugin uses the generated Python host function wrappers from `plugins/host/python/`. These wrappers are generated by `hostgen` and provide type-safe, Pythonic interfaces to Navidrome host functions:
|
||||
|
||||
```python
|
||||
import extism
|
||||
import json
|
||||
# Import generated host function wrappers
|
||||
from nd_host_scheduler import scheduler_schedule_recurring, HostFunctionError
|
||||
from nd_host_subsonicapi import subsonicapi_call
|
||||
|
||||
# Import the host function
|
||||
@extism.import_fn("extism:host/user", "subsonicapi_call")
|
||||
def _subsonicapi_call(offset: int) -> int:
|
||||
"""Raw host function - returns memory offset."""
|
||||
...
|
||||
# Use them directly - no manual JSON marshalling needed!
|
||||
schedule_id = scheduler_schedule_recurring(
|
||||
cron_expression="*/1 * * * *",
|
||||
payload="check",
|
||||
schedule_i_d="nowplaying-check"
|
||||
)
|
||||
|
||||
# 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", "{}"))
|
||||
```
|
||||
response_json = subsonicapi_call(uri="getNowPlaying")
|
||||
```
|
||||
|
||||
The wrappers handle:
|
||||
- JSON marshalling/unmarshalling
|
||||
- Memory allocation and management
|
||||
- Error handling (raises `HostFunctionError` on failure)
|
||||
- Type hints for better IDE support
|
||||
@ -17,85 +17,28 @@
|
||||
import extism
|
||||
import json
|
||||
|
||||
# Import generated host function wrappers
|
||||
from nd_host_scheduler import scheduler_schedule_recurring, HostFunctionError
|
||||
from nd_host_subsonicapi import subsonicapi_call as _subsonicapi_call_raw
|
||||
|
||||
# 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.
|
||||
|
||||
"""Call a Subsonic API endpoint and parse the response.
|
||||
|
||||
This is a convenience wrapper around the generated subsonicapi_call
|
||||
that parses the JSON response string into a dict.
|
||||
|
||||
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)
|
||||
response_json = _subsonicapi_call_raw(uri)
|
||||
return json.loads(response_json) if response_json else {}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
142
plugins/examples/nowplaying-py/plugin/nd_host_scheduler.py
Normal file
142
plugins/examples/nowplaying-py/plugin/nd_host_scheduler.py
Normal file
@ -0,0 +1,142 @@
|
||||
# Code generated by hostgen. DO NOT EDIT.
|
||||
#
|
||||
# This file contains client wrappers for the Scheduler host service.
|
||||
# It is intended for use in Navidrome plugins built with extism-py.
|
||||
#
|
||||
# Usage:
|
||||
# from nd_host_scheduler import scheduler_schedule_one_time, scheduler_schedule_recurring, scheduler_cancel_schedule
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import extism
|
||||
import json
|
||||
|
||||
|
||||
class HostFunctionError(Exception):
|
||||
"""Raised when a host function returns an error."""
|
||||
pass
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "scheduler_scheduleonetime")
|
||||
def _scheduler_scheduleonetime(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
@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", "scheduler_cancelschedule")
|
||||
def _scheduler_cancelschedule(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
def scheduler_schedule_one_time(delay_seconds: int, payload: str, schedule_i_d: str) -> str:
|
||||
"""ScheduleOneTime schedules a one-time event to be triggered after the specified delay.
|
||||
Plugins that use this function must also implement the SchedulerCallback capability
|
||||
|
||||
Parameters:
|
||||
- delaySeconds: Number of seconds to wait before triggering the event
|
||||
- payload: Data to be passed to the scheduled event handler
|
||||
- scheduleID: Optional unique identifier for the scheduled job. If empty, one will be generated
|
||||
|
||||
Returns the schedule ID that can be used to cancel the job, or an error if scheduling fails.
|
||||
|
||||
Args:
|
||||
delay_seconds: int parameter.
|
||||
payload: str parameter.
|
||||
schedule_i_d: str parameter.
|
||||
|
||||
Returns:
|
||||
str: The result value.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"delaySeconds": delay_seconds,
|
||||
"payload": payload,
|
||||
"scheduleID": schedule_i_d,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _scheduler_scheduleonetime(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
return response.get("newScheduleID", "")
|
||||
|
||||
|
||||
def scheduler_schedule_recurring(cron_expression: str, payload: str, schedule_i_d: str) -> str:
|
||||
"""ScheduleRecurring schedules a recurring event using a cron expression.
|
||||
Plugins that use this function must also implement the SchedulerCallback capability
|
||||
|
||||
Parameters:
|
||||
- cronExpression: Standard cron format expression (e.g., "0 0 * * *" for daily at midnight)
|
||||
- payload: Data to be passed to each scheduled event handler invocation
|
||||
- scheduleID: Optional unique identifier for the scheduled job. If empty, one will be generated
|
||||
|
||||
Returns the schedule ID that can be used to cancel the job, or an error if scheduling fails.
|
||||
|
||||
Args:
|
||||
cron_expression: str parameter.
|
||||
payload: str parameter.
|
||||
schedule_i_d: str parameter.
|
||||
|
||||
Returns:
|
||||
str: The result value.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"cronExpression": cron_expression,
|
||||
"payload": payload,
|
||||
"scheduleID": schedule_i_d,
|
||||
}
|
||||
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 HostFunctionError(response["error"])
|
||||
|
||||
return response.get("newScheduleID", "")
|
||||
|
||||
|
||||
def scheduler_cancel_schedule(schedule_i_d: str) -> None:
|
||||
"""CancelSchedule cancels a scheduled job identified by its schedule ID.
|
||||
|
||||
This works for both one-time and recurring schedules. Once cancelled, the job will not trigger
|
||||
any future events.
|
||||
|
||||
Returns an error if the schedule ID is not found or if cancellation fails.
|
||||
|
||||
Args:
|
||||
schedule_i_d: str parameter.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"scheduleID": schedule_i_d,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _scheduler_cancelschedule(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
if response.get("error"):
|
||||
raise HostFunctionError(response["error"])
|
||||
|
||||
54
plugins/examples/nowplaying-py/plugin/nd_host_subsonicapi.py
Normal file
54
plugins/examples/nowplaying-py/plugin/nd_host_subsonicapi.py
Normal file
@ -0,0 +1,54 @@
|
||||
# Code generated by hostgen. DO NOT EDIT.
|
||||
#
|
||||
# This file contains client wrappers for the SubsonicAPI host service.
|
||||
# It is intended for use in Navidrome plugins built with extism-py.
|
||||
#
|
||||
# Usage:
|
||||
# from nd_host_subsonicapi import subsonicapi_call
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import extism
|
||||
import json
|
||||
|
||||
|
||||
class HostFunctionError(Exception):
|
||||
"""Raised when a host function returns an error."""
|
||||
pass
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "subsonicapi_call")
|
||||
def _subsonicapi_call(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
def subsonicapi_call(uri: str) -> str:
|
||||
"""Call executes a Subsonic API request and returns the JSON response.
|
||||
|
||||
The uri parameter should be the Subsonic API path without the server prefix,
|
||||
e.g., "getAlbumList2?type=random&size=10". The response is returned as raw JSON.
|
||||
|
||||
Args:
|
||||
uri: str parameter.
|
||||
|
||||
Returns:
|
||||
str: The result value.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
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 HostFunctionError(response["error"])
|
||||
|
||||
return response.get("responseJSON", "")
|
||||
Loading…
x
Reference in New Issue
Block a user