1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4#[derive(Debug, Clone, Deserialize, Serialize)]
6pub struct Firmware {
7 pub id: u64,
9 pub file_name: String,
11 pub file_name_no_tags: String,
13 pub file_name_no_ext: String,
15 pub file_extension: String,
17 pub file_path: String,
19 pub file_size_bytes: u64,
21 pub full_path: String,
23 pub is_verified: bool,
25 pub crc_hash: String,
27 pub md5_hash: String,
29 pub sha1_hash: String,
31 pub missing_from_fs: bool,
33 pub created_at: String,
35 pub updated_at: String,
37}
38
39#[derive(Debug, Clone, Deserialize, Serialize)]
41pub struct Platform {
42 pub id: u64,
44 pub slug: String,
46 pub fs_slug: String,
48 pub rom_count: u64,
50 pub name: String,
52 pub igdb_slug: Option<String>,
54 pub moby_slug: Option<String>,
56 pub hltb_slug: Option<String>,
58 pub custom_name: Option<String>,
60 pub igdb_id: Option<i64>,
62 pub sgdb_id: Option<i64>,
64 pub moby_id: Option<i64>,
66 pub launchbox_id: Option<i64>,
68 pub ss_id: Option<i64>,
70 pub ra_id: Option<i64>,
72 pub hasheous_id: Option<i64>,
74 pub tgdb_id: Option<i64>,
76 pub flashpoint_id: Option<i64>,
78 pub category: Option<String>,
80 pub generation: Option<i64>,
82 pub family_name: Option<String>,
84 pub family_slug: Option<String>,
86 pub url: Option<String>,
88 pub url_logo: Option<String>,
90 pub firmware: Vec<Firmware>,
92 pub aspect_ratio: Option<String>,
94 pub created_at: String,
96 pub updated_at: String,
98 pub fs_size_bytes: u64,
100 pub is_unidentified: bool,
102 pub is_identified: bool,
104 pub missing_from_fs: bool,
106 pub display_name: Option<String>,
108}
109
110#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash)]
112#[serde(rename_all = "lowercase")]
113pub enum RomFileCategory {
114 Game,
115 Dlc,
116 Hack,
117 Manual,
118 Patch,
119 Update,
120 Mod,
121 Demo,
122 Translation,
123 Prototype,
124 Cheat,
125}
126
127#[derive(Debug, Clone, Deserialize, Serialize)]
129pub struct RomFile {
130 pub id: u64,
131 pub rom_id: u64,
132 pub file_name: String,
133 pub file_path: String,
134 pub file_size_bytes: u64,
135 #[serde(default)]
136 pub category: Option<RomFileCategory>,
137}
138
139#[derive(Debug, Clone, Deserialize, Serialize)]
141pub struct Rom {
142 pub id: u64,
144 pub platform_id: u64,
146 pub platform_slug: Option<String>,
148 pub platform_fs_slug: Option<String>,
150 pub platform_custom_name: Option<String>,
152 pub platform_display_name: Option<String>,
154 pub fs_name: String,
156 pub fs_name_no_tags: String,
158 pub fs_name_no_ext: String,
160 pub fs_extension: String,
162 pub fs_path: String,
164 pub fs_size_bytes: u64,
166 pub name: String,
168 pub slug: Option<String>,
170 pub summary: Option<String>,
172 pub path_cover_small: Option<String>,
174 pub path_cover_large: Option<String>,
176 pub url_cover: Option<String>,
178 #[serde(default)]
180 pub has_manual: bool,
181 #[serde(default)]
183 pub path_manual: Option<String>,
184 #[serde(default)]
186 pub url_manual: Option<String>,
187 pub is_unidentified: bool,
189 pub is_identified: bool,
191 #[serde(default)]
193 pub files: Vec<RomFile>,
194}
195
196#[derive(Debug, Clone, Deserialize, Serialize)]
198pub struct RomList {
199 pub items: Vec<Rom>,
201 pub total: u64,
203 pub limit: u64,
205 pub offset: u64,
207}
208
209#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
211pub struct SaveMetadata {
212 pub id: u64,
213 #[serde(default, alias = "filename", alias = "name")]
214 pub file_name: String,
215 #[serde(default)]
216 pub emulator: Option<String>,
217 #[serde(default)]
218 pub slot: Option<String>,
219 #[serde(default, alias = "updated")]
220 pub updated_at: Option<String>,
221 #[serde(default, alias = "sha256", alias = "content_hash")]
222 pub hash: Option<String>,
223 #[serde(default, alias = "file_size_bytes", alias = "size")]
224 pub size_bytes: Option<u64>,
225 #[serde(default)]
226 pub device_id: Option<String>,
227 #[serde(default)]
228 pub device_name: Option<String>,
229}
230
231impl SaveMetadata {
232 pub fn from_api_value(value: Value) -> anyhow::Result<Vec<Self>> {
233 let rows = value
234 .get("items")
235 .or_else(|| value.get("saves"))
236 .cloned()
237 .unwrap_or(value);
238 Ok(serde_json::from_value(rows)?)
239 }
240}
241
242#[derive(Debug, Clone, Deserialize, Serialize)]
244pub struct VirtualCollectionRow {
245 pub id: String,
246 pub name: String,
247 #[serde(rename = "type")]
248 pub collection_type: String,
249 #[serde(default)]
250 pub rom_count: u64,
251 #[serde(default)]
252 pub is_virtual: bool,
253}
254
255impl From<VirtualCollectionRow> for Collection {
256 fn from(v: VirtualCollectionRow) -> Self {
257 Self {
258 id: 0,
259 name: v.name,
260 collection_type: Some(v.collection_type),
261 rom_count: Some(v.rom_count),
262 is_smart: false,
263 is_virtual: true,
264 virtual_id: Some(v.id),
265 }
266 }
267}
268
269#[derive(Debug, Clone, Deserialize, Serialize)]
274pub struct Collection {
275 pub id: u64,
276 pub name: String,
277 #[serde(rename = "type")]
278 pub collection_type: Option<String>,
279 pub rom_count: Option<u64>,
280 #[serde(default)]
282 pub is_smart: bool,
283 #[serde(default)]
285 pub is_virtual: bool,
286 #[serde(default)]
287 pub virtual_id: Option<String>,
288}
289
290#[cfg(test)]
291mod rom_files_serde_tests {
292 use super::{Rom, RomFile, RomFileCategory};
293 use serde_json::json;
294
295 fn minimal_rom_json() -> serde_json::Value {
296 json!({
297 "id": 1,
298 "platform_id": 2,
299 "platform_slug": null,
300 "platform_fs_slug": null,
301 "platform_custom_name": null,
302 "platform_display_name": null,
303 "fs_name": "game.nsp",
304 "fs_name_no_tags": "game",
305 "fs_name_no_ext": "game",
306 "fs_extension": "nsp",
307 "fs_path": "/game.nsp",
308 "fs_size_bytes": 100,
309 "name": "Game",
310 "slug": null,
311 "summary": null,
312 "path_cover_small": null,
313 "path_cover_large": null,
314 "url_cover": null,
315 "has_manual": false,
316 "path_manual": null,
317 "url_manual": null,
318 "is_unidentified": false,
319 "is_identified": true
320 })
321 }
322
323 #[test]
324 fn rom_deserializes_empty_files_when_field_missing() {
325 let rom: Rom = serde_json::from_value(minimal_rom_json()).expect("rom");
326 assert!(rom.files.is_empty());
327 }
328
329 #[test]
330 fn rom_deserializes_when_manual_fields_missing() {
331 let mut v = minimal_rom_json();
332 let obj = v.as_object_mut().expect("object");
333 obj.remove("has_manual");
334 obj.remove("path_manual");
335 obj.remove("url_manual");
336
337 let rom: Rom = serde_json::from_value(v).expect("rom");
338 assert!(!rom.has_manual);
339 assert_eq!(rom.path_manual, None);
340 assert_eq!(rom.url_manual, None);
341 }
342
343 #[test]
344 fn rom_deserializes_files_array() {
345 let mut v = minimal_rom_json();
346 v["files"] = json!([
347 {
348 "id": 10,
349 "rom_id": 1,
350 "file_name": "base.nsp",
351 "file_path": "/base.nsp",
352 "file_size_bytes": 60,
353 "category": "game"
354 },
355 {
356 "id": 11,
357 "rom_id": 1,
358 "file_name": "upd.nsp",
359 "file_path": "/upd.nsp",
360 "file_size_bytes": 40,
361 "category": "update"
362 }
363 ]);
364 let rom: Rom = serde_json::from_value(v).expect("rom");
365 assert_eq!(rom.files.len(), 2);
366 assert_eq!(rom.files[0].category, Some(RomFileCategory::Game));
367 assert_eq!(rom.files[1].category, Some(RomFileCategory::Update));
368 }
369
370 #[test]
371 fn rom_file_category_none_deserializes() {
372 let f: RomFile = serde_json::from_value(json!({
373 "id": 1,
374 "rom_id": 2,
375 "file_name": "x.bin",
376 "file_path": "/x.bin",
377 "file_size_bytes": 1
378 }))
379 .expect("romfile");
380 assert_eq!(f.category, None);
381 }
382}