Skip to main content

selene_core/database/
tx_extensions.rs

1use lunar_lib::{
2    database::{
3        CompareAndSwapTransaction, CustomTransactionError, Database, DatabaseEntry,
4        TransactionError,
5    },
6    iterator_ext::IteratorExtensions,
7};
8
9use crate::{
10    database::{Createable, LibraryDb, Mergeable},
11    library::{
12        album::{Album, AlbumId, TrackReference},
13        artist::{Artist, ArtistGroup, ArtistId},
14        track::{Track, TrackId},
15    },
16};
17
18pub trait CasTxExtensions<CasDb: Database, T: DatabaseEntry<Db = CasDb>> {
19    fn tx_merge(&mut self, from: T, into: T::Id) -> Result<T, CustomTransactionError<T::Err>>
20    where
21        Self: Sized,
22        T: Mergeable;
23
24    fn tx_create(&mut self, args: T::CreateArgs) -> Result<T, CustomTransactionError<T::Err>>
25    where
26        T: Createable;
27}
28
29impl<CasDb: Database, T: DatabaseEntry<Db = CasDb>> CasTxExtensions<CasDb, T>
30    for CompareAndSwapTransaction<CasDb>
31{
32    fn tx_merge(&mut self, from: T, into: T::Id) -> Result<T, CustomTransactionError<T::Err>>
33    where
34        Self: Sized,
35        T: Mergeable,
36    {
37        T::tx_merge(from, into, self)
38    }
39
40    fn tx_create(&mut self, args: T::CreateArgs) -> Result<T, CustomTransactionError<T::Err>>
41    where
42        T: Createable,
43    {
44        T::tx_create(self, args)
45    }
46}
47
48pub trait CasTxLibraryExtensions {
49    fn album_set_and_relink_tracks(
50        &mut self,
51        album: Album,
52        tracks: Vec<Track>,
53    ) -> Result<(Album, Vec<Track>), TransactionError>;
54
55    fn album_set_and_relink_artists(
56        &mut self,
57        album_id: Album,
58        artists: impl IntoIterator<Item = Artist>,
59    ) -> Result<(Album, Vec<Artist>), TransactionError>;
60
61    fn relink_track_to_album(
62        &mut self,
63        track_id: TrackId,
64        album: Option<AlbumId>,
65    ) -> Result<bool, TransactionError>;
66}
67
68impl CasTxLibraryExtensions for CompareAndSwapTransaction<LibraryDb> {
69    /// Applies a two-way relinking operation, relinking to the track to a new (or no) album, and disconnecting the tracks old album, if any
70    fn relink_track_to_album(
71        &mut self,
72        track_id: TrackId,
73        album: Option<AlbumId>,
74    ) -> Result<bool, TransactionError> {
75        let Some(mut track) = self.tx_get(track_id)? else {
76            return Ok(false);
77        };
78
79        if track.metadata().album == album {
80            return Ok(false);
81        }
82
83        let old_album_id = track.metadata.album;
84
85        track.metadata.album = album;
86
87        if let Some(old_album_id) = old_album_id {
88            let mut old_album = self.tx_get(old_album_id)?.unwrap_or_else(|| {
89                panic!(
90                    "Track '{}' contains a reference to an album that doesnt exist",
91                    track.metadata.safe_title()
92                )
93            });
94
95            old_album.tracks.retain(|t| t.id != track_id);
96
97            self.tx_upsert(old_album)?;
98        }
99
100        if let Some(new_album_id) = album {
101            let mut new_album = self
102                .tx_get(new_album_id)?
103                .ok_or(TransactionError::MissingEntry)?;
104
105            new_album.tracks.push(TrackReference {
106                id: track_id,
107                track_num: None,
108                disc_num: None,
109            });
110
111            self.tx_upsert(new_album)?;
112        }
113
114        self.tx_upsert(track)?;
115
116        Ok(true)
117    }
118
119    /// Applies a two-way relinking operation, relinking artists to the album, and disconnecting references from removed artists
120    fn album_set_and_relink_artists(
121        &mut self,
122        mut album: Album,
123        artists: impl IntoIterator<Item = Artist>,
124    ) -> Result<(Album, Vec<Artist>), TransactionError> {
125        let mut artists = artists.into_iter().to_vec();
126        let artist_ids = artists.iter().map(Artist::id).to_vec();
127
128        let old_artists = album.artists.artist_ids().to_vec();
129
130        let mut removed_artists = old_artists
131            .iter()
132            .filter_map(|a| -> Option<Result<Artist, TransactionError>> {
133                (!artist_ids.contains(a)).then_some(
134                    self.tx_get(*a)
135                        .transpose()
136                        .ok_or(TransactionError::MissingEntry)
137                        .flatten(),
138                )
139            })
140            .collect::<Result<Vec<Artist>, _>>()?;
141
142        album.artists = artist_ids;
143
144        let album_id = album.id();
145        artists_add_album(&mut artists, album_id);
146        artists_remove_album(&mut removed_artists, album_id);
147
148        self.tx_upsert(album.clone())?;
149        for artist in artists.iter().chain(removed_artists.iter()) {
150            self.tx_upsert(artist.clone())?;
151        }
152
153        Ok((album, artists))
154    }
155
156    /// Applies a two-way relinking operation, relinking tracks to the album, and disconnecting references from removed tracks
157    fn album_set_and_relink_tracks(
158        &mut self,
159        mut album: Album,
160        mut tracks: Vec<Track>,
161    ) -> Result<(Album, Vec<Track>), TransactionError> {
162        let track_ids: Vec<TrackId> = tracks.iter().map(Track::id).collect();
163        let old_tracks: Vec<TrackId> = album.tracks.iter().map(|t| t.id).collect();
164
165        let mut removed_tracks: Vec<Track> = old_tracks
166            .into_iter()
167            .filter(|old_track| !track_ids.contains(old_track))
168            .try_map(|id| {
169                self.tx_get(id)
170                    .transpose()
171                    .ok_or(TransactionError::MissingEntry)
172                    .flatten()
173            })?
174            .collect();
175
176        album_set_tracks(&mut album, &track_ids);
177        tracks_set_album(&mut tracks, Some(album.id()));
178        tracks_set_album(&mut removed_tracks, None);
179
180        Ok((album, tracks))
181    }
182}
183
184// Unsafe one-way helpers
185pub(crate) fn album_set_tracks(album: &mut Album, tracks: &[TrackId]) {
186    album.tracks = tracks
187        .iter()
188        .map(|t| {
189            album
190                .tracks
191                .iter()
192                .find(|old| old.id == *t)
193                .copied()
194                .unwrap_or(TrackReference {
195                    id: *t,
196                    track_num: None,
197                    disc_num: None,
198                })
199        })
200        .collect();
201}
202
203// pub(crate) fn album_add_track(album: &mut Album, track: TrackId) {
204//     album.tracks.push(TrackReference {
205//         id: track,
206//         track_num: None,
207//         disc_num: None,
208//     });
209// }
210
211pub(crate) fn tracks_set_album<'a>(
212    tracks: impl IntoIterator<Item = &'a mut Track>,
213    album_id: Option<AlbumId>,
214) {
215    for track in tracks {
216        track.metadata.album = album_id;
217    }
218}
219
220pub(crate) fn tracks_add_artist<'a>(
221    tracks: impl IntoIterator<Item = &'a mut Track>,
222    artist_id: ArtistId,
223) {
224    for track in tracks {
225        if !track.metadata.artists.contains(&artist_id) {
226            track.metadata.artists.push(artist_id);
227        }
228    }
229}
230
231pub(crate) fn tracks_remove_artist<'a>(
232    tracks: impl IntoIterator<Item = &'a mut Track>,
233    artist_id: ArtistId,
234) {
235    for track in tracks {
236        track.metadata.artists.retain(|a| *a != artist_id);
237    }
238}
239
240pub(crate) fn artists_remove_album<'a>(
241    artists: impl IntoIterator<Item = &'a mut Artist>,
242    album_id: AlbumId,
243) {
244    for artist in artists {
245        artist.albums.retain(|a| *a != album_id);
246    }
247}
248
249pub(crate) fn artists_add_album<'a>(
250    artists: impl IntoIterator<Item = &'a mut Artist>,
251    album_id: AlbumId,
252) {
253    for artist in artists {
254        if !artist.albums.contains(&album_id) {
255            artist.albums.push(album_id);
256        }
257    }
258}
259
260pub(crate) fn artist_add_tracks<'a>(
261    artist: &mut Artist,
262    track_ids: impl IntoIterator<Item = TrackId>,
263) {
264    for track in track_ids {
265        if !artist.tracks.contains(&track) {
266            artist.tracks.push(track);
267        }
268    }
269}