selene-core 0.3.1

selene-core is the backend for Selene, a local-first music player
Documentation
use std::{
    collections::VecDeque,
    fs::{self},
    path::{Path, PathBuf},
};

use crate::{codec::Codec, container::ContainerFormat};
use blake3::{Hash, Hasher};
use lunar_lib::ask;

pub fn hash_file(path: impl AsRef<Path>) -> std::io::Result<Hash> {
    let mut hasher = Hasher::new();
    hasher.update_mmap(path)?;
    Ok(hasher.finalize())
}

pub fn recurse_list_from_root<P: AsRef<Path>>(
    root: P,
    yield_dirs: bool,
) -> impl Iterator<Item = PathBuf> {
    let mut stack = VecDeque::new();
    stack.push_back(root.as_ref().to_owned());

    std::iter::from_fn(move || {
        while let Some(path) = stack.pop_back() {
            if path.is_dir() {
                if let Ok(entries) = path.read_dir() {
                    let filtered = entries.filter_map(Result::ok).map(|a| a.path());

                    stack.extend(filtered);
                }

                if yield_dirs {
                    return Some(path);
                }
            } else if path.is_file() {
                return Some(path);
            }
        }

        None
    })
}

pub fn recurse_list_dirs<P: AsRef<Path>>(root: P) -> impl Iterator<Item = PathBuf> {
    let mut stack = VecDeque::new();
    stack.push_back(root.as_ref().to_owned());

    std::iter::from_fn(move || {
        while let Some(path) = stack.pop_back() {
            if path.is_dir() {
                if let Ok(entries) = path.read_dir() {
                    let filtered = entries.filter_map(Result::ok).map(|a| a.path());
                    stack.extend(filtered);
                }

                return Some(path);
            }
        }

        None
    })
}

pub fn confirm_directory_delete(directory: impl AsRef<Path>) -> std::io::Result<bool> {
    let directory = directory.as_ref();

    if directory.is_dir() {
        let count = recurse_list_from_root(directory, false).count();

        let confirm = ask!(
            false,
            "Rebuilding the library will delete all files contained inside the library directory ({}). Are you sure you want to do this?\n{count} files with be deleted",
            directory.display()
        );

        if !confirm.unwrap_or(false) {
            println!("Aborting library rebuild");
            return Ok(false);
        }

        fs::remove_dir_all(directory)?;
        fs::create_dir(directory)?;
    }

    Ok(true)
}

/// Returns the file extension the [`ContainerFormat`] and [`Codec`] uses. Returns [`Option::None`] for invalid or unrecognized pairs
#[must_use]
pub fn pair_extension(format: &ContainerFormat, codec: &Codec) -> Option<&'static str> {
    match format {
        ContainerFormat::Flac => Some("flac"),
        ContainerFormat::Mp3 => Some("mp3"),
        ContainerFormat::Ogg => match codec {
            Codec::Flac => Some("oga"),
            Codec::LibOpus => Some("opus"),
            Codec::LibVorbis => Some("ogg"),
            _ => None,
        },
        ContainerFormat::Wav => Some("wav"),
    }
}

pub fn list_dirs_to_string(dirs: &[impl AsRef<Path>]) -> String {
    let mut buf = String::new();

    dirs.iter().enumerate().for_each(|(i, path)| {
        if i > 0 {
            buf.push('\n');
        }
        buf.push_str("\t- ");
        buf.push_str(&path.as_ref().display().to_string());
    });

    buf
}

pub fn clean_empty_dirs(root: impl AsRef<Path>) -> std::io::Result<()> {
    let mut dirs: Vec<PathBuf> = recurse_list_dirs(root).collect();
    dirs.sort_by_key(|d| std::cmp::Reverse(d.components().count()));

    for dir in dirs {
        match fs::remove_dir(dir) {
            Ok(()) => (),
            Err(e) if e.kind() == std::io::ErrorKind::DirectoryNotEmpty => (),
            Err(e) => return Err(e),
        }
    }

    Ok(())
}