From 31b5e6ee8bb4a5fb3c05af09a5cfe0f7c13af84e Mon Sep 17 00:00:00 2001 From: Xevion Date: Fri, 17 Jan 2025 14:10:34 -0600 Subject: [PATCH] Setup basic parsing, 'proper' implementation --- CARGO_README.md | 26 +++++-- src/main.rs | 195 +++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 180 insertions(+), 41 deletions(-) diff --git a/CARGO_README.md b/CARGO_README.md index becf08e..7e90226 100644 --- a/CARGO_README.md +++ b/CARGO_README.md @@ -1,3 +1,11 @@ +> [!IMPORTANT] +> This application is not 'real'; I mostly created it to learn about building a CLI application in Rust with high-end CI/CD pipelines and easy to execute multi-platform scripts. +> Interestingly enough, I never really developed the program itself, just the scripts and the CI/CD pipeline. +> And by the time I was ready to write it, the `spotify-player` project had already solved the [primary issue](https://github.com/aome510/spotify-player/issues/201#issuecomment-2439565162) I was trying to address. +> Thus, I'm archiving this project, as it's no longer necessary. +> +> This project is still pretty cool though in it's own right, with multi-platform ephemeral binaries over `curl` commands, Windows support, Linux MUSL targets (smaller binaries), ARM64 MacOS support, binary deployment to GitHub pages, automatic GitHub release uploads, and a pretty decent README - this project has it all. + # spotify-quickauth [![Build Status](https://github.com/Xevion/spotify-quickauth/workflows/build/badge.svg)](https://github.com/Xevion/spotify-quickauth/actions) @@ -5,8 +13,8 @@ [![Crates.io](https://img.shields.io/crates/v/spotify-quickauth.svg)](https://crates.io/crates/spotify-quickauth) ![Crates.io MSRV](https://img.shields.io/crates/msrv/spotify-quickauth) ![GitHub last commit](https://img.shields.io/github/last-commit/Xevion/spotify-quickauth) - + A simple CLI-based application for creating a `credentials.json` file, used by `librespot` derived applications, such as [spotify-player][spotify-player], [spotifyd][spotifyd], and [raspotify][raspotify]. @@ -24,6 +32,7 @@ curl -sSL https://xevion.github.io/spotify-quickauth/run.sh | sh -s -- The default invocation is likely fine for most users, it will try to understand the available paths for `credentials.json` to be written to, and allow you to select them. +> [!NOTE] > Automatic detection is dependent on the related software being installed and/or relevant configuration files being present. For **Windows**, you can paste this command into PowerShell: @@ -36,7 +45,8 @@ iex (irm "https://xevion.github.io/spotify-quickauth/run.ps1") This application is dead simple to use. Just run the command, and it'll tell you to connect to a fake 'device' in your Spotify interface. -> You must be connected to the same network running `spotify-quickauth`, as the `zeroconf` technology **does not work** across **networks** nor **proxies**. +> [!NOTE] +> You must be connected to the same network running `spotify-quickauth`, as the `zeroconf` technology **does not work** across **networks** nor **proxies**. Once you connect, the credentials file will be created, and you'll be prompted to select which location(s) to place it in. Even if none of the relevant `librespot` applications are detected or installed, you can specify manual locations, or the current working directory. @@ -44,14 +54,15 @@ Once you connect, the credentials file will be created, and you'll be prompted t Installation is not necessary to use this application, but if you're having trouble, want to compile it yourself, or are using it frequently, you might want to install it. ->The scripts above can be given the `-K` or `--keep` flag to keep the downloaded binary. This will prevent repeated API calls to GitHub if you're using the script frequently within a short period. - +> [!NOTE] +> The scripts above can be given the `-K` or `--keep` flag to keep the downloaded binary. This will prevent repeated API calls to GitHub if you're using the script frequently within a short period. ### Pre-built Binaries Binaries are always available for download from the [releases page][latestRelease], and they're the same ones used by the shell scripts above. Currently, the following targets are available for download: + - x64 Linux (MUSL) `x86_64-unknown-linux-musl` - ARM64 Linux (MUSL) `aarch64-unknown-linux-musl` - ARMv7 Linux `armv7-unknown-linux-musleabihf` @@ -64,6 +75,7 @@ Please [file an issue][new-issue] if you are on a platform that is not supported ### Via `cargo-binstall` +> [!NOTE] > If the package cannot be found for your target or fails to be downloaded for any reason, `cargo-binstall` will automatically fall back to building the package from source. `cargo-binstall` is a tool that allows you to install binaries from crates.io without needing to compile them yourself. @@ -74,7 +86,6 @@ cargo binstall spotify-quickauth If you're curious where the binary comes from, `cargo-binstall` will likely pull the binary directly from the [latest release][latestRelease] by this repository, selecting the most appropriate target for your host. - ### Manual Installation If you'd like to use the shell script above to install the binary, you can use the `-S/--stop` flag to prevent the script from running the binary after downloading it. It implicitly applies the `--keep` flag too. @@ -103,8 +114,9 @@ spotify-quickauth --help ``` If you have any troubles building the project + - Make sure you're using a target that's supported by the project (see above). -- Certain targets may require specfic linkers. For example, +- Certain targets may require specfic linkers. For example, [latestRelease]: https://github.com/Xevion/spotify-quickauth/releases/latest/ [spotify-player]: https://github.com/aome510/spotify-player @@ -115,4 +127,4 @@ If you have any troubles building the project [binstall]: https://github.com/cargo-bins/cargo-binstall [quickinstall]: https://github.com/cargo-bins/cargo-quickinstall [binstall-installation]: https://github.com/cargo-bins/cargo-binstall#installation -[new-issue]: https://github.com/Xevion/spotify-quickauth/issues/new \ No newline at end of file +[new-issue]: https://github.com/Xevion/spotify-quickauth/issues/new diff --git a/src/main.rs b/src/main.rs index fbf26cd..47f2c90 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,42 +1,174 @@ -use std::{env, fs::File, process::exit}; +use std::{env, fs::File, path::PathBuf, process::exit}; use futures::StreamExt; use librespot_core::config::DeviceType; use librespot_discovery::Discovery; -use log::{info, warn}; +use log::{debug, error, info, warn}; use sha1::{Digest, Sha1}; use std::io::Write; +struct Arguments { + force: bool, + path: PathBuf, +} + +fn parse_arguments(args: &Vec) -> Result { + let mut force: Option = None; + let mut path: Option = None; + + let mut skip = 1; + // If '--' is provided, all arguments before it are skipped + if let Some(index) = args.iter().position(|arg| arg == "--") { + skip = index + 1; + } + + let args = &args[skip..]; + debug!("Arguments: {:?}", args); + + for arg in args.iter() { + match arg.as_str() { + "-f" | "--force" => { + if force.is_some() { + return Err("Force flag provided multiple times".to_string()); + } + force = Some(true) + } + _ => { + if path.is_some() { + return Err("Path provided multiple times".to_string()); + } + + // Parse the path, validate that the argument looks like a path + let parsed = PathBuf::from(arg); + debug!("Parsed path: {}", parsed.display()); + + if parsed.exists() { + if parsed.is_dir() { + path = Some(parsed.join("credentials.json")); + } else if parsed.is_file() { + if path.is_some() { + return Err("Path provided multiple times".to_string()); + } + path = Some(parsed); + } else { + return Err("Path is not a file or directory".to_string()); + } + } else { + // File does not exist, check if it looks like a directory + if parsed.ends_with("/") || parsed.ends_with("\\") { + // If the parent directory exists, it's okay to create a directory then the file in it + if parsed.parent().is_some_and(|p| p.exists()) { + path = Some(parsed.join("credentials.json")); + } else { + return Err( + "Cannot create more than one folder for output path".to_string() + ); + } + } else { + // No need to create a directory, just create the file + if parsed.parent().is_some_and(|p| p.exists()) { + path = Some(parsed); + } else { + return Err( + "Cannot create a file in a non-existent directory".to_string() + ); + } + } + } + } + } + } + + // If no path was provided, default to credentials.json in the current directory + let path = match path { + Some(p) => match std::path::absolute(p) { + Ok(p) => p, + Err(e) => return Err(format!("Invalid path: {}", e)), + }, + None => { + // No path provided, try to identify a default in the current directory + let pwd = env::current_dir(); + if pwd.is_err() { + // For some reason the current directory + return Err("Current directory is invalid or indeterminate, please provide an explicit output path".to_string()); + } + + // Default to credentials.json in the current directory + pwd.unwrap().join("credentials.json") + } + }; + + // If the *file* already exists, check if the force flag was provided + if path.exists() && path.is_file() && !force.unwrap_or(false) { + return Err(format!( + "Output file already exists, use -f to overwrite ({})", + path.display() + )); + } + + Ok(Arguments { + force: force.unwrap_or(false), + path, + }) +} + #[tokio::main(flavor = "current_thread")] -async fn main() { +async fn main() { + // Initialize the logger 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."); + // Parse the arguments + let args = match parse_arguments(&env::args().collect()) { + Ok(a) => a, + Err(e) => { + error!("Error parsing arguments: {}", e); exit(1); } }; - info!("Credentials file: {}", &credentials_file.display()); - // TODO: If credentials file exists, confirm overwrite - if credentials_file.exists() { - warn!("Credentials file already exists: {}", &credentials_file.display()); - exit(1); - } + info!("Credentials file: {}", &args.path.display()); // TODO: If spotifyd is running, ask if shutdown is desired - - let username = match env::consts::OS { + + // Figure out the username + let mut username = match env::consts::OS { "windows" => env::var("USERNAME"), _ => env::var("USER"), - }.unwrap_or_else(|_| "unknown".to_string()); + } + // Trim whitespace from the username + .map(|u| u.trim().to_string()) + .unwrap_or("unknown".to_string()); + // Default the username to 'unknown' if it doesn't fit the expected format + if username != "unknown" { + let valid_characters = r"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"; + if match &username { + u if u.is_empty() => { + warn!("Cannot determine username, defaulting to 'unknown'"); + true + } + u if u.len() > 20 => { + warn!("Username is too long, defaulting to 'unknown'"); + true + } + u if u.len() < 2 => { + warn!("Username is too short, defaulting to 'unknown'"); + true + } + u if u.contains(|c| !valid_characters.contains(c)) => { + warn!("Username contains invalid characters, defaulting to 'unknown'"); + true + } + _ => false, + } { + username = "unknown".to_string(); + } + } + + // Create the device metadata let device_name = format!("spotify-quickauth-{}", username); let device_id = hex::encode(Sha1::digest(device_name.as_bytes())); let device_type = DeviceType::Computer; @@ -47,35 +179,30 @@ async fn main() { .launch() .unwrap(); - println!("Open Spotify and select output device: {}", device_name); + info!("Open Spotify and select output device: {}", device_name); - let mut written = false; while let Some(credentials) = server.next().await { - let result = File::create("./credentials.json").and_then(|mut file| { + // Check if file exists + if args.path.exists() && !args.force { + warn!("Output file already exists (appeared after startup), use -f to overwrite"); + exit(1); + } + + // Write the credentials to the file + let result = File::create(&args.path).and_then(|mut file| { let data = serde_json::to_string(&credentials)?; write!(file, "{data}") }); - written = true; + // Check if the file was created successfully if let Err(e) = result { warn!("Cannot save credentials to cache: {}", e); exit(1); } else { - println!("Credentials saved: {}", &credentials_file.display()); + info!("Credentials saved: {}", &args.path.display()); exit(0); } } - - if !written { - warn!("No credentials were written."); - exit(1); - } } - -mod tests { - #[test] - fn test_nothing() { - assert_eq!(1, 1); - } -} +mod tests {}