Skip to main content

game_manager_lib/services/integration/
rawg.rs

1//! Módulo de integração com a API RAWG.
2//!
3//! Fornece funcionalidades para buscar informações sobre jogos, incluindo
4//! tendências, detalhes específicos e lançamentos futuros.
5//!
6//! A RAWG é um banco de dados abrangente de jogos que fornece metadados,
7//! ratings, gêneros e outras informações relevantes.
8
9use crate::constants::{RAWG_SEARCH_PAGE_SIZE, RAWG_TRENDING_PAGE_SIZE, RAWG_UPCOMING_PAGE_SIZE};
10use crate::database::AppState;
11use crate::services::cache;
12use crate::utils::http_client::HTTP_CLIENT;
13use chrono::Datelike;
14use serde::{Deserialize, Serialize};
15use tauri::{AppHandle, Manager};
16
17// === ESTRUTURAS DE DADOS PRINCIPAIS ===
18
19#[derive(Debug, Serialize, Deserialize)]
20pub struct RawgTag {
21    pub id: i32,
22    pub name: String,
23    pub slug: String,
24    pub language: Option<String>,
25    pub games_count: i32,
26    pub image_background: Option<String>,
27}
28#[derive(Debug, Serialize, Deserialize)]
29pub struct RawgDeveloper {
30    pub id: i32,
31    pub name: String,
32    pub slug: String,
33}
34#[derive(Debug, Serialize, Deserialize)]
35pub struct RawgPublisher {
36    pub id: i32,
37    pub name: String,
38    pub slug: String,
39}
40#[derive(Debug, Deserialize, Serialize)]
41pub struct RawgGenre {
42    pub name: String,
43}
44
45#[derive(Debug, Serialize, Deserialize)]
46pub struct EsrbRating {
47    pub id: Option<i32>,
48    pub name: String,
49    pub slug: Option<String>,
50}
51
52/// Informações sobre a loja onde o jogo está disponível.
53#[derive(Debug, Serialize, Deserialize)]
54pub struct StoreInfo {
55    pub id: i32,
56    pub name: String,
57    pub slug: String,
58}
59
60/// Wrapper para informações de loja com URL específica do jogo.
61#[derive(Debug, Serialize, Deserialize)]
62pub struct StoreWrapper {
63    pub id: i32,
64    pub url: String,
65    pub store: StoreInfo,
66}
67
68/// Resposta da API RAWG para listagens de jogos.
69#[derive(Debug, Deserialize, Serialize)]
70struct RawgResponse {
71    results: Vec<RawgGame>,
72}
73
74/// Representação básica de um jogo na RAWG.
75#[derive(Debug, Deserialize, Serialize)]
76pub struct RawgGame {
77    pub id: u32,
78    pub name: String,
79    #[serde(rename(deserialize = "background_image", serialize = "backgroundImage"))]
80    pub background_image: Option<String>,
81    pub rating: f32,
82    pub released: Option<String>,
83    pub genres: Vec<RawgGenre>,
84    #[serde(default)]
85    pub tags: Vec<RawgTag>,
86    pub slug: String,
87}
88
89/// Detalhes completos de um jogo na RAWG.
90///
91/// Inclui informações expandidas como descrição, metacritic score, desenvolvedoras e tags.
92#[derive(Debug, Serialize, Deserialize)]
93pub struct RawgGameDetails {
94    pub id: i32,
95    pub name: String,
96    #[serde(default)]
97    pub alternative_names: Vec<String>,
98    #[serde(rename(deserialize = "description_raw", serialize = "descriptionRaw"))]
99    pub description_raw: Option<String>,
100    pub metacritic: Option<i32>,
101    pub website: Option<String>,
102    pub released: Option<String>,
103    pub background_image: Option<String>,
104    #[serde(default)]
105    pub genres: Vec<RawgGenre>,
106    #[serde(default)]
107    pub tags: Vec<RawgTag>,
108    #[serde(default)]
109    pub developers: Vec<RawgDeveloper>,
110    #[serde(default)]
111    pub publishers: Vec<RawgPublisher>,
112    #[serde(default)]
113    pub reddit_url: Option<String>,
114    #[serde(default)]
115    pub metacritic_url: Option<String>,
116    #[serde(default)]
117    pub stores: Vec<StoreWrapper>,
118    #[serde(default)]
119    pub esrb_rating: Option<EsrbRating>,
120}
121
122// === FUNÇÕES DE API ===
123
124/// Busca jogos por texto (Nome, Série, etc).
125///
126/// Substitui a busca da Steam Store na Wishlist e adição manual.
127pub async fn search_games(api_key: &str, query: &str) -> Result<Vec<RawgGame>, String> {
128    let url = format!(
129        "https://api.rawg.io/api/games?key={}&search={}&page_size={}",
130        api_key,
131        urlencoding::encode(query),
132        RAWG_SEARCH_PAGE_SIZE
133    );
134
135    let res = HTTP_CLIENT
136        .get(&url)
137        .send()
138        .await
139        .map_err(|e| e.to_string())?;
140
141    if !res.status().is_success() {
142        return Err(format!("Erro RAWG Search: {}", res.status()));
143    }
144
145    let data: RawgResponse = res.json().await.map_err(|e| e.to_string())?;
146    Ok(data.results)
147}
148
149/// Busca detalhes completos de um jogo específico.
150///
151/// Converte o nome do jogo em slug (formato URL-friendly) e busca informações detalhadas na API RAWG.
152pub async fn fetch_game_details(api_key: &str, query: String) -> Result<RawgGameDetails, String> {
153    let identifier = if query.chars().all(char::is_numeric) {
154        query
155    } else {
156        query
157            .to_lowercase()
158            .replace(" ", "-")
159            .replace(":", "")
160            .replace("'", "")
161            .replace("&", "")
162            .replace(".", "")
163    };
164
165    let url = format!(
166        "https://api.rawg.io/api/games/{}?key={}",
167        identifier, api_key
168    );
169
170    let res = HTTP_CLIENT
171        .get(&url)
172        .send()
173        .await
174        .map_err(|e| e.to_string())?;
175
176    if res.status().is_success() {
177        let details: RawgGameDetails = res.json().await.map_err(|e| e.to_string())?;
178        Ok(details)
179    } else if res.status().as_u16() == 404 {
180        Err("Jogo não encontrado na RAWG".into())
181    } else {
182        Err(format!("Erro RAWG Details: {}", res.status()))
183    }
184}
185
186/// Busca os jogos mais populares do momento ('trending games').
187///
188/// Retorna até 20 jogos lançados entre o ano passado e o ano atual,
189/// ordenados por popularidade (adições recentes).
190pub async fn fetch_trending_games(app: &AppHandle, api_key: &str) -> Result<Vec<RawgGame>, String> {
191    let current_year = chrono::Utc::now().year();
192    let last_year = current_year - 1;
193    let cache_key = "rawg_list_trending";
194
195    let url = format!(
196        "https://api.rawg.io/api/games?key={}&dates={}-01-01,{}-12-31&ordering=-added&page_size={}",
197        api_key, last_year, current_year, RAWG_TRENDING_PAGE_SIZE
198    );
199
200    // 1. Tenta buscar ONLINE
201    match HTTP_CLIENT.get(&url).send().await {
202        Ok(res) => {
203            if res.status().is_success() {
204                let data: RawgResponse = res.json().await.map_err(|e| e.to_string())?;
205
206                // Sucesso: Salva no Cache (Fire & Forget)
207                if let Ok(conn) = app.state::<AppState>().cache_db.lock() {
208                    if let Ok(json) = serde_json::to_string(&data.results) {
209                        let _ = cache::save_cached_api_data(&conn, "rawg", cache_key, &json);
210                    }
211                }
212
213                return Ok(data.results);
214            }
215        }
216        Err(_) => {
217            // Falha de rede: Silenciosamente cai para o fallback
218        }
219    }
220
221    // 2. FALLBACK: Tenta buscar Cache Offline ("Stale")
222    if let Ok(conn) = app.state::<AppState>().cache_db.lock() {
223        if let Some(payload) = cache::get_stale_api_data(&conn, "rawg", cache_key) {
224            if let Ok(cached_games) = serde_json::from_str::<Vec<RawgGame>>(&payload) {
225                return Ok(cached_games);
226            }
227        }
228    }
229
230    Err("Não foi possível carregar os jogos em alta (sem conexão e sem cache).".to_string())
231}
232
233/// Busca jogos com lançamento futuro.
234///
235/// Retorna até 10 jogos que ainda serão lançados, desde a data atual
236/// até o final do próximo ano, ordenados por popularidade.
237pub async fn fetch_upcoming_games(app: &AppHandle, api_key: &str) -> Result<Vec<RawgGame>, String> {
238    let current_date = chrono::Utc::now();
239    let next_year = current_date.year() + 1;
240    let date_start = current_date.format("%Y-%m-%d").to_string();
241    let date_end = format!("{}-12-31", next_year);
242    let cache_key = "rawg_list_upcoming";
243
244    let url = format!(
245        "https://api.rawg.io/api/games?key={}&dates={},{}&ordering=-added&page_size={}",
246        api_key, date_start, date_end, RAWG_UPCOMING_PAGE_SIZE
247    );
248
249    // 1. Tenta buscar ONLINE
250    if let Ok(res) = HTTP_CLIENT.get(&url).send().await {
251        if res.status().is_success() {
252            let data: RawgResponse = res.json().await.map_err(|e| e.to_string())?;
253
254            // Sucesso: Salva no Cache
255            if let Ok(conn) = app.state::<AppState>().cache_db.lock() {
256                if let Ok(json) = serde_json::to_string(&data.results) {
257                    let _ = cache::save_cached_api_data(&conn, "rawg", cache_key, &json);
258                }
259            }
260
261            return Ok(data.results);
262        }
263    }
264
265    // 2. FALLBACK: Cache Offline
266    if let Ok(conn) = app.state::<AppState>().cache_db.lock() {
267        if let Some(payload) = cache::get_stale_api_data(&conn, "rawg", cache_key) {
268            if let Ok(cached_games) = serde_json::from_str::<Vec<RawgGame>>(&payload) {
269                return Ok(cached_games);
270            }
271        }
272    }
273
274    Err("Não foi possível carregar lançamentos (sem conexão e sem cache).".to_string())
275}