selene-core 0.5.5

selene-core is the backend for Selene, a local-first music player
Documentation
use std::path::PathBuf;

use blake3::Hash;
use chrono::{Datelike, Timelike};
use lofty::{
    ogg::VorbisComments,
    tag::{Accessor, items::Timestamp},
};
use lunar_lib::{
    database::{Database, DatabaseEntry, DatabaseError, TransactionError},
    formatter::{FormatError, FormatTable, format_str},
};
use thiserror::Error;

use crate::{
    config::common::common_config,
    database::LibraryDb,
    errors::{LibraryError, MetadataError},
    library::{
        album::Album,
        artist::{Artist, add_from_artists},
        loudnorm::LoudnormAnalysis,
        track::{
            Track, TrackId,
            track_meta::{TrackAlbumInfo, TrackMeta},
        },
    },
    media_container::MediaContainer,
};

use super::lyric_data::LyricData;

// Core
impl Track {
    #[must_use]
    pub fn new(hash: Hash, container: MediaContainer, metadata: TrackMeta) -> Self {
        Self {
            id: TrackId::new(hash),
            container,
            metadata,
            loudnorm_analysis: None,
            version: Track::VERSION_NUMBER,
        }
    }
}

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

    #[must_use]
    pub fn container(&self) -> &MediaContainer {
        &self.container
    }

    #[must_use]
    pub fn loudnorm_analysis(&self) -> Option<&LoudnormAnalysis> {
        self.loudnorm_analysis.as_ref()
    }

    #[must_use]
    pub fn is_single(&self) -> bool {
        self.metadata.album.is_none()
    }

    pub fn album(&self, db: &LibraryDb) -> Result<Option<TrackAlbumInfo>, DatabaseError> {
        if let Some(album_id) = self.metadata.album {
            Ok(Some({
                let album = Album::db_get_from(album_id, db)?.expect("Dangling album reference");

                let reference = album
                    .track_refs()
                    .iter()
                    .find(|t| t.id == self.id)
                    .expect("Track not found in album");

                let track_num = reference.track_num;
                let disc_num = reference.disc_num;

                TrackAlbumInfo {
                    album,
                    track_num,
                    disc_num,
                }
            }))
        } else {
            Ok(None)
        }
    }
}

// Mutators
impl Track {
    pub fn metadata_key_values(&self) -> Result<VorbisComments, MetadataError> {
        let mut tags = VorbisComments::new();
        {
            let db = LibraryDb::open()?;
            if let Some(track_album_info) = self.album(&db)? {
                tags.set_album(track_album_info.album.name.clone());

                let artists = track_album_info
                    .album
                    .artists()
                    .artists(&db)?
                    .iter()
                    .map(Artist::name)
                    .collect::<Vec<_>>()
                    .join(";");
                tags.insert("ALBUMARTIST".to_owned(), artists);

                if let Some(track_num) = track_album_info.track_num {
                    tags.set_track(track_num);
                    if let Some(track_total) = track_album_info.album.track_total {
                        tags.set_track_total(track_total);
                    }
                }

                if let Some(disc_num) = track_album_info.disc_num {
                    tags.set_disk(disc_num);
                    if let Some(disc_total) = track_album_info.album.disc_total {
                        tags.set_disk_total(disc_total);
                    }
                }
            }

            let artists = self
                .metadata
                .artists
                .artists(&db)?
                .iter()
                .map(Artist::name)
                .collect::<Vec<_>>()
                .join(";");
            tags.set_artist(artists);
        }

        if let Some(date) = self.metadata.date {
            let ts = Timestamp {
                year: date.year() as u16,
                month: Some(date.month() as u8),
                day: Some(date.day() as u8),
                hour: Some(date.hour() as u8),
                minute: Some(date.minute() as u8),
                second: Some(date.second() as u8),
            };
            tags.set_date(ts);
        }

        for genre in &self.metadata.genre {
            tags.push("GENRE".to_owned(), genre.to_owned());
        }

        if let Some(title) = &self.metadata.title {
            tags.set_title(title.to_owned());
        }

        if let Some(lyric_data) = &self.metadata.lyric_data {
            match lyric_data {
                LyricData::Instrumental => {
                    tags.insert("INSTRUMENTAL".to_owned(), "1".to_owned());
                }
                LyricData::Plain(lyrics) => {
                    tags.insert("UNSYNCEDLYRICS".to_owned(), lyrics.to_string());
                }
                LyricData::Synced(lyrics) => {
                    tags.insert("SYNCEDLYRICS".to_owned(), lyrics.to_lrc_string());
                }
            }
        }

        self.metadata.other.iter().for_each(|(k, v)| {
            tags.insert(k.to_uppercase(), v.to_owned());
        });

        Ok(tags)
    }
}

#[derive(Debug, Error)]
pub enum TrackRenameError {
    #[error("IoError: {0}")]
    Io(#[from] std::io::Error),

    #[error("DatabaseError: {0}")]
    Database(#[from] DatabaseError),

    #[error("TransactionError: {0}")]
    Transaction(#[from] TransactionError),

    #[error("FormatError: {0}")]
    Format(#[from] FormatError),

    #[error("LibraryError: {0}")]
    Library(#[from] LibraryError),

    #[error("{0}")]
    ConflictingNames(String),
}

/// Calculates the relative path for a track.
///
/// # Errors
///
/// Returns an error if database references cannot be obtained
pub fn calculate_rel_path(metadata: &TrackMeta) -> Result<PathBuf, TrackRenameError> {
    let db = LibraryDb::open()?;
    let artists = metadata.artists.artists(&db)?;
    let album = metadata.album(&db)?;

    let mut format_table = FormatTable::new();
    format_table.extend_from_taggable(metadata);
    add_from_artists(&mut format_table, &artists, "track");
    if let Some(album) = album {
        format_table.extend_from_taggable(&album);
    }

    let path = PathBuf::from(format_str(
        &common_config().track_name.format_string,
        &format_table,
    )?);

    Ok(path)
}