game_manager_lib/commands/metadata/
refresh.rs1use crate::constants::{
6 BACKGROUND_TASK_INTERVAL_SECS, GAMERPOWER_CACHE_SOURCE, GAMERPOWER_LIST_ACTIVE_CACHE_KEY,
7 STARTUP_DELAY_SECS,
8};
9use crate::database::AppState;
10use crate::errors::AppError;
11use crate::services::cache;
12use crate::services::integration::{gamerpower, itad, steam_api};
13use lazy_static::lazy_static;
14use rusqlite::params;
15use std::sync::Arc;
16use std::time::Duration;
17use tauri::{AppHandle, Emitter, Manager, State};
18use tokio::sync::Semaphore;
19use tokio::time::sleep;
20use tracing::{error, info, warn};
21
22lazy_static! {
25 static ref BACKGROUND_REFRESH_SEMAPHORE: Arc<Semaphore> = Arc::new(Semaphore::new(1));
26}
27
28#[tauri::command]
32pub async fn check_and_refresh_background(app: AppHandle) -> Result<(), AppError> {
33 let permit = match BACKGROUND_REFRESH_SEMAPHORE.try_acquire() {
35 Ok(permit) => permit,
36 Err(_) => {
37 tracing::debug!("Background refresh já em execução, ignorando chamada duplicada");
39 return Ok(());
40 }
41 };
42
43 let app_clone = app.clone();
45
46 tauri::async_runtime::spawn(async move {
48 let _permit = permit;
51
52 sleep(Duration::from_secs(STARTUP_DELAY_SECS)).await;
54
55 let state: State<AppState> = app_clone.state();
56
57 if let Err(e) = refresh_steam_reviews_background(&app_clone, &state).await {
59 warn!("Falha ao atualizar reviews: {}", e);
60 }
61
62 sleep(Duration::from_secs(BACKGROUND_TASK_INTERVAL_SECS)).await;
64
65 if let Err(e) = refresh_gamerpower_background(&app_clone, &state).await {
66 warn!("Falha ao atualizar GamerPower: {}", e);
67 }
68
69 sleep(Duration::from_secs(BACKGROUND_TASK_INTERVAL_SECS)).await;
71
72 if let Err(e) = refresh_wishlist_prices_background(&app_clone, &state).await {
73 warn!("Falha ao atualizar preços: {}", e);
74 }
75
76 let _ = app_clone.emit("background_refresh_complete", ());
78
79 });
81
82 Ok(())
83}
84
85async fn refresh_steam_reviews_background(
88 app: &AppHandle,
89 state: &State<'_, AppState>,
90) -> Result<(), String> {
91 let steam_games: Vec<(u32, String)> = {
93 let conn = state.games_db.lock().map_err(|_| "Falha DB Lock")?;
94
95 conn.prepare("SELECT platform_game_id, name FROM games WHERE platform = 'Steam'")
96 .and_then(|mut stmt| {
97 stmt.query_map([], |row| {
98 let id_str: String = row.get(0)?;
99 let name: String = row.get(1)?;
100 Ok((id_str.parse::<u32>().unwrap_or(0), name))
101 })
102 .and_then(|mapped| mapped.collect::<Result<Vec<_>, _>>())
103 })
104 .map_err(|e| e.to_string())?
105 .into_iter()
106 .filter(|(id, _)| *id > 0)
107 .collect()
108 };
109
110 if steam_games.is_empty() {
111 return Ok(());
112 }
113
114 let mut updated_count = 0;
115
116 for (app_id, _title) in steam_games {
118 let should_update = {
119 match state.cache_db.lock() {
120 Ok(cache_conn) => {
121 let cache_key = format!("reviews_{}", app_id);
122 cache::get_cached_api_data(&cache_conn, "steam", &cache_key).is_none()
124 }
125 Err(_) => false, }
127 };
128
129 if should_update {
130 let app_id_str = app_id.to_string();
131 match steam_api::get_app_reviews(&app_id_str).await {
133 Ok(Some(summary)) => {
134 {
136 if let Ok(cache_conn) = state.cache_db.lock() {
138 let cache_key = format!("reviews_{}", app_id);
139 if let Ok(json) = serde_json::to_string(&summary) {
140 let _ = cache::save_cached_api_data(
141 &cache_conn,
142 "steam",
143 &cache_key,
144 &json,
145 );
146 }
147 }
148 }
149
150 updated_count += 1;
151 }
152 Ok(None) => { }
153 Err(e) => {
154 warn!("Falha ao buscar review {}: {}", app_id, e);
156 }
157 }
158 sleep(Duration::from_millis(200)).await;
160 }
161 }
162
163 if updated_count > 0 {
164 info!("{} avaliações atualizadas", updated_count);
165 let _ = app.emit(
166 "reviews_refresh_complete",
167 format!(
168 "{} avaliações Steam atualizadas em background.",
169 updated_count
170 ),
171 );
172 }
173
174 Ok(())
175}
176
177async fn refresh_gamerpower_background(
179 app: &AppHandle,
180 state: &State<'_, AppState>,
181) -> Result<(), String> {
182 let should_refresh = {
183 let cache_conn = state.cache_db.lock().map_err(|_| "Falha DB Cache Lock")?;
184 cache::get_cached_api_data(
185 &cache_conn,
186 GAMERPOWER_CACHE_SOURCE,
187 GAMERPOWER_LIST_ACTIVE_CACHE_KEY,
188 )
189 .is_none()
190 };
191
192 if !should_refresh {
193 return Ok(());
194 }
195
196 match gamerpower::fetch_giveaways(app).await {
197 Ok(_) => {
198 info!("Cache GamerPower atualizado em background");
199 let _ = app.emit("gamerpower_refresh_complete", ());
200 Ok(())
201 }
202 Err(e) => {
203 warn!("Falha ao atualizar GamerPower em background: {}", e);
204 Err(e)
205 }
206 }
207}
208
209async fn refresh_wishlist_prices_background(
211 app: &AppHandle,
212 state: &State<'_, AppState>,
213) -> Result<(), String> {
214 let wishlist_items: Vec<(String, String, Option<String>)> = {
216 let conn = state.games_db.lock().map_err(|_| "Falha DB")?;
217
218 conn.prepare("SELECT id, name, itad_id FROM wishlist")
219 .and_then(|mut stmt| {
220 stmt.query_map([], |row| {
221 Ok((
222 row.get::<_, String>(0)?,
223 row.get::<_, String>(1)?,
224 row.get::<_, Option<String>>(2)?,
225 ))
226 })
227 .and_then(|mapped| mapped.collect::<Result<Vec<_>, _>>())
228 })
229 .map_err(|e| e.to_string())?
230 };
231
232 if wishlist_items.is_empty() {
233 return Ok(());
234 }
235
236 let mut itad_ids_to_fetch = Vec::new();
238 let mut game_map = std::collections::HashMap::new();
239
240 for (local_id, name, itad_id_opt) in wishlist_items {
241 let itad_id = match itad_id_opt {
243 Some(id) if !id.is_empty() => id,
244 _ => {
245 match itad::find_game_id(&name).await {
247 Ok(found_id) => {
248 let conn = state.games_db.lock().unwrap();
250 let _ = conn.execute(
251 "UPDATE wishlist SET itad_id = ?1 WHERE id = ?2",
252 params![&found_id, &local_id],
253 );
254 found_id
255 }
256 Err(_) => {
257 continue; }
259 }
260 }
261 };
262
263 let should_update = {
265 let cache_conn = state.cache_db.lock().unwrap();
266 let cache_key = format!("price_{}", itad_id);
267 cache::get_cached_api_data(&cache_conn, "itad", &cache_key).is_none()
269 };
270
271 if should_update {
272 itad_ids_to_fetch.push(itad_id.clone());
273 game_map.insert(itad_id, (local_id, name));
274 }
275 }
276
277 if itad_ids_to_fetch.is_empty() {
278 return Ok(());
279 }
280
281 let overviews = match itad::get_prices(itad_ids_to_fetch).await {
283 Ok(data) => data,
284 Err(e) => {
285 error!("Erro ao buscar preços da ITAD: {}", e);
286 return Err(e);
287 }
288 };
289
290 let mut updated_count = 0;
291
292 for game_data in overviews {
294 if let Some((local_id, _game_name)) = game_map.get(&game_data.id) {
295 {
297 if let Ok(cache_conn) = state.cache_db.lock() {
298 let cache_key = format!("price_{}", game_data.id);
299
300 let cache_data = serde_json::json!({
302 "id": game_data.id,
303 "current_price": game_data.current.as_ref().map(|d| d.price),
304 "currency": game_data.current.as_ref().map(|d| &d.currency),
305 "lowest_price": game_data.lowest.as_ref().map(|d| d.price),
306 });
307
308 let json = cache_data.to_string();
309 let _ = cache::save_cached_api_data(&cache_conn, "itad", &cache_key, &json);
310 }
311 }
312
313 if let Some(deal) = game_data.current {
315 let lowest = game_data.lowest.map(|l| l.price).unwrap_or(deal.price);
316
317 let cut = deal.cut.unwrap_or(0) as f64;
318 let normal_price = if cut > 0.0 {
319 deal.price / (1.0 - (cut / 100.0))
320 } else {
321 deal.price
322 };
323
324 if let Ok(conn) = state.games_db.lock() {
325 match conn.execute(
326 "UPDATE wishlist SET
327 current_price = ?1,
328 currency = ?2,
329 lowest_price = ?3,
330 store_platform = ?4,
331 store_url = ?5,
332 on_sale = ?6,
333 normal_price = ?7,
334 voucher = ?8
335 WHERE id = ?9",
336 params![
337 deal.price,
338 deal.currency,
339 lowest,
340 deal.shop.name,
341 deal.url,
342 deal.cut > Some(0),
343 normal_price,
344 deal.voucher,
345 local_id
346 ],
347 ) {
348 Ok(_) => updated_count += 1,
349 Err(e) => error!("Erro ao atualizar preço: {}", e),
350 }
351 }
352 }
353 }
354 }
355
356 if updated_count > 0 {
357 info!("{} preços atualizados", updated_count);
358 let _ = app.emit(
359 "wishlist_refresh_complete",
360 format!(
361 "{} preços da Wishlist atualizados em background.",
362 updated_count
363 ),
364 );
365 }
366
367 Ok(())
368}