use std::{
borrow::{Borrow, Cow},
sync::LazyLock,
};
use crate::{
SeleneIdExt,
library::{Entry, EntryExt, Id, album::Album, image_art::ImageArt, track::Track},
};
use lunar_lib::{
formatter::FormatTable, iterator_ext::IteratorExtensions, paths::sys::sanitize_str,
};
use regex::Regex;
use serde::{Deserialize, Serialize};
mod core_impls;
#[cfg(feature = "database-impls")]
mod database_impls;
mod trait_impls;
mod artist_group;
pub use artist_group::*;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Artist {
name: String,
pub art: Option<Id<ImageArt>>,
pub description: Option<String>,
pub tracks: Vec<Id<Track>>,
pub albums: Vec<Id<Album>>,
}
impl Artist {
pub const UNKNOWN: &str = "UNKNOWN ARTIST";
}
impl Artist {
pub fn new(name: impl AsRef<str>) -> Result<Self, ArtistCreationError> {
let name = name.as_ref().trim();
if name.is_empty() {
return Err(ArtistCreationError::EmptyName);
}
Ok(Self {
name: name.to_owned(),
description: None,
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
}
#[must_use]
pub fn albums(&self) -> &[Id<Album>] {
&self.albums
}
#[must_use]
pub fn tracks(&self) -> &[Id<Track>] {
&self.tracks
}
}
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<Entry<Artist>> {
ARTIST_SPLIT_REGEX
.split(artists.as_ref())
.map(str::trim)
.filter_map(|name| {
Artist::new(name)
.ok()
.map(|a| a.to_entry(Id::from_string_hash(name)))
})
.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<Entry<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)]
pub struct ArtistCreateArgs {
pub name: String,
pub albums: Vec<Id<Album>>,
pub tracks: Vec<Id<Track>>,
}
impl ArtistCreateArgs {
#[must_use]
pub fn new(name: String, albums: Vec<Id<Album>>, tracks: Vec<Id<Track>>) -> Self {
Self {
name,
albums,
tracks,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum ArtistCreationError {
#[error("Artist name resolved to an empty string")]
EmptyName,
}