game_manager_lib\commands\metadata/
refresh.rs1use crate::constants::{BACKGROUND_TASK_INTERVAL_SECS, STARTUP_DELAY_SECS};
6use crate::database::AppState;
7use crate::errors::AppError;
8use crate::services::{cache, itad, steam};
9use rusqlite::params;
10use std::sync::atomic::{AtomicBool, Ordering};
11use std::time::Duration;
12use tauri::{AppHandle, Emitter, Manager, State};
13use tokio::time::sleep;
14use tracing::{error, info, warn};
15
16static BACKGROUND_REFRESH_RUNNING: AtomicBool = AtomicBool::new(false);
18
19#[tauri::command]
23pub async fn check_and_refresh_background(app: AppHandle) -> Result<(), AppError> {
24 if BACKGROUND_REFRESH_RUNNING.swap(true, Ordering::SeqCst) {
26 return Ok(());
28 }
29
30 let app_clone = app.clone();
32
33 tauri::async_runtime::spawn(async move {
35 sleep(Duration::from_secs(STARTUP_DELAY_SECS)).await;
37
38 let state: State<AppState> = app_clone.state();
39
40 if let Err(e) = refresh_steam_reviews_background(&state).await {
42 warn!("Falha ao atualizar reviews: {}", e);
43 }
44
45 sleep(Duration::from_secs(BACKGROUND_TASK_INTERVAL_SECS)).await;
47
48 if let Err(e) = refresh_wishlist_prices_background(&app_clone, &state).await {
49 warn!("Falha ao atualizar preços: {}", e);
50 }
51
52 let _ = app_clone.emit("background_refresh_complete", ());
54
55 BACKGROUND_REFRESH_RUNNING.store(false, Ordering::SeqCst);
57 });
58
59 Ok(())
60}
61
62async fn refresh_steam_reviews_background(state: &State<'_, AppState>) -> Result<(), String> {
64 let steam_games: Vec<(u32, String)> = {
66 let conn = state.library_db.lock().map_err(|_| "Falha DB Lock")?;
67
68 conn.prepare("SELECT platform_id, title FROM games WHERE platform = 'Steam'")
69 .and_then(|mut stmt| {
70 stmt.query_map([], |row| {
71 let id_str: String = row.get(0)?;
72 let title: String = row.get(1)?;
73 Ok((id_str.parse::<u32>().unwrap_or(0), title))
74 })
75 .and_then(|mapped| mapped.collect::<Result<Vec<_>, _>>())
76 })
77 .map_err(|e| e.to_string())?
78 .into_iter()
79 .filter(|(id, _)| *id > 0)
80 .collect()
81 };
82
83 if steam_games.is_empty() {
84 return Ok(());
85 }
86
87 let mut updated_count = 0;
88
89 for (app_id, _title) in steam_games {
91 let should_update = {
92 match state.metadata_db.lock() {
93 Ok(cache_conn) => {
94 let cache_key = format!("reviews_{}", app_id);
95 cache::get_cached_api_data(&cache_conn, "steam", &cache_key).is_none()
97 }
98 Err(_) => false, }
100 };
101
102 if should_update {
103 let app_id_str = app_id.to_string();
104 match steam::get_app_reviews(&app_id_str).await {
106 Ok(Some(summary)) => {
107 {
109 if let Ok(cache_conn) = state.metadata_db.lock() {
111 let cache_key = format!("reviews_{}", app_id);
112 if let Ok(json) = serde_json::to_string(&summary) {
113 let _ = cache::save_cached_api_data(
114 &cache_conn,
115 "steam",
116 &cache_key,
117 &json,
118 );
119 }
120 }
121 }
122
123 {
124 let total = summary.total_reviews;
127 let percent_positive = if total > 0 {
128 (summary.total_positive as f64 / total as f64 * 100.0) as i32
129 } else {
130 0
131 };
132
133 if let Ok(conn) = state.library_db.lock() {
134 let _ = conn.execute(
135 "UPDATE games SET user_rating = ?1 WHERE platform = 'Steam' AND platform_id = ?2",
136 params![percent_positive, app_id_str],
137 );
138 }
139 }
140 updated_count += 1;
141 }
142 Ok(None) => { }
143 Err(e) => {
144 warn!("Falha ao buscar review {}: {}", app_id, e);
146 }
147 }
148 sleep(Duration::from_millis(200)).await;
150 }
151 }
152
153 if updated_count > 0 {
154 info!("{} avaliações atualizadas", updated_count);
155 }
156
157 Ok(())
158}
159
160async fn refresh_wishlist_prices_background(
162 app: &AppHandle,
163 state: &State<'_, AppState>,
164) -> Result<(), String> {
165 let wishlist_items: Vec<(String, String, Option<String>)> = {
167 let conn = state.library_db.lock().map_err(|_| "Falha DB")?;
168
169 conn.prepare("SELECT id, name, itad_id FROM wishlist")
170 .and_then(|mut stmt| {
171 stmt.query_map([], |row| {
172 Ok((
173 row.get::<_, String>(0)?,
174 row.get::<_, String>(1)?,
175 row.get::<_, Option<String>>(2)?,
176 ))
177 })
178 .and_then(|mapped| mapped.collect::<Result<Vec<_>, _>>())
179 })
180 .map_err(|e| e.to_string())?
181 };
182
183 if wishlist_items.is_empty() {
184 return Ok(());
185 }
186
187 let mut itad_ids_to_fetch = Vec::new();
189 let mut game_map = std::collections::HashMap::new();
190
191 for (local_id, name, itad_id_opt) in wishlist_items {
192 let itad_id = match itad_id_opt {
194 Some(id) if !id.is_empty() => id,
195 _ => {
196 match itad::find_game_id(&name).await {
198 Ok(found_id) => {
199 let conn = state.library_db.lock().unwrap();
201 let _ = conn.execute(
202 "UPDATE wishlist SET itad_id = ?1 WHERE id = ?2",
203 params![&found_id, &local_id],
204 );
205 found_id
206 }
207 Err(_) => {
208 continue; }
210 }
211 }
212 };
213
214 let should_update = {
216 let cache_conn = state.metadata_db.lock().unwrap();
217 let cache_key = format!("price_{}", itad_id);
218 cache::get_cached_api_data(&cache_conn, "itad", &cache_key).is_none()
220 };
221
222 if should_update {
223 itad_ids_to_fetch.push(itad_id.clone());
224 game_map.insert(itad_id, (local_id, name));
225 }
226 }
227
228 if itad_ids_to_fetch.is_empty() {
229 return Ok(());
230 }
231
232 let overviews = match itad::get_prices(itad_ids_to_fetch).await {
234 Ok(data) => data,
235 Err(e) => {
236 error!("Erro ao buscar preços da ITAD: {}", e);
237 return Err(e);
238 }
239 };
240
241 let mut updated_count = 0;
242
243 for game_data in overviews {
245 if let Some((local_id, _game_name)) = game_map.get(&game_data.id) {
246 {
248 if let Ok(cache_conn) = state.metadata_db.lock() {
249 let cache_key = format!("price_{}", game_data.id);
250
251 let cache_data = serde_json::json!({
253 "id": game_data.id,
254 "current_price": game_data.current.as_ref().map(|d| d.price),
255 "currency": game_data.current.as_ref().map(|d| &d.currency),
256 "lowest_price": game_data.lowest.as_ref().map(|d| d.price),
257 });
258
259 let json = cache_data.to_string();
260 let _ = cache::save_cached_api_data(&cache_conn, "itad", &cache_key, &json);
261 }
262 }
263
264 if let Some(deal) = game_data.current {
266 let lowest = game_data.lowest.map(|l| l.price).unwrap_or(deal.price);
267
268 let cut = deal.cut.unwrap_or(0) as f64;
269 let normal_price = if cut > 0.0 {
270 deal.price / (1.0 - (cut / 100.0))
271 } else {
272 deal.price
273 };
274
275 if let Ok(conn) = state.library_db.lock() {
276 match conn.execute(
277 "UPDATE wishlist SET
278 current_price = ?1,
279 currency = ?2,
280 lowest_price = ?3,
281 store_platform = ?4,
282 store_url = ?5,
283 on_sale = ?6,
284 normal_price = ?7,
285 voucher = ?8
286 WHERE id = ?9",
287 params![
288 deal.price,
289 deal.currency,
290 lowest,
291 deal.shop.name,
292 deal.url,
293 deal.cut > Some(0),
294 normal_price,
295 deal.voucher,
296 local_id
297 ],
298 ) {
299 Ok(_) => updated_count += 1,
300 Err(e) => error!("Erro ao atualizar preço: {}", e),
301 }
302 }
303 }
304 }
305 }
306
307 if updated_count > 0 {
308 info!("{} preços atualizados", updated_count);
309 let _ = app.emit("wishlist_prices_updated", ());
310 }
311
312 Ok(())
313}