game_manager_lib/services/
cache.rs1use crate::constants::{
7 CACHE_AMAZON_LUNA_TTL_DAYS, CACHE_DEFAULT_TTL_DAYS, CACHE_EA_PLAY_TTL_DAYS,
8 CACHE_GAMEBRAIN_ID_TTL_DAYS, CACHE_GAMEBRAIN_MEDIA_TTL_DAYS, CACHE_GAMEBRAIN_SIMILAR_TTL_DAYS,
9 CACHE_GAMERPOWER_TTL_DAYS, CACHE_GAME_PASS_FULL_TTL_DAYS, CACHE_RAWG_GAME_TTL_DAYS,
10 CACHE_RAWG_LIST_TTL_DAYS, CACHE_STEAM_PLAYTIME_TTL_DAYS, CACHE_STEAM_RESOLVE_TTL_DAYS,
11 CACHE_STEAM_REVIEWS_TTL_DAYS, CACHE_STEAM_STORE_TTL_DAYS, CACHE_UBISOFT_PLUS_TTL_DAYS,
12};
13use crate::errors::AppError;
14use rusqlite::{params, Connection};
15use std::time::{SystemTime, UNIX_EPOCH};
16use tracing::{info, warn};
17
18#[derive(Debug, serde::Serialize)]
20pub struct CacheStats {
21 pub total_entries: i32,
22 pub rawg_entries: i32,
23 pub gamebrain_entries: i32,
24 pub steam_entries: i32,
25 pub expired_entries: i32,
26}
27
28fn current_timestamp() -> i64 {
30 SystemTime::now()
31 .duration_since(UNIX_EPOCH)
32 .unwrap()
33 .as_secs() as i64
34}
35
36pub fn initialize_cache_db(conn: &Connection) -> Result<(), String> {
38 conn.execute(
39 "CREATE TABLE IF NOT EXISTS api_cache (
40 source TEXT NOT NULL,
41 external_id TEXT NOT NULL,
42 payload TEXT NOT NULL,
43 updated_at INTEGER NOT NULL,
44 PRIMARY KEY (source, external_id)
45 )",
46 [],
47 )
48 .map_err(|e| format!("Erro ao criar tabela api_cache: {}", e))?;
49
50 conn.execute(
52 "CREATE INDEX IF NOT EXISTS idx_cache_updated
53 ON api_cache(source, updated_at)",
54 [],
55 )
56 .map_err(|e| format!("Erro ao criar índice: {}", e))?;
57
58 Ok(())
59}
60
61fn get_ttl_for_cache_type(cache_key: &str) -> i64 {
63 if cache_key.contains("_list_") {
65 CACHE_RAWG_LIST_TTL_DAYS * 24 * 60 * 60
66 } else if cache_key.starts_with("resolve_") {
67 CACHE_STEAM_RESOLVE_TTL_DAYS * 24 * 60 * 60
68 } else if cache_key.starts_with("gamebrain_id:") {
69 CACHE_GAMEBRAIN_ID_TTL_DAYS * 24 * 60 * 60
70 } else if cache_key.starts_with("gamebrain_similar:") {
71 CACHE_GAMEBRAIN_SIMILAR_TTL_DAYS * 24 * 60 * 60
72 } else if cache_key.starts_with("gamebrain_media:") {
73 CACHE_GAMEBRAIN_MEDIA_TTL_DAYS * 24 * 60 * 60
74 } else if cache_key.starts_with("catalog_amazon_luna") {
75 CACHE_AMAZON_LUNA_TTL_DAYS * 24 * 60 * 60
76 } else if cache_key.starts_with("gamerpower_list_active") {
77 CACHE_GAMERPOWER_TTL_DAYS * 24 * 60 * 60
78 } else if cache_key.starts_with("catalog_game_pass_full") {
79 CACHE_GAME_PASS_FULL_TTL_DAYS * 24 * 60 * 60
80 } else if cache_key.starts_with("catalog_ubisoft_plus") {
81 CACHE_UBISOFT_PLUS_TTL_DAYS * 24 * 60 * 60
82 } else if cache_key.starts_with("catalog_ea_play") {
83 CACHE_EA_PLAY_TTL_DAYS * 24 * 60 * 60
84 } else if cache_key.starts_with("rawg_") {
85 CACHE_RAWG_GAME_TTL_DAYS * 24 * 60 * 60
86 } else if cache_key.starts_with("store_") {
87 CACHE_STEAM_STORE_TTL_DAYS * 24 * 60 * 60
88 } else if cache_key.starts_with("reviews_") {
89 CACHE_STEAM_REVIEWS_TTL_DAYS * 24 * 60 * 60
90 } else if cache_key.starts_with("playtime_") {
91 CACHE_STEAM_PLAYTIME_TTL_DAYS * 24 * 60 * 60
92 } else {
93 CACHE_DEFAULT_TTL_DAYS * 24 * 60 * 60 }
95}
96
97fn is_cache_expired(cache_key: &str, updated_at: i64) -> bool {
99 let now = current_timestamp();
100 let ttl_seconds = get_ttl_for_cache_type(cache_key);
101
102 (now - updated_at) > ttl_seconds
103}
104
105pub fn get_cached_api_data(conn: &Connection, source: &str, external_id: &str) -> Option<String> {
111 let result: Result<(String, i64), rusqlite::Error> = conn.query_row(
112 "SELECT payload, updated_at FROM api_cache
113 WHERE source = ?1 AND external_id = ?2",
114 params![source, external_id],
115 |row| Ok((row.get(0)?, row.get(1)?)),
116 );
117
118 match result {
119 Ok((payload, updated_at)) => {
120 let full_key = external_id;
122 if is_cache_expired(full_key, updated_at) {
123 None
124 } else {
125 Some(payload)
126 }
127 }
128 Err(rusqlite::Error::QueryReturnedNoRows) => None,
129 Err(e) => {
130 warn!("Erro ao buscar cache: {}", e);
131 None
132 }
133 }
134}
135
136pub fn save_cached_api_data(
138 conn: &Connection,
139 source: &str,
140 external_id: &str,
141 payload: &str,
142) -> Result<(), String> {
143 let now = current_timestamp();
144
145 conn.execute(
146 "INSERT OR REPLACE INTO api_cache (source, external_id, payload, updated_at)
147 VALUES (?1, ?2, ?3, ?4)",
148 params![source, external_id, payload, now],
149 )
150 .map_err(|e| format!("Erro ao salvar cache: {}", e))?;
151
152 Ok(())
153}
154
155pub fn cleanup_expired_cache(conn: &Connection) -> Result<usize, String> {
157 let now = current_timestamp();
158
159 let rawg_cutoff = now - (CACHE_RAWG_GAME_TTL_DAYS * 24 * 60 * 60);
161
162 let gamebrain_id_cutoff = now - (CACHE_GAMEBRAIN_ID_TTL_DAYS * 24 * 60 * 60);
163 let gamebrain_similar_cutoff = now - (CACHE_GAMEBRAIN_SIMILAR_TTL_DAYS * 24 * 60 * 60);
164 let gamebrain_media_cutoff = now - (CACHE_GAMEBRAIN_MEDIA_TTL_DAYS * 24 * 60 * 60);
165 let amazon_luna_cutoff = now - (CACHE_AMAZON_LUNA_TTL_DAYS * 24 * 60 * 60);
166 let gamerpower_cutoff = now - (CACHE_GAMERPOWER_TTL_DAYS * 24 * 60 * 60);
167 let game_pass_full_cutoff = now - (CACHE_GAME_PASS_FULL_TTL_DAYS * 24 * 60 * 60);
168 let ubisoft_plus_cutoff = now - (CACHE_UBISOFT_PLUS_TTL_DAYS * 24 * 60 * 60);
169 let ea_play_cutoff = now - (CACHE_EA_PLAY_TTL_DAYS * 24 * 60 * 60);
170 let store_cutoff = now - (CACHE_STEAM_STORE_TTL_DAYS * 24 * 60 * 60);
171 let reviews_cutoff = now - (CACHE_STEAM_REVIEWS_TTL_DAYS * 24 * 60 * 60);
172 let playtime_cutoff = now - (CACHE_STEAM_PLAYTIME_TTL_DAYS * 24 * 60 * 60);
173 let steam_resolve_cutoff = now - (CACHE_STEAM_RESOLVE_TTL_DAYS * 24 * 60 * 60);
174
175 let deleted = conn
176 .execute(
177 "DELETE FROM api_cache
178 WHERE (source = 'rawg' AND external_id LIKE 'search_%' AND updated_at < ?1)
179 OR (source = 'gamebrain' AND external_id LIKE 'gamebrain_id:%' AND updated_at < ?2)
180 OR (source = 'gamebrain' AND external_id LIKE 'gamebrain_similar:%' AND updated_at < ?3)
181 OR (source = 'gamebrain' AND external_id LIKE 'gamebrain_media:%' AND updated_at < ?4)
182 OR (source = 'amazon_luna' AND external_id LIKE 'catalog_amazon_luna%' AND updated_at < ?5)
183 OR (source = 'gamerpower' AND external_id LIKE 'gamerpower_list_active%' AND updated_at < ?6)
184 OR (source = 'game_pass_pc' AND external_id LIKE 'catalog_game_pass_full%' AND updated_at < ?7)
185 OR (source = 'ubisoft_plus' AND external_id LIKE 'catalog_ubisoft_plus%' AND updated_at < ?8)
186 OR (source = 'ea_play' AND external_id LIKE 'catalog_ea_play%' AND updated_at < ?9)
187 OR (source = 'steam' AND external_id LIKE 'store_%' AND updated_at < ?10)
188 OR (source = 'steam' AND external_id LIKE 'reviews_%' AND updated_at < ?11)
189 OR (source = 'steam' AND external_id LIKE 'playtime_%' AND updated_at < ?12)
190 OR (source = 'steam_resolve' AND updated_at < ?13)",
191 params![
192 rawg_cutoff,
193 gamebrain_id_cutoff,
194 gamebrain_similar_cutoff,
195 gamebrain_media_cutoff,
196 amazon_luna_cutoff,
197 gamerpower_cutoff,
198 game_pass_full_cutoff,
199 ubisoft_plus_cutoff,
200 ea_play_cutoff,
201 store_cutoff,
202 reviews_cutoff,
203 playtime_cutoff,
204 steam_resolve_cutoff,
205 ],
206 )
207 .map_err(|e| AppError::CacheCleanupError(e.to_string()).to_string())?;
208
209 if deleted > 0 {
210 info!("Cache cleanup: {} entradas removidas", deleted);
211 }
212
213 Ok(deleted)
214}
215
216pub fn get_stale_api_data(conn: &Connection, source: &str, external_id: &str) -> Option<String> {
220 let result: Result<String, rusqlite::Error> = conn.query_row(
221 "SELECT payload FROM api_cache
222 WHERE source = ?1 AND external_id = ?2",
223 params![source, external_id],
224 |row| row.get(0),
225 );
226
227 result.ok() }
229
230pub fn get_cache_stats(conn: &Connection) -> Result<CacheStats, String> {
232 let total: i32 = conn
233 .query_row("SELECT COUNT(*) FROM api_cache", [], |row| row.get(0))
234 .unwrap_or(0);
235
236 let rawg: i32 = conn
237 .query_row(
238 "SELECT COUNT(*) FROM api_cache WHERE source = 'rawg'",
239 [],
240 |row| row.get(0),
241 )
242 .unwrap_or(0);
243
244 let gamebrain: i32 = conn
245 .query_row(
246 "SELECT COUNT(*) FROM api_cache WHERE source = 'gamebrain'",
247 [],
248 |row| row.get(0),
249 )
250 .unwrap_or(0);
251
252 let steam: i32 = conn
253 .query_row(
254 "SELECT COUNT(*) FROM api_cache WHERE source = 'steam'",
255 [],
256 |row| row.get(0),
257 )
258 .unwrap_or(0);
259
260 let now = current_timestamp();
261 let rawg_cutoff = now - (CACHE_RAWG_GAME_TTL_DAYS * 24 * 60 * 60);
262 let gamebrain_id_cutoff = now - (CACHE_GAMEBRAIN_ID_TTL_DAYS * 24 * 60 * 60);
263 let gamebrain_similar_cutoff = now - (CACHE_GAMEBRAIN_SIMILAR_TTL_DAYS * 24 * 60 * 60);
264 let gamebrain_media_cutoff = now - (CACHE_GAMEBRAIN_MEDIA_TTL_DAYS * 24 * 60 * 60);
265 let amazon_luna_cutoff = now - (CACHE_AMAZON_LUNA_TTL_DAYS * 24 * 60 * 60);
266 let gamerpower_cutoff = now - (CACHE_GAMERPOWER_TTL_DAYS * 24 * 60 * 60);
267 let game_pass_full_cutoff = now - (CACHE_GAME_PASS_FULL_TTL_DAYS * 24 * 60 * 60);
268 let ubisoft_plus_cutoff = now - (CACHE_UBISOFT_PLUS_TTL_DAYS * 24 * 60 * 60);
269 let ea_play_cutoff = now - (CACHE_EA_PLAY_TTL_DAYS * 24 * 60 * 60);
270 let store_cutoff = now - (CACHE_STEAM_STORE_TTL_DAYS * 24 * 60 * 60);
271 let reviews_cutoff = now - (CACHE_STEAM_REVIEWS_TTL_DAYS * 24 * 60 * 60);
272 let playtime_cutoff = now - (CACHE_STEAM_PLAYTIME_TTL_DAYS * 24 * 60 * 60);
273 let steam_resolve_cutoff = now - (CACHE_STEAM_RESOLVE_TTL_DAYS * 24 * 60 * 60);
274
275 let expired: i32 = conn
276 .query_row(
277 "SELECT COUNT(*) FROM api_cache
278 WHERE (source = 'rawg' AND external_id LIKE 'search_%' AND updated_at < ?1)
279 OR (source = 'gamebrain' AND external_id LIKE 'gamebrain_id:%' AND updated_at < ?2)
280 OR (source = 'gamebrain' AND external_id LIKE 'gamebrain_similar:%' AND updated_at < ?3)
281 OR (source = 'gamebrain' AND external_id LIKE 'gamebrain_media:%' AND updated_at < ?4)
282 OR (source = 'amazon_luna' AND external_id LIKE 'catalog_amazon_luna%' AND updated_at < ?5)
283 OR (source = 'gamerpower' AND external_id LIKE 'gamerpower_list_active%' AND updated_at < ?6)
284 OR (source = 'game_pass_pc' AND external_id LIKE 'catalog_game_pass_full%' AND updated_at < ?7)
285 OR (source = 'ubisoft_plus' AND external_id LIKE 'catalog_ubisoft_plus%' AND updated_at < ?8)
286 OR (source = 'ea_play' AND external_id LIKE 'catalog_ea_play%' AND updated_at < ?9)
287 OR (source = 'steam' AND external_id LIKE 'store_%' AND updated_at < ?10)
288 OR (source = 'steam' AND external_id LIKE 'reviews_%' AND updated_at < ?11)
289 OR (source = 'steam' AND external_id LIKE 'playtime_%' AND updated_at < ?12)
290 OR (source = 'steam_resolve' AND updated_at < ?13)",
291 params![
292 rawg_cutoff,
293 gamebrain_id_cutoff,
294 gamebrain_similar_cutoff,
295 gamebrain_media_cutoff,
296 amazon_luna_cutoff,
297 gamerpower_cutoff,
298 game_pass_full_cutoff,
299 ubisoft_plus_cutoff,
300 ea_play_cutoff,
301 store_cutoff,
302 reviews_cutoff,
303 playtime_cutoff,
304 steam_resolve_cutoff,
305 ],
306 |row| row.get(0),
307 )
308 .unwrap_or(0);
309
310 Ok(CacheStats {
311 total_entries: total,
312 rawg_entries: rawg,
313 gamebrain_entries: gamebrain,
314 steam_entries: steam,
315 expired_entries: expired,
316 })
317}