#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AuthMethod { Device, Pkce, } #[derive(Debug, Clone)] pub struct ApiKey { pub name: String, pub client_id: String, pub client_secret: String, pub auth_method: AuthMethod, pub scope: String, pub redirect_uri: String, } const PKCE_CLIENT_ID: &str = "3a5dzbD1VqdaUmqG"; const PKCE_SCOPE: &str = "r_usr w_usr"; const PKCE_REDIRECT_URI: &str = "tidal://login/auth"; fn default_scope(method: AuthMethod) -> &'static str { match method { AuthMethod::Pkce => PKCE_SCOPE, AuthMethod::Device => crate::tidal::DEVICE_SCOPE, } } fn embedded_key() -> ApiKey { ApiKey { name: "TIDAL desktop app (public client)".to_string(), client_id: PKCE_CLIENT_ID.to_string(), client_secret: String::new(), auth_method: AuthMethod::Pkce, scope: PKCE_SCOPE.to_string(), redirect_uri: PKCE_REDIRECT_URI.to_string(), } } /// The single client to authenticate with: the embedded PKCE client, with /// optional overrides from the environment (see `load_dotenv`). pub fn load_key() -> ApiKey { load_dotenv(); let mut key = embedded_key(); if let Ok(id) = std::env::var("TIDAL_CLIENT_ID") { if !id.is_empty() { key.name = std::env::var("TIDAL_CLIENT_NAME") .unwrap_or_else(|_| "Custom (env)".to_string()); key.client_id = id; key.auth_method = match std::env::var("TIDAL_AUTH_METHOD") .unwrap_or_default() .as_str() { "device" => AuthMethod::Device, _ => AuthMethod::Pkce, }; key.scope = default_scope(key.auth_method).to_string(); key.redirect_uri = PKCE_REDIRECT_URI.to_string(); } } if let Ok(v) = std::env::var("TIDAL_CLIENT_SECRET") { key.client_secret = v; } if let Ok(v) = std::env::var("TIDAL_SCOPE") { if !v.is_empty() { key.scope = v; } } if let Ok(v) = std::env::var("TIDAL_REDIRECT_URI") { if !v.is_empty() { key.redirect_uri = v; } } key } fn load_dotenv() { let home = dirs::home_dir().unwrap_or_else(|| ".".into()); let candidates = [ std::env::var("TIDAL_ENV_FILE").ok(), Some(home.join(".tidal-dl.env").to_string_lossy().into_owned()), Some(".env".to_string()), ]; for path in candidates.into_iter().flatten() { if let Ok(text) = std::fs::read_to_string(&path) { for line in text.lines() { let line = line.trim(); if line.is_empty() || line.starts_with('#') || !line.contains('=') { continue; } let (k, v) = line.split_once('=').unwrap(); let k = k.trim().to_string(); let v = v.trim().trim_matches('"').trim_matches('\'').to_string(); if !k.is_empty() && std::env::var(&k).is_err() { std::env::set_var(k, v); } } break; } } }