spotify_rs/endpoint/
track.rs

1use std::{collections::HashMap, fmt::Debug, marker::PhantomData};
2
3use serde::{ser::SerializeMap, Serialize};
4use strum::IntoStaticStr;
5
6use crate::{
7    auth::{AuthFlow, Authorised},
8    body_list,
9    error::Result,
10    model::{
11        audio::{AudioAnalysis, AudioFeatures, AudioFeaturesList, Mode},
12        recommendation::Recommendations,
13        track::{SavedTrack, Track, Tracks},
14        Page,
15    },
16    query_list, Nil,
17};
18
19use super::{Client, Endpoint};
20
21pub fn track(id: impl Into<String>) -> TrackEndpoint {
22    TrackEndpoint {
23        id: id.into(),
24        market: None,
25    }
26}
27
28pub fn tracks<T: AsRef<str>>(ids: &[T]) -> TracksEndpoint {
29    TracksEndpoint {
30        ids: query_list(ids),
31        market: None,
32    }
33}
34
35pub fn saved_tracks() -> SavedTracksEndpoint {
36    SavedTracksEndpoint::default()
37}
38
39pub async fn save_tracks<T: AsRef<str>>(
40    ids: &[T],
41    spotify: &Client<impl AuthFlow + Authorised>,
42) -> Result<Nil> {
43    spotify
44        .put("/me/tracks".to_owned(), body_list("ids", ids))
45        .await
46}
47
48pub async fn remove_saved_tracks<T: AsRef<str>>(
49    ids: &[T],
50    spotify: &Client<impl AuthFlow + Authorised>,
51) -> Result<Nil> {
52    spotify
53        .delete("/me/tracks".to_owned(), body_list("ids", ids))
54        .await
55}
56
57pub async fn check_saved_tracks<T: AsRef<str>>(
58    ids: &[T],
59    spotify: &Client<impl AuthFlow + Authorised>,
60) -> Result<Vec<bool>> {
61    spotify
62        .get("/me/tracks/contains".to_owned(), [("ids", query_list(ids))])
63        .await
64}
65
66/// **Note:** This endpoint has been deprecated by Spotify. It continues to work for
67/// applications already using the extended mode in the API.
68///
69/// You can read more about this [here](https://developer.spotify.com/blog/2024-11-27-changes-to-the-web-api).
70pub async fn get_track_audio_features(
71    id: impl Into<String>,
72    spotify: &Client<impl AuthFlow>,
73) -> Result<AudioFeatures> {
74    spotify
75        .get::<(), _>(format!("/audio-features/{}", id.into()), None)
76        .await
77}
78
79/// **Note:** This endpoint has been deprecated by Spotify. It continues to work for
80/// applications already using the extended mode in the API.
81///
82/// You can read more about this [here](https://developer.spotify.com/blog/2024-11-27-changes-to-the-web-api).
83pub async fn get_tracks_audio_features<T: AsRef<str>>(
84    ids: &[T],
85    spotify: &Client<impl AuthFlow>,
86) -> Result<Vec<Option<AudioFeatures>>> {
87    spotify
88        .get("/audio-features".to_owned(), [("ids", query_list(ids))])
89        .await
90        .map(|a: AudioFeaturesList| a.audio_features)
91}
92
93pub async fn get_track_audio_analysis(
94    id: impl Into<String>,
95    spotify: &Client<impl AuthFlow>,
96) -> Result<AudioAnalysis> {
97    spotify
98        .get::<(), _>(format!("/audio-analysis/{}", id.into()), None)
99        .await
100}
101
102/// Get recommendations based on given seeds. You must specify at least one
103/// seed (whether that be a seed artist, track or genre). More seed types can
104/// be used optionally via the builder.
105///
106/// **Note:** This endpoint has been deprecated by Spotify. It continues to work for
107/// applications already using the extended mode in the API.
108///
109/// You can read more about this [here](https://developer.spotify.com/blog/2024-11-27-changes-to-the-web-api).
110#[doc = include_str!("../docs/seed_limit.md")]
111pub fn recommendations<S: SeedType, T: AsRef<str>>(seed: Seed<T, S>) -> RecommendationsEndpoint<S> {
112    let (seed_artists, seed_genres, seed_tracks) = match seed {
113        Seed::Artists(ids, _) => (Some(query_list(ids)), None, None),
114        Seed::Genres(genres, _) => (None, Some(query_list(genres)), None),
115        Seed::Tracks(ids, _) => (None, None, Some(query_list(ids))),
116    };
117
118    RecommendationsEndpoint {
119        seed_artists,
120        seed_genres,
121        seed_tracks,
122        limit: None,
123        market: None,
124        features: None,
125        marker: std::marker::PhantomData,
126    }
127}
128
129impl Endpoint for TrackEndpoint {}
130impl Endpoint for TracksEndpoint {}
131impl Endpoint for SavedTracksEndpoint {}
132impl<S: SeedType> Endpoint for RecommendationsEndpoint<S> {}
133
134pub trait SeedType: Debug {}
135impl SeedType for SeedArtists {}
136impl SeedType for SeedGenres {}
137impl SeedType for SeedTracks {}
138
139#[derive(Clone, Copy, Debug)]
140pub enum SeedArtists {}
141#[derive(Clone, Copy, Debug)]
142pub enum SeedGenres {}
143#[derive(Clone, Copy, Debug)]
144pub enum SeedTracks {}
145
146#[derive(Clone, Debug)]
147#[non_exhaustive]
148pub enum Seed<'a, T: AsRef<str>, S: SeedType> {
149    Artists(&'a [T], PhantomData<S>),
150    Genres(&'a [T], PhantomData<S>),
151    Tracks(&'a [T], PhantomData<S>),
152}
153
154impl<'a, T: AsRef<str> + Clone> Seed<'a, T, SeedArtists> {
155    #[doc = include_str!("../docs/seed_limit.md")]
156    pub fn artists(ids: &'a [T]) -> Self {
157        Self::Artists(ids, PhantomData)
158    }
159}
160
161impl<'a, T: AsRef<str> + Clone> Seed<'a, T, SeedGenres> {
162    #[doc = include_str!("../docs/seed_limit.md")]
163    pub fn genres(genres: &'a [T]) -> Self {
164        Self::Genres(genres, PhantomData)
165    }
166}
167
168impl<'a, T: AsRef<str> + Clone> Seed<'a, T, SeedTracks> {
169    #[doc = include_str!("../docs/seed_limit.md")]
170    pub fn tracks(ids: &'a [T]) -> Self {
171        Self::Tracks(ids, PhantomData)
172    }
173}
174
175// #[derive(Clone, Copy, Debug, Serialize, IntoStaticStr)]
176// #[serde(untagged)]
177// #[serde(rename_all = "snake_case")]
178// #[strum(serialize_all = "snake_case")]
179// pub enum Feature {
180//     MinAcousticness(f32),
181//     MaxAcousticness(f32),
182//     TargetAcousticness(f32),
183//     MinDanceability(f32),
184//     MaxDanceability(f32),
185//     TargetDanceability(f32),
186//     MinDurationMs(u32),
187//     MaxDurationMs(u32),
188//     TargetDurationMs(u32),
189//     MinEnergy(f32),
190//     MaxEnergy(f32),
191//     TargetEnergy(f32),
192//     MinInstrumentalness(f32),
193//     MaxInstrumentalness(f32),
194//     TargetInstrumentalness(f32),
195//     MinKey(u32),
196//     MaxKey(u32),
197//     TargetKey(u32),
198//     MinLiveness(f32),
199//     MaxLiveness(f32),
200//     TargetLiveness(f32),
201//     MinLoudness(f32),
202//     MaxLoudness(f32),
203//     TargetLoudness(f32),
204//     MinMode(u32),
205//     MaxMode(u32),
206//     TargetMode(u32),
207//     MinPopularity(u32),
208//     MaxPopularity(u32),
209//     TargetPopularity(u32),
210//     MinSpeechiness(f32),
211//     MaxSpeechiness(f32),
212//     TargetSpeechiness(f32),
213//     MinTempo(f32),
214//     MaxTempo(f32),
215//     TargetTempo(f32),
216//     MinTimeSignature(u32),
217//     MaxTimeSignature(u32),
218//     TargetTimeSignature(u32),
219//     MinValence(f32),
220//     MaxValence(f32),
221//     TargetValence(f32),
222// }
223
224/// Represents what feature exactly is set.
225#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoStaticStr)]
226#[strum(serialize_all = "snake_case")]
227pub enum FeatureKind {
228    Acousticness,
229    Danceability,
230    DurationMs,
231    Energy,
232    Instrumentalness,
233    Key,
234    Liveness,
235    Loudness,
236    Mode,
237    Popularity,
238    Speechiness,
239    Tempo,
240    TimeSignature,
241    Valence,
242}
243
244/// Represents the value of a feature. They can be either floats or values.
245///
246/// The value can be directly specified (e.g. 0, 100, 0.5, Mode::Major etc.), as this type
247/// implements `From` for various type.
248///
249/// **Note:** You *must* use the right values yourself for each option. They can either take
250/// integers from 0 - 100, floats from 0 - 1, or special types (like [`Mode`](crate::model::audio::Mode))
251///
252/// In order to know what values each feature takes, you can use
253/// [this](https://developer.spotify.com/documentation/web-api/reference/get-recommendations)
254/// page.
255///
256/// # Example
257///
258/// ```rs
259/// let recommendations = recommendations(Seed::artists(&["59XQUEHhy5830QsAsmhe2M"]))
260/// .features(&[
261///     Feature::min(FeatureKind::Energy, 0.5),
262///     Feature::max(FeatureKind::Popularity, 64),
263///     Feature::target(FeatureKind::Mode, Mode::Major),
264/// ])
265/// .get(spotify)
266/// .await?;
267/// ```
268#[derive(Clone, Copy, Debug, Serialize)]
269#[serde(untagged)]
270pub enum FeatureValue {
271    Float(f32),
272    Int(u32),
273}
274
275impl From<Vec<Feature>> for Features {
276    fn from(value: Vec<Feature>) -> Self {
277        Self(value)
278    }
279}
280
281impl<const N: usize> From<[Feature; N]> for Features {
282    fn from(value: [Feature; N]) -> Self {
283        Self(value.to_vec())
284    }
285}
286
287impl From<&[Feature]> for Features {
288    fn from(value: &[Feature]) -> Self {
289        Self(value.to_vec())
290    }
291}
292
293/// Represesnts a list of features. You can use an array or vector instead of it,
294/// as this type implements the `From` trait.
295#[derive(Clone, Debug, Default)]
296pub struct Features(Vec<Feature>);
297
298impl Serialize for Features {
299    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
300    where
301        S: serde::Serializer,
302    {
303        let mut map = HashMap::new();
304
305        for element in &self.0 {
306            let kind: &'static str = element.kind.into();
307
308            if let Some(target) = element.target {
309                map.insert(format!("target_{kind}"), target);
310            }
311
312            if let Some(min) = element.min {
313                map.insert(format!("min_{kind}"), min);
314            }
315
316            if let Some(max) = element.max {
317                map.insert(format!("max_{kind}"), max);
318            }
319        }
320
321        let mut serialized_map = serializer.serialize_map(Some(map.len()))?;
322
323        for (k, v) in map {
324            serialized_map.serialize_entry(&k, &v)?
325        }
326
327        serialized_map.end()
328    }
329}
330
331#[derive(Clone, Copy, Debug)]
332pub struct Feature {
333    kind: FeatureKind,
334    target: Option<FeatureValue>,
335    min: Option<FeatureValue>,
336    max: Option<FeatureValue>,
337}
338
339impl From<u32> for FeatureValue {
340    fn from(value: u32) -> Self {
341        Self::Int(value)
342    }
343}
344
345impl From<f32> for FeatureValue {
346    fn from(value: f32) -> Self {
347        Self::Float(value)
348    }
349}
350
351impl From<Mode> for FeatureValue {
352    fn from(value: Mode) -> Self {
353        Self::Int(value as u32)
354    }
355}
356
357impl Feature {
358    pub fn new<T: Into<FeatureValue>>(
359        kind: FeatureKind,
360        target: Option<T>,
361        min: Option<T>,
362        max: Option<T>,
363    ) -> Self {
364        Self {
365            kind,
366            target: target.map(Into::into),
367            min: min.map(Into::into),
368            max: max.map(Into::into),
369        }
370    }
371
372    pub fn target<T: Into<FeatureValue>>(kind: FeatureKind, target: T) -> Self {
373        Self {
374            kind,
375            target: Some(target.into()),
376            min: None,
377            max: None,
378        }
379    }
380
381    pub fn min<T: Into<FeatureValue>>(kind: FeatureKind, min: T) -> Self {
382        Self {
383            kind,
384            target: None,
385            min: Some(min.into()),
386            max: None,
387        }
388    }
389
390    pub fn max<T: Into<FeatureValue>>(kind: FeatureKind, max: T) -> Self {
391        Self {
392            kind,
393            target: None,
394            min: None,
395            max: Some(max.into()),
396        }
397    }
398
399    pub fn exact<T: Into<FeatureValue>>(kind: FeatureKind, value: T) -> Self {
400        let value = value.into();
401
402        Self {
403            kind,
404            target: Some(value),
405            min: Some(value),
406            max: Some(value),
407        }
408    }
409}
410
411#[derive(Clone, Debug, Default, Serialize)]
412pub struct TrackEndpoint {
413    #[serde(skip)]
414    pub(crate) id: String,
415    pub(crate) market: Option<String>,
416}
417
418impl TrackEndpoint {
419    #[doc = include_str!("../docs/market.md")]
420    pub fn market(mut self, market: impl Into<String>) -> Self {
421        self.market = Some(market.into());
422        self
423    }
424
425    #[doc = include_str!("../docs/send.md")]
426    pub async fn get(self, spotify: &Client<impl AuthFlow>) -> Result<Track> {
427        spotify.get(format!("/tracks/{}", self.id), self).await
428    }
429}
430#[derive(Clone, Debug, Default, Serialize)]
431pub struct TracksEndpoint {
432    pub(crate) ids: String,
433    pub(crate) market: Option<String>,
434}
435
436impl TracksEndpoint {
437    #[doc = include_str!("../docs/market.md")]
438    pub fn market(mut self, market: impl Into<String>) -> Self {
439        self.market = Some(market.into());
440        self
441    }
442
443    #[doc = include_str!("../docs/send.md")]
444    pub async fn get(self, spotify: &Client<impl AuthFlow>) -> Result<Vec<Track>> {
445        spotify
446            .get("/tracks".to_owned(), self)
447            .await
448            .map(|t: Tracks| t.tracks)
449    }
450}
451
452#[derive(Clone, Debug, Default, Serialize)]
453pub struct SavedTracksEndpoint {
454    pub(crate) market: Option<String>,
455    pub(crate) limit: Option<u32>,
456    pub(crate) offset: Option<u32>,
457}
458
459impl SavedTracksEndpoint {
460    #[doc = include_str!("../docs/market.md")]
461    pub fn market(mut self, market: impl Into<String>) -> Self {
462        self.market = Some(market.into());
463        self
464    }
465
466    #[doc = include_str!("../docs/limit.md")]
467    pub fn limit(mut self, limit: u32) -> Self {
468        self.limit = Some(limit);
469        self
470    }
471
472    #[doc = include_str!("../docs/offset.md")]
473    pub fn offset(mut self, offset: u32) -> Self {
474        self.offset = Some(offset);
475        self
476    }
477
478    #[doc = include_str!("../docs/send.md")]
479    pub async fn get(
480        self,
481        spotify: &Client<impl AuthFlow + Authorised>,
482    ) -> Result<Page<SavedTrack>> {
483        spotify.get("/me/tracks".to_owned(), self).await
484    }
485}
486
487#[derive(Clone, Debug, Default, Serialize)]
488pub struct RecommendationsEndpoint<S: SeedType> {
489    pub(crate) seed_artists: Option<String>,
490    pub(crate) seed_genres: Option<String>,
491    pub(crate) seed_tracks: Option<String>,
492    pub(crate) limit: Option<u32>,
493    pub(crate) market: Option<String>,
494    #[serde(flatten)]
495    pub(crate) features: Option<Features>,
496    #[serde(skip)]
497    pub(crate) marker: PhantomData<S>,
498}
499
500impl RecommendationsEndpoint<SeedArtists> {
501    #[doc = include_str!("../docs/seed_limit.md")]
502    pub fn seed_genres<T: AsRef<str>>(mut self, genres: &[T]) -> Self {
503        self.seed_genres = Some(query_list(genres));
504        self
505    }
506
507    #[doc = include_str!("../docs/seed_limit.md")]
508    pub fn seed_tracks<T: AsRef<str>>(mut self, track_ids: &[T]) -> Self {
509        self.seed_tracks = Some(query_list(track_ids));
510        self
511    }
512}
513
514impl RecommendationsEndpoint<SeedGenres> {
515    #[doc = include_str!("../docs/seed_limit.md")]
516    pub fn seed_artists<T: AsRef<str>>(mut self, artist_ids: &[T]) -> Self {
517        self.seed_genres = Some(query_list(artist_ids));
518        self
519    }
520
521    #[doc = include_str!("../docs/seed_limit.md")]
522    pub fn seed_tracks<T: AsRef<str>>(mut self, track_ids: &[T]) -> Self {
523        self.seed_tracks = Some(query_list(track_ids));
524        self
525    }
526}
527
528impl RecommendationsEndpoint<SeedTracks> {
529    #[doc = include_str!("../docs/seed_limit.md")]
530    pub fn seed_genres<T: AsRef<str>>(mut self, genres: &[T]) -> Self {
531        self.seed_genres = Some(query_list(genres));
532        self
533    }
534
535    #[doc = include_str!("../docs/seed_limit.md")]
536    pub fn seed_artists<T: AsRef<str>>(mut self, artist_ids: &[T]) -> Self {
537        self.seed_genres = Some(query_list(artist_ids));
538        self
539    }
540}
541
542impl<S: SeedType> RecommendationsEndpoint<S> {
543    #[doc = include_str!("../docs/limit.md")]
544    pub fn limit(mut self, limit: u32) -> Self {
545        self.limit = Some(limit);
546        self
547    }
548
549    #[doc = include_str!("../docs/market.md")]
550    pub fn market(mut self, market: impl Into<String>) -> Self {
551        self.market = Some(market.into());
552        self
553    }
554
555    /// A list of [`Features`](Feature). Read more about the available features
556    /// [here](https://developer.spotify.com/documentation/web-api/reference/get-recommendations).
557    pub fn features(mut self, features: &[Feature]) -> Self {
558        self.features = Some(features.into());
559        self
560    }
561
562    #[doc = include_str!("../docs/send.md")]
563    pub async fn get(self, spotify: &Client<impl AuthFlow>) -> Result<Recommendations> {
564        spotify.get("/recommendations".to_owned(), self).await
565    }
566}