game_manager_lib/commands/platforms/
itch.rs1use 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
13async 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 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 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 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#[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}