selene-core 0.8.1

selene-core is the backend for Selene, a local-first music player
Documentation
use std::{ops::Deref, str::FromStr, sync::Arc};

use blake3::{Hash, hash};
use chrono::{DateTime, Utc};
use lunar_lib::{
    database::{EntryId, EntryIdIteratorExt, TransactionError},
    iterator_ext::IteratorExtensions,
};
use serde::{Deserialize, Serialize};

use crate::{
    database::{LibraryDb, Resolveable},
    library::{
        artist::{Artist, ArtistId},
        image_art::ImageArt,
        track::{Track, TrackId},
    },
};

pub mod frontend_impls;
pub mod trait_impls;

pub const UNKNOWN_ALBUM: &str = "UNKNOWN ALBUM";

#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AlbumId(Hash);

impl EntryId for AlbumId {
    type Entry = Album;
    type IdDb = LibraryDb;
}

impl Deref for AlbumId {
    type Target = [u8; 32];

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

impl FromStr for AlbumId {
    type Err = <Hash as FromStr>::Err;

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

impl AlbumId {
    fn new(name: &str) -> Self {
        Self(hash(name.as_bytes()))
    }

    #[must_use]
    pub fn to_hash(&self) -> Hash {
        self.0
    }

    #[must_use]
    pub fn to_selene_id(&self) -> String {
        format!("album:{}", self.0)
    }
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Album {
    id: AlbumId,

    pub name: String,
    pub art: Option<ImageArt>,

    /// Do not modify this variable without handling dangling references
    pub(crate) tracks: Vec<TrackReference>,

    pub(crate) artists: Vec<ArtistId>,
    pub disc_total: Option<u32>,
    pub genre: Vec<String>,
    pub track_total: Option<u32>,
    pub date: Option<DateTime<Utc>>,
}

// Core
impl Album {
    pub(crate) fn new(name: String, artists: Vec<ArtistId>, tracks: Vec<TrackReference>) -> Self {
        let hash = AlbumId::new(&name);

        Self {
            artists,
            art: None,
            disc_total: None,
            genre: Vec::new(),
            id: hash,
            name,
            track_total: None,
            date: None,
            tracks,
        }
    }
}

// Accessors
impl Album {
    #[must_use]
    pub fn id(&self) -> AlbumId {
        self.id
    }

    #[must_use]
    pub fn name(&self) -> &str {
        &self.name
    }

    pub fn tracks(&self, db: &LibraryDb) -> Result<Vec<Track>, TransactionError> {
        self.track_refs().iter().map(|t| t.id).db_get_batch(db)
    }

    pub fn tracks_cache(&self, db: &LibraryDb) -> Result<Vec<Arc<Track>>, TransactionError> {
        self.track_refs().iter().map(|t| t.id).cache_get_batch(db)
    }

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

    #[must_use]
    pub fn artists(&self, db: &LibraryDb) -> Result<Vec<Artist>, TransactionError> {
        self.artists.iter().copied().db_get_batch(db)
    }

    #[must_use]
    pub fn artists_cache(&self, db: &LibraryDb) -> Result<Vec<Arc<Artist>>, TransactionError> {
        self.artists.iter().copied().cache_get_batch(db)
    }

    #[must_use]
    pub fn track_refs(&self) -> &[TrackReference] {
        &self.tracks
    }
}

#[derive(Debug, Deserialize, Serialize, Clone, Copy)]
pub struct TrackReference {
    pub id: TrackId,
    pub track_num: Option<u32>,
    pub disc_num: Option<u32>,
}

impl PartialEq for TrackReference {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id
    }
}

impl Eq for TrackReference {}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolvedAlbum {
    pub album: Arc<Album>,
    pub artists: Vec<Arc<Artist>>,

    pub tracks: Vec<(Arc<Track>, Vec<Arc<Artist>>)>,
}

impl Deref for ResolvedAlbum {
    type Target = Album;

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

impl Resolveable for Album {
    type Resolved = ResolvedAlbum;

    fn resolve(album: Arc<Self>, db: &Self::Db) -> Result<Self::Resolved, TransactionError> {
        let artists = album.artists_cache(db)?;

        let tracks = album.tracks_cache(db)?;
        let track_artists = tracks
            .iter()
            .try_map(|t| t.metadata.artists_cache(db))?
            .to_vec();

        Ok(ResolvedAlbum {
            album,
            artists,
            tracks: tracks.into_iter().zip(track_artists).collect(),
        })
    }
}