1use crate::models::{
4 album::{Album, Image},
5 artist::Artist,
6 track::Track,
7};
8
9#[derive(Clone, Debug, PartialEq)]
11pub struct AlbumArtistBrief {
12 pub name: Option<String>,
14 pub roles: Option<Vec<String>>,
16}
17
18#[derive(Clone, Debug, Default, PartialEq)]
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 metadata::extractor::{best_cover_url, extract_comprehensive_metadata},
199 models::{
200 album::{Album, Image},
201 track::Track,
202 },
203 };
204
205 fn make_track() -> Result<Track> {
206 let json =
207 r#"{"id":1,"title":"So What","track_number":1,"media_number":1,"isrc":"USPJ11001001"}"#;
208 Ok(from_str(json)?)
209 }
210
211 fn make_album() -> Result<Album> {
212 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"}}"#;
213 Ok(from_str(json)?)
214 }
215
216 #[test]
217 fn extract_all_fields() -> Result<()> {
218 let track = make_track()?;
219 let album = make_album()?;
220 let meta = extract_comprehensive_metadata(&track, Some(&album), None);
221 ensure!(meta.title.as_deref() == Some("So What"));
222 ensure!(meta.album.as_deref() == Some("Kind of Blue"));
223 ensure!(meta.album_artist_name.as_deref() == Some("Miles Davis"));
224 ensure!(meta.genre.as_deref() == Some("Jazz"));
225 ensure!(meta.album_release_date_original.as_deref() == Some("1959-08-17"));
226 ensure!(meta.copyright.is_none());
227 ensure!(meta.label.as_deref() == Some("Columbia"));
228 ensure!(meta.track_number == Some(1));
229 ensure!(meta.disc_number == Some(1));
230 ensure!(meta.isrc.as_deref() == Some("USPJ11001001"));
231 ensure!(meta.cover_art_url.as_deref() == Some("mega"));
232 Ok(())
233 }
234
235 #[test]
236 fn extract_with_no_album() -> Result<()> {
237 let track = make_track()?;
238 let meta = extract_comprehensive_metadata(&track, None, None);
239 ensure!(meta.album.is_none());
240 ensure!(meta.album_artist_name.is_none());
241 ensure!(meta.genre.is_none());
242 ensure!(meta.cover_art_url.is_none());
243 ensure!(meta.title.as_deref() == Some("So What"));
244 Ok(())
245 }
246
247 #[test]
248 fn cover_art_priority() -> Result<()> {
249 let image = Image {
250 small: Some("s".into()),
251 ..Image::default()
252 };
253 ensure!(best_cover_url(&image) == Some("s".to_string()));
254
255 let image = Image {
256 small: Some("s".into()),
257 large: Some("l".into()),
258 ..Image::default()
259 };
260 ensure!(best_cover_url(&image) == Some("l".to_string()));
261
262 let image = Image::default();
263 ensure!(best_cover_url(&image).is_none());
264 Ok(())
265 }
266
267 #[test]
268 fn special_characters_preserved() -> Result<()> {
269 let json = r#"{"id":4,"title":"Café Résumé — Über","track_number":1}"#;
270 let track: Track = from_str(json)?;
271 let meta = extract_comprehensive_metadata(&track, None, None);
272 ensure!(meta.title.as_deref() == Some("Café Résumé — Über"));
273 Ok(())
274 }
275}