Skip to main content

game_manager_lib/services/integration/hltb/
client.rs

1use crate::services::integration::hltb::models::{AuthInitResponse, GamesFilter, SearchOptions, SearchPayload};
2use serde_json::{json, Value};
3use std::collections::HashMap;
4use std::time::{SystemTime, UNIX_EPOCH};
5
6pub struct HltbClient {
7    client: reqwest::Client,
8    base_url: String,
9    endpoint: String,
10}
11
12impl HltbClient {
13    pub fn new() -> Self {
14        // HLTB exige um User-Agent de navegador válido para não bloquear a requisição
15        let client = reqwest::Client::builder()
16            .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
17            .build()
18            .unwrap();
19
20        Self {
21            client,
22            base_url: "https://howlongtobeat.com".to_string(),
23            endpoint: "/api/bleed".to_string(), // Endpoint fallback utilizado pelo plugin
24        }
25    }
26
27    /// Passo 1: Obter as chaves criptográficas dinâmicas
28    async fn get_auth_tokens(&self) -> Result<AuthInitResponse, Box<dyn std::error::Error>> {
29        let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis();
30        let init_url = format!("{}{}/init?t={}", self.base_url, self.endpoint, timestamp);
31
32        let response = self.client.get(&init_url)
33            .header("Referer", &self.base_url)
34            .send()
35            .await?;
36
37        let auth_data: AuthInitResponse = response.json().await?;
38        Ok(auth_data)
39    }
40
41    /// Passo 2: Executar a busca injetando as chaves
42    pub async fn search(&self, game_name: &str) -> Result<Value, Box<dyn std::error::Error>> {
43        let auth = self.get_auth_tokens().await?;
44
45        let search_url = format!("{}{}", self.base_url, self.endpoint);
46
47        // Prepara o payload base
48        let terms: Vec<String> = game_name.split_whitespace().map(|s| s.to_string()).collect();
49        let payload = SearchPayload {
50            search_terms: terms,
51            search_options: SearchOptions {
52                games: GamesFilter { platform: "".to_string() },
53            },
54        };
55
56        // Converte para um mapa para podermos injetar a chave dinâmica no corpo do JSON
57        let mut json_body: HashMap<String, Value> = serde_json::to_value(payload)?
58            .as_object()
59            .unwrap()
60            .clone()
61            .into_iter()
62            .collect();
63
64        // O HLTB exige que a chave dinâmica seja enviada também no corpo da requisição
65        json_body.insert(auth.hp_key.clone(), json!(auth.hp_val));
66
67        // Envia o POST com os headers exigidos
68        let response = self.client.post(&search_url)
69            .header("Origin", &self.base_url)
70            .header("Referer", &self.base_url)
71            .header("x-auth-token", &auth.token)
72            .header("x-hp-key", &auth.hp_key)
73            .header("x-hp-val", &auth.hp_val)
74            .json(&json_body)
75            .send()
76            .await?;
77
78        let data: Value = response.json().await?;
79        Ok(data)
80    }
81}