on avance un peu pmoqobuz

This commit is contained in:
2025-12-08 22:32:42 +01:00
parent 31ee25c7f2
commit 76132086b6
31 changed files with 9047 additions and 309 deletions

View File

@@ -0,0 +1,118 @@
# Exemple Spoofer Qobuz
Cet exemple reproduit le comportement du spoofer Python original pour extraire dynamiquement l'AppID et les secrets de l'API Qobuz.
## Vue d'ensemble
Le spoofer effectue les opérations suivantes :
1. **Récupère la page de login** : `https://play.qobuz.com/login`
2. **Extrait l'URL du bundle.js** : Via regex sur la page HTML
3. **Télécharge le bundle** : JavaScript obfusqué contenant les secrets
4. **Extrait l'AppID** : Via regex spécifique
5. **Extrait les secrets** : Via une série de regex et décodage base64
## Équivalences Python ↔ Rust
| Python | Rust | Notes |
|--------|------|-------|
| `requests.get()` | `reqwest::Client::get()` | Client HTTP asynchrone |
| `re.search()` / `re.finditer()` | `regex::Regex::captures()` / `captures_iter()` | Expressions régulières |
| `OrderedDict` | `indexmap::IndexMap` | Maintient l'ordre d'insertion |
| `base64.standard_b64decode()` | `base64::STANDARD.decode()` | Décodage base64 |
| String slicing `[:-44]` | `&string[..len-44]` | Extraction de sous-chaînes |
## Différences notables
### 1. Gestion asynchrone
Le code Rust est entièrement asynchrone avec Tokio :
```rust
#[tokio::main]
async fn main() -> Result<()> {
let spoofer = Spoofer::new().await?;
// ...
}
```
### 2. Gestion d'erreurs explicite
Rust utilise `Result<T, E>` pour la gestion d'erreurs :
```rust
fn get_app_id(&self) -> Result<String> {
let captures = self.app_id_regex
.captures(&self.bundle)
.ok_or_else(|| anyhow::anyhow!("AppID non trouvé"))?;
// ...
}
```
### 3. Propriété et emprunt
Rust nécessite une gestion explicite de la propriété :
```rust
// Clone pour éviter les problèmes de borrowing
let second_key = keys[1].clone();
let second_value = secrets.get(&second_key).unwrap().clone();
```
### 4. Réorganisation de l'IndexMap
Le code Python utilise `move_to_end()` :
```python
secrets.move_to_end(keypairs[1][0], last=False)
```
En Rust, on reconstruit une nouvelle map :
```rust
secrets.shift_remove(&second_key);
let mut new_secrets = IndexMap::new();
new_secrets.insert(second_key, second_value);
for (k, v) in secrets {
new_secrets.insert(k, v);
}
```
## Usage
```bash
# Compiler et lancer l'exemple
cargo run --example spoofer
# Ou compiler uniquement
cargo check --example spoofer
```
## Sortie attendue
```
=== Spoofer Qobuz ===
Récupération de la page de login...
Téléchargement du bundle depuis: /resources/x.x.x-xxxx/bundle.js
Bundle téléchargé (xxxxx bytes)
Timezones trouvées: ["america", "europe", "asia", ...]
--- App ID ---
App ID: 123456789
--- Secrets ---
america: xxxxxxxxxxxxxxxxxxxxxxxxx
europe: yyyyyyyyyyyyyyyyyyyyyyyyy
...
```
## Dépendances
Les dépendances suivantes sont nécessaires (ajoutées dans `[dev-dependencies]`) :
```toml
regex = "1.10"
base64 = "0.22"
indexmap = "2.0"
```
## Avertissement
⚠️ **Note importante** : Ce code est fourni à des fins éducatives et de reverse engineering. L'extraction de secrets depuis des applications web peut violer les conditions d'utilisation de certains services. Utilisez-le de manière responsable et conformément aux lois applicables.
## Références
- Code Python original : Basé sur le spoofer Qobuz de la communauté
- Documentation Qobuz API : https://github.com/Qobuz/api-documentation

View File

@@ -0,0 +1,56 @@
# Configuration exemple pour pmoqobuz
#
# Ce fichier montre comment configurer l'accès à Qobuz avec AppID et Secret.
# Pour utiliser cette configuration :
#
# 1. Copier ce fichier vers ~/.pmomusic/config.yaml (ou le répertoire de config approprié)
# 2. Remplacer les valeurs par vos propres credentials
# 3. Utiliser le Spoofer pour obtenir un AppID et Secret valides :
# cargo run --example spoofer
host:
http_port: '8080'
cover_cache:
directory: cache_covers
size: 2000
audio_cache:
directory: cache_audio
size: 500
logger:
buffer_capacity: 200
enable_console: true
min_level: INFO
playlists:
directory: playlists
devices:
mediarenderer:
pmo_mediarenderer:
udn: e4b68fbc-2bd5-4cea-98d8-be843fec0bd4
mediaserver:
pmo_mediaserver:
udn: 17fe2ea6-8908-4e30-bc52-b28ea4cab3e4
accounts:
qobuz:
# Credentials utilisateur (REQUIS)
username: "your_email@example.com"
password: "your_password"
# AppID et Secret (OPTIONNEL mais RECOMMANDÉ)
# Pour obtenir ces valeurs, exécutez : cargo run --example spoofer
#
# Exemple de valeurs récupérées le 2025-12-10 :
appid: "798273057"
secret: "f69a7734686cb9427629378a4b7ac381" # Secret pour timezone "london"
# Autres secrets disponibles (testez si "london" ne fonctionne pas) :
# secret: "806331c3b0b641da923b890aed01d04a" # Secret pour timezone "abidjan"
# secret: "abb21364945c0583309667d13ca3d93a" # Secret pour timezone "berlin"
# Note sur les secrets :
# - Les secrets sont des valeurs base64-encodées retournées par le Spoofer
# - Ils sont nécessaires pour les requêtes signées (streaming, bibliothèque)
# - Sans secret, seules les fonctionnalités de base sont disponibles
# - Les secrets peuvent expirer : réexécutez le Spoofer pour en obtenir de nouveaux

View File

@@ -0,0 +1,63 @@
//! Exemple d'utilisation du trait QobuzConfigExt
//!
//! Cet exemple montre comment utiliser le trait d'extension pour gérer
//! les credentials Qobuz via pmoconfig.
//!
//! Usage:
//! ```bash
//! cargo run --example config_usage
//! ```
use pmoconfig::get_config;
use pmoqobuz::QobuzConfigExt;
fn main() -> anyhow::Result<()> {
// Initialiser le logging
tracing_subscriber::fmt::init();
println!("=== QobuzConfigExt Example ===\n");
// Récupérer la configuration globale
let config = get_config();
// Exemple 1: Lire les credentials existants
println!("--- Lecture des credentials ---");
match config.get_qobuz_credentials() {
Ok((username, password)) => {
println!("Username: {}", username);
println!("Password: {}", "*".repeat(password.len()));
}
Err(e) => {
println!("Credentials non configurés: {}", e);
}
}
// Exemple 2: Lire username et password séparément
println!("\n--- Lecture séparée ---");
match config.get_qobuz_username() {
Ok(username) => println!("Username: {}", username),
Err(e) => println!("Username non configuré: {}", e),
}
match config.get_qobuz_password() {
Ok(password) => println!("Password: {}", "*".repeat(password.len())),
Err(e) => println!("Password non configuré: {}", e),
}
// Exemple 3: Définir de nouveaux credentials (commenté pour ne pas modifier la config)
/*
println!("\n--- Définition de nouveaux credentials ---");
config.set_qobuz_username("user@example.com")?;
config.set_qobuz_password("my_secure_password")?;
println!("Nouveaux credentials enregistrés !");
*/
// Exemple 4: Utilisation avec QobuzClient
println!("\n--- Utilisation avec QobuzClient ---");
println!("Pour créer un client Qobuz à partir de la config:");
println!(" let client = QobuzClient::from_config().await?;");
println!("\nCette méthode utilise automatiquement QobuzConfigExt");
println!("pour récupérer les credentials depuis pmoconfig.");
Ok(())
}

View File

@@ -0,0 +1,241 @@
//! Exemple de Spoofer Qobuz - Extraction dynamique des AppID et secrets
//!
//! Cet exemple reproduit le comportement du spoofer Python :
//! 1. Récupère la page de login Qobuz
//! 2. Extrait l'URL du bundle.js
//! 3. Télécharge le bundle
//! 4. Extrait l'AppID et les secrets via regex
//! 5. Décode les secrets en base64
//!
//! Usage:
//! ```bash
//! cargo run --example spoofer
//! ```
use anyhow::Result;
use base64::{engine::general_purpose::STANDARD, Engine};
use indexmap::IndexMap;
use regex::Regex;
use reqwest::Client;
struct Spoofer {
bundle: String,
seed_timezone_regex: Regex,
info_extras_regex_template: String,
app_id_regex: Regex,
}
impl Spoofer {
/// Crée un nouveau Spoofer et télécharge le bundle.js
async fn new() -> Result<Self> {
// Expressions régulières (équivalent Python)
let seed_timezone_regex = Regex::new(
r#"[a-z]\.initialSeed\("(?P<seed>[\w=]+)",window\.utimezone\.(?P<timezone>[a-z]+)\)"#,
)?;
let info_extras_regex_template =
r#"name:"\w+/(?P<timezone>{timezones})",info:"(?P<info>[\w=]+)",extras:"(?P<extras>[\w=]+)""#
.to_string();
let app_id_regex = Regex::new(
r#"production:\{api:\{appId:"(?P<app_id>\d{9})",appSecret:"(?P<secret>\w{32})"\},braze:.\(.\(\{\},.\),\{\},\{apiKey:"([-0-9a-fA-F]{36})"\}\),extra:.\}"#,
)?;
// Créer un client HTTP
let client = Client::builder()
.user_agent("Mozilla/5.0 (compatible; PMOMusic/1.0)")
.build()?;
println!("Récupération de la page de login...");
let login_page = client
.get("https://play.qobuz.com/login")
.send()
.await?
.text()
.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 = bundle_url_regex
.captures(&login_page)
.and_then(|cap| cap.get(1))
.ok_or_else(|| anyhow::anyhow!("Impossible de trouver l'URL du bundle"))?
.as_str();
println!("Téléchargement du bundle depuis: {}", bundle_url);
let bundle_full_url = format!("https://play.qobuz.com{}", bundle_url);
let bundle = client.get(&bundle_full_url).send().await?.text().await?;
println!("Bundle téléchargé ({} bytes)", bundle.len());
Ok(Self {
bundle,
seed_timezone_regex,
info_extras_regex_template,
app_id_regex,
})
}
/// Extrait l'App ID depuis le bundle
fn get_app_id(&self) -> Result<String> {
let captures = self
.app_id_regex
.captures(&self.bundle)
.ok_or_else(|| anyhow::anyhow!("AppID non trouvé dans le bundle"))?;
Ok(captures
.name("app_id")
.ok_or_else(|| anyhow::anyhow!("Groupe app_id non trouvé"))?
.as_str()
.to_string())
}
/// Extrait les secrets depuis le bundle
fn get_secrets(&self) -> Result<IndexMap<String, String>> {
// Étape 1: Extraire tous les seed/timezone pairs
let mut secrets: IndexMap<String, Vec<String>> = IndexMap::new();
for captures in self.seed_timezone_regex.captures_iter(&self.bundle) {
let seed = captures
.name("seed")
.ok_or_else(|| anyhow::anyhow!("Groupe seed non trouvé"))?
.as_str();
let timezone = captures
.name("timezone")
.ok_or_else(|| anyhow::anyhow!("Groupe timezone non trouvé"))?
.as_str();
secrets
.entry(timezone.to_string())
.or_insert_with(Vec::new)
.push(seed.to_string());
}
println!("Timezones trouvées: {:?}", secrets.keys());
// Étape 2: Réordonner - on met la deuxième timezone en premier
// (comme le fait le code Python avec move_to_end)
if secrets.len() >= 2 {
let keys: Vec<String> = secrets.keys().cloned().collect();
let second_key = keys[1].clone();
let second_value = secrets.get(&second_key).unwrap().clone();
// Retirer et réinsérer pour le mettre en premier
secrets.shift_remove(&second_key);
let mut new_secrets = IndexMap::new();
new_secrets.insert(second_key, second_value);
for (k, v) in secrets {
new_secrets.insert(k, v);
}
secrets = new_secrets;
}
// Étape 3: Construire la regex pour info/extras
let timezones_capitalized: Vec<String> = secrets
.keys()
.map(|tz| {
let mut chars = tz.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
})
.collect();
let info_extras_regex_str = self
.info_extras_regex_template
.replace("{timezones}", &timezones_capitalized.join("|"));
let info_extras_regex = Regex::new(&info_extras_regex_str)?;
// Étape 4: Extraire info et extras pour chaque timezone
for captures in info_extras_regex.captures_iter(&self.bundle) {
let timezone_cap = captures
.name("timezone")
.ok_or_else(|| anyhow::anyhow!("Groupe timezone non trouvé"))?
.as_str();
let info = captures
.name("info")
.ok_or_else(|| anyhow::anyhow!("Groupe info non trouvé"))?
.as_str();
let extras = captures
.name("extras")
.ok_or_else(|| anyhow::anyhow!("Groupe extras non trouvé"))?
.as_str();
let timezone_lower = timezone_cap.to_lowercase();
if let Some(vec) = secrets.get_mut(&timezone_lower) {
vec.push(info.to_string());
vec.push(extras.to_string());
}
}
// Étape 5: Décoder les secrets en base64
let mut decoded_secrets = IndexMap::new();
for (timezone, parts) in secrets {
let concatenated = parts.join("");
// Retirer les 44 derniers caractères (comme Python [:-44])
if concatenated.len() > 44 {
let trimmed = &concatenated[..concatenated.len() - 44];
// 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
);
}
}
}
Err(e) => {
eprintln!(
"Erreur de décodage base64 pour timezone {}: {}",
timezone, e
);
}
}
}
}
Ok(decoded_secrets)
}
}
#[tokio::main]
async fn main() -> Result<()> {
// Initialiser le logging
tracing_subscriber::fmt::init();
println!("=== Spoofer Qobuz ===\n");
// Créer le spoofer
let spoofer = Spoofer::new().await?;
// Extraire l'App ID
println!("\n--- App ID ---");
match spoofer.get_app_id() {
Ok(app_id) => println!("App ID: {}", app_id),
Err(e) => eprintln!("Erreur lors de l'extraction de l'App ID: {}", e),
}
// Extraire les secrets
println!("\n--- Secrets ---");
match spoofer.get_secrets() {
Ok(secrets) => {
for (timezone, secret) in secrets {
println!("{}: {}", timezone, secret);
}
}
Err(e) => eprintln!("Erreur lors de l'extraction des secrets: {}", e),
}
Ok(())
}