Setup basic parsing, 'proper' implementation

This commit is contained in:
2025-01-17 14:10:34 -06:00
parent 20c863e660
commit 31b5e6ee8b
2 changed files with 180 additions and 41 deletions

View File

@@ -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)
<!-- TODO: Add testing status badge -->
<!-- TODO: Add testing status badge -->
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,6 +45,7 @@ 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.
> [!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.
> [!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,6 +114,7 @@ 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,

View File

@@ -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<String>) -> Result<Arguments, String> {
let mut force: Option<bool> = None;
let mut path: Option<PathBuf> = 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() {
// 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 {}