selene-core 0.7.1

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

use chrono::{DateTime, Utc};
use lunar_lib::database::{
    CompareAndSwapTransaction, CustomTransactionError, DatabaseEntry, TransactionError,
    caching::Cacheable,
};

use crate::{
    database::{
        ARTIST_CACHE, Createable, LibraryDb, Mergeable, artist_add_tracks, patch_option_replace,
        patch_vec, tracks_add_artist, tracks_remove_artist,
    },
    library::{
        album::Album,
        artist::{Artist, ArtistGroup, ArtistId},
        track::Track,
    },
};

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

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

impl Eq for Artist {}

impl DatabaseEntry for Artist {
    type Id = ArtistId;
    type Db = LibraryDb;
    const VERSION_NUMBER: u32 = 1;
    const TREE_NAME: &str = "artist";

    fn id(&self) -> Self::Id {
        self.id
    }

    fn pre_upsert(
        &mut self,
        cas_tx: &CompareAndSwapTransaction<Self::Db>,
    ) -> Result<(), TransactionError> {
        let mut albums = self.tx_albums(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.name.cmp(&b.name))
        });

        self.albums = albums.iter().map(Album::id).collect();

        Ok(())
    }
}

impl Cacheable for Artist {
    fn cache() -> &'static std::sync::Mutex<lunar_lib::database::caching::DbCache<Self>> {
        &ARTIST_CACHE
    }
}

impl Mergeable for Artist {
    type Err = Infallible;

    fn tx_merge(
        self,
        merge_into: Self::Id,
        cas_tx: &mut CompareAndSwapTransaction<Self::Db>,
    ) -> Result<Self, CustomTransactionError<Self::Err>> {
        let self_id = self.id();

        let Some(mut artist) = cas_tx.tx_get(merge_into)? else {
            if self_id == merge_into {
                cas_tx.tx_upsert(self.clone())?;
                return Ok(self);
            }
            return Err(TransactionError::MissingEntry.into());
        };

        // Patch the album we are merging into with the data from self
        artist.name = self.name;
        patch_option_replace(&mut artist.cover_art, self.cover_art);
        patch_option_replace(&mut artist.description, self.description);
        patch_vec(&mut artist.tracks, self.tracks);
        patch_vec(&mut artist.albums, self.albums);

        // Relink tracks
        let mut tracks: Vec<Track> = cas_tx.tx_get_batch(&artist.tracks)?;
        tracks_add_artist(&mut tracks, merge_into);

        // Upsert artist
        cas_tx.tx_upsert(artist.clone())?;
        if self_id != merge_into {
            tracks_remove_artist(&mut tracks, self_id);
            cas_tx.tx_remove(self_id)?;
        }

        // Upsert tracks
        for track in tracks {
            cas_tx.tx_upsert(track)?;
        }

        Ok(artist)
    }
}

// impl Patchable<Self> for Artist {
//     fn patch(&mut self, patch: Self) {
//         let Artist {
//             id: _,
//             name,
//             cover_art,
//             description,
//             tracks,
//             albums,
//             version: _,
//         } = patch;

//         patch_option_replace(&mut self.cover_art, cover_art);
//         patch_option_replace(&mut self.description, description);
//         patch_replace(&mut self.name, Some(name));
//         patch_vec(&mut self.tracks, tracks);
//         patch_vec(&mut self.albums, albums);
//     }
// }

impl std::ops::Deref for Artist {
    type Target = ArtistId;

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

#[derive(Debug, Clone)]
pub struct ArtistCreateArgs {
    pub name: String,
    pub albums: Vec<Album>,
    pub tracks: Vec<Track>,
}

impl ArtistCreateArgs {
    #[must_use]
    pub fn new(name: String, albums: Vec<Album>, tracks: Vec<Track>) -> Self {
        Self {
            name,
            albums,
            tracks,
        }
    }
}

#[derive(Debug, thiserror::Error)]
pub enum ArtistCreationError {
    #[error("Artist name resolved to an empty string")]
    EmptyName,
}

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

    fn tx_create(
        cas_tx: &mut CompareAndSwapTransaction<Self::Db>,
        args: Self::CreateArgs,
    ) -> Result<Self, CustomTransactionError<Self::Err>> {
        let mut artist = Artist::new(args.name).ok_or(CustomTransactionError::Closure(
            ArtistCreationError::EmptyName,
        ))?;
        let artist_id = artist.id();

        artist_add_tracks(&mut artist, args.tracks.iter().map(Track::id));

        cas_tx.tx_insert(artist.clone())?;

        for mut track in args.tracks {
            track.metadata.artists.add_artist(artist_id);
            cas_tx.tx_upsert(track)?;
        }

        Ok(artist)
    }
}