spotify_cli/spotify/
albums.rs1use anyhow::bail;
2use reqwest::blocking::Client as HttpClient;
3use serde::Deserialize;
4
5use crate::domain::album::{Album, AlbumTrack};
6use crate::error::Result;
7use crate::spotify::auth::AuthService;
8use crate::spotify::base::api_base;
9use crate::spotify::error::format_api_error;
10
11#[derive(Debug, Clone)]
13pub struct AlbumsClient {
14 http: HttpClient,
15 auth: AuthService,
16}
17
18impl AlbumsClient {
19 pub fn new(http: HttpClient, auth: AuthService) -> Self {
20 Self { http, auth }
21 }
22
23 pub fn get(&self, album_id: &str) -> Result<Album> {
24 let token = self.auth.token()?;
25 let url = format!("{}/albums/{album_id}", api_base());
26
27 let access_token = token.access_token.clone();
28 let response = self
29 .http
30 .get(url)
31 .bearer_auth(access_token.clone())
32 .send()?;
33
34 if !response.status().is_success() {
35 let status = response.status();
36 let body = response.text().unwrap_or_else(|_| "<no body>".to_string());
37 bail!(format_api_error(
38 "spotify album request failed",
39 status,
40 &body
41 ));
42 }
43
44 let payload: SpotifyAlbum = response.json()?;
45 let tracks = self.fetch_tracks(album_id, &access_token)?;
46 let duration_ms = tracks
47 .iter()
48 .map(|track| track.duration_ms as u64)
49 .sum::<u64>();
50 Ok(Album {
51 id: payload.id,
52 name: payload.name,
53 uri: payload.uri,
54 artists: payload
55 .artists
56 .into_iter()
57 .map(|artist| artist.name)
58 .collect(),
59 release_date: payload.release_date,
60 total_tracks: payload.total_tracks,
61 tracks,
62 duration_ms: Some(duration_ms),
63 })
64 }
65
66 fn fetch_tracks(&self, album_id: &str, access_token: &str) -> Result<Vec<AlbumTrack>> {
67 let mut tracks = Vec::new();
68 let mut url = format!("{}/albums/{album_id}/tracks?limit=50", api_base());
69
70 loop {
71 let response = self.http.get(&url).bearer_auth(access_token).send()?;
72
73 if !response.status().is_success() {
74 let status = response.status();
75 let body = response.text().unwrap_or_else(|_| "<no body>".to_string());
76 bail!(format_api_error(
77 "spotify album tracks failed",
78 status,
79 &body
80 ));
81 }
82
83 let payload: AlbumTracksResponse = response.json()?;
84 tracks.extend(payload.items.into_iter().map(|item| AlbumTrack {
85 name: item.name,
86 duration_ms: item.duration_ms,
87 track_number: item.track_number,
88 }));
89
90 if let Some(next) = payload.next {
91 url = next;
92 } else {
93 break;
94 }
95 }
96
97 Ok(tracks)
98 }
99}
100
101#[derive(Debug, Deserialize)]
102struct SpotifyAlbum {
103 id: String,
104 name: String,
105 uri: String,
106 release_date: Option<String>,
107 total_tracks: Option<u32>,
108 artists: Vec<SpotifyArtistRef>,
109}
110
111#[derive(Debug, Deserialize)]
112struct SpotifyArtistRef {
113 name: String,
114}
115
116#[derive(Debug, Deserialize)]
117struct AlbumTracksResponse {
118 items: Vec<SpotifyAlbumTrack>,
119 next: Option<String>,
120}
121
122#[derive(Debug, Deserialize)]
123struct SpotifyAlbumTrack {
124 name: String,
125 duration_ms: u32,
126 track_number: u32,
127}