Skip to main content

selene_core/library/
metadata.rs

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/// [`RawMetadata`] represents all known keys and their raw (valid) values
225#[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/// Extracts the date from a string formatted as `YYYY`. Returns [`None`] if the parsing fails
283#[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        // Returns true if the album name != track name
342        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        // Returns true if the album artists are not the same as the track artists
349        let album_artist_differs = album_artists != track_artists;
350
351        // Returns true if the track/disc value or total is greater is some and is greater than 1
352        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/// Merges a vec of metadata keys, combining groups of artists into one key, leaving everything else untouched
391#[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}