Skip to main content

selene_core/library/album/
trait_impls.rs

1use std::convert::Infallible;
2
3use lunar_lib::{
4    database::{
5        CompareAndSwapTransaction, CustomTransactionError, DatabaseEntry, DatabaseError,
6        TransactionError, Tree,
7    },
8    formatter::Taggable,
9    paths::sys::sanitize_str,
10};
11
12use crate::{
13    database::{
14        Createable, LibraryDb, Mergeable, Patchable, album_tree, patch_option_replace, patch_vec,
15        patch_vec_merge, tx_extensions::CasTxUnsafeLibraryExtensions,
16    },
17    library::{
18        album::{Album, AlbumId, TrackReference},
19        artist::{Artist, ArtistGroup},
20        track::{Track, TrackId},
21    },
22};
23
24impl DatabaseEntry for Album {
25    const VERSION_NUMBER: u32 = 1;
26    type Id = AlbumId;
27    type EntryDb = LibraryDb;
28
29    fn tree(db: &Self::EntryDb) -> Tree {
30        album_tree(db)
31    }
32
33    fn id(&self) -> Self::Id {
34        self.id
35    }
36
37    fn pre_upsert(
38        &mut self,
39        _cas_tx: &CompareAndSwapTransaction<Self::EntryDb>,
40    ) -> Result<(), TransactionError> {
41        self.tracks.sort_by(|a, b| {
42            let a_disc = a.disc_num.unwrap_or(u32::MAX);
43            let a_track = a.track_num.unwrap_or(u32::MAX);
44            let b_disc = b.disc_num.unwrap_or(u32::MAX);
45            let b_track = b.track_num.unwrap_or(u32::MAX);
46
47            a_disc.cmp(&b_disc).then(a_track.cmp(&b_track))
48        });
49
50        Ok(())
51    }
52}
53
54impl Patchable<Self> for Album {
55    fn patch(&mut self, patch: Album) {
56        let Album {
57            id: _,
58            name,
59            cover_art,
60            tracks,
61            artist_group,
62            disc_total,
63            genre,
64            track_total,
65            date,
66            version: _,
67        } = patch;
68
69        self.name = name;
70        patch_option_replace(&mut self.cover_art, cover_art);
71        self.artist_group.patch(artist_group);
72        patch_vec_merge(&mut self.tracks, tracks);
73        patch_option_replace(&mut self.disc_total, disc_total);
74        patch_option_replace(&mut self.track_total, track_total);
75        patch_vec(&mut self.genre, genre);
76        patch_option_replace(&mut self.date, date);
77    }
78}
79
80impl Taggable for Album {
81    type Err = Infallible;
82
83    fn fill_table(&self, table: &mut lunar_lib::formatter::FormatTable) -> Result<(), Self::Err> {
84        if let Some(value) = self.disc_total {
85            table.add_entry("disc_total", value.to_string());
86        }
87        if !self.genre.is_empty() {
88            table.add_entry("album_genre", sanitize_str(self.genre.join(";")));
89        }
90
91        table.add_entry("album", &self.name);
92
93        if let Some(value) = self.track_total {
94            table.add_entry("track_total", value.to_string());
95        }
96        if let Some(value) = self.date {
97            table.add_entry("album_year", value.to_string());
98        }
99
100        Ok(())
101    }
102}
103
104impl Mergeable for Album {
105    type Err = Infallible;
106
107    fn tx_merge(
108        &self,
109        merge_into: Self::Id,
110        cas_tx: &mut CompareAndSwapTransaction<Self::EntryDb>,
111    ) -> Result<(), CustomTransactionError<Self::Err>> {
112        // Get and patch album
113        let mut album = cas_tx
114            .tx_get(merge_into)?
115            .expect("Album was deleted in this transaction");
116
117        album.patch(self.clone());
118
119        // Relink tracks
120        cas_tx.tracks_set_album(Some(merge_into), self.tracks.iter().map(|t| &t.id))?;
121
122        // Relink artists
123        cas_tx.artists_remove_album(self.id(), self.artist_group.artist_ids())?;
124        cas_tx.artists_add_album(merge_into, self.artist_group.artist_ids())?;
125
126        // Upsert album
127        cas_tx.tx_upsert(album.id(), Some(album))?;
128        if self.id() != merge_into {
129            cas_tx.tx_remove(self.id())?;
130        }
131
132        Ok(())
133    }
134}
135
136#[derive(Debug, Clone)]
137pub struct AlbumCreateArgs {
138    pub name: String,
139    pub artists: Vec<Artist>,
140    pub tracks: Vec<Track>,
141}
142
143impl AlbumCreateArgs {
144    #[must_use]
145    pub fn new(name: String, artists: Vec<Artist>, tracks: Vec<Track>) -> Self {
146        Self {
147            name,
148            artists,
149            tracks,
150        }
151    }
152}
153
154#[derive(Debug, thiserror::Error)]
155pub enum AlbumCreationError {
156    #[error("A database already exists with the same identifier: {0}")]
157    AlreadyExists(String),
158
159    #[error("{0}")]
160    Database(#[from] DatabaseError),
161}
162
163impl Createable for Album {
164    type CreateArgs = AlbumCreateArgs;
165    type Err = Infallible;
166
167    fn tx_create(
168        cas_tx: &mut CompareAndSwapTransaction<Self::EntryDb>,
169        mut args: Self::CreateArgs,
170    ) -> Result<Self, CustomTransactionError<Self::Err>> {
171        let track_ids: Vec<TrackId> = args.tracks.iter().map(Track::id).collect();
172        for artist in &mut args.artists {
173            artist.tracks.retain(|t| !track_ids.contains(t));
174        }
175
176        let artists = ArtistGroup::from_artists(&args.artists);
177
178        let tracks = (1u32..)
179            .zip(args.tracks.iter())
180            .map(|(i, t)| TrackReference {
181                id: t.id(),
182                track_num: Some(i),
183                disc_num: None,
184            });
185
186        // Create album
187        let album = Album::new(args.name, artists, tracks.collect());
188        if cas_tx.tx_check(album.id())? {
189            return Err(TransactionError::Database(DatabaseError::AlreadyInDatabase).into());
190        }
191
192        // Relink tracks
193        args.tracks
194            .into_iter()
195            .try_for_each(|mut track| -> Result<(), TransactionError> {
196                track.metadata.album = Some(album.id());
197
198                cas_tx.tx_upsert(track.id(), Some(track))?;
199
200                Ok(())
201            })?;
202
203        // Upsert album
204        cas_tx.tx_insert(album.clone())?;
205
206        Ok(album)
207    }
208}
209
210impl PartialEq for Album {
211    fn eq(&self, other: &Self) -> bool {
212        self.id == other.id
213    }
214}
215
216impl Eq for Album {}