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