selene-core 0.9.0-alpha.2

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

use lunar_lib::{caching::Cachable, hash_file, id::Id, log::warn, paths::cache_dir};
use rayon::iter::{IntoParallelIterator, ParallelIterator};

use crate::{config::common_config, library::track::Track};

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

pub mod image_art;
pub mod metadata;

mod extract;
pub use extract::*;

pub mod loudnorm;

pub fn hash_source_files() -> io::Result<HashMap<Id<Track>, 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, (Id<Track>, u64)>;

    let mut cached = Cached::load_from_cache(&cache_file);
    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())
    });

    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());
    }

    if multithreaded {
        let results: Vec<(PathBuf, (Id<Track>, 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();
                Ok((source, (Id::<Track>::from(*hash.as_bytes()), 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, (Id::<Track>::from(*hash.as_bytes()), time));
        }
    }

    if let Err(err) = cached.save_to_cache(cache_file) {
        warn!("Failed to save to cache: {err}");
    }

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

#[derive(Clone)]
pub struct Entry<T> {
    pub entry: T,
    pub id: Id<T>,
}

impl<T> std::ops::Deref for Entry<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.entry
    }
}

impl<T> std::ops::DerefMut for Entry<T> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.entry
    }
}

impl<T> PartialEq for Entry<T> {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id
    }
}

impl<T> Eq for Entry<T> {}

impl<T> std::hash::Hash for Entry<T> {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.id.hash(state);
    }
}

impl<T> Entry<T> {
    pub fn id(&self) -> Id<T> {
        self.id
    }
}

#[cfg(feature = "database-impls")]
impl<T: lunar_lib::database::DatabaseEntry> From<Entry<T>> for lunar_lib::database::Entry<T> {
    fn from(value: Entry<T>) -> Self {
        lunar_lib::database::Entry::new(value.entry, value.id)
    }
}

#[cfg(feature = "database-impls")]
impl<T: lunar_lib::database::DatabaseEntry> From<lunar_lib::database::Entry<T>> for Entry<T> {
    fn from(value: lunar_lib::database::Entry<T>) -> Self {
        Entry::<T> {
            id: value.id(),
            entry: value.into_item(),
        }
    }
}

pub trait EntryExt {
    #[must_use]
    fn to_entry(self, id: Id<Self>) -> Entry<Self>
    where
        Self: Sized,
    {
        Entry { entry: self, id }
    }
}

impl<T> EntryExt for T {}