Skip to main content

selene_core/database/
tx_extensions.rs

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