1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
6pub struct SearchRom {
7 pub name: String,
8 #[serde(default)]
9 pub slug: Option<String>,
10 #[serde(default)]
11 pub summary: Option<String>,
12 pub platform_id: u64,
13 #[serde(default)]
15 pub id: Option<i64>,
16 #[serde(default)]
17 pub igdb_id: Option<i64>,
18 #[serde(default)]
19 pub moby_id: Option<i64>,
20 #[serde(default)]
21 pub ss_id: Option<i64>,
22 #[serde(default)]
23 pub launchbox_id: Option<i64>,
24 #[serde(default)]
25 pub flashpoint_id: Option<String>,
26 #[serde(default)]
27 pub sgdb_id: Option<i64>,
28 #[serde(default)]
29 pub libretro_id: Option<String>,
30 #[serde(default)]
31 pub is_identified: bool,
32 #[serde(default)]
33 pub is_unidentified: bool,
34 #[serde(default)]
35 pub igdb_url_cover: Option<String>,
36 #[serde(default)]
37 pub ss_url_cover: Option<String>,
38 #[serde(default)]
39 pub moby_url_cover: Option<String>,
40 #[serde(flatten)]
41 pub extra: Value,
42}
43
44#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
46pub struct RomMatchFields {
47 pub igdb_id: Option<i64>,
48 pub moby_id: Option<i64>,
49 pub ss_id: Option<i64>,
50 pub launchbox_id: Option<i64>,
51 pub flashpoint_id: Option<String>,
52 pub sgdb_id: Option<i64>,
53 pub ra_id: Option<i64>,
54 pub hasheous_id: Option<i64>,
55 pub tgdb_id: Option<i64>,
56 pub hltb_id: Option<i64>,
57 pub libretro_id: Option<String>,
58}
59
60impl SearchRom {
61 pub fn primary_match_fields(&self) -> RomMatchFields {
63 RomMatchFields {
64 igdb_id: self.igdb_id.or(self.id),
65 moby_id: self.moby_id,
66 ss_id: self.ss_id,
67 launchbox_id: self.launchbox_id,
68 flashpoint_id: self.flashpoint_id.clone(),
69 sgdb_id: self.sgdb_id,
70 libretro_id: self.libretro_id.clone(),
71 ..Default::default()
72 }
73 }
74
75 pub fn best_url_cover(&self) -> Option<String> {
77 [
78 self.igdb_url_cover.as_deref(),
79 self.ss_url_cover.as_deref(),
80 self.moby_url_cover.as_deref(),
81 ]
82 .into_iter()
83 .flatten()
84 .find(|url| !url.is_empty())
85 .map(str::to_string)
86 }
87}
88
89impl RomMatchFields {
90 pub fn is_empty(&self) -> bool {
91 self.igdb_id.is_none()
92 && self.moby_id.is_none()
93 && self.ss_id.is_none()
94 && self.launchbox_id.is_none()
95 && self.flashpoint_id.is_none()
96 && self.sgdb_id.is_none()
97 && self.ra_id.is_none()
98 && self.hasheous_id.is_none()
99 && self.tgdb_id.is_none()
100 && self.hltb_id.is_none()
101 && self.libretro_id.is_none()
102 }
103}
104
105#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
107pub struct SearchCover {
108 pub name: String,
109 #[serde(default)]
110 pub resources: Vec<SgdbResource>,
111}
112
113#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
114pub struct SgdbResource {
115 #[serde(default)]
116 pub width: Option<u32>,
117 #[serde(default)]
118 pub height: Option<u32>,
119 #[serde(default)]
120 pub url: Option<String>,
121 #[serde(flatten)]
122 pub extra: Value,
123}
124
125#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
127pub struct RomUpdateResponse {
128 pub id: u64,
129 pub platform_id: u64,
130 #[serde(default)]
131 pub name: Option<String>,
132 #[serde(default)]
133 pub summary: Option<String>,
134 #[serde(default)]
135 pub url_cover: Option<String>,
136 #[serde(default)]
137 pub path_cover_small: Option<String>,
138 #[serde(default)]
139 pub path_cover_large: Option<String>,
140 #[serde(default)]
141 pub is_identified: bool,
142 #[serde(default)]
143 pub is_unidentified: bool,
144 #[serde(flatten)]
145 pub extra: Value,
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn search_row_apply_fields_includes_name_summary_and_cover() {
154 let row: SearchRom = serde_json::from_str(
155 r#"{
156 "name": "Super Mario Bros.",
157 "summary": "A platformer.",
158 "platform_id": 1,
159 "igdb_id": 1234,
160 "igdb_url_cover": "https://example.com/cover.jpg",
161 "is_identified": true,
162 "is_unidentified": false
163 }"#,
164 )
165 .unwrap();
166 let fields = crate::core::metadata::search_row_apply_fields(&row);
167 assert_eq!(fields.name.as_deref(), Some("Super Mario Bros."));
168 assert_eq!(fields.summary.as_deref(), Some("A platformer."));
169 assert_eq!(
170 fields.url_cover.as_deref(),
171 Some("https://example.com/cover.jpg")
172 );
173 assert_eq!(fields.match_fields.igdb_id, Some(1234));
174 }
175
176 #[test]
177 fn primary_match_fields_falls_back_to_id() {
178 let row: SearchRom = serde_json::from_str(
179 r#"{"name": "Zelda", "platform_id": 1, "id": 999, "igdb_id": null}"#,
180 )
181 .unwrap();
182 assert_eq!(row.primary_match_fields().igdb_id, Some(999));
183 }
184
185 #[test]
186 fn search_rom_deserializes_demo_shape() {
187 let json = r#"{
188 "name": "Super Mario Bros.",
189 "slug": "super-mario-bros",
190 "summary": "A platformer.",
191 "platform_id": 1,
192 "igdb_id": 1234,
193 "ss_id": null,
194 "moby_id": null,
195 "is_identified": true,
196 "is_unidentified": false,
197 "igdb_url_cover": "https://example.com/cover.jpg"
198 }"#;
199 let row: SearchRom = serde_json::from_str(json).unwrap();
200 assert_eq!(row.name, "Super Mario Bros.");
201 assert_eq!(row.igdb_id, Some(1234));
202 assert_eq!(row.primary_match_fields().igdb_id, Some(1234));
203 }
204}