1use chrono::Utc;
2use rusqlite::{
3 params,
4 types::{FromSql, FromSqlError, Value, ValueRef},
5 Connection, OptionalExtension, Result,
6};
7use serde::{Deserialize, Serialize};
8use unicase::UniCase;
9use uuid::Uuid;
10use std::{collections::{HashMap, HashSet}, fmt::Display, ops::{Deref, DerefMut}, rc::Rc, vec::Vec};
11
12use crate::{game_data::{GameData, PartialGameData}, platform::{self, PlatformAppPath}, tag::{self, Tag}, MAX_SEARCH};
13
14use self::search::{mark_index_dirty, GameSearch, GameSearchRelations};
15
16pub mod search;
17pub mod ext;
18
19#[cfg(feature = "napi")]
20use napi::bindgen_prelude::{ToNapiValue, FromNapiValue};
21
22#[derive(Debug, Clone)]
23pub struct TagVec (Vec<String>);
24
25impl serde::Serialize for TagVec {
26 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
27 where
28 S: serde::Serializer,
29 {
30 let combined = self.0.join(";");
31 serializer.serialize_str(&combined)
32 }
33}
34
35impl<'de> serde::Deserialize<'de> for TagVec {
36 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
37 where
38 D: serde::Deserializer<'de>,
39 {
40 struct TagVecVisitor;
41
42 impl<'de> serde::de::Visitor<'de> for TagVecVisitor {
43 type Value = TagVec;
44
45 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
46 formatter.write_str("a string separated by ;")
47 }
48
49 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
50 where
51 E: serde::de::Error,
52 {
53 let parts: Vec<String> = value.split(';').map(String::from).collect();
54 Ok(TagVec(parts))
55 }
56 }
57
58 deserializer.deserialize_str(TagVecVisitor)
59 }
60}
61
62impl Deref for TagVec {
63 type Target = Vec<String>;
64
65 fn deref(&self) -> &Self::Target {
66 &self.0
67 }
68}
69
70impl DerefMut for TagVec {
71 fn deref_mut(&mut self) -> &mut Self::Target {
72 &mut self.0
73 }
74}
75
76impl Default for TagVec {
77 fn default() -> Self {
78 TagVec (vec![])
79 }
80}
81
82impl Display for TagVec {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 f.write_str(self.join("; ").as_str())?;
85 Ok(())
86 }
87}
88
89#[cfg(feature = "napi")]
90impl FromNapiValue for TagVec {
91 unsafe fn from_napi_value(env: napi::sys::napi_env, napi_val: napi::sys::napi_value) -> napi::Result<Self> {
92 let mut len = 0;
93 napi::sys::napi_get_array_length(env, napi_val, &mut len);
94
95 let mut result = Vec::with_capacity(len as usize);
96
97 for i in 0..len {
98 let mut element_value: napi::sys::napi_value = std::ptr::null_mut();
99 napi::sys::napi_get_element(env, napi_val, i, &mut element_value);
100
101 let str_length = {
103 let mut str_length = 0;
104 napi::sys::napi_get_value_string_utf8(
105 env,
106 element_value,
107 std::ptr::null_mut(),
108 0,
109 &mut str_length
110 );
111 str_length
112 };
113
114 let mut buffer = Vec::with_capacity(str_length as usize + 1);
115 let buffer_ptr = buffer.as_mut_ptr() as *mut _;
116 napi::sys::napi_get_value_string_utf8(
117 env,
118 element_value,
119 buffer_ptr,
120 buffer.capacity(),
121 std::ptr::null_mut()
122 );
123
124 buffer.set_len(str_length as usize);
125 let string = String::from_utf8_lossy(&buffer).to_string();
126 result.push(string);
127 }
128
129 Ok(TagVec(result))
130 }
131}
132
133#[cfg(feature = "napi")]
134impl ToNapiValue for TagVec {
135 unsafe fn to_napi_value(env: napi::sys::napi_env, val: Self) -> napi::Result<napi::sys::napi_value> {
136 let len = val.len();
137 let mut js_array: napi::sys::napi_value = std::ptr::null_mut();
138 napi::sys::napi_create_array_with_length(env, len, &mut js_array);
139
140 for (i, item) in val.iter().enumerate() {
141 let mut js_string: napi::sys::napi_value = std::ptr::null_mut();
142 napi::sys::napi_create_string_utf8(env, item.as_ptr() as *const _, item.len(), &mut js_string);
143
144 napi::sys::napi_set_element(env, js_array, i as u32, js_string);
145 }
146
147 Ok(js_array)
148 }
149}
150
151impl IntoIterator for TagVec {
152 type Item = String;
153 type IntoIter = std::vec::IntoIter<Self::Item>;
154
155 fn into_iter(self) -> Self::IntoIter {
156 self.0.into_iter()
157 }
158}
159
160impl From<Vec<&str>> for TagVec {
161 fn from(vec: Vec<&str>) -> Self {
162 let strings: Vec<String> = vec.iter().map(|&s| s.to_string()).collect();
163 TagVec (strings)
164 }
165}
166
167trait FromDelimitedString: Sized {
175 fn from_delimited_string(s: &str) -> Result<Self, Box<dyn std::error::Error>>;
176}
177
178impl FromDelimitedString for TagVec {
179 fn from_delimited_string(s: &str) -> Result<Self, Box<dyn std::error::Error>> {
180 let values: Vec<String> = s
181 .split(';')
182 .map(|part| part.trim().to_string())
183 .filter(|part| !part.is_empty())
184 .collect();
185
186 Ok(TagVec(values))
187 }
188}
189
190impl FromSql for TagVec {
192 fn column_result(value: ValueRef) -> Result<Self, FromSqlError> {
193 match value {
194 ValueRef::Text(_) => {
195 let s = value.as_str()?;
196 FromDelimitedString::from_delimited_string(s)
197 .map_err(|_| FromSqlError::OutOfRange(0))
198 }
199 _ => Err(FromSqlError::InvalidType),
200 }
201 }
202}
203
204#[cfg_attr(feature = "napi", napi(object))]
205#[derive(Debug, Clone, Deserialize, Serialize)]
206pub struct AdditionalApp {
207 pub id: String,
208 pub name: String,
209 pub application_path: String,
210 pub launch_command: String,
211 pub auto_run_before: bool,
212 pub wait_for_exit: bool,
213 pub parent_game_id: String,
214}
215
216#[cfg_attr(feature = "napi", napi(object))]
217#[derive(Debug, Clone, Deserialize, Serialize)]
218pub struct Game {
219 pub id: String,
220 pub owner: String,
221 pub library: String,
222 pub title: String,
223 pub alternate_titles: String,
224 pub series: String,
225 pub developer: String,
226 pub publisher: String,
227 pub primary_platform: String,
228 pub platforms: TagVec,
229 pub date_added: String,
230 pub date_modified: String,
231 pub detailed_platforms: Option<Vec<Tag>>,
232 pub legacy_broken: bool,
233 pub legacy_extreme: bool,
234 pub play_mode: String,
235 pub status: String,
236 pub notes: String,
237 pub tags: TagVec,
238 pub detailed_tags: Option<Vec<Tag>>,
239 pub source: String,
240 pub legacy_application_path: String,
241 pub legacy_launch_command: String,
242 pub release_date: String,
243 pub version: String,
244 pub original_description: String,
245 pub language: String,
246 pub active_data_id: Option<i64>,
247 pub active_data_on_disk: bool,
248 pub last_played: Option<String>,
249 pub playtime: i64,
250 pub play_counter: i64,
251 pub active_game_config_id: Option<i64>,
252 pub active_game_config_owner: Option<String>,
253 pub archive_state: i64,
254 pub game_data: Option<Vec<GameData>>,
255 pub add_apps: Option<Vec<AdditionalApp>>,
256 pub ruffle_support: String,
257 pub logo_path: String,
258 pub screenshot_path: String,
259 pub ext_data: Option<HashMap<String, serde_json::Value>>,
260}
261
262#[cfg_attr(feature = "napi", napi(object))]
263#[derive(Debug, Clone, Deserialize, Serialize)]
264pub struct PartialGame {
265 pub id: String,
266 pub owner: Option<String>,
267 pub library: Option<String>,
268 pub title: Option<String>,
269 pub alternate_titles: Option<String>,
270 pub series: Option<String>,
271 pub developer: Option<String>,
272 pub publisher: Option<String>,
273 pub primary_platform: Option<String>,
274 pub platforms: Option<TagVec>,
275 pub date_added: Option<String>,
276 pub date_modified: Option<String>,
277 pub legacy_broken: Option<bool>,
278 pub legacy_extreme: Option<bool>,
279 pub play_mode: Option<String>,
280 pub status: Option<String>,
281 pub notes: Option<String>,
282 pub tags: Option<TagVec>,
283 pub detailed_tags: Option<Vec<Tag>>,
284 pub source: Option<String>,
285 pub legacy_application_path: Option<String>,
286 pub legacy_launch_command: Option<String>,
287 pub release_date: Option<String>,
288 pub version: Option<String>,
289 pub original_description: Option<String>,
290 pub language: Option<String>,
291 pub active_data_id: Option<i64>,
292 pub active_data_on_disk: Option<bool>,
293 pub last_played: Option<String>,
294 pub playtime: Option<i64>,
295 pub play_counter: Option<i64>,
296 pub active_game_config_id: Option<i64>,
297 pub active_game_config_owner: Option<String>,
298 pub archive_state: Option<i64>,
299 pub add_apps: Option<Vec<AdditionalApp>>,
300 pub ruffle_support: Option<String>,
301 pub logo_path: Option<String>,
302 pub screenshot_path: Option<String>,
303 pub ext_data: Option<HashMap<String, serde_json::Value>>,
304}
305
306#[cfg_attr(feature = "napi", napi(object))]
307#[derive(Debug, Clone, Deserialize, Serialize)]
308pub struct GameRedirect {
309 pub source_id: String,
310 pub dest_id: String,
311}
312
313pub fn find_all_ids(conn: &Connection) -> Result<Vec<String>> {
314 let mut stmt = conn.prepare("SELECT id FROM game")?;
315
316 let ids = stmt.query_map([], |row| {
317 row.get(0)
318 })?
319 .collect::<Result<Vec<String>>>()?;
320
321 Ok(ids)
322}
323
324pub fn find(conn: &Connection, id: &str) -> Result<Option<Game>> {
325 let mut stmt = conn.prepare(
326 "SELECT id, title, alternateTitles, series, developer, publisher, platformsStr, \
327 platformName, dateAdded, dateModified, broken, extreme, playMode, status, notes, \
328 tagsStr, source, applicationPath, launchCommand, releaseDate, version, \
329 originalDescription, language, activeDataId, activeDataOnDisk, lastPlayed, playtime, \
330 activeGameConfigId, activeGameConfigOwner, archiveState, library, playCounter, \
331 logoPath, screenshotPath, ruffleSupport, owner \
332 FROM game WHERE id = COALESCE((SELECT id FROM game_redirect WHERE sourceId = ?), ?)",
333 )?;
334
335 let game_result = stmt
336 .query_row(params![id, id], |row| {
337 Ok(Game {
338 id: row.get(0)?,
339 title: row.get(1)?,
340 alternate_titles: row.get(2)?,
341 series: row.get(3)?,
342 developer: row.get(4)?,
343 publisher: row.get(5)?,
344 platforms: row.get(6)?,
345 primary_platform: row.get(7)?,
346 date_added: row.get(8)?,
347 date_modified: row.get(9)?,
348 legacy_broken: row.get(10)?,
349 legacy_extreme: row.get(11)?,
350 play_mode: row.get(12)?,
351 status: row.get(13)?,
352 notes: row.get(14)?,
353 tags: row.get(15)?,
354 source: row.get(16)?,
355 legacy_application_path: row.get(17)?,
356 legacy_launch_command: row.get(18)?,
357 release_date: row.get(19)?,
358 version: row.get(20)?,
359 original_description: row.get(21)?,
360 language: row.get(22)?,
361 active_data_id: row.get(23)?,
362 active_data_on_disk: row.get(24)?,
363 last_played: row.get(25)?,
364 playtime: row.get(26)?,
365 active_game_config_id: row.get(27)?,
366 active_game_config_owner: row.get(28)?,
367 archive_state: row.get(29)?,
368 library: row.get(30)?,
369 play_counter: row.get(31)?,
370 detailed_platforms: None,
371 detailed_tags: None,
372 game_data: None,
373 add_apps: None,
374 logo_path: row.get(32)?,
375 screenshot_path: row.get(33)?,
376 ruffle_support: row.get(34)?,
377 owner: row.get(35)?,
378 ext_data: None,
379 })
380 })
381 .optional()?; if let Some(mut game) = game_result {
384 game.detailed_platforms = Some(get_game_platforms(conn, id)?);
385 game.detailed_tags = Some(get_game_tags(conn, id)?);
386 game.game_data = Some(get_game_data(conn, id)?);
387 game.add_apps = Some(get_game_add_apps(conn, id)?);
388 game.ext_data = Some(find_ext_data(conn, &game.id)?);
389 Ok(Some(game))
390 } else {
391 Ok(None)
392 }
393}
394
395fn find_ext_data(conn: &Connection, id: &str) -> Result<HashMap<String, serde_json::Value>> {
396 let mut stmt = conn.prepare("SELECT extId, data FROM ext_data WHERE gameId = ?")?;
397 let rows = stmt.query_map(params![id], |row| {
398 let ext_id: String = row.get(0)?;
399 let data: serde_json::Value = row.get(1)?;
400 Ok((ext_id, data))
401 })?;
402
403 let mut res = HashMap::new();
404
405 for result in rows {
406 let (ext_id, data) = result?;
407 res.insert(ext_id, data);
408 }
409
410 Ok(res)
411}
412
413pub fn create(conn: &Connection, partial: &PartialGame) -> Result<Game> {
414 let mut game: Game = partial.into();
415
416 let tags_copy = game.tags.clone();
417 let platforms_copy = game.platforms.clone();
418 game.tags = vec![].into();
419 game.platforms = vec![].into();
420
421 let mut detailed_tags = vec![];
422
423 match game.detailed_tags.as_deref() {
424 Some(dtags) if !dtags.is_empty() => {
425 for tag in dtags {
426 detailed_tags.push(tag.id);
427 }
428 },
429 _ => {
430 for name in tags_copy {
431 let detailed_tag = tag::find_or_create(conn, &name)?;
432 game.tags.push(detailed_tag.name);
433 detailed_tags.push(detailed_tag.id);
434 }
435 }
436 }
437
438 let mut detailed_platforms = vec![];
439
440 for name in platforms_copy {
441 let detailed_platform = platform::find_or_create(conn, &name, None)?;
442 game.platforms.push(detailed_platform.name);
443 detailed_platforms.push(detailed_platform.id);
444 }
445
446 conn.execute(
447 "INSERT INTO game (id, owner, library, title, alternateTitles, series, developer, publisher, \
448 platformName, platformsStr, dateAdded, dateModified, broken, extreme, playMode, status, \
449 notes, tagsStr, source, applicationPath, launchCommand, releaseDate, version, \
450 originalDescription, language, activeDataId, activeDataOnDisk, lastPlayed, playtime, \
451 activeGameConfigId, activeGameConfigOwner, archiveState, orderTitle, logoPath, screenshotPath, ruffleSupport) VALUES (?, ?, ?, ?, ?, ?, ?, \
452 ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', ?, ?, ?)",
453 params![
454 &game.id,
455 &game.owner,
456 &game.library,
457 &game.title,
458 &game.alternate_titles,
459 &game.series,
460 &game.developer,
461 &game.publisher,
462 &game.primary_platform,
463 &game.platforms.join("; "),
464 &game.date_added,
465 &game.date_modified,
466 &game.legacy_broken,
467 &game.legacy_extreme,
468 &game.play_mode,
469 &game.status,
470 &game.notes,
471 &game.tags.join("; "),
472 &game.source,
473 &game.legacy_application_path,
474 &game.legacy_launch_command,
475 &game.release_date,
476 &game.version,
477 &game.original_description,
478 &game.language,
479 &game.active_data_id,
480 &game.active_data_on_disk,
481 &game.last_played,
482 &game.playtime,
483 &game.active_game_config_id,
484 &game.active_game_config_owner,
485 &game.archive_state,
486 &game.logo_path,
487 &game.screenshot_path,
488 &game.ruffle_support,
489 ],
490 )?;
491
492 for tag in detailed_tags {
493 conn.execute("INSERT OR IGNORE INTO game_tags_tag (gameId, tagId) VALUES (?, ?)", params![game.id, tag])?;
494 }
495
496 for platform in detailed_platforms {
497 conn.execute("INSERT OR IGNORE INTO game_platforms_platform (gameId, platformId) VALUES (?, ?)", params![game.id, platform])?;
498 }
499
500 if let Some(ext_data) = &game.ext_data {
501 for (ext_id, data) in ext_data {
502 conn.execute("INSERT INTO ext_data (extId, gameId, data) VALUES(?, ?, ?)
503 ON CONFLICT(extId, gameId)
504 DO UPDATE SET data = ?", params![ext_id, game.id, data, data])?;
505 }
506 }
507
508 mark_index_dirty(conn)?;
509
510 Ok(game)
511}
512
513pub fn save(conn: &Connection, game: &PartialGame) -> Result<Game> {
514 rusqlite::vtab::array::load_module(conn)?;
516
517 let existing_game_result = find(conn, game.id.as_str())?;
518 if let Some(mut existing_game) = existing_game_result {
519 existing_game.apply_partial(game);
520
521 let tags_copy = existing_game.tags.clone();
523 let platforms_copy = existing_game.platforms.clone();
524 let mut detailed_tags_copy: Vec<Tag> = vec![];
525 let mut detailed_platforms_copy: Vec<Tag> = vec![];
526 existing_game.tags = vec![].into();
527 existing_game.platforms = vec![].into();
528
529 for name in tags_copy {
530 let detailed_tag = tag::find_or_create(conn, &name)?;
531 detailed_tags_copy.push(detailed_tag.clone());
532 existing_game.tags.push(detailed_tag.name);
533 }
534
535 for name in platforms_copy {
536 let detailed_platform = platform::find_or_create(conn, &name, None)?;
537 detailed_platforms_copy.push(detailed_platform.clone());
538 existing_game.platforms.push(detailed_platform.name);
539 }
540
541 let tag_ids: Vec<i64> = detailed_tags_copy.iter().map(|t| t.id).collect::<Vec<i64>>();
543 let tag_values = Rc::new(tag_ids.iter().copied().map(Value::from).collect::<Vec<Value>>());
544 let mut stmt = conn.prepare("DELETE FROM game_tags_tag WHERE gameId = ? AND tagId NOT IN rarray(?)")?;
545 stmt.execute(params![existing_game.id.as_str(), tag_values]).map(|changes| changes as usize)?;
546 for tag_id in tag_ids {
547 stmt = conn.prepare("INSERT OR IGNORE INTO game_tags_tag (gameId, tagId) VALUES (?, ?)")?;
548 stmt.execute(params![existing_game.id.as_str(), tag_id])?;
549 }
550
551 let platform_ids: Vec<i64> = detailed_platforms_copy.iter().map(|t| t.id).collect::<Vec<i64>>();
552 let platform_values = Rc::new(platform_ids.iter().copied().map(Value::from).collect::<Vec<Value>>());
553 let mut stmt = conn.prepare("DELETE FROM game_platforms_platform WHERE gameId = ? AND platformId NOT IN rarray(?)")?;
554 stmt.execute(params![existing_game.id.as_str(), platform_values]).map(|changes| changes as usize)?;
555 for platform_id in platform_ids {
556 stmt = conn.prepare("INSERT OR IGNORE INTO game_platforms_platform (gameId, platformId) VALUES (?, ?)")?;
557 stmt.execute(params![existing_game.id.as_str(), platform_id])?;
558 }
559
560 if let Some(ext_data) = &game.ext_data {
562 for (ext_id, data) in ext_data {
563 conn.execute("INSERT INTO ext_data (extId, gameId, data) VALUES(?, ?, ?)
564 ON CONFLICT(extId, gameId)
565 DO UPDATE SET data = ?", params![ext_id, game.id, data, data])?;
566 }
567 }
568
569 conn.execute(
571 "UPDATE game SET owner = ?, library = ?, title = ?, alternateTitles = ?, series = ?, developer = ?, publisher = ?, \
572 platformName = ?, platformsStr = ?, dateAdded = ?, dateModified = ?, broken = ?, \
573 extreme = ?, playMode = ?, status = ?, notes = ?, tagsStr = ?, source = ?, \
574 applicationPath = ?, launchCommand = ?, releaseDate = ?, version = ?, \
575 originalDescription = ?, language = ?, activeDataId = ?, activeDataOnDisk = ?, \
576 lastPlayed = ?, playtime = ?, playCounter = ?, activeGameConfigId = ?, activeGameConfigOwner = ?, \
577 archiveState = ?, logoPath = ?, screenshotPath = ?, ruffleSupport = ? WHERE id = ?",
578 params![
579 &existing_game.owner,
580 &existing_game.library,
581 &existing_game.title,
582 &existing_game.alternate_titles,
583 &existing_game.series,
584 &existing_game.developer,
585 &existing_game.publisher,
586 &existing_game.primary_platform,
587 &existing_game.platforms.join("; "),
588 &existing_game.date_added,
589 &existing_game.date_modified,
590 &existing_game.legacy_broken,
591 &existing_game.legacy_extreme,
592 &existing_game.play_mode,
593 &existing_game.status,
594 &existing_game.notes,
595 &existing_game.tags.join("; "),
596 &existing_game.source,
597 &existing_game.legacy_application_path,
598 &existing_game.legacy_launch_command,
599 &existing_game.release_date,
600 &existing_game.version,
601 &existing_game.original_description,
602 &existing_game.language,
603 &existing_game.active_data_id,
604 &existing_game.active_data_on_disk,
605 &existing_game.last_played,
606 &existing_game.playtime,
607 &existing_game.play_counter,
608 &existing_game.active_game_config_id,
609 &existing_game.active_game_config_owner,
610 &existing_game.archive_state,
611 &existing_game.logo_path,
612 &existing_game.screenshot_path,
613 &existing_game.ruffle_support,
614 &existing_game.id,
615 ],
616 )?;
617
618 existing_game.detailed_platforms = get_game_platforms(conn, &existing_game.id)?.into();
619 existing_game.detailed_tags = get_game_tags(conn, &existing_game.id)?.into();
620 existing_game.game_data = get_game_data(conn, &existing_game.id)?.into();
621
622 mark_index_dirty(conn)?;
623
624 Ok(existing_game)
625 } else {
626 Err(rusqlite::Error::QueryReturnedNoRows)
627 }
628}
629
630pub fn delete(conn: &Connection, id: &str) -> Result<()> {
631 let mut stmt = "DELETE FROM game WHERE id = ?";
632 conn.execute(stmt, params![id])?;
633
634 stmt = "DELETE FROM additional_app WHERE parentGameId = ?";
635 conn.execute(stmt, params![id])?;
636
637 stmt = "DELETE FROM game_tags_tag WHERE gameId = ?";
638 conn.execute(stmt, params![id])?;
639
640 stmt = "DELETE FROM game_platforms_platform WHERE gameId = ?";
641 conn.execute(stmt, params![id])?;
642
643 stmt = "DELETE FROM ext_data WHERE gameId = ?";
644 conn.execute(stmt, params![id])?;
645
646 Ok(())
647}
648
649pub fn count(conn: &Connection) -> Result<i64> {
650 conn.query_row("SELECT COUNT(*) FROM game", (), |row| row.get::<_, i64>(0))
651}
652
653fn get_game_platforms(conn: &Connection, id: &str) -> Result<Vec<Tag>> {
654 let mut platform_stmt = conn.prepare(
655 "SELECT p.id, p.description, pa.name, p.dateModified FROM platform p
656 INNER JOIN game_platforms_platform gpp ON gpp.platformId = p.id
657 INNER JOIN platform_alias pa ON p.primaryAliasId = pa.id
658 WHERE gpp.gameId = ?",
659 )?;
660
661 let platform_iter = platform_stmt.query_map(params![id], |row| {
662 Ok(Tag {
663 id: row.get(0)?,
664 description: row.get(1)?,
665 name: row.get(2)?,
666 date_modified: row.get(3)?,
667 category: None,
668 aliases: vec![],
669 })
670 })?;
671
672 let mut platforms: Vec<Tag> = vec![];
673
674 for platform_result in platform_iter {
675 let mut platform = platform_result?;
676
677 let mut platform_aliases_stmt =
679 conn.prepare("SELECT pa.name FROM platform_alias pa WHERE pa.platformId = ?")?;
680
681 let aliases_iter = platform_aliases_stmt
682 .query_map(params![platform.id], |row| Ok(row.get::<_, String>(0)?))?;
683
684 for alias_result in aliases_iter {
686 platform.aliases.push(alias_result?);
687 }
688
689 platforms.push(platform);
690 }
691
692 Ok(platforms)
693}
694
695fn get_game_tags(conn: &Connection, id: &str) -> Result<Vec<Tag>> {
696 let mut tag_stmt = conn.prepare(
697 "SELECT t.id, t.description, ta.name, t.dateModified, tc.name FROM tag t
698 INNER JOIN game_tags_tag gtt ON gtt.tagId = t.id
699 INNER JOIN tag_alias ta ON t.primaryAliasId = ta.id
700 INNER JOIN tag_category tc ON t.categoryId = tc.id
701 WHERE gtt.gameId = ?",
702 )?;
703
704 let tag_iter = tag_stmt.query_map(params![id], |row| {
705 Ok(Tag {
706 id: row.get(0)?,
707 description: row.get(1)?,
708 name: row.get(2)?,
709 date_modified: row.get(3)?,
710 category: row.get(4)?,
711 aliases: vec![],
712 })
713 })?;
714
715 let mut tags: Vec<Tag> = vec![];
716
717 for tag_result in tag_iter {
718 let mut tag = tag_result?;
719
720 let mut tag_aliases_stmt =
722 conn.prepare("SELECT ta.name FROM tag_alias ta WHERE ta.tagId = ?")?;
723
724 let aliases_iter =
725 tag_aliases_stmt.query_map(params![tag.id], |row| Ok(row.get::<_, String>(0)?))?;
726
727 for alias_result in aliases_iter {
729 tag.aliases.push(alias_result?);
730 }
731
732 tags.push(tag);
733 }
734
735 Ok(tags)
736}
737
738pub fn get_game_data(conn: &Connection, id: &str) -> Result<Vec<GameData>> {
739 let mut game_data: Vec<GameData> = vec![];
740
741 let mut game_data_stmt = conn.prepare("
742 SELECT id, title, dateAdded, sha256, crc32, presentOnDisk,
743 path, size, parameters, applicationPath, launchCommand
744 FROM game_data
745 WHERE gameId = ?
746 ")?;
747
748 let rows = game_data_stmt.query_map(params![id], |row| {
749 Ok(GameData {
750 id: row.get(0)?,
751 game_id: id.to_owned(),
752 title: row.get(1)?,
753 date_added: row.get(2)?,
754 sha256: row.get(3)?,
755 crc32: row.get(4)?,
756 present_on_disk: row.get(5)?,
757 path: row.get(6)?,
758 size: row.get(7)?,
759 parameters: row.get(8)?,
760 application_path: row.get(9)?,
761 launch_command: row.get(10)?,
762 })
763 })?;
764
765 for result in rows {
766 game_data.push(result?);
767 }
768
769 Ok(game_data)
770}
771
772fn get_game_add_apps(conn: &Connection, game_id: &str) -> Result<Vec<AdditionalApp>> {
773 let mut add_app_stmt = conn.prepare(
774 "SELECT id, name, applicationPath, launchCommand, autoRunBefore, waitForExit
775 FROM additional_app WHERE parentGameId = ?"
776 )?;
777
778 let mut add_apps: Vec<AdditionalApp> = vec![];
779
780 let add_app_iter = add_app_stmt.query_map(params![game_id], |row| {
781 Ok(AdditionalApp {
782 id: row.get(0)?,
783 parent_game_id: game_id.to_owned(),
784 name: row.get(1)?,
785 application_path: row.get(2)?,
786 launch_command: row.get(3)?,
787 auto_run_before: row.get(4)?,
788 wait_for_exit: row.get(5)?,
789 })
790 })?;
791
792 for add_app in add_app_iter {
793 add_apps.push(add_app?);
794 }
795
796 Ok(add_apps)
797}
798
799pub fn find_game_data_by_id(conn: &Connection, id: i64) -> Result<Option<GameData>> {
800 let mut game_data_stmt = conn.prepare("
801 SELECT gameId, title, dateAdded, sha256, crc32, presentOnDisk,
802 path, size, parameters, applicationPath, launchCommand
803 FROM game_data
804 WHERE id = ?
805 ")?;
806
807 Ok(game_data_stmt.query_row(params![id], |row| {
808 Ok(GameData {
809 id: id.to_owned(),
810 game_id: row.get(0)?,
811 title: row.get(1)?,
812 date_added: row.get(2)?,
813 sha256: row.get(3)?,
814 crc32: row.get(4)?,
815 present_on_disk: row.get(5)?,
816 path: row.get(6)?,
817 size: row.get(7)?,
818 parameters: row.get(8)?,
819 application_path: row.get(9)?,
820 launch_command: row.get(10)?,
821 })
822 }).optional()?)
823}
824
825pub fn create_game_data(conn: &Connection, partial: &PartialGameData) -> Result<GameData> {
826 let game = find(conn, &partial.game_id)?;
828 if game.is_none() {
829 println!("{} missing", &partial.game_id);
830 return Err(rusqlite::Error::QueryReturnedNoRows);
831 }
832
833 let mut game_data: GameData = partial.into();
834
835 let mut stmt = conn.prepare("INSERT INTO game_data (gameId, title, dateAdded, sha256, crc32, presentOnDisk
836 , path, size, parameters, applicationPath, launchCommand)
837 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id")?;
838 let game_data_id: i64 = stmt.query_row(params![
839 &game_data.game_id,
840 &game_data.title,
841 &game_data.date_added,
842 &game_data.sha256,
843 &game_data.crc32,
844 &game_data.present_on_disk,
845 &game_data.path,
846 &game_data.size,
847 &game_data.parameters,
848 &game_data.application_path,
849 &game_data.launch_command,
850 ], |row| row.get(0))?;
851
852 game_data.id = game_data_id;
853 Ok(game_data)
854}
855
856pub fn save_game_data(conn: &Connection, partial: &PartialGameData) -> Result<GameData> {
857 let game_data: GameData = partial.into();
858
859 let mut stmt = conn.prepare("UPDATE game_data
860 SET gameId = ?, title = ?, dateAdded = ?, sha256 = ?, crc32 = ?, presentOnDisk = ?,
861 path = ?, size = ?, parameters = ?, applicationPath = ?, launchCommand = ? WHERE id = ?")?;
862 stmt.execute(params![
863 &game_data.game_id,
864 &game_data.title,
865 &game_data.date_added,
866 &game_data.sha256,
867 &game_data.crc32,
868 &game_data.present_on_disk,
869 &game_data.path,
870 &game_data.size,
871 &game_data.parameters,
872 &game_data.application_path,
873 &game_data.launch_command,
874 &game_data.id,
875 ])?;
876
877 let res = find_game_data_by_id(conn, game_data.id)?;
878 match res {
879 Some(r) => Ok(r),
880 None => Err(rusqlite::Error::QueryReturnedNoRows),
881 }
882}
883
884pub fn find_with_tag(conn: &Connection, tag: &str) -> Result<Vec<Game>> {
885 let mut search = GameSearch::default();
886 search.load_relations = GameSearchRelations {
887 tags: true,
888 platforms: true,
889 game_data: true,
890 add_apps: true,
891 ext_data: true,
892 };
893 search.filter.exact_whitelist.tags = Some(vec![tag.to_owned()]);
894 search.limit = MAX_SEARCH;
895 search::search(conn, &search)
896}
897
898pub fn find_split_string_col(conn: &Connection, column: &str, search_opt: Option<GameSearch>) -> Result<Vec<String>> {
899 let map_closure = |row: &rusqlite::Row<'_>| row.get::<_, String>(0);
900 let selection= format!("SELECT DISTINCT {} FROM game", column);
901 let dev_iter: Vec<String> = match search_opt {
902 None => {
903 let mut stmt = conn.prepare(&selection)?;
904 let mapped_rows = stmt.query_map((), map_closure)?;
905 mapped_rows
906 .map(|result| result.map_err(|e| e.into()))
907 .collect::<Result<Vec<String>>>()?
908 },
909 Some(mut search_data) => {
910 search_data.limit = MAX_SEARCH;
911 search_data.load_relations = GameSearchRelations::default();
912 search::search_custom(conn, &search_data, &selection, map_closure)?
913 }
914 };
915
916 let mut data_set: HashSet<UniCase<String>> = HashSet::new();
917
918 for row in dev_iter {
919 for data in row.split(|c| c == ';' || c == ',') {
920 data_set.insert(UniCase::new(data.trim().to_string()));
921 }
922 }
923
924 let data: Vec<String> = data_set.into_iter().map(|unicase| unicase.into_inner()).collect();
925
926 Ok(data)
927}
928
929pub fn find_developers(conn: &Connection, search_opt: Option<GameSearch>) -> Result<Vec<String>> {
930 find_split_string_col(conn, "developer", search_opt)
931}
932
933pub fn find_publishers(conn: &Connection, search_opt: Option<GameSearch>) -> Result<Vec<String>> {
934 find_split_string_col(conn, "publisher", search_opt)
935}
936
937pub fn find_series(conn: &Connection, search_opt: Option<GameSearch>) -> Result<Vec<String>> {
938 let map_closure = |row: &rusqlite::Row<'_>| row.get::<_, String>(0);
939 let selection = "SELECT DISTINCT series FROM game";
940 match search_opt {
941 None => {
942 let mut stmt = conn.prepare(selection)?;
943 let mapped_rows = stmt.query_map((), map_closure)?;
944 Ok(mapped_rows
945 .map(|result| result.map_err(|e| e.into()))
946 .collect::<Result<Vec<String>>>()?)
947 },
948 Some(mut search_data) => {
949 search_data.limit = MAX_SEARCH;
950 search_data.load_relations = GameSearchRelations::default();
951 Ok(search::search_custom(conn, &search_data, selection, map_closure)?)
952 }
953 }
954}
955
956pub fn find_libraries(conn: &Connection) -> Result<Vec<String>> {
957 let mut stmt = conn.prepare("SELECT DISTINCT library FROM game")?;
958 let libraries_iter = stmt.query_map((), |row| row.get(0))?;
959
960 let mut libraries = vec![];
961
962 for library in libraries_iter {
963 libraries.push(library?);
964 }
965
966 Ok(libraries)
967}
968
969pub fn find_statuses(conn: &Connection) -> Result<Vec<String>> {
970 let mut stmt = conn.prepare("SELECT DISTINCT status FROM game")?;
971 let status_iter = stmt.query_map((), |row| {
972 let value: String = row.get(0)?;
973 Ok(value)
974 })?;
975
976 let mut statuses = HashSet::new();
977
978 for status in status_iter {
979 if let Ok(status) = status {
980
981 status.split(';').for_each(|v| { statuses.insert(v.trim().to_string()); });
982 }
983 }
984
985 Ok(statuses.into_iter().collect())
986}
987
988pub fn find_play_modes(conn: &Connection) -> Result<Vec<String>> {
989 let mut stmt = conn.prepare("SELECT DISTINCT playMode FROM game")?;
990 let play_modes_iter = stmt.query_map((), |row| {
991 let value: String = row.get(0)?;
992 Ok(value)
993 })?;
994
995 let mut play_modes = HashSet::new();
996
997 for play_mode in play_modes_iter {
998 if let Ok(play_mode) = play_mode {
999
1000 play_mode.split(';').for_each(|v| { play_modes.insert(v.trim().to_string()); });
1001 }
1002 }
1003
1004 Ok(play_modes.into_iter().collect())
1005}
1006
1007pub fn find_application_paths(conn: &Connection) -> Result<Vec<String>> {
1008 let mut stmt = conn.prepare("
1009 SELECT COUNT(*) as games_count, applicationPath FROM (
1010 SELECT applicationPath FROM game WHERE applicationPath != ''
1011 UNION ALL
1012 SELECT applicationPath FROM game_data WHERE applicationPath != ''
1013 ) GROUP BY applicationPath ORDER BY games_count DESC")?;
1014 let ap_iter = stmt.query_map((), |row| row.get(1))?;
1015
1016 let mut app_paths = vec![];
1017
1018 for app_path in ap_iter {
1019 app_paths.push(app_path?);
1020 }
1021
1022 Ok(app_paths)
1023}
1024
1025pub fn find_platform_app_paths(conn: &Connection) -> Result<HashMap<String, Vec<PlatformAppPath>>> {
1026 let mut suggestions = HashMap::new();
1027 let platforms = platform::find(conn)?;
1028
1029 for platform in platforms {
1030 let mut stmt = conn.prepare("
1031 SELECT COUNT(*) as games_count, applicationPath FROM (
1032 SELECT applicationPath FROM game WHERE applicationPath != '' AND game.id IN (
1033 SELECT gameId FROM game_platforms_platform WHERE platformId = ?
1034 )
1035 UNION ALL
1036 SELECT applicationPath FROM game_data WHERE applicationPath != '' AND game_data.gameId IN (
1037 SELECT gameId FROM game_platforms_platform WHERE platformId = ?
1038 )
1039 ) GROUP BY applicationPath ORDER BY games_count DESC")?;
1040
1041 let results = stmt.query_map(params![platform.id, platform.id], |row| {
1042 Ok(PlatformAppPath {
1043 app_path: row.get(1)?,
1044 count: row.get(0)?,
1045 })
1046 })?;
1047
1048 let mut platform_list = vec![];
1049
1050 for app_path in results {
1051 platform_list.push(app_path?);
1052 }
1053
1054 suggestions.insert(platform.name, platform_list);
1055 }
1056
1057 Ok(suggestions)
1058}
1059
1060pub fn find_add_app_by_id(conn: &Connection, id: &str) -> Result<Option<AdditionalApp>> {
1061 let mut stmt = conn.prepare("SELECT name, applicationPath, launchCommand, autoRunBefore,
1062 waitForExit, parentGameId FROM additional_app WHERE id = ?")?;
1063
1064 stmt.query_row(params![id], |row| {
1065 Ok(AdditionalApp{
1066 id: id.to_owned(),
1067 name: row.get(0)?,
1068 application_path: row.get(1)?,
1069 launch_command: row.get(2)?,
1070 auto_run_before: row.get(3)?,
1071 wait_for_exit: row.get(4)?,
1072 parent_game_id: row.get(5)?
1073 })
1074 }).optional()
1075}
1076
1077pub fn create_add_app(conn: &Connection, add_app: &mut AdditionalApp) -> Result<()> {
1078 let id = conn.query_row("INSERT INTO additional_app (
1079 id, applicationPath, launchCommand, name, parentGameId, autoRunBefore, waitForExit
1080 ) VALUES (?, ?, ?, ?, ?, ? , ?) RETURNING id", params![add_app.id, add_app.application_path, add_app.launch_command,
1081 add_app.name, add_app.parent_game_id, add_app.auto_run_before, add_app.wait_for_exit], |row| row.get::<_, String>(0))?;
1082 add_app.id = id;
1083 Ok(())
1084}
1085
1086pub fn add_playtime(conn: &Connection, game_id: &str, seconds: i64) -> Result<()> {
1087 let mut game = match find(conn, game_id)? {
1088 Some(g) => g,
1089 None => return Err(rusqlite::Error::QueryReturnedNoRows)
1090 };
1091
1092 game.play_counter += 1;
1093 game.playtime += seconds;
1094 game.last_played = Some(Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string());
1095
1096 save(conn, &(game.into()))?;
1097 Ok(())
1098}
1099
1100pub fn clear_playtime_tracking(conn: &Connection) -> Result<()> {
1101 let mut stmt = conn.prepare("UPDATE game SET playtime = 0, playCounter = 0, lastPlayed = NULL")?;
1102 stmt.execute(())?;
1103 Ok(())
1104}
1105
1106pub fn clear_playtime_tracking_by_id(conn: &Connection, game_id: &str) -> Result<()> {
1107 let mut stmt = conn.prepare("UPDATE game SET playtime = 0, playCounter = 0, lastPlayed = NULL WHERE id = ?")?;
1108 stmt.execute(params![game_id])?;
1109 Ok(())
1110}
1111
1112pub fn force_active_data_most_recent(conn: &Connection) -> Result<()> {
1113 conn.execute("UPDATE game
1114 SET activeDataId = (SELECT game_data.id FROM game_data WHERE game.id = game_data.gameId ORDER BY game_data.dateAdded DESC LIMIT 1)
1115 WHERE game.activeDataId = -1", ())?;
1116 Ok(())
1117}
1118
1119pub fn find_redirects(conn: &Connection) -> Result<Vec<GameRedirect>> {
1120 let mut redirects = vec![];
1121
1122 let mut stmt = conn.prepare("SELECT sourceId, id, dateAdded FROM game_redirect")?;
1123 let redirects_iter = stmt.query_map((), |row| Ok(GameRedirect{
1124 source_id: row.get(0)?,
1125 dest_id: row.get(1)?
1126 }))?;
1127
1128 for r in redirects_iter {
1129 redirects.push(r?);
1130 }
1131
1132 Ok(redirects)
1133}
1134
1135pub fn create_redirect(conn: &Connection, src_id: &str, dest_id: &str) -> Result<()> {
1136 conn.execute("INSERT OR IGNORE INTO game_redirect (sourceId, id) VALUES (?, ?)", params![src_id, dest_id])?;
1137 Ok(())
1138}
1139
1140pub fn delete_redirect(conn: &Connection, src_id: &str, dest_id: &str) -> Result<()> {
1141 conn.execute("DELETE FROM game_redirect WHERE sourceId = ? AND id = ?", params![src_id, dest_id])?;
1142 Ok(())
1143}
1144
1145impl Default for PartialGame {
1146 fn default() -> Self {
1147 PartialGame {
1148 id: String::from(""),
1149 owner: None,
1150 library: None,
1151 title: None,
1152 alternate_titles: None,
1153 series: None,
1154 developer: None,
1155 publisher: None,
1156 primary_platform: None,
1157 platforms: None,
1158 date_added: None,
1159 date_modified: None,
1160 legacy_broken: None,
1161 legacy_extreme: None,
1162 play_mode: None,
1163 status: None,
1164 notes: None,
1165 tags: None,
1166 detailed_tags: None,
1167 source: None,
1168 legacy_application_path: None,
1169 legacy_launch_command: None,
1170 release_date: None,
1171 version: None,
1172 original_description: None,
1173 language: None,
1174 active_data_id: None,
1175 active_data_on_disk: None,
1176 last_played: None,
1177 playtime: None,
1178 play_counter: None,
1179 active_game_config_id: None,
1180 active_game_config_owner: None,
1181 archive_state: None,
1182 add_apps: None,
1183 logo_path: None,
1184 screenshot_path: None,
1185 ruffle_support: None,
1186 ext_data: None,
1187 }
1188 }
1189}
1190
1191impl Default for Game {
1192 fn default() -> Self {
1193 Game {
1194 id: Uuid::new_v4().to_string(),
1195 owner: String::from("local"),
1196 library: String::from("arcade"),
1197 title: String::default(),
1198 alternate_titles: String::default(),
1199 series: String::default(),
1200 developer: String::default(),
1201 publisher: String::default(),
1202 primary_platform: String::default(),
1203 platforms: TagVec::default(),
1204 date_added: Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(),
1205 date_modified: Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(),
1206 detailed_platforms: None,
1207 legacy_broken: false,
1208 legacy_extreme: false,
1209 play_mode: String::default(),
1210 status: String::default(),
1211 notes: String::default(),
1212 tags: TagVec::default(),
1213 detailed_tags: None,
1214 source: String::default(),
1215 legacy_application_path: String::default(),
1216 legacy_launch_command: String::default(),
1217 release_date: String::default(),
1218 version: String::default(),
1219 original_description: String::default(),
1220 language: String::default(),
1221 active_data_id: None,
1222 active_data_on_disk: false,
1223 last_played: None,
1224 playtime: 0,
1225 play_counter: 0,
1226 active_game_config_id: None,
1227 active_game_config_owner: None,
1228 archive_state: 0,
1229 game_data: None,
1230 add_apps: None,
1231 logo_path: String::default(),
1232 screenshot_path: String::default(),
1233 ruffle_support: String::default(),
1234 ext_data: None,
1235 }
1236 }
1237}
1238
1239impl Game {
1240 fn apply_partial(&mut self, source: &PartialGame) {
1241 if source.id == "" {
1242 self.id = Uuid::new_v4().to_string();
1243 } else {
1244 self.id = source.id.clone();
1245 }
1246
1247 if let Some(owner) = source.owner.clone() {
1248 self.owner = owner;
1249 }
1250
1251 if let Some(library) = source.library.clone() {
1252 self.library = library;
1253 }
1254
1255 if let Some(title) = source.title.clone() {
1256 self.title = title;
1257 }
1258
1259 if let Some(alternate_titles) = source.alternate_titles.clone() {
1260 self.alternate_titles = alternate_titles;
1261 }
1262
1263 if let Some(series) = source.series.clone() {
1264 self.series = series;
1265 }
1266
1267 if let Some(developer) = source.developer.clone() {
1268 self.developer = developer;
1269 }
1270
1271 if let Some(publisher) = source.publisher.clone() {
1272 self.publisher = publisher;
1273 }
1274
1275 if let Some(platforms) = source.platforms.clone() {
1276 self.platforms = platforms;
1277 }
1278
1279 if let Some(platform) = source.primary_platform.clone() {
1280 if !self.platforms.contains(&platform) {
1282 self.platforms.push(platform.clone());
1283 }
1284
1285 self.primary_platform = platform;
1286 }
1287
1288 if let Some(date_added) = source.date_added.clone() {
1289 self.date_added = date_added;
1290 }
1291
1292 if let Some(date_modified) = source.date_modified.clone() {
1293 self.date_modified = date_modified;
1294 }
1295
1296 if let Some(legacy_broken) = source.legacy_broken {
1297 self.legacy_broken = legacy_broken;
1298 }
1299
1300 if let Some(legacy_extreme) = source.legacy_extreme {
1301 self.legacy_extreme = legacy_extreme;
1302 }
1303
1304 if let Some(play_mode) = source.play_mode.clone() {
1305 self.play_mode = play_mode;
1306 }
1307
1308 if let Some(status) = source.status.clone() {
1309 self.status = status;
1310 }
1311
1312 if let Some(notes) = source.notes.clone() {
1313 self.notes = notes;
1314 }
1315
1316 if let Some(tags) = source.tags.clone() {
1317 self.tags = tags;
1318 }
1319
1320 if let Some(detailed_tags) = source.detailed_tags.clone() {
1321 self.detailed_tags = Some(detailed_tags);
1322 }
1323
1324 if let Some(source) = source.source.clone() {
1325 self.source = source;
1326 }
1327
1328 if let Some(legacy_application_path) = source.legacy_application_path.clone() {
1329 self.legacy_application_path = legacy_application_path;
1330 }
1331
1332 if let Some(legacy_launch_command) = source.legacy_launch_command.clone() {
1333 self.legacy_launch_command = legacy_launch_command;
1334 }
1335
1336 if let Some(release_date) = source.release_date.clone() {
1337 self.release_date = release_date;
1338 }
1339
1340 if let Some(version) = source.version.clone() {
1341 self.version = version;
1342 }
1343
1344 if let Some(original_description) = source.original_description.clone() {
1345 self.original_description = original_description;
1346 }
1347
1348 if let Some(language) = source.language.clone() {
1349 self.language = language;
1350 }
1351
1352 if let Some(active_data_id) = source.active_data_id {
1353 self.active_data_id = Some(active_data_id);
1354 }
1355
1356 if let Some(active_data_on_disk) = source.active_data_on_disk {
1357 self.active_data_on_disk = active_data_on_disk;
1358 }
1359
1360 if let Some(last_played) = source.last_played.clone() {
1361 self.last_played = Some(last_played);
1362 }
1363
1364 if let Some(playtime) = source.playtime {
1365 self.playtime = playtime;
1366 }
1367
1368 if let Some(play_counter) = source.play_counter {
1369 self.play_counter = play_counter;
1370 }
1371
1372 if let Some(active_game_config_id) = source.active_game_config_id {
1373 self.active_game_config_id = Some(active_game_config_id);
1374 }
1375
1376 if let Some(active_game_config_owner) = source.active_game_config_owner.clone() {
1377 self.active_game_config_owner = Some(active_game_config_owner);
1378 }
1379
1380 if let Some(archive_state) = source.archive_state {
1381 self.archive_state = archive_state;
1382 }
1383
1384 if let Some(ruffle_support) = source.ruffle_support.clone() {
1385 self.ruffle_support = ruffle_support;
1386 }
1387
1388 if let Some(logo_path) = &source.logo_path {
1389 self.logo_path = logo_path.clone();
1390 }
1391
1392 if let Some(screenshot_path) = &source.screenshot_path {
1393 self.screenshot_path = screenshot_path.clone();
1394 }
1395
1396 if let Some(ext_data) = source.ext_data.clone() {
1397 self.ext_data = Some(ext_data);
1398 }
1399 }
1400}
1401
1402impl From<&PartialGame> for Game {
1403 fn from(source: &PartialGame) -> Self {
1404 let mut game = Game::default();
1405 game.apply_partial(source);
1406 game
1407 }
1408}
1409
1410impl From<Game> for PartialGame {
1411 fn from(game: Game) -> Self {
1412 let mut new_plats = game.platforms.clone();
1413 if !new_plats.contains(&game.primary_platform) {
1415 new_plats.push(game.primary_platform.clone());
1416 }
1417
1418 PartialGame {
1419 id: game.id,
1420 owner: Some(game.owner),
1421 library: Some(game.library),
1422 title: Some(game.title),
1423 alternate_titles: Some(game.alternate_titles),
1424 series: Some(game.series),
1425 developer: Some(game.developer),
1426 publisher: Some(game.publisher),
1427 primary_platform: Some(game.primary_platform),
1428 platforms: Some(new_plats),
1429 date_added: Some(game.date_added),
1430 date_modified: Some(game.date_modified),
1431 legacy_broken: Some(game.legacy_broken),
1432 legacy_extreme: Some(game.legacy_extreme),
1433 play_mode: Some(game.play_mode),
1434 status: Some(game.status),
1435 notes: Some(game.notes),
1436 tags: Some(game.tags),
1437 detailed_tags: game.detailed_tags,
1438 source: Some(game.source),
1439 legacy_application_path: Some(game.legacy_application_path),
1440 legacy_launch_command: Some(game.legacy_launch_command),
1441 release_date: Some(game.release_date),
1442 version: Some(game.version),
1443 original_description: Some(game.original_description),
1444 language: Some(game.language),
1445 active_data_id: game.active_data_id,
1446 active_data_on_disk: Some(game.active_data_on_disk),
1447 last_played: game.last_played,
1448 playtime: Some(game.playtime),
1449 play_counter: Some(game.play_counter),
1450 active_game_config_id: game.active_game_config_id,
1451 active_game_config_owner: game.active_game_config_owner,
1452 archive_state: Some(game.archive_state),
1453 add_apps: game.add_apps,
1454 ruffle_support: Some(game.ruffle_support),
1455 logo_path: Some(game.logo_path),
1456 screenshot_path: Some(game.screenshot_path),
1457 ext_data: game.ext_data,
1458 }
1459 }
1460}
1461
1462impl GameData {
1463 fn apply_partial(&mut self, value: &PartialGameData) {
1464 if let Some(id) = value.id {
1465 self.id = id;
1466 }
1467
1468 if let Some(title) = value.title.clone() {
1469 self.title = title;
1470 }
1471
1472 if let Some(data_added) = value.date_added.clone() {
1473 self.date_added = data_added;
1474 }
1475
1476 if let Some(sha256) = value.sha256.clone() {
1477 self.sha256 = sha256;
1478 }
1479
1480 if let Some(crc32) = value.crc32 {
1481 self.crc32 = crc32;
1482 }
1483
1484 if let Some(size) = value.size {
1485 self.size = size;
1486 }
1487
1488 if let Some(present_on_disk) = value.present_on_disk {
1489 self.present_on_disk = present_on_disk;
1490 }
1491
1492 if let Some(path) = value.path.clone() {
1493 self.path = Some(path);
1494 }
1495
1496 if let Some(parameters) = value.parameters.clone() {
1497 self.parameters = Some(parameters);
1498 }
1499
1500 if let Some(application_path) = value.application_path.clone() {
1501 self.application_path = application_path;
1502 }
1503
1504 if let Some(launch_command) = value.launch_command.clone() {
1505 self.launch_command = launch_command;
1506 }
1507
1508 }
1509}
1510
1511impl Default for GameData {
1512 fn default() -> Self {
1513 GameData {
1514 id: -1,
1515 game_id: "".to_owned(),
1516 title: "".to_owned(),
1517 date_added: Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(),
1518 sha256: "".to_owned(),
1519 crc32: 0,
1520 size: 0,
1521 present_on_disk: false,
1522 path: None,
1523 parameters: None,
1524 application_path: "".to_owned(),
1525 launch_command: "".to_owned(),
1526 }
1527 }
1528}
1529
1530impl From<&PartialGameData> for GameData {
1531 fn from(value: &PartialGameData) -> Self {
1532 let mut data = GameData {
1533 id: -1,
1534 game_id: value.game_id.clone(),
1535 ..Default::default()
1536 };
1537
1538 data.apply_partial(value);
1539
1540 data
1541 }
1542}