use extism_pdk::{config, error, http, info, warn, HttpRequest};
use nd_pdk::scrobbler::{
Error, IsAuthorizedRequest, NowPlayingRequest, ScrobbleRequest,
Scrobbler,
};
nd_pdk::register_scrobbler!(WebhookPlugin);
#[derive(Default)]
struct WebhookPlugin;
impl Scrobbler for WebhookPlugin {
fn is_authorized(&self, req: IsAuthorizedRequest) -> Result<bool, Error> {
info!("Authorization check for user: {}", req.username);
Ok(true)
}
fn now_playing(&self, req: NowPlayingRequest) -> Result<(), Error> {
info!(
"Now playing (ignored): {} - {} for user {}",
req.track.artist, req.track.title, req.username
);
Ok(())
}
fn scrobble(&self, req: ScrobbleRequest) -> Result<(), Error> {
let urls_config = match config::get("urls") {
Ok(Some(urls)) if !urls.is_empty() => urls,
_ => {
warn!("No webhook URLs configured. Set 'urls' in plugin config.");
return Ok(());
}
};
info!(
"Scrobble: {} - {} by user {}",
req.track.artist, req.track.title, req.username
);
let query = format!(
"?title={}&artist={}&album={}&user={}×tamp={}",
urlencode(&req.track.title),
urlencode(&req.track.artist),
urlencode(&req.track.album),
urlencode(&req.username),
req.timestamp
);
for url in urls_config.split(',') {
let url = url.trim();
if url.is_empty() {
continue;
}
let full_url = format!("{}{}", url, query);
info!("Sending webhook to: {}", full_url);
let http_req = HttpRequest::new(&full_url);
match http::request::<()>(&http_req, None) {
Ok(res) => {
let status = res.status_code();
if status >= 200 && status < 300 {
info!("Webhook succeeded: {} (status {})", url, status);
} else {
warn!("Webhook returned non-2xx status: {} (status {})", url, status);
}
}
Err(e) => {
error!("Webhook failed for {}: {:?}", url, e);
}
}
}
Ok(())
}
}
fn urlencode(s: &str) -> String {
let mut result = String::with_capacity(s.len() * 3);
for c in s.chars() {
match c {
'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' => result.push(c),
' ' => result.push_str("%20"),
_ => {
for b in c.to_string().as_bytes() {
result.push_str(&format!("%{:02X}", b));
}
}
}
}
result
}