Skip to main content

romm_api/
types.rs

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