From 4bc80dfd08c5a5d85b4e429de7d58a544fd19dc9 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Mon, 15 Dec 2025 10:23:43 +0100 Subject: [PATCH] PMOQobuz client is now working --- pmoqobuz/src/api/catalog.rs | 6 +-- pmoqobuz/src/api/mod.rs | 10 +++-- pmoqobuz/src/api/signing.rs | 14 +++---- pmoqobuz/src/client.rs | 74 +++++++++++++++++++++++++++++-------- pmoqobuz/src/config_ext.rs | 36 ++++++++++++++++++ pmoqobuz/src/models.rs | 4 +- 6 files changed, 113 insertions(+), 31 deletions(-) diff --git a/pmoqobuz/src/api/catalog.rs b/pmoqobuz/src/api/catalog.rs index f83f4736..e2455042 100644 --- a/pmoqobuz/src/api/catalog.rs +++ b/pmoqobuz/src/api/catalog.rs @@ -167,7 +167,7 @@ struct SearchResponse { struct FileUrlResponse { url: String, mime_type: String, - sampling_rate: u32, + sampling_rate: f64, bit_depth: u32, format_id: u8, } @@ -260,8 +260,8 @@ impl QobuzApi { ("track_id", track_id), ]; - // Utiliser POST (comme Python) - let response: FileUrlResponse = self.post("/track/getFileUrl", ¶ms).await?; + // Utiliser GET (comme qobuz-player-client qui fonctionne) + let response: FileUrlResponse = self.get("/track/getFileUrl", ¶ms).await?; Ok(StreamInfo { url: response.url, diff --git a/pmoqobuz/src/api/mod.rs b/pmoqobuz/src/api/mod.rs index 4284f4ec..07b8738a 100644 --- a/pmoqobuz/src/api/mod.rs +++ b/pmoqobuz/src/api/mod.rs @@ -251,6 +251,10 @@ impl QobuzApi { request = request.header("X-User-Auth-Token", token); } + // Headers additionnels pour compatibilité avec qobuz-player-client + request = request.header("Accept-Language", "en,en-US;q=0.8,ko;q=0.6,zh;q=0.4,zh-CN;q=0.2"); + request = request.header("Access-Control-Request-Headers", "x-user-auth-token,x-app-id"); + // Ajouter les paramètres if method == "GET" { request = request.query(params); @@ -272,7 +276,7 @@ impl QobuzApi { if !status.is_success() { let error_text = response.text().await.unwrap_or_default(); - warn!("API error ({}) on {}: {}", status_code, endpoint, error_text); + debug!("API error ({}) on {}: {}", status_code, endpoint, error_text); return Err(QobuzError::from_status_code(status_code, error_text)); } @@ -286,7 +290,7 @@ impl QobuzApi { .get("message") .and_then(|m| m.as_str()) .unwrap_or("Unknown error"); - warn!("Qobuz API error on {}: {}", endpoint, message); + debug!("Qobuz API error on {}: {}", endpoint, message); return Err(QobuzError::ApiError { code: status_code, message: message.to_string(), @@ -297,7 +301,7 @@ impl QobuzApi { // Parser la réponse serde_json::from_str(&text).map_err(|e| { - warn!("Failed to parse response: {}", e); + debug!("Failed to parse response: {}", e); QobuzError::JsonParse(e) }) } diff --git a/pmoqobuz/src/api/signing.rs b/pmoqobuz/src/api/signing.rs index 0d3e0569..e4f5889c 100644 --- a/pmoqobuz/src/api/signing.rs +++ b/pmoqobuz/src/api/signing.rs @@ -10,7 +10,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; /// /// # Returns /// -/// Timestamp Unix sous forme de string avec décimales +/// Timestamp Unix sous forme de string (integer, sans décimales) /// /// # Exemple /// @@ -23,7 +23,7 @@ pub fn get_timestamp() -> String { SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() - .as_secs_f64() + .as_secs() .to_string() } @@ -107,16 +107,16 @@ mod tests { #[test] fn test_get_timestamp() { let ts = get_timestamp(); - // Vérifier que c'est un nombre valide - assert!(ts.parse::().is_ok()); + // Vérifier que c'est un nombre entier valide + assert!(ts.parse::().is_ok()); // Vérifier que c'est proche du temps actuel (>= 2024) - assert!(ts.parse::().unwrap() > 1704067200.0); // 1er janvier 2024 + assert!(ts.parse::().unwrap() > 1704067200); // 1er janvier 2024 } #[test] fn test_sign_track_get_file_url() { let signature = - sign_track_get_file_url("27", "stream", "12345", "1234567890.123", b"test_secret"); + sign_track_get_file_url("27", "stream", "12345", "1234567890", b"test_secret"); // Vérifier que c'est un hash MD5 valide (32 caractères hex) assert_eq!(signature.len(), 32); @@ -125,7 +125,7 @@ mod tests { #[test] fn test_sign_userlib_get_albums() { - let signature = sign_userlib_get_albums("1234567890.123", b"test_secret"); + let signature = sign_userlib_get_albums("1234567890", b"test_secret"); // Vérifier que c'est un hash MD5 valide (32 caractères hex) assert_eq!(signature.len(), 32); diff --git a/pmoqobuz/src/client.rs b/pmoqobuz/src/client.rs index 64b37499..3996a480 100644 --- a/pmoqobuz/src/client.rs +++ b/pmoqobuz/src/client.rs @@ -127,11 +127,33 @@ impl QobuzClient { let config_appid = config.get_qobuz_appid()?; let config_secret = config.get_qobuz_secret()?; + let config_spoofer_secret = config.get_qobuz_spoofer_secret()?; let mut used_config_credentials = false; - let mut api = match (config_appid, config_secret) { - (Some(app_id), Some(secret)) => { + let mut api = match (config_appid.clone(), config_spoofer_secret, config_secret) { + // Priority 1: Try memorized Spoofer secret (raw, no XOR) + (Some(app_id), Some(spoofer_secret), _) => { + info!( + "Trying memorized Spoofer secret with App ID: {}", + app_id + ); + match QobuzApi::with_raw_secret(&app_id, &spoofer_secret) { + Ok(api) => { + used_config_credentials = true; + api + } + Err(e) => { + info!( + "✗ Memorized Spoofer secret failed: {}. Re-fetching from Spoofer...", + e + ); + Self::try_spoofer_fallback(config).await? + } + } + } + // Priority 2: Try XOR secret (legacy configvalue) + (Some(app_id), None, Some(secret)) => { info!( "Creating Qobuz API with configured App ID: {} and XOR secret", app_id @@ -150,9 +172,10 @@ impl QobuzClient { } } } + // Priority 3: Fallback to Spoofer _ => { info!( - "AppID or secret not configured (or app_id without secret), using Spoofer..." + "AppID or secret not configured, using Spoofer..." ); Self::try_spoofer_fallback(config).await? } @@ -240,21 +263,40 @@ impl QobuzClient { info!("Testing {} timezone secret(s)...", secrets.len()); let (username, password) = config.get_qobuz_credentials()?; - for (timezone, secret) in secrets.iter() { - debug!("Testing timezone secret: {}", timezone); + // Optimization: Login once with first secret to get auth token + // Then test all secrets using the same token + if let Some((first_timezone, first_secret)) = secrets.first() { + if let Ok(temp_api) = QobuzApi::with_raw_secret(&app_id, first_secret) { + if let Ok(_auth_info) = temp_api.login(&username, &password).await { + // Now test each secret with the authenticated token + for (timezone, secret) in secrets.iter() { + debug!("Testing timezone secret: {}", timezone); - if let Ok(test_api) = QobuzApi::with_raw_secret(&app_id, secret) { - if let Ok(auth_info) = test_api.login(&username, &password).await { - // Test the secret using userlib_get_albums (like Python's setSec()) - // This matches Python's behavior exactly - if test_api.userlib_get_albums().await.is_ok() { - info!("✓ Secret from timezone '{}' works!", timezone); - if let Err(e) = config.set_qobuz_appid(&app_id) { - debug!("Could not save appid: {}", e); + if let Ok(test_api) = QobuzApi::with_raw_secret(&app_id, secret) { + // Set the auth token from our initial login + test_api.set_auth_token( + temp_api.auth_token().unwrap(), + temp_api.user_id().unwrap(), + ); + + // Test the secret using track/getFileUrl (like qobuz-player-client) + // Use the same hardcoded track_id (64868955) as qobuz-player-client + if test_api.get_file_url("64868955").await.is_ok() { + info!("✓ Secret from timezone '{}' works!", timezone); + + // Save both appid and the working secret + if let Err(e) = config.set_qobuz_appid(&app_id) { + debug!("Could not save appid: {}", e); + } + if let Err(e) = config.set_qobuz_spoofer_secret(secret) { + debug!("Could not save spoofer secret: {}", e); + } + + return Ok(Some((app_id.clone(), secret.clone()))); + } else { + debug!("✗ Secret from timezone '{}' failed track/getFileUrl test", timezone); + } } - return Ok(Some((app_id.clone(), secret.clone()))); - } else { - debug!("✗ Secret from timezone '{}' failed userlib test", timezone); } } } diff --git a/pmoqobuz/src/config_ext.rs b/pmoqobuz/src/config_ext.rs index fdf9025c..085a63a8 100644 --- a/pmoqobuz/src/config_ext.rs +++ b/pmoqobuz/src/config_ext.rs @@ -129,6 +129,26 @@ pub trait QobuzConfigExt { /// * `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 @@ -291,6 +311,22 @@ impl QobuzConfigExt for Config { ) } + 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)), diff --git a/pmoqobuz/src/models.rs b/pmoqobuz/src/models.rs index 64196c1b..7b4be722 100644 --- a/pmoqobuz/src/models.rs +++ b/pmoqobuz/src/models.rs @@ -191,8 +191,8 @@ pub struct StreamInfo { pub url: String, /// Type MIME pub mime_type: String, - /// Fréquence d'échantillonnage (Hz) - pub sampling_rate: u32, + /// Fréquence d'échantillonnage (kHz) + pub sampling_rate: f64, /// Profondeur de bits pub bit_depth: u32, /// Format ID Qobuz