selene-core 0.7.1

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

use lunar_lib::{
    database::{
        CompareAndSwapTransaction, CustomTransactionError, DatabaseEntry, TransactionError,
        caching::Cacheable,
    },
    formatter::Taggable,
    paths::sys::sanitize_str,
};

use crate::{
    database::{
        ALBUM_CACHE, Createable, LibraryDb, Mergeable, artists_add_album, artists_remove_album,
        patch_option_replace, patch_vec, tracks_set_album,
    },
    library::{
        album::{Album, AlbumId, TrackReference},
        artist::Artist,
        track::{Track, TrackId},
    },
};

impl DatabaseEntry for Album {
    const VERSION_NUMBER: u32 = 1;
    const TREE_NAME: &str = "album";
    type Id = AlbumId;
    type Db = LibraryDb;

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

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

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

        Ok(())
    }
}

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

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 !self.genre.is_empty() {
            table.add_entry("album_genre", sanitize_str(self.genre.join(";")));
        }

        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 {
    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();

        // Get the album we are merging into OR upsert if missing and merging into self
        let Some(mut album) = 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 with the data from self
        album.name = self.name.clone();
        patch_option_replace(&mut album.art, self.art);
        patch_option_replace(&mut album.disc_total, self.disc_total);
        patch_option_replace(&mut album.track_total, self.track_total);
        patch_vec(&mut album.genre, self.genre);
        patch_vec(&mut album.tracks, self.tracks);
        patch_vec(&mut album.artists, self.artists);
        patch_option_replace(&mut album.date, self.date);

        let mut tracks: Vec<Track> = cas_tx.tx_get_batch(album.tracks.iter().map(|t| t.id))?;
        let mut artists: Vec<Artist> = cas_tx.tx_get_batch(&album.artists)?;

        // Relink tracks && Artists
        tracks_set_album(&mut tracks, Some(merge_into));
        artists_add_album(&mut artists, merge_into);

        // Upsert album
        cas_tx.tx_upsert(album.clone())?;
        if self_id != merge_into {
            artists_remove_album(&mut artists, merge_into);
            cas_tx.tx_remove(self_id)?;
        }

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

        Ok(album)
    }
}

#[derive(Debug, Clone)]
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}")]
    Transaction(#[from] TransactionError),
}

impl Createable for Album {
    type CreateArgs = AlbumCreateArgs;
    type Err = Infallible;

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

        let artists = args.artists.iter().map(Artist::id).collect();

        let tracks = (1u32..)
            .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 cas_tx.tx_check(album.id())? {
            return Err(TransactionError::AlreadyInDatabase.into());
        }

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

                cas_tx.tx_upsert(track)?;

                Ok(())
            })?;

        // Upsert album
        cas_tx.tx_insert(album.clone())?;

        Ok(album)
    }
}

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

impl Eq for Album {}