mirror of
https://github.com/navidrome/navidrome.git
synced 2026-01-03 06:15:22 +00:00
169 lines
5.8 KiB
Python
169 lines
5.8 KiB
Python
# 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
|
|
#
|
|
# 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.
|
|
# They were copied from plugins/host/python due to extism-py limitations.
|
|
|
|
|
|
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_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
|
|
# No output - lifecycle callbacks don't return responses
|
|
|
|
|
|
@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("scheduleId", "")
|
|
|
|
# Only handle our schedule
|
|
if schedule_id != SCHEDULE_ID:
|
|
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})"
|
|
)
|
|
# No output - scheduler callbacks don't return responses
|
|
|
|
except Exception as e:
|
|
extism.log(extism.LogLevel.Error, f"Failed to get now playing: {e}")
|
|
# Errors are logged but scheduler callbacks don't return responses
|