Skip to main content

selene_core/library/
artist.rs

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    /// Creates a new artist with the input name
94    ///
95    /// # Errors
96    ///
97    /// This function will return `None` if the input name is empty or is trimmed to an empty string
98    #[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    /// Sets the name of the artist
116    ///
117    /// # Errors
118    ///
119    /// Returns `false` if the input name is empty, or is trimmed to an empty string
120    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}