selene-core 0.6.0

selene-core is the backend for Selene, a local-first music player
Documentation
use std::{borrow::Cow, sync::LazyLock};

use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
use lofty::tag::{ItemKey, Tag, items::Timestamp};
use regex::Regex;

use crate::library::{
    artist::{Artist, artists_from_string, extract_from_featuring},
    track::lyric_data::LyricData,
};

static INSTRUMENTAL_REGEX: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"(?i)\s*\(Inst(?:rumental)?(?:\s+Mix)?\)").unwrap());

#[must_use]
pub fn extract_instrumental(str: &str) -> (Cow<'_, str>, bool) {
    if !INSTRUMENTAL_REGEX.is_match(str) {
        return (Cow::Borrowed(str.trim()), false);
    }

    let returned_str = INSTRUMENTAL_REGEX
        .replace(str, " (Instrumental)")
        .trim()
        .to_owned();
    (Cow::Owned(returned_str), true)
}

pub trait LoftyTagExtensions {
    fn title_and_artists(
        &mut self,
        title_key: ItemKey,
        artist_key: ItemKey,
        artists_key: ItemKey,
    ) -> (Option<String>, Vec<Artist>);

    fn track_title_and_artists(&mut self) -> (Option<String>, Vec<Artist>) {
        self.title_and_artists(
            ItemKey::TrackTitle,
            ItemKey::TrackArtist,
            ItemKey::TrackArtists,
        )
    }

    fn album_title_and_artists(&mut self) -> (Option<String>, Vec<Artist>) {
        self.title_and_artists(
            ItemKey::AlbumTitle,
            ItemKey::AlbumArtist,
            ItemKey::AlbumArtists,
        )
    }

    fn date(&mut self) -> Option<DateTime<Utc>>;

    fn track_num(&mut self) -> Option<u32>;
    fn track_total(&mut self) -> Option<u32>;
    fn disc_num(&mut self) -> Option<u32>;
    fn disc_total(&mut self) -> Option<u32>;

    fn lyrics(&mut self) -> Option<LyricData>;
}

impl LoftyTagExtensions for Tag {
    fn title_and_artists(
        &mut self,
        title_key: ItemKey,
        artist_key: ItemKey,
        artists_key: ItemKey,
    ) -> (Option<String>, Vec<Artist>) {
        let mut all_artists = Vec::new();

        self.take_strings(artist_key)
            .flat_map(artists_from_string)
            .for_each(|a| {
                if !all_artists.contains(&a) {
                    all_artists.push(a);
                }
            });

        self.take_strings(artists_key)
            .flat_map(artists_from_string)
            .for_each(|a| {
                if !all_artists.contains(&a) {
                    all_artists.push(a);
                }
            });

        let title = if let Some(title) = self.take_strings(title_key).next() {
            let (title, other_artists) = extract_from_featuring(&title);

            other_artists.into_iter().for_each(|a| {
                if !all_artists.contains(&a) {
                    all_artists.push(a);
                }
            });

            Some(title.into_owned())
        } else {
            None
        };

        (title, all_artists)
    }

    fn date(&mut self) -> Option<DateTime<Utc>> {
        self.take_strings(ItemKey::RecordingDate)
            .next()
            .and_then(|d| {
                Timestamp::parse(&mut d.as_bytes(), lofty::config::ParsingMode::Relaxed)
                    .ok()
                    .flatten()
            })
            .and_then(|ts| {
                let date = NaiveDate::from_ymd_opt(
                    i32::from(ts.year),
                    u32::from(ts.month.unwrap_or(0)),
                    u32::from(ts.day.unwrap_or(0)),
                )?;

                let time = NaiveTime::from_hms_opt(
                    u32::from(ts.hour.unwrap_or(0)),
                    u32::from(ts.minute.unwrap_or(0)),
                    u32::from(ts.second.unwrap_or(0)),
                )?;

                Some(NaiveDateTime::new(date, time).and_utc())
            })
    }

    fn track_num(&mut self) -> Option<u32> {
        self.take_strings(ItemKey::TrackNumber)
            .next()
            .and_then(|v| v.parse().ok())
    }

    fn track_total(&mut self) -> Option<u32> {
        self.take_strings(ItemKey::TrackTotal)
            .next()
            .and_then(|v| v.parse().ok())
    }

    fn disc_num(&mut self) -> Option<u32> {
        self.take_strings(ItemKey::DiscNumber)
            .next()
            .and_then(|v| v.parse().ok())
    }

    fn disc_total(&mut self) -> Option<u32> {
        self.take_strings(ItemKey::DiscTotal)
            .next()
            .and_then(|v| v.parse().ok())
    }

    fn lyrics(&mut self) -> Option<LyricData> {
        let synced = self.take_strings(ItemKey::Lyrics).next();
        let plain = self.take_strings(ItemKey::Lyrics).next();

        if let Some(lyrics) = synced
            && let Ok(lyric_data) = LyricData::infer_from_string(lyrics)
        {
            return Some(lyric_data);
        }

        if let Some(lyrics) = plain
            && let Ok(lyric_data) = LyricData::infer_from_string(lyrics)
        {
            return Some(lyric_data);
        }

        None
    }
}