romm_api/client/
rom_update.rs1use reqwest::multipart;
2use reqwest::Url;
3
4use crate::endpoints::roms::{PutRom, RomUpdateFields};
5use crate::endpoints::Endpoint;
6use crate::error::ApiError;
7use crate::types::metadata::{RomMatchFields, RomUpdateResponse};
8
9use super::response::{
10 api_error_from_response, decode_json_response_body, read_error_response_text,
11};
12use super::RommClient;
13
14pub(crate) fn rom_update_text_parts(fields: &RomUpdateFields) -> Vec<(String, String)> {
16 let mut out = Vec::new();
17 if let Some(ref n) = fields.name {
18 out.push(("name".into(), n.clone()));
19 }
20 if let Some(ref s) = fields.summary {
21 out.push(("summary".into(), s.clone()));
22 }
23 if let Some(ref u) = fields.url_cover {
24 out.push(("url_cover".into(), u.clone()));
25 }
26 push_match_fields(&mut out, &fields.match_fields);
27 out
28}
29
30fn push_match_fields(out: &mut Vec<(String, String)>, m: &RomMatchFields) {
31 push_i64(out, "igdb_id", m.igdb_id);
32 push_i64(out, "moby_id", m.moby_id);
33 push_i64(out, "ss_id", m.ss_id);
34 push_i64(out, "launchbox_id", m.launchbox_id);
35 push_i64(out, "sgdb_id", m.sgdb_id);
36 push_i64(out, "ra_id", m.ra_id);
37 push_i64(out, "hasheous_id", m.hasheous_id);
38 push_i64(out, "tgdb_id", m.tgdb_id);
39 push_i64(out, "hltb_id", m.hltb_id);
40 if let Some(ref id) = m.flashpoint_id {
41 out.push(("flashpoint_id".into(), id.clone()));
42 }
43 if let Some(ref id) = m.libretro_id {
44 out.push(("libretro_id".into(), id.clone()));
45 }
46}
47
48fn push_i64(out: &mut Vec<(String, String)>, key: &str, v: Option<i64>) {
49 if let Some(n) = v {
50 out.push((key.into(), n.to_string()));
51 }
52}
53
54impl RommClient {
55 pub async fn update_rom(&self, ep: &PutRom) -> Result<RomUpdateResponse, ApiError> {
57 let path = ep.path();
58 let mut url = Url::parse(&format!("{}{}", self.base_url.trim_end_matches('/'), path))
59 .map_err(|e| ApiError::UnexpectedResponse(format!("invalid update_rom URL: {e}")))?;
60
61 {
62 let mut pairs = url.query_pairs_mut();
63 if ep.remove_cover {
64 pairs.append_pair("remove_cover", "true");
65 }
66 if ep.unmatch_metadata {
67 pairs.append_pair("unmatch_metadata", "true");
68 }
69 }
70
71 let mut form = multipart::Form::new();
72 if !ep.unmatch_metadata {
73 for (key, value) in rom_update_text_parts(&ep.fields) {
74 form = form.text(key, value);
75 }
76 if let Some(ref artwork_path) = ep.artwork {
77 let bytes = tokio::fs::read(artwork_path).await.map_err(|e| {
78 ApiError::Io(std::io::Error::new(
79 e.kind(),
80 format!("read artwork {}: {e}", artwork_path.display()),
81 ))
82 })?;
83 let fname = artwork_path
84 .file_name()
85 .and_then(|n| n.to_str())
86 .unwrap_or("artwork.png");
87 let part = multipart::Part::bytes(bytes).file_name(fname.to_string());
88 form = form.part("artwork", part);
89 }
90 }
91
92 let resp = self
93 .http
94 .put(url)
95 .headers(self.build_headers()?)
96 .multipart(form)
97 .send()
98 .await?;
99
100 let status = resp.status();
101 if !status.is_success() {
102 let body = read_error_response_text(resp).await;
103 return Err(api_error_from_response(status, &body));
104 }
105
106 let bytes = resp.bytes().await?;
107 let value = decode_json_response_body(&bytes);
108 serde_json::from_value(value).map_err(|e| {
109 ApiError::UnexpectedResponse(format!("failed to decode update_rom response: {e}"))
110 })
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use crate::endpoints::roms::RomUpdateFields;
118
119 #[test]
120 fn form_parts_include_only_set_fields() {
121 let fields = RomUpdateFields {
122 name: Some("Foo".into()),
123 summary: None,
124 url_cover: None,
125 match_fields: RomMatchFields {
126 igdb_id: Some(99),
127 ..Default::default()
128 },
129 };
130 let parts = rom_update_text_parts(&fields);
131 assert!(parts.contains(&("name".into(), "Foo".into())));
132 assert!(parts.contains(&("igdb_id".into(), "99".into())));
133 assert!(!parts.iter().any(|(k, _)| k == "summary"));
134 }
135}