Skip to main content

game_manager_lib/commands/metadata/
search.rs

1//! Comandos para busca de metadados externos
2//!
3//! Permite buscar detalhes de jogos, listas de tendências e giveaways
4//! usando APIs externas como RAWG e GamerPower.
5
6use crate::database;
7use crate::errors::AppError;
8use crate::models::Game;
9use crate::services::integration::gamebrain::{GameMedia, SimilarGame};
10use crate::services::integration::gamerpower::{self, Giveaway};
11use crate::services::integration::{gamebrain, rawg};
12use crate::services::recommendation::core::calculate_game_weight;
13use serde::{Deserialize, Serialize};
14use tauri::AppHandle;
15
16// === ESTRUTURAS ===
17
18/// Jogo similar retornado para a seção de perfil em Trending.
19/// Reutiliza SimilarGame do gamebrain, mas adiciona de qual jogo
20/// âncora ele veio — útil para o frontend mostrar "Porque você jogou X".
21#[derive(Debug, Serialize, Deserialize, Clone)]
22pub struct ProfileSimilarGame {
23    #[serde(flatten)]
24    pub game: SimilarGame,
25    /// Nome do jogo da biblioteca que originou esta sugestão.
26    #[serde(rename = "becauseOf")]
27    pub because_of: String,
28}
29
30// === FUNÇÕES ===
31
32/// Busca detalhes de um jogo na RAWG
33#[tauri::command]
34pub async fn fetch_game_details(
35    app: AppHandle,
36    query: String,
37) -> Result<rawg::GameDetails, AppError> {
38    let api_key = database::get_secret(&app, "rawg_api_key")?;
39    rawg::fetch_game_details(&api_key, query)
40        .await
41        .map_err(AppError::NetworkError)
42}
43
44/// Busca giveaways ativos na GamerPower
45#[tauri::command]
46pub async fn get_active_giveaways(app: AppHandle) -> Result<Vec<Giveaway>, AppError> {
47    gamerpower::fetch_giveaways(&app)
48        .await
49        .map_err(AppError::NetworkError)
50}
51
52/// Busca jogos similares baseados nos 2 jogos com maior peso na biblioteca.
53///
54/// Fluxo:
55///   1. Calcula peso de cada jogo via calculate_game_weight
56///   2. Ordena desc, pega os 2 primeiros
57///   3. Chama fetch_similar_games para cada um em paralelo (tokio::join!)
58///   4. Filtra jogos que o usuário já possui (por nome, case-insensitive)
59///   5. Deduplica por gamebrain_id
60///   6. Retorna até 10 jogos (5 de cada âncora, intercalados)
61#[tauri::command]
62pub async fn get_profile_similar_games(
63    app: AppHandle,
64    // Jogos da biblioteca — frontend já tem, evita query extra ao banco
65    user_games: Vec<Game>,
66) -> Result<Vec<ProfileSimilarGame>, String> {
67    if user_games.is_empty() {
68        return Ok(vec![]);
69    }
70
71    // Etapa 1 e 2: pesos e top 2
72    let mut weighted: Vec<(&Game, f32)> = user_games
73        .iter()
74        .map(|g| (g, calculate_game_weight(g)))
75        .collect();
76
77    weighted.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
78
79    let anchors: Vec<&Game> = weighted.iter().take(2).map(|(g, _)| *g).collect();
80
81    // Etapa 3: chamadas em paralelo para os 2 âncoras
82    let (result_a, result_b) = tokio::join!(
83        gamebrain::fetch_similar_games(&app, &anchors[0].id, &anchors[0].name, Some(5)),
84        // Se só tiver 1 jogo, usa o mesmo para não falhar
85        gamebrain::fetch_similar_games(
86            &app,
87            &anchors.get(1).unwrap_or(&anchors[0]).id,
88            &anchors.get(1).unwrap_or(&anchors[0]).name,
89            Some(5),
90        ),
91    );
92
93    // Normaliza nomes da biblioteca para filtro (lowercase, sem espaços extras)
94    let library_names: std::collections::HashSet<String> = user_games
95        .iter()
96        .map(|g| g.name.trim().to_lowercase())
97        .collect();
98
99    // Etapa 4 e 5: filtra, deduplica e anota because_of
100    let mut seen_ids = std::collections::HashSet::new();
101    let mut results: Vec<ProfileSimilarGame> = Vec::new();
102
103    let anchor_a_name = anchors[0].name.clone();
104    let anchor_b_name = anchors.get(1).map(|g| g.name.clone()).unwrap_or_default();
105
106    // Intercala A e B para variedade
107    let games_a = result_a.unwrap_or_default();
108    let games_b = result_b.unwrap_or_default();
109    let max_len = games_a.len().max(games_b.len());
110
111    for i in 0..max_len {
112        for (games, because_of) in [(&games_a, &anchor_a_name), (&games_b, &anchor_b_name)] {
113            if let Some(similar) = games.get(i) {
114                // Filtra se já está na biblioteca
115                if library_names.contains(&similar.name.trim().to_lowercase()) {
116                    continue;
117                }
118                // Deduplica por ID
119                if !seen_ids.insert(similar.id.clone()) {
120                    continue;
121                }
122                results.push(ProfileSimilarGame {
123                    game: similar.clone(),
124                    because_of: because_of.clone(),
125                });
126            }
127        }
128    }
129
130    Ok(results)
131}
132
133/// Busca jogos em tendência no momento na RAWG
134#[tauri::command]
135pub async fn get_trending_games(app: AppHandle) -> Result<Vec<rawg::RawgGame>, AppError> {
136    let api_key = database::get_secret(&app, "rawg_api_key")?;
137    rawg::fetch_trending_games(&app, &api_key)
138        .await
139        .map_err(AppError::NetworkError)
140}
141
142/// Busca jogos que serão lançados em breve na RAWG
143#[tauri::command]
144pub async fn get_upcoming_games(app: AppHandle) -> Result<Vec<rawg::RawgGame>, AppError> {
145    let api_key = database::get_secret(&app, "rawg_api_key")?;
146    rawg::fetch_upcoming_games(&app, &api_key)
147        .await
148        .map_err(AppError::NetworkError)
149}
150
151/// Busca jogos similares usando a API do GameBrain
152#[tauri::command]
153pub async fn get_similar_games(
154    app: AppHandle,
155    game_id: String,
156    game_name: String,
157) -> Result<Vec<SimilarGame>, String> {
158    gamebrain::fetch_similar_games(&app, &game_id, &game_name, Some(12)).await
159}
160
161/// Busca midia de jogos (screenshots, trailers) usando a API do GameBrain
162#[tauri::command]
163pub async fn get_game_media(
164    app: AppHandle,
165    game_id: String,
166    game_name: String,
167) -> Result<GameMedia, String> {
168    gamebrain::fetch_game_media(&app, &game_id, &game_name).await
169}