initial discovery implementation

This commit is contained in:
2024-10-03 18:29:25 -05:00
parent bbe5e24e33
commit bafdd942e6
4 changed files with 3182 additions and 79 deletions

3099
Cargo.lock generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -4,3 +4,15 @@ version = "0.0.5"
edition = "2021"
[dependencies]
librespot-discovery = { git = "https://github.com/librespot-org/librespot", branch = "dev" }
librespot-core = { git = "https://github.com/librespot-org/librespot", branch = "dev" }
serde = { version = "1.0.210", features = ["derive"] }
tokio = { version = "1.40.0", features = ["full"] }
futures = "0.3.30"
serde_json = "1.0.128"
sha1 = "0.10.6"
hex = "0.4.3"
log = "0.4.22"
hex-literal = "0.4.1"
home = "0.5.9"
env_logger = "0.11.5"

View File

@@ -4,7 +4,7 @@ A simple CLI-based application for creating a `credentials.json` file, used by t
## Usage
You can install this applicaiton, but most people will just need it once.
You can install this application, but most people will just need it once. The following commands will run the application without installing it.
For Linux and macOS, you can paste this command into your terminal:

View File

@@ -1,82 +1,74 @@
use std::env;
use std::process::Command;
use std::{env, fs::File, process::exit};
fn main() {
let os = env::consts::OS;
let arch = env::consts::ARCH;
let username = if os == "windows" {
env::var("USERNAME").unwrap_or_else(|_| "unknown".to_string())
} else {
env::var("USER").unwrap_or_else(|_| "unknown".to_string())
use futures::StreamExt;
use librespot_core::config::DeviceType;
use librespot_discovery::Discovery;
use log::{info, warn};
use sha1::{Digest, Sha1};
use librespot_core::SessionConfig;
use std::io::Write;
#[tokio::main(flavor = "current_thread")]
async fn main() {
if env::var("RUST_LOG").is_err() {
env::set_var("RUST_LOG", "info")
}
env_logger::builder().init();
let credentials_file = match home::home_dir() {
// ~/.cache/spotify_player/credentials.json
Some(path) => path.join(".cache/spotify_player/credentials.json"),
None => {
warn!("Cannot determine home directory for credentials file.");
exit(1);
}
};
let hostname = Command::new("hostname")
.output()
.map(|output| String::from_utf8_lossy(&output.stdout).trim().to_string())
.unwrap_or_else(|_| "unknown".to_string());
let execution_path = env::current_exe()
.map(|path| path.display().to_string())
.unwrap_or_else(|_| "unknown".to_string());
let working_directory = env::current_dir()
.map(|path| path.display().to_string())
.unwrap_or_else(|_| "unknown".to_string());
info!("Credentials file: {}", &credentials_file.display());
println!(
"{}@{} ({}/{}) [{}] [{}]",
username, hostname, os, arch, working_directory, execution_path
);
// TODO: If credentials file exists, confirm overwrite
if credentials_file.exists() {
warn!("Credentials file already exists: {}", &credentials_file.display());
exit(1);
}
// TODO: If spotifyd is running, ask if shutdown is desired
let username = match env::consts::OS {
"windows" => env::var("USERNAME"),
_ => env::var("USER"),
}.unwrap_or_else(|_| "unknown".to_string());
let device_name = format!("spotify-quickauth-{}", username);
let device_id = hex::encode(Sha1::digest(device_name.as_bytes()));
let device_type = DeviceType::Computer;
let mut server = Discovery::builder(device_id, SessionConfig::default().client_id)
.name(device_name.clone())
.device_type(device_type)
.launch()
.unwrap();
println!("Open Spotify and select output device: {}", device_name);
let mut written = false;
while let Some(credentials) = server.next().await {
let result = File::create(&credentials_file).and_then(|mut file| {
let data = serde_json::to_string(&credentials)?;
write!(file, "{data}")
});
written = true;
if let Err(e) = result {
warn!("Cannot save credentials to cache: {}", e);
exit(1);
} else {
println!("Credentials saved: {}", &credentials_file.display());
exit(0);
}
}
if !written {
warn!("No credentials were written.");
exit(1);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_os_arch() {
let os = env::consts::OS;
let arch = env::consts::ARCH;
// Make sure that OS and ARCH are not empty
assert!(!os.is_empty());
assert!(!arch.is_empty());
}
#[test]
fn test_username() {
let username = env::var("USER").unwrap_or_else(|_| "unknown".to_string());
// In CI environments, USER might not be set, so allow "unknown"
assert!(!username.is_empty());
}
#[test]
fn test_hostname() {
let hostname = Command::new("hostname")
.output()
.map(|output| String::from_utf8_lossy(&output.stdout).trim().to_string())
.unwrap_or_else(|_| "unknown".to_string());
// Since hostname might vary, just ensure it's not empty
assert!(!hostname.is_empty());
}
#[test]
fn test_execution_path() {
let execution_path = env::current_exe()
.map(|path| path.display().to_string())
.unwrap_or_else(|_| "unknown".to_string());
// Ensure execution path is not "unknown" when it's expected to resolve
assert!(execution_path != "unknown");
}
#[test]
fn test_working_directory() {
let working_directory = env::current_dir()
.map(|path| path.display().to_string())
.unwrap_or_else(|_| "unknown".to_string());
// Ensure the working directory is a valid path
assert!(working_directory != "unknown");
}
}