spotify_cli/spotify/
playback.rs

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::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/// Spotify playback API client.
17#[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}