Skip to main content

game_manager_lib/services/integration/pcgamingwiki/
client.rs

1//! Cliente HTTP para a MediaWiki Action API do PCGamingWiki.
2//!
3//! Encapsula a construção do cliente `reqwest`, o helper de `cargoquery` com
4//! rate limiting automático, e a busca de páginas por nome de jogo.
5
6use crate::constants::{PCGW_API_BASE, REQUEST_PCGW_DELAY_MS};
7use crate::errors::AppError;
8use reqwest::Client;
9use serde_json::Value;
10use std::time::Duration;
11use tokio::time::sleep;
12
13/// User-Agent obrigatorio pela politica da PCGW. Formato recomendado: "NomeDoApp/versao (contato)"
14const PCGW_USER_AGENT: &str = "Playlite/1.0 (playlite.app.dev@gmail.com)";
15
16// === ESTRUTURA PUBLICA ===
17
18/// Resultado de busca por nome no PCGamingWiki.
19#[derive(Debug, serde::Serialize, serde::Deserialize)]
20pub struct PcgwSearchResult {
21    #[serde(rename = "pageId")]
22    pub page_id: String,
23    #[serde(rename = "pageName")]
24    pub page_name: String,
25}
26
27// === Helpers de HTTP ===
28
29/// Constroi um cliente `reqwest` com User-Agent e timeout configurados.
30pub(crate) fn build_http_client() -> Result<Client, AppError> {
31    Client::builder()
32        .user_agent(PCGW_USER_AGENT)
33        .timeout(Duration::from_secs(15))
34        .build()
35        .map_err(|e| AppError::NetworkError(e.to_string()))
36}
37
38/// Faz uma cargoquery na PCGW API e retorna o array `cargoquery` do JSON.
39///
40/// Adiciona o delay de rate limiting antes de cada requisicao.
41pub(crate) async fn cargo_query(
42    client: &Client,
43    tables: &str,
44    fields: &str,
45    where_clause: &str,
46    limit: u32,
47) -> Result<Vec<Value>, AppError> {
48    sleep(Duration::from_millis(REQUEST_PCGW_DELAY_MS)).await;
49
50    let response = client
51        .get(PCGW_API_BASE)
52        .query(&[
53            ("action", "cargoquery"),
54            ("tables", tables),
55            ("fields", fields),
56            ("where", where_clause),
57            ("limit", &limit.to_string()),
58            ("format", "json"),
59        ])
60        .send()
61        .await
62        .map_err(|e| AppError::NetworkError(e.to_string()))?;
63
64    if !response.status().is_success() {
65        let status = response.status();
66        return Err(AppError::NetworkError(format!(
67            "PCGW API retornou HTTP {}",
68            status
69        )));
70    }
71
72    let json: Value = response
73        .json()
74        .await
75        .map_err(|e| AppError::ParseError(e.to_string()))?;
76
77    // Extrai o array de resultados; retorna vazio se ausente
78    let rows = json
79        .get("cargoquery")
80        .and_then(|v| v.as_array())
81        .cloned()
82        .unwrap_or_default();
83
84    Ok(rows)
85}
86
87/// Busca a página via `action=query&list=search` por nome de jogo.
88///
89/// Usado como fallback quando o jogo não tem Steam AppID ou a busca por
90/// AppID não retornar resultados. Retorna uma lista de candidatos para
91/// que o usuário confirme qual é o correto.
92pub async fn search_pcgw_by_name(game_name: &str) -> Result<Vec<PcgwSearchResult>, AppError> {
93    let client = build_http_client()?;
94
95    sleep(Duration::from_millis(REQUEST_PCGW_DELAY_MS)).await;
96
97    let response = client
98        .get(PCGW_API_BASE)
99        .query(&[
100            ("action", "query"),
101            ("list", "search"),
102            ("srsearch", game_name),
103            ("srnamespace", "0"),
104            ("srlimit", "5"),
105            ("format", "json"),
106        ])
107        .send()
108        .await
109        .map_err(|e| AppError::NetworkError(e.to_string()))?;
110
111    let json: Value = response
112        .json()
113        .await
114        .map_err(|e| AppError::ParseError(e.to_string()))?;
115
116    let results = json
117        .pointer("/query/search")
118        .and_then(|v| v.as_array())
119        .map(|hits| {
120            hits.iter()
121                .filter_map(|hit| {
122                    Some(PcgwSearchResult {
123                        page_id: hit.get("pageid")?.as_u64()?.to_string(),
124                        page_name: hit.get("title")?.as_str()?.to_string(),
125                    })
126                })
127                .collect()
128        })
129        .unwrap_or_default();
130
131    Ok(results)
132}