1use crate::api::content::{
4 albums::{Album, Image},
5 artists::Artist,
6 tracks::Track,
7};
8
9#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct AlbumArtistBrief {
12 pub name: Option<String>,
14 pub roles: Option<Vec<String>>,
16}
17
18#[derive(Clone, Debug, Default, PartialEq, Eq)]
24pub struct ComprehensiveMetadata {
25 pub title: Option<String>,
27 pub track_version: Option<String>,
29 pub album: Option<String>,
31 pub album_version: Option<String>,
33 pub performers: Option<String>,
35 pub performer_name: Option<String>,
37 pub album_artist_name: Option<String>,
39 pub album_artists: Vec<AlbumArtistBrief>,
41 pub album_composer_name: Option<String>,
43 pub track_composer_name: Option<String>,
45 pub genre: Option<String>,
47 pub album_release_date_download: Option<String>,
49 pub album_release_date_original: Option<String>,
51 pub track_release_date_original: Option<String>,
53 pub released_at: Option<i64>,
55 pub copyright: Option<String>,
57 pub isrc: Option<String>,
59 pub upc: Option<String>,
61 pub product_url: Option<String>,
63 pub label: Option<String>,
65 pub release_type: Option<String>,
67 pub product_type: Option<String>,
69 pub track_number: Option<i32>,
71 pub track_total: Option<i32>,
73 pub disc_number: Option<i32>,
75 pub disc_total: Option<i32>,
77 pub parental_warning: Option<bool>,
79 pub is_classical: bool,
81 pub cover_art_url: Option<String>,
83 pub cover_art_data: Option<Vec<u8>>,
85}
86
87#[must_use]
91pub fn best_cover_url(image: &Image) -> Option<String> {
92 image
93 .mega
94 .clone()
95 .or_else(|| image.extra_large.clone())
96 .or_else(|| image.large.clone())
97 .or_else(|| image.medium.clone())
98 .or_else(|| image.thumbnail.clone())
99 .or_else(|| image.small.clone())
100}
101
102pub fn extract_comprehensive_metadata(
117 track: &Track,
118 album: Option<&Album>,
119 artist: Option<&Artist>,
120) -> ComprehensiveMetadata {
121 let performer_name = track
122 .performer
123 .as_ref()
124 .and_then(|a| a.name.clone())
125 .or_else(|| {
126 let a = artist?;
127 a.name.clone()
128 });
129
130 let album_artists: Vec<AlbumArtistBrief> = album
131 .and_then(|a| a.artists.as_ref())
132 .map(|artists| {
133 artists
134 .iter()
135 .map(|a| AlbumArtistBrief {
136 name: a.name.clone(),
137 roles: a.roles.clone(),
138 })
139 .collect()
140 })
141 .unwrap_or_default();
142
143 ComprehensiveMetadata {
144 title: track.title.clone(),
145 track_version: track.version.clone(),
146 album: album.and_then(|a| a.title.clone()),
147 album_version: album.and_then(|a| a.version.clone()),
148
149 performers: track.performers.clone(),
150 performer_name,
151 album_artist_name: album
152 .and_then(|a| a.artist.as_ref())
153 .and_then(|a| a.name.clone()),
154 album_artists,
155 album_composer_name: album
156 .and_then(|a| a.composer.as_ref())
157 .and_then(|a| a.name.clone()),
158 track_composer_name: track.composer.as_ref().and_then(|a| a.name.clone()),
159
160 genre: album
161 .and_then(|a| a.genre.as_ref())
162 .and_then(|g| g.name.clone()),
163 album_release_date_download: album.and_then(|a| a.release_date_download.clone()),
164 album_release_date_original: album.and_then(|a| a.release_date_original.clone()),
165 track_release_date_original: track.release_date_original.clone(),
166 released_at: album.and_then(|a| a.released_at),
167 copyright: track.copyright.clone(),
168 isrc: track.isrc.clone(),
169 upc: album.and_then(|a| a.upc.clone()),
170 product_url: album.and_then(|a| a.product_url.clone()),
171 label: album
172 .and_then(|a| a.label.as_ref())
173 .and_then(|l| l.name.clone()),
174 release_type: album.and_then(|a| a.release_type.clone()),
175 product_type: album.and_then(|a| a.product_type.clone()),
176 track_number: track.track_number,
177 track_total: album.and_then(|a| a.tracks_count),
178 disc_number: track.media_number,
179 disc_total: album.and_then(|a| a.media_count),
180 parental_warning: track.parental_warning,
181 is_classical: track.work.is_some(),
182
183 cover_art_url: album
184 .and_then(|a| a.image.as_ref())
185 .and_then(best_cover_url),
186 cover_art_data: None,
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use {
193 anyhow::{Result, ensure},
194 serde_json::from_str,
195 };
196
197 use crate::{
198 api::content::{
199 albums::{Album, Image},
200 tracks::Track,
201 },
202 metadata::extractor::{best_cover_url, extract_comprehensive_metadata},
203 };
204
205 fn make_track() -> Result<Track> {
211 let json =
212 r#"{"id":1,"title":"So What","track_number":1,"media_number":1,"isrc":"USPJ11001001"}"#;
213 Ok(from_str(json)?)
214 }
215
216 fn make_album() -> Result<Album> {
222 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"}}"#;
223 Ok(from_str(json)?)
224 }
225
226 #[test]
232 fn extract_all_fields() -> Result<()> {
233 let track = make_track()?;
234 let album = make_album()?;
235 let meta = extract_comprehensive_metadata(&track, Some(&album), None);
236 ensure!(meta.title.as_deref() == Some("So What"));
237 ensure!(meta.album.as_deref() == Some("Kind of Blue"));
238 ensure!(meta.album_artist_name.as_deref() == Some("Miles Davis"));
239 ensure!(meta.genre.as_deref() == Some("Jazz"));
240 ensure!(meta.album_release_date_original.as_deref() == Some("1959-08-17"));
241 ensure!(meta.copyright.is_none());
242 ensure!(meta.label.as_deref() == Some("Columbia"));
243 ensure!(meta.track_number == Some(1));
244 ensure!(meta.disc_number == Some(1));
245 ensure!(meta.isrc.as_deref() == Some("USPJ11001001"));
246 ensure!(meta.cover_art_url.as_deref() == Some("mega"));
247 Ok(())
248 }
249
250 #[test]
256 fn extract_with_no_album() -> Result<()> {
257 let track = make_track()?;
258 let meta = extract_comprehensive_metadata(&track, None, None);
259 ensure!(meta.album.is_none());
260 ensure!(meta.album_artist_name.is_none());
261 ensure!(meta.genre.is_none());
262 ensure!(meta.cover_art_url.is_none());
263 ensure!(meta.title.as_deref() == Some("So What"));
264 Ok(())
265 }
266
267 #[test]
273 fn cover_art_priority() -> Result<()> {
274 let image = Image {
275 small: Some("s".into()),
276 ..Image::default()
277 };
278 ensure!(best_cover_url(&image) == Some("s".to_string()));
279
280 let image = Image {
281 small: Some("s".into()),
282 large: Some("l".into()),
283 ..Image::default()
284 };
285 ensure!(best_cover_url(&image) == Some("l".to_string()));
286
287 let image = Image::default();
288 ensure!(best_cover_url(&image).is_none());
289 Ok(())
290 }
291
292 #[test]
298 fn special_characters_preserved() -> Result<()> {
299 let json = r#"{"id":4,"title":"Café Résumé — Über","track_number":1}"#;
300 let track: Track = from_str(json)?;
301 let meta = extract_comprehensive_metadata(&track, None, None);
302 ensure!(meta.title.as_deref() == Some("Café Résumé — Über"));
303 Ok(())
304 }
305}