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::format_import_summary;
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, Emitter, State};
11use tracing::info;
12use uuid::Uuid;
13
14/// Persiste jogos da IndieGala nas tabelas `games` e `game_details`.
15///
16/// Difere de `persist_source_games` por também gravar `description_raw` e `tags`
17/// em `game_details`. Diferente da Legacy Games, aqui não há `cover_url`.
18///
19/// `playtime_minutes` é passado como `Option` (não `unwrap_or(0)`) pro `UPDATE`
20/// porque no modo `full`, jogos possuídos mas que não foram instalados não têm playtime
21/// conhecido — usar `COALESCE` preserva o valor real já salvo de uma importação anterior
22/// (ex: jogo que foi desinstalado depois de já ter sido jogado) em vez de zerar por engano.
23async fn persist_indiegala_games(
24    state: &AppState,
25    games: Vec<crate::sources::indiegala::IndiegalaGame>,
26) -> Result<(u32, u32), AppError> {
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
52            tx.execute(
53                "INSERT INTO games (
54                    id, name, cover_url, platform, platform_game_id,
55                    installed, status, playtime, last_played, added_at,
56                    favorite, user_rating, install_path, executable_path
57                ) VALUES (?1, ?2, NULL, ?3, ?4, ?5, ?6, ?7, NULL, ?8, 0, NULL, ?9, ?10)",
58                params![
59                    new_id,
60                    game.name.as_deref().unwrap_or("Unknown"),
61                    game.platform,
62                    game.platform_game_id,
63                    game.installed,
64                    status,
65                    game.playtime_minutes.unwrap_or(0),
66                    now,
67                    game.install_path,
68                    game.executable_path,
69                ],
70            )
71            .map_err(|e| AppError::DatabaseError(e.to_string()))?;
72
73            let tags_json = indiegala_game
74                .tags
75                .as_ref()
76                .and_then(|tags| crate::database::serialize_tags(tags).ok());
77
78            if indiegala_game.description_raw.is_some() || tags_json.is_some() {
79                tx.execute(
80                    "INSERT OR IGNORE INTO game_details (game_id, description_raw, tags)
81                     VALUES (?1, ?2, ?3)",
82                    params![new_id, indiegala_game.description_raw, tags_json],
83                )
84                .map_err(|e| AppError::DatabaseError(e.to_string()))?;
85            }
86
87            inserted += 1;
88        } else {
89            tx.execute(
90                "UPDATE games SET
91                    installed       = ?1,
92                    status          = ?2,
93                    playtime        = COALESCE(?3, playtime),
94                    install_path    = COALESCE(?4, install_path),
95                    executable_path = COALESCE(?5, executable_path)
96                 WHERE platform = ?6 AND platform_game_id = ?7",
97                params![
98                    game.installed,
99                    status,
100                    game.playtime_minutes, // Option<u32> cru — sem unwrap_or(0)
101                    game.install_path,
102                    game.executable_path,
103                    game.platform,
104                    game.platform_game_id,
105                ],
106            )
107            .map_err(|e| AppError::DatabaseError(e.to_string()))?;
108
109            updated += 1;
110        }
111    }
112
113    tx.commit()
114        .map_err(|e| AppError::DatabaseError(e.to_string()))?;
115
116    Ok((inserted, updated))
117}
118
119/// Importa jogos da IndieGala via IGClient.
120///
121/// `full=false` (padrão): só jogos instalados no momento, via `installed.json`.
122/// `full=true`: biblioteca completa de posse via `config.json` (`user_collection`),
123/// cruzada com `installed.json` pra marcar o que está instalado e reaproveitar metadados desses casos.
124///
125/// `installed_json_path`/`config_json_path` — caminhos customizados opcionais.
126/// Se omitidos, usam os caminhos padrão do Windows:
127/// - `installed.json`: `%APPDATA%\IGClient\storage\installed.json`
128/// - `config.json`: `%APPDATA%\IGClient\config.json`
129#[tauri::command]
130pub async fn import_indiegala_games(
131    app: AppHandle,
132    state: State<'_, AppState>,
133    full: bool,
134    installed_json_path: Option<String>,
135    config_json_path: Option<String>,
136) -> Result<String, AppError> {
137    let installed_path = installed_json_path
138        .filter(|s| !s.trim().is_empty())
139        .map(std::path::PathBuf::from);
140    let config_path = config_json_path
141        .filter(|s| !s.trim().is_empty())
142        .map(std::path::PathBuf::from);
143
144    let source = IndiegalaSource::new(installed_path);
145
146    let games = if full {
147        source.fetch_full_library_detailed(config_path).await?
148    } else {
149        source.fetch_installed_detailed().await?
150    };
151
152    if games.is_empty() {
153        let msg = if full {
154            "Nenhum jogo IndieGala encontrado na biblioteca."
155        } else {
156            "Nenhum jogo IndieGala instalado encontrado."
157        };
158        return Ok(msg.to_string());
159    }
160
161    let (inserted, updated) = persist_indiegala_games(&state, games).await?;
162    let message = format_import_summary("IndieGala", inserted, updated);
163    info!("{}", message);
164
165    let _ = app.emit("library_updated", ());
166
167    Ok(message)
168}