use std::convert::Infallible;
use lunar_lib::{
database::{
CompareAndSwapTransaction, CustomTransactionError, DatabaseEntry, DatabaseError,
TransactionError, Tree,
},
formatter::Taggable,
paths::sys::sanitize_str,
};
use crate::{
database::{
Createable, LibraryDb, Mergeable, Patchable, album_tree, patch_option_replace, patch_vec,
patch_vec_merge, tx_extensions::CasTxUnsafeLibraryExtensions,
},
library::{
album::{Album, AlbumId, TrackReference},
artist::{Artist, ArtistGroup},
track::{Track, TrackId},
},
};
impl DatabaseEntry for Album {
const VERSION_NUMBER: u32 = 1;
type Id = AlbumId;
type EntryDb = LibraryDb;
fn tree(db: &Self::EntryDb) -> Tree {
album_tree(db)
}
fn id(&self) -> Self::Id {
self.id
}
fn pre_upsert(
&mut self,
_cas_tx: &CompareAndSwapTransaction<Self::EntryDb>,
) -> Result<(), TransactionError> {
self.tracks.sort_by(|a, b| {
let a_disc = a.disc_num.unwrap_or(u32::MAX);
let a_track = a.track_num.unwrap_or(u32::MAX);
let b_disc = b.disc_num.unwrap_or(u32::MAX);
let b_track = b.track_num.unwrap_or(u32::MAX);
a_disc.cmp(&b_disc).then(a_track.cmp(&b_track))
});
Ok(())
}
}
impl Patchable<Self> for Album {
fn patch(&mut self, patch: Album) {
let Album {
id: _,
name,
cover_art,
tracks,
artist_group,
disc_total,
genre,
track_total,
date,
version: _,
} = patch;
self.name = name;
patch_option_replace(&mut self.cover_art, cover_art);
self.artist_group.patch(artist_group);
patch_vec_merge(&mut self.tracks, tracks);
patch_option_replace(&mut self.disc_total, disc_total);
patch_option_replace(&mut self.track_total, track_total);
patch_vec(&mut self.genre, genre);
patch_option_replace(&mut self.date, date);
}
}
impl Taggable for Album {
type Err = Infallible;
fn fill_table(&self, table: &mut lunar_lib::formatter::FormatTable) -> Result<(), Self::Err> {
if let Some(value) = self.disc_total {
table.add_entry("disc_total", value.to_string());
}
if !self.genre.is_empty() {
table.add_entry("album_genre", sanitize_str(self.genre.join(";")));
}
table.add_entry("album", &self.name);
if let Some(value) = self.track_total {
table.add_entry("track_total", value.to_string());
}
if let Some(value) = self.date {
table.add_entry("album_year", value.to_string());
}
Ok(())
}
}
impl Mergeable for Album {
type Err = Infallible;
fn tx_merge(
&self,
merge_into: Self::Id,
cas_tx: &mut CompareAndSwapTransaction<Self::EntryDb>,
) -> Result<(), CustomTransactionError<Self::Err>> {
let mut album = cas_tx
.tx_get(merge_into)?
.expect("Album was deleted in this transaction");
album.patch(self.clone());
cas_tx.tracks_set_album(Some(merge_into), self.tracks.iter().map(|t| &t.id))?;
cas_tx.artists_remove_album(self.id(), self.artist_group.artist_ids())?;
cas_tx.artists_add_album(merge_into, self.artist_group.artist_ids())?;
cas_tx.tx_upsert(album.id(), Some(album))?;
if self.id() != merge_into {
cas_tx.tx_remove(self.id())?;
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct AlbumCreateArgs {
pub name: String,
pub artists: Vec<Artist>,
pub tracks: Vec<Track>,
}
impl AlbumCreateArgs {
#[must_use]
pub fn new(name: String, artists: Vec<Artist>, tracks: Vec<Track>) -> Self {
Self {
name,
artists,
tracks,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum AlbumCreationError {
#[error("A database already exists with the same identifier: {0}")]
AlreadyExists(String),
#[error("{0}")]
Database(#[from] DatabaseError),
}
impl Createable for Album {
type CreateArgs = AlbumCreateArgs;
type Err = Infallible;
fn tx_create(
cas_tx: &mut CompareAndSwapTransaction<Self::EntryDb>,
mut args: Self::CreateArgs,
) -> Result<Self, CustomTransactionError<Self::Err>> {
let track_ids: Vec<TrackId> = args.tracks.iter().map(Track::id).collect();
for artist in &mut args.artists {
artist.tracks.retain(|t| !track_ids.contains(t));
}
let artists = ArtistGroup::from_artists(&args.artists);
let tracks = (1u32..)
.zip(args.tracks.iter())
.map(|(i, t)| TrackReference {
id: t.id(),
track_num: Some(i),
disc_num: None,
});
let album = Album::new(args.name, artists, tracks.collect());
if cas_tx.tx_check(album.id())? {
return Err(TransactionError::Database(DatabaseError::AlreadyInDatabase).into());
}
args.tracks
.into_iter()
.try_for_each(|mut track| -> Result<(), TransactionError> {
track.metadata.album = Some(album.id());
cas_tx.tx_upsert(track.id(), Some(track))?;
Ok(())
})?;
cas_tx.tx_insert(album.clone())?;
Ok(album)
}
}
impl PartialEq for Album {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for Album {}