use extism_pdk::{config, error, info, warn};
use nd_pdk::host::http::{self, HTTPRequest};
use nd_pdk::scrobbler::{
Error, IsAuthorizedRequest, NowPlayingRequest, PlaybackReportRequest, 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 playback_report(&self, req: PlaybackReportRequest) -> Result<(), Error> {
info!(
"Playback report (ignored): {} - {} for user {} (state: {})",
req.track.artist, req.track.title, req.username, req.state
);
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 {
method: "GET".into(),
url: full_url,
..Default::default()
};
match http::send(http_req) {
Ok(res) => {
let status = res.map_or(0, |r| r.status_code);
if (200..300).contains(&status) {
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
}