selene_core/library/album/
trait_impls.rs1use 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 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 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 tracks_set_album(&mut tracks, Some(merge_into));
115 artists_add_album(&mut artists, merge_into);
116
117 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 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 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 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 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 {}