selene-core 0.5.0

selene-core is the backend for Selene, a local-first music player
Documentation
use std::{
    ops::Deref,
    path::{Path, PathBuf},
    sync::LazyLock,
};

use lunar_lib::database::{
    ConflictableTransactionResult, Database, DatabaseEntry, DatabaseError, Db, Transactional, Tree,
};

pub mod entry_extensions;
pub mod tx_extensions;

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

mod mergeable;
pub use mergeable::*;

mod createable;
pub use createable::*;

mod deleteable;
pub use deleteable::*;

use crate::{
    data_dir,
    library::collection::{Collection, STATIC_COLLECTIONS, STATIC_COLLECTIONS_VERSION},
};

pub mod validator;

#[derive(Clone)]
pub struct LibraryDb {
    db: Db,
}

impl Deref for LibraryDb {
    type Target = Db;

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

static LIBRARY_DB_PATH: LazyLock<PathBuf> = LazyLock::new(|| data_dir().join("library_data"));

impl Database for LibraryDb {
    const RETRY_MAX_ATTEMPTS: Option<usize> = None;

    const RETRY_DURATION: std::time::Duration = std::time::Duration::from_millis(1);

    fn new(db: Db) -> Self {
        Self { db }
    }

    fn pre_open(db: &LibraryDb) -> Result<(), Box<dyn std::error::Error>> {
        ensure_hardcoded_collections(db)?;
        Ok(())
    }

    fn path() -> &'static Path {
        &LIBRARY_DB_PATH
    }
}

pub(crate) fn track_tree(db: &LibraryDb) -> Tree {
    db.open_tree("track").expect("Failed to open 'track' tree")
}
pub(crate) fn album_tree(db: &LibraryDb) -> Tree {
    db.open_tree("album").expect("Failed to open 'album' tree")
}
pub(crate) fn artist_tree(db: &LibraryDb) -> Tree {
    db.open_tree("artist")
        .expect("Failed to open 'artist' tree")
}
pub(crate) fn collection_tree(db: &LibraryDb) -> Tree {
    db.open_tree("collection")
        .expect("Failed to open 'collection' tree")
}

fn ensure_hardcoded_collections(db: &LibraryDb) -> Result<(), Box<dyn std::error::Error>> {
    const KEY: &[u8] = b"hardcoded_collections_version";

    if db
        .get(KEY)?
        .is_none_or(|v| *v != STATIC_COLLECTIONS_VERSION.to_be_bytes())
    {
        let db_tree = &***db;
        let collection_tree = Collection::tree(db);

        (db_tree, &collection_tree).transaction(
            |(db_tree, collection_tree)| -> ConflictableTransactionResult<(), DatabaseError> {
                for collection in STATIC_COLLECTIONS.iter() {
                    let collection = *collection;

                    let mut buf = Vec::new();
                    ciborium::into_writer(collection, &mut buf).unwrap();

                    collection_tree.insert(&*collection.id(), &*buf)?;
                }

                db_tree.insert(KEY, &STATIC_COLLECTIONS_VERSION.to_be_bytes())?;

                Ok(())
            },
        )?;
    }

    Ok(())
}