spotify_cli/spotify/
albums.rs

1use 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/// Spotify album API client.
12#[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}