1use std::rc::Rc;
2
3use rusqlite::types::{ToSqlOutput, Value};
4use rusqlite::{params, Connection, ToSql};
5use serde::{Deserialize, Serialize};
6use snafu::ResultExt;
7use uuid::Uuid;
8
9use crate::game::GameRedirect;
10use crate::{error, game, tag, tag_category};
11use crate::error::Result;
12use crate::game::search::mark_index_dirty;
13use crate::platform;
14
15#[derive(Debug, Clone)]
16pub struct SqlVec<T> (pub Vec<T>);
17
18impl ToSql for SqlVec<i64> {
19 fn to_sql(&self) -> std::result::Result<ToSqlOutput<'_>, rusqlite::Error> {
20 let v = Rc::new(self.0.iter().map(|v| Value::from(v.clone())).collect::<Vec<Value>>());
21 Ok(ToSqlOutput::Array(v))
22 }
23}
24
25impl ToSql for SqlVec<String> {
26 fn to_sql(&self) -> std::result::Result<ToSqlOutput<'_>, rusqlite::Error> {
27 let v = Rc::new(self.0.iter().map(|v| Value::from(v.clone())).collect::<Vec<Value>>());
28 Ok(ToSqlOutput::Array(v))
29 }
30}
31
32#[cfg_attr(feature = "napi", napi(object))]
33#[derive(Debug, Clone, Deserialize, Serialize)]
34pub struct RemoteDeletedGamesRes {
35 pub games: Vec<RemoteDeletedGame>,
36}
37
38#[cfg_attr(feature = "napi", napi(object))]
39#[derive(Debug, Clone, Deserialize, Serialize)]
40pub struct RemoteDeletedGame {
41 pub id: String,
42 pub date_modified: String,
43 pub reason: String,
44}
45
46#[cfg_attr(feature = "napi", napi(object))]
47#[derive(Debug, Clone, Deserialize, Serialize)]
48pub struct RemoteGamesRes {
49 pub games: Vec<RemoteGame>,
50 pub add_apps: Vec<RemoteAddApp>,
51 pub game_data: Vec<RemoteGameData>,
52 pub tag_relations: Vec<Vec<String>>,
53 pub platform_relations: Vec<Vec<String>>,
54}
55
56#[cfg_attr(feature = "napi", napi(object))]
57#[derive(Debug, Clone, Deserialize, Serialize)]
58pub struct RemoteGameData {
59 pub game_id: String,
60 pub title: String,
61 pub date_added: String,
62 pub sha_256: String,
63 pub crc_32: u32,
64 pub size: i64,
65 pub parameters: Option<String>,
66 pub application_path: String,
67 pub launch_command: String,
68}
69
70#[cfg_attr(feature = "napi", napi(object))]
71#[derive(Debug, Clone, Deserialize, Serialize)]
72pub struct RemoteAddApp {
73 pub name: String,
74 pub application_path: String,
75 pub launch_command: String,
76 pub wait_for_exit: bool,
77 pub auto_run_before: bool,
78 pub parent_game_id: String,
79}
80
81#[cfg_attr(feature = "napi", napi(object))]
82#[derive(Debug, Clone, Deserialize, Serialize)]
83pub struct RemoteGame {
84 pub id: String,
85 pub title: String,
86 pub alternate_titles: String,
87 pub series: String,
88 pub developer: String,
89 pub publisher: String,
90 pub date_added: String,
91 pub date_modified: String,
92 pub play_mode: String,
93 pub status: String,
94 pub notes: String,
95 pub source: String,
96 pub application_path: String,
97 pub launch_command: String,
98 pub release_date: String,
99 pub version: String,
100 pub original_description: String,
101 pub language: String,
102 pub library: String,
103 pub platform_name: String,
104 pub archive_state: i32,
105 pub logo_path: String,
106 pub screenshot_path: String,
107 pub ruffle_support: String,
108}
109
110#[cfg_attr(feature = "napi", napi(object))]
111#[derive(Debug, Clone, Deserialize, Serialize)]
112pub struct RemoteCategory {
113 pub id: i64,
114 pub name: String,
115 pub color: String,
116 pub description: String,
117}
118
119#[cfg_attr(feature = "napi", napi(object))]
120#[derive(Debug, Clone, Deserialize, Serialize)]
121pub struct RemoteTag {
122 pub id: i64,
123 pub name: String,
124 pub description: String,
125 pub category: String,
126 pub date_modified: String,
127 pub aliases: Vec<String>,
128 pub deleted: bool,
129}
130
131#[cfg_attr(feature = "napi", napi(object))]
132#[derive(Debug, Clone, Deserialize, Serialize)]
133pub struct RemotePlatform {
134 pub id: i64,
135 pub name: String,
136 pub description: String,
137 pub date_modified: String,
138 pub aliases: Vec<String>,
139 pub deleted: bool,
140}
141
142#[derive(Debug, Deserialize, Serialize)]
143pub struct Alias {
144 id: i64,
145 value: String,
146}
147
148pub fn apply_platforms(conn: &Connection, platforms: Vec<RemotePlatform>) -> Result<()> {
149 rusqlite::vtab::array::load_module(conn).context(error::SqliteSnafu)?;
151
152 let changed_aliases: Vec<Alias> = platforms.iter()
154 .flat_map(|cur| cur.aliases.iter().map(move |alias| Alias { id: cur.id, value: alias.clone() }))
155 .collect();
156
157 let existing_platforms = platform::find(conn).context(error::SqliteSnafu)?;
158 let existing_ids: std::collections::HashSet<i64> = existing_platforms.iter().map(|p| p.id).collect();
159
160 let changed_alias_names = SqlVec(changed_aliases.iter().map(|a| a.value.clone()).collect::<Vec<String>>());
162 conn.execute("DELETE FROM platform_alias WHERE name IN rarray(?)", params![changed_alias_names]).context(error::SqliteSnafu)?;
163
164 let mut update_platform_stmt = conn.prepare("UPDATE platform SET dateModified = ?, primaryAliasId = (SELECT id FROM platform_alias WHERE name = ?), description = ? WHERE id = ?").context(error::SqliteSnafu)?;
165 let mut insert_platform_stmt = conn.prepare("INSERT INTO platform (id, dateModified, primaryAliasId, description) VALUES (?, ?, (SELECT id FROM platform_alias WHERE name = ?), ?)").context(error::SqliteSnafu)?;
166 let mut delete_platform_alias_stmt = conn.prepare("DELETE FROM platform_alias WHERE platformId = ?").context(error::SqliteSnafu)?;
167 let mut delete_platform_stmt = conn.prepare("DELETE FROM platform WHERE id = ?").context(error::SqliteSnafu)?;
168
169 let mut insert_alias_stmt = conn.prepare("INSERT INTO platform_alias (platformId, name) VALUES (?, ?)").context(error::SqliteSnafu)?;
171 for alias in changed_aliases {
172 insert_alias_stmt.execute(params![alias.id, alias.value]).context(error::SqliteSnafu)?;
173 }
174
175 let deleted_platform_ids = SqlVec(platforms.iter().filter(|p| existing_ids.contains(&p.id) && p.deleted).map(|p| p.id).collect::<Vec<i64>>());
177 conn.execute("UPDATE game
179 SET platformsStr = (
180 SELECT IFNULL(string_agg(pa.name, '; '), '')
181 FROM game_platforms_platform gpp
182 JOIN platform p ON gpp.platformId = p.id
183 JOIN platform_alias pa ON p.primaryAliasId = pa.id
184 WHERE gpp.gameId = game.id AND p.id NOT IN rarray(?)
185 ) WHERE game.id IN (
186 SELECT gameId FROM game_platforms_platform WHERE platformId IN rarray(?)
187 )", params![deleted_platform_ids, deleted_platform_ids]).context(error::SqliteSnafu)?;
188 conn.execute("UPDATE game
190 SET platformName = 'BROKEN'
191 WHERE platformName IN (
192 SELECT name FROM platform_alias WHERE platformId IN rarray(?)
193 )", params![deleted_platform_ids]).context(error::SqliteSnafu)?;
194 conn.execute("DELETE FROM game_platforms_platform WHERE platformId IN rarray(?)", params![deleted_platform_ids]).context(error::SqliteSnafu)?;
196 conn.execute("DELETE FROM platform_alias WHERE platformId IN rarray(?)", params![deleted_platform_ids]).context(error::SqliteSnafu)?;
197 conn.execute("DELETE FROM platform WHERE id IN rarray(?)", params![deleted_platform_ids]).context(error::SqliteSnafu)?;
198
199 for platform in platforms.iter().filter(|p| existing_ids.contains(&p.id) && !p.deleted) {
201 update_platform_stmt.execute(params![platform.date_modified, platform.name, platform.description, platform.id]).context(error::SqliteSnafu)?;
202 }
203
204 for platform in platforms.iter().filter(|p| !existing_ids.contains(&p.id) && !p.deleted) {
206 delete_platform_alias_stmt.execute(params![platform.id]).context(error::SqliteSnafu)?;
208 delete_platform_stmt.execute(params![platform.id]).context(error::SqliteSnafu)?;
209
210 for alias in &platform.aliases {
212 insert_alias_stmt.execute(params![platform.id, &alias]).context(error::SqliteSnafu)?;
213 }
214 insert_platform_stmt.execute(params![platform.id, platform.date_modified, platform.name, platform.description]).context(error::SqliteSnafu)?;
215 }
216
217 Ok(())
218}
219
220pub fn apply_categories(conn: &Connection, categories: Vec<RemoteCategory>) -> Result<()> {
221 let existing_categories = tag_category::find(conn).context(error::SqliteSnafu)?;
222 let existing_ids: std::collections::HashSet<i64> = existing_categories.iter().map(|p| p.id).collect();
223
224 let mut update_stmt = conn.prepare("UPDATE tag_category SET description = ?, color = ?, name = ? WHERE id = ?").context(error::SqliteSnafu)?;
225 let mut insert_stmt = conn.prepare("INSERT INTO tag_category (id, description, color, name) VALUES (?, ?, ?, ?)").context(error::SqliteSnafu)?;
226
227 for cat in categories.iter().filter(|p| existing_ids.contains(&p.id)) {
229 update_stmt.execute(params![cat.description, cat.color, cat.name, cat.id]).context(error::SqliteSnafu)?;
230 }
231
232 for cat in categories.iter().filter(|p| !existing_ids.contains(&p.id)) {
234 insert_stmt.execute(params![cat.id, cat.description, cat.color, cat.name]).context(error::SqliteSnafu)?;
235 }
236
237 Ok(())
238}
239
240pub fn apply_tags(conn: &Connection, tags: Vec<RemoteTag>) -> Result<()> {
241 rusqlite::vtab::array::load_module(conn).context(error::SqliteSnafu)?;
243
244 let changed_aliases: Vec<Alias> = tags.iter()
246 .flat_map(|cur| cur.aliases.iter().map(move |alias| Alias { id: cur.id, value: alias.clone() }))
247 .collect();
248
249 let changed_ids: Vec<i64> = tags.iter().map(|cur| cur.id).collect();
250
251 let existing_tags = tag::find(conn).context(error::SqliteSnafu)?;
252 let existing_ids: std::collections::HashSet<i64> = existing_tags.iter().map(|p| p.id).collect();
253
254 let changed_alias_names = SqlVec(changed_aliases.iter().map(|a| a.value.clone()).collect::<Vec<String>>());
256 conn.execute("DELETE FROM tag_alias WHERE name IN rarray(?)", params![changed_alias_names]).context(error::SqliteSnafu)?;
257
258 let changed_ids_vec = SqlVec(changed_ids);
260 conn.execute("DELETE FROM tag_alias WHERE tagId IN rarray(?)", params![changed_ids_vec]).context(error::SqliteSnafu)?;
261
262 let mut update_tag_stmt = conn.prepare("UPDATE tag SET dateModified = ?, primaryAliasId = (SELECT id FROM tag_alias WHERE name = ?), description = ?, categoryId = (SELECT id FROM tag_category WHERE name = ?) WHERE id = ?").context(error::SqliteSnafu)?;
263 let mut insert_tag_stmt = conn.prepare("INSERT INTO tag (id, dateModified, primaryAliasId, description, categoryId)
264 VALUES (?, ?, (SELECT id FROM tag_alias WHERE name = ?), ?, (SELECT id FROM tag_category WHERE name = ?))").context(error::SqliteSnafu)?;
265 let mut delete_tag_alias_stmt = conn.prepare("DELETE FROM tag_alias WHERE tagId = ?").context(error::SqliteSnafu)?;
266 let mut delete_tag_stmt = conn.prepare("DELETE FROM tag WHERE id = ?").context(error::SqliteSnafu)?;
267
268 let mut insert_alias_stmt = conn.prepare("INSERT INTO tag_alias (tagId, name) VALUES (?, ?)").context(error::SqliteSnafu)?;
270 for alias in changed_aliases {
271 insert_alias_stmt.execute(params![alias.id, alias.value]).context(error::SqliteSnafu)?;
272 }
273
274 let deleted_tag_ids = SqlVec(tags.iter().filter(|p| existing_ids.contains(&p.id) && p.deleted).map(|p| p.id).collect::<Vec<i64>>());
276 conn.execute("UPDATE game
278 SET tagsStr = (
279 SELECT IFNULL(string_agg(ta.name, '; '), '')
280 FROM game_tags_tag gtt
281 JOIN tag t ON gtt.tagId = t.id
282 JOIN tag_alias ta ON t.primaryAliasId = ta.id
283 WHERE gtt.gameId = game.id AND t.id NOT IN rarray(?)
284 ) WHERE game.id IN (
285 SELECT gameId FROM game_tags_tag WHERE tagId IN rarray(?)
286 )", params![deleted_tag_ids, deleted_tag_ids]).context(error::SqliteSnafu)?;
287 conn.execute("DELETE FROM game_tags_tag WHERE tagId IN rarray(?)", params![deleted_tag_ids]).context(error::SqliteSnafu)?;
289 conn.execute("DELETE FROM tag_alias WHERE tagId IN rarray(?)", params![deleted_tag_ids]).context(error::SqliteSnafu)?;
290 conn.execute("DELETE FROM tag WHERE id IN rarray(?)", params![deleted_tag_ids]).context(error::SqliteSnafu)?;
291
292 for tag in tags.iter().filter(|p| existing_ids.contains(&p.id) && !p.deleted) {
294 update_tag_stmt.execute(params![tag.date_modified, tag.name, tag.description, tag.category, tag.id]).context(error::SqliteSnafu)?;
295 }
296
297 for tag in tags.iter().filter(|p| !existing_ids.contains(&p.id) && !p.deleted) {
299 delete_tag_alias_stmt.execute(params![tag.id]).context(error::SqliteSnafu)?;
301 delete_tag_stmt.execute(params![tag.id]).context(error::SqliteSnafu)?;
302
303 for alias in &tag.aliases {
305 insert_alias_stmt.execute(params![tag.id, &alias]).context(error::SqliteSnafu)?;
306 }
307 insert_tag_stmt.execute(params![tag.id, tag.date_modified, tag.name, tag.description, tag.category]).context(error::SqliteSnafu)?;
308 }
309
310 mark_index_dirty(conn).context(error::SqliteSnafu)?;
311
312 Ok(())
313}
314
315pub fn apply_games(conn: &Connection, games_res: &RemoteGamesRes) -> Result<()> {
316 rusqlite::vtab::array::load_module(conn).context(error::SqliteSnafu)?;
318
319 let changed_ids = SqlVec(games_res.games.iter().map(|g| g.id.clone()).collect::<Vec<String>>());
320
321 println!("Reassigning relations");
322
323 conn.execute("DELETE FROM game_tags_tag WHERE gameId IN rarray(?)", params![changed_ids]).context(error::SqliteSnafu)?;
325 conn.execute("DELETE FROM game_platforms_platform WHERE gameId IN rarray(?)", params![changed_ids]).context(error::SqliteSnafu)?;
326 let mut insert_tag_relation_stmt = conn.prepare("INSERT INTO game_tags_tag (gameId, tagId)
328 VALUES (?, ?)").context(error::SqliteSnafu)?;
329 let mut insert_platform_relation_stmt = conn.prepare("INSERT INTO game_platforms_platform (gameId, platformId)
330 VALUES (?, ?)").context(error::SqliteSnafu)?;
331 for ta in &games_res.tag_relations {
332 insert_tag_relation_stmt.execute(params![ta[0], ta[1]]).context(error::SqliteSnafu)?;
333 }
334 for pa in &games_res.platform_relations {
335 insert_platform_relation_stmt.execute(params![pa[0], pa[1]]).context(error::SqliteSnafu)?;
336 }
337
338 println!("Reassigning add apps");
339
340 conn.execute("DELETE FROM additional_app WHERE parentGameId IN rarray(?)", params![changed_ids]).context(error::SqliteSnafu)?;
342 let mut insert_add_app_stmt = conn.prepare("INSERT INTO additional_app
344 (id, applicationPath, launchCommand, name, parentGameId, autoRunBefore, waitForExit)
345 VALUES
346 (?, ?, ?, ?, ?, ?, ?)").context(error::SqliteSnafu)?;
347 for aa in &games_res.add_apps {
348 insert_add_app_stmt.execute(params![Uuid::new_v4().to_string(), aa.application_path, aa.launch_command, aa.name, aa.parent_game_id,
349 aa.auto_run_before, aa.wait_for_exit])
350 .context(error::SqliteSnafu)?;
351 }
352
353 println!("Reassigning game data");
354
355 conn.execute("DELETE FROM game_data WHERE gameId IN rarray(?) AND presentOnDisk == false", params![changed_ids]).context(error::SqliteSnafu)?;
357 let mut insert_game_data_stmt = conn.prepare("INSERT INTO game_data
359 (gameId, title, dateAdded, sha256, crc32, presentOnDisk, path, size, parameters, applicationPath, launchCommand)
360 VALUES
361 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
362 ON CONFLICT(gameId, dateAdded)
363 DO UPDATE SET parameters = ?, applicationPath = ?, launchCommand = ?").context(error::SqliteSnafu)?;
364 for gd in &games_res.game_data {
365 insert_game_data_stmt.execute(params![gd.game_id, gd.title, gd.date_added, gd.sha_256,
366 gd.crc_32, false, "", gd.size, gd.parameters, gd.application_path, gd.launch_command,
367 gd.parameters, gd.application_path, gd.launch_command])
368 .context(error::SqliteSnafu)?;
369 }
370
371 let existing_ids = game::find_all_ids(conn).context(error::SqliteSnafu)?;
372
373 println!("Updating games");
374
375 let mut update_game_stmt = conn.prepare("UPDATE game SET library = ?, title = ?, alternateTitles = ?, series = ?, developer = ?, publisher = ?,
377 platformName = ?, platformId = (SELECT platformId FROM platform_alias WHERE name = ?), platformsStr = ?, dateAdded = ?, dateModified = ?,
378 playMode = ?, status = ?, notes = ?, source = ?, activeDataId = -1,
379 applicationPath = ?, launchCommand = ?, releaseDate = ?, version = ?,
380 originalDescription = ?, language = ?, archiveState = ?, logoPath = ?, screenshotPath = ?, ruffleSupport = ? WHERE id = ?").context(error::SqliteSnafu)?;
381
382 for g in games_res.games.iter().filter(|p| existing_ids.contains(&p.id)) {
383 update_game_stmt.execute(params![
384 g.library, g.title, g.alternate_titles, g.series, g.developer, g.publisher,
385 g.platform_name, g.platform_name, "", g.date_added, g.date_modified,
386 g.play_mode, g.status, g.notes, g.source,
387 g.application_path, g.launch_command, g.release_date, g.version,
388 g.original_description, g.language, g.archive_state, g.logo_path, g.screenshot_path, g.ruffle_support, g.id]).context(error::SqliteSnafu)?;
389 }
390
391 println!("Inserting games");
392
393 let mut insert_game_stmt = conn.prepare("INSERT INTO game (id, library, title, alternateTitles, series, developer, publisher,
395 platformName, platformId, platformsStr, dateAdded, dateModified, broken, extreme, playMode, status,
396 notes, tagsStr, source, applicationPath, launchCommand, releaseDate, version,
397 originalDescription, language, activeDataId, activeDataOnDisk, playtime,
398 archiveState, orderTitle, logoPath, screenshotPath, ruffleSupport) VALUES (?, ?, ?, ?, ?, ?, ?,
399 ?, ?, (SELECT platformId FROM platform_alias WHERE name = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").context(error::SqliteSnafu)?;
400
401 for g in games_res.games.iter().filter(|p| !existing_ids.contains(&p.id)) {
402 insert_game_stmt.execute(params![
403 g.id, g.library, g.title, g.alternate_titles, g.series, g.developer, g.publisher,
404 g.platform_name, g.platform_name, "", g.date_added, g.date_modified, false, false, g.play_mode, g.status,
405 g.notes, "", g.source, g.application_path, g.launch_command, g.release_date, g.version,
406 g.original_description, g.language, -1, false, 0,
407 g.archive_state, "", g.logo_path, g.screenshot_path, g.ruffle_support,
408 ]).context(error::SqliteSnafu)?;
409 }
410
411 println!("Updating games - cleanup");
412
413 conn.execute("UPDATE game
415 SET tagsStr = (
416 SELECT IFNULL(string_agg(ta.name, '; '), '')
417 FROM game_tags_tag gtt
418 JOIN tag t ON gtt.tagId = t.id
419 JOIN tag_alias ta ON t.primaryAliasId = ta.id
420 WHERE gtt.gameId = game.id
421 ) WHERE game.id IN rarray(?)", params![changed_ids]).context(error::SqliteSnafu)?;
422 conn.execute("UPDATE game
423 SET platformsStr = (
424 SELECT IFNULL(string_agg(pa.name, '; '), '')
425 FROM game_platforms_platform gpp
426 JOIN platform p ON gpp.platformId = p.id
427 JOIN platform_alias pa ON p.primaryAliasId = pa.id
428 WHERE gpp.gameId = game.id
429 ) WHERE game.id IN rarray(?)", params![changed_ids]).context(error::SqliteSnafu)?;
430
431 println!("Active game id cleanup");
432
433 conn.execute("UPDATE game
435 SET activeDataId = (SELECT game_data.id FROM game_data WHERE game.id = game_data.gameId ORDER BY game_data.dateAdded DESC LIMIT 1)
436 WHERE game.activeDataId = -1", ()).context(error::SqliteSnafu)?;
437
438 mark_index_dirty(conn).context(error::SqliteSnafu)?;
439
440 Ok(())
441}
442
443pub fn delete_games(conn: &Connection, games_res: &RemoteDeletedGamesRes) -> Result<()> {
444 rusqlite::vtab::array::load_module(conn).context(error::SqliteSnafu)?;
446
447 let ids = SqlVec(games_res.games.iter().map(|g| g.id.clone()).collect::<Vec<String>>());
448
449 conn.execute("DELETE FROM game_tags_tag WHERE gameId IN rarray(?)", params![ids]).context(error::SqliteSnafu)?;
450 conn.execute("DELETE FROM game_platforms_platform WHERE gameId IN rarray(?)", params![ids]).context(error::SqliteSnafu)?;
451 conn.execute("DELETE FROM game_data WHERE gameId IN rarray(?)", params![ids]).context(error::SqliteSnafu)?;
452 conn.execute("DELETE FROM additional_app WHERE parentGameId IN rarray(?)", params![ids]).context(error::SqliteSnafu)?;
453 conn.execute("DELETE FROM game WHERE id IN rarray(?)", params![ids]).context(error::SqliteSnafu)?;
454
455 Ok(())
456}
457
458pub fn apply_redirects(conn: &Connection, redirects: Vec<GameRedirect>) -> Result<()> {
459 let mut stmt = conn.prepare("INSERT OR IGNORE INTO game_redirect (sourceId, id) VALUES (?, ?)").context(error::SqliteSnafu)?;
460 for r in redirects.iter() {
461 stmt.execute(params![r.source_id, r.dest_id]).context(error::SqliteSnafu)?;
462 }
463 conn.execute("DELETE FROM game_redirect WHERE sourceId IN (SELECT id FROM game)", ()).context(error::SqliteSnafu)?;
464 Ok(())
465}