selene-core 0.8.0

selene-core is the backend for Selene, a local-first music player
Documentation
use std::{
    collections::{HashMap, HashSet},
    fs, io,
    path::PathBuf,
    sync::Arc,
    time::SystemTime,
};

use barber::{ProgressBar, ProgressRenderer};
use lunar_lib::warn;
use rayon::iter::{IntoParallelIterator, ParallelIterator};

use crate::{cache_dir, config::common_config, library::track::TrackId, utils::hash_file};

pub mod album;
pub mod artist;
pub mod collectable;
pub mod collection;
pub mod track;

pub mod image_art;
pub mod metadata;

mod clean;
pub use clean::*;

mod extract;
pub use extract::*;

mod linking;
pub use linking::*;

mod export;
pub use export::*;

pub mod loudnorm;

#[cfg(feature = "lrclib")]
pub mod lyric_download;

pub fn hash_source_files(
    progress_renderer: Arc<dyn ProgressRenderer>,
) -> io::Result<HashMap<TrackId, PathBuf>> {
    let (sources, multithreaded) = {
        let common = common_config();
        (common.get_source_files(), common.main.multithreading)
    };
    let cache_file = cache_dir().join("sources");

    type Cached = HashMap<PathBuf, (TrackId, u64)>;

    let mut cached = if cache_file.is_file() {
        let bytes = std::fs::read(&cache_file)?;
        let mut cached: Cached = match postcard::from_bytes(&bytes) {
            Ok(cached) => cached,
            Err(err) => {
                warn!(
                    "Failed to read cache file '{file}': {err}",
                    file = cache_file.display()
                );
                HashMap::new()
            }
        };

        cached.retain(|p, (_, ct)| {
            p.metadata()
                .ok()
                .and_then(|p| p.modified().ok())
                .and_then(|d| d.duration_since(SystemTime::UNIX_EPOCH).ok())
                .is_some_and(|pt| *ct == pt.as_secs())
        });

        cached
    } else {
        HashMap::new()
    };

    let cached_sources: HashSet<&PathBuf> = cached.keys().collect();
    let rehash: Vec<PathBuf> = sources
        .iter()
        .filter(|source| !cached_sources.contains(source))
        .cloned()
        .collect();

    if rehash.is_empty() {
        return Ok(cached
            .into_iter()
            .map(|(path, (id, _))| (id, path))
            .collect());
    }

    let progress_bar = ProgressBar::new(0, rehash.len(), progress_renderer.clone());
    progress_bar.set_label("Hashing sources...");

    if multithreaded {
        let results: Vec<(PathBuf, (TrackId, u64))> = rehash
            .into_par_iter()
            .map(|source| -> io::Result<_> {
                let hash = hash_file(&source)?;
                let time = source
                    .metadata()?
                    .modified()?
                    .duration_since(SystemTime::UNIX_EPOCH)
                    .expect("Time went backwards")
                    .as_secs();
                progress_bar.increment();
                Ok((source, (TrackId::new(hash), time)))
            })
            .collect::<io::Result<_>>()?;
        cached.extend(results);
    } else {
        for source in rehash {
            let hash = hash_file(&source)?;
            let time = source
                .metadata()?
                .modified()?
                .duration_since(SystemTime::UNIX_EPOCH)
                .expect("Time went backwards")
                .as_secs();
            cached.insert(source, (TrackId::new(hash), time));
            progress_bar.increment();
        }
    }

    fs::create_dir_all(cache_dir())?;
    let writer = fs::OpenOptions::new()
        .write(true)
        .truncate(true)
        .create(true)
        .open(cache_file)?;

    let _ = postcard::to_io(&cached, writer);

    progress_bar.flush();

    Ok(cached
        .into_iter()
        .map(|(path, (id, _))| (id, path))
        .collect())
}