selene-core 0.9.0-rc.1

Backend for selene-server
Documentation
use lunar_lib::{
    database::{
        CompareAndSwapTransaction, DatabaseEntry, Db, DbIdExt, DbIdIterExt, Deleteable, Entry,
        TransactionError, caching::Cacheable,
    },
    id::{ID_SIZE, Id},
};

use crate::{
    database::{GenreIndexable, Searchable, Selene, album_remove_track, artist_remove_track},
    library::{artist::Artist, track::Track},
};

impl DatabaseEntry for Track {
    type DbInner = Selene;

    const VERSION_NUMBER: u32 = 1;
    const TREE_NAME: &str = "track";

    fn pre_upsert(
        entry: &mut Entry<Self>,
        is_new: bool,
        cas_tx: &mut CompareAndSwapTransaction<Self::DbInner>,
    ) -> Result<(), TransactionError> {
        if is_new {
            Self::update_search_index(entry, cas_tx)?;
            Self::add_to_genres(entry, &entry.metadata.genres, cas_tx)?;
            Self::update_to_singles(entry, &entry.metadata.artists, cas_tx)?;
        }
        Ok(())
    }
}

impl Cacheable for Track {}

impl Deleteable for Track {
    fn unlink_references(
        track: Entry<Self>,
        cas_tx: &mut CompareAndSwapTransaction<Self::DbInner>,
    ) -> Result<(), TransactionError> {
        Self::remove_search_index(&track, cas_tx)?;
        Self::remove_from_genres(&track, &track.metadata.genres, cas_tx)?;
        Self::remove_from_artists(&track, &track.metadata.artists, cas_tx)?;

        if let Some(album_id) = track.metadata.album {
            album_id.tx_fetch_and_update(
                |old, _| {
                    let mut album = old.expect("Dangling ref");
                    album_remove_track(&mut album, track.id());
                    Ok(Some(album))
                },
                cas_tx,
            )?;
        }

        track.metadata.artists().tx_fetch_and_update(
            |old, _| {
                let mut artist = old.expect("Dangling ref");
                artist_remove_track(&mut artist, track.id());
                Ok(Some(artist))
            },
            cas_tx,
        )?;

        Ok(())
    }
}

impl Searchable for Track {
    const SEARCH_INDEX: &'static str = "track_search";

    fn search_name(&self) -> Option<&str> {
        self.metadata.title.as_deref()
    }
}

impl GenreIndexable for Track {
    const GENRE_INDEX: &'static str = "track_genres";
}

impl Track {
    const SINGLES_INDEX: &'static str = "artist_singles";

    pub fn get_singles_for_artist(
        artist: Id<Artist>,
        db: &Db<Selene>,
    ) -> Result<Vec<Id<Track>>, TransactionError> {
        let Some(value) = db.index(Self::SINGLES_INDEX).get(artist)? else {
            return Ok(Vec::new());
        };

        Ok(value
            .as_chunks::<{ ID_SIZE }>()
            .0
            .iter()
            .copied()
            .map(Id::from)
            .collect())
    }

    fn update_to_singles(
        entry: &Entry<Self>,
        artists: &[Id<Artist>],
        cas_tx: &mut CompareAndSwapTransaction<Selene>,
    ) -> Result<(), TransactionError> {
        if !entry.is_single() {
            return Ok(());
        }

        let request = cas_tx.get_or_new_index(Self::SINGLES_INDEX);
        for id in artists {
            request.fetch_and_update(&**id, |old| {
                let existing = old.as_deref().unwrap_or_default();

                if existing.as_chunks::<{ ID_SIZE }>().0.contains(&*entry.id()) {
                    return old;
                }

                let mut new = Vec::with_capacity(existing.len() + ID_SIZE);
                new.extend_from_slice(existing);
                new.extend_from_slice(&*entry.id());

                Some(new.into())
            })?;
        }

        Ok(())
    }

    fn remove_from_artists(
        entry: &Entry<Self>,
        artists: &[Id<Artist>],
        cas_tx: &mut CompareAndSwapTransaction<Selene>,
    ) -> Result<(), TransactionError> {
        let request = cas_tx.get_or_new_index(Self::SINGLES_INDEX);
        for id in artists {
            request.fetch_and_update(&**id, |old| {
                let new = old.as_deref().map(|old| {
                    let chunks = old.as_chunks::<{ ID_SIZE }>().0;

                    if let Some(idx) = chunks.iter().position(|c| *c == *entry.id()) {
                        let mut v = Vec::with_capacity((chunks.len() - 1) * { ID_SIZE });
                        v.extend_from_slice(&old[..idx * { ID_SIZE }]);
                        v.extend_from_slice(&old[(idx + 1) * { ID_SIZE }..]);
                        v
                    } else {
                        old.to_vec()
                    }
                });

                if let Some(new) = new
                    && !new.is_empty()
                {
                    Some(new.into())
                } else {
                    None
                }
            })?;
        }

        Ok(())
    }
}