1use anyhow::{bail, Context};
2use reqwest::blocking::Client as HttpClient;
3use reqwest::Method;
4use serde::Deserialize;
5use serde_json::json;
6
7use crate::domain::device::Device;
8use crate::domain::player::{PlaybackContext, PlayerStatus};
9use crate::domain::track::Track;
10use crate::error::Result;
11use crate::spotify::auth::AuthService;
12use crate::spotify::base::api_base;
13use crate::spotify::error::format_api_error;
14
15
16#[derive(Debug, Clone)]
18pub struct PlaybackClient {
19 http: HttpClient,
20 auth: AuthService,
21}
22
23#[derive(Debug)]
24pub struct QueueState {
25 pub now_playing: Option<Track>,
26 pub queue: Vec<Track>,
27}
28
29impl PlaybackClient {
30 pub fn new(http: HttpClient, auth: AuthService) -> Self {
31 Self { http, auth }
32 }
33
34 pub fn play(&self) -> Result<()> {
35 self.send(Method::PUT, "/me/player/play", None)
36 }
37
38 pub fn pause(&self) -> Result<()> {
39 self.send(Method::PUT, "/me/player/pause", None)
40 }
41
42 pub fn next(&self) -> Result<()> {
43 self.send(Method::POST, "/me/player/next", None)
44 }
45
46 pub fn previous(&self) -> Result<()> {
47 self.send(Method::POST, "/me/player/previous", None)
48 }
49
50 pub fn play_context(&self, uri: &str) -> Result<()> {
51 let body = json!({ "context_uri": uri });
52 self.send(Method::PUT, "/me/player/play", Some(body))
53 }
54
55 pub fn play_track(&self, uri: &str) -> Result<()> {
56 let body = json!({ "uris": [uri] });
57 self.send(Method::PUT, "/me/player/play", Some(body))
58 }
59
60 pub fn status(&self) -> Result<PlayerStatus> {
61 let token = self.auth.token()?;
62 let url = format!("{}/me/player", api_base());
63
64 let response = self
65 .http
66 .get(url)
67 .bearer_auth(token.access_token)
68 .send()
69 .context("spotify status request failed")?;
70
71 if response.status() == reqwest::StatusCode::NO_CONTENT {
72 return Ok(PlayerStatus {
73 is_playing: false,
74 track: None,
75 device: None,
76 context: None,
77 progress_ms: None,
78 repeat_state: None,
79 shuffle_state: None,
80 });
81 }
82
83 if !response.status().is_success() {
84 let status = response.status();
85 let body = response.text().unwrap_or_else(|_| "<no body>".to_string());
86 bail!(format_api_error("spotify status failed", status, &body));
87 }
88
89 let payload: SpotifyPlayerStatus = response.json()?;
90 Ok(payload.into())
91 }
92
93 pub fn shuffle(&self, state: bool) -> Result<()> {
94 let path = format!("/me/player/shuffle?state={}", state);
95 self.send(Method::PUT, &path, None)
96 }
97
98 pub fn repeat(&self, state: &str) -> Result<()> {
99 let path = format!("/me/player/repeat?state={}", state);
100 self.send(Method::PUT, &path, None)
101 }
102
103 pub fn queue(&self, limit: u32) -> Result<QueueState> {
104 let token = self.auth.token()?;
105 let url = format!("{}/me/player/queue", api_base());
106
107 let response = self
108 .http
109 .get(url)
110 .bearer_auth(token.access_token)
111 .send()
112 .context("spotify queue request failed")?;
113
114 if !response.status().is_success() {
115 let status = response.status();
116 let body = response.text().unwrap_or_else(|_| "<no body>".to_string());
117 bail!(format_api_error("spotify queue failed", status, &body));
118 }
119
120 let payload: SpotifyQueueResponse = response.json()?;
121 let now_playing = payload
122 .currently_playing
123 .and_then(map_track);
124 let mut queue = Vec::new();
125 for track in payload.queue {
126 if let Some(track) = map_track(track) {
127 queue.push(track);
128 }
129 if queue.len() >= limit as usize {
130 break;
131 }
132 }
133 Ok(QueueState { now_playing, queue })
134 }
135
136 fn send(&self, method: Method, path: &str, body: Option<serde_json::Value>) -> Result<()> {
137 let token = self.auth.token()?;
138 let url = format!("{}{}", api_base(), path);
139
140 let mut request = self.http.request(method, url).bearer_auth(token.access_token);
141 if let Some(body) = body {
142 request = request.json(&body);
143 } else {
144 request = request.body(Vec::new());
145 }
146
147 let response = request.send().context("spotify request failed")?;
148
149 if response.status().is_success() {
150 return Ok(());
151 }
152
153 let status = response.status();
154 let body = response.text().unwrap_or_else(|_| "<no body>".to_string());
155 bail!(format_api_error("spotify request failed", status, &body))
156 }
157}
158
159#[derive(Debug, Deserialize)]
160struct SpotifyPlayerStatus {
161 #[serde(default)]
162 is_playing: bool,
163 progress_ms: Option<u32>,
164 item: Option<SpotifyTrack>,
165 device: Option<SpotifyDevice>,
166 context: Option<SpotifyContext>,
167 repeat_state: Option<String>,
168 shuffle_state: Option<bool>,
169}
170
171#[derive(Debug, Deserialize)]
172struct SpotifyTrack {
173 id: Option<String>,
174 name: String,
175 duration_ms: Option<u32>,
176 album: Option<SpotifyAlbum>,
177 artists: Vec<SpotifyArtist>,
178}
179
180#[derive(Debug, Deserialize)]
181struct SpotifyArtist {
182 id: Option<String>,
183 name: String,
184}
185
186#[derive(Debug, Deserialize)]
187struct SpotifyDevice {
188 id: String,
189 name: String,
190 volume_percent: Option<u32>,
191}
192
193#[derive(Debug, Deserialize)]
194struct SpotifyAlbum {
195 id: Option<String>,
196 name: String,
197}
198
199#[derive(Debug, Deserialize)]
200struct SpotifyContext {
201 #[serde(rename = "type")]
202 kind: Option<String>,
203 uri: Option<String>,
204}
205
206#[derive(Debug, Deserialize)]
207struct SpotifyQueueResponse {
208 currently_playing: Option<SpotifyTrack>,
209 #[serde(default)]
210 queue: Vec<SpotifyTrack>,
211}
212
213impl From<SpotifyPlayerStatus> for PlayerStatus {
214 fn from(value: SpotifyPlayerStatus) -> Self {
215 let track = value.item.and_then(|item| {
216 item.id.map(|id| {
217 let (album, album_id) = match item.album {
218 Some(album) => (Some(album.name), album.id),
219 None => (None, None),
220 };
221
222 Track {
223 id,
224 name: item.name,
225 album,
226 album_id,
227 artists: item.artists.iter().map(|a| a.name.clone()).collect(),
228 artist_ids: item
229 .artists
230 .into_iter()
231 .filter_map(|a| a.id)
232 .collect(),
233 duration_ms: item.duration_ms,
234 }
235 })
236 });
237
238 let device = value.device.map(|device| Device {
239 id: device.id,
240 name: device.name,
241 volume_percent: device.volume_percent,
242 });
243
244 let context = value.context.and_then(|context| {
245 let kind = context.kind?;
246 let uri = context.uri?;
247 Some(PlaybackContext { kind, uri })
248 });
249
250 PlayerStatus {
251 is_playing: value.is_playing,
252 track,
253 device,
254 context,
255 progress_ms: value.progress_ms,
256 repeat_state: value.repeat_state,
257 shuffle_state: value.shuffle_state,
258 }
259 }
260}
261
262fn map_track(item: SpotifyTrack) -> Option<Track> {
263 item.id.map(|id| {
264 let (album, album_id) = match item.album {
265 Some(album) => (Some(album.name), album.id),
266 None => (None, None),
267 };
268
269 Track {
270 id,
271 name: item.name,
272 album,
273 album_id,
274 artists: item.artists.iter().map(|a| a.name.clone()).collect(),
275 artist_ids: item.artists.into_iter().filter_map(|a| a.id).collect(),
276 duration_ms: item.duration_ms,
277 }
278 })
279}