selene-core 0.9.0-alpha.2

selene-core is the backend for Selene, a local-first music player
Documentation
use chrono::{DateTime, Utc};
use lunar_lib::{
    database::{
        CompareAndSwapTransaction, Createable, CustomTransactionError, DatabaseEntry, DbIdExt,
        DbIdIterExt, Entry, Mergeable, TransactionError, caching::Cacheable,
    },
    id::Id,
    log::warn,
    vec_ext::VecExtensions,
};

use crate::{
    SeleneIdExt,
    database::{Library, Searchable},
    library::artist::{Artist, ArtistCreateArgs, ArtistCreationError},
};

impl DatabaseEntry for Artist {
    type DbInner = Library;
    const VERSION_NUMBER: u32 = 1;
    const TREE_NAME: &str = "artist";

    fn pre_upsert(
        entry: &mut Entry<Self>,
        cas_tx: &mut CompareAndSwapTransaction<Library>,
    ) -> Result<(), TransactionError> {
        let mut albums = entry.albums.iter().tx_get(cas_tx)?;

        albums.sort_unstable_by(|a, b| {
            let a_date = a.date.unwrap_or(DateTime::<Utc>::MAX_UTC);
            let b_date = b.date.unwrap_or(DateTime::<Utc>::MAX_UTC);
            a_date.cmp(&b_date).then(a.title.cmp(&b.title))
        });

        entry.albums = albums.iter().map(Entry::id).collect();

        Ok(())
    }
}

impl Cacheable for Artist {}

impl Searchable for Artist {
    const SEARCH_INDEX: &'static str = "artist_search";

    fn search_name(&self) -> Option<&str> {
        Some(&*self.name)
    }
}

impl Mergeable for Artist {
    fn merge_data(&mut self, from: Self) {
        self.name = from.name;
        self.art = self.art.take().or(from.art);
        self.description = self.description.take().or(from.description);
        self.tracks.extend_unique(from.tracks);
        self.albums.extend_unique(from.albums);
    }

    fn relink_references(
        from: &Entry<Self>,
        to: Id<Self>,
        cas_tx: &mut CompareAndSwapTransaction<Self::DbInner>,
    ) -> Result<(), TransactionError> {
        // Relink tracks
        from.tracks.iter().tx_fetch_and_update(
            |old| {
                let Some(mut track) = old else {
                    warn!(
                        "Artist '{}' attempted to relink to a track which does not exist yet",
                        from.name()
                    );
                    return None;
                };
                track.metadata.artists.push_unique(to);
                Some(track)
            },
            cas_tx,
        )?;

        // Relink tracks
        from.albums.iter().tx_fetch_and_update(
            |old| {
                let Some(mut album) = old else {
                    warn!(
                        "Artist '{}' attempted to relink to a album which does not exist yet",
                        from.name()
                    );
                    return None;
                };
                album.artists.push_unique(to);
                Some(album)
            },
            cas_tx,
        )?;

        Ok(())
    }
}

impl Createable for Artist {
    type CreateArgs = ArtistCreateArgs;
    type Err = ArtistCreationError;

    fn create(
        args: Self::CreateArgs,
        cas_tx: &mut CompareAndSwapTransaction<Self::DbInner>,
    ) -> Result<Entry<Self>, CustomTransactionError<Self::Err>> {
        let id = Id::from_string_hash(&args.name);
        if id.tx_check(cas_tx)? {
            return Err(CustomTransactionError::Transaction(
                TransactionError::AlreadyInDatabase,
            ));
        }

        args.tracks.tx_fetch_and_update(
            |old| {
                let mut track = old.expect("Invalid ref");
                track.metadata.artists.push_unique(id);
                Some(track)
            },
            cas_tx,
        )?;

        args.albums.tx_fetch_and_update(
            |old| {
                let mut album = old.expect("Invalid ref");
                album.artists.push_unique(id);
                Some(album)
            },
            cas_tx,
        )?;

        let artist = Artist::new(args.name)
            .map_err(CustomTransactionError::Closure)?
            .to_entry(id);

        Ok(artist)
    }
}