1use anyhow::{Context, bail};
2use reqwest::Method;
3use reqwest::blocking::Client as HttpClient;
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#[derive(Debug, Clone)]
17pub struct PlaybackClient {
18 http: HttpClient,
19 auth: AuthService,
20}
21
22#[derive(Debug)]
23pub struct QueueState {
24 pub now_playing: Option<Track>,
25 pub queue: Vec<Track>,
26}
27
28impl PlaybackClient {
29 pub fn new(http: HttpClient, auth: AuthService) -> Self {
30 Self { http, auth }
31 }
32
33 pub fn play(&self) -> Result<()> {
34 self.send(Method::PUT, "/me/player/play", None)
35 }
36
37 pub fn pause(&self) -> Result<()> {
38 self.send(Method::PUT, "/me/player/pause", None)
39 }
40
41 pub fn next(&self) -> Result<()> {
42 self.send(Method::POST, "/me/player/next", None)
43 }
44
45 pub fn previous(&self) -> Result<()> {
46 self.send(Method::POST, "/me/player/previous", None)
47 }
48
49 pub fn play_context(&self, uri: &str) -> Result<()> {
50 let body = json!({ "context_uri": uri });
51 self.send(Method::PUT, "/me/player/play", Some(body))
52 }
53
54 pub fn play_track(&self, uri: &str) -> Result<()> {
55 let body = json!({ "uris": [uri] });
56 self.send(Method::PUT, "/me/player/play", Some(body))
57 }
58
59 pub fn status(&self) -> Result<PlayerStatus> {
60 let token = self.auth.token()?;
61 let url = format!("{}/me/player", api_base());
62
63 let response = self
64 .http
65 .get(url)
66 .bearer_auth(token.access_token)
67 .send()
68 .context("spotify status request failed")?;
69
70 if response.status() == reqwest::StatusCode::NO_CONTENT {
71 return Ok(PlayerStatus {
72 is_playing: false,
73 track: None,
74 device: None,
75 context: None,
76 progress_ms: None,
77 repeat_state: None,
78 shuffle_state: None,
79 });
80 }
81
82 if !response.status().is_success() {
83 let status = response.status();
84 let body = response.text().unwrap_or_else(|_| "<no body>".to_string());
85 bail!(format_api_error("spotify status failed", status, &body));
86 }
87
88 let payload: SpotifyPlayerStatus = response.json()?;
89 Ok(payload.into())
90 }
91
92 pub fn shuffle(&self, state: bool) -> Result<()> {
93 let path = format!("/me/player/shuffle?state={}", state);
94 self.send(Method::PUT, &path, None)
95 }
96
97 pub fn repeat(&self, state: &str) -> Result<()> {
98 let path = format!("/me/player/repeat?state={}", state);
99 self.send(Method::PUT, &path, None)
100 }
101
102 pub fn queue(&self, limit: u32) -> Result<QueueState> {
103 let token = self.auth.token()?;
104 let url = format!("{}/me/player/queue", api_base());
105
106 let response = self
107 .http
108 .get(url)
109 .bearer_auth(token.access_token)
110 .send()
111 .context("spotify queue request failed")?;
112
113 if !response.status().is_success() {
114 let status = response.status();
115 let body = response.text().unwrap_or_else(|_| "<no body>".to_string());
116 bail!(format_api_error("spotify queue failed", status, &body));
117 }
118
119 let payload: SpotifyQueueResponse = response.json()?;
120 let now_playing = payload.currently_playing.and_then(map_track);
121 let mut queue = Vec::new();
122 for track in payload.queue {
123 if let Some(track) = map_track(track) {
124 queue.push(track);
125 }
126 if queue.len() >= limit as usize {
127 break;
128 }
129 }
130 Ok(QueueState { now_playing, queue })
131 }
132
133 fn send(&self, method: Method, path: &str, body: Option<serde_json::Value>) -> Result<()> {
134 let token = self.auth.token()?;
135 let url = format!("{}{}", api_base(), path);
136
137 let mut request = self
138 .http
139 .request(method, url)
140 .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.artists.into_iter().filter_map(|a| a.id).collect(),
229 duration_ms: item.duration_ms,
230 }
231 })
232 });
233
234 let device = value.device.map(|device| Device {
235 id: device.id,
236 name: device.name,
237 volume_percent: device.volume_percent,
238 });
239
240 let context = value.context.and_then(|context| {
241 let kind = context.kind?;
242 let uri = context.uri?;
243 Some(PlaybackContext { kind, uri })
244 });
245
246 PlayerStatus {
247 is_playing: value.is_playing,
248 track,
249 device,
250 context,
251 progress_ms: value.progress_ms,
252 repeat_state: value.repeat_state,
253 shuffle_state: value.shuffle_state,
254 }
255 }
256}
257
258fn map_track(item: SpotifyTrack) -> Option<Track> {
259 item.id.map(|id| {
260 let (album, album_id) = match item.album {
261 Some(album) => (Some(album.name), album.id),
262 None => (None, None),
263 };
264
265 Track {
266 id,
267 name: item.name,
268 album,
269 album_id,
270 artists: item.artists.iter().map(|a| a.name.clone()).collect(),
271 artist_ids: item.artists.into_iter().filter_map(|a| a.id).collect(),
272 duration_ms: item.duration_ms,
273 }
274 })
275}