Skip to main content

selene_core/database/
tx_extensions.rs

1use lunar_lib::database::{
2    CompareAndSwapTransaction, CustomTransactionError, Database, DatabaseEntry, DatabaseError,
3    TransactionError,
4};
5
6use crate::{
7    database::{Createable, LibraryDb, Mergeable, Patchable},
8    library::{
9        album::{Album, AlbumId, TrackReference},
10        artist::{ArtistGroup, ArtistId},
11        track::TrackId,
12    },
13};
14
15pub trait CasTxExtensions<CasDb: Database, T: DatabaseEntry<EntryDb = CasDb>> {
16    fn tx_patch(&mut self, item: T) -> Result<(), TransactionError>
17    where
18        T: Patchable<T>;
19
20    fn tx_merge(&mut self, from: &T, into: T::Id) -> Result<(), CustomTransactionError<T::Err>>
21    where
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<EntryDb = CasDb>> CasTxExtensions<CasDb, T>
30    for CompareAndSwapTransaction<CasDb>
31{
32    /// Patches `item` with the existing database entry, if any, else inserts
33    ///
34    /// Patching rules depend on how [`T`] implements [`Patchable<T>`]
35    ///
36    /// # Warning
37    ///
38    /// This function will create dangling references if not used correctly
39    fn tx_patch(&mut self, item: T) -> Result<(), TransactionError>
40    where
41        T: Patchable<T>,
42    {
43        if let Some(mut old_item) = self.tx_get(item.id())? {
44            let item_id = item.id();
45            old_item.patch(item);
46            self.tx_upsert(item_id, Some(old_item))?;
47        } else {
48            self.tx_upsert(item.id(), Some(item))?;
49        }
50
51        Ok(())
52    }
53
54    fn tx_merge(&mut self, from: &T, into: T::Id) -> Result<(), CustomTransactionError<T::Err>>
55    where
56        T: Mergeable,
57    {
58        T::tx_merge(from, into, self)
59    }
60
61    fn tx_create(&mut self, args: T::CreateArgs) -> Result<T, CustomTransactionError<T::Err>>
62    where
63        T: Createable,
64    {
65        T::tx_create(self, args)
66    }
67}
68
69pub trait CasTxLibraryExtensions {
70    fn album_set_and_relink_tracks(
71        &mut self,
72        album_id: AlbumId,
73        tracks: &[TrackId],
74    ) -> Result<bool, TransactionError>;
75    fn album_set_and_relink_artists(
76        &mut self,
77        album_id: AlbumId,
78        artists: &[ArtistId],
79    ) -> Result<bool, TransactionError>;
80    fn relink_track_to_album(
81        &mut self,
82        track_id: TrackId,
83        album: Option<AlbumId>,
84    ) -> Result<bool, TransactionError>;
85}
86
87impl CasTxLibraryExtensions for CompareAndSwapTransaction<LibraryDb> {
88    /// Applies a two-way relinking operation, relinking to the track to a new (or no) album, and disconnecting the tracks old album, if any
89    fn relink_track_to_album(
90        &mut self,
91        track_id: TrackId,
92        album: Option<AlbumId>,
93    ) -> Result<bool, TransactionError> {
94        let Some(mut track) = self.tx_get(track_id)? else {
95            return Ok(false);
96        };
97
98        if track.metadata.album == album {
99            return Ok(false);
100        }
101
102        let old_album_id = track.metadata.album;
103
104        track.metadata.album = album;
105        self.tx_upsert(track.id(), Some(track.clone()))?;
106
107        if let Some(old_album_id) = old_album_id {
108            let mut old_album = self.tx_get(old_album_id)?.unwrap_or_else(|| {
109                panic!(
110                    "Track '{}' contains a reference to an album that doesnt exist",
111                    track.metadata.safe_title()
112                )
113            });
114
115            old_album.tracks.retain(|t| t.id != track_id);
116
117            self.tx_upsert(old_album.id(), Some(old_album))?;
118        }
119
120        if let Some(new_album_id) = album {
121            let mut new_album = self
122                .tx_get(new_album_id)?
123                .ok_or(DatabaseError::MissingEntry)?;
124
125            new_album.tracks.push(TrackReference {
126                id: track_id,
127                track_num: None,
128                disc_num: None,
129            });
130
131            self.tx_upsert(new_album.id(), Some(new_album))?;
132        }
133
134        Ok(true)
135    }
136
137    /// Applies a two-way relinking operation, relinking artists to the album, and disconnecting references from removed artists
138    fn album_set_and_relink_artists(
139        &mut self,
140        album_id: AlbumId,
141        artists: &[ArtistId],
142    ) -> Result<bool, TransactionError> {
143        let mut album = self.tx_get(album_id)?.ok_or(DatabaseError::MissingEntry)?;
144
145        let old_artists: Vec<ArtistId> = album.artist_group.artist_ids().to_vec();
146
147        album.artist_group = ArtistGroup::from_artist_ids(artists.iter().copied());
148
149        let removed_artists: Vec<ArtistId> = old_artists
150            .into_iter()
151            .filter(|old_artist| !artists.contains(old_artist))
152            .collect();
153
154        self.artists_add_album(album_id, artists)?;
155        self.artists_remove_album(album_id, &removed_artists)?;
156
157        self.tx_upsert(album_id, Some(album))?;
158        Ok(true)
159    }
160
161    /// Applies a two-way relinking operation, relinking tracks to the album, and disconnecting references from removed tracks
162    fn album_set_and_relink_tracks(
163        &mut self,
164        album_id: AlbumId,
165        tracks: &[TrackId],
166    ) -> Result<bool, TransactionError> {
167        let album = self.tx_get(album_id)?.ok_or(DatabaseError::MissingEntry)?;
168
169        let old_tracks: Vec<TrackId> = album.tracks.iter().map(|t| t.id).collect();
170
171        let removed_tracks: Vec<TrackId> = old_tracks
172            .iter()
173            .filter(|old_track| !tracks.contains(old_track))
174            .copied()
175            .collect();
176
177        self.album_set_tracks(album, tracks)?;
178        self.tracks_set_album(Some(album_id), tracks)?;
179        self.tracks_set_album(None, &removed_tracks)?;
180        Ok(true)
181    }
182}
183
184pub(crate) trait CasTxUnsafeLibraryExtensions {
185    fn album_set_tracks(
186        &mut self,
187        album: Album,
188        tracks: &[TrackId],
189    ) -> Result<(), TransactionError>;
190
191    fn tracks_set_album<'a>(
192        &mut self,
193        album_id: Option<AlbumId>,
194        tracks: impl IntoIterator<Item = &'a TrackId>,
195    ) -> Result<(), TransactionError>;
196
197    fn artists_remove_album(
198        &mut self,
199        album_id: AlbumId,
200        artists: &[ArtistId],
201    ) -> Result<(), TransactionError>;
202
203    fn artists_add_album(
204        &mut self,
205        album_id: AlbumId,
206        artists: &[ArtistId],
207    ) -> Result<(), TransactionError>;
208
209    fn artist_add_tracks(
210        &mut self,
211        artist_id: ArtistId,
212        tracks: &[TrackId],
213    ) -> Result<(), TransactionError>;
214}
215
216// Unsafe one-way helpers
217impl CasTxUnsafeLibraryExtensions for CompareAndSwapTransaction<LibraryDb> {
218    fn album_set_tracks(
219        &mut self,
220        mut album: Album,
221        tracks: &[TrackId],
222    ) -> Result<(), TransactionError> {
223        album.tracks = tracks
224            .iter()
225            .map(|t| {
226                album
227                    .tracks
228                    .iter()
229                    .find(|old| old.id == *t)
230                    .copied()
231                    .unwrap_or(TrackReference {
232                        id: *t,
233                        track_num: None,
234                        disc_num: None,
235                    })
236            })
237            .collect();
238        self.tx_upsert(album.id(), Some(album))?;
239        Ok(())
240    }
241
242    fn tracks_set_album<'a>(
243        &mut self,
244        album_id: Option<AlbumId>,
245        tracks: impl IntoIterator<Item = &'a TrackId>,
246    ) -> Result<(), TransactionError> {
247        for track_id in tracks {
248            let Some(mut track) = self.tx_get(*track_id)? else {
249                return Err(DatabaseError::MissingEntry.into());
250            };
251
252            track.metadata.album = album_id;
253
254            for artist_id in track.metadata.artists.artist_ids() {
255                if let Some(album_id) = album_id {
256                    let Some(mut artist) = self.tx_get(*artist_id)? else {
257                        return Err(DatabaseError::MissingEntry.into());
258                    };
259
260                    if artist.albums.contains(&album_id) {
261                        artist.tracks.retain(|t| t != track_id);
262                        self.tx_upsert(*artist_id, Some(artist))?;
263                    } else {
264                        self.artist_add_tracks(*artist_id, &[*track_id])?;
265                    }
266                } else {
267                    self.artist_add_tracks(*artist_id, &[*track_id])?;
268                }
269            }
270
271            self.tx_upsert(*track_id, Some(track))?;
272        }
273        Ok(())
274    }
275
276    fn artists_remove_album(
277        &mut self,
278        album_id: AlbumId,
279        artists: &[ArtistId],
280    ) -> Result<(), TransactionError> {
281        for artist_id in artists {
282            let Some(mut artist) = self.tx_get(*artist_id)? else {
283                return Err(DatabaseError::MissingEntry.into());
284            };
285
286            artist.albums.retain(|a| *a != album_id);
287
288            self.tx_upsert(*artist_id, Some(artist))?;
289        }
290
291        Ok(())
292    }
293
294    fn artists_add_album(
295        &mut self,
296        album_id: AlbumId,
297        artists: &[ArtistId],
298    ) -> Result<(), TransactionError> {
299        for artist_id in artists {
300            let Some(mut artist) = self.tx_get(*artist_id)? else {
301                return Err(DatabaseError::MissingEntry.into());
302            };
303
304            if !artist.albums.contains(&album_id) {
305                artist.albums.push(album_id);
306            }
307            self.tx_upsert(*artist_id, Some(artist))?;
308        }
309        Ok(())
310    }
311
312    fn artist_add_tracks(
313        &mut self,
314        artist_id: ArtistId,
315        tracks: &[TrackId],
316    ) -> Result<(), TransactionError> {
317        let Some(mut artist) = self.tx_get(artist_id)? else {
318            return Err(TransactionError::Database(DatabaseError::MissingEntry));
319        };
320
321        for track_id in tracks {
322            let Some(track) = self.tx_get(*track_id)? else {
323                return Err(TransactionError::Database(DatabaseError::MissingEntry));
324            };
325
326            if let Some(album_id) = track.metadata.album
327                && artist.albums.contains(&album_id)
328            {
329                continue;
330            }
331
332            if !artist.tracks.contains(track_id) {
333                artist.tracks.push(*track_id);
334            }
335        }
336
337        self.tx_upsert(artist_id, Some(artist))?;
338
339        Ok(())
340    }
341}