selene-core 0.7.1

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

use blake3::Hash;
use lofty::ogg::VorbisComments;
use lunar_lib::{
    database::{DatabaseEntry, DatabaseError, TransactionError},
    iterator_ext::IteratorExtensions,
};

use crate::{
    config::{ExportConfig, MultiValueStrategy},
    database::LibraryDb,
    library::{
        album::Album,
        artist::Artist,
        loudnorm::LoudnormAnalysis,
        track::{
            ResolvedTrack, Track, TrackId,
            lyric_data::LyricData,
            track_meta::{TrackAlbumInfo, TrackMeta},
        },
    },
    lyrics::LyricFormat,
    media_container::MediaContainer,
};

// 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,
        }
    }
}

// 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()
    }

    #[must_use]
    pub fn metadata(&self) -> &TrackMeta {
        &self.metadata
    }

    #[must_use]
    pub fn metadata_mut(&mut self) -> &mut TrackMeta {
        &mut self.metadata
    }

    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(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)
        }
    }
}

impl ResolvedTrack {
    pub fn metadata_key_values(
        &self,
        export_settings: &ExportConfig,
    ) -> Result<VorbisComments, TransactionError> {
        let mut tags = VorbisComments::new();

        if let Some((al, ar, tn, dn)) = self.album_info() {
            tags.insert(String::from("ALBUM"), al.name().to_owned());

            apply_artists(
                &mut tags,
                ar,
                &export_settings.multi_value_strategy,
                "ALBUMARTIST",
            );

            if let Some(track_total) = al.track_total {
                tags.insert(String::from("TRACKTOTAL"), track_total.to_string());
            }
            if let Some(disc_total) = al.disc_total {
                tags.insert(String::from("DISCTOTAL"), disc_total.to_string());
            }
            if let Some(track_num) = tn {
                tags.insert(String::from("TRACKNUMBER"), track_num.to_string());
            }
            if let Some(disc_num) = dn {
                tags.insert(String::from("DISCNUMBER"), disc_num.to_string());
            }
        } else if export_settings.singles_as_albums {
            tags.insert(
                String::from("ALBUM"),
                self.track.metadata().safe_title().to_owned(),
            );

            apply_artists(
                &mut tags,
                self.artists(),
                &export_settings.multi_value_strategy,
                "ALBUMARTIST",
            );
            tags.insert(String::from("TRACKTOTAL"), String::from("1"));
            tags.insert(String::from("DISCTOTAL"), String::from("1"));
            tags.insert(String::from("TRACKNUMBER"), String::from("1"));
            tags.insert(String::from("DISCNUMBER"), String::from("1"));
        }

        apply_artists(
            &mut tags,
            &self.artists,
            &export_settings.multi_value_strategy,
            "ARTIST",
        );

        if let Some(date) = self.metadata.date {
            tags.insert(
                String::from("DATE"),
                date.format("%Y-%m-%dT%H:%M:%S").to_string(),
            );
        }

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

        if let Some(title) = &self.metadata.title {
            tags.insert(String::from("TITLE"), title.to_owned())
        }

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

        if let Some(loudnorm_analysis) = self.loudnorm_analysis() {
            tags.insert(
                String::from("REPLAYGAIN_TRACK_GAIN"),
                format!("{} dB", loudnorm_analysis.calculated_gain_db()),
            );
            tags.insert(
                String::from("REPLAYGAIN_TRACK_PEAK"),
                format!("{}", loudnorm_analysis.calculated_replay_gain_peak()),
            );
        }

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

        Ok(tags)
    }
}

fn apply_artists(
    tags: &mut VorbisComments,
    artists: &[Arc<Artist>],
    strategy: &MultiValueStrategy,
    tag: &'static str,
) {
    match strategy {
        MultiValueStrategy::MultipleTags => {
            for artist in artists {
                tags.push(tag.to_owned(), artist.name().to_owned());
            }
        }
        MultiValueStrategy::JoinBy(sep) => {
            tags.insert(
                tag.to_owned(),
                artists.iter().map(|a| a.name()).to_vec().join(&sep),
            );
        }
        MultiValueStrategy::First => {
            if let Some(artist) = artists.first() {
                tags.insert(tag.to_owned(), artist.name().to_owned());
            }
        }
    }
}