1use std::{
2 borrow::{Borrow, Cow},
3 ops::Deref,
4 str::FromStr,
5 sync::{Arc, LazyLock},
6};
7
8use crate::{
9 database::{LibraryDb, Resolveable},
10 library::{
11 album::{Album, AlbumId},
12 artist::trait_impls::ArtistCreationError,
13 image_art::ImageArt,
14 track::{Track, TrackId},
15 },
16};
17use blake3::{Hash, hash};
18use lunar_lib::{
19 database::{CompareAndSwapTransaction, EntryId, EntryIdIteratorExt, TransactionError},
20 formatter::FormatTable,
21 iterator_ext::IteratorExtensions,
22 paths::sys::sanitize_str,
23};
24use regex::Regex;
25use serde::{Deserialize, Serialize};
26
27pub mod core_impls;
28pub mod frontend_impls;
29pub mod trait_impls;
30
31mod artist_group;
32pub use artist_group::*;
33
34pub const UNKNOWN_ARTIST: &str = "UNKNOWN ARTIST";
35
36#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash)]
37pub struct ArtistId(Hash);
38
39impl EntryId for ArtistId {
40 type Entry = Artist;
41 type IdDb = LibraryDb;
42}
43
44impl Deref for ArtistId {
45 type Target = [u8; 32];
46
47 fn deref(&self) -> &Self::Target {
48 self.0.as_bytes()
49 }
50}
51
52impl ArtistId {
53 fn new(name: &str) -> Self {
54 Self(hash(name.to_ascii_lowercase().as_bytes()))
55 }
56
57 #[must_use]
58 pub fn to_selene_id(&self) -> String {
59 format!("artist:{}", self.0)
60 }
61}
62
63impl FromStr for ArtistId {
64 type Err = <Hash as FromStr>::Err;
65
66 fn from_str(s: &str) -> Result<Self, Self::Err> {
67 Ok(Self(Hash::from_str(s)?))
68 }
69}
70
71impl<T> From<T> for ArtistId
72where
73 T: AsRef<str>,
74{
75 fn from(value: T) -> Self {
76 Self::new(value.as_ref())
77 }
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct Artist {
82 id: ArtistId,
83 name: String,
84
85 pub cover_art: Option<ImageArt>,
86 pub description: Option<String>,
87
88 pub(crate) tracks: Vec<TrackId>,
89 pub(crate) albums: Vec<AlbumId>,
90}
91
92impl Artist {
93 #[must_use]
99 pub fn new(name: impl AsRef<str>) -> Result<Self, ArtistCreationError> {
100 let name = name.as_ref().trim();
101 if name.is_empty() {
102 return Err(ArtistCreationError::EmptyName);
103 }
104
105 Ok(Self {
106 id: ArtistId::new(name),
107 name: name.to_owned(),
108 description: None,
109 cover_art: None,
110 tracks: Vec::new(),
111 albums: Vec::new(),
112 })
113 }
114
115 pub fn set_name(&mut self, name: impl AsRef<str>) -> bool {
121 let name = name.as_ref().trim();
122 if name.is_empty() {
123 return false;
124 }
125
126 name.clone_into(&mut self.name);
127 true
128 }
129
130 pub fn albums(&self, db: &LibraryDb) -> Result<Vec<Album>, TransactionError> {
131 self.albums.iter().copied().db_get_batch(db)
132 }
133
134 pub fn albums_cache(&self, db: &LibraryDb) -> Result<Vec<Arc<Album>>, TransactionError> {
135 self.albums.iter().copied().cache_get_batch(db)
136 }
137
138 pub fn all_tracks(&self, db: &LibraryDb) -> Result<Vec<Track>, TransactionError> {
139 self.tracks.iter().copied().db_get_batch(db)
140 }
141
142 pub fn all_tracks_cache(&self, db: &LibraryDb) -> Result<Vec<Arc<Track>>, TransactionError> {
143 self.tracks.iter().copied().cache_get_batch(db)
144 }
145
146 pub fn tx_albums(
147 &self,
148 cas_tx: &CompareAndSwapTransaction<LibraryDb>,
149 ) -> Result<Vec<Album>, TransactionError> {
150 self.albums.iter().copied().tx_get_batch(cas_tx)
151 }
152
153 #[must_use]
154 pub fn albums_raw(&self) -> &[AlbumId] {
155 &self.albums
156 }
157}
158
159pub fn add_from_artists<I, A>(
160 format_table: &mut FormatTable,
161 artists: I,
162 artist_type: &str,
163 main_sep: &str,
164 alt_sep: &str,
165) where
166 I: IntoIterator<Item = A>,
167 A: Borrow<Artist>,
168{
169 let names = artists
170 .into_iter()
171 .map(|a| sanitize_str(a.borrow().name()))
172 .to_vec();
173
174 if names.is_empty() {
175 return;
176 }
177
178 match names.as_slice() {
179 [] => unreachable!(),
180 [main] => {
181 format_table.add_entry(format!("main_{artist_type}_artist"), main);
182 format_table.add_entry(format!("all_{artist_type}_artists"), main);
183 }
184 [main, duo] => {
185 format_table.add_entry(format!("main_{artist_type}_artist"), main);
186 format_table.add_entry(
187 format!("all_{artist_type}_artists"),
188 format!("{main}{alt_sep}{duo}"),
189 );
190 format_table.add_entry(format!("feat_{artist_type}_artists"), duo);
191 }
192 [main, many @ .., last] => {
193 format_table.add_entry(format!("main_{artist_type}_artist"), main);
194
195 let many = many.join(main_sep);
196 format_table.add_entry(
197 format!("all_{artist_type}_artists"),
198 format!("{main}{main_sep}{many}{alt_sep}{last}"),
199 );
200 format_table.add_entry(
201 format!("feat_{artist_type}_artists"),
202 format!("{many}{alt_sep}{last}"),
203 );
204 }
205 }
206}
207
208static ARTIST_SPLIT_REGEX: LazyLock<Regex> =
209 LazyLock::new(|| Regex::new(r"(?i)\s*(?:,|;|\band\b|\bfeat\.?|\bft\.?|w/)\s*").unwrap());
210
211pub fn artists_from_string(artists: impl AsRef<str>) -> Vec<Artist> {
212 ARTIST_SPLIT_REGEX
213 .split(artists.as_ref())
214 .map(str::trim)
215 .filter_map(|a| Artist::new(a).ok())
216 .collect()
217}
218
219static ARTIST_FEATURING_REGEX: LazyLock<Regex> = LazyLock::new(|| {
220 Regex::new(r"(?i)\((?:feat|ft|featuring|with)(?:\.|:)?\s+([^)]*?)\)").unwrap()
221});
222
223#[must_use]
224pub fn extract_from_featuring(str: &str) -> (Cow<'_, str>, Vec<Artist>) {
225 let Some(artists) = ARTIST_FEATURING_REGEX.captures(str) else {
226 return (Cow::Borrowed(str.trim()), Vec::new());
227 };
228
229 let returned_str = ARTIST_FEATURING_REGEX.replace(str, "").trim().to_owned();
230 let artists = artists_from_string(artists.get(1).unwrap().as_str());
231
232 (Cow::Owned(returned_str), artists)
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct ResolvedArtist {
237 pub album: Arc<Artist>,
238
239 pub tracks: Vec<(Arc<Track>, Vec<Arc<Artist>>)>,
240 pub albums: Vec<(Arc<Album>, Vec<Arc<Artist>>)>,
241}
242
243impl Deref for ResolvedArtist {
244 type Target = Artist;
245
246 fn deref(&self) -> &Self::Target {
247 &self.album
248 }
249}
250
251impl Resolveable for Artist {
252 type Resolved = ResolvedArtist;
253
254 fn resolve(artist: Arc<Self>, db: &Self::Db) -> Result<Self::Resolved, TransactionError> {
255 let tracks = artist.all_tracks_cache(db)?;
256 let track_artists = tracks
257 .iter()
258 .try_map(|t| t.metadata.artists_cache(db))?
259 .to_vec();
260
261 let albums = artist.albums_cache(db)?;
262 let album_artists = albums.iter().try_map(|a| a.artists_cache(db))?.to_vec();
263
264 Ok(ResolvedArtist {
265 album: artist,
266 tracks: tracks.into_iter().zip(track_artists).collect(),
267 albums: albums.into_iter().zip(album_artists).collect(),
268 })
269 }
270}