game_manager_lib/commands/platforms/
indiegala.rs1use 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
13async 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(), 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#[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}