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
66pub 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
79pub 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#[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, 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#[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#[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 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}