use image::DynamicImage;
use crate::prelude::*;
#[derive(Debug, Deserialize)]
pub struct ModResult {
#[serde(deserialize_with = "deserialize_null_default")]
#[serde(default)]
pub project_id: String,
#[serde(deserialize_with = "deserialize_null_default")]
pub slug: String,
#[serde(deserialize_with = "deserialize_null_default")]
pub icon_url: String,
pub title: String,
pub description: String,
}
#[derive(Debug, Deserialize)]
pub struct ModFile {
pub url: String,
pub filename: String,
pub primary: bool,
}
#[derive(Debug, Deserialize)]
pub struct ModVersion {
pub files: Vec<ModFile>,
pub game_versions: Vec<String>,
pub loaders: Vec<String>,
}
fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
T: Default + Deserialize<'de>,
D: Deserializer<'de>,
{
let opt = Option::deserialize(deserializer)?;
Ok(opt.unwrap_or_default())
}
#[derive(Debug, Deserialize)]
pub struct ModSearchResult {
pub hits: Vec<ModResult>,
}
#[derive(Default)]
pub struct SearchParams {
pub search_filter: String,
pub index: u64,
pub page_size: u64,
}
pub async fn get_mod_files(modid: &str) -> DynResult<Vec<ModVersion>> {
crate::http::retry_get_json(format!(
"https://api.modrinth.com/v2/project/{modid}/version"
))
.await
}
pub async fn get_mod_info(modid: &str) -> DynResult<ModResult> {
crate::http::retry_get_json(format!("https://api.modrinth.com/v2/project/{modid}")).await
}
pub async fn get_mod_icon(modid: &str) -> DynResult<DynamicImage> {
let info = get_mod_info(modid).await?;
get_mod_icon_by_url(&info.icon_url).await
}
pub async fn get_mod_icon_by_url(url: &str) -> DynResult<DynamicImage> {
if url.is_empty() {
let mut img = image::RgbaImage::new(1, 1);
img.put_pixel(0, 0, image::Rgba([0xFF, 0xFF, 0xFF, 0]));
return Ok(image::DynamicImage::ImageRgba8(img));
}
let data = crate::http::get(url)
.recv_bytes()
.await
.map_err(|e| anyhow::anyhow!(e))?;
if url.ends_with(".webp") {
let img = webp::Decoder::new(&data)
.decode()
.ok_or_else(|| anyhow::anyhow!("can't load webp file"))?;
match img.len() / (img.width() as usize * img.height() as usize) {
3 => {
let img = image::ImageBuffer::from_raw(img.width(), img.height(), img.to_owned())
.ok_or_else(|| anyhow::anyhow!("can't load rgb webp file"))?;
Ok(image::DynamicImage::ImageRgb8(img))
}
4 => {
let img = image::ImageBuffer::from_raw(img.width(), img.height(), img.to_owned())
.ok_or_else(|| anyhow::anyhow!("can't load rgba webp file"))?;
Ok(image::DynamicImage::ImageRgba8(img))
}
_ => anyhow::bail!("unknown webp data struct"),
}
} else if url.ends_with(".svg") || url.ends_with(".svgz") {
let mut img = image::RgbaImage::new(1, 1);
img.put_pixel(0, 0, image::Rgba([0xFF, 0xFF, 0xFF, 0]));
Ok(image::DynamicImage::ImageRgba8(img))
} else if let Ok(img) = image::load_from_memory_with_format(
&data,
image::ImageFormat::from_path(url).map_err(|e| anyhow::anyhow!(e))?,
) {
Ok(img)
} else {
let mut img = image::RgbaImage::new(1, 1);
img.put_pixel(0, 0, image::Rgba([0xFF, 0xFF, 0xFF, 0]));
Ok(image::DynamicImage::ImageRgba8(img))
}
}
pub async fn search_mods(
SearchParams {
search_filter,
index,
page_size,
}: SearchParams,
) -> DynResult<Vec<ModResult>> {
let search_filter = urlencoding::encode(&search_filter);
let r: ModSearchResult = crate::http::get(format!(
"https://api.modrinth.com/v2/search?offset={}&limit={}&query={}",
(index - 1) * page_size,
page_size,
search_filter
))
.recv_json()
.await
.map_err(|e| anyhow::anyhow!(e))?;
Ok(r.hits
.into_iter()
.map(|mut a| {
a.project_id = a.project_id.trim_start_matches("local-").to_owned();
a
})
.collect())
}