feat(search): search feature via modrinth api for current project

This commit is contained in:
RafayAhmad7548 2026-09-01 19:46:24 +05:00
parent 1092071968
commit 10e3ca2efb
Signed by: RafayAhmad
SSH key fingerprint: SHA256:WURX8viobA1uawb4dWM3LqYrY+XPcZcXhAXAlrYdhtE
8 changed files with 226 additions and 32 deletions

64
Cargo.lock generated
View file

@ -204,6 +204,15 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "colored"
version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "combine"
version = "4.6.8"
@ -876,12 +885,15 @@ name = "mcsm"
version = "0.1.0"
dependencies = [
"clap",
"colored",
"dialoguer",
"futures-util",
"indicatif",
"reqwest",
"serde",
"sha1",
"terminal_size",
"textwrap",
"tokio",
"toml",
]
@ -1128,6 +1140,7 @@ dependencies = [
"rustls-platform-verifier",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tokio-rustls",
@ -1265,6 +1278,12 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "same-file"
version = "1.0.6"
@ -1370,6 +1389,18 @@ dependencies = [
"serde_core",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "sha1"
version = "0.11.0"
@ -1431,6 +1462,12 @@ version = "1.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
[[package]]
name = "smawk"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100"
[[package]]
name = "socket2"
version = "0.6.5"
@ -1535,6 +1572,27 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "terminal_size"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
dependencies = [
"rustix",
"windows-sys 0.61.2",
]
[[package]]
name = "textwrap"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
dependencies = [
"smawk",
"unicode-linebreak",
"unicode-width",
]
[[package]]
name = "thiserror"
version = "2.0.20"
@ -1762,6 +1820,12 @@ version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-linebreak"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
[[package]]
name = "unicode-width"
version = "0.2.2"

View file

@ -5,11 +5,14 @@ edition = "2024"
[dependencies]
clap = { version = "4.6.6", features = ["derive"] }
colored = "3.1.1"
dialoguer = { version = "0.12.0", features = ["fuzzy-select"] }
futures-util = "0.3.34"
indicatif = "0.18.6"
reqwest = { version = "0.13.4", features = ["json", "stream"] }
reqwest = { version = "0.13.4", features = ["json", "stream", "query"] }
serde = { version = "1.0.229", features = ["derive"] }
sha1 = "0.11.0"
terminal_size = "0.4.4"
textwrap = "0.16.2"
tokio = { version = "1.53.1", features = ["full"] }
toml = "1.1.4"

View file

@ -43,7 +43,7 @@ impl Create {
std::env::set_current_dir(&project_name)?;
let config_file = tokio::fs::File::create("mcsm.toml").await?;
Ok(Create {
/*project_name,*/
/*project_name,*/
config_file,
version,
loader,
@ -101,21 +101,22 @@ impl LoaderDownloader for VanillaDownloader {
.await?;
if let Some(version) = create.version.clone() {
let version_exists = res
.versions
.iter()
.any(|v| v.id == version);
if !version_exists { return Err("Invalid minecraft version specified".into()) }
}
else {
let version_exists = res.versions.iter().any(|v| v.id == version);
if !version_exists {
return Err("Invalid minecraft version specified".into());
}
} else {
let versions = res
.versions
.iter()
.filter(|version| {
version.version_type == VanillaVersionType::Release ||
(create.show_snapshots && version.version_type == VanillaVersionType::Snapshot) ||
(create.show_betas && version.version_type == VanillaVersionType::OldBeta) ||
(create.show_alphas && version.version_type == VanillaVersionType::OldAlpha)
version.version_type == VanillaVersionType::Release
|| (create.show_snapshots
&& version.version_type == VanillaVersionType::Snapshot)
|| (create.show_betas
&& version.version_type == VanillaVersionType::OldBeta)
|| (create.show_alphas
&& version.version_type == VanillaVersionType::OldAlpha)
})
.collect::<Vec<_>>();
let version_names = versions
@ -140,10 +141,7 @@ impl LoaderDownloader for VanillaDownloader {
let version_detail: VanillaVersionDetail = with_spinner("", "", || async {
let version_detail: VanillaVersionDetail =
reqwest::get(version.url.clone())
.await?
.json()
.await?;
reqwest::get(version.url.clone()).await?.json().await?;
Ok(version_detail)
})
.await?;
@ -215,13 +213,11 @@ impl LoaderDownloader for FabricDownloader {
.await?;
if let Some(version) = create.version.clone() {
let exists = res
.game
.iter()
.any(|game| game.version == version);
if !exists { return Err("Invalid minecraft version specified".into()) }
}
else {
let exists = res.game.iter().any(|game| game.version == version);
if !exists {
return Err("Invalid minecraft version specified".into());
}
} else {
let game_version_names = res
.game
.iter()
@ -243,9 +239,10 @@ impl LoaderDownloader for FabricDownloader {
.loader
.iter()
.any(|loader| loader.version == loader_version);
if !exists { return Err(format!("Invalid {} loader version specified", Self::name()).into()) }
}
else {
if !exists {
return Err(format!("Invalid {} loader version specified", Self::name()).into());
}
} else {
let loader_version_names = res
.loader
.iter()
@ -283,7 +280,10 @@ impl LoaderDownloader for FabricDownloader {
with_spinner(&start_msg, "download finished", || async {
let download_url = format!(
"{}/loader/{}/{}/{}/server/jar",
URL, create.version.as_ref().unwrap(), create.loader_version.as_ref().unwrap(), installer_version
URL,
create.version.as_ref().unwrap(),
create.loader_version.as_ref().unwrap(),
installer_version
);
let res = reqwest::get(download_url).await?;
let mut file = tokio::fs::File::create("server.jar").await?;

View file

@ -1 +1,2 @@
pub mod create;
pub mod search;

97
src/commands/search.rs Normal file
View file

@ -0,0 +1,97 @@
use std::error::Error;
use colored::Colorize;
use serde::Serialize;
use terminal_size;
use textwrap::{Options, fill};
use terminal_size::{terminal_size as get_terminal_size, Width};
use crate::models::{config::Config, search::SearchResult};
#[derive(Serialize)]
struct SearchQueryParams {
query: String,
facets: String,
offset: Option<u32>,
limit: Option<u32>,
}
pub struct Search {
query: String,
}
impl Search {
const BASE_URL: &str = "https://api.modrinth.com/v2";
pub fn new(query: String) -> Self {
Search { query }
}
pub async fn run(&self) -> Result<(), Box<dyn Error>> {
let file_res = tokio::fs::read_to_string("mcsm.toml").await;
if file_res.is_err() {
return Err("current directory is not a mcsm project".into());
}
let config: Config = toml::from_str(&file_res.unwrap())?;
if config.server.loader == "Vanilla" {
return Err("cannot search for mods on Vanilla server".into());
}
let client = reqwest::Client::new();
let search = format!("{}/search", Self::BASE_URL);
let facets = format!(
r#"[["project_type:mod"],["environment!=client_only"],["environment!=singleplayer_only"],["environment!=unknown"],["categories:{}"],["versions:{}"]]"#,
config.server.loader.to_lowercase(),
config.server.version,
);
let params = SearchQueryParams {
query: self.query.clone(),
facets: facets.into(),
offset: None,
limit: None,
};
let res: SearchResult = client
.get(search)
.query(&params)
.send()
.await?
.json()
.await?;
self.print_results(&res);
Ok(())
}
fn print_results(&self, res: &SearchResult) {
println!(
"{}\n",
format!("Found {} result{}", res.total_hits, if res.total_hits == 1 { "" } else { "s" })
.green()
.bold()
);
let width = get_terminal_size().map(|(Width(w), _)| w as usize).unwrap_or(80);
for (i, hit) in res.hits.iter().enumerate() {
let wrapped = fill(
&hit.description,
Options::new(width.saturating_sub(3))
.initial_indent(" ")
.subsequent_indent(" "),
);
println!(
"{} {} {}\n by {}\n{}\n",
format!("{}.", i + 1).bold(),
hit.title.bold(),
format!("({})", hit.slug).cyan(),
hit.author.yellow(),
wrapped,
);
}
}
}

View file

@ -2,7 +2,7 @@ mod commands;
mod models;
mod utils;
use crate::commands::create::Create;
use crate::commands::{create::Create, search::Search};
use clap::{
Parser, Subcommand,
builder::styling::{self},
@ -53,6 +53,10 @@ enum Commands {
#[arg(long)]
show_alphas: bool,
},
Search {
query: String,
},
}
#[tokio::main]
@ -77,12 +81,19 @@ async fn main() -> Result<(), Box<dyn Error>> {
loader_version,
show_snapshots,
show_betas,
show_alphas
).await?;
if loader_is_unset { create.init_loader().await? }
show_alphas,
)
.await?;
if loader_is_unset {
create.init_loader().await?
}
let config = create.download_server().await?;
create.save_config(config).await?;
}
Commands::Search { query } => {
let search = Search::new(query);
search.run().await?;
}
}
Ok(())

View file

@ -1,3 +1,4 @@
pub mod config;
pub mod fabric;
pub mod search;
pub mod vanilla;

17
src/models/search.rs Normal file
View file

@ -0,0 +1,17 @@
use serde::Deserialize;
#[derive(Deserialize)]
pub struct SearchResult {
pub hits: Vec<SearchHit>,
pub offset: u32,
pub limit: u32,
pub total_hits: u32,
}
#[derive(Deserialize)]
pub struct SearchHit {
pub slug: String,
pub author: String,
pub title: String,
pub description: String,
}