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 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 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 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 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
216impl 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}