Big Debug of PMOQobuz step 3
This commit is contained in:
@@ -72,7 +72,13 @@ impl QobuzApi {
|
||||
|
||||
let params = [("username", username), ("password", password)];
|
||||
|
||||
let response: LoginResponse = self.post("/user/login", ¶ms).await?;
|
||||
let response: LoginResponse = match self.post("/user/login", ¶ms).await {
|
||||
Ok(resp) => resp,
|
||||
Err(e) => {
|
||||
info!("✗ Login failed for {}: {}", username, e);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
// Vérifier que l'utilisateur a un abonnement valide
|
||||
if response.user.credential.parameters.is_none() {
|
||||
@@ -88,8 +94,8 @@ impl QobuzApi {
|
||||
.parameters
|
||||
.and_then(|p| p.short_label);
|
||||
|
||||
debug!(
|
||||
"Login successful - User ID: {}, Subscription: {:?}",
|
||||
info!(
|
||||
"✓ Login successful - User ID: {}, Subscription: {:?}",
|
||||
user_id, subscription_label
|
||||
);
|
||||
|
||||
|
||||
@@ -234,9 +234,9 @@ impl QobuzApi {
|
||||
let format_id = self.format_id.id().to_string();
|
||||
let intent = "stream";
|
||||
let timestamp = signing::get_timestamp();
|
||||
let app_id = self.app_id();
|
||||
let user_auth_token = self
|
||||
.auth_token()
|
||||
|
||||
// Vérifier que le token d'authentification est disponible
|
||||
self.auth_token()
|
||||
.ok_or_else(|| QobuzError::Unauthorized("Missing auth token".to_string()))?;
|
||||
|
||||
// Signer la requête (comme Python: track_getFileUrl)
|
||||
@@ -249,18 +249,19 @@ impl QobuzApi {
|
||||
);
|
||||
|
||||
// Construire les paramètres signés
|
||||
// Note: app_id et user_auth_token sont envoyés automatiquement comme headers
|
||||
// par la méthode request() (X-App-Id et X-User-Auth-Token)
|
||||
// IMPORTANT: L'ordre doit correspondre EXACTEMENT à Python (raw.py)
|
||||
let params = [
|
||||
("track_id", track_id),
|
||||
("format_id", format_id.as_str()),
|
||||
("intent", intent),
|
||||
("request_ts", timestamp.as_str()),
|
||||
("request_sig", signature.as_str()),
|
||||
("app_id", app_id.as_str()),
|
||||
("user_auth_token", user_auth_token.as_str()),
|
||||
("track_id", track_id),
|
||||
];
|
||||
|
||||
// Utiliser GET (comme Python après sept 2024 selon le commentaire)
|
||||
let response: FileUrlResponse = self.get("/track/getFileUrl", ¶ms).await?;
|
||||
// Utiliser POST (comme Python)
|
||||
let response: FileUrlResponse = self.post("/track/getFileUrl", ¶ms).await?;
|
||||
|
||||
Ok(StreamInfo {
|
||||
url: response.url,
|
||||
|
||||
@@ -21,6 +21,7 @@ pub use spoofer::Spoofer;
|
||||
|
||||
/// URL de base de l'API Qobuz
|
||||
const API_BASE_URL: &str = "https://www.qobuz.com/api.json/0.2";
|
||||
//const API_BASE_URL: &str = "http://localhost:8080/api.json/0.2";
|
||||
|
||||
/// App ID Qobuz par défaut
|
||||
///
|
||||
@@ -91,6 +92,23 @@ impl QobuzApi {
|
||||
Ok(api)
|
||||
}
|
||||
|
||||
/// Crée une API avec un secret brut (déjà décodé/dérivé)
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `app_id` - App ID Qobuz
|
||||
/// * `raw_secret` - Secret prêt à l'emploi (comme ceux du Spoofer)
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// Utilisez cette méthode pour les secrets du Spoofer qui sont déjà
|
||||
/// décodés et prêts à l'emploi (pas besoin de XOR avec l'app_id)
|
||||
pub fn with_raw_secret(app_id: impl Into<String>, raw_secret: &str) -> Result<Self> {
|
||||
let api = Self::new(app_id)?;
|
||||
api.set_secret(raw_secret.as_bytes().to_vec());
|
||||
Ok(api)
|
||||
}
|
||||
|
||||
/// Définit le secret s4 directement
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -160,7 +178,7 @@ impl QobuzApi {
|
||||
self.app_id.read().unwrap().clone()
|
||||
}
|
||||
|
||||
/// Met à jour dynamiquement l'app_id et le secret associés
|
||||
/// Met à jour dynamiquement l'app_id et le secret associés (avec XOR)
|
||||
pub fn update_credentials(&self, app_id: impl Into<String>, configvalue: &str) -> Result<()> {
|
||||
{
|
||||
let mut current = self.app_id.write().unwrap();
|
||||
@@ -170,6 +188,16 @@ impl QobuzApi {
|
||||
self.set_secret_from_configvalue(configvalue)
|
||||
}
|
||||
|
||||
/// Met à jour dynamiquement l'app_id et le secret brut (sans XOR)
|
||||
pub fn update_credentials_raw(&self, app_id: impl Into<String>, raw_secret: &str) {
|
||||
{
|
||||
let mut current = self.app_id.write().unwrap();
|
||||
*current = app_id.into();
|
||||
}
|
||||
|
||||
self.set_secret(raw_secret.as_bytes().to_vec());
|
||||
}
|
||||
|
||||
/// Retourne le token d'authentification si disponible
|
||||
pub fn auth_token(&self) -> Option<String> {
|
||||
self.user_auth_token.read().unwrap().clone()
|
||||
@@ -232,11 +260,11 @@ impl QobuzApi {
|
||||
|
||||
// Envoyer la requête
|
||||
let response = request.send().await?;
|
||||
self.handle_response(response).await
|
||||
self.handle_response(response, endpoint).await
|
||||
}
|
||||
|
||||
/// Traite la réponse HTTP
|
||||
async fn handle_response<T: DeserializeOwned>(&self, response: Response) -> Result<T> {
|
||||
async fn handle_response<T: DeserializeOwned>(&self, response: Response, endpoint: &str) -> Result<T> {
|
||||
let status = response.status();
|
||||
let status_code = status.as_u16();
|
||||
|
||||
@@ -244,7 +272,7 @@ impl QobuzApi {
|
||||
|
||||
if !status.is_success() {
|
||||
let error_text = response.text().await.unwrap_or_default();
|
||||
warn!("API error ({}): {}", status_code, error_text);
|
||||
warn!("API error ({}) on {}: {}", status_code, endpoint, error_text);
|
||||
return Err(QobuzError::from_status_code(status_code, error_text));
|
||||
}
|
||||
|
||||
@@ -258,7 +286,7 @@ impl QobuzApi {
|
||||
.get("message")
|
||||
.and_then(|m| m.as_str())
|
||||
.unwrap_or("Unknown error");
|
||||
warn!("Qobuz API error: {}", message);
|
||||
warn!("Qobuz API error on {}: {}", endpoint, message);
|
||||
return Err(QobuzError::ApiError {
|
||||
code: status_code,
|
||||
message: message.to_string(),
|
||||
|
||||
@@ -78,6 +78,23 @@ impl Spoofer {
|
||||
.to_string())
|
||||
}
|
||||
|
||||
/// Extrait l'appSecret depuis le bundle (secret MD5 à 32 caractères)
|
||||
///
|
||||
/// Ce secret est utilisé directement par Qobuz (nouvelle méthode)
|
||||
/// au lieu d'être XORé avec l'app_id
|
||||
pub fn get_app_secret(&self) -> Result<String> {
|
||||
let captures = self
|
||||
.app_id_regex
|
||||
.captures(&self.bundle)
|
||||
.ok_or_else(|| anyhow::anyhow!("AppSecret non trouvé dans le bundle"))?;
|
||||
|
||||
Ok(captures
|
||||
.name("secret")
|
||||
.ok_or_else(|| anyhow::anyhow!("Groupe secret non trouvé"))?
|
||||
.as_str()
|
||||
.to_string())
|
||||
}
|
||||
|
||||
/// Extrait les secrets depuis le bundle
|
||||
pub fn get_secrets(&self) -> Result<IndexMap<String, String>> {
|
||||
// Étape 1: Extraire tous les seed/timezone pairs
|
||||
|
||||
@@ -15,7 +15,7 @@ struct PaginatedResponse<T> {
|
||||
|
||||
/// Réponse de l'endpoint /favorite/getUserFavorites
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct FavoritesResponse {
|
||||
pub(crate) struct FavoritesResponse {
|
||||
#[serde(default)]
|
||||
albums: Option<PaginatedResponse<AlbumResponse>>,
|
||||
#[serde(default)]
|
||||
|
||||
@@ -133,7 +133,7 @@ impl QobuzClient {
|
||||
let mut api = match (config_appid, config_secret) {
|
||||
(Some(app_id), Some(secret)) => {
|
||||
info!(
|
||||
"Creating Qobuz API with configured App ID: {} and secret",
|
||||
"Creating Qobuz API with configured App ID: {} and XOR secret",
|
||||
app_id
|
||||
);
|
||||
match QobuzApi::with_secret(&app_id, &secret) {
|
||||
@@ -152,7 +152,7 @@ impl QobuzClient {
|
||||
}
|
||||
_ => {
|
||||
info!(
|
||||
"AppID or secret not configured, using Spoofer to obtain valid credentials..."
|
||||
"AppID or secret not configured (or app_id without secret), using Spoofer..."
|
||||
);
|
||||
Self::try_spoofer_fallback(config).await?
|
||||
}
|
||||
@@ -219,7 +219,8 @@ impl QobuzClient {
|
||||
/// - Quand les credentials configurés sont invalides/expirés
|
||||
async fn try_spoofer_fallback(config: &Config) -> Result<QobuzApi> {
|
||||
if let Some((app_id, secret)) = Self::fetch_spoofer_credentials(config).await? {
|
||||
return QobuzApi::with_secret(app_id, &secret);
|
||||
// Use raw secret from Spoofer (no XOR)
|
||||
return QobuzApi::with_raw_secret(app_id, &secret);
|
||||
}
|
||||
|
||||
info!(
|
||||
@@ -231,48 +232,41 @@ impl QobuzClient {
|
||||
async fn fetch_spoofer_credentials(config: &Config) -> Result<Option<(String, String)>> {
|
||||
match crate::api::Spoofer::new().await {
|
||||
Ok(spoofer) => match spoofer.get_app_id() {
|
||||
Ok(app_id) => match spoofer.get_secrets() {
|
||||
Ok(secrets) => {
|
||||
info!("Spoofer found {} secret(s), testing them...", secrets.len());
|
||||
Ok(app_id) => {
|
||||
// Use timezone secrets (like Python)
|
||||
// Note: App Secret from bundle doesn't work for signed requests
|
||||
match spoofer.get_secrets() {
|
||||
Ok(secrets) => {
|
||||
info!("Testing {} timezone secret(s)...", secrets.len());
|
||||
let (username, password) = config.get_qobuz_credentials()?;
|
||||
|
||||
for (timezone, secret) in secrets.iter() {
|
||||
debug!("Testing secret for timezone: {}", timezone);
|
||||
for (timezone, secret) in secrets.iter() {
|
||||
debug!("Testing timezone secret: {}", timezone);
|
||||
|
||||
match QobuzApi::with_secret(&app_id, secret) {
|
||||
Ok(_) => {
|
||||
info!(
|
||||
"✓ Successfully created API with secret from timezone: {}",
|
||||
timezone
|
||||
);
|
||||
|
||||
if let Err(e) = config.set_qobuz_appid(&app_id) {
|
||||
debug!("Could not save appid to config: {}", e);
|
||||
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);
|
||||
}
|
||||
return Ok(Some((app_id.clone(), secret.clone())));
|
||||
} else {
|
||||
debug!("✗ Secret from timezone '{}' failed userlib test", timezone);
|
||||
}
|
||||
}
|
||||
if let Err(e) = config.set_qobuz_secret(secret) {
|
||||
debug!("Could not save secret to config: {}", e);
|
||||
}
|
||||
|
||||
return Ok(Some((app_id.clone(), secret.clone())));
|
||||
}
|
||||
Err(e) => {
|
||||
debug!(
|
||||
"Failed to create API with secret from {}: {}",
|
||||
timezone, e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("✗ No valid secret from Spoofer secrets list");
|
||||
Ok(None)
|
||||
}
|
||||
Err(e) => {
|
||||
info!(
|
||||
"Spoofer failed to extract secrets: {}, falling back to DEFAULT_APP_ID",
|
||||
e
|
||||
);
|
||||
Ok(None)
|
||||
info!("✗ No valid secret found");
|
||||
Ok(None)
|
||||
}
|
||||
Err(e) => {
|
||||
info!("Failed to extract timezone secrets: {}", e);
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
@@ -301,7 +295,8 @@ impl QobuzClient {
|
||||
|
||||
match Self::fetch_spoofer_credentials(config_arc.as_ref()).await? {
|
||||
Some((app_id, secret)) => {
|
||||
self.api.update_credentials(app_id, &secret)?;
|
||||
// Use raw secret (no XOR) from Spoofer
|
||||
self.api.update_credentials_raw(app_id, &secret);
|
||||
info!("✓ Updated Qobuz API credentials using Spoofer");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user