This commit is contained in:
xeovalyte 2024-05-30 16:04:30 +02:00
parent 6f6c78d3ff
commit 56f659a182
No known key found for this signature in database
15 changed files with 399 additions and 243 deletions

95
Cargo.lock generated
View File

@ -4,9 +4,9 @@ version = 3
[[package]]
name = "addr2line"
version = "0.21.0"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
dependencies = [
"gimli",
]
@ -66,6 +66,12 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "anyhow"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "atomic-waker"
version = "1.1.2"
@ -80,9 +86,9 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
[[package]]
name = "backtrace"
version = "0.3.71"
version = "0.3.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d"
checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11"
dependencies = [
"addr2line",
"cc",
@ -169,7 +175,7 @@ version = "4.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
dependencies = [
"heck",
"heck 0.5.0",
"proc-macro2",
"quote",
"syn",
@ -197,6 +203,18 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "comfy-table"
version = "7.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b34115915337defe99b2aff5c2ce6771e5fbc4079f4b506301f5cf394c8452f7"
dependencies = [
"crossterm 0.27.0",
"strum",
"strum_macros",
"unicode-width",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
@ -229,6 +247,19 @@ dependencies = [
"winapi",
]
[[package]]
name = "crossterm"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
dependencies = [
"bitflags 2.5.0",
"crossterm_winapi",
"libc",
"parking_lot",
"winapi",
]
[[package]]
name = "crossterm_winapi"
version = "0.9.1"
@ -364,9 +395,9 @@ dependencies = [
[[package]]
name = "gimli"
version = "0.28.1"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
[[package]]
name = "h2"
@ -393,6 +424,12 @@ version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "heck"
version = "0.5.0"
@ -483,9 +520,9 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.4"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d8d52be92d09acc2e01dddb7fde3ad983fc6489c7db4837e605bc3fca4cb63e"
checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56"
dependencies = [
"bytes",
"futures-channel",
@ -528,7 +565,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a"
dependencies = [
"bitflags 2.5.0",
"crossterm",
"crossterm 0.25.0",
"dyn-clone",
"fuzzy-matcher",
"fxhash",
@ -670,9 +707,9 @@ dependencies = [
[[package]]
name = "object"
version = "0.32.2"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e"
dependencies = [
"memchr",
]
@ -731,13 +768,17 @@ dependencies = [
name = "packium"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"colored",
"comfy-table",
"inquire",
"reqwest",
"serde",
"serde_json",
"tokio",
"toml",
"url",
]
[[package]]
@ -911,6 +952,12 @@ version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d"
[[package]]
name = "rustversion"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
[[package]]
name = "ryu"
version = "1.0.18"
@ -1068,6 +1115,25 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29"
[[package]]
name = "strum_macros"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946"
dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
"rustversion",
"syn",
]
[[package]]
name = "syn"
version = "2.0.66"
@ -1324,6 +1390,7 @@ dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
"serde",
]
[[package]]
@ -1592,9 +1659,9 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
[[package]]
name = "winnow"
version = "0.6.8"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d"
checksum = "86c949fede1d13936a99f14fafd3e76fd642b556dd2ce96287fbe2e0151bfac6"
dependencies = [
"memchr",
]

View File

@ -8,6 +8,10 @@ clap = { version = "4.5.4", features = [ "derive" ]}
toml = "0.8.13"
inquire = "0.7.5"
serde = { version = "1.0.203", features = [ "derive" ]}
serde_json = "1.0.117"
colored = "2.1.0"
reqwest = { version = "0.12.4", features = [ "json" ]}
tokio = { version = "1.37.0", features = [ "full" ] }
comfy-table = "7.1.1"
url = { version = "2.5.0", features = [ "serde" ] }
anyhow = "1.0.86"

View File

@ -20,10 +20,7 @@ pub enum Commands {
Info,
/// Add a mod to the modpack
Add {
/// The name of the mod to add
name: String,
},
Add,
/// Remove a mod from the modpack
Remove,

24
src/commands/add.rs Normal file
View File

@ -0,0 +1,24 @@
use crate::config;
use crate::modrinth::project::get_project_versions;
use crate::modrinth::search::search;
use inquire::{Select, Text};
pub async fn add() {
let config = config::Config::get().unwrap();
let query = Text::new("Search").prompt().unwrap();
let response = search(&query, &config.game_version, &config.loader)
.await
.unwrap();
let project = Select::new("Select a mod", response.hits).prompt().unwrap();
let project_versions = get_project_versions(&project.project_id, &config.game_version)
.await
.unwrap();
let project_version = project_versions.get(0).unwrap();
config::Config::add(project.project_id, project_version.id.to_owned()).unwrap();
}

33
src/commands/init.rs Normal file
View File

@ -0,0 +1,33 @@
use crate::config;
use crate::modrinth::tags;
use inquire::Select;
pub async fn init(snaphots: bool) {
let mut game_versions = tags::get_minecraft_versions().await.unwrap();
if snaphots {
game_versions = game_versions
.into_iter()
.filter(|v| (&v.version_type == "snapshot" || &v.version_type == "release"))
.collect()
} else {
game_versions = game_versions
.into_iter()
.filter(|v| &v.version_type == "release")
.collect()
}
let options = game_versions.iter().map(|v| &v.version).collect();
let game_version = Select::new("Select a Minecraft version", options)
.prompt()
.unwrap();
let options = vec!["fabric", "quilt", "forge", "neoforge"];
let loader = Select::new("Select a mod loader", options)
.prompt()
.unwrap();
config::Config::init(game_version.to_string(), loader.to_string()).unwrap();
}

21
src/commands/list.rs Normal file
View File

@ -0,0 +1,21 @@
use crate::{config, modrinth::project::get_multiple_projects};
use colored::*;
use comfy_table::{presets::NOTHING, Table};
pub async fn list() {
let config = config::Config::get().unwrap();
let projects = get_multiple_projects(config.projects.keys().to_owned().collect())
.await
.unwrap();
let rows: Vec<Vec<ColoredString>> = projects
.iter()
.map(|p| vec![p.title.bold(), p.id.dimmed()])
.collect();
let mut table = Table::new();
table.load_preset(NOTHING).add_rows(rows);
println!("{table}");
}

3
src/commands/mod.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod add;
pub mod init;
pub mod list;

View File

@ -1,139 +1,50 @@
use crate::modrinth;
use crate::pack;
use inquire::Select;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::{
fmt::{Display, Formatter},
fs,
};
use std::collections::HashMap;
use std::fs;
#[derive(Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
pub struct Config {
pub info: Info,
pub mods: Vec<Mod>,
}
#[derive(Serialize, Deserialize)]
pub struct Mod {
pub id: String,
pub version_id: String,
}
#[derive(Serialize, Deserialize)]
pub struct Info {
loader: Modloader,
minecraft_version: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
pub enum Modloader {
Fabric,
Quilt,
Forge,
Neoforge,
}
impl Modloader {
const VARIANTS: &'static [Modloader] =
&[Self::Fabric, Self::Quilt, Self::Forge, Self::Neoforge];
}
impl Display for Modloader {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(f, "{self:?}")
}
pub game_version: String,
pub loader: String,
pub projects: HashMap<String, String>,
}
impl Config {
pub fn get() -> Self {
let pack_string = fs::read_to_string(".packium.toml").unwrap();
pub fn get() -> Result<Self> {
let config_raw = fs::read_to_string(".packium.toml")?;
toml::from_str(&pack_string).unwrap()
let config = toml::from_str(&config_raw)?;
Ok(config)
}
pub async fn mods(&self) -> Vec<modrinth::Project> {
modrinth::get_multiple_project(self.mods.iter().map(|x| x.id.clone()).collect())
.await
.unwrap()
pub fn save(&self) -> Result<()> {
let config_toml = toml::to_string_pretty(&self)?;
fs::write(".packium.toml", &config_toml)?;
Ok(())
}
pub async fn init() {
println!("Fetching Minecraft information...");
let versions = modrinth::get_minecraft_versions().await.unwrap();
let versions = versions
.iter()
.filter(|x| x.version_type == "release".to_string())
.map(|x| &x.version)
.collect();
let modloader = Select::new("Modloader?", Modloader::VARIANTS.to_vec())
.prompt()
.unwrap();
let version = Select::new("Minecraft version?", versions)
.prompt()
.unwrap();
let pack = Config {
info: Info {
minecraft_version: version.to_owned(),
loader: modloader,
},
mods: vec![],
pub fn init(game_version: String, loader: String) -> Result<Self> {
let config = Self {
game_version,
loader,
projects: HashMap::new(),
};
let pack_toml = toml::to_string_pretty(&pack).unwrap();
config.save()?;
std::fs::write(".packium.toml", pack_toml).unwrap();
Ok(config)
}
pub async fn add(name: &String) {
let mut config = Self::get();
pub fn add(project_id: String, version_id: String) -> Result<Self> {
let mut config = Self::get()?;
let mods = modrinth::search_projects(
name.to_owned(),
&config.info.minecraft_version,
&config.info.loader.to_string().to_lowercase(),
)
.await
.unwrap();
config.projects.insert(project_id, version_id);
config.save()?;
let project = Select::new("Choose a mod", mods).prompt().unwrap();
let versions = modrinth::get_project_versions(
&project.project_id,
&config.info.minecraft_version,
&config.info.loader.to_string().to_lowercase(),
)
.await
.unwrap();
let version = versions.get(0).unwrap();
config.mods.push(Mod {
id: project.project_id.clone(),
version_id: version.id.clone(),
});
let config_toml = toml::to_string_pretty(&config).unwrap();
std::fs::write(".packium.toml", config_toml).unwrap();
}
pub async fn remove() {
let mut config = Self::get();
let mods = config.mods().await;
let project = Select::new("Choose a mod to remove", mods)
.prompt()
.unwrap();
config.mods.retain(|x| x.id != project.id);
let config_toml = toml::to_string_pretty(&config).unwrap();
std::fs::write(".packium.toml", config_toml).unwrap();
Ok(config)
}
}

View File

@ -1,7 +1,7 @@
mod cli;
mod commands;
mod config;
mod modrinth;
mod pack;
use clap::Parser;
@ -11,13 +11,16 @@ async fn main() {
match &args.command {
cli::Commands::Init => {
config::Config::init().await;
commands::init::init(false).await;
}
cli::Commands::Add { name } => {
config::Config::add(name).await;
cli::Commands::Add => {
commands::add::add().await;
}
cli::Commands::Remove => {
config::Config::remove().await;
// config::Config::remove().await;
}
cli::Commands::List => {
commands::list::list().await;
}
_ => (),
};

View File

@ -1,101 +0,0 @@
use crate::config;
use core::fmt;
use serde::Deserialize;
#[derive(Deserialize, Debug)]
pub struct MinecraftVersion {
pub version: String,
pub version_type: String,
pub date: String,
pub major: bool,
}
#[derive(Deserialize, Debug)]
pub struct SearchProject {
pub title: String,
pub description: String,
pub project_id: String,
pub game_versions: Vec<String>,
pub versions: Vec<String>,
}
impl fmt::Display for SearchProject {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.title)
}
}
#[derive(Deserialize, Debug)]
pub struct Project {
pub title: String,
pub description: String,
pub id: String,
pub game_versions: Vec<String>,
pub versions: Vec<String>,
}
impl fmt::Display for Project {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.title)
}
}
#[derive(Deserialize, Debug)]
pub struct Search {
hits: Vec<SearchProject>,
}
#[derive(Deserialize, Debug)]
pub struct ProjectVersion {
pub name: String,
pub game_versions: Vec<String>,
pub version_type: String,
pub id: String,
pub project_id: String,
}
pub async fn get_minecraft_versions() -> Result<Vec<MinecraftVersion>, reqwest::Error> {
reqwest::get("https://api.modrinth.com/v2/tag/game_version")
.await?
.json::<Vec<MinecraftVersion>>()
.await
}
pub async fn search_projects(
query: String,
version: &String,
loader: &String,
) -> Result<Vec<SearchProject>, reqwest::Error> {
let url = format!("https://api.modrinth.com/v2/search?query={query}&facets=[[\"project_type:mod\"],[\"versions:{version}\"],[\"categories:{loader}\"]]");
let response = reqwest::get(url).await?.json::<Search>().await?;
Ok(response.hits)
}
pub async fn get_project_versions(
id: &String,
version: &String,
loader: &String,
) -> Result<Vec<ProjectVersion>, reqwest::Error> {
let url = format!("https://api.modrinth.com/v2/project/{id}/version?loaders=[\"{loader}\"]&game_versions=[\"{version}\"]");
let response = reqwest::get(url)
.await?
.json::<Vec<ProjectVersion>>()
.await?;
Ok(response)
}
pub async fn get_multiple_project(
project_ids: Vec<String>,
) -> Result<Vec<Project>, reqwest::Error> {
let ids = project_ids.join("\", \"");
let url = format!("https://api.modrinth.com/v2/projects?ids=[\"{ids}\"]");
let response = reqwest::get(url).await?.json::<Vec<Project>>().await?;
Ok(response)
}

6
src/modrinth/mod.rs Normal file
View File

@ -0,0 +1,6 @@
pub mod project;
pub mod search;
pub mod tags;
pub mod versions;
pub const API_BASE_URL: &str = "https://api.modrinth.com/v2";

51
src/modrinth/project.rs Normal file
View File

@ -0,0 +1,51 @@
use super::versions::ProjectVersion;
use super::API_BASE_URL;
use anyhow::{bail, Result};
use serde::Deserialize;
type ProjectVersionResponse = Vec<ProjectVersion>;
pub async fn get_project_versions(
project_id: &str,
game_version: &str,
) -> Result<ProjectVersionResponse> {
let url = format!(
"{API_BASE_URL}/project/{}/version?game_versions=[\"{}\"]",
project_id, game_version
);
let response = reqwest::get(url)
.await?
.json::<ProjectVersionResponse>()
.await;
match response {
Ok(r) => Ok(r),
Err(err) => bail!("Error with Modrinth API: {}", err.to_string()),
}
}
type ProjectResponse = Vec<Project>;
#[derive(Debug, Deserialize)]
pub struct Project {
slug: String,
pub title: String,
pub description: String,
pub downloads: i64,
pub id: String,
pub followers: i64,
}
pub async fn get_multiple_projects(version_ids: Vec<&String>) -> Result<ProjectResponse> {
let version_ids_json = serde_json::to_string(&version_ids)?;
let url = format!("{API_BASE_URL}/projects?ids={}", version_ids_json);
let response = reqwest::get(url).await?.json::<ProjectResponse>().await;
match response {
Ok(r) => Ok(r),
Err(err) => bail!("Error with Modrinth API: {}", err.to_string()),
}
}

70
src/modrinth/search.rs Normal file
View File

@ -0,0 +1,70 @@
use super::API_BASE_URL;
use anyhow::{bail, Result};
use serde::Deserialize;
use std::fmt;
#[derive(Deserialize, Debug)]
pub struct Response {
pub hits: Vec<SearchHit>,
offset: i64,
limit: i64,
total_hits: i64,
}
#[derive(Deserialize, Debug)]
pub struct SearchHit {
pub slug: String,
pub title: String,
pub description: String,
pub client_side: String,
pub server_side: String,
pub project_type: String,
pub downloads: i64,
pub project_id: String,
pub author: String,
pub versions: Vec<String>,
pub follows: i64,
}
impl fmt::Display for SearchHit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.title)
}
}
pub async fn search(query: &str, game_version: &str, loader: &str) -> Result<Response> {
let game_version_facet = format!("versions:{game_version}");
let loader_facet = format!("categories:{loader}");
let facets: Vec<Vec<&str>> = vec![
vec!["project_type:mod"],
vec![&game_version_facet],
vec![&loader_facet],
];
let facets_json = serde_json::to_string(&facets)?;
let url = format!(
"{API_BASE_URL}/search?query={}&facets={}",
query, facets_json
);
let response = reqwest::get(url).await?.json::<Response>().await;
match response {
Ok(r) => Ok(r),
Err(err) => bail!("Error with Modrinth API: {}", err.to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_search() {
let response = search("Fabric API", "1.20.4", "fabric").await;
assert!(response.is_ok())
}
}

36
src/modrinth/tags.rs Normal file
View File

@ -0,0 +1,36 @@
use super::API_BASE_URL;
use anyhow::{bail, Result};
use serde::Deserialize;
type Response = Vec<MinecraftVersion>;
#[derive(Debug, Deserialize)]
pub struct MinecraftVersion {
pub version: String,
pub version_type: String,
date: String,
major: bool,
}
pub async fn get_minecraft_versions() -> Result<Response> {
let url = format!("{API_BASE_URL}/tag/game_version");
let response = reqwest::get(url).await?.json::<Response>().await;
match response {
Ok(r) => Ok(r),
Err(err) => bail!("Error with Modrinth API: {}", err.to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_game_versions() {
let response = get_minecraft_versions().await;
assert!(response.is_ok())
}
}

31
src/modrinth/versions.rs Normal file
View File

@ -0,0 +1,31 @@
use super::API_BASE_URL;
use anyhow::{bail, Result};
use serde::Deserialize;
type Response = Vec<ProjectVersion>;
#[derive(Debug, Deserialize)]
pub struct ProjectVersion {
pub name: String,
pub version_number: String,
game_versions: Vec<String>,
version_type: String,
loaders: Vec<String>,
featured: bool,
pub id: String,
project_id: String,
downloads: i64,
}
pub async fn get_multiple_versions(version_ids: Vec<&String>) -> Result<Response> {
let version_ids_json = serde_json::to_string(&version_ids)?;
let url = format!("{API_BASE_URL}/versions?ids={}", version_ids_json);
let response = reqwest::get(url).await?.json::<Response>().await;
match response {
Ok(r) => Ok(r),
Err(err) => bail!("Error with Modrinth API: {}", err.to_string()),
}
}