game_manager_lib/database/
core.rs1use crate::constants::{
12 DB_FILENAME_CACHE, DB_FILENAME_GAMES, DB_FILENAME_SECRETS, DB_JOURNAL_MODE,
13};
14use crate::errors::AppError;
15use crate::security;
16use crate::services::integration::pcgamingwiki::db::initialize_pcgamingwiki_tables;
17use rusqlite::{params, Connection};
18use std::sync::Mutex;
19use tauri::State;
20use tauri::{AppHandle, Manager};
21
22pub struct AppState {
24 pub games_db: Mutex<Connection>,
25 pub secrets_db: Mutex<Connection>,
26 pub cache_db: Mutex<Connection>,
27}
28
29pub fn current_schema_version(conn: &Connection) -> Result<u32, AppError> {
31 let version: i32 = conn
32 .query_row("PRAGMA user_version", [], |row| row.get(0))
33 .unwrap_or(0);
34
35 Ok(version.max(0) as u32)
36}
37
38pub fn expected_schema_version(app: &AppHandle) -> u32 {
40 let version = app.package_info().version.clone();
42 version.major as u32
43}
44
45pub fn initialize_databases(app: &AppHandle) -> Result<AppState, String> {
54 let app_data_dir = app
55 .path()
56 .app_data_dir()
57 .map_err(|e| format!("Falha ao obter app_data_dir: {}", e))?;
58
59 std::fs::create_dir_all(&app_data_dir)
60 .map_err(|e| format!("Falha ao criar diretório: {}", e))?;
61
62 let games_path = app_data_dir.join(DB_FILENAME_GAMES);
64 let games_conn = Connection::open(&games_path)
65 .map_err(|e| format!("Erro ao abrir {}: {}", DB_FILENAME_GAMES, e))?;
66
67 games_conn
68 .pragma_update(None, "journal_mode", DB_JOURNAL_MODE)
69 .map_err(|e| format!("Erro ao configurar WAL no library.db: {}", e))?;
70
71 let cache_path = app_data_dir.join(DB_FILENAME_CACHE);
73 let cache_conn = Connection::open(&cache_path)
74 .map_err(|e| format!("Erro ao abrir {}: {}", DB_FILENAME_CACHE, e))?;
75
76 cache_conn
77 .pragma_update(None, "journal_mode", DB_JOURNAL_MODE)
78 .map_err(|e| {
79 AppError::DatabaseWalConfigError("cache.db".to_string(), e.to_string()).to_string()
80 })?;
81
82 crate::services::cache::initialize_cache_db(&cache_conn)?;
84
85 let secrets_path = app_data_dir.join(DB_FILENAME_SECRETS);
87 let secrets_conn = Connection::open(&secrets_path)
88 .map_err(|e| format!("Erro ao abrir {}: {}", DB_FILENAME_SECRETS, e))?;
89
90 secrets_conn
91 .pragma_update(None, "journal_mode", DB_JOURNAL_MODE)
92 .map_err(|e| format!("Erro ao configurar WAL no secrets.db: {}", e))?;
93
94 crate::database::migrations::run_migrations(app, &games_conn)?;
96
97 let schema_version = app.package_info().version.major as u32;
99 create_schema(&games_conn, schema_version)?;
100
101 Ok(AppState {
102 games_db: Mutex::new(games_conn),
103 secrets_db: Mutex::new(secrets_conn),
104 cache_db: Mutex::new(cache_conn),
105 })
106}
107
108fn create_schema(conn: &Connection, schema_version: u32) -> Result<(), String> {
122 conn.execute(
123 "CREATE TABLE IF NOT EXISTS games (
124 id TEXT PRIMARY KEY,
125 name TEXT NOT NULL,
126 cover_url TEXT,
127 platform TEXT NOT NULL,
128 platform_game_id TEXT NOT NULL,
129 installed BOOLEAN DEFAULT 0,
130 import_confidence TEXT,
131 install_path TEXT,
132 executable_path TEXT,
133 launch_args TEXT,
134 user_rating INTEGER,
135 favorite BOOLEAN DEFAULT 0,
136 status TEXT,
137 playtime INTEGER,
138 last_played TEXT,
139 added_at TEXT NOT NULL
140 )",
141 [],
142 )
143 .map_err(|e| e.to_string())?;
144
145 conn.execute(
146 "CREATE TABLE IF NOT EXISTS game_details (
147 game_id TEXT PRIMARY KEY,
148 steam_app_id TEXT,
149 developer TEXT,
150 publisher TEXT,
151 release_date TEXT,
152 genres TEXT,
153 tags TEXT,
154 series TEXT,
155 description_raw TEXT,
156 description_ptbr TEXT,
157 background_image TEXT,
158 critic_score INTEGER,
159 steam_review_label TEXT,
160 steam_review_count INTEGER,
161 steam_review_score REAL,
162 steam_review_updated_at TEXT,
163 esrb_rating TEXT,
164 is_adult BOOLEAN DEFAULT 0,
165 adult_tags TEXT,
166 external_links TEXT,
167 median_playtime INTEGER,
168 estimated_playtime REAL,
169 FOREIGN KEY(game_id) REFERENCES games(id) ON DELETE CASCADE
170 )",
171 [],
172 )
173 .map_err(|e| e.to_string())?;
174
175 conn.execute(
176 "CREATE TABLE IF NOT EXISTS wishlist (
177 id TEXT PRIMARY KEY,
178 name TEXT NOT NULL,
179 cover_url TEXT,
180 store_url TEXT,
181 store_platform TEXT,
182 current_price REAL,
183 normal_price REAL,
184 lowest_price REAL,
185 currency TEXT,
186 on_sale BOOLEAN DEFAULT 0,
187 voucher TEXT,
188 added_at TEXT,
189 itad_id TEXT
190 )",
191 [],
192 )
193 .map_err(|e| e.to_string())?;
194
195 conn.execute(
196 "CREATE TABLE IF NOT EXISTS subscriptions (
197 service TEXT PRIMARY KEY, -- 'prime_gaming', 'game_pass', etc.
198 enabled BOOLEAN DEFAULT 0,
199 last_synced TEXT -- ISO timestamp do último fetch
200 )",
201 [],
202 )
203 .map_err(|e| e.to_string())?;
204
205 conn.execute(
207 "CREATE INDEX IF NOT EXISTS idx_name ON games(name COLLATE NOCASE)",
208 [],
209 )
210 .map_err(|e| e.to_string())?;
211
212 conn.execute(
213 "CREATE INDEX IF NOT EXISTS idx_platform ON games(platform)",
214 [],
215 )
216 .map_err(|e| e.to_string())?;
217
218 conn.execute(
219 "CREATE INDEX IF NOT EXISTS idx_favorite ON games(favorite)",
220 [],
221 )
222 .map_err(|e| e.to_string())?;
223
224 conn.execute("CREATE INDEX IF NOT EXISTS idx_status ON games(status)", [])
225 .map_err(|e| e.to_string())?;
226
227 initialize_pcgamingwiki_tables(conn).map_err(|e| e.to_string())?;
229
230 conn.pragma_update(None, "user_version", schema_version)
232 .map_err(|e| format!("Erro ao definir versão do schema: {}", e))?;
233
234 Ok(())
235}
236
237#[tauri::command]
241pub fn init_db(app: AppHandle, state: State<AppState>) -> Result<String, String> {
242 let conn = state
243 .games_db
244 .lock()
245 .map_err(|_| "Falha ao bloquear mutex do games_db")?;
246
247 let current_version = current_schema_version(&conn).unwrap_or(0) as i32;
248 let expected_version = expected_schema_version(&app) as i32;
249
250 if current_version == 0 {
251 let schema_version = expected_schema_version(&app);
252 return Ok(format!("Banco de dados novo criado (v{})", schema_version));
253 }
254
255 if current_version != expected_version {
256 let app_data_dir = app
257 .path()
258 .app_data_dir()
259 .map_err(|e| format!("Falha ao obter app_data_dir: {}", e))?;
260
261 return Err(format!(
262 "Banco desatualizado: Schema atual: v{}, esperado: v{}. Faça backup, exclua o diretório da aplicação em: {:?} e reinicie para recriar o banco.",
263 current_version, expected_version, app_data_dir
264 ));
265 }
266
267 Ok(format!("Banco de dados OK (v{})", current_version))
268}
269
270pub fn serialize_tags(tags: &[crate::models::GameTag]) -> Result<String, String> {
272 serde_json::to_string(tags).map_err(|e| e.to_string())
273}
274
275pub fn deserialize_tags(tags_json: &str) -> Vec<crate::models::GameTag> {
277 use crate::utils::tag_utils::{TagCategory, TagRole};
278
279 if let Ok(tags) = serde_json::from_str::<Vec<crate::models::GameTag>>(tags_json) {
281 return tags;
282 }
283
284 tags_json
286 .split(',')
287 .map(|s| s.trim())
288 .filter(|s| !s.is_empty())
289 .map(|slug| crate::models::GameTag {
290 slug: slug.to_string(),
291 name: slug.to_string(),
292 category: TagCategory::Meta,
293 role: TagRole::Context,
294 relevance: 5.0,
295 })
296 .collect()
297}
298
299fn get_secrets_connection<'a>(
304 state: &'a State<AppState>,
305) -> Result<std::sync::MutexGuard<'a, Connection>, String> {
306 let conn = state
307 .secrets_db
308 .lock()
309 .map_err(|_| "Falha ao bloquear mutex do secrets_db".to_string())?;
310
311 conn.execute(
312 r#"
313 CREATE TABLE IF NOT EXISTS encrypted_keys (
314 key TEXT PRIMARY KEY,
315 value TEXT NOT NULL
316 )
317 "#,
318 [],
319 )
320 .map_err(|e: rusqlite::Error| e.to_string())?;
321
322 Ok(conn)
323}
324
325pub fn set_secret(app: &AppHandle, key_name: &str, value: &str) -> Result<(), AppError> {
327 let state: tauri::State<AppState> = app.state();
328 let conn = get_secrets_connection(&state)?;
329
330 let encrypted = security::encrypt(app, value)?;
331
332 conn.execute(
333 "INSERT OR REPLACE INTO encrypted_keys (key, value) VALUES (?1, ?2)",
334 params![key_name, encrypted],
335 )?;
336
337 Ok(())
338}
339
340pub fn get_secret(app: &AppHandle, key_name: &str) -> Result<String, AppError> {
342 let state: tauri::State<AppState> = app.state();
343 let conn = get_secrets_connection(&state)?;
344
345 let result: Result<String, rusqlite::Error> = conn.query_row(
346 "SELECT value FROM encrypted_keys WHERE key = ?1",
347 params![key_name],
348 |row| row.get::<_, String>(0),
349 );
350
351 match result {
352 Ok(encrypted) => {
353 let decrypted = security::decrypt(app, &encrypted)?;
354 Ok(decrypted)
355 }
356 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(String::new()),
357 Err(e) => Err(AppError::DatabaseError(e.to_string())),
358 }
359}
360
361pub fn delete_secret(app: &AppHandle, key_name: &str) -> Result<(), AppError> {
363 let state: tauri::State<AppState> = app.state();
364 let conn = get_secrets_connection(&state)?;
365
366 conn.execute(
367 "DELETE FROM encrypted_keys WHERE key = ?1",
368 params![key_name],
369 )?;
370
371 Ok(())
372}
373
374pub fn list_supported_keys() -> Vec<&'static str> {
376 vec![
377 "steam_id",
378 "steam_api_key",
379 "rawg_api_key",
380 "gemini_api_key",
381 "gamebrain_api_key",
382 ]
383}