210 lines
7.5 KiB
Rust
210 lines
7.5 KiB
Rust
use anyhow::Result;
|
|
use base64::{engine::general_purpose::STANDARD, Engine};
|
|
use indexmap::IndexMap;
|
|
use regex::Regex;
|
|
use reqwest::Client;
|
|
|
|
pub 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
|
|
pub 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
|
|
pub 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 l'appSecret depuis le bundle (secret MD5 à 32 caractères)
|
|
///
|
|
/// Ce secret est utilisé directement par Qobuz (nouvelle méthode)
|
|
/// au lieu d'être XORé avec l'app_id
|
|
pub fn get_app_secret(&self) -> Result<String> {
|
|
let captures = self
|
|
.app_id_regex
|
|
.captures(&self.bundle)
|
|
.ok_or_else(|| anyhow::anyhow!("AppSecret non trouvé dans le bundle"))?;
|
|
|
|
Ok(captures
|
|
.name("secret")
|
|
.ok_or_else(|| anyhow::anyhow!("Groupe secret non trouvé"))?
|
|
.as_str()
|
|
.to_string())
|
|
}
|
|
|
|
/// Extrait les secrets depuis le bundle
|
|
pub 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)
|
|
}
|
|
}
|