selene-core 0.8.0

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,
        EntryIdIteratorExt, Mergeable, TransactionError, caching::Cacheable,
    },
    vec_ext::VecExtensions,
};

use crate::{
    database::{
        ARTIST_CACHE, LibraryDb, artist_add_tracks, 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 {
    fn tx_merge(
        mut self,
        from: Self,
        cas_tx: &mut CompareAndSwapTransaction<Self::Db>,
    ) -> Result<Self, TransactionError> {
        let self_id = self.id();
        let from_id = from.id();

        // Patch self with data from `from`
        self.name = from.name;
        self.cover_art = self.cover_art.or(from.cover_art);
        self.description = self.description.or(from.description);
        self.tracks.extend_unique(from.tracks);
        self.albums.extend_unique(from.albums);

        // Relink tracks
        let mut tracks: Vec<Track> = self.tracks.iter().copied().tx_get_batch(cas_tx)?;
        tracks_add_artist(&mut tracks, self_id);

        // Delete `from`
        if self_id != from_id {
            tracks_remove_artist(&mut tracks, from_id);
            cas_tx.tx_remove(from_id)?;
        }

        // Upsert artist
        cas_tx.tx_upsert(self.clone())?;

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

        Ok(self)
    }
}

// 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).map_err(CustomTransactionError::Closure)?;
        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)
    }
}