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