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