feat: implement Qobuz CMAF streaming and AES decryption
Introduces a new CMAF streaming module for Qobuz that handles progressive segment fetching and full-track decryption. The implementation adds HKDF session key derivation, AES-128-CBC key unwrapping, and AES-128-CTR frame decryption, alongside an ISO BMFF parser for extracting FLAC headers and segment metadata. It also integrates automatic session renewal with double-checked locking, a generic async retry mechanism for transient HTTP errors, and centralized error handling. Finally, it exposes `CmafStreamInfo` and async streaming methods in the public API, updating cryptographic dependencies accordingly.
This commit is contained in:
@@ -4,10 +4,12 @@
|
||||
|
||||
pub mod auth;
|
||||
pub mod catalog;
|
||||
pub mod cmaf;
|
||||
pub mod signing;
|
||||
pub mod spoofer;
|
||||
pub mod user;
|
||||
|
||||
use crate::api::cmaf::CmafSessionManager;
|
||||
use crate::error::{QobuzError, Result};
|
||||
use crate::models::AudioFormat;
|
||||
use reqwest::{Client, Response};
|
||||
@@ -95,6 +97,8 @@ pub struct QobuzApi {
|
||||
user_id: RwLock<Option<String>>,
|
||||
/// Format audio par défaut
|
||||
format_id: AudioFormat,
|
||||
/// Gestionnaire de session CMAF (renouvellement automatique thread-safe)
|
||||
pub(crate) cmaf_session: CmafSessionManager,
|
||||
}
|
||||
|
||||
impl QobuzApi {
|
||||
@@ -114,6 +118,7 @@ impl QobuzApi {
|
||||
user_auth_token: RwLock::new(None),
|
||||
user_id: RwLock::new(None),
|
||||
format_id: AudioFormat::default(),
|
||||
cmaf_session: CmafSessionManager::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -316,6 +321,57 @@ impl QobuzApi {
|
||||
self.handle_response(response, endpoint, params).await
|
||||
}
|
||||
|
||||
/// Retourne le client HTTP interne (utilisé par les endpoints CMAF).
|
||||
pub(crate) fn http_client(&self) -> &Client {
|
||||
&self.client
|
||||
}
|
||||
|
||||
/// Assure qu'une session CMAF valide existe et retourne `(session_id, infos)`.
|
||||
///
|
||||
/// Crée ou renouvelle automatiquement la session si elle est absente ou expire
|
||||
/// dans moins de 60 secondes. Les appels concurrents sont sérialisés pour
|
||||
/// éviter des sessions incohérentes.
|
||||
pub async fn ensure_cmaf_session(&self) -> Result<(String, String)> {
|
||||
let app_id = self.app_id.read().unwrap().clone();
|
||||
let auth_token = self
|
||||
.user_auth_token
|
||||
.read()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.ok_or_else(|| QobuzError::Unauthorized("Token d'authentification manquant pour CMAF".into()))?;
|
||||
|
||||
self.cmaf_session
|
||||
.ensure_session(&self.client, &app_id, &auth_token)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Récupère l'URL de fichier CMAF pour un track avec le format donné.
|
||||
pub async fn get_cmaf_file_url(
|
||||
&self,
|
||||
track_id: &str,
|
||||
format_id: u32,
|
||||
) -> Result<crate::api::cmaf::CmafFileUrlResponse> {
|
||||
let app_id = self.app_id.read().unwrap().clone();
|
||||
let auth_token = self
|
||||
.user_auth_token
|
||||
.read()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.ok_or_else(|| QobuzError::Unauthorized("Token d'authentification manquant pour CMAF".into()))?;
|
||||
|
||||
let (session_id, _infos) = self.ensure_cmaf_session().await?;
|
||||
|
||||
crate::api::cmaf::get_file_url(
|
||||
&self.client,
|
||||
&app_id,
|
||||
&auth_token,
|
||||
&session_id,
|
||||
track_id,
|
||||
format_id,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Traite la réponse HTTP
|
||||
async fn handle_response<T: DeserializeOwned>(
|
||||
&self,
|
||||
|
||||
Reference in New Issue
Block a user