spotify_cli/spotify/
playback.rs1use 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::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
23impl PlaybackClient {
24 pub fn new(http: HttpClient, auth: AuthService) -> Self {
25 Self { http, auth }
26 }
27
28 pub fn play(&self) -> Result<()> {
29 self.send(Method::PUT, "/me/player/play", None)
30 }
31
32 pub fn pause(&self) -> Result<()> {
33 self.send(Method::PUT, "/me/player/pause", None)
34 }
35
36 pub fn next(&self) -> Result<()> {
37 self.send(Method::POST, "/me/player/next", None)
38 }
39
40 pub fn previous(&self) -> Result<()> {
41 self.send(Method::POST, "/me/player/previous", None)
42 }
43
44 pub fn play_context(&self, uri: &str) -> Result<()> {
45 let body = json!({ "context_uri": uri });
46 self.send(Method::PUT, "/me/player/play", Some(body))
47 }
48
49 pub fn play_track(&self, uri: &str) -> Result<()> {
50 let body = json!({ "uris": [uri] });
51 self.send(Method::PUT, "/me/player/play", Some(body))
52 }
53
54 pub fn status(&self) -> Result<PlayerStatus> {
55 let token = self.auth.token()?;
56 let url = format!("{}/me/player", api_base());
57
58 let response = self
59 .http
60 .get(url)
61 .bearer_auth(token.access_token)
62 .send()
63 .context("spotify status request failed")?;
64
65 if response.status() == reqwest::StatusCode::NO_CONTENT {
66 return Ok(PlayerStatus {
67 is_playing: false,
68 track: None,
69 device: None,
70 progress_ms: None,
71 repeat_state: None,
72 shuffle_state: None,
73 });
74 }
75
76 if !response.status().is_success() {
77 let status = response.status();
78 let body = response.text().unwrap_or_else(|_| "<no body>".to_string());
79 bail!(format_api_error("spotify status failed", status, &body));
80 }
81
82 let payload: SpotifyPlayerStatus = response.json()?;
83 Ok(payload.into())
84 }
85
86 pub fn shuffle(&self, state: bool) -> Result<()> {
87 let path = format!("/me/player/shuffle?state={}", state);
88 self.send(Method::PUT, &path, None)
89 }
90
91 pub fn repeat(&self, state: &str) -> Result<()> {
92 let path = format!("/me/player/repeat?state={}", state);
93 self.send(Method::PUT, &path, None)
94 }
95
96 fn send(&self, method: Method, path: &str, body: Option<serde_json::Value>) -> Result<()> {
97 let token = self.auth.token()?;
98 let url = format!("{}{}", api_base(), path);
99
100 let mut request = self.http.request(method, url).bearer_auth(token.access_token);
101 if let Some(body) = body {
102 request = request.json(&body);
103 } else {
104 request = request.body(Vec::new());
105 }
106
107 let response = request.send().context("spotify request failed")?;
108
109 if response.status().is_success() {
110 return Ok(());
111 }
112
113 let status = response.status();
114 let body = response.text().unwrap_or_else(|_| "<no body>".to_string());
115 bail!(format_api_error("spotify request failed", status, &body))
116 }
117}
118
119#[derive(Debug, Deserialize)]
120struct SpotifyPlayerStatus {
121 #[serde(default)]
122 is_playing: bool,
123 progress_ms: Option<u32>,
124 item: Option<SpotifyTrack>,
125 device: Option<SpotifyDevice>,
126 repeat_state: Option<String>,
127 shuffle_state: Option<bool>,
128}
129
130#[derive(Debug, Deserialize)]
131struct SpotifyTrack {
132 id: Option<String>,
133 name: String,
134 duration_ms: Option<u32>,
135 album: Option<SpotifyAlbum>,
136 artists: Vec<SpotifyArtist>,
137}
138
139#[derive(Debug, Deserialize)]
140struct SpotifyArtist {
141 name: String,
142}
143
144#[derive(Debug, Deserialize)]
145struct SpotifyDevice {
146 id: String,
147 name: String,
148 volume_percent: Option<u32>,
149}
150
151#[derive(Debug, Deserialize)]
152struct SpotifyAlbum {
153 name: String,
154}
155
156impl From<SpotifyPlayerStatus> for PlayerStatus {
157 fn from(value: SpotifyPlayerStatus) -> Self {
158 let track = value.item.and_then(|item| {
159 item.id.map(|id| Track {
160 id,
161 name: item.name,
162 album: item.album.map(|album| album.name),
163 artists: item.artists.into_iter().map(|a| a.name).collect(),
164 duration_ms: item.duration_ms,
165 })
166 });
167
168 let device = value.device.map(|device| Device {
169 id: device.id,
170 name: device.name,
171 volume_percent: device.volume_percent,
172 });
173
174 PlayerStatus {
175 is_playing: value.is_playing,
176 track,
177 device,
178 progress_ms: value.progress_ms,
179 repeat_state: value.repeat_state,
180 shuffle_state: value.shuffle_state,
181 }
182 }
183}