scl_core/download/
curseforge.rs1use std::{
28 fmt::Write as _,
29 ops::{Deref, DerefMut},
30 path::PathBuf,
31};
32
33use crate::prelude::*;
34
35const API_KEY: Option<&str> = std::option_env!("CURSEFORGE_API_KEY");
36const BASE_URL: &str = "https://api.curseforge.com/v1/";
37const BASE_URL_SEARCH: &str = "https://api.curseforge.com/v1/mods/search?gameId=432&classId=6";
38
39#[derive(Debug, Deserialize)]
40struct Response<T> {
41 pub data: T,
42}
43
44impl<T> Deref for Response<T> {
45 type Target = T;
46 fn deref(&self) -> &Self::Target {
47 &self.data
48 }
49}
50
51impl<T> DerefMut for Response<T> {
52 fn deref_mut(&mut self) -> &mut Self::Target {
53 &mut self.data
54 }
55}
56
57#[derive(Debug, Deserialize)]
59#[serde(rename_all = "camelCase")]
60pub struct ModAsset {
61 pub id: i32,
63 pub mod_id: i32,
65 pub title: String,
67 pub description: String,
69 pub thumbnail_url: String,
71 pub url: String,
73}
74
75#[derive(Debug, Deserialize)]
77pub struct ModInfo {
78 pub id: u64,
80 pub name: String,
82 pub summary: String,
84 pub slug: String,
86 pub logo: Option<ModAsset>,
88}
89
90#[derive(Debug, Deserialize)]
94#[serde(rename_all = "camelCase")]
95pub struct Dependency {
96 }
99
100#[derive(Debug, Deserialize)]
102#[serde(rename_all = "camelCase")]
103pub struct ModFile {
104 pub file_name: String,
106 pub download_url: String,
108 pub dependencies: Vec<Dependency>,
110 pub game_versions: Vec<String>,
112}
113
114#[derive(Debug, Clone, Copy, Default)]
116pub enum SearchSortMethod {
117 #[default]
119 Featured,
120 Populatity,
122 LastUpdate,
124 Name,
126 Author,
128 TotalDownloads,
130}
131
132impl SearchSortMethod {
133 fn to_query(self) -> u8 {
134 match self {
135 SearchSortMethod::Featured => 0,
136 SearchSortMethod::Populatity => 1,
137 SearchSortMethod::LastUpdate => 2,
138 SearchSortMethod::Name => 3,
139 SearchSortMethod::Author => 4,
140 SearchSortMethod::TotalDownloads => 5,
141 }
142 }
143}
144
145#[derive(Default)]
147pub struct SearchParams {
148 pub game_version: String,
150 pub index: u64,
152 pub page_size: u64,
154 pub category_id: u64,
156 pub search_filter: String,
158 pub sort: SearchSortMethod,
160}
161
162pub async fn search_mods(
164 SearchParams {
165 game_version,
166 index,
167 page_size,
168 category_id,
169 search_filter,
170 sort,
171 }: SearchParams,
172) -> DynResult<Vec<ModInfo>> {
173 let mut base_url = BASE_URL_SEARCH.to_string();
174 let _ = write!(&mut base_url, "&sort={}", sort.to_query());
175 if !search_filter.is_empty() {
176 let _ = write!(
177 &mut base_url,
178 "&searchFilter={}",
179 urlencoding::encode(&search_filter)
180 );
181 }
182 if !game_version.is_empty() {
183 let _ = write!(&mut base_url, "&gameVersion={game_version}");
184 }
185 if index > 0 {
186 let _ = write!(&mut base_url, "&index={index}");
187 }
188 if page_size > 0 && page_size <= 30 {
189 let _ = write!(&mut base_url, "&pageSize={page_size}");
190 } else {
191 let _ = write!(&mut base_url, "&pageSize={}", 20);
192 }
193 if category_id > 0 {
194 let _ = write!(&mut base_url, "&categoryID={category_id}");
195 }
196 println!("Searching by {base_url}");
197 let data: Response<Vec<ModInfo>> = crate::http::get(&base_url)
198 .header("x-api-key", API_KEY.unwrap_or_default())
199 .await
200 .map_err(|e| anyhow::anyhow!(e))?
201 .body_json()
202 .await
203 .map_err(|e| anyhow::anyhow!(e))?;
204 Ok(data.data)
205}
206
207pub async fn get_mod_info(modid: u64) -> DynResult<ModInfo> {
209 let data: Response<ModInfo> = crate::http::get(&(format!("{BASE_URL}mods/{modid}")))
210 .header("x-api-key", API_KEY.unwrap_or_default())
211 .await
212 .map_err(|e| anyhow::anyhow!(e))?
213 .body_json()
214 .await
215 .map_err(|e| anyhow::anyhow!(e))?;
216 Ok(data.data)
217}
218
219pub async fn get_mod_files(modid: u64) -> DynResult<Vec<ModFile>> {
221 let data: Response<Vec<ModFile>> = crate::http::get(&format!("{BASE_URL}mods/{modid}/files"))
222 .header("x-api-key", API_KEY.unwrap_or_default())
223 .await
224 .map_err(|e| anyhow::anyhow!(e))?
225 .body_json()
226 .await
227 .map_err(|e| anyhow::anyhow!(e))?;
228 Ok(data.data)
229}
230
231pub async fn get_mod_icon(mod_info: &ModInfo) -> DynResult<image::DynamicImage> {
233 if let Some(logo) = &mod_info.logo {
234 let data = crate::http::get(&logo.thumbnail_url)
235 .await
236 .map_err(|e| anyhow::anyhow!(e))?
237 .body_bytes()
238 .await
239 .map_err(|e| anyhow::anyhow!(e))?;
240 if let Ok(img) = image::load_from_memory(&data) {
241 Ok(img)
242 } else {
243 anyhow::bail!("Can't load mod icon image")
244 }
245 } else {
246 anyhow::bail!("Mod icon image is empty")
247 }
248}
249
250pub async fn get_mod_icon_by_id(modid: u64) -> DynResult<image::DynamicImage> {
252 let mod_info = get_mod_info(modid).await?;
253 get_mod_icon(&mod_info).await
254}
255
256pub async fn download_mod(
258 _ctx: Option<impl Reporter>,
259 _name: &str,
260 url: &str,
261 dest: PathBuf,
262) -> DynResult {
263 let mut file = inner_future::fs::OpenOptions::new()
264 .create(true)
265 .truncate(true)
266 .write(true)
267 .open(format!("{}.tmp", dest.to_str().unwrap()))
268 .await?;
269 let res = crate::http::get(url)
270 .await
271 .map_err(|e| anyhow::anyhow!(e))?;
272 inner_future::io::copy(res, &mut file).await?;
273 inner_future::fs::rename(format!("{}.tmp", dest.to_str().unwrap()), dest).await?;
274 Ok(())
275}