spotify_cli/cli/commands/player/
playback.rs

1//! Playback control commands: play, pause, toggle, next, previous
2
3use crate::endpoints::player::{
4    pause_playback, skip_to_next, skip_to_previous, start_resume_playback,
5};
6use crate::io::output::{ErrorKind, Response};
7
8use crate::cli::commands::{init_pin_store, now_playing, with_client};
9
10/// Convert a Spotify URL to a Spotify URI, or return input if already a URI
11fn url_to_uri(input: &str) -> String {
12    if input.contains("open.spotify.com") {
13        let parts: Vec<&str> = input.split('/').collect();
14        if parts.len() >= 2 {
15            let id = parts.last().unwrap_or(&"").split('?').next().unwrap_or("");
16            let resource_type = parts.get(parts.len() - 2).unwrap_or(&"");
17            return format!("spotify:{}:{}", resource_type, id);
18        }
19    }
20    input.to_string()
21}
22
23pub async fn player_next() -> Response {
24    with_client(|client| async move {
25        match skip_to_next::skip_to_next(&client).await {
26            Ok(_) => Response::success(204, "Skipped to next track"),
27            Err(e) => Response::from_http_error(&e, "Failed to skip to next track"),
28        }
29    })
30    .await
31}
32
33pub async fn player_previous() -> Response {
34    with_client(|client| async move {
35        match skip_to_previous::skip_to_previous(&client).await {
36            Ok(_) => Response::success(204, "Skipped to previous track"),
37            Err(e) => Response::from_http_error(&e, "Failed to skip to previous track"),
38        }
39    })
40    .await
41}
42
43pub async fn player_toggle() -> Response {
44    with_client(|client| async move {
45        let playing = match now_playing::is_playing(&client).await {
46            Ok(p) => p,
47            Err(e) => return e,
48        };
49
50        if playing {
51            match pause_playback::pause_playback(&client).await {
52                Ok(_) => Response::success(204, "Playback paused"),
53                Err(e) => Response::from_http_error(&e, "Failed to pause playback"),
54            }
55        } else {
56            match start_resume_playback::start_resume_playback(&client, None, None).await {
57                Ok(_) => Response::success(204, "Playback started"),
58                Err(e) => Response::from_http_error(&e, "Failed to start playback"),
59            }
60        }
61    })
62    .await
63}
64
65pub async fn player_play(uri: Option<&str>, pin: Option<&str>) -> Response {
66    // Resolve pin to URI if provided (before auth)
67    let context_uri: Option<String> = if let Some(pin_alias) = pin {
68        let store = match init_pin_store() {
69            Ok(s) => s,
70            Err(e) => return e,
71        };
72
73        match store.find_by_alias(pin_alias) {
74            Some(p) => Some(p.uri()),
75            None => return Response::err(404, "Pin not found", ErrorKind::NotFound),
76        }
77    } else {
78        uri.map(url_to_uri)
79    };
80
81    let has_uri = context_uri.is_some();
82
83    // Determine if this is a track URI (needs uris param) or context URI (album/playlist/artist)
84    let is_track_uri = context_uri
85        .as_ref()
86        .map(|u| u.starts_with("spotify:track:"))
87        .unwrap_or(false);
88
89    with_client(|client| async move {
90        // If no URI provided, check if already playing to avoid 403
91        if !has_uri {
92            match now_playing::is_playing(&client).await {
93                Ok(true) => return Response::success(204, "Already playing"),
94                Ok(false) => {}
95                Err(e) => return e,
96            }
97        }
98
99        // Track URIs must be passed via `uris` param, context URIs via `context_uri`
100        let result = if is_track_uri {
101            let track_uris = vec![context_uri.clone().unwrap()];
102            start_resume_playback::start_resume_playback(&client, None, Some(&track_uris)).await
103        } else {
104            start_resume_playback::start_resume_playback(&client, context_uri.as_deref(), None)
105                .await
106        };
107
108        match result {
109            Ok(_) => {
110                if has_uri {
111                    Response::success(204, "Playing requested content")
112                } else {
113                    Response::success(204, "Playback started")
114                }
115            }
116            Err(e) => Response::from_http_error(&e, "Failed to start playback"),
117        }
118    })
119    .await
120}
121
122pub async fn player_pause() -> Response {
123    with_client(|client| async move {
124        // Check if already paused to avoid 403
125        match now_playing::is_playing(&client).await {
126            Ok(false) => return Response::success(204, "Already paused"),
127            Ok(true) => {}
128            Err(e) => return e,
129        }
130
131        match pause_playback::pause_playback(&client).await {
132            Ok(_) => Response::success(204, "Playback paused"),
133            Err(e) => Response::from_http_error(&e, "Failed to pause playback"),
134        }
135    })
136    .await
137}