1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4pub mod achievements;
5pub mod metadata;
6pub mod user;
7
8pub use achievements::{
9 AchievementRow, EarnedAchievement, MergedRaMetadata, RaAchievement, RaUserGameProgression,
10 RaUserProgression,
11};
12pub use metadata::{RomMatchFields, RomUpdateResponse, SearchCover, SearchRom, SgdbResource};
13pub use user::CurrentUser;
14
15#[derive(Debug, Clone, Deserialize, Serialize)]
17pub struct Firmware {
18 pub id: u64,
20 pub file_name: String,
22 pub file_name_no_tags: String,
24 pub file_name_no_ext: String,
26 pub file_extension: String,
28 pub file_path: String,
30 pub file_size_bytes: u64,
32 pub full_path: String,
34 pub is_verified: bool,
36 pub crc_hash: String,
38 pub md5_hash: String,
40 pub sha1_hash: String,
42 pub missing_from_fs: bool,
44 pub created_at: String,
46 pub updated_at: String,
48}
49
50#[derive(Debug, Clone, Deserialize, Serialize)]
52pub struct Platform {
53 pub id: u64,
55 pub slug: String,
57 pub fs_slug: String,
59 pub rom_count: u64,
61 pub name: String,
63 pub igdb_slug: Option<String>,
65 pub moby_slug: Option<String>,
67 pub hltb_slug: Option<String>,
69 pub custom_name: Option<String>,
71 pub igdb_id: Option<i64>,
73 pub sgdb_id: Option<i64>,
75 pub moby_id: Option<i64>,
77 pub launchbox_id: Option<i64>,
79 pub ss_id: Option<i64>,
81 pub ra_id: Option<i64>,
83 pub hasheous_id: Option<i64>,
85 pub tgdb_id: Option<i64>,
87 pub flashpoint_id: Option<i64>,
89 pub category: Option<String>,
91 pub generation: Option<i64>,
93 pub family_name: Option<String>,
95 pub family_slug: Option<String>,
97 pub url: Option<String>,
99 pub url_logo: Option<String>,
101 pub firmware: Vec<Firmware>,
103 pub aspect_ratio: Option<String>,
105 pub created_at: String,
107 pub updated_at: String,
109 pub fs_size_bytes: u64,
111 pub is_unidentified: bool,
113 pub is_identified: bool,
115 pub missing_from_fs: bool,
117 pub display_name: Option<String>,
119}
120
121#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash)]
123#[serde(rename_all = "lowercase")]
124pub enum RomFileCategory {
125 Game,
126 Dlc,
127 Hack,
128 Manual,
129 Patch,
130 Update,
131 Mod,
132 Demo,
133 Translation,
134 Prototype,
135 Cheat,
136}
137
138#[derive(Debug, Clone, Deserialize, Serialize)]
140pub struct RomFile {
141 pub id: u64,
142 pub rom_id: u64,
143 pub file_name: String,
144 pub file_path: String,
145 pub file_size_bytes: u64,
146 #[serde(default)]
147 pub category: Option<RomFileCategory>,
148}
149
150#[derive(Debug, Clone, Deserialize, Serialize)]
152pub struct Rom {
153 pub id: u64,
155 pub platform_id: u64,
157 pub platform_slug: Option<String>,
159 pub platform_fs_slug: Option<String>,
161 pub platform_custom_name: Option<String>,
163 pub platform_display_name: Option<String>,
165 pub fs_name: String,
167 pub fs_name_no_tags: String,
169 pub fs_name_no_ext: String,
171 pub fs_extension: String,
173 pub fs_path: String,
175 pub fs_size_bytes: u64,
177 pub name: String,
179 pub slug: Option<String>,
181 pub summary: Option<String>,
183 pub path_cover_small: Option<String>,
185 pub path_cover_large: Option<String>,
187 pub url_cover: Option<String>,
189 #[serde(default)]
191 pub has_manual: bool,
192 #[serde(default)]
194 pub path_manual: Option<String>,
195 #[serde(default)]
197 pub url_manual: Option<String>,
198 pub is_unidentified: bool,
200 pub is_identified: bool,
202 #[serde(default)]
204 pub files: Vec<RomFile>,
205 #[serde(default)]
207 pub ra_id: Option<i64>,
208 #[serde(default)]
210 pub merged_ra_metadata: Option<MergedRaMetadata>,
211}
212
213#[derive(Debug, Clone, Deserialize, Serialize)]
215pub struct RomList {
216 pub items: Vec<Rom>,
218 pub total: u64,
220 pub limit: u64,
222 pub offset: u64,
224}
225
226#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
228pub struct SaveScreenshot {
229 pub id: u64,
230 #[serde(default)]
231 pub download_path: Option<String>,
232 #[serde(default)]
233 pub file_name: Option<String>,
234}
235
236#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
238pub struct SaveMetadata {
239 pub id: u64,
240 #[serde(default, alias = "filename", alias = "name")]
241 pub file_name: String,
242 #[serde(default)]
243 pub emulator: Option<String>,
244 #[serde(default)]
245 pub slot: Option<String>,
246 #[serde(default, alias = "updated")]
247 pub updated_at: Option<String>,
248 #[serde(default, alias = "sha256", alias = "content_hash")]
249 pub hash: Option<String>,
250 #[serde(default, alias = "file_size_bytes", alias = "size")]
251 pub size_bytes: Option<u64>,
252 #[serde(default)]
253 pub device_id: Option<String>,
254 #[serde(default)]
255 pub device_name: Option<String>,
256 #[serde(default)]
257 pub screenshot: Option<SaveScreenshot>,
258}
259
260impl SaveMetadata {
261 pub fn from_api_value(value: Value) -> anyhow::Result<Vec<Self>> {
262 let rows = value
263 .get("items")
264 .or_else(|| value.get("saves"))
265 .cloned()
266 .unwrap_or(value);
267 Ok(serde_json::from_value(rows)?)
268 }
269}
270
271#[derive(Debug, Clone, Deserialize, Serialize)]
273pub struct VirtualCollectionRow {
274 pub id: String,
275 pub name: String,
276 #[serde(rename = "type")]
277 pub collection_type: String,
278 #[serde(default)]
279 pub rom_count: u64,
280 #[serde(default)]
281 pub is_virtual: bool,
282}
283
284impl From<VirtualCollectionRow> for Collection {
285 fn from(v: VirtualCollectionRow) -> Self {
286 Self {
287 id: 0,
288 name: v.name,
289 collection_type: Some(v.collection_type),
290 rom_count: Some(v.rom_count),
291 is_smart: false,
292 is_virtual: true,
293 virtual_id: Some(v.id),
294 }
295 }
296}
297
298#[derive(Debug, Clone, Deserialize, Serialize)]
303pub struct Collection {
304 pub id: u64,
305 pub name: String,
306 #[serde(rename = "type")]
307 pub collection_type: Option<String>,
308 pub rom_count: Option<u64>,
309 #[serde(default)]
311 pub is_smart: bool,
312 #[serde(default)]
314 pub is_virtual: bool,
315 #[serde(default)]
316 pub virtual_id: Option<String>,
317}
318
319#[cfg(test)]
320mod rom_files_serde_tests {
321 use super::{Rom, RomFile, RomFileCategory};
322 use serde_json::json;
323
324 fn minimal_rom_json() -> serde_json::Value {
325 json!({
326 "id": 1,
327 "platform_id": 2,
328 "platform_slug": null,
329 "platform_fs_slug": null,
330 "platform_custom_name": null,
331 "platform_display_name": null,
332 "fs_name": "game.nsp",
333 "fs_name_no_tags": "game",
334 "fs_name_no_ext": "game",
335 "fs_extension": "nsp",
336 "fs_path": "/game.nsp",
337 "fs_size_bytes": 100,
338 "name": "Game",
339 "slug": null,
340 "summary": null,
341 "path_cover_small": null,
342 "path_cover_large": null,
343 "url_cover": null,
344 "has_manual": false,
345 "path_manual": null,
346 "url_manual": null,
347 "is_unidentified": false,
348 "is_identified": true
349 })
350 }
351
352 #[test]
353 fn rom_deserializes_empty_files_when_field_missing() {
354 let rom: Rom = serde_json::from_value(minimal_rom_json()).expect("rom");
355 assert!(rom.files.is_empty());
356 }
357
358 #[test]
359 fn rom_deserializes_when_manual_fields_missing() {
360 let mut v = minimal_rom_json();
361 let obj = v.as_object_mut().expect("object");
362 obj.remove("has_manual");
363 obj.remove("path_manual");
364 obj.remove("url_manual");
365
366 let rom: Rom = serde_json::from_value(v).expect("rom");
367 assert!(!rom.has_manual);
368 assert_eq!(rom.path_manual, None);
369 assert_eq!(rom.url_manual, None);
370 }
371
372 #[test]
373 fn rom_deserializes_files_array() {
374 let mut v = minimal_rom_json();
375 v["files"] = json!([
376 {
377 "id": 10,
378 "rom_id": 1,
379 "file_name": "base.nsp",
380 "file_path": "/base.nsp",
381 "file_size_bytes": 60,
382 "category": "game"
383 },
384 {
385 "id": 11,
386 "rom_id": 1,
387 "file_name": "upd.nsp",
388 "file_path": "/upd.nsp",
389 "file_size_bytes": 40,
390 "category": "update"
391 }
392 ]);
393 let rom: Rom = serde_json::from_value(v).expect("rom");
394 assert_eq!(rom.files.len(), 2);
395 assert_eq!(rom.files[0].category, Some(RomFileCategory::Game));
396 assert_eq!(rom.files[1].category, Some(RomFileCategory::Update));
397 }
398
399 #[test]
400 fn rom_file_category_none_deserializes() {
401 let f: RomFile = serde_json::from_value(json!({
402 "id": 1,
403 "rom_id": 2,
404 "file_name": "x.bin",
405 "file_path": "/x.bin",
406 "file_size_bytes": 1
407 }))
408 .expect("romfile");
409 assert_eq!(f.category, None);
410 }
411}