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#[derive(Debug, Default, Clone)]
28pub struct GetRoms {
29 pub search_term: Option<String>,
30 pub platform_id: Option<u64>,
32 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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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}