use std::{env::var, iter::once};
use reqwest::{
Client,
header::{HeaderName, HeaderValue},
};
use crate::{
errors::QobuzApiError::{
self, AuthenticationError, CredentialsError, HttpError, QobuzApiInitializationError,
},
models::Login,
utils::{
get_web_player_app_id, get_web_player_app_secret, read_app_credentials_from_env,
write_app_credentials_to_env,
},
};
pub mod constants {
pub const API_BASE_URL: &str = "https://www.qobuz.com/api.json/0.2";
pub const WEB_PLAYER_BASE_URL: &str = "https://play.qobuz.com";
}
pub struct QobuzApiService {
pub app_id: String,
pub app_secret: String,
pub user_auth_token: Option<String>,
pub(crate) client: Client,
}
impl QobuzApiService {
pub async fn new() -> Result<Self, QobuzApiError> {
if let Ok((Some(cached_app_id), Some(cached_app_secret))) = read_app_credentials_from_env()
{
if !cached_app_id.is_empty() && !cached_app_secret.is_empty() {
println!("Using cached credentials from .env file");
match Self::with_credentials(
Some(cached_app_id.clone()),
Some(cached_app_secret.clone()),
)
.await
{
Ok(service) => {
return Ok(service);
}
Err(e) => {
println!(
"Cached credentials failed to initialize ({}), fetching new ones...",
e
);
}
}
}
} else {
println!("No cached credentials found, fetching new ones...");
}
let app_id = get_web_player_app_id()
.await
.map_err(|e| QobuzApiInitializationError {
message: format!("Failed to fetch app ID from web player: {}", e),
})?;
let app_secret =
get_web_player_app_secret()
.await
.map_err(|e| QobuzApiInitializationError {
message: format!("Failed to fetch app secret from web player: {}", e),
})?;
if let Err(e) = write_app_credentials_to_env(&app_id, &app_secret) {
eprintln!("Warning: Failed to write credentials to .env file: {}", e);
} else {
println!("Successfully stored new credentials in .env file");
}
Self::with_credentials(Some(app_id), Some(app_secret)).await
}
pub async fn with_credentials(
app_id: Option<String>,
app_secret: Option<String>,
) -> Result<Self, QobuzApiError> {
let has_credentials = app_id.is_some() && app_secret.is_some();
if !has_credentials {
return Err(CredentialsError {
message: "App ID and App Secret must be provided".to_string(),
});
}
let app_id = match app_id {
Some(id) if !id.is_empty() => id,
_ => {
return Err(CredentialsError {
message: "App ID cannot be empty".to_string(),
});
}
};
let app_secret = match app_secret {
Some(secret) if !secret.is_empty() => secret,
_ => {
return Err(CredentialsError {
message: "App Secret cannot be empty".to_string(),
});
}
};
let client = Client::builder()
.user_agent(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/110.0",
)
.default_headers(
once((
HeaderName::from_static("x-app-id"),
HeaderValue::from_str(&app_id).map_err(|e| QobuzApiInitializationError {
message: format!("Failed to create header value for app ID: {}", e),
})?,
))
.collect(),
)
.build()
.map_err(HttpError)?;
let service = QobuzApiService {
app_id,
app_secret,
user_auth_token: None,
client,
};
Ok(service)
}
pub fn set_user_auth_token(&mut self, token: String) {
self.user_auth_token = Some(token);
}
pub(super) async fn authenticate_with_creds(
&mut self,
user_id: Option<&str>,
user_auth_token: Option<&str>,
email: Option<&str>,
password: Option<&str>,
username: Option<&str>,
) -> Result<Login, QobuzApiError> {
if let (Some(uid), Some(token)) = (user_id, user_auth_token)
&& !uid.is_empty()
&& !token.is_empty()
{
println!("Using token-based authentication");
return self.login_with_token(uid, token).await;
}
if let (Some(em), Some(pwd)) = (email, password)
&& !em.is_empty()
&& !pwd.is_empty()
{
println!("Using email/password authentication");
return self.login(em, pwd).await;
}
if let (Some(un), Some(pwd)) = (username, password)
&& !un.is_empty()
&& !pwd.is_empty()
{
println!("Using username/password authentication");
return self.login(un, pwd).await;
}
Err(AuthenticationError {
message: "No valid authentication credentials provided. Please provide either: (user_id and user_auth_token) or (email and password) or (username and password)".to_string(),
})
}
pub async fn authenticate_with_env(&mut self) -> Result<Login, QobuzApiError> {
let user_id = var("QOBUZ_USER_ID").ok();
let user_auth_token = var("QOBUZ_USER_AUTH_TOKEN").ok();
let email = var("QOBUZ_EMAIL").ok();
let password = var("QOBUZ_PASSWORD").ok();
let username = var("QOBUZ_USERNAME").ok();
self.authenticate_with_creds(
user_id.as_deref(),
user_auth_token.as_deref(),
email.as_deref(),
password.as_deref(),
username.as_deref(),
)
.await
}
pub async fn refresh_app_credentials(&self) -> Result<Self, QobuzApiError> {
println!("Fetching new app credentials from web player...");
let app_id = get_web_player_app_id()
.await
.map_err(|e| QobuzApiInitializationError {
message: format!("Failed to fetch app ID from web player: {}", e),
})?;
let app_secret =
get_web_player_app_secret()
.await
.map_err(|e| QobuzApiInitializationError {
message: format!("Failed to fetch app secret from web player: {}", e),
})?;
if let Err(e) = write_app_credentials_to_env(&app_id, &app_secret) {
eprintln!("Warning: Failed to update credentials in .env file: {}", e);
} else {
println!("Successfully updated credentials in .env file");
}
let mut new_service = Self::with_credentials(Some(app_id), Some(app_secret)).await?;
new_service.user_auth_token = self.user_auth_token.clone();
Ok(new_service)
}
}