1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
//! CurseForge 模组下载的结构和接口
//!
//! 在使用这个模块提供的功能前,请先设定好 `CURSEFORGE_API_KEY` 环境变量为你 CurseForge 的开发者令牌,否则服务将无法使用

/*
    基本链接:https://addons-ecs.forgesvc.net/api/v2/addon/
    某个模组:https://addons-ecs.forgesvc.net/api/v2/addon/[MOD_ID]
    模组详情:https://addons-ecs.forgesvc.net/api/v2/addon/[MOD_ID]/description
    模组文件:https://addons-ecs.forgesvc.net/api/v2/addon/[MOD_ID]/files
    搜索模组:https://addons-ecs.forgesvc.net/api/v2/addon/search
            请求字符串: gameId = 432
                        gameVersion
                        sectionId = 6
                        searchFilter
                        categoryID
                        index
                        pageSize
                        sort:
                            FEATURED: 0
                            POPULARITY: 1
                            LAST_UPDATE: 2
                            NAME: 3
                            AUTHOR: 4
                            TOTAL_DOWNLOADS: 5
*/

use std::{
    fmt::Write as _,
    ops::{Deref, DerefMut},
    path::PathBuf,
};

use crate::prelude::*;

const API_KEY: Option<&str> = std::option_env!("CURSEFORGE_API_KEY");
const BASE_URL: &str = "https://api.curseforge.com/v1/";
const BASE_URL_SEARCH: &str = "https://api.curseforge.com/v1/mods/search?gameId=432&classId=6";

#[derive(Debug, Deserialize)]
struct Response<T> {
    pub data: T,
}

impl<T> Deref for Response<T> {
    type Target = T;
    fn deref(&self) -> &Self::Target {
        &self.data
    }
}

impl<T> DerefMut for Response<T> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.data
    }
}

/// 一个模组资源信息
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModAsset {
    /// 此模组文件的文件 ID 编号
    pub id: i32,
    /// 此模组文件对应的模组 ID
    pub mod_id: i32,
    /// 模组文件的标题(不一定是文件名)
    pub title: String,
    /// 模组文件的介绍(一般是作者的更新记录什么的)
    pub description: String,
    /// 模组文件的缩略图
    pub thumbnail_url: String,
    /// 模组文件的下载链接
    pub url: String,
}

/// 一个模组的信息
#[derive(Debug, Deserialize)]
pub struct ModInfo {
    /// 模组的 ID
    pub id: u64,
    /// 模组的名称
    pub name: String,
    /// 模组的简短介绍
    pub summary: String,
    /// 模组的 Slug(一般是模组的字符串 ID)
    pub slug: String,
    /// 模组的 LOGO 图标
    pub logo: Option<ModAsset>,
}

/// 模组的所需依赖
///
/// TODO:完善模组依赖下载功能
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Dependency {
    // mod_id: i32,
    // relation_type: u8,
}

/// 一个模组文件信息
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModFile {
    /// 模组文件的文件名
    pub file_name: String,
    /// 模组文件的下载链接
    pub download_url: String,
    /// 模组的所需依赖
    pub dependencies: Vec<Dependency>,
    /// 模组支持的游戏版本
    pub game_versions: Vec<String>,
}

/// 使用搜索 API 时的排序方式
#[derive(Debug, Clone, Copy, Default)]
pub enum SearchSortMethod {
    /// 按推荐排序
    #[default]
    Featured,
    /// 按热门度排序
    Populatity,
    /// 按最新更新排序
    LastUpdate,
    /// 按名称排序
    Name,
    /// 按作者名称排序
    Author,
    /// 按总下载量排序
    TotalDownloads,
}

impl SearchSortMethod {
    fn to_query(self) -> u8 {
        match self {
            SearchSortMethod::Featured => 0,
            SearchSortMethod::Populatity => 1,
            SearchSortMethod::LastUpdate => 2,
            SearchSortMethod::Name => 3,
            SearchSortMethod::Author => 4,
            SearchSortMethod::TotalDownloads => 5,
        }
    }
}

/// 搜索参数,将其传入到 [`self::search_mods`] 方法以搜索模组
#[derive(Default)]
pub struct SearchParams {
    /// 搜索支持指定游戏版本的模组
    pub game_version: String,
    /// 当前的搜索页码
    pub index: u64,
    /// 当前搜索的每页项目数量
    pub page_size: u64,
    /// 模组类型 ID
    pub category_id: u64,
    /// 搜索的关键字
    pub search_filter: String,
    /// 搜索结果的排序方式
    pub sort: SearchSortMethod,
}

/// 根据关键词从 Curseforge 搜索模组列表
pub async fn search_mods(
    SearchParams {
        game_version,
        index,
        page_size,
        category_id,
        search_filter,
        sort,
    }: SearchParams,
) -> DynResult<Vec<ModInfo>> {
    let mut base_url = BASE_URL_SEARCH.to_string();
    let _ = write!(&mut base_url, "&sort={}", sort.to_query());
    if !search_filter.is_empty() {
        let _ = write!(
            &mut base_url,
            "&searchFilter={}",
            urlencoding::encode(&search_filter)
        );
    }
    if !game_version.is_empty() {
        let _ = write!(&mut base_url, "&gameVersion={game_version}");
    }
    if index > 0 {
        let _ = write!(&mut base_url, "&index={index}");
    }
    if page_size > 0 && page_size <= 30 {
        let _ = write!(&mut base_url, "&pageSize={page_size}");
    } else {
        let _ = write!(&mut base_url, "&pageSize={}", 20);
    }
    if category_id > 0 {
        let _ = write!(&mut base_url, "&categoryID={category_id}");
    }
    tracing::trace!("Searching by {base_url}");
    let data: Response<Vec<ModInfo>> = crate::http::get(&base_url)
        .header("x-api-key", API_KEY.unwrap_or_default())
        .await
        .map_err(|e| anyhow::anyhow!(e))?
        .body_json()
        .await
        .map_err(|e| anyhow::anyhow!(e))?;
    Ok(data.data)
}

/// 通过模组在 Curseforge 的 ID 获取详情信息
pub async fn get_mod_info(modid: u64) -> DynResult<ModInfo> {
    let data: Response<ModInfo> = crate::http::get(&(format!("{BASE_URL}mods/{modid}")))
        .header("x-api-key", API_KEY.unwrap_or_default())
        .await
        .map_err(|e| anyhow::anyhow!(e))?
        .body_json()
        .await
        .map_err(|e| anyhow::anyhow!(e))?;
    Ok(data.data)
}

/// 获取模组在 Curseforge 的 ID 获取可下载的模组文件列表
pub async fn get_mod_files(modid: u64) -> DynResult<Vec<ModFile>> {
    let data: Response<Vec<ModFile>> = crate::http::get(&format!("{BASE_URL}mods/{modid}/files"))
        .header("x-api-key", API_KEY.unwrap_or_default())
        .await
        .map_err(|e| anyhow::anyhow!(e))?
        .body_json()
        .await
        .map_err(|e| anyhow::anyhow!(e))?;
    Ok(data.data)
}

/// 获取模组在 Curseforge 的 ID 获取模组的图标
pub async fn get_mod_icon(mod_info: &ModInfo) -> DynResult<image::DynamicImage> {
    if let Some(logo) = &mod_info.logo {
        let data = crate::http::get(&logo.thumbnail_url)
            .await
            .map_err(|e| anyhow::anyhow!(e))?
            .body_bytes()
            .await
            .map_err(|e| anyhow::anyhow!(e))?;
        if let Ok(img) = image::load_from_memory(&data) {
            Ok(img)
        } else {
            anyhow::bail!("Can't load mod icon image")
        }
    } else {
        anyhow::bail!("Mod icon image is empty")
    }
}

/// 获取模组在 Curseforge 的 ID 获取模组的图标
pub async fn get_mod_icon_by_id(modid: u64) -> DynResult<image::DynamicImage> {
    let mod_info = get_mod_info(modid).await?;
    get_mod_icon(&mod_info).await
}

/// 下载模组
pub async fn download_mod(
    _ctx: Option<impl Reporter>,
    _name: &str,
    url: &str,
    dest: PathBuf,
) -> DynResult {
    let mut file = inner_future::fs::OpenOptions::new()
        .create(true)
        .truncate(true)
        .write(true)
        .open(format!("{}.tmp", dest.to_str().unwrap()))
        .await?;
    let res = crate::http::get(url)
        .await
        .map_err(|e| anyhow::anyhow!(e))?;
    inner_future::io::copy(res, &mut file).await?;
    inner_future::fs::rename(format!("{}.tmp", dest.to_str().unwrap()), dest).await?;
    Ok(())
}