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::format_import_summary;
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, Emitter, State};
11use tracing::info;
12use uuid::Uuid;
13
14/// Persiste jogos da Itch.io nas tabelas `games` e `game_details`.
15///
16/// Grava `cover_url` diretamente na tabela `games` e envia `description_raw` para a tabela `game_details`.
17async fn persist_itch_games(
18    state: &AppState,
19    games: Vec<ItchioGame>,
20) -> Result<(u32, u32), AppError> {
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
55            tx.execute(
56                "INSERT INTO games (
57                    id, name, cover_url, platform, platform_game_id,
58                    installed, status, playtime, last_played, added_at,
59                    favorite, user_rating, install_path, executable_path
60                ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, 0, NULL, ?11, ?12)",
61                params![
62                    new_id,
63                    game.name.as_deref().unwrap_or("Unknown"),
64                    itchio_game.cover_url, // Capa oficial da API da itch
65                    game.platform,
66                    game.platform_game_id,
67                    game.installed,
68                    status,
69                    game.playtime_minutes.unwrap_or(0),
70                    last_played_iso,
71                    now,
72                    game.install_path,
73                    game.executable_path,
74                ],
75            )
76            .map_err(|e| AppError::DatabaseError(e.to_string()))?;
77
78            // Salva a descrição (se existir) na tabela de detalhes
79            if itchio_game.description_raw.is_some() {
80                tx.execute(
81                    "INSERT OR IGNORE INTO game_details (game_id, description_raw)
82                     VALUES (?1, ?2)",
83                    params![new_id, itchio_game.description_raw],
84                )
85                .map_err(|e| AppError::DatabaseError(e.to_string()))?;
86            }
87
88            inserted += 1;
89        } else {
90            tx.execute(
91                "UPDATE games SET
92                    installed       = ?1,
93                    status          = ?2,
94                    playtime        = COALESCE(?3, playtime),
95                    last_played     = COALESCE(?4, last_played),
96                    install_path    = COALESCE(?5, install_path),
97                    executable_path = COALESCE(?6, executable_path),
98                    cover_url       = COALESCE(?7, cover_url)
99                 WHERE platform = ?8 AND platform_game_id = ?9",
100                params![
101                    game.installed,
102                    status,
103                    game.playtime_minutes,
104                    last_played_iso,
105                    game.install_path,
106                    game.executable_path,
107                    itchio_game.cover_url,
108                    game.platform,
109                    game.platform_game_id,
110                ],
111            )
112            .map_err(|e| AppError::DatabaseError(e.to_string()))?;
113
114            // Atualiza a descrição caso o jogo já exista e não tenha (ou tenha mudado)
115            if let Some(desc) = &itchio_game.description_raw {
116                tx.execute(
117                    "INSERT INTO game_details (game_id, description_raw)
118                     VALUES ((SELECT id FROM games WHERE platform = ?1 AND platform_game_id = ?2), ?3)
119                     ON CONFLICT(game_id) DO UPDATE SET description_raw = excluded.description_raw",
120                    params![game.platform, game.platform_game_id, desc],
121                ).map_err(|e| AppError::DatabaseError(e.to_string()))?;
122            }
123
124            updated += 1;
125        }
126    }
127
128    tx.commit()
129        .map_err(|e| AppError::DatabaseError(e.to_string()))?;
130
131    Ok((inserted, updated))
132}
133
134/// Importa jogos da Itch.io lendo o butler.db.
135///
136/// `full=false` (padrão): apenas jogos efetivamente instalados localmente.
137/// `full=true`: biblioteca completa de posse do usuário na plataforma.
138///
139/// `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.
140#[tauri::command]
141pub async fn import_itch_games(
142    app: AppHandle,
143    state: State<'_, AppState>,
144    full: bool,
145    butler_db_path: Option<String>,
146) -> Result<String, AppError> {
147    let db_path = butler_db_path
148        .filter(|s| !s.trim().is_empty())
149        .map(std::path::PathBuf::from);
150
151    let source = ItchioSource::new(db_path);
152
153    let games = if full {
154        source.fetch_full_library_detailed().await?
155    } else {
156        source.fetch_installed_detailed().await?
157    };
158
159    if games.is_empty() {
160        let msg = if full {
161            "Nenhum jogo Itch.io encontrado na biblioteca."
162        } else {
163            "Nenhum jogo Itch.io instalado encontrado."
164        };
165        return Ok(msg.to_string());
166    }
167
168    let (inserted, updated) = persist_itch_games(&state, games).await?;
169    let message = format_import_summary("Itch.io", inserted, updated);
170    info!("{}", message);
171
172    let _ = app.emit("library_updated", ());
173
174    Ok(message)
175}