selene-core 0.3.1

selene-core is the backend for Selene, a local-first music player
Documentation
use chrono::{DateTime, Utc};
use sled::{Db, Tree};

use crate::{
    database::{
        CompareAndSwapTransaction, Createable, DatabaseEntry, DatabaseError, Patchable,
        artist_tree, patch_option_replace, patch_replace, patch_vec,
        validator::DatabaseReferenceError,
    },
    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.as_bytes());
    }
}

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;
    const VERSION_NUMBER: u32 = 1;

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

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

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

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;

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

        for track in &args.tracks {
            if let Some(album) = args.albums.iter().find(|album| {
                album
                    .tracks
                    .iter()
                    .any(|track_ref| track_ref.id == track.id())
            }) {
                return Err(DatabaseError::ValidationError(
                    DatabaseReferenceError::ArtistInvalidTrackRef {
                        artist: artist.id(),
                        track: track.id(),
                        album: album.id(),
                    },
                ));
            }

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

        cas_tx.tx_insert(artist)?;

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

        Ok(artist_id)
    }
}