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