binocular-cli 0.2.3

Not exactly a telescope, but it's useful sometimes. TUI to search/navigate through files and workspaces.
Documentation
use std::path::Path;
use std::process::Command;

use super::types::{has_any_metadata, MediaKind, MediaMetadata};

pub(crate) fn read_spotlight_metadata(path: &Path, kind: &MediaKind) -> Option<MediaMetadata> {
    #[cfg(not(target_os = "macos"))]
    {
        let _ = (path, kind);
        None
    }

    #[cfg(target_os = "macos")]
    {
        let mut m = MediaMetadata::default();
        m.title = mdls(path, "kMDItemTitle");
        m.artist = mdls(path, "kMDItemAuthors").or_else(|| mdls(path, "kMDItemAuthor"));
        m.album = mdls(path, "kMDItemAlbum");
        m.genre = mdls(path, "kMDItemMusicalGenre");
        m.composer = mdls(path, "kMDItemComposer");
        m.duration = mdls(path, "kMDItemDurationSeconds").map(format_duration_seconds_string);
        m.bitrate = mdls(path, "kMDItemAudioBitRate")
            .or_else(|| mdls(path, "kMDItemTotalBitRate"))
            .map(|v| format_kbps(&v));
        m.sample_rate = mdls(path, "kMDItemAudioSampleRate").map(|v| format!("{v} Hz"));
        m.channels = mdls(path, "kMDItemAudioChannelCount").map(|v| format!("{v} ch"));

        if matches!(kind, MediaKind::Video) {
            let width = mdls(path, "kMDItemPixelWidth");
            let height = mdls(path, "kMDItemPixelHeight");
            if let (Some(w), Some(h)) = (width, height) {
                m.resolution = Some(format!("{w}x{h}"));
            }
            m.codec = mdls(path, "kMDItemCodecs");
        }

        if has_any_metadata(&m) {
            Some(m)
        } else {
            None
        }
    }
}

#[cfg(target_os = "macos")]
fn mdls(path: &Path, attr: &str) -> Option<String> {
    let output = Command::new("mdls")
        .args(["-raw", "-name", attr, path.to_string_lossy().as_ref()])
        .output()
        .ok()?;
    if !output.status.success() {
        return None;
    }

    let raw = String::from_utf8_lossy(&output.stdout).trim().to_string();
    if raw.is_empty() || raw == "(null)" {
        return None;
    }

    Some(clean_mdls_value(&raw))
}

#[cfg(target_os = "macos")]
fn clean_mdls_value(input: &str) -> String {
    let trimmed = input.trim();
    if trimmed.starts_with('(') && trimmed.ends_with(')') {
        let inner = &trimmed[1..trimmed.len() - 1];
        let parts: Vec<String> = inner
            .lines()
            .map(|line| {
                line.trim()
                    .trim_end_matches(',')
                    .trim_matches('"')
                    .to_string()
            })
            .filter(|s| !s.is_empty())
            .collect();
        return parts.join(", ");
    }

    trimmed.trim_matches('"').to_string()
}

fn format_kbps(v: &str) -> String {
    if let Ok(n) = v.parse::<u64>() {
        format!("{} kbps", n / 1000)
    } else {
        v.to_string()
    }
}

fn format_duration_seconds_string(v: String) -> String {
    let secs = v.parse::<f64>().unwrap_or(0.0);
    let total = secs.round() as u64;
    let h = total / 3600;
    let m = (total % 3600) / 60;
    let s = total % 60;
    if h > 0 {
        format!("{h}:{m:02}:{s:02}")
    } else {
        format!("{m}:{s:02}")
    }
}