selene_core/library/album/
trait_impls.rs1use 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 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 cas_tx.tracks_set_album(Some(merge_into), self.tracks.iter().map(|t| &t.id))?;
121
122 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 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 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 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 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 {}