Skip to main content

game_manager_lib/database/
backup.rs

1//! Módulo de ‘backup’ e restauração de dados.
2//!
3//! Fornece funcionalidades para exportar e importar a base de dados completa
4//! em formato JSON, incluindo biblioteca de jogos e lista de desejos.
5//!
6//! **Nota:**
7//! Todas as operações usam transações ACID para garantir consistência dos dados.
8
9use crate::database;
10use crate::database::{current_schema_version, AppState};
11use crate::errors::AppError;
12use crate::models::{
13    Game, GameDataPath, GameDetails, GameExtras, Platform, SystemRequirements, WishlistGame,
14};
15use chrono::Utc;
16use rusqlite::params;
17use std::fs;
18use std::path::PathBuf;
19use tauri::{AppHandle, Manager, State};
20
21/// Type alias para dados de backup
22type BackupDataTuple = (
23    Vec<Game>,
24    Vec<GameDetails>,
25    Vec<WishlistGame>,
26    Vec<GameExtras>,
27    Vec<SystemRequirements>,
28    Vec<GameDataPath>,
29    u32,
30);
31
32/// Estrutura do arquivo de ‘backup’.
33///
34/// Contém metadados e todos os dados exportados da aplicação.
35#[derive(serde::Serialize, serde::Deserialize)]
36pub struct BackupData {
37    pub version: u32, // schema == backup
38    pub app_version: String,
39    pub date: String,
40    pub games: Vec<Game>,
41    pub game_details: Vec<GameDetails>,
42    pub wishlist_game: Vec<WishlistGame>,
43    /// Dados técnicos obtidos do PCGamingWiki.
44    /// Campo ausente em backups anteriores ao schema v4 — tratado como lista vazia.
45    #[serde(default)]
46    pub game_extras: Vec<GameExtras>,
47    /// Requisitos de sistema por jogo/OS/tier.
48    #[serde(default)]
49    pub system_requirements: Vec<SystemRequirements>,
50    /// Caminhos de save e config por jogo/OS.
51    #[serde(default)]
52    pub game_data_paths: Vec<GameDataPath>,
53}
54
55/// Função auxiliar interna para buscar dados do backup com transação ACID
56///
57/// Retorna tupla com (games, game_details, wishlist_game, schema_version)
58fn fetch_backup_data(state: &State<AppState>) -> Result<BackupDataTuple, AppError> {
59    let conn = state.games_db.lock()?;
60
61    // Inicia transação READ para consistência
62    conn.execute("BEGIN TRANSACTION", [])?;
63
64    let games = fetch_games(&conn)?;
65    let game_details = fetch_game_details(&conn)?;
66    let wishlist_game = fetch_wishlist(&conn)?;
67    let game_extras = fetch_game_extras(&conn)?;
68    let system_requirements = fetch_system_requirements(&conn)?;
69    let game_data_paths = fetch_game_data_paths(&conn)?;
70    let schema_version = current_schema_version(&conn)?;
71
72    conn.execute("COMMIT", [])?;
73
74    Ok((
75        games,
76        game_details,
77        wishlist_game,
78        game_extras,
79        system_requirements,
80        game_data_paths,
81        schema_version,
82    ))
83}
84
85/// Exporta toda a base de dados para um arquivo JSON.
86///
87/// Cria um snapshot completo e consistente de todos os dados da aplicação,
88/// incluindo jogos e wishlist, num único arquivo JSON formatado.
89#[tauri::command]
90pub async fn export_database(
91    app: AppHandle,
92    state: State<'_, AppState>,
93    file_path: String,
94) -> Result<(), AppError> {
95    // Buscar dados com transação ACID
96    let (
97        games,
98        game_details,
99        wishlist_game,
100        game_extras,
101        system_requirements,
102        game_data_paths,
103        schema_version,
104    ) = fetch_backup_data(&state)?;
105
106    let backup = BackupData {
107        version: schema_version,
108        app_version: app.package_info().version.to_string(),
109        date: chrono::Local::now().to_rfc3339(),
110        games,
111        game_details,
112        wishlist_game,
113        game_extras,
114        system_requirements,
115        game_data_paths,
116    };
117
118    let json = serde_json::to_string_pretty(&backup)?;
119    fs::write(file_path, json)?;
120
121    // Atualiza timestamp do último backup manual
122    let cache_conn = state.cache_db.lock().map_err(|_| AppError::MutexError)?;
123    let now = Utc::now().to_rfc3339();
124    database::configs::set_config(&cache_conn, "last_backup_at", &now)?;
125
126    Ok(())
127}
128
129pub fn backup_if_major_update(
130    app: &AppHandle,
131    previous_version: &str,
132    current_version: &str,
133) -> Result<Option<PathBuf>, AppError> {
134    // Parse das versões
135    let parse_version = |v: &str| -> (u32, u32, u32) {
136        let parts: Vec<&str> = v.split('.').collect();
137        let major = parts.first().and_then(|s| s.parse().ok()).unwrap_or(0);
138        let minor = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
139        let patch = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
140        (major, minor, patch)
141    };
142
143    let (prev_major, _, _) = parse_version(previous_version);
144    let (curr_major, _, _) = parse_version(current_version);
145
146    // Se versão major mudou, faz backup
147    if prev_major != curr_major && prev_major > 0 {
148        tracing::info!(
149            "Mudança de versão major detectada: v{} -> v{}",
150            previous_version,
151            current_version
152        );
153        let backup_path = backup_before_update(app, previous_version)?;
154        Ok(Some(backup_path))
155    } else {
156        Ok(None)
157    }
158}
159
160/// Cria backup automático antes de atualização de versão
161///
162/// Chamado automaticamente quando detecta mudança de versão major
163pub fn backup_before_update(app: &AppHandle, previous_version: &str) -> Result<PathBuf, AppError> {
164    tracing::info!("Criando backup automático antes da atualização...");
165
166    let app_data_dir = app
167        .path()
168        .app_data_dir()
169        .map_err(|e| AppError::IoError(format!("Falha ao obter app_data_dir: {}", e)))?;
170
171    let backups_dir = app_data_dir.join("backups");
172    std::fs::create_dir_all(&backups_dir)?;
173
174    // Nome do backup com timestamp e versão anterior
175    let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
176    let backup_filename = format!("auto_backup_v{}_{}.json", previous_version, timestamp);
177    let backup_path = backups_dir.join(backup_filename);
178
179    // Reutiliza a função auxiliar de fetch
180    let state: tauri::State<AppState> = app.state();
181    let (
182        games,
183        game_details,
184        wishlist_game,
185        game_extras,
186        system_requirements,
187        game_data_paths,
188        schema_version,
189    ) = fetch_backup_data(&state)?;
190
191    let backup = BackupData {
192        version: schema_version,
193        app_version: previous_version.to_string(),
194        date: chrono::Local::now().to_rfc3339(),
195        games,
196        game_details,
197        wishlist_game,
198        game_extras,
199        system_requirements,
200        game_data_paths,
201    };
202
203    let json = serde_json::to_string_pretty(&backup)?;
204    fs::write(&backup_path, json)?;
205
206    // Atualiza timestamp do último backup automático
207    let cache_conn = state.cache_db.lock().map_err(|_| AppError::MutexError)?;
208    let now = Utc::now().to_rfc3339();
209    database::configs::set_config(&cache_conn, "last_auto_backup_at", &now)?;
210
211    tracing::info!("Backup automático criado: {:?}", backup_path);
212    Ok(backup_path)
213}
214
215/// Importa e restaura dados de um arquivo de 'backup'.
216///
217/// Lê um arquivo JSON de 'backup' restaura todos os dados no banco,
218/// substituindo registros existentes (INSERT OR REPLACE).
219///
220/// **Nota:**
221/// Esta operação pode sobrescrever dados existentes. Considere criar um 'backup' antes de importar.
222#[tauri::command]
223pub async fn import_database(
224    _app: AppHandle,
225    state: State<'_, AppState>,
226    file_path: String,
227) -> Result<String, AppError> {
228    let content = fs::read_to_string(file_path)?;
229    let backup: BackupData = serde_json::from_str(&content)
230        .map_err(|_| AppError::ValidationError("Arquivo de backup inválido".to_string()))?;
231
232    // Validação de versão
233    let current_version = {
234        let conn = state.games_db.lock()?;
235        current_schema_version(&conn)?
236    };
237
238    if backup.version != current_version {
239        return Err(AppError::ValidationError(format!(
240            "Backup incompatível. Backup v{}, app espera v{}",
241            backup.version, current_version
242        )));
243    }
244
245    let conn = state.games_db.lock()?;
246
247    // Transação única para todas as operações
248    conn.execute("BEGIN IMMEDIATE TRANSACTION", [])?;
249
250    // Usa prepared statements para melhor desempenho
251    let mut game_stmt = conn.prepare(
252        "INSERT OR REPLACE INTO games (id, name, cover_url, platform, platform_game_id, installed, import_confidence, install_path, executable_path, launch_args, user_rating, favorite, status, playtime, last_played, added_at)
253         VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16)"
254    )?;
255
256    // Prepared Statement para Details (ATUALIZADO COM NOVOS CAMPOS)
257    let mut details_stmt = conn.prepare(
258        "INSERT OR REPLACE INTO game_details (
259        game_id, steam_app_id, developer, publisher, release_date, genres, tags, series,
260        description_raw, description_ptbr, background_image, critic_score, steam_review_label,
261        steam_review_count, steam_review_score, steam_review_updated_at, esrb_rating, is_adult,
262        adult_tags, external_links, median_playtime, estimated_playtime
263    )
264     VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22)"
265    )?;
266
267    let mut wishlist_stmt = conn.prepare(
268        "INSERT OR REPLACE INTO wishlist (id, name, cover_url, store_url, store_platform, current_price, normal_price, lowest_price, currency, on_sale, voucher, added_at, itad_id)
269         VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)"
270    )?;
271
272    for game in &backup.games {
273        game_stmt.execute(rusqlite::params![
274            game.id,
275            game.name,
276            game.cover_url,
277            game.platform.to_string(),
278            game.platform_game_id,
279            game.installed,
280            game.import_confidence.as_ref().map(|ic| ic.to_string()),
281            game.install_path,
282            game.executable_path,
283            game.launch_args,
284            game.user_rating,
285            game.favorite,
286            game.status,
287            game.playtime,
288            game.last_played,
289            game.added_at
290        ])?;
291    }
292
293    // Loop Details
294    for detail in &backup.game_details {
295        // Serializa o HashMap de links e as tags para JSON String antes de salvar
296        let links_json = detail
297            .external_links
298            .as_ref()
299            .and_then(|links| serde_json::to_string(links).ok());
300
301        let tags_json = detail
302            .tags
303            .as_ref()
304            .and_then(|tags| crate::database::serialize_tags(tags).ok());
305
306        details_stmt.execute(params![
307            detail.game_id,
308            detail.steam_app_id,
309            detail.developer,
310            detail.publisher,
311            detail.release_date,
312            detail.genres,
313            tags_json,
314            detail.series,
315            detail.description_raw,
316            detail.description_ptbr,
317            detail.background_image,
318            detail.critic_score,
319            detail.steam_review_label,
320            detail.steam_review_count,
321            detail.steam_review_score,
322            detail.steam_review_updated_at,
323            detail.esrb_rating,
324            detail.is_adult,
325            detail.adult_tags,
326            links_json, // JSON String
327            detail.median_playtime,
328            detail.estimated_playtime
329        ])?;
330    }
331
332    for item in &backup.wishlist_game {
333        wishlist_stmt.execute(rusqlite::params![
334            item.id,
335            item.name,
336            item.cover_url,
337            item.store_url,
338            item.store_platform,
339            item.current_price,
340            item.normal_price,
341            item.lowest_price,
342            item.currency,
343            item.on_sale,
344            item.voucher,
345            item.added_at,
346            item.itad_id
347        ])?;
348    }
349
350    // Prepared statement para game_extras
351    let mut extras_stmt = conn.prepare(
352        "INSERT OR REPLACE INTO game_extras (
353        steam_app_id, pcgw_page_id, pcgw_page_name, engine,
354        available_on,
355        dx_versions, vulkan_versions, opengl_versions,
356        win64, linux64, macos_arm, macos_intel64,
357        ray_tracing, upscaling, frame_gen,
358        ultrawidescreen, four_k_support, hdr, high_fps, fov, borderless_windowed, color_blind,
359        controller_support, full_controller, playstation_controllers, xinput_controllers,
360        surround_sound, subtitles, closed_captions,
361        has_save_data, has_config_data,
362        languages_interface, languages_audio, languages_subtitles,
363        fetched_at
364    ) VALUES (
365        ?1,  ?2,  ?3,  ?4,
366        ?5,
367        ?6,  ?7,  ?8,
368        ?9,  ?10, ?11, ?12,
369        ?13, ?14, ?15,
370        ?16, ?17, ?18, ?19, ?20, ?21, ?22,
371        ?23, ?24, ?25, ?26,
372        ?27, ?28, ?29,
373        ?30, ?31,
374        ?32, ?33, ?34,
375        ?35
376    )",
377    )?;
378
379    let mut sysreq_stmt = conn.prepare(
380        "INSERT INTO system_requirements (
381            steam_app_id, os_family, tier_title, target,
382            min_os, min_cpu, min_cpu2, min_ram, min_gpu, min_gpu2, min_vram, min_dx, min_storage,
383            rec_os, rec_cpu, rec_cpu2, rec_ram, rec_gpu, rec_gpu2, rec_vram, rec_dx, rec_storage,
384            fetched_at
385        ) VALUES (
386            ?1,  ?2,  ?3,  ?4,
387            ?5,  ?6,  ?7,  ?8,  ?9,  ?10, ?11, ?12, ?13,
388            ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22,
389            ?23
390        )",
391    )?;
392
393    let mut paths_stmt = conn.prepare(
394        "INSERT INTO game_data_paths (steam_app_id, kind, os, raw_path, fetched_at)
395         VALUES (?1, ?2, ?3, ?4, ?5)",
396    )?;
397
398    let serialize_vec = |v: &Option<Vec<String>>| -> Option<String> {
399        v.as_ref().and_then(|list| serde_json::to_string(list).ok())
400    };
401
402    let now = Utc::now().to_rfc3339();
403
404    // Limpa dados anteriores das tabelas com múltiplas linhas por jogo antes de reinserir
405    conn.execute("DELETE FROM system_requirements", [])?;
406    conn.execute("DELETE FROM game_data_paths", [])?;
407
408    for extras in &backup.game_extras {
409        extras_stmt.execute(params![
410            extras.steam_app_id,
411            extras.pcgw_page_id,
412            extras.pcgw_page_name,
413            extras.engine,
414            extras.available_on,
415            extras.dx_versions,
416            extras.vulkan_versions,
417            extras.opengl_versions,
418            extras.win64,
419            extras.linux64,
420            extras.macos_arm,
421            extras.macos_intel64,
422            extras.ray_tracing,
423            extras.upscaling,
424            extras.frame_gen,
425            extras.ultrawidescreen,
426            extras.four_k_support,
427            extras.hdr,
428            extras.high_fps,
429            extras.fov,
430            extras.borderless_windowed,
431            extras.color_blind,
432            extras.controller_support,
433            extras.full_controller,
434            extras.playstation_controllers,
435            extras.xinput_controllers,
436            extras.surround_sound,
437            extras.subtitles,
438            extras.closed_captions,
439            extras.has_save_data,
440            extras.has_config_data,
441            serialize_vec(&extras.languages_interface),
442            serialize_vec(&extras.languages_audio),
443            serialize_vec(&extras.languages_subtitles),
444            extras.fetched_at,
445        ])?;
446    }
447
448    for req in &backup.system_requirements {
449        sysreq_stmt.execute(params![
450            req.steam_app_id,
451            req.os_family,
452            req.tier_title,
453            req.target,
454            req.min_os,
455            req.min_cpu,
456            req.min_cpu2,
457            req.min_ram,
458            req.min_gpu,
459            req.min_gpu2,
460            req.min_vram,
461            req.min_dx,
462            req.min_storage,
463            req.rec_os,
464            req.rec_cpu,
465            req.rec_cpu2,
466            req.rec_ram,
467            req.rec_gpu,
468            req.rec_gpu2,
469            req.rec_vram,
470            req.rec_dx,
471            req.rec_storage,
472            now,
473        ])?;
474    }
475
476    for path in &backup.game_data_paths {
477        paths_stmt.execute(params![
478            path.steam_app_id,
479            path.kind,
480            path.os,
481            path.raw_path,
482            now,
483        ])?;
484    }
485
486    conn.execute("COMMIT", [])?;
487
488    Ok(format!(
489        "Backup restaurado! {} jogos, {} detalhes, {} itens da wishlist, {} dados técnicos, {} requisitos de sistema e {} caminhos.",
490        backup.games.len(),
491        backup.game_details.len(),
492        backup.wishlist_game.len(),
493        backup.game_extras.len(),
494        backup.system_requirements.len(),
495        backup.game_data_paths.len(),
496    ))
497}
498
499/// Busca todos os jogos na biblioteca
500///
501/// Retorna um vetor de `Game` ou um erro AppError.
502fn fetch_games(conn: &rusqlite::Connection) -> Result<Vec<Game>, AppError> {
503    let mut stmt = conn.prepare(
504        "SELECT id, name, cover_url, platform, platform_game_id, installed, import_confidence, install_path, executable_path, launch_args, user_rating, favorite, status, playtime, last_played, added_at FROM games"
505    )?;
506
507    let game_iter = stmt.query_map([], |row| {
508        Ok(Game {
509            id: row.get(0)?,
510            name: row.get(1)?,
511            cover_url: row.get(2)?,
512            genres: None,
513            developer: None,
514            platform: row.get::<_, String>(3)?.parse().unwrap_or(Platform::Outra),
515            platform_game_id: row.get(4)?,
516            installed: row.get(5)?,
517            import_confidence: row
518                .get::<_, Option<String>>(6)?
519                .and_then(|s| s.parse().ok()),
520            install_path: row.get(7)?,
521            executable_path: row.get(8)?,
522            launch_args: row.get(9)?,
523            user_rating: row.get(10)?,
524            favorite: row.get(11)?,
525            status: row.get(12)?,
526            playtime: row.get(13)?,
527            last_played: row.get(14)?,
528            added_at: row.get(15)?,
529            is_adult: false,
530        })
531    })?;
532
533    Ok(game_iter.collect::<Result<Vec<_>, _>>()?)
534}
535
536/// Busca todos os detalhes dos jogos na biblioteca
537///
538/// Retorna um vetor de `GameDetails` ou um erro AppError.
539fn fetch_game_details(conn: &rusqlite::Connection) -> Result<Vec<GameDetails>, AppError> {
540    let mut stmt = conn.prepare(
541        "SELECT
542        game_id, steam_app_id, developer, publisher, release_date, genres, tags, series,
543        description_raw, description_ptbr, background_image, critic_score,
544        steam_review_label, steam_review_count, steam_review_score, steam_review_updated_at,
545        esrb_rating, is_adult, adult_tags, external_links, median_playtime,
546        estimated_playtime
547     FROM game_details",
548    )?;
549
550    let details_iter = stmt.query_map([], |row| {
551        // Deserializa JSON strings
552        let links_json: Option<String> = row.get(19)?;
553        let external_links = links_json.and_then(|s| serde_json::from_str(&s).ok());
554
555        let tags_json: Option<String> = row.get(6)?;
556        let tags = tags_json.map(|s| crate::database::deserialize_tags(&s));
557
558        Ok(GameDetails {
559            game_id: row.get(0)?,
560            steam_app_id: row.get(1)?,
561            developer: row.get(2)?,
562            publisher: row.get(3)?,
563            release_date: row.get(4)?,
564            genres: row.get(5)?,
565            tags,
566            series: row.get(7)?,
567            description_raw: row.get(8)?,
568            description_ptbr: row.get(9)?,
569            background_image: row.get(10)?,
570            critic_score: row.get(11)?,
571            steam_review_label: row.get(12)?,
572            steam_review_count: row.get(13)?,
573            steam_review_score: row.get(14)?,
574            steam_review_updated_at: row.get(15)?,
575            esrb_rating: row.get(16)?,
576            is_adult: row.get(17).unwrap_or(false),
577            adult_tags: row.get(18)?,
578            external_links,
579            median_playtime: row.get(20)?,
580            estimated_playtime: row.get(21)?,
581        })
582    })?;
583
584    Ok(details_iter.collect::<Result<Vec<_>, _>>()?)
585}
586
587/// Busca todos os jogos da wishlist
588///
589/// Retorna um vetor de `WishlistGame` ou um erro AppError.
590fn fetch_wishlist(conn: &rusqlite::Connection) -> Result<Vec<WishlistGame>, AppError> {
591    let mut stmt = conn.prepare(
592        "SELECT id, name, cover_url, store_url, store_platform, itad_id, current_price, normal_price, lowest_price, currency, on_sale, voucher, added_at FROM wishlist"
593    )?;
594
595    let wishlist_iter = stmt.query_map([], |row| {
596        Ok(WishlistGame {
597            id: row.get(0)?,
598            name: row.get(1)?,
599            cover_url: row.get(2)?,
600            store_url: row.get(3)?,
601            store_platform: row.get(4)?,
602            itad_id: row.get(5)?,
603            current_price: row.get(6)?,
604            normal_price: row.get(7)?,
605            lowest_price: row.get(8)?,
606            currency: row.get(9)?,
607            on_sale: row.get(10)?,
608            voucher: row.get(11)?,
609            added_at: row.get(12)?,
610        })
611    })?;
612
613    Ok(wishlist_iter.collect::<Result<Vec<_>, _>>()?)
614}
615
616/// Busca todos os dados técnicos do PCGamingWiki para backup.
617///
618/// Inclui apenas registros que já foram buscados (`fetched_at IS NOT NULL`).
619fn fetch_game_extras(conn: &rusqlite::Connection) -> Result<Vec<GameExtras>, AppError> {
620    let mut stmt = conn.prepare(
621        "SELECT
622            steam_app_id, pcgw_page_id, pcgw_page_name, engine,
623            available_on,
624            dx_versions, vulkan_versions, opengl_versions,
625            win64, linux64, macos_arm, macos_intel64,
626            ray_tracing, upscaling, frame_gen,
627            ultrawidescreen, four_k_support, hdr, high_fps, fov, borderless_windowed, color_blind,
628            controller_support, full_controller, playstation_controllers, xinput_controllers,
629            surround_sound, subtitles, closed_captions,
630            has_save_data, has_config_data,
631            languages_interface, languages_audio, languages_subtitles,
632            fetched_at
633         FROM game_extras
634         WHERE fetched_at IS NOT NULL",
635    )?;
636
637    let parse_json_vec = |s: Option<String>| -> Option<Vec<String>> {
638        s.and_then(|v| serde_json::from_str(&v).ok())
639    };
640
641    let iter = stmt.query_map([], |row| {
642        Ok(GameExtras {
643            steam_app_id: row.get(0)?,
644            pcgw_page_id: row.get(1)?,
645            pcgw_page_name: row.get(2)?,
646            engine: row.get(3)?,
647            available_on: row.get(4)?,
648            dx_versions: row.get(5)?,
649            vulkan_versions: row.get(6)?,
650            opengl_versions: row.get(7)?,
651            win64: row.get(8)?,
652            linux64: row.get(9)?,
653            macos_arm: row.get(10)?,
654            macos_intel64: row.get(11)?,
655            ray_tracing: row.get(12)?,
656            upscaling: row.get(13)?,
657            frame_gen: row.get(14)?,
658            ultrawidescreen: row.get(15)?,
659            four_k_support: row.get(16)?,
660            hdr: row.get(17)?,
661            high_fps: row.get(18)?,
662            fov: row.get(19)?,
663            borderless_windowed: row.get(20)?,
664            color_blind: row.get(21)?,
665            controller_support: row.get(22)?,
666            full_controller: row.get(23)?,
667            playstation_controllers: row.get(24)?,
668            xinput_controllers: row.get(25)?,
669            surround_sound: row.get(26)?,
670            subtitles: row.get(27)?,
671            closed_captions: row.get(28)?,
672            has_save_data: row.get(29)?,
673            has_config_data: row.get(30)?,
674            languages_interface: parse_json_vec(row.get(31)?),
675            languages_audio: parse_json_vec(row.get(32)?),
676            languages_subtitles: parse_json_vec(row.get(33)?),
677            fetched_at: row.get(34)?,
678        })
679    })?;
680
681    Ok(iter.collect::<Result<Vec<_>, _>>()?)
682}
683
684/// Busca todos os requisitos de sistema para backup.
685fn fetch_system_requirements(
686    conn: &rusqlite::Connection,
687) -> Result<Vec<SystemRequirements>, AppError> {
688    let mut stmt = conn.prepare(
689        "SELECT
690            steam_app_id, os_family, tier_title, target,
691            min_os, min_cpu, min_cpu2, min_ram, min_gpu, min_gpu2, min_vram, min_dx, min_storage,
692            rec_os, rec_cpu, rec_cpu2, rec_ram, rec_gpu, rec_gpu2, rec_vram, rec_dx, rec_storage
693         FROM system_requirements
694         ORDER BY steam_app_id, id ASC",
695    )?;
696
697    let iter = stmt.query_map([], |row| {
698        Ok(SystemRequirements {
699            steam_app_id: row.get(0)?,
700            os_family: row.get(1)?,
701            tier_title: row.get(2)?,
702            target: row.get(3)?,
703            min_os: row.get(4)?,
704            min_cpu: row.get(5)?,
705            min_cpu2: row.get(6)?,
706            min_ram: row.get(7)?,
707            min_gpu: row.get(8)?,
708            min_gpu2: row.get(9)?,
709            min_vram: row.get(10)?,
710            min_dx: row.get(11)?,
711            min_storage: row.get(12)?,
712            rec_os: row.get(13)?,
713            rec_cpu: row.get(14)?,
714            rec_cpu2: row.get(15)?,
715            rec_ram: row.get(16)?,
716            rec_gpu: row.get(17)?,
717            rec_gpu2: row.get(18)?,
718            rec_vram: row.get(19)?,
719            rec_dx: row.get(20)?,
720            rec_storage: row.get(21)?,
721        })
722    })?;
723
724    Ok(iter.collect::<Result<Vec<_>, _>>()?)
725}
726
727/// Busca todos os caminhos de game data para backup.
728fn fetch_game_data_paths(conn: &rusqlite::Connection) -> Result<Vec<GameDataPath>, AppError> {
729    let mut stmt = conn.prepare(
730        "SELECT steam_app_id, kind, os, raw_path
731         FROM game_data_paths
732         ORDER BY steam_app_id, id ASC",
733    )?;
734
735    let iter = stmt.query_map([], |row| {
736        Ok(GameDataPath {
737            steam_app_id: row.get(0)?,
738            kind: row.get(1)?,
739            os: row.get(2)?,
740            raw_path: row.get(3)?,
741            expanded_path: None,
742        })
743    })?;
744
745    Ok(iter.collect::<Result<Vec<_>, _>>()?)
746}