Big debug of pmoqobuz step 1

This commit is contained in:
2025-12-12 21:42:04 +00:00
parent 7389c55970
commit cd19d68703
26 changed files with 18565 additions and 652 deletions

View File

@@ -67,7 +67,7 @@ impl QobuzApi {
///
/// * `QobuzError::Unauthorized` - Credentials invalides
/// * `QobuzError::SubscriptionRequired` - Compte gratuit (non éligible)
pub async fn login(&mut self, username: &str, password: &str) -> Result<AuthInfo> {
pub async fn login(&self, username: &str, password: &str) -> Result<AuthInfo> {
info!("Attempting to login to Qobuz as {}", username);
let params = [("username", username), ("password", password)];
@@ -105,14 +105,13 @@ impl QobuzApi {
/// Vérifie si le client est authentifié
pub fn is_authenticated(&self) -> bool {
self.user_auth_token.is_some() && self.user_id.is_some()
self.auth_token().is_some() && self.user_id().is_some()
}
/// Déconnecte l'utilisateur
pub fn logout(&mut self) {
pub fn logout(&self) {
debug!("Logging out");
self.user_auth_token = None;
self.user_id = None;
self.clear_auth();
}
}
@@ -122,7 +121,7 @@ mod tests {
#[test]
fn test_is_authenticated() {
let mut api = QobuzApi::new("test_app_id").unwrap();
let api = QobuzApi::new("test_app_id").unwrap();
assert!(!api.is_authenticated());
api.set_auth_token("token".to_string(), "user123".to_string());

View File

@@ -225,26 +225,19 @@ impl QobuzApi {
debug!("Fetching file URL for track {}", track_id);
// Vérifier que le secret est disponible
let secret = self
.secret()
.ok_or_else(|| {
QobuzError::Configuration(
"Secret not configured. Cannot sign track/getFileUrl request.".to_string(),
)
})?;
let secret = self.secret().ok_or_else(|| {
QobuzError::Configuration(
"Secret not configured. Cannot sign track/getFileUrl request.".to_string(),
)
})?;
let format_id = self.format_id.id().to_string();
let intent = "stream";
let timestamp = signing::get_timestamp();
// Signer la requête (comme Python: track_getFileUrl)
let signature = signing::sign_track_get_file_url(
&format_id,
intent,
track_id,
&timestamp,
secret,
);
let signature =
signing::sign_track_get_file_url(&format_id, intent, track_id, &timestamp, &secret);
debug!(
"Signing track/getFileUrl: track_id={}, format_id={}, ts={}",

View File

@@ -13,6 +13,7 @@ use crate::models::AudioFormat;
use reqwest::{Client, Response};
use serde::de::DeserializeOwned;
use serde_json::Value;
use std::sync::RwLock;
use std::time::Duration;
use tracing::{debug, warn};
@@ -45,9 +46,9 @@ pub struct QobuzApi {
/// - Depuis le Spoofer (secrets dynamiques)
secret: Option<Vec<u8>>,
/// Token d'authentification utilisateur
user_auth_token: Option<String>,
user_auth_token: RwLock<Option<String>>,
/// ID utilisateur
user_id: Option<String>,
user_id: RwLock<Option<String>>,
/// Format audio par défaut
format_id: AudioFormat,
}
@@ -66,8 +67,8 @@ impl QobuzApi {
client,
app_id: app_id.into(),
secret: None,
user_auth_token: None,
user_id: None,
user_auth_token: RwLock::new(None),
user_id: RwLock::new(None),
format_id: AudioFormat::default(),
})
}
@@ -132,9 +133,15 @@ impl QobuzApi {
}
/// Définit le token d'authentification
pub fn set_auth_token(&mut self, token: String, user_id: String) {
self.user_auth_token = Some(token);
self.user_id = Some(user_id);
pub fn set_auth_token(&self, token: String, user_id: String) {
*self.user_auth_token.write().unwrap() = Some(token);
*self.user_id.write().unwrap() = Some(user_id);
}
/// Efface les informations d'authentification
pub fn clear_auth(&self) {
*self.user_auth_token.write().unwrap() = None;
*self.user_id.write().unwrap() = None;
}
/// Définit le format audio par défaut
@@ -153,13 +160,13 @@ impl QobuzApi {
}
/// Retourne le token d'authentification si disponible
pub fn auth_token(&self) -> Option<&str> {
self.user_auth_token.as_deref()
pub fn auth_token(&self) -> Option<String> {
self.user_auth_token.read().unwrap().clone()
}
/// Retourne l'ID utilisateur si disponible
pub fn user_id(&self) -> Option<&str> {
self.user_id.as_deref()
pub fn user_id(&self) -> Option<String> {
self.user_id.read().unwrap().clone()
}
/// Effectue une requête GET à l'API
@@ -200,7 +207,7 @@ impl QobuzApi {
// Ajouter les headers
request = request.header("X-App-Id", &self.app_id);
if let Some(ref token) = self.user_auth_token {
if let Some(token) = self.auth_token() {
request = request.header("X-User-Auth-Token", token);
}
@@ -269,10 +276,10 @@ mod tests {
#[test]
fn test_set_auth_token() {
let mut api = QobuzApi::new("test_app_id").unwrap();
let api = QobuzApi::new("test_app_id").unwrap();
api.set_auth_token("test_token".to_string(), "user123".to_string());
assert_eq!(api.auth_token(), Some("test_token"));
assert_eq!(api.user_id(), Some("user123"));
assert_eq!(api.auth_token().as_deref(), Some("test_token"));
assert_eq!(api.user_id().as_deref(), Some("user123"));
}
#[test]

View File

@@ -115,13 +115,8 @@ mod tests {
#[test]
fn test_sign_track_get_file_url() {
let signature = sign_track_get_file_url(
"27",
"stream",
"12345",
"1234567890.123",
b"test_secret",
);
let signature =
sign_track_get_file_url("27", "stream", "12345", "1234567890.123", b"test_secret");
// Vérifier que c'est un hash MD5 valide (32 caractères hex)
assert_eq!(signature.len(), 32);

View File

@@ -41,8 +41,9 @@ impl Spoofer {
.await?;
// Extraire l'URL du bundle
let bundle_url_regex =
Regex::new(r#"<script src="(/resources/\d+\.\d+\.\d+-[a-z]\d{3}/bundle\.js)"></script>"#)?;
let bundle_url_regex = Regex::new(
r#"<script src="(/resources/\d+\.\d+\.\d+-[a-z]\d{3}/bundle\.js)"></script>"#,
)?;
let bundle_url = bundle_url_regex
.captures(&login_page)
.and_then(|cap| cap.get(1))
@@ -168,19 +169,14 @@ impl Spoofer {
// Décoder en base64
match STANDARD.decode(trimmed) {
Ok(decoded_bytes) => {
match String::from_utf8(decoded_bytes) {
Ok(decoded_str) => {
decoded_secrets.insert(timezone, decoded_str);
}
Err(e) => {
eprintln!(
"Erreur UTF-8 pour timezone {}: {}",
timezone, e
);
}
Ok(decoded_bytes) => match String::from_utf8(decoded_bytes) {
Ok(decoded_str) => {
decoded_secrets.insert(timezone, decoded_str);
}
}
Err(e) => {
eprintln!("Erreur UTF-8 pour timezone {}: {}", timezone, e);
}
},
Err(e) => {
eprintln!(
"Erreur de décodage base64 pour timezone {}: {}",

View File

@@ -32,9 +32,8 @@ struct UserPlaylistsResponse {
impl QobuzApi {
/// Vérifie que l'utilisateur est authentifié
fn ensure_authenticated(&self) -> Result<&str> {
self.user_id
.as_deref()
fn ensure_authenticated(&self) -> Result<String> {
self.user_id()
.ok_or_else(|| QobuzError::Unauthorized("Not authenticated".to_string()))
}
@@ -43,7 +42,11 @@ impl QobuzApi {
let user_id = self.ensure_authenticated()?;
debug!("Fetching favorite albums for user {}", user_id);
let params = [("user_id", user_id), ("type", "albums"), ("limit", "1000")];
let params = [
("user_id", user_id.as_str()),
("type", "albums"),
("limit", "1000"),
];
let response: FavoritesResponse = self.get("/favorite/getUserFavorites", &params).await?;
@@ -64,7 +67,11 @@ impl QobuzApi {
let user_id = self.ensure_authenticated()?;
debug!("Fetching favorite artists for user {}", user_id);
let params = [("user_id", user_id), ("type", "artists"), ("limit", "1000")];
let params = [
("user_id", user_id.as_str()),
("type", "artists"),
("limit", "1000"),
];
let response: FavoritesResponse = self.get("/favorite/getUserFavorites", &params).await?;
@@ -84,7 +91,11 @@ impl QobuzApi {
let user_id = self.ensure_authenticated()?;
debug!("Fetching favorite tracks for user {}", user_id);
let params = [("user_id", user_id), ("type", "tracks"), ("limit", "1000")];
let params = [
("user_id", user_id.as_str()),
("type", "tracks"),
("limit", "1000"),
];
let response: FavoritesResponse = self.get("/favorite/getUserFavorites", &params).await?;
@@ -105,7 +116,7 @@ impl QobuzApi {
let user_id = self.ensure_authenticated()?;
debug!("Fetching playlists for user {}", user_id);
let params = [("user_id", user_id), ("limit", "1000")];
let params = [("user_id", user_id.as_str()), ("limit", "1000")];
let response: UserPlaylistsResponse =
self.get("/playlist/getUserPlaylists", &params).await?;
@@ -126,7 +137,7 @@ impl QobuzApi {
album_id, user_id
);
let params = [("album_id", album_id), ("user_id", user_id)];
let params = [("album_id", album_id), ("user_id", user_id.as_str())];
self.get::<serde_json::Value>("/favorite/create", &params)
.await?;
@@ -141,7 +152,7 @@ impl QobuzApi {
album_id, user_id
);
let params = [("album_ids", album_id), ("user_id", user_id)];
let params = [("album_ids", album_id), ("user_id", user_id.as_str())];
self.get::<serde_json::Value>("/favorite/delete", &params)
.await?;
@@ -156,7 +167,7 @@ impl QobuzApi {
track_id, user_id
);
let params = [("track_id", track_id), ("user_id", user_id)];
let params = [("track_id", track_id), ("user_id", user_id.as_str())];
self.get::<serde_json::Value>("/favorite/create", &params)
.await?;
@@ -171,7 +182,7 @@ impl QobuzApi {
track_id, user_id
);
let params = [("track_ids", track_id), ("user_id", user_id)];
let params = [("track_ids", track_id), ("user_id", user_id.as_str())];
self.get::<serde_json::Value>("/favorite/delete", &params)
.await?;
@@ -212,19 +223,16 @@ impl QobuzApi {
self.ensure_authenticated()?;
// Vérifier que le secret est disponible
let secret = self
.secret()
.ok_or_else(|| {
QobuzError::Configuration(
"Secret not configured. Cannot sign userLibrary/getAlbumsList request."
.to_string(),
)
})?;
let secret = self.secret().ok_or_else(|| {
QobuzError::Configuration(
"Secret not configured. Cannot sign userLibrary/getAlbumsList request.".to_string(),
)
})?;
let timestamp = signing::get_timestamp();
// Signer la requête (comme Python: userlib_getAlbums)
let signature = signing::sign_userlib_get_albums(&timestamp, secret);
let signature = signing::sign_userlib_get_albums(&timestamp, &secret);
debug!(
"Signing userLibrary/getAlbumsList: app_id={}, ts={}",
@@ -239,7 +247,7 @@ impl QobuzApi {
let params = [
("app_id", self.app_id()),
("user_auth_token", user_auth_token),
("user_auth_token", user_auth_token.as_str()),
("request_ts", timestamp.as_str()),
("request_sig", signature.as_str()),
];
@@ -252,9 +260,9 @@ impl QobuzApi {
///
/// Cette méthode est équivalente au test fait dans `setSec()` en Python.
/// Elle retourne `true` si le secret fonctionne, `false` sinon.
pub async fn test_secret(&self, secret: &[u8]) -> bool {
pub async fn test_secret(&self, _secret: &[u8]) -> bool {
// Sauvegarder le secret actuel
let current_secret = self.secret().map(|s| s.to_vec());
let _current_secret = self.secret();
// Définir temporairement le nouveau secret
// Note: cette méthode nécessite &mut self, donc on doit la rendre mutable