Skip to main content

selene_core/library/
artist.rs

1use std::{borrow::Cow, collections::HashSet, convert::Infallible, ops::Deref, str::FromStr};
2
3use crate::{
4    config::common::common_config,
5    database::{
6        CompareAndSwapTransaction, DatabaseEntry, DatabaseError, EntryId, Patchable, patch_vec,
7    },
8    library::{
9        album::{Album, AlbumId},
10        cover_art::CoverArt,
11        track::{Track, TrackId},
12    },
13};
14use blake3::{Hash, hash};
15use lunar_lib::{formatter::FormatTable, paths::sys::sanitize_str};
16use regex::Regex;
17use serde::{Deserialize, Serialize};
18
19pub mod accessors;
20pub mod mutators;
21
22pub mod frontend_impls;
23pub mod trait_impls;
24
25pub const UNKNOWN_ARTIST: &str = "UNKNOWN ARTIST";
26
27#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash)]
28pub struct ArtistId {
29    id: Hash,
30}
31
32impl EntryId for ArtistId {
33    type Entry = Artist;
34}
35
36impl Deref for ArtistId {
37    type Target = Hash;
38
39    fn deref(&self) -> &Self::Target {
40        &self.id
41    }
42}
43
44impl ArtistId {
45    fn new(name: &str) -> Self {
46        Self {
47            id: hash(name.to_ascii_lowercase().as_bytes()),
48        }
49    }
50}
51
52impl FromStr for ArtistId {
53    type Err = Infallible;
54
55    fn from_str(s: &str) -> Result<Self, Self::Err> {
56        Ok(Self::new(s))
57    }
58}
59
60impl<T> From<T> for ArtistId
61where
62    T: AsRef<str>,
63{
64    fn from(value: T) -> Self {
65        Self::new(value.as_ref())
66    }
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct Artist {
71    id: ArtistId,
72    name: String,
73
74    pub cover_art: Option<CoverArt>,
75    pub description: Option<String>,
76
77    pub(crate) tracks: Vec<TrackId>,
78    pub(crate) albums: Vec<AlbumId>,
79
80    version: usize,
81}
82
83impl Artist {
84    /// Creates a new artist with the input name
85    ///
86    /// # Errors
87    ///
88    /// This function will return `None` if the input name is empty or is trimmed to an empty string
89    #[must_use]
90    pub fn new(name: impl AsRef<str>) -> Option<Self> {
91        let name = name.as_ref().trim();
92        if name.is_empty() {
93            return None;
94        }
95
96        Some(Self {
97            version: 1,
98            id: ArtistId::new(name),
99            name: name.to_owned(),
100            description: None,
101            cover_art: None,
102            tracks: Vec::new(),
103            albums: Vec::new(),
104        })
105    }
106
107    /// Sets the name of the artist
108    ///
109    /// # Errors
110    ///
111    /// Returns `false` if the input name is empty, or is trimmed to an empty string
112    pub fn set_name(&mut self, name: impl AsRef<str>) -> bool {
113        let name = name.as_ref().trim();
114        if name.is_empty() {
115            return false;
116        }
117
118        name.clone_into(&mut self.name);
119        true
120    }
121
122    pub fn albums(&self) -> Result<Vec<Album>, DatabaseError> {
123        Album::db_get_batch(&self.albums)
124    }
125
126    pub fn all_tracks(&self) -> Result<Vec<Track>, DatabaseError> {
127        Track::db_get_batch(&self.tracks)
128    }
129
130    fn tx_albums(&self, cas_tx: &CompareAndSwapTransaction) -> Result<Vec<Album>, DatabaseError> {
131        cas_tx.tx_get_batch(&self.albums)
132    }
133
134    pub fn albums_raw(&self) -> &[AlbumId] {
135        &self.albums
136    }
137}
138
139#[must_use]
140pub fn join_artists(artists: &[Artist]) -> String {
141    if artists.is_empty() {
142        return String::new();
143    }
144
145    let main_sep = &common_config().track_name_config.artist_separator;
146    let alt_sep = &common_config().track_name_config.alt_artist_separator;
147
148    let names: Vec<&str> = artists.iter().map(Artist::name).collect();
149
150    match names.len() {
151        1 => names[0].to_owned(),
152        _ => {
153            format!(
154                "{}{alt_sep}{}",
155                names[..names.len() - 1].join(main_sep),
156                names.last().unwrap(),
157            )
158        }
159    }
160}
161
162pub fn add_from_artists(format_table: &mut FormatTable, artists: &[Artist], artist_type: &str) {
163    if artists.is_empty() {
164        return;
165    }
166
167    let (main_sep, alt_sep) = {
168        let common_config = common_config();
169        (
170            common_config.track_name_config.artist_separator.clone(),
171            common_config.track_name_config.alt_artist_separator.clone(),
172        )
173    };
174
175    let names: Vec<String> = artists.iter().map(|a| sanitize_str(a.name())).collect();
176
177    let main = &names[0];
178    format_table.add_entry(format!("main_{artist_type}_artist"), main);
179
180    if names.len() == 1 {
181        format_table.add_entry(format!("all_{artist_type}_artists"), &names[0]);
182    } else {
183        let all = format!(
184            "{}{alt_sep}{}",
185            names[..names.len() - 1].join(&main_sep),
186            names.last().unwrap(),
187        );
188
189        format_table.add_entry(format!("all_{artist_type}_artists"), all);
190    }
191
192    let featuring = &names[1..];
193    match featuring.len() {
194        0 => (),
195        1 => format_table.add_entry(format!("feat_{artist_type}_artists"), &featuring[0]),
196        _ => {
197            let feat = format!(
198                "{}{alt_sep}{}",
199                featuring[..featuring.len() - 1].join(&main_sep),
200                featuring.last().unwrap(),
201            );
202
203            format_table.add_entry(format!("feat_{artist_type}_artists"), feat);
204        }
205    }
206}
207
208#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq)]
209pub struct ArtistGroup {
210    artists: Vec<ArtistId>,
211}
212
213impl ArtistGroup {
214    #[must_use]
215    pub fn artist_ids(&self) -> &[ArtistId] {
216        &self.artists
217    }
218
219    pub fn artists(&self) -> Result<Vec<Artist>, DatabaseError> {
220        Artist::db_get_batch(&self.artists)
221    }
222
223    #[must_use]
224    pub fn main_artist_id(&self) -> Option<ArtistId> {
225        self.artists.first().copied()
226    }
227
228    pub fn main_artist(&self) -> Result<Option<Artist>, DatabaseError> {
229        if let Some(first) = self.artists.first() {
230            let artist = Artist::db_get(*first)?
231                .expect("ArtistGroup main artist did not exist in the database");
232            Ok(Some(artist))
233        } else {
234            Ok(None)
235        }
236    }
237
238    #[must_use]
239    pub fn featuring_artist_ids(&self) -> &[ArtistId] {
240        self.artists.get(1..).unwrap_or_default()
241    }
242
243    pub fn featuring_artists(&self) -> Result<Vec<Artist>, DatabaseError> {
244        Artist::db_get_batch(self.featuring_artist_ids())
245    }
246
247    pub fn add_artist(&mut self, artist: ArtistId) {
248        if !self.artists.contains(&artist) {
249            self.artists.push(artist);
250        }
251    }
252
253    pub fn add_artists(&mut self, artists: impl IntoIterator<Item = ArtistId>) {
254        artists.into_iter().for_each(|artist| {
255            self.add_artist(artist);
256        });
257    }
258
259    pub fn remove_artist(&mut self, artist: ArtistId) {
260        self.artists.retain(|a| *a != artist);
261    }
262
263    pub fn remove_artists(&mut self, artists: impl IntoIterator<Item = ArtistId>) {
264        let artists: Vec<ArtistId> = artists.into_iter().collect();
265        self.artists.retain(|a| !artists.contains(a));
266    }
267
268    pub fn from_artist_ids<I>(value: I) -> ArtistGroup
269    where
270        I: IntoIterator<Item = ArtistId>,
271    {
272        Self {
273            artists: value
274                .into_iter()
275                .collect::<HashSet<_>>()
276                .into_iter()
277                .collect(),
278        }
279    }
280
281    pub fn from_artists<'a, I>(value: I) -> ArtistGroup
282    where
283        I: IntoIterator<Item = &'a Artist>,
284    {
285        Self {
286            artists: value
287                .into_iter()
288                .collect::<HashSet<_>>()
289                .into_iter()
290                .map(Artist::id)
291                .collect(),
292        }
293    }
294
295    #[must_use]
296    pub fn new() -> Self {
297        Self {
298            artists: Vec::new(),
299        }
300    }
301}
302
303impl std::ops::Deref for ArtistGroup {
304    type Target = [ArtistId];
305
306    fn deref(&self) -> &Self::Target {
307        &self.artists
308    }
309}
310
311impl Patchable<ArtistGroup> for ArtistGroup {
312    fn patch(&mut self, patch: ArtistGroup) {
313        patch_vec(&mut self.artists, patch.to_vec());
314    }
315}
316
317pub fn artists_from_string(artists: impl AsRef<str>) -> Vec<Artist> {
318    let split_regex =
319        Regex::new(r"(?i)\s*(?:,|;|\band\b)\s*").expect("Invalid regex string for 'split_regex'");
320
321    split_regex
322        .split(artists.as_ref())
323        .map(str::trim)
324        .filter_map(Artist::new)
325        .collect()
326}
327
328#[must_use]
329pub fn extract_from_featuring(str: &str) -> (Cow<'_, str>, Vec<Artist>) {
330    let find_regex = Regex::new(r"(?i)\((?:feat|ft|featuring|with)(?:\.|:)?\s+([^)]*?)\)")
331        .expect("Invalid regex string for 'find_regex");
332
333    let Some(artists) = find_regex.captures(str) else {
334        return (Cow::Borrowed(str), Vec::new());
335    };
336
337    let returned_str = find_regex.replace(str, "");
338    let artists = artists_from_string(artists.get(1).unwrap().as_str());
339
340    (returned_str, artists)
341}