scl_core/download/
curseforge.rs

1//! CurseForge 模组下载的结构和接口
2//!
3//! 在使用这个模块提供的功能前,请先设定好 `CURSEFORGE_API_KEY` 环境变量为你 CurseForge 的开发者令牌,否则服务将无法使用
4
5/*
6    基本链接:https://addons-ecs.forgesvc.net/api/v2/addon/
7    某个模组:https://addons-ecs.forgesvc.net/api/v2/addon/[MOD_ID]
8    模组详情:https://addons-ecs.forgesvc.net/api/v2/addon/[MOD_ID]/description
9    模组文件:https://addons-ecs.forgesvc.net/api/v2/addon/[MOD_ID]/files
10    搜索模组:https://addons-ecs.forgesvc.net/api/v2/addon/search
11            请求字符串: gameId = 432
12                        gameVersion
13                        sectionId = 6
14                        searchFilter
15                        categoryID
16                        index
17                        pageSize
18                        sort:
19                            FEATURED: 0
20                            POPULARITY: 1
21                            LAST_UPDATE: 2
22                            NAME: 3
23                            AUTHOR: 4
24                            TOTAL_DOWNLOADS: 5
25*/
26
27use 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/// 一个模组资源信息
58#[derive(Debug, Deserialize)]
59#[serde(rename_all = "camelCase")]
60pub struct ModAsset {
61    /// 此模组文件的文件 ID 编号
62    pub id: i32,
63    /// 此模组文件对应的模组 ID
64    pub mod_id: i32,
65    /// 模组文件的标题(不一定是文件名)
66    pub title: String,
67    /// 模组文件的介绍(一般是作者的更新记录什么的)
68    pub description: String,
69    /// 模组文件的缩略图
70    pub thumbnail_url: String,
71    /// 模组文件的下载链接
72    pub url: String,
73}
74
75/// 一个模组的信息
76#[derive(Debug, Deserialize)]
77pub struct ModInfo {
78    /// 模组的 ID
79    pub id: u64,
80    /// 模组的名称
81    pub name: String,
82    /// 模组的简短介绍
83    pub summary: String,
84    /// 模组的 Slug(一般是模组的字符串 ID)
85    pub slug: String,
86    /// 模组的 LOGO 图标
87    pub logo: Option<ModAsset>,
88}
89
90/// 模组的所需依赖
91///
92/// TODO:完善模组依赖下载功能
93#[derive(Debug, Deserialize)]
94#[serde(rename_all = "camelCase")]
95pub struct Dependency {
96    // mod_id: i32,
97    // relation_type: u8,
98}
99
100/// 一个模组文件信息
101#[derive(Debug, Deserialize)]
102#[serde(rename_all = "camelCase")]
103pub struct ModFile {
104    /// 模组文件的文件名
105    pub file_name: String,
106    /// 模组文件的下载链接
107    pub download_url: String,
108    /// 模组的所需依赖
109    pub dependencies: Vec<Dependency>,
110    /// 模组支持的游戏版本
111    pub game_versions: Vec<String>,
112}
113
114/// 使用搜索 API 时的排序方式
115#[derive(Debug, Clone, Copy, Default)]
116pub enum SearchSortMethod {
117    /// 按推荐排序
118    #[default]
119    Featured,
120    /// 按热门度排序
121    Populatity,
122    /// 按最新更新排序
123    LastUpdate,
124    /// 按名称排序
125    Name,
126    /// 按作者名称排序
127    Author,
128    /// 按总下载量排序
129    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/// 搜索参数,将其传入到 [`self::search_mods`] 方法以搜索模组
146#[derive(Default)]
147pub struct SearchParams {
148    /// 搜索支持指定游戏版本的模组
149    pub game_version: String,
150    /// 当前的搜索页码
151    pub index: u64,
152    /// 当前搜索的每页项目数量
153    pub page_size: u64,
154    /// 模组类型 ID
155    pub category_id: u64,
156    /// 搜索的关键字
157    pub search_filter: String,
158    /// 搜索结果的排序方式
159    pub sort: SearchSortMethod,
160}
161
162/// 根据关键词从 Curseforge 搜索模组列表
163pub 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
207/// 通过模组在 Curseforge 的 ID 获取详情信息
208pub 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
219/// 获取模组在 Curseforge 的 ID 获取可下载的模组文件列表
220pub 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
231/// 获取模组在 Curseforge 的 ID 获取模组的图标
232pub 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
250/// 获取模组在 Curseforge 的 ID 获取模组的图标
251pub 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
256/// 下载模组
257pub 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}