1use crate::constants;
6use crate::database::AppState;
7use crate::errors::AppError;
8use crate::sources::providers::SourceGame;
9use crate::sources::scanner::GameDiscovery;
10use crate::utils::status_logic;
11use chrono::{TimeZone, Utc};
12use rusqlite::params;
13use serde::{Deserialize, Serialize};
14use tauri::{AppHandle, Emitter, Manager};
15use tracing::{info, warn};
16use uuid::Uuid;
17
18#[derive(Serialize, Deserialize, Debug)]
21pub struct ScanResult {
22 pub success: bool,
23 pub message: String,
24 pub discoveries: Vec<GameDiscovery>,
25}
26
27#[derive(Deserialize)]
28#[serde(rename_all = "camelCase")]
29pub struct ScanGameInput {
30 pub name: String,
31 pub executable_path: String,
32 pub base_path: String,
33}
34
35#[derive(Debug, Clone)]
37pub struct NewlyImportedGame {
38 pub game_id: String,
39 pub name: String,
40 pub platform: String,
41 pub platform_game_id: String,
42}
43
44pub(crate) async fn persist_source_games(
50 state: &AppState,
51 games: Vec<crate::sources::providers::SourceGame>,
52) -> Result<(u32, u32, Vec<NewlyImportedGame>), AppError> {
53 let mut conn = state.games_db.lock().map_err(|_| AppError::MutexError)?;
54
55 let tx = conn
57 .transaction()
58 .map_err(|e| AppError::DatabaseError(e.to_string()))?;
59
60 let mut inserted = 0;
61 let mut updated = 0;
62 let mut newly_imported = Vec::new();
63 let now = Utc::now().to_rfc3339();
64
65 for game in games {
66 let exists: bool = tx
68 .query_row(
69 "SELECT EXISTS(SELECT 1 FROM games WHERE platform = ?1 AND platform_game_id = ?2)",
70 params![&game.platform, &game.platform_game_id],
71 |row| row.get(0),
72 )
73 .unwrap_or(false);
74
75 let status = status_logic::calculate_status(game.playtime_minutes.unwrap_or(0) as i32);
76
77 let last_played_iso = game.last_played.and_then(|ts| {
78 if ts > 0 {
79 Some(Utc.timestamp_opt(ts, 0).single().map(|dt| dt.to_rfc3339()))
80 } else {
81 None
82 }
83 });
84
85 let is_official_playtime_platform = game.platform == "Steam";
86
87 if !exists {
88 let new_id = Uuid::new_v4().to_string();
89 let display_name = game.name.clone().unwrap_or_else(|| "Unknown".to_string());
90 let slug = crate::utils::text::slugify(&display_name);
91
92 let cover_url = if game.platform == "Steam" {
93 Some(format!(
94 "{}/{}",
95 constants::STEAM_CDN_URL,
96 constants::STEAM_LIBRARY_IMAGE_PATH.replace("{}", &game.platform_game_id)
97 ))
98 } else {
99 None
100 };
101
102 let playtime_source = if is_official_playtime_platform {
105 Some(
106 crate::models::PlaytimeSource::Platform(crate::models::Platform::Steam)
107 .as_db_str(),
108 )
109 } else {
110 None
111 };
112
113 tx.execute(
114 "INSERT INTO games (
115 id, name, slug, cover_url, platform, platform_game_id,
116 installed, status, playtime, playtime_source, last_played, added_at,
117 favorite, user_rating, install_path, executable_path
118 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, 0, NULL, ?13, ?14)",
119 params![
120 new_id,
121 display_name,
122 slug,
123 cover_url,
124 game.platform,
125 game.platform_game_id,
126 game.installed,
127 status,
128 game.playtime_minutes.unwrap_or(0),
129 playtime_source,
130 last_played_iso,
131 now,
132 game.install_path,
133 game.executable_path,
134 ],
135 )
136 .map_err(|e| AppError::DatabaseError(e.to_string()))?;
137
138 newly_imported.push(NewlyImportedGame {
139 game_id: new_id,
140 name: display_name,
141 platform: game.platform.clone(),
142 platform_game_id: game.platform_game_id.clone(),
143 });
144
145 inserted += 1;
146 } else {
147 if is_official_playtime_platform {
148 tx.execute(
150 "UPDATE games SET
151 installed = ?1,
152 status = ?2,
153 playtime = ?3,
154 playtime_source = ?4,
155 last_played = ?5,
156 install_path = COALESCE(?6, install_path),
157 executable_path = COALESCE(?7, executable_path)
158 WHERE platform = ?8 AND platform_game_id = ?9",
159 params![
160 game.installed,
161 status,
162 game.playtime_minutes.unwrap_or(0),
163 crate::models::PlaytimeSource::Platform(crate::models::Platform::Steam)
164 .as_db_str(),
165 last_played_iso,
166 game.install_path,
167 game.executable_path,
168 game.platform,
169 game.platform_game_id
170 ],
171 )
172 .map_err(|e| AppError::DatabaseError(e.to_string()))?;
173 } else {
174 tx.execute(
177 "UPDATE games SET
178 installed = ?1,
179 status = ?2,
180 last_played = ?3,
181 install_path = COALESCE(?4, install_path),
182 executable_path = COALESCE(?5, executable_path)
183 WHERE platform = ?6 AND platform_game_id = ?7",
184 params![
185 game.installed,
186 status,
187 last_played_iso,
188 game.install_path,
189 game.executable_path,
190 game.platform,
191 game.platform_game_id
192 ],
193 )
194 .map_err(|e| AppError::DatabaseError(e.to_string()))?;
195 }
196
197 updated += 1;
198 }
199 }
200
201 tx.commit()
203 .map_err(|e| AppError::DatabaseError(e.to_string()))?;
204
205 Ok((inserted, updated, newly_imported))
206}
207
208pub fn trigger_enrichment_if_needed(app: &AppHandle, newly_imported: Vec<NewlyImportedGame>) {
210 if !newly_imported.is_empty() {
211 let app_clone = app.clone();
212 tauri::async_runtime::spawn(async move {
213 crate::commands::metadata::enrichment::enrich_newly_imported(app_clone, newly_imported)
214 .await;
215 });
216 }
217}
218
219pub fn spawn_import<F, Fut>(app: AppHandle, platform: &'static str, fetch: F)
232where
233 F: FnOnce(AppHandle) -> Fut + Send + 'static,
234 Fut: std::future::Future<Output=Result<Vec<SourceGame>, AppError>> + Send + 'static,
235{
236 let _ = app.emit("import_started", platform);
237 let app_task = app.clone();
238 let app_for_fetch = app.clone();
239
240 tauri::async_runtime::spawn(async move {
241 let games = match fetch(app_for_fetch).await {
242 Ok(g) => g,
243 Err(e) => {
244 warn!("Import {} falhou: {}", platform, e);
245 let _ = app_task.emit("import_error", (platform, e.to_string()));
246 return;
247 }
248 };
249
250 if games.is_empty() {
251 let msg = format_import_empty(platform);
252 let _ = app_task.emit("import_complete", (platform, msg));
253 return;
254 }
255
256 let state: tauri::State<AppState> = app_task.state();
257 let (inserted, updated, newly_imported) = match persist_source_games(&state, games).await {
258 Ok(r) => r,
259 Err(e) => {
260 warn!("Persist {} falhou: {}", platform, e);
261 let _ = app_task.emit("import_error", (platform, e.to_string()));
262 return;
263 }
264 };
265
266 let message = format_import_summary(platform, inserted, updated);
267 info!("{}", message);
268
269 let _ = app_task.emit("library_updated", ());
270 let _ = app_task.emit("import_complete", (platform, message));
271
272 trigger_enrichment_if_needed(&app_task, newly_imported);
273 });
274}
275
276pub enum ImportOutcome {
280 Empty,
281 Persisted {
282 inserted: u32,
283 updated: u32,
284 newly_imported: Vec<NewlyImportedGame>,
285 },
286}
287
288pub fn spawn_import_custom<F, Fut>(app: AppHandle, platform: &'static str, run: F)
291where
292 F: FnOnce(AppHandle) -> Fut + Send + 'static,
293 Fut: std::future::Future<Output=Result<ImportOutcome, AppError>> + Send + 'static,
294{
295 let _ = app.emit("import_started", platform);
296 let app_task = app.clone();
297 let app_for_run = app.clone();
298
299 tauri::async_runtime::spawn(async move {
300 match run(app_for_run).await {
301 Ok(ImportOutcome::Empty) => {
302 let msg = format_import_empty(platform);
303 let _ = app_task.emit("import_complete", (platform, msg));
304 }
305 Ok(ImportOutcome::Persisted {
306 inserted,
307 updated,
308 newly_imported,
309 }) => {
310 let message = format_import_summary(platform, inserted, updated);
311 info!("{}", message);
312 let _ = app_task.emit("library_updated", ());
313 let _ = app_task.emit("import_complete", (platform, message));
314 trigger_enrichment_if_needed(&app_task, newly_imported);
315 }
316 Err(e) => {
317 warn!("Import {} falhou: {}", platform, e);
318 let _ = app_task.emit("import_error", (platform, e.to_string()));
319 }
320 }
321 });
322}
323
324pub fn format_import_summary(platform: &str, inserted: u32, updated: u32) -> String {
328 format!("{platform}: {inserted} adicionados, {updated} atualizados")
329}
330
331pub fn format_import_empty(platform: &str) -> String {
333 format!("Nenhum jogo {platform} encontrado.")
334}
335
336pub fn format_login_success(platform: &str) -> String {
338 format!("Conta {platform} conectada com sucesso!")
339}