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