208 lines
6.3 KiB
Rust
208 lines
6.3 KiB
Rust
|
|
//! Library Inspector Plugin for Navidrome
|
||
|
|
//!
|
||
|
|
//! This plugin demonstrates how to use the nd-pdk crate for accessing Navidrome
|
||
|
|
//! host services and implementing capabilities in Rust. It periodically logs details
|
||
|
|
//! about all music libraries and finds the largest file in the root of each library.
|
||
|
|
//!
|
||
|
|
//! ## Configuration
|
||
|
|
//!
|
||
|
|
//! Set the `cron` config key to customize the schedule (default: "@every 1m"):
|
||
|
|
//! ```toml
|
||
|
|
//! [PluginConfig.library-inspector]
|
||
|
|
//! cron = "@every 5m"
|
||
|
|
//! ```
|
||
|
|
|
||
|
|
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;
|
||
|
|
|
||
|
|
// Register capabilities using PDK macros
|
||
|
|
nd_pdk::register_lifecycle_init!(LibraryInspector);
|
||
|
|
nd_pdk::register_scheduler_callback!(LibraryInspector);
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Plugin Implementation
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
/// The library inspector plugin type.
|
||
|
|
#[derive(Default)]
|
||
|
|
struct LibraryInspector;
|
||
|
|
|
||
|
|
impl InitProvider for LibraryInspector {
|
||
|
|
fn on_init(&self) -> Result<(), LifecycleError> {
|
||
|
|
info!("Library Inspector plugin initializing...");
|
||
|
|
|
||
|
|
// Get cron expression from config, default to every minute
|
||
|
|
let cron = config::get("cron")
|
||
|
|
.ok()
|
||
|
|
.flatten()
|
||
|
|
.unwrap_or_else(|| "@every 1m".to_string());
|
||
|
|
|
||
|
|
info!("Scheduling library inspection with cron: {}", cron);
|
||
|
|
|
||
|
|
// Schedule the recurring task using nd-pdk host scheduler
|
||
|
|
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));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Run an initial inspection
|
||
|
|
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(())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Helper Functions
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
/// Format bytes into human-readable size
|
||
|
|
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)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Format duration in seconds to human-readable format
|
||
|
|
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)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Find the largest file in a directory (non-recursive)
|
||
|
|
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();
|
||
|
|
|
||
|
|
// Only consider files, not directories
|
||
|
|
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
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Inspect and log all library details
|
||
|
|
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 we have filesystem access, find the largest file
|
||
|
|
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 ===");
|
||
|
|
}
|