Skip to main content

game_manager_lib/commands/platforms/
indiegala.rs

1//! IndieGala - Importa jogos instalados ou biblioteca completa via IGClient
2
3use crate::commands::platforms::core::{spawn_import_custom, ImportOutcome, NewlyImportedGame};
4use crate::database::AppState;
5use crate::errors::AppError;
6use crate::sources::indiegala::IndiegalaSource;
7use crate::utils::status_logic;
8use chrono::Utc;
9use rusqlite::params;
10use tauri::{AppHandle, Manager};
11use uuid::Uuid;
12
13/// Persiste jogos da IndieGala nas tabelas `games` e `game_details`.
14///
15/// Difere de `persist_source_games` por também gravar `description_raw` e `tags`
16/// em `game_details`. Diferente da Legacy Games, aqui não há `cover_url`.
17///
18/// `playtime_minutes` é passado como `Option` (não `unwrap_or(0)`) pro `UPDATE`
19/// porque no modo `full`, jogos possuídos mas que não foram instalados não têm playtime
20/// conhecido — usar `COALESCE` preserva o valor real já salvo de uma importação anterior
21/// (ex: jogo que foi desinstalado depois de já ter sido jogado) em vez de zerar por engano.
22async fn persist_indiegala_games(
23    state: &AppState,
24    games: Vec<crate::sources::indiegala::IndiegalaGame>,
25) -> Result<(u32, u32, Vec<NewlyImportedGame>), AppError> {
26    let mut newly_imported = Vec::new();
27    let mut conn = state.games_db.lock().map_err(|_| AppError::MutexError)?;
28    let tx = conn
29        .transaction()
30        .map_err(|e| AppError::DatabaseError(e.to_string()))?;
31
32    let mut inserted = 0u32;
33    let mut updated = 0u32;
34    let now = Utc::now().to_rfc3339();
35
36    for indiegala_game in games {
37        let game = &indiegala_game.source;
38
39        let exists: bool = tx
40            .query_row(
41                "SELECT EXISTS(SELECT 1 FROM games WHERE platform = ?1 AND platform_game_id = ?2)",
42                params![&game.platform, &game.platform_game_id],
43                |row| row.get(0),
44            )
45            .unwrap_or(false);
46
47        let status = status_logic::calculate_status(game.playtime_minutes.unwrap_or(0) as i32);
48
49        if !exists {
50            let new_id = Uuid::new_v4().to_string();
51            let display_name = game.name.clone().unwrap_or_else(|| "Unknown".to_string());
52            let slug = crate::utils::text::slugify(&display_name);
53
54            tx.execute(
55                "INSERT INTO games (
56                        id, name, slug, cover_url, platform, platform_game_id,
57                        installed, status, playtime, playtime_source, last_played, added_at,
58                        favorite, user_rating, install_path, executable_path
59                    ) VALUES (?1, ?2, ?3, NULL, ?4, ?5, ?6, ?7, ?8, ?9, NULL, ?10, 0, NULL, ?11, ?12)",
60                params![
61                        new_id,
62                        display_name,
63                        slug,
64                        game.platform,
65                        game.platform_game_id,
66                        game.installed,
67                        status,
68                        game.playtime_minutes.unwrap_or(0),
69                        crate::models::PlaytimeSource::Platform(crate::models::Platform::Indiegala).as_db_str(), // <- novo
70                        now,
71                        game.install_path,
72                        game.executable_path,
73                    ],
74            )
75                .map_err(|e| AppError::DatabaseError(e.to_string()))?;
76
77            let tags_json = indiegala_game
78                .tags
79                .as_ref()
80                .and_then(|tags| crate::database::serialize_tags(tags).ok());
81
82            if indiegala_game.description_raw.is_some() || tags_json.is_some() {
83                tx.execute(
84                    "INSERT OR IGNORE INTO game_details (game_id, description_raw, tags)
85                     VALUES (?1, ?2, ?3)",
86                    params![new_id, indiegala_game.description_raw, tags_json],
87                )
88                    .map_err(|e| AppError::DatabaseError(e.to_string()))?;
89            }
90
91            newly_imported.push(NewlyImportedGame {
92                game_id: new_id,
93                name: display_name,
94                platform: game.platform.clone(),
95                platform_game_id: game.platform_game_id.clone(),
96            });
97
98            inserted += 1;
99        } else {
100            tx.execute(
101                "UPDATE games SET
102                        installed       = ?1,
103                        status          = ?2,
104                        playtime        = COALESCE(?3, playtime),
105                        playtime_source = CASE WHEN ?3 IS NOT NULL THEN ?4 ELSE playtime_source END,
106                        install_path    = COALESCE(?5, install_path),
107                        executable_path = COALESCE(?6, executable_path)
108                    WHERE platform = ?7 AND platform_game_id = ?8",
109                params![
110                        game.installed,
111                        status,
112                        game.playtime_minutes,
113                        crate::models::PlaytimeSource::Platform(crate::models::Platform::Indiegala).as_db_str(),
114                        game.install_path,
115                        game.executable_path,
116                        game.platform,
117                        game.platform_game_id,
118                ],
119            )
120                .map_err(|e| AppError::DatabaseError(e.to_string()))?;
121
122            updated += 1;
123        }
124    }
125
126    tx.commit()
127        .map_err(|e| AppError::DatabaseError(e.to_string()))?;
128
129    Ok((inserted, updated, newly_imported))
130}
131
132/// Importa jogos da IndieGala via IGClient.
133///
134/// `full=false` (padrão): só jogos instalados no momento, via `installed.json`.
135/// `full=true`: biblioteca completa de posse via `config.json` (`user_collection`),
136/// cruzada com `installed.json` pra marcar o que está instalado e reaproveitar metadados desses casos.
137///
138/// `installed_json_path`/`config_json_path` — caminhos customizados opcionais.
139/// Se omitidos, usam os caminhos padrão do Windows:
140/// - `installed.json`: `%APPDATA%\IGClient\storage\installed.json`
141/// - `config.json`: `%APPDATA%\IGClient\config.json`
142#[tauri::command]
143pub async fn import_indiegala_games(
144    app: AppHandle,
145    full: bool,
146    installed_json_path: Option<String>,
147    config_json_path: Option<String>,
148) -> Result<(), AppError> {
149    let installed_path = installed_json_path
150        .filter(|s| !s.trim().is_empty())
151        .map(std::path::PathBuf::from);
152    let config_path = config_json_path
153        .filter(|s| !s.trim().is_empty())
154        .map(std::path::PathBuf::from);
155
156    spawn_import_custom(app, "Indiegala", move |app| async move {
157        let state: tauri::State<crate::database::AppState> = app.state();
158        let source = IndiegalaSource::new(installed_path);
159
160        let games = if full {
161            source.fetch_full_library_detailed(config_path).await?
162        } else {
163            source.fetch_installed_detailed().await?
164        };
165
166        if games.is_empty() {
167            return Ok(ImportOutcome::Empty);
168        }
169
170        let (inserted, updated, newly_imported) = persist_indiegala_games(&state, games).await?;
171        Ok(ImportOutcome::Persisted {
172            inserted,
173            updated,
174            newly_imported,
175        })
176    });
177
178    Ok(())
179}