use crate::models::{
album::{Album, Image},
artist::Artist,
track::Track,
};
#[derive(Clone, Debug, PartialEq)]
pub struct AlbumArtistBrief {
pub name: Option<String>,
pub roles: Option<Vec<String>>,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ComprehensiveMetadata {
pub title: Option<String>,
pub track_version: Option<String>,
pub album: Option<String>,
pub album_version: Option<String>,
pub performers: Option<String>,
pub performer_name: Option<String>,
pub album_artist_name: Option<String>,
pub album_artists: Vec<AlbumArtistBrief>,
pub album_composer_name: Option<String>,
pub track_composer_name: Option<String>,
pub genre: Option<String>,
pub album_release_date_download: Option<String>,
pub album_release_date_original: Option<String>,
pub track_release_date_original: Option<String>,
pub released_at: Option<i64>,
pub copyright: Option<String>,
pub isrc: Option<String>,
pub upc: Option<String>,
pub product_url: Option<String>,
pub label: Option<String>,
pub release_type: Option<String>,
pub product_type: Option<String>,
pub track_number: Option<i32>,
pub track_total: Option<i32>,
pub disc_number: Option<i32>,
pub disc_total: Option<i32>,
pub parental_warning: Option<bool>,
pub is_classical: bool,
pub cover_art_url: Option<String>,
pub cover_art_data: Option<Vec<u8>>,
}
#[must_use]
pub fn best_cover_url(image: &Image) -> Option<String> {
image
.mega
.clone()
.or_else(|| image.extra_large.clone())
.or_else(|| image.large.clone())
.or_else(|| image.medium.clone())
.or_else(|| image.thumbnail.clone())
.or_else(|| image.small.clone())
}
pub fn extract_comprehensive_metadata(
track: &Track,
album: Option<&Album>,
artist: Option<&Artist>,
) -> ComprehensiveMetadata {
let performer_name = track
.performer
.as_ref()
.and_then(|a| a.name.clone())
.or_else(|| {
let a = artist?;
a.name.clone()
});
let album_artists: Vec<AlbumArtistBrief> = album
.and_then(|a| a.artists.as_ref())
.map(|artists| {
artists
.iter()
.map(|a| AlbumArtistBrief {
name: a.name.clone(),
roles: a.roles.clone(),
})
.collect()
})
.unwrap_or_default();
ComprehensiveMetadata {
title: track.title.clone(),
track_version: track.version.clone(),
album: album.and_then(|a| a.title.clone()),
album_version: album.and_then(|a| a.version.clone()),
performers: track.performers.clone(),
performer_name,
album_artist_name: album
.and_then(|a| a.artist.as_ref())
.and_then(|a| a.name.clone()),
album_artists,
album_composer_name: album
.and_then(|a| a.composer.as_ref())
.and_then(|a| a.name.clone()),
track_composer_name: track.composer.as_ref().and_then(|a| a.name.clone()),
genre: album
.and_then(|a| a.genre.as_ref())
.and_then(|g| g.name.clone()),
album_release_date_download: album.and_then(|a| a.release_date_download.clone()),
album_release_date_original: album.and_then(|a| a.release_date_original.clone()),
track_release_date_original: track.release_date_original.clone(),
released_at: album.and_then(|a| a.released_at),
copyright: track.copyright.clone(),
isrc: track.isrc.clone(),
upc: album.and_then(|a| a.upc.clone()),
product_url: album.and_then(|a| a.product_url.clone()),
label: album
.and_then(|a| a.label.as_ref())
.and_then(|l| l.name.clone()),
release_type: album.and_then(|a| a.release_type.clone()),
product_type: album.and_then(|a| a.product_type.clone()),
track_number: track.track_number,
track_total: album.and_then(|a| a.tracks_count),
disc_number: track.media_number,
disc_total: album.and_then(|a| a.media_count),
parental_warning: track.parental_warning,
is_classical: track.work.is_some(),
cover_art_url: album
.and_then(|a| a.image.as_ref())
.and_then(best_cover_url),
cover_art_data: None,
}
}
#[cfg(test)]
mod tests {
use {
anyhow::{Result, ensure},
serde_json::from_str,
};
use crate::{
metadata::extractor::{best_cover_url, extract_comprehensive_metadata},
models::{
album::{Album, Image},
track::Track,
},
};
fn make_track() -> Result<Track> {
let json =
r#"{"id":1,"title":"So What","track_number":1,"media_number":1,"isrc":"USPJ11001001"}"#;
Ok(from_str(json)?)
}
fn make_album() -> Result<Album> {
let json = r#"{"id":"123","title":"Kind of Blue","release_date_original":"1959-08-17","copyright":"(C) 1959","genre":{"id":1,"name":"Jazz"},"label":{"id":1,"name":"Columbia"},"artist":{"id":1,"name":"Miles Davis"},"image":{"small":"s","thumbnail":"t","medium":"m","large":"l","extralarge":"xl","mega":"mega"}}"#;
Ok(from_str(json)?)
}
#[test]
fn extract_all_fields() -> Result<()> {
let track = make_track()?;
let album = make_album()?;
let meta = extract_comprehensive_metadata(&track, Some(&album), None);
ensure!(meta.title.as_deref() == Some("So What"));
ensure!(meta.album.as_deref() == Some("Kind of Blue"));
ensure!(meta.album_artist_name.as_deref() == Some("Miles Davis"));
ensure!(meta.genre.as_deref() == Some("Jazz"));
ensure!(meta.album_release_date_original.as_deref() == Some("1959-08-17"));
ensure!(meta.copyright.is_none());
ensure!(meta.label.as_deref() == Some("Columbia"));
ensure!(meta.track_number == Some(1));
ensure!(meta.disc_number == Some(1));
ensure!(meta.isrc.as_deref() == Some("USPJ11001001"));
ensure!(meta.cover_art_url.as_deref() == Some("mega"));
Ok(())
}
#[test]
fn extract_with_no_album() -> Result<()> {
let track = make_track()?;
let meta = extract_comprehensive_metadata(&track, None, None);
ensure!(meta.album.is_none());
ensure!(meta.album_artist_name.is_none());
ensure!(meta.genre.is_none());
ensure!(meta.cover_art_url.is_none());
ensure!(meta.title.as_deref() == Some("So What"));
Ok(())
}
#[test]
fn cover_art_priority() -> Result<()> {
let image = Image {
small: Some("s".into()),
..Image::default()
};
ensure!(best_cover_url(&image) == Some("s".to_string()));
let image = Image {
small: Some("s".into()),
large: Some("l".into()),
..Image::default()
};
ensure!(best_cover_url(&image) == Some("l".to_string()));
let image = Image::default();
ensure!(best_cover_url(&image).is_none());
Ok(())
}
#[test]
fn special_characters_preserved() -> Result<()> {
let json = r#"{"id":4,"title":"Café Résumé — Über","track_number":1}"#;
let track: Track = from_str(json)?;
let meta = extract_comprehensive_metadata(&track, None, None);
ensure!(meta.title.as_deref() == Some("Café Résumé — Über"));
Ok(())
}
}