selene-core 0.2.0

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

use chrono::{DateTime, Utc};
use lunar_lib::{
    formatter::{FormatTable, Taggable},
    paths::sys::sanitize_str,
};
use serde::{Deserialize, Serialize};

use crate::{
    database::{DatabaseEntry, DatabaseError, Patchable, patch_option_replace},
    errors::MetadataError,
    library::{
        album::{Album, AlbumId},
        artist::{Artist, ArtistGroup},
        metadata::{
            ALBUM_ARTIST_KEY, ALBUM_KEY, ARTIST_KEY, DATE_KEY, DISC_NUM_KEY, GENRE_KEY,
            MetadataKey, TITLE_KEY, TRACK_NUM_KEY,
        },
        track::lyric_data::LyricData,
    },
};

/// Track metadata. Defines metadata to be read from and stored on files
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct TrackMeta {
    pub album: Option<AlbumReference>,
    pub artists: ArtistGroup,
    pub date: Option<DateTime<Utc>>,
    pub genre: Option<String>,
    pub lyric_data: Option<LyricData>,
    pub other: HashMap<String, String>,
    pub title: Option<String>,
}

impl TrackMeta {
    #[must_use]
    pub fn new() -> Self {
        Self {
            album: None,
            artists: ArtistGroup::new(),
            date: None,
            genre: None,
            lyric_data: None,
            other: HashMap::new(),
            title: None,
        }
    }

    pub fn to_key_values(&self) -> Result<HashMap<String, String>, DatabaseError> {
        let mut map = HashMap::new();

        if let Some(album_reference) = self.album {
            let album = Album::db_get(album_reference.album)?.expect("AlbumReference pointed to an album that didnt exist. This shouldn't be possible unless an error happened internally");

            if let Some(name) = &album.name {
                map.insert(ALBUM_KEY, name.clone());
            }

            let artists = album
                .artists
                .artists()?
                .iter()
                .map(Artist::name)
                .collect::<Vec<_>>()
                .join(",");
            map.insert(ALBUM_ARTIST_KEY, artists);

            if let Some(track_num) = album_reference.track_num
                && let Some(track_total) = album.track_total
            {
                map.insert(TRACK_NUM_KEY, format!("{track_num}/{track_total}"));
            }

            if let Some(disc_num) = album_reference.disc_num
                && let Some(disc_total) = album.disc_total
            {
                map.insert(DISC_NUM_KEY, format!("{disc_num}/{disc_total}"));
            }
        }

        let artists = self
            .artists
            .artists()?
            .iter()
            .map(Artist::name)
            .collect::<Vec<_>>()
            .join(",");
        map.insert(ARTIST_KEY, artists);

        if let Some(date) = self.date {
            map.insert(DATE_KEY, date.to_string());
        }

        if let Some(genre) = &self.genre {
            map.insert(GENRE_KEY, genre.to_owned());
        }

        if let Some(title) = &self.title {
            map.insert(TITLE_KEY, title.to_owned());
        }

        if let Some(lyric_data) = &self.lyric_data {
            let (k, v) = lyric_data.get_metadata_value();
            map.insert(k, v);
        }

        let mut collected = self.other.clone();
        collected.extend(map.into_iter().map(|(k, v)| (k.to_owned(), v)));

        Ok(collected)
    }

    pub fn apply_metadata_key(&mut self, key: MetadataKey) -> Result<(), MetadataError> {
        match key {
            MetadataKey::Album(v) => {
                self.album = v.map(|v| AlbumReference {
                    album: v.id(),
                    track_num: None,
                    disc_num: None,
                });
            }
            MetadataKey::Artist(v) => self.artists = ArtistGroup::from_artists(&v),
            MetadataKey::Date(v) => self.date = v,
            MetadataKey::DiscNum(v) => {
                if let Some(album_reference) = &mut self.album {
                    album_reference.disc_num = v;
                } else {
                    return Err(MetadataError::MissingAlbum(
                        "disc cannot be set because track has no album".to_owned(),
                    ));
                }
            }
            MetadataKey::Genre(v) => self.genre = v,
            MetadataKey::Lyrics(v) => self.lyric_data = v,
            MetadataKey::Instrumental(v) => self.lyric_data = v.then_some(LyricData::Instrumental),
            MetadataKey::Title(v) => self.title = v,
            MetadataKey::TrackNum(v) => {
                if let Some(album_reference) = &mut self.album {
                    album_reference.track_num = v;
                } else {
                    return Err(MetadataError::MissingAlbum(
                        "track cannot be set because track has no album".to_owned(),
                    ));
                }
            }
            MetadataKey::Other(key, v) => {
                if let Some(v) = v {
                    self.other.insert(key, v);
                } else {
                    self.other.remove(&key);
                }
            }
            _ => {
                return Err(MetadataError::KeyNotAllowed(format!(
                    "{key} cannot be used on track metadata",
                    key = key.to_key()
                )));
            }
        }
        Ok(())
    }
}

impl Patchable<TrackMeta> for TrackMeta {
    fn patch(&mut self, patch: TrackMeta) {
        patch_option_replace(&mut self.album, patch.album);
        self.artists.patch(patch.artists);
        patch_option_replace(&mut self.date, patch.date);
        patch_option_replace(&mut self.genre, patch.genre);
        patch_option_replace(&mut self.lyric_data, patch.lyric_data);
        self.other.extend(patch.other);
        patch_option_replace(&mut self.title, patch.title);
    }
}

impl Taggable for TrackMeta {
    type Err = Infallible;

    fn fill_table(&self, table: &mut FormatTable) -> Result<(), Self::Err> {
        if let Some(value) = self.date {
            table.add_entry("date", value.to_string());
        }
        if let Some(value) = self.album {
            if let Some(track_num) = value.track_num {
                table.add_entry("track_num", track_num.to_string());
            }
            if let Some(disc_num) = value.disc_num {
                table.add_entry("disc_num", disc_num.to_string());
            }
        }
        if let Some(value) = &self.title {
            table.add_entry("title", sanitize_str(value));
        }
        if let Some(value) = &self.genre {
            table.add_entry("genre", sanitize_str(value));
        }
        table.add_table(self.other.clone());

        if let Some(lyric_data) = &self.lyric_data {
            match lyric_data {
                LyricData::Instrumental => table.add_entry("instrumental", "1"),
                LyricData::Plain(_) => {
                    table.add_entry("plain_lyrics", "1");
                    table.add_entry("lyrics", "1");
                }
                LyricData::Synced(_) => {
                    table.add_entry("synced_lyrics", "1");
                    table.add_entry("lyrics", "1");
                }
            }
        }

        Ok(())
    }
}

#[derive(Debug, Deserialize, Serialize, Clone, Copy)]
pub struct AlbumReference {
    pub album: AlbumId,
    pub track_num: Option<u16>,
    pub disc_num: Option<u16>,
}

impl AlbumReference {
    #[must_use]
    pub fn new(album: AlbumId, track_num: Option<u16>, disc_num: Option<u16>) -> Self {
        Self {
            album,
            track_num,
            disc_num,
        }
    }

    /// Looks for the album in the database with this ID
    ///
    /// # Panics
    ///
    /// Panics if the item could not be found in the database. This can only occur if the database was modified externally, or the program failed to correctly clean up after itself either due to logical error or being unable to do so
    pub fn lookup(&self) -> Result<Album, DatabaseError> {
        let album = Album::db_get(self.album)?.expect("Album reference was pointing to an invalid album. This can only happen if modfications could not be completed or the database was modified externally");
        Ok(album)
    }
}