Files
pmomusic/pmoqobuz/API_ANALYSIS.md

9.4 KiB

Analyse des différences entre l'API Rust et Python

Vue d'ensemble

L'implémentation actuelle de pmoqobuz ne suit pas complètement l'API de référence Python (qobuz.api.raw). Voici les principales différences et ce qui doit être corrigé.

Problèmes identifiés

1. Gestion du secret s4 manquante

Python :

  • Accepte soit appid + configvalue (secret encodé en base64)
  • Soit utilise le Spoofer pour obtenir l'appID et les secrets dynamiquement
  • Le configvalue est décodé et XORé avec l'appID pour obtenir le secret s4
  • Le secret s4 est utilisé pour signer certaines requêtes critiques

Rust actuel :

  • Utilise un DEFAULT_APP_ID codé en dur
  • Pas de gestion du secret s4
  • Pas d'utilisation du Spoofer pour obtenir l'appID/secret
  • Pas de méthode pour décoder et dériver le secret depuis un configvalue

Impact :

  • Les requêtes track/getFileUrl et userLibrary/getAlbumsList échoueront probablement car elles nécessitent une signature MD5

2. Signature MD5 des requêtes manquante

Python - track_getFileUrl :

ts = str(time.time())
stringvalue = ("trackgetFileUrlformat_id" + fmt_id +
               "intent" + intent +
               "track_id" + track_id + ts).encode("ASCII")
stringvalue += self.s4  # Secret ajouté
rq_sig = str(hashlib.md5(stringvalue).hexdigest())
params = {
    "format_id": fmt_id,
    "intent": intent,
    "request_ts": ts,        # ← Timestamp
    "request_sig": rq_sig,   # ← Signature MD5
    "track_id": track_id,
}

Rust actuel (catalog.rs:210-218) :

let params = [
    ("track_id", track_id),
    ("format_id", &format_id),
    ("intent", "stream"),
    // ❌ MANQUE: request_ts
    // ❌ MANQUE: request_sig
];

Impact :

  • Les requêtes de streaming peuvent échouer ou retourner des URLs invalides

3. Méthode userlib_getAlbums manquante

Python :

def userlib_getAlbums(self, **ka):
    ts = str(time.time())
    r_sig = "userLibrarygetAlbumsList" + str(ts) + str(ka["sec"])
    r_sig_hashed = hashlib.md5(r_sig.encode("utf-8")).hexdigest()
    params = {
        "app_id": self.appid,
        "user_auth_token": self.user_auth_token,
        "request_ts": ts,
        "request_sig": r_sig_hashed,
    }
    return self._api_request(params, "/userLibrary/getAlbumsList")

Rust actuel :

  • Méthode totalement absente

Impact :

  • Impossible de tester les secrets (méthode setSec())
  • Impossible de récupérer la bibliothèque d'albums de l'utilisateur

4. Méthode setSec() manquante

Python :

def setSec(self):
    # Teste tous les secrets du spoofer
    for value in self.spoofer.getSecrets().values():
        self.s4 = value.encode("utf-8")
        if self.userlib_getAlbums(sec=self.s4) is not None:
            # Ce secret fonctionne !
            return

Rust actuel :

  • Méthode totalement absente
  • Pas de mécanisme pour tester et sélectionner le bon secret

Impact :

  • Si on utilise le Spoofer, impossible de trouver le bon secret parmi ceux retournés

5. ⚠️ Configuration incomplète

Python :

  • Peut être initialisé avec appid + configvalue OU utiliser le Spoofer

Rust actuel :

  • Configuration du username/password via QobuzConfigExt
  • Pas de configuration pour appid et secret/configvalue

Impact :

  • Impossible de configurer manuellement un appID et secret valides
  • Dépendance à un appID codé en dur qui peut devenir obsolète

Plan de correction

Phase 1: Extension de la configuration

Fichier: pmoqobuz/src/config_ext.rs

Ajouter au trait QobuzConfigExt :

  • get_qobuz_appid() / set_qobuz_appid()
  • get_qobuz_secret() / set_qobuz_secret() (stocke la valeur base64)

Phase 2: Ajout du support du secret dans QobuzApi

Fichier: pmoqobuz/src/api/mod.rs

Modifications de QobuzApi :

pub struct QobuzApi {
    client: Client,
    app_id: String,
    secret: Option<Vec<u8>>,  // ← Nouveau : secret s4 décodé
    user_auth_token: Option<String>,
    user_id: Option<String>,
    format_id: AudioFormat,
}

Nouvelles méthodes :

impl QobuzApi {
    /// Crée une API avec appid + configvalue
    pub fn with_secret(app_id: impl Into<String>, configvalue: &str) -> Result<Self>;

    /// Crée une API en utilisant le Spoofer
    pub async fn with_spoofer() -> Result<Self>;

    /// Définit le secret s4
    pub fn set_secret(&mut self, secret: Vec<u8>);

    /// Teste un secret en appelant userlib_getAlbums
    async fn test_secret(&self, secret: &[u8]) -> bool;

    /// Teste et sélectionne le bon secret depuis le Spoofer
    async fn set_secret_from_spoofer(&mut self, spoofer: &Spoofer) -> Result<()>;
}

Phase 3: Implémentation des méthodes signées

Fichier: pmoqobuz/src/api/signing.rs (nouveau)

use md5::{Md5, Digest};
use std::time::{SystemTime, UNIX_EPOCH};

/// Génère un timestamp Unix
pub fn get_timestamp() -> String {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_secs_f64()
        .to_string()
}

/// Signe une requête track/getFileUrl
pub fn sign_track_get_file_url(
    format_id: &str,
    intent: &str,
    track_id: &str,
    timestamp: &str,
    secret: &[u8],
) -> String {
    let mut hasher = Md5::new();
    hasher.update(b"trackgetFileUrlformat_id");
    hasher.update(format_id.as_bytes());
    hasher.update(b"intent");
    hasher.update(intent.as_bytes());
    hasher.update(b"track_id");
    hasher.update(track_id.as_bytes());
    hasher.update(timestamp.as_bytes());
    hasher.update(secret);
    format!("{:x}", hasher.finalize())
}

/// Signe une requête userLibrary/getAlbumsList
pub fn sign_userlib_get_albums(timestamp: &str, secret: &[u8]) -> String {
    let mut hasher = Md5::new();
    hasher.update(b"userLibrarygetAlbumsList");
    hasher.update(timestamp.as_bytes());
    hasher.update(secret);
    format!("{:x}", hasher.finalize())
}

Fichier: pmoqobuz/src/api/catalog.rs

Modifier get_file_url :

pub async fn get_file_url(&self, track_id: &str) -> Result<StreamInfo> {
    let format_id = self.format_id.id().to_string();
    let timestamp = signing::get_timestamp();

    // Signature MD5 requise !
    let secret = self.secret.as_ref()
        .ok_or_else(|| QobuzError::Configuration("Secret not configured".into()))?;

    let signature = signing::sign_track_get_file_url(
        &format_id,
        "stream",
        track_id,
        &timestamp,
        secret,
    );

    let params = [
        ("track_id", track_id),
        ("format_id", format_id.as_str()),
        ("intent", "stream"),
        ("request_ts", timestamp.as_str()),
        ("request_sig", signature.as_str()),
    ];

    let response: FileUrlResponse = self.get("/track/getFileUrl", &params).await?;
    // ...
}

Fichier: pmoqobuz/src/api/user.rs

Ajouter :

pub async fn get_user_albums(&self) -> Result<UserAlbumsResponse> {
    let timestamp = signing::get_timestamp();

    let secret = self.secret.as_ref()
        .ok_or_else(|| QobuzError::Configuration("Secret not configured".into()))?;

    let signature = signing::sign_userlib_get_albums(&timestamp, secret);

    let params = [
        ("app_id", self.app_id.as_str()),
        ("user_auth_token", self.user_auth_token.as_ref()
            .ok_or_else(|| QobuzError::Unauthorized("Not logged in".into()))?
            .as_str()),
        ("request_ts", timestamp.as_str()),
        ("request_sig", signature.as_str()),
    ];

    self.post("/userLibrary/getAlbumsList", &params).await
}

Phase 4: Modification de QobuzClient

Fichier: pmoqobuz/src/client.rs

impl QobuzClient {
    /// Crée un client avec appID et secret depuis la config
    pub async fn from_config() -> Result<Self> {
        let config = pmoconfig::get_config();

        // Essayer d'obtenir appid et secret depuis la config
        let api = if let (Ok(appid), Ok(secret)) = (
            config.get_qobuz_appid(),
            config.get_qobuz_secret()
        ) {
            QobuzApi::with_secret(appid, &secret)?
        } else {
            // Sinon, utiliser le Spoofer
            warn!("AppID/secret not configured, using Spoofer");
            QobuzApi::with_spoofer().await?
        };

        // Login...
        let (username, password) = config.get_qobuz_credentials()?;
        // ...
    }
}

Dépendances à ajouter

Cargo.toml :

md5 = "0.7"

Résumé des fichiers à modifier/créer

Modifications

  • pmoqobuz/src/config_ext.rs - Ajouter appid et secret
  • pmoqobuz/src/api/mod.rs - Ajouter champ secret et nouvelles méthodes
  • pmoqobuz/src/api/auth.rs - Appeler set_secret_from_spoofer après login
  • pmoqobuz/src/api/catalog.rs - Ajouter signature à get_file_url
  • pmoqobuz/src/api/user.rs - Ajouter get_user_albums avec signature
  • pmoqobuz/src/client.rs - Utiliser Spoofer si pas de config
  • pmoqobuz/Cargo.toml - Ajouter dépendance md5

Nouveaux fichiers

  • pmoqobuz/src/api/signing.rs - Fonctions de signature MD5

Tests nécessaires

  1. Test avec Spoofer : Vérifier que l'obtention automatique de l'appID fonctionne
  2. Test avec config manuelle : Vérifier qu'on peut configurer un appID/secret
  3. Test de signature : Vérifier que les signatures MD5 sont correctes
  4. Test de setSec : Vérifier que le bon secret est sélectionné
  5. Test de streaming : Vérifier qu'on obtient des URLs valides avec get_file_url