Skip to main content

hexomc_lib/mods/
curseforge.rs

1use serde::{Deserialize, Serialize};
2
3use crate::error::Result;
4use crate::mods::detector::ModLoader;
5
6const CF_BASE: &str = "https://api.curseforge.com/v1";
7
8pub struct CurseForgeClient {
9    client: reqwest::Client,
10    api_key: String,
11}
12
13impl CurseForgeClient {
14    pub fn new(api_key: impl Into<String>) -> Self {
15        Self {
16            client: reqwest::Client::new(),
17            api_key: api_key.into(),
18        }
19    }
20
21    fn headers(&self) -> reqwest::header::HeaderMap {
22        let mut headers = reqwest::header::HeaderMap::new();
23        headers.insert("x-api-key", self.api_key.parse().unwrap());
24        headers.insert("Content-Type", "application/json".parse().unwrap());
25        headers
26    }
27}
28
29#[derive(Debug, Deserialize, Serialize, Clone)]
30pub struct CfMod {
31    pub id: u64,
32    pub name: String,
33    pub slug: String,
34    pub summary: String,
35    #[serde(rename = "downloadCount")]
36    pub download_count: u64,
37}
38
39#[derive(Debug, Deserialize, Serialize, Clone)]
40pub struct CfModFile {
41    pub id: u64,
42    #[serde(rename = "displayName")]
43    pub display_name: String,
44    #[serde(rename = "fileName")]
45    pub file_name: String,
46    #[serde(rename = "downloadUrl")]
47    pub download_url: Option<String>,
48    #[serde(rename = "gameVersions")]
49    pub game_versions: Vec<String>,
50    #[serde(rename = "fileDate")]
51    pub file_date: String,
52    pub hashes: Vec<CfHash>,
53}
54
55#[derive(Debug, Deserialize, Serialize, Clone)]
56pub struct CfHash {
57    pub value: String,
58    pub algo: u32, // 1=Sha1, 2=Md5
59}
60
61impl CfModFile {
62    pub fn sha1(&self) -> Option<&str> {
63        self.hashes
64            .iter()
65            .find(|h| h.algo == 1)
66            .map(|h| h.value.as_str())
67    }
68}
69
70fn loader_type_id(loader: &ModLoader) -> u32 {
71    match loader {
72        ModLoader::Forge => 1,
73        ModLoader::Fabric => 4,
74        ModLoader::Quilt => 5,
75        ModLoader::NeoForge => 6,
76        ModLoader::Unknown => 0,
77    }
78}
79
80impl CurseForgeClient {
81    /// Search for mods.
82    pub async fn search_mod(
83        &self,
84        name: &str,
85        loader: &ModLoader,
86    ) -> Result<Vec<CfMod>> {
87        #[derive(Deserialize)]
88        struct Resp {
89            data: Vec<CfMod>,
90        }
91
92        let resp: Resp = self
93            .client
94            .get(format!("{}/mods/search", CF_BASE))
95            .headers(self.headers())
96            .query(&[
97                ("gameId", "432"),
98                ("searchFilter", name),
99                ("modLoaderType", &loader_type_id(loader).to_string()),
100                ("sortOrder", "desc"),
101            ])
102            .send()
103            .await?
104            .json()
105            .await?;
106
107        Ok(resp.data)
108    }
109
110    /// All file versions of a mod.
111    pub async fn get_mod_files(&self, mod_id: u64) -> Result<Vec<CfModFile>> {
112        #[derive(Deserialize)]
113        struct Resp {
114            data: Vec<CfModFile>,
115        }
116
117        let resp: Resp = self
118            .client
119            .get(format!("{}/mods/{}/files", CF_BASE, mod_id))
120            .headers(self.headers())
121            .send()
122            .await?
123            .json()
124            .await?;
125
126        Ok(resp.data)
127    }
128
129    /// Latest file matching the given MC version and loader.
130    pub async fn get_latest_file(
131        &self,
132        mod_id: u64,
133        mc_version: &str,
134        loader: &ModLoader,
135    ) -> Result<Option<CfModFile>> {
136        let files = self.get_mod_files(mod_id).await?;
137        let loader_str = format!("{:?}", loader);
138
139        let best = files
140            .into_iter()
141            .filter(|f| {
142                f.game_versions.iter().any(|v| v == mc_version)
143                    && f.game_versions
144                        .iter()
145                        .any(|v| v.to_lowercase() == loader_str.to_lowercase())
146            })
147            .max_by_key(|f| f.file_date.clone());
148
149        Ok(best)
150    }
151}