selene-core 0.8.2

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

pub use lofty::{config::WriteOptions, tag::TagExt};
use lofty::{
    error::LoftyError,
    picture::{Picture, PictureType},
    tag::{ItemKey, ItemValue, Tag, TagItem, TagType, items::Timestamp},
};

use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
use image::ImageError;
use lunar_lib::iterator_ext::IteratorExtensions;
use regex::Regex;

use crate::library::{
    artist::{Artist, artists_from_string, extract_from_featuring},
    track::{cover_art::CoverArt, 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 LoftyTagTakeAccessors {
    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 LoftyTagTakeAccessors 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::UnsyncLyrics).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
    }
}

pub trait LoftyTagRefAccessors {
    fn set_main_artist(&mut self, main_artist: &Artist, key: ItemKey);
    fn set_main_track_artist(&mut self, main_artist: &Artist) {
        self.set_main_artist(main_artist, ItemKey::TrackArtist);
    }
    fn set_main_album_artist(&mut self, main_artist: &Artist) {
        self.set_main_artist(main_artist, ItemKey::AlbumArtist);
    }

    fn set_track_artists(&mut self, artists: &[Artist]) {
        self.set_artists(artists, ItemKey::TrackArtist);
    }
    fn set_album_artists(&mut self, artists: &[Artist]) {
        self.set_artists(artists, ItemKey::AlbumArtist);
    }

    fn set_title(&mut self, title: String);
    fn set_album(&mut self, album: Option<String>);

    fn has_album(&self) -> bool;

    fn set_lyrics(&mut self, lyric_data: &LyricData);

    fn set_cover_from_file(&mut self, source: PathBuf) -> Result<(), ImageError>;

    fn set_cover_track(&mut self, source: &CoverArt) -> Result<(), LoftyError>;

    fn set_artists<'a>(&mut self, artists: impl IntoIterator<Item = &'a Artist>, key: ItemKey);
}

impl LoftyTagRefAccessors for Tag {
    fn set_main_artist(&mut self, main_artist: &Artist, key: ItemKey) {
        let mut artists = self.take_strings(key).to_vec();

        if let Some(artist_index) = artists.iter().position(|a| *a == main_artist.name()) {
            artists[..=artist_index].rotate_right(1);
        } else {
            artists.insert(0, main_artist.name().to_owned());
        }

        for artist in artists {
            self.push(TagItem::new(key, ItemValue::Text(artist)));
        }
    }

    fn set_title(&mut self, title: String) {
        let (title, _) = extract_from_featuring(&title);
        self.insert_text(ItemKey::TrackTitle, title.into_owned());
    }
    fn set_album(&mut self, album: Option<String>) {
        if let Some(album) = album {
            let (album, _) = extract_from_featuring(&album);
            self.insert_text(ItemKey::AlbumTitle, album.into_owned());
        } else {
            self.remove_key(ItemKey::AlbumTitle);
        }
    }

    fn has_album(&self) -> bool {
        self.get(ItemKey::AlbumTitle).is_some()
    }

    fn set_lyrics(&mut self, lyric_data: &LyricData) {
        match lyric_data {
            LyricData::Instrumental => (),
            LyricData::Plain(plain_lyrics) => {
                self.insert_text(ItemKey::UnsyncLyrics, (*plain_lyrics).to_string());
            }
            LyricData::Synced(synced_lyrics) => {
                self.insert_text(
                    ItemKey::Lyrics,
                    synced_lyrics.to_lyrics(crate::lyrics::LyricFormat::Lrc { a2: false }),
                );
            }
        }
    }

    fn set_cover_from_file(&mut self, source: PathBuf) -> Result<(), ImageError> {
        let image = image::open(source)?;
        let mut reader = io::Cursor::new(image.into_bytes());
        let mut picture = Picture::from_reader(&mut reader)
            .expect("Lofty should be able to open any image Image can");
        picture.set_pic_type(PictureType::CoverFront);
        self.set_picture(0, picture);
        Ok(())
    }

    fn set_artists<'a>(&mut self, artists: impl IntoIterator<Item = &'a Artist>, key: ItemKey) {
        match self.tag_type() {
            TagType::Ape | TagType::Id3v1 | TagType::Id3v2 | TagType::Mp4Ilst => {
                apply_single_artist(self, artists, ';', key);
            }
            TagType::VorbisComments => apply_multiple_artists(self, artists, key),
            TagType::RiffInfo | TagType::AiffText => apply_single_artist(self, artists, ';', key),
            _ => {
                unimplemented!(
                    "Support for this tag type has not been implemented as it is a new type"
                )
            }
        }
    }

    fn set_cover_track(&mut self, source: &CoverArt) -> Result<(), LoftyError> {
        self.set_picture(0, source.to_picture()?);
        Ok(())
    }
}

fn apply_multiple_artists<'a>(
    tags: &mut Tag,
    artists: impl IntoIterator<Item = &'a Artist>,
    key: ItemKey,
) {
    for artist in artists {
        tags.push_unchecked(TagItem::new(key, ItemValue::Text(artist.name().to_owned())));
    }
}

fn apply_single_artist<'a>(
    tags: &mut Tag,
    artists: impl IntoIterator<Item = &'a Artist>,
    sep: char,
    key: ItemKey,
) {
    let artists = artists
        .into_iter()
        .map(super::artist::Artist::name)
        .to_vec()
        .join(&sep.to_string());

    tags.insert_text(key, artists);
}