mirror of
https://github.com/Felitendo/spotifast.git
synced 2026-11-01 09:23:26 +01:00
The log file, stderr and the panic log are set up by fastframe-log with Spotifast's own default filters and paths, replacing the env_logger set-up, its tee to the log file, and the panic hook. Behaviour changes: - A panic's line in panic.log now keeps its message with any link removed, on the same line as the location (`... on thread "main", panic at src/x.rs:1:2: message`), where it used to write the raw message, which could quote a URL with a token. The default hook is no longer chained, so the raw message and the backtrace hint are not printed to stderr either; the same redacted line is. - Log lines read `[time LEVEL target] message`, with the level no longer padded or coloured on a terminal. - The start line says `Starting spotifast <version> on <os> (<arch>)`, lowercase, and a log file that cannot be created is reported through the log (on stderr) instead of a bare message.
124 lines
4.1 KiB
Rust
124 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<()> {
|
|
fastframe_log::Logging::new("spotifast", env!("CARGO_PKG_VERSION"))
|
|
.filter("warn")
|
|
.init()?;
|
|
let wanted = std::env::args().nth(1);
|
|
|
|
println!("browsing for _spotify-connect._tcp ...");
|
|
let receivers = spotifast::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 spotifast::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(spotifast::credentials::Grant::Playback(grant)) =
|
|
stored_grant(spotifast::credentials::Slot::Playback)?
|
|
else {
|
|
anyhow::bail!("Enable playback in Spotifast first");
|
|
};
|
|
let credentials = spotifast::zeroconf::Credentials::from_playback(&grant)?;
|
|
println!("\nhanding the account to {} ...", receiver.name);
|
|
let info = spotifast::zeroconf::get_info(&http, receiver)?;
|
|
match spotifast::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(spotifast::credentials::Grant::Web(token)) =
|
|
stored_grant(spotifast::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: spotifast::credentials::Slot,
|
|
) -> anyhow::Result<Option<spotifast::credentials::Grant>> {
|
|
let runtime = tokio::runtime::Builder::new_multi_thread()
|
|
.enable_all()
|
|
.build()?;
|
|
let loaded = runtime.block_on(async {
|
|
let store = spotifast::credentials::Store::new(spotifast::paths::AppDirs::discover());
|
|
store.lease(slot).load().await
|
|
})?;
|
|
if let Some(warning) = loaded.warning {
|
|
eprintln!("{warning}");
|
|
}
|
|
Ok(loaded.grant)
|
|
}
|