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 {
pub id: i32,
pub mod_id: i32,
pub title: String,
pub description: String,
pub thumbnail_url: String,
pub url: String,
}
#[derive(Debug, Deserialize)]
pub struct ModInfo {
pub id: u64,
pub name: String,
pub summary: String,
pub slug: String,
pub logo: Option<ModAsset>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Dependency {
}
#[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>,
}
#[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,
}
}
}
#[derive(Default)]
pub struct SearchParams {
pub game_version: String,
pub index: u64,
pub page_size: u64,
pub category_id: u64,
pub search_filter: String,
pub sort: SearchSortMethod,
}
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)
}
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)
}
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)
}
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")
}
}
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(())
}