selene-core 0.6.0

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

use blake3::Hash;
use lunar_lib::database::{DatabaseEntry, DatabaseError, EntryId};
use serde::{Deserialize, Serialize};

use crate::{
    database::LibraryDb,
    library::{
        album::{Album, AlbumId},
        artist::ArtistId,
        collection::rules::{AlbumRule, RuleGroup, TrackRule},
        image_art::ImageArt,
        track::{Track, TrackId},
    },
};

pub mod frontend_impls;
pub mod trait_impls;

pub mod rules;

mod hardcoded_dynamic_collections;
pub use hardcoded_dynamic_collections::*;

#[derive(Debug, thiserror::Error)]
pub enum CollectionCreationError {
    #[error("Collection name resolved to an empty string")]
    EmptyName,

    #[error("Collection name resolved to a reserved collection name")]
    ReservedName(String),
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CollectionType {
    Static { collectables: Vec<Collectable> },
    Dynamic { rules: Vec<DynamicCollectionRules> },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DynamicCollectionRules {
    Track(RuleGroup<TrackRule>),
    Album(RuleGroup<AlbumRule>),
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Collection {
    id: CollectionId,
    pub name: String,
    pub cover_art: Option<ImageArt>,

    pub items: CollectionType,
    read_only: bool,
}

impl Collection {
    /// Creates a new static collection with the input name
    ///
    /// # Errors
    ///
    /// This function will return `None` if the input name is empty or is trimmed to an empty string
    fn new_static(name: String) -> Result<Self, CollectionCreationError> {
        let id = CollectionId::from_str(&name).unwrap();

        Ok(Self {
            id,
            name,
            cover_art: None,
            items: CollectionType::Static {
                collectables: Vec::new(),
            },
            read_only: false,
        })
    }

    pub fn collectables(self, db: &LibraryDb) -> Result<Vec<Collectable>, DatabaseError> {
        match self.items {
            CollectionType::Static { collectables } => Ok(collectables),
            CollectionType::Dynamic { rules } => Ok(resolve_rules(&rules, db)?),
        }
    }
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
pub enum Collectable {
    Track(TrackId),
    Artist(ArtistId),
    Album(AlbumId),
    Collection(CollectionId),
}

impl Collectable {
    #[must_use]
    pub fn to_selene_id(&self) -> String {
        match self {
            Collectable::Track(track_id) => track_id.to_selene_id(),
            Collectable::Artist(artist_id) => artist_id.to_selene_id(),
            Collectable::Album(album_id) => album_id.to_selene_id(),
            Collectable::Collection(collection_id) => collection_id.to_selene_id(),
        }
    }
}

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

impl CollectionId {
    #[must_use]
    pub fn to_selene_id(&self) -> String {
        format!("collection:{}", self.id)
    }
}

impl EntryId for CollectionId {
    type Entry = Collection;
    type IdDb = LibraryDb;
}

impl std::ops::Deref for CollectionId {
    type Target = [u8; 32];

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

impl FromStr for CollectionId {
    type Err = Infallible;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(Self {
            id: blake3::hash(s.as_bytes()),
        })
    }
}

pub fn resolve_rules(
    rules: &[DynamicCollectionRules],
    db: &LibraryDb,
) -> Result<Vec<Collectable>, DatabaseError> {
    let tracks = Track::db_get_all_from(db)?;
    let albums = Album::db_get_all_from(db)?;

    let mut collectables = Vec::new();

    for group in rules {
        match group {
            DynamicCollectionRules::Track(rule_group) => {
                let tracks = rule_group.filter(&tracks);
                collectables.extend(tracks.map(|t| Collectable::Track(t.id())));
            }
            DynamicCollectionRules::Album(rule_group) => {
                let albums = rule_group.filter(&albums);
                collectables.extend(albums.map(|t| Collectable::Album(t.id())));
            }
        }
    }

    Ok(collectables)
}