use std::{borrow::Cow, collections::HashSet, ops::Deref, str::FromStr, sync::LazyLock};
use crate::{
config::common_config,
database::{LibraryDb, Patchable, patch_vec},
library::{
album::{Album, AlbumId},
image_art::ImageArt,
track::{Track, TrackId},
},
};
use blake3::{Hash, hash};
use lunar_lib::{
database::{
CompareAndSwapTransaction, DatabaseEntry, DatabaseError, EntryId, TransactionError,
},
formatter::FormatTable,
paths::sys::sanitize_str,
};
use regex::Regex;
use serde::{Deserialize, Serialize};
pub mod accessors;
pub mod frontend_impls;
pub mod trait_impls;
pub const UNKNOWN_ARTIST: &str = "UNKNOWN ARTIST";
#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ArtistId {
id: 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.id.as_bytes()
}
}
impl ArtistId {
fn new(name: &str) -> Self {
Self {
id: hash(name.to_ascii_lowercase().as_bytes()),
}
}
#[must_use]
pub fn to_selene_id(&self) -> String {
format!("artist:{}", self.id)
}
}
impl FromStr for ArtistId {
type Err = <Hash as FromStr>::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self {
id: 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>,
version: usize,
}
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 {
version: 1,
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>, DatabaseError> {
Album::db_get_batch_from(&self.albums, db)
}
pub fn all_tracks(&self, db: &LibraryDb) -> Result<Vec<Track>, DatabaseError> {
Track::db_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(format_table: &mut FormatTable, artists: &[Artist], artist_type: &str) {
if artists.is_empty() {
return;
}
let (main_sep, alt_sep) = {
let common_config = common_config();
(
common_config.track_name.artist_separator.clone(),
common_config.track_name.alt_artist_separator.clone(),
)
};
let names: Vec<String> = artists.iter().map(|a| sanitize_str(a.name())).collect();
let main = &names[0];
format_table.add_entry(format!("main_{artist_type}_artist"), main);
if names.len() == 1 {
format_table.add_entry(format!("all_{artist_type}_artists"), &names[0]);
} else {
let all = format!(
"{}{alt_sep}{}",
names[..names.len() - 1].join(&main_sep),
names.last().unwrap(),
);
format_table.add_entry(format!("all_{artist_type}_artists"), all);
}
let featuring = &names[1..];
match featuring.len() {
0 => (),
1 => format_table.add_entry(format!("feat_{artist_type}_artists"), &featuring[0]),
_ => {
let feat = format!(
"{}{alt_sep}{}",
featuring[..featuring.len() - 1].join(&main_sep),
featuring.last().unwrap(),
);
format_table.add_entry(format!("feat_{artist_type}_artists"), feat);
}
}
}
#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq)]
pub struct ArtistGroup {
artists: Vec<ArtistId>,
}
impl ArtistGroup {
#[must_use]
pub fn artist_ids(&self) -> &[ArtistId] {
&self.artists
}
pub fn artists(&self, db: &LibraryDb) -> Result<Vec<Artist>, DatabaseError> {
Artist::db_get_batch_from(&self.artists, db)
}
#[must_use]
pub fn main_artist_id(&self) -> Option<ArtistId> {
self.artists.first().copied()
}
pub fn main_artist(&self, db: &LibraryDb) -> Result<Option<Artist>, DatabaseError> {
if let Some(first) = self.artists.first() {
let artist = Artist::db_get_from(*first, db)?
.expect("ArtistGroup main artist did not exist in the database");
Ok(Some(artist))
} else {
Ok(None)
}
}
#[must_use]
pub fn featuring_artist_ids(&self) -> &[ArtistId] {
self.artists.get(1..).unwrap_or_default()
}
pub fn featuring_artists(&self) -> Result<Vec<Artist>, DatabaseError> {
Artist::db_get_batch(self.featuring_artist_ids())
}
pub fn add_artist(&mut self, artist: ArtistId) {
if !self.artists.contains(&artist) {
self.artists.push(artist);
}
}
pub fn add_artists(&mut self, artists: impl IntoIterator<Item = ArtistId>) {
artists.into_iter().for_each(|artist| {
self.add_artist(artist);
});
}
pub fn remove_artist(&mut self, artist: ArtistId) {
self.artists.retain(|a| *a != artist);
}
pub fn remove_artists(&mut self, artists: impl IntoIterator<Item = ArtistId>) {
let artists: Vec<ArtistId> = artists.into_iter().collect();
self.artists.retain(|a| !artists.contains(a));
}
pub fn from_artist_ids<I>(value: I) -> ArtistGroup
where
I: IntoIterator<Item = ArtistId>,
{
Self {
artists: value
.into_iter()
.collect::<HashSet<_>>()
.into_iter()
.collect(),
}
}
pub fn from_artists<'a, I>(value: I) -> ArtistGroup
where
I: IntoIterator<Item = &'a Artist>,
{
let mut seen = HashSet::new();
Self {
artists: value
.into_iter()
.map(Artist::id)
.filter(|id| seen.insert(*id))
.collect(),
}
}
pub(crate) fn from_artists_unchecked<'a, I>(value: I) -> ArtistGroup
where
I: IntoIterator<Item = &'a Artist>,
{
Self {
artists: value.into_iter().map(Artist::id).collect(),
}
}
#[must_use]
pub fn new() -> Self {
Self {
artists: Vec::new(),
}
}
}
impl std::ops::Deref for ArtistGroup {
type Target = [ArtistId];
fn deref(&self) -> &Self::Target {
&self.artists
}
}
impl Patchable<ArtistGroup> for ArtistGroup {
fn patch(&mut self, patch: ArtistGroup) {
patch_vec(&mut self.artists, patch.to_vec());
}
}
static ARTIST_SPLIT_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)\s*(?:,|;|\band\b|\bfeat\.?|\bft\.?|\bwith\b|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)
}