use extism_pdk::*;
use nd_pdk::host::{library, scheduler};
use nd_pdk::lifecycle::{Error as LifecycleError, InitProvider};
use nd_pdk::scheduler::{CallbackProvider, Error as SchedulerError, SchedulerCallbackRequest};
use std::fs;
nd_pdk::register_lifecycle_init!(LibraryInspector);
nd_pdk::register_scheduler_callback!(LibraryInspector);
#[derive(Default)]
struct LibraryInspector;
impl InitProvider for LibraryInspector {
fn on_init(&self) -> Result<(), LifecycleError> {
info!("Library Inspector plugin initializing...");
let cron = config::get("cron")
.ok()
.flatten()
.unwrap_or_else(|| "@every 1m".to_string());
info!("Scheduling library inspection with cron: {}", cron);
match scheduler::schedule_recurring(&cron, "inspect", "library-inspect") {
Ok(schedule_id) => {
info!("Scheduled inspection task with ID: {}", schedule_id);
}
Err(e) => {
let error_msg = format!("Failed to schedule inspection: {}", e);
error!("{}", error_msg);
return Err(LifecycleError::new(error_msg));
}
}
inspect_libraries();
info!("Library Inspector plugin initialized successfully");
Ok(())
}
}
impl CallbackProvider for LibraryInspector {
fn on_callback(&self, req: SchedulerCallbackRequest) -> Result<(), SchedulerError> {
info!(
"Scheduler callback fired: schedule_id={}, payload={}, recurring={}",
req.schedule_id, req.payload, req.is_recurring
);
if req.payload == "inspect" {
inspect_libraries();
}
Ok(())
}
}
fn format_size(bytes: i64) -> String {
const KB: i64 = 1024;
const MB: i64 = KB * 1024;
const GB: i64 = MB * 1024;
const TB: i64 = GB * 1024;
if bytes >= TB {
format!("{:.2} TB", bytes as f64 / TB as f64)
} else if bytes >= GB {
format!("{:.2} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.2} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.2} KB", bytes as f64 / KB as f64)
} else {
format!("{} bytes", bytes)
}
}
fn format_duration(seconds: f64) -> String {
let total_seconds = seconds as i64;
let hours = total_seconds / 3600;
let minutes = (total_seconds % 3600) / 60;
if hours > 0 {
format!("{}h {}m", hours, minutes)
} else {
format!("{}m", minutes)
}
}
fn find_largest_file(mount_point: &str) -> Option<(String, u64)> {
let entries = match fs::read_dir(mount_point) {
Ok(entries) => entries,
Err(e) => {
warn!("Failed to read directory {}: {}", mount_point, e);
return None;
}
};
let mut largest: Option<(String, u64)> = None;
for entry in entries.flatten() {
let path = entry.path();
if !path.is_file() {
continue;
}
let metadata = match entry.metadata() {
Ok(m) => m,
Err(_) => continue,
};
let size = metadata.len();
let name = entry.file_name().to_string_lossy().to_string();
match &largest {
None => largest = Some((name, size)),
Some((_, current_size)) if size > *current_size => {
largest = Some((name, size));
}
_ => {}
}
}
largest
}
fn inspect_libraries() {
info!("=== Library Inspection Started ===");
let libraries = match library::get_all_libraries() {
Ok(libs) => libs,
Err(e) => {
error!("Failed to get libraries: {}", e);
return;
}
};
if libraries.is_empty() {
info!("No libraries configured");
return;
}
info!("Found {} libraries", libraries.len());
for lib in &libraries {
info!("----------------------------------------");
info!("Library: {} (ID: {})", lib.name, lib.id);
info!(" Songs: {} tracks", lib.total_songs);
info!(" Albums: {}", lib.total_albums);
info!(" Artists: {}", lib.total_artists);
info!(" Size: {}", format_size(lib.total_size));
info!(" Duration: {}", format_duration(lib.total_duration));
if !lib.mount_point.is_empty() {
info!(" Mount: {}", lib.mount_point);
match find_largest_file(&lib.mount_point) {
Some((name, size)) => {
info!(
" Largest file in root: {} ({})",
name,
format_size(size as i64)
);
}
None => {
info!(" Largest file in root: (no files found)");
}
}
} else {
info!(" (Filesystem access not enabled)");
}
}
info!("=== Library Inspection Complete ===");
}