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 follow(&self, playlist_id: &str) -> Result<()> {
90 let token = self.auth.token()?;
91 let url = format!("{}/playlists/{playlist_id}/followers", api_base());
92
93 let response = self
94 .http
95 .put(url)
96 .bearer_auth(token.access_token)
97 .body(Vec::new())
98 .send()?;
99
100 if !response.status().is_success() {
101 let status = response.status();
102 let body = response.text().unwrap_or_else(|_| "<no body>".to_string());
103 bail!(format_api_error("spotify playlist follow failed", status, &body));
104 }
105 Ok(())
106 }
107
108 pub fn unfollow(&self, playlist_id: &str) -> Result<()> {
109 let token = self.auth.token()?;
110 let url = format!("{}/playlists/{playlist_id}/followers", api_base());
111
112 let response = self
113 .http
114 .delete(url)
115 .bearer_auth(token.access_token)
116 .body(Vec::new())
117 .send()?;
118
119 if !response.status().is_success() {
120 let status = response.status();
121 let body = response.text().unwrap_or_else(|_| "<no body>".to_string());
122 bail!(format_api_error("spotify playlist unfollow failed", status, &body));
123 }
124 Ok(())
125 }
126
127 pub fn add_tracks(&self, playlist_id: &str, uris: &[String]) -> Result<()> {
128 let token = self.auth.token()?;
129 let url = format!("{}/playlists/{playlist_id}/tracks", api_base());
130
131 let response = self
132 .http
133 .post(url)
134 .bearer_auth(token.access_token)
135 .json(&serde_json::json!({ "uris": uris }))
136 .send()?;
137
138 if !response.status().is_success() {
139 let status = response.status();
140 let body = response.text().unwrap_or_else(|_| "<no body>".to_string());
141 bail!(format_api_error("spotify playlist add failed", status, &body));
142 }
143 Ok(())
144 }
145}
146
147#[derive(Debug, Deserialize)]
148struct PlaylistsResponse {
149 items: Vec<SpotifyPlaylist>,
150 next: Option<String>,
151}
152
153#[derive(Debug, Deserialize)]
154struct SpotifyPlaylist {
155 id: String,
156 name: String,
157 owner: Option<SpotifyOwner>,
158 #[serde(default)]
159 collaborative: bool,
160 public: Option<bool>,
161}
162
163#[derive(Debug, Deserialize)]
164struct SpotifyOwner {
165 display_name: Option<String>,
166}
167
168#[derive(Debug, Deserialize)]
169struct PlaylistDetailResponse {
170 id: String,
171 name: String,
172 uri: String,
173 owner: Option<SpotifyOwner>,
174 tracks: Option<SpotifyTracks>,
175 #[serde(default)]
176 collaborative: bool,
177 public: Option<bool>,
178}
179
180#[derive(Debug, Deserialize)]
181struct SpotifyTracks {
182 total: u32,
183}