//! Extension pour intégrer la configuration Qobuz dans pmoconfig //! //! Ce module fournit le trait `QobuzConfigExt` qui permet d'ajouter facilement //! des méthodes de gestion des credentials Qobuz à pmoconfig::Config. use anyhow::{anyhow, Result}; use pmoconfig::Config; use serde_yaml::Value; /// Trait d'extension pour gérer la configuration Qobuz dans pmoconfig /// /// Ce trait étend `pmoconfig::Config` avec des méthodes spécifiques /// aux credentials et paramètres Qobuz. /// /// # Exemple /// /// ```rust,ignore /// use pmoconfig::get_config; /// use pmoqobuz::QobuzConfigExt; /// /// let config = get_config(); /// let (username, password) = config.get_qobuz_credentials()?; /// println!("Qobuz user: {}", username); /// ``` pub trait QobuzConfigExt { /// Récupère le nom d'utilisateur Qobuz depuis la configuration /// /// # Returns /// /// Le nom d'utilisateur (email) configuré pour Qobuz /// /// # Errors /// /// Retourne une erreur si le nom d'utilisateur n'est pas configuré fn get_qobuz_username(&self) -> Result; /// Définit le nom d'utilisateur Qobuz dans la configuration /// /// # Arguments /// /// * `username` - Le nom d'utilisateur (email) Qobuz fn set_qobuz_username(&self, username: &str) -> Result<()>; /// Récupère le mot de passe Qobuz depuis la configuration /// /// # Returns /// /// Le mot de passe configuré pour Qobuz /// /// # Errors /// /// Retourne une erreur si le mot de passe n'est pas configuré fn get_qobuz_password(&self) -> Result; /// Définit le mot de passe Qobuz dans la configuration /// /// # Arguments /// /// * `password` - Le mot de passe Qobuz fn set_qobuz_password(&self, password: &str) -> Result<()>; /// Récupère les credentials Qobuz (username et password) /// /// # Returns /// /// Un tuple (username, password) contenant les credentials Qobuz /// /// # Errors /// /// Retourne une erreur si l'un des credentials n'est pas configuré /// /// # Exemple /// /// ```rust,ignore /// use pmoconfig::get_config; /// use pmoqobuz::QobuzConfigExt; /// /// let config = get_config(); /// match config.get_qobuz_credentials() { /// Ok((username, password)) => { /// println!("Credentials configured for: {}", username); /// } /// Err(e) => { /// eprintln!("Qobuz credentials not configured: {}", e); /// } /// } /// ``` fn get_qobuz_credentials(&self) -> Result<(String, String)>; /// Récupère l'App ID Qobuz depuis la configuration /// /// # Returns /// /// L'App ID configuré pour Qobuz, ou None si non configuré /// /// # Note /// /// Si aucun App ID n'est configuré, le client utilisera soit le Spoofer /// pour en obtenir un dynamiquement, soit un App ID par défaut. fn get_qobuz_appid(&self) -> Result>; /// Définit l'App ID Qobuz dans la configuration /// /// # Arguments /// /// * `appid` - L'App ID Qobuz (ex: "1401488693436528") fn set_qobuz_appid(&self, appid: &str) -> Result<()>; /// Récupère le secret Qobuz depuis la configuration /// /// # Returns /// /// Le secret encodé en base64, ou None si non configuré /// /// # Note /// /// Le secret est la valeur `configvalue` du code Python. /// Il est décodé et XORé avec l'App ID pour obtenir le secret `s4` /// utilisé pour signer les requêtes sensibles. /// /// Si aucun secret n'est configuré, le client utilisera le Spoofer /// pour en obtenir un dynamiquement. fn get_qobuz_secret(&self) -> Result>; /// Définit le secret Qobuz dans la configuration /// /// # Arguments /// /// * `secret` - Le secret encodé en base64 (configvalue) fn set_qobuz_secret(&self, secret: &str) -> Result<()>; /// Récupère le secret brut du Spoofer depuis la configuration /// /// # Returns /// /// Le secret brut (non XORé), ou None si non configuré /// /// # Note /// /// Ce secret est obtenu par le Spoofer et est utilisé directement /// sans XOR avec l'App ID. Si ce secret est présent, il est testé /// en priorité avant de relancer le Spoofer. fn get_qobuz_spoofer_secret(&self) -> Result>; /// Définit le secret brut du Spoofer dans la configuration /// /// # Arguments /// /// * `secret` - Le secret brut obtenu par le Spoofer fn set_qobuz_spoofer_secret(&self, secret: &str) -> Result<()>; /// Récupère le token d'authentification depuis la configuration /// /// # Returns /// /// Le token d'authentification, ou None si non configuré ou expiré fn get_qobuz_auth_token(&self) -> Result>; /// Récupère l'ID utilisateur depuis la configuration /// /// # Returns /// /// L'ID utilisateur, ou None si non configuré fn get_qobuz_user_id(&self) -> Result>; /// Récupère le timestamp d'expiration du token /// /// # Returns /// /// Le timestamp d'expiration (Unix timestamp), ou None si non configuré fn get_qobuz_token_expires_at(&self) -> Result>; /// Récupère le label de l'abonnement depuis la configuration fn get_qobuz_subscription_label(&self) -> Result>; /// Sauvegarde les informations d'authentification dans la configuration /// /// # Arguments /// /// * `token` - Le token d'authentification /// * `user_id` - L'ID utilisateur /// * `subscription_label` - Le label de l'abonnement (optionnel) /// * `expires_at` - Timestamp d'expiration (Unix timestamp) fn set_qobuz_auth_info( &self, token: &str, user_id: &str, subscription_label: Option<&str>, expires_at: u64, ) -> Result<()>; /// Supprime les informations d'authentification de la configuration fn clear_qobuz_auth_info(&self) -> Result<()>; /// Vérifie si le token d'authentification est encore valide /// /// # Returns /// /// true si un token existe et n'est pas expiré, false sinon fn is_qobuz_auth_valid(&self) -> bool; /// Récupère le répertoire de cache Qobuz /// /// # Returns /// /// Le chemin absolu du répertoire de cache, créé s'il n'existe pas fn get_qobuz_cache_dir(&self) -> Result; /// Définit le répertoire de cache Qobuz fn set_qobuz_cache_dir(&self, directory: String) -> Result<()>; /// Récupère le nombre maximum de requêtes concurrentes /// /// # Returns /// /// Le nombre maximum de requêtes concurrentes, ou None si non configuré (défaut: 2) fn get_qobuz_rate_limit_max_concurrent(&self) -> Result>; /// Définit le nombre maximum de requêtes concurrentes fn set_qobuz_rate_limit_max_concurrent(&self, max: usize) -> Result<()>; /// Récupère le délai minimum entre requêtes en millisecondes /// /// # Returns /// /// Le délai minimum en ms, ou None si non configuré (défaut: 400ms) fn get_qobuz_rate_limit_min_delay_ms(&self) -> Result>; /// Définit le délai minimum entre requêtes fn set_qobuz_rate_limit_min_delay_ms(&self, delay_ms: u64) -> Result<()>; /// Vérifie si le rate limiting est activé /// /// # Returns /// /// true si activé (défaut), false sinon fn is_qobuz_rate_limiting_enabled(&self) -> bool; /// Active ou désactive le rate limiting fn set_qobuz_rate_limiting_enabled(&self, enabled: bool) -> Result<()>; } impl QobuzConfigExt for Config { fn get_qobuz_username(&self) -> Result { match self.get_value(&["accounts", "qobuz", "username"])? { Value::String(s) => Ok(s), _ => Err(anyhow!("Qobuz username not configured")), } } fn set_qobuz_username(&self, username: &str) -> Result<()> { self.set_value( &["accounts", "qobuz", "username"], Value::String(username.to_string()), ) } fn get_qobuz_password(&self) -> Result { match self.get_value(&["accounts", "qobuz", "password"])? { Value::String(s) => { // Déchiffrement automatique si le mot de passe est chiffré pmoconfig::encryption::get_password(&s) .map_err(|e| anyhow!("Failed to decrypt password: {}", e)) } _ => Err(anyhow!("Qobuz password not configured")), } } fn set_qobuz_password(&self, password: &str) -> Result<()> { self.set_value( &["accounts", "qobuz", "password"], Value::String(password.to_string()), ) } fn get_qobuz_credentials(&self) -> Result<(String, String)> { let username = self.get_qobuz_username()?; let password = self.get_qobuz_password()?; Ok((username, password)) } fn get_qobuz_appid(&self) -> Result> { match self.get_value(&["accounts", "qobuz", "appid"]) { Ok(Value::String(s)) if !s.is_empty() => Ok(Some(s)), Ok(Value::String(_)) => Ok(None), // Empty string Ok(_) => Ok(None), // Wrong type Err(_) => Ok(None), // Not configured } } fn set_qobuz_appid(&self, appid: &str) -> Result<()> { self.set_value( &["accounts", "qobuz", "appid"], Value::String(appid.to_string()), ) } fn get_qobuz_secret(&self) -> Result> { match self.get_value(&["accounts", "qobuz", "secret"]) { Ok(Value::String(s)) if !s.is_empty() => Ok(Some(s)), Ok(Value::String(_)) => Ok(None), // Empty string Ok(_) => Ok(None), // Wrong type Err(_) => Ok(None), // Not configured } } fn set_qobuz_secret(&self, secret: &str) -> Result<()> { self.set_value( &["accounts", "qobuz", "secret"], Value::String(secret.to_string()), ) } fn get_qobuz_spoofer_secret(&self) -> Result> { match self.get_value(&["accounts", "qobuz", "spoofer_secret"]) { Ok(Value::String(s)) if !s.is_empty() => Ok(Some(s)), Ok(Value::String(_)) => Ok(None), // Empty string Ok(_) => Ok(None), // Wrong type Err(_) => Ok(None), // Not configured } } fn set_qobuz_spoofer_secret(&self, secret: &str) -> Result<()> { self.set_value( &["accounts", "qobuz", "spoofer_secret"], Value::String(secret.to_string()), ) } fn get_qobuz_auth_token(&self) -> Result> { match self.get_value(&["accounts", "qobuz", "auth_token"]) { Ok(Value::String(s)) if !s.is_empty() => Ok(Some(s)), Ok(Value::String(_)) => Ok(None), // Empty string Ok(_) => Ok(None), // Wrong type Err(_) => Ok(None), // Not configured } } fn get_qobuz_user_id(&self) -> Result> { match self.get_value(&["accounts", "qobuz", "user_id"]) { Ok(Value::String(s)) if !s.is_empty() => Ok(Some(s)), Ok(Value::String(_)) => Ok(None), // Empty string Ok(_) => Ok(None), // Wrong type Err(_) => Ok(None), // Not configured } } fn get_qobuz_token_expires_at(&self) -> Result> { match self.get_value(&["accounts", "qobuz", "token_expires_at"]) { Ok(Value::Number(n)) if n.is_u64() => Ok(Some(n.as_u64().unwrap())), Ok(Value::Number(n)) if n.is_i64() => Ok(Some(n.as_i64().unwrap() as u64)), Ok(_) => Ok(None), // Wrong type Err(_) => Ok(None), // Not configured } } fn get_qobuz_subscription_label(&self) -> Result> { match self.get_value(&["accounts", "qobuz", "subscription_label"]) { Ok(Value::String(s)) if !s.is_empty() => Ok(Some(s)), Ok(Value::String(_)) => Ok(None), // Empty string Ok(_) => Ok(None), // Wrong type Err(_) => Ok(None), // Not configured } } fn set_qobuz_auth_info( &self, token: &str, user_id: &str, subscription_label: Option<&str>, expires_at: u64, ) -> Result<()> { self.set_value( &["accounts", "qobuz", "auth_token"], Value::String(token.to_string()), )?; self.set_value( &["accounts", "qobuz", "user_id"], Value::String(user_id.to_string()), )?; self.set_value( &["accounts", "qobuz", "token_expires_at"], Value::Number(serde_yaml::Number::from(expires_at)), )?; if let Some(label) = subscription_label { self.set_value( &["accounts", "qobuz", "subscription_label"], Value::String(label.to_string()), )?; } Ok(()) } fn clear_qobuz_auth_info(&self) -> Result<()> { // On ne propage pas les erreurs car les valeurs peuvent ne pas exister let _ = self.set_value( &["accounts", "qobuz", "auth_token"], Value::String(String::new()), ); let _ = self.set_value( &["accounts", "qobuz", "user_id"], Value::String(String::new()), ); let _ = self.set_value( &["accounts", "qobuz", "token_expires_at"], Value::Number(serde_yaml::Number::from(0)), ); let _ = self.set_value( &["accounts", "qobuz", "subscription_label"], Value::String(String::new()), ); Ok(()) } fn is_qobuz_auth_valid(&self) -> bool { let token_present = self .get_qobuz_auth_token() .ok() .flatten() .map(|token| !token.is_empty()) .unwrap_or(false); let user_present = self .get_qobuz_user_id() .ok() .flatten() .map(|user_id| !user_id.is_empty()) .unwrap_or(false); token_present && user_present } fn get_qobuz_cache_dir(&self) -> Result { self.get_managed_dir(&["host", "qobuz_cache", "directory"], "cache_qobuz") } fn set_qobuz_cache_dir(&self, directory: String) -> Result<()> { self.set_managed_dir(&["host", "qobuz_cache", "directory"], directory) } fn get_qobuz_rate_limit_max_concurrent(&self) -> Result> { match self.get_value(&["accounts", "qobuz", "rate_limit", "max_concurrent"]) { Ok(Value::Number(n)) if n.is_u64() => Ok(Some(n.as_u64().unwrap() as usize)), Ok(_) => Ok(None), Err(_) => Ok(Some(2)), // Default: 2 concurrent requests } } fn set_qobuz_rate_limit_max_concurrent(&self, max: usize) -> Result<()> { self.set_value( &["accounts", "qobuz", "rate_limit", "max_concurrent"], Value::Number(serde_yaml::Number::from(max)), ) } fn get_qobuz_rate_limit_min_delay_ms(&self) -> Result> { match self.get_value(&["accounts", "qobuz", "rate_limit", "min_delay_ms"]) { Ok(Value::Number(n)) if n.is_u64() => Ok(Some(n.as_u64().unwrap())), Ok(_) => Ok(None), Err(_) => Ok(Some(400)), // Default: 400ms } } fn set_qobuz_rate_limit_min_delay_ms(&self, delay_ms: u64) -> Result<()> { self.set_value( &["accounts", "qobuz", "rate_limit", "min_delay_ms"], Value::Number(serde_yaml::Number::from(delay_ms)), ) } fn is_qobuz_rate_limiting_enabled(&self) -> bool { match self.get_value(&["accounts", "qobuz", "rate_limit", "enabled"]) { Ok(Value::Bool(b)) => b, _ => true, // Default: enabled } } fn set_qobuz_rate_limiting_enabled(&self, enabled: bool) -> Result<()> { self.set_value( &["accounts", "qobuz", "rate_limit", "enabled"], Value::Bool(enabled), ) } }