use std::{borrow::Cow, sync::LazyLock};
pub use lofty::{
config::WriteOptions,
tag::{Tag, TagExt},
};
use lofty::{
error::LoftyError,
tag::{Accessor, ItemKey, ItemValue, TagItem, TagType, items::Timestamp},
};
use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
use lunar_lib::iterator_ext::IteratorExtensions;
use regex::Regex;
use crate::{
SeleneIdExt,
config::ExportConfig,
library::{
Entry, EntryExt, Id,
album::Album,
artist::{Artist, artists_from_string, extract_from_featuring},
image_art::ImageArt,
track::{Track, lyric_data::LyricData},
},
lyrics::LyricFormat,
media_container::ContainerFormat,
};
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<Entry<Artist>>);
fn track_title_and_artists(&mut self) -> (Option<String>, Vec<Entry<Artist>>) {
self.title_and_artists(
ItemKey::TrackTitle,
ItemKey::TrackArtist,
ItemKey::TrackArtists,
)
}
fn album_title_and_artists(&mut self) -> (Option<String>, Vec<Entry<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>;
fn album(
&mut self,
track_title: Option<&str>,
track_num: Option<u32>,
disc_num: Option<u32>,
) -> Option<(Entry<Album>, Vec<Entry<Artist>>)>;
}
impl LoftyTagTakeAccessors for Tag {
fn title_and_artists(
&mut self,
title_key: ItemKey,
artist_key: ItemKey,
artists_key: ItemKey,
) -> (Option<String>, Vec<Entry<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
}
fn album(
&mut self,
track_title: Option<&str>,
track_num: Option<u32>,
disc_num: Option<u32>,
) -> Option<(Entry<Album>, Vec<Entry<Artist>>)> {
let (album_title, mut artists) = self.album_title_and_artists();
let track_total = self.track_total();
let disc_total = self.disc_total();
let Some(title) = album_title else {
return None;
};
if track_total == Some(1) && disc_total.is_none_or(|d| d <= 1) {
return None;
}
if track_title.is_some_and(|t| t == title) {
return None;
}
if track_num.is_none_or(|v| v <= 1)
&& track_total.is_none_or(|v| v <= 1)
&& disc_num.is_none_or(|v| v <= 1)
&& disc_total.is_none_or(|v| v <= 1)
{
return None;
}
let id = Id::from_string_hash(&title);
let mut album =
Album::new(title, artists.iter().map(Entry::id).collect(), Vec::new()).to_entry(id);
album.track_total = track_total;
album.disc_total = disc_total;
for artist in &mut artists {
artist.albums.push(id);
}
Some((album, artists))
}
}
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_art(&mut self, source: &ImageArt) -> 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(LyricFormat::Lrc { a2: false }),
);
}
}
}
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_art(&mut self, source: &ImageArt) -> 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(Artist::name)
.to_vec()
.join(&sep.to_string());
tags.insert_text(key, artists);
}
pub trait LoftyTagExt {
fn metadata_key_values<'a, I: IntoIterator<Item = &'a Artist>>(
export_settings: &ExportConfig,
track: &Track,
track_artists: I,
album: Option<(Album, I, Option<u32>, Option<u32>)>,
) -> Self;
}
impl LoftyTagExt for Tag {
fn metadata_key_values<'a, I: IntoIterator<Item = &'a Artist>>(
export_settings: &ExportConfig,
track: &Track,
track_artists: I,
album: Option<(Album, I, Option<u32>, Option<u32>)>,
) -> Self {
let track_artists = track_artists.into_iter().to_vec();
let mut tags = match track.container().format {
ContainerFormat::Flac | ContainerFormat::Ogg => Tag::new(TagType::VorbisComments),
ContainerFormat::Ape => Tag::new(TagType::Ape),
ContainerFormat::Mp3 | ContainerFormat::Wav | ContainerFormat::Aiff => {
Tag::new(TagType::Id3v2)
}
ContainerFormat::Mp4 => Tag::new(TagType::Mp4Ilst),
};
if let Some((album, artists, track_num, disc_num)) = album {
tags.insert_text(ItemKey::AlbumTitle, album.name().to_owned());
tags.set_artists(artists, ItemKey::AlbumArtist);
if let Some(track_total) = album.track_total {
tags.set_track_total(track_total);
}
if let Some(disc_total) = album.disc_total {
tags.set_disk_total(disc_total);
}
if let Some(track_num) = track_num {
tags.set_track(track_num);
}
if let Some(disc_num) = disc_num {
tags.set_disk_total(disc_num);
}
} else if export_settings.singles_as_albums {
tags.insert_text(ItemKey::AlbumTitle, track.metadata.safe_title().to_owned());
tags.set_artists(track_artists.iter().copied(), ItemKey::AlbumArtist);
tags.set_track_total(1);
tags.set_disk_total(1);
tags.set_track(1);
tags.set_disk_total(1);
}
tags.set_artists(track_artists, ItemKey::TrackArtist);
if let Some(date) = track.metadata.date {
tags.insert_text(
ItemKey::RecordingDate,
date.format("%Y-%m-%dT%H:%M:%S").to_string(),
);
}
for genre in &track.metadata.genre {
tags.set_genre(genre.to_owned());
}
if let Some(title) = &track.metadata.title {
Accessor::set_title(&mut tags, title.to_owned());
}
if let Some(lyric_data) = &track.metadata.lyric_data {
match lyric_data {
LyricData::Instrumental => (),
LyricData::Plain(lyrics) => {
tags.insert_text(ItemKey::UnsyncLyrics, lyrics.to_string());
}
LyricData::Synced(lyrics) => {
tags.insert_text(
ItemKey::Lyrics,
lyrics.to_lyrics(LyricFormat::Lrc { a2: false }),
);
}
}
}
if let Some(loudnorm_analysis) = track.loudnorm_analysis() {
tags.insert_text(
ItemKey::ReplayGainTrackGain,
format!("{} dB", loudnorm_analysis.calculated_gain_db()),
);
tags.insert_text(
ItemKey::ReplayGainTrackPeak,
format!("{}", loudnorm_analysis.calculated_replay_gain_peak()),
);
}
track.metadata.other.iter().for_each(|(k, v)| {
if let Some(item_key) = ItemKey::from_key(TagType::VorbisComments, k) {
tags.insert_text(item_key, v.to_owned());
}
});
tags
}
}