use anyhow::{Context, Result};
use rspotify::AuthCodePkceSpotify;
use rspotify::prelude::*;
use crate::auth;
use crate::config::Config;
pub async fn run(cfg: &Config) -> Result<()> {
let spotify = auth::authed_client(cfg).await?;
println!("{}", execute(&spotify).await?);
Ok(())
}
async fn execute(spotify: &AuthCodePkceSpotify) -> Result<String> {
let devices = spotify
.device()
.await
.context("failed to fetch the device list")?;
if devices.is_empty() {
return Ok("No playable devices. Please open the Spotify app".to_string());
}
let mut lines = vec!["Available devices:".to_string()];
for d in &devices {
let type_label = format!("{:?}", d._type);
lines.push(render_device(
&d.name,
&type_label,
d.volume_percent,
d.is_active,
d.is_restricted,
));
}
Ok(lines.join("\n"))
}
fn render_device(
name: &str,
type_label: &str,
vol: Option<u32>,
is_active: bool,
is_restricted: bool,
) -> String {
let mark = if is_active { "●" } else { "○" };
let vol_s = match vol {
Some(v) => format!("vol {v}%"),
None => "vol -".to_string(),
};
let mut line = format!(" {mark} {name} [{type_label}] {vol_s}");
if is_active {
line.push_str(" (active)");
}
if is_restricted {
line.push_str(" (restricted)");
}
line
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_fixtures as fx;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
async fn mount_devices(server: &MockServer, body: serde_json::Value) {
Mock::given(method("GET"))
.and(path("/me/player/devices"))
.respond_with(ResponseTemplate::new(200).set_body_json(body))
.mount(server)
.await;
}
#[tokio::test]
async fn execute_reports_empty_device_list() {
let server = MockServer::start().await;
mount_devices(&server, fx::devices_envelope(vec![])).await;
let client = crate::auth::test_client(&server.uri()).await;
let out = execute(&client).await.unwrap();
assert!(out.contains("No playable devices"), "{out}");
}
#[tokio::test]
async fn execute_lists_devices() {
let server = MockServer::start().await;
mount_devices(
&server,
fx::devices_envelope(vec![fx::device("d1", "My Mac", true)]),
)
.await;
let client = crate::auth::test_client(&server.uri()).await;
let out = execute(&client).await.unwrap();
assert!(out.contains("Available devices:"), "{out}");
assert!(out.contains("My Mac"), "{out}");
assert!(out.contains("(active)"), "{out}");
}
#[test]
fn render_device_active_with_volume() {
let out = render_device("MacBook Pro", "Computer", Some(65), true, false);
assert_eq!(out, " ● MacBook Pro [Computer] vol 65% (active)");
}
#[test]
fn render_device_inactive_without_volume() {
let out = render_device("Speaker", "Speaker", None, false, false);
assert_eq!(out, " ○ Speaker [Speaker] vol -");
}
#[test]
fn render_device_restricted() {
let out = render_device("TV", "Tv", Some(40), false, true);
assert_eq!(out, " ○ TV [Tv] vol 40% (restricted)");
}
}