spotify-cli 0.5.0

A command-line interface for Spotify
Documentation
use super::{init_token_store, load_config};
use crate::io::output::{ErrorKind, Response};
use crate::oauth::flow::OAuthFlow;
use crate::{auth_error, storage_error};
use tracing::{debug, warn};

pub async fn auth_login(force: bool) -> Response {
    debug!(force = force, "Starting login flow");

    let config = match load_config() {
        Ok(c) => c,
        Err(e) => return e,
    };

    let token_store = match init_token_store() {
        Ok(s) => s,
        Err(e) => return e,
    };

    if !force && let Ok(token) = token_store.load() {
        debug!(expired = token.is_expired(), "Found existing token");
        if !token.is_expired() {
            return Response::success_with_payload(
                200,
                "Already logged in",
                serde_json::json!({
                    "expires_in": token.seconds_until_expiry()
                }),
            );
        }

        if let Some(refresh_token) = &token.refresh_token {
            let flow = OAuthFlow::new(config.client_id().to_string());
            match flow.refresh(refresh_token).await {
                Ok(new_token) => {
                    if let Err(e) = token_store.save(&new_token) {
                        return storage_error!("Failed to save token", e);
                    }
                    return Response::success_with_payload(
                        200,
                        "Token refreshed",
                        serde_json::json!({
                            "expires_in": new_token.seconds_until_expiry()
                        }),
                    );
                }
                Err(e) => {
                    warn!(error = %e, "Token refresh failed, opening browser login");
                }
            }
        }
    }

    let flow = OAuthFlow::new(config.client_id().to_string());
    match flow.authenticate().await {
        Ok(token) => {
            if let Err(e) = token_store.save(&token) {
                return storage_error!("Failed to save token", e);
            }
            Response::success_with_payload(
                200,
                "Login successful",
                serde_json::json!({
                    "expires_in": token.seconds_until_expiry()
                }),
            )
        }
        Err(e) => auth_error!("Login failed", e),
    }
}

pub async fn auth_logout() -> Response {
    let token_store = match init_token_store() {
        Ok(s) => s,
        Err(e) => return e,
    };

    if !token_store.exists() {
        return Response::success(200, "Already logged out");
    }

    match token_store.delete() {
        Ok(_) => Response::success(200, "Logged out successfully"),
        Err(e) => storage_error!("Failed to delete token", e),
    }
}

pub async fn auth_refresh() -> Response {
    let config = match load_config() {
        Ok(c) => c,
        Err(e) => return e,
    };

    let token_store = match init_token_store() {
        Ok(s) => s,
        Err(e) => return e,
    };

    let token = match token_store.load() {
        Ok(t) => t,
        Err(_) => {
            return Response::err(
                401,
                "Not logged in. Run: spotify-cli auth login",
                ErrorKind::Auth,
            );
        }
    };

    let refresh_token = match &token.refresh_token {
        Some(t) => t,
        None => return Response::err(401, "No refresh token available", ErrorKind::Auth),
    };

    let flow = OAuthFlow::new(config.client_id().to_string());
    match flow.refresh(refresh_token).await {
        Ok(new_token) => {
            if let Err(e) = token_store.save(&new_token) {
                return storage_error!("Failed to save token", e);
            }
            Response::success_with_payload(
                200,
                "Token refreshed",
                serde_json::json!({
                    "expires_in": new_token.seconds_until_expiry()
                }),
            )
        }
        Err(e) => auth_error!("Failed to refresh token", e),
    }
}

pub async fn auth_status() -> Response {
    let token_store = match init_token_store() {
        Ok(s) => s,
        Err(e) => return e,
    };

    if !token_store.exists() {
        return Response::success_with_payload(
            200,
            "Not authenticated",
            serde_json::json!({
                "authenticated": false
            }),
        );
    }

    match token_store.load() {
        Ok(token) => {
            let expired = token.is_expired();
            let expires_in = token.seconds_until_expiry();

            Response::success_with_payload(
                200,
                if expired {
                    "Token expired"
                } else {
                    "Authenticated"
                },
                serde_json::json!({
                    "authenticated": !expired,
                    "expired": expired,
                    "expires_in": expires_in
                }),
            )
        }
        Err(e) => storage_error!("Failed to load token", e),
    }
}