Skip to main content

game_manager_lib/services/integration/gamebrain/
search.rs

1//! Busca de jogos por texto e filtros.
2
3use crate::database;
4use crate::services::integration::gemini;
5use crate::utils::http_client::HTTP_CLIENT;
6
7use std::collections::HashSet;
8use tauri::AppHandle;
9
10use super::models::{
11    GameBrainFilter, GameBrainFilterValue, GameBrainSearchParams, GameBrainSearchResult,
12};
13use super::raw::RawSearchResponse;
14
15/// Busca jogos por descrição/características.
16///
17/// Exemplos: "RPG medieval" / "Co-op zombie survival"
18///
19/// - Use `params` para aplicar filtros e ordenação.
20/// - Para uma busca simples sem filtros: `GameBrainSearchParams::default()`.
21/// - Retorna uma lista simplificada compatível com o frontend.
22/// - Retorna erro caso: API key não esteja configurada, falha de rede, Erro HTTP ou JSON inválido
23pub async fn search_games_by_features(
24    app: &AppHandle,
25    query: &str,
26    params: GameBrainSearchParams,
27) -> Result<Vec<GameBrainSearchResult>, String> {
28    let api_key = database::get_secret(app, "gamebrain_api_key").map_err(|e| e.to_string())?;
29
30    if api_key.trim().is_empty() {
31        return Err("GameBrain API key não configurada".into());
32    }
33
34    let cleaned_query = query.trim();
35
36    if cleaned_query.is_empty() {
37        return Ok(vec![]);
38    }
39
40    // Traduz a query para inglês. Falha silenciosa: se a tradução falhar, usa a query original.
41    let english_query = match database::get_secret(app, "gemini_api_key") {
42        Ok(gemini_key) if !gemini_key.trim().is_empty() => {
43            gemini::translate_query_to_english(&gemini_key, cleaned_query)
44                .await
45                .unwrap_or_else(|_| cleaned_query.to_string())
46        }
47        _ => cleaned_query.to_string(),
48    };
49
50    tracing::debug!(
51        "GameBrain search => original='{}' translated='{}'",
52        cleaned_query,
53        english_query
54    );
55
56    let url = "https://api.gamebrain.co/v1/games";
57
58    tracing::debug!(
59        "GameBrain search => query='{}' filters={} sort={:?} limit={:?} offset={:?}",
60        cleaned_query,
61        params.filters.len(),
62        params.sort.as_ref().map(|s| s.as_str()),
63        params.limit,
64        params.offset,
65    );
66
67    let mut request = HTTP_CLIENT
68        .get(url)
69        .header("x-api-key", api_key)
70        .query(&[("query", english_query.as_str())]);
71
72    // Filtros: serializa o Vec como JSON compacto e passa como query param.
73    // Formato esperado pela API: [{"key":"platform","values":[{"value":"pc"}],"connection":"OR"}]
74    if !params.filters.is_empty() {
75        let filters_json = serde_json::to_string(&params.filters).map_err(|e| e.to_string())?;
76
77        request = request.query(&[("filters", filters_json)]);
78    }
79
80    if let Some(sort) = &params.sort {
81        request = request.query(&[("sort", sort.as_str())]);
82    }
83
84    if let Some(order) = &params.sort_order {
85        request = request.query(&[("sort-order", order.as_str())]);
86    }
87
88    if let Some(limit) = params.limit {
89        request = request.query(&[("limit", limit.to_string())]);
90    }
91
92    if let Some(offset) = params.offset {
93        request = request.query(&[("offset", offset.to_string())]);
94    }
95
96    let response = request.send().await.map_err(|e| {
97        tracing::error!("GameBrain request error: {}", e);
98        e.to_string()
99    })?;
100
101    let status = response.status();
102
103    if !status.is_success() {
104        let body = response.text().await.unwrap_or_default();
105
106        tracing::error!("GameBrain HTTP Error => status={} body={}", status, body);
107
108        return Err(format!("Erro GameBrain: {}", status));
109    }
110
111    let response_text = response.text().await.map_err(|e| e.to_string())?;
112
113    tracing::debug!("GameBrain response size: {} bytes", response_text.len());
114
115    let raw_response: RawSearchResponse = serde_json::from_str(&response_text).map_err(|e| {
116        tracing::error!("GameBrain JSON parse error: {}", e);
117
118        format!(
119            "Erro JSON GameBrain (linha {}, coluna {}): {}",
120            e.line(),
121            e.column(),
122            e
123        )
124    })?;
125
126    let results: Vec<GameBrainSearchResult> = raw_response
127        .results
128        .into_iter()
129        .map(|game| {
130            // Fallback de imagem: image -> cover.url
131            let cover_url = game
132                .image
133                .or_else(|| game.cover.filter(|c| !c.url.is_empty()).map(|c| c.url));
134
135            // Coleta apenas os nomes das plataformas, deduplicando.
136            let platforms: Vec<String> = {
137                let mut seen = HashSet::new();
138                game.platforms
139                    .into_iter()
140                    .filter(|p| !p.name.is_empty() && seen.insert(p.name.clone()))
141                    .map(|p| p.name)
142                    .collect()
143            };
144
145            GameBrainSearchResult {
146                // Prefixo importante para evitar colisão entre providers.
147                id: format!("gamebrain:{}", game.id),
148                name: game.name,
149                cover_url,
150                genre: game.genre,
151                year: game.year.map(|y| y as u32),
152                // Converte o score 0.0–1.0 para percentual 0–100.
153                rating: game
154                    .rating
155                    .and_then(|r| r.mean)
156                    .map(|m| (m * 100.0).round()),
157                platforms,
158                link: game.link,
159            }
160        })
161        .collect();
162
163    tracing::debug!("GameBrain parsed {} results", results.len());
164
165    Ok(results)
166}
167
168/// Busca jogos por características filtrando apenas jogos de PC.
169///
170/// Atalho conveniente para o Playlite, que é focado em PC.
171/// Equivale a chamar `search_games_by_features` com o filtro `platform: pc` já aplicado.
172pub async fn search_pc_games_by_features(
173    app: &AppHandle,
174    query: &str,
175    mut params: GameBrainSearchParams,
176) -> Result<Vec<GameBrainSearchResult>, String> {
177    // Injeta o filtro de PC caso ainda não esteja presente, preservando quaisquer filtros extras.
178    let already_has_platform = params.filters.iter().any(|f| f.key == "platform");
179
180    if !already_has_platform {
181        params.filters.push(GameBrainFilter {
182            key: "platform".into(),
183            values: vec![GameBrainFilterValue { value: "pc".into() }],
184            connection: Some("OR".into()),
185        });
186    }
187
188    search_games_by_features(app, query, params).await
189}