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