hexomc_lib/mods/
curseforge.rs1use 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, }
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 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 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 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}