use lunar_lib::{
database::{
CompareAndSwapTransaction, Createable, CustomTransactionError, DatabaseEntry, DbIdExt,
DbIdIterExt, Entry, Mergeable, TransactionError, caching::Cacheable,
},
formatter::Taggable,
id::Id,
iterator_ext::IteratorExtensions,
log::warn,
paths::sys::sanitize_str,
vec_ext::VecExtensions,
};
use crate::{
SeleneIdExt,
database::{Library, Searchable, track_set_album},
library::{
album::{Album, TrackReference},
artist::Artist,
track::Track,
},
};
impl DatabaseEntry for Album {
const VERSION_NUMBER: u32 = 1;
const TREE_NAME: &str = "album";
type DbInner = Library;
}
impl Cacheable for Album {}
impl Searchable for Album {
const SEARCH_INDEX: &'static str = "album_search";
fn search_name(&self) -> Option<&str> {
Some(&*self.title)
}
}
impl Taggable for Album {
type Err = std::convert::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.title);
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 merge_data(&mut self, from: Self) {
self.title = from.title;
self.art = self.art.take().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);
}
fn relink_references(
from: &Entry<Self>,
to: Id<Self>,
cas_tx: &mut CompareAndSwapTransaction<Self::DbInner>,
) -> Result<(), TransactionError> {
from.tracks
.iter()
.map(|t| Id::from(*t.id))
.tx_fetch_and_update(
|old| {
let Some(mut track) = old else {
warn!(
"Album '{}' attempted to relink to a track which does not exist yet",
from.name()
);
return None;
};
track_set_album(&mut track, Some(to));
Some(track)
},
cas_tx,
)?;
from.artists.iter().tx_fetch_and_update(
|old| {
let Some(mut artist) = old else {
warn!(
"Album '{}' attempted to relink to a artist which does not exist yet",
from.name()
);
return None;
};
artist.albums.push_unique(to);
Some(artist)
},
cas_tx,
)?;
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct AlbumCreateArgs {
pub name: String,
pub artists: Vec<Id<Artist>>,
pub tracks: Vec<Id<Track>>,
}
impl AlbumCreateArgs {
#[must_use]
pub fn new(name: String, artists: Vec<Id<Artist>>, tracks: Vec<Id<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 = std::convert::Infallible;
fn create(
args: Self::CreateArgs,
cas_tx: &mut CompareAndSwapTransaction<Self::DbInner>,
) -> Result<Entry<Self>, CustomTransactionError<Self::Err>> {
let id: Id<Album> = Id::from_string_hash(&args.name);
if id.tx_check(cas_tx)? {
return Err(TransactionError::AlreadyInDatabase.into());
}
args.tracks.iter().tx_fetch_and_update(
|old| {
let mut track = old.expect("Invalid ref");
track_set_album(&mut track, Some(id));
Some(track)
},
cas_tx,
)?;
args.artists.iter().tx_fetch_and_update(
|old| {
let mut artist = old.expect("Invalid ref");
artist.albums.push_unique(id);
Some(artist)
},
cas_tx,
)?;
let tracks = (1u32..)
.zip(args.tracks)
.map(|(i, id)| TrackReference {
id: id,
track_num: Some(i),
disc_num: None,
})
.to_vec();
let album = Album::new(args.name, args.artists, tracks).to_entry(id);
Ok(album)
}
}