librespot_metadata/audio/
item.rs

1use 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 / album_artists can't be a Vec here, they are retrieved from metadata as a String,
55        // and we cannot make any assumptions about them being e.g. comma-separated
56        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            // A restriction will specify either a whitelast *or* a blacklist,
235            // but not both. So restrict availability if there is a whitelist
236            // and the country isn't on it.
237            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(()) // no restrictions in place
257}
258
259fn available(availability: &Availabilities) -> AudioItemAvailability {
260    if availability.is_empty() {
261        // not all items have availability specified
262        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}