selene-core 0.9.0-alpha.2

selene-core is the backend for Selene, a local-first music player
Documentation
use lunar_lib::{
    database::{
        CompareAndSwapTransaction, Createable, CustomTransactionError, DatabaseEntry, DbIdExt,
        DbIdIterExt, Entry, Mergeable, TransactionError, caching::Cacheable,
    },
    formatter::Taggable,
    id::Id,
    iterator_ext::IteratorExtensions,
    log::warn,
    paths::sys::sanitize_str,
    vec_ext::VecExtensions,
};

use crate::{
    SeleneIdExt,
    database::{Library, Searchable, track_set_album},
    library::{
        album::{Album, TrackReference},
        artist::Artist,
        track::Track,
    },
};

impl DatabaseEntry for Album {
    const VERSION_NUMBER: u32 = 1;
    const TREE_NAME: &str = "album";
    type DbInner = Library;
}

impl Cacheable for Album {}

impl Searchable for Album {
    const SEARCH_INDEX: &'static str = "album_search";

    fn search_name(&self) -> Option<&str> {
        Some(&*self.title)
    }
}

impl Taggable for Album {
    type Err = std::convert::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.title);

        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 merge_data(&mut self, from: Self) {
        self.title = from.title;
        self.art = self.art.take().or(from.art);
        self.disc_total = self.disc_total.max(from.disc_total);
        self.track_total = self.track_total.max(from.track_total);
        self.genre.extend_unique(from.genre);
        self.tracks.extend_unique(from.tracks);
        self.artists.extend_unique(from.artists);
        self.date = self.date.or(from.date);
    }

    fn relink_references(
        from: &Entry<Self>,
        to: Id<Self>,
        cas_tx: &mut CompareAndSwapTransaction<Self::DbInner>,
    ) -> Result<(), TransactionError> {
        // Relink tracks
        from.tracks
            .iter()
            .map(|t| Id::from(*t.id))
            .tx_fetch_and_update(
                |old| {
                    let Some(mut track) = old else {
                        warn!(
                            "Album '{}' attempted to relink to a track which does not exist yet",
                            from.name()
                        );
                        return None;
                    };
                    track_set_album(&mut track, Some(to));
                    Some(track)
                },
                cas_tx,
            )?;

        // Relink artists
        from.artists.iter().tx_fetch_and_update(
            |old| {
                let Some(mut artist) = old else {
                    warn!(
                        "Album '{}' attempted to relink to a artist which does not exist yet",
                        from.name()
                    );
                    return None;
                };
                artist.albums.push_unique(to);
                Some(artist)
            },
            cas_tx,
        )?;

        Ok(())
    }
}

#[derive(Debug, Clone)]
pub struct AlbumCreateArgs {
    pub name: String,
    pub artists: Vec<Id<Artist>>,
    pub tracks: Vec<Id<Track>>,
}

impl AlbumCreateArgs {
    #[must_use]
    pub fn new(name: String, artists: Vec<Id<Artist>>, tracks: Vec<Id<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 = std::convert::Infallible;

    fn create(
        args: Self::CreateArgs,
        cas_tx: &mut CompareAndSwapTransaction<Self::DbInner>,
    ) -> Result<Entry<Self>, CustomTransactionError<Self::Err>> {
        let id: Id<Album> = Id::from_string_hash(&args.name);

        if id.tx_check(cas_tx)? {
            return Err(TransactionError::AlreadyInDatabase.into());
        }

        // Relink tracks
        args.tracks.iter().tx_fetch_and_update(
            |old| {
                let mut track = old.expect("Invalid ref");
                track_set_album(&mut track, Some(id));
                Some(track)
            },
            cas_tx,
        )?;

        // Relink artists
        args.artists.iter().tx_fetch_and_update(
            |old| {
                let mut artist = old.expect("Invalid ref");
                artist.albums.push_unique(id);
                Some(artist)
            },
            cas_tx,
        )?;

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

        // Create album
        let album = Album::new(args.name, args.artists, tracks).to_entry(id);

        Ok(album)
    }
}