selene-core 0.3.1

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

use lunar_lib::{formatter::Taggable, paths::sys::sanitize_str};
use sled::{Db, Tree};

use crate::{
    database::{
        CompareAndSwapTransaction, Createable, DatabaseEntry, DatabaseError, Mergeable, Patchable,
        album_tree, patch_option_replace, patch_vec_merge,
    },
    library::{
        album::{Album, AlbumId, TrackReference},
        artist::{Artist, ArtistGroup},
        track::{Track, TrackId},
    },
};

impl DatabaseEntry for Album {
    const VERSION_NUMBER: u32 = 1;
    type Id = AlbumId;

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

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

    fn pre_upsert(&mut self, _cas_tx: &CompareAndSwapTransaction) -> Result<(), DatabaseError> {
        self.tracks.sort_by(|a, b| {
            let a_disc = a.disc_num.unwrap_or(u16::MAX);
            let a_track = a.track_num.unwrap_or(u16::MAX);
            let b_disc = b.disc_num.unwrap_or(u16::MAX);
            let b_track = b.track_num.unwrap_or(u16::MAX);

            a_disc.cmp(&b_disc).then(a_track.cmp(&b_track))
        });

        Ok(())
    }
}

impl Patchable<Self> for Album {
    fn patch(&mut self, patch: Album) {
        let Album {
            id: _,
            name,
            cover_art,
            tracks,
            artist_group,
            disc_total,
            genre,
            track_total,
            date,
            version: _,
        } = patch;

        self.name = name;
        patch_option_replace(&mut self.cover_art, cover_art);
        self.artist_group.patch(artist_group);
        patch_vec_merge(&mut self.tracks, tracks);
        patch_option_replace(&mut self.disc_total, disc_total);
        patch_option_replace(&mut self.track_total, track_total);
        patch_option_replace(&mut self.genre, genre);
        patch_option_replace(&mut self.date, date);
    }
}

impl Taggable for Album {
    type Err = Infallible;

    fn fill_table(&self, table: &mut lunar_lib::formatter::FormatTable) -> Result<(), Self::Err> {
        if let Some(value) = self.disc_total {
            table.add_entry("disc_total", value.to_string());
        }
        if let Some(value) = &self.genre {
            table.add_entry("album_genre", sanitize_str(value));
        }

        table.add_entry("album", &self.name);

        if let Some(value) = self.track_total {
            table.add_entry("track_total", value.to_string());
        }
        if let Some(value) = self.date {
            table.add_entry("album_year", value.to_string());
        }

        Ok(())
    }
}

impl Mergeable for Album {
    fn tx_merge(
        &self,
        merge_into: Self::Id,
        cas_tx: &mut CompareAndSwapTransaction,
    ) -> Result<(), DatabaseError> {
        // Get and patch album
        let mut album = cas_tx
            .tx_get(merge_into)?
            .expect("Album was deleted in this transaction");

        album.patch(self.clone());

        // Relink tracks
        cas_tx.tracks_set_album(Some(merge_into), self.tracks.iter().map(|t| &t.id))?;

        // Relink artists
        cas_tx.artists_remove_album(self.id(), self.artist_group.artist_ids())?;
        cas_tx.artists_add_album(merge_into, self.artist_group.artist_ids())?;

        // Upsert album
        cas_tx.tx_upsert(album.id(), Some(album))?;
        if self.id() != merge_into {
            cas_tx.tx_remove(self.id())?
        }

        Ok(())
    }
}

pub struct AlbumCreateArgs {
    pub name: String,
    pub artists: Vec<Artist>,
    pub tracks: Vec<Track>,
}

impl AlbumCreateArgs {
    #[must_use]
    pub fn new(name: String, artists: Vec<Artist>, tracks: Vec<Track>) -> Self {
        Self {
            name,
            artists,
            tracks,
        }
    }
}

#[derive(Debug, thiserror::Error)]
pub enum AlbumCreationError {
    #[error("A database already exists with the same identifier: {0}")]
    AlreadyExists(String),

    #[error("{0}")]
    Database(#[from] DatabaseError),
}

impl Createable for Album {
    type CreateArgs = AlbumCreateArgs;

    fn tx_create(
        cas_tx: &mut CompareAndSwapTransaction,
        mut args: Self::CreateArgs,
    ) -> Result<Self::Id, DatabaseError> {
        let track_ids: Vec<TrackId> = args.tracks.iter().map(|i| i.id()).collect();
        for artist in &mut args.artists {
            artist.tracks.retain(|t| !track_ids.contains(t));
        }

        let artists = ArtistGroup::from_artists(&args.artists);

        let tracks = (1u16..)
            .zip(args.tracks.iter())
            .map(|(i, t)| TrackReference {
                id: t.id(),
                track_num: Some(i),
                disc_num: None,
            });

        // Create album
        let album = Album::new(args.name, artists, tracks.collect());
        if Self::db_check(album.id())? {
            return Err(DatabaseError::AlreadyInDatabase);
        }

        // Relink tracks
        args.tracks
            .into_iter()
            .try_for_each(|mut track| -> Result<(), DatabaseError> {
                track.metadata.album = Some(album.id());

                cas_tx.tx_upsert(track.id(), Some(track))?;

                Ok(())
            })?;

        // Upsert album
        let album_id = album.id();
        cas_tx.tx_insert(album)?;

        Ok(album_id)
    }
}

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

impl Eq for Album {}