librespot_metadata/audio/
item.rs1use std::{fmt::Debug, path::PathBuf};
2
3use crate::{
4 Metadata,
5 artist::ArtistsWithRole,
6 availability::{AudioItemAvailability, Availabilities, UnavailabilityReason},
7 episode::Episode,
8 error::MetadataError,
9 image::{ImageSize, Images},
10 restriction::Restrictions,
11 track::{Track, Tracks},
12};
13
14use super::file::AudioFiles;
15
16use librespot_core::{Error, Session, SpotifyUri, date::Date, session::UserData};
17
18pub type AudioItemResult = Result<AudioItem, Error>;
19
20#[derive(Debug, Clone)]
21pub struct CoverImage {
22 pub url: String,
23 pub size: ImageSize,
24 pub width: i32,
25 pub height: i32,
26}
27
28#[derive(Debug, Clone)]
29pub struct AudioItem {
30 pub track_id: SpotifyUri,
31 pub uri: String,
32 pub files: AudioFiles,
33 pub name: String,
34 pub covers: Vec<CoverImage>,
35 pub language: Vec<String>,
36 pub duration_ms: u32,
37 pub is_explicit: bool,
38 pub availability: AudioItemAvailability,
39 pub alternatives: Option<Tracks>,
40 pub unique_fields: UniqueFields,
41}
42
43#[derive(Debug, Clone)]
44pub enum UniqueFields {
45 Track {
46 artists: ArtistsWithRole,
47 album: String,
48 album_artists: Vec<String>,
49 popularity: u8,
50 number: u32,
51 disc_number: u32,
52 },
53 Local {
54 artists: Option<String>,
57 album: Option<String>,
58 album_artists: Option<String>,
59 number: Option<u32>,
60 disc_number: Option<u32>,
61 path: PathBuf,
62 },
63 Episode {
64 description: String,
65 publish_time: Date,
66 show_name: String,
67 },
68}
69
70impl AudioItem {
71 pub async fn get_file(session: &Session, uri: SpotifyUri) -> AudioItemResult {
72 let image_url = session
73 .get_user_attribute("image-url")
74 .unwrap_or_else(|| String::from("https://i.scdn.co/image/{file_id}"));
75
76 match uri {
77 SpotifyUri::Track { .. } => {
78 let track = Track::get(session, &uri).await?;
79
80 if track.duration <= 0 {
81 return Err(Error::unavailable(MetadataError::InvalidDuration(
82 track.duration,
83 )));
84 }
85
86 if track.is_explicit && session.filter_explicit_content() {
87 return Err(Error::unavailable(MetadataError::ExplicitContentFiltered));
88 }
89
90 let uri_string = uri.to_uri()?;
91 let album = track.album.name;
92
93 let album_artists = track
94 .album
95 .artists
96 .0
97 .into_iter()
98 .map(|a| a.name)
99 .collect::<Vec<String>>();
100
101 let covers = get_covers(track.album.covers, image_url);
102
103 let alternatives = if track.alternatives.is_empty() {
104 None
105 } else {
106 Some(track.alternatives)
107 };
108
109 let availability = if Date::now_utc() < track.earliest_live_timestamp {
110 Err(UnavailabilityReason::Embargo)
111 } else {
112 available_for_user(
113 &session.user_data(),
114 &track.availability,
115 &track.restrictions,
116 )
117 };
118
119 let popularity = track.popularity.clamp(0, 100) as u8;
120 let number = track.number.max(0) as u32;
121 let disc_number = track.disc_number.max(0) as u32;
122
123 let unique_fields = UniqueFields::Track {
124 artists: track.artists_with_role,
125 album,
126 album_artists,
127 popularity,
128 number,
129 disc_number,
130 };
131
132 Ok(Self {
133 track_id: uri,
134 uri: uri_string,
135 files: track.files,
136 name: track.name,
137 covers,
138 language: track.language_of_performance,
139 duration_ms: track.duration as u32,
140 is_explicit: track.is_explicit,
141 availability,
142 alternatives,
143 unique_fields,
144 })
145 }
146 SpotifyUri::Episode { .. } => {
147 let episode = Episode::get(session, &uri).await?;
148
149 if episode.duration <= 0 {
150 return Err(Error::unavailable(MetadataError::InvalidDuration(
151 episode.duration,
152 )));
153 }
154
155 if episode.is_explicit && session.filter_explicit_content() {
156 return Err(Error::unavailable(MetadataError::ExplicitContentFiltered));
157 }
158
159 let uri_string = uri.to_uri()?;
160
161 let covers = get_covers(episode.covers, image_url);
162
163 let availability = available_for_user(
164 &session.user_data(),
165 &episode.availability,
166 &episode.restrictions,
167 );
168
169 let unique_fields = UniqueFields::Episode {
170 description: episode.description,
171 publish_time: episode.publish_time,
172 show_name: episode.show_name,
173 };
174
175 Ok(Self {
176 track_id: uri,
177 uri: uri_string,
178 files: episode.audio,
179 name: episode.name,
180 covers,
181 language: vec![episode.language],
182 duration_ms: episode.duration as u32,
183 is_explicit: episode.is_explicit,
184 availability,
185 alternatives: None,
186 unique_fields,
187 })
188 }
189 _ => Err(Error::unavailable(MetadataError::NonPlayable)),
190 }
191 }
192}
193
194fn get_covers(covers: Images, image_url: String) -> Vec<CoverImage> {
195 let mut covers = covers;
196
197 covers.sort_by(|a, b| b.width.cmp(&a.width));
198
199 covers
200 .iter()
201 .filter_map(|cover| {
202 let cover_id = cover.id.to_string();
203
204 if !cover_id.is_empty() {
205 let cover_image = CoverImage {
206 url: image_url.replace("{file_id}", &cover_id),
207 size: cover.size,
208 width: cover.width,
209 height: cover.height,
210 };
211
212 Some(cover_image)
213 } else {
214 None
215 }
216 })
217 .collect()
218}
219
220fn allowed_for_user(user_data: &UserData, restrictions: &Restrictions) -> AudioItemAvailability {
221 let country = &user_data.country;
222 let user_catalogue = match user_data.attributes.get("catalogue") {
223 Some(catalogue) => catalogue,
224 None => "premium",
225 };
226
227 for premium_restriction in restrictions.iter().filter(|restriction| {
228 restriction
229 .catalogue_strs
230 .iter()
231 .any(|restricted_catalogue| restricted_catalogue == user_catalogue)
232 }) {
233 if let Some(allowed_countries) = &premium_restriction.countries_allowed {
234 if allowed_countries.iter().any(|allowed| country == allowed) {
238 return Ok(());
239 } else {
240 return Err(UnavailabilityReason::NotWhitelisted);
241 }
242 }
243
244 if let Some(forbidden_countries) = &premium_restriction.countries_forbidden {
245 if forbidden_countries
246 .iter()
247 .any(|forbidden| country == forbidden)
248 {
249 return Err(UnavailabilityReason::Blacklisted);
250 } else {
251 return Ok(());
252 }
253 }
254 }
255
256 Ok(()) }
258
259fn available(availability: &Availabilities) -> AudioItemAvailability {
260 if availability.is_empty() {
261 return Ok(());
263 }
264
265 if !(availability
266 .iter()
267 .any(|availability| Date::now_utc() >= availability.start))
268 {
269 return Err(UnavailabilityReason::Embargo);
270 }
271
272 Ok(())
273}
274
275fn available_for_user(
276 user_data: &UserData,
277 availability: &Availabilities,
278 restrictions: &Restrictions,
279) -> AudioItemAvailability {
280 available(availability)?;
281 allowed_for_user(user_data, restrictions)?;
282 Ok(())
283}