//! 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<()>; /// Version du bundle Qobuz extrait en dernier (ex : `"8.1.0-b019"`). /// Permet de détecter une rotation de bundle sans télécharger les 7 MB. fn get_qobuz_bundle_version(&self) -> Result>; /// Persiste la version du bundle après une extraction réussie. fn set_qobuz_bundle_version(&self, version: &str) -> Result<()>; /// Nombre de workers concurrents pour l'enregistrement des tracks en cache. /// /// Contrôle le semaphore dans `register_tracks_lazy` : plus la valeur est /// haute, plus les covers sont téléchargées en parallèle, mais plus la /// contention sur le mutex SQLite est forte. /// /// Défaut : 4 (adapté à une machine sous contrainte mémoire / Docker). fn get_qobuz_register_concurrency(&self) -> usize; /// Nombre de pages de playlist chargées en parallèle via `/playlist/get`. /// /// La page 1 est toujours séquentielle (pour obtenir `total`). Les pages /// suivantes sont lancées simultanément jusqu'à cette limite. /// Valeur trop haute → risque de rate limiting Qobuz. /// /// Défaut : 3. fn get_qobuz_page_concurrency(&self) -> usize; } 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), ) } fn get_qobuz_bundle_version(&self) -> Result> { match self.get_value(&["accounts", "qobuz", "bundle_version"]) { Ok(Value::String(s)) if !s.is_empty() => Ok(Some(s)), _ => Ok(None), } } fn set_qobuz_bundle_version(&self, version: &str) -> Result<()> { self.set_value( &["accounts", "qobuz", "bundle_version"], Value::String(version.to_string()), ) } fn get_qobuz_register_concurrency(&self) -> usize { match self.get_value(&["accounts", "qobuz", "register_concurrency"]) { Ok(Value::Number(n)) if n.as_u64().unwrap_or(0) >= 1 => { n.as_u64().unwrap() as usize } _ => 4, } } fn get_qobuz_page_concurrency(&self) -> usize { match self.get_value(&["accounts", "qobuz", "page_concurrency"]) { Ok(Value::Number(n)) if n.as_u64().unwrap_or(0) >= 1 => { n.as_u64().unwrap() as usize } _ => 3, } } }