on avance un peu pmoqobuz
This commit is contained in:
118
pmoqobuz/examples/README_SPOOFER.md
Normal file
118
pmoqobuz/examples/README_SPOOFER.md
Normal 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
|
||||
56
pmoqobuz/examples/config.yaml.example
Normal file
56
pmoqobuz/examples/config.yaml.example
Normal 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
|
||||
63
pmoqobuz/examples/config_usage.rs
Normal file
63
pmoqobuz/examples/config_usage.rs
Normal 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(())
|
||||
}
|
||||
241
pmoqobuz/examples/spoofer.rs
Normal file
241
pmoqobuz/examples/spoofer.rs
Normal 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(())
|
||||
}
|
||||
Reference in New Issue
Block a user