use std::convert::Infallible;
use lunar_lib::{
database::{
CompareAndSwapTransaction, Createable, CustomTransactionError, DatabaseEntry, Mergeable,
TransactionError, caching::Cacheable,
},
formatter::Taggable,
paths::sys::sanitize_str,
vec_ext::VecExtensions,
};
use crate::{
database::{ALBUM_CACHE, LibraryDb, artists_add_album, artists_remove_album, tracks_set_album},
library::{
album::{Album, AlbumId, TrackReference},
artist::Artist,
track::{Track, TrackId},
},
};
impl DatabaseEntry for Album {
const VERSION_NUMBER: u32 = 1;
const TREE_NAME: &str = "album";
type Id = AlbumId;
type Db = LibraryDb;
fn id(&self) -> Self::Id {
self.id
}
fn pre_upsert(
&mut self,
_cas_tx: &CompareAndSwapTransaction<Self::Db>,
) -> 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 Cacheable for Album {
fn cache() -> &'static std::sync::Mutex<lunar_lib::database::caching::DbCache<Self>> {
&ALBUM_CACHE
}
}
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 {
fn tx_merge(
mut self,
from: Self,
cas_tx: &mut CompareAndSwapTransaction<Self::Db>,
) -> Result<Self, TransactionError> {
let self_id = self.id();
let from_id = from.id();
self.name = from.name.clone();
self.art = self.art.or(from.art);
self.disc_total = self.disc_total.max(from.disc_total);
self.track_total = self.track_total.max(from.track_total);
self.genre.extend_unique(from.genre);
self.tracks.extend_unique(from.tracks);
self.artists.extend_unique(from.artists);
self.date = self.date.or(from.date);
let mut tracks: Vec<Track> = cas_tx.tx_get_batch(self.tracks.iter().map(|t| t.id))?;
let mut artists: Vec<Artist> = cas_tx.tx_get_batch(self.artists.iter().copied())?;
tracks_set_album(&mut tracks, Some(self_id));
artists_add_album(&mut artists, self_id);
if self_id != from_id {
artists_remove_album(&mut artists, from_id);
cas_tx.tx_remove(from_id)?;
}
cas_tx.tx_upsert(self.clone())?;
for track in tracks {
cas_tx.tx_upsert(track)?;
}
for artist in artists {
cas_tx.tx_upsert(artist)?;
}
Ok(self)
}
}
#[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}")]
Transaction(#[from] TransactionError),
}
impl Createable for Album {
type CreateArgs = AlbumCreateArgs;
type Err = Infallible;
fn tx_create(
cas_tx: &mut CompareAndSwapTransaction<Self::Db>,
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 = args.artists.iter().map(Artist::id).collect();
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::AlreadyInDatabase.into());
}
args.tracks
.into_iter()
.try_for_each(|mut track| -> Result<(), TransactionError> {
track.metadata.album = Some(album.id());
cas_tx.tx_upsert(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 {}