feat: add generated host function wrappers for Scheduler and SubsonicAPI services

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan 2025-12-26 18:12:29 -05:00
parent e2ab381cac
commit dfa7e0d9fc
5 changed files with 232 additions and 97 deletions

View File

@ -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)

View 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

View File

@ -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 {}
# =============================================================================

View 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"])

View 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", "")