Skip to main content

romm_cli/endpoints/
roms.rs

1use crate::types::RomList;
2
3use super::Endpoint;
4use serde_json::{json, Value};
5
6fn push_bool(q: &mut Vec<(String, String)>, key: &str, v: Option<bool>) {
7    if let Some(b) = v {
8        q.push((key.into(), b.to_string()));
9    }
10}
11
12fn push_str(q: &mut Vec<(String, String)>, key: &str, v: &Option<String>) {
13    if let Some(s) = v {
14        if !s.is_empty() {
15            q.push((key.into(), s.clone()));
16        }
17    }
18}
19
20fn push_str_list(q: &mut Vec<(String, String)>, key: &str, items: &[String]) {
21    for it in items {
22        q.push((key.into(), it.clone()));
23    }
24}
25
26/// Retrieve ROMs with optional filters (`GET /api/roms`).
27#[derive(Debug, Default, Clone)]
28pub struct GetRoms {
29    pub search_term: Option<String>,
30    /// When set, emits one `platform_ids` query entry.
31    pub platform_id: Option<u64>,
32    /// Additional platform IDs (repeat `platform_ids` in the query).
33    pub platform_ids: Vec<u64>,
34    pub collection_id: Option<u64>,
35    pub smart_collection_id: Option<u64>,
36    pub virtual_collection_id: Option<String>,
37    pub matched: Option<bool>,
38    pub favorite: Option<bool>,
39    pub duplicate: Option<bool>,
40    pub last_played: Option<bool>,
41    pub playable: Option<bool>,
42    pub missing: Option<bool>,
43    pub has_ra: Option<bool>,
44    pub verified: Option<bool>,
45    pub group_by_meta_id: Option<bool>,
46    pub genres: Vec<String>,
47    pub franchises: Vec<String>,
48    pub collections: Vec<String>,
49    pub companies: Vec<String>,
50    pub age_ratings: Vec<String>,
51    pub statuses: Vec<String>,
52    pub regions: Vec<String>,
53    pub languages: Vec<String>,
54    pub player_counts: Vec<String>,
55    pub genres_logic: Option<String>,
56    pub franchises_logic: Option<String>,
57    pub collections_logic: Option<String>,
58    pub companies_logic: Option<String>,
59    pub age_ratings_logic: Option<String>,
60    pub regions_logic: Option<String>,
61    pub languages_logic: Option<String>,
62    pub statuses_logic: Option<String>,
63    pub player_counts_logic: Option<String>,
64    pub order_by: Option<String>,
65    pub order_dir: Option<String>,
66    pub updated_after: Option<String>,
67    pub with_char_index: Option<bool>,
68    pub with_filter_values: Option<bool>,
69    pub limit: Option<u32>,
70    pub offset: Option<u32>,
71}
72
73impl Endpoint for GetRoms {
74    type Output = RomList;
75
76    fn method(&self) -> &'static str {
77        "GET"
78    }
79
80    fn path(&self) -> String {
81        "/api/roms".into()
82    }
83
84    fn query(&self) -> Vec<(String, String)> {
85        let mut q = Vec::new();
86
87        if let Some(term) = &self.search_term {
88            q.push(("search_term".into(), term.clone()));
89        }
90
91        let mut seen = std::collections::HashSet::new();
92        for pid in &self.platform_ids {
93            if seen.insert(*pid) {
94                q.push(("platform_ids".into(), pid.to_string()));
95            }
96        }
97        if let Some(pid) = self.platform_id {
98            if seen.insert(pid) {
99                q.push(("platform_ids".into(), pid.to_string()));
100            }
101        }
102
103        if let Some(cid) = self.collection_id {
104            q.push(("collection_id".into(), cid.to_string()));
105        }
106        if let Some(sid) = self.smart_collection_id {
107            q.push(("smart_collection_id".into(), sid.to_string()));
108        }
109        if let Some(ref vid) = self.virtual_collection_id {
110            q.push(("virtual_collection_id".into(), vid.clone()));
111        }
112
113        push_bool(&mut q, "matched", self.matched);
114        push_bool(&mut q, "favorite", self.favorite);
115        push_bool(&mut q, "duplicate", self.duplicate);
116        push_bool(&mut q, "last_played", self.last_played);
117        push_bool(&mut q, "playable", self.playable);
118        push_bool(&mut q, "missing", self.missing);
119        push_bool(&mut q, "has_ra", self.has_ra);
120        push_bool(&mut q, "verified", self.verified);
121        push_bool(&mut q, "group_by_meta_id", self.group_by_meta_id);
122        push_bool(&mut q, "with_char_index", self.with_char_index);
123        push_bool(&mut q, "with_filter_values", self.with_filter_values);
124
125        push_str_list(&mut q, "genres", &self.genres);
126        push_str_list(&mut q, "franchises", &self.franchises);
127        push_str_list(&mut q, "collections", &self.collections);
128        push_str_list(&mut q, "companies", &self.companies);
129        push_str_list(&mut q, "age_ratings", &self.age_ratings);
130        push_str_list(&mut q, "statuses", &self.statuses);
131        push_str_list(&mut q, "regions", &self.regions);
132        push_str_list(&mut q, "languages", &self.languages);
133        push_str_list(&mut q, "player_counts", &self.player_counts);
134
135        push_str(&mut q, "genres_logic", &self.genres_logic);
136        push_str(&mut q, "franchises_logic", &self.franchises_logic);
137        push_str(&mut q, "collections_logic", &self.collections_logic);
138        push_str(&mut q, "companies_logic", &self.companies_logic);
139        push_str(&mut q, "age_ratings_logic", &self.age_ratings_logic);
140        push_str(&mut q, "regions_logic", &self.regions_logic);
141        push_str(&mut q, "languages_logic", &self.languages_logic);
142        push_str(&mut q, "statuses_logic", &self.statuses_logic);
143        push_str(&mut q, "player_counts_logic", &self.player_counts_logic);
144
145        push_str(&mut q, "order_by", &self.order_by);
146        push_str(&mut q, "order_dir", &self.order_dir);
147        push_str(&mut q, "updated_after", &self.updated_after);
148
149        if let Some(limit) = self.limit {
150            q.push(("limit".into(), limit.to_string()));
151        }
152        if let Some(offset) = self.offset {
153            q.push(("offset".into(), offset.to_string()));
154        }
155
156        q
157    }
158}
159
160/// Retrieve a single ROM by ID.
161#[derive(Debug, Clone)]
162pub struct GetRom {
163    pub id: u64,
164}
165
166impl Endpoint for GetRom {
167    type Output = crate::types::Rom;
168
169    fn method(&self) -> &'static str {
170        "GET"
171    }
172
173    fn path(&self) -> String {
174        format!("/api/roms/{}", self.id)
175    }
176}
177
178/// `GET /api/roms/by-hash`
179#[derive(Debug, Default, Clone)]
180pub struct GetRomByHash {
181    pub crc_hash: Option<String>,
182    pub md5_hash: Option<String>,
183    pub sha1_hash: Option<String>,
184}
185
186impl Endpoint for GetRomByHash {
187    type Output = Value;
188
189    fn method(&self) -> &'static str {
190        "GET"
191    }
192
193    fn path(&self) -> String {
194        "/api/roms/by-hash".into()
195    }
196
197    fn query(&self) -> Vec<(String, String)> {
198        let mut q = Vec::new();
199        push_str(&mut q, "crc_hash", &self.crc_hash);
200        push_str(&mut q, "md5_hash", &self.md5_hash);
201        push_str(&mut q, "sha1_hash", &self.sha1_hash);
202        q
203    }
204}
205
206/// `GET /api/roms/by-metadata-provider`
207#[derive(Debug, Default, Clone)]
208pub struct GetRomByMetadataProvider {
209    pub igdb_id: Option<i64>,
210    pub moby_id: Option<i64>,
211    pub ss_id: Option<i64>,
212    pub ra_id: Option<i64>,
213    pub launchbox_id: Option<i64>,
214    pub hasheous_id: Option<i64>,
215    pub tgdb_id: Option<i64>,
216    pub flashpoint_id: Option<String>,
217    pub hltb_id: Option<i64>,
218}
219
220impl Endpoint for GetRomByMetadataProvider {
221    type Output = Value;
222
223    fn method(&self) -> &'static str {
224        "GET"
225    }
226
227    fn path(&self) -> String {
228        "/api/roms/by-metadata-provider".into()
229    }
230
231    fn query(&self) -> Vec<(String, String)> {
232        let mut q = Vec::new();
233        if let Some(v) = self.igdb_id {
234            q.push(("igdb_id".into(), v.to_string()));
235        }
236        if let Some(v) = self.moby_id {
237            q.push(("moby_id".into(), v.to_string()));
238        }
239        if let Some(v) = self.ss_id {
240            q.push(("ss_id".into(), v.to_string()));
241        }
242        if let Some(v) = self.ra_id {
243            q.push(("ra_id".into(), v.to_string()));
244        }
245        if let Some(v) = self.launchbox_id {
246            q.push(("launchbox_id".into(), v.to_string()));
247        }
248        if let Some(v) = self.hasheous_id {
249            q.push(("hasheous_id".into(), v.to_string()));
250        }
251        if let Some(v) = self.tgdb_id {
252            q.push(("tgdb_id".into(), v.to_string()));
253        }
254        push_str(&mut q, "flashpoint_id", &self.flashpoint_id);
255        if let Some(v) = self.hltb_id {
256            q.push(("hltb_id".into(), v.to_string()));
257        }
258        q
259    }
260}
261
262/// `GET /api/roms/filters`
263#[derive(Debug, Default, Clone)]
264pub struct GetRomFilters;
265
266impl Endpoint for GetRomFilters {
267    type Output = Value;
268
269    fn method(&self) -> &'static str {
270        "GET"
271    }
272
273    fn path(&self) -> String {
274        "/api/roms/filters".into()
275    }
276}
277
278/// `POST /api/roms/delete`
279#[derive(Debug, Clone)]
280pub struct DeleteRoms {
281    pub roms: Vec<u64>,
282    pub delete_from_fs: Vec<u64>,
283}
284
285impl Endpoint for DeleteRoms {
286    type Output = Value;
287
288    fn method(&self) -> &'static str {
289        "POST"
290    }
291
292    fn path(&self) -> String {
293        "/api/roms/delete".into()
294    }
295
296    fn body(&self) -> Option<Value> {
297        Some(json!({
298            "roms": self.roms,
299            "delete_from_fs": self.delete_from_fs,
300        }))
301    }
302}
303
304/// `PUT /api/roms/{id}/props` — RomM accepts JSON body plus optional query flags.
305#[derive(Debug, Clone)]
306pub struct PutRomUserProps {
307    pub rom_id: u64,
308    pub body: Value,
309    pub update_last_played: bool,
310    pub remove_last_played: bool,
311}
312
313impl Endpoint for PutRomUserProps {
314    type Output = Value;
315
316    fn method(&self) -> &'static str {
317        "PUT"
318    }
319
320    fn path(&self) -> String {
321        format!("/api/roms/{}/props", self.rom_id)
322    }
323
324    fn query(&self) -> Vec<(String, String)> {
325        let mut q = Vec::new();
326        if self.update_last_played {
327            q.push(("update_last_played".into(), "true".into()));
328        }
329        if self.remove_last_played {
330            q.push(("remove_last_played".into(), "true".into()));
331        }
332        q
333    }
334
335    fn body(&self) -> Option<Value> {
336        Some(self.body.clone())
337    }
338}
339
340/// `GET /api/roms/{id}/notes`
341#[derive(Debug, Clone)]
342pub struct GetRomNotes {
343    pub rom_id: u64,
344    pub public_only: Option<bool>,
345    pub search: Option<String>,
346    pub tags: Vec<String>,
347}
348
349impl Endpoint for GetRomNotes {
350    type Output = Value;
351
352    fn method(&self) -> &'static str {
353        "GET"
354    }
355
356    fn path(&self) -> String {
357        format!("/api/roms/{}/notes", self.rom_id)
358    }
359
360    fn query(&self) -> Vec<(String, String)> {
361        let mut q = Vec::new();
362        push_bool(&mut q, "public_only", self.public_only);
363        push_str(&mut q, "search", &self.search);
364        push_str_list(&mut q, "tags", &self.tags);
365        q
366    }
367}
368
369/// `POST /api/roms/{id}/notes`
370#[derive(Debug, Clone)]
371pub struct PostRomNote {
372    pub rom_id: u64,
373    pub body: Value,
374}
375
376impl Endpoint for PostRomNote {
377    type Output = Value;
378
379    fn method(&self) -> &'static str {
380        "POST"
381    }
382
383    fn path(&self) -> String {
384        format!("/api/roms/{}/notes", self.rom_id)
385    }
386
387    fn body(&self) -> Option<Value> {
388        Some(self.body.clone())
389    }
390}
391
392/// `PUT /api/roms/{id}/notes/{note_id}`
393#[derive(Debug, Clone)]
394pub struct PutRomNote {
395    pub rom_id: u64,
396    pub note_id: u64,
397    pub body: Value,
398}
399
400impl Endpoint for PutRomNote {
401    type Output = Value;
402
403    fn method(&self) -> &'static str {
404        "PUT"
405    }
406
407    fn path(&self) -> String {
408        format!("/api/roms/{}/notes/{}", self.rom_id, self.note_id)
409    }
410
411    fn body(&self) -> Option<Value> {
412        Some(self.body.clone())
413    }
414}
415
416/// `DELETE /api/roms/{id}/notes/{note_id}`
417#[derive(Debug, Clone)]
418pub struct DeleteRomNote {
419    pub rom_id: u64,
420    pub note_id: u64,
421}
422
423impl Endpoint for DeleteRomNote {
424    type Output = Value;
425
426    fn method(&self) -> &'static str {
427        "DELETE"
428    }
429
430    fn path(&self) -> String {
431        format!("/api/roms/{}/notes/{}", self.rom_id, self.note_id)
432    }
433}
434
435/// `GET /api/search/cover`
436#[derive(Debug, Clone)]
437pub struct GetSearchCover {
438    pub search_term: String,
439}
440
441impl Endpoint for GetSearchCover {
442    type Output = Value;
443
444    fn method(&self) -> &'static str {
445        "GET"
446    }
447
448    fn path(&self) -> String {
449        "/api/search/cover".into()
450    }
451
452    fn query(&self) -> Vec<(String, String)> {
453        vec![("search_term".into(), self.search_term.clone())]
454    }
455}
456
457/// `GET /api/search/roms`
458#[derive(Debug, Clone)]
459pub struct GetSearchRoms {
460    pub rom_id: u64,
461    pub search_term: Option<String>,
462    pub search_by: Option<String>,
463}
464
465impl Endpoint for GetSearchRoms {
466    type Output = Value;
467
468    fn method(&self) -> &'static str {
469        "GET"
470    }
471
472    fn path(&self) -> String {
473        "/api/search/roms".into()
474    }
475
476    fn query(&self) -> Vec<(String, String)> {
477        let mut q = vec![("rom_id".into(), self.rom_id.to_string())];
478        push_str(&mut q, "search_term", &self.search_term);
479        push_str(&mut q, "search_by", &self.search_by);
480        q
481    }
482}
483
484#[cfg(test)]
485mod tests {
486    use super::{Endpoint, GetRoms};
487
488    #[test]
489    fn get_roms_query_sends_collection_id() {
490        let ep = GetRoms {
491            collection_id: Some(7),
492            limit: Some(100),
493            ..Default::default()
494        };
495        let q = ep.query();
496        assert!(q.iter().any(|(k, v)| k == "collection_id" && v == "7"));
497        assert!(!q.iter().any(|(k, _)| k == "smart_collection_id"));
498    }
499
500    #[test]
501    fn get_roms_query_sends_smart_collection_id() {
502        let ep = GetRoms {
503            smart_collection_id: Some(3),
504            limit: Some(50),
505            ..Default::default()
506        };
507        let q = ep.query();
508        assert!(q
509            .iter()
510            .any(|(k, v)| k == "smart_collection_id" && v == "3"));
511        assert!(!q.iter().any(|(k, _)| k == "collection_id"));
512        assert!(!q.iter().any(|(k, _)| k == "virtual_collection_id"));
513    }
514
515    #[test]
516    fn get_roms_query_sends_virtual_collection_id() {
517        let ep = GetRoms {
518            virtual_collection_id: Some("recent".into()),
519            limit: Some(10),
520            ..Default::default()
521        };
522        let q = ep.query();
523        assert!(q
524            .iter()
525            .any(|(k, v)| k == "virtual_collection_id" && v == "recent"));
526        assert!(!q.iter().any(|(k, _)| k == "collection_id"));
527    }
528
529    #[test]
530    fn get_roms_dedupes_platform_ids_with_platform_id() {
531        let ep = GetRoms {
532            platform_id: Some(5),
533            platform_ids: vec![5, 7],
534            ..Default::default()
535        };
536        let q = ep.query();
537        let ids: Vec<_> = q
538            .iter()
539            .filter(|(k, _)| k == "platform_ids")
540            .map(|(_, v)| v.as_str())
541            .collect();
542        assert_eq!(ids, vec!["5", "7"]);
543    }
544}