selene-core 0.3.1

selene-core is the backend for Selene, a local-first music player
Documentation
use std::{borrow::Cow, collections::HashSet, convert::Infallible, ops::Deref, str::FromStr};

use crate::{
    config::common::common_config,
    database::{
        CompareAndSwapTransaction, DatabaseEntry, DatabaseError, EntryId, Patchable, patch_vec,
    },
    library::{
        album::{Album, AlbumId},
        cover_art::CoverArt,
        track::{Track, TrackId},
    },
};
use blake3::{Hash, hash};
use lunar_lib::{formatter::FormatTable, paths::sys::sanitize_str};
use regex::Regex;
use serde::{Deserialize, Serialize};

pub mod accessors;
pub mod mutators;

pub mod frontend_impls;
pub mod trait_impls;

pub const UNKNOWN_ARTIST: &str = "UNKNOWN ARTIST";

#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ArtistId {
    id: Hash,
}

impl EntryId for ArtistId {
    type Entry = Artist;
}

impl Deref for ArtistId {
    type Target = Hash;

    fn deref(&self) -> &Self::Target {
        &self.id
    }
}

impl ArtistId {
    fn new(name: &str) -> Self {
        Self {
            id: hash(name.to_ascii_lowercase().as_bytes()),
        }
    }
}

impl FromStr for ArtistId {
    type Err = Infallible;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(Self::new(s))
    }
}

impl<T> From<T> for ArtistId
where
    T: AsRef<str>,
{
    fn from(value: T) -> Self {
        Self::new(value.as_ref())
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Artist {
    id: ArtistId,
    name: String,

    pub cover_art: Option<CoverArt>,
    pub description: Option<String>,

    pub(crate) tracks: Vec<TrackId>,
    pub(crate) albums: Vec<AlbumId>,

    version: usize,
}

impl Artist {
    /// Creates a new artist with the input name
    ///
    /// # Errors
    ///
    /// This function will return `None` if the input name is empty or is trimmed to an empty string
    #[must_use]
    pub fn new(name: impl AsRef<str>) -> Option<Self> {
        let name = name.as_ref().trim();
        if name.is_empty() {
            return None;
        }

        Some(Self {
            version: 1,
            id: ArtistId::new(name),
            name: name.to_owned(),
            description: None,
            cover_art: None,
            tracks: Vec::new(),
            albums: Vec::new(),
        })
    }

    /// Sets the name of the artist
    ///
    /// # Errors
    ///
    /// Returns `false` if the input name is empty, or is trimmed to an empty string
    pub fn set_name(&mut self, name: impl AsRef<str>) -> bool {
        let name = name.as_ref().trim();
        if name.is_empty() {
            return false;
        }

        name.clone_into(&mut self.name);
        true
    }

    pub fn albums(&self) -> Result<Vec<Album>, DatabaseError> {
        Album::db_get_batch(&self.albums)
    }

    pub fn all_tracks(&self) -> Result<Vec<Track>, DatabaseError> {
        Track::db_get_batch(&self.tracks)
    }

    fn tx_albums(&self, cas_tx: &CompareAndSwapTransaction) -> Result<Vec<Album>, DatabaseError> {
        cas_tx.tx_get_batch(&self.albums)
    }

    pub fn albums_raw(&self) -> &[AlbumId] {
        &self.albums
    }
}

#[must_use]
pub fn join_artists(artists: &[Artist]) -> String {
    if artists.is_empty() {
        return String::new();
    }

    let main_sep = &common_config().track_name_config.artist_separator;
    let alt_sep = &common_config().track_name_config.alt_artist_separator;

    let names: Vec<&str> = artists.iter().map(Artist::name).collect();

    match names.len() {
        1 => names[0].to_owned(),
        _ => {
            format!(
                "{}{alt_sep}{}",
                names[..names.len() - 1].join(main_sep),
                names.last().unwrap(),
            )
        }
    }
}

pub fn add_from_artists(format_table: &mut FormatTable, artists: &[Artist], artist_type: &str) {
    if artists.is_empty() {
        return;
    }

    let (main_sep, alt_sep) = {
        let common_config = common_config();
        (
            common_config.track_name_config.artist_separator.clone(),
            common_config.track_name_config.alt_artist_separator.clone(),
        )
    };

    let names: Vec<String> = artists.iter().map(|a| sanitize_str(a.name())).collect();

    let main = &names[0];
    format_table.add_entry(format!("main_{artist_type}_artist"), main);

    if names.len() == 1 {
        format_table.add_entry(format!("all_{artist_type}_artists"), &names[0]);
    } else {
        let all = format!(
            "{}{alt_sep}{}",
            names[..names.len() - 1].join(&main_sep),
            names.last().unwrap(),
        );

        format_table.add_entry(format!("all_{artist_type}_artists"), all);
    }

    let featuring = &names[1..];
    match featuring.len() {
        0 => (),
        1 => format_table.add_entry(format!("feat_{artist_type}_artists"), &featuring[0]),
        _ => {
            let feat = format!(
                "{}{alt_sep}{}",
                featuring[..featuring.len() - 1].join(&main_sep),
                featuring.last().unwrap(),
            );

            format_table.add_entry(format!("feat_{artist_type}_artists"), feat);
        }
    }
}

#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq)]
pub struct ArtistGroup {
    artists: Vec<ArtistId>,
}

impl ArtistGroup {
    #[must_use]
    pub fn artist_ids(&self) -> &[ArtistId] {
        &self.artists
    }

    pub fn artists(&self) -> Result<Vec<Artist>, DatabaseError> {
        Artist::db_get_batch(&self.artists)
    }

    #[must_use]
    pub fn main_artist_id(&self) -> Option<ArtistId> {
        self.artists.first().copied()
    }

    pub fn main_artist(&self) -> Result<Option<Artist>, DatabaseError> {
        if let Some(first) = self.artists.first() {
            let artist = Artist::db_get(*first)?
                .expect("ArtistGroup main artist did not exist in the database");
            Ok(Some(artist))
        } else {
            Ok(None)
        }
    }

    #[must_use]
    pub fn featuring_artist_ids(&self) -> &[ArtistId] {
        self.artists.get(1..).unwrap_or_default()
    }

    pub fn featuring_artists(&self) -> Result<Vec<Artist>, DatabaseError> {
        Artist::db_get_batch(self.featuring_artist_ids())
    }

    pub fn add_artist(&mut self, artist: ArtistId) {
        if !self.artists.contains(&artist) {
            self.artists.push(artist);
        }
    }

    pub fn add_artists(&mut self, artists: impl IntoIterator<Item = ArtistId>) {
        artists.into_iter().for_each(|artist| {
            self.add_artist(artist);
        });
    }

    pub fn remove_artist(&mut self, artist: ArtistId) {
        self.artists.retain(|a| *a != artist);
    }

    pub fn remove_artists(&mut self, artists: impl IntoIterator<Item = ArtistId>) {
        let artists: Vec<ArtistId> = artists.into_iter().collect();
        self.artists.retain(|a| !artists.contains(a));
    }

    pub fn from_artist_ids<I>(value: I) -> ArtistGroup
    where
        I: IntoIterator<Item = ArtistId>,
    {
        Self {
            artists: value
                .into_iter()
                .collect::<HashSet<_>>()
                .into_iter()
                .collect(),
        }
    }

    pub fn from_artists<'a, I>(value: I) -> ArtistGroup
    where
        I: IntoIterator<Item = &'a Artist>,
    {
        Self {
            artists: value
                .into_iter()
                .collect::<HashSet<_>>()
                .into_iter()
                .map(Artist::id)
                .collect(),
        }
    }

    #[must_use]
    pub fn new() -> Self {
        Self {
            artists: Vec::new(),
        }
    }
}

impl std::ops::Deref for ArtistGroup {
    type Target = [ArtistId];

    fn deref(&self) -> &Self::Target {
        &self.artists
    }
}

impl Patchable<ArtistGroup> for ArtistGroup {
    fn patch(&mut self, patch: ArtistGroup) {
        patch_vec(&mut self.artists, patch.to_vec());
    }
}

pub fn artists_from_string(artists: impl AsRef<str>) -> Vec<Artist> {
    let split_regex =
        Regex::new(r"(?i)\s*(?:,|;|\band\b)\s*").expect("Invalid regex string for 'split_regex'");

    split_regex
        .split(artists.as_ref())
        .map(str::trim)
        .filter_map(Artist::new)
        .collect()
}

#[must_use]
pub fn extract_from_featuring(str: &str) -> (Cow<'_, str>, Vec<Artist>) {
    let find_regex = Regex::new(r"(?i)\((?:feat|ft|featuring|with)(?:\.|:)?\s+([^)]*?)\)")
        .expect("Invalid regex string for 'find_regex");

    let Some(artists) = find_regex.captures(str) else {
        return (Cow::Borrowed(str), Vec::new());
    };

    let returned_str = find_regex.replace(str, "");
    let artists = artists_from_string(artists.get(1).unwrap().as_str());

    (returned_str, artists)
}