Big debug of pmoqobuz step 1
This commit is contained in:
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
×tamp,
|
||||
secret,
|
||||
);
|
||||
let signature =
|
||||
signing::sign_track_get_file_url(&format_id, intent, track_id, ×tamp, &secret);
|
||||
|
||||
debug!(
|
||||
"Signing track/getFileUrl: track_id={}, format_id={}, ts={}",
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {}: {}",
|
||||
|
||||
@@ -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", ¶ms).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", ¶ms).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", ¶ms).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", ¶ms).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", ¶ms)
|
||||
.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", ¶ms)
|
||||
.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", ¶ms)
|
||||
.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", ¶ms)
|
||||
.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(×tamp, secret);
|
||||
let signature = signing::sign_userlib_get_albums(×tamp, &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
|
||||
|
||||
Reference in New Issue
Block a user