Skip to main content

romm_api/client/
rom_update.rs

1use 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
14/// Build `(field, value)` pairs for RomM `PUT /api/roms/{id}` multipart text fields.
15pub(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    /// `PUT /api/roms/{id}` with `multipart/form-data` (metadata match, edit, unmatch).
56    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}