selene-core 0.5.3

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, DatabaseError,
    TransactionError, Tree,
};

use crate::{
    database::{
        Createable, LibraryDb, Patchable, artist_tree, patch_option_replace, patch_replace,
        patch_vec, tx_extensions::CasTxExtensions,
    },
    library::{
        album::Album,
        artist::{Artist, 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 EntryDb = LibraryDb;
    const VERSION_NUMBER: u32 = 1;

    fn tree(db: &Self::EntryDb) -> Tree {
        artist_tree(db)
    }

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

    fn pre_upsert(
        &mut self,
        cas_tx: &CompareAndSwapTransaction<Self::EntryDb>,
    ) -> 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 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,
        }
    }
}

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

    fn tx_create(
        cas_tx: &mut CompareAndSwapTransaction<Self::EntryDb>,
        args: Self::CreateArgs,
    ) -> Result<Self, CustomTransactionError<Self::Err>> {
        let mut artist = Artist::new(args.name).ok_or(TransactionError::Database(
            DatabaseError::InvalidInput("Name field must not be empty".to_owned()),
        ))?;
        let artist_id = artist.id();

        for track in &args.tracks {
            if !artist.tracks.contains(&track.id()) {
                artist.tracks.push(track.id());
            }
        }

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

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

        Ok(artist)
    }
}