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