game_manager_lib/services/integration/gamebrain/
similar.rs1use crate::constants::GAMEBRAIN_SIMILAR_REQUEST_LIMIT;
4use crate::database;
5use crate::utils::http_client::HTTP_CLIENT;
6
7use tauri::AppHandle;
8
9use super::cache::{
10 gamebrain_id_cache_key, gamebrain_similar_cache_key, read_cached_json, read_stale_gamebrain_id,
11 read_stale_similar_games, save_cached_json, take_similar_limit,
12};
13use super::helpers::{parse_gamebrain_id, resolve_gamebrain_id};
14use super::models::SimilarGame;
15use super::raw::RawSimilarResponse;
16
17pub async fn fetch_similar_games(
42 app: &AppHandle,
43 playlite_game_id: &str,
44 game_name: &str,
45 limit: Option<u32>,
46) -> Result<Vec<SimilarGame>, String> {
47 let api_key = database::get_secret(app, "gamebrain_api_key").map_err(|e| e.to_string())?;
48
49 if api_key.trim().is_empty() {
50 return Err("GameBrain API key não configurada".into());
51 }
52
53 let requested_limit = limit.unwrap_or(10);
54
55 let gamebrain_id = {
57 let id_cache_key = gamebrain_id_cache_key(playlite_game_id);
58
59 if let Some(id) = read_cached_json::<u64>(app, "gamebrain", &id_cache_key, false)? {
60 tracing::debug!(
61 "GameBrain ID cache hit => game_id='{}' gamebrain_id={}",
62 playlite_game_id,
63 id
64 );
65 id
66 } else {
67 tracing::debug!(
69 "GameBrain ID cache miss => resolvendo '{}' via suggestions",
70 game_name
71 );
72
73 match resolve_gamebrain_id(&api_key, game_name).await {
74 Ok(Some(id)) => {
75 let _ = save_cached_json(app, "gamebrain", &id_cache_key, &id);
76 id
77 }
78 Ok(None) => {
79 if let Some(stale_id) =
80 read_stale_gamebrain_id(app, &id_cache_key, playlite_game_id)?
81 {
82 stale_id
83 } else {
84 return Err(format!("Jogo '{}' não encontrado na GameBrain", game_name));
85 }
86 }
87 Err(err) => {
88 if let Some(stale_id) =
89 read_stale_gamebrain_id(app, &id_cache_key, playlite_game_id)?
90 {
91 stale_id
92 } else {
93 return Err(err);
94 }
95 }
96 }
97 }
98 };
99
100 let similar_cache_key = gamebrain_similar_cache_key(gamebrain_id);
102
103 if let Some(cached_results) =
104 read_cached_json::<Vec<SimilarGame>>(app, "gamebrain", &similar_cache_key, false)?
105 {
106 tracing::debug!(
107 "GameBrain similar cache hit => gamebrain_id={}",
108 gamebrain_id
109 );
110 return Ok(take_similar_limit(cached_results, requested_limit));
111 }
112
113 tracing::debug!(
115 "GameBrain similar cache miss => chamando /similar para gamebrain_id={}",
116 gamebrain_id
117 );
118
119 let url = format!("https://api.gamebrain.co/v1/games/{}/similar", gamebrain_id);
120
121 let mut request = HTTP_CLIENT.get(&url).header("x-api-key", &api_key);
122
123 request = request.query(&[("limit", GAMEBRAIN_SIMILAR_REQUEST_LIMIT.to_string())]);
124
125 let response = request.send().await.map_err(|e| {
126 tracing::error!("GameBrain similar request error: {}", e);
127 e.to_string()
128 });
129
130 let response = match response {
131 Ok(response) => response,
132 Err(err) => {
133 if let Some(cached_results) =
134 read_stale_similar_games(app, &similar_cache_key, gamebrain_id, requested_limit)?
135 {
136 return Ok(cached_results);
137 }
138
139 return Err(err);
140 }
141 };
142
143 let status = response.status();
144
145 if !status.is_success() {
146 let body = response.text().await.unwrap_or_default();
147 tracing::error!(
148 "GameBrain similar HTTP Error => status={} body={}",
149 status,
150 body
151 );
152 if let Some(cached_results) =
153 read_stale_similar_games(app, &similar_cache_key, gamebrain_id, requested_limit)?
154 {
155 return Ok(cached_results);
156 }
157
158 return Err(format!("Erro GameBrain similar: {}", status));
159 }
160
161 let text = match response.text().await {
162 Ok(text) => text,
163 Err(err) => {
164 if let Some(cached_results) =
165 read_stale_similar_games(app, &similar_cache_key, gamebrain_id, requested_limit)?
166 {
167 return Ok(cached_results);
168 }
169
170 return Err(err.to_string());
171 }
172 };
173 tracing::debug!("GameBrain similar response size: {} bytes", text.len());
174
175 let raw: RawSimilarResponse = match serde_json::from_str(&text) {
176 Ok(raw) => raw,
177 Err(e) => {
178 tracing::error!("GameBrain similar JSON parse error: {}", e);
179 if let Some(cached_results) =
180 read_stale_similar_games(app, &similar_cache_key, gamebrain_id, requested_limit)?
181 {
182 return Ok(cached_results);
183 }
184
185 return Err(format!("Erro JSON GameBrain similar: {}", e));
186 }
187 };
188
189 let results: Vec<SimilarGame> = raw
190 .results
191 .into_iter()
192 .filter_map(|g| {
193 let raw_id = parse_gamebrain_id(&g.id)?;
195
196 Some(SimilarGame {
197 id: format!("gamebrain:{}", raw_id),
198 name: g.name,
199 cover_url: g.image,
200 genre: g.genre,
201 year: g.year.map(|y| y as u32),
202 rating: g.rating.and_then(|r| r.mean).map(|m| (m * 100.0).round()),
203 link: g.link,
204 screenshots: g.screenshots,
205 micro_trailer: g.micro_trailer,
206 adult_only: g.adult_only,
207 })
208 })
209 .collect();
210
211 tracing::debug!(
212 "GameBrain similar => {} jogos para gamebrain_id={}",
213 results.len(),
214 gamebrain_id
215 );
216
217 let _ = save_cached_json(app, "gamebrain", &similar_cache_key, &results);
219
220 Ok(take_similar_limit(results, requested_limit))
221}