romm-api 1.2.0

HTTP client and domain logic for the RomM API
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
use serde::{Deserialize, Serialize};
use serde_json::Value;

pub mod achievements;
pub mod metadata;
pub mod user;

pub use achievements::{
    AchievementRow, EarnedAchievement, MergedRaMetadata, RaAchievement, RaUserGameProgression,
    RaUserProgression,
};
pub use metadata::{RomMatchFields, RomUpdateResponse, SearchCover, SearchRom, SgdbResource};
pub use user::CurrentUser;

/// Represents a firmware file associated with a platform.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Firmware {
    /// Unique identifier for the firmware.
    pub id: u64,
    /// Original file name of the firmware.
    pub file_name: String,
    /// File name without RomM tags.
    pub file_name_no_tags: String,
    /// File name without extension.
    pub file_name_no_ext: String,
    /// File extension (e.g., ".bin").
    pub file_extension: String,
    /// Relative file path within the RomM storage.
    pub file_path: String,
    /// File size in bytes.
    pub file_size_bytes: u64,
    /// Full absolute path to the file.
    pub full_path: String,
    /// Whether the firmware hash has been verified against a database.
    pub is_verified: bool,
    /// CRC32 hash of the file.
    pub crc_hash: String,
    /// MD5 hash of the file.
    pub md5_hash: String,
    /// SHA1 hash of the file.
    pub sha1_hash: String,
    /// True if the file is missing from the filesystem.
    pub missing_from_fs: bool,
    /// ISO 8601 creation timestamp.
    pub created_at: String,
    /// ISO 8601 update timestamp.
    pub updated_at: String,
}

/// A gaming platform (console or system) supported by RomM.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Platform {
    /// Unique identifier for the platform.
    pub id: u64,
    /// URL-friendly slug (e.g., "nes").
    pub slug: String,
    /// Filesystem-friendly slug used for directory naming.
    pub fs_slug: String,
    /// Total number of ROMs assigned to this platform.
    pub rom_count: u64,
    /// Canonical name of the platform.
    pub name: String,
    /// IGDB slug for metadata lookup.
    pub igdb_slug: Option<String>,
    /// MobyGames slug for metadata lookup.
    pub moby_slug: Option<String>,
    /// HowLongToBeat slug for metadata lookup.
    pub hltb_slug: Option<String>,
    /// Custom user-defined name for the platform.
    pub custom_name: Option<String>,
    /// IGDB ID for metadata lookup.
    pub igdb_id: Option<i64>,
    /// ScreenScraper ID for metadata lookup.
    pub sgdb_id: Option<i64>,
    /// MobyGames ID for metadata lookup.
    pub moby_id: Option<i64>,
    /// LaunchBox ID for metadata lookup.
    pub launchbox_id: Option<i64>,
    /// ScreenScraper ID for metadata lookup.
    pub ss_id: Option<i64>,
    /// RetroAchievements ID for metadata lookup.
    pub ra_id: Option<i64>,
    /// Hasheous ID for metadata lookup.
    pub hasheous_id: Option<i64>,
    /// The Games DB ID for metadata lookup.
    pub tgdb_id: Option<i64>,
    /// Flashpoint ID for metadata lookup.
    pub flashpoint_id: Option<i64>,
    /// Category of the platform (e.g., "Console", "Handheld").
    pub category: Option<String>,
    /// Console generation (e.g., 3).
    pub generation: Option<i64>,
    /// Name of the platform family (e.g., "Nintendo").
    pub family_name: Option<String>,
    /// Slug of the platform family (e.g., "nintendo").
    pub family_slug: Option<String>,
    /// Official website URL.
    pub url: Option<String>,
    /// URL to the platform logo image.
    pub url_logo: Option<String>,
    /// List of firmware files required or associated with this platform.
    pub firmware: Vec<Firmware>,
    /// Preferred aspect ratio for the platform.
    pub aspect_ratio: Option<String>,
    /// ISO 8601 creation timestamp.
    pub created_at: String,
    /// ISO 8601 update timestamp.
    pub updated_at: String,
    /// Total size of all ROMs for this platform in bytes.
    pub fs_size_bytes: u64,
    /// True if the platform is not yet fully identified in the RomM database.
    pub is_unidentified: bool,
    /// True if the platform has been identified and linked to metadata.
    pub is_identified: bool,
    /// True if the platform directory is missing from the filesystem.
    pub missing_from_fs: bool,
    /// Name used for display in the UI (custom name or original name).
    pub display_name: Option<String>,
}

/// Category of an internal file within a multi-file RomM ROM (see `Rom::files`).
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum RomFileCategory {
    Game,
    Dlc,
    Hack,
    Manual,
    Patch,
    Update,
    Mod,
    Demo,
    Translation,
    Prototype,
    Cheat,
}

/// One file row under a RomM `Rom` (base game, update, DLC, etc.).
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RomFile {
    pub id: u64,
    pub rom_id: u64,
    pub file_name: String,
    pub file_path: String,
    pub file_size_bytes: u64,
    #[serde(default)]
    pub category: Option<RomFileCategory>,
}

/// Represents a single ROM file and its associated metadata.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Rom {
    /// Unique identifier for the ROM.
    pub id: u64,
    /// ID of the parent platform.
    pub platform_id: u64,
    /// Slug of the parent platform.
    pub platform_slug: Option<String>,
    /// Filesystem slug of the parent platform.
    pub platform_fs_slug: Option<String>,
    /// Custom name of the parent platform.
    pub platform_custom_name: Option<String>,
    /// Display name of the parent platform.
    pub platform_display_name: Option<String>,
    /// Name of the ROM file on disk.
    pub fs_name: String,
    /// ROM file name without RomM tags.
    pub fs_name_no_tags: String,
    /// ROM file name without extension.
    pub fs_name_no_ext: String,
    /// File extension of the ROM (e.g., ".nes").
    pub fs_extension: String,
    /// Relative path to the ROM file.
    pub fs_path: String,
    /// Size of the ROM file in bytes.
    pub fs_size_bytes: u64,
    /// Canonical name of the game.
    pub name: String,
    /// URL-friendly slug for the game.
    pub slug: Option<String>,
    /// Brief description or summary of the game.
    pub summary: Option<String>,
    /// Path to a small thumbnail cover image.
    pub path_cover_small: Option<String>,
    /// Path to a large cover image.
    pub path_cover_large: Option<String>,
    /// Original URL of the cover image.
    pub url_cover: Option<String>,
    /// True if the ROM has an associated manual.
    #[serde(default)]
    pub has_manual: bool,
    /// Path to the manual file.
    #[serde(default)]
    pub path_manual: Option<String>,
    /// Original URL of the manual file.
    #[serde(default)]
    pub url_manual: Option<String>,
    /// True if the ROM is not yet fully identified.
    pub is_unidentified: bool,
    /// True if the ROM has been identified and linked to metadata.
    pub is_identified: bool,
    /// Internal files for multi-part ROMs (Switch, PS3, etc.); empty for legacy single-file ROMs.
    #[serde(default)]
    pub files: Vec<RomFile>,
    /// RetroAchievements game ID (detail responses only).
    #[serde(default)]
    pub ra_id: Option<i64>,
    /// RetroAchievements catalog for this ROM (detail responses only).
    #[serde(default)]
    pub merged_ra_metadata: Option<MergedRaMetadata>,
}

/// A paginated list of ROMs returned by the API.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RomList {
    /// The list of ROM items in this page.
    pub items: Vec<Rom>,
    /// Total number of ROMs matching the query across all pages.
    pub total: u64,
    /// Maximum number of items returned in this request.
    pub limit: u64,
    /// Number of items skipped from the beginning.
    pub offset: u64,
}

/// Screenshot attached to a save file.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct SaveScreenshot {
    pub id: u64,
    #[serde(default)]
    pub download_path: Option<String>,
    #[serde(default)]
    pub file_name: Option<String>,
}

/// Save metadata returned by RomM `/api/saves`.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct SaveMetadata {
    pub id: u64,
    #[serde(default, alias = "filename", alias = "name")]
    pub file_name: String,
    #[serde(default)]
    pub emulator: Option<String>,
    #[serde(default)]
    pub slot: Option<String>,
    #[serde(default, alias = "updated")]
    pub updated_at: Option<String>,
    #[serde(default, alias = "sha256", alias = "content_hash")]
    pub hash: Option<String>,
    #[serde(default, alias = "file_size_bytes", alias = "size")]
    pub size_bytes: Option<u64>,
    #[serde(default)]
    pub device_id: Option<String>,
    #[serde(default)]
    pub device_name: Option<String>,
    #[serde(default)]
    pub screenshot: Option<SaveScreenshot>,
}

impl SaveMetadata {
    pub fn from_api_value(value: Value) -> anyhow::Result<Vec<Self>> {
        let rows = value
            .get("items")
            .or_else(|| value.get("saves"))
            .cloned()
            .unwrap_or(value);
        Ok(serde_json::from_value(rows)?)
    }
}

/// Response row from [`GET /api/collections/virtual`](crate::endpoints::collections::ListVirtualCollections).
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct VirtualCollectionRow {
    pub id: String,
    pub name: String,
    #[serde(rename = "type")]
    pub collection_type: String,
    #[serde(default)]
    pub rom_count: u64,
    #[serde(default)]
    pub is_virtual: bool,
}

impl From<VirtualCollectionRow> for Collection {
    fn from(v: VirtualCollectionRow) -> Self {
        Self {
            id: 0,
            name: v.name,
            collection_type: Some(v.collection_type),
            rom_count: Some(v.rom_count),
            is_smart: false,
            is_virtual: true,
            virtual_id: Some(v.id),
        }
    }
}

/// Manual / smart / virtual row for the library collections pane.
///
/// Virtual (autogenerated) collections use string IDs from RomM; see [`virtual_id`](Self::virtual_id)
/// and [`is_virtual`](Self::is_virtual). Numeric [`id`](Self::id) is unused (0) for virtual rows.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Collection {
    pub id: u64,
    pub name: String,
    #[serde(rename = "type")]
    pub collection_type: Option<String>,
    pub rom_count: Option<u64>,
    /// Smart collections are listed separately by RomM; used for ROM filter/cache keys.
    #[serde(default)]
    pub is_smart: bool,
    /// Autogenerated / virtual collections from `GET /api/collections/virtual`.
    #[serde(default)]
    pub is_virtual: bool,
    #[serde(default)]
    pub virtual_id: Option<String>,
}

#[cfg(test)]
mod rom_files_serde_tests {
    use super::{Rom, RomFile, RomFileCategory};
    use serde_json::json;

    fn minimal_rom_json() -> serde_json::Value {
        json!({
            "id": 1,
            "platform_id": 2,
            "platform_slug": null,
            "platform_fs_slug": null,
            "platform_custom_name": null,
            "platform_display_name": null,
            "fs_name": "game.nsp",
            "fs_name_no_tags": "game",
            "fs_name_no_ext": "game",
            "fs_extension": "nsp",
            "fs_path": "/game.nsp",
            "fs_size_bytes": 100,
            "name": "Game",
            "slug": null,
            "summary": null,
            "path_cover_small": null,
            "path_cover_large": null,
            "url_cover": null,
            "has_manual": false,
            "path_manual": null,
            "url_manual": null,
            "is_unidentified": false,
            "is_identified": true
        })
    }

    #[test]
    fn rom_deserializes_empty_files_when_field_missing() {
        let rom: Rom = serde_json::from_value(minimal_rom_json()).expect("rom");
        assert!(rom.files.is_empty());
    }

    #[test]
    fn rom_deserializes_when_manual_fields_missing() {
        let mut v = minimal_rom_json();
        let obj = v.as_object_mut().expect("object");
        obj.remove("has_manual");
        obj.remove("path_manual");
        obj.remove("url_manual");

        let rom: Rom = serde_json::from_value(v).expect("rom");
        assert!(!rom.has_manual);
        assert_eq!(rom.path_manual, None);
        assert_eq!(rom.url_manual, None);
    }

    #[test]
    fn rom_deserializes_files_array() {
        let mut v = minimal_rom_json();
        v["files"] = json!([
            {
                "id": 10,
                "rom_id": 1,
                "file_name": "base.nsp",
                "file_path": "/base.nsp",
                "file_size_bytes": 60,
                "category": "game"
            },
            {
                "id": 11,
                "rom_id": 1,
                "file_name": "upd.nsp",
                "file_path": "/upd.nsp",
                "file_size_bytes": 40,
                "category": "update"
            }
        ]);
        let rom: Rom = serde_json::from_value(v).expect("rom");
        assert_eq!(rom.files.len(), 2);
        assert_eq!(rom.files[0].category, Some(RomFileCategory::Game));
        assert_eq!(rom.files[1].category, Some(RomFileCategory::Update));
    }

    #[test]
    fn rom_file_category_none_deserializes() {
        let f: RomFile = serde_json::from_value(json!({
            "id": 1,
            "rom_id": 2,
            "file_name": "x.bin",
            "file_path": "/x.bin",
            "file_size_bytes": 1
        }))
        .expect("romfile");
        assert_eq!(f.category, None);
    }
}