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
12#[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}