use crate::download::{self, user_agent};
use anyhow::Context;
use nitro_shared::pkg::{PackageCategory, PackageKind, PackageSearchParameters};
use reqwest::{Client, StatusCode};
use serde::{Deserialize, Serialize};
const API_URL: &str = "https://api.smithed.dev/v2";
pub async fn get_pack(id: &str, client: &Client) -> anyhow::Result<Pack> {
let url = format!("{API_URL}/packs/{id}");
download::json(url, client).await
}
pub async fn get_pack_optional(id: &str, client: &Client) -> anyhow::Result<Option<Pack>> {
let url = format!("{API_URL}/packs/{id}");
let resp = client
.get(url)
.header("User-Agent", user_agent())
.send()
.await
.context("Failed to send request")?;
if resp.status() == StatusCode::NOT_FOUND {
return Ok(None);
}
let resp = resp
.error_for_status()
.context("Server returned an error")?;
resp.json()
.await
.map(Some)
.context("Failed to deserialize JSON")
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Pack {
pub id: String,
pub display: PackDisplay,
pub versions: Vec<PackVersion>,
#[serde(default)]
pub categories: Vec<String>,
}
pub async fn get_pack_meta(id: &str, client: &Client) -> anyhow::Result<PackMeta> {
let url = format!("{API_URL}/packs/{id}/meta");
download::json(url, client).await
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct PackMeta {
#[serde(rename = "rawId")]
pub raw_id: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct PackDisplay {
pub name: String,
pub description: String,
pub icon: String,
pub hidden: bool,
#[serde(default)]
pub web_page: Option<String>,
#[serde(default)]
pub gallery: Vec<GalleryEntry>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct GalleryEntry {}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct PackVersion {
pub name: String,
pub downloads: PackDownloads,
pub supports: Vec<String>,
#[serde(default)]
pub dependencies: Vec<PackReference>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct PackDownloads {
pub datapack: Option<String>,
pub resourcepack: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct PackReference {
pub id: String,
pub version: String,
}
pub async fn get_bundle(id: &str, client: &Client) -> anyhow::Result<Bundle> {
let url = format!("{API_URL}/bundles/{id}");
download::json(url, client).await
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Bundle {
pub id: String,
pub versions: Vec<BundleVersion>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct BundleVersion {
pub name: String,
pub supports: Vec<String>,
pub packs: Vec<PackReference>,
}
pub async fn search_packs(
params: PackageSearchParameters,
client: &Client,
) -> anyhow::Result<Vec<PackSearchResult>> {
let Some(filters) = create_search_filters(¶ms) else {
return Ok(Vec::new());
};
let limit = if params.count > 100 {
100
} else {
params.count
};
let search = if let Some(search) = params.search {
format!("&search={search}")
} else {
String::new()
};
let page = params.skip / params.count as usize + 1;
let url = format!(
"{API_URL}/packs?limit={limit}{search}&page={page}{filters}&scope=data&scope=meta.rawId"
);
download::json(url, client).await
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct PackSearchResult {
pub id: String,
#[serde(rename = "displayName")]
pub display_name: String,
pub data: Pack,
pub meta: PackSearchResultMeta,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct PackSearchResultMeta {
#[serde(rename = "rawId")]
pub raw_id: String,
}
pub async fn count_packs(
params: PackageSearchParameters,
client: &Client,
) -> anyhow::Result<usize> {
let Some(mut filters) = create_search_filters(¶ms) else {
return Ok(0);
};
let search = if let Some(search) = params.search {
format!("search={search}")
} else {
String::new()
};
if search.is_empty()
&& let Some(stripped) = filters.strip_prefix("&")
{
filters = stripped.to_string();
}
let url = format!("{API_URL}/packs/count?{search}{filters}");
download::json(url, client).await
}
fn create_search_filters(params: &PackageSearchParameters) -> Option<String> {
if params.count == 0 {
return None;
}
if !params.types.is_empty()
&& !params
.types
.iter()
.all(|x| *x == PackageKind::Datapack || *x == PackageKind::ResourcePack)
{
return None;
}
let versions = params
.minecraft_versions
.iter()
.map(|x| format!("&version={x}"))
.collect::<Vec<_>>()
.join("");
let categories = params
.categories
.iter()
.filter_map(|x| convert_category(*x))
.map(|x| format!("&category={x}"))
.collect::<Vec<_>>();
if categories.is_empty() && !params.categories.is_empty() {
return None;
}
let categories = categories.join("");
Some(format!("{versions}{categories}"))
}
pub fn get_gallery_url(pack_id: &str, index: u8) -> String {
format!("https://api.smithed.dev/v2/packs/{pack_id}/gallery/{index}")
}
fn convert_category(category: PackageCategory) -> Option<&'static str> {
match category {
PackageCategory::Extensive => Some("Extensive"),
PackageCategory::Lightweight => Some("Lightweight"),
PackageCategory::Tweaks => Some("QoL"),
PackageCategory::VanillaPlus => Some("Vanilla+"),
PackageCategory::Technology => Some("Tech"),
PackageCategory::Magic => Some("Magic"),
PackageCategory::Exploration => Some("Exploration"),
PackageCategory::Worldgen => Some("World Overhaul"),
PackageCategory::Library => Some("Library"),
_ => None,
}
}
pub async fn get_supported_versions(client: &Client) -> anyhow::Result<Vec<String>> {
let url = format!("{API_URL}/supported-versions");
download::json(url, client).await
}