spotify_cli/spotify/
playlists.rs1use anyhow::bail;
2use reqwest::blocking::Client as HttpClient;
3use serde::Deserialize;
4
5use crate::domain::playlist::{Playlist, PlaylistDetail};
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 PlaylistsClient {
15 http: HttpClient,
16 auth: AuthService,
17}
18
19impl PlaylistsClient {
20 pub fn new(http: HttpClient, auth: AuthService) -> Self {
21 Self { http, auth }
22 }
23
24 pub fn list_all(&self) -> Result<Vec<Playlist>> {
25 let token = self.auth.token()?;
26 let mut url = format!("{}/me/playlists?limit=50", api_base());
27 let mut playlists = Vec::new();
28
29 loop {
30 let response = self
31 .http
32 .get(&url)
33 .bearer_auth(token.access_token.clone())
34 .send()?;
35
36 if !response.status().is_success() {
37 let status = response.status();
38 let body = response.text().unwrap_or_else(|_| "<no body>".to_string());
39 bail!(format_api_error("spotify playlists request failed", status, &body));
40 }
41
42 let payload: PlaylistsResponse = response.json()?;
43 playlists.extend(payload.items.into_iter().map(|item| Playlist {
44 id: item.id,
45 name: item.name,
46 owner: item.owner.and_then(|owner| owner.display_name),
47 collaborative: item.collaborative,
48 public: item.public,
49 }));
50
51 if let Some(next) = payload.next {
52 url = next;
53 } else {
54 break;
55 }
56 }
57
58 Ok(playlists)
59 }
60
61 pub fn get(&self, playlist_id: &str) -> Result<PlaylistDetail> {
62 let token = self.auth.token()?;
63 let url = format!("{}/playlists/{playlist_id}", api_base());
64
65 let response = self
66 .http
67 .get(url)
68 .bearer_auth(token.access_token)
69 .send()?;
70
71 if !response.status().is_success() {
72 let status = response.status();
73 let body = response.text().unwrap_or_else(|_| "<no body>".to_string());
74 bail!(format_api_error("spotify playlist request failed", status, &body));
75 }
76
77 let payload: PlaylistDetailResponse = response.json()?;
78 Ok(PlaylistDetail {
79 id: payload.id,
80 name: payload.name,
81 uri: payload.uri,
82 owner: payload.owner.and_then(|owner| owner.display_name),
83 tracks_total: payload.tracks.map(|tracks| tracks.total),
84 collaborative: payload.collaborative,
85 public: payload.public,
86 })
87 }
88
89 pub fn create(&self, name: &str, public: Option<bool>) -> Result<PlaylistDetail> {
90 let token = self.auth.token()?;
91 let user_id = self.current_user_id(&token.access_token)?;
92 let url = format!("{}/users/{user_id}/playlists", api_base());
93
94 let mut body = serde_json::json!({ "name": name });
95 if let Some(public) = public {
96 body["public"] = serde_json::json!(public);
97 }
98
99 let response = self
100 .http
101 .post(url)
102 .bearer_auth(token.access_token)
103 .json(&body)
104 .send()?;
105
106 if !response.status().is_success() {
107 let status = response.status();
108 let body = response.text().unwrap_or_else(|_| "<no body>".to_string());
109 bail!(format_api_error("spotify playlist create failed", status, &body));
110 }
111
112 let payload: PlaylistDetailResponse = response.json()?;
113 Ok(PlaylistDetail {
114 id: payload.id,
115 name: payload.name,
116 uri: payload.uri,
117 owner: payload.owner.and_then(|owner| owner.display_name),
118 tracks_total: payload.tracks.map(|tracks| tracks.total),
119 collaborative: payload.collaborative,
120 public: payload.public,
121 })
122 }
123
124 pub fn rename(&self, playlist_id: &str, name: &str) -> Result<()> {
125 let token = self.auth.token()?;
126 let url = format!("{}/playlists/{playlist_id}", api_base());
127 let body = serde_json::json!({ "name": name });
128
129 let response = self
130 .http
131 .put(url)
132 .bearer_auth(token.access_token)
133 .json(&body)
134 .send()?;
135
136 if !response.status().is_success() {
137 let status = response.status();
138 let body = response.text().unwrap_or_else(|_| "<no body>".to_string());
139 bail!(format_api_error("spotify playlist rename failed", status, &body));
140 }
141 Ok(())
142 }
143
144 pub fn delete(&self, playlist_id: &str) -> Result<()> {
145 self.unfollow(playlist_id)
146 }
147
148 pub fn follow(&self, playlist_id: &str) -> Result<()> {
149 let token = self.auth.token()?;
150 let url = format!("{}/playlists/{playlist_id}/followers", api_base());
151
152 let response = self
153 .http
154 .put(url)
155 .bearer_auth(token.access_token)
156 .body(Vec::new())
157 .send()?;
158
159 if !response.status().is_success() {
160 let status = response.status();
161 let body = response.text().unwrap_or_else(|_| "<no body>".to_string());
162 bail!(format_api_error("spotify playlist follow failed", status, &body));
163 }
164 Ok(())
165 }
166
167 pub fn unfollow(&self, playlist_id: &str) -> Result<()> {
168 let token = self.auth.token()?;
169 let url = format!("{}/playlists/{playlist_id}/followers", api_base());
170
171 let response = self
172 .http
173 .delete(url)
174 .bearer_auth(token.access_token)
175 .body(Vec::new())
176 .send()?;
177
178 if !response.status().is_success() {
179 let status = response.status();
180 let body = response.text().unwrap_or_else(|_| "<no body>".to_string());
181 bail!(format_api_error("spotify playlist unfollow failed", status, &body));
182 }
183 Ok(())
184 }
185
186 pub fn add_tracks(&self, playlist_id: &str, uris: &[String]) -> Result<()> {
187 let token = self.auth.token()?;
188 let url = format!("{}/playlists/{playlist_id}/tracks", api_base());
189
190 let response = self
191 .http
192 .post(url)
193 .bearer_auth(token.access_token)
194 .json(&serde_json::json!({ "uris": uris }))
195 .send()?;
196
197 if !response.status().is_success() {
198 let status = response.status();
199 let body = response.text().unwrap_or_else(|_| "<no body>".to_string());
200 bail!(format_api_error("spotify playlist add failed", status, &body));
201 }
202 Ok(())
203 }
204
205 fn current_user_id(&self, access_token: &str) -> Result<String> {
206 let url = format!("{}/me", api_base());
207 let response = self
208 .http
209 .get(url)
210 .bearer_auth(access_token)
211 .send()?;
212
213 if !response.status().is_success() {
214 let status = response.status();
215 let body = response.text().unwrap_or_else(|_| "<no body>".to_string());
216 bail!(format_api_error("spotify profile request failed", status, &body));
217 }
218
219 let payload: SpotifyUser = response.json()?;
220 Ok(payload.id)
221 }
222}
223
224#[derive(Debug, Deserialize)]
225struct PlaylistsResponse {
226 items: Vec<SpotifyPlaylist>,
227 next: Option<String>,
228}
229
230#[derive(Debug, Deserialize)]
231struct SpotifyPlaylist {
232 id: String,
233 name: String,
234 owner: Option<SpotifyOwner>,
235 #[serde(default)]
236 collaborative: bool,
237 public: Option<bool>,
238}
239
240#[derive(Debug, Deserialize)]
241struct SpotifyOwner {
242 display_name: Option<String>,
243}
244
245#[derive(Debug, Deserialize)]
246struct SpotifyUser {
247 id: String,
248}
249
250#[derive(Debug, Deserialize)]
251struct PlaylistDetailResponse {
252 id: String,
253 name: String,
254 uri: String,
255 owner: Option<SpotifyOwner>,
256 tracks: Option<SpotifyTracks>,
257 #[serde(default)]
258 collaborative: bool,
259 public: Option<bool>,
260}
261
262#[derive(Debug, Deserialize)]
263struct SpotifyTracks {
264 total: u32,
265}