selene-core 0.9.0-alpha.2

selene-core is the backend for Selene, a local-first music player
Documentation
use std::path::PathBuf;

use lunar_lib::{
    database::{DatabaseEntry, Db, DbIdIterExt, Entry, TransactionError},
    define_db,
    id::Id,
    iterator_ext::{IntoIteratorExtensions, IteratorExtensions},
    log::trace,
    paths::data_dir,
};

mod tx_extensions;
pub(crate) use tx_extensions::*;

#[cfg(debug_assertions)]
pub mod validator;

define_db!(Library {
    fn path() -> Option<PathBuf> {
        Some(data_dir().join("library_data"))
    }
});

pub trait SeleneEntryExt<T: DatabaseEntry> {
    fn to_db_entry(self) -> Entry<T>;
}

impl<T: DatabaseEntry> SeleneEntryExt<T> for crate::library::Entry<T> {
    fn to_db_entry(self) -> Entry<T> {
        self.into()
    }
}

pub trait Searchable: DatabaseEntry {
    const SEARCH_INDEX: &'static str;

    fn search_name(&self) -> Option<&str>;

    fn build_search_index(db: &Db<Self::DbInner>) -> Result<(), TransactionError> {
        db.index::<Self>(Self::SEARCH_INDEX).rebuild(|tree, entry| {
            if let Some(title) = entry.search_name().map(str::to_ascii_lowercase) {
                let mut key = [0u8; 8];
                let bytes = title.as_bytes();
                let len = bytes.len().min(8);
                key[..len].copy_from_slice(&bytes[..len]);

                tree.fetch_and_update(key, |old| match old {
                    Some(v) => {
                        let mut new_value = v.to_vec();
                        new_value.extend_from_slice(&*entry.id());
                        Some(new_value)
                    }
                    None => Some(entry.id().to_vec()),
                })?;
            }

            Ok(())
        })
    }

    fn search(
        db: &Db<Self::DbInner>,
        query: &str,
        limit: usize,
        offset: usize,
    ) -> Result<Vec<Entry<Self>>, TransactionError> {
        trace!(
            "Searching index {} with query '{query}'",
            Self::SEARCH_INDEX
        );
        let query = query.to_ascii_lowercase();
        let len = query.len().min(8);
        let key = &query.as_bytes()[..len];

        let mut items = db
            .index::<Self>(Self::SEARCH_INDEX)
            .scan_prefix(key)
            .try_map(|i| -> Result<Vec<Id<Self>>, TransactionError> {
                let (_, v) = i.map_err(TransactionError::from)?;
                Ok(v.chunks_exact(32)
                    .map(|chunk| Id::<Self>::from(<[u8; 32]>::try_from(chunk).unwrap()))
                    .collect())
            })?
            .flatten()
            .db_get(db)?;

        items.retain(|item| {
            item.search_name()
                .expect("Items without names cant be indexed")
                .to_ascii_lowercase()
                .starts_with(&query)
        });

        {
            let result = items
                .iter()
                .map(|t| t.search_name().unwrap())
                .join_to_string(", ");
            trace!("Search found: {result}");
        }

        Ok(items.into_iter().skip(offset).take(limit).to_vec())
    }
}