🏡 index : ironmagma/quintodrome.git

//! 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 ===");
}