use std::{
borrow::{Borrow, Cow},
sync::LazyLock,
};
use crate::{
SeleneIdExt,
library::{Entry, EntryExt, Id, album::Album, image_art::ImageArt, track::Track},
};
use chrono::{DateTime, Utc};
use lunar_lib::{
formatter::FormatTable, iterator_ext::IteratorExtensions, paths::sys::sanitize_str,
};
use regex::Regex;
use serde::{Deserialize, Serialize};
mod database_impls;
mod artist_group;
pub use artist_group::*;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Artist {
pub name: String,
pub art: Option<Id<ImageArt>>,
pub description: Option<String>,
pub tracks: Vec<Id<Track>>,
pub albums: Vec<Id<Album>>,
pub added_at: DateTime<Utc>,
}
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(),
added_at: Utc::now(),
})
}
#[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*").expect("Valid regex")
});
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()
}
#[must_use]
pub fn extract_from_featuring(str: &str) -> (Cow<'_, str>, Vec<Entry<Artist>>) {
static FEATURING_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)\((?:feat|ft|featuring|with)(?:\.|:)?\s+([^)]*?)\)").expect("Valid regex")
});
let Some(captures) = FEATURING_REGEX.captures(str) else {
return (Cow::Borrowed(str.trim()), Vec::new());
};
let artists = artists_from_string(
captures
.get(1)
.expect("Group must exist based off match")
.as_str(),
);
let full_match = captures.get(0).expect("Group must exist based off match ");
let prefix = str[..full_match.start()].trim();
let suffix = str[full_match.end()..].trim();
let cow = if prefix.is_empty() {
Cow::Borrowed(suffix)
} else if suffix.is_empty() {
Cow::Borrowed(prefix)
} else {
Cow::Owned(format!("{prefix} {suffix}"))
};
(cow, 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,
}