mirror of
https://github.com/Felitendo/spotifast.git
synced 2026-11-01 17:33:26 +01:00
Use Spotifast across the native app, commands, packages and documentation. Keep existing state, credential stores, protocol identities and package names; provide both command names and compatible updater payloads. Rename the guides with jekyll-redirect-from and prepare spotifast.rocks as the website domain. Keep product descriptions name-free.
122 lines
4.1 KiB
Rust
122 lines
4.1 KiB
Rust
//! Diagnostic: discover Spotify Connect receivers on the LAN and hand one an
|
|
//! account, then check whether Spotify's device list picks it up.
|
|
//!
|
|
//! cargo run --example connect_probe # just discover
|
|
//! cargo run --example connect_probe -- "House" # discover and activate
|
|
|
|
use std::time::Duration;
|
|
|
|
fn main() -> anyhow::Result<()> {
|
|
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
|
|
let wanted = std::env::args().nth(1);
|
|
|
|
println!("browsing for _spotify-connect._tcp ...");
|
|
let receivers = fastpotify::zeroconf::discover(Duration::from_secs(4))?;
|
|
if receivers.is_empty() {
|
|
println!(" none found");
|
|
return Ok(());
|
|
}
|
|
let http = reqwest::blocking::Client::builder()
|
|
.timeout(Duration::from_secs(8))
|
|
.build()?;
|
|
for receiver in &receivers {
|
|
print!(
|
|
" {} at {}:{}",
|
|
receiver.name, receiver.address, receiver.port
|
|
);
|
|
match fastpotify::zeroconf::get_info(&http, receiver) {
|
|
Ok(info) => println!(
|
|
" [{} {} | token={} | active_user={:?}]",
|
|
info.remote_name, info.device_type, info.token_type, info.active_user
|
|
),
|
|
Err(error) => println!(" (getInfo failed: {error})"),
|
|
}
|
|
}
|
|
|
|
let Some(wanted) = wanted else { return Ok(()) };
|
|
let Some(receiver) = receivers
|
|
.iter()
|
|
.find(|r| r.name.to_lowercase().contains(&wanted.to_lowercase()))
|
|
else {
|
|
println!("no receiver matching {wanted:?}");
|
|
return Ok(());
|
|
};
|
|
|
|
let Some(fastpotify::credentials::Grant::Playback(grant)) =
|
|
stored_grant(fastpotify::credentials::Slot::Playback)?
|
|
else {
|
|
anyhow::bail!("Enable playback in Spotifast first");
|
|
};
|
|
let credentials = fastpotify::zeroconf::Credentials::from_playback(&grant)?;
|
|
println!("\nhanding the account to {} ...", receiver.name);
|
|
let info = fastpotify::zeroconf::get_info(&http, receiver)?;
|
|
match fastpotify::zeroconf::add_user(&http, receiver, &info, &credentials, "Spotifast") {
|
|
Ok(()) => println!(" accepted"),
|
|
Err(error) => {
|
|
println!(" refused: {error}");
|
|
return Ok(());
|
|
}
|
|
}
|
|
|
|
println!("\nwaiting for it to appear in Spotify's device list ...");
|
|
for attempt in 1..=10 {
|
|
std::thread::sleep(Duration::from_secs(2));
|
|
let listed = devices()?;
|
|
if !listed.is_empty() {
|
|
for name in &listed {
|
|
println!(" [{attempt}] {name}");
|
|
}
|
|
return Ok(());
|
|
}
|
|
println!(" [{attempt}] still empty");
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// The account's devices as Spotify currently sees them.
|
|
fn devices() -> anyhow::Result<Vec<String>> {
|
|
let Some(fastpotify::credentials::Grant::Web(token)) =
|
|
stored_grant(fastpotify::credentials::Slot::Shared)?
|
|
else {
|
|
anyhow::bail!("Sign in to Spotifast first");
|
|
};
|
|
let http = reqwest::blocking::Client::new();
|
|
let body: serde_json::Value = http
|
|
.get("https://api.spotify.com/v1/me/player/devices")
|
|
.bearer_auth(&token.access_token)
|
|
.send()?
|
|
.json()?;
|
|
Ok(body["devices"]
|
|
.as_array()
|
|
.map(|devices| {
|
|
devices
|
|
.iter()
|
|
.map(|d| {
|
|
format!(
|
|
"{} ({}, active={})",
|
|
d["name"].as_str().unwrap_or("?"),
|
|
d["type"].as_str().unwrap_or("?"),
|
|
d["is_active"]
|
|
)
|
|
})
|
|
.collect()
|
|
})
|
|
.unwrap_or_default())
|
|
}
|
|
|
|
fn stored_grant(
|
|
slot: fastpotify::credentials::Slot,
|
|
) -> anyhow::Result<Option<fastpotify::credentials::Grant>> {
|
|
let runtime = tokio::runtime::Builder::new_multi_thread()
|
|
.enable_all()
|
|
.build()?;
|
|
let loaded = runtime.block_on(async {
|
|
let store = fastpotify::credentials::Store::new(fastpotify::paths::AppDirs::discover());
|
|
store.lease(slot).load().await
|
|
})?;
|
|
if let Some(warning) = loaded.warning {
|
|
eprintln!("{warning}");
|
|
}
|
|
Ok(loaded.grant)
|
|
}
|