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, TransactionError,
6        caching::Cacheable,
7    },
8    formatter::Taggable,
9    paths::sys::sanitize_str,
10};
11
12use crate::{
13    database::{
14        ALBUM_CACHE, Createable, LibraryDb, Mergeable, artists_add_album, artists_remove_album,
15        patch_option_replace, patch_vec, tracks_set_album,
16    },
17    library::{
18        album::{Album, AlbumId, TrackReference},
19        artist::Artist,
20        track::{Track, TrackId},
21    },
22};
23
24impl DatabaseEntry for Album {
25    const VERSION_NUMBER: u32 = 1;
26    const TREE_NAME: &str = "album";
27    type Id = AlbumId;
28    type Db = LibraryDb;
29
30    fn id(&self) -> Self::Id {
31        self.id
32    }
33
34    fn pre_upsert(
35        &mut self,
36        _cas_tx: &CompareAndSwapTransaction<Self::Db>,
37    ) -> Result<(), TransactionError> {
38        self.tracks.sort_by(|a, b| {
39            let a_disc = a.disc_num.unwrap_or(u32::MAX);
40            let a_track = a.track_num.unwrap_or(u32::MAX);
41            let b_disc = b.disc_num.unwrap_or(u32::MAX);
42            let b_track = b.track_num.unwrap_or(u32::MAX);
43
44            a_disc.cmp(&b_disc).then(a_track.cmp(&b_track))
45        });
46
47        Ok(())
48    }
49}
50
51impl Cacheable for Album {
52    fn cache() -> &'static std::sync::Mutex<lunar_lib::database::caching::DbCache<Self>> {
53        &ALBUM_CACHE
54    }
55}
56
57impl Taggable for Album {
58    type Err = Infallible;
59
60    fn fill_table(&self, table: &mut lunar_lib::formatter::FormatTable) -> Result<(), Self::Err> {
61        if let Some(value) = self.disc_total {
62            table.add_entry("disc_total", value.to_string());
63        }
64        if !self.genre.is_empty() {
65            table.add_entry("album_genre", sanitize_str(self.genre.join(";")));
66        }
67
68        table.add_entry("album", &self.name);
69
70        if let Some(value) = self.track_total {
71            table.add_entry("track_total", value.to_string());
72        }
73        if let Some(value) = self.date {
74            table.add_entry("album_year", value.to_string());
75        }
76
77        Ok(())
78    }
79}
80
81impl Mergeable for Album {
82    type Err = Infallible;
83
84    fn tx_merge(
85        self,
86        merge_into: Self::Id,
87        cas_tx: &mut CompareAndSwapTransaction<Self::Db>,
88    ) -> Result<Self, CustomTransactionError<Self::Err>> {
89        let self_id = self.id();
90
91        // Get the album we are merging into OR upsert if missing and merging into self
92        let Some(mut album) = cas_tx.tx_get(merge_into)? else {
93            if self_id == merge_into {
94                cas_tx.tx_upsert(self.clone())?;
95                return Ok(self);
96            }
97            return Err(TransactionError::MissingEntry.into());
98        };
99
100        // Patch the album with the data from self
101        album.name = self.name.clone();
102        patch_option_replace(&mut album.art, self.art);
103        patch_option_replace(&mut album.disc_total, self.disc_total);
104        patch_option_replace(&mut album.track_total, self.track_total);
105        patch_vec(&mut album.genre, self.genre);
106        patch_vec(&mut album.tracks, self.tracks);
107        patch_vec(&mut album.artists, self.artists);
108        patch_option_replace(&mut album.date, self.date);
109
110        let mut tracks: Vec<Track> = cas_tx.tx_get_batch(album.tracks.iter().map(|t| t.id))?;
111        let mut artists: Vec<Artist> = cas_tx.tx_get_batch(&album.artists)?;
112
113        // Relink tracks && Artists
114        tracks_set_album(&mut tracks, Some(merge_into));
115        artists_add_album(&mut artists, merge_into);
116
117        // Upsert album
118        cas_tx.tx_upsert(album.clone())?;
119        if self_id != merge_into {
120            artists_remove_album(&mut artists, merge_into);
121            cas_tx.tx_remove(self_id)?;
122        }
123
124        // Upsert tracks and artists
125        for track in tracks {
126            cas_tx.tx_upsert(track)?;
127        }
128        for artist in artists {
129            cas_tx.tx_upsert(artist)?;
130        }
131
132        Ok(album)
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    Transaction(#[from] TransactionError),
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::Db>,
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 = args.artists.iter().map(Artist::id).collect();
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::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)?;
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 {}