use std::{
collections::{HashMap, HashSet},
str::FromStr,
sync::LazyLock,
};
use chrono::{DateTime, TimeZone, Utc};
use lunar_lib::warn;
use regex::Regex;
use crate::{
database::DatabaseEntry,
errors::MetadataError,
library::{
album::{Album, UNKNOWN_ALBUM},
artist::{Artist, ArtistGroup, artists_from_string},
track::lyric_data::LyricData,
},
};
pub const TRACK_NUM_KEY: &str = "track";
static TRACK_NUM_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new("(?i)^(tra?ck)(.*(num(ber)?))?$").unwrap());
pub const TRACK_TOTAL_KEY: &str = "track_total";
static TRACK_TOTAL_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new("(?i)^(tra?ck|tot(al)?)(.*(tot(al)?|tra?cks?))$").unwrap());
pub const DISC_NUM_KEY: &str = "disc";
static DISC_NUM_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new("(?i)^disc(.*(num(ber)?))?$").unwrap());
pub const DISC_TOTAL_KEY: &str = "disc_total";
static DISC_TOTAL_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new("(?i)^(disc|tot(al)?)(.*(tot(al)?|disc(s)?))$").unwrap());
pub const LYRIC_KEY: &str = "lyrics";
static LYRIC_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new("(?i)^((un)?synced)?(.?lyric(s)?)|sylt|uslt$").unwrap());
pub const INSTRUMENTAL_KEY: &str = "instrumental";
static INSTRUMENTAL_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new("(?i)inst").unwrap());
pub const ALBUM_KEY: &str = "album";
pub const GENRE_KEY: &str = "genre";
pub const TITLE_KEY: &str = "title";
pub const DATE_KEY: &str = "date";
static DATE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new("(?i)^year|date$").unwrap());
pub const ARTIST_KEY: &str = "artist";
static ARTIST_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new("(?i)^artist(s)?$").unwrap());
pub const ALBUM_ARTIST_KEY: &str = "album_artist";
static ALBUM_ARTIST_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new("(?i)^album.*artist(s)?$").unwrap());
static KEY_REGEX: LazyLock<Box<[(&'static Regex, &'static str)]>> = LazyLock::new(|| {
Box::from([
(&*TRACK_NUM_REGEX, TRACK_NUM_KEY),
(&*DISC_NUM_REGEX, DISC_NUM_KEY),
(&*TRACK_TOTAL_REGEX, TRACK_TOTAL_KEY),
(&*DISC_TOTAL_REGEX, DISC_TOTAL_KEY),
(&*LYRIC_REGEX, LYRIC_KEY),
(&*INSTRUMENTAL_REGEX, INSTRUMENTAL_KEY),
(&*DATE_REGEX, DATE_KEY),
(&*ARTIST_REGEX, ARTIST_KEY),
(&*ALBUM_ARTIST_REGEX, ALBUM_ARTIST_KEY),
])
});
pub fn canonicalize_metadata_key(key: impl AsRef<str>) -> String {
let key = key.as_ref();
match key.to_ascii_lowercase().as_str() {
"title" => return TITLE_KEY.to_owned(),
"genre" => return GENRE_KEY.to_owned(),
"album" => return ALBUM_KEY.to_owned(),
_ => {}
}
for &(regex, cannon_key) in KEY_REGEX.iter() {
if regex.is_match(key) {
return cannon_key.to_owned();
}
}
key.to_owned()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MetadataKey {
Album(Option<Album>),
AlbumArtists(Vec<Artist>),
Artist(Vec<Artist>),
Date(Option<DateTime<Utc>>),
DiscNum(Option<u16>),
DiscTotal(Option<u16>),
Genre(Option<String>),
Lyrics(Option<LyricData>),
Instrumental(bool),
Title(Option<String>),
TrackNum(Option<u16>),
TrackTotal(Option<u16>),
Other(String, Option<String>),
}
impl std::hash::Hash for MetadataKey {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.to_key().hash(state);
}
}
impl MetadataKey {
#[must_use]
pub fn to_key(&self) -> String {
match self {
MetadataKey::Album(_) => ALBUM_KEY.to_owned(),
MetadataKey::AlbumArtists(_) => ALBUM_ARTIST_KEY.to_owned(),
MetadataKey::Artist(_) => ARTIST_KEY.to_owned(),
MetadataKey::Date(_) => DATE_KEY.to_owned(),
MetadataKey::DiscNum(_) => DISC_NUM_KEY.to_owned(),
MetadataKey::DiscTotal(_) => DISC_TOTAL_KEY.to_owned(),
MetadataKey::Genre(_) => GENRE_KEY.to_owned(),
MetadataKey::Lyrics(_) => LYRIC_KEY.to_owned(),
MetadataKey::Instrumental(_) => INSTRUMENTAL_KEY.to_owned(),
MetadataKey::Title(_) => TITLE_KEY.to_owned(),
MetadataKey::TrackNum(_) => TRACK_NUM_KEY.to_owned(),
MetadataKey::TrackTotal(_) => TRACK_TOTAL_KEY.to_owned(),
MetadataKey::Other(key, _) => key.to_owned(),
}
}
pub fn key_from_str(str: impl AsRef<str>) -> Self {
let key = canonicalize_metadata_key(str);
match key.as_str() {
ALBUM_KEY => Self::Album(None),
ALBUM_ARTIST_KEY => Self::AlbumArtists(Vec::new()),
ARTIST_KEY => Self::Artist(Vec::new()),
DATE_KEY => Self::Date(None),
DISC_NUM_KEY => Self::DiscNum(None),
DISC_TOTAL_KEY => Self::DiscTotal(None),
GENRE_KEY => Self::Genre(None),
LYRIC_KEY => Self::Lyrics(None),
INSTRUMENTAL_KEY => Self::Instrumental(false),
TITLE_KEY => Self::Title(None),
TRACK_NUM_KEY => Self::TrackNum(None),
TRACK_TOTAL_KEY => Self::TrackTotal(None),
_ => Self::Other(key, None),
}
}
pub fn key_value_from_str(str: impl AsRef<str>) -> Result<Self, MetadataError> {
let (key_str, value) = str
.as_ref()
.split_once('=')
.ok_or(MetadataError::InvalidKey(str.as_ref().to_owned()))?;
let mut key = Self::key_from_str(key_str);
let value = (!value.is_empty()).then_some(value);
match &mut key {
MetadataKey::Title(v) | MetadataKey::Genre(v) | MetadataKey::Other(_, v) => {
*v = value.map(str::to_owned);
}
MetadataKey::DiscNum(v)
| MetadataKey::DiscTotal(v)
| MetadataKey::TrackNum(v)
| MetadataKey::TrackTotal(v) => *v = value.map(u16::from_str).transpose()?,
MetadataKey::Album(v) => {
*v = if let Some(value) = value {
Some(
Album::db_find_by_name(value)?
.into_iter()
.next()
.ok_or(MetadataError::MissingAlbum(value.to_owned()))?,
)
} else {
None
};
}
MetadataKey::Artist(v) | MetadataKey::AlbumArtists(v) => {
*v = if let Some(value) = value {
let artists = artists_from_string(value);
for a in &artists {
if !Artist::db_check(a.id())? {
return Err(MetadataError::MissingArtist(a.name().to_owned()));
}
}
artists
} else {
Vec::new()
}
}
MetadataKey::Date(v) => {
*v = value
.map(|s| {
extract_date_str(s).ok_or(MetadataError::InvalidValue(
key_str.to_owned(),
s.to_owned(),
))
})
.transpose()?;
}
MetadataKey::Lyrics(v) => *v = value.map(LyricData::infer_from_string),
MetadataKey::Instrumental(v) => *v = value.is_some_and(|v| v == "1" || v == "true"),
}
Ok(key)
}
}
impl FromStr for MetadataKey {
type Err = MetadataError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
MetadataKey::key_value_from_str(s)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct RawMetadata {
pub album: Option<String>,
pub album_artists: Option<String>,
pub artists: Option<String>,
pub date: Option<String>,
pub disc_num: Option<String>,
pub disc_total: Option<String>,
pub genre: Option<String>,
pub lyrics: Option<String>,
pub instrumental: Option<bool>,
pub title: Option<String>,
pub track_num: Option<String>,
pub track_total: Option<String>,
pub other: HashMap<String, String>,
}
impl FromIterator<(String, String)> for RawMetadata {
fn from_iter<T: IntoIterator<Item = (String, String)>>(iter: T) -> Self {
let mut raw = RawMetadata::default();
for (k, v) in iter {
let key = MetadataKey::key_from_str(k);
let v = Some(v);
match key {
MetadataKey::Album(_) => raw.album = v,
MetadataKey::AlbumArtists(_) => raw.album_artists = v,
MetadataKey::Artist(_) => raw.artists = v,
MetadataKey::Date(_) => raw.date = v,
MetadataKey::DiscNum(_) => raw.disc_num = v,
MetadataKey::DiscTotal(_) => raw.disc_total = v,
MetadataKey::Genre(_) => raw.genre = v,
MetadataKey::Lyrics(_) => raw.lyrics = v,
MetadataKey::Instrumental(_) => {
raw.instrumental = Some(v.is_some_and(|v| v == "1" || v == "true"));
}
MetadataKey::Title(_) => raw.title = v,
MetadataKey::TrackNum(_) => raw.track_num = v,
MetadataKey::TrackTotal(_) => raw.track_total = v,
MetadataKey::Other(k, _) => {
if let Some(v) = v {
raw.other.insert(k, v);
} else {
warn!(
"MetadataPair::Other had an invalid key/value when extracting: '{k}=None'"
);
}
}
}
}
raw
}
}
#[must_use]
pub fn extract_date_str(date: &str) -> Option<DateTime<Utc>> {
let year: i32 = date.parse().ok()?;
Utc.with_ymd_and_hms(year, 1, 1, 0, 0, 0).single()
}
impl RawMetadata {
#[must_use]
pub fn extract_date(&self) -> Option<DateTime<Utc>> {
self.date.as_deref().and_then(extract_date_str)
}
#[must_use]
pub fn extract_track_num(&self) -> (Option<u16>, Option<u16>) {
extract_num(self.track_num.as_deref(), self.track_total.as_deref())
}
#[must_use]
pub fn extract_disc_num(&self) -> (Option<u16>, Option<u16>) {
extract_num(self.disc_num.as_deref(), self.disc_total.as_deref())
}
#[must_use]
pub fn extract_lyric_data(&self) -> Option<LyricData> {
self.lyrics
.as_deref()
.map(LyricData::infer_from_string)
.or_else(|| {
self.instrumental
.unwrap_or(false)
.then_some(LyricData::Instrumental)
})
}
pub fn extract_track_artists(&self) -> Vec<Artist> {
self.artists
.as_ref()
.map(artists_from_string)
.unwrap_or_default()
}
pub fn extract_album_artists(&self) -> Vec<Artist> {
self.artists
.as_ref()
.map(artists_from_string)
.unwrap_or_default()
}
pub fn extract_album(&self, track_artists: &[Artist]) -> Option<(Album, Vec<Artist>)> {
let album_artists = self
.album_artists
.as_ref()
.map(artists_from_string)
.unwrap_or_default();
let (track_num, track_total) = self.extract_track_num();
let (disc_num, disc_total) = self.extract_disc_num();
let album_name_differs = self
.album
.as_deref()
.zip(self.title.as_deref())
.is_none_or(|(a, b)| a != b);
let album_artist_differs = album_artists != track_artists;
let multiple_tracks_or_discs = {
track_num.is_some_and(|v| v > 1)
|| track_total.is_some_and(|v| v > 1)
|| disc_num.is_some_and(|v| v > 1)
|| disc_total.is_some_and(|v| v > 1)
};
if !(album_name_differs || multiple_tracks_or_discs || album_artist_differs) {
return None;
}
let mut album = Album::new(
self.album.clone().unwrap_or(UNKNOWN_ALBUM.to_owned()),
ArtistGroup::from_artists(&album_artists),
Vec::new(),
);
album.date = self.date.as_deref().and_then(extract_date_str);
album.track_total = track_total;
album.disc_total = disc_total;
Some((album, album_artists))
}
}
#[must_use]
pub fn extract_num(num: Option<&str>, total: Option<&str>) -> (Option<u16>, Option<u16>) {
if let Some(track_values) = num {
match track_values.split_once('/') {
Some((num, total)) => (num.parse().ok(), total.parse().ok()),
None => (
track_values.parse().ok(),
total.and_then(|v| v.parse().ok()),
),
}
} else {
(None, total.and_then(|v| v.parse().ok()))
}
}
#[must_use]
pub fn merge_keys(keys: Vec<MetadataKey>) -> Vec<MetadataKey> {
let mut artists: Vec<Artist> = Vec::new();
let mut album_artists: Vec<Artist> = Vec::new();
let mut unique = HashSet::new();
for key in keys {
match key {
MetadataKey::Artist(group) => {
artists.extend(group);
}
MetadataKey::AlbumArtists(group) => {
album_artists.extend(group);
}
_ => {
unique.replace(key);
}
}
}
let mut keys: Vec<MetadataKey> = unique.into_iter().collect();
keys.push(MetadataKey::Artist(artists));
keys.push(MetadataKey::AlbumArtists(album_artists));
keys
}