1use std::{
2 collections::{HashMap, HashSet},
3 str::FromStr,
4 sync::LazyLock,
5};
6
7use chrono::{DateTime, TimeZone, Utc};
8use lunar_lib::warn;
9use regex::Regex;
10
11use crate::{
12 database::DatabaseEntry,
13 errors::MetadataError,
14 library::{
15 album::{Album, UNKNOWN_ALBUM},
16 artist::{Artist, ArtistGroup, artists_from_string},
17 track::lyric_data::LyricData,
18 },
19};
20
21pub const TRACK_NUM_KEY: &str = "track";
22static TRACK_NUM_REGEX: LazyLock<Regex> =
23 LazyLock::new(|| Regex::new("(?i)^(tra?ck)(.*(num(ber)?))?$").unwrap());
24
25pub const TRACK_TOTAL_KEY: &str = "track_total";
26static TRACK_TOTAL_REGEX: LazyLock<Regex> =
27 LazyLock::new(|| Regex::new("(?i)^(tra?ck|tot(al)?)(.*(tot(al)?|tra?cks?))$").unwrap());
28
29pub const DISC_NUM_KEY: &str = "disc";
30static DISC_NUM_REGEX: LazyLock<Regex> =
31 LazyLock::new(|| Regex::new("(?i)^disc(.*(num(ber)?))?$").unwrap());
32
33pub const DISC_TOTAL_KEY: &str = "disc_total";
34static DISC_TOTAL_REGEX: LazyLock<Regex> =
35 LazyLock::new(|| Regex::new("(?i)^(disc|tot(al)?)(.*(tot(al)?|disc(s)?))$").unwrap());
36
37pub const LYRIC_KEY: &str = "lyrics";
38static LYRIC_REGEX: LazyLock<Regex> =
39 LazyLock::new(|| Regex::new("(?i)^((un)?synced)?(.?lyric(s)?)|sylt|uslt$").unwrap());
40
41pub const INSTRUMENTAL_KEY: &str = "instrumental";
42static INSTRUMENTAL_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new("(?i)inst").unwrap());
43
44pub const ALBUM_KEY: &str = "album";
45pub const GENRE_KEY: &str = "genre";
46pub const TITLE_KEY: &str = "title";
47
48pub const DATE_KEY: &str = "date";
49static DATE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new("(?i)^year|date$").unwrap());
50
51pub const ARTIST_KEY: &str = "artist";
52static ARTIST_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new("(?i)^artist(s)?$").unwrap());
53
54pub const ALBUM_ARTIST_KEY: &str = "album_artist";
55static ALBUM_ARTIST_REGEX: LazyLock<Regex> =
56 LazyLock::new(|| Regex::new("(?i)^album.*artist(s)?$").unwrap());
57
58static KEY_REGEX: LazyLock<Box<[(&'static Regex, &'static str)]>> = LazyLock::new(|| {
59 Box::from([
60 (&*TRACK_NUM_REGEX, TRACK_NUM_KEY),
61 (&*DISC_NUM_REGEX, DISC_NUM_KEY),
62 (&*TRACK_TOTAL_REGEX, TRACK_TOTAL_KEY),
63 (&*DISC_TOTAL_REGEX, DISC_TOTAL_KEY),
64 (&*LYRIC_REGEX, LYRIC_KEY),
65 (&*INSTRUMENTAL_REGEX, INSTRUMENTAL_KEY),
66 (&*DATE_REGEX, DATE_KEY),
67 (&*ARTIST_REGEX, ARTIST_KEY),
68 (&*ALBUM_ARTIST_REGEX, ALBUM_ARTIST_KEY),
69 ])
70});
71
72pub fn canonicalize_metadata_key(key: impl AsRef<str>) -> String {
73 let key = key.as_ref();
74
75 match key.to_ascii_lowercase().as_str() {
76 "title" => return TITLE_KEY.to_owned(),
77 "genre" => return GENRE_KEY.to_owned(),
78 "album" => return ALBUM_KEY.to_owned(),
79 _ => {}
80 }
81
82 for &(regex, cannon_key) in KEY_REGEX.iter() {
83 if regex.is_match(key) {
84 return cannon_key.to_owned();
85 }
86 }
87
88 key.to_owned()
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub enum MetadataKey {
93 Album(Option<Album>),
94 AlbumArtists(Vec<Artist>),
95 Artist(Vec<Artist>),
96 Date(Option<DateTime<Utc>>),
97 DiscNum(Option<u16>),
98 DiscTotal(Option<u16>),
99 Genre(Option<String>),
100 Lyrics(Option<LyricData>),
101 Instrumental(bool),
102 Title(Option<String>),
103 TrackNum(Option<u16>),
104 TrackTotal(Option<u16>),
105 Other(String, Option<String>),
106}
107
108impl std::hash::Hash for MetadataKey {
109 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
110 self.to_key().hash(state);
111 }
112}
113
114impl MetadataKey {
115 #[must_use]
116 pub fn to_key(&self) -> String {
117 match self {
118 MetadataKey::Album(_) => ALBUM_KEY.to_owned(),
119 MetadataKey::AlbumArtists(_) => ALBUM_ARTIST_KEY.to_owned(),
120 MetadataKey::Artist(_) => ARTIST_KEY.to_owned(),
121 MetadataKey::Date(_) => DATE_KEY.to_owned(),
122 MetadataKey::DiscNum(_) => DISC_NUM_KEY.to_owned(),
123 MetadataKey::DiscTotal(_) => DISC_TOTAL_KEY.to_owned(),
124 MetadataKey::Genre(_) => GENRE_KEY.to_owned(),
125 MetadataKey::Lyrics(_) => LYRIC_KEY.to_owned(),
126 MetadataKey::Instrumental(_) => INSTRUMENTAL_KEY.to_owned(),
127 MetadataKey::Title(_) => TITLE_KEY.to_owned(),
128 MetadataKey::TrackNum(_) => TRACK_NUM_KEY.to_owned(),
129 MetadataKey::TrackTotal(_) => TRACK_TOTAL_KEY.to_owned(),
130 MetadataKey::Other(key, _) => key.to_owned(),
131 }
132 }
133
134 pub fn key_from_str(str: impl AsRef<str>) -> Self {
135 let key = canonicalize_metadata_key(str);
136
137 match key.as_str() {
138 ALBUM_KEY => Self::Album(None),
139 ALBUM_ARTIST_KEY => Self::AlbumArtists(Vec::new()),
140 ARTIST_KEY => Self::Artist(Vec::new()),
141 DATE_KEY => Self::Date(None),
142 DISC_NUM_KEY => Self::DiscNum(None),
143 DISC_TOTAL_KEY => Self::DiscTotal(None),
144 GENRE_KEY => Self::Genre(None),
145 LYRIC_KEY => Self::Lyrics(None),
146 INSTRUMENTAL_KEY => Self::Instrumental(false),
147 TITLE_KEY => Self::Title(None),
148 TRACK_NUM_KEY => Self::TrackNum(None),
149 TRACK_TOTAL_KEY => Self::TrackTotal(None),
150 _ => Self::Other(key, None),
151 }
152 }
153
154 pub fn key_value_from_str(str: impl AsRef<str>) -> Result<Self, MetadataError> {
155 let (key_str, value) = str
156 .as_ref()
157 .split_once('=')
158 .ok_or(MetadataError::InvalidKey(str.as_ref().to_owned()))?;
159
160 let mut key = Self::key_from_str(key_str);
161 let value = (!value.is_empty()).then_some(value);
162
163 match &mut key {
164 MetadataKey::Title(v) | MetadataKey::Genre(v) | MetadataKey::Other(_, v) => {
165 *v = value.map(str::to_owned);
166 }
167 MetadataKey::DiscNum(v)
168 | MetadataKey::DiscTotal(v)
169 | MetadataKey::TrackNum(v)
170 | MetadataKey::TrackTotal(v) => *v = value.map(u16::from_str).transpose()?,
171 MetadataKey::Album(v) => {
172 *v = if let Some(value) = value {
173 Some(
174 Album::db_find_by_name(value)?
175 .into_iter()
176 .next()
177 .ok_or(MetadataError::MissingAlbum(value.to_owned()))?,
178 )
179 } else {
180 None
181 };
182 }
183 MetadataKey::Artist(v) | MetadataKey::AlbumArtists(v) => {
184 *v = if let Some(value) = value {
185 let artists = artists_from_string(value);
186
187 for a in &artists {
188 if !Artist::db_check(a.id())? {
189 return Err(MetadataError::MissingArtist(a.name().to_owned()));
190 }
191 }
192
193 artists
194 } else {
195 Vec::new()
196 }
197 }
198 MetadataKey::Date(v) => {
199 *v = value
200 .map(|s| {
201 extract_date_str(s).ok_or(MetadataError::InvalidValue(
202 key_str.to_owned(),
203 s.to_owned(),
204 ))
205 })
206 .transpose()?;
207 }
208 MetadataKey::Lyrics(v) => *v = value.map(LyricData::infer_from_string),
209 MetadataKey::Instrumental(v) => *v = value.is_some_and(|v| v == "1" || v == "true"),
210 }
211
212 Ok(key)
213 }
214}
215
216impl FromStr for MetadataKey {
217 type Err = MetadataError;
218
219 fn from_str(s: &str) -> Result<Self, Self::Err> {
220 MetadataKey::key_value_from_str(s)
221 }
222}
223
224#[derive(Debug, Clone, PartialEq, Eq, Default)]
226pub struct RawMetadata {
227 pub album: Option<String>,
228 pub album_artists: Option<String>,
229 pub artists: Option<String>,
230 pub date: Option<String>,
231 pub disc_num: Option<String>,
232 pub disc_total: Option<String>,
233 pub genre: Option<String>,
234 pub lyrics: Option<String>,
235 pub instrumental: Option<bool>,
236 pub title: Option<String>,
237 pub track_num: Option<String>,
238 pub track_total: Option<String>,
239
240 pub other: HashMap<String, String>,
241}
242
243impl FromIterator<(String, String)> for RawMetadata {
244 fn from_iter<T: IntoIterator<Item = (String, String)>>(iter: T) -> Self {
245 let mut raw = RawMetadata::default();
246
247 for (k, v) in iter {
248 let key = MetadataKey::key_from_str(k);
249 let v = Some(v);
250
251 match key {
252 MetadataKey::Album(_) => raw.album = v,
253 MetadataKey::AlbumArtists(_) => raw.album_artists = v,
254 MetadataKey::Artist(_) => raw.artists = v,
255 MetadataKey::Date(_) => raw.date = v,
256 MetadataKey::DiscNum(_) => raw.disc_num = v,
257 MetadataKey::DiscTotal(_) => raw.disc_total = v,
258 MetadataKey::Genre(_) => raw.genre = v,
259 MetadataKey::Lyrics(_) => raw.lyrics = v,
260 MetadataKey::Instrumental(_) => {
261 raw.instrumental = Some(v.is_some_and(|v| v == "1" || v == "true"));
262 }
263 MetadataKey::Title(_) => raw.title = v,
264 MetadataKey::TrackNum(_) => raw.track_num = v,
265 MetadataKey::TrackTotal(_) => raw.track_total = v,
266 MetadataKey::Other(k, _) => {
267 if let Some(v) = v {
268 raw.other.insert(k, v);
269 } else {
270 warn!(
271 "MetadataPair::Other had an invalid key/value when extracting: '{k}=None'"
272 );
273 }
274 }
275 }
276 }
277
278 raw
279 }
280}
281
282#[must_use]
284pub fn extract_date_str(date: &str) -> Option<DateTime<Utc>> {
285 let year: i32 = date.parse().ok()?;
286 Utc.with_ymd_and_hms(year, 1, 1, 0, 0, 0).single()
287}
288
289impl RawMetadata {
290 #[must_use]
291 pub fn extract_date(&self) -> Option<DateTime<Utc>> {
292 self.date.as_deref().and_then(extract_date_str)
293 }
294
295 #[must_use]
296 pub fn extract_track_num(&self) -> (Option<u16>, Option<u16>) {
297 extract_num(self.track_num.as_deref(), self.track_total.as_deref())
298 }
299
300 #[must_use]
301 pub fn extract_disc_num(&self) -> (Option<u16>, Option<u16>) {
302 extract_num(self.disc_num.as_deref(), self.disc_total.as_deref())
303 }
304
305 #[must_use]
306 pub fn extract_lyric_data(&self) -> Option<LyricData> {
307 self.lyrics
308 .as_deref()
309 .map(LyricData::infer_from_string)
310 .or_else(|| {
311 self.instrumental
312 .unwrap_or(false)
313 .then_some(LyricData::Instrumental)
314 })
315 }
316
317 pub fn extract_track_artists(&self) -> Vec<Artist> {
318 self.artists
319 .as_ref()
320 .map(artists_from_string)
321 .unwrap_or_default()
322 }
323
324 pub fn extract_album_artists(&self) -> Vec<Artist> {
325 self.artists
326 .as_ref()
327 .map(artists_from_string)
328 .unwrap_or_default()
329 }
330
331 pub fn extract_album(&self, track_artists: &[Artist]) -> Option<(Album, Vec<Artist>)> {
332 let album_artists = self
333 .album_artists
334 .as_ref()
335 .map(artists_from_string)
336 .unwrap_or_default();
337
338 let (track_num, track_total) = self.extract_track_num();
339 let (disc_num, disc_total) = self.extract_disc_num();
340
341 let album_name_differs = self
343 .album
344 .as_deref()
345 .zip(self.title.as_deref())
346 .is_none_or(|(a, b)| a != b);
347
348 let album_artist_differs = album_artists != track_artists;
350
351 let multiple_tracks_or_discs = {
353 track_num.is_some_and(|v| v > 1)
354 || track_total.is_some_and(|v| v > 1)
355 || disc_num.is_some_and(|v| v > 1)
356 || disc_total.is_some_and(|v| v > 1)
357 };
358
359 if !(album_name_differs || multiple_tracks_or_discs || album_artist_differs) {
360 return None;
361 }
362
363 let mut album = Album::new(
364 self.album.clone().unwrap_or(UNKNOWN_ALBUM.to_owned()),
365 ArtistGroup::from_artists(&album_artists),
366 Vec::new(),
367 );
368 album.date = self.date.as_deref().and_then(extract_date_str);
369 album.track_total = track_total;
370 album.disc_total = disc_total;
371 Some((album, album_artists))
372 }
373}
374
375#[must_use]
376pub fn extract_num(num: Option<&str>, total: Option<&str>) -> (Option<u16>, Option<u16>) {
377 if let Some(track_values) = num {
378 match track_values.split_once('/') {
379 Some((num, total)) => (num.parse().ok(), total.parse().ok()),
380 None => (
381 track_values.parse().ok(),
382 total.and_then(|v| v.parse().ok()),
383 ),
384 }
385 } else {
386 (None, total.and_then(|v| v.parse().ok()))
387 }
388}
389
390#[must_use]
392pub fn merge_keys(keys: Vec<MetadataKey>) -> Vec<MetadataKey> {
393 let mut artists: Vec<Artist> = Vec::new();
394 let mut album_artists: Vec<Artist> = Vec::new();
395 let mut unique = HashSet::new();
396
397 for key in keys {
398 match key {
399 MetadataKey::Artist(group) => {
400 artists.extend(group);
401 }
402 MetadataKey::AlbumArtists(group) => {
403 album_artists.extend(group);
404 }
405 _ => {
406 unique.replace(key);
407 }
408 }
409 }
410
411 let mut keys: Vec<MetadataKey> = unique.into_iter().collect();
412 keys.push(MetadataKey::Artist(artists));
413 keys.push(MetadataKey::AlbumArtists(album_artists));
414
415 keys
416}