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