Skip to main content

romm_api/
types.rs

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/// Represents a firmware file associated with a platform.
16#[derive(Debug, Clone, Deserialize, Serialize)]
17pub struct Firmware {
18    /// Unique identifier for the firmware.
19    pub id: u64,
20    /// Original file name of the firmware.
21    pub file_name: String,
22    /// File name without RomM tags.
23    pub file_name_no_tags: String,
24    /// File name without extension.
25    pub file_name_no_ext: String,
26    /// File extension (e.g., ".bin").
27    pub file_extension: String,
28    /// Relative file path within the RomM storage.
29    pub file_path: String,
30    /// File size in bytes.
31    pub file_size_bytes: u64,
32    /// Full absolute path to the file.
33    pub full_path: String,
34    /// Whether the firmware hash has been verified against a database.
35    pub is_verified: bool,
36    /// CRC32 hash of the file.
37    pub crc_hash: String,
38    /// MD5 hash of the file.
39    pub md5_hash: String,
40    /// SHA1 hash of the file.
41    pub sha1_hash: String,
42    /// True if the file is missing from the filesystem.
43    pub missing_from_fs: bool,
44    /// ISO 8601 creation timestamp.
45    pub created_at: String,
46    /// ISO 8601 update timestamp.
47    pub updated_at: String,
48}
49
50/// A gaming platform (console or system) supported by RomM.
51#[derive(Debug, Clone, Deserialize, Serialize)]
52pub struct Platform {
53    /// Unique identifier for the platform.
54    pub id: u64,
55    /// URL-friendly slug (e.g., "nes").
56    pub slug: String,
57    /// Filesystem-friendly slug used for directory naming.
58    pub fs_slug: String,
59    /// Total number of ROMs assigned to this platform.
60    pub rom_count: u64,
61    /// Canonical name of the platform.
62    pub name: String,
63    /// IGDB slug for metadata lookup.
64    pub igdb_slug: Option<String>,
65    /// MobyGames slug for metadata lookup.
66    pub moby_slug: Option<String>,
67    /// HowLongToBeat slug for metadata lookup.
68    pub hltb_slug: Option<String>,
69    /// Custom user-defined name for the platform.
70    pub custom_name: Option<String>,
71    /// IGDB ID for metadata lookup.
72    pub igdb_id: Option<i64>,
73    /// ScreenScraper ID for metadata lookup.
74    pub sgdb_id: Option<i64>,
75    /// MobyGames ID for metadata lookup.
76    pub moby_id: Option<i64>,
77    /// LaunchBox ID for metadata lookup.
78    pub launchbox_id: Option<i64>,
79    /// ScreenScraper ID for metadata lookup.
80    pub ss_id: Option<i64>,
81    /// RetroAchievements ID for metadata lookup.
82    pub ra_id: Option<i64>,
83    /// Hasheous ID for metadata lookup.
84    pub hasheous_id: Option<i64>,
85    /// The Games DB ID for metadata lookup.
86    pub tgdb_id: Option<i64>,
87    /// Flashpoint ID for metadata lookup.
88    pub flashpoint_id: Option<i64>,
89    /// Category of the platform (e.g., "Console", "Handheld").
90    pub category: Option<String>,
91    /// Console generation (e.g., 3).
92    pub generation: Option<i64>,
93    /// Name of the platform family (e.g., "Nintendo").
94    pub family_name: Option<String>,
95    /// Slug of the platform family (e.g., "nintendo").
96    pub family_slug: Option<String>,
97    /// Official website URL.
98    pub url: Option<String>,
99    /// URL to the platform logo image.
100    pub url_logo: Option<String>,
101    /// List of firmware files required or associated with this platform.
102    pub firmware: Vec<Firmware>,
103    /// Preferred aspect ratio for the platform.
104    pub aspect_ratio: Option<String>,
105    /// ISO 8601 creation timestamp.
106    pub created_at: String,
107    /// ISO 8601 update timestamp.
108    pub updated_at: String,
109    /// Total size of all ROMs for this platform in bytes.
110    pub fs_size_bytes: u64,
111    /// True if the platform is not yet fully identified in the RomM database.
112    pub is_unidentified: bool,
113    /// True if the platform has been identified and linked to metadata.
114    pub is_identified: bool,
115    /// True if the platform directory is missing from the filesystem.
116    pub missing_from_fs: bool,
117    /// Name used for display in the UI (custom name or original name).
118    pub display_name: Option<String>,
119}
120
121/// Category of an internal file within a multi-file RomM ROM (see `Rom::files`).
122#[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/// One file row under a RomM `Rom` (base game, update, DLC, etc.).
139#[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/// Represents a single ROM file and its associated metadata.
151#[derive(Debug, Clone, Deserialize, Serialize)]
152pub struct Rom {
153    /// Unique identifier for the ROM.
154    pub id: u64,
155    /// ID of the parent platform.
156    pub platform_id: u64,
157    /// Slug of the parent platform.
158    pub platform_slug: Option<String>,
159    /// Filesystem slug of the parent platform.
160    pub platform_fs_slug: Option<String>,
161    /// Custom name of the parent platform.
162    pub platform_custom_name: Option<String>,
163    /// Display name of the parent platform.
164    pub platform_display_name: Option<String>,
165    /// Name of the ROM file on disk.
166    pub fs_name: String,
167    /// ROM file name without RomM tags.
168    pub fs_name_no_tags: String,
169    /// ROM file name without extension.
170    pub fs_name_no_ext: String,
171    /// File extension of the ROM (e.g., ".nes").
172    pub fs_extension: String,
173    /// Relative path to the ROM file.
174    pub fs_path: String,
175    /// Size of the ROM file in bytes.
176    pub fs_size_bytes: u64,
177    /// Canonical name of the game.
178    pub name: String,
179    /// URL-friendly slug for the game.
180    pub slug: Option<String>,
181    /// Brief description or summary of the game.
182    pub summary: Option<String>,
183    /// Path to a small thumbnail cover image.
184    pub path_cover_small: Option<String>,
185    /// Path to a large cover image.
186    pub path_cover_large: Option<String>,
187    /// Original URL of the cover image.
188    pub url_cover: Option<String>,
189    /// True if the ROM has an associated manual.
190    #[serde(default)]
191    pub has_manual: bool,
192    /// Path to the manual file.
193    #[serde(default)]
194    pub path_manual: Option<String>,
195    /// Original URL of the manual file.
196    #[serde(default)]
197    pub url_manual: Option<String>,
198    /// True if the ROM is not yet fully identified.
199    pub is_unidentified: bool,
200    /// True if the ROM has been identified and linked to metadata.
201    pub is_identified: bool,
202    /// Internal files for multi-part ROMs (Switch, PS3, etc.); empty for legacy single-file ROMs.
203    #[serde(default)]
204    pub files: Vec<RomFile>,
205    /// RetroAchievements game ID (detail responses only).
206    #[serde(default)]
207    pub ra_id: Option<i64>,
208    /// RetroAchievements catalog for this ROM (detail responses only).
209    #[serde(default)]
210    pub merged_ra_metadata: Option<MergedRaMetadata>,
211}
212
213/// A paginated list of ROMs returned by the API.
214#[derive(Debug, Clone, Deserialize, Serialize)]
215pub struct RomList {
216    /// The list of ROM items in this page.
217    pub items: Vec<Rom>,
218    /// Total number of ROMs matching the query across all pages.
219    pub total: u64,
220    /// Maximum number of items returned in this request.
221    pub limit: u64,
222    /// Number of items skipped from the beginning.
223    pub offset: u64,
224}
225
226/// Screenshot attached to a save file.
227#[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/// Save metadata returned by RomM `/api/saves`.
237#[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/// Response row from [`GET /api/collections/virtual`](crate::endpoints::collections::ListVirtualCollections).
272#[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/// Manual / smart / virtual row for the library collections pane.
299///
300/// Virtual (autogenerated) collections use string IDs from RomM; see [`virtual_id`](Self::virtual_id)
301/// and [`is_virtual`](Self::is_virtual). Numeric [`id`](Self::id) is unused (0) for virtual rows.
302#[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    /// Smart collections are listed separately by RomM; used for ROM filter/cache keys.
310    #[serde(default)]
311    pub is_smart: bool,
312    /// Autogenerated / virtual collections from `GET /api/collections/virtual`.
313    #[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}