selene-core 0.5.2

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

use crate::{
    database::{Createable, LibraryDb, Mergeable, Patchable},
    library::{
        album::{Album, AlbumId, TrackReference},
        artist::{ArtistGroup, ArtistId},
        track::TrackId,
    },
};

pub trait CasTxExtensions<CasDb: Database, T: DatabaseEntry<EntryDb = CasDb>> {
    fn tx_patch(&mut self, item: T) -> Result<(), TransactionError>
    where
        T: Patchable<T>;

    fn tx_merge(&mut self, from: &T, into: T::Id) -> Result<(), CustomTransactionError<T::Err>>
    where
        T: Mergeable;

    fn tx_create(&mut self, args: T::CreateArgs) -> Result<T, CustomTransactionError<T::Err>>
    where
        T: Createable;
}

impl<CasDb: Database, T: DatabaseEntry<EntryDb = CasDb>> CasTxExtensions<CasDb, T>
    for CompareAndSwapTransaction<CasDb>
{
    /// Patches `item` with the existing database entry, if any, else inserts
    ///
    /// Patching rules depend on how [`T`] implements [`Patchable<T>`]
    ///
    /// # Warning
    ///
    /// This function will create dangling references if not used correctly
    fn tx_patch(&mut self, item: T) -> Result<(), TransactionError>
    where
        T: Patchable<T>,
    {
        if let Some(mut old_item) = self.tx_get(item.id())? {
            let item_id = item.id();
            old_item.patch(item);
            self.tx_upsert(item_id, Some(old_item))?;
        } else {
            self.tx_upsert(item.id(), Some(item))?;
        }

        Ok(())
    }

    fn tx_merge(&mut self, from: &T, into: T::Id) -> Result<(), CustomTransactionError<T::Err>>
    where
        T: Mergeable,
    {
        T::tx_merge(from, into, self)
    }

    fn tx_create(&mut self, args: T::CreateArgs) -> Result<T, CustomTransactionError<T::Err>>
    where
        T: Createable,
    {
        T::tx_create(self, args)
    }
}

pub trait CasTxLibraryExtensions {
    fn album_set_and_relink_tracks(
        &mut self,
        album_id: AlbumId,
        tracks: &[TrackId],
    ) -> Result<bool, TransactionError>;
    fn album_set_and_relink_artists(
        &mut self,
        album_id: AlbumId,
        artists: &[ArtistId],
    ) -> Result<bool, 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;
        self.tx_upsert(track.id(), Some(track.clone()))?;

        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.id(), Some(old_album))?;
        }

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

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

            self.tx_upsert(new_album.id(), Some(new_album))?;
        }

        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,
        album_id: AlbumId,
        artists: &[ArtistId],
    ) -> Result<bool, TransactionError> {
        let mut album = self.tx_get(album_id)?.ok_or(DatabaseError::MissingEntry)?;

        let old_artists: Vec<ArtistId> = album.artist_group.artist_ids().to_vec();

        album.artist_group = ArtistGroup::from_artist_ids(artists.iter().copied());

        let removed_artists: Vec<ArtistId> = old_artists
            .into_iter()
            .filter(|old_artist| !artists.contains(old_artist))
            .collect();

        self.artists_add_album(album_id, artists)?;
        self.artists_remove_album(album_id, &removed_artists)?;

        self.tx_upsert(album_id, Some(album))?;
        Ok(true)
    }

    /// 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,
        album_id: AlbumId,
        tracks: &[TrackId],
    ) -> Result<bool, TransactionError> {
        let album = self.tx_get(album_id)?.ok_or(DatabaseError::MissingEntry)?;

        let old_tracks: Vec<TrackId> = album.tracks.iter().map(|t| t.id).collect();

        let removed_tracks: Vec<TrackId> = old_tracks
            .iter()
            .filter(|old_track| !tracks.contains(old_track))
            .copied()
            .collect();

        self.album_set_tracks(album, tracks)?;
        self.tracks_set_album(Some(album_id), tracks)?;
        self.tracks_set_album(None, &removed_tracks)?;
        Ok(true)
    }
}

pub(crate) trait CasTxUnsafeLibraryExtensions {
    fn album_set_tracks(
        &mut self,
        album: Album,
        tracks: &[TrackId],
    ) -> Result<(), TransactionError>;

    fn tracks_set_album<'a>(
        &mut self,
        album_id: Option<AlbumId>,
        tracks: impl IntoIterator<Item = &'a TrackId>,
    ) -> Result<(), TransactionError>;

    fn artists_remove_album(
        &mut self,
        album_id: AlbumId,
        artists: &[ArtistId],
    ) -> Result<(), TransactionError>;

    fn artists_add_album(
        &mut self,
        album_id: AlbumId,
        artists: &[ArtistId],
    ) -> Result<(), TransactionError>;

    fn artist_add_tracks(
        &mut self,
        artist_id: ArtistId,
        tracks: &[TrackId],
    ) -> Result<(), TransactionError>;
}

// Unsafe one-way helpers
impl CasTxUnsafeLibraryExtensions for CompareAndSwapTransaction<LibraryDb> {
    fn album_set_tracks(
        &mut self,
        mut album: Album,
        tracks: &[TrackId],
    ) -> Result<(), TransactionError> {
        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();
        self.tx_upsert(album.id(), Some(album))?;
        Ok(())
    }

    fn tracks_set_album<'a>(
        &mut self,
        album_id: Option<AlbumId>,
        tracks: impl IntoIterator<Item = &'a TrackId>,
    ) -> Result<(), TransactionError> {
        for track_id in tracks {
            let Some(mut track) = self.tx_get(*track_id)? else {
                return Err(DatabaseError::MissingEntry.into());
            };

            track.metadata.album = album_id;

            for artist_id in track.metadata.artists.artist_ids() {
                if let Some(album_id) = album_id {
                    let Some(mut artist) = self.tx_get(*artist_id)? else {
                        return Err(DatabaseError::MissingEntry.into());
                    };

                    if artist.albums.contains(&album_id) {
                        artist.tracks.retain(|t| t != track_id);
                        self.tx_upsert(*artist_id, Some(artist))?;
                    } else {
                        self.artist_add_tracks(*artist_id, &[*track_id])?;
                    }
                } else {
                    self.artist_add_tracks(*artist_id, &[*track_id])?;
                }
            }

            self.tx_upsert(*track_id, Some(track))?;
        }
        Ok(())
    }

    fn artists_remove_album(
        &mut self,
        album_id: AlbumId,
        artists: &[ArtistId],
    ) -> Result<(), TransactionError> {
        for artist_id in artists {
            let Some(mut artist) = self.tx_get(*artist_id)? else {
                return Err(DatabaseError::MissingEntry.into());
            };

            artist.albums.retain(|a| *a != album_id);

            self.tx_upsert(*artist_id, Some(artist))?;
        }

        Ok(())
    }

    fn artists_add_album(
        &mut self,
        album_id: AlbumId,
        artists: &[ArtistId],
    ) -> Result<(), TransactionError> {
        for artist_id in artists {
            let Some(mut artist) = self.tx_get(*artist_id)? else {
                return Err(DatabaseError::MissingEntry.into());
            };

            if !artist.albums.contains(&album_id) {
                artist.albums.push(album_id);
            }
            self.tx_upsert(*artist_id, Some(artist))?;
        }
        Ok(())
    }

    fn artist_add_tracks(
        &mut self,
        artist_id: ArtistId,
        tracks: &[TrackId],
    ) -> Result<(), TransactionError> {
        let Some(mut artist) = self.tx_get(artist_id)? else {
            return Err(TransactionError::Database(DatabaseError::MissingEntry));
        };

        for track_id in tracks {
            let Some(track) = self.tx_get(*track_id)? else {
                return Err(TransactionError::Database(DatabaseError::MissingEntry));
            };

            if let Some(album_id) = track.metadata.album
                && artist.albums.contains(&album_id)
            {
                continue;
            }

            if !artist.tracks.contains(track_id) {
                artist.tracks.push(*track_id);
            }
        }

        self.tx_upsert(artist_id, Some(artist))?;

        Ok(())
    }
}