use std::{
borrow::{Borrow, Cow},
ops::Deref,
str::FromStr,
sync::{Arc, LazyLock},
};
use crate::{
database::{LibraryDb, Resolveable},
library::{
album::{Album, AlbumId},
image_art::ImageArt,
track::{Track, TrackId},
},
};
use blake3::{Hash, hash};
use lunar_lib::{
database::{
CompareAndSwapTransaction, DatabaseEntry, EntryId, TransactionError, caching::Cacheable,
},
formatter::FormatTable,
iterator_ext::IteratorExtensions,
paths::sys::sanitize_str,
};
use regex::Regex;
use serde::{Deserialize, Serialize};
pub mod accessors;
pub mod frontend_impls;
pub mod trait_impls;
mod artist_group;
pub use artist_group::*;
pub const UNKNOWN_ARTIST: &str = "UNKNOWN ARTIST";
#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ArtistId(Hash);
impl EntryId for ArtistId {
type Entry = Artist;
type IdDb = LibraryDb;
}
impl Deref for ArtistId {
type Target = [u8; 32];
fn deref(&self) -> &Self::Target {
self.0.as_bytes()
}
}
impl ArtistId {
fn new(name: &str) -> Self {
Self(hash(name.to_ascii_lowercase().as_bytes()))
}
#[must_use]
pub fn to_selene_id(&self) -> String {
format!("artist:{}", self.0)
}
}
impl FromStr for ArtistId {
type Err = <Hash as FromStr>::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(Hash::from_str(s)?))
}
}
impl<T> From<T> for ArtistId
where
T: AsRef<str>,
{
fn from(value: T) -> Self {
Self::new(value.as_ref())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Artist {
id: ArtistId,
name: String,
pub cover_art: Option<ImageArt>,
pub description: Option<String>,
pub(crate) tracks: Vec<TrackId>,
pub(crate) albums: Vec<AlbumId>,
}
impl Artist {
#[must_use]
pub fn new(name: impl AsRef<str>) -> Option<Self> {
let name = name.as_ref().trim();
if name.is_empty() {
return None;
}
Some(Self {
id: ArtistId::new(name),
name: name.to_owned(),
description: None,
cover_art: None,
tracks: Vec::new(),
albums: Vec::new(),
})
}
pub fn set_name(&mut self, name: impl AsRef<str>) -> bool {
let name = name.as_ref().trim();
if name.is_empty() {
return false;
}
name.clone_into(&mut self.name);
true
}
pub fn albums(&self, db: &LibraryDb) -> Result<Vec<Album>, TransactionError> {
Album::db_get_batch(&self.albums, db)
}
pub fn albums_cache(&self, db: &LibraryDb) -> Result<Vec<Arc<Album>>, TransactionError> {
Album::cache_get_batch_from(&self.albums, db)
}
pub fn all_tracks(&self, db: &LibraryDb) -> Result<Vec<Track>, TransactionError> {
Track::db_get_batch(&self.tracks, db)
}
pub fn all_tracks_cache(&self, db: &LibraryDb) -> Result<Vec<Arc<Track>>, TransactionError> {
Track::cache_get_batch_from(&self.tracks, db)
}
fn tx_albums(
&self,
cas_tx: &CompareAndSwapTransaction<LibraryDb>,
) -> Result<Vec<Album>, TransactionError> {
cas_tx.tx_get_batch(&self.albums)
}
#[must_use]
pub fn albums_raw(&self) -> &[AlbumId] {
&self.albums
}
}
pub fn add_from_artists<I, A>(
format_table: &mut FormatTable,
artists: I,
artist_type: &str,
main_sep: &str,
alt_sep: &str,
) where
I: IntoIterator<Item = A>,
A: Borrow<Artist>,
{
let names = artists
.into_iter()
.map(|a| sanitize_str(a.borrow().name()))
.to_vec();
if names.is_empty() {
return;
}
match names.as_slice() {
[] => unreachable!(),
[main] => {
format_table.add_entry(format!("main_{artist_type}_artist"), main);
format_table.add_entry(format!("all_{artist_type}_artists"), main);
}
[main, duo] => {
format_table.add_entry(format!("main_{artist_type}_artist"), main);
format_table.add_entry(
format!("all_{artist_type}_artists"),
format!("{main}{alt_sep}{duo}"),
);
format_table.add_entry(format!("feat_{artist_type}_artists"), duo);
}
[main, many @ .., last] => {
format_table.add_entry(format!("main_{artist_type}_artist"), main);
let many = many.join(main_sep);
format_table.add_entry(
format!("all_{artist_type}_artists"),
format!("{main}{main_sep}{many}{alt_sep}{last}"),
);
format_table.add_entry(
format!("feat_{artist_type}_artists"),
format!("{many}{alt_sep}{last}"),
);
}
}
}
static ARTIST_SPLIT_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?i)\s*(?:,|;|\band\b|\bfeat\.?|\bft\.?|w/)\s*").unwrap());
pub fn artists_from_string(artists: impl AsRef<str>) -> Vec<Artist> {
ARTIST_SPLIT_REGEX
.split(artists.as_ref())
.map(str::trim)
.filter_map(Artist::new)
.collect()
}
static ARTIST_FEATURING_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)\((?:feat|ft|featuring|with)(?:\.|:)?\s+([^)]*?)\)").unwrap()
});
#[must_use]
pub fn extract_from_featuring(str: &str) -> (Cow<'_, str>, Vec<Artist>) {
let Some(artists) = ARTIST_FEATURING_REGEX.captures(str) else {
return (Cow::Borrowed(str.trim()), Vec::new());
};
let returned_str = ARTIST_FEATURING_REGEX.replace(str, "").trim().to_owned();
let artists = artists_from_string(artists.get(1).unwrap().as_str());
(Cow::Owned(returned_str), artists)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolvedArtist {
pub album: Arc<Artist>,
pub tracks: Vec<(Arc<Track>, Vec<Arc<Artist>>)>,
pub albums: Vec<(Arc<Album>, Vec<Arc<Artist>>)>,
}
impl Deref for ResolvedArtist {
type Target = Artist;
fn deref(&self) -> &Self::Target {
&self.album
}
}
impl Resolveable for Artist {
type Resolved = ResolvedArtist;
fn resolve(artist: Arc<Self>, db: &Self::Db) -> Result<Self::Resolved, TransactionError> {
let tracks = artist.all_tracks_cache(db)?;
let track_artists = tracks
.iter()
.try_map(|t| t.metadata.artists_cache(db))?
.to_vec();
let albums = artist.albums_cache(db)?;
let album_artists = albums.iter().try_map(|a| a.artists_cache(db))?.to_vec();
Ok(ResolvedArtist {
album: artist,
tracks: tracks.into_iter().zip(track_artists).collect(),
albums: albums.into_iter().zip(album_artists).collect(),
})
}
}