selene-core 0.8.2

selene-core is the backend for Selene, a local-first music player
Documentation
use lunar_lib::{
    database::{CompareAndSwapTransaction, TransactionError},
    iterator_ext::IteratorExtensions,
};

use crate::{
    database::LibraryDb,
    library::{
        album::{Album, AlbumId, TrackReference},
        artist::{Artist, ArtistGroup, ArtistId},
        track::{Track, TrackId},
    },
};

pub trait CasTxLibraryExtensions {
    fn album_set_and_relink_tracks(
        &mut self,
        album: Album,
        tracks: Vec<Track>,
    ) -> Result<(Album, Vec<Track>), TransactionError>;

    fn album_set_and_relink_artists(
        &mut self,
        album_id: Album,
        artists: impl IntoIterator<Item = Artist>,
    ) -> Result<(Album, Vec<Artist>), TransactionError>;

    fn relink_track_to_album(
        &mut self,
        track_id: TrackId,
        album: Option<AlbumId>,
    ) -> Result<bool, TransactionError>;
}

impl CasTxLibraryExtensions for CompareAndSwapTransaction<LibraryDb> {
    /// Applies a two-way relinking operation, relinking to the track to a new (or no) album, and disconnecting the tracks old album, if any
    fn relink_track_to_album(
        &mut self,
        track_id: TrackId,
        album: Option<AlbumId>,
    ) -> Result<bool, TransactionError> {
        let Some(mut track) = self.tx_get(track_id)? else {
            return Ok(false);
        };

        if track.metadata().album == album {
            return Ok(false);
        }

        let old_album_id = track.metadata.album;

        track.metadata.album = album;

        if let Some(old_album_id) = old_album_id {
            let mut old_album = self.tx_get(old_album_id)?.unwrap_or_else(|| {
                panic!(
                    "Track '{}' contains a reference to an album that doesnt exist",
                    track.metadata.safe_title()
                )
            });

            old_album.tracks.retain(|t| t.id != track_id);

            self.tx_upsert(old_album)?;
        }

        if let Some(new_album_id) = album {
            let mut new_album = self
                .tx_get(new_album_id)?
                .ok_or(TransactionError::MissingEntry)?;

            new_album.tracks.push(TrackReference {
                id: track_id,
                track_num: None,
                disc_num: None,
            });

            self.tx_upsert(new_album)?;
        }

        self.tx_upsert(track)?;

        Ok(true)
    }

    /// Applies a two-way relinking operation, relinking artists to the album, and disconnecting references from removed artists
    fn album_set_and_relink_artists(
        &mut self,
        mut album: Album,
        artists: impl IntoIterator<Item = Artist>,
    ) -> Result<(Album, Vec<Artist>), TransactionError> {
        let mut artists = artists.into_iter().to_vec();
        let artist_ids = artists.iter().map(Artist::id).to_vec();

        let old_artists = album.artists.artist_ids().to_vec();

        let mut removed_artists = old_artists
            .iter()
            .filter_map(|a| -> Option<Result<Artist, TransactionError>> {
                (!artist_ids.contains(a)).then_some(
                    self.tx_get(*a)
                        .transpose()
                        .ok_or(TransactionError::MissingEntry)
                        .flatten(),
                )
            })
            .collect::<Result<Vec<Artist>, _>>()?;

        album.artists = artist_ids;

        let album_id = album.id();
        artists_add_album(&mut artists, album_id);
        artists_remove_album(&mut removed_artists, album_id);

        self.tx_upsert(album.clone())?;
        for artist in artists.iter().chain(removed_artists.iter()) {
            self.tx_upsert(artist.clone())?;
        }

        Ok((album, artists))
    }

    /// Applies a two-way relinking operation, relinking tracks to the album, and disconnecting references from removed tracks
    fn album_set_and_relink_tracks(
        &mut self,
        mut album: Album,
        mut tracks: Vec<Track>,
    ) -> Result<(Album, Vec<Track>), TransactionError> {
        let track_ids: Vec<TrackId> = tracks.iter().map(Track::id).collect();
        let old_tracks: Vec<TrackId> = album.tracks.iter().map(|t| t.id).collect();

        let mut removed_tracks: Vec<Track> = old_tracks
            .into_iter()
            .filter(|old_track| !track_ids.contains(old_track))
            .try_map(|id| {
                self.tx_get(id)
                    .transpose()
                    .ok_or(TransactionError::MissingEntry)
                    .flatten()
            })?
            .collect();

        album_set_tracks(&mut album, &track_ids);
        tracks_set_album(&mut tracks, Some(album.id()));
        tracks_set_album(&mut removed_tracks, None);

        Ok((album, tracks))
    }
}

// Unsafe one-way helpers
pub(crate) fn album_set_tracks(album: &mut Album, tracks: &[TrackId]) {
    album.tracks = tracks
        .iter()
        .map(|t| {
            album
                .tracks
                .iter()
                .find(|old| old.id == *t)
                .copied()
                .unwrap_or(TrackReference {
                    id: *t,
                    track_num: None,
                    disc_num: None,
                })
        })
        .collect();
}

pub(crate) fn album_add_track(album: &mut Album, track: TrackId) {
    album.tracks.push(TrackReference {
        id: track,
        track_num: None,
        disc_num: None,
    });
}

pub(crate) fn tracks_set_album<'a>(
    tracks: impl IntoIterator<Item = &'a mut Track>,
    album_id: Option<AlbumId>,
) {
    for track in tracks {
        track.metadata.album = album_id;
    }
}

pub(crate) fn tracks_add_artist<'a>(
    tracks: impl IntoIterator<Item = &'a mut Track>,
    artist_id: ArtistId,
) {
    for track in tracks {
        if !track.metadata.artists.contains(&artist_id) {
            track.metadata.artists.push(artist_id);
        }
    }
}

pub(crate) fn tracks_remove_artist<'a>(
    tracks: impl IntoIterator<Item = &'a mut Track>,
    artist_id: ArtistId,
) {
    for track in tracks {
        track.metadata.artists.retain(|a| *a != artist_id);
    }
}

pub(crate) fn artists_remove_album<'a>(
    artists: impl IntoIterator<Item = &'a mut Artist>,
    album_id: AlbumId,
) {
    for artist in artists {
        artist.albums.retain(|a| *a != album_id);
    }
}

pub(crate) fn artists_add_album<'a>(
    artists: impl IntoIterator<Item = &'a mut Artist>,
    album_id: AlbumId,
) {
    for artist in artists {
        if !artist.albums.contains(&album_id) {
            artist.albums.push(album_id);
        }
    }
}

pub(crate) fn artist_add_tracks<'a>(
    artist: &mut Artist,
    track_ids: impl IntoIterator<Item = TrackId>,
) {
    for track in track_ids {
        if !artist.tracks.contains(&track) {
            artist.tracks.push(track);
        }
    }
}