Skip to main content

game_manager_lib/commands/platforms/
itch.rs

1//! Itch.io - Importa jogos instalados ou biblioteca completa lendo o butler.db
2
3use crate::commands::platforms::core::{spawn_import_custom, ImportOutcome, NewlyImportedGame};
4use crate::database::AppState;
5use crate::errors::AppError;
6use crate::sources::itch::{ItchioGame, ItchioSource};
7use crate::utils::status_logic;
8use chrono::{TimeZone, Utc};
9use rusqlite::params;
10use tauri::{AppHandle, Manager};
11use uuid::Uuid;
12
13/// Persiste jogos da Itch.io nas tabelas `games` e `game_details`.
14///
15/// Grava `cover_url` diretamente na tabela `games` e envia `description_raw` para a tabela `game_details`.
16async fn persist_itch_games(
17    state: &AppState,
18    games: Vec<ItchioGame>,
19) -> Result<(u32, u32, Vec<NewlyImportedGame>), AppError> {
20    let mut newly_imported = Vec::new();
21    let mut conn = state.games_db.lock().map_err(|_| AppError::MutexError)?;
22    let tx = conn
23        .transaction()
24        .map_err(|e| AppError::DatabaseError(e.to_string()))?;
25
26    let mut inserted = 0u32;
27    let mut updated = 0u32;
28    let now = Utc::now().to_rfc3339();
29
30    for itchio_game in games {
31        let game = &itchio_game.source;
32
33        let exists: bool = tx
34            .query_row(
35                "SELECT EXISTS(SELECT 1 FROM games WHERE platform = ?1 AND platform_game_id = ?2)",
36                params![&game.platform, &game.platform_game_id],
37                |row| row.get(0),
38            )
39            .unwrap_or(false);
40
41        let status = status_logic::calculate_status(game.playtime_minutes.unwrap_or(0) as i32);
42
43        // O app itch salva o timestamp em unix (segundos). Convertendo para ISO 8601
44        let last_played_iso = game.last_played.and_then(|ts| {
45            if ts > 0 {
46                Utc.timestamp_opt(ts, 0).single().map(|dt| dt.to_rfc3339())
47            } else {
48                None
49            }
50        });
51
52        if !exists {
53            let new_id = Uuid::new_v4().to_string();
54            let display_name = game.name.clone().unwrap_or_else(|| "Unknown".to_string());
55            let slug = crate::utils::text::slugify(&display_name);
56
57            tx.execute(
58                "INSERT INTO games (
59                            id, name, slug, cover_url, platform, platform_game_id,
60                            installed, status, playtime, playtime_source, last_played, added_at,
61                            favorite, user_rating, install_path, executable_path
62                    ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, 0, NULL, ?13, ?14)",
63                params![
64                        new_id,
65                        display_name,
66                        slug,
67                        itchio_game.cover_url,
68                        game.platform,
69                        game.platform_game_id,
70                        game.installed,
71                        status,
72                        game.playtime_minutes.unwrap_or(0),
73                        crate::models::PlaytimeSource::Platform(crate::models::Platform::Itch).as_db_str(),
74                        last_played_iso,
75                        now,
76                        game.install_path,
77                        game.executable_path,
78                    ],
79            )
80                .map_err(|e| AppError::DatabaseError(e.to_string()))?;
81
82            // Salva a descrição (se existir) na tabela de detalhes
83            if itchio_game.description_raw.is_some() {
84                tx.execute(
85                    "INSERT OR IGNORE INTO game_details (game_id, description_raw)
86                     VALUES (?1, ?2)",
87                    params![new_id, itchio_game.description_raw],
88                )
89                    .map_err(|e| AppError::DatabaseError(e.to_string()))?;
90            }
91
92            newly_imported.push(NewlyImportedGame {
93                game_id: new_id,
94                name: display_name,
95                platform: game.platform.clone(),
96                platform_game_id: game.platform_game_id.clone(),
97            });
98
99            inserted += 1;
100        } else {
101            tx.execute(
102                "UPDATE games SET
103                    installed       = ?1,
104                    status          = ?2,
105                    playtime        = COALESCE(?3, playtime),
106                    playtime_source = CASE WHEN ?3 IS NOT NULL THEN ?4 ELSE playtime_source END,
107                    last_played     = COALESCE(?5, last_played),
108                    install_path    = COALESCE(?6, install_path),
109                    executable_path = COALESCE(?7, executable_path),
110                    cover_url       = COALESCE(?8, cover_url)
111                WHERE platform = ?9 AND platform_game_id = ?10",
112                params![
113                    game.installed,
114                    status,
115                    game.playtime_minutes,
116                    crate::models::PlaytimeSource::Platform(crate::models::Platform::Itch)
117                        .as_db_str(),
118                    last_played_iso,
119                    game.install_path,
120                    game.executable_path,
121                    itchio_game.cover_url,
122                    game.platform,
123                    game.platform_game_id,
124                ],
125            )
126                .map_err(|e| AppError::DatabaseError(e.to_string()))?;
127
128            // Atualiza a descrição caso o jogo já exista e não tenha (ou tenha mudado)
129            if let Some(desc) = &itchio_game.description_raw {
130                tx.execute(
131                    "INSERT INTO game_details (game_id, description_raw)
132                     VALUES ((SELECT id FROM games WHERE platform = ?1 AND platform_game_id = ?2), ?3)
133                     ON CONFLICT(game_id) DO UPDATE SET description_raw = excluded.description_raw",
134                    params![game.platform, game.platform_game_id, desc],
135                ).map_err(|e| AppError::DatabaseError(e.to_string()))?;
136            }
137
138            updated += 1;
139        }
140    }
141
142    tx.commit()
143        .map_err(|e| AppError::DatabaseError(e.to_string()))?;
144
145    Ok((inserted, updated, newly_imported))
146}
147
148/// Importa jogos da Itch.io lendo o butler.db.
149///
150/// `full=false` (padrão): apenas jogos efetivamente instalados localmente.
151/// `full=true`: biblioteca completa de posse do usuário na plataforma.
152///
153/// `butler_db_path`: caminho customizado opcional caso o usuário use o app itch de forma portátil ou em um local não-padrão.
154#[tauri::command]
155pub async fn import_itch_games(
156    app: AppHandle,
157    full: bool,
158    butler_db_path: Option<String>,
159) -> Result<(), AppError> {
160    let db_path = butler_db_path
161        .filter(|s| !s.trim().is_empty())
162        .map(std::path::PathBuf::from);
163
164    spawn_import_custom(app, "Itch", move |app| async move {
165        let state: tauri::State<crate::database::AppState> = app.state();
166        let source = ItchioSource::new(db_path);
167
168        let games = if full {
169            source.fetch_full_library_detailed().await?
170        } else {
171            source.fetch_installed_detailed().await?
172        };
173
174        if games.is_empty() {
175            return Ok(ImportOutcome::Empty);
176        }
177
178        let (inserted, updated, newly_imported) = persist_itch_games(&state, games).await?;
179        Ok(ImportOutcome::Persisted {
180            inserted,
181            updated,
182            newly_imported,
183        })
184    });
185
186    Ok(())
187}